mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-10-29 17:32:20 +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",
|
"name": "ucentral-client",
|
||||||
"version": "2.9.0(22)",
|
"version": "2.9.0(23)",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.9.0(22)",
|
"version": "2.9.0(23)",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/icons": "^2.0.11",
|
"@chakra-ui/icons": "^2.0.11",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.9.0(22)",
|
"version": "2.9.0(23)",
|
||||||
"description": "",
|
"description": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "index.tsx",
|
"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 { axiosGw } from 'constants/axiosInstances';
|
||||||
import { AxiosError } from 'models/Axios';
|
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',
|
label: 'Free',
|
||||||
data: data.free.map((free) => Math.floor(free / 1024 / 1024)),
|
data: data.free.map((free) => Math.floor(free / 1024 / 1024)),
|
||||||
borderColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // blue-300 - blue-100
|
borderColor: colorMode === 'light' ? 'rgb(99, 179, 237, 1)' : 'rgb(190, 227, 248, 1)', // blue-300 - blue-100
|
||||||
backgroundColor: colorMode === 'light' ? '#63B3ED' : '#BEE3F8', // 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,
|
||||||
label: 'Buffered',
|
fill: '+1',
|
||||||
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
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Cached',
|
label: 'Cached',
|
||||||
data: data.cached.map((cached) => Math.floor(cached / 1024 / 1024)),
|
data: data.cached.map((cached) => Math.floor(cached / 1024 / 1024)),
|
||||||
borderColor: 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' ? '#ED64A6' : '#FBB6CE', // 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) => {
|
export const useStatisticsCard = ({ serialNumber }: Props) => {
|
||||||
const [selected, setSelected] = React.useState('memory');
|
const [selected, setSelected] = React.useState('memory');
|
||||||
const [progress, setProgress] = React.useState(0);
|
const [progress, setProgress] = React.useState(0);
|
||||||
|
const [hasSelectedNew, setHasSelectedNew] = React.useState(false);
|
||||||
const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>();
|
const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>();
|
||||||
const onProgressChange = React.useCallback((newProgress: number) => {
|
const onProgressChange = React.useCallback((newProgress: number) => {
|
||||||
setProgress(newProgress);
|
setProgress(newProgress);
|
||||||
@@ -29,6 +30,7 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSelectInterface = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
const onSelectInterface = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
setHasSelectedNew(true);
|
||||||
setSelected(event.target.value);
|
setSelected(event.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,7 +61,7 @@ export const useStatisticsCard = ({ serialNumber }: Props) => {
|
|||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
let updated = false;
|
let updated = false;
|
||||||
for (const inter of stat.data.interfaces ?? []) {
|
for (const inter of stat.data.interfaces ?? []) {
|
||||||
if (!updated && selected === 'memory') {
|
if (!hasSelectedNew && !updated && selected === 'memory') {
|
||||||
updated = true;
|
updated = true;
|
||||||
setSelected(inter.name);
|
setSelected(inter.name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
import * as React from 'react';
|
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 {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
@@ -11,20 +28,57 @@ import {
|
|||||||
Legend,
|
Legend,
|
||||||
ChartData,
|
ChartData,
|
||||||
ArcElement,
|
ArcElement,
|
||||||
|
ChartTypeRegistry,
|
||||||
|
ScatterDataPoint,
|
||||||
|
BubbleDataPoint,
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import { Pie } from 'react-chartjs-2';
|
import { Pie, getElementAtEvent } from 'react-chartjs-2';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import GraphStatDisplay from 'components/Containers/GraphStatDisplay';
|
import GraphStatDisplay from 'components/Containers/GraphStatDisplay';
|
||||||
|
import { Modal } from 'components/Modals/Modal';
|
||||||
import { ControllerDashboardHealth } from 'hooks/Network/Controller';
|
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);
|
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, ArcElement);
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
data: ControllerDashboardHealth[];
|
data: ControllerDashboardHealth[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const OverallHealthPieChart = ({ data }: Props) => {
|
const OverallHealthPieChart = ({ data }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { colorMode } = useColorMode();
|
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 parsedData: ChartData<'pie', number[], unknown> = React.useMemo(() => {
|
||||||
const totalDevices = data.reduce(
|
const totalDevices = data.reduce(
|
||||||
@@ -85,7 +139,7 @@ const OverallHealthPieChart = ({ data }: Props) => {
|
|||||||
}
|
}
|
||||||
if (totalDevices['<60%'] > 0) {
|
if (totalDevices['<60%'] > 0) {
|
||||||
newData.push(totalDevices['<60%']);
|
newData.push(totalDevices['<60%']);
|
||||||
labels.push('<60%');
|
labels.push('<=60%');
|
||||||
const color = colorMode === 'light' ? '#FC8181' : '#FC8181';
|
const color = colorMode === 'light' ? '#FC8181' : '#FC8181';
|
||||||
backgroundColor.push(color);
|
backgroundColor.push(color);
|
||||||
borderColor.push(color);
|
borderColor.push(color);
|
||||||
@@ -105,38 +159,108 @@ const OverallHealthPieChart = ({ data }: Props) => {
|
|||||||
};
|
};
|
||||||
}, [data, colorMode]);
|
}, [data, colorMode]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (serialNumbersFromCategory.data) setValue(serialNumbersFromCategory.data.join(','));
|
||||||
|
}, [serialNumbersFromCategory.data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GraphStatDisplay
|
<>
|
||||||
title={t('controller.dashboard.overall_health')}
|
<GraphStatDisplay
|
||||||
explanation={t('controller.dashboard.overall_health_explanation_pie')}
|
title={t('controller.dashboard.overall_health')}
|
||||||
chart={
|
explanation={t('controller.dashboard.overall_health_explanation_pie')}
|
||||||
<Pie
|
chart={
|
||||||
data={parsedData}
|
<Pie
|
||||||
options={{
|
// @ts-ignore
|
||||||
plugins: {
|
ref={chartRef}
|
||||||
legend: {
|
data={parsedData}
|
||||||
position: 'top' as const,
|
onClick={onClick}
|
||||||
labels: {
|
options={{
|
||||||
color: colorMode === 'dark' ? 'white' : undefined,
|
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: {
|
<Modal
|
||||||
label: (context) =>
|
title={t('controller.dashboard.overall_health')}
|
||||||
`${context.label}: ${context.formattedValue} (${Math.round(
|
{...modalProps}
|
||||||
// @ts-ignore
|
options={{
|
||||||
(context.raw / context.dataset.data.reduce((acc, curr) => acc + curr, 0)) * 100,
|
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