1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

Feat/release management overview (#8672)

This commit is contained in:
David Leek 2024-11-06 14:02:42 +01:00 committed by GitHub
parent 1b568d1503
commit fa597aa340
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 366 additions and 7 deletions

View File

@ -0,0 +1,39 @@
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g clip-path="url(#clip0_28054_15343)">
<path d="M62.9208 75.54C69.8033 75.54 75.3826 69.9606 75.3826 63.0781C75.3826 56.1957 69.8033 50.6163 62.9208 50.6163C56.0383 50.6163 50.459 56.1957 50.459 63.0781C50.459 69.9606 56.0383 75.54 62.9208 75.54Z" fill="#194049"/>
<path d="M16.834 71.409V67.2181H12.6431V71.409H16.834Z" fill="#F5A69A"/>
<path d="M37.8536 15.4618H12.6445V67.2163H37.8536V15.4618Z" fill="#1A4049"/>
<path d="M37.8554 67.2181H63.0645L63.0645 15.4636H37.8554L37.8554 67.2181Z" fill="#31545C"/>
<path d="M55.4316 46.9564H21.668V55.1928H55.4316V46.9564Z" fill="#B3DAED"/>
<g opacity="0.8">
<mask id="mask0_28054_15343" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="21" y="46" width="17" height="10">
<path d="M37.8389 46.9564V55.2346L21.668 55.1491V46.9564H37.8389Z" fill="white"/>
</mask>
<g mask="url(#mask0_28054_15343)">
<rect width="114.233" height="158.028" transform="matrix(-0.851658 0.524097 0.524097 0.851658 44.8789 -54.2181)" fill="url(#pattern0_28054_15343)"/>
</g>
</g>
<path d="M55.4316 27.4454H21.668V35.6818H55.4316V27.4454Z" fill="#B3DAED"/>
<g opacity="0.8">
<mask id="mask1_28054_15343" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="21" y="27" width="17" height="9">
<path d="M37.8389 27.4454V35.7236L21.668 35.6382V27.4454H37.8389Z" fill="white"/>
</mask>
<g mask="url(#mask1_28054_15343)">
<rect width="114.233" height="158.028" transform="matrix(-0.851658 0.524097 0.524097 0.851658 44.8789 -73.731)" fill="url(#pattern1_28054_15343)"/>
</g>
</g>
<path d="M56.5577 15.4636H63.0723V8.94908H56.5577V15.4636Z" fill="#F5A69A"/>
<path d="M63.0728 19.6545H67.2637V15.4636H63.0728V19.6545Z" fill="#F5A69A"/>
</g>
<defs>
<pattern id="pattern0_28054_15343" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_28054_15343" transform="scale(0.00145773 0.00105374)"/>
</pattern>
<pattern id="pattern1_28054_15343" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_28054_15343" transform="scale(0.00145773 0.00105374)"/>
</pattern>
<clipPath id="clip0_28054_15343">
<rect width="80" height="80" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -58,7 +58,7 @@ const icons: Record<string, typeof SvgIcon> = {
'/admin/cors': CorsIcon,
'/admin/billing': BillingIcon,
'/history': EventLogIcon,
'/releases-management': LaunchIcon,
'/release-management': LaunchIcon,
'/personal': PersonalDashboardIcon,
GitHub: GitHubIcon,
Documentation: LibraryBooksIcon,

View File

@ -240,7 +240,7 @@ exports[`returns all baseRoutes 1`] = `
"enterprise",
],
},
"path": "/releases-management",
"path": "/release-management",
"title": "Release management",
"type": "protected",
},

View File

@ -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',

View File

@ -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';

View File

@ -1,3 +0,0 @@
export const ReleaseManagement = () => {
return null;
};

View File

@ -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 (
<StyledDiv>
<StyledCenter>
<ReleaseTemplateIcon />
</StyledCenter>
<NoReleaseTemplatesMessage>
You have no release templates set up
</NoReleaseTemplatesMessage>
<TemplatesEasierMessage>
Make the set up of strategies easier for your
<br />
teams by creating templates
</TemplatesEasierMessage>
</StyledDiv>
);
};

View File

@ -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 (
<>
<PageContent
header={
<PageHeader
title={`Release templates`}
actions={
<ResponsiveButton
Icon={Add}
onClick={() => {
navigate(
'/release-management/create-template',
);
}}
maxWidth='700px'
permission={CREATE_RELEASE_TEMPLATE}
disabled={false}
>
New template
</ResponsiveButton>
}
/>
}
>
{data.templates.length > 0 && (
<Grid container spacing={2}>
<ReleasePlanTemplateList templates={data.templates} />
</Grid>
)}
{data.templates.length === 0 && (
<div className={themeStyles.fullwidth}>
<EmptyTemplatesListMessage />
</div>
)}
</PageContent>
</>
);
};

View File

@ -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 (
<StyledTemplateCard>
<TemplateCardHeader>
<StyledCenter>
<ReleaseTemplateIcon />
</StyledCenter>
</TemplateCardHeader>
<TemplateCardBody>
<div>{template.name}</div>
<StyledDiv>
<StyledCreatedBy>
Created by {template.createdByUserId}
</StyledCreatedBy>
<StyledMenu onClick={(e) => e.preventDefault()}>
<ReleasePlanTemplateCardMenu template={template} />
</StyledMenu>
</StyledDiv>
</TemplateCardBody>
</StyledTemplateCard>
);
};

View File

@ -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<Element | null>(null);
const closeMenu = () => {
setIsMenuOpen(false);
setAnchorEl(null);
};
const handleMenuClick = (event: React.SyntheticEvent) => {
if (isMenuOpen) {
closeMenu();
} else {
setAnchorEl(event.currentTarget);
setIsMenuOpen(true);
}
};
return (
<>
<Tooltip title='Release plan template actions' arrow describeChild>
<IconButton
id={template.id}
aria-controls={isMenuOpen ? 'actions-menu' : undefined}
aria-haspopup='true'
aria-expanded={isMenuOpen ? 'true' : undefined}
onClick={handleMenuClick}
type='button'
>
<MoreVertIcon />
</IconButton>
</Tooltip>
<Menu
id='project-card-menu'
open={Boolean(anchorEl)}
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
onClose={handleMenuClick}
>
<MenuItem
onClick={() => {
closeMenu();
}}
>
<ListItemText>Edit template</ListItemText>
</MenuItem>
<MenuItem
onClick={() => {
closeMenu();
}}
>
<ListItemText>Delete template </ListItemText>
</MenuItem>
</Menu>
</>
);
};

View File

@ -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<ITemplateList> = ({
templates,
}) => {
return (
<>
{templates.map((template) => (
<Grid key={template.id} item xs={6} md={4}>
<ReleasePlanTemplateCard template={template} />
</Grid>
))}
</>
);
};

View File

@ -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<IReleasePlanTemplate[]>(
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());
};

View File

@ -0,0 +1,7 @@
export interface IReleasePlanTemplate {
id: string;
name: string;
description: string;
createdAt: string;
createdByUserId: number;
}