mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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