mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-11 00:08:30 +01:00
feat: lead time for changes (#6592)
![image](https://github.com/Unleash/unleash/assets/964450/3b4d4ed7-6b8d-42b7-81ad-3f006eb1efca)
This commit is contained in:
parent
19ae9b5486
commit
e4af2fbcd5
@ -0,0 +1,41 @@
|
|||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
import { render } from 'utils/testRenderer';
|
||||||
|
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||||
|
import type { ProjectDoraMetricsSchema } from 'openapi';
|
||||||
|
import { LeadTimeForChanges } from './LeadTimeForChanges';
|
||||||
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
|
const server = testServerSetup();
|
||||||
|
|
||||||
|
const setupApi = (outdatedSdks: ProjectDoraMetricsSchema) => {
|
||||||
|
testServerRoute(server, '/api/admin/projects/default/dora', outdatedSdks);
|
||||||
|
};
|
||||||
|
|
||||||
|
test('Show outdated SDKs and apps using them', async () => {
|
||||||
|
setupApi({
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
name: 'ABCD',
|
||||||
|
timeToProduction: 57,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
projectAverage: 67,
|
||||||
|
});
|
||||||
|
render(
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path={'/projects/:projectId'}
|
||||||
|
element={<LeadTimeForChanges />}
|
||||||
|
/>
|
||||||
|
</Routes>,
|
||||||
|
{
|
||||||
|
route: '/projects/default',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findByText('Lead time for changes (per release toggle)');
|
||||||
|
await screen.findByText('ABCD');
|
||||||
|
await screen.findByText('57 days');
|
||||||
|
await screen.findByText('Low');
|
||||||
|
await screen.findByText('10 days');
|
||||||
|
});
|
@ -0,0 +1,261 @@
|
|||||||
|
import { Box, styled, Tooltip, Typography, useMediaQuery } from '@mui/material';
|
||||||
|
import { useProjectDoraMetrics } from 'hooks/api/getters/useProjectDoraMetrics/useProjectDoraMetrics';
|
||||||
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useTable, useGlobalFilter, useSortBy } from 'react-table';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
SortableTableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
TablePlaceholder,
|
||||||
|
} from 'component/common/Table';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
||||||
|
import theme from 'themes/theme';
|
||||||
|
|
||||||
|
const Container = styled(Box)(({ theme }) => ({
|
||||||
|
gridColumn: 'span 5',
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
padding: theme.spacing(3),
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
|
overflowY: 'auto',
|
||||||
|
maxHeight: theme.spacing(100),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const resolveDoraMetrics = (input: number) => {
|
||||||
|
const ONE_MONTH = 30;
|
||||||
|
const ONE_WEEK = 7;
|
||||||
|
|
||||||
|
if (input >= ONE_MONTH) {
|
||||||
|
return <Badge color='error'>Low</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input <= ONE_MONTH && input >= ONE_WEEK + 1) {
|
||||||
|
return <Badge>Medium</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input <= ONE_WEEK) {
|
||||||
|
return <Badge color='success'>High</Badge>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LeadTimeForChanges = () => {
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
|
||||||
|
const { dora, loading } = useProjectDoraMetrics(projectId);
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
if (loading) {
|
||||||
|
return Array(5).fill({
|
||||||
|
name: 'Featurename',
|
||||||
|
timeToProduction: 'Data for production',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return dora.features;
|
||||||
|
}, [dora, loading]);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
Header: 'Name',
|
||||||
|
accessor: 'name',
|
||||||
|
width: '40%',
|
||||||
|
Cell: ({
|
||||||
|
row: {
|
||||||
|
original: { name },
|
||||||
|
},
|
||||||
|
}: any) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
data-loading
|
||||||
|
sx={{
|
||||||
|
pl: 2,
|
||||||
|
pr: 1,
|
||||||
|
paddingTop: 2,
|
||||||
|
paddingBottom: 2,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sortType: 'alphanumeric',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Time to production',
|
||||||
|
id: 'timetoproduction',
|
||||||
|
align: 'center',
|
||||||
|
Cell: ({ row: { original } }: any) => (
|
||||||
|
<Tooltip
|
||||||
|
title='The time from the feature toggle of type release was created until it was turned on in a production environment'
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
{original.timeToProduction} days
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
width: 200,
|
||||||
|
disableGlobalFilter: true,
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: `Deviation`,
|
||||||
|
id: 'deviation',
|
||||||
|
align: 'center',
|
||||||
|
Cell: ({ row: { original } }: any) => (
|
||||||
|
<Tooltip
|
||||||
|
title={`Deviation from project average. Average for this project is: ${
|
||||||
|
dora.projectAverage || 0
|
||||||
|
} days`}
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
{Math.round(
|
||||||
|
(dora.projectAverage
|
||||||
|
? dora.projectAverage
|
||||||
|
: 0) - original.timeToProduction,
|
||||||
|
)}{' '}
|
||||||
|
days
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
width: 300,
|
||||||
|
disableGlobalFilter: true,
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'DORA',
|
||||||
|
id: 'dora',
|
||||||
|
align: 'center',
|
||||||
|
Cell: ({ row: { original } }: any) => (
|
||||||
|
<Tooltip
|
||||||
|
title='Dora score. High = less than a week to production. Medium = less than a month to production. Low = Less than 6 months to production'
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
data-loading
|
||||||
|
>
|
||||||
|
{resolveDoraMetrics(original.timeToProduction)}
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
width: 200,
|
||||||
|
disableGlobalFilter: true,
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[JSON.stringify(dora.features), loading],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialState = useMemo(
|
||||||
|
() => ({
|
||||||
|
sortBy: [
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
desc: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isExtraSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
|
const {
|
||||||
|
getTableProps,
|
||||||
|
getTableBodyProps,
|
||||||
|
headerGroups,
|
||||||
|
rows,
|
||||||
|
prepareRow,
|
||||||
|
state: { globalFilter },
|
||||||
|
setHiddenColumns,
|
||||||
|
} = useTable(
|
||||||
|
{
|
||||||
|
columns: columns as any[],
|
||||||
|
data,
|
||||||
|
initialState,
|
||||||
|
autoResetGlobalFilter: false,
|
||||||
|
autoResetSortBy: false,
|
||||||
|
disableSortRemove: true,
|
||||||
|
},
|
||||||
|
useGlobalFilter,
|
||||||
|
useSortBy,
|
||||||
|
);
|
||||||
|
|
||||||
|
useConditionallyHiddenColumns(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
condition: isExtraSmallScreen,
|
||||||
|
columns: ['deviation'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
setHiddenColumns,
|
||||||
|
columns,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Typography variant='h3'>
|
||||||
|
Lead time for changes (per release toggle)
|
||||||
|
</Typography>
|
||||||
|
<Table {...getTableProps()}>
|
||||||
|
<SortableTableHeader headerGroups={headerGroups} />
|
||||||
|
<TableBody {...getTableBodyProps()}>
|
||||||
|
{rows.map((row) => {
|
||||||
|
prepareRow(row);
|
||||||
|
return (
|
||||||
|
<TableRow hover {...row.getRowProps()}>
|
||||||
|
{row.cells.map((cell) => (
|
||||||
|
<TableCell {...cell.getCellProps()}>
|
||||||
|
{cell.render('Cell')}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={rows.length === 0}
|
||||||
|
show={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={globalFilter?.length > 0}
|
||||||
|
show={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No features with data found “
|
||||||
|
{globalFilter}
|
||||||
|
”
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
import { Box, styled } from '@mui/material';
|
import { Box, styled } from '@mui/material';
|
||||||
|
import { LeadTimeForChanges } from './LeadTimeForChanges/LeadTimeForChanges';
|
||||||
|
|
||||||
const Grid = styled(Box)(({ theme }) => ({
|
const Grid = styled(Box)(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@ -14,10 +15,6 @@ const Health = styled(Box)(({ theme }) => ({
|
|||||||
gridColumn: 'span 5',
|
gridColumn: 'span 5',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const LeadTime = styled(Box)(({ theme }) => ({
|
|
||||||
gridColumn: 'span 5',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const ToggleTypesUsed = styled(Box)(({ theme }) => ({
|
const ToggleTypesUsed = styled(Box)(({ theme }) => ({
|
||||||
gridColumn: 'span 2',
|
gridColumn: 'span 2',
|
||||||
}));
|
}));
|
||||||
@ -38,7 +35,7 @@ export const ProjectInsights = () => {
|
|||||||
flags
|
flags
|
||||||
</Overview>
|
</Overview>
|
||||||
<Health>Project Health</Health>
|
<Health>Project Health</Health>
|
||||||
<LeadTime>Lead time</LeadTime>
|
<LeadTimeForChanges />
|
||||||
<ToggleTypesUsed>Toggle types used</ToggleTypesUsed>
|
<ToggleTypesUsed>Toggle types used</ToggleTypesUsed>
|
||||||
<ProjectMembers>Project members</ProjectMembers>
|
<ProjectMembers>Project members</ProjectMembers>
|
||||||
<ChangeRequests>Change Requests</ChangeRequests>
|
<ChangeRequests>Change Requests</ChangeRequests>
|
||||||
|
Loading…
Reference in New Issue
Block a user