mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
feat: health score components in personal dashboard (#8348)
This commit is contained in:
parent
f5c78605ed
commit
10dffcd232
@ -36,6 +36,11 @@ const setupLongRunningProject = () => {
|
|||||||
insights: {
|
insights: {
|
||||||
avgHealthCurrentWindow: 80,
|
avgHealthCurrentWindow: 80,
|
||||||
avgHealthPastWindow: 70,
|
avgHealthPastWindow: 70,
|
||||||
|
totalFlags: 39,
|
||||||
|
potentiallyStaleFlags: 14,
|
||||||
|
staleFlags: 13,
|
||||||
|
activeFlags: 12,
|
||||||
|
health: 81,
|
||||||
},
|
},
|
||||||
latestEvents: [{ summary: 'someone created a flag', id: 0 }],
|
latestEvents: [{ summary: 'someone created a flag', id: 0 }],
|
||||||
roles: [{ name: 'Member' }],
|
roles: [{ name: 'Member' }],
|
||||||
@ -135,7 +140,10 @@ test('Render personal dashboard for a long running project', async () => {
|
|||||||
await screen.findByText('70%'); // avg health past window
|
await screen.findByText('70%'); // avg health past window
|
||||||
await screen.findByText('someone created a flag');
|
await screen.findByText('someone created a flag');
|
||||||
await screen.findByText('Member');
|
await screen.findByText('Member');
|
||||||
|
await screen.findByText('81%'); // current health score
|
||||||
|
await screen.findByText('12 feature flags'); // active flags
|
||||||
|
await screen.findByText('13 feature flags'); // stale flags
|
||||||
|
await screen.findByText('14 feature flags'); // potentially stale flags
|
||||||
await screen.findByText('myFlag');
|
await screen.findByText('myFlag');
|
||||||
await screen.findByText('No feature flag metrics data');
|
await screen.findByText('No feature flag metrics data');
|
||||||
await screen.findByText('production');
|
await screen.findByText('production');
|
||||||
|
@ -3,6 +3,8 @@ import type { FC } from 'react';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Lightbulb from '@mui/icons-material/LightbulbOutlined';
|
import Lightbulb from '@mui/icons-material/LightbulbOutlined';
|
||||||
import type { PersonalDashboardProjectDetailsSchemaInsights } from '../../openapi';
|
import type { PersonalDashboardProjectDetailsSchemaInsights } from '../../openapi';
|
||||||
|
import { ProjectHealthChart } from 'component/project/Project/ProjectInsights/ProjectHealth/ProjectHealthChart';
|
||||||
|
import { FlagCounts } from '../project/Project/ProjectInsights/ProjectHealth/FlagCounts';
|
||||||
|
|
||||||
const TitleContainer = styled('div')(({ theme }) => ({
|
const TitleContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -12,8 +14,16 @@ const TitleContainer = styled('div')(({ theme }) => ({
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const Health = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
const ActionBox = styled('article')(({ theme }) => ({
|
const ActionBox = styled('article')(({ theme }) => ({
|
||||||
padding: theme.spacing(4, 2),
|
padding: theme.spacing(0, 2),
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: theme.spacing(3),
|
gap: theme.spacing(3),
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@ -128,6 +138,22 @@ export const ProjectSetupComplete: FC<{
|
|||||||
<h3>Project Insight</h3>
|
<h3>Project Insight</h3>
|
||||||
</TitleContainer>
|
</TitleContainer>
|
||||||
|
|
||||||
|
<Health>
|
||||||
|
<ProjectHealthChart
|
||||||
|
health={insights.health}
|
||||||
|
active={insights.activeFlags}
|
||||||
|
potentiallyStale={insights.potentiallyStaleFlags}
|
||||||
|
stale={insights.staleFlags}
|
||||||
|
/>
|
||||||
|
<FlagCounts
|
||||||
|
projectId={project}
|
||||||
|
activeCount={insights.activeFlags}
|
||||||
|
potentiallyStaleCount={insights.potentiallyStaleFlags}
|
||||||
|
staleCount={insights.staleFlags}
|
||||||
|
hideLinks={true}
|
||||||
|
/>
|
||||||
|
</Health>
|
||||||
|
|
||||||
<ProjectHealthMessage
|
<ProjectHealthMessage
|
||||||
trend={projectHealthTrend}
|
trend={projectHealthTrend}
|
||||||
insights={insights}
|
insights={insights}
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
import { Box, styled, useTheme } from '@mui/material';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
const Dot = styled('span', {
|
||||||
|
shouldForwardProp: (prop) => prop !== 'color',
|
||||||
|
})<{ color?: string }>(({ theme, color }) => ({
|
||||||
|
height: '15px',
|
||||||
|
width: '15px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'inline-block',
|
||||||
|
backgroundColor: color,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const FlagCountsWrapper = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const FlagsCount = styled('p')(({ theme }) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
marginLeft: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StatusWithDot = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FlagCounts: FC<{
|
||||||
|
projectId: string;
|
||||||
|
activeCount: number;
|
||||||
|
potentiallyStaleCount: number;
|
||||||
|
staleCount: number;
|
||||||
|
hideLinks?: boolean;
|
||||||
|
}> = ({
|
||||||
|
projectId,
|
||||||
|
activeCount,
|
||||||
|
potentiallyStaleCount,
|
||||||
|
staleCount,
|
||||||
|
hideLinks = false,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlagCountsWrapper>
|
||||||
|
<Box>
|
||||||
|
<StatusWithDot>
|
||||||
|
<Dot color={theme.palette.success.border} />
|
||||||
|
<Box sx={{ fontWeight: 'bold' }}>Active</Box>
|
||||||
|
</StatusWithDot>
|
||||||
|
<FlagsCount>{activeCount} feature flags</FlagsCount>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<StatusWithDot>
|
||||||
|
<Dot color={theme.palette.warning.border} />
|
||||||
|
<Box sx={{ fontWeight: 'bold' }}>Potentially stale</Box>
|
||||||
|
{hideLinks ? null : (
|
||||||
|
<Link to='/feature-toggle-type'>(configure)</Link>
|
||||||
|
)}
|
||||||
|
</StatusWithDot>
|
||||||
|
<FlagsCount>{potentiallyStaleCount} feature flags</FlagsCount>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<StatusWithDot>
|
||||||
|
<Dot color={theme.palette.error.border} />
|
||||||
|
<Box sx={{ fontWeight: 'bold' }}>Stale</Box>
|
||||||
|
{hideLinks ? null : (
|
||||||
|
<Link to={`/projects/${projectId}?state=IS%3Astale`}>
|
||||||
|
(view flags)
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</StatusWithDot>
|
||||||
|
<FlagsCount>{staleCount} feature flags</FlagsCount>
|
||||||
|
</Box>
|
||||||
|
</FlagCountsWrapper>
|
||||||
|
);
|
||||||
|
};
|
@ -1,31 +1,10 @@
|
|||||||
import { ProjectHealthChart } from './ProjectHealthChart';
|
import { ProjectHealthChart } from './ProjectHealthChart';
|
||||||
import { Alert, Box, styled, Typography, useTheme } from '@mui/material';
|
import { Alert, Box, styled, Typography } from '@mui/material';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import type { ProjectInsightsSchemaHealth } from '../../../../../openapi';
|
import type { ProjectInsightsSchemaHealth } from '../../../../../openapi';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { FlagCounts } from './FlagCounts';
|
||||||
const Dot = styled('span', {
|
|
||||||
shouldForwardProp: (prop) => prop !== 'color',
|
|
||||||
})<{ color?: string }>(({ theme, color }) => ({
|
|
||||||
height: '15px',
|
|
||||||
width: '15px',
|
|
||||||
borderRadius: '50%',
|
|
||||||
display: 'inline-block',
|
|
||||||
backgroundColor: color,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const FlagsCount = styled('p')(({ theme }) => ({
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
marginLeft: theme.spacing(3),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const FlagCounts = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const Container = styled(Box)(({ theme }) => ({
|
const Container = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -33,16 +12,9 @@ const Container = styled(Box)(({ theme }) => ({
|
|||||||
gap: theme.spacing(2),
|
gap: theme.spacing(2),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StatusWithDot = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const ProjectHealth: FC<{ health: ProjectInsightsSchemaHealth }> = ({
|
export const ProjectHealth: FC<{ health: ProjectInsightsSchemaHealth }> = ({
|
||||||
health,
|
health,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const { staleCount, potentiallyStaleCount, activeCount, rating } = health;
|
const { staleCount, potentiallyStaleCount, activeCount, rating } = health;
|
||||||
|
|
||||||
@ -73,39 +45,13 @@ export const ProjectHealth: FC<{ health: ProjectInsightsSchemaHealth }> = ({
|
|||||||
potentiallyStale={potentiallyStaleCount}
|
potentiallyStale={potentiallyStaleCount}
|
||||||
health={rating}
|
health={rating}
|
||||||
/>
|
/>
|
||||||
<FlagCounts>
|
|
||||||
<Box>
|
<FlagCounts
|
||||||
<StatusWithDot>
|
projectId={projectId}
|
||||||
<Dot color={theme.palette.success.border} />
|
activeCount={activeCount}
|
||||||
<Box sx={{ fontWeight: 'bold' }}>Active</Box>
|
potentiallyStaleCount={potentiallyStaleCount}
|
||||||
</StatusWithDot>
|
staleCount={staleCount}
|
||||||
<FlagsCount>{activeCount} feature flags</FlagsCount>
|
/>
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<StatusWithDot>
|
|
||||||
<Dot color={theme.palette.warning.border} />
|
|
||||||
<Box sx={{ fontWeight: 'bold' }}>
|
|
||||||
Potentially stale
|
|
||||||
</Box>
|
|
||||||
<Link to='/feature-toggle-type'>(configure)</Link>
|
|
||||||
</StatusWithDot>
|
|
||||||
<FlagsCount>
|
|
||||||
{potentiallyStaleCount} feature flags
|
|
||||||
</FlagsCount>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<StatusWithDot>
|
|
||||||
<Dot color={theme.palette.error.border} />
|
|
||||||
<Box sx={{ fontWeight: 'bold' }}>Stale</Box>
|
|
||||||
<Link
|
|
||||||
to={`/projects/${projectId}?state=IS%3Astale`}
|
|
||||||
>
|
|
||||||
(view flags)
|
|
||||||
</Link>
|
|
||||||
</StatusWithDot>
|
|
||||||
<FlagsCount>{staleCount} feature flags</FlagsCount>
|
|
||||||
</Box>
|
|
||||||
</FlagCounts>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
@ -18,4 +18,9 @@ export type PersonalDashboardProjectDetailsSchemaInsights = {
|
|||||||
* @nullable
|
* @nullable
|
||||||
*/
|
*/
|
||||||
avgHealthPastWindow: number | null;
|
avgHealthPastWindow: number | null;
|
||||||
|
totalFlags: number;
|
||||||
|
activeFlags: number;
|
||||||
|
staleFlags: number;
|
||||||
|
potentiallyStaleFlags: number;
|
||||||
|
health: number;
|
||||||
};
|
};
|
||||||
|
@ -326,6 +326,11 @@ test('should return personal dashboard project details', async () => {
|
|||||||
insights: {
|
insights: {
|
||||||
avgHealthPastWindow: 80,
|
avgHealthPastWindow: 80,
|
||||||
avgHealthCurrentWindow: 91,
|
avgHealthCurrentWindow: 91,
|
||||||
|
totalFlags: 3,
|
||||||
|
potentiallyStaleFlags: 0,
|
||||||
|
staleFlags: 0,
|
||||||
|
activeFlags: 3,
|
||||||
|
health: 100,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -165,6 +165,16 @@ export class PersonalDashboardService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [projectInsights] =
|
||||||
|
await this.projectReadModel.getProjectsForInsights({
|
||||||
|
id: projectId,
|
||||||
|
});
|
||||||
|
const totalFlags = projectInsights?.featureCount || 0;
|
||||||
|
const potentiallyStaleFlags =
|
||||||
|
projectInsights?.potentiallyStaleFeatureCount || 0;
|
||||||
|
const staleFlags = projectInsights?.staleFeatureCount || 0;
|
||||||
|
const currentHealth = projectInsights?.health || 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
latestEvents,
|
latestEvents,
|
||||||
onboardingStatus,
|
onboardingStatus,
|
||||||
@ -173,6 +183,11 @@ export class PersonalDashboardService {
|
|||||||
insights: {
|
insights: {
|
||||||
avgHealthCurrentWindow,
|
avgHealthCurrentWindow,
|
||||||
avgHealthPastWindow,
|
avgHealthPastWindow,
|
||||||
|
totalFlags,
|
||||||
|
potentiallyStaleFlags,
|
||||||
|
staleFlags,
|
||||||
|
activeFlags: totalFlags - staleFlags - potentiallyStaleFlags,
|
||||||
|
health: currentHealth,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,15 @@ export const personalDashboardProjectDetailsSchema = {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
description: 'Insights for the project',
|
description: 'Insights for the project',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
required: ['avgHealthCurrentWindow', 'avgHealthPastWindow'],
|
required: [
|
||||||
|
'avgHealthCurrentWindow',
|
||||||
|
'avgHealthPastWindow',
|
||||||
|
'totalFlags',
|
||||||
|
'activeFlags',
|
||||||
|
'staleFlags',
|
||||||
|
'potentiallyStaleFlags',
|
||||||
|
'health',
|
||||||
|
],
|
||||||
properties: {
|
properties: {
|
||||||
avgHealthCurrentWindow: {
|
avgHealthCurrentWindow: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@ -35,6 +43,33 @@ export const personalDashboardProjectDetailsSchema = {
|
|||||||
example: 70,
|
example: 70,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
},
|
},
|
||||||
|
totalFlags: {
|
||||||
|
type: 'number',
|
||||||
|
example: 100,
|
||||||
|
description: 'The current number of all flags',
|
||||||
|
},
|
||||||
|
activeFlags: {
|
||||||
|
type: 'number',
|
||||||
|
example: 98,
|
||||||
|
description: 'The current number of active flags',
|
||||||
|
},
|
||||||
|
staleFlags: {
|
||||||
|
type: 'number',
|
||||||
|
example: 0,
|
||||||
|
description:
|
||||||
|
'The current number of user marked stale flags',
|
||||||
|
},
|
||||||
|
potentiallyStaleFlags: {
|
||||||
|
type: 'number',
|
||||||
|
example: 2,
|
||||||
|
description:
|
||||||
|
'The current number of time calculated potentially stale flags',
|
||||||
|
},
|
||||||
|
health: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'The current health score of the project',
|
||||||
|
example: 80,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onboardingStatus: projectOverviewSchema.properties.onboardingStatus,
|
onboardingStatus: projectOverviewSchema.properties.onboardingStatus,
|
||||||
|
Loading…
Reference in New Issue
Block a user