diff --git a/package-lock.json b/package-lock.json index ea9f614..b2d2e05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ce4c2ce..0c6801c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ucentral-client", - "version": "2.9.0(22)", + "version": "2.9.0(23)", "description": "", "private": true, "main": "index.tsx", diff --git a/src/hooks/Network/HealthChecks.ts b/src/hooks/Network/HealthChecks.ts index dbaefd4..0206087 100644 --- a/src/hooks/Network/HealthChecks.ts +++ b/src/hooks/Network/HealthChecks.ts @@ -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); diff --git a/src/pages/Device/StatisticsCard/MemoryChart.tsx b/src/pages/Device/StatisticsCard/MemoryChart.tsx index 010e4c5..74794ac 100644 --- a/src/pages/Device/StatisticsCard/MemoryChart.tsx +++ b/src/pages/Device/StatisticsCard/MemoryChart.tsx @@ -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', }, ], }; diff --git a/src/pages/Device/StatisticsCard/useStatisticsCard.ts b/src/pages/Device/StatisticsCard/useStatisticsCard.ts index 50aa1ee..c3a7c07 100644 --- a/src/pages/Device/StatisticsCard/useStatisticsCard.ts +++ b/src/pages/Device/StatisticsCard/useStatisticsCard.ts @@ -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) => { + 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); } diff --git a/src/pages/Devices/Dashboard/OverallHealthPieChart.tsx b/src/pages/Devices/Dashboard/OverallHealthPieChart.tsx index 346ccf6..a641ef6 100644 --- a/src/pages/Devices/Dashboard/OverallHealthPieChart.tsx +++ b/src/pages/Devices/Dashboard/OverallHealthPieChart.tsx @@ -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>( + null, + ); + + const onClick = (event: React.MouseEvent) => { + 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 ( - + { + 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, - )}%)`, - }, - }, - }, - }} - /> - } - /> + }} + /> + } + /> + + } + onClick={onCopy} + colorScheme="teal" + hidden={!serialNumbersFromCategory.data || serialNumbersFromCategory.data.length === 0} + /> + + } + > + {serialNumbersFromCategory.isFetching ? ( +
+ +
+ ) : ( + + {serialNumbersFromCategory.error ? ( + + {t('common.error')} + + {(serialNumbersFromCategory.error as AxiosError).response?.data.ErrorDescription} + + + ) : null} + {serialNumbersFromCategory.data ? ( + + + {serialNumbersFromCategory.data.length} {t('devices.title')} {deviceCategory.label} + + + + {serialNumbersFromCategory.data + .sort((a, b) => a.localeCompare(b)) + .map((device) => ( + + {device} + + ))} + + + + ) : null} + + )} +
+ ); };