From fa597aa340752fb95ba6746484ce20825635c602 Mon Sep 17 00:00:00 2001 From: David Leek Date: Wed, 6 Nov 2024 14:02:42 +0100 Subject: [PATCH] Feat/release management overview (#8672) --- frontend/src/assets/img/releaseTemplates.svg | 39 +++++++++ .../NavigationSidebar/IconRenderer.tsx | 2 +- .../__snapshots__/routes.test.tsx.snap | 2 +- frontend/src/component/menu/routes.ts | 4 +- .../providers/AccessProvider/permissions.ts | 2 + .../component/releases/ReleaseManagement.tsx | 3 - .../EmptyTemplatesListMessage.tsx | 38 +++++++++ .../ReleaseManagement/ReleaseManagement.tsx | 56 +++++++++++++ .../ReleasePlanTemplateCard.tsx | 81 +++++++++++++++++++ .../ReleasePlanTemplateCardMenu.tsx | 77 ++++++++++++++++++ .../ReleasePlanTemplateList.tsx | 21 +++++ .../useReleasePlanTemplates.ts | 41 ++++++++++ frontend/src/interfaces/releasePlans.ts | 7 ++ 13 files changed, 366 insertions(+), 7 deletions(-) create mode 100644 frontend/src/assets/img/releaseTemplates.svg delete mode 100644 frontend/src/component/releases/ReleaseManagement.tsx create mode 100644 frontend/src/component/releases/ReleaseManagement/EmptyTemplatesListMessage.tsx create mode 100644 frontend/src/component/releases/ReleaseManagement/ReleaseManagement.tsx create mode 100644 frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateCard.tsx create mode 100644 frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateCardMenu.tsx create mode 100644 frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateList.tsx create mode 100644 frontend/src/hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates.ts create mode 100644 frontend/src/interfaces/releasePlans.ts diff --git a/frontend/src/assets/img/releaseTemplates.svg b/frontend/src/assets/img/releaseTemplates.svg new file mode 100644 index 0000000000..775dc81b56 --- /dev/null +++ b/frontend/src/assets/img/releaseTemplates.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/IconRenderer.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/IconRenderer.tsx index 3ce28dcecd..e2316ca056 100644 --- a/frontend/src/component/layout/MainLayout/NavigationSidebar/IconRenderer.tsx +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/IconRenderer.tsx @@ -58,7 +58,7 @@ const icons: Record = { '/admin/cors': CorsIcon, '/admin/billing': BillingIcon, '/history': EventLogIcon, - '/releases-management': LaunchIcon, + '/release-management': LaunchIcon, '/personal': PersonalDashboardIcon, GitHub: GitHubIcon, Documentation: LibraryBooksIcon, diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap index 3652622dc2..09b3a8f64a 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap @@ -240,7 +240,7 @@ exports[`returns all baseRoutes 1`] = ` "enterprise", ], }, - "path": "/releases-management", + "path": "/release-management", "title": "Release management", "type": "protected", }, diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts index 07090d77c1..eed9ef85c9 100644 --- a/frontend/src/component/menu/routes.ts +++ b/frontend/src/component/menu/routes.ts @@ -48,7 +48,7 @@ import { Application } from 'component/application/Application'; import { Signals } from 'component/signals/Signals'; import { LazyCreateProject } from '../project/Project/CreateProject/LazyCreateProject'; import { PersonalDashboard } from '../personalDashboard/PersonalDashboard'; -import { ReleaseManagement } from 'component/releases/ReleaseManagement'; +import { ReleaseManagement } from 'component/releases/ReleaseManagement/ReleaseManagement'; export const routes: IRoute[] = [ // Splash @@ -248,7 +248,7 @@ export const routes: IRoute[] = [ menu: { mobile: true, advanced: true }, }, { - path: '/releases-management', + path: '/release-management', title: 'Release management', component: ReleaseManagement, type: 'protected', diff --git a/frontend/src/component/providers/AccessProvider/permissions.ts b/frontend/src/component/providers/AccessProvider/permissions.ts index 08a081e98a..48eea06871 100644 --- a/frontend/src/component/providers/AccessProvider/permissions.ts +++ b/frontend/src/component/providers/AccessProvider/permissions.ts @@ -48,3 +48,5 @@ export const PROJECT_USER_ACCESS_WRITE = 'PROJECT_USER_ACCESS_WRITE'; export const PROJECT_DEFAULT_STRATEGY_WRITE = 'PROJECT_DEFAULT_STRATEGY_WRITE'; export const PROJECT_CHANGE_REQUEST_WRITE = 'PROJECT_CHANGE_REQUEST_WRITE'; export const PROJECT_SETTINGS_WRITE = 'PROJECT_SETTINGS_WRITE'; + +export const CREATE_RELEASE_TEMPLATE = 'CREATE_RELEASE_TEMPLATE'; diff --git a/frontend/src/component/releases/ReleaseManagement.tsx b/frontend/src/component/releases/ReleaseManagement.tsx deleted file mode 100644 index 929eedca37..0000000000 --- a/frontend/src/component/releases/ReleaseManagement.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export const ReleaseManagement = () => { - return null; -}; diff --git a/frontend/src/component/releases/ReleaseManagement/EmptyTemplatesListMessage.tsx b/frontend/src/component/releases/ReleaseManagement/EmptyTemplatesListMessage.tsx new file mode 100644 index 0000000000..026d617585 --- /dev/null +++ b/frontend/src/component/releases/ReleaseManagement/EmptyTemplatesListMessage.tsx @@ -0,0 +1,38 @@ +import { styled } from '@mui/material'; +import { ReactComponent as ReleaseTemplateIcon } from 'assets/img/releaseTemplates.svg'; + +const NoReleaseTemplatesMessage = styled('div')(({ theme }) => ({ + textAlign: 'center', + padding: theme.spacing(1, 0, 0, 0), +})); + +const TemplatesEasierMessage = styled('div')(({ theme }) => ({ + textAlign: 'center', + padding: theme.spacing(1, 0, 9, 0), + color: theme.palette.text.secondary, +})); + +const StyledCenter = styled('div')(({ theme }) => ({ + textAlign: 'center', +})); + +const StyledDiv = styled('div')(({ theme }) => ({ + paddingTop: theme.spacing(5), +})); +export const EmptyTemplatesListMessage = () => { + return ( + + + + + + You have no release templates set up + + + Make the set up of strategies easier for your +
+ teams by creating templates +
+
+ ); +}; diff --git a/frontend/src/component/releases/ReleaseManagement/ReleaseManagement.tsx b/frontend/src/component/releases/ReleaseManagement/ReleaseManagement.tsx new file mode 100644 index 0000000000..5962d7eaab --- /dev/null +++ b/frontend/src/component/releases/ReleaseManagement/ReleaseManagement.tsx @@ -0,0 +1,56 @@ +import { PageContent } from 'component/common/PageContent/PageContent'; +import { Grid } from '@mui/material'; +import { styles as themeStyles } from 'component/common'; +import { usePageTitle } from 'hooks/usePageTitle'; +import { PageHeader } from 'component/common/PageHeader/PageHeader'; +import Add from '@mui/icons-material/Add'; +import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton'; +import { CREATE_RELEASE_TEMPLATE } from 'component/providers/AccessProvider/permissions'; +import { useNavigate } from 'react-router-dom'; +import { useReleasePlanTemplates } from 'hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates'; +import { EmptyTemplatesListMessage } from './EmptyTemplatesListMessage'; +import { ReleasePlanTemplateList } from './ReleasePlanTemplateList'; + +export const ReleaseManagement = () => { + usePageTitle('Release management'); + const navigate = useNavigate(); + const data = useReleasePlanTemplates(); + + return ( + <> + { + navigate( + '/release-management/create-template', + ); + }} + maxWidth='700px' + permission={CREATE_RELEASE_TEMPLATE} + disabled={false} + > + New template + + } + /> + } + > + {data.templates.length > 0 && ( + + + + )} + {data.templates.length === 0 && ( +
+ +
+ )} +
+ + ); +}; diff --git a/frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateCard.tsx b/frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateCard.tsx new file mode 100644 index 0000000000..7b155b5e17 --- /dev/null +++ b/frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateCard.tsx @@ -0,0 +1,81 @@ +import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; +import { ReactComponent as ReleaseTemplateIcon } from 'assets/img/releaseTemplates.svg'; +import { styled, Typography } from '@mui/material'; +import { ReleasePlanTemplateCardMenu } from './ReleasePlanTemplateCardMenu'; + +const StyledTemplateCard = styled('aside')(({ theme }) => ({ + height: '100%', + '&:hover': { + transition: 'background-color 0.2s ease-in-out', + backgroundColor: theme.palette.neutral.light, + }, + overflow: 'hidden', +})); + +const TemplateCardHeader = styled('div')(({ theme }) => ({ + backgroundColor: theme.palette.primary.main, + padding: theme.spacing(2.5), + borderTopLeftRadius: theme.shape.borderRadiusLarge, + borderTopRightRadius: theme.shape.borderRadiusLarge, +})); + +const TemplateCardBody = styled('div')(({ theme }) => ({ + padding: theme.spacing(1.25), + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadiusLarge, + borderTop: 'none', + borderTopLeftRadius: 0, + borderTopRightRadius: 0, + display: 'flex', + flexDirection: 'column', +})); + +const StyledCenter = styled('div')(({ theme }) => ({ + textAlign: 'center', +})); + +const StyledDiv = styled('div')(({ theme }) => ({ + display: 'flex', +})); + +const StyledCreatedBy = styled(Typography)(({ theme }) => ({ + color: theme.palette.text.secondary, + fontSize: theme.fontSizes.smallBody, + display: 'flex', + alignItems: 'center', + marginRight: 'auto', +})); + +const StyledMenu = styled('div')(({ theme }) => ({ + marginLeft: theme.spacing(1), + marginTop: theme.spacing(-1), + marginBottom: theme.spacing(-1), + marginRight: theme.spacing(-1), + display: 'flex', + alignItems: 'center', +})); + +export const ReleasePlanTemplateCard = ({ + template, +}: { template: IReleasePlanTemplate }) => { + return ( + + + + + + + +
{template.name}
+ + + Created by {template.createdByUserId} + + e.preventDefault()}> + + + +
+
+ ); +}; diff --git a/frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateCardMenu.tsx b/frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateCardMenu.tsx new file mode 100644 index 0000000000..921953fece --- /dev/null +++ b/frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateCardMenu.tsx @@ -0,0 +1,77 @@ +import { useState } from 'react'; +import { + IconButton, + Tooltip, + Menu, + MenuItem, + ListItemText, +} from '@mui/material'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; + +export const ReleasePlanTemplateCardMenu = ({ + template, +}: { template: IReleasePlanTemplate }) => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + + const closeMenu = () => { + setIsMenuOpen(false); + setAnchorEl(null); + }; + + const handleMenuClick = (event: React.SyntheticEvent) => { + if (isMenuOpen) { + closeMenu(); + } else { + setAnchorEl(event.currentTarget); + setIsMenuOpen(true); + } + }; + + return ( + <> + + + + + + + { + closeMenu(); + }} + > + Edit template + + { + closeMenu(); + }} + > + Delete template + + + + ); +}; diff --git a/frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateList.tsx b/frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateList.tsx new file mode 100644 index 0000000000..9ae7c1093c --- /dev/null +++ b/frontend/src/component/releases/ReleaseManagement/ReleasePlanTemplateList.tsx @@ -0,0 +1,21 @@ +import { Grid } from '@mui/material'; +import { ReleasePlanTemplateCard } from './ReleasePlanTemplateCard'; +import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; + +interface ITemplateList { + templates: IReleasePlanTemplate[]; +} + +export const ReleasePlanTemplateList: React.FC = ({ + templates, +}) => { + return ( + <> + {templates.map((template) => ( + + + + ))} + + ); +}; diff --git a/frontend/src/hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates.ts b/frontend/src/hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates.ts new file mode 100644 index 0000000000..3e08a42783 --- /dev/null +++ b/frontend/src/hooks/api/getters/useReleasePlanTemplates/useReleasePlanTemplates.ts @@ -0,0 +1,41 @@ +import { useContext, useMemo } from 'react'; +import useUiConfig from '../useUiConfig/useUiConfig'; +import AccessContext from 'contexts/AccessContext'; +import { formatApiPath } from 'utils/formatPath'; +import handleErrorResponses from '../httpErrorResponseHandler'; +import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR'; +import { useUiFlag } from 'hooks/useUiFlag'; +import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; + +const ENDPOINT = 'api/admin/release-plan-templates'; + +const DEFAULT_DATA: IReleasePlanTemplate[] = []; + +export const useReleasePlanTemplates = () => { + const { isAdmin } = useContext(AccessContext); + const { isEnterprise } = useUiConfig(); + const signalsEnabled = useUiFlag('releasePlans'); + + const { data, error, mutate } = useConditionalSWR( + isEnterprise() && isAdmin && signalsEnabled, + DEFAULT_DATA, + formatApiPath(ENDPOINT), + fetcher, + ); + + return useMemo( + () => ({ + templates: data ?? [], + loading: !error && !data, + refetch: () => mutate(), + error, + }), + [data, error, mutate], + ); +}; + +const fetcher = (path: string) => { + return fetch(path) + .then(handleErrorResponses('Release plan templates')) + .then((res) => res.json()); +}; diff --git a/frontend/src/interfaces/releasePlans.ts b/frontend/src/interfaces/releasePlans.ts new file mode 100644 index 0000000000..099a48a48b --- /dev/null +++ b/frontend/src/interfaces/releasePlans.ts @@ -0,0 +1,7 @@ +export interface IReleasePlanTemplate { + id: string; + name: string; + description: string; + createdAt: string; + createdByUserId: number; +}