add grid view

add config editor
This commit is contained in:
NlightN22 2024-02-21 01:35:26 +07:00
parent d68edcf6f2
commit 2692e00787
47 changed files with 2613 additions and 222 deletions

View File

@ -9,6 +9,7 @@
"@mantine/core": "^6.0.16",
"@mantine/dates": "^6.0.16",
"@mantine/hooks": "^6.0.16",
"@monaco-editor/react": "^4.6.0",
"@tabler/icons-react": "^2.24.0",
"@tanstack/react-query": "^5.21.2",
"@testing-library/jest-dom": "^5.14.1",
@ -20,19 +21,24 @@
"@types/react-dom": "^18.0.0",
"@types/validator": "^13.7.17",
"axios": "^1.4.0",
"bson-objectid": "^2.0.4",
"cookies-next": "^4.1.1",
"dayjs": "^1.11.9",
"embla-carousel-react": "^8.0.0-rc10",
"idb-keyval": "^6.2.1",
"mantine-react-table": "^1.0.0-beta.25",
"mobx": "^6.9.0",
"mobx-react-lite": "^3.4.3",
"mobx-utils": "^6.0.7",
"monaco-editor": "^0.46.0",
"monaco-yaml": "^5.1.1",
"oidc-client-ts": "^2.2.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-oidc-context": "^2.2.2",
"react-router-dom": "^6.14.1",
"react-scripts": "5.0.1",
"react-use-websocket": "^4.7.0",
"typescript": "^4.4.2",
"validator": "^13.9.0",
"web-vitals": "^2.1.0",

View File

@ -1,10 +1,11 @@
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { AppShell, useMantineTheme, } from "@mantine/core"
import { HeaderAction } from './widgets/header/HeaderAction';
import { testHeaderLinks } from './widgets/header/header.links';
import AppRouter from './router/AppRouter';
import { SideBar } from './shared/components/SideBar';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Context } from '.';
const queryClient = new QueryClient()

View File

@ -0,0 +1,65 @@
import { useEffect, useMemo, useState } from "react";
import { CameraConfig } from "../types/frigateConfig";
import { useAudioActivity, useFrigateEvents, useMotionActivity } from "../services/ws";
type useCameraActivityReturn = {
activeTracking: boolean;
activeMotion: boolean;
activeAudio: boolean;
};
export default function useCameraActivity(
camera: CameraConfig
): useCameraActivityReturn {
const [activeObjects, setActiveObjects] = useState<string[]>([]);
const hasActiveObjects = useMemo(
() => activeObjects.length > 0,
[activeObjects]
);
const { payload: detectingMotion } = useMotionActivity(camera.name);
const { payload: event } = useFrigateEvents();
const { payload: audioRms } = useAudioActivity(camera.name);
useEffect(() => {
if (!event) {
return;
}
if (event.after.camera != camera.name) {
return;
}
const eventIndex = activeObjects.indexOf(event.after.id);
if (event.type == "end") {
if (eventIndex != -1) {
const newActiveObjects = [...activeObjects];
newActiveObjects.splice(eventIndex, 1);
setActiveObjects(newActiveObjects);
}
} else {
if (eventIndex == -1) {
// add unknown event to list if not stationary
if (!event.after.stationary) {
const newActiveObjects = [...activeObjects, event.after.id];
setActiveObjects(newActiveObjects);
}
} else {
// remove known event from list if it has become stationary
if (event.after.stationary) {
activeObjects.splice(eventIndex, 1);
}
}
}
}, [event, activeObjects]);
return {
activeTracking: hasActiveObjects,
activeMotion: detectingMotion == "ON",
activeAudio: camera.audio.enabled_in_config
? audioRms >= camera.audio.min_volume
: false,
};
}

View File

@ -0,0 +1,55 @@
import { useMemo } from "react";
import { usePersistence } from "./use-persistence";
import { CameraConfig, FrigateConfig } from "../types/frigateConfig";
import { LivePlayerMode } from "../types/live";
export default function useCameraLiveMode(
cameraConfig: CameraConfig,
preferredMode?: string
): LivePlayerMode {
// const { data: config } = useSWR<FrigateConfig>("config");
const { data: config } = {
data: {
go2rtc: { streams: 'test' },
ui: {live_mode: ''}
},
}
const restreamEnabled = useMemo(() => {
if (!config) {
return false;
}
return (
cameraConfig &&
Object.keys(config.go2rtc.streams || {}).includes(
cameraConfig.live.stream_name
)
);
}, [config, cameraConfig]);
const defaultLiveMode = useMemo(() => {
if (config && cameraConfig) {
if (restreamEnabled) {
return cameraConfig.ui.live_mode || config?.ui.live_mode;
}
return "jsmpeg";
}
return undefined;
}, [cameraConfig, restreamEnabled]);
const [viewSource] = usePersistence(
`${cameraConfig.name}-source`,
defaultLiveMode
);
if (
restreamEnabled &&
(preferredMode == "mse" || preferredMode == "webrtc")
) {
return preferredMode;
} else {
return viewSource;
}
}

View File

@ -0,0 +1,45 @@
import { useEffect, useState, useCallback } from "react";
import { get as getData, set as setData } from "idb-keyval";
type usePersistenceReturn = [
value: any | undefined,
setValue: (value: string | boolean) => void,
loaded: boolean,
];
export function usePersistence(
key: string,
defaultValue: any | undefined = undefined
): usePersistenceReturn {
const [value, setInternalValue] = useState<any | undefined>(defaultValue);
const [loaded, setLoaded] = useState<boolean>(false);
const setValue = useCallback(
(value: string | boolean) => {
setInternalValue(value);
async function update() {
await setData(key, value);
}
update();
},
[key]
);
useEffect(() => {
setLoaded(false);
setInternalValue(defaultValue);
async function load() {
const value = await getData(key);
if (typeof value !== "undefined") {
setValue(value);
}
setLoaded(true);
}
load();
}, [key, defaultValue, setValue]);
return [value, setValue, loaded];
}

View File

@ -3,7 +3,7 @@ import React, { useContext, useEffect, useState } from 'react';
import CogWheelWithText from '../shared/components/CogWheelWithText';
import { strings } from '../shared/strings/strings';
import { redirect, useNavigate } from 'react-router-dom';
import { pathRoutes } from '../router/routes.path';
import { routesPath } from '../router/routes.path';
import { Context } from '..';
const Forbidden = () => {
@ -11,12 +11,12 @@ const Forbidden = () => {
const { sideBarsStore } = useContext(Context)
useEffect(() => {
sideBarsStore.setLeftSidebar(null)
sideBarsStore.setRightSidebar(null)
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
}, [])
const handleGoToMain = () => {
window.location.replace(pathRoutes.MAIN_PATH)
window.location.replace(routesPath.MAIN_PATH)
}
return (

View File

@ -3,7 +3,7 @@ import React, { useContext, useEffect, useState } from 'react';
import CogWheelWithText from '../shared/components/CogWheelWithText';
import { strings } from '../shared/strings/strings';
import { redirect, useNavigate } from 'react-router-dom';
import { pathRoutes } from '../router/routes.path';
import { routesPath } from '../router/routes.path';
import { Context } from '..';
const NotFound = () => {
@ -11,12 +11,12 @@ const NotFound = () => {
const { sideBarsStore } = useContext(Context)
useEffect(() => {
sideBarsStore.setLeftSidebar(null)
sideBarsStore.setRightSidebar(null)
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
}, [])
const handleGoToMain = () => {
window.location.replace(pathRoutes.MAIN_PATH)
window.location.replace(routesPath.MAIN_PATH)
}
return (

View File

@ -1,26 +1,89 @@
import React from 'react';
import React, { useContext, useEffect, useState } from 'react';
import FrigateHostsTable from '../widgets/FrigateHostsTable';
import { useQuery } from '@tanstack/react-query';
import { frigateApi } from '../services/frigate.proxy/frigate.api';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import { deleteFrigateHostSchema, GetFrigateHost, putFrigateHostSchema} from '../services/frigate.proxy/frigate.schema';
import CenterLoader from '../shared/components/CenterLoader';
import RetryError from './RetryError';
import { Context } from '..';
import { strings } from '../shared/strings/strings';
import { Button, Flex } from '@mantine/core';
import { observer } from 'mobx-react-lite'
const FrigateHostsPage = () => {
const { isPending: hostsPending, error: hostsError, data, refetch } = useQuery({
queryKey: ['frigate-hosts'],
const FrigateHostsPage = observer(() => {
const queryClient = useQueryClient()
const { isPending: hostsPending, error: hostsError, data } = useQuery({
queryKey: [frigateQueryKeys.getFrigateHosts],
queryFn: frigateApi.getHosts,
})
const { sideBarsStore } = useContext(Context)
useEffect(() => {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
}, [])
const [pageData, setPageData] = useState(data)
useEffect(() => {
if (data) setPageData(data)
}, [data])
const { mutate } = useMutation({
mutationFn: (tableData: GetFrigateHost[]) => {
let fetchPromises = []
const toDelete = data?.filter(host => !tableData.some(table => table.id === host.id))
if (toDelete && toDelete.length > 0) {
const parsedDelete = deleteFrigateHostSchema.array().parse(toDelete)
fetchPromises.push(frigateApi.deleteHosts(parsedDelete))
}
if (tableData && tableData.length > 0) {
const parsedChanged = putFrigateHostSchema.array().parse(tableData)
fetchPromises.push(frigateApi.putHosts(parsedChanged))
}
return Promise.all(fetchPromises)
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getFrigateHosts] })
},
onError: () => {
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getFrigateHosts] })
},
onSettled: () => {
if (data) setPageData([...data])
}
})
const handleSave = () => {
if (pageData) {
mutate(pageData)
}
}
const handleChange = (data: GetFrigateHost[]) => {
setPageData(data)
}
const handleDiscard = () => {
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getFrigateHosts] })
if (data) setPageData([...data])
}
if (hostsPending) return <CenterLoader />
if (hostsError) return <RetryError />
return (
<div>
<FrigateHostsTable data={data} showAddButton/>
{
!pageData ? <></> :
<FrigateHostsTable data={pageData} showAddButton changedCallback={handleChange} />
}
<Flex justify='center'>
<Button m='0.5rem' onClick={handleDiscard}>{strings.discard}</Button>
<Button m='0.5rem' onClick={handleSave}>{strings.save}</Button>
</Flex>
</div>
);
};
})
export default FrigateHostsPage;

View File

