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 (
+ <>
+
+
+
+
+
+
+ >
+ );
+};
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;
+}