add save config

This commit is contained in:
NlightN22 2024-03-12 04:07:17 +07:00
parent 342940d27f
commit 529885498d
18 changed files with 325 additions and 233 deletions

View File

@ -9,7 +9,8 @@
"@mantine/core": "^6.0.16", "@mantine/core": "^6.0.16",
"@mantine/dates": "^6.0.16", "@mantine/dates": "^6.0.16",
"@mantine/hooks": "^6.0.16", "@mantine/hooks": "^6.0.16",
"@mantine/notifications": "6.0.16", "@mantine/modals": "^6.0.16",
"@mantine/notifications": "^6.0.16",
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@tabler/icons-react": "^2.24.0", "@tabler/icons-react": "^2.24.0",
"@tanstack/react-query": "^5.21.2", "@tanstack/react-query": "^5.21.2",

View File

@ -11,6 +11,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import RetryErrorPage from './pages/RetryErrorPage'; import RetryErrorPage from './pages/RetryErrorPage';
import { keycloakConfig } from '.'; import { keycloakConfig } from '.';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { ModalsProvider } from '@mantine/modals';
import { FfprobeModal } from './shared/components/modal.windows/FfprobeModal';
import { VaInfoModal } from './shared/components/modal.windows/VaInfoModal';
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@ -20,6 +23,17 @@ const queryClient = new QueryClient({
} }
}) })
const modals = {
ffprobeModal: FfprobeModal,
vaInfoModal: VaInfoModal,
}
declare module '@mantine/modals' {
export interface MantineModalsOverride {
modals: typeof modals;
}
}
function App() { function App() {
const maxErrorAuthCounts = 2 const maxErrorAuthCounts = 2
const systemColorScheme = useColorScheme() const systemColorScheme = useColorScheme()
@ -104,8 +118,10 @@ function App() {
} }
}} }}
> >
<Notifications /> <ModalsProvider modals={modals}>
<AppBody /> <Notifications />
<AppBody />
</ModalsProvider>
</MantineProvider > </MantineProvider >
</ColorSchemeProvider> </ColorSchemeProvider>
</div> </div>

View File

