1
0
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:
Mateusz Kwasniewski 2024-10-03 10:21:27 +02:00 committed by GitHub
parent f5c78605ed
commit 10dffcd232
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 186 additions and 66 deletions

View File

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

View File

@ -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}

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
}, },
}; };
} }

View File

@ -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,