diff --git a/frontend/src/component/insights/InsightsCharts.tsx b/frontend/src/component/insights/InsightsCharts.tsx index 1c53bb5bab..5833b50e77 100644 --- a/frontend/src/component/insights/InsightsCharts.tsx +++ b/frontend/src/component/insights/InsightsCharts.tsx @@ -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 = ({ 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 = ({ potentiallyStale={summary.potentiallyStale} title={ } diff --git a/frontend/src/component/insights/componentsChart/ProjectHealthChart/HealthChartTooltip/HealthChartTooltip.tsx b/frontend/src/component/insights/componentsChart/ProjectHealthChart/HealthChartTooltip/HealthChartTooltip.tsx index 96f35ba764..7f3162d904 100644 --- a/frontend/src/component/insights/componentsChart/ProjectHealthChart/HealthChartTooltip/HealthChartTooltip.tsx +++ b/frontend/src/component/insights/componentsChart/ProjectHealthChart/HealthChartTooltip/HealthChartTooltip.tsx @@ -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'} @@ -150,9 +165,21 @@ export const HealthTooltip: FC<{ tooltip: TooltipState | null }> = ({ {point.title} - - {point.value.health}% - + {healthToTechDebtEnabled ? ( + + {point.value.technicalDebt}% + + ) : ( + + {point.value.health}% + + )} {' '} ({ margin: theme.spacing(1.5, 0) })} diff --git a/frontend/src/component/insights/componentsChart/ProjectHealthChart/ProjectHealthChart.tsx b/frontend/src/component/insights/componentsChart/ProjectHealthChart/ProjectHealthChart.tsx index df2652e33d..721ed46c1f 100644 --- a/frontend/src/component/insights/componentsChart/ProjectHealthChart/ProjectHealthChart.tsx +++ b/frontend/src/component/insights/componentsChart/ProjectHealthChart/ProjectHealthChart.tsx @@ -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 = ({ 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 = ({ 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 = ({ notEnoughData ? {} : { - parsing: { yAxisKey: 'health', xAxisKey: 'date' }, + parsing: { + yAxisKey: healthToTechDebtEnabled + ? 'technicalDebt' + : 'health', + xAxisKey: 'date', + }, scales: { y: { min: 0, diff --git a/frontend/src/component/insights/componentsStat/HealthStats/HealthStats.tsx b/frontend/src/component/insights/componentsStat/HealthStats/HealthStats.tsx index 0519679cc4..80ada4d234 100644 --- a/frontend/src/component/insights/componentsStat/HealthStats/HealthStats.tsx +++ b/frontend/src/component/insights/componentsStat/HealthStats/HealthStats.tsx @@ -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 = ({ stale, potentiallyStale, title, -}) => ( - - - {title} - {/* TODO: trend */} - - - - - - Instance health - {`${value || 0}%`} - - - - - - Healthy flags - {healthy || 0} - - - Stale flags - {stale || 0} - - - Potentially stale flags - {potentiallyStale || 0} - - - - What affects instance health? - - - - -); +}) => { + const healthToDebtEnabled = useFlag('healthToTechDebt'); + + // TODO: get the following from backend + const unhealthy = stale + potentiallyStale; + const technicalDebtValue = ( + (unhealthy / (healthy + unhealthy)) * + 100 + ).toFixed(1); + + return ( + + + {title} + + + + + + {healthToDebtEnabled ? 'Technical debt' : 'Instance health'} + {healthToDebtEnabled ? ( + {`${technicalDebtValue}%`} + ) : ( + {`${value || 0}%`} + )} + + + + + + Healthy flags + {healthy || 0} + + + Stale flags + {stale || 0} + + + Potentially stale flags + {potentiallyStale || 0} + + + + {healthToDebtEnabled + ? 'What affects technical debt?' + : 'What affects instance health?'} + + + + + ); +}; diff --git a/frontend/src/component/insights/hooks/useProjectChartData.ts b/frontend/src/component/insights/hooks/useProjectChartData.ts index e5429d5c03..2e9836eff8 100644 --- a/frontend/src/component/insights/hooks/useProjectChartData.ts +++ b/frontend/src/component/insights/hooks/useProjectChartData.ts @@ -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, ) => { 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, diff --git a/frontend/src/component/insights/sections/PerformanceInsights.tsx b/frontend/src/component/insights/sections/PerformanceInsights.tsx index 8e5e560bb6..cb03518bd5 100644 --- a/frontend/src/component/insights/sections/PerformanceInsights.tsx +++ b/frontend/src/component/insights/sections/PerformanceInsights.tsx @@ -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={ - + healthToTechDebtEnabled ? ( + + ) : ( + + ) } /> diff --git a/frontend/src/component/personalDashboard/MyProjects.tsx b/frontend/src/component/personalDashboard/MyProjects.tsx index 573c7e1789..b24ee3415a 100644 --- a/frontend/src/component/personalDashboard/MyProjects.tsx +++ b/frontend/src/component/personalDashboard/MyProjects.tsx @@ -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 ( @@ -63,10 +67,10 @@ const ActiveProjectDetails: FC<{ - {project.health}% + {healthToTechDebtEnabled ? techicalDebt : project.health}% - health + {healthToTechDebtEnabled ? 'technical debt' : 'health'} diff --git a/frontend/src/component/personalDashboard/ProjectSetupComplete.tsx b/frontend/src/component/personalDashboard/ProjectSetupComplete.tsx index 8471f4bb1a..19a5edbf65 100644 --- a/frontend/src/component/personalDashboard/ProjectSetupComplete.tsx +++ b/frontend/src/component/personalDashboard/ProjectSetupComplete.tsx @@ -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.'; diff --git a/frontend/src/component/project/Project/Project.tsx b/frontend/src/component/project/Project/Project.tsx index bcaaf80949..b7f7584b1a 100644 --- a/frontend/src/component/project/Project/Project.tsx +++ b/frontend/src/component/project/Project/Project.tsx @@ -373,6 +373,7 @@ export const Project = () => { }} /> + {/* FIXME: remove /health with `healthToTechDebt` flag - redirect to project status */} } /> { ); }; +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 ( @@ -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} /> { fill={theme.palette.text.primary} fontSize={theme.typography.h1.fontSize} > - {healthRating}% + {healthToDebtEnabled ? technicalDebt : healthRating} + % - Your current project health rating is {healthRating}% + {healthToDebtEnabled ? ( + <> + Your current technical debt rating is{' '} + {technicalDebt}%. + + ) : ( + <> + Your current project health rating is{' '} + {healthRating}%. + + )} {!isOss() && ( - View health over time + {healthToDebtEnabled + ? 'View technical debt over time' + : 'View health over time'} )} diff --git a/frontend/src/component/project/Project/ProjectStatus/ProjectStatusModal.tsx b/frontend/src/component/project/Project/ProjectStatus/ProjectStatusModal.tsx index 32fe1a453f..6cff5c07a3 100644 --- a/frontend/src/component/project/Project/ProjectStatus/ProjectStatusModal.tsx +++ b/frontend/src/component/project/Project/ProjectStatus/ProjectStatusModal.tsx @@ -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 ( { - Health + + {healthToDebtEnabled ? 'Technical debt' : 'Health'} + {!isOss() && ( diff --git a/frontend/src/utils/getTechnicalDebtColor.ts b/frontend/src/utils/getTechnicalDebtColor.ts new file mode 100644 index 0000000000..9686982319 --- /dev/null +++ b/frontend/src/utils/getTechnicalDebtColor.ts @@ -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'; +};