add system page
This commit is contained in:
parent
a3ff2960fc
commit
765cca1f31
@ -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',
|
||||
|
||||
@ -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: 'Выбери период',
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
42
src/shared/components/stats/DetectorsStat.tsx
Normal file
42
src/shared/components/stats/DetectorsStat.tsx
Normal 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;
|
||||
42
src/shared/components/stats/GpuStat.tsx
Normal file
42
src/shared/components/stats/GpuStat.tsx
Normal 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;
|
||||
65
src/shared/components/stats/StorageRingStat.tsx
Normal file
65
src/shared/components/stats/StorageRingStat.tsx
Normal 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;
|
||||
@ -1,6 +0,0 @@
|
||||
export function valueIsNotEmpty(value: any) {
|
||||
if (value) {
|
||||
return true
|
||||
} else if( typeof value === 'boolean') return true
|
||||
return false
|
||||
}
|
||||
13
src/shared/utils/data.size.ts
Normal file
13
src/shared/utils/data.size.ts
Normal 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);
|
||||
}
|
||||
@ -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) => {
|
||||
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user