1
0
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:
Jaanus Sellin 2024-03-18 16:22:06 +02:00 committed by GitHub
parent 19ae9b5486
commit e4af2fbcd5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 304 additions and 5 deletions

View File

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

View File

@ -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 &ldquo;
{globalFilter}
&rdquo;
</TablePlaceholder>
}
/>
}
/>
</Container>
);
};

View File

@ -1,4 +1,5 @@
import { Box, styled } from '@mui/material';
import { LeadTimeForChanges } from './LeadTimeForChanges/LeadTimeForChanges';
const Grid = styled(Box)(({ theme }) => ({
display: 'grid',
@ -14,10 +15,6 @@ const Health = styled(Box)(({ theme }) => ({
gridColumn: 'span 5',
}));
const LeadTime = styled(Box)(({ theme }) => ({
gridColumn: 'span 5',
}));
const ToggleTypesUsed = styled(Box)(({ theme }) => ({
gridColumn: 'span 2',
}));
@ -38,7 +35,7 @@ export const ProjectInsights = () => {
flags
</Overview>
<Health>Project Health</Health>
<LeadTime>Lead time</LeadTime>
<LeadTimeForChanges />
<ToggleTypesUsed>Toggle types used</ToggleTypesUsed>
<ProjectMembers>Project members</ProjectMembers>
<ChangeRequests>Change Requests</ChangeRequests>