From 7682429839643c168319a531b84c668a32e490c6 Mon Sep 17 00:00:00 2001 From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com> Date: Fri, 23 Feb 2024 09:05:59 +0100 Subject: [PATCH] Dashboard health stats widget (#6262) --- .../executiveDashboard/ExecutiveDashboard.tsx | 26 +- .../FlagsProjectChart/FlagsProjectChart.tsx | 2 +- .../executiveDashboard/Gauge/Gauge.tsx | 4 +- .../HealthStats/HealthStats.tsx | 312 ++++++++++++++++++ .../LineChart/LineChartComponent.tsx | 25 +- .../ProjectHealthChart/ProjectHealthChart.tsx | 2 +- .../TimeToProductionChart.tsx | 2 +- .../UsersChart/UsersChart.tsx | 35 +- .../executive-dashboard-utils.ts | 21 +- .../executiveDashboard/useProjectChartData.ts | 4 +- frontend/src/themes/dark-theme.ts | 12 + frontend/src/themes/theme.ts | 12 + frontend/src/themes/themeTypes.ts | 12 + 13 files changed, 425 insertions(+), 44 deletions(-) create mode 100644 frontend/src/component/executiveDashboard/HealthStats/HealthStats.tsx diff --git a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx index 25c9e682f4..3645731f9b 100644 --- a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx +++ b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx @@ -18,6 +18,7 @@ import { ProjectHealthChart } from './ProjectHealthChart/ProjectHealthChart'; import { TimeToProductionChart } from './TimeToProductionChart/TimeToProductionChart'; import { TimeToProduction } from './TimeToProduction/TimeToProduction'; import { ProjectSelect, allOption } from './ProjectSelect/ProjectSelect'; +import { HealthStats } from './HealthStats/HealthStats'; const StyledGrid = styled(Box)(({ theme }) => ({ display: 'grid', @@ -144,20 +145,27 @@ export const ExecutiveDashboard: VFC = () => { projectFlagTrends={filteredProjectFlagTrends} /> - + + + + - - {/* FIXME: data from API */} - + + - + diff --git a/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChart.tsx b/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChart.tsx index 53e9a4aad1..88a943c9a1 100644 --- a/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChart.tsx +++ b/frontend/src/component/executiveDashboard/FlagsProjectChart/FlagsProjectChart.tsx @@ -13,5 +13,5 @@ export const FlagsProjectChart: VFC = ({ }) => { const data = useProjectChartData(projectFlagTrends, 'total'); - return ; + return ; }; diff --git a/frontend/src/component/executiveDashboard/Gauge/Gauge.tsx b/frontend/src/component/executiveDashboard/Gauge/Gauge.tsx index 6cc1af7050..d0d059d8f1 100644 --- a/frontend/src/component/executiveDashboard/Gauge/Gauge.tsx +++ b/frontend/src/component/executiveDashboard/Gauge/Gauge.tsx @@ -23,7 +23,7 @@ const describeArc = (radius: number, startAngle: number, endAngle: number) => { const start = polarToCartesian(0, 0, radius, endAngle); const end = polarToCartesian(0, 0, radius, startAngle); const largeArcFlag = endAngle - startAngle <= Math.PI ? '0' : '1'; - const d = [ + const dSvgAttribute = [ 'M', start.x, start.y, @@ -36,7 +36,7 @@ const describeArc = (radius: number, startAngle: number, endAngle: number) => { end.x, end.y, ].join(' '); - return d; + return dSvgAttribute; }; const GaugeLines = () => { diff --git a/frontend/src/component/executiveDashboard/HealthStats/HealthStats.tsx b/frontend/src/component/executiveDashboard/HealthStats/HealthStats.tsx new file mode 100644 index 0000000000..821847742b --- /dev/null +++ b/frontend/src/component/executiveDashboard/HealthStats/HealthStats.tsx @@ -0,0 +1,312 @@ +import { VFC } from 'react'; +import { useThemeMode } from 'hooks/useThemeMode'; +import { useTheme } from '@mui/material'; + +interface IHealthStatsProps { + value: number; + healthy: number; + stale: number; + potenciallyStale: number; +} + +export const HealthStats: VFC = ({ + value, + healthy, + stale, + potenciallyStale, +}) => { + const { themeMode } = useThemeMode(); + const isDark = themeMode === 'dark'; + const theme = useTheme(); + + return ( + + Health Stats + + + + + + {value !== undefined ? `${value}%` : 'N/A'} + + + + + + {healthy || 0} + + + Healthy + + + + + + {stale || 0} + + + Stale + + + + + + {potenciallyStale || 0} + + + + Potentially + + + stale + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/frontend/src/component/executiveDashboard/LineChart/LineChartComponent.tsx b/frontend/src/component/executiveDashboard/LineChart/LineChartComponent.tsx index 9350647c7f..34b2bec050 100644 --- a/frontend/src/component/executiveDashboard/LineChart/LineChartComponent.tsx +++ b/frontend/src/component/executiveDashboard/LineChart/LineChartComponent.tsx @@ -28,6 +28,7 @@ const createOptions = ( locationSettings: ILocationSettings, setTooltip: React.Dispatch>, isPlaceholder?: boolean, + localTooltip?: boolean, ) => ({ responsive: true, @@ -82,13 +83,12 @@ const createOptions = ( tooltip: { enabled: false, external: (context: any) => { - const tooltipModel = context.tooltip; - if (tooltipModel.opacity === 0) { + const tooltip = context.tooltip; + if (tooltip.opacity === 0) { setTooltip(null); return; } - const tooltip = context.tooltip; setTooltip({ caretX: tooltip?.caretX, caretY: tooltip?.caretY, @@ -106,15 +106,17 @@ const createOptions = ( }, locale: locationSettings.locale, interaction: { - intersect: false, + intersect: localTooltip || false, axis: 'x', }, elements: { point: { radius: 0, + hitRadius: 15, }, }, // cubicInterpolationMode: 'monotone', + tension: 0.1, color: theme.palette.text.secondary, scales: { y: { @@ -127,12 +129,14 @@ const createOptions = ( ticks: { color: theme.palette.text.secondary, display: !isPlaceholder, + precision: 0, }, }, x: { type: 'time', time: { - unit: 'month', + unit: 'day', + tooltipFormat: 'PPP', }, grid: { color: 'transparent', @@ -206,14 +210,21 @@ const LineChartComponent: VFC<{ data: ChartData<'line', (number | ScatterDataPoint | null)[], unknown>; aspectRatio?: number; cover?: ReactNode; -}> = ({ data, aspectRatio, cover }) => { + isLocalTooltip?: boolean; +}> = ({ data, aspectRatio, cover, isLocalTooltip }) => { const theme = useTheme(); const { locationSettings } = useLocationSettings(); const [tooltip, setTooltip] = useState(null); const options = useMemo( () => - createOptions(theme, locationSettings, setTooltip, Boolean(cover)), + createOptions( + theme, + locationSettings, + setTooltip, + Boolean(cover), + isLocalTooltip, + ), [theme, locationSettings], ); diff --git a/frontend/src/component/executiveDashboard/ProjectHealthChart/ProjectHealthChart.tsx b/frontend/src/component/executiveDashboard/ProjectHealthChart/ProjectHealthChart.tsx index b54989a185..f436fa836d 100644 --- a/frontend/src/component/executiveDashboard/ProjectHealthChart/ProjectHealthChart.tsx +++ b/frontend/src/component/executiveDashboard/ProjectHealthChart/ProjectHealthChart.tsx @@ -13,5 +13,5 @@ export const ProjectHealthChart: VFC = ({ }) => { const data = useProjectChartData(projectFlagTrends, 'health'); - return ; + return ; }; diff --git a/frontend/src/component/executiveDashboard/TimeToProductionChart/TimeToProductionChart.tsx b/frontend/src/component/executiveDashboard/TimeToProductionChart/TimeToProductionChart.tsx index 5c3ec80cff..b1ebe444c8 100644 --- a/frontend/src/component/executiveDashboard/TimeToProductionChart/TimeToProductionChart.tsx +++ b/frontend/src/component/executiveDashboard/TimeToProductionChart/TimeToProductionChart.tsx @@ -13,5 +13,5 @@ export const TimeToProductionChart: VFC = ({ }) => { const data = useProjectChartData(projectFlagTrends, 'timeToProduction'); - return ; + return ; }; diff --git a/frontend/src/component/executiveDashboard/UsersChart/UsersChart.tsx b/frontend/src/component/executiveDashboard/UsersChart/UsersChart.tsx index 44d590feda..b70fd3efa9 100644 --- a/frontend/src/component/executiveDashboard/UsersChart/UsersChart.tsx +++ b/frontend/src/component/executiveDashboard/UsersChart/UsersChart.tsx @@ -7,6 +7,7 @@ import { LineChart, NotEnoughData, } from '../LineChart/LineChart'; +import { useUiFlag } from 'hooks/useUiFlag'; interface IUsersChartProps { userTrends: ExecutiveSummarySchema['userTrends']; @@ -17,6 +18,7 @@ export const UsersChart: VFC = ({ userTrends, isLoading, }) => { + const showInactiveUsers = useUiFlag('showInactiveUsers'); const theme = useTheme(); const notEnoughData = userTrends.length < 2; const placeholderData = useMemo( @@ -50,23 +52,28 @@ export const UsersChart: VFC = ({ data: userTrends.map((item) => item.total), borderColor: theme.palette.primary.light, backgroundColor: fillGradientPrimary, + pointBackgroundColor: theme.palette.primary.main, fill: true, order: 3, }, - { - label: 'Active users', - data: userTrends.map((item) => item.active), - borderColor: theme.palette.success.border, - backgroundColor: theme.palette.success.border, - order: 2, - }, - { - label: 'Inactive users', - data: userTrends.map((item) => item.inactive), - borderColor: theme.palette.warning.border, - backgroundColor: theme.palette.warning.border, - order: 1, - }, + ...(showInactiveUsers + ? [ + { + label: 'Active users', + data: userTrends.map((item) => item.active), + borderColor: theme.palette.success.border, + backgroundColor: theme.palette.success.border, + order: 2, + }, + { + label: 'Inactive users', + data: userTrends.map((item) => item.inactive), + borderColor: theme.palette.warning.border, + backgroundColor: theme.palette.warning.border, + order: 1, + }, + ] + : []), ], }), [theme, userTrends], diff --git a/frontend/src/component/executiveDashboard/executive-dashboard-utils.ts b/frontend/src/component/executiveDashboard/executive-dashboard-utils.ts index e71f771285..7c682425a8 100644 --- a/frontend/src/component/executiveDashboard/executive-dashboard-utils.ts +++ b/frontend/src/component/executiveDashboard/executive-dashboard-utils.ts @@ -1,9 +1,16 @@ -// TODO: Replace this function with something more tailored to our color palette -export const getRandomColor = () => { - const letters = '0123456789ABCDEF'; - let color = '#'; - for (let i = 0; i < 6; i++) { - color += letters[Math.floor(Math.random() * 16)]; +import { colors } from 'themes/colors'; + +export const getProjectColor = (str: string): string => { + if (str === 'default') { + // Special case for default project - use primary color + return colors.purple[800]; } - return color; + + let hash = 0; + + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + const c = (hash & 0x00ffffff).toString(16).toUpperCase(); + return `#${'00000'.substring(0, 6 - c.length)}${c}`; }; diff --git a/frontend/src/component/executiveDashboard/useProjectChartData.ts b/frontend/src/component/executiveDashboard/useProjectChartData.ts index 2f9788f962..1ccea8ad48 100644 --- a/frontend/src/component/executiveDashboard/useProjectChartData.ts +++ b/frontend/src/component/executiveDashboard/useProjectChartData.ts @@ -3,7 +3,7 @@ import { ExecutiveSummarySchema, ExecutiveSummarySchemaProjectFlagTrendsItem, } from '../../openapi'; -import { getRandomColor } from './executive-dashboard-utils'; +import { getProjectColor } from './executive-dashboard-utils'; import { useTheme } from '@mui/material'; type ProjectFlagTrends = ExecutiveSummarySchema['projectFlagTrends']; @@ -27,7 +27,7 @@ export const useProjectChartData = ( const datasets = Object.entries(groupedFlagTrends).map( ([project, trends]) => { - const color = getRandomColor(); + const color = getProjectColor(project); return { label: project, data: trends.map((item) => { diff --git a/frontend/src/themes/dark-theme.ts b/frontend/src/themes/dark-theme.ts index 920f1fd0b1..6bbb91fe13 100644 --- a/frontend/src/themes/dark-theme.ts +++ b/frontend/src/themes/dark-theme.ts @@ -290,6 +290,18 @@ const theme = { sectionLine: '#8c89bf', text: colors.grey[800], }, + health: { + mainCircleBackground: '#34325E', + orbit: '#4C4992', + circles: '#2B2A3C', + text: colors.grey[500], + title: colors.grey[50], + healthy: colors.purple[800], + stale: colors.red[800], + potenciallyStale: colors.orange[800], + gradientStale: '#8A3E45', + gradientPotenciallyStale: '#875D21', + }, }, }, }; diff --git a/frontend/src/themes/theme.ts b/frontend/src/themes/theme.ts index 75395e6413..0f9d926d11 100644 --- a/frontend/src/themes/theme.ts +++ b/frontend/src/themes/theme.ts @@ -275,6 +275,18 @@ export const theme = { sectionLine: colors.purple[500], text: colors.grey[600], }, + health: { + mainCircleBackground: colors.purple[800], + orbit: colors.grey[300], + circles: colors.grey[50], + text: colors.grey[900], + title: colors.grey[50], + healthy: colors.purple[800], + stale: colors.red[800], + potenciallyStale: colors.orange[800], + gradientStale: colors.red[300], + gradientPotenciallyStale: colors.orange[500], + }, }, }, }; diff --git a/frontend/src/themes/themeTypes.ts b/frontend/src/themes/themeTypes.ts index aa66d9f6c2..835c1e41b0 100644 --- a/frontend/src/themes/themeTypes.ts +++ b/frontend/src/themes/themeTypes.ts @@ -133,6 +133,18 @@ declare module '@mui/material/styles' { sectionLine: string; text: string; }; + health: { + mainCircleBackground: string; + orbit: string; + circles: string; + text: string; + title: string; + healthy: string; + stale: string; + potenciallyStale: string; + gradientStale: string; + gradientPotenciallyStale: string; + }; }; } interface Theme extends CustomTheme {}