1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-13 13:48:59 +02:00

feat: rename health to technical debt (#10063)

On insights and project status, we would like to show "technica debt"
instead of "health". New value is that of `1/health`, or simplified:
`healthy flags / total flags`
This commit is contained in:
Tymoteusz Czech 2025-06-04 11:01:17 +02:00 committed by GitHub
parent 8d7a0fdd7f
commit 37548c3436
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 254 additions and 75 deletions

View File

@ -18,6 +18,7 @@ import { allOption } from 'component/common/ProjectSelect/ProjectSelect';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { WidgetTitle } from './components/WidgetTitle/WidgetTitle.tsx';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useFlag } from '@unleash/proxy-client-react';
export interface IChartsProps {
flagTrends: InstanceInsightsSchema['flagTrends'];
@ -104,6 +105,7 @@ export const InsightsCharts: FC<IChartsProps> = ({
const showAllProjects = projects[0] === allOption.id;
const isOneProjectSelected = projects.length === 1;
const { isEnterprise } = useUiConfig();
const healthToDebtEnabled = useFlag('healthToTechDebt');
const lastUserTrend = userTrends[userTrends.length - 1];
const lastFlagTrend = flagTrends[flagTrends.length - 1];
@ -189,9 +191,15 @@ export const InsightsCharts: FC<IChartsProps> = ({
potentiallyStale={summary.potentiallyStale}
title={
<WidgetTitle
title='Health'
title={
healthToDebtEnabled
? 'Technical debt'
: 'Health'
}
tooltip={
'Percentage of flags that are not stale or potentially stale.'
healthToDebtEnabled
? 'Percentage of stale and potentially stale flags.'
: 'Percentage of flags that are not stale or potentially stale.'
}
/>
}

View File

@ -5,6 +5,8 @@ import { Badge } from 'component/common/Badge/Badge';
import type { TooltipState } from 'component/insights/components/LineChart/ChartTooltip/ChartTooltip';
import { HorizontalDistributionChart } from 'component/insights/components/HorizontalDistributionChart/HorizontalDistributionChart';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useFlag } from '@unleash/proxy-client-react';
import { getTechnicalDebtColor } from 'utils/getTechnicalDebtColor.ts';
const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({
padding: theme.spacing(2),
@ -33,6 +35,13 @@ const getHealthBadgeColor = (health?: number | null) => {
return 'error';
};
const getTechnicalDebtBadgeColor = (technicalDebt?: number | null) => {
if (technicalDebt === undefined || technicalDebt === null) {
return 'info';
}
return getTechnicalDebtColor(technicalDebt);
};
const Distribution = ({ stale = 0, potentiallyStale = 0, total = 0 }) => {
const healthyFlagCount = total - stale - potentiallyStale;
@ -99,12 +108,16 @@ const Distribution = ({ stale = 0, potentiallyStale = 0, total = 0 }) => {
export const HealthTooltip: FC<{ tooltip: TooltipState | null }> = ({
tooltip,
}) => {
const healthToTechDebtEnabled = useFlag('healthToTechDebt');
const data = tooltip?.dataPoints.map((point) => {
return {
label: point.label,
title: point.dataset.label,
color: point.dataset.borderColor,
value: point.raw as InstanceInsightsSchemaProjectFlagTrendsItem,
value: point.raw as InstanceInsightsSchemaProjectFlagTrendsItem & {
technicalDebt?: number | null;
}, // TODO: get from backend
};
});
@ -137,7 +150,9 @@ export const HealthTooltip: FC<{ tooltip: TooltipState | null }> = ({
color='textSecondary'
component='span'
>
Project health
{healthToTechDebtEnabled
? 'Technical debt'
: 'Project health'}
</Typography>
</StyledItemHeader>
<StyledItemHeader>
@ -150,9 +165,21 @@ export const HealthTooltip: FC<{ tooltip: TooltipState | null }> = ({
</Typography>
<strong>{point.title}</strong>
</Typography>
<Badge color={getHealthBadgeColor(point.value.health)}>
{point.value.health}%
</Badge>
{healthToTechDebtEnabled ? (
<Badge
color={getTechnicalDebtBadgeColor(
point.value.technicalDebt,
)}
>
{point.value.technicalDebt}%
</Badge>
) : (
<Badge
color={getHealthBadgeColor(point.value.health)}
>
{point.value.health}%
</Badge>
)}
</StyledItemHeader>{' '}
<Divider
sx={(theme) => ({ margin: theme.spacing(1.5, 0) })}

View File

@ -2,7 +2,10 @@ import 'chartjs-adapter-date-fns';
import { type FC, useMemo } from 'react';
import type { InstanceInsightsSchema } from 'openapi';
import { HealthTooltip } from './HealthChartTooltip/HealthChartTooltip.tsx';
import { useProjectChartData } from 'component/insights/hooks/useProjectChartData';
import {
calculateTechDebt,
useProjectChartData,
} from 'component/insights/hooks/useProjectChartData';
import {
fillGradientPrimary,
LineChart,
@ -11,6 +14,7 @@ import {
import { useTheme } from '@mui/material';
import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends';
import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData';
import { useFlag } from '@unleash/proxy-client-react';
interface IProjectHealthChartProps {
projectFlagTrends: GroupedDataByProject<
@ -42,6 +46,7 @@ export const ProjectHealthChart: FC<IProjectHealthChartProps> = ({
const projectsData = useProjectChartData(projectFlagTrends);
const theme = useTheme();
const placeholderData = usePlaceholderData();
const healthToTechDebtEnabled = useFlag('healthToTechDebt');
const aggregateHealthData = useMemo(() => {
const labels = Array.from(
@ -80,9 +85,18 @@ export const ProjectHealthChart: FC<IProjectHealthChartProps> = ({
return {
datasets: [
{
label: 'Health',
label: healthToTechDebtEnabled
? 'Technical debt'
: 'Health',
data: weeks.map((item) => ({
health: item.total ? calculateHealth(item) : undefined,
...(healthToTechDebtEnabled
? {
technicalDebt: item.total
? calculateTechDebt(item)
: undefined,
}
: {}),
date: item.date,
total: item.total,
stale: item.stale,
@ -117,7 +131,12 @@ export const ProjectHealthChart: FC<IProjectHealthChartProps> = ({
notEnoughData
? {}
: {
parsing: { yAxisKey: 'health', xAxisKey: 'date' },
parsing: {
yAxisKey: healthToTechDebtEnabled
? 'technicalDebt'
: 'health',
xAxisKey: 'date',
},
scales: {
y: {
min: 0,

View File

@ -1,6 +1,7 @@
import type { FC, ReactNode } from 'react';
import { Box, Divider, Link, styled } from '@mui/material';
import { ReactComponent as InstanceHealthIcon } from 'assets/icons/instance-health.svg';
import { useFlag } from '@unleash/proxy-client-react';
interface IHealthStatsProps {
value?: string | number;
@ -69,43 +70,59 @@ export const HealthStats: FC<IHealthStatsProps> = ({
stale,
potentiallyStale,
title,
}) => (
<StyledContainer>
<StyledHeader>
<StyledSection>{title}</StyledSection>
<StyledSection>{/* TODO: trend */}</StyledSection>
</StyledHeader>
<Divider />
<StyledSection>
<StyledStatsRow>
<StyledIcon />
Instance health
<StyledMainValue>{`${value || 0}%`}</StyledMainValue>
</StyledStatsRow>
</StyledSection>
<Divider />
<FlagsSection>
<StyledStatsRow>
Healthy flags
<StyledValue>{healthy || 0}</StyledValue>
</StyledStatsRow>
<StyledStatsRow>
Stale flags
<StyledValue>{stale || 0}</StyledValue>
</StyledStatsRow>
<StyledStatsRow>
Potentially stale flags
<StyledValue>{potentiallyStale || 0}</StyledValue>
</StyledStatsRow>
<ExplanationRow>
<Link
href='https://docs.getunleash.io/reference/insights#health'
target='_blank'
rel='noreferrer'
>
What affects instance health?
</Link>
</ExplanationRow>
</FlagsSection>
</StyledContainer>
);
}) => {
const healthToDebtEnabled = useFlag('healthToTechDebt');
// TODO: get the following from backend
const unhealthy = stale + potentiallyStale;
const technicalDebtValue = (
(unhealthy / (healthy + unhealthy)) *
100
).toFixed(1);
return (
<StyledContainer>
<StyledHeader>
<StyledSection>{title}</StyledSection>
</StyledHeader>
<Divider />
<StyledSection>
<StyledStatsRow>
<StyledIcon />
{healthToDebtEnabled ? 'Technical debt' : 'Instance health'}
{healthToDebtEnabled ? (
<StyledMainValue>{`${technicalDebtValue}%`}</StyledMainValue>
) : (
<StyledMainValue>{`${value || 0}%`}</StyledMainValue>
)}
</StyledStatsRow>
</StyledSection>
<Divider />
<FlagsSection>
<StyledStatsRow>
Healthy flags
<StyledValue>{healthy || 0}</StyledValue>
</StyledStatsRow>
<StyledStatsRow>
Stale flags
<StyledValue>{stale || 0}</StyledValue>
</StyledStatsRow>
<StyledStatsRow>
Potentially stale flags
<StyledValue>{potentiallyStale || 0}</StyledValue>
</StyledStatsRow>
<ExplanationRow>
<Link
href='https://docs.getunleash.io/reference/insights#health'
target='_blank'
rel='noreferrer'
>
{healthToDebtEnabled
? 'What affects technical debt?'
: 'What affects instance health?'}
</Link>
</ExplanationRow>
</FlagsSection>
</StyledContainer>
);
};

View File

@ -4,13 +4,29 @@ import { useProjectColor } from './useProjectColor.js';
import { useTheme } from '@mui/material';
import type { GroupedDataByProject } from './useGroupedProjectTrends.js';
import useProjects from 'hooks/api/getters/useProjects/useProjects';
import { useFlag } from '@unleash/proxy-client-react';
type ProjectFlagTrends = InstanceInsightsSchema['projectFlagTrends'];
export const calculateTechDebt = (item: {
total: number;
stale: number;
potentiallyStale: number;
}) => {
if (!item.total) {
return '0';
}
return (((item.stale + item.potentiallyStale) / item.total) * 100).toFixed(
2,
);
};
export const useProjectChartData = (
projectFlagTrends: GroupedDataByProject<ProjectFlagTrends>,
) => {
const theme = useTheme();
const healthToTechDebtEnabled = useFlag('healthToTechDebt');
const getProjectColor = useProjectColor();
const { projects } = useProjects();
const projectNames = new Map(
@ -23,7 +39,17 @@ export const useProjectChartData = (
const color = getProjectColor(project);
return {
label: projectNames.get(project) || project,
data: trends,
data: trends.map((item) => ({
...item,
...(healthToTechDebtEnabled
? {
technicalDebt: item.total
? calculateTechDebt(item)
: undefined,
}
: {}),
})),
borderColor: color,
backgroundColor: color,
fill: false,

View File

@ -23,6 +23,7 @@ import {
StyledWidgetContent,
StyledWidgetStats,
} from '../InsightsCharts.styles';
import { useFlag } from '@unleash/proxy-client-react';
export const PerformanceInsights: FC = () => {
const statePrefix = 'performance-';
@ -47,6 +48,8 @@ export const PerformanceInsights: FC = () => {
state[`${statePrefix}to`]?.values[0],
);
const healthToTechDebtEnabled = useFlag('healthToTechDebt');
const projects = state[`${statePrefix}project`]?.values ?? [allOption.id];
const showAllProjects = projects[0] === allOption.id;
@ -131,12 +134,21 @@ export const PerformanceInsights: FC = () => {
stale={summary.stale}
potentiallyStale={summary.potentiallyStale}
title={
<WidgetTitle
title='Health'
tooltip={
'Percentage of flags that are not stale or potentially stale.'
}
/>
healthToTechDebtEnabled ? (
<WidgetTitle
title='Technical debt'
tooltip={
'Percentage of flags that are stale or potentially stale.'
}
/>
) : (
<WidgetTitle
title='Health'
tooltip={
'Percentage of flags that are not stale or potentially stale.'
}
/>
)
}
/>
</StyledWidgetStats>

View File

@ -39,10 +39,14 @@ import { ActionBox } from './ActionBox.tsx';
import useLoading from 'hooks/useLoading';
import { NoProjectsContactAdmin } from './NoProjectsContactAdmin.tsx';
import { AskOwnerToAddYouToTheirProject } from './AskOwnerToAddYouToTheirProject.tsx';
import { useFlag } from '@unleash/proxy-client-react';
const ActiveProjectDetails: FC<{
project: PersonalDashboardSchemaProjectsItem;
}> = ({ project }) => {
const healthToTechDebtEnabled = useFlag('healthToTechDebt');
const techicalDebt = 100 - project.health; // TODO: health to technical debt from backend
return (
<Box sx={{ display: 'flex', gap: 2 }}>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
@ -63,10 +67,10 @@ const ActiveProjectDetails: FC<{
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant='subtitle2' color='primary'>
{project.health}%
{healthToTechDebtEnabled ? techicalDebt : project.health}%
</Typography>
<Typography variant='caption' color='text.secondary'>
health
{healthToTechDebtEnabled ? 'technical debt' : 'health'}
</Typography>
</Box>
</Box>

View File

@ -4,6 +4,7 @@ import { Link } from 'react-router-dom';
import Lightbulb from '@mui/icons-material/LightbulbOutlined';
import type { PersonalDashboardProjectDetailsSchemaInsights } from 'openapi';
import { ActionBox } from './ActionBox.tsx';
import { useFlag } from '@unleash/proxy-client-react';
const PercentageScore = styled('span')(({ theme }) => ({
fontWeight: theme.typography.fontWeightBold,
@ -57,9 +58,11 @@ const ProjectHealthMessage: FC<{
insights: PersonalDashboardProjectDetailsSchemaInsights;
project: string;
}> = ({ trend, insights, project }) => {
const healthToTechDebtEnabled = useFlag('healthToTechDebt');
const { avgHealthCurrentWindow, avgHealthPastWindow, health } = insights;
const improveMessage =
'Remember to archive your stale feature flags to keep the project health growing.';
const improveMessage = healthToTechDebtEnabled
? 'Remember to archive your stale feature flags to keep the technical debt low.'
: 'Remember to archive your stale feature flags to keep the project health growing.';
const keepDoingMessage =
'This indicates that you are doing a good job of archiving your feature flags.';

View File

@ -373,6 +373,7 @@ export const Project = () => {
}}
/>
<Routes>
{/* FIXME: remove /health with `healthToTechDebt` flag - redirect to project status */}
<Route path='health' element={<ProjectHealth />} />
<Route
path='access/*'

View File

@ -5,6 +5,8 @@ import { useProjectStatus } from 'hooks/api/getters/useProjectStatus/useProjectS
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { HealthGridTile } from './ProjectHealthGrid.styles';
import { PrettifyLargeNumber } from 'component/common/PrettifyLargeNumber/PrettifyLargeNumber';
import { useFlag } from '@unleash/proxy-client-react';
import { getTechnicalDebtColor } from 'utils/getTechnicalDebtColor.ts';
const ChartRadius = 40;
const ChartStrokeWidth = 13;
@ -80,6 +82,29 @@ const UnhealthyFlagBox = ({ flagCount }: { flagCount: number }) => {
);
};
const useHealthColor = (healthRating: number) => {
const theme = useTheme();
if (healthRating <= 24) {
return theme.palette.error.main;
}
if (healthRating <= 74) {
return theme.palette.warning.border;
}
return theme.palette.success.border;
};
const useTechnicalDebtColor = (techicalDebt: number) => {
const theme = useTheme();
switch (getTechnicalDebtColor(techicalDebt)) {
case 'error':
return theme.palette.error.main;
case 'warning':
return theme.palette.warning.border;
default:
return theme.palette.success.border;
}
};
const Wrapper = styled(HealthGridTile)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
@ -92,22 +117,21 @@ export const ProjectHealth = () => {
const {
data: { health, staleFlags },
} = useProjectStatus(projectId);
const healthRating = health.current;
const { isOss } = useUiConfig();
const theme = useTheme();
const circumference = 2 * Math.PI * ChartRadius; //
const healthToDebtEnabled = useFlag('healthToTechDebt');
const circumference = 2 * Math.PI * ChartRadius;
const healthRating = health.current;
const technicalDebt = 100 - healthRating; // TODO: get from backend
const gapLength = 0.3;
const filledLength = 1 - gapLength;
const offset = 0.75 - gapLength / 2;
const healthLength = (healthRating / 100) * circumference * 0.7;
const technicalDebtLength = (technicalDebt / 100) * circumference * 0.7;
const healthColor =
healthRating >= 0 && healthRating <= 24
? theme.palette.error.main
: healthRating >= 25 && healthRating <= 74
? theme.palette.warning.border
: theme.palette.success.border;
const healthColor = useHealthColor(healthRating);
const technicalDebtColor = useTechnicalDebtColor(technicalDebt);
return (
<Wrapper>
@ -129,9 +153,17 @@ export const ProjectHealth = () => {
cy='50'
r={ChartRadius}
fill='none'
stroke={healthColor}
stroke={
healthToDebtEnabled
? technicalDebtColor
: healthColor
}
strokeWidth={ChartStrokeWidth}
strokeDasharray={`${healthLength} ${circumference - healthLength}`}
strokeDasharray={
healthToDebtEnabled
? `${technicalDebtLength} ${circumference - technicalDebtLength}`
: `${healthLength} ${circumference - healthLength}`
}
strokeDashoffset={offset * circumference}
/>
<text
@ -142,17 +174,30 @@ export const ProjectHealth = () => {
fill={theme.palette.text.primary}
fontSize={theme.typography.h1.fontSize}
>
{healthRating}%
{healthToDebtEnabled ? technicalDebt : healthRating}
%
</text>
</StyledSVG>
</SVGWrapper>
<TextContainer>
<Typography>
Your current project health rating is {healthRating}%
{healthToDebtEnabled ? (
<>
Your current technical debt rating is{' '}
{technicalDebt}%.
</>
) : (
<>
Your current project health rating is{' '}
{healthRating}%.
</>
)}
</Typography>
{!isOss() && (
<Link to={`/insights?project=IS%3A${projectId}`}>
View health over time
{healthToDebtEnabled
? 'View technical debt over time'
: 'View health over time'}
</Link>
)}
</TextContainer>

View File

@ -9,6 +9,7 @@ import { ProjectHealthGrid } from './ProjectHealthGrid.tsx';
import { useFeedback } from 'component/feedbackNew/useFeedback';
import FeedbackIcon from '@mui/icons-material/ChatOutlined';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useFlag } from '@unleash/proxy-client-react';
const ModalContentContainer = styled('section')(({ theme }) => ({
minHeight: '100vh',
@ -140,6 +141,7 @@ export const ProjectStatusModal = ({ open, onClose, onFollowLink }: Props) => {
});
};
const { isOss } = useUiConfig();
const healthToDebtEnabled = useFlag('healthToTechDebt');
return (
<DynamicSidebarModal
@ -159,7 +161,9 @@ export const ProjectStatusModal = ({ open, onClose, onFollowLink }: Props) => {
</HeaderRow>
<WidgetContainer>
<Row>
<RowHeader>Health</RowHeader>
<RowHeader>
{healthToDebtEnabled ? 'Technical debt' : 'Health'}
</RowHeader>
<ProjectHealthGrid />
</Row>
{!isOss() && (

View File

@ -0,0 +1,13 @@
/**
* Consistent values for boundries between healthy, warning and error colors
* @param technicalDebt {Number} 0-100
*/
export const getTechnicalDebtColor = (technicalDebt: number) => {
if (technicalDebt >= 50) {
return 'error';
}
if (technicalDebt >= 25) {
return 'warning';
}
return 'success';
};