mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
feat: improved health chart tooltip (#6359)
This commit is contained in:
parent
b82a650dab
commit
96c86b221b
@ -178,7 +178,7 @@ export const ExecutiveDashboard: VFC = () => {
|
||||
value={80}
|
||||
healthy={4}
|
||||
stale={1}
|
||||
potenciallyStale={0}
|
||||
potentiallyStale={0}
|
||||
/>
|
||||
</Widget>
|
||||
<Widget title='Health per project' order={7} span={chartSpan}>
|
||||
|
@ -6,14 +6,14 @@ interface IHealthStatsProps {
|
||||
value: number;
|
||||
healthy: number;
|
||||
stale: number;
|
||||
potenciallyStale: number;
|
||||
potentiallyStale: number;
|
||||
}
|
||||
|
||||
export const HealthStats: VFC<IHealthStatsProps> = ({
|
||||
value,
|
||||
healthy,
|
||||
stale,
|
||||
potenciallyStale,
|
||||
potentiallyStale,
|
||||
}) => {
|
||||
const { themeMode } = useThemeMode();
|
||||
const isDark = themeMode === 'dark';
|
||||
@ -116,12 +116,12 @@ export const HealthStats: VFC<IHealthStatsProps> = ({
|
||||
<text
|
||||
x={144}
|
||||
y={216}
|
||||
fill={theme.palette.charts.health.potenciallyStale}
|
||||
fill={theme.palette.charts.health.potentiallyStale}
|
||||
fontWeight={700}
|
||||
fontSize={20}
|
||||
textAnchor='middle'
|
||||
>
|
||||
{potenciallyStale || 0}
|
||||
{potentiallyStale || 0}
|
||||
</text>
|
||||
<text
|
||||
x={144}
|
||||
@ -302,7 +302,7 @@ export const HealthStats: VFC<IHealthStatsProps> = ({
|
||||
<stop
|
||||
offset='1'
|
||||
stopColor={
|
||||
theme.palette.charts.health.gradientPotenciallyStale
|
||||
theme.palette.charts.health.gradientPotentiallyStale
|
||||
}
|
||||
/>
|
||||
</linearGradient>
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { VFC } from 'react';
|
||||
import { Box, styled } from '@mui/material';
|
||||
|
||||
type DistributionLineTypes = 'default' | 'success' | 'warning' | 'error';
|
||||
|
||||
const StyledDistributionLine = styled(Box)<{
|
||||
type: DistributionLineTypes;
|
||||
size?: 'large' | 'small';
|
||||
}>(({ theme, type, size = 'large' }) => {
|
||||
const color: Record<DistributionLineTypes, string | undefined> = {
|
||||
default: theme.palette.secondary.border,
|
||||
success: theme.palette.success.border,
|
||||
warning: theme.palette.warning.border,
|
||||
error: theme.palette.error.border,
|
||||
};
|
||||
|
||||
return {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
height: size === 'large' ? theme.spacing(2) : theme.spacing(1),
|
||||
backgroundColor: color[type],
|
||||
marginBottom: theme.spacing(0.5),
|
||||
};
|
||||
});
|
||||
|
||||
export const HorizontalDistributionChart: VFC<{
|
||||
sections: Array<{ type: DistributionLineTypes; value: number }>;
|
||||
size?: 'large' | 'small';
|
||||
}> = ({ sections, size }) => (
|
||||
<Box sx={(theme) => ({ display: 'flex', gap: theme.spacing(0.5) })}>
|
||||
{sections.map((section, index) =>
|
||||
section.value ? (
|
||||
<StyledDistributionLine
|
||||
type={section.type}
|
||||
sx={{ width: `${section.value}%` }}
|
||||
key={`${section.type}-${index}`}
|
||||
size={size}
|
||||
/>
|
||||
) : null,
|
||||
)}
|
||||
</Box>
|
||||
);
|
@ -7,7 +7,7 @@ export type TooltipState = {
|
||||
caretX: number;
|
||||
caretY: number;
|
||||
title: string;
|
||||
align: 'left' | 'right';
|
||||
align: 'left' | 'right' | 'center';
|
||||
body: {
|
||||
title: string;
|
||||
color: string;
|
||||
@ -40,22 +40,47 @@ const StyledLabelIcon = styled('span')(({ theme }) => ({
|
||||
marginRight: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const offset = 16;
|
||||
|
||||
const getAlign = (align?: 'left' | 'right' | 'center') => {
|
||||
if (align === 'left') {
|
||||
return 'flex-start';
|
||||
}
|
||||
|
||||
if (align === 'right') {
|
||||
return 'flex-end';
|
||||
}
|
||||
|
||||
return 'center';
|
||||
};
|
||||
|
||||
const getLeftOffset = (caretX = 0, align?: 'left' | 'right' | 'center') => {
|
||||
if (align === 'left') {
|
||||
return caretX + offset;
|
||||
}
|
||||
|
||||
if (align === 'right') {
|
||||
return caretX - offset;
|
||||
}
|
||||
|
||||
return caretX;
|
||||
};
|
||||
|
||||
export const ChartTooltipContainer: FC<IChartTooltipProps> = ({
|
||||
tooltip,
|
||||
children,
|
||||
}) => (
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
top: tooltip?.caretY,
|
||||
left: tooltip?.align === 'left' ? tooltip?.caretX + 20 : 0,
|
||||
right:
|
||||
tooltip?.align === 'right' ? tooltip?.caretX + 20 : undefined,
|
||||
top: (tooltip?.caretY || 0) + offset,
|
||||
left: getLeftOffset(tooltip?.caretX, tooltip?.align),
|
||||
width: '1px',
|
||||
position: 'absolute',
|
||||
display: tooltip ? 'flex' : 'none',
|
||||
pointerEvents: 'none',
|
||||
zIndex: theme.zIndex.tooltip,
|
||||
flexDirection: 'column',
|
||||
alignItems: tooltip?.align === 'left' ? 'flex-start' : 'flex-end',
|
||||
alignItems: getAlign(tooltip?.align),
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
@ -10,16 +10,12 @@ import {
|
||||
Chart,
|
||||
Filler,
|
||||
type ChartData,
|
||||
TooltipModel,
|
||||
ChartOptions,
|
||||
type ChartOptions,
|
||||
} from 'chart.js';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { Theme, useTheme } from '@mui/material';
|
||||
import {
|
||||
useLocationSettings,
|
||||
type ILocationSettings,
|
||||
} from 'hooks/useLocationSettings';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||
import {
|
||||
ChartTooltip,
|
||||
ChartTooltipContainer,
|
||||
@ -27,144 +23,7 @@ import {
|
||||
} from './ChartTooltip/ChartTooltip';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { styled } from '@mui/material';
|
||||
|
||||
const createTooltip =
|
||||
(setTooltip: React.Dispatch<React.SetStateAction<TooltipState | null>>) =>
|
||||
(context: {
|
||||
chart: Chart;
|
||||
tooltip: TooltipModel<any>;
|
||||
}) => {
|
||||
const tooltip = context.tooltip;
|
||||
if (tooltip.opacity === 0) {
|
||||
setTooltip(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setTooltip({
|
||||
caretX:
|
||||
tooltip?.xAlign === 'right'
|
||||
? context.chart.width - tooltip?.caretX
|
||||
: tooltip?.caretX,
|
||||
caretY: tooltip?.caretY,
|
||||
title: tooltip?.title?.join(' ') || '',
|
||||
align: tooltip?.xAlign === 'right' ? 'right' : 'left',
|
||||
body:
|
||||
tooltip?.body?.map((item: any, index: number) => ({
|
||||
title: item?.lines?.join(' '),
|
||||
color: tooltip?.labelColors?.[index]?.borderColor as string,
|
||||
value: '',
|
||||
})) || [],
|
||||
dataPoints: tooltip?.dataPoints || [],
|
||||
});
|
||||
};
|
||||
|
||||
const createOptions = (
|
||||
theme: Theme,
|
||||
locationSettings: ILocationSettings,
|
||||
setTooltip: React.Dispatch<React.SetStateAction<TooltipState | null>>,
|
||||
isPlaceholder?: boolean,
|
||||
localTooltip?: boolean,
|
||||
) =>
|
||||
({
|
||||
responsive: true,
|
||||
...(isPlaceholder
|
||||
? {
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
plugins: {
|
||||
legend: {
|
||||
display: !isPlaceholder,
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 12,
|
||||
padding: 30,
|
||||
generateLabels: (chart: Chart) => {
|
||||
const datasets = chart.data.datasets;
|
||||
const {
|
||||
labels: {
|
||||
usePointStyle,
|
||||
pointStyle,
|
||||
textAlign,
|
||||
color,
|
||||
},
|
||||
} = chart?.legend?.options || {
|
||||
labels: {},
|
||||
};
|
||||
return (chart as any)
|
||||
._getSortedDatasetMetas()
|
||||
.map((meta: any) => {
|
||||
const style = meta.controller.getStyle(
|
||||
usePointStyle ? 0 : undefined,
|
||||
);
|
||||
return {
|
||||
text: datasets[meta.index].label,
|
||||
fillStyle: style.backgroundColor,
|
||||
fontColor: color,
|
||||
hidden: !meta.visible,
|
||||
lineWidth: 0,
|
||||
borderRadius: 6,
|
||||
strokeStyle: style.borderColor,
|
||||
pointStyle: pointStyle || style.pointStyle,
|
||||
textAlign: textAlign || style.textAlign,
|
||||
datasetIndex: meta.index,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
external: createTooltip(setTooltip),
|
||||
},
|
||||
},
|
||||
locale: locationSettings.locale,
|
||||
interaction: {
|
||||
intersect: localTooltip || false,
|
||||
axis: 'x',
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
hitRadius: 15,
|
||||
},
|
||||
},
|
||||
// cubicInterpolationMode: 'monotone',
|
||||
tension: 0.1,
|
||||
color: theme.palette.text.secondary,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
type: 'linear',
|
||||
grid: {
|
||||
color: theme.palette.divider,
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
ticks: {
|
||||
color: theme.palette.text.secondary,
|
||||
display: !isPlaceholder,
|
||||
precision: 0,
|
||||
},
|
||||
},
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: 'day',
|
||||
tooltipFormat: 'PPP',
|
||||
},
|
||||
grid: {
|
||||
color: 'transparent',
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
ticks: {
|
||||
color: theme.palette.text.secondary,
|
||||
display: !isPlaceholder,
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as const;
|
||||
import { createOptions } from './createChartOptions';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
position: 'relative',
|
||||
|
@ -0,0 +1,77 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import { ILocationSettings } from 'hooks/useLocationSettings';
|
||||
import { TooltipState } from './ChartTooltip/ChartTooltip';
|
||||
import { createTooltip } from './createTooltip';
|
||||
import { legendOptions } from './legendOptions';
|
||||
|
||||
export const createOptions = (
|
||||
theme: Theme,
|
||||
locationSettings: ILocationSettings,
|
||||
setTooltip: React.Dispatch<React.SetStateAction<TooltipState | null>>,
|
||||
isPlaceholder?: boolean,
|
||||
localTooltip?: boolean,
|
||||
) =>
|
||||
({
|
||||
responsive: true,
|
||||
...(isPlaceholder
|
||||
? {
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
plugins: {
|
||||
legend: {
|
||||
...legendOptions,
|
||||
display: !isPlaceholder,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
external: createTooltip(setTooltip),
|
||||
},
|
||||
},
|
||||
locale: locationSettings.locale,
|
||||
interaction: {
|
||||
intersect: localTooltip || false,
|
||||
axis: 'x',
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
hitRadius: 15,
|
||||
},
|
||||
},
|
||||
// cubicInterpolationMode: 'monotone',
|
||||
tension: 0.1,
|
||||
color: theme.palette.text.secondary,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
type: 'linear',
|
||||
grid: {
|
||||
color: theme.palette.divider,
|
||||
borderColor: theme.palette.divider,
|
||||
},
|
||||
ticks: {
|
||||
color: theme.palette.text.secondary,
|
||||
display: !isPlaceholder,
|
||||
precision: 0,
|
||||
},
|
||||
},
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: 'day',
|
||||
tooltipFormat: 'PPP',
|
||||
},
|
||||
grid: {
|
||||
color: 'transparent',
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
ticks: {
|
||||
color: theme.palette.text.secondary,
|
||||
display: !isPlaceholder,
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as const;
|
@ -0,0 +1,29 @@
|
||||
import { type Chart, type TooltipModel } from 'chart.js';
|
||||
import { type TooltipState } from './ChartTooltip/ChartTooltip';
|
||||
|
||||
export const createTooltip =
|
||||
(setTooltip: React.Dispatch<React.SetStateAction<TooltipState | null>>) =>
|
||||
(context: {
|
||||
chart: Chart;
|
||||
tooltip: TooltipModel<any>;
|
||||
}) => {
|
||||
const tooltip = context.tooltip;
|
||||
if (tooltip.opacity === 0) {
|
||||
setTooltip(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setTooltip({
|
||||
caretX: tooltip?.caretX,
|
||||
caretY: tooltip?.caretY,
|
||||
title: tooltip?.title?.join(' ') || '',
|
||||
align: tooltip?.xAlign,
|
||||
body:
|
||||
tooltip?.body?.map((item: any, index: number) => ({
|
||||
title: item?.lines?.join(' '),
|
||||
color: tooltip?.labelColors?.[index]?.borderColor as string,
|
||||
value: '',
|
||||
})) || [],
|
||||
dataPoints: tooltip?.dataPoints || [],
|
||||
});
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
import { Chart } from 'chart.js';
|
||||
|
||||
export const legendOptions = {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 12,
|
||||
padding: 30,
|
||||
generateLabels: (chart: Chart) => {
|
||||
const datasets = chart.data.datasets;
|
||||
const {
|
||||
labels: { usePointStyle, pointStyle, textAlign, color },
|
||||
} = chart?.legend?.options || {
|
||||
labels: {},
|
||||
};
|
||||
return (chart as any)._getSortedDatasetMetas().map((meta: any) => {
|
||||
const style = meta.controller.getStyle(
|
||||
usePointStyle ? 0 : undefined,
|
||||
);
|
||||
return {
|
||||
text: datasets[meta.index].label,
|
||||
fillStyle: style.backgroundColor,
|
||||
fontColor: color,
|
||||
hidden: !meta.visible,
|
||||
lineWidth: 0,
|
||||
borderRadius: 6,
|
||||
strokeStyle: style.borderColor,
|
||||
pointStyle: pointStyle || style.pointStyle,
|
||||
textAlign: textAlign || style.textAlign,
|
||||
datasetIndex: meta.index,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
} as const;
|
@ -0,0 +1,164 @@
|
||||
import { type VFC } from 'react';
|
||||
import { type ExecutiveSummarySchemaProjectFlagTrendsItem } from 'openapi';
|
||||
import { Box, Divider, Paper, Typography, styled } from '@mui/material';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
import { TooltipState } from '../../LineChart/ChartTooltip/ChartTooltip';
|
||||
import { HorizontalDistributionChart } from '../../HorizontalDistributionChart/HorizontalDistributionChart';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledItemHeader = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: theme.spacing(2),
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const getHealthBadgeColor = (health?: number | null) => {
|
||||
if (health === undefined || health === null) {
|
||||
return 'info';
|
||||
}
|
||||
|
||||
if (health >= 75) {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
if (health >= 50) {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
return 'error';
|
||||
};
|
||||
|
||||
const Distribution = ({ stale = 0, potentiallyStale = 0, total = 0 }) => (
|
||||
<>
|
||||
<HorizontalDistributionChart
|
||||
sections={[{ type: 'default', value: 100 }]}
|
||||
size='small'
|
||||
/>
|
||||
<HorizontalDistributionChart
|
||||
sections={[
|
||||
{
|
||||
type: 'error',
|
||||
value: (stale / total) * 100,
|
||||
},
|
||||
{
|
||||
type: 'warning',
|
||||
value: (potentiallyStale / total) * 100,
|
||||
},
|
||||
]}
|
||||
size='small'
|
||||
/>
|
||||
<Typography
|
||||
variant='body2'
|
||||
component='p'
|
||||
sx={(theme) => ({ marginTop: theme.spacing(0.5) })}
|
||||
>
|
||||
<Typography
|
||||
component='span'
|
||||
sx={(theme) => ({
|
||||
color: theme.palette.error.border,
|
||||
})}
|
||||
>
|
||||
{'● '}
|
||||
</Typography>
|
||||
Stale flags: {stale}
|
||||
</Typography>
|
||||
<Typography variant='body2' component='p'>
|
||||
<Typography
|
||||
component='span'
|
||||
sx={(theme) => ({
|
||||
color: theme.palette.warning.border,
|
||||
})}
|
||||
>
|
||||
{'● '}
|
||||
</Typography>
|
||||
Potentially stale flags: {potentiallyStale}
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
|
||||
export const HealthTooltip: VFC<{ tooltip: TooltipState | null }> = ({
|
||||
tooltip,
|
||||
}) => {
|
||||
const data = tooltip?.dataPoints.map((point) => {
|
||||
return {
|
||||
label: point.label,
|
||||
title: point.dataset.label,
|
||||
color: point.dataset.borderColor,
|
||||
value: point.raw as ExecutiveSummarySchemaProjectFlagTrendsItem,
|
||||
};
|
||||
});
|
||||
|
||||
const limitedData = data?.slice(0, 5);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
width: '300px',
|
||||
})}
|
||||
>
|
||||
{limitedData?.map((point, index) => (
|
||||
<StyledTooltipItemContainer
|
||||
elevation={3}
|
||||
key={`${point.title}-${index}`}
|
||||
>
|
||||
<StyledItemHeader>
|
||||
<Typography
|
||||
variant='body2'
|
||||
color='textSecondary'
|
||||
component='span'
|
||||
>
|
||||
{point.label}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant='body2'
|
||||
color='textSecondary'
|
||||
component='span'
|
||||
>
|
||||
Project health
|
||||
</Typography>
|
||||
</StyledItemHeader>
|
||||
<StyledItemHeader>
|
||||
<Typography variant='body2' component='span'>
|
||||
<Typography
|
||||
sx={{ color: point.color }}
|
||||
component='span'
|
||||
>
|
||||
{'● '}
|
||||
</Typography>
|
||||
<strong>{point.title}</strong>
|
||||
</Typography>
|
||||
<Badge color={getHealthBadgeColor(point.value.health)}>
|
||||
{point.value.health}%
|
||||
</Badge>
|
||||
</StyledItemHeader>{' '}
|
||||
<Divider
|
||||
sx={(theme) => ({ margin: theme.spacing(1.5, 0) })}
|
||||
/>
|
||||
<Typography
|
||||
variant='body2'
|
||||
component='p'
|
||||
sx={(theme) => ({
|
||||
marginBottom: theme.spacing(0.5),
|
||||
})}
|
||||
>
|
||||
Total flags: {point.value.total}
|
||||
</Typography>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(
|
||||
point.value.stale || point.value.potentiallyStale,
|
||||
)}
|
||||
show={<Distribution {...point.value} />}
|
||||
/>
|
||||
</StyledTooltipItemContainer>
|
||||
)) || null}
|
||||
</Box>
|
||||
);
|
||||
};
|
@ -1,76 +1,14 @@
|
||||
import { type VFC } from 'react';
|
||||
import 'chartjs-adapter-date-fns';
|
||||
import { ExecutiveSummarySchema } from 'openapi';
|
||||
import { type VFC } from 'react';
|
||||
import { type ExecutiveSummarySchema } from 'openapi';
|
||||
import { HealthTooltip } from './HealthChartTooltip/HealthChartTooltip';
|
||||
import { LineChart } from '../LineChart/LineChart';
|
||||
import { useProjectChartData } from '../useProjectChartData';
|
||||
import { TooltipState } from '../LineChart/ChartTooltip/ChartTooltip';
|
||||
import { Box, Paper, styled } from '@mui/material';
|
||||
import { Badge } from 'component/common/Badge/Badge';
|
||||
|
||||
interface IFlagsProjectChartProps {
|
||||
projectFlagTrends: ExecutiveSummarySchema['projectFlagTrends'];
|
||||
}
|
||||
|
||||
const StyledTooltipItemContainer = styled(Paper)(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledItemHeader = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: theme.spacing(2),
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const getHealthBadgeColor = (health?: number | null) => {
|
||||
if (health === undefined || health === null) {
|
||||
return 'info';
|
||||
}
|
||||
|
||||
if (health >= 75) {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
if (health >= 50) {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
return 'error';
|
||||
};
|
||||
|
||||
const TooltipComponent: VFC<{ tooltip: TooltipState | null }> = ({
|
||||
tooltip,
|
||||
}) => {
|
||||
const data = tooltip?.dataPoints.map((point) => {
|
||||
return {
|
||||
title: point.dataset.label,
|
||||
color: point.dataset.borderColor,
|
||||
value: point.raw as number,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(3),
|
||||
})}
|
||||
>
|
||||
{data?.map((point, index) => (
|
||||
<StyledTooltipItemContainer elevation={3} key={point.title}>
|
||||
<StyledItemHeader>
|
||||
<div>{point.title}</div>{' '}
|
||||
<Badge color={getHealthBadgeColor(point.value)}>
|
||||
{point.value}%
|
||||
</Badge>
|
||||
</StyledItemHeader>
|
||||
</StyledTooltipItemContainer>
|
||||
)) || null}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProjectHealthChart: VFC<IFlagsProjectChartProps> = ({
|
||||
projectFlagTrends,
|
||||
}) => {
|
||||
@ -80,12 +18,9 @@ export const ProjectHealthChart: VFC<IFlagsProjectChartProps> = ({
|
||||
<LineChart
|
||||
data={data}
|
||||
isLocalTooltip
|
||||
TooltipComponent={TooltipComponent}
|
||||
TooltipComponent={HealthTooltip}
|
||||
overrideOptions={{
|
||||
parsing: {
|
||||
yAxisKey: 'health',
|
||||
xAxisKey: 'date',
|
||||
},
|
||||
parsing: { yAxisKey: 'health', xAxisKey: 'date' },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, styled } from '@mui/material';
|
||||
|
||||
type UserType = 'active' | 'inactive';
|
||||
|
||||
interface StyledLinearProgressProps {
|
||||
type: UserType;
|
||||
}
|
||||
const StyledUserDistContainer = styled(Box)(({ theme }) => ({
|
||||
padding: `${theme.spacing(1.5)} ${theme.spacing(2)}`,
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}));
|
||||
|
||||
const StyledUserDistIndicator = styled(Box)<StyledLinearProgressProps>(
|
||||
({ theme, type }) => ({
|
||||
width: 8,
|
||||
height: 8,
|
||||
backgroundColor:
|
||||
type === 'active'
|
||||
? theme.palette.success.border
|
||||
: theme.palette.warning.border,
|
||||
borderRadius: `2px`,
|
||||
marginRight: theme.spacing(1),
|
||||
}),
|
||||
);
|
||||
|
||||
interface IUserDistributionInfoProps {
|
||||
type: UserType;
|
||||
count: string;
|
||||
percentage: string;
|
||||
}
|
||||
|
||||
const StyledDistInfoInnerContainer = styled(Box)(() => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
const StyledDistInfoTextContainer = styled(Box)(() => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}));
|
||||
|
||||
const StyledCountTypography = styled(Typography)(() => ({
|
||||
marginLeft: 'auto',
|
||||
fontWeight: 'normal',
|
||||
}));
|
||||
|
||||
export const UserDistributionInfo: React.FC<IUserDistributionInfoProps> = ({
|
||||
type,
|
||||
count,
|
||||
percentage,
|
||||
}) => {
|
||||
return (
|
||||
<StyledUserDistContainer>
|
||||
<StyledDistInfoInnerContainer>
|
||||
<StyledDistInfoTextContainer>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<StyledUserDistIndicator type={type} />
|
||||
<Typography variant='body1' component='span'>
|
||||
{type === 'active' ? 'Active' : 'Inactive'} users
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant='body2'>{percentage}%</Typography>
|
||||
</StyledDistInfoTextContainer>
|
||||
<StyledCountTypography variant='h2'>
|
||||
{count}
|
||||
</StyledCountTypography>
|
||||
</StyledDistInfoInnerContainer>
|
||||
</StyledUserDistContainer>
|
||||
);
|
||||
};
|
@ -1,9 +1,11 @@
|
||||
import React, { type FC } from 'react';
|
||||
import { type FC } from 'react';
|
||||
import { ChevronRight } from '@mui/icons-material';
|
||||
import { Box, Typography, styled } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { HorizontalDistributionChart } from '../HorizontalDistributionChart/HorizontalDistributionChart';
|
||||
import { UserDistributionInfo } from './UserDistributionInfo';
|
||||
|
||||
const StyledUserContainer = styled(Box)(({ theme }) => ({
|
||||
position: 'relative',
|
||||
@ -43,12 +45,6 @@ const StyledUserCount = styled(Typography)(({ theme }) => ({
|
||||
padding: 0,
|
||||
}));
|
||||
|
||||
const StyledHeader = styled(Typography)(({ theme }) => ({
|
||||
marginBottom: theme.spacing(3),
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
fontWeight: 'bold',
|
||||
}));
|
||||
|
||||
const StyledDistInfoContainer = styled(Box)({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@ -95,8 +91,17 @@ export const UserStats: FC<IUserStatsProps> = ({ count, active, inactive }) => {
|
||||
show={
|
||||
<>
|
||||
<StyledUserDistributionContainer>
|
||||
<UserDistribution
|
||||
activeUsersPercentage={activeUsersPercentage}
|
||||
<HorizontalDistributionChart
|
||||
sections={[
|
||||
{
|
||||
type: 'success',
|
||||
value: activeUsersPercentage,
|
||||
},
|
||||
{
|
||||
type: 'warning',
|
||||
value: 100 - activeUsersPercentage,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</StyledUserDistributionContainer>
|
||||
|
||||
@ -124,115 +129,3 @@ export const UserStats: FC<IUserStatsProps> = ({ count, active, inactive }) => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type UserType = 'active' | 'inactive';
|
||||
|
||||
interface StyledLinearProgressProps {
|
||||
type: UserType;
|
||||
}
|
||||
|
||||
const StyledUserDistributionLine = styled(Box)<StyledLinearProgressProps>(
|
||||
({ theme, type }) => ({
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
height: 16,
|
||||
backgroundColor:
|
||||
type === 'active'
|
||||
? theme.palette.success.border
|
||||
: theme.palette.warning.border,
|
||||
}),
|
||||
);
|
||||
|
||||
const UserDistribution = ({ activeUsersPercentage = 100 }) => {
|
||||
const getLineWidth = () => {
|
||||
return [activeUsersPercentage, 100 - activeUsersPercentage];
|
||||
};
|
||||
|
||||
const [activeWidth, inactiveWidth] = getLineWidth();
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<StyledUserDistributionLine
|
||||
type='active'
|
||||
sx={{ width: `${activeWidth}%` }}
|
||||
/>
|
||||
<StyledUserDistributionLine
|
||||
type='inactive'
|
||||
sx={(theme) => ({
|
||||
width: `${inactiveWidth}%`,
|
||||
marginLeft: theme.spacing(0.5),
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledUserDistContainer = styled(Box)(({ theme }) => ({
|
||||
padding: `${theme.spacing(1.5)} ${theme.spacing(2)}`,
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
}));
|
||||
|
||||
const StyledUserDistIndicator = styled(Box)<StyledLinearProgressProps>(
|
||||
({ theme, type }) => ({
|
||||
width: 8,
|
||||
height: 8,
|
||||
backgroundColor:
|
||||
type === 'active'
|
||||
? theme.palette.success.border
|
||||
: theme.palette.warning.border,
|
||||
borderRadius: `2px`,
|
||||
marginRight: theme.spacing(1),
|
||||
}),
|
||||
);
|
||||
|
||||
interface IUserDistributionInfoProps {
|
||||
type: UserType;
|
||||
count: string;
|
||||
percentage: string;
|
||||
}
|
||||
|
||||
const StyledDistInfoInnerContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
const StyledDistInfoTextContainer = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}));
|
||||
|
||||
const StyledCountTypography = styled(Typography)(({ theme }) => ({
|
||||
marginLeft: 'auto',
|
||||
fontWeight: 'normal',
|
||||
}));
|
||||
|
||||
const UserDistributionInfo: React.FC<IUserDistributionInfoProps> = ({
|
||||
type,
|
||||
count,
|
||||
percentage,
|
||||
}) => {
|
||||
return (
|
||||
<StyledUserDistContainer>
|
||||
<StyledDistInfoInnerContainer>
|
||||
<StyledDistInfoTextContainer>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<StyledUserDistIndicator type={type} />
|
||||
<Typography variant='body1' component='span'>
|
||||
{type === 'active' ? 'Active' : 'Inactive'} users
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant='body2'>{percentage}%</Typography>
|
||||
</StyledDistInfoTextContainer>
|
||||
<StyledCountTypography variant='h2'>
|
||||
{count}
|
||||
</StyledCountTypography>
|
||||
</StyledDistInfoInnerContainer>
|
||||
</StyledUserDistContainer>
|
||||
);
|
||||
};
|
||||
|
@ -298,9 +298,9 @@ const theme = {
|
||||
title: colors.grey[50],
|
||||
healthy: colors.purple[800],
|
||||
stale: colors.red[800],
|
||||
potenciallyStale: colors.orange[800],
|
||||
potentiallyStale: colors.orange[800],
|
||||
gradientStale: '#8A3E45',
|
||||
gradientPotenciallyStale: '#875D21',
|
||||
gradientPotentiallyStale: '#875D21',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -283,9 +283,9 @@ export const theme = {
|
||||
title: colors.grey[50],
|
||||
healthy: colors.purple[800],
|
||||
stale: colors.red[800],
|
||||
potenciallyStale: colors.orange[800],
|
||||
potentiallyStale: colors.orange[800],
|
||||
gradientStale: colors.red[300],
|
||||
gradientPotenciallyStale: colors.orange[500],
|
||||
gradientPotentiallyStale: colors.orange[500],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -141,9 +141,9 @@ declare module '@mui/material/styles' {
|
||||
title: string;
|
||||
healthy: string;
|
||||
stale: string;
|
||||
potenciallyStale: string;
|
||||
potentiallyStale: string;
|
||||
gradientStale: string;
|
||||
gradientPotenciallyStale: string;
|
||||
gradientPotentiallyStale: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user