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

Dashboard health stats widget (#6262)

This commit is contained in:
Tymoteusz Czech 2024-02-23 09:05:59 +01:00 committed by GitHub
parent 474e53460a
commit 7682429839
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 425 additions and 44 deletions

View File

@ -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}
/>
</Widget>
<Widget
title='Health per project'
order={6}
span={largeChartSpan}
>
<Widget title='Average health' order={6}>
<HealthStats
// FIXME: data from API
value={90}
healthy={50}
stale={10}
potenciallyStale={5}
/>
</Widget>
<Widget title='Health per project' order={7} span={chartSpan}>
<ProjectHealthChart
projectFlagTrends={filteredProjectFlagTrends}
/>
</Widget>
<Widget title='Average time to production' order={7}>
{/* FIXME: data from API */}
<TimeToProduction daysToProduction={12} />
<Widget title='Average time to production' order={8}>
<TimeToProduction
//FIXME: data from API
daysToProduction={12}
/>
</Widget>
<Widget title='Time to production' order={8} span={chartSpan}>
<Widget title='Time to production' order={9} span={chartSpan}>
<TimeToProductionChart
projectFlagTrends={filteredProjectFlagTrends}
/>

View File

@ -13,5 +13,5 @@ export const FlagsProjectChart: VFC<IFlagsProjectChartProps> = ({
}) => {
const data = useProjectChartData(projectFlagTrends, 'total');
return <LineChart data={data} />;
return <LineChart data={data} isLocalTooltip />;
};

View File

