fix selected SelectedCameraList bug

add translate
add system page and cameras stat table
This commit is contained in:
NlightN22 2024-03-11 00:03:52 +07:00
parent 516ee9bc5d
commit a3ff2960fc
16 changed files with 355 additions and 66 deletions

View File

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Build commands: # Build commands:
# - $VERSION=0.7 # - $VERSION=0.8
# - rm build -r -Force ; rm ./node_modules/.cache/babel-loader -r -Force ; yarn build # - 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 build --pull --rm -t oncharterliz/multi-frigate:latest -t oncharterliz/multi-frigate:$VERSION "."
# - docker image push --all-tags oncharterliz/multi-frigate # - docker image push --all-tags oncharterliz/multi-frigate

View File

@ -1,11 +1,25 @@
const en = { const en = {
cameraStatTable: {
process: 'Process',
pid: 'PID',
fps: 'FPS',
cpu: 'CPU %',
memory: 'Memory %'
},
hostMenu: {
editConfig: 'Edit config',
restart: 'Restart',
system: 'System',
storage: 'Storage',
},
header: { header: {
home: 'Main', home: 'Main',
settings: 'Settings', settings: 'Settings',
recordings: 'Recordings', recordings: 'Recordings',
hostsConfig: 'Frigate servers', hostsConfig: 'Frigate servers',
acessSettings: 'Access settings', acessSettings: 'Access settings',
}, hostArr: { },
hostArr: {
host: 'Host', host: 'Host',
name: 'Host name', name: 'Host name',
url: 'Address', url: 'Address',

View File

@ -1,4 +1,10 @@
const ru = { const ru = {
hostMenu: {
editConfig: 'Редакт. конфиг.',
restart: 'Перезагрузка',
system: 'Система',
storage: 'Хранилище',
},
header: { header: {
home: 'Главная', home: 'Главная',
settings: 'Настройки', settings: 'Настройки',

View File

@ -11,7 +11,7 @@ import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.
import { GetFrigateHost, deleteFrigateHostSchema, putFrigateHostSchema } from '../services/frigate.proxy/frigate.schema'; import { GetFrigateHost, deleteFrigateHostSchema, putFrigateHostSchema } from '../services/frigate.proxy/frigate.schema';
import CenterLoader from '../shared/components/loaders/CenterLoader'; import CenterLoader from '../shared/components/loaders/CenterLoader';
import { isProduction } from '../shared/env.const'; import { isProduction } from '../shared/env.const';
import FrigateHostsTable from '../widgets/FrigateHostsTable'; import FrigateHostsTable from '../widgets/hosts.table/FrigateHostsTable';
import Forbidden from './403'; import Forbidden from './403';
import RetryErrorPage from './RetryErrorPage'; import RetryErrorPage from './RetryErrorPage';

View File

@ -1,8 +1,19 @@
import React, { useContext, useEffect, useRef } from 'react'; import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
import { Context } from '..'; import { Context } from '..';
import { useAdminRole } from '../hooks/useAdminRole'; import { useAdminRole } from '../hooks/useAdminRole';
import Forbidden from './403'; import Forbidden from './403';
import { observer } from 'mobx-react-lite'; 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 HostSystemPage = () => {
const executed = useRef(false) const executed = useRef(false)
@ -18,12 +29,58 @@ const HostSystemPage = () => {
} }
}, [sideBarsStore]) }, [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 <Forbidden /> if (!isAdmin) return <Forbidden />
if (isPending) return <CenterLoader />
if (isError) return <RetryErrorPage onRetry={refetch} />
if (!paramHostId || !data) return null
const mappedCameraStat: CameraItem[] = mapCameraData()
return ( return (
<div> <Flex w='100%' h='100%'>
System Page - NOT YET IMPLEMENTED <FrigateCamerasStateTable data={mappedCameraStat} />
</div> </Flex>
); );
}; };

View File

@ -10,6 +10,7 @@ import { RecordSummary } from "../../types/record";
import { EventFrigate } from "../../types/event"; import { EventFrigate } from "../../types/event";
import { keycloakConfig } from "../.."; import { keycloakConfig } from "../..";
import { getResolvedTimeZone } from "../../shared/utils/dateUtil"; import { getResolvedTimeZone } from "../../shared/utils/dateUtil";
import { FrigateStats } from "../../types/frigateStats";
export const getToken = (): string | undefined => { export const getToken = (): string | undefined => {
@ -171,7 +172,8 @@ export const proxyApi = {
getExportedVideoList: (hostName: string) => instanceApi.get<GetExportedFile[]>(`proxy/${hostName}/exports/`).then(res => res.data), getExportedVideoList: (hostName: string) => instanceApi.get<GetExportedFile[]>(`proxy/${hostName}/exports/`).then(res => res.data),
getVideoUrl: (hostName: string, fileName: string) => `${proxyPrefix}${hostName}/exports/${fileName}`, 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 // 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<FrigateStats>(`proxy/${hostName}/api/stats`).then( res => res.data),
} }
export const mapCamerasFromConfig = (config: FrigateConfig): string[] => { export const mapCamerasFromConfig = (config: FrigateConfig): string[] => {
@ -194,6 +196,7 @@ export const frigateQueryKeys = {
getCameraWHost: 'camera-frigate-host', getCameraWHost: 'camera-frigate-host',
getCameraByHostId: 'camera-by-hostId', getCameraByHostId: 'camera-by-hostId',
getHostConfig: 'host-config', getHostConfig: 'host-config',
getHostStats: 'host-stats',
getRecordingsSummary: 'recordings-frigate-summary', getRecordingsSummary: 'recordings-frigate-summary',
getRecordings: 'recordings-frigate', getRecordings: 'recordings-frigate',
getEvents: 'events-frigate', getEvents: 'events-frigate',

View File

@ -64,6 +64,9 @@ const CameraAccordion = () => {
if (!data || !camera) return null 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') if (!isProduction) console.log('CameraAccordion rendered')
return ( return (

View File

@ -17,6 +17,8 @@ const AccordionShareButton = ({
const url = recordUrl ? `${routesPath.PLAYER_PATH}?link=${encodeURIComponent(recordUrl)}` : '' const url = recordUrl ? `${routesPath.PLAYER_PATH}?link=${encodeURIComponent(recordUrl)}` : ''
const handleShare = async () => { const handleShare = async () => {
if (!isProduction) console.log('canShare', canShare)
if (!isProduction) console.log('shared URL', url)
if (canShare && url) { if (canShare && url) {
try { try {
await navigator.share({ url }); await navigator.share({ url });

View File

@ -5,6 +5,7 @@ import { Context } from '../../..';
import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api'; import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api';
import HostSelect from './HostSelect'; import HostSelect from './HostSelect';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { isProduction } from '../../env.const';
const RecordingsHostFilter = () => { const RecordingsHostFilter = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -17,6 +18,7 @@ const RecordingsHostFilter = () => {
const handleSelect = (value: string) => { const handleSelect = (value: string) => {
if (!isProduction) console.log('handleSelect value', value)
const host = hosts?.find(host => host.id === value) const host = hosts?.find(host => host.id === value)
if (!host) { if (!host) {
recStore.filteredHost = undefined recStore.filteredHost = undefined

View File

@ -1,9 +1,9 @@
import { Button, Menu, rem, Text } from '@mantine/core'; import { Button, Menu, rem } from '@mantine/core';
import { IconEdit, IconGraph, IconMessageCircle, IconRotateClockwise, IconServer, IconSettings } from '@tabler/icons-react'; import { IconEdit, IconGraph, IconRotateClockwise, IconServer, IconSettings } from '@tabler/icons-react';
import React from 'react'; import { useMutation } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { routesPath } from '../../../router/routes.path'; import { routesPath } from '../../../router/routes.path';
import { useMutation } from '@tanstack/react-query';
import { mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api'; import { mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
import { GetFrigateHost } from '../../../services/frigate.proxy/frigate.schema'; import { GetFrigateHost } from '../../../services/frigate.proxy/frigate.schema';
@ -12,6 +12,7 @@ interface HostSettingsMenuProps {
} }
const HostSettingsMenu = ({ host }: HostSettingsMenuProps) => { const HostSettingsMenu = ({ host }: HostSettingsMenuProps) => {
const {t} = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (hostName: string) => proxyApi.getHostRestart(hostName) mutationFn: (hostName: string) => proxyApi.getHostRestart(hostName)
@ -44,23 +45,23 @@ const HostSettingsMenu = ({ host }: HostSettingsMenuProps) => {
<Menu.Item <Menu.Item
onClick={handleConfig} onClick={handleConfig}
icon={<IconEdit style={{ width: rem(14), height: rem(14) }} />}> icon={<IconEdit style={{ width: rem(14), height: rem(14) }} />}>
Edit Config {t('hostMenu.editConfig')}
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
onClick={handleRestart} onClick={handleRestart}
icon={<IconRotateClockwise style={{ width: rem(14), height: rem(14) }} />}> icon={<IconRotateClockwise style={{ width: rem(14), height: rem(14) }} />}>
Restart {t('hostMenu.restart')}
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
onClick={handleSystem} onClick={handleSystem}
icon={<IconGraph style={{ width: rem(14), height: rem(14) }} />}> icon={<IconGraph style={{ width: rem(14), height: rem(14) }} />}>
System {t('hostMenu.system')}
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
onClick={handleStorage} onClick={handleStorage}
icon={<IconServer icon={<IconServer
style={{ width: rem(14), height: rem(14) }} />}> style={{ width: rem(14), height: rem(14) }} />}>
Storage {t('hostMenu.storage')}
</Menu.Item> </Menu.Item>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>

View File

@ -17,3 +17,17 @@ export function sortArrayByObjectIndex<T extends object>(
const key = keys[objectIndex] const key = keys[objectIndex]
callBack(arrayData, key as keyof T) callBack(arrayData, key as keyof T)
} }
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];
const stringValueA = String(valueA).toLowerCase();
const stringValueB = String(valueB).toLowerCase();
if (stringValueA < stringValueB) return -1;
if (stringValueA > stringValueB) return 1;
return 0;
});
}

88
src/types/frigateStats.ts Normal file
View File

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

View File

@ -30,7 +30,9 @@ const SelectedCameraList = () => {
if (cameraPending) return <CenterLoader /> if (cameraPending) return <CenterLoader />
if (cameraError) return <RetryErrorPage onRetry={handleRetry} /> if (cameraError) return <RetryErrorPage onRetry={handleRetry} />
if (!camera?.frigateHost) return null if (!camera || !camera?.frigateHost) return null
recStore.openedCamera = camera
return ( return (
<Flex w='100%' h='100%' direction='column' align='center'> <Flex w='100%' h='100%' direction='column' align='center'>

View File

@ -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<T> {
data: T[],
}
const FrigateCamerasStateTable = ({
data
}: TableProps<CameraItem>) => {
const { t } = useTranslation()
const [tableData, setTableData] = useState(data)
const [reversed, setReversed] = useState(false)
const [sortedName, setSortedName] = useState<string | null>(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 (
<SortedTh
key={uuidv4()}
title={head.title}
reversed={reversed}
sortedName={sortedName}
onSort={() => handleSort(head.title, head.propertyName ? head.propertyName : '')}
sorting={head.sorting} />
)
})
if (!isProduction) console.log('FrigateHostsTable rendered')
const rows = tableData.map(item => {
return (
<tr key={item.cameraName + item.process}>
<td><Text align='center'>{item.cameraName}</Text></td>
<td><Text align='center'>{item.process}</Text></td>
<td><Text align='center'>{item.pid}</Text></td>
<td><Text align='center'>{item.fps}</Text></td>
<td><Text align='center'>{item.cpu}</Text></td>
<td><Text align='center'>{item.mem}</Text></td>
</tr>
)
})
return (
<Table >
<thead>
<tr>
{tableHead}
</tr>
</thead>
<tbody>
{rows}
</tbody>
</Table>
);
};
export default FrigateCamerasStateTable;

View File

@ -3,14 +3,15 @@ import { IconPlus, IconTrash } from '@tabler/icons-react';
import ObjectId from 'bson-objectid'; import ObjectId from 'bson-objectid';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { GetFrigateHost } from '../services/frigate.proxy/frigate.schema'; import { GetFrigateHost } from '../../services/frigate.proxy/frigate.schema';
import HostSettingsMenu from '../shared/components/menu/HostSettingsMenu'; import HostSettingsMenu from '../../shared/components/menu/HostSettingsMenu';
import SortedTh from '../shared/components/table.aps/SortedTh'; import SortedTh from '../../shared/components/table.aps/SortedTh';
import { isProduction } from '../shared/env.const'; import { isProduction } from '../../shared/env.const';
import StateCell from './hosts.table/StateCell'; import StateCell from './StateCell';
import SwitchCell from './hosts.table/SwitchCell'; import SwitchCell from './SwitchCell';
import TextInputCell from './hosts.table/TextInputCell'; import TextInputCell from './TextInputCell';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { sortByKey } from '../../shared/utils/sort.array';
interface TableProps<T> { interface TableProps<T> {
data: T[], data: T[],
@ -37,20 +38,6 @@ const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedC
changedCallback(tableData) changedCallback(tableData)
}, [tableData]) }, [tableData])
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];
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 handleSort = (headName: string, propertyName: string,) => {
const reverse = headName === sortedName ? !reversed : false; const reverse = headName === sortedName ? !reversed : false;
setReversed(reverse) setReversed(reverse)

View File

@ -1,4 +1,4 @@
import { Flex } from '@mantine/core'; import { Flex, Loader } from '@mantine/core';
import { IconPower } from '@tabler/icons-react'; import { IconPower } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { frigateApi, frigateQueryKeys } from '../../services/frigate.proxy/frigate.api'; import { frigateApi, frigateQueryKeys } from '../../services/frigate.proxy/frigate.api';
@ -11,7 +11,7 @@ const StateCell = ({
id, id,
width, width,
}: StateCellProps) => { }: StateCellProps) => {
const { data } = useQuery({ const { data, isPending } = useQuery({
queryKey: [frigateQueryKeys.getFrigateHosts, id], queryKey: [frigateQueryKeys.getFrigateHosts, id],
queryFn: frigateApi.getHosts, queryFn: frigateApi.getHosts,
staleTime: 60 * 1000, staleTime: 60 * 1000,
@ -23,9 +23,13 @@ const StateCell = ({
return ( return (
<td style={{ width: width }}> <td style={{ width: width }}>
{isPending ?
<Loader size='sm'/>
:
<Flex w='100%' justify='center'> <Flex w='100%' justify='center'>
<IconPower color={state ? 'green' : 'red'} /> <IconPower color={state ? 'green' : 'red'} />
</Flex> </Flex>
}
</td> </td>
); );
}; };