@ -0,0 +1,119 @@
import React, { useCallback, useContext, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
import { Context } from '..';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { frigateApi, frigateQueryKeys, mapHostToHostname } from '../services/frigate.proxy/frigate.api';
import { Button, Flex, Text, useMantineTheme } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { configureMonacoYaml } from "monaco-yaml";
import Editor, { DiffEditor, useMonaco, loader, Monaco } from '@monaco-editor/react'
import * as monaco from "monaco-editor";
import CenterLoader from '../shared/components/CenterLoader';
import RetryError from './RetryError';
const HostConfigPage = () => {
let { id } = useParams<'id'>()
const queryClient = useQueryClient()
const theme = useMantineTheme();
const { isPending: configPending, error: configError, data: config, refetch } = useQuery({
queryKey: [frigateQueryKeys.getFrigateHost, id],
queryFn: async () => {
const host = await frigateApi.getHost(id || '')
const hostName = mapHostToHostname(host)
return frigateApi.getHostConfigRaw(hostName)
},
})
const clipboard = useClipboard({ timeout: 500 })
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
function handleEditorWillMount(monaco: Monaco) {
monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true);
// TODO add yaml schema
// const modelUri = monaco.Uri.parse("http://localhost:4000/proxy/api/config/schema.json?hostName=localhost:5001")
// configureMonacoYaml(monaco, {
// enableSchemaRequest: true,
// hover: true,
// completion: true,
// validate: true,
// format: true,
// schemas: [
// {
// uri: `http://localhost:4000/proxy/api/config/schema.json?hostName=localhost:5001`,
// fileMatch: ['**/.schema.*'],
// },
// ],
// })
}
function handleEditorDidMount(editor: monaco.editor.IStandaloneCodeEditor, monaco: Monaco) {
// here is another way to get monaco instance
// you can also store it in `useRef` for further usage
editorRef.current = editor;
}
const handleCopyConfig = useCallback(async () => {
if (!editorRef.current) {
return;
}
clipboard.copy(editorRef.current.getValue());
}, [editorRef]);
const onHandleSaveConfig = useCallback(
async (save_option: string) => {
if (!editorRef.current) {
return;
}
console.log('save config', save_option)
}, [editorRef])
if (configPending) return <CenterLoader />
if (configError) return <RetryError onRetry={refetch} />
return (
<Flex direction='column' h='100%' w='100%' justify='stretch'>
<Flex w='100%' justify='center' wrap='nowrap'>
<Button
size="sm"
className="mx-1"
onClick={handleCopyConfig}
>
Copy Config
</Button>
<Button
size="sm"
className="mx-1"
onClick={(_) => onHandleSaveConfig("restart")}
>
Save & Restart
</Button>
<Button
size="sm"
className="mx-1"
onClick={(_) => onHandleSaveConfig("saveonly")}
>
Save Only
</Button>
</Flex>
<Flex h='100%'>
<Editor
defaultLanguage='yaml'
value={config}
defaultValue="// Data empty"
theme={theme.colorScheme == "dark" ? "vs-dark" : "vs-light"}
beforeMount={handleEditorWillMount}
onMount={handleEditorDidMount}
/>
</Flex>
</Flex>
);
}
export default HostConfigPage;

View File

@ -0,0 +1,14 @@
import React from 'react';
import { useParams } from 'react-router-dom';
const HostStoragePage = () => {
let { id } = useParams<'id'>()
return (
<div>
Storage Page - NOT YET IMPLEMENTED
</div>
);
};
export default HostStoragePage;

View File

@ -0,0 +1,14 @@
import React from 'react';
import { useParams } from 'react-router-dom';
const HostSystemPage = () => {
let { id } = useParams<'id'>()
return (
<div>
System Page - NOT YET IMPLEMENTED
</div>
);
};
export default HostSystemPage;

View File

