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
|
||||
# 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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
const ru = {
|
||||
hostMenu: {
|
||||
editConfig: 'Редакт. конфиг.',
|
||||
restart: 'Перезагрузка',
|
||||
system: 'Система',
|
||||
storage: 'Хранилище',
|
||||
},
|
||||
header: {
|
||||
home: 'Главная',
|
||||
settings: 'Настройки',
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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 <Forbidden />
|
||||
if (isPending) return <CenterLoader />
|
||||
if (isError) return <RetryErrorPage onRetry={refetch} />
|
||||
if (!paramHostId || !data) return null
|
||||
|
||||
const mappedCameraStat: CameraItem[] = mapCameraData()
|
||||
|
||||
return (
|
||||
<div>
|
||||
System Page - NOT YET IMPLEMENTED
|
||||
</div>
|
||||
<Flex w='100%' h='100%'>
|
||||
<FrigateCamerasStateTable data={mappedCameraStat} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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<GetExportedFile[]>(`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<FrigateStats>(`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',
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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 (
|
||||
<HostSelect
|
||||
label={t('selectHost')}
|
||||
valueId={recStore.filteredHost?.id}
|
||||
defaultId={recStore.filteredHost?.id}
|
||||
onChange={handleSelect}
|
||||
onSuccess={handleSuccess}
|
||||
label={t('selectHost')}
|
||||
valueId={recStore.filteredHost?.id}
|
||||
defaultId={recStore.filteredHost?.id}
|
||||
onChange={handleSelect}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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) => {
|
||||
<Menu.Item
|
||||
onClick={handleConfig}
|
||||
icon={<IconEdit style={{ width: rem(14), height: rem(14) }} />}>
|
||||
Edit Config
|
||||
{t('hostMenu.editConfig')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={handleRestart}
|
||||
icon={<IconRotateClockwise style={{ width: rem(14), height: rem(14) }} />}>
|
||||
Restart
|
||||
{t('hostMenu.restart')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={handleSystem}
|
||||
icon={<IconGraph style={{ width: rem(14), height: rem(14) }} />}>
|
||||
System
|
||||
{t('hostMenu.system')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={handleStorage}
|
||||
icon={<IconServer
|
||||
style={{ width: rem(14), height: rem(14) }} />}>
|
||||
Storage
|
||||
{t('hostMenu.storage')}
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
@ -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<T extends object>(
|
||||
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)
|
||||
}
|
||||
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<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 (cameraError) return <RetryErrorPage onRetry={handleRetry} />
|
||||
|
||||
if (!camera?.frigateHost) return null
|
||||
if (!camera || !camera?.frigateHost) return null
|
||||
|
||||
recStore.openedCamera = camera
|
||||
|
||||
return (
|
||||
<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 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<T> {
|
||||
data: T[],
|
||||
@ -37,20 +38,6 @@ const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedC
|
||||
changedCallback(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 reverse = headName === sortedName ? !reversed : false;
|
||||
setReversed(reverse)
|
||||
@ -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 (
|
||||
<td style={{ width: width }}>
|
||||
<Flex w='100%' justify='center'>
|
||||
<IconPower color={state ? 'green' : 'red'}/>
|
||||
</Flex>
|
||||
{isPending ?
|
||||
<Loader size='sm'/>
|
||||
:
|
||||
<Flex w='100%' justify='center'>
|
||||
<IconPower color={state ? 'green' : 'red'} />
|
||||
</Flex>
|
||||
}
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user