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/dates": "^6.0.16",
|
||||
"@mantine/hooks": "^6.0.16",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@tabler/icons-react": "^2.24.0",
|
||||
"@tanstack/react-query": "^5.21.2",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
@ -20,19 +21,24 @@
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/validator": "^13.7.17",
|
||||
"axios": "^1.4.0",
|
||||
"bson-objectid": "^2.0.4",
|
||||
"cookies-next": "^4.1.1",
|
||||
"dayjs": "^1.11.9",
|
||||
"embla-carousel-react": "^8.0.0-rc10",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"mantine-react-table": "^1.0.0-beta.25",
|
||||
"mobx": "^6.9.0",
|
||||
"mobx-react-lite": "^3.4.3",
|
||||
"mobx-utils": "^6.0.7",
|
||||
"monaco-editor": "^0.46.0",
|
||||
"monaco-yaml": "^5.1.1",
|
||||
"oidc-client-ts": "^2.2.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-oidc-context": "^2.2.2",
|
||||
"react-router-dom": "^6.14.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-use-websocket": "^4.7.0",
|
||||
"typescript": "^4.4.2",
|
||||
"validator": "^13.9.0",
|
||||
"web-vitals": "^2.1.0",
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { AppShell, useMantineTheme, } from "@mantine/core"
|
||||
import { HeaderAction } from './widgets/header/HeaderAction';
|
||||
import { testHeaderLinks } from './widgets/header/header.links';
|
||||
import AppRouter from './router/AppRouter';
|
||||
import { SideBar } from './shared/components/SideBar';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Context } from '.';
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
|
||||
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 { strings } from '../shared/strings/strings';
|
||||
import { redirect, useNavigate } from 'react-router-dom';
|
||||
import { pathRoutes } from '../router/routes.path';
|
||||
import { routesPath } from '../router/routes.path';
|
||||
import { Context } from '..';
|
||||
|
||||
const Forbidden = () => {
|
||||
@ -11,12 +11,12 @@ const Forbidden = () => {
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
|
||||
useEffect(() => {
|
||||
sideBarsStore.setLeftSidebar(null)
|
||||
sideBarsStore.setRightSidebar(null)
|
||||
sideBarsStore.setLeftChildren(null)
|
||||
sideBarsStore.setRightChildren(null)
|
||||
}, [])
|
||||
|
||||
const handleGoToMain = () => {
|
||||
window.location.replace(pathRoutes.MAIN_PATH)
|
||||
window.location.replace(routesPath.MAIN_PATH)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -3,7 +3,7 @@ import React, { useContext, useEffect, useState } from 'react';
|
||||
import CogWheelWithText from '../shared/components/CogWheelWithText';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { redirect, useNavigate } from 'react-router-dom';
|
||||
import { pathRoutes } from '../router/routes.path';
|
||||
import { routesPath } from '../router/routes.path';
|
||||
import { Context } from '..';
|
||||
|
||||
const NotFound = () => {
|
||||
@ -11,12 +11,12 @@ const NotFound = () => {
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
|
||||
useEffect(() => {
|
||||
sideBarsStore.setLeftSidebar(null)
|
||||
sideBarsStore.setRightSidebar(null)
|
||||
sideBarsStore.setLeftChildren(null)
|
||||
sideBarsStore.setRightChildren(null)
|
||||
}, [])
|
||||
|
||||
const handleGoToMain = () => {
|
||||
window.location.replace(pathRoutes.MAIN_PATH)
|
||||
window.location.replace(routesPath.MAIN_PATH)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -1,26 +1,89 @@
|
||||
import React from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import FrigateHostsTable from '../widgets/FrigateHostsTable';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { frigateApi } from '../services/frigate.proxy/frigate.api';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import { deleteFrigateHostSchema, GetFrigateHost, putFrigateHostSchema} from '../services/frigate.proxy/frigate.schema';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import RetryError from './RetryError';
|
||||
import { Context } from '..';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { Button, Flex } from '@mantine/core';
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
const FrigateHostsPage = () => {
|
||||
|
||||
const { isPending: hostsPending, error: hostsError, data, refetch } = useQuery({
|
||||
queryKey: ['frigate-hosts'],
|
||||
const FrigateHostsPage = observer(() => {
|
||||
const queryClient = useQueryClient()
|
||||
const { isPending: hostsPending, error: hostsError, data } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getFrigateHosts],
|
||||
queryFn: frigateApi.getHosts,
|
||||
})
|
||||
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
useEffect(() => {
|
||||
sideBarsStore.rightVisible = false
|
||||
sideBarsStore.setLeftChildren(null)
|
||||
sideBarsStore.setRightChildren(null)
|
||||
}, [])
|
||||
|
||||
const [pageData, setPageData] = useState(data)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) setPageData(data)
|
||||
}, [data])
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: (tableData: GetFrigateHost[]) => {
|
||||
let fetchPromises = []
|
||||
const toDelete = data?.filter(host => !tableData.some(table => table.id === host.id))
|
||||
if (toDelete && toDelete.length > 0) {
|
||||
const parsedDelete = deleteFrigateHostSchema.array().parse(toDelete)
|
||||
fetchPromises.push(frigateApi.deleteHosts(parsedDelete))
|
||||
}
|
||||
if (tableData && tableData.length > 0) {
|
||||
const parsedChanged = putFrigateHostSchema.array().parse(tableData)
|
||||
fetchPromises.push(frigateApi.putHosts(parsedChanged))
|
||||
}
|
||||
return Promise.all(fetchPromises)
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getFrigateHosts] })
|
||||
},
|
||||
onError: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getFrigateHosts] })
|
||||
},
|
||||
onSettled: () => {
|
||||
if (data) setPageData([...data])
|
||||
}
|
||||
})
|
||||
|
||||
const handleSave = () => {
|
||||
if (pageData) {
|
||||
mutate(pageData)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (data: GetFrigateHost[]) => {
|
||||
setPageData(data)
|
||||
}
|
||||
|
||||
const handleDiscard = () => {
|
||||
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getFrigateHosts] })
|
||||
if (data) setPageData([...data])
|
||||
}
|
||||
|
||||
if (hostsPending) return <CenterLoader />
|
||||
|
||||
if (hostsError) return <RetryError />
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FrigateHostsTable data={data} showAddButton/>
|
||||
{
|
||||
!pageData ? <></> :
|
||||
<FrigateHostsTable data={pageData} showAddButton changedCallback={handleChange} />
|
||||
}
|
||||
<Flex justify='center'>
|
||||
<Button m='0.5rem' onClick={handleDiscard}>{strings.discard}</Button>
|
||||
<Button m='0.5rem' onClick={handleSave}>{strings.save}</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
})
|
||||
|
||||
export default FrigateHostsPage;
|
||||
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 HeadSearch from '../shared/components/HeadSearch';
|
||||
import ViewSelector, { SelectorViewState } from '../shared/components/ViewSelector';
|
||||
@ -8,11 +8,25 @@ import { getCookie, setCookie } from 'cookies-next';
|
||||
import { Context } from '..';
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { frigateApi, frigateQueryKeys, mapHostToHostname } from '../services/frigate.proxy/frigate.api';
|
||||
import RetryError from './RetryError';
|
||||
import { CameraConfig, FrigateConfig } from '../types/frigateConfig';
|
||||
import { GetFrigateHostWConfig } from '../services/frigate.proxy/frigate.schema';
|
||||
import { host } from '../shared/env.const';
|
||||
import AutoUpdatingCameraImage from '../shared/components/frigate/AutoUpdatingCameraImage';
|
||||
import CameraCard from '../shared/components/CameraCard';
|
||||
|
||||
|
||||
|
||||
|
||||
const MainBody = observer(() => {
|
||||
const { productStore, cartStore, sideBarsStore } = useContext(Context)
|
||||
const { updateProductFromServer, products, isLoading: productsLoading } = productStore
|
||||
const { updateCartFromServer, products: cartProducts, isLoading: cardLoading } = cartStore
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
useEffect(() => {
|
||||
sideBarsStore.rightVisible = false
|
||||
sideBarsStore.setLeftChildren(null)
|
||||
sideBarsStore.setRightChildren(null)
|
||||
}, [])
|
||||
|
||||
const [viewState, setTableState] = useState(getCookie('aps-main-view') as SelectorViewState || SelectorViewState.GRID)
|
||||
const handleToggleState = (state: SelectorViewState) => {
|
||||
@ -20,21 +34,43 @@ const MainBody = observer(() => {
|
||||
setTableState(state)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateProductFromServer()
|
||||
updateCartFromServer()
|
||||
sideBarsStore.setLeftSidebar(<div />)
|
||||
}, [])
|
||||
const { data: hosts, isPending, isError, refetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getFrigateHostsConfigs],
|
||||
queryFn: async () => {
|
||||
const hosts = await frigateApi.getHosts()
|
||||
let fetchedConfigs = []
|
||||
for (const host of hosts) {
|
||||
if (host.enabled) {
|
||||
const hostName = mapHostToHostname(host)
|
||||
const config: FrigateConfig = await frigateApi.getHostConfig(hostName)
|
||||
if (config) {
|
||||
const hostWConfig: GetFrigateHostWConfig = { config: config, ...host }
|
||||
fetchedConfigs.push(hostWConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
return fetchedConfigs
|
||||
}
|
||||
})
|
||||
|
||||
if (productsLoading || cardLoading) return <CenterLoader />
|
||||
if (productsLoading || cardLoading) return <div>Error</div> // add state manager
|
||||
if (isPending) return <CenterLoader />
|
||||
|
||||
if (isError) return <RetryError onRetry={refetch} />
|
||||
|
||||
|
||||
let tableData: TableAdapter[] = []
|
||||
let gridData: GridAdapter[] = []
|
||||
if (products && cartProducts) {
|
||||
tableData = productStore.mapToTable(products, cartProducts)
|
||||
gridData = productStore.mapToGrid(products, cartProducts)
|
||||
// const child = () => {
|
||||
// return ( <Skeleton mih='20rem' miw='20rem' radius="md" animate={false} /> )
|
||||
// }
|
||||
|
||||
const cards = (host: GetFrigateHostWConfig) => {
|
||||
return Object.entries(host.config.cameras).map(
|
||||
([cameraName, cameraConfig]) => (
|
||||
<CameraCard
|
||||
key={host.id + cameraName}
|
||||
cameraName={cameraName}
|
||||
hostName={host.name}
|
||||
cameraConfig={cameraConfig}
|
||||
imageUrl={frigateApi.cameraImageURL(mapHostToHostname(host), cameraName)} />))
|
||||
}
|
||||
|
||||
return (
|
||||
@ -56,6 +92,13 @@ const MainBody = observer(() => {
|
||||
<ViewSelector state={viewState} onChange={handleToggleState} />
|
||||
</Group>
|
||||
</Flex>
|
||||
<Flex justify='center' h='100%' direction='column'>
|
||||
{hosts.map(host => (
|
||||
<Grid mt='sm' key={host.id} justify="center" mb='sm' align='stretch'>
|
||||
{cards(host)}
|
||||
</Grid>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
})
|
||||
|
||||
@ -1,29 +1,33 @@
|
||||
import { Flex, Button, Text } from '@mantine/core';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { pathRoutes } from '../router/routes.path';
|
||||
import { CogWheelHeartSVG } from '../shared/components/svg/CogWheelHeartSVG';
|
||||
import { routesPath } from '../router/routes.path';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ExclamationCogWheel } from '../shared/components/svg/ExclamationCogWheel';
|
||||
import { Context } from '..';
|
||||
|
||||
const RetryError = () => {
|
||||
interface RetryErrorProps {
|
||||
onRetry?: () => void
|
||||
}
|
||||
|
||||
const RetryError = ( {onRetry} : RetryErrorProps) => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
sideBarsStore.setLeftSidebar(null)
|
||||
sideBarsStore.setRightSidebar(null)
|
||||
sideBarsStore.setLeftChildren(null)
|
||||
sideBarsStore.setRightChildren(null)
|
||||
}, [])
|
||||
|
||||
const handleGoToMain = () => {
|
||||
navigate(pathRoutes.MAIN_PATH)
|
||||
navigate(routesPath.MAIN_PATH)
|
||||
}
|
||||
|
||||
function handleRetry(event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
if (onRetry) onRetry()
|
||||
else window.location.reload()
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import { Config, frigateApi } from '../services/frigate.proxy/frigate.api';
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import RetryError from './RetryError';
|
||||
import { Button, Flex, Space } from '@mantine/core';
|
||||
@ -12,16 +12,17 @@ import { FloatingLabelInput } from '../shared/components/FloatingLabelInput';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { dimensions } from '../shared/dimensions/dimensions';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { GetConfig } from '../services/frigate.proxy/frigate.schema';
|
||||
|
||||
const SettingsPage = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const { isPending: configPending, error: configError, data, refetch } = useQuery({
|
||||
queryKey: ['config'],
|
||||
queryKey: [frigateQueryKeys.getConfig],
|
||||
queryFn: frigateApi.getConfig,
|
||||
})
|
||||
|
||||
const ecryptedValue = '**********'
|
||||
const mapEncryptedToView = (data: Config[] | undefined): Config[] | undefined => {
|
||||
const mapEncryptedToView = (data: GetConfig[] | undefined): GetConfig[] | undefined => {
|
||||
return data?.map(item => {
|
||||
const { value, encrypted, ...rest } = item
|
||||
if (encrypted) return { value: ecryptedValue, encrypted, ...rest }
|
||||
@ -35,7 +36,7 @@ const SettingsPage = () => {
|
||||
const mutation = useMutation({
|
||||
mutationFn: frigateApi.putConfig,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['config'] })
|
||||
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getConfig] })
|
||||
},
|
||||
})
|
||||
|
||||
@ -82,7 +83,7 @@ const SettingsPage = () => {
|
||||
|
||||
if (configPending) return <CenterLoader />
|
||||
|
||||
if (configError) return <RetryError />
|
||||
if (configError) return <RetryError onRetry={refetch} />
|
||||
|
||||
return (
|
||||
<Flex h='100%'>
|
||||
|
||||
@ -1,51 +1,29 @@
|
||||
import React, { Fragment, useContext, useEffect } from 'react';
|
||||
import { Context } from '..';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import JSMpegPlayer from '../shared/components/JSMpegPlayer';
|
||||
import { cameraLiveViewURL } from '../router/frigate.routes';
|
||||
import JSMpegPlayer from '../shared/components/frigate/JSMpegPlayer';
|
||||
import { frigateApi } from '../services/frigate.proxy/frigate.api';
|
||||
import { Flex } from '@mantine/core';
|
||||
import AutoUpdatingCameraImage from '../shared/components/frigate/AutoUpdatingCameraImage';
|
||||
|
||||
const Test = observer(() => {
|
||||
// const { postStore } = useContext(Context)
|
||||
// const { getPostsAction, posts } = postStore
|
||||
// const test = {
|
||||
// camera: 'Buhgalteria',
|
||||
// host: 'localhost:5000',
|
||||
// width: 800,
|
||||
// height: 600,
|
||||
// url : function() { return frigateApi.cameraWsURL(this.host, this.camera)},
|
||||
// }
|
||||
|
||||
// useEffect( () => {
|
||||
// console.log("render Test")
|
||||
// getPostsAction()
|
||||
// }, [])
|
||||
// return (
|
||||
// <Flex w='100%' h='100%'>
|
||||
// <JSMpegPlayer wsUrl={test.url()} camera={test.camera} width={test.width} height={test.height} />
|
||||
// </Flex>
|
||||
// );
|
||||
|
||||
|
||||
const test = {
|
||||
camera: 'Buhgalteria',
|
||||
host: 'localhost:5000',
|
||||
width: 800,
|
||||
height: 600,
|
||||
url : function() { return cameraLiveViewURL(this.host, this.camera)},
|
||||
}
|
||||
const test2 = {
|
||||
camera: 'IT',
|
||||
host: 'localhost:5000',
|
||||
width: 800,
|
||||
height: 600,
|
||||
url : function() { return cameraLiveViewURL(this.host, this.camera)},
|
||||
}
|
||||
const test3 = {
|
||||
camera: 'Magazin1',
|
||||
host: 'localhost:5001',
|
||||
width: 800,
|
||||
height: 600,
|
||||
url : function() { return cameraLiveViewURL(this.host, this.camera)},
|
||||
}
|
||||
|
||||
|
||||
// console.log(posts)
|
||||
return (
|
||||
<Fragment>
|
||||
<div>
|
||||
<JSMpegPlayer url={test.url()} camera={test.camera} width={test.width} height={test.height} />
|
||||
<JSMpegPlayer url={test2.url()} camera={test2.camera} width={test2.width} height={test2.height} />
|
||||
<JSMpegPlayer url={test3.url()} camera={test3.camera} width={test3.width} height={test3.height} />
|
||||
</div>
|
||||
</Fragment>
|
||||
<Flex w='100%' h='100%'>
|
||||
|
||||
</Flex>
|
||||
);
|
||||
})
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Navigate, Route, Routes } from "react-router-dom";
|
||||
import { routes } from "./routes";
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { pathRoutes } from "./routes.path";
|
||||
import { routesPath } from "./routes.path";
|
||||
|
||||
const AppRouter = () => {
|
||||
return (
|
||||
@ -9,7 +9,7 @@ const AppRouter = () => {
|
||||
{routes.map(({ path, component }) =>
|
||||
<Route key={uuidv4()} path={path} element={component} />
|
||||
)}
|
||||
<Route key={uuidv4()} path="*" element={<Navigate to={pathRoutes.MAIN_PATH} replace />} />
|
||||
<Route key={uuidv4()} path="*" element={<Navigate to={routesPath.MAIN_PATH} replace />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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: '/',
|
||||
BIRDSEYE_PATH: '/birdseye',
|
||||
EVENTS_PATH: '/events',
|
||||
RECORDINGS_PATH: '/recordings',
|
||||
SETTINGS_PATH: '/settings',
|
||||
HOST_CONFIG_PATH: '/host-config',
|
||||
HOSTS_PATH: '/hosts',
|
||||
HOST_CONFIG_PATH: '/hosts/:id/config',
|
||||
HOST_SYSTEM_PATH: '/hosts/:id/system',
|
||||
HOST_STORAGE_PATH: '/hosts/:id/storage',
|
||||
ROLES_PATH: '/roles',
|
||||
LIVE_PATH: '/live',
|
||||
THANKS_PATH: '/thanks',
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import {JSX} from "react";
|
||||
import Test from "../pages/Test"
|
||||
import MainBody from "../pages/MainBody";
|
||||
import {pathRoutes} from "./routes.path";
|
||||
import {routesPath} from "./routes.path";
|
||||
import RetryError from "../pages/RetryError";
|
||||
import Forbidden from "../pages/403";
|
||||
import NotFound from "../pages/404";
|
||||
import SettingsPage from "../pages/SettingsPage";
|
||||
import FrigateHostsPage from "../pages/FrigateHostsPage";
|
||||
import HostConfigPage from "../pages/HostConfigPage";
|
||||
import HostSystemPage from "../pages/HostSystemPage";
|
||||
import HostStoragePage from "../pages/HostStoragePage";
|
||||
|
||||
interface IRoute {
|
||||
path: string,
|
||||
@ -15,31 +18,43 @@ interface IRoute {
|
||||
|
||||
export const routes: IRoute[] = [
|
||||
{ //todo delete
|
||||
path: pathRoutes.TEST_PATH,
|
||||
path: routesPath.TEST_PATH,
|
||||
component: <Test />,
|
||||
},
|
||||
{
|
||||
path: pathRoutes.SETTINGS_PATH,
|
||||
path: routesPath.SETTINGS_PATH,
|
||||
component: <SettingsPage />,
|
||||
},
|
||||
{
|
||||
path: pathRoutes.HOST_CONFIG_PATH,
|
||||
path: routesPath.HOSTS_PATH,
|
||||
component: <FrigateHostsPage />,
|
||||
},
|
||||
{
|
||||
path: pathRoutes.MAIN_PATH,
|
||||
path: routesPath.HOST_CONFIG_PATH,
|
||||
component: <HostConfigPage />,
|
||||
},
|
||||
{
|
||||
path: routesPath.HOST_SYSTEM_PATH,
|
||||
component: <HostSystemPage />,
|
||||
},
|
||||
{
|
||||
path: routesPath.HOST_STORAGE_PATH,
|
||||
component: <HostStoragePage />,
|
||||
},
|
||||
{
|
||||
path: routesPath.MAIN_PATH,
|
||||
component: <MainBody />,
|
||||
},
|
||||
{
|
||||
path: pathRoutes.RETRY_ERROR_PATH,
|
||||
path: routesPath.RETRY_ERROR_PATH,
|
||||
component: <RetryError />,
|
||||
},
|
||||
{
|
||||
path: pathRoutes.FORBIDDEN_ERROR_PATH,
|
||||
path: routesPath.FORBIDDEN_ERROR_PATH,
|
||||
component: <Forbidden />,
|
||||
},
|
||||
{
|
||||
path: pathRoutes.NOT_FOUND_ERROR_PATH,
|
||||
path: routesPath.NOT_FOUND_ERROR_PATH,
|
||||
component: <NotFound />,
|
||||
},
|
||||
]
|
||||
@ -1,25 +1,9 @@
|
||||
import axios from "axios"
|
||||
import { proxyURL } from "../../shared/env.const"
|
||||
import { z } from "zod"
|
||||
import { GetConfig, DeleteFrigateHost, GetFrigateHost, PutConfig, PutFrigateHost, GetFrigateHostWithCameras } from "./frigate.schema";
|
||||
import { FrigateConfig } from "../../types/frigateConfig";
|
||||
|
||||
export interface Config {
|
||||
key: string,
|
||||
value: string,
|
||||
description: string,
|
||||
encrypted: boolean,
|
||||
}
|
||||
export interface PutConfig {
|
||||
key: string,
|
||||
value: string,
|
||||
}
|
||||
|
||||
export interface FrigateHost {
|
||||
id: string
|
||||
createAt: string
|
||||
updateAt: string
|
||||
name: string
|
||||
host: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: proxyURL.toString(),
|
||||
@ -27,10 +11,50 @@ const instance = axios.create({
|
||||
});
|
||||
|
||||
export const frigateApi = {
|
||||
getConfig: () => instance.get<Config[]>('apiv1/config').then(res => res.data),
|
||||
getConfig: () => instance.get<GetConfig[]>('apiv1/config').then(res => res.data),
|
||||
putConfig: (config: PutConfig[]) => instance.put('apiv1/config', config).then(res => res.data),
|
||||
getHosts: () => instance.get<FrigateHost[]>('apiv1/frigate-hosts').then(res => {
|
||||
// return new Map(res.data.map(item => [item.id, item]))
|
||||
getHosts: () => instance.get<GetFrigateHost[]>('apiv1/frigate-hosts').then(res => {
|
||||
return res.data
|
||||
}),
|
||||
getHostWithCameras: () => instance.get<GetFrigateHostWithCameras[]>('apiv1/frigate-hosts', { params: { include: 'cameras'}}).then(res => {
|
||||
return res.data
|
||||
}),
|
||||
getHost: (id: string) => instance.get<GetFrigateHost>(`apiv1/frigate-hosts/${id}`).then(res => {
|
||||
return res.data
|
||||
}),
|
||||
putHosts: (hosts: PutFrigateHost[]) => instance.put<GetFrigateHost[]>('apiv1/frigate-hosts', hosts).then(res => {
|
||||
return res.data
|
||||
}),
|
||||
deleteHosts: (hosts: DeleteFrigateHost[]) => instance.delete<GetFrigateHost[]>('apiv1/frigate-hosts', { data: hosts }).then(res => {
|
||||
return res.data
|
||||
}),
|
||||
getHostConfigRaw: (hostName: string) => instance.get('proxy/api/config/raw', { params: { hostName: hostName } }).then(res => res.data),
|
||||
getHostConfig: (hostName: string) => instance.get('proxy/api/config', { params: { hostName: hostName } }).then(res => res.data),
|
||||
cameraWsURL: (hostName: string, cameraName: string) => {
|
||||
return `ws://${proxyURL.host}/proxy-ws/live/jsmpeg/${cameraName}?hostName=${hostName}`
|
||||
},
|
||||
cameraImageURL: (hostName: string, cameraName: string) => {
|
||||
return `http://${proxyURL.host}/proxy/api/${cameraName}/latest.jpg?hostName=${hostName}`
|
||||
},
|
||||
}
|
||||
|
||||
export const mapCamerasFromConfig = (config: FrigateConfig): string[] => {
|
||||
return Object.keys(config.cameras)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const mapHostToHostname = (host: GetFrigateHost): string => {
|
||||
const url = new URL(host.host)
|
||||
const hostName = url.host
|
||||
return hostName
|
||||
}
|
||||
|
||||
|
||||
export const frigateQueryKeys = {
|
||||
getConfig: 'config',
|
||||
getFrigateHosts: 'frigate-hosts',
|
||||
getFrigateHostsConfigs: 'frigate-hosts-configs',
|
||||
getFrigateHost: 'frigate-host',
|
||||
getHostConfig: 'host-config',
|
||||
}
|
||||
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)
|
||||
|
||||
useEffect( () => {
|
||||
if (sideBarsStore.rightVisible && side === 'right' && !visible) {
|
||||
open()
|
||||
} else if (!sideBarsStore.rightVisible && side === 'right' && visible) {
|
||||
close()
|
||||
}
|
||||
}, [sideBarsStore.rightVisible])
|
||||
|
||||
const [leftChildren, setLeftChildren] = useState<React.ReactNode>(() => {
|
||||
if (children && side === 'left') return children
|
||||
else if (sideBarsStore.leftSideBar) return sideBarsStore.leftSideBar
|
||||
else if (sideBarsStore.leftChildren) return sideBarsStore.leftChildren
|
||||
return null
|
||||
})
|
||||
const [rightChildren, setRightChildren] = useState<React.ReactNode>(() => {
|
||||
if (children && side === 'right') return children
|
||||
else if (sideBarsStore.rightSideBar) return sideBarsStore.rightSideBar
|
||||
else if (sideBarsStore.rightChildren) return sideBarsStore.rightChildren
|
||||
return null
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setLeftChildren(sideBarsStore.leftSideBar)
|
||||
}, [sideBarsStore.leftSideBar])
|
||||
setLeftChildren(sideBarsStore.leftChildren)
|
||||
}, [sideBarsStore.leftChildren])
|
||||
|
||||
useEffect(() => {
|
||||
setRightChildren(sideBarsStore.rightSideBar)
|
||||
}, [sideBarsStore.rightSideBar])
|
||||
setRightChildren(sideBarsStore.rightChildren)
|
||||
}, [sideBarsStore.rightChildren])
|
||||
|
||||
useEffect(() => {
|
||||
isHidden(!visible)
|
||||
}, [visible])
|
||||
|
||||
useEffect(() => {
|
||||
}, [manualVisible.current])
|
||||
|
||||
// resize controller
|
||||
useEffect(() => {
|
||||
const checkWindowSize = () => {
|
||||
if (window.innerWidth <= hideSizePx && visible) {
|
||||
|
||||
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
|
||||
import JSMpeg from "@cycjimmy/jsmpeg-player";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useResizeObserver } from "../utils/resize-observer";
|
||||
import { useResizeObserver } from "../../utils/resize-observer";
|
||||
|
||||
type JSMpegPlayerProps = {
|
||||
className?: string;
|
||||
url: string;
|
||||
wsUrl: string;
|
||||
camera: string;
|
||||
width: number;
|
||||
height: number;
|
||||
@ -13,7 +13,7 @@ type JSMpegPlayerProps = {
|
||||
|
||||
export default function JSMpegPlayer({
|
||||
camera,
|
||||
url,
|
||||
wsUrl,
|
||||
width,
|
||||
height,
|
||||
className,
|
||||
@ -60,7 +60,7 @@ export default function JSMpegPlayer({
|
||||
|
||||
const video = new JSMpeg.VideoElement(
|
||||
playerRef.current,
|
||||
url,
|
||||
wsUrl,
|
||||
{},
|
||||
{ protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 }
|
||||
);
|
||||
@ -83,7 +83,7 @@ export default function JSMpegPlayer({
|
||||
playerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [url]);
|
||||
}, [wsUrl]);
|
||||
|
||||
return (
|
||||
<div className={className} ref={containerRef}>
|
||||
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"
|
||||
|
||||
export class SideBarsStore {
|
||||
private _leftSideBar: React.ReactNode = null
|
||||
public get leftSideBar(): React.ReactNode {
|
||||
return this._leftSideBar
|
||||
|
||||
private _rightVisible: boolean = true
|
||||
public get rightVisible(): boolean {
|
||||
return this._rightVisible
|
||||
}
|
||||
public set rightVisible(visible: boolean) {
|
||||
console.log(`set rightVisible`, visible)
|
||||
this._rightVisible = visible
|
||||
}
|
||||
private _leftVisible: boolean = true
|
||||
public get leftVisible(): boolean {
|
||||
return this._leftVisible
|
||||
}
|
||||
public set leftVisible(visible: boolean) {
|
||||
this._leftVisible = visible
|
||||
}
|
||||
|
||||
private _leftChildren: React.ReactNode = null
|
||||
public get leftChildren(): React.ReactNode {
|
||||
return this._leftChildren
|
||||
}
|
||||
|
||||
|
||||
private _rightSideBar: React.ReactNode = null
|
||||
public get rightSideBar(): React.ReactNode {
|
||||
return this._rightSideBar
|
||||
private _rightChildren: React.ReactNode = null
|
||||
public get rightChildren(): React.ReactNode {
|
||||
return this._rightChildren
|
||||
}
|
||||
|
||||
constructor () {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
setRightSidebar = (value: React.ReactNode) => {
|
||||
this._rightSideBar = value
|
||||
setRightChildren = (value: React.ReactNode) => {
|
||||
this._rightChildren = value
|
||||
}
|
||||
|
||||
setLeftSidebar = (value: React.ReactNode) => {
|
||||
this._leftSideBar = value
|
||||
setLeftChildren = (value: React.ReactNode) => {
|
||||
this._leftChildren = value
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,6 @@ export const headerMenu = {
|
||||
home:"На главную",
|
||||
test:"Тест",
|
||||
settings:"Настройки",
|
||||
rolesAcess:"Доступ",
|
||||
acessSettings:" Настройка доступа",
|
||||
hostsConfig:"Серверы Frigate",
|
||||
}
|
||||
|
||||
@ -60,6 +60,7 @@ export const strings = {
|
||||
weight: "Вес:",
|
||||
total: "Итого:",
|
||||
confirm: "Подтвердить",
|
||||
save: "Сохранить",
|
||||
discard: "Отменить",
|
||||
next: "Далее",
|
||||
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 React, { useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import SortedTh from '../shared/components/table.aps/SortedTh';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { FrigateHost } from '../services/frigate.proxy/frigate.api';
|
||||
import { IconBulbFilled, IconBulbOff, IconDeviceFloppy, IconPencil, IconPlus, IconSettings, IconTrash } from '@tabler/icons-react';
|
||||
import SwitchCell from '../shared/components/table.aps/SwitchCell';
|
||||
import TextInputCell from '../shared/components/table.aps/TextInputCell';
|
||||
import SwitchCell from '../shared/components/hosts.table/SwitchCell';
|
||||
import TextInputCell from '../shared/components/hosts.table/TextInputCell';
|
||||
import ObjectId from 'bson-objectid';
|
||||
import { debounce } from '../shared/utils/debounce';
|
||||
import HostSettingsMenu from '../shared/components/menu/HostSettingsMenu';
|
||||
import { GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
|
||||
|
||||
interface FrigateHostsTableProps {
|
||||
data: FrigateHost[],
|
||||
interface TableProps<T> {
|
||||
data: T[],
|
||||
showAddButton?: boolean,
|
||||
handleInputChange?: () => void,
|
||||
handleSwtitchToggle?: () => void,
|
||||
saveCallback?: (tableData: T[]) => void,
|
||||
changedCallback?: (tableData: T[]) => void,
|
||||
}
|
||||
|
||||
const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, handleSwtitchToggle }: FrigateHostsTableProps) => {
|
||||
const [tableData, setData] = useState(data)
|
||||
const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedCallback }: TableProps<GetFrigateHost>) => {
|
||||
console.log('FrigateHostsTable rendered')
|
||||
const [tableData, setTableData] = useState(data)
|
||||
const [reversed, setReversed] = useState(false)
|
||||
const [sortedName, setSortedName] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
console.log('data changed')
|
||||
setTableData(data)
|
||||
}, [data])
|
||||
|
||||
const debouncedChanged = useCallback(debounce((tableData: GetFrigateHost[]) => {
|
||||
if (changedCallback) changedCallback(tableData)
|
||||
}, 200), [])
|
||||
|
||||
useEffect(() => {
|
||||
debouncedChanged(tableData)
|
||||
}, [tableData, debouncedChanged])
|
||||
|
||||
function sortByKey<T, K extends keyof T>(array: T[], key: K): T[] {
|
||||
return array.sort((a, b) => {
|
||||
let valueA = a[key];
|
||||
@ -37,9 +54,9 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
|
||||
const handleSort = (headName: string, propertyName: string,) => {
|
||||
const reverse = headName === sortedName ? !reversed : false;
|
||||
setReversed(reverse)
|
||||
const arr = sortByKey(tableData, propertyName as keyof FrigateHost)
|
||||
const arr = sortByKey(tableData, propertyName as keyof GetFrigateHost)
|
||||
if (reverse) arr.reverse()
|
||||
setData(arr)
|
||||
setTableData(arr)
|
||||
setSortedName(headName)
|
||||
}
|
||||
|
||||
@ -63,7 +80,7 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
|
||||
})
|
||||
|
||||
const handleTextChange = (id: string | number, propertyName: string, value: string,) => {
|
||||
setData(tableData.map(item => {
|
||||
setTableData(tableData.map(item => {
|
||||
if (item.id === id) {
|
||||
return {
|
||||
...item,
|
||||
@ -74,7 +91,7 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
|
||||
}));
|
||||
}
|
||||
const handleSwitchChange = (id: string | number, propertyName: string, value: string,) => {
|
||||
setData(tableData.map(item => {
|
||||
setTableData(tableData.map(item => {
|
||||
if (item.id === id) {
|
||||
return {
|
||||
...item,
|
||||
@ -86,19 +103,19 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
|
||||
}
|
||||
|
||||
const handleDeleteRow = (id: string | number) => {
|
||||
setData(tableData.filter(item => item.id !== id))
|
||||
setTableData(tableData.filter(item => item.id !== id))
|
||||
}
|
||||
|
||||
const handleAddRow = () => {
|
||||
const newHost: FrigateHost = {
|
||||
id: String(Math.random()),
|
||||
const handleAddRow = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const newHost: GetFrigateHost = {
|
||||
id: ObjectId().toHexString(),
|
||||
createAt: '',
|
||||
updateAt: '',
|
||||
host: '',
|
||||
name: '',
|
||||
enabled: true
|
||||
}
|
||||
setData(prevTableData => [...prevTableData, newHost])
|
||||
setTableData(prevTableData => [...prevTableData, newHost])
|
||||
}
|
||||
|
||||
const rows = tableData.map(item => {
|
||||
@ -106,12 +123,10 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
|
||||
<tr key={item.id}>
|
||||
<TextInputCell text={item.name} width='40%' id={item.id} propertyName='name' onChange={handleTextChange} />
|
||||
<TextInputCell text={item.host} width='40%' id={item.id} propertyName='host' onChange={handleTextChange} />
|
||||
{/* {textInputCell(item.host, '40%', item.id, 'host', handleTextChange)} */}
|
||||
<SwitchCell value={item.enabled} width='10%' id={item.id} propertyName='enabled' toggle={handleSwitchChange} />
|
||||
<td align='right' style={{ width: '10%', padding: '0', }}>
|
||||
<Flex justify='center'>
|
||||
<Button size='xs' ><IconSettings /></Button>
|
||||
<Button size='xs' ><IconDeviceFloppy /></Button>
|
||||
<HostSettingsMenu id={item.id} />
|
||||
<Button size='xs' onClick={() => handleDeleteRow(item.id)}><IconTrash /></Button>
|
||||
</Flex>
|
||||
</td>
|
||||
@ -133,7 +148,7 @@ const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, han
|
||||
</Table>
|
||||
{showAddButton ?
|
||||
<Flex w='100%' justify='end'>
|
||||
<Button size='xs' onClick={() => handleAddRow()}><IconPlus /></Button>
|
||||
<Button size='xs' onClick={handleAddRow}><IconPlus /></Button>
|
||||
</Flex>
|
||||
: <></>
|
||||
}
|
||||
|
||||
@ -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 ColorSchemeToggle from "../../shared/components/ColorSchemeToggle";
|
||||
import Logo from "../../shared/components/Logo";
|
||||
import { pathRoutes } from "../../router/routes.path";
|
||||
import { routesPath } from "../../router/routes.path";
|
||||
|
||||
const HEADER_HEIGHT = rem(60)
|
||||
|
||||
@ -93,7 +93,7 @@ export const HeaderAction = ({ links }: HeaderActionProps) => {
|
||||
<Header height={HEADER_HEIGHT} sx={{ borderBottom: 0 }}>
|
||||
<Container className={classes.inner} fluid>
|
||||
<Flex wrap='nowrap' >
|
||||
<Logo onClick={() => handleNavigate(pathRoutes.MAIN_PATH)} />
|
||||
<Logo onClick={() => handleNavigate(routesPath.MAIN_PATH)} />
|
||||
<Burger opened={opened} onClick={toggle} className={classes.burger} size="sm" />
|
||||
<Flex className={classes.leftLinksMenu}>
|
||||
{ items }
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { pathRoutes } from "../../router/routes.path";
|
||||
import { routesPath } from "../../router/routes.path";
|
||||
import { headerMenu } from "../../shared/strings/header.menu.strings";
|
||||
import { HeaderActionProps } from "./HeaderAction";
|
||||
|
||||
export const testHeaderLinks: HeaderActionProps =
|
||||
{
|
||||
links: [
|
||||
{link: pathRoutes.MAIN_PATH, label: headerMenu.home, links: []},
|
||||
{link: pathRoutes.TEST_PATH, label: headerMenu.test, links: []},
|
||||
{link: pathRoutes.SETTINGS_PATH, label: headerMenu.settings, links: []},
|
||||
{link: pathRoutes.HOST_CONFIG_PATH, label: headerMenu.hostsConfig, links: []},
|
||||
{link: pathRoutes.ROLES_PATH, label: headerMenu.rolesAcess, links: []},
|
||||
{link: routesPath.MAIN_PATH, label: headerMenu.home, links: []},
|
||||
{link: routesPath.TEST_PATH, label: headerMenu.test, links: []},
|
||||
{link: routesPath.SETTINGS_PATH, label: headerMenu.settings, links: []},
|
||||
{link: routesPath.HOSTS_PATH, label: headerMenu.hostsConfig, links: []},
|
||||
{link: routesPath.ROLES_PATH, label: headerMenu.acessSettings, links: []},
|
||||
]
|
||||
}
|
||||
134
yarn.lock
134
yarn.lock
@ -1808,6 +1808,20 @@
|
||||
resolved "https://registry.yarnpkg.com/@mantine/utils/-/utils-6.0.16.tgz#b39e47ef8fa4463322e9aa10cdd5980f4310b705"
|
||||
integrity sha512-UFel9DbifL3zS8pTJlr6GfwGd6464OWXCJdUq0oLydgimbC1VV2PnptBr6FMwIpPVcxouLOtY1cChzwFH95PSA==
|
||||
|
||||
"@monaco-editor/loader@^1.4.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.4.0.tgz#f08227057331ec890fa1e903912a5b711a2ad558"
|
||||
integrity sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==
|
||||
dependencies:
|
||||
state-local "^1.0.6"
|
||||
|
||||
"@monaco-editor/react@^4.6.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.6.0.tgz#bcc68671e358a21c3814566b865a54b191e24119"
|
||||
integrity sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==
|
||||
dependencies:
|
||||
"@monaco-editor/loader" "^1.4.0"
|
||||
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
|
||||
version "5.1.1-v1"
|
||||
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
|
||||
@ -2424,6 +2438,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
|
||||
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
|
||||
|
||||
"@types/json-schema@^7.0.0":
|
||||
version "7.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||
|
||||
"@types/json5@^0.0.29":
|
||||
version "0.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
@ -3407,6 +3426,11 @@ bser@2.1.1:
|
||||
dependencies:
|
||||
node-int64 "^0.4.0"
|
||||
|
||||
bson-objectid@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/bson-objectid/-/bson-objectid-2.0.4.tgz#339211572ef97dc98f2d68eaee7b99b7be59a089"
|
||||
integrity sha512-vgnKAUzcDoa+AeyYwXCoHyF2q6u/8H46dxu5JN+4/TZeq/Dlinn0K6GvxsCLb3LHUJl0m/TLiEK31kUwtgocMQ==
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||
@ -5525,6 +5549,11 @@ icss-utils@^5.0.0, icss-utils@^5.1.0:
|
||||
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
|
||||
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
|
||||
|
||||
idb-keyval@^6.2.1:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33"
|
||||
integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==
|
||||
|
||||
idb@^7.0.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b"
|
||||
@ -6557,6 +6586,11 @@ json5@^2.1.2, json5@^2.2.0, json5@^2.2.2:
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
|
||||
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
|
||||
|
||||
jsonc-parser@^3.0.0:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a"
|
||||
integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==
|
||||
|
||||
jsonfile@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
|
||||
@ -6908,6 +6942,53 @@ mobx@^6.9.0:
|
||||
resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.9.0.tgz#8a894c26417c05bed2cf7499322e589ee9787397"
|
||||
integrity sha512-HdKewQEREEJgsWnErClfbFoVebze6rGazxFLU/XUyrII8dORfVszN1V0BMRnQSzcgsNNtkX8DHj3nC6cdWE9YQ==
|
||||
|
||||
monaco-editor@^0.46.0:
|
||||
version "0.46.0"
|
||||
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.46.0.tgz#013e453fd2408997e4fe0bf67b36a80a24bc7bcc"
|
||||
integrity sha512-ADwtLIIww+9FKybWscd7OCfm9odsFYHImBRI1v9AviGce55QY8raT+9ihH8jX/E/e6QVSGM+pKj4jSUSRmALNQ==
|
||||
|
||||
monaco-languageserver-types@^0.3.0:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/monaco-languageserver-types/-/monaco-languageserver-types-0.3.2.tgz#5e7c9ee50d01c68a64e28b40c3866ce4a46a752d"
|
||||
integrity sha512-KiGVYK/DiX1pnacnOjGNlM85bhV3ZTyFlM+ce7B8+KpWCbF1XJVovu51YyuGfm+K7+K54mIpT4DFX16xmi+tYA==
|
||||
dependencies:
|
||||
monaco-types "^0.1.0"
|
||||
vscode-languageserver-protocol "^3.0.0"
|
||||
vscode-uri "^3.0.0"
|
||||
|
||||
monaco-marker-data-provider@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/monaco-marker-data-provider/-/monaco-marker-data-provider-1.1.1.tgz#0ca69f367152f5aa12cec2bda95f32b7403e876f"
|
||||
integrity sha512-PGB7TJSZE5tmHzkxv/OEwK2RGNC2A7dcq4JRJnnj31CUAsfmw0Gl+1QTrH0W0deKhcQmQM0YVPaqgQ+0wCt8Mg==
|
||||
|
||||
monaco-types@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/monaco-types/-/monaco-types-0.1.0.tgz#3a3066aba499cb5923cd60efc736f3f14a169e10"
|
||||
integrity sha512-aWK7SN9hAqNYi0WosPoMjenMeXJjwCxDibOqWffyQ/qXdzB/86xshGQobRferfmNz7BSNQ8GB0MD0oby9/5fTQ==
|
||||
|
||||
monaco-worker-manager@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz#f67c54dfca34ed4b225d5de84e77b24b4e36de8a"
|
||||
integrity sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==
|
||||
|
||||
monaco-yaml@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/monaco-yaml/-/monaco-yaml-5.1.1.tgz#932ad3790707d6598eea2f71102e400a9a6dc074"
|
||||
integrity sha512-BuZ0/ZCGjrPNRzYMZ/MoxH8F/SdM+mATENXnpOhDYABi1Eh+QvxSszEct+ACSCarZiwLvy7m6yEF/pvW8XJkyQ==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.0"
|
||||
jsonc-parser "^3.0.0"
|
||||
monaco-languageserver-types "^0.3.0"
|
||||
monaco-marker-data-provider "^1.0.0"
|
||||
monaco-types "^0.1.0"
|
||||
monaco-worker-manager "^2.0.0"
|
||||
path-browserify "^1.0.0"
|
||||
prettier "^2.0.0"
|
||||
vscode-languageserver-textdocument "^1.0.0"
|
||||
vscode-languageserver-types "^3.0.0"
|
||||
vscode-uri "^3.0.0"
|
||||
yaml "^2.0.0"
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
@ -7264,6 +7345,11 @@ pascal-case@^3.1.2:
|
||||
no-case "^3.0.4"
|
||||
tslib "^2.0.3"
|
||||
|
||||
path-browserify@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
|
||||
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
|
||||
|
||||
path-exists@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
|
||||
@ -7900,6 +7986,11 @@ prelude-ls@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
prettier@^2.0.0:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
|
||||
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
|
||||
|
||||
pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
||||
@ -8231,6 +8322,11 @@ react-textarea-autosize@8.3.4:
|
||||
use-composed-ref "^1.3.0"
|
||||
use-latest "^1.2.1"
|
||||
|
||||
react-use-websocket@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/react-use-websocket/-/react-use-websocket-4.7.0.tgz#e45545ed48eb60171bf6401d1884cc80c700a0ea"
|
||||
integrity sha512-YjR62jB7vB94IZy5UPBGZSR3c0hxu796q9IuJ0vbNg7InJ7Z84NHOd/LHzVI5nAKtaGy1oqvf8EmjKxX+cNz4A==
|
||||
|
||||
react@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
@ -8821,6 +8917,11 @@ stackframe@^1.3.4:
|
||||
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310"
|
||||
integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==
|
||||
|
||||
state-local@^1.0.6:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5"
|
||||
integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==
|
||||
|
||||
statuses@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
||||
@ -9524,6 +9625,34 @@ vary@~1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
||||
|
||||
vscode-jsonrpc@8.2.0:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9"
|
||||
integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==
|
||||
|
||||
vscode-languageserver-protocol@^3.0.0:
|
||||
version "3.17.5"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea"
|
||||
integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==
|
||||
dependencies:
|
||||
vscode-jsonrpc "8.2.0"
|
||||
vscode-languageserver-types "3.17.5"
|
||||
|
||||
vscode-languageserver-textdocument@^1.0.0:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf"
|
||||
integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==
|
||||
|
||||
vscode-languageserver-types@3.17.5, vscode-languageserver-types@^3.0.0:
|
||||
version "3.17.5"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a"
|
||||
integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==
|
||||
|
||||
vscode-uri@^3.0.0:
|
||||
version "3.0.8"
|
||||
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f"
|
||||
integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==
|
||||
|
||||
w3c-hr-time@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
|
||||
@ -10015,6 +10144,11 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2:
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||
|
||||
yaml@^2.0.0:
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2"
|
||||
integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==
|
||||
|
||||
yaml@^2.1.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user