fix selected SelectedCameraList bug
add translate add system page and cameras stat table
This commit is contained in:
parent
516ee9bc5d
commit
a3ff2960fc
@ -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
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
const ru = {
|
const ru = {
|
||||||
|
hostMenu: {
|
||||||
|
editConfig: 'Редакт. конфиг.',
|
||||||
|
restart: 'Перезагрузка',
|
||||||
|
system: 'Система',
|
||||||
|
storage: 'Хранилище',
|
||||||
|
},
|
||||||
header: {
|
header: {
|
||||||
home: 'Главная',
|
home: 'Главная',
|
||||||
settings: 'Настройки',
|
settings: 'Настройки',
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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 });
|
||||||
|
|||||||
@ -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
|
||||||
@ -39,11 +41,11 @@ const RecordingsHostFilter = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HostSelect
|
<HostSelect
|
||||||
label={t('selectHost')}
|
label={t('selectHost')}
|
||||||
valueId={recStore.filteredHost?.id}
|
valueId={recStore.filteredHost?.id}
|
||||||
defaultId={recStore.filteredHost?.id}
|
defaultId={recStore.filteredHost?.id}
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
onSuccess={handleSuccess}
|
onSuccess={handleSuccess}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -1,19 +1,33 @@
|
|||||||
/**
|
/**
|
||||||
* Function get array and sort it by index of key
|
* Function get array and sort it by index of key
|
||||||
* @param uniqueValue Name of table head, to change image on it
|
* @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 objectIndex Index of head, must be equal to idex of array object
|
||||||
* @param arrayData Array data
|
* @param arrayData Array data
|
||||||
* @param reverse If you need to reverse array
|
* @param reverse If you need to reverse array
|
||||||
* @returns uniqueValue and sorted array
|
* @returns uniqueValue and sorted array
|
||||||
*/
|
*/
|
||||||
export function sortArrayByObjectIndex<T extends object>(
|
export function sortArrayByObjectIndex<T extends object>(
|
||||||
objectIndex: number,
|
objectIndex: number,
|
||||||
arrayData: T[],
|
arrayData: T[],
|
||||||
callBack: (arrayData: T[], key: string | number | symbol) => void,
|
callBack: (arrayData: T[], key: string | number | symbol) => void,
|
||||||
reverse?: boolean,
|
reverse?: boolean,
|
||||||
) {
|
) {
|
||||||
if (arrayData.length === 0) throw Error('handleSort failed, array is empty')
|
if (arrayData.length === 0) throw Error('handleSort failed, array is empty')
|
||||||
const keys = Object.keys(arrayData[0])
|
const keys = Object.keys(arrayData[0])
|
||||||
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
88
src/types/frigateStats.ts
Normal 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 { }
|
||||||
@ -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'>
|
||||||
|
|||||||
106
src/widgets/camera.stat.table/FrigateCameraStateTable.tsx
Normal file
106
src/widgets/camera.stat.table/FrigateCameraStateTable.tsx
Normal 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;
|
||||||
@ -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)
|
||||||
@ -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 }}>
|
||||||
<Flex w='100%' justify='center'>
|
{isPending ?
|
||||||
<IconPower color={state ? 'green' : 'red'}/>
|
<Loader size='sm'/>
|
||||||
</Flex>
|
:
|
||||||
|
<Flex w='100%' justify='center'>
|
||||||
|
<IconPower color={state ? 'green' : 'red'} />
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user