Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: basic platform admin panel for checking onboarding status & available integrations #3017

Merged
merged 4 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 54 additions & 50 deletions editor.planx.uk/src/@planx/components/shared/Preview/SummaryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ComponentProps> | undefined;
Expand Down
13 changes: 9 additions & 4 deletions editor.planx.uk/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -554,11 +554,16 @@ const EditorToolbar: React.FC<{
</MenuItem>
)}

{/* Only show global settings link from top-level admin view */}
{/* Only show global settings & admin panel links from top-level view */}
{isGlobalSettingsVisible && (
<MenuItem onClick={() => navigate("/global-settings")}>
Global Settings
</MenuItem>
<>
<MenuItem onClick={() => navigate("/global-settings")}>
Global Settings
</MenuItem>
<MenuItem onClick={() => navigate("/admin-panel")}>
Admin Panel
</MenuItem>
</>
)}

<MenuItem onClick={() => navigate("/logout")}>Log out</MenuItem>
Expand Down
13 changes: 12 additions & 1 deletion editor.planx.uk/src/pages/FlowEditor/lib/store/settings.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -14,6 +19,8 @@ export interface SettingsStore {
setGlobalSettings: (globalSettings: GlobalSettings) => void;
updateFlowSettings: (newSettings: FlowSettings) => Promise<number>;
updateGlobalSettings: (newSettings: { [key: string]: TextContent }) => void;
adminPanelData?: AdminPanelData[];
setAdminPanelData: (adminPanelData: AdminPanelData[]) => void;
}

export const settingsStore: StateCreator<
Expand Down Expand Up @@ -91,4 +98,8 @@ export const settingsStore: StateCreator<
},
});
},

adminPanelData: undefined,

setAdminPanelData: (adminPanelData) => set({ adminPanelData }),
});
168 changes: 168 additions & 0 deletions editor.planx.uk/src/pages/PlatformAdminPanel.tsx
Original file line number Diff line number Diff line change
@@ -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}`,
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved
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 (
<Box p={3}>
<Typography variant="h1">Platform Admin Panel</Typography>
<Typography variant="body1" mb={3}>
{`This is an overview of each team's integrations and settings for the `}
<strong>{process.env.REACT_APP_ENV}</strong>
{` environment`}
</Typography>
{adminPanelData?.map((team) => <TeamData key={team.id} data={team} />)}
</Box>
);
}

interface TeamData {
data: AdminPanelData;
}

const NotConfigured: React.FC = () => <Close color="error" fontSize="small" />;

const Configured: React.FC = () => <Done color="success" fontSize="small" />;

const TeamData: React.FC<TeamData> = ({ data }) => {
return (
<StyledTeamAccordion primaryColour={data.primaryColour} elevation={0}>
<AccordionSummary
id={`${data.name}-header`}
aria-controls={`${data.name}-panel`}
expandIcon={<Caret />}
sx={{ pr: 1.5 }}
>
<Typography variant="h2">{data.name}</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid
container
direction="row"
justifyContent="flex-start"
alignItems="flex-start"
spacing={3}
>
<Grid item xs={4}>
<SummaryListTable dense={true}>
<>
<Box component="dt">{"Slug"}</Box>
<Box component="dd">
<code>
{`/`}
{data.slug}
</code>
</Box>
</>
<>
<Box component="dt">{"Homepage"}</Box>
<Box component="dd">{data.homepage || <NotConfigured />}</Box>
</>
<>
<Box component="dt">{"Logo"}</Box>
<Box component="dd">
{data.logo ? <Configured /> : <NotConfigured />}
</Box>
</>
<>
<Box component="dt">{"Favicon"}</Box>
<Box component="dd">
{data.favicon ? <Configured /> : <NotConfigured />}
</Box>
</>
</SummaryListTable>
</Grid>
<Grid item xs={4}>
<SummaryListTable dense={true}>
<>
<Box component="dt">{"Planning constraints"}</Box>
<Box component="dd">
{data.planningDataEnabled ? (
<Configured />
) : (
<NotConfigured />
)}
</Box>
</>
<>
<Box component="dt">{"Article 4s"}</Box>
<Box component="dd">{"?"}</Box>
Copy link
Member Author

@jessicamcinchak jessicamcinchak Apr 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Followup PR: figure out how to check if granular A4s are being returning by our Planx GIS API yet !

This is perhaps the trickiest one to currently measure (do we mean the spreadsheet, flow content, API response etc) - but I've included this "?" placeholder as a reminder to manually check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is interesting! It's not one which we can get a DB value for currently.

Maybe the solution is a /gis/:teamSlug/status endpoint which just returns an enum? This won't handle spreadsheet or flow content, but could serve as an indicator (if API GIS is setup, flow is ready to be done?).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep I was thinking of a similar endpoint variation - agree API is the thing we care about checking most here compared to flow content.

Will plan to pick this one back up when I pick up other constraints work in the coming weeks!

</>
<>
<Box component="dt">{"Reference code"}</Box>
<Box component="dd">
{data.referenceCode || <NotConfigured />}
</Box>
</>
<>
<Box component="dt">{"Subdomain"}</Box>
<Box component="dd">{data.subdomain || <NotConfigured />}</Box>
</>
</SummaryListTable>
</Grid>
<Grid item xs={4}>
<SummaryListTable dense={true}>
<>
<Box component="dt">{"GOV.UK Notify"}</Box>
<Box component="dd">
{data.govnotifyPersonalisation?.helpEmail || (
<NotConfigured />
)}
</Box>
jessicamcinchak marked this conversation as resolved.
Show resolved Hide resolved
</>
<>
<Box component="dt">{"GOV.UK Pay"}</Box>
<Box component="dd">
{data.govpayEnabled ? <Configured /> : <NotConfigured />}
</Box>
</>
<>
<Box component="dt">{"Send to email"}</Box>
<Box component="dd">
{data.sendToEmailAddress || <NotConfigured />}
</Box>
</>
<>
<Box component="dt">{"BOPS"}</Box>
<Box component="dd">
{data.bopsSubmissionURL ? <Configured /> : <NotConfigured />}
</Box>
</>
</SummaryListTable>
</Grid>
</Grid>
</AccordionDetails>
</StyledTeamAccordion>
);
};

export default Component;
43 changes: 43 additions & 0 deletions editor.planx.uk/src/routes/authenticated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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}`,
);
Copy link
Member Author

@jessicamcinchak jessicamcinchak Apr 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Followup PR: the /admin-panel page is successfully reachable from the index, but throws this error if you hard-refresh from the page itself. This also appears to be a current bug with /global-settings, so not introduced here. Since these routes are only accessible by platform admins to begin with, there shouldn't be much harm in merging & pushing an overall fix as a followup.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed - this is an existing issue we should take a look at but it's a showstopper!


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: <AdminPanelView />,
};
});
}),

"/:team": lazy(() => import("./team")),
}),
);
Expand Down
Loading
Loading