mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-10-29 09:22:21 +00:00
[WIFI-12418] Memory chart display hidden automatically
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.9.0(22)",
|
||||
"version": "2.9.0(23)",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.tsx",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user