add grid view
add config editor
This commit is contained in:
parent
d68edcf6f2
commit
2692e00787
@ -9,6 +9,7 @@
|
|||||||
"@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",
|
||||||
|
"@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",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
@ -20,19 +21,24 @@
|
|||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
"@types/validator": "^13.7.17",
|
"@types/validator": "^13.7.17",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
|
"bson-objectid": "^2.0.4",
|
||||||
"cookies-next": "^4.1.1",
|
"cookies-next": "^4.1.1",
|
||||||
"dayjs": "^1.11.9",
|
"dayjs": "^1.11.9",
|
||||||
"embla-carousel-react": "^8.0.0-rc10",
|
"embla-carousel-react": "^8.0.0-rc10",
|
||||||
|
"idb-keyval": "^6.2.1",
|
||||||
"mantine-react-table": "^1.0.0-beta.25",
|
"mantine-react-table": "^1.0.0-beta.25",
|
||||||
"mobx": "^6.9.0",
|
"mobx": "^6.9.0",
|
||||||
"mobx-react-lite": "^3.4.3",
|
"mobx-react-lite": "^3.4.3",
|
||||||
"mobx-utils": "^6.0.7",
|
"mobx-utils": "^6.0.7",
|
||||||
|
"monaco-editor": "^0.46.0",
|
||||||
|
"monaco-yaml": "^5.1.1",
|
||||||
"oidc-client-ts": "^2.2.4",
|
"oidc-client-ts": "^2.2.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-oidc-context": "^2.2.2",
|
"react-oidc-context": "^2.2.2",
|
||||||
"react-router-dom": "^6.14.1",
|
"react-router-dom": "^6.14.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"react-use-websocket": "^4.7.0",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^4.4.2",
|
||||||
"validator": "^13.9.0",
|
"validator": "^13.9.0",
|
||||||
"web-vitals": "^2.1.0",
|
"web-vitals": "^2.1.0",
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { AppShell, useMantineTheme, } from "@mantine/core"
|
import { AppShell, useMantineTheme, } from "@mantine/core"
|
||||||
import { HeaderAction } from './widgets/header/HeaderAction';
|
import { HeaderAction } from './widgets/header/HeaderAction';
|
||||||
import { testHeaderLinks } from './widgets/header/header.links';
|
import { testHeaderLinks } from './widgets/header/header.links';
|
||||||
import AppRouter from './router/AppRouter';
|
import AppRouter from './router/AppRouter';
|
||||||
import { SideBar } from './shared/components/SideBar';
|
import { SideBar } from './shared/components/SideBar';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { Context } from '.';
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
|
|||||||
65
src/hooks/use-camera-activity.ts
Normal file
65
src/hooks/use-camera-activity.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
55
src/hooks/use-camera-live-mode.ts
Normal file
55
src/hooks/use-camera-live-mode.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/hooks/use-persistence.ts
Normal file
45
src/hooks/use-persistence.ts
Normal 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];
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ import React, { useContext, useEffect, useState } from 'react';
|
|||||||
import CogWheelWithText from '../shared/components/CogWheelWithText';
|
import CogWheelWithText from '../shared/components/CogWheelWithText';
|
||||||
import { strings } from '../shared/strings/strings';
|
import { strings } from '../shared/strings/strings';
|
||||||
import { redirect, useNavigate } from 'react-router-dom';
|
import { redirect, useNavigate } from 'react-router-dom';
|
||||||
import { pathRoutes } from '../router/routes.path';
|
import { routesPath } from '../router/routes.path';
|
||||||
import { Context } from '..';
|
import { Context } from '..';
|
||||||
|
|
||||||
const Forbidden = () => {
|
const Forbidden = () => {
|
||||||
@ -11,12 +11,12 @@ const Forbidden = () => {
|
|||||||
const { sideBarsStore } = useContext(Context)
|
const { sideBarsStore } = useContext(Context)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sideBarsStore.setLeftSidebar(null)
|
sideBarsStore.setLeftChildren(null)
|
||||||
sideBarsStore.setRightSidebar(null)
|
sideBarsStore.setRightChildren(null)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleGoToMain = () => {
|
const handleGoToMain = () => {
|
||||||
window.location.replace(pathRoutes.MAIN_PATH)
|
window.location.replace(routesPath.MAIN_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React, { useContext, useEffect, useState } from 'react';
|
|||||||
import CogWheelWithText from '../shared/components/CogWheelWithText';
|
import CogWheelWithText from '../shared/components/CogWheelWithText';
|
||||||
import { strings } from '../shared/strings/strings';
|
import { strings } from '../shared/strings/strings';
|
||||||
import { redirect, useNavigate } from 'react-router-dom';
|
import { redirect, useNavigate } from 'react-router-dom';
|
||||||
import { pathRoutes } from '../router/routes.path';
|
import { routesPath } from '../router/routes.path';
|
||||||
import { Context } from '..';
|
import { Context } from '..';
|
||||||
|
|
||||||
const NotFound = () => {
|
const NotFound = () => {
|
||||||
@ -11,12 +11,12 @@ const NotFound = () => {
|
|||||||
const { sideBarsStore } = useContext(Context)
|
const { sideBarsStore } = useContext(Context)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sideBarsStore.setLeftSidebar(null)
|
sideBarsStore.setLeftChildren(null)
|
||||||
sideBarsStore.setRightSidebar(null)
|
sideBarsStore.setRightChildren(null)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleGoToMain = () => {
|
const handleGoToMain = () => {
|
||||||
window.location.replace(pathRoutes.MAIN_PATH)
|
window.location.replace(routesPath.MAIN_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,26 +1,89 @@
|
|||||||
import React from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import FrigateHostsTable from '../widgets/FrigateHostsTable';
|
import FrigateHostsTable from '../widgets/FrigateHostsTable';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { frigateApi } from '../services/frigate.proxy/frigate.api';
|
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 CenterLoader from '../shared/components/CenterLoader';
|
||||||
import RetryError from './RetryError';
|
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 FrigateHostsPage = observer(() => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const { isPending: hostsPending, error: hostsError, data, refetch } = useQuery({
|
const { isPending: hostsPending, error: hostsError, data } = useQuery({
|
||||||
queryKey: ['frigate-hosts'],
|
queryKey: [frigateQueryKeys.getFrigateHosts],
|
||||||
queryFn: frigateApi.getHosts,
|
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 (hostsPending) return <CenterLoader />
|
||||||
|
|
||||||
if (hostsError) return <RetryError />
|
if (hostsError) return <RetryError />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
})
|
||||||
|
|
||||||
export default FrigateHostsPage;
|
export default FrigateHostsPage;
|
||||||
119
src/pages/HostConfigPage.tsx
Normal file
119
src/pages/HostConfigPage.tsx
Normal 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;
|
||||||
14
src/pages/HostStoragePage.tsx
Normal file
14
src/pages/HostStoragePage.tsx
Normal 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;
|
||||||
14
src/pages/HostSystemPage.tsx
Normal file
14
src/pages/HostSystemPage.tsx
Normal 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;
|
||||||
@ -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 ProductTable, { TableAdapter } from '../widgets/ProductTable';
|
||||||
import HeadSearch from '../shared/components/HeadSearch';
|
import HeadSearch from '../shared/components/HeadSearch';
|
||||||
import ViewSelector, { SelectorViewState } from '../shared/components/ViewSelector';
|
import ViewSelector, { SelectorViewState } from '../shared/components/ViewSelector';
|
||||||
@ -8,11 +8,25 @@ import { getCookie, setCookie } from 'cookies-next';
|
|||||||
import { Context } from '..';
|
import { Context } from '..';
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import CenterLoader from '../shared/components/CenterLoader';
|
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 MainBody = observer(() => {
|
||||||
const { productStore, cartStore, sideBarsStore } = useContext(Context)
|
const { sideBarsStore } = useContext(Context)
|
||||||
const { updateProductFromServer, products, isLoading: productsLoading } = productStore
|
useEffect(() => {
|
||||||
const { updateCartFromServer, products: cartProducts, isLoading: cardLoading } = cartStore
|
sideBarsStore.rightVisible = false
|
||||||
|
sideBarsStore.setLeftChildren(null)
|
||||||
|
sideBarsStore.setRightChildren(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const [viewState, setTableState] = useState(getCookie('aps-main-view') as SelectorViewState || SelectorViewState.GRID)
|
const [viewState, setTableState] = useState(getCookie('aps-main-view') as SelectorViewState || SelectorViewState.GRID)
|
||||||
const handleToggleState = (state: SelectorViewState) => {
|
const handleToggleState = (state: SelectorViewState) => {
|
||||||
@ -20,21 +34,43 @@ const MainBody = observer(() => {
|
|||||||
setTableState(state)
|
setTableState(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: hosts, isPending, isError, refetch } = useQuery({
|
||||||
updateProductFromServer()
|
queryKey: [frigateQueryKeys.getFrigateHostsConfigs],
|
||||||
updateCartFromServer()
|
queryFn: async () => {
|
||||||
sideBarsStore.setLeftSidebar(<div />)
|
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 (isPending) return <CenterLoader />
|
||||||
if (productsLoading || cardLoading) return <div>Error</div> // add state manager
|
|
||||||
|
if (isError) return <RetryError onRetry={refetch} />
|
||||||
|
|
||||||
|
|
||||||
let tableData: TableAdapter[] = []
|
// const child = () => {
|
||||||
let gridData: GridAdapter[] = []
|
// return ( <Skeleton mih='20rem' miw='20rem' radius="md" animate={false} /> )
|
||||||
if (products && cartProducts) {
|
// }
|
||||||
tableData = productStore.mapToTable(products, cartProducts)
|
|
||||||
gridData = productStore.mapToGrid(products, cartProducts)
|
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 (
|
return (
|
||||||
@ -56,6 +92,13 @@ const MainBody = observer(() => {
|
|||||||
<ViewSelector state={viewState} onChange={handleToggleState} />
|
<ViewSelector state={viewState} onChange={handleToggleState} />
|
||||||
</Group>
|
</Group>
|
||||||
</Flex>
|
</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>
|
</Flex>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,29 +1,33 @@
|
|||||||
import { Flex, Button, Text } from '@mantine/core';
|
import { Flex, Button, Text } from '@mantine/core';
|
||||||
import React, { useContext, useEffect } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import { pathRoutes } from '../router/routes.path';
|
import { routesPath } from '../router/routes.path';
|
||||||
import { CogWheelHeartSVG } from '../shared/components/svg/CogWheelHeartSVG';
|
|
||||||
import { strings } from '../shared/strings/strings';
|
import { strings } from '../shared/strings/strings';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { ExclamationCogWheel } from '../shared/components/svg/ExclamationCogWheel';
|
import { ExclamationCogWheel } from '../shared/components/svg/ExclamationCogWheel';
|
||||||
import { Context } from '..';
|
import { Context } from '..';
|
||||||
|
|
||||||
const RetryError = () => {
|
interface RetryErrorProps {
|
||||||
|
onRetry?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const RetryError = ( {onRetry} : RetryErrorProps) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { sideBarsStore } = useContext(Context)
|
const { sideBarsStore } = useContext(Context)
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sideBarsStore.setLeftSidebar(null)
|
sideBarsStore.setLeftChildren(null)
|
||||||
sideBarsStore.setRightSidebar(null)
|
sideBarsStore.setRightChildren(null)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleGoToMain = () => {
|
const handleGoToMain = () => {
|
||||||
navigate(pathRoutes.MAIN_PATH)
|
navigate(routesPath.MAIN_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRetry(event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void {
|
function handleRetry(event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void {
|
||||||
throw new Error('Function not implemented.');
|
if (onRetry) onRetry()
|
||||||
|
else window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
useMutation,
|
useMutation,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from '@tanstack/react-query'
|
} 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 CenterLoader from '../shared/components/CenterLoader';
|
||||||
import RetryError from './RetryError';
|
import RetryError from './RetryError';
|
||||||
import { Button, Flex, Space } from '@mantine/core';
|
import { Button, Flex, Space } from '@mantine/core';
|
||||||
@ -12,16 +12,17 @@ import { FloatingLabelInput } from '../shared/components/FloatingLabelInput';
|
|||||||
import { strings } from '../shared/strings/strings';
|
import { strings } from '../shared/strings/strings';
|
||||||
import { dimensions } from '../shared/dimensions/dimensions';
|
import { dimensions } from '../shared/dimensions/dimensions';
|
||||||
import { useMediaQuery } from '@mantine/hooks';
|
import { useMediaQuery } from '@mantine/hooks';
|
||||||
|
import { GetConfig } from '../services/frigate.proxy/frigate.schema';
|
||||||
|
|
||||||
const SettingsPage = () => {
|
const SettingsPage = () => {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { isPending: configPending, error: configError, data, refetch } = useQuery({
|
const { isPending: configPending, error: configError, data, refetch } = useQuery({
|
||||||
queryKey: ['config'],
|
queryKey: [frigateQueryKeys.getConfig],
|
||||||
queryFn: frigateApi.getConfig,
|
queryFn: frigateApi.getConfig,
|
||||||
})
|
})
|
||||||
|
|
||||||
const ecryptedValue = '**********'
|
const ecryptedValue = '**********'
|
||||||
const mapEncryptedToView = (data: Config[] | undefined): Config[] | undefined => {
|
const mapEncryptedToView = (data: GetConfig[] | undefined): GetConfig[] | undefined => {
|
||||||
return data?.map(item => {
|
return data?.map(item => {
|
||||||
const { value, encrypted, ...rest } = item
|
const { value, encrypted, ...rest } = item
|
||||||
if (encrypted) return { value: ecryptedValue, encrypted, ...rest }
|
if (encrypted) return { value: ecryptedValue, encrypted, ...rest }
|
||||||
@ -35,7 +36,7 @@ const SettingsPage = () => {
|
|||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: frigateApi.putConfig,
|
mutationFn: frigateApi.putConfig,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['config'] })
|
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getConfig] })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -82,7 +83,7 @@ const SettingsPage = () => {
|
|||||||
|
|
||||||
if (configPending) return <CenterLoader />
|
if (configPending) return <CenterLoader />
|
||||||
|
|
||||||
if (configError) return <RetryError />
|
if (configError) return <RetryError onRetry={refetch} />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex h='100%'>
|
<Flex h='100%'>
|
||||||
|
|||||||
@ -1,51 +1,29 @@
|
|||||||
import React, { Fragment, useContext, useEffect } from 'react';
|
import React, { Fragment, useContext, useEffect } from 'react';
|
||||||
import { Context } from '..';
|
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import JSMpegPlayer from '../shared/components/JSMpegPlayer';
|
import JSMpegPlayer from '../shared/components/frigate/JSMpegPlayer';
|
||||||
import { cameraLiveViewURL } from '../router/frigate.routes';
|
import { frigateApi } from '../services/frigate.proxy/frigate.api';
|
||||||
|
import { Flex } from '@mantine/core';
|
||||||
|
import AutoUpdatingCameraImage from '../shared/components/frigate/AutoUpdatingCameraImage';
|
||||||
|
|
||||||
const Test = observer(() => {
|
const Test = observer(() => {
|
||||||
// const { postStore } = useContext(Context)
|
// const test = {
|
||||||
// const { getPostsAction, posts } = postStore
|
// camera: 'Buhgalteria',
|
||||||
|
// host: 'localhost:5000',
|
||||||
|
// width: 800,
|
||||||
|
// height: 600,
|
||||||
|
// url : function() { return frigateApi.cameraWsURL(this.host, this.camera)},
|
||||||
|
// }
|
||||||
|
|
||||||
// useEffect( () => {
|
// return (
|
||||||
// console.log("render Test")
|
// <Flex w='100%' h='100%'>
|
||||||
// getPostsAction()
|
// <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 (
|
return (
|
||||||
<Fragment>
|
<Flex w='100%' h='100%'>
|
||||||
<div>
|
|
||||||
<JSMpegPlayer url={test.url()} camera={test.camera} width={test.width} height={test.height} />
|
</Flex>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Navigate, Route, Routes } from "react-router-dom";
|
import { Navigate, Route, Routes } from "react-router-dom";
|
||||||
import { routes } from "./routes";
|
import { routes } from "./routes";
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { pathRoutes } from "./routes.path";
|
import { routesPath } from "./routes.path";
|
||||||
|
|
||||||
const AppRouter = () => {
|
const AppRouter = () => {
|
||||||
return (
|
return (
|
||||||
@ -9,7 +9,7 @@ const AppRouter = () => {
|
|||||||
{routes.map(({ path, component }) =>
|
{routes.map(({ path, component }) =>
|
||||||
<Route key={uuidv4()} path={path} element={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>
|
</Routes>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}`
|
|
||||||
}
|
|
||||||
@ -1,10 +1,13 @@
|
|||||||
export const pathRoutes = {
|
export const routesPath = {
|
||||||
MAIN_PATH: '/',
|
MAIN_PATH: '/',
|
||||||
BIRDSEYE_PATH: '/birdseye',
|
BIRDSEYE_PATH: '/birdseye',
|
||||||
EVENTS_PATH: '/events',
|
EVENTS_PATH: '/events',
|
||||||
RECORDINGS_PATH: '/recordings',
|
RECORDINGS_PATH: '/recordings',
|
||||||
SETTINGS_PATH: '/settings',
|
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',
|
ROLES_PATH: '/roles',
|
||||||
LIVE_PATH: '/live',
|
LIVE_PATH: '/live',
|
||||||
THANKS_PATH: '/thanks',
|
THANKS_PATH: '/thanks',
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import {JSX} from "react";
|
import {JSX} from "react";
|
||||||
import Test from "../pages/Test"
|
import Test from "../pages/Test"
|
||||||
import MainBody from "../pages/MainBody";
|
import MainBody from "../pages/MainBody";
|
||||||
import {pathRoutes} from "./routes.path";
|
import {routesPath} from "./routes.path";
|
||||||
import RetryError from "../pages/RetryError";
|
import RetryError from "../pages/RetryError";
|
||||||
import Forbidden from "../pages/403";
|
import Forbidden from "../pages/403";
|
||||||
import NotFound from "../pages/404";
|
import NotFound from "../pages/404";
|
||||||
import SettingsPage from "../pages/SettingsPage";
|
import SettingsPage from "../pages/SettingsPage";
|
||||||
import FrigateHostsPage from "../pages/FrigateHostsPage";
|
import FrigateHostsPage from "../pages/FrigateHostsPage";
|
||||||
|
import HostConfigPage from "../pages/HostConfigPage";
|
||||||
|
import HostSystemPage from "../pages/HostSystemPage";
|
||||||
|
import HostStoragePage from "../pages/HostStoragePage";
|
||||||
|
|
||||||
interface IRoute {
|
interface IRoute {
|
||||||
path: string,
|
path: string,
|
||||||
@ -15,31 +18,43 @@ interface IRoute {
|
|||||||
|
|
||||||
export const routes: IRoute[] = [
|
export const routes: IRoute[] = [
|
||||||
{ //todo delete
|
{ //todo delete
|
||||||
path: pathRoutes.TEST_PATH,
|
path: routesPath.TEST_PATH,
|
||||||
component: <Test />,
|
component: <Test />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: pathRoutes.SETTINGS_PATH,
|
path: routesPath.SETTINGS_PATH,
|
||||||
component: <SettingsPage />,
|
component: <SettingsPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: pathRoutes.HOST_CONFIG_PATH,
|
path: routesPath.HOSTS_PATH,
|
||||||
component: <FrigateHostsPage />,
|
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 />,
|
component: <MainBody />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: pathRoutes.RETRY_ERROR_PATH,
|
path: routesPath.RETRY_ERROR_PATH,
|
||||||
component: <RetryError />,
|
component: <RetryError />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: pathRoutes.FORBIDDEN_ERROR_PATH,
|
path: routesPath.FORBIDDEN_ERROR_PATH,
|
||||||
component: <Forbidden />,
|
component: <Forbidden />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: pathRoutes.NOT_FOUND_ERROR_PATH,
|
path: routesPath.NOT_FOUND_ERROR_PATH,
|
||||||
component: <NotFound />,
|
component: <NotFound />,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -1,25 +1,9 @@
|
|||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import { proxyURL } from "../../shared/env.const"
|
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({
|
const instance = axios.create({
|
||||||
baseURL: proxyURL.toString(),
|
baseURL: proxyURL.toString(),
|
||||||
@ -27,10 +11,50 @@ const instance = axios.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const frigateApi = {
|
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),
|
putConfig: (config: PutConfig[]) => instance.put('apiv1/config', config).then(res => res.data),
|
||||||
getHosts: () => instance.get<FrigateHost[]>('apiv1/frigate-hosts').then(res => {
|
getHosts: () => instance.get<GetFrigateHost[]>('apiv1/frigate-hosts').then(res => {
|
||||||
// return new Map(res.data.map(item => [item.id, item]))
|
|
||||||
return res.data
|
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',
|
||||||
}
|
}
|
||||||
63
src/services/frigate.proxy/frigate.schema.ts
Normal file
63
src/services/frigate.proxy/frigate.schema.ts
Normal 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
235
src/services/ws.tsx
Normal 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 };
|
||||||
|
}
|
||||||
74
src/shared/components/CameraCard.tsx
Normal file
74
src/shared/components/CameraCard.tsx
Normal 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;
|
||||||
@ -43,32 +43,38 @@ export const SideBar = observer(({ isHidden, side, children }: SideBarProps) =>
|
|||||||
|
|
||||||
const { sideBarsStore } = useContext(Context)
|
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>(() => {
|
const [leftChildren, setLeftChildren] = useState<React.ReactNode>(() => {
|
||||||
if (children && side === 'left') return children
|
if (children && side === 'left') return children
|
||||||
else if (sideBarsStore.leftSideBar) return sideBarsStore.leftSideBar
|
else if (sideBarsStore.leftChildren) return sideBarsStore.leftChildren
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
const [rightChildren, setRightChildren] = useState<React.ReactNode>(() => {
|
const [rightChildren, setRightChildren] = useState<React.ReactNode>(() => {
|
||||||
if (children && side === 'right') return children
|
if (children && side === 'right') return children
|
||||||
else if (sideBarsStore.rightSideBar) return sideBarsStore.rightSideBar
|
else if (sideBarsStore.rightChildren) return sideBarsStore.rightChildren
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect(() => {
|
||||||
setLeftChildren(sideBarsStore.leftSideBar)
|
setLeftChildren(sideBarsStore.leftChildren)
|
||||||
}, [sideBarsStore.leftSideBar])
|
}, [sideBarsStore.leftChildren])
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect(() => {
|
||||||
setRightChildren(sideBarsStore.rightSideBar)
|
setRightChildren(sideBarsStore.rightChildren)
|
||||||
}, [sideBarsStore.rightSideBar])
|
}, [sideBarsStore.rightChildren])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isHidden(!visible)
|
isHidden(!visible)
|
||||||
}, [visible])
|
}, [visible])
|
||||||
|
|
||||||
useEffect(() => {
|
// resize controller
|
||||||
}, [manualVisible.current])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkWindowSize = () => {
|
const checkWindowSize = () => {
|
||||||
if (window.innerWidth <= hideSizePx && visible) {
|
if (window.innerWidth <= hideSizePx && visible) {
|
||||||
|
|||||||
96
src/shared/components/frigate/AutoUpdatingCameraImage.tsx
Normal file
96
src/shared/components/frigate/AutoUpdatingCameraImage.tsx
Normal 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 >
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/shared/components/frigate/CameraImage.tsx
Normal file
53
src/shared/components/frigate/CameraImage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
159
src/shared/components/frigate/DebugCameraImage.tsx
Normal file
159
src/shared/components/frigate/DebugCameraImage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,11 +1,11 @@
|
|||||||
// @ts-ignore we know this doesn't have types
|
// @ts-ignore we know this doesn't have types
|
||||||
import JSMpeg from "@cycjimmy/jsmpeg-player";
|
import JSMpeg from "@cycjimmy/jsmpeg-player";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
import { useResizeObserver } from "../utils/resize-observer";
|
import { useResizeObserver } from "../../utils/resize-observer";
|
||||||
|
|
||||||
type JSMpegPlayerProps = {
|
type JSMpegPlayerProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
url: string;
|
wsUrl: string;
|
||||||
camera: string;
|
camera: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
@ -13,7 +13,7 @@ type JSMpegPlayerProps = {
|
|||||||
|
|
||||||
export default function JSMpegPlayer({
|
export default function JSMpegPlayer({
|
||||||
camera,
|
camera,
|
||||||
url,
|
wsUrl,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
className,
|
className,
|
||||||
@ -60,7 +60,7 @@ export default function JSMpegPlayer({
|
|||||||
|
|
||||||
const video = new JSMpeg.VideoElement(
|
const video = new JSMpeg.VideoElement(
|
||||||
playerRef.current,
|
playerRef.current,
|
||||||
url,
|
wsUrl,
|
||||||
{},
|
{},
|
||||||
{ protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 }
|
{ protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 }
|
||||||
);
|
);
|
||||||
@ -83,7 +83,7 @@ export default function JSMpegPlayer({
|
|||||||
playerRef.current = null;
|
playerRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [url]);
|
}, [wsUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} ref={containerRef}>
|
<div className={className} ref={containerRef}>
|
||||||
269
src/shared/components/frigate/MsePlayer.tsx
Normal file
269
src/shared/components/frigate/MsePlayer.tsx
Normal 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;
|
||||||
105
src/shared/components/frigate/Player.tsx
Normal file
105
src/shared/components/frigate/Player.tsx
Normal 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;
|
||||||
168
src/shared/components/frigate/WebRTCPlayer.tsx
Normal file
168
src/shared/components/frigate/WebRTCPlayer.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
src/shared/components/frigate/card.tsx
Normal file
80
src/shared/components/frigate/card.tsx
Normal 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 }
|
||||||
61
src/shared/components/menu/HostSettingsMenu.tsx
Normal file
61
src/shared/components/menu/HostSettingsMenu.tsx
Normal 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
|
||||||
@ -1,26 +1,43 @@
|
|||||||
import { makeAutoObservable } from "mobx"
|
import { makeAutoObservable } from "mobx"
|
||||||
|
|
||||||
export class SideBarsStore {
|
export class SideBarsStore {
|
||||||
private _leftSideBar: React.ReactNode = null
|
|
||||||
public get leftSideBar(): React.ReactNode {
|
private _rightVisible: boolean = true
|
||||||
return this._leftSideBar
|
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
|
private _rightChildren: React.ReactNode = null
|
||||||
public get rightSideBar(): React.ReactNode {
|
public get rightChildren(): React.ReactNode {
|
||||||
return this._rightSideBar
|
return this._rightChildren
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
makeAutoObservable(this)
|
makeAutoObservable(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
setRightSidebar = (value: React.ReactNode) => {
|
setRightChildren = (value: React.ReactNode) => {
|
||||||
this._rightSideBar = value
|
this._rightChildren = value
|
||||||
}
|
}
|
||||||
|
|
||||||
setLeftSidebar = (value: React.ReactNode) => {
|
setLeftChildren = (value: React.ReactNode) => {
|
||||||
this._leftSideBar = value
|
this._leftChildren = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,6 +2,6 @@ export const headerMenu = {
|
|||||||
home:"На главную",
|
home:"На главную",
|
||||||
test:"Тест",
|
test:"Тест",
|
||||||
settings:"Настройки",
|
settings:"Настройки",
|
||||||
rolesAcess:"Доступ",
|
acessSettings:" Настройка доступа",
|
||||||
hostsConfig:"Серверы Frigate",
|
hostsConfig:"Серверы Frigate",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,6 +60,7 @@ export const strings = {
|
|||||||
weight: "Вес:",
|
weight: "Вес:",
|
||||||
total: "Итого:",
|
total: "Итого:",
|
||||||
confirm: "Подтвердить",
|
confirm: "Подтвердить",
|
||||||
|
save: "Сохранить",
|
||||||
discard: "Отменить",
|
discard: "Отменить",
|
||||||
next: "Далее",
|
next: "Далее",
|
||||||
back: "Назад",
|
back: "Назад",
|
||||||
|
|||||||
406
src/types/frigateConfig.ts
Normal file
406
src/types/frigateConfig.ts
Normal 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
1
src/types/live.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type LivePlayerMode = "webrtc" | "mse" | "jsmpeg" | "debug";
|
||||||
36
src/types/ws.ts
Normal file
36
src/types/ws.ts
Normal 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"
|
||||||
@ -1,25 +1,42 @@
|
|||||||
import { Button, Flex, Switch, Table, Text, TextInput, useMantineTheme } from '@mantine/core';
|
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 SortedTh from '../shared/components/table.aps/SortedTh';
|
||||||
import { strings } from '../shared/strings/strings';
|
import { strings } from '../shared/strings/strings';
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
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 { IconBulbFilled, IconBulbOff, IconDeviceFloppy, IconPencil, IconPlus, IconSettings, IconTrash } from '@tabler/icons-react';
|
||||||
import SwitchCell from '../shared/components/table.aps/SwitchCell';
|
import SwitchCell from '../shared/components/hosts.table/SwitchCell';
|
||||||
import TextInputCell from '../shared/components/table.aps/TextInputCell';
|
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 {
|
interface TableProps<T> {
|
||||||
data: FrigateHost[],
|
data: T[],
|
||||||
showAddButton?: boolean,
|
showAddButton?: boolean,
|
||||||
handleInputChange?: () => void,
|
saveCallback?: (tableData: T[]) => void,
|
||||||
handleSwtitchToggle?: () => void,
|
changedCallback?: (tableData: T[]) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, handleSwtitchToggle }: FrigateHostsTableProps) => {
|
const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedCallback }: TableProps<GetFrigateHost>) => {
|
||||||
const [tableData, setData] = useState(data)
|
console.log('FrigateHostsTable rendered')
|
||||||
|
const [tableData, setTableData] = useState(data)
|
||||||
const [reversed, setReversed] = useState(false)
|
const [reversed, setReversed] = useState(false)
|
||||||
const [sortedName, setSortedName] = useState<string | null>(null)
|
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[] {
|
function sortByKey<T, K extends keyof T>(array: T[], key: K): T[] {
|
||||||
return array.sort((a, b) => {
|
return array.sort((a, b) => {
|
||||||
let valueA = a[key];
|
let valueA = a[key];
|
||||||
@ -37,9 +54,9 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
|
|||||||
const handleSort = (headName: string, propertyName: string,) => {
|
const handleSort = (headName: string, propertyName: string,) => {
|
||||||
const reverse = headName === sortedName ? !reversed : false;
|
const reverse = headName === sortedName ? !reversed : false;
|
||||||
setReversed(reverse)
|
setReversed(reverse)
|
||||||
const arr = sortByKey(tableData, propertyName as keyof FrigateHost)
|
const arr = sortByKey(tableData, propertyName as keyof GetFrigateHost)
|
||||||
if (reverse) arr.reverse()
|
if (reverse) arr.reverse()
|
||||||
setData(arr)
|
setTableData(arr)
|
||||||
setSortedName(headName)
|
setSortedName(headName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +80,7 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
|
|||||||
})
|
})
|
||||||
|
|
||||||
const handleTextChange = (id: string | number, propertyName: string, value: string,) => {
|
const handleTextChange = (id: string | number, propertyName: string, value: string,) => {
|
||||||
setData(tableData.map(item => {
|
setTableData(tableData.map(item => {
|
||||||
if (item.id === id) {
|
if (item.id === id) {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
@ -74,7 +91,7 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
const handleSwitchChange = (id: string | number, propertyName: string, value: string,) => {
|
const handleSwitchChange = (id: string | number, propertyName: string, value: string,) => {
|
||||||
setData(tableData.map(item => {
|
setTableData(tableData.map(item => {
|
||||||
if (item.id === id) {
|
if (item.id === id) {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
@ -86,19 +103,19 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteRow = (id: string | number) => {
|
const handleDeleteRow = (id: string | number) => {
|
||||||
setData(tableData.filter(item => item.id !== id))
|
setTableData(tableData.filter(item => item.id !== id))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddRow = () => {
|
const handleAddRow = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
const newHost: FrigateHost = {
|
const newHost: GetFrigateHost = {
|
||||||
id: String(Math.random()),
|
id: ObjectId().toHexString(),
|
||||||
createAt: '',
|
createAt: '',
|
||||||
updateAt: '',
|
updateAt: '',
|
||||||
host: '',
|
host: '',
|
||||||
name: '',
|
name: '',
|
||||||
enabled: true
|
enabled: true
|
||||||
}
|
}
|
||||||
setData(prevTableData => [...prevTableData, newHost])
|
setTableData(prevTableData => [...prevTableData, newHost])
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows = tableData.map(item => {
|
const rows = tableData.map(item => {
|
||||||
@ -106,12 +123,10 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
|
|||||||
<tr key={item.id}>
|
<tr key={item.id}>
|
||||||
<TextInputCell text={item.name} width='40%' id={item.id} propertyName='name' onChange={handleTextChange} />
|
<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 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} />
|
<SwitchCell value={item.enabled} width='10%' id={item.id} propertyName='enabled' toggle={handleSwitchChange} />
|
||||||
<td align='right' style={{ width: '10%', padding: '0', }}>
|
<td align='right' style={{ width: '10%', padding: '0', }}>
|
||||||
<Flex justify='center'>
|
<Flex justify='center'>
|
||||||
<Button size='xs' ><IconSettings /></Button>
|
<HostSettingsMenu id={item.id} />
|
||||||
<Button size='xs' ><IconDeviceFloppy /></Button>
|
|
||||||
<Button size='xs' onClick={() => handleDeleteRow(item.id)}><IconTrash /></Button>
|
<Button size='xs' onClick={() => handleDeleteRow(item.id)}><IconTrash /></Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</td>
|
</td>
|
||||||
@ -133,7 +148,7 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
|
|||||||
</Table>
|
</Table>
|
||||||
{showAddButton ?
|
{showAddButton ?
|
||||||
<Flex w='100%' justify='end'>
|
<Flex w='100%' justify='end'>
|
||||||
<Button size='xs' onClick={() => handleAddRow()}><IconPlus /></Button>
|
<Button size='xs' onClick={handleAddRow}><IconPlus /></Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
: <></>
|
: <></>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -5,7 +5,7 @@ import { useAuth } from 'react-oidc-context';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import ColorSchemeToggle from "../../shared/components/ColorSchemeToggle";
|
import ColorSchemeToggle from "../../shared/components/ColorSchemeToggle";
|
||||||
import Logo from "../../shared/components/Logo";
|
import Logo from "../../shared/components/Logo";
|
||||||
import { pathRoutes } from "../../router/routes.path";
|
import { routesPath } from "../../router/routes.path";
|
||||||
|
|
||||||
const HEADER_HEIGHT = rem(60)
|
const HEADER_HEIGHT = rem(60)
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ export const HeaderAction = ({ links }: HeaderActionProps) => {
|
|||||||
<Header height={HEADER_HEIGHT} sx={{ borderBottom: 0 }}>
|
<Header height={HEADER_HEIGHT} sx={{ borderBottom: 0 }}>
|
||||||
<Container className={classes.inner} fluid>
|
<Container className={classes.inner} fluid>
|
||||||
<Flex wrap='nowrap' >
|
<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" />
|
<Burger opened={opened} onClick={toggle} className={classes.burger} size="sm" />
|
||||||
<Flex className={classes.leftLinksMenu}>
|
<Flex className={classes.leftLinksMenu}>
|
||||||
{ items }
|
{ items }
|
||||||
|
|||||||
@ -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 { headerMenu } from "../../shared/strings/header.menu.strings";
|
||||||
import { HeaderActionProps } from "./HeaderAction";
|
import { HeaderActionProps } from "./HeaderAction";
|
||||||
|
|
||||||
export const testHeaderLinks: HeaderActionProps =
|
export const testHeaderLinks: HeaderActionProps =
|
||||||
{
|
{
|
||||||
links: [
|
links: [
|
||||||
{link: pathRoutes.MAIN_PATH, label: headerMenu.home, links: []},
|
{link: routesPath.MAIN_PATH, label: headerMenu.home, links: []},
|
||||||
{link: pathRoutes.TEST_PATH, label: headerMenu.test, links: []},
|
{link: routesPath.TEST_PATH, label: headerMenu.test, links: []},
|
||||||
{link: pathRoutes.SETTINGS_PATH, label: headerMenu.settings, links: []},
|
{link: routesPath.SETTINGS_PATH, label: headerMenu.settings, links: []},
|
||||||
{link: pathRoutes.HOST_CONFIG_PATH, label: headerMenu.hostsConfig, links: []},
|
{link: routesPath.HOSTS_PATH, label: headerMenu.hostsConfig, links: []},
|
||||||
{link: pathRoutes.ROLES_PATH, label: headerMenu.rolesAcess, links: []},
|
{link: routesPath.ROLES_PATH, label: headerMenu.acessSettings, links: []},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
134
yarn.lock
134
yarn.lock
@ -1808,6 +1808,20 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@mantine/utils/-/utils-6.0.16.tgz#b39e47ef8fa4463322e9aa10cdd5980f4310b705"
|
resolved "https://registry.yarnpkg.com/@mantine/utils/-/utils-6.0.16.tgz#b39e47ef8fa4463322e9aa10cdd5980f4310b705"
|
||||||
integrity sha512-UFel9DbifL3zS8pTJlr6GfwGd6464OWXCJdUq0oLydgimbC1VV2PnptBr6FMwIpPVcxouLOtY1cChzwFH95PSA==
|
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":
|
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
|
||||||
version "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"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
|
||||||
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
|
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":
|
"@types/json5@^0.0.29":
|
||||||
version "0.0.29"
|
version "0.0.29"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||||
@ -3407,6 +3426,11 @@ bser@2.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
node-int64 "^0.4.0"
|
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:
|
buffer-from@^1.0.0:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
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"
|
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
|
||||||
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
|
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:
|
idb@^7.0.1:
|
||||||
version "7.1.1"
|
version "7.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b"
|
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"
|
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
|
||||||
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
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:
|
jsonfile@^6.0.1:
|
||||||
version "6.1.0"
|
version "6.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
|
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"
|
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.9.0.tgz#8a894c26417c05bed2cf7499322e589ee9787397"
|
||||||
integrity sha512-HdKewQEREEJgsWnErClfbFoVebze6rGazxFLU/XUyrII8dORfVszN1V0BMRnQSzcgsNNtkX8DHj3nC6cdWE9YQ==
|
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:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
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"
|
no-case "^3.0.4"
|
||||||
tslib "^2.0.3"
|
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:
|
path-exists@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
|
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"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
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:
|
pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
|
||||||
version "5.6.0"
|
version "5.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
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-composed-ref "^1.3.0"
|
||||||
use-latest "^1.2.1"
|
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:
|
react@^18.2.0:
|
||||||
version "18.2.0"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
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"
|
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310"
|
||||||
integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==
|
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:
|
statuses@2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
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"
|
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||||
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
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:
|
w3c-hr-time@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
|
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"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
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:
|
yaml@^2.1.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user