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 { 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>
|
||||
|
Loading…
Reference in New Issue
Block a user