[WIFI-12418] Memory chart display hidden automatically

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2023-03-20 17:15:16 +01:00
parent 09184b0402
commit f1f62efe6f
6 changed files with 197 additions and 47 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-client",
"version": "2.9.0(22)",
"version": "2.9.0(23)",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ucentral-client",
"version": "2.9.0(22)",
"version": "2.9.0(23)",
"license": "ISC",
"dependencies": {
"@chakra-ui/icons": "^2.0.11",

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "2.9.0(22)",
"version": "2.9.0(23)",
"description": "",
"private": true,
"main": "index.tsx",

View File

@@ -1,4 +1,4 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { axiosGw } from 'constants/axiosInstances';
import { AxiosError } from 'models/Axios';
@@ -89,3 +89,18 @@ export const useDeleteHealthChecks = () => {
},
});
};
const getDevicesWithHealthBetween = (
context: QueryFunctionContext<[string, string, { lowerLimit: number; upperLimit: number }]>,
) =>
axiosGw
.get(`devices?health=true&lowLimit=${context.queryKey[2].lowerLimit}&highLimit=${context.queryKey[2].upperLimit}`)
.then((res) => res.data.serialNumbers as string[]);
export const useGetDevicesWithHealthBetween = ({
lowerLimit,
upperLimit,
}: {
lowerLimit: number;
upperLimit: number;
}) => useQuery(['devices', 'health', { lowerLimit, upperLimit }], getDevicesWithHealthBetween);

View File

@@ -34,20 +34,29 @@ const DeviceMemoryChart = ({ data }: Props) => {
{
label: 'Free',
data: data.free.map((free) => Math.floor(free / 1024 / 1024)),
borderColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
backgroundColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
},
{
label: 'Buffered',
data: data.buffered.map((buffered) => Math.floor(buffered / 1024 / 1024)),
borderColor: colorMode === 'light' ? '#ECC94B' : '#FAF089', // yellow-400 - yellow-200
backgroundColor: colorMode === 'light' ? '#ECC94B' : '#FAF089', // yellow-400 - yellow-200
borderColor: colorMode === 'light' ? 'rgb(99, 179, 237, 1)' : 'rgb(190, 227, 248, 1)', // blue-300 - blue-100
backgroundColor: colorMode === 'light' ? 'rgb(99, 179, 237, 0.3)' : 'rgb(190, 227, 248, 0.3)', // blue-300 - blue-100
tension: 0.5,
pointRadius: 0,
fill: '+1',
},
{
label: 'Cached',
data: data.cached.map((cached) => Math.floor(cached / 1024 / 1024)),
borderColor: colorMode === 'light' ? '#ED64A6' : '#FBB6CE', // pink-400 - pink-200
backgroundColor: colorMode === 'light' ? '#ED64A6' : '#FBB6CE', // pink-400 - pink-200
borderColor: colorMode === 'light' ? 'rgb(237, 100, 166, 1)' : 'rgb(251, 182, 206, 1)', // pink-400 - pink-200
backgroundColor: colorMode === 'light' ? 'rgb(237, 100, 166, 0.3)' : 'rgb(251, 182, 206, 0.3)', // pink-400 - pink-200
tension: 0.5,
pointRadius: 0,
fill: '+1',
},
{
label: 'Buffered',
data: data.buffered.map((buffered) => Math.floor(buffered / 1024 / 1024)),
borderColor: colorMode === 'light' ? 'rgb(255, 240, 31, 1)' : 'rgb(250, 240, 137, 1)', // yellow-400 - yellow-200
backgroundColor: colorMode === 'light' ? 'rgb(255, 240, 31, 0.3)' : 'rgb(250, 240, 137, 0.3)', // yellow-400 - yellow-200
tension: 0.5,
pointRadius: 0,
fill: 'origin',
},
],
};

View File

