add storage table

This commit is contained in:
NlightN22 2024-03-13 03:06:55 +07:00
parent 3c231a7bf3
commit b896c88f6c
9 changed files with 207 additions and 21 deletions

View File

@ -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',

View File

@ -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: 'Перезагрузка',

View File

@ -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<GetFrigateHost | undefined>()
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()}
/>
/>
</Grid.Col>
)
})
@ -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}
</Grid>
<FrigateCamerasStateTable data={mappedCameraStat} onFfprobeClick={handleFfprobeClick} />
<SegmentedControl
value={selector}
onChange={handleSelectView}
data={[
{ label: t('systemPage.cameraStats'), value: SelectorItems.Cameras },
{ label: t('systemPage.storageStats'), value: SelectorItems.Storage },
]}
/>
{selector === SelectorItems.Cameras ?
<FrigateCamerasStateTable data={mappedCameraStat} onFfprobeClick={handleFfprobeClick} />
:
<FrigateStorageStateTable host={host.current} />
}
</Flex>
);
};

View File

@ -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<GetHostStorage>(`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',

View File

@ -20,14 +20,18 @@ export function sortArrayByObjectIndex<T extends object>(
export function sortByKey<T, K extends keyof T>(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
});
}

View File

@ -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

5
src/types/table.ts Normal file
View File

@ -0,0 +1,5 @@
export interface TableHead {
propertyName: string,
title: string,
sorting?: boolean,
}

View File

@ -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<T> {
data: T[],
onFfprobeClick?: (cameraName: string) => void

View File

@ -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<TableProps> = ({
host
}) => {
const [reversed, setReversed] = useState(false)
const [sortedName, setSortedName] = useState<string | null>(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<StorageItem>(([name, storage]) => {
return {
cameraName: name,
usage: storage.usage,
usagePercent: storage.usage_percent,
sreamBandwidth: storage.bandwidth,
}
})
}, [data])
const [tableData, setTableData] = useState<StorageItem[]>(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 <CogwheelLoader />
if (isError) return <RetryError onRetry={refetch} />
// if (!tableData || tableData.length < 1) return <Center><Text>Empty response</Text></Center>
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 (
<SortedTh
key={uuidv4()}
title={head.title}
reversed={reversed}
sortedName={sortedName}
onSort={() => handleSort(head.title, head.propertyName ? head.propertyName : '')}
sorting={head.sorting} />
)
})
const rows = tableData.map(item => {
return (
<tr key={item.cameraName}>
<td><Text align='center'>{item.cameraName}</Text></td>
<td><Text align='center'>{formatMBytes(item.usage)}</Text></td>
<td><Text align='center'>{item.usagePercent.toFixed(4)} %</Text></td>
<td><Text align='center'>{item.sreamBandwidth} MiB/hr</Text></td>
</tr>
)
})
return (
<Table >
<thead>
<tr>
{tableHead}
</tr>
</thead>
<tbody>
{rows}
</tbody>
</Table>
);
};
export default FrigateStorageStateTable;