mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-09 13:47:13 +02:00
Feat/release management overview (#8672)
This commit is contained in:
parent
1b568d1503
commit
fa597aa340
39
frontend/src/assets/img/releaseTemplates.svg
Normal file
39
frontend/src/assets/img/releaseTemplates.svg
Normal 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 |
@ -58,7 +58,7 @@ const icons: Record<string, typeof SvgIcon> = {
|
|||||||
'/admin/cors': CorsIcon,
|
'/admin/cors': CorsIcon,
|
||||||
'/admin/billing': BillingIcon,
|
'/admin/billing': BillingIcon,
|
||||||
'/history': EventLogIcon,
|
'/history': EventLogIcon,
|
||||||
'/releases-management': LaunchIcon,
|
'/release-management': LaunchIcon,
|
||||||
'/personal': PersonalDashboardIcon,
|
'/personal': PersonalDashboardIcon,
|
||||||
GitHub: GitHubIcon,
|
GitHub: GitHubIcon,
|
||||||
Documentation: LibraryBooksIcon,
|
Documentation: LibraryBooksIcon,
|
||||||
|
@ -240,7 +240,7 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"enterprise",
|
"enterprise",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"path": "/releases-management",
|
"path": "/release-management",
|
||||||
"title": "Release management",
|
"title": "Release management",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
@ -48,7 +48,7 @@ import { Application } from 'component/application/Application';
|
|||||||
import { Signals } from 'component/signals/Signals';
|
import { Signals } from 'component/signals/Signals';
|
||||||
import { LazyCreateProject } from '../project/Project/CreateProject/LazyCreateProject';
|
import { LazyCreateProject } from '../project/Project/CreateProject/LazyCreateProject';
|
||||||
import { PersonalDashboard } from '../personalDashboard/PersonalDashboard';
|
import { PersonalDashboard } from '../personalDashboard/PersonalDashboard';
|
||||||
import { ReleaseManagement } from 'component/releases/ReleaseManagement';
|
import { ReleaseManagement } from 'component/releases/ReleaseManagement/ReleaseManagement';
|
||||||
|
|
||||||
export const routes: IRoute[] = [
|
export const routes: IRoute[] = [
|
||||||
// Splash
|
// Splash
|
||||||
@ -248,7 +248,7 @@ export const routes: IRoute[] = [
|
|||||||
menu: { mobile: true, advanced: true },
|
menu: { mobile: true, advanced: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/releases-management',
|
path: '/release-management',
|
||||||
title: 'Release management',
|
title: 'Release management',
|
||||||
component: ReleaseManagement,
|
component: ReleaseManagement,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
|
@ -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_DEFAULT_STRATEGY_WRITE = 'PROJECT_DEFAULT_STRATEGY_WRITE';
|
||||||
export const PROJECT_CHANGE_REQUEST_WRITE = 'PROJECT_CHANGE_REQUEST_WRITE';
|
export const PROJECT_CHANGE_REQUEST_WRITE = 'PROJECT_CHANGE_REQUEST_WRITE';
|
||||||
export const PROJECT_SETTINGS_WRITE = 'PROJECT_SETTINGS_WRITE';
|
export const PROJECT_SETTINGS_WRITE = 'PROJECT_SETTINGS_WRITE';
|
||||||
|
|
||||||
|
export const CREATE_RELEASE_TEMPLATE = 'CREATE_RELEASE_TEMPLATE';
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
export const ReleaseManagement = () => {
|
|
||||||
return null;
|
|
||||||
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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());
|
||||||
|
};
|
7
frontend/src/interfaces/releasePlans.ts
Normal file
7
frontend/src/interfaces/releasePlans.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface IReleasePlanTemplate {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
createdAt: string;
|
||||||
|
createdByUserId: number;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user