From a3ff2960fc5a3e7dc1c7817e8b35168f6b0ddb31 Mon Sep 17 00:00:00 2001 From: NlightN22 Date: Mon, 11 Mar 2024 00:03:52 +0700 Subject: [PATCH] fix selected SelectedCameraList bug add translate add system page and cameras stat table --- Dockerfile | 2 +- src/locales/en.ts | 16 ++- src/locales/ru.ts | 6 + src/pages/FrigateHostsPage.tsx | 2 +- src/pages/HostSystemPage.tsx | 65 ++++++++++- src/services/frigate.proxy/frigate.api.ts | 5 +- .../components/accordion/CameraAccordion.tsx | 3 + .../buttons/AccordionShareButton.tsx | 2 + .../filters/RecordingsHostFilter.tsx | 12 +- .../components/menu/HostSettingsMenu.tsx | 17 +-- src/shared/utils/sort.array.ts | 50 ++++++--- src/types/frigateStats.ts | 88 +++++++++++++++ src/widgets/SelectedCameraList.tsx | 4 +- .../FrigateCameraStateTable.tsx | 106 ++++++++++++++++++ .../{ => hosts.table}/FrigateHostsTable.tsx | 29 ++--- src/widgets/hosts.table/StateCell.tsx | 14 ++- 16 files changed, 355 insertions(+), 66 deletions(-) create mode 100644 src/types/frigateStats.ts create mode 100644 src/widgets/camera.stat.table/FrigateCameraStateTable.tsx rename src/widgets/{ => hosts.table}/FrigateHostsTable.tsx (83%) diff --git a/Dockerfile b/Dockerfile index 44805b2..0c51d51 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 # Build commands: -# - $VERSION=0.7 +# - $VERSION=0.8 # - rm build -r -Force ; rm ./node_modules/.cache/babel-loader -r -Force ; yarn build # - docker build --pull --rm -t oncharterliz/multi-frigate:latest -t oncharterliz/multi-frigate:$VERSION "." # - docker image push --all-tags oncharterliz/multi-frigate diff --git a/src/locales/en.ts b/src/locales/en.ts index ce5b35f..a416a63 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1,11 +1,25 @@ const en = { + cameraStatTable: { + process: 'Process', + pid: 'PID', + fps: 'FPS', + cpu: 'CPU %', + memory: 'Memory %' + }, + hostMenu: { + editConfig: 'Edit config', + restart: 'Restart', + system: 'System', + storage: 'Storage', + }, header: { home: 'Main', settings: 'Settings', recordings: 'Recordings', hostsConfig: 'Frigate servers', acessSettings: 'Access settings', - }, hostArr: { + }, + hostArr: { host: 'Host', name: 'Host name', url: 'Address', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index 25e31ab..aab9af5 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -1,4 +1,10 @@ const ru = { + hostMenu: { + editConfig: 'Редакт. конфиг.', + restart: 'Перезагрузка', + system: 'Система', + storage: 'Хранилище', + }, header: { home: 'Главная', settings: 'Настройки', diff --git a/src/pages/FrigateHostsPage.tsx b/src/pages/FrigateHostsPage.tsx index ac434db..2108353 100644 --- a/src/pages/FrigateHostsPage.tsx +++ b/src/pages/FrigateHostsPage.tsx @@ -11,7 +11,7 @@ import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate. import { GetFrigateHost, deleteFrigateHostSchema, putFrigateHostSchema } from '../services/frigate.proxy/frigate.schema'; import CenterLoader from '../shared/components/loaders/CenterLoader'; import { isProduction } from '../shared/env.const'; -import FrigateHostsTable from '../widgets/FrigateHostsTable'; +import FrigateHostsTable from '../widgets/hosts.table/FrigateHostsTable'; import Forbidden from './403'; import RetryErrorPage from './RetryErrorPage'; diff --git a/src/pages/HostSystemPage.tsx b/src/pages/HostSystemPage.tsx index a5b8345..8ed242a 100644 --- a/src/pages/HostSystemPage.tsx +++ b/src/pages/HostSystemPage.tsx @@ -1,8 +1,19 @@ -import React, { useContext, useEffect, useRef } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; import { Context } from '..'; import { useAdminRole } from '../hooks/useAdminRole'; import Forbidden from './403'; import { observer } from 'mobx-react-lite'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; +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 FrigateCamerasStateTable, { CameraItem, ProcessType } from '../widgets/camera.stat.table/FrigateCameraStateTable'; + +export const hostSystemPageQuery = { + hostId: 'hostId', +} const HostSystemPage = () => { const executed = useRef(false) @@ -18,12 +29,58 @@ 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({ + queryKey: [frigateQueryKeys.getHostStats, paramHostId], + queryFn: async () => { + if (!paramHostId) return null + const host = await frigateApi.getHost(paramHostId) + const hostName = mapHostToHostname(host) + if (!hostName) return null + return proxyApi.getHostStats(hostName) + } + }) + + const mapCameraData = useCallback(() => { + if (!data) return [] + 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; + const fps = type === ProcessType.Ffmpeg ? stats.camera_fps : + type === ProcessType.Capture ? stats.process_fps : stats.detection_fps; + const cpu = data.cpu_usages[pid]?.cpu; + const mem = data.cpu_usages[pid]?.mem; + + return { + cameraName: name, + process: type, + pid: pid, + fps: fps, + cpu: cpu ?? 0, + mem: mem ?? 0, + }; + }); + }); + }, [data]); + if (!isAdmin) return + if (isPending) return + if (isError) return + if (!paramHostId || !data) return null + + const mappedCameraStat: CameraItem[] = mapCameraData() return ( -
- System Page - NOT YET IMPLEMENTED -
+ + + ); }; diff --git a/src/services/frigate.proxy/frigate.api.ts b/src/services/frigate.proxy/frigate.api.ts index 574d35b..62e87b3 100644 --- a/src/services/frigate.proxy/frigate.api.ts +++ b/src/services/frigate.proxy/frigate.api.ts @@ -10,6 +10,7 @@ import { RecordSummary } from "../../types/record"; import { EventFrigate } from "../../types/event"; import { keycloakConfig } from "../.."; import { getResolvedTimeZone } from "../../shared/utils/dateUtil"; +import { FrigateStats } from "../../types/frigateStats"; export const getToken = (): string | undefined => { @@ -171,7 +172,8 @@ export const proxyApi = { getExportedVideoList: (hostName: string) => instanceApi.get(`proxy/${hostName}/exports/`).then(res => res.data), getVideoUrl: (hostName: string, fileName: string) => `${proxyPrefix}${hostName}/exports/${fileName}`, // filename example Home_1_Backyard_2024_02_26_16_25__2024_02_26_16_26.mp4 - deleteExportedVideo: (hostName: string, videoName: string) => instanceApi.delete(`proxy/${hostName}/api/export/${videoName}`).then(res => res.data) + deleteExportedVideo: (hostName: string, videoName: string) => instanceApi.delete(`proxy/${hostName}/api/export/${videoName}`).then(res => res.data), + getHostStats: (hostName: string) => instanceApi.get(`proxy/${hostName}/api/stats`).then( res => res.data), } export const mapCamerasFromConfig = (config: FrigateConfig): string[] => { @@ -194,6 +196,7 @@ export const frigateQueryKeys = { getCameraWHost: 'camera-frigate-host', getCameraByHostId: 'camera-by-hostId', getHostConfig: 'host-config', + getHostStats: 'host-stats', getRecordingsSummary: 'recordings-frigate-summary', getRecordings: 'recordings-frigate', getEvents: 'events-frigate', diff --git a/src/shared/components/accordion/CameraAccordion.tsx b/src/shared/components/accordion/CameraAccordion.tsx index 56224ed..d2d7257 100644 --- a/src/shared/components/accordion/CameraAccordion.tsx +++ b/src/shared/components/accordion/CameraAccordion.tsx @@ -64,6 +64,9 @@ const CameraAccordion = () => { if (!data || !camera) return null + if (!isProduction) console.log('camera', camera) + if (!isProduction) console.log('data', data) + if (!isProduction) console.log('hostName', hostName) if (!isProduction) console.log('CameraAccordion rendered') return ( diff --git a/src/shared/components/buttons/AccordionShareButton.tsx b/src/shared/components/buttons/AccordionShareButton.tsx index c7ececc..22f4d44 100644 --- a/src/shared/components/buttons/AccordionShareButton.tsx +++ b/src/shared/components/buttons/AccordionShareButton.tsx @@ -17,6 +17,8 @@ const AccordionShareButton = ({ const url = recordUrl ? `${routesPath.PLAYER_PATH}?link=${encodeURIComponent(recordUrl)}` : '' const handleShare = async () => { + if (!isProduction) console.log('canShare', canShare) + if (!isProduction) console.log('shared URL', url) if (canShare && url) { try { await navigator.share({ url }); diff --git a/src/shared/components/filters/RecordingsHostFilter.tsx b/src/shared/components/filters/RecordingsHostFilter.tsx index 75576a7..6495b60 100644 --- a/src/shared/components/filters/RecordingsHostFilter.tsx +++ b/src/shared/components/filters/RecordingsHostFilter.tsx @@ -5,6 +5,7 @@ import { Context } from '../../..'; import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api'; import HostSelect from './HostSelect'; import { useTranslation } from 'react-i18next'; +import { isProduction } from '../../env.const'; const RecordingsHostFilter = () => { const { t } = useTranslation() @@ -17,6 +18,7 @@ const RecordingsHostFilter = () => { const handleSelect = (value: string) => { + if (!isProduction) console.log('handleSelect value', value) const host = hosts?.find(host => host.id === value) if (!host) { recStore.filteredHost = undefined @@ -39,11 +41,11 @@ const RecordingsHostFilter = () => { return ( ); }; diff --git a/src/shared/components/menu/HostSettingsMenu.tsx b/src/shared/components/menu/HostSettingsMenu.tsx index 8f322f4..cf01d95 100644 --- a/src/shared/components/menu/HostSettingsMenu.tsx +++ b/src/shared/components/menu/HostSettingsMenu.tsx @@ -1,9 +1,9 @@ -import { Button, Menu, rem, Text } from '@mantine/core'; -import { IconEdit, IconGraph, IconMessageCircle, IconRotateClockwise, IconServer, IconSettings } from '@tabler/icons-react'; -import React from 'react'; +import { Button, Menu, rem } from '@mantine/core'; +import { IconEdit, IconGraph, IconRotateClockwise, IconServer, IconSettings } from '@tabler/icons-react'; +import { useMutation } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { routesPath } from '../../../router/routes.path'; -import { useMutation } from '@tanstack/react-query'; import { mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api'; import { GetFrigateHost } from '../../../services/frigate.proxy/frigate.schema'; @@ -12,6 +12,7 @@ interface HostSettingsMenuProps { } const HostSettingsMenu = ({ host }: HostSettingsMenuProps) => { + const {t} = useTranslation() const navigate = useNavigate() const mutation = useMutation({ mutationFn: (hostName: string) => proxyApi.getHostRestart(hostName) @@ -44,23 +45,23 @@ const HostSettingsMenu = ({ host }: HostSettingsMenuProps) => { }> - Edit Config + {t('hostMenu.editConfig')} }> - Restart + {t('hostMenu.restart')} }> - System + {t('hostMenu.system')} }> - Storage + {t('hostMenu.storage')} diff --git a/src/shared/utils/sort.array.ts b/src/shared/utils/sort.array.ts index 07ecc01..e5a8f48 100644 --- a/src/shared/utils/sort.array.ts +++ b/src/shared/utils/sort.array.ts @@ -1,19 +1,33 @@ - /** - * Function get array and sort it by index of key - * @param uniqueValue Name of table head, to change image on it - * @param objectIndex Index of head, must be equal to idex of array object - * @param arrayData Array data - * @param reverse If you need to reverse array - * @returns uniqueValue and sorted array - */ +/** + * Function get array and sort it by index of key + * @param uniqueValue Name of table head, to change image on it + * @param objectIndex Index of head, must be equal to idex of array object + * @param arrayData Array data + * @param reverse If you need to reverse array + * @returns uniqueValue and sorted array + */ export function sortArrayByObjectIndex( - objectIndex: number, - arrayData: T[], - callBack: (arrayData: T[], key: string | number | symbol) => void, - reverse?: boolean, - ) { - if (arrayData.length === 0) throw Error('handleSort failed, array is empty') - const keys = Object.keys(arrayData[0]) - const key = keys[objectIndex] - callBack(arrayData, key as keyof T) - } \ No newline at end of file + objectIndex: number, + arrayData: T[], + callBack: (arrayData: T[], key: string | number | symbol) => void, + reverse?: boolean, +) { + if (arrayData.length === 0) throw Error('handleSort failed, array is empty') + const keys = Object.keys(arrayData[0]) + const key = keys[objectIndex] + callBack(arrayData, key as keyof T) +} + +export function sortByKey(array: T[], key: K): T[] { + return array.sort((a, b) => { + let valueA = a[key]; + let valueB = b[key]; + + const stringValueA = String(valueA).toLowerCase(); + const stringValueB = String(valueB).toLowerCase(); + + 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 new file mode 100644 index 0000000..3b42888 --- /dev/null +++ b/src/types/frigateStats.ts @@ -0,0 +1,88 @@ +export interface FrigateStats { + cameras: { + [cameraName: string]: CameraStat + } + cpu_usages: { + [processId: string]: ProcessStat + } + detection_fps: number + detectors: { + [detectorName: string]: DetectorStat + } + gpu_usages: { + [gpuName: string] : GpuStat + } + processes: Processes + service: Service +} + +export interface CameraStat { + audio_dBFS: number + audio_rms: number + camera_fps: number // Ffmpeg + capture_pid: number // Capture PID + detection_enabled: number // Detect + detection_fps: number // Detect + ffmpeg_pid: number // Ffmpeg PID + pid: number // Detect PID + process_fps: number // Capture + skipped_fps: number // Detect +} + +export interface ProcessStat { + cmdline: string + cpu: string + cpu_average: string + mem: string +} + +export interface DetectorStat { + detection_start: number + inference_speed: number + pid: number +} + +export interface GpuStat { + dec: string + enc: string + gpu: string + mem: string +} + +export interface Processes { + go2rtc: Go2rtc + logger: Logger + recording: Recording +} + +export interface Go2rtc { + pid: number +} + +export interface Logger { + pid: number +} + +export interface Recording { + pid: number +} + +export interface Service { + last_updated: number + latest_version: string + storage: { + [storagePath: string] : StorageStat + } + temperatures: Temperatures + uptime: number + version: string +} + +export interface StorageStat { + free: number + mount_type: string + total: number + used: number +} + +export interface Temperatures { } diff --git a/src/widgets/SelectedCameraList.tsx b/src/widgets/SelectedCameraList.tsx index 05ae59a..2541e85 100644 --- a/src/widgets/SelectedCameraList.tsx +++ b/src/widgets/SelectedCameraList.tsx @@ -30,7 +30,9 @@ const SelectedCameraList = () => { if (cameraPending) return if (cameraError) return - if (!camera?.frigateHost) return null + if (!camera || !camera?.frigateHost) return null + + recStore.openedCamera = camera return ( diff --git a/src/widgets/camera.stat.table/FrigateCameraStateTable.tsx b/src/widgets/camera.stat.table/FrigateCameraStateTable.tsx new file mode 100644 index 0000000..7bd4233 --- /dev/null +++ b/src/widgets/camera.stat.table/FrigateCameraStateTable.tsx @@ -0,0 +1,106 @@ +import { Table, Text } from '@mantine/core'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +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'; + + +export interface CameraItem { + cameraName: string, + process: ProcessType, + pid: number, + fps: number, + cpu: string, + mem: string, +} + +export enum ProcessType { + Ffmpeg = "Ffmpeg", + Capture = "Capture", + Detect = "Detect", +} + +interface TableHead { + propertyName: string, + title: string, + sorting?: boolean, +} + +interface TableProps { + data: T[], +} + +const FrigateCamerasStateTable = ({ + data +}: TableProps) => { + + const { t } = useTranslation() + + const [tableData, setTableData] = useState(data) + const [reversed, setReversed] = useState(false) + const [sortedName, setSortedName] = useState(null) + + + const handleSort = (headName: string, propertyName: string,) => { + const reverse = headName === sortedName ? !reversed : false; + setReversed(reverse) + const arr = sortByKey(tableData, propertyName as keyof CameraItem) + if (reverse) arr.reverse() + setTableData(arr) + setSortedName(headName) + } + + const headTitle: TableHead[] = [ + { propertyName: 'cameraName', title: t('camera') }, + { propertyName: 'process', title: t('cameraStatTable.process') }, + { propertyName: 'pid', title: t('cameraStatTable.pid'), sorting: false }, + { propertyName: 'fps', title: t('cameraStatTable.fps') }, + { propertyName: 'cpu', title: t('cameraStatTable.cpu') }, + { propertyName: 'mem', title: t('cameraStatTable.memory') }, + ] + + + const tableHead = headTitle.map(head => { + return ( + handleSort(head.title, head.propertyName ? head.propertyName : '')} + sorting={head.sorting} /> + ) + }) + + if (!isProduction) console.log('FrigateHostsTable rendered') + + const rows = tableData.map(item => { + return ( + + {item.cameraName} + {item.process} + {item.pid} + {item.fps} + {item.cpu} + {item.mem} + + ) + }) + + return ( + + + + {tableHead} + + + + {rows} + +
+ ); +}; + +export default FrigateCamerasStateTable; \ No newline at end of file diff --git a/src/widgets/FrigateHostsTable.tsx b/src/widgets/hosts.table/FrigateHostsTable.tsx similarity index 83% rename from src/widgets/FrigateHostsTable.tsx rename to src/widgets/hosts.table/FrigateHostsTable.tsx index 82d802f..fa27353 100644 --- a/src/widgets/FrigateHostsTable.tsx +++ b/src/widgets/hosts.table/FrigateHostsTable.tsx @@ -3,14 +3,15 @@ import { IconPlus, IconTrash } from '@tabler/icons-react'; import ObjectId from 'bson-objectid'; import React, { useEffect, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; -import { GetFrigateHost } from '../services/frigate.proxy/frigate.schema'; -import HostSettingsMenu from '../shared/components/menu/HostSettingsMenu'; -import SortedTh from '../shared/components/table.aps/SortedTh'; -import { isProduction } from '../shared/env.const'; -import StateCell from './hosts.table/StateCell'; -import SwitchCell from './hosts.table/SwitchCell'; -import TextInputCell from './hosts.table/TextInputCell'; +import { GetFrigateHost } from '../../services/frigate.proxy/frigate.schema'; +import HostSettingsMenu from '../../shared/components/menu/HostSettingsMenu'; +import SortedTh from '../../shared/components/table.aps/SortedTh'; +import { isProduction } from '../../shared/env.const'; +import StateCell from './StateCell'; +import SwitchCell from './SwitchCell'; +import TextInputCell from './TextInputCell'; import { useTranslation } from 'react-i18next'; +import { sortByKey } from '../../shared/utils/sort.array'; interface TableProps { data: T[], @@ -37,20 +38,6 @@ const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedC changedCallback(tableData) }, [tableData]) - function sortByKey(array: T[], key: K): T[] { - return array.sort((a, b) => { - let valueA = a[key]; - let valueB = b[key]; - - const stringValueA = String(valueA).toLowerCase(); - const stringValueB = String(valueB).toLowerCase(); - - if (stringValueA < stringValueB) return -1; - if (stringValueA > stringValueB) return 1; - return 0; - }); - } - const handleSort = (headName: string, propertyName: string,) => { const reverse = headName === sortedName ? !reversed : false; setReversed(reverse) diff --git a/src/widgets/hosts.table/StateCell.tsx b/src/widgets/hosts.table/StateCell.tsx index a2c8381..8f7d5e0 100644 --- a/src/widgets/hosts.table/StateCell.tsx +++ b/src/widgets/hosts.table/StateCell.tsx @@ -1,4 +1,4 @@ -import { Flex } from '@mantine/core'; +import { Flex, Loader } from '@mantine/core'; import { IconPower } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { frigateApi, frigateQueryKeys } from '../../services/frigate.proxy/frigate.api'; @@ -11,7 +11,7 @@ const StateCell = ({ id, width, }: StateCellProps) => { - const { data } = useQuery({ + const { data, isPending } = useQuery({ queryKey: [frigateQueryKeys.getFrigateHosts, id], queryFn: frigateApi.getHosts, staleTime: 60 * 1000, @@ -23,9 +23,13 @@ const StateCell = ({ return ( - - - + {isPending ? + + : + + + + } ); };