From c57f7a5c94dc048631c3045af042c1d432b053f9 Mon Sep 17 00:00:00 2001 From: Jessica McInchak Date: Tue, 16 Apr 2024 09:15:47 +0100 Subject: [PATCH] feat: basic platform admin panel for checking onboarding status & available integrations (#3017) --- .../components/shared/Preview/SummaryList.tsx | 104 +++++------ editor.planx.uk/src/components/Header.tsx | 13 +- .../pages/FlowEditor/lib/store/settings.ts | 13 +- .../src/pages/PlatformAdminPanel.tsx | 168 ++++++++++++++++++ editor.planx.uk/src/routes/authenticated.tsx | 43 +++++ editor.planx.uk/src/types.ts | 26 ++- hasura.planx.uk/metadata/tables.yaml | 26 +++ .../down.sql | 1 + .../up.sql | 26 +++ 9 files changed, 364 insertions(+), 56 deletions(-) create mode 100644 editor.planx.uk/src/pages/PlatformAdminPanel.tsx create mode 100644 hasura.planx.uk/migrations/1713084872473_create_view_teams_summary/down.sql create mode 100644 hasura.planx.uk/migrations/1713084872473_create_view_teams_summary/up.sql diff --git a/editor.planx.uk/src/@planx/components/shared/Preview/SummaryList.tsx b/editor.planx.uk/src/@planx/components/shared/Preview/SummaryList.tsx index c76eea4eb9..eba2d82db3 100644 --- a/editor.planx.uk/src/@planx/components/shared/Preview/SummaryList.tsx +++ b/editor.planx.uk/src/@planx/components/shared/Preview/SummaryList.tsx @@ -20,63 +20,67 @@ const FIND_PROPERTY_DT = "Property address"; const DRAW_BOUNDARY_DT = "Location plan"; export const SummaryListTable = styled("dl", { - shouldForwardProp: (prop) => prop !== "showChangeButton", -})<{ showChangeButton?: boolean }>(({ theme, showChangeButton }) => ({ - display: "grid", - gridTemplateColumns: showChangeButton ? "1fr 2fr 100px" : "1fr 2fr", - gridRowGap: "10px", - marginTop: theme.spacing(2), - marginBottom: theme.spacing(4), - "& > *": { - borderBottom: `1px solid ${theme.palette.border.main}`, - paddingBottom: theme.spacing(2), - paddingTop: theme.spacing(2), - verticalAlign: "top", - margin: 0, - }, - "& ul": { - listStylePosition: "inside", - padding: 0, - margin: 0, - }, - "& dt": { - // left column - fontWeight: FONT_WEIGHT_SEMI_BOLD, - }, - "& dd:nth-of-type(n)": { - // middle column - paddingLeft: "10px", - }, - "& dd:nth-of-type(2n)": { - // right column - textAlign: showChangeButton ? "right" : "left", - }, - [theme.breakpoints.down("sm")]: { - display: "flex", - flexDirection: "column", + shouldForwardProp: (prop) => + !["showChangeButton", "dense"].includes(prop as string), +})<{ showChangeButton?: boolean; dense?: boolean }>( + ({ theme, showChangeButton, dense }) => ({ + display: "grid", + gridTemplateColumns: showChangeButton ? "1fr 2fr 100px" : "1fr 2fr", + gridRowGap: "10px", + marginTop: dense ? theme.spacing(1) : theme.spacing(2), + marginBottom: dense ? theme.spacing(2) : theme.spacing(4), + fontSize: dense ? theme.typography.body2.fontSize : "inherit", + "& > *": { + borderBottom: `1px solid ${theme.palette.border.main}`, + paddingBottom: dense ? theme.spacing(1) : theme.spacing(2), + paddingTop: dense ? theme.spacing(1) : theme.spacing(2), + verticalAlign: "top", + margin: 0, + }, + "& ul": { + listStylePosition: "inside", + padding: 0, + margin: 0, + }, "& dt": { - // top row - paddingLeft: theme.spacing(1), - paddingTop: theme.spacing(2), - marginTop: theme.spacing(1), - borderTop: `1px solid ${theme.palette.border.main}`, - borderBottom: "none", + // left column fontWeight: FONT_WEIGHT_SEMI_BOLD, }, "& dd:nth-of-type(n)": { - // middle row - textAlign: "left", - paddingTop: 0, - paddingBottom: 0, - margin: 0, - borderBottom: "none", + // middle column + paddingLeft: "10px", }, "& dd:nth-of-type(2n)": { - // bottom row - textAlign: "left", + // right column + textAlign: showChangeButton ? "right" : "left", + }, + [theme.breakpoints.down("sm")]: { + display: "flex", + flexDirection: "column", + "& dt": { + // top row + paddingLeft: theme.spacing(1), + paddingTop: dense ? theme.spacing(1) : theme.spacing(2), + marginTop: theme.spacing(1), + borderTop: `1px solid ${theme.palette.border.main}`, + borderBottom: "none", + fontWeight: FONT_WEIGHT_SEMI_BOLD, + }, + "& dd:nth-of-type(n)": { + // middle row + textAlign: "left", + paddingTop: 0, + paddingBottom: 0, + margin: 0, + borderBottom: "none", + }, + "& dd:nth-of-type(2n)": { + // bottom row + textAlign: "left", + }, }, - }, -})); + }), +); const presentationalComponents: { [key in TYPES]: React.FC | undefined; diff --git a/editor.planx.uk/src/components/Header.tsx b/editor.planx.uk/src/components/Header.tsx index 8d5ae9e540..0078c4d336 100644 --- a/editor.planx.uk/src/components/Header.tsx +++ b/editor.planx.uk/src/components/Header.tsx @@ -554,11 +554,16 @@ const EditorToolbar: React.FC<{ )} - {/* Only show global settings link from top-level admin view */} + {/* Only show global settings & admin panel links from top-level view */} {isGlobalSettingsVisible && ( - navigate("/global-settings")}> - Global Settings - + <> + navigate("/global-settings")}> + Global Settings + + navigate("/admin-panel")}> + Admin Panel + + )} navigate("/logout")}>Log out diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/settings.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/settings.ts index 55504aa09d..6c0d6e3a88 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/settings.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/settings.ts @@ -1,7 +1,12 @@ import { gql } from "@apollo/client"; import camelcaseKeys from "camelcase-keys"; import { client } from "lib/graphql"; -import { FlowSettings, GlobalSettings, TextContent } from "types"; +import { + AdminPanelData, + FlowSettings, + GlobalSettings, + TextContent, +} from "types"; import type { StateCreator } from "zustand"; import { SharedStore } from "./shared"; @@ -14,6 +19,8 @@ export interface SettingsStore { setGlobalSettings: (globalSettings: GlobalSettings) => void; updateFlowSettings: (newSettings: FlowSettings) => Promise; updateGlobalSettings: (newSettings: { [key: string]: TextContent }) => void; + adminPanelData?: AdminPanelData[]; + setAdminPanelData: (adminPanelData: AdminPanelData[]) => void; } export const settingsStore: StateCreator< @@ -91,4 +98,8 @@ export const settingsStore: StateCreator< }, }); }, + + adminPanelData: undefined, + + setAdminPanelData: (adminPanelData) => set({ adminPanelData }), }); diff --git a/editor.planx.uk/src/pages/PlatformAdminPanel.tsx b/editor.planx.uk/src/pages/PlatformAdminPanel.tsx new file mode 100644 index 0000000000..8acb58c27f --- /dev/null +++ b/editor.planx.uk/src/pages/PlatformAdminPanel.tsx @@ -0,0 +1,168 @@ +import Close from "@mui/icons-material/Close"; +import Done from "@mui/icons-material/Done"; +import Accordion from "@mui/material/Accordion"; +import AccordionDetails from "@mui/material/AccordionDetails"; +import AccordionSummary from "@mui/material/AccordionSummary"; +import Box from "@mui/material/Box"; +import Grid from "@mui/material/Grid"; +import { styled } from "@mui/material/styles"; +import Typography from "@mui/material/Typography"; +import { SummaryListTable } from "@planx/components/shared/Preview/SummaryList"; +import { useStore } from "pages/FlowEditor/lib/store"; +import React from "react"; +import { AdminPanelData } from "types"; +import Caret from "ui/icons/Caret"; + +const StyledTeamAccordion = styled(Accordion, { + shouldForwardProp: (prop) => prop !== "primaryColour", +})<{ primaryColour?: string }>(({ theme, primaryColour }) => ({ + borderTop: "none", // TODO figure out how to remove top border (box shadow?) when collapsed + borderLeft: `10px solid ${primaryColour}`, + backgroundColor: theme.palette.background.paper, + width: "100%", + position: "relative", + marginBottom: theme.spacing(2), + padding: theme.spacing(1), + "&::after": { + position: "absolute", + width: "100%", + }, +})); + +function Component() { + const adminPanelData = useStore((state) => state.adminPanelData); + + return ( + + Platform Admin Panel + + {`This is an overview of each team's integrations and settings for the `} + {process.env.REACT_APP_ENV} + {` environment`} + + {adminPanelData?.map((team) => )} + + ); +} + +interface TeamData { + data: AdminPanelData; +} + +const NotConfigured: React.FC = () => ; + +const Configured: React.FC = () => ; + +const TeamData: React.FC = ({ data }) => { + return ( + + } + sx={{ pr: 1.5 }} + > + {data.name} + + + + + + <> + {"Slug"} + + + {`/`} + {data.slug} + + + + <> + {"Homepage"} + {data.homepage || } + + <> + {"Logo"} + + {data.logo ? : } + + + <> + {"Favicon"} + + {data.favicon ? : } + + + + + + + <> + {"Planning constraints"} + + {data.planningDataEnabled ? ( + + ) : ( + + )} + + + <> + {"Article 4s"} + {"?"} + + <> + {"Reference code"} + + {data.referenceCode || } + + + <> + {"Subdomain"} + {data.subdomain || } + + + + + + <> + {"GOV.UK Notify"} + + {data.govnotifyPersonalisation?.helpEmail || ( + + )} + + + <> + {"GOV.UK Pay"} + + {data.govpayEnabled ? : } + + + <> + {"Send to email"} + + {data.sendToEmailAddress || } + + + <> + {"BOPS"} + + {data.bopsSubmissionURL ? : } + + + + + + + + ); +}; + +export default Component; diff --git a/editor.planx.uk/src/routes/authenticated.tsx b/editor.planx.uk/src/routes/authenticated.tsx index d1b2e42b7b..659b907d79 100644 --- a/editor.planx.uk/src/routes/authenticated.tsx +++ b/editor.planx.uk/src/routes/authenticated.tsx @@ -13,6 +13,7 @@ import React from "react"; import { client } from "../lib/graphql"; import GlobalSettingsView from "../pages/GlobalSettings"; +import AdminPanelView from "../pages/PlatformAdminPanel"; import Teams from "../pages/Teams"; import { makeTitle } from "./utils"; import { authenticatedView } from "./views/authenticated"; @@ -68,6 +69,48 @@ const editorRoutes = compose( }); }), + "/admin-panel": map(async (req) => { + const isAuthorised = useStore.getState().user?.isPlatformAdmin; + if (!isAuthorised) + throw new NotFoundError( + `User does not have access to ${req.originalUrl}`, + ); + + return route(async () => { + const { data } = await client.query({ + query: gql` + query { + adminPanel: teams_summary { + id + name + slug + referenceCode: reference_code + homepage + subdomain + planningDataEnabled: planning_data_enabled + article4sEnabled: article_4s_enabled + govnotifyPersonalisation: govnotify_personalisation + govpayEnabled: govpay_enabled + sendToEmailAddress: send_to_email_address + bopsSubmissionURL: bops_submission_url + logo + favicon + primaryColour: primary_colour + linkColour: link_colour + actionColour: action_colour + } + } + `, + }); + useStore.getState().setAdminPanelData(data.adminPanel); + + return { + title: makeTitle("Platform Admin Panel"), + view: , + }; + }); + }), + "/:team": lazy(() => import("./team")), }), ); diff --git a/editor.planx.uk/src/types.ts b/editor.planx.uk/src/types.ts index 3d8a63117f..66d691976d 100644 --- a/editor.planx.uk/src/types.ts +++ b/editor.planx.uk/src/types.ts @@ -1,4 +1,8 @@ -import { GovUKPayment, Team } from "@opensystemslab/planx-core/types"; +import { + GovUKPayment, + NotifyPersonalisation, + Team, +} from "@opensystemslab/planx-core/types"; import { useFormik } from "formik"; import { Store } from "./pages/FlowEditor/lib/store/index"; @@ -98,3 +102,23 @@ export interface SectionNode extends Store.node { description?: string; }; } + +export interface AdminPanelData { + id: string; + name: string; + slug: string; + referenceCode?: string; + homepage?: string; + subdomain?: string; + planningDataEnabled: boolean; + article4sEnabled: string; + govnotifyPersonalisation?: NotifyPersonalisation; + govpayEnabled: boolean; + sendToEmailAddress?: string; + bopsSubmissionURL?: string; + logo?: string; + favicon?: string; + primaryColour?: string; + linkColour?: string; + actionColour?: string; +} diff --git a/hasura.planx.uk/metadata/tables.yaml b/hasura.planx.uk/metadata/tables.yaml index 87da4e2400..aa82b8aee3 100644 --- a/hasura.planx.uk/metadata/tables.yaml +++ b/hasura.planx.uk/metadata/tables.yaml @@ -1714,6 +1714,32 @@ - submission_email filter: {} check: null +- table: + name: teams_summary + schema: public + select_permissions: + - role: platformAdmin + permission: + columns: + - govpay_enabled + - planning_data_enabled + - id + - govnotify_personalisation + - action_colour + - article_4s_enabled + - bops_submission_url + - favicon + - homepage + - link_colour + - logo + - name + - primary_colour + - reference_code + - send_to_email_address + - slug + - subdomain + filter: {} + comment: "" - table: name: uniform_applications schema: public diff --git a/hasura.planx.uk/migrations/1713084872473_create_view_teams_summary/down.sql b/hasura.planx.uk/migrations/1713084872473_create_view_teams_summary/down.sql new file mode 100644 index 0000000000..ad6843f7d1 --- /dev/null +++ b/hasura.planx.uk/migrations/1713084872473_create_view_teams_summary/down.sql @@ -0,0 +1 @@ +DROP VIEW IF EXISTS "public"."teams_summary" CASCADE; \ No newline at end of file diff --git a/hasura.planx.uk/migrations/1713084872473_create_view_teams_summary/up.sql b/hasura.planx.uk/migrations/1713084872473_create_view_teams_summary/up.sql new file mode 100644 index 0000000000..76180557a4 --- /dev/null +++ b/hasura.planx.uk/migrations/1713084872473_create_view_teams_summary/up.sql @@ -0,0 +1,26 @@ +CREATE OR REPLACE VIEW "public"."teams_summary" AS SELECT + t.id, + t.name, + t.slug, + t.reference_code, + t.settings->>'homepage' as homepage, + t.domain as subdomain, + ti.has_planning_data as planning_data_enabled, + '@todo' as article_4s_enabled, + t.notify_personalisation as govnotify_personalisation, + CASE + WHEN coalesce(ti.production_govpay_secret, ti.staging_govpay_secret) is not null + THEN true + ELSE false + END as govpay_enabled, + t.submission_email as send_to_email_address, + coalesce(ti.production_bops_submission_url, ti.staging_bops_submission_url) as bops_submission_url, + tt.logo, + tt.favicon, + tt.primary_colour, + tt.link_colour, + tt.action_colour +FROM teams t + JOIN team_integrations ti on ti.team_id = t.id + JOIN team_themes tt on tt.team_id = t.id +ORDER BY t.name ASC;