add system page

This commit is contained in:
NlightN22 2024-03-11 03:39:27 +07:00
parent a3ff2960fc
commit 765cca1f31
11 changed files with 285 additions and 70 deletions

View File

@ -1,4 +1,15 @@
const en = {
detectorCard: {
pid: 'PID',
inferenceSpeed: 'Inference Speed',
memory: 'Memory',
},
gpuStatCard: {
gpu: 'GPU',
memory: 'Memory',
decoder: 'Decoder',
encoder: 'Encoder',
},
cameraStatTable: {
process: 'Process',
pid: 'PID',
@ -35,6 +46,8 @@ const en = {
doubleClickToFullHint: 'Double click for fullscreen',
rating: 'Rating',
},
version: 'Version',
uptime: 'Uptime',
pleaseSelectRole: 'Please select Role',
pleaseSelectHost: 'Please select Host',
pleaseSelectCamera: 'Please select Camera',
@ -46,10 +59,12 @@ const en = {
camersDoesNotExist: 'No cameras',
search: 'Search',
recordings: 'Recordings',
day: 'Day',
hour: 'Hour',
minute: 'Minute',
second: 'Second',
events: 'Events',
notHaveEvents: 'No events',
day: 'Day',
selectHost: 'Select host',
selectCamera: 'Select Camera',
selectRange: 'Select period',

View File

@ -1,4 +1,15 @@
const ru = {
detectorCard: {
pid: 'PID',
inferenceSpeed: 'Скорость вывода',
memory: 'Память',
},
gpuStatCard: {
gpu: 'GPU',
memory: 'Память',
decoder: 'Декодер',
encoder: 'Кодер',
},
hostMenu: {
editConfig: 'Редакт. конфиг.',
restart: 'Перезагрузка',
@ -28,6 +39,8 @@ const ru = {
doubleClickToFullHint: 'Двойное нажатие мышью для полноэкранного просмотра',
rating: 'Рейтинг',
},
version: 'Версия',
uptime: 'Время работы',
pleaseSelectRole: 'Пожалуйста выберите роль',
pleaseSelectHost: 'Пожалуйста выберите хост',
pleaseSelectCamera: 'Пожалуйста выберите камеру',
@ -39,10 +52,12 @@ const ru = {
camersDoesNotExist: 'Камер нет',
search: 'Поиск',
recordings: 'Записи',
day: 'День',
hour: 'Час',
minute: 'Минута',
second: 'Час',
events: 'События',
notHaveEvents: 'Событий нет',
day: 'День',
selectHost:'Выбери хост',
selectCamera: 'Выбери камеру',
selectRange: 'Выбери период',

View File

@ -8,14 +8,21 @@ import { useQuery } from '@tanstack/react-query';
import { frigateApi, frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
import CenterLoader from '../shared/components/loaders/CenterLoader';
import RetryErrorPage from './RetryErrorPage';
import { Flex } from '@mantine/core';
import { Flex, Grid, Text } from '@mantine/core';
import FrigateCamerasStateTable, { CameraItem, ProcessType } from '../widgets/camera.stat.table/FrigateCameraStateTable';
import StorageRingStat from '../shared/components/stats/StorageRingStat';
import { useTranslation } from 'react-i18next';
import { formatUptime } from '../shared/utils/dateUtil';
import GpuStat from '../shared/components/stats/GpuStat';
import { v4 } from 'uuid';
import DetectorsStat from '../shared/components/stats/DetectorsStat';
export const hostSystemPageQuery = {
hostId: 'hostId',
}
const HostSystemPage = () => {
const { t } = useTranslation()
const executed = useRef(false)
const { sideBarsStore } = useContext(Context)
const { isAdmin } = useAdminRole()
@ -29,11 +36,6 @@ const HostSystemPage = () => {
}
}, [sideBarsStore])
const location = useLocation()
const queryParams = useMemo(() => {
return new URLSearchParams(location.search);
}, [location.search])
let { id: paramHostId } = useParams<'id'>()
const { data, isError, isPending, refetch } = useQuery({
@ -52,9 +54,9 @@ const HostSystemPage = () => {
return Object.entries(data.cameras).flatMap(([name, stats]) => {
return (['Ffmpeg', 'Capture', 'Detect'] as ProcessType[]).map(type => {
const pid = type === ProcessType.Ffmpeg ? stats.ffmpeg_pid :
type === ProcessType.Capture ? stats.capture_pid : stats.pid;
type === ProcessType.Capture ? stats.capture_pid : stats.pid;
const fps = type === ProcessType.Ffmpeg ? stats.camera_fps :
type === ProcessType.Capture ? stats.process_fps : stats.detection_fps;
type === ProcessType.Capture ? stats.process_fps : stats.detection_fps;
const cpu = data.cpu_usages[pid]?.cpu;
const mem = data.cpu_usages[pid]?.mem;
@ -76,9 +78,68 @@ const HostSystemPage = () => {
if (!paramHostId || !data) return null
const mappedCameraStat: CameraItem[] = mapCameraData()
const storageStats = Object.entries(data.service.storage).map(([name, stats]) => {
return (
<Grid.Col key={name + stats.mount_type} xs={6} sm={5} md={4} lg={3} p='0.2rem'>
<StorageRingStat
used={stats.used}
free={stats.free}
storageType={stats.mount_type}
total={stats.total}
path={name} />
</Grid.Col>
)
})
const formattedUptime = () => {
const time = formatUptime(data.service.uptime)
const translatedUnit = t(time.unit).toLowerCase().slice(0, 1)
return `${time.value.toFixed(1)} ${translatedUnit}`
}
const gpuStats = Object.entries(data.gpu_usages).map(([name, stats]) => {
return (
<Grid.Col key={name + stats.gpu} xs={7} sm={6} md={5} lg={4} p='0.2rem'>
<GpuStat
name={name}
decoder={stats.dec}
encoder={stats.enc}
gpu={stats.gpu}
mem={stats.mem} />
</Grid.Col>
)
})
const detectorsStats = Object.entries(data.detectors).map(([name, stats]) => {
const pid = stats.pid
const cpu = data.cpu_usages[pid]?.cpu;
const mem = data.cpu_usages[pid]?.mem;
return (
<Grid.Col key={pid} xs={6} sm={5} md={4} lg={3} p='0.2rem'>
<DetectorsStat
name={name}
pid={pid}
inferenceSpeed={stats.inference_speed}
cpu={cpu}
mem={mem}
/>
</Grid.Col>
)
})
return (
<Flex w='100%' h='100%'>
<Flex w='100%' h='100%' direction='column'>
<Flex w='100%' justify='space-around'>
<Text>{t('version')} : {data.service.version}</Text>
<Text>{t('uptime')} : {formattedUptime()}</Text>
</Flex>
<Grid mt='sm' justify="center" mb='sm' align='stretch'>
{storageStats}
</Grid>
<Grid mt='sm' justify="center" mb='sm' align='stretch'>
{gpuStats}
{detectorsStats}
</Grid>
<FrigateCamerasStateTable data={mappedCameraStat} />
</Flex>
);

View File

@ -0,0 +1,42 @@
import { Card, Flex, Group, Text } from '@mantine/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
interface DetectorsStatProps {
name: string
pid: number,
inferenceSpeed?: number,
cpu?: string,
mem?: string,
}
const DetectorsStat: React.FC<DetectorsStatProps> = ({
name,
pid,
inferenceSpeed,
cpu,
mem
}) => {
const { t } = useTranslation()
return (
<Card withBorder radius="md" p='0.7rem'>
<Flex align='center'>
<Text c="dimmed" size="xs" tt="uppercase" fw={700} mr='0.5rem'>
{name}
</Text>
<Flex w='100%' direction='column' align='center'>
<Flex w='100%' justify='space-between'>
<Text>{t('detectorCard.pid')}: {pid}</Text>
<Text>{t('detectorCard.inferenceSpeed')}: {inferenceSpeed}</Text>
</Flex>
<Flex w='100%' justify='space-between'>
<Text>CPU: {cpu}</Text>
<Text>{t('detectorCard.memory')}: {mem}</Text>
</Flex>
</Flex>
</Flex>
</Card>
);
};
export default DetectorsStat;

View File

@ -0,0 +1,42 @@
import { Card, Flex, Group, Text } from '@mantine/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
interface GpuStatProps {
name: string,
decoder?: string,
encoder?: string,
gpu?: string,
mem?: string
}
const GpuStat: React.FC<GpuStatProps> = ({
name,
decoder,
encoder,
gpu,
mem
}) => {
const { t } = useTranslation()
return (
<Card withBorder radius="md" p='0.7rem'>
<Flex align='center'>
<Text c="dimmed" size="xs" tt="uppercase" fw={700} mr='0.5rem'>
{name}
</Text>
<Flex w='100%' direction='column' align='center'>
<Flex w='100%' justify='space-between'>
<Text>{t('gpuStatCard.gpu')}: {gpu}</Text>
<Text>{t('gpuStatCard.memory')}: {mem}</Text>
</Flex>
<Flex w='100%' justify='space-between'>
<Text>{t('gpuStatCard.decoder')}: {decoder}</Text>
<Text>{t('gpuStatCard.encoder')}: {encoder}</Text>
</Flex>
</Flex>
</Flex>
</Card>
);
};
export default GpuStat;

View File

@ -0,0 +1,65 @@
import { Card, Center, Flex, Group, Paper, RingProgress, Stack, Text } from '@mantine/core';
import React from 'react';
import { formatMBytes } from '../../utils/data.size';
interface StorageRingStatProps {
used: number
free: number
total?: number
storageType: string
path?: string
}
const StorageRingStat: React.FC<StorageRingStatProps> = ({
used,
free,
storageType,
total,
path
}) => {
const calcTotal = total || used + free
const availablePercent = (used / calcTotal * 100)
return (
<Card withBorder radius="md" key={storageType} p='0.2rem'>
<Flex align='center'>
<RingProgress
size={80}
roundCaps
thickness={8}
sections={[
{ value: availablePercent, color: 'blue' },
]}
label={
<Center>
<Text size='md'>{availablePercent.toFixed(0)}%</Text>
</Center>
}
/>
<Flex w='100%' direction='column' align='center'>
<Text c="dimmed" size="xs" tt="uppercase" fw={700}>
{storageType}
</Text>
{!path ? null :
<Text fw={700} size="md">
{path}
</Text>
}
<Flex w='100%' justify='space-around'>
<Flex direction='column'>
<Text>Used:</Text>
<Text>{formatMBytes(used)}</Text>
</Flex>
<Flex direction='column'>
<Text>Free:</Text>
<Text>{formatMBytes(free)}</Text>
</Flex>
</Flex>
</Flex>
</Flex>
</Card>
);
};
export default StorageRingStat;

View File

@ -1,6 +0,0 @@
export function valueIsNotEmpty(value: any) {
if (value) {
return true
} else if( typeof value === 'boolean') return true
return false
}

View File

@ -0,0 +1,13 @@
export const formatBytes = (bytes: number, decimals = 2): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
export const formatMBytes = (mb: number, decimals?: number): string => {
const bytes = mb * 1024 * 1024;
return formatBytes(bytes, decimals);
}

View File

@ -4,6 +4,26 @@ export const longToDate = (long: number): Date => new Date(long * 1000);
export const epochToLong = (date: number): number => date / 1000;
export const dateToLong = (date: Date): number => epochToLong(date.getTime());
/**
*
* @param uptimeInSeconds
* @returns value: number, unit: 'day' / 'hour' / 'minute' / 'second'
*/
export const formatUptime = (uptimeInSeconds: number) => {
const secondsInAMinute = 60
const secondsInAnHour = 3600
const secondsInADay = 86400
if (uptimeInSeconds >= secondsInADay) {
return { value: (uptimeInSeconds / secondsInADay), unit: 'day' };
} else if (uptimeInSeconds >= secondsInAnHour) {
return { value: (uptimeInSeconds / secondsInAnHour), unit: 'hour' };
} else if (uptimeInSeconds >= secondsInAMinute) {
return { value: (uptimeInSeconds / secondsInAMinute), unit: 'minute' };
} else {
return { value: uptimeInSeconds, unit: 'second' };
}
}
export const formatFileTimestamps = (startUnixTime: number, endUnixTime: number, cameraName: string) => {
const formatTime = (time: number) => {

View File

@ -1,13 +0,0 @@
export function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...funcArgs: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | null = null;
return function(...args: Parameters<T>): void {
const later = () => {
timeout = null;
func(...args);
};
if (timeout !== null) {
clearTimeout(timeout);
}
timeout = setTimeout(later, wait);
};
}

View File

@ -1,39 +0,0 @@
import { MutableRefObject, useEffect, useMemo, useState } from "react";
export function useResizeObserver(...refs: MutableRefObject<Element | null>[]) {
const [dimensions, setDimensions] = useState(
new Array(refs.length).fill({
width: 0,
height: 0,
x: -Infinity,
y: -Infinity,
})
);
const resizeObserver = useMemo(
() =>
new ResizeObserver((entries) => {
window.requestAnimationFrame(() => {
setDimensions(entries.map((entry) => entry.contentRect));
});
}),
[]
);
useEffect(() => {
refs.forEach((ref) => {
if (ref.current) {
resizeObserver.observe(ref.current);
}
});
return () => {
refs.forEach((ref) => {
if (ref.current) {
resizeObserver.unobserve(ref.current);
}
});
};
}, [refs, resizeObserver]);
return dimensions;
}