From b896c88f6c2d4f8c7c162daafb058154ab3dbab6 Mon Sep 17 00:00:00 2001 From: NlightN22 Date: Wed, 13 Mar 2024 03:06:55 +0700 Subject: [PATCH] add storage table --- src/locales/en.ts | 9 ++ src/locales/ru.ts | 16 +++ src/pages/HostSystemPage.tsx | 37 +++++- src/services/frigate.proxy/frigate.api.ts | 4 +- src/shared/utils/sort.array.ts | 18 ++- src/types/frigateStats.ts | 10 ++ src/types/table.ts | 5 + .../FrigateCameraStateTable.tsx | 7 +- .../FrigateStorageStateTable.tsx | 122 ++++++++++++++++++ 9 files changed, 207 insertions(+), 21 deletions(-) create mode 100644 src/types/table.ts create mode 100644 src/widgets/camera.stat.table/FrigateStorageStateTable.tsx diff --git a/src/locales/en.ts b/src/locales/en.ts index f09b123..6aaefe2 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1,4 +1,8 @@ const en = { + systemPage: { + cameraStats: 'Cameras stats', + storageStats: 'Storages stats', + }, detectorCard: { pid: 'PID', inferenceSpeed: 'Inference Speed', @@ -10,6 +14,11 @@ const en = { decoder: 'Decoder', encoder: 'Encoder', }, + cameraStorageTable: { + usage: 'Usage', + usagePercent: 'Usage %', + sreamBandwidth: 'Stream Bandwidth', + }, cameraStatTable: { process: 'Process', pid: 'PID', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index 3400609..b0f33fc 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -1,4 +1,8 @@ const ru = { + systemPage: { + cameraStats: 'Статистика Камер', + storageStats: 'Статистика Хранения', + }, detectorCard: { pid: 'PID', inferenceSpeed: 'Скорость вывода', @@ -10,6 +14,18 @@ const ru = { decoder: 'Декодер', encoder: 'Кодер', }, + cameraStorageTable: { + usage: 'Занято', + usagePercent: 'Занято %', + sreamBandwidth: 'Скорость потока', + }, + cameraStatTable: { + process: 'Процесс', + pid: 'PID', + fps: 'FPS', + cpu: 'CPU %', + memory: 'Память %' + }, hostMenu: { editConfig: 'Редакт. конфиг.', restart: 'Перезагрузка', diff --git a/src/pages/HostSystemPage.tsx b/src/pages/HostSystemPage.tsx index 66accf7..85afb90 100644 --- a/src/pages/HostSystemPage.tsx +++ b/src/pages/HostSystemPage.tsx @@ -1,7 +1,8 @@ -import { Flex, Grid, Text } from '@mantine/core'; +import { Flex, Grid, SegmentedControl, Text } from '@mantine/core'; +import { openContextModal } from '@mantine/modals'; import { useQuery } from '@tanstack/react-query'; import { observer } from 'mobx-react-lite'; -import { useCallback, useContext, useEffect, useRef } from 'react'; +import { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; import { Context } from '..'; @@ -17,19 +18,24 @@ import { formatUptime } from '../shared/utils/dateUtil'; import FrigateCamerasStateTable, { CameraItem, ProcessType } from '../widgets/camera.stat.table/FrigateCameraStateTable'; import Forbidden from './403'; import RetryErrorPage from './RetryErrorPage'; -import { openContextModal } from '@mantine/modals'; -import { FfprobeModalProps } from '../shared/components/modal.windows/FfprobeModal'; +import FrigateStorageStateTable from '../widgets/camera.stat.table/FrigateStorageStateTable'; export const hostSystemPageQuery = { hostId: 'hostId', } +enum SelectorItems { + Cameras = 'cameras', + Storage = 'storage' +} + const HostSystemPage = () => { const { t } = useTranslation() const executed = useRef(false) const { sideBarsStore } = useContext(Context) const { isAdmin } = useAdminRole() const host = useRef() + const [selector, setSelector] = useState(SelectorItems.Cameras) useEffect(() => { if (!executed.current) { @@ -119,9 +125,9 @@ const HostSystemPage = () => { decoder={stats.dec} encoder={stats.enc} gpu={stats.gpu} - mem={stats.mem} + mem={stats.mem} onVaInfoClick={() => handleVaInfoClick()} - /> + /> ) }) @@ -152,6 +158,11 @@ const HostSystemPage = () => { } }) + const handleSelectView = (value: string) => { + if (value === SelectorItems.Cameras) setSelector(SelectorItems.Cameras) + else setSelector(SelectorItems.Storage) + } + if (!isProduction) console.log('HostSystemPage rendered') return ( @@ -168,7 +179,19 @@ const HostSystemPage = () => { {gpuStats} {detectorsStats} - + + {selector === SelectorItems.Cameras ? + + : + + } ); }; diff --git a/src/services/frigate.proxy/frigate.api.ts b/src/services/frigate.proxy/frigate.api.ts index adf8495..4a42dce 100644 --- a/src/services/frigate.proxy/frigate.api.ts +++ b/src/services/frigate.proxy/frigate.api.ts @@ -10,7 +10,7 @@ import { RecordSummary } from "../../types/record"; import { EventFrigate } from "../../types/event"; import { keycloakConfig } from "../.."; import { getResolvedTimeZone } from "../../shared/utils/dateUtil"; -import { FrigateStats, GetFfprobe, GetVaInfo } from "../../types/frigateStats"; +import { FrigateStats, GetFfprobe, GetHostStorage, GetVaInfo } from "../../types/frigateStats"; import { hostname } from "os"; import { PostSaveConfig, SaveOption } from "../../types/saveConfig"; @@ -192,6 +192,7 @@ export const proxyApi = { save_option: saveOption } }).then(res => res.data), + getHostStorage: (hostName: string) => instanceApi.get(`proxy/${hostName}/api/recordings/storage`).then(res => res.data), } export const mapCamerasFromConfig = (config: FrigateConfig): string[] => { @@ -218,6 +219,7 @@ export const frigateQueryKeys = { getHostStats: 'host-stats', getCameraFfprobe: 'camera-ffprobe', getHostVaInfo: 'host-vainfo', + getHostStorage: 'host-storage', getRecordingsSummary: 'recordings-frigate-summary', getRecordings: 'recordings-frigate', getEvents: 'events-frigate', diff --git a/src/shared/utils/sort.array.ts b/src/shared/utils/sort.array.ts index e5a8f48..fabb723 100644 --- a/src/shared/utils/sort.array.ts +++ b/src/shared/utils/sort.array.ts @@ -20,14 +20,18 @@ export function sortArrayByObjectIndex( export function sortByKey(array: T[], key: K): T[] { return array.sort((a, b) => { - let valueA = a[key]; - let valueB = b[key]; + let valueA = a[key] + let valueB = b[key] - const stringValueA = String(valueA).toLowerCase(); - const stringValueB = String(valueB).toLowerCase(); + if (typeof valueA === 'number' && typeof valueB === 'number') { + return valueA - valueB + } else { + const stringValueA = String(valueA).toLowerCase() + const stringValueB = String(valueB).toLowerCase() - if (stringValueA < stringValueB) return -1; - if (stringValueA > stringValueB) return 1; - return 0; + if (stringValueA < stringValueB) return -1 + if (stringValueA > stringValueB) return 1 + } + return 0 }); } \ No newline at end of file diff --git a/src/types/frigateStats.ts b/src/types/frigateStats.ts index 2ceafb7..7574831 100644 --- a/src/types/frigateStats.ts +++ b/src/types/frigateStats.ts @@ -1,3 +1,13 @@ +export interface GetHostStorage { + [cameraName: string]: CameraStorage +} + +export interface CameraStorage { + bandwidth: number // MiB/hr + usage: number // MB + usage_percent: number // Usage / 1024 / Total storage size * 100 +} + export interface GetVaInfo { return_code: number stderr: string diff --git a/src/types/table.ts b/src/types/table.ts new file mode 100644 index 0000000..e5a8b7f --- /dev/null +++ b/src/types/table.ts @@ -0,0 +1,5 @@ +export interface TableHead { + propertyName: string, + title: string, + sorting?: boolean, +} \ No newline at end of file diff --git a/src/widgets/camera.stat.table/FrigateCameraStateTable.tsx b/src/widgets/camera.stat.table/FrigateCameraStateTable.tsx index 04f8a41..da33b0a 100644 --- a/src/widgets/camera.stat.table/FrigateCameraStateTable.tsx +++ b/src/widgets/camera.stat.table/FrigateCameraStateTable.tsx @@ -6,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import SortedTh from '../../shared/components/table.aps/SortedTh'; import { isProduction } from '../../shared/env.const'; import { sortByKey } from '../../shared/utils/sort.array'; +import { TableHead } from '../../types/table'; export interface CameraItem { @@ -23,12 +24,6 @@ export enum ProcessType { Detect = "Detect", } -interface TableHead { - propertyName: string, - title: string, - sorting?: boolean, -} - interface TableProps { data: T[], onFfprobeClick?: (cameraName: string) => void diff --git a/src/widgets/camera.stat.table/FrigateStorageStateTable.tsx b/src/widgets/camera.stat.table/FrigateStorageStateTable.tsx new file mode 100644 index 0000000..bbdf430 --- /dev/null +++ b/src/widgets/camera.stat.table/FrigateStorageStateTable.tsx @@ -0,0 +1,122 @@ +import { useQuery } from '@tanstack/react-query'; +import React, { useCallback, useEffect, useState } from 'react'; +import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../services/frigate.proxy/frigate.api'; +import { GetFrigateHost } from '../../services/frigate.proxy/frigate.schema'; +import CogwheelLoader from '../../shared/components/loaders/CogwheelLoader'; +import RetryError from '../../shared/components/RetryError'; +import { Center, Flex, Table, Text } from '@mantine/core'; +import { TableHead } from '../../types/table'; +import SortedTh from '../../shared/components/table.aps/SortedTh'; +import { useTranslation } from 'react-i18next'; +import { v4 as uuidv4 } from 'uuid'; +import { sortByKey } from '../../shared/utils/sort.array'; +import { formatMBytes } from '../../shared/utils/data.size'; + + +export interface StorageItem { + cameraName: string + usage: number + usagePercent: number + sreamBandwidth: number // MiB/hr +} + +interface TableProps { + host?: GetFrigateHost +} + +const FrigateStorageStateTable: React.FC = ({ + host +}) => { + + const [reversed, setReversed] = useState(false) + const [sortedName, setSortedName] = useState(null) + const { t } = useTranslation() + + const { data, isError, isPending, refetch } = useQuery({ + queryKey: [frigateQueryKeys.getHostStorage, host?.id], + queryFn: () => { + const hostName = mapHostToHostname(host) + if (!hostName) return null + return proxyApi.getHostStorage(hostName) + } + }) + + const mapToTable = useCallback(() => { + if (!data) return [] + return Object.entries(data).map(([name, storage]) => { + return { + cameraName: name, + usage: storage.usage, + usagePercent: storage.usage_percent, + sreamBandwidth: storage.bandwidth, + } + }) + }, [data]) + + const [tableData, setTableData] = useState(mapToTable()) + + useEffect( () => { + setTableData(mapToTable()) + }, [data]) + + const handleSort = (headName: string, propertyName: string,) => { + if (!data || !tableData) return + const reverse = headName === sortedName ? !reversed : false; + setReversed(reverse) + const arr = sortByKey(tableData, propertyName as keyof StorageItem) + if (reverse) arr.reverse() + setTableData(arr) + setSortedName(headName) + } + + if (isPending) return + if (isError) return + // if (!tableData || tableData.length < 1) return
Empty response
+ + + const headTitle: TableHead[] = [ + { propertyName: 'cameraName', title: t('camera') }, + { propertyName: 'usage', title: t('cameraStorageTable.usage') }, + { propertyName: 'usagePercent', title: t('cameraStorageTable.usagePercent') }, + { propertyName: 'sreamBandwidth', title: t('cameraStorageTable.sreamBandwidth') }, + ] + + const tableHead = headTitle.map(head => { + return ( + handleSort(head.title, head.propertyName ? head.propertyName : '')} + sorting={head.sorting} /> + ) + }) + + + const rows = tableData.map(item => { + return ( + + {item.cameraName} + {formatMBytes(item.usage)} + {item.usagePercent.toFixed(4)} % + {item.sreamBandwidth} MiB/hr + + ) + }) + + return ( + + + + {tableHead} + + + + {rows} + +
+ ); +}; + +export default FrigateStorageStateTable; \ No newline at end of file