@ -1,4 +1,4 @@
import { Container, Flex, Group, Text } from '@mantine/core';
import { Container, Flex, Grid, Group, Skeleton, Text } from '@mantine/core';
import ProductTable, { TableAdapter } from '../widgets/ProductTable';
import HeadSearch from '../shared/components/HeadSearch';
import ViewSelector, { SelectorViewState } from '../shared/components/ViewSelector';
@ -8,11 +8,25 @@ import { getCookie, setCookie } from 'cookies-next';
import { Context } from '..';
import { observer } from 'mobx-react-lite'
import CenterLoader from '../shared/components/CenterLoader';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { frigateApi, frigateQueryKeys, mapHostToHostname } from '../services/frigate.proxy/frigate.api';
import RetryError from './RetryError';
import { CameraConfig, FrigateConfig } from '../types/frigateConfig';
import { GetFrigateHostWConfig } from '../services/frigate.proxy/frigate.schema';
import { host } from '../shared/env.const';
import AutoUpdatingCameraImage from '../shared/components/frigate/AutoUpdatingCameraImage';
import CameraCard from '../shared/components/CameraCard';
const MainBody = observer(() => {
const { productStore, cartStore, sideBarsStore } = useContext(Context)
const { updateProductFromServer, products, isLoading: productsLoading } = productStore
const { updateCartFromServer, products: cartProducts, isLoading: cardLoading } = cartStore
const { sideBarsStore } = useContext(Context)
useEffect(() => {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
}, [])
const [viewState, setTableState] = useState(getCookie('aps-main-view') as SelectorViewState || SelectorViewState.GRID)
const handleToggleState = (state: SelectorViewState) => {
@ -20,21 +34,43 @@ const MainBody = observer(() => {
setTableState(state)
}
useEffect(() => {
updateProductFromServer()
updateCartFromServer()
sideBarsStore.setLeftSidebar(<div />)
}, [])
const { data: hosts, isPending, isError, refetch } = useQuery({
queryKey: [frigateQueryKeys.getFrigateHostsConfigs],
queryFn: async () => {
const hosts = await frigateApi.getHosts()
let fetchedConfigs = []
for (const host of hosts) {
if (host.enabled) {
const hostName = mapHostToHostname(host)
const config: FrigateConfig = await frigateApi.getHostConfig(hostName)
if (config) {
const hostWConfig: GetFrigateHostWConfig = { config: config, ...host }
fetchedConfigs.push(hostWConfig)
}
}
}
return fetchedConfigs
}
})
if (productsLoading || cardLoading) return <CenterLoader />
if (productsLoading || cardLoading) return <div>Error</div> // add state manager
if (isPending) return <CenterLoader />
if (isError) return <RetryError onRetry={refetch} />
let tableData: TableAdapter[] = []
let gridData: GridAdapter[] = []
if (products && cartProducts) {
tableData = productStore.mapToTable(products, cartProducts)
gridData = productStore.mapToGrid(products, cartProducts)
// const child = () => {
// return ( <Skeleton mih='20rem' miw='20rem' radius="md" animate={false} /> )
// }
const cards = (host: GetFrigateHostWConfig) => {
return Object.entries(host.config.cameras).map(
([cameraName, cameraConfig]) => (
<CameraCard
key={host.id + cameraName}
cameraName={cameraName}
hostName={host.name}
cameraConfig={cameraConfig}
imageUrl={frigateApi.cameraImageURL(mapHostToHostname(host), cameraName)} />))
}
return (
@ -56,6 +92,13 @@ const MainBody = observer(() => {
<ViewSelector state={viewState} onChange={handleToggleState} />
</Group>
</Flex>
<Flex justify='center' h='100%' direction='column'>
{hosts.map(host => (
<Grid mt='sm' key={host.id} justify="center" mb='sm' align='stretch'>
{cards(host)}
</Grid>
))}
</Flex>
</Flex>
);
})

View File

@ -1,29 +1,33 @@
import { Flex, Button, Text } from '@mantine/core';
import React, { useContext, useEffect } from 'react';
import { pathRoutes } from '../router/routes.path';
import { CogWheelHeartSVG } from '../shared/components/svg/CogWheelHeartSVG';
import { routesPath } from '../router/routes.path';
import { strings } from '../shared/strings/strings';
import { useNavigate } from 'react-router-dom';
import { ExclamationCogWheel } from '../shared/components/svg/ExclamationCogWheel';
import { Context } from '..';
const RetryError = () => {
interface RetryErrorProps {
onRetry?: () => void
}
const RetryError = ( {onRetry} : RetryErrorProps) => {
const navigate = useNavigate()
const { sideBarsStore } = useContext(Context)
useEffect(() => {
sideBarsStore.setLeftSidebar(null)
sideBarsStore.setRightSidebar(null)
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
}, [])
const handleGoToMain = () => {
navigate(pathRoutes.MAIN_PATH)
navigate(routesPath.MAIN_PATH)
}
function handleRetry(event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void {
throw new Error('Function not implemented.');
if (onRetry) onRetry()
else window.location.reload()
}
return (

View File

@ -4,7 +4,7 @@ import {
useMutation,
useQueryClient,
} from '@tanstack/react-query'
import { Config, frigateApi } from '../services/frigate.proxy/frigate.api';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import CenterLoader from '../shared/components/CenterLoader';
import RetryError from './RetryError';
import { Button, Flex, Space } from '@mantine/core';
@ -12,16 +12,17 @@ import { FloatingLabelInput } from '../shared/components/FloatingLabelInput';
import { strings } from '../shared/strings/strings';
import { dimensions } from '../shared/dimensions/dimensions';
import { useMediaQuery } from '@mantine/hooks';
import { GetConfig } from '../services/frigate.proxy/frigate.schema';
const SettingsPage = () => {
const queryClient = useQueryClient()
const { isPending: configPending, error: configError, data, refetch } = useQuery({
queryKey: ['config'],
queryKey: [frigateQueryKeys.getConfig],
queryFn: frigateApi.getConfig,
})
const ecryptedValue = '**********'
const mapEncryptedToView = (data: Config[] | undefined): Config[] | undefined => {
const mapEncryptedToView = (data: GetConfig[] | undefined): GetConfig[] | undefined => {
return data?.map(item => {
const { value, encrypted, ...rest } = item
if (encrypted) return { value: ecryptedValue, encrypted, ...rest }
@ -35,7 +36,7 @@ const SettingsPage = () => {
const mutation = useMutation({
mutationFn: frigateApi.putConfig,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['config'] })
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getConfig] })
},
})
@ -82,7 +83,7 @@ const SettingsPage = () => {
if (configPending) return <CenterLoader />
if (configError) return <RetryError />
if (configError) return <RetryError onRetry={refetch} />
return (
<Flex h='100%'>

View File

@ -1,51 +1,29 @@
import React, { Fragment, useContext, useEffect } from 'react';
import { Context } from '..';
import { observer } from 'mobx-react-lite';
import JSMpegPlayer from '../shared/components/JSMpegPlayer';
import { cameraLiveViewURL } from '../router/frigate.routes';
import JSMpegPlayer from '../shared/components/frigate/JSMpegPlayer';
import { frigateApi } from '../services/frigate.proxy/frigate.api';
import { Flex } from '@mantine/core';
import AutoUpdatingCameraImage from '../shared/components/frigate/AutoUpdatingCameraImage';
const Test = observer(() => {
// const { postStore } = useContext(Context)
// const { getPostsAction, posts } = postStore
// const test = {
// camera: 'Buhgalteria',
// host: 'localhost:5000',
// width: 800,
// height: 600,
// url : function() { return frigateApi.cameraWsURL(this.host, this.camera)},
// }
// useEffect( () => {
// console.log("render Test")
// getPostsAction()
// }, [])
// return (
// <Flex w='100%' h='100%'>
// <JSMpegPlayer wsUrl={test.url()} camera={test.camera} width={test.width} height={test.height} />
// </Flex>
// );
const test = {
camera: 'Buhgalteria',
host: 'localhost:5000',
width: 800,
height: 600,
url : function() { return cameraLiveViewURL(this.host, this.camera)},
}
const test2 = {
camera: 'IT',
host: 'localhost:5000',
width: 800,
height: 600,
url : function() { return cameraLiveViewURL(this.host, this.camera)},
}
const test3 = {
camera: 'Magazin1',
host: 'localhost:5001',
width: 800,
height: 600,
url : function() { return cameraLiveViewURL(this.host, this.camera)},
}
// console.log(posts)
return (
<Fragment>
<div>
<JSMpegPlayer url={test.url()} camera={test.camera} width={test.width} height={test.height} />
<JSMpegPlayer url={test2.url()} camera={test2.camera} width={test2.width} height={test2.height} />
<JSMpegPlayer url={test3.url()} camera={test3.camera} width={test3.width} height={test3.height} />
</div>
</Fragment>
<Flex w='100%' h='100%'>
</Flex>
);
})

View File

@ -1,7 +1,7 @@
import { Navigate, Route, Routes } from "react-router-dom";
import { routes } from "./routes";
import { v4 as uuidv4 } from 'uuid'
import { pathRoutes } from "./routes.path";
import { routesPath } from "./routes.path";
const AppRouter = () => {
return (
@ -9,7 +9,7 @@ const AppRouter = () => {
{routes.map(({ path, component }) =>
<Route key={uuidv4()} path={path} element={component} />
)}
<Route key={uuidv4()} path="*" element={<Navigate to={pathRoutes.MAIN_PATH} replace />} />
<Route key={uuidv4()} path="*" element={<Navigate to={routesPath.MAIN_PATH} replace />} />
</Routes>
)
}

View File

@ -1,5 +0,0 @@
import { hostURL } from ".."
export const cameraLiveViewURL = (host: string, cameraName: string) => {
return `ws://${hostURL.host}/proxy-ws/live/jsmpeg/${cameraName}?hostName=${host}`
}

View File

@ -1,10 +1,13 @@
export const pathRoutes = {
export const routesPath = {
MAIN_PATH: '/',
BIRDSEYE_PATH: '/birdseye',
EVENTS_PATH: '/events',
RECORDINGS_PATH: '/recordings',
SETTINGS_PATH: '/settings',
HOST_CONFIG_PATH: '/host-config',
HOSTS_PATH: '/hosts',
HOST_CONFIG_PATH: '/hosts/:id/config',
HOST_SYSTEM_PATH: '/hosts/:id/system',
HOST_STORAGE_PATH: '/hosts/:id/storage',
ROLES_PATH: '/roles',
LIVE_PATH: '/live',
THANKS_PATH: '/thanks',

View File

@ -1,12 +1,15 @@
import {JSX} from "react";
import Test from "../pages/Test"
import MainBody from "../pages/MainBody";
import {pathRoutes} from "./routes.path";
import {routesPath} from "./routes.path";
import RetryError from "../pages/RetryError";
import Forbidden from "../pages/403";
import NotFound from "../pages/404";
import SettingsPage from "../pages/SettingsPage";
import FrigateHostsPage from "../pages/FrigateHostsPage";
import HostConfigPage from "../pages/HostConfigPage";
import HostSystemPage from "../pages/HostSystemPage";
import HostStoragePage from "../pages/HostStoragePage";
interface IRoute {
path: string,
@ -15,31 +18,43 @@ interface IRoute {
export const routes: IRoute[] = [
{ //todo delete
path: pathRoutes.TEST_PATH,
path: routesPath.TEST_PATH,
component: <Test />,
},
{
path: pathRoutes.SETTINGS_PATH,
path: routesPath.SETTINGS_PATH,
component: <SettingsPage />,
},
{
path: pathRoutes.HOST_CONFIG_PATH,
path: routesPath.HOSTS_PATH,
component: <FrigateHostsPage />,
},
{
path: pathRoutes.MAIN_PATH,
path: routesPath.HOST_CONFIG_PATH,
component: <HostConfigPage />,
},
{
path: routesPath.HOST_SYSTEM_PATH,
component: <HostSystemPage />,
},
{
path: routesPath.HOST_STORAGE_PATH,
component: <HostStoragePage />,
},
{
path: routesPath.MAIN_PATH,
component: <MainBody />,
},
{
path: pathRoutes.RETRY_ERROR_PATH,
path: routesPath.RETRY_ERROR_PATH,
component: <RetryError />,
},
{
path: pathRoutes.FORBIDDEN_ERROR_PATH,
path: routesPath.FORBIDDEN_ERROR_PATH,
component: <Forbidden />,
},
{
path: pathRoutes.NOT_FOUND_ERROR_PATH,
path: routesPath.NOT_FOUND_ERROR_PATH,
component: <NotFound />,
},
]

View File

@ -1,25 +1,9 @@
import axios from "axios"
import { proxyURL } from "../../shared/env.const"
import { z } from "zod"
import { GetConfig, DeleteFrigateHost, GetFrigateHost, PutConfig, PutFrigateHost, GetFrigateHostWithCameras } from "./frigate.schema";
import { FrigateConfig } from "../../types/frigateConfig";
export interface Config {
key: string,
value: string,
description: string,
encrypted: boolean,
}
export interface PutConfig {
key: string,
value: string,
}
export interface FrigateHost {
id: string
createAt: string
updateAt: string
name: string
host: string
enabled: boolean
}
const instance = axios.create({
baseURL: proxyURL.toString(),
@ -27,10 +11,50 @@ const instance = axios.create({
});
export const frigateApi = {
getConfig: () => instance.get<Config[]>('apiv1/config').then(res => res.data),
getConfig: () => instance.get<GetConfig[]>('apiv1/config').then(res => res.data),
putConfig: (config: PutConfig[]) => instance.put('apiv1/config', config).then(res => res.data),
getHosts: () => instance.get<FrigateHost[]>('apiv1/frigate-hosts').then(res => {
// return new Map(res.data.map(item => [item.id, item]))
getHosts: () => instance.get<GetFrigateHost[]>('apiv1/frigate-hosts').then(res => {
return res.data
}),
getHostWithCameras: () => instance.get<GetFrigateHostWithCameras[]>('apiv1/frigate-hosts', { params: { include: 'cameras'}}).then(res => {
return res.data
}),
getHost: (id: string) => instance.get<GetFrigateHost>(`apiv1/frigate-hosts/${id}`).then(res => {
return res.data
}),
putHosts: (hosts: PutFrigateHost[]) => instance.put<GetFrigateHost[]>('apiv1/frigate-hosts', hosts).then(res => {
return res.data
}),
deleteHosts: (hosts: DeleteFrigateHost[]) => instance.delete<GetFrigateHost[]>('apiv1/frigate-hosts', { data: hosts }).then(res => {
return res.data
}),
getHostConfigRaw: (hostName: string) => instance.get('proxy/api/config/raw', { params: { hostName: hostName } }).then(res => res.data),
getHostConfig: (hostName: string) => instance.get('proxy/api/config', { params: { hostName: hostName } }).then(res => res.data),
cameraWsURL: (hostName: string, cameraName: string) => {
return `ws://${proxyURL.host}/proxy-ws/live/jsmpeg/${cameraName}?hostName=${hostName}`
},
cameraImageURL: (hostName: string, cameraName: string) => {
return `http://${proxyURL.host}/proxy/api/${cameraName}/latest.jpg?hostName=${hostName}`
},
}
export const mapCamerasFromConfig = (config: FrigateConfig): string[] => {
return Object.keys(config.cameras)
}
export const mapHostToHostname = (host: GetFrigateHost): string => {
const url = new URL(host.host)
const hostName = url.host
return hostName
}
export const frigateQueryKeys = {
getConfig: 'config',
getFrigateHosts: 'frigate-hosts',
getFrigateHostsConfigs: 'frigate-hosts-configs',
getFrigateHost: 'frigate-host',
getHostConfig: 'host-config',
}

View File

@ -0,0 +1,63 @@
import { any, z } from "zod";
import { FrigateConfig } from "../../types/frigateConfig";
export const putConfigSchema = z.object({
key: z.string(),
value: z.string(),
})
export const getConfigSchema = z.object({
key: z.string(),
value: z.string(),
description: z.string(),
encrypted: z.boolean(),
})
export const getFrigateHostSchema = z.object({
id: z.string(),
createAt: z.string(),
updateAt: z.string(),
name: z.string(),
host: z.string(),
enabled: z.boolean(),
});
export const getFrigateHostWConfigSchema = z.object({
id: z.string(),
createAt: z.string(),
updateAt: z.string(),
name: z.string(),
host: z.string(),
enabled: z.boolean(),
config: z.any(),
});
const getCameraSchema = z.object({
id: z.string(),
createAt: z.string(),
updateAt: z.string(),
name: z.string(),
url: z.string(),
state: z.boolean().nullable(),
});
export const getFrigateHostWithCamerasSchema = getFrigateHostSchema.merge(z.object({
cameras: z.array(getCameraSchema),
}))
export const putFrigateHostSchema = getFrigateHostSchema.omit({
createAt: true,
updateAt: true,
});
export const deleteFrigateHostSchema = putFrigateHostSchema.pick({
id: true,
});
export type GetConfig = z.infer<typeof getConfigSchema>
export type PutConfig = z.infer<typeof putConfigSchema>
export type GetFrigateHost = z.infer<typeof getFrigateHostSchema>
export type GetFrigateHostWithCameras = z.infer<typeof getFrigateHostWithCamerasSchema>
export type GetFrigateHostWConfig = GetFrigateHost & { config: FrigateConfig}
export type PutFrigateHost = z.infer<typeof putFrigateHostSchema>
export type DeleteFrigateHost = z.infer<typeof deleteFrigateHostSchema>

235
src/services/ws.tsx Normal file
View File

@ -0,0 +1,235 @@
import {
ReactNode,
createContext,
useCallback,
useContext,
useEffect,
useReducer,
} from "react";
import { produce, Draft } from "immer";
import useWebSocket, { ReadyState } from "react-use-websocket";
import { FrigateConfig } from "../types/frigateConfig";
import { FrigateEvent, ToggleableSetting } from "../types/ws";
type ReducerState = {
[topic: string]: {
lastUpdate: number;
payload: any;
retain: boolean;
};
};
type ReducerAction = {
topic: string;
payload: any;
retain: boolean;
};
const initialState: ReducerState = {
_initial_state: {
lastUpdate: 0,
payload: "",
retain: false,
},
};
type WebSocketContextProps = {
state: ReducerState;
readyState: ReadyState;
sendJsonMessage: (message: any) => void;
};
export const WS = createContext<WebSocketContextProps>({
state: initialState,
readyState: ReadyState.CLOSED,
sendJsonMessage: () => {},
});
export const useWebSocketContext = (): WebSocketContextProps => {
const context = useContext(WS);
if (!context) {
throw new Error(
"useWebSocketContext must be used within a WebSocketProvider"
);
}
return context;
};
function reducer(state: ReducerState, action: ReducerAction): ReducerState {
switch (action.topic) {
default:
return produce(state, (draftState: Draft<ReducerState>) => {
let parsedPayload = action.payload;
try {
parsedPayload = action.payload && JSON.parse(action.payload);
} catch (e) {}
draftState[action.topic] = {
lastUpdate: Date.now(),
payload: parsedPayload,
retain: action.retain,
};
});
}
}
type WsProviderType = {
config: FrigateConfig;
children: ReactNode;
wsUrl: string;
};
export function WsProvider({
config,
children,
wsUrl,
}: WsProviderType) {
const [state, dispatch] = useReducer(reducer, initialState);
const { sendJsonMessage, readyState } = useWebSocket(wsUrl, {
onMessage: (event) => {
dispatch(JSON.parse(event.data));
},
onOpen: () => dispatch({ topic: "", payload: "", retain: false }),
shouldReconnect: () => true,
});
useEffect(() => {
Object.keys(config.cameras).forEach((camera) => {
const { name, record, detect, snapshots, audio } = config.cameras[camera];
dispatch({
topic: `${name}/recordings/state`,
payload: record.enabled ? "ON" : "OFF",
retain: false,
});
dispatch({
topic: `${name}/detect/state`,
payload: detect.enabled ? "ON" : "OFF",
retain: false,
});
dispatch({
topic: `${name}/snapshots/state`,
payload: snapshots.enabled ? "ON" : "OFF",
retain: false,
});
dispatch({
topic: `${name}/audio/state`,
payload: audio.enabled ? "ON" : "OFF",
retain: false,
});
});
}, [config]);
return (
<WS.Provider value={{ state, readyState, sendJsonMessage }}>
{children}
</WS.Provider>
);
}
export function useWs(watchTopic: string, publishTopic: string) {
const { state, readyState, sendJsonMessage } = useWebSocketContext();
const value = state[watchTopic] || { payload: null };
const send = useCallback(
(payload: any, retain = false) => {
if (readyState === ReadyState.OPEN) {
sendJsonMessage({
topic: publishTopic || watchTopic,
payload,
retain,
});
}
},
[sendJsonMessage, readyState, watchTopic, publishTopic]
);
return { value, send };
}
export function useDetectState(camera: string): {
payload: ToggleableSetting;
send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
const {
value: { payload },
send,
} = useWs(`${camera}/detect/state`, `${camera}/detect/set`);
return { payload, send };
}
export function useRecordingsState(camera: string): {
payload: ToggleableSetting;
send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
const {
value: { payload },
send,
} = useWs(`${camera}/recordings/state`, `${camera}/recordings/set`);
return { payload, send };
}
export function useSnapshotsState(camera: string): {
payload: ToggleableSetting;
send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
const {
value: { payload },
send,
} = useWs(`${camera}/snapshots/state`, `${camera}/snapshots/set`);
return { payload, send };
}
export function useAudioState(camera: string): {
payload: ToggleableSetting;
send: (payload: ToggleableSetting, retain?: boolean) => void;
} {
const {
value: { payload },
send,
} = useWs(`${camera}/audio/state`, `${camera}/audio/set`);
return { payload, send };
}
export function usePtzCommand(camera: string): {
payload: string;
send: (payload: string, retain?: boolean) => void;
} {
const {
value: { payload },
send,
} = useWs(`${camera}/ptz`, `${camera}/ptz`);
return { payload, send };
}
export function useRestart(): {
payload: string;
send: (payload: string, retain?: boolean) => void;
} {
const {
value: { payload },
send,
} = useWs("restart", "restart");
return { payload, send };
}
export function useFrigateEvents(): { payload: FrigateEvent } {
const {
value: { payload },
} = useWs(`events`, "");
return { payload };
}
export function useMotionActivity(camera: string): { payload: string } {
const {
value: { payload },
} = useWs(`${camera}/motion`, "");
return { payload };
}
export function useAudioActivity(camera: string): { payload: number } {
const {
value: { payload },
} = useWs(`${camera}/audio/rms`, "");
return { payload };
}

View File

@ -0,0 +1,74 @@
import React from 'react';
import { CameraConfig } from '../../types/frigateConfig';
import { AspectRatio, Button, Card, Flex, Grid, Group, Space, Text, createStyles, useMantineTheme } from '@mantine/core';
import AutoUpdatingCameraImage from './frigate/AutoUpdatingCameraImage';
interface CameraCardProps {
cameraName: string,
hostName: string,
cameraConfig: CameraConfig,
imageUrl: string
}
const useStyles = createStyles((theme) => ({
mainCard: {
display: 'flex',
justifyContent: 'space-between',
flexDirection: 'column',
backgroundColor: theme.colorScheme === 'dark' ? theme.fn.darken(theme.colors.gray[7], 0.5) : theme.colors.gray[2],
'&:hover': {
backgroundColor: theme.colorScheme === 'dark' ? theme.fn.darken(theme.colors.cyan[9], 0.5) : theme.colors.cyan[1],
},
},
bottomGroup: {
marginTop: 'auto',
},
headText: {
color: theme.colorScheme === 'dark' ? theme.colors.gray[4] : theme.colors.gray[9],
fontWeight: 'bold'
}
}))
const CameraCard = ({
cameraName,
hostName,
cameraConfig,
imageUrl,
}: CameraCardProps) => {
const { classes } = useStyles();
const handleOpenLiveView = () => {
throw Error('Not yet implemented')
}
const handleOpenRecordings = () => {
throw Error('Not yet implemented')
}
const handleOpenEvents = () => {
throw Error('Not yet implemented')
}
return (
<Grid.Col md={6} lg={3} p='0.2rem'>
<Card h='100%' radius="lg" padding='0.5rem' className={classes.mainCard}>
{/* <Card maw='25rem' mah='25rem' mih='15rem' miw='15rem'> */}
<Text align='center' size='md' className={classes.headText} >{cameraName} / {hostName}</Text>
<AutoUpdatingCameraImage
onClick={handleOpenLiveView}
cameraConfig={cameraConfig}
url={imageUrl}
showFps={false}
/>
<Group
className={classes.bottomGroup}>
<Flex justify='space-evenly' mt='0.5rem' w='100%'>
<Button size='sm' onClick={handleOpenRecordings}>Recordings</Button>
<Button size='sm' onClick={handleOpenEvents}>Events</Button>
</Flex>
</Group>
</Card>
</Grid.Col >
);
};
export default CameraCard;

View File

@ -43,32 +43,38 @@ export const SideBar = observer(({ isHidden, side, children }: SideBarProps) =>
const { sideBarsStore } = useContext(Context)
useEffect( () => {
if (sideBarsStore.rightVisible && side === 'right' && !visible) {
open()
} else if (!sideBarsStore.rightVisible && side === 'right' && visible) {
close()
}
}, [sideBarsStore.rightVisible])
const [leftChildren, setLeftChildren] = useState<React.ReactNode>(() => {
if (children && side === 'left') return children
else if (sideBarsStore.leftSideBar) return sideBarsStore.leftSideBar
else if (sideBarsStore.leftChildren) return sideBarsStore.leftChildren
return null
})
const [rightChildren, setRightChildren] = useState<React.ReactNode>(() => {
if (children && side === 'right') return children
else if (sideBarsStore.rightSideBar) return sideBarsStore.rightSideBar
else if (sideBarsStore.rightChildren) return sideBarsStore.rightChildren
return null
})
useEffect(() => {
setLeftChildren(sideBarsStore.leftSideBar)
}, [sideBarsStore.leftSideBar])
setLeftChildren(sideBarsStore.leftChildren)
}, [sideBarsStore.leftChildren])
useEffect(() => {
setRightChildren(sideBarsStore.rightSideBar)
}, [sideBarsStore.rightSideBar])
setRightChildren(sideBarsStore.rightChildren)
}, [sideBarsStore.rightChildren])
useEffect(() => {
isHidden(!visible)
}, [visible])
useEffect(() => {
}, [manualVisible.current])
// resize controller
useEffect(() => {
const checkWindowSize = () => {
if (window.innerWidth <= hideSizePx && visible) {

View File

@ -0,0 +1,96 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import CameraImage from "./CameraImage";
import { CameraConfig } from "../../../types/frigateConfig";
import { useDocumentVisibility } from "@mantine/hooks";
import { AspectRatio, Flex } from "@mantine/core";
interface AutoUpdatingCameraImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
cameraConfig: CameraConfig
searchParams?: {};
showFps?: boolean;
className?: string;
url: string
};
export default function AutoUpdatingCameraImage({
cameraConfig,
searchParams = "",
showFps = true,
className,
url,
...rest
}: AutoUpdatingCameraImageProps) {
const [key, setKey] = useState(Date.now());
const [fps, setFps] = useState<string>("0");
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout>();
const windowVisible = useDocumentVisibility()
const reloadInterval = useMemo(() => {
if (windowVisible === "hidden") {
return -1; // no reason to update the image when the window is not visible
}
// if (liveReady) {
// return 60000;
// }
// if (cameraActive) {
// return 200;
// }
return 30000;
}, [windowVisible]);
useEffect(() => {
if (reloadInterval == -1) {
return;
}
setKey(Date.now());
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
setTimeoutId(undefined);
}
};
}, [reloadInterval]);
const handleLoad = useCallback(() => {
if (reloadInterval == -1) {
return;
}
const loadTime = Date.now() - key;
if (showFps) {
setFps((1000 / Math.max(loadTime, reloadInterval)).toFixed(1));
}
setTimeoutId(
setTimeout(
() => {
setKey(Date.now());
},
loadTime > reloadInterval ? 1 : reloadInterval
)
);
}, [key, setFps]);
return (
// <AspectRatio ratio={1}>
<Flex direction='column' h='100%'>
<CameraImage
cameraConfig={cameraConfig}
onload={handleLoad}
searchParams={`cache=${key}&${searchParams}`}
url={url}
{...rest}
/>
{showFps ? <span className="text-xs">Displaying at {fps}fps</span> : null}
</Flex>
// </AspectRatio >
);
}

View File

@ -0,0 +1,53 @@
import { useEffect, useRef } from "react";
import { CameraConfig } from "../../../types/frigateConfig";
import { AspectRatio, Flex, createStyles, Text } from "@mantine/core";
interface CameraImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
className?: string;
cameraConfig: CameraConfig;
onload?: () => void;
searchParams?: {};
url: string
};
const useStyles = createStyles((theme) => ({
}))
export default function CameraImage({
className,
cameraConfig,
onload,
searchParams = "",
url, ...rest }: CameraImageProps) {
const imgRef = useRef<HTMLImageElement | null>(null);
const { classes } = useStyles();
const name = cameraConfig.name
const enabled = cameraConfig.enabled
useEffect(() => {
if (!cameraConfig || !imgRef.current) {
return;
}
imgRef.current.src = url
}, [name, imgRef, searchParams]);
return (
<Flex direction='column' justify='center' h='100%'>
{enabled ? (
<AspectRatio ratio={1.5}>
<img
ref={imgRef}
{...rest}
/>
</AspectRatio>
) : (
<Text align='center'>
Camera is disabled in config, no stream or snapshot available!
</Text>
)}
</Flex>
);
}

View File

@ -0,0 +1,159 @@
import { useCallback, useMemo, useState } from "react";
import AutoUpdatingCameraImage from "./AutoUpdatingCameraImage";
import { CameraConfig } from "../../../types/frigateConfig";
import { usePersistence } from "../../../hooks/use-persistence";
import { Button, Switch, Text } from "@mantine/core";
import { Card, CardContent, CardHeader, CardTitle } from "./card";
import { IconSettings } from "@tabler/icons-react";
type Options = { [key: string]: boolean };
const emptyObject = Object.freeze({});
type DebugCameraImageProps = {
className?: string;
cameraConfig: CameraConfig
url: string
};
export default function DebugCameraImage({
className,
cameraConfig,
url,
}: DebugCameraImageProps) {
const [showSettings, setShowSettings] = useState(false);
const [options, setOptions] = usePersistence(
`${cameraConfig?.name}-feed`,
emptyObject
);
const handleSetOption = useCallback(
(id: string, value: boolean) => {
const newOptions = { ...options, [id]: value };
setOptions(newOptions);
},
[options]
);
const searchParams = useMemo(
() =>
new URLSearchParams(
Object.keys(options).reduce((memo, key) => {
//@ts-ignore we know this is correct
memo.push([key, options[key] === true ? "1" : "0"]);
return memo;
}, [])
),
[options]
);
const handleToggleSettings = useCallback(() => {
setShowSettings(!showSettings);
}, [showSettings]);
return (
<div className={className}>
<AutoUpdatingCameraImage
cameraConfig={cameraConfig}
searchParams={searchParams}
url={url}
/>
<Button onClick={handleToggleSettings} variant="link" size="sm">
<span className="w-5 h-5">
<IconSettings />
</span>{" "}
<span>{showSettings ? "Hide" : "Show"} Options</span>
</Button>
{showSettings ? (
<Card>
<CardHeader>
<CardTitle>Options</CardTitle>
</CardHeader>
<CardContent>
<DebugSettings
handleSetOption={handleSetOption}
options={options}
/>
</CardContent>
</Card>
) : null}
</div>
);
}
type DebugSettingsProps = {
handleSetOption: (id: string, value: boolean) => void;
options: Options;
};
function DebugSettings({ handleSetOption, options }: DebugSettingsProps) {
return (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div className="flex items-center space-x-2">
<Switch
id="bbox"
checked={options["bbox"]}
onChange={() => { }}
// onCheckedChange={(isChecked) => {
// handleSetOption("bbox", isChecked);
// }}
/>
{/* <Label htmlFor="bbox">Bounding Box</Label> */}
<Text>Bounding Box</Text>
</div>
<div className="flex items-center space-x-2">
<Switch
id="timestamp"
checked={options["timestamp"]}
// onCheckedChange={(isChecked) => {
// handleSetOption("timestamp", isChecked);
// }}
/>
{/* <Label htmlFor="timestamp">Timestamp</Label> */}
<Text>Timestamp</Text>
</div>
<div className="flex items-center space-x-2">
<Switch
id="zones"
checked={options["zones"]}
// onCheckedChange={(isChecked) => {
// handleSetOption("zones", isChecked);
// }}
/>
{/* <Label htmlFor="zones">Zones</Label> */}
<Text>Zones</Text>
</div>
<div className="flex items-center space-x-2">
<Switch
id="mask"
checked={options["mask"]}
// onCheckedChange={(isChecked) => {
// handleSetOption("mask", isChecked);
// }}
/>
{/* <Label htmlFor="mask">Mask</Label> */}
<Text>Mask</Text>
</div>
<div className="flex items-center space-x-2">
<Switch
id="motion"
checked={options["motion"]}
// onCheckedChange={(isChecked) => {
// handleSetOption("motion", isChecked);
// }}
/>
{/* <Label htmlFor="motion">Motion</Label> */}
<Text>Motion</Text>
</div>
<div className="flex items-center space-x-2">
<Switch
id="regions"
checked={options["regions"]}
// onCheckedChange={(isChecked) => {
// handleSetOption("regions", isChecked);
// }}
/>
{/* <Label htmlFor="regions">Regions</Label> */}
<Text>Regions</Text>
</div>
</div>
);
}

View File

@ -1,11 +1,11 @@
// @ts-ignore we know this doesn't have types
import JSMpeg from "@cycjimmy/jsmpeg-player";
import { useEffect, useMemo, useRef } from "react";
import { useResizeObserver } from "../utils/resize-observer";
import { useResizeObserver } from "../../utils/resize-observer";
type JSMpegPlayerProps = {
className?: string;
url: string;
wsUrl: string;
camera: string;
width: number;
height: number;
@ -13,7 +13,7 @@ type JSMpegPlayerProps = {
export default function JSMpegPlayer({
camera,
url,
wsUrl,
width,
height,
className,
@ -60,7 +60,7 @@ export default function JSMpegPlayer({
const video = new JSMpeg.VideoElement(
playerRef.current,
url,
wsUrl,
{},
{ protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 }
);
@ -83,7 +83,7 @@ export default function JSMpegPlayer({
playerRef.current = null;
}
};
}, [url]);
}, [wsUrl]);
return (
<div className={className} ref={containerRef}>

View File

@ -0,0 +1,269 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
type MSEPlayerProps = {
camera: string;
className?: string;
playbackEnabled?: boolean;
onPlaying?: () => void;
wsUrl: string,
};
function MSEPlayer({
camera,
className,
playbackEnabled = true,
onPlaying,
wsUrl,
}: MSEPlayerProps) {
let connectTS: number = 0;
const RECONNECT_TIMEOUT: number = 30000;
const CODECS: string[] = [
"avc1.640029", // H.264 high 4.1 (Chromecast 1st and 2nd Gen)
"avc1.64002A", // H.264 high 4.2 (Chromecast 3rd Gen)
"avc1.640033", // H.264 high 5.1 (Chromecast with Google TV)
"hvc1.1.6.L153.B0", // H.265 main 5.1 (Chromecast Ultra)
"mp4a.40.2", // AAC LC
"mp4a.40.5", // AAC HE
"flac", // FLAC (PCM compatible)
"opus", // OPUS Chrome, Firefox
];
const visibilityThreshold: number = 0;
const visibilityCheck: boolean = true;
const [wsState, setWsState] = useState<number>(WebSocket.CLOSED);
const videoRef = useRef<HTMLVideoElement>(null);
const wsRef = useRef<WebSocket | null>(null);
const reconnectTIDRef = useRef<number | null>(null);
const ondataRef = useRef<((data: any) => void) | null>(null);
const onmessageRef = useRef<{ [key: string]: (msg: any) => void }>({});
const msRef = useRef<MediaSource | null>(null);
const wsURL = useMemo(() => {
return wsUrl;
}, [camera]);
const play = () => {
const currentVideo = videoRef.current;
if (currentVideo) {
currentVideo.play().catch((er: any) => {
if (er.name === "NotAllowedError" && !currentVideo.muted) {
currentVideo.muted = true;
currentVideo.play().catch(() => {});
}
});
}
};
const send = useCallback(
(value: any) => {
if (wsRef.current) wsRef.current.send(JSON.stringify(value));
},
[wsRef]
);
const codecs = useCallback((isSupported: (type: string) => boolean) => {
return CODECS.filter((codec) =>
isSupported(`video/mp4; codecs="${codec}"`)
).join();
}, []);
const onConnect = useCallback(() => {
if (!videoRef.current?.isConnected || !wsURL || wsRef.current) return false;
setWsState(WebSocket.CONNECTING);
connectTS = Date.now();
wsRef.current = new WebSocket(wsURL);
wsRef.current.binaryType = "arraybuffer";
wsRef.current.addEventListener("open", () => onOpen());
wsRef.current.addEventListener("close", () => onClose());
}, [wsURL]);
const onDisconnect = useCallback(() => {
setWsState(WebSocket.CLOSED);
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
}, []);
const onOpen = useCallback(() => {
setWsState(WebSocket.OPEN);
wsRef.current?.addEventListener("message", (ev) => {
if (typeof ev.data === "string") {
const msg = JSON.parse(ev.data);
for (const mode in onmessageRef.current) {
onmessageRef.current[mode](msg);
}
} else {
ondataRef.current?.(ev.data);
}
});
ondataRef.current = null;
onmessageRef.current = {};
onMse();
}, []);
const onClose = useCallback(() => {
if (wsState === WebSocket.CLOSED) return;
setWsState(WebSocket.CONNECTING);
wsRef.current = null;
const delay = Math.max(RECONNECT_TIMEOUT - (Date.now() - connectTS), 0);
reconnectTIDRef.current = window.setTimeout(() => {
reconnectTIDRef.current = null;
onConnect();
}, delay);
}, [wsState, connectTS, onConnect]);
const onMse = () => {
if ("ManagedMediaSource" in window) {
const MediaSource = window.ManagedMediaSource;
msRef.current?.addEventListener(
"sourceopen",
() => {
send({
type: "mse",
// @ts-ignore
value: codecs(MediaSource.isTypeSupported),
});
},
{ once: true }
);
if (videoRef.current) {
videoRef.current.disableRemotePlayback = true;
videoRef.current.srcObject = msRef.current;
}
} else {
msRef.current?.addEventListener(
"sourceopen",
() => {
URL.revokeObjectURL(videoRef.current?.src || "");
send({
type: "mse",
value: codecs(MediaSource.isTypeSupported),
});
},
{ once: true }
);
videoRef.current!.src = URL.createObjectURL(msRef.current!);
videoRef.current!.srcObject = null;
}
play();
onmessageRef.current["mse"] = (msg) => {
if (msg.type !== "mse") return;
const sb = msRef.current?.addSourceBuffer(msg.value);
sb?.addEventListener("updateend", () => {
if (sb.updating) return;
try {
if (bufLen > 0) {
const data = buf.slice(0, bufLen);
bufLen = 0;
sb.appendBuffer(data);
} else if (sb.buffered && sb.buffered.length) {
const end = sb.buffered.end(sb.buffered.length - 1) - 15;
const start = sb.buffered.start(0);
if (end > start) {
sb.remove(start, end);
msRef.current?.setLiveSeekableRange(end, end + 15);
}
}
} catch (e) {
console.debug(e);
}
});
const buf = new Uint8Array(2 * 1024 * 1024);
let bufLen = 0;
ondataRef.current = (data) => {
if (sb?.updating || bufLen > 0) {
const b = new Uint8Array(data);
buf.set(b, bufLen);
bufLen += b.byteLength;
// console.debug("VideoRTC.buffer", b.byteLength, bufLen);
} else {
try {
sb?.appendBuffer(data);
} catch (e) {
console.debug(e);
}
}
};
};
};
useEffect(() => {
if (!playbackEnabled) {
return;
}
// iOS 17.1+ uses ManagedMediaSource
const MediaSourceConstructor =
"ManagedMediaSource" in window ? window.ManagedMediaSource : MediaSource;
// @ts-ignore
msRef.current = new MediaSourceConstructor();
if ("hidden" in document && visibilityCheck) {
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
onDisconnect();
} else if (videoRef.current?.isConnected) {
onConnect();
}
});
}
if ("IntersectionObserver" in window && visibilityThreshold) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
onDisconnect();
} else if (videoRef.current?.isConnected) {
onConnect();
}
});
},
{ threshold: visibilityThreshold }
);
observer.observe(videoRef.current!);
}
onConnect();
return () => {
onDisconnect();
};
}, [playbackEnabled, onDisconnect, onConnect]);
return (
<video
ref={videoRef}
className={className}
playsInline
preload="auto"
onLoadedData={onPlaying}
muted
/>
);
}
export default MSEPlayer;

View File

@ -0,0 +1,105 @@
import React, { useEffect, useMemo, useState } from 'react';
import JSMpegPlayer from './JSMpegPlayer';
import MSEPlayer from './MsePlayer';
import { CameraConfig } from '../../../types/frigateConfig';
import { LivePlayerMode } from '../../../types/live';
import useCameraActivity from '../../../hooks/use-camera-activity';
import useCameraLiveMode from '../../../hooks/use-camera-live-mode';
import WebRtcPlayer from './WebRTCPlayer';
type LivePlayerProps = {
className?: string;
cameraConfig: CameraConfig;
preferredLiveMode?: LivePlayerMode;
showStillWithoutActivity?: boolean;
windowVisible?: boolean;
host: string
};
const Player = ({
className,
cameraConfig,
preferredLiveMode,
showStillWithoutActivity = true,
windowVisible = true,
host
}: LivePlayerProps) => {
const wsUrl = 'ws://localhost:4000/proxy-ws/ws?hostName=localhost:5000'
const { activeMotion, activeAudio, activeTracking } =
useCameraActivity(cameraConfig);
const cameraActive = useMemo(
() => windowVisible && (activeMotion || activeTracking),
[activeMotion, activeTracking, windowVisible]
);
// camera live state
const liveMode = useCameraLiveMode(cameraConfig, preferredLiveMode);
const [liveReady, setLiveReady] = useState(false);
useEffect(() => {
if (!liveReady) {
if (cameraActive && liveMode == "jsmpeg") {
setLiveReady(true);
}
return;
}
if (!cameraActive) {
setLiveReady(false);
}
}, [cameraActive, liveReady]);
let player;
if (liveMode == "webrtc") {
player = (
<WebRtcPlayer
className={`rounded-2xl h-full ${liveReady ? "" : "hidden"}`}
camera={cameraConfig.live.stream_name}
playbackEnabled={cameraActive}
onPlaying={() => setLiveReady(true)}
wsUrl={wsUrl}
/>
);
} else if (liveMode == "mse") {
if ("MediaSource" in window || "ManagedMediaSource" in window) {
player = (
<MSEPlayer
className={`rounded-2xl h-full ${liveReady ? "" : "hidden"}`}
camera='Not yet implemented'
playbackEnabled={cameraActive}
onPlaying={() => setLiveReady(true)}
wsUrl={wsUrl}
/>
);
} else {
player = (
<div className="w-5xl text-center text-sm">
MSE is only supported on iOS 17.1+. You'll need to update if available
or use jsmpeg / webRTC streams. See the docs for more info.
</div>
);
}
} else if (liveMode == "jsmpeg") {
player = (
<JSMpegPlayer
className="w-full flex justify-center rounded-2xl overflow-hidden"
camera='Not yet implemented'
width={600}
height={800}
wsUrl='Not yet implemented'
/>
);
return (
<div>
</div>
);
}
}
export default Player;

View File

@ -0,0 +1,168 @@
import { useCallback, useEffect, useRef } from "react";
type WebRtcPlayerProps = {
className?: string;
camera: string;
playbackEnabled?: boolean;
onPlaying?: () => void,
wsUrl: string
};
export default function WebRtcPlayer({
className,
camera,
playbackEnabled = true,
onPlaying,
wsUrl
}: WebRtcPlayerProps) {
// camera states
const pcRef = useRef<RTCPeerConnection | undefined>();
const videoRef = useRef<HTMLVideoElement | null>(null);
const PeerConnection = useCallback(
async (media: string) => {
if (!videoRef.current) {
return;
}
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
const localTracks = [];
if (/camera|microphone/.test(media)) {
const tracks = await getMediaTracks("user", {
video: media.indexOf("camera") >= 0,
audio: media.indexOf("microphone") >= 0,
});
tracks.forEach((track) => {
pc.addTransceiver(track, { direction: "sendonly" });
if (track.kind === "video") localTracks.push(track);
});
}
if (media.indexOf("display") >= 0) {
const tracks = await getMediaTracks("display", {
video: true,
audio: media.indexOf("speaker") >= 0,
});
tracks.forEach((track) => {
pc.addTransceiver(track, { direction: "sendonly" });
if (track.kind === "video") localTracks.push(track);
});
}
if (/video|audio/.test(media)) {
const tracks = ["video", "audio"]
.filter((kind) => media.indexOf(kind) >= 0)
.map(
(kind) =>
pc.addTransceiver(kind, { direction: "recvonly" }).receiver.track
);
localTracks.push(...tracks);
}
videoRef.current.srcObject = new MediaStream(localTracks);
return pc;
},
[videoRef]
);
async function getMediaTracks(
media: string,
constraints: MediaStreamConstraints
) {
try {
const stream =
media === "user"
? await navigator.mediaDevices.getUserMedia(constraints)
: await navigator.mediaDevices.getDisplayMedia(constraints);
return stream.getTracks();
} catch (e) {
return [];
}
}
const connect = useCallback(
async (ws: WebSocket, aPc: Promise<RTCPeerConnection | undefined>) => {
if (!aPc) {
return;
}
pcRef.current = await aPc;
ws.addEventListener("open", () => {
pcRef.current?.addEventListener("icecandidate", (ev) => {
if (!ev.candidate) return;
const msg = {
type: "webrtc/candidate",
value: ev.candidate.candidate,
};
ws.send(JSON.stringify(msg));
});
pcRef.current
?.createOffer()
.then((offer) => pcRef.current?.setLocalDescription(offer))
.then(() => {
const msg = {
type: "webrtc/offer",
value: pcRef.current?.localDescription?.sdp,
};
ws.send(JSON.stringify(msg));
});
});
ws.addEventListener("message", (ev) => {
const msg = JSON.parse(ev.data);
if (msg.type === "webrtc/candidate") {
pcRef.current?.addIceCandidate({ candidate: msg.value, sdpMid: "0" });
} else if (msg.type === "webrtc/answer") {
pcRef.current?.setRemoteDescription({
type: "answer",
sdp: msg.value,
});
}
});
},
[]
);
useEffect(() => {
if (!videoRef.current) {
return;
}
if (!playbackEnabled) {
return;
}
// const url = `$baseUrl{.replace(
// /^http/,
// "ws"
// )}live/webrtc/api/ws?src=${camera}`;
const ws = new WebSocket(wsUrl);
const aPc = PeerConnection("video+audio");
connect(ws, aPc);
return () => {
if (pcRef.current) {
pcRef.current.close();
pcRef.current = undefined;
}
};
}, [camera, connect, PeerConnection, pcRef, videoRef, playbackEnabled]);
return (
<video
ref={videoRef}
className={className}
autoPlay
playsInline
muted
onLoadedData={onPlaying}
/>
);
}

View File

@ -0,0 +1,80 @@
import * as React from "react"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
// className={cn(
// "rounded-lg border bg-card text-card-foreground shadow-sm",
// className
// )}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
// className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
// className={cn(
// "text-2xl font-semibold leading-none tracking-tight",
// className
// )}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
// className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref}
// className= {cn("p-6 pt-0", className)}
{...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
// className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,61 @@
import { Button, Menu, rem, Text } from '@mantine/core';
import { IconEdit, IconGraph, IconMessageCircle, IconRotateClockwise, IconServer, IconSettings } from '@tabler/icons-react';
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { routesPath } from '../../../router/routes.path';
interface HostSettingsMenuProps {
id: string
}
const HostSettingsMenu = ({ id }: HostSettingsMenuProps) => {
const navigate = useNavigate()
const handleConfig = () => {
const url = routesPath.HOST_CONFIG_PATH.replace(':id', id)
navigate(url)
}
const handleStorage = () => {
const url = routesPath.HOST_STORAGE_PATH.replace(':id', id)
navigate(url)
}
const handleSystem = () => {
const url = routesPath.HOST_SYSTEM_PATH.replace(':id', id)
navigate(url)
}
const handleRestart = () => {
}
return (
<Menu shadow="md" width={200}>
<Menu.Target>
<Button size='xs' ><IconSettings /></Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
onClick={handleConfig}
icon={<IconEdit style={{ width: rem(14), height: rem(14) }} />}>
Edit Config
</Menu.Item>
<Menu.Item
onClick={handleRestart}
icon={<IconRotateClockwise style={{ width: rem(14), height: rem(14) }} />}>
Restart
</Menu.Item>
<Menu.Item
onClick={handleSystem}
icon={<IconGraph style={{ width: rem(14), height: rem(14) }} />}>
System
</Menu.Item>
<Menu.Item
onClick={handleStorage}
icon={<IconServer
style={{ width: rem(14), height: rem(14) }} />}>
Storage
</Menu.Item>
</Menu.Dropdown>
</Menu>
)
}
export default HostSettingsMenu

View File

@ -1,26 +1,43 @@
import { makeAutoObservable } from "mobx"
export class SideBarsStore {
private _leftSideBar: React.ReactNode = null
public get leftSideBar(): React.ReactNode {
return this._leftSideBar
private _rightVisible: boolean = true
public get rightVisible(): boolean {
return this._rightVisible
}
public set rightVisible(visible: boolean) {
console.log(`set rightVisible`, visible)
this._rightVisible = visible
}
private _leftVisible: boolean = true
public get leftVisible(): boolean {
return this._leftVisible
}
public set leftVisible(visible: boolean) {
this._leftVisible = visible
}
private _leftChildren: React.ReactNode = null
public get leftChildren(): React.ReactNode {
return this._leftChildren
}
private _rightSideBar: React.ReactNode = null
public get rightSideBar(): React.ReactNode {
return this._rightSideBar
private _rightChildren: React.ReactNode = null
public get rightChildren(): React.ReactNode {
return this._rightChildren
}
constructor () {
makeAutoObservable(this)
}
setRightSidebar = (value: React.ReactNode) => {
this._rightSideBar = value
setRightChildren = (value: React.ReactNode) => {
this._rightChildren = value
}
setLeftSidebar = (value: React.ReactNode) => {
this._leftSideBar = value
setLeftChildren = (value: React.ReactNode) => {
this._leftChildren = value
}
}

View File

@ -2,6 +2,6 @@ export const headerMenu = {
home:"На главную",
test:"Тест",
settings:"Настройки",
rolesAcess:"Доступ",
acessSettings:" Настройка доступа",
hostsConfig:"Серверы Frigate",
}

View File

@ -60,6 +60,7 @@ export const strings = {
weight: "Вес:",
total: "Итого:",
confirm: "Подтвердить",
save: "Сохранить",
discard: "Отменить",
next: "Далее",
back: "Назад",

406
src/types/frigateConfig.ts Normal file
View File

@ -0,0 +1,406 @@
export interface UiConfig {
timezone?: string;
time_format?: "browser" | "12hour" | "24hour";
date_style?: "full" | "long" | "medium" | "short";
time_style?: "full" | "long" | "medium" | "short";
strftime_fmt?: string;
live_mode?: string;
use_experimental?: boolean;
dashboard: boolean;
order: number;
}
export interface BirdseyeConfig {
enabled: boolean;
height: number;
mode: "objects" | "continuous" | "motion";
quality: number;
restream: boolean;
width: number;
}
export interface CameraConfig {
audio: {
enabled: boolean;
enabled_in_config: boolean;
filters: string[] | null;
listen: string[];
max_not_heard: number;
min_volume: number;
num_threads: number;
};
best_image_timeout: number;
birdseye: {
enabled: boolean;
mode: "objects" | "continuous" | "motion";
order: number;
};
detect: {
annotation_offset: number;
enabled: boolean;
fps: number;
height: number;
max_disappeared: number;
min_initialized: number;
stationary: {
interval: number;
max_frames: {
default: number | null;
objects: Record<string, unknown>;
};
threshold: number;
};
width: number;
};
enabled: boolean;
ffmpeg: {
global_args: string[];
hwaccel_args: string;
input_args: string;
inputs: {
global_args: string[];
hwaccel_args: string[];
input_args: string;
path: string;
roles: string[];
}[];
output_args: {
detect: string[];
record: string;
rtmp: string;
};
retry_interval: number;
};
ffmpeg_cmds: {
cmd: string;
roles: string[];
}[];
live: {
height: number;
quality: number;
stream_name: string;
};
motion: {
contour_area: number;
delta_alpha: number;
frame_alpha: number;
frame_height: number;
improve_contrast: boolean;
lightning_threshold: number;
mask: string[];
mqtt_off_delay: number;
threshold: number;
};
mqtt: {
bounding_box: boolean;
crop: boolean;
enabled: boolean;
height: number;
quality: number;
required_zones: string[];
timestamp: boolean;
};
name: string;
objects: {
filters: {
[objectName: string]: {
mask: string | null;
max_area: number;
max_ratio: number;
min_area: number;
min_ratio: number;
min_score: number;
threshold: number;
};
};
mask: string;
track: string[];
};
onvif: {
autotracking: {
calibrate_on_startup: boolean;
enabled: boolean;
enabled_in_config: boolean;
movement_weights: string[];
required_zones: string[];
return_preset: string;
timeout: number;
track: string[];
zoom_factor: number;
zooming: string;
};
host: string;
password: string | null;
port: number;
user: string | null;
};
record: {
enabled: boolean;
enabled_in_config: boolean;
events: {
objects: string[] | null;
post_capture: number;
pre_capture: number;
required_zones: string[];
retain: {
default: number;
mode: string;
objects: Record<string, unknown>;
};
};
expire_interval: number;
export: {
timelapse_args: string;
};
preview: {
quality: string;
};
retain: {
days: number;
mode: string;
};
sync_recordings: boolean;
};
rtmp: {
enabled: boolean;
};
snapshots: {
bounding_box: boolean;
clean_copy: boolean;
crop: boolean;
enabled: boolean;
height: number | null;
quality: number;
required_zones: string[];
retain: {
default: number;
mode: string;
objects: Record<string, unknown>;
};
timestamp: boolean;
};
timestamp_style: {
color: {
blue: number;
green: number;
red: number;
};
effect: string | null;
format: string;
position: string;
thickness: number;
};
ui: UiConfig;
webui_url: string | null;
zones: {
[zoneName: string]: {
coordinates: string;
filters: Record<string, unknown>;
inertia: number;
objects: any[];
};
};
}
export interface FrigateConfig {
audio: {
enabled: boolean;
enabled_in_config: boolean | null;
filters: string[] | null;
listen: string[];
max_not_heard: number;
min_volume: number;
num_threads: number;
};
birdseye: BirdseyeConfig;
cameras: {
[cameraName: string]: CameraConfig;
};
database: {
path: string;
};
detect: {
annotation_offset: number;
enabled: boolean;
fps: number;
height: number | null;
max_disappeared: number | null;
min_initialized: number | null;
stationary: {
interval: number | null;
max_frames: {
default: number | null;
objects: Record<string, unknown>;
};
threshold: number | null;
};
width: number | null;
};
detectors: {
coral: {
device: string;
model: {
height: number;
input_pixel_format: string;
input_tensor: string;
labelmap: Record<string, string>;
labelmap_path: string | null;
model_type: string;
path: string;
width: number;
};
type: string;
};
};
environment_vars: Record<string, unknown>;
ffmpeg: {
global_args: string[];
hwaccel_args: string;
input_args: string;
output_args: {
detect: string[];
record: string;
rtmp: string;
};
retry_interval: number;
};
go2rtc: Record<string, unknown>;
live: {
height: number;
quality: number;
stream_name: string;
};
logger: {
default: string;
logs: Record<string, string>;
};
model: {
height: number;
input_pixel_format: string;
input_tensor: string;
labelmap: Record<string, unknown>;
labelmap_path: string | null;
model_type: string;
path: string | null;
width: number;
};
motion: Record<string, unknown> | null;
mqtt: {
client_id: string;
enabled: boolean;
host: string;
port: number;
stats_interval: number;
tls_ca_certs: string | null;
tls_client_cert: string | null;
tls_client_key: string | null;
tls_insecure: boolean | null;
topic_prefix: string;
user: string | null;
};
objects: {
filters: {
[objectName: string]: {
mask: string | null;
max_area: number;
max_ratio: number;
min_area: number;
min_ratio: number;
min_score: number;
threshold: number;
};
};
mask: string;
track: string[];
};
plus: {
enabled: boolean;
};
record: {
enabled: boolean;
enabled_in_config: boolean | null;
events: {
objects: string[] | null;
post_capture: number;
pre_capture: number;
required_zones: string[];
retain: {
default: number;
mode: string;
objects: Record<string, unknown>;
};
};
expire_interval: number;
export: {
timelapse_args: string;
};
preview: {
quality: string;
};
retain: {
days: number;
mode: string;
};
sync_recordings: boolean;
};
rtmp: {
enabled: boolean;
};
snapshots: {
bounding_box: boolean;
clean_copy: boolean;
crop: boolean;
enabled: boolean;
height: number | null;
quality: number;
required_zones: string[];
retain: {
default: number;
mode: string;
objects: Record<string, unknown>;
};
timestamp: boolean;
};
telemetry: {
network_interfaces: any[];
stats: {
amd_gpu_stats: boolean;
intel_gpu_stats: boolean;
network_bandwidth: boolean;
};
version_check: boolean;
};
timestamp_style: {
color: {
blue: number;
green: number;
red: number;
};
effect: string | null;
format: string;
position: string;
thickness: number;
};
ui: UiConfig;
}

1
src/types/live.ts Normal file
View File

@ -0,0 +1 @@
export type LivePlayerMode = "webrtc" | "mse" | "jsmpeg" | "debug";

36
src/types/ws.ts Normal file
View File

@ -0,0 +1,36 @@
type FrigateObjectState = {
id: string;
camera: string;
frame_time: number;
snapshot_time: number;
label: string;
sub_label: string | null;
top_score: number;
false_positive: boolean;
start_time: number;
end_time: number | null;
score: number;
box: [number, number, number, number];
area: number;
ratio: number;
region: [number, number, number, number];
current_zones: string[];
entered_zones: string[];
thumbnail: string | null;
has_snapshot: boolean;
has_clip: boolean;
stationary: boolean;
motionless_count: number;
position_changes: number;
attributes: {
[key: string]: number;
};
};
export interface FrigateEvent {
type: "new" | "update" | "end";
before: FrigateObjectState;
after: FrigateObjectState;
}
export type ToggleableSetting = "ON" | "OFF"

View File

@ -1,25 +1,42 @@
import { Button, Flex, Switch, Table, Text, TextInput, useMantineTheme } from '@mantine/core';
import React, { useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import SortedTh from '../shared/components/table.aps/SortedTh';
import { strings } from '../shared/strings/strings';
import { v4 as uuidv4 } from 'uuid'
import { FrigateHost } from '../services/frigate.proxy/frigate.api';
import { IconBulbFilled, IconBulbOff, IconDeviceFloppy, IconPencil, IconPlus, IconSettings, IconTrash } from '@tabler/icons-react';
import SwitchCell from '../shared/components/table.aps/SwitchCell';
import TextInputCell from '../shared/components/table.aps/TextInputCell';
import SwitchCell from '../shared/components/hosts.table/SwitchCell';
import TextInputCell from '../shared/components/hosts.table/TextInputCell';
import ObjectId from 'bson-objectid';
import { debounce } from '../shared/utils/debounce';
import HostSettingsMenu from '../shared/components/menu/HostSettingsMenu';
import { GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
interface FrigateHostsTableProps {
data: FrigateHost[],
interface TableProps<T> {
data: T[],
showAddButton?: boolean,
handleInputChange?: () => void,
handleSwtitchToggle?: () => void,
saveCallback?: (tableData: T[]) => void,
changedCallback?: (tableData: T[]) => void,
}
const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, handleSwtitchToggle }: FrigateHostsTableProps) => {
const [tableData, setData] = useState(data)
const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedCallback }: TableProps<GetFrigateHost>) => {
console.log('FrigateHostsTable rendered')
const [tableData, setTableData] = useState(data)
const [reversed, setReversed] = useState(false)
const [sortedName, setSortedName] = useState<string | null>(null)
useEffect(() => {
console.log('data changed')
setTableData(data)
}, [data])
const debouncedChanged = useCallback(debounce((tableData: GetFrigateHost[]) => {
if (changedCallback) changedCallback(tableData)
}, 200), [])
useEffect(() => {
debouncedChanged(tableData)
}, [tableData, debouncedChanged])
function sortByKey<T, K extends keyof T>(array: T[], key: K): T[] {
return array.sort((a, b) => {
let valueA = a[key];
@ -37,9 +54,9 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
const handleSort = (headName: string, propertyName: string,) => {
const reverse = headName === sortedName ? !reversed : false;
setReversed(reverse)
const arr = sortByKey(tableData, propertyName as keyof FrigateHost)
const arr = sortByKey(tableData, propertyName as keyof GetFrigateHost)
if (reverse) arr.reverse()
setData(arr)
setTableData(arr)
setSortedName(headName)
}
@ -63,7 +80,7 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
})
const handleTextChange = (id: string | number, propertyName: string, value: string,) => {
setData(tableData.map(item => {
setTableData(tableData.map(item => {
if (item.id === id) {
return {
...item,
@ -74,7 +91,7 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
}));
}
const handleSwitchChange = (id: string | number, propertyName: string, value: string,) => {
setData(tableData.map(item => {
setTableData(tableData.map(item => {
if (item.id === id) {
return {
...item,
@ -86,19 +103,19 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
}
const handleDeleteRow = (id: string | number) => {
setData(tableData.filter(item => item.id !== id))
setTableData(tableData.filter(item => item.id !== id))
}
const handleAddRow = () => {
const newHost: FrigateHost = {
id: String(Math.random()),
const handleAddRow = (event: React.MouseEvent<HTMLButtonElement>) => {
const newHost: GetFrigateHost = {
id: ObjectId().toHexString(),
createAt: '',
updateAt: '',
host: '',
name: '',
enabled: true
}
setData(prevTableData => [...prevTableData, newHost])
setTableData(prevTableData => [...prevTableData, newHost])
}
const rows = tableData.map(item => {
@ -106,12 +123,10 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
<tr key={item.id}>
<TextInputCell text={item.name} width='40%' id={item.id} propertyName='name' onChange={handleTextChange} />
<TextInputCell text={item.host} width='40%' id={item.id} propertyName='host' onChange={handleTextChange} />
{/* {textInputCell(item.host, '40%', item.id, 'host', handleTextChange)} */}
<SwitchCell value={item.enabled} width='10%' id={item.id} propertyName='enabled' toggle={handleSwitchChange} />
<td align='right' style={{ width: '10%', padding: '0', }}>
<Flex justify='center'>
<Button size='xs' ><IconSettings /></Button>
<Button size='xs' ><IconDeviceFloppy /></Button>
<HostSettingsMenu id={item.id} />
<Button size='xs' onClick={() => handleDeleteRow(item.id)}><IconTrash /></Button>
</Flex>
</td>
@ -133,7 +148,7 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
</Table>
{showAddButton ?
<Flex w='100%' justify='end'>
<Button size='xs' onClick={() => handleAddRow()}><IconPlus /></Button>
<Button size='xs' onClick={handleAddRow}><IconPlus /></Button>
</Flex>
: <></>
}

View File

@ -1,15 +0,0 @@
import React from 'react';
import { SideBar, SideBarProps, } from '../shared/components/SideBar';
interface LeftSideBarProps {
isHidden: (isHidden: boolean) => void
}
const LeftSideBar = ({ isHidden }: LeftSideBarProps) => {
return (
<SideBar isHidden={isHidden} side="left" />
);
};
export default LeftSideBar;

View File

@ -1,18 +0,0 @@
import React from 'react';
import { SideBar, SideBarProps } from '../shared/components/SideBar';
import { Text } from '@mantine/core';
import { v4 as uuidv4 } from 'uuid'
interface RightSideBarProps {
isHidden: (isHidden: boolean) => void
}
const RightSideBar = ({ isHidden }: RightSideBarProps) => {
return (
<SideBar isHidden={isHidden} side="right">
</SideBar>
);
};
export default RightSideBar;

View File

@ -5,7 +5,7 @@ import { useAuth } from 'react-oidc-context';
import { useNavigate } from 'react-router-dom';
import ColorSchemeToggle from "../../shared/components/ColorSchemeToggle";
import Logo from "../../shared/components/Logo";
import { pathRoutes } from "../../router/routes.path";
import { routesPath } from "../../router/routes.path";
const HEADER_HEIGHT = rem(60)
@ -93,7 +93,7 @@ export const HeaderAction = ({ links }: HeaderActionProps) => {
<Header height={HEADER_HEIGHT} sx={{ borderBottom: 0 }}>
<Container className={classes.inner} fluid>
<Flex wrap='nowrap' >
<Logo onClick={() => handleNavigate(pathRoutes.MAIN_PATH)} />
<Logo onClick={() => handleNavigate(routesPath.MAIN_PATH)} />
<Burger opened={opened} onClick={toggle} className={classes.burger} size="sm" />
<Flex className={classes.leftLinksMenu}>
{ items }

View File

@ -1,14 +1,14 @@
import { pathRoutes } from "../../router/routes.path";
import { routesPath } from "../../router/routes.path";
import { headerMenu } from "../../shared/strings/header.menu.strings";
import { HeaderActionProps } from "./HeaderAction";
export const testHeaderLinks: HeaderActionProps =
{
links: [
{link: pathRoutes.MAIN_PATH, label: headerMenu.home, links: []},
{link: pathRoutes.TEST_PATH, label: headerMenu.test, links: []},
{link: pathRoutes.SETTINGS_PATH, label: headerMenu.settings, links: []},
{link: pathRoutes.HOST_CONFIG_PATH, label: headerMenu.hostsConfig, links: []},
{link: pathRoutes.ROLES_PATH, label: headerMenu.rolesAcess, links: []},
{link: routesPath.MAIN_PATH, label: headerMenu.home, links: []},
{link: routesPath.TEST_PATH, label: headerMenu.test, links: []},
{link: routesPath.SETTINGS_PATH, label: headerMenu.settings, links: []},
{link: routesPath.HOSTS_PATH, label: headerMenu.hostsConfig, links: []},
{link: routesPath.ROLES_PATH, label: headerMenu.acessSettings, links: []},
]
}

134
yarn.lock
View File

@ -1808,6 +1808,20 @@
resolved "https://registry.yarnpkg.com/@mantine/utils/-/utils-6.0.16.tgz#b39e47ef8fa4463322e9aa10cdd5980f4310b705"
integrity sha512-UFel9DbifL3zS8pTJlr6GfwGd6464OWXCJdUq0oLydgimbC1VV2PnptBr6FMwIpPVcxouLOtY1cChzwFH95PSA==
"@monaco-editor/loader@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558"
integrity sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==
dependencies:
state-local "^1.0.6"
"@monaco-editor/react@^4.6.0":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.6.0.tgz#bcc68671e358a21c3814566b865a54b191e24119"
integrity sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==
dependencies:
"@monaco-editor/loader" "^1.4.0"
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1"
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
@ -2424,6 +2438,11 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
"@types/json-schema@^7.0.0":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/json5@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@ -3407,6 +3426,11 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
bson-objectid@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/bson-objectid/-/bson-objectid-2.0.4.tgz#339211572ef97dc98f2d68eaee7b99b7be59a089"
integrity sha512-vgnKAUzcDoa+AeyYwXCoHyF2q6u/8H46dxu5JN+4/TZeq/Dlinn0K6GvxsCLb3LHUJl0m/TLiEK31kUwtgocMQ==
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@ -5525,6 +5549,11 @@ icss-utils@^5.0.0, icss-utils@^5.1.0:
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
idb-keyval@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33"
integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==
idb@^7.0.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b"
@ -6557,6 +6586,11 @@ json5@^2.1.2, json5@^2.2.0, json5@^2.2.2:
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
jsonc-parser@^3.0.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a"
integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
@ -6908,6 +6942,53 @@ mobx@^6.9.0:
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.9.0.tgz#8a894c26417c05bed2cf7499322e589ee9787397"
integrity sha512-HdKewQEREEJgsWnErClfbFoVebze6rGazxFLU/XUyrII8dORfVszN1V0BMRnQSzcgsNNtkX8DHj3nC6cdWE9YQ==
monaco-editor@^0.46.0:
version "0.46.0"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.46.0.tgz#013e453fd2408997e4fe0bf67b36a80a24bc7bcc"
integrity sha512-ADwtLIIww+9FKybWscd7OCfm9odsFYHImBRI1v9AviGce55QY8raT+9ihH8jX/E/e6QVSGM+pKj4jSUSRmALNQ==
monaco-languageserver-types@^0.3.0:
version "0.3.2"
resolved "https://registry.yarnpkg.com/monaco-languageserver-types/-/monaco-languageserver-types-0.3.2.tgz#5e7c9ee50d01c68a64e28b40c3866ce4a46a752d"
integrity sha512-KiGVYK/DiX1pnacnOjGNlM85bhV3ZTyFlM+ce7B8+KpWCbF1XJVovu51YyuGfm+K7+K54mIpT4DFX16xmi+tYA==
dependencies:
monaco-types "^0.1.0"
vscode-languageserver-protocol "^3.0.0"
vscode-uri "^3.0.0"
monaco-marker-data-provider@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/monaco-marker-data-provider/-/monaco-marker-data-provider-1.1.1.tgz#0ca69f367152f5aa12cec2bda95f32b7403e876f"
integrity sha512-PGB7TJSZE5tmHzkxv/OEwK2RGNC2A7dcq4JRJnnj31CUAsfmw0Gl+1QTrH0W0deKhcQmQM0YVPaqgQ+0wCt8Mg==
monaco-types@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/monaco-types/-/monaco-types-0.1.0.tgz#3a3066aba499cb5923cd60efc736f3f14a169e10"
integrity sha512-aWK7SN9hAqNYi0WosPoMjenMeXJjwCxDibOqWffyQ/qXdzB/86xshGQobRferfmNz7BSNQ8GB0MD0oby9/5fTQ==
monaco-worker-manager@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz#f67c54dfca34ed4b225d5de84e77b24b4e36de8a"
integrity sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==
monaco-yaml@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/monaco-yaml/-/monaco-yaml-5.1.1.tgz#932ad3790707d6598eea2f71102e400a9a6dc074"
integrity sha512-BuZ0/ZCGjrPNRzYMZ/MoxH8F/SdM+mATENXnpOhDYABi1Eh+QvxSszEct+ACSCarZiwLvy7m6yEF/pvW8XJkyQ==
dependencies:
"@types/json-schema" "^7.0.0"
jsonc-parser "^3.0.0"
monaco-languageserver-types "^0.3.0"
monaco-marker-data-provider "^1.0.0"
monaco-types "^0.1.0"
monaco-worker-manager "^2.0.0"
path-browserify "^1.0.0"
prettier "^2.0.0"
vscode-languageserver-textdocument "^1.0.0"
vscode-languageserver-types "^3.0.0"
vscode-uri "^3.0.0"
yaml "^2.0.0"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@ -7264,6 +7345,11 @@ pascal-case@^3.1.2:
no-case "^3.0.4"
tslib "^2.0.3"
path-browserify@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
path-exists@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
@ -7900,6 +7986,11 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier@^2.0.0:
version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
@ -8231,6 +8322,11 @@ react-textarea-autosize@8.3.4:
use-composed-ref "^1.3.0"
use-latest "^1.2.1"
react-use-websocket@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/react-use-websocket/-/react-use-websocket-4.7.0.tgz#e45545ed48eb60171bf6401d1884cc80c700a0ea"
integrity sha512-YjR62jB7vB94IZy5UPBGZSR3c0hxu796q9IuJ0vbNg7InJ7Z84NHOd/LHzVI5nAKtaGy1oqvf8EmjKxX+cNz4A==
react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@ -8821,6 +8917,11 @@ stackframe@^1.3.4:
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310"
integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==
state-local@^1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5"
integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==
statuses@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
@ -9524,6 +9625,34 @@ vary@~1.1.2:
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
vscode-jsonrpc@8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9"
integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==
vscode-languageserver-protocol@^3.0.0:
version "3.17.5"
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea"
integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==
dependencies:
vscode-jsonrpc "8.2.0"
vscode-languageserver-types "3.17.5"
vscode-languageserver-textdocument@^1.0.0:
version "1.0.11"
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf"
integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==
vscode-languageserver-types@3.17.5, vscode-languageserver-types@^3.0.0:
version "3.17.5"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a"
integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==
vscode-uri@^3.0.0:
version "3.0.8"
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f"
integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==
w3c-hr-time@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
@ -10015,6 +10144,11 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yaml@^2.0.0:
version "2.3.4"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2"
integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==
yaml@^2.1.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"