@ -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 = () => {

View File

@ -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<IHealthStatsProps> = ({
value,
healthy,
stale,
potenciallyStale,
}) => {
const { themeMode } = useThemeMode();
const isDark = themeMode === 'dark';
const theme = useTheme();
return (
<svg
viewBox='0 0 268 281'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<title>Health Stats</title>
<g filter={!isDark ? 'url(#filter0_d_22043_268578)' : undefined}>
<circle
cx='134'
cy='129'
r='97'
fill={theme.palette.charts.health.mainCircleBackground}
/>
</g>
<circle
cx='134'
cy='129'
r='121'
stroke={theme.palette.charts.health.orbit}
stroke-width='3'
/>
<text
x={134}
y={149}
textAnchor='middle'
fontSize={48}
fill={theme.palette.charts.health.title}
fontWeight={700}
>
{value !== undefined ? `${value}%` : 'N/A'}
</text>
<g filter={!isDark ? 'url(#filter1_d_22043_268578)' : undefined}>
<circle
cx='206'
cy='58'
r='50'
fill={theme.palette.charts.health.circles}
/>
</g>
<text
x={206}
y={56}
fill={theme.palette.charts.health.healthy}
fontWeight={700}
fontSize={20}
textAnchor='middle'
>
{healthy || 0}
</text>
<text
x={206}
y={72}
fill={theme.palette.charts.health.text}
fontSize={12}
textAnchor='middle'
>
Healthy
</text>
<g filter={!isDark ? 'url(#filter2_d_22043_268578)' : undefined}>
<circle
cx='53'
cy='66'
r='41'
fill={theme.palette.charts.health.circles}
/>
</g>
<text
x={53}
y={65}
fill={theme.palette.charts.health.stale}
fontWeight={700}
fontSize={20}
textAnchor='middle'
>
{stale || 0}
</text>
<text
x={53}
y={81}
fill={theme.palette.charts.health.text}
fontSize={12}
textAnchor='middle'
>
Stale
</text>
<g filter={!isDark ? 'url(#filter3_d_22043_268578)' : undefined}>
<circle
cx='144'
cy='224'
r='41'
fill={theme.palette.charts.health.circles}
/>
</g>
<text
x={144}
y={216}
fill={theme.palette.charts.health.potenciallyStale}
fontWeight={700}
fontSize={20}
textAnchor='middle'
>
{potenciallyStale || 0}
</text>
<text
x={144}
y={232}
fill={theme.palette.charts.health.text}
fontSize={12}
textAnchor='middle'
>
<tspan x={144} dy='0'>
Potentially
</tspan>
<tspan x={144} dy='1.2em'>
stale
</tspan>
</text>
<path
d='M99.5 247.275C99.5 251.693 103.082 255.275 107.5 255.275C111.918 255.275 115.5 251.693 115.5 247.275C115.5 242.857 111.918 239.275 107.5 239.275C103.082 239.275 99.5 242.857 99.5 247.275ZM10.8811 92C10.8811 96.4183 14.4629 100 18.8811 100C23.2994 100 26.8811 96.4183 26.8811 92C26.8811 87.5817 23.2994 84 18.8811 84C14.4629 84 10.8811 87.5817 10.8811 92ZM107.827 245.811C54.4151 233.886 14.5 186.258 14.5 129.325H11.5C11.5 187.696 52.4223 236.515 107.173 248.739L107.827 245.811ZM14.5 129.325C14.5 116.458 16.5379 104.07 20.3078 92.4634L17.4545 91.5366C13.5886 103.439 11.5 116.14 11.5 129.325H14.5Z'
fill='url(#paint0_linear_22043_268578)'
/>
<defs>
<filter
id='filter0_d_22043_268578'
x='15'
y='13'
width='238'
height='238'
filterUnits='userSpaceOnUse'
color-interpolation-filters='sRGB'
>
<feFlood flood-opacity='0' result='BackgroundImageFix' />
<feColorMatrix
in='SourceAlpha'
type='matrix'
values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'
result='hardAlpha'
/>
<feMorphology
radius='2'
operator='dilate'
in='SourceAlpha'
result='effect1_dropShadow_22043_268578'
/>
<feOffset dy='3' />
<feGaussianBlur stdDeviation='10' />
<feComposite in2='hardAlpha' operator='out' />
<feColorMatrix
type='matrix'
values='0 0 0 0 0.505882 0 0 0 0 0.478431 0 0 0 0 0.996078 0 0 0 0.36 0'
/>
<feBlend
mode='normal'
in2='BackgroundImageFix'
result='effect1_dropShadow_22043_268578'
/>
<feBlend
mode='normal'
in='SourceGraphic'
in2='effect1_dropShadow_22043_268578'
result='shape'
/>
</filter>
<filter
id='filter1_d_22043_268578'
x='144'
y='0'
width='124'
height='124'
filterUnits='userSpaceOnUse'
color-interpolation-filters='sRGB'
>
<feFlood flood-opacity='0' result='BackgroundImageFix' />
<feColorMatrix
in='SourceAlpha'
type='matrix'
values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'
result='hardAlpha'
/>
<feOffset dy='4' />
<feGaussianBlur stdDeviation='6' />
<feComposite in2='hardAlpha' operator='out' />
<feColorMatrix
type='matrix'
values='0 0 0 0 0.380392 0 0 0 0 0.356863 0 0 0 0 0.760784 0 0 0 0.16 0'
/>
<feBlend
mode='normal'
in2='BackgroundImageFix'
result='effect1_dropShadow_22043_268578'
/>
<feBlend
mode='normal'
in='SourceGraphic'
in2='effect1_dropShadow_22043_268578'
result='shape'
/>
</filter>
<filter
id='filter2_d_22043_268578'
x='0'
y='17'
width='106'
height='106'
filterUnits='userSpaceOnUse'
color-interpolation-filters='sRGB'
>
<feFlood flood-opacity='0' result='BackgroundImageFix' />
<feColorMatrix
in='SourceAlpha'
type='matrix'
values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'
result='hardAlpha'
/>
<feOffset dy='4' />
<feGaussianBlur stdDeviation='6' />
<feComposite in2='hardAlpha' operator='out' />
<feColorMatrix
type='matrix'
values='0 0 0 0 0.380392 0 0 0 0 0.356863 0 0 0 0 0.760784 0 0 0 0.16 0'
/>
<feBlend
mode='normal'
in2='BackgroundImageFix'
result='effect1_dropShadow_22043_268578'
/>
<feBlend
mode='normal'
in='SourceGraphic'
in2='effect1_dropShadow_22043_268578'
result='shape'
/>
</filter>
<filter
id='filter3_d_22043_268578'
x='91'
y='175'
width='106'
height='106'
filterUnits='userSpaceOnUse'
color-interpolation-filters='sRGB'
>
<feFlood flood-opacity='0' result='BackgroundImageFix' />
<feColorMatrix
in='SourceAlpha'
type='matrix'
values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0'
result='hardAlpha'
/>
<feOffset dy='4' />
<feGaussianBlur stdDeviation='6' />
<feComposite in2='hardAlpha' operator='out' />
<feColorMatrix
type='matrix'
values='0 0 0 0 0.380392 0 0 0 0 0.356863 0 0 0 0 0.760784 0 0 0 0.16 0'
/>
<feBlend
mode='normal'
in2='BackgroundImageFix'
result='effect1_dropShadow_22043_268578'
/>
<feBlend
mode='normal'
in='SourceGraphic'
in2='effect1_dropShadow_22043_268578'
result='shape'
/>
</filter>
<linearGradient
id='paint0_linear_22043_268578'
x1='64.2447'
y1='87'
x2='64.2447'
y2='249'
gradientUnits='userSpaceOnUse'
>
<stop
stop-color={theme.palette.charts.health.gradientStale}
/>
<stop
offset='1'
stop-color={
theme.palette.charts.health.gradientPotenciallyStale
}
/>
</linearGradient>
</defs>
</svg>
);
};

View File

@ -28,6 +28,7 @@ const createOptions = (
locationSettings: ILocationSettings,
setTooltip: React.Dispatch<React.SetStateAction<TooltipState | null>>,
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 | TooltipState>(null);
const options = useMemo(
() =>
createOptions(theme, locationSettings, setTooltip, Boolean(cover)),
createOptions(
theme,
locationSettings,
setTooltip,
Boolean(cover),
isLocalTooltip,
),
[theme, locationSettings],
);

View File

@ -13,5 +13,5 @@ export const ProjectHealthChart: VFC<IFlagsProjectChartProps> = ({
}) => {
const data = useProjectChartData(projectFlagTrends, 'health');
return <LineChart data={data} />;
return <LineChart data={data} isLocalTooltip />;
};

View File

@ -13,5 +13,5 @@ export const TimeToProductionChart: VFC<IFlagsProjectChartProps> = ({
}) => {
const data = useProjectChartData(projectFlagTrends, 'timeToProduction');
return <LineChart data={data} />;
return <LineChart data={data} isLocalTooltip />;
};

View File

@ -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<IUsersChartProps> = ({
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<IUsersChartProps> = ({
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],

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

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