@ -1,9 +1,9 @@
import React, { useCallback, useContext, useEffect, useRef } from 'react'; import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Context } from '..'; import { Context } from '..';
import { useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { frigateApi, frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api'; import { frigateApi, frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
import { Button, Flex, useMantineTheme } from '@mantine/core'; import { Button, Flex, useMantineTheme, Text } from '@mantine/core';
import { useClipboard } from '@mantine/hooks'; import { useClipboard } from '@mantine/hooks';
import Editor, { Monaco } from '@monaco-editor/react' import Editor, { Monaco } from '@monaco-editor/react'
import * as monaco from "monaco-editor"; import * as monaco from "monaco-editor";
@ -13,11 +13,16 @@ 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 { isProduction } from '../shared/env.const'; import { isProduction } from '../shared/env.const';
import { SaveOption } from '../types/saveConfig';
import { GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
import { error } from 'console';
const HostConfigPage = () => { const HostConfigPage = () => {
const executed = useRef(false) const executed = useRef(false)
const host = useRef<GetFrigateHost | undefined>()
const { sideBarsStore } = useContext(Context) const { sideBarsStore } = useContext(Context)
const [saveMessage, setSaveMessage] = useState<string>()
let { id } = useParams<'id'>() let { id } = useParams<'id'>()
const { isAdmin, isLoading: adminLoading } = useAdminRole() const { isAdmin, isLoading: adminLoading } = useAdminRole()
@ -25,22 +30,42 @@ const HostConfigPage = () => {
const { isPending: configPending, error: configError, data: config, refetch } = useQuery({ const { isPending: configPending, error: configError, data: config, refetch } = useQuery({
queryKey: [frigateQueryKeys.getFrigateHost, id], queryKey: [frigateQueryKeys.getFrigateHost, id],
queryFn: async () => { queryFn: async () => {
const host = await frigateApi.getHost(id || '') host.current = await frigateApi.getHost(id || '')
const hostName = mapHostToHostname(host) const hostName = mapHostToHostname(host.current)
if (hostName) if (!hostName) return null
return proxyApi.getHostConfigRaw(hostName) return proxyApi.getHostConfigRaw(hostName)
return null
}, },
}) })
const { mutate: saveConfig } = useMutation({
mutationKey: [frigateQueryKeys.postHostConfig],
mutationFn: ({ saveOption, config }: { saveOption: SaveOption, config: string }) => {
const hostName = mapHostToHostname(host.current)
if (!hostName || !editorRef.current) return Promise.resolve(null)
return proxyApi.postHostConfig(hostName, saveOption, config)
.catch(error => {
if (error.response && error.response.data) {
return Promise.reject(error.response.data)
}
return Promise.reject(error)
})
},
onSuccess: (data) => {
setSaveMessage(data?.message)
},
onError: (error) => {
setSaveMessage(error.message)
}
})
useEffect(() => { useEffect(() => {
if (!executed.current) { if (!executed.current) {
sideBarsStore.rightVisible = false sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null) sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null) sideBarsStore.setRightChildren(null)
executed.current = true executed.current = true
} }
}, [sideBarsStore]) }, [sideBarsStore])
const clipboard = useClipboard({ timeout: 500 }) const clipboard = useClipboard({ timeout: 500 })
@ -81,11 +106,13 @@ const HostConfigPage = () => {
}, [editorRef, clipboard]); }, [editorRef, clipboard]);
const onHandleSaveConfig = useCallback( const onHandleSaveConfig = useCallback(
async (save_option: string) => { async (saveOption: SaveOption) => {
if (!editorRef.current) { if (!editorRef.current) {
return; throw Error('Editor does not exists')
} }
if (!isProduction) console.log('save config', save_option) if (!isProduction) console.log('saveOption', saveOption)
if (!isProduction) console.log('editorRef.current', editorRef.current.getValue().slice(0, 50))
saveConfig({ saveOption: saveOption, config: editorRef.current.getValue() })
}, [editorRef]) }, [editorRef])
if (configPending || adminLoading) return <CenterLoader /> if (configPending || adminLoading) return <CenterLoader />
@ -99,14 +126,19 @@ const HostConfigPage = () => {
<Button size="sm" onClick={handleCopyConfig}> <Button size="sm" onClick={handleCopyConfig}>
Copy Config Copy Config
</Button> </Button>
<Button ml='1rem' size="sm" onClick={(_) => onHandleSaveConfig("restart")}> <Button ml='1rem' size="sm" onClick={(_) => onHandleSaveConfig(SaveOption.SaveRestart)}>
Save & Restart Save & Restart
</Button> </Button>
<Button ml='1rem' size="sm" onClick={(_) => onHandleSaveConfig("saveonly")}> <Button ml='1rem' size="sm" onClick={(_) => onHandleSaveConfig(SaveOption.SaveOnly)}>
Save Only Save Only
</Button> </Button>
</Flex> </Flex>
<Flex h='100%'> {!saveMessage ? null :
<Flex w='100%' justify='center' wrap='nowrap' mt='1rem'>
<Text>{saveMessage}</Text>
</Flex>
}
<Flex h='100%' mt='1rem'>
<Editor <Editor
defaultLanguage='yaml' defaultLanguage='yaml'
value={config} value={config}

View File

@ -1,21 +1,24 @@
import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; import { Flex, Grid, Text } from '@mantine/core';
import { useQuery } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';
import { useCallback, useContext, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { Context } from '..'; import { Context } from '..';
import { useAdminRole } from '../hooks/useAdminRole'; 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 { frigateApi, frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
import { GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
import CenterLoader from '../shared/components/loaders/CenterLoader'; import CenterLoader from '../shared/components/loaders/CenterLoader';
import RetryErrorPage from './RetryErrorPage';
import { Flex, Grid, Text } from '@mantine/core';
import FrigateCamerasStateTable, { CameraItem, ProcessType } from '../widgets/camera.stat.table/FrigateCameraStateTable';
import StorageRingStat from '../shared/components/stats/StorageRingStat';
import { useTranslation } from 'react-i18next';
import { formatUptime } from '../shared/utils/dateUtil';
import GpuStat from '../shared/components/stats/GpuStat';
import { v4 } from 'uuid';
import DetectorsStat from '../shared/components/stats/DetectorsStat'; import DetectorsStat from '../shared/components/stats/DetectorsStat';
import GpuStat from '../shared/components/stats/GpuStat';
import StorageRingStat from '../shared/components/stats/StorageRingStat';
import { isProduction } from '../shared/env.const';
import { formatUptime } from '../shared/utils/dateUtil';
import FrigateCamerasStateTable, { CameraItem, ProcessType } from '../widgets/camera.stat.table/FrigateCameraStateTable';
import Forbidden from './403';
import RetryErrorPage from './RetryErrorPage';
import { openContextModal } from '@mantine/modals';
import { FfprobeModalProps } from '../shared/components/modal.windows/FfprobeModal';
export const hostSystemPageQuery = { export const hostSystemPageQuery = {
hostId: 'hostId', hostId: 'hostId',
@ -26,6 +29,7 @@ const HostSystemPage = () => {
const executed = useRef(false) const executed = useRef(false)
const { sideBarsStore } = useContext(Context) const { sideBarsStore } = useContext(Context)
const { isAdmin } = useAdminRole() const { isAdmin } = useAdminRole()
const host = useRef<GetFrigateHost | undefined>()
useEffect(() => { useEffect(() => {
if (!executed.current) { if (!executed.current) {
@ -42,11 +46,13 @@ const HostSystemPage = () => {
queryKey: [frigateQueryKeys.getHostStats, paramHostId], queryKey: [frigateQueryKeys.getHostStats, paramHostId],
queryFn: async () => { queryFn: async () => {
if (!paramHostId) return null if (!paramHostId) return null
const host = await frigateApi.getHost(paramHostId) host.current = await frigateApi.getHost(paramHostId)
const hostName = mapHostToHostname(host) const hostName = mapHostToHostname(host.current)
if (!hostName) return null if (!hostName) return null
return proxyApi.getHostStats(hostName) return proxyApi.getHostStats(hostName)
} },
staleTime: 2 * 60 * 1000,
refetchInterval: 60 * 1000,
}) })
const mapCameraData = useCallback(() => { const mapCameraData = useCallback(() => {
@ -72,9 +78,9 @@ const HostSystemPage = () => {
}); });
}, [data]); }, [data]);
if (!isAdmin) return <Forbidden />
if (isPending) return <CenterLoader /> if (isPending) return <CenterLoader />
if (isError) return <RetryErrorPage onRetry={refetch} /> if (isError) return <RetryErrorPage onRetry={refetch} />
if (!isAdmin) return <Forbidden />
if (!paramHostId || !data) return null if (!paramHostId || !data) return null
const mappedCameraStat: CameraItem[] = mapCameraData() const mappedCameraStat: CameraItem[] = mapCameraData()
@ -97,6 +103,14 @@ const HostSystemPage = () => {
return `${time.value.toFixed(1)} ${translatedUnit}` return `${time.value.toFixed(1)} ${translatedUnit}`
} }
const handleVaInfoClick = () => openContextModal({
modal: 'vaInfoModal',
title: 'VaInfo',
innerProps: {
hostName: mapHostToHostname(host.current)
}
})
const gpuStats = Object.entries(data.gpu_usages).map(([name, stats]) => { const gpuStats = Object.entries(data.gpu_usages).map(([name, stats]) => {
return ( return (
<Grid.Col key={name + stats.gpu} xs={7} sm={6} md={5} lg={4} p='0.2rem'> <Grid.Col key={name + stats.gpu} xs={7} sm={6} md={5} lg={4} p='0.2rem'>
@ -105,7 +119,9 @@ const HostSystemPage = () => {
decoder={stats.dec} decoder={stats.dec}
encoder={stats.enc} encoder={stats.enc}
gpu={stats.gpu} gpu={stats.gpu}
mem={stats.mem} /> mem={stats.mem}
onVaInfoClick={() => handleVaInfoClick()}
/>
</Grid.Col> </Grid.Col>
) )
}) })
@ -127,10 +143,22 @@ const HostSystemPage = () => {
) )
}) })
const handleFfprobeClick = (cameraName: string) => openContextModal({
modal: 'ffprobeModal',
title: 'Ffprobe',
innerProps: {
hostName: mapHostToHostname(host.current),
cameraName: cameraName
}
})
if (!isProduction) console.log('HostSystemPage rendered')
return ( return (
<Flex w='100%' h='100%' direction='column'> <Flex w='100%' h='100%' direction='column'>
<Flex w='100%' justify='space-around'> <Flex w='100%' justify='space-around' align='baseline'>
<Text>{t('version')} : {data.service.version}</Text> <Text>{t('version')} : {data.service.version}</Text>
<Text size='xl' w='900'>{host.current?.name}</Text>
<Text>{t('uptime')} : {formattedUptime()}</Text> <Text>{t('uptime')} : {formattedUptime()}</Text>
</Flex> </Flex>
<Grid mt='sm' justify="center" mb='sm' align='stretch'> <Grid mt='sm' justify="center" mb='sm' align='stretch'>
@ -140,7 +168,7 @@ const HostSystemPage = () => {
{gpuStats} {gpuStats}
{detectorsStats} {detectorsStats}
</Grid> </Grid>
<FrigateCamerasStateTable data={mappedCameraStat} /> <FrigateCamerasStateTable data={mappedCameraStat} onFfprobeClick={handleFfprobeClick} />
</Flex> </Flex>
); );
}; };

View File

@ -8,7 +8,6 @@ import { observer } from 'mobx-react-lite';
export const playRecordPageQuery = { export const playRecordPageQuery = {
link: 'link', link: 'link',
// hostName: 'hostName',
} }
const PlayRecordPage = () => { const PlayRecordPage = () => {
@ -26,7 +25,6 @@ const PlayRecordPage = () => {
const location = useLocation() const location = useLocation()
const queryParams = new URLSearchParams(location.search) const queryParams = new URLSearchParams(location.search)
const paramLink = queryParams.get(playRecordPageQuery.link) const paramLink = queryParams.get(playRecordPageQuery.link)
// const paramHostName = queryParams.get(playRecordPageQuery.hostName);
if (!paramLink) return (<NotFound />) if (!paramLink) return (<NotFound />)
return ( return (

View File

@ -96,7 +96,6 @@ const RecordingsPage = () => {
navigate({ pathname: location.pathname, search: queryParams.toString() }); navigate({ pathname: location.pathname, search: queryParams.toString() });
}, [recStore.selectedRange, location.pathname, navigate, queryParams]) }, [recStore.selectedRange, location.pathname, navigate, queryParams])
if (!isProduction) console.log('RecordingsPage rendered')
const [startDay, endDay] = period const [startDay, endDay] = period
if (startDay && endDay) { if (startDay && endDay) {
@ -115,6 +114,8 @@ const RecordingsPage = () => {
return <SelectedHostList hostId={hostId} /> return <SelectedHostList hostId={hostId} />
} }
if (!isProduction) console.log('RecordingsPage rendered')
return ( return (
<Flex w='100%' h='100%' direction='column' justify='center' align='center'> <Flex w='100%' h='100%' direction='column' justify='center' align='center'>
{!hostId ? {!hostId ?

View File

@ -10,7 +10,9 @@ 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"; import { FrigateStats, GetFfprobe, GetVaInfo } from "../../types/frigateStats";
import { hostname } from "os";
import { PostSaveConfig, SaveOption } from "../../types/saveConfig";
export const getToken = (): string | undefined => { export const getToken = (): string | undefined => {
@ -176,7 +178,19 @@ export const proxyApi = {
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), getHostStats: (hostName: string) => instanceApi.get<FrigateStats>(`proxy/${hostName}/api/stats`).then(res => res.data),
getCameraFfprobe: (hostName: string, cameraName: string) =>
instanceApi.get<GetFfprobe[]>(`proxy/${hostName}/api/ffprobe`, { params: { paths: `camera:${cameraName}` } }).then(res => res.data),
getHostVaInfo: (hostName: string) => instanceApi.get<GetVaInfo>(`proxy/${hostName}/api/vainfo`).then(res => res.data),
postHostConfig: (hostName: string, saveOption: SaveOption, config: string) =>
instanceApi.post<PostSaveConfig>(`proxy/${hostName}/api/config/save`, config, {
headers: {
'Content-Type': 'text/plain'
},
params: {
save_option: saveOption
}
}).then(res => res.data),
} }
export const mapCamerasFromConfig = (config: FrigateConfig): string[] => { export const mapCamerasFromConfig = (config: FrigateConfig): string[] => {
@ -199,7 +213,10 @@ 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',
postHostConfig: 'host-config-save',
getHostStats: 'host-stats', getHostStats: 'host-stats',
getCameraFfprobe: 'camera-ffprobe',
getHostVaInfo: 'host-vainfo',
getRecordingsSummary: 'recordings-frigate-summary', getRecordingsSummary: 'recordings-frigate-summary',
getRecordings: 'recordings-frigate', getRecordings: 'recordings-frigate',
getEvents: 'events-frigate', getEvents: 'events-frigate',

View File

@ -0,0 +1,62 @@
import { useQuery } from "@tanstack/react-query"
import { frigateQueryKeys, proxyApi } from "../../../services/frigate.proxy/frigate.api"
import CogwheelLoader from "../loaders/CogwheelLoader"
import RetryError from "../RetryError"
import { Button, Center, Text } from "@mantine/core"
import { ContextModalProps } from "@mantine/modals"
export interface FfprobeModalProps {
hostName?: string
cameraName: string
}
export const FfprobeModal = ({ context, id, innerProps }: ContextModalProps<FfprobeModalProps>) => {
const { hostName, cameraName } = innerProps
const { data, isError, isPending, refetch } = useQuery({
queryKey: [frigateQueryKeys.getCameraFfprobe, hostName, cameraName],
queryFn: () => {
if (!hostName) return null
return proxyApi.getCameraFfprobe(hostName, cameraName)
}
})
if (isPending) return <CogwheelLoader />
if (isError) return <RetryError onRetry={refetch} />
if (!data || data.length < 1) return <Text>Data is empty</Text>
const streamItems = data.map((res, streamIndex) => {
if (res.return_code !== 0) {
return (
<>
<Center><Text weight={700}>Stream: {streamIndex}</Text></Center>
<Text>{res.return_code}</Text>
<Text>{res.stderr}</Text>
</>
)
}
const flows = res.stdout.streams.map((stream) => (
<>
<Text>Codec: {stream.codec_long_name}</Text>
{!stream.width && !stream.height ? null :
<Text>Resolution: {stream.width}x{stream.height} </Text>
}
<Text>FPS: {stream.avg_frame_rate}</Text>
</>
))
return (
<>
<Center><Text weight={700}>Stream: {streamIndex}</Text></Center>
{flows}
</>
)
})
return (
<>
{streamItems}
<Center>
<Button onClick={() => context.closeModal(id)}>Close</Button >
</Center>
</>
)
}

View File

@ -47,7 +47,7 @@ interface FullImageModalProps {
close?(): void close?(): void
} }
const FullImageModal = observer(({ images, opened, open, close }: FullImageModalProps) => { const FullImageModal = ({ images, opened, open, close }: FullImageModalProps) => {
const { modalStore } = useContext(Context) const { modalStore } = useContext(Context)
const { isFullImageOpened, fullImageData, closeFullImage } = modalStore const { isFullImageOpened, fullImageData, closeFullImage } = modalStore
const { classes } = useStyles(); const { classes } = useStyles();
@ -105,6 +105,6 @@ const FullImageModal = observer(({ images, opened, open, close }: FullImageModal
</Modal> </Modal>
); );
}) }
export default FullImageModal; export default observer(FullImageModal)

View File

@ -1,91 +0,0 @@
import { ActionIcon, CloseButton, Flex, Modal, NumberInput, TextInput, Tooltip, createStyles, } from '@mantine/core';
import { getHotkeyHandler, useMediaQuery } from '@mantine/hooks';
import React, { ReactEventHandler, useState, FocusEvent, useRef, Ref } from 'react';
import { IconAlertCircle, IconX } from '@tabler/icons-react';
import { dimensions } from '../../dimensions/dimensions';
import { useTranslation } from 'react-i18next';
const useStyles = createStyles((theme) => ({
rightSection: {
width: '3rem',
marginRight: '0.2rem',
}
}))
interface InputModalProps {
inValue: number
putValue?(value: number): void
opened: boolean
open(): void
close(): void
}
const InputModal = ({ inValue, putValue, opened, open, close }: InputModalProps) => {
const { t } = useTranslation()
const { classes } = useStyles()
const [value, setValue] = useState(inValue)
const isMobile = useMediaQuery(dimensions.mobileSize)
const refInput: React.LegacyRef<HTMLInputElement> = useRef(null)
const handeLoaded = (event: FocusEvent<HTMLInputElement, Element>) => {
event.target.select()
}
const handeClear = () => {
setValue(0)
refInput.current?.select()
}
const handleSetValue = (value: number | "") => {
if (typeof value === "number") {
setValue(value)
}
}
const handleClose = () => {
if (putValue) putValue(value)
close()
}
return (
<Modal
opened={opened}
onClose={handleClose}
withCloseButton={false}
centered
fullScreen={isMobile}
>
<Flex justify="space-between">
<div>{t('enterQuantity')}</div>
<CloseButton size="lg" onClick={handleClose} />
</Flex>
<NumberInput
ref={refInput}
classNames={classes}
type="number"
value={value}
onChange={handleSetValue}
data-autofocus
placeholder={t('quantity')}
hideControls
min={0}
onFocus={handeLoaded}
onKeyDown={
getHotkeyHandler([
['Enter', handleClose]
])
}
rightSection={
<Flex w='100%' h='100%' justify='right' align='center'>
<Tooltip label={t('tooltipСlose')} position="top-end" withArrow>
<div>
<IconAlertCircle size="1.4rem" style={{ display: 'block', opacity: 0.5 }} />
</div>
</Tooltip>
</Flex>
}
/>
</Modal>
);
};
export default InputModal;

View File

@ -0,0 +1,41 @@
import { ContextModalProps } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import { frigateQueryKeys, proxyApi } from '../../../services/frigate.proxy/frigate.api';
import CogwheelLoader from '../loaders/CogwheelLoader';
import RetryError from '../RetryError';
import { Button, Center, Flex, Text } from '@mantine/core';
interface VaInfoModalProps {
hostName?: string
}
export const VaInfoModal = ({
context,
id,
innerProps
}: ContextModalProps<VaInfoModalProps>) => {
const { hostName } = innerProps
const { data, isError, isPending, refetch } = useQuery({
queryKey: [frigateQueryKeys.getHostVaInfo, hostName],
queryFn: () => {
if (!hostName) return null
return proxyApi.getHostVaInfo(hostName)
}
})
if (isPending) return <CogwheelLoader />
if (isError) return <RetryError onRetry={refetch} />
if (!data) return <Text>Data is empty</Text>
return (
<Flex direction='column' w='100%'>
<Text>Return code: {data.return_code}</Text>
{data.stderr ? <Text>{data.stderr}</Text> : null}
<Text>{data.stdout}</Text>
<Center>
<Button onClick={() => context.closeModal(id)}>Close</Button >
</Center>
</Flex>
);
};

View File

@ -1,4 +1,5 @@
import { Card, Flex, Group, Text } from '@mantine/core'; import { Card, Flex, Group, Text } from '@mantine/core';
import { IconZoomCheck, IconZoomQuestion } from '@tabler/icons-react';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -7,7 +8,8 @@ interface GpuStatProps {
decoder?: string, decoder?: string,
encoder?: string, encoder?: string,
gpu?: string, gpu?: string,
mem?: string mem?: string,
onVaInfoClick?: () => void
} }
const GpuStat: React.FC<GpuStatProps> = ({ const GpuStat: React.FC<GpuStatProps> = ({
@ -15,13 +17,19 @@ const GpuStat: React.FC<GpuStatProps> = ({
decoder, decoder,
encoder, encoder,
gpu, gpu,
mem mem,
onVaInfoClick
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Card withBorder radius="md" p='0.7rem'> <Card withBorder radius="md" p='0.7rem'>
<Flex align='center'> <Flex align='center'>
<Text c="dimmed" size="xs" tt="uppercase" fw={700} mr='0.5rem'> <IconZoomQuestion
size='2rem'
color='cyan'
cursor='pointer'
onClick={onVaInfoClick} />
<Text ml='0.5rem' c="dimmed" size="xs" tt="uppercase" fw={700} mr='0.5rem'>
{name} {name}
</Text> </Text>
<Flex w='100%' direction='column' align='center'> <Flex w='100%' direction='column' align='center'>

View File

@ -1,73 +0,0 @@
import { ActionIcon, Badge, Box, Flex, Text, useMantineTheme } from '@mantine/core';
import { useCounter, useDisclosure } from '@mantine/hooks';
import { IconMinus, IconPlus, IconX } from '@tabler/icons-react';
import InputModal from '../modal.windows/InputModal';
import { v4 as uuidv4 } from 'uuid'
import { useEffect } from 'react';
interface RowCounterProps {
counter?: number
setValue?(value: number): void,
showDelete?: boolean
onDelete?(): void
}
const RowCounter = ({ counter, setValue, showDelete, onDelete }: RowCounterProps) => {
const [opened, { open, close }] = useDisclosure(false)
// const [count, handlers] = useCounter(counter, { min: 0 })
const count = counter || 0
const handleSetValue = (value: number) => {
if (setValue) setValue(value)
// else handlers.set(value)
}
const handleOpen = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.stopPropagation()
open()
}
const handleInrease = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation()
handleSetValue(count + 1)
}
const handleDerease = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation()
handleSetValue(count - 1)
}
const handleDelete = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation()
if (onDelete) onDelete()
}
return (
<>
<InputModal key={uuidv4()} inValue={counter ? counter : count} putValue={handleSetValue} opened={opened} open={open} close={close} />
<Flex direction="row">
<ActionIcon onClick={handleDerease} mt="0.1rem" color="red.3" size="md" radius="xl" variant="filled">
<IconMinus size="1.125rem" />
</ActionIcon>
<Box w="3rem">
<Badge size="xl" pl="0.2rem" pr="0.2rem" fullWidth onClick={handleOpen}>
{count}
</Badge>
</Box>
<ActionIcon onClick={handleInrease} mt="0.1rem" color="blue.6" size="md" radius="xl" variant="filled">
<IconPlus size="1.125rem" />
</ActionIcon>
{
showDelete ?
<ActionIcon onClick={handleDelete} ml='0.1rem' mt="0.1rem" color="red" size="md" radius="xl" variant="filled">
<IconX size="1.125rem" />
</ActionIcon>
:
<></>
}
</Flex>
</>
);
};
export default RowCounter;

View File

@ -1,3 +1,27 @@
export interface GetVaInfo {
return_code: number
stderr: string
stdout: string
}
export interface GetFfprobe {
return_code: number
stderr: string
stdout: Stdout
}
export interface Stdout {
programs: any[]
streams: Stream[]
}
export interface Stream {
avg_frame_rate: string // FPS
codec_long_name: string // Codec
height: number
width: number
}
export interface FrigateStats { export interface FrigateStats {
cameras: { cameras: {
[cameraName: string]: CameraStat [cameraName: string]: CameraStat
@ -10,7 +34,7 @@ export interface FrigateStats {
[detectorName: string]: DetectorStat [detectorName: string]: DetectorStat
} }
gpu_usages: { gpu_usages: {
[gpuName: string] : GpuStat [gpuName: string]: GpuStat
} }
processes: Processes processes: Processes
service: Service service: Service
@ -71,7 +95,7 @@ export interface Service {
last_updated: number last_updated: number
latest_version: string latest_version: string
storage: { storage: {
[storagePath: string] : StorageStat [storagePath: string]: StorageStat
} }
temperatures: Temperatures temperatures: Temperatures
uptime: number uptime: number

View File

@ -6,4 +6,5 @@ declare global {
} }
} }
export {}; export {};

9
src/types/saveConfig.ts Normal file
View File

@ -0,0 +1,9 @@
export interface PostSaveConfig {
message: string,
success: boolean
}
export enum SaveOption {
SaveOnly = 'saveonly',
SaveRestart = 'restart',
}

View File

@ -1,4 +1,5 @@
import { Table, Text } from '@mantine/core'; import { Flex, Table, Text } from '@mantine/core';
import { IconZoomQuestion } from '@tabler/icons-react';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -30,10 +31,12 @@ interface TableHead {
interface TableProps<T> { interface TableProps<T> {
data: T[], data: T[],
onFfprobeClick?: (cameraName: string) => void
} }
const FrigateCamerasStateTable = ({ const FrigateCamerasStateTable = ({
data data,
onFfprobeClick
}: TableProps<CameraItem>) => { }: TableProps<CameraItem>) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -74,13 +77,26 @@ const FrigateCamerasStateTable = ({
) )
}) })
if (!isProduction) console.log('FrigateHostsTable rendered') if (!isProduction) console.log('FrigateCamerasStateTable rendered')
const handleFfprobe = (cameraName: string) => {
onFfprobeClick?.(cameraName)
}
const rows = tableData.map(item => { const rows = tableData.map(item => {
return ( return (
<tr key={item.cameraName + item.process}> <tr key={item.cameraName + item.process}>
<td><Text align='center'>{item.cameraName}</Text></td> <td><Text align='center'>{item.cameraName}</Text></td>
<td><Text align='center'>{item.process}</Text></td> <td>
<Flex justify='center'>
<Text align='center' mr='0.2rem'>{item.process}</Text>
{item.process !== ProcessType.Ffmpeg ? null :
<IconZoomQuestion
color='cyan'
cursor='pointer'
onClick={() => handleFfprobe(item.cameraName)} />}
</Flex>
</td>
<td><Text align='center'>{item.pid}</Text></td> <td><Text align='center'>{item.pid}</Text></td>
<td><Text align='center'>{item.fps}</Text></td> <td><Text align='center'>{item.fps}</Text></td>
<td><Text align='center'>{item.cpu}</Text></td> <td><Text align='center'>{item.cpu}</Text></td>

View File

@ -1799,12 +1799,19 @@
resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-6.0.21.tgz#bc009d8380ad18455b90f3ddaf484de16a13da95" resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-6.0.21.tgz#bc009d8380ad18455b90f3ddaf484de16a13da95"
integrity sha512-sYwt5wai25W6VnqHbS5eamey30/HD5dNXaZuaVEAJ2i2bBv8C0cCiczygMDpAFiSYdXoSMRr/SZ2CrrPTzeNew== integrity sha512-sYwt5wai25W6VnqHbS5eamey30/HD5dNXaZuaVEAJ2i2bBv8C0cCiczygMDpAFiSYdXoSMRr/SZ2CrrPTzeNew==
"@mantine/notifications@6.0.16": "@mantine/modals@^6.0.16":
version "6.0.16" version "6.0.21"
resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-6.0.16.tgz#e3259f9bea564ae58d34810096b9288be47ca815" resolved "https://registry.yarnpkg.com/@mantine/modals/-/modals-6.0.21.tgz#6d7f89b55d1817c0a20b10989bf69ecb03f7a642"
integrity sha512-KqlPW51sxgQoJmIC2lEWMVlwPqy04D35iRMkCSget8aNgzk0K5csJppXo6qwMFn2GHKVGXFKJMBUp06IXQbiig== integrity sha512-Gx2D/ZHMUuYF197JKMWey4K9FeGP9rxYp4lmAEXUrjXiST2fEhLZOdiD75KuOHXd1/sYAU9NcNRo9wXrlF/gUA==
dependencies: dependencies:
"@mantine/utils" "6.0.16" "@mantine/utils" "6.0.21"
"@mantine/notifications@^6.0.16":
version "6.0.21"
resolved "https://registry.yarnpkg.com/@mantine/notifications/-/notifications-6.0.21.tgz#bec53664abce13a2cc61a1be1840d82a746f62da"
integrity sha512-qsrqxuJHK8b67sf9Pfk+xyhvpf9jMsivW8vchfnJfjv7yz1lLvezjytMFp4fMDoYhjHnDPOEc/YFockK4muhOw==
dependencies:
"@mantine/utils" "6.0.21"
react-transition-group "4.4.2" react-transition-group "4.4.2"
"@mantine/styles@6.0.21": "@mantine/styles@6.0.21":
@ -1815,11 +1822,6 @@
clsx "1.1.1" clsx "1.1.1"
csstype "3.0.9" csstype "3.0.9"
"@mantine/utils@6.0.16":
version "6.0.16"
resolved "https://registry.yarnpkg.com/@mantine/utils/-/utils-6.0.16.tgz#b39e47ef8fa4463322e9aa10cdd5980f4310b705"
integrity sha512-UFel9DbifL3zS8pTJlr6GfwGd6464OWXCJdUq0oLydgimbC1VV2PnptBr6FMwIpPVcxouLOtY1cChzwFH95PSA==
"@mantine/utils@6.0.21": "@mantine/utils@6.0.21":
version "6.0.21" version "6.0.21"
resolved "https://registry.yarnpkg.com/@mantine/utils/-/utils-6.0.21.tgz#6185506e91cba3e308aaa8ea9ababc8e767995d6" resolved "https://registry.yarnpkg.com/@mantine/utils/-/utils-6.0.21.tgz#6185506e91cba3e308aaa8ea9ababc8e767995d6"