Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feat/layout-cls
Browse files Browse the repository at this point in the history
  • Loading branch information
adriangohjw committed Dec 10, 2024
2 parents 6bccd11 + 90f44c4 commit 605dc45
Show file tree
Hide file tree
Showing 78 changed files with 1,989 additions and 552 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/chromatic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ jobs:
- 'packages/components/**'
studio:
- 'apps/studio/**'
- 'packages/components/src/interfaces/**'
- 'packages/components/src/types/**'
ui:
needs: [changes, optimize_ci]
# Only run if the user is not a bot and has changes
Expand Down
5 changes: 4 additions & 1 deletion apps/studio/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
FROM node:22-alpine AS base
# needed to pin to 3.20 because Prisma uses OpenSSL but it has been shifted to another location in 3.21
# Ref: https://github.com/prisma/prisma/issues/25817
# Solution: https://github.com/prisma/prisma/issues/25817#issuecomment-2529926082
FROM node:22-alpine3.20 AS base

ARG NEXT_PUBLIC_APP_ENV
ENV NEXT_PUBLIC_APP_ENV=$NEXT_PUBLIC_APP_ENV
Expand Down
6 changes: 6 additions & 0 deletions apps/studio/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ const ContentSecurityPolicy = `
const config = {
output: "standalone",
reactStrictMode: true,
// NOTE: this is required for datadog to work because
// the trace/logs are initialised via the `instrumentation` file
// https://nextjs.org/docs/14/app/api-reference/next-config-js/instrumentationHook
experimental: {
instrumentationHook: true,
},
/**
* Dynamic configuration available for the browser and server.
* Note: requires `ssr: true` or a `getInitialProps` in `_app.tsx`
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"@opengovsg/design-system-react": "^1.15.0",
"@opengovsg/isomer-components": "*",
"@opengovsg/sgid-client": "^2.2.0",
"@opengovsg/starter-kitty-validators": "^1.1.0",
"@opengovsg/starter-kitty-validators": "^1.2.10",
"@paralleldrive/cuid2": "^2.2.2",
"@prisma/client": "5.10.2",
"@sendgrid/mail": "^8.1.3",
Expand Down
22 changes: 22 additions & 0 deletions apps/studio/prisma/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const ISOMER_ADMINS = [
"alex",
"jan",
"jiachin",
"sehyun",
"harish",
"zhongjun",
"adriangoh",
"shazli",
"jinhui",
]

export const ISOMER_MIGRATORS = [
"tingshian",
"hakeem",
"elora",
"junxiang",
"rayyan",
"yongteng",
"huaying",
"weiping",
]
26 changes: 26 additions & 0 deletions apps/studio/prisma/scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Using these scripts

1. first, run `npm run jump` for production or `npm run jump:<env>`
a. if you are running this against production, find another engineer to pair

2. then, update your `$DATABASE_URL` in your local `apps/studio/.env` to the one for the environment
b. ensure that the port you have specified is 5433 as we are using a jump host to tunnel our connection

3. update the script you want to invoke to pass in the correct arguments
4. invoke the command via `source .env && npx tsx <path_to_script>`
5. note that tsx might export the env vars to be exported. Hence, minor change to the `.env` is required to add `export` before each required env var.

## Create site script

1. On the last line, update the site name to the name you require. This should be exactly as what should be shown on Studio (e.g. "Site ABC")
2. This already adds the Isomer admins and migrators with the Admin role.

## Add users to site

1. Edit the array of `User` objects to be added at the end of the script
2. Each entry contains 4 fields: email, name, phone and role. Note that name, phone and role are optional fields. If left empty, name will be empty string, phone will be empty string and role will default to Editor.

## Update all user permissions for site

1. This script updates all users of a site to have a specific `RoleType`
2. Edit the `siteId` and `role` at the end of the script
63 changes: 63 additions & 0 deletions apps/studio/prisma/scripts/addUsersToSite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import cuid2 from "@paralleldrive/cuid2"

import { db, RoleType } from "~/server/modules/database"

interface User {
email: string
name?: string
phone?: string
role?: RoleType
}
interface AddUsersToSiteProps {
siteId: number
users: User[]
}
// NOTE: add them as editor first as that is the one with the least permissions
export const addUsersToSite = async ({
siteId,
users,
}: AddUsersToSiteProps) => {
const processedUsers = users.map(({ email, phone, name, role }) => ({
email: email.toLowerCase(),
name: name ?? "",
phone: phone ?? "",
role: role ?? RoleType.Editor,
}))

await Promise.all(
processedUsers.map(async ({ role, ...props }) => {
await db.transaction().execute(async (tx) => {
const user = await tx
.insertInto("User")
.values({
id: cuid2.createId(),
...props,
})
.onConflict((oc) =>
oc
.column("email")
.doUpdateSet((eb) => ({ email: eb.ref("excluded.email") })),
)
.returning(["id", "name", "email"])
.executeTakeFirstOrThrow()

await tx
.insertInto("ResourcePermission")
.values({
userId: user.id,
siteId,
role,
})
.execute()

console.log(`User added: ${user.email} with id: ${user.id}`)
})
}),
)
}

// NOTE: Update the list of users and siteId here before executing!
const users: User[] = []
const siteId = -1

await addUsersToSite({ siteId, users })
241 changes: 241 additions & 0 deletions apps/studio/prisma/scripts/createSite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import type { IsomerSchema } from "@opengovsg/isomer-components"
import { ISOMER_ADMINS, ISOMER_MIGRATORS } from "~prisma/constants"

import type { Navbar } from "~/server/modules/resource/resource.types"
import { db, jsonb, RoleType } from "~/server/modules/database"
import { addUsersToSite } from "./addUsersToSite"

const PAGE_BLOB: IsomerSchema = {
version: "0.1.0",
layout: "homepage",
page: {},
content: [
{
type: "hero",
variant: "gradient",
title: "Isomer",
subtitle:
"A leading global city of enterprise and talent, a vibrant nation of innovation and opportunity",
buttonLabel: "Main CTA",
buttonUrl: "/",
secondaryButtonLabel: "Sub CTA",
secondaryButtonUrl: "/",
backgroundUrl: "https://ohno.isomer.gov.sg/images/hero-banner.png",
},
{
type: "infobar",
title: "This is an infobar",
description: "This is the description that goes into the Infobar section",
},
{
type: "infopic",
title: "This is an infopic",
description: "This is the description for the infopic component",
imageSrc: "https://placehold.co/600x400",
},
{
type: "keystatistics",
statistics: [
{
label: "Average all nighters pulled in a typical calendar month",
value: "3",
},
{
label: "Growth in tasks assigned Q4 2024 (YoY)",
value: "+12.2%",
},
{
label: "Creative blocks met per single evening",
value: "89",
},
{
value: "4.0",
label: "Number of lies in this stat block",
},
],
title: "Irrationality in numbers",
},
],
}
const NAV_BAR_ITEMS: Navbar["items"] = [
{
name: "Expandable nav item",
url: "/item-one",
items: [
{
name: "PA's network one",
url: "/item-one/pa-network-one",
description: "Click here and brace yourself for mild disappointment.",
},
{
name: "PA's network two",
url: "/item-one/pa-network-two",
description: "Click here and brace yourself for mild disappointment.",
},
{
name: "PA's network three",
url: "/item-one/pa-network-three",
},
{
name: "PA's network four",
url: "/item-one/pa-network-four",
description:
"Click here and brace yourself for mild disappointment. This one has a pretty long one",
},
{
name: "PA's network five",
url: "/item-one/pa-network-five",
description:
"Click here and brace yourself for mild disappointment. This one has a pretty long one",
},
{
name: "PA's network six",
url: "/item-one/pa-network-six",
description: "Click here and brace yourself for mild disappointment.",
},
],
},
]
const FOOTER_ITEMS = [
{
title: "About us",
url: "/about",
},
{
title: "Our partners",
url: "/partners",
},
{
title: "Grants and programmes",
url: "/grants-and-programmes",
},
{
title: "Contact us",
url: "/contact-us",
},
{
title: "Something else",
url: "/something-else",
},
{
title: "Resources",
url: "/resources",
},
]

const FOOTER = {
contactUsLink: "/contact-us",
feedbackFormLink: "https://www.form.gov.sg",
privacyStatementLink: "/privacy",
termsOfUseLink: "/terms-of-use",
siteNavItems: FOOTER_ITEMS,
}

interface CreateSiteProps {
siteName: string
}
export const createSite = async ({ siteName }: CreateSiteProps) => {
const siteId = await db.transaction().execute(async (tx) => {
const { id: siteId } = await tx
.insertInto("Site")
.values({
name: siteName,
theme: jsonb({
colors: {
brand: {
canvas: {
alt: "#bfcfd7",
default: "#e6ecef",
inverse: "#00405f",
backdrop: "#80a0af",
},
interaction: {
hover: "#002e44",
default: "#00405f",
pressed: "#00283b",
},
},
},
}),
config: jsonb({
theme: "isomer-next",
siteName: "MTI",
logoUrl: "https://www.isomer.gov.sg/images/isomer-logo.svg",
search: undefined,
isGovernment: true,
}),
})
.onConflict((oc) =>
oc
.column("name")
.doUpdateSet((eb) => ({ name: eb.ref("excluded.name") })),
)
.returning("id")
.executeTakeFirstOrThrow()

await tx
.insertInto("Footer")
.values({
siteId,
content: jsonb(FOOTER),
})
.onConflict((oc) =>
oc
.column("siteId")
.doUpdateSet((eb) => ({ siteId: eb.ref("excluded.siteId") })),
)
.execute()

await tx
.insertInto("Navbar")
.values({
siteId,
content: jsonb(NAV_BAR_ITEMS),
})
.onConflict((oc) =>
oc
.column("siteId")
.doUpdateSet((eb) => ({ siteId: eb.ref("excluded.siteId") })),
)
.execute()

const { id: blobId } = await tx
.insertInto("Blob")
.values({ content: jsonb(PAGE_BLOB) })
.returning("id")
.executeTakeFirstOrThrow()

await tx
.insertInto("Resource")
.values({
draftBlobId: String(blobId),
permalink: "",
siteId,
type: "RootPage",
title: "Home",
})

.onConflict((oc) =>
oc.column("draftBlobId").doUpdateSet((eb) => ({
draftBlobId: eb.ref("excluded.draftBlobId"),
})),
)
.executeTakeFirstOrThrow()

console.log(`Added site ${siteName} with id ${siteId} to database`)
return siteId
})

await addUsersToSite({
siteId,
users: [...ISOMER_ADMINS, ...ISOMER_MIGRATORS].map((email) => ({
email: `${email}@open.gov.sg`,
role: RoleType.Admin,
})),
})

return siteId
}

// NOTE: Update the site name here before executing!
await createSite({ siteName: "" })
Loading

0 comments on commit 605dc45

Please sign in to comment.