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:
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/billing': BillingIcon,
|
||||
'/history': EventLogIcon,
|
||||
'/releases-management': LaunchIcon,
|
||||
'/release-management': LaunchIcon,
|
||||
'/personal': PersonalDashboardIcon,
|
||||
GitHub: GitHubIcon,
|
||||
Documentation: LibraryBooksIcon,
|
||||
|
@ -240,7 +240,7 @@ exports[`returns all baseRoutes 1`] = `
|
||||
"enterprise",
|
||||
],
|
||||
},
|
||||
"path": "/releases-management",
|
||||
"path": "/release-management",
|
||||
"title": "Release management",
|
||||
"type": "protected",
|
||||
},
|
||||
|
@ -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',
|
||||
|
@ -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';
|
||||
|
@ -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