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: {
|
||||
avgHealthCurrentWindow: 80,
|
||||
avgHealthPastWindow: 70,
|
||||
totalFlags: 39,
|
||||
potentiallyStaleFlags: 14,
|
||||
staleFlags: 13,
|
||||
activeFlags: 12,
|
||||
health: 81,
|
||||
},
|
||||
latestEvents: [{ summary: 'someone created a flag', id: 0 }],
|
||||
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('someone created a flag');
|
||||
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('No feature flag metrics data');
|
||||
await screen.findByText('production');
|
||||
|
@ -3,6 +3,8 @@ import type { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Lightbulb from '@mui/icons-material/LightbulbOutlined';
|
||||
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 }) => ({
|
||||
display: 'flex',
|
||||
@ -12,8 +14,16 @@ const TitleContainer = styled('div')(({ theme }) => ({
|
||||
justifyContent: 'center',
|
||||
}));
|
||||
|
||||
const Health = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: theme.spacing(3),
|
||||
}));
|
||||
|
||||
const ActionBox = styled('article')(({ theme }) => ({
|
||||
padding: theme.spacing(4, 2),
|
||||
padding: theme.spacing(0, 2),
|
||||
display: 'flex',
|
||||
gap: theme.spacing(3),
|
||||
flexDirection: 'column',
|
||||
@ -128,6 +138,22 @@ export const ProjectSetupComplete: FC<{
|
||||
<h3>Project Insight</h3>
|
||||
</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
|
||||
trend={projectHealthTrend}
|
||||
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 { Alert, Box, styled, Typography, useTheme } from '@mui/material';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Alert, Box, styled, Typography } from '@mui/material';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import type { ProjectInsightsSchemaHealth } from '../../../../../openapi';
|
||||
import type { FC } from 'react';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
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),
|
||||
}));
|
||||
import { FlagCounts } from './FlagCounts';
|
||||
|
||||
const Container = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
@ -33,16 +12,9 @@ const Container = styled(Box)(({ theme }) => ({
|
||||
gap: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StatusWithDot = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
}));
|
||||
|
||||
export const ProjectHealth: FC<{ health: ProjectInsightsSchemaHealth }> = ({
|
||||
health,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const { staleCount, potentiallyStaleCount, activeCount, rating } = health;
|
||||
|
||||
@ -73,39 +45,13 @@ export const ProjectHealth: FC<{ health: ProjectInsightsSchemaHealth }> = ({
|
||||
potentiallyStale={potentiallyStaleCount}
|
||||
health={rating}
|
||||
/>
|
||||
<FlagCounts>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<FlagCounts
|
||||
projectId={projectId}
|
||||
activeCount={activeCount}
|
||||
potentiallyStaleCount={potentiallyStaleCount}
|
||||
staleCount={staleCount}
|
||||
/>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
|
@ -18,4 +18,9 @@ export type PersonalDashboardProjectDetailsSchemaInsights = {
|
||||
* @nullable
|
||||
*/
|
||||
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: {
|
||||
avgHealthPastWindow: 80,
|
||||
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 {
|
||||
latestEvents,
|
||||
onboardingStatus,
|
||||
@ -173,6 +183,11 @@ export class PersonalDashboardService {
|
||||
insights: {
|
||||
avgHealthCurrentWindow,
|
||||
avgHealthPastWindow,
|
||||
totalFlags,
|
||||
potentiallyStaleFlags,
|
||||
staleFlags,
|
||||
activeFlags: totalFlags - staleFlags - potentiallyStaleFlags,
|
||||
health: currentHealth,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -19,7 +19,15 @@ export const personalDashboardProjectDetailsSchema = {
|
||||
type: 'object',
|
||||
description: 'Insights for the project',
|
||||
additionalProperties: false,
|
||||
required: ['avgHealthCurrentWindow', 'avgHealthPastWindow'],
|
||||
required: [
|
||||
'avgHealthCurrentWindow',
|
||||
'avgHealthPastWindow',
|
||||
'totalFlags',
|
||||
'activeFlags',
|
||||
'staleFlags',
|
||||
'potentiallyStaleFlags',
|
||||
'health',
|
||||
],
|
||||
properties: {
|
||||
avgHealthCurrentWindow: {
|
||||
type: 'number',
|
||||
@ -35,6 +43,33 @@ export const personalDashboardProjectDetailsSchema = {
|
||||
example: 70,
|
||||
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,
|
||||
|
Loading…
Reference in New Issue
Block a user