@@ -16,6 +16,7 @@ type Props = {
export const useStatisticsCard = ({ serialNumber }: Props) => {
const [selected, setSelected] = React.useState('memory');
const [progress, setProgress] = React.useState(0);
const [hasSelectedNew, setHasSelectedNew] = React.useState(false);
const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>();
const onProgressChange = React.useCallback((newProgress: number) => {
setProgress(newProgress);
@@ -29,6 +30,7 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
});
const onSelectInterface = (event: React.ChangeEvent<HTMLSelectElement>) => {
setHasSelectedNew(true);
setSelected(event.target.value);
};
@@ -59,7 +61,7 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
if (index === 0) {
let updated = false;
for (const inter of stat.data.interfaces ?? []) {
if (!updated && selected === 'memory') {
if (!hasSelectedNew && !updated && selected === 'memory') {
updated = true;
setSelected(inter.name);
}

View File

@@ -1,5 +1,22 @@
import * as React from 'react';
import { useColorMode } from '@chakra-ui/react';
import { CopyIcon } from '@chakra-ui/icons';
import {
useColorMode,
Alert,
AlertDescription,
AlertTitle,
Box,
Center,
Heading,
IconButton,
Link,
ListItem,
Spinner,
UnorderedList,
useClipboard,
Tooltip as ChakraTooltip,
useDisclosure,
} from '@chakra-ui/react';
import {
Chart as ChartJS,
CategoryScale,
@@ -11,20 +28,57 @@ import {
Legend,
ChartData,
ArcElement,
ChartTypeRegistry,
ScatterDataPoint,
BubbleDataPoint,
} from 'chart.js';
import { Pie } from 'react-chartjs-2';
import { Pie, getElementAtEvent } from 'react-chartjs-2';
import { useTranslation } from 'react-i18next';
import GraphStatDisplay from 'components/Containers/GraphStatDisplay';
import { Modal } from 'components/Modals/Modal';
import { ControllerDashboardHealth } from 'hooks/Network/Controller';
import { useGetDevicesWithHealthBetween } from 'hooks/Network/HealthChecks';
import { AxiosError } from 'models/Axios';
const LABEL_TO_LIMITS = {
'100%': { lowerLimit: 100, upperLimit: 100, label: 'With 100% Health' },
'>90%': { lowerLimit: 90, upperLimit: 99, label: 'Between 90% and 99%' },
'>60%': { lowerLimit: 60, upperLimit: 89, label: 'Between 60% and 89%' },
'<=60%': { lowerLimit: 0, upperLimit: 59, label: 'Between 0% and 59%' },
} as const;
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, ArcElement);
type Props = {
data: ControllerDashboardHealth[];
};
const OverallHealthPieChart = ({ data }: Props) => {
const { t } = useTranslation();
const { colorMode } = useColorMode();
const { hasCopied, onCopy, setValue } = useClipboard('');
const modalProps = useDisclosure();
const [deviceCategory, setDeviceCategory] = React.useState<{ lowerLimit: number; upperLimit: number; label: string }>(
LABEL_TO_LIMITS['100%'],
);
const serialNumbersFromCategory = useGetDevicesWithHealthBetween(deviceCategory);
const chartRef =
React.useRef<ChartJS<keyof ChartTypeRegistry, (number | ScatterDataPoint | BubbleDataPoint | null)[], unknown>>(
null,
);
const onClick = (event: React.MouseEvent<HTMLCanvasElement>) => {
if (chartRef.current) {
const element = getElementAtEvent(chartRef.current, event)?.[0];
if (element && element.index !== undefined) {
const label = chartRef.current?.data?.labels?.[element.index] as keyof typeof LABEL_TO_LIMITS | undefined;
if (label && LABEL_TO_LIMITS[label]) {
setDeviceCategory(LABEL_TO_LIMITS[label]);
modalProps.onOpen();
}
}
}
};
const parsedData: ChartData<'pie', number[], unknown> = React.useMemo(() => {
const totalDevices = data.reduce(
@@ -85,7 +139,7 @@ const OverallHealthPieChart = ({ data }: Props) => {
}
if (totalDevices['<60%'] > 0) {
newData.push(totalDevices['<60%']);
labels.push('<60%');
labels.push('<=60%');
const color = colorMode === 'light' ? '#FC8181' : '#FC8181';
backgroundColor.push(color);
borderColor.push(color);
@@ -105,38 +159,108 @@ const OverallHealthPieChart = ({ data }: Props) => {
};
}, [data, colorMode]);
React.useEffect(() => {
if (serialNumbersFromCategory.data) setValue(serialNumbersFromCategory.data.join(','));
}, [serialNumbersFromCategory.data]);
return (
<GraphStatDisplay
title={t('controller.dashboard.overall_health')}
explanation={t('controller.dashboard.overall_health_explanation_pie')}
chart={
<Pie
data={parsedData}
options={{
plugins: {
legend: {
position: 'top' as const,
labels: {
color: colorMode === 'dark' ? 'white' : undefined,
<>
<GraphStatDisplay
title={t('controller.dashboard.overall_health')}
explanation={t('controller.dashboard.overall_health_explanation_pie')}
chart={
<Pie
// @ts-ignore
ref={chartRef}
data={parsedData}
onClick={onClick}
options={{
onHover: (e, elements) => {
const element = e.native?.target as unknown as { style: { cursor: string } };
if (element && elements.length > 0) {
element.style.cursor = 'pointer';
} else if (element) {
element.style.cursor = 'default';
}
},
plugins: {
legend: {
position: 'top' as const,
labels: {
color: colorMode === 'dark' ? 'white' : undefined,
},
},
title: {
display: false,
},
tooltip: {
callbacks: {
label: (context) =>
`${context.label}: ${context.formattedValue} (${Math.round(
// @ts-ignore
(context.raw / context.dataset.data.reduce((acc, curr) => acc + curr, 0)) * 100,
)}%)`,
},
},
},
title: {
display: false,
},
tooltip: {
callbacks: {
label: (context) =>
`${context.label}: ${context.formattedValue} (${Math.round(
// @ts-ignore
(context.raw / context.dataset.data.reduce((acc, curr) => acc + curr, 0)) * 100,
)}%)`,
},
},
},
}}
/>
}
/>
}}
/>
}
/>
<Modal
title={t('controller.dashboard.overall_health')}
{...modalProps}
options={{
modalSize: 'sm',
}}
topRightButtons={
<ChakraTooltip label={hasCopied ? `${t('common.copied')}!` : t('common.copy')} hasArrow closeOnClick={false}>
<IconButton
aria-label={t('common.copy')}
icon={<CopyIcon h={5} w={5} />}
onClick={onCopy}
colorScheme="teal"
hidden={!serialNumbersFromCategory.data || serialNumbersFromCategory.data.length === 0}
/>
</ChakraTooltip>
}
>
{serialNumbersFromCategory.isFetching ? (
<Center my={8}>
<Spinner size="xl" />
</Center>
) : (
<Box>
{serialNumbersFromCategory.error ? (
<Alert mb={4} status="error">
<AlertTitle>{t('common.error')}</AlertTitle>
<AlertDescription>
{(serialNumbersFromCategory.error as AxiosError).response?.data.ErrorDescription}
</AlertDescription>
</Alert>
) : null}
{serialNumbersFromCategory.data ? (
<Box>
<Heading size="md" mb={4}>
{serialNumbersFromCategory.data.length} {t('devices.title')} {deviceCategory.label}
</Heading>
<Box maxH="70vh" overflowY="auto" overflowX="hidden">
<UnorderedList pl={2}>
{serialNumbersFromCategory.data
.sort((a, b) => a.localeCompare(b))
.map((device) => (
<ListItem key={device} fontFamily="mono">
<Link href={`#/devices/${device}`}>{device}</Link>
</ListItem>
))}
</UnorderedList>
</Box>
</Box>
) : null}
</Box>
)}
</Modal>
</>
);
};