add events page
This commit is contained in:
parent
12b508661a
commit
f935de1eeb
@ -1,14 +1,13 @@
|
|||||||
import { AppShell, useMantineTheme, } from "@mantine/core";
|
import { AppShell, useMantineTheme, } from "@mantine/core";
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { useContext, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Context } from '.';
|
import { useLocation } from "react-router-dom";
|
||||||
import AppRouter from './router/AppRouter';
|
import AppRouter from './router/AppRouter';
|
||||||
import { routesPath } from './router/routes.path';
|
import { routesPath } from './router/routes.path';
|
||||||
|
import RightSideBar from "./shared/components/RightSideBar";
|
||||||
import { isProduction } from './shared/env.const';
|
import { isProduction } from './shared/env.const';
|
||||||
import { HeaderAction } from './widgets/header/HeaderAction';
|
import { HeaderAction } from './widgets/header/HeaderAction';
|
||||||
import RightSideBar from "./shared/components/RightSideBar";
|
|
||||||
import { useLocation } from "react-router-dom";
|
|
||||||
|
|
||||||
const AppBody = () => {
|
const AppBody = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -17,6 +16,7 @@ const AppBody = () => {
|
|||||||
{ link: routesPath.MAIN_PATH, label: t('header.home') },
|
{ link: routesPath.MAIN_PATH, label: t('header.home') },
|
||||||
{ link: routesPath.SETTINGS_PATH, label: t('header.settings'), admin: true },
|
{ link: routesPath.SETTINGS_PATH, label: t('header.settings'), admin: true },
|
||||||
{ link: routesPath.RECORDINGS_PATH, label: t('header.recordings') },
|
{ link: routesPath.RECORDINGS_PATH, label: t('header.recordings') },
|
||||||
|
{ link: routesPath.EVENTS_PATH, label: t('header.events') },
|
||||||
{ link: routesPath.HOSTS_PATH, label: t('header.hostsConfig'), admin: true },
|
{ link: routesPath.HOSTS_PATH, label: t('header.hostsConfig'), admin: true },
|
||||||
{ link: routesPath.ACCESS_PATH, label: t('header.acessSettings'), admin: true },
|
{ link: routesPath.ACCESS_PATH, label: t('header.acessSettings'), admin: true },
|
||||||
]
|
]
|
||||||
@ -25,7 +25,7 @@ const AppBody = () => {
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
const pathsWithLeftSidebar: string[] = []
|
const pathsWithLeftSidebar: string[] = []
|
||||||
const pathsWithRightSidebar: string[] = [routesPath.MAIN_PATH, routesPath.RECORDINGS_PATH]
|
const pathsWithRightSidebar: string[] = [routesPath.MAIN_PATH, routesPath.RECORDINGS_PATH, routesPath.EVENTS_PATH]
|
||||||
|
|
||||||
const [leftSideBar, setLeftSidebar] = useState(pathsWithLeftSidebar.includes(location.pathname))
|
const [leftSideBar, setLeftSidebar] = useState(pathsWithLeftSidebar.includes(location.pathname))
|
||||||
const [rightSideBar, setRightSidebar] = useState(pathsWithRightSidebar.includes(location.pathname))
|
const [rightSideBar, setRightSidebar] = useState(pathsWithRightSidebar.includes(location.pathname))
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import React, { createContext } from 'react';
|
import { ReactKeycloakProvider } from '@react-keycloak/web';
|
||||||
|
import { createContext } from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
import RootStore from './shared/stores/root.store';
|
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import './services/i18n';
|
import './services/i18n';
|
||||||
import { ReactKeycloakProvider } from '@react-keycloak/web';
|
import keycloakInstance from './services/keycloak-config';
|
||||||
import keycloak from './services/keycloak-config';
|
|
||||||
import CenterLoader from './shared/components/loaders/CenterLoader';
|
import CenterLoader from './shared/components/loaders/CenterLoader';
|
||||||
|
import RootStore from './shared/stores/root.store';
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement
|
document.getElementById('root') as HTMLElement
|
||||||
@ -15,7 +15,6 @@ const root = ReactDOM.createRoot(
|
|||||||
|
|
||||||
export const hostURL = new URL(window.location.href)
|
export const hostURL = new URL(window.location.href)
|
||||||
|
|
||||||
|
|
||||||
const rootStore = new RootStore()
|
const rootStore = new RootStore()
|
||||||
export const Context = createContext<RootStore>(rootStore)
|
export const Context = createContext<RootStore>(rootStore)
|
||||||
|
|
||||||
@ -25,11 +24,11 @@ const eventLogger = (event: string, error?: any) => {
|
|||||||
|
|
||||||
const tokenLogger = (tokens: any) => {
|
const tokenLogger = (tokens: any) => {
|
||||||
console.log('onKeycloakTokens', tokens);
|
console.log('onKeycloakTokens', tokens);
|
||||||
};
|
}
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<ReactKeycloakProvider
|
<ReactKeycloakProvider
|
||||||
authClient={keycloak}
|
authClient={keycloakInstance}
|
||||||
LoadingComponent={<CenterLoader />}
|
LoadingComponent={<CenterLoader />}
|
||||||
onEvent={eventLogger}
|
onEvent={eventLogger}
|
||||||
onTokens={tokenLogger}
|
onTokens={tokenLogger}
|
||||||
|
|||||||
@ -14,6 +14,10 @@ const en = {
|
|||||||
height: 'Height',
|
height: 'Height',
|
||||||
points: 'Points',
|
points: 'Points',
|
||||||
},
|
},
|
||||||
|
eventsPage: {
|
||||||
|
selectStartTime: 'Select start time:',
|
||||||
|
selectEndTime: 'Select end time:',
|
||||||
|
},
|
||||||
frigateConfigPage: {
|
frigateConfigPage: {
|
||||||
copyConfig: 'Copy Config',
|
copyConfig: 'Copy Config',
|
||||||
saveOnly: 'Save Only',
|
saveOnly: 'Save Only',
|
||||||
@ -74,6 +78,7 @@ const en = {
|
|||||||
home: 'Main',
|
home: 'Main',
|
||||||
settings: 'Settings',
|
settings: 'Settings',
|
||||||
recordings: 'Recordings',
|
recordings: 'Recordings',
|
||||||
|
events: 'Events',
|
||||||
hostsConfig: 'Frigate servers',
|
hostsConfig: 'Frigate servers',
|
||||||
acessSettings: 'Access settings',
|
acessSettings: 'Access settings',
|
||||||
},
|
},
|
||||||
@ -93,6 +98,7 @@ const en = {
|
|||||||
doubleClickToFullHint: 'Double click for fullscreen',
|
doubleClickToFullHint: 'Double click for fullscreen',
|
||||||
rating: 'Rating',
|
rating: 'Rating',
|
||||||
},
|
},
|
||||||
|
maxRetries: 'Error: Unable to fetch data after {{maxRetries}} retries. Please try again later or change period to smaller.',
|
||||||
config: 'Config',
|
config: 'Config',
|
||||||
create: 'Create',
|
create: 'Create',
|
||||||
clear: 'Clear',
|
clear: 'Clear',
|
||||||
@ -116,6 +122,7 @@ const en = {
|
|||||||
second: 'Second',
|
second: 'Second',
|
||||||
events: 'Events',
|
events: 'Events',
|
||||||
notHaveEvents: 'No events',
|
notHaveEvents: 'No events',
|
||||||
|
notHaveEventsAtThatPeriod: 'Not have events at that period',
|
||||||
selectHost: 'Select host',
|
selectHost: 'Select host',
|
||||||
selectCamera: 'Select Camera',
|
selectCamera: 'Select Camera',
|
||||||
selectRange: 'Select period',
|
selectRange: 'Select period',
|
||||||
|
|||||||
@ -12,6 +12,11 @@ const ru = {
|
|||||||
height: 'Высота',
|
height: 'Высота',
|
||||||
points: 'Точки',
|
points: 'Точки',
|
||||||
},
|
},
|
||||||
|
eventsPage: {
|
||||||
|
selectStartTime: 'Выбери время начала:',
|
||||||
|
selectEndTime: 'Выбери время окончания:',
|
||||||
|
maxEventsFetches: 'Ошибка: Невозможно получить события после {{maxRetries}} попыток. Пожалуйста попробуйте позже или установите меньший период.',
|
||||||
|
},
|
||||||
frigateConfigPage: {
|
frigateConfigPage: {
|
||||||
copyConfig: 'Копировать Конфиг.',
|
copyConfig: 'Копировать Конфиг.',
|
||||||
saveOnly: 'Только Сохранить',
|
saveOnly: 'Только Сохранить',
|
||||||
@ -72,6 +77,7 @@ const ru = {
|
|||||||
home: 'Главная',
|
home: 'Главная',
|
||||||
settings: 'Настройки',
|
settings: 'Настройки',
|
||||||
recordings: 'Записи',
|
recordings: 'Записи',
|
||||||
|
events: 'События',
|
||||||
hostsConfig: 'Серверы Frigate',
|
hostsConfig: 'Серверы Frigate',
|
||||||
acessSettings: 'Настройка доступа',
|
acessSettings: 'Настройка доступа',
|
||||||
},
|
},
|
||||||
@ -114,6 +120,7 @@ const ru = {
|
|||||||
second: 'Час',
|
second: 'Час',
|
||||||
events: 'События',
|
events: 'События',
|
||||||
notHaveEvents: 'Событий нет',
|
notHaveEvents: 'Событий нет',
|
||||||
|
notHaveEventsAtThatPeriod: 'Нет событий за этот период',
|
||||||
selectHost: 'Выбери хост',
|
selectHost: 'Выбери хост',
|
||||||
selectCamera: 'Выбери камеру',
|
selectCamera: 'Выбери камеру',
|
||||||
selectRange: 'Выбери период',
|
selectRange: 'Выбери период',
|
||||||
|
|||||||
62
src/pages/EventsPage.tsx
Normal file
62
src/pages/EventsPage.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { Flex, Text } from '@mantine/core';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { useContext, useEffect } from 'react';
|
||||||
|
import { Context } from '..';
|
||||||
|
import EventsBody from '../widgets/EventsBody';
|
||||||
|
import EventsRightFilters from '../widgets/sidebars/EventsRightFilters';
|
||||||
|
import { SideBarContext } from '../widgets/sidebars/SideBarContext';
|
||||||
|
import { isProduction } from '../shared/env.const';
|
||||||
|
|
||||||
|
export const eventsPageQuery = {
|
||||||
|
hostId: 'hostId',
|
||||||
|
cameraId: 'cameraId',
|
||||||
|
startDay: 'startDay',
|
||||||
|
endDay: 'endDay',
|
||||||
|
hour: 'hour',
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventsPage = () => {
|
||||||
|
|
||||||
|
const { setRightChildren } = useContext(SideBarContext)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRightChildren(<EventsRightFilters />)
|
||||||
|
return () => setRightChildren(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const { eventsStore } = useContext(Context)
|
||||||
|
const { hostId, cameraId, period, startTime, endTime } = eventsStore.filters
|
||||||
|
|
||||||
|
if (hostId && cameraId && period && period[0] && period[1]) {
|
||||||
|
return (
|
||||||
|
<EventsBody
|
||||||
|
hostId={hostId}
|
||||||
|
cameraId={cameraId}
|
||||||
|
period={[period[0], period[1]]}
|
||||||
|
startTime={startTime}
|
||||||
|
endTime={endTime}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex w='100%' h='100%' direction='column' justify='center' align='center'>
|
||||||
|
{!hostId ?
|
||||||
|
<Text size='xl'>{t('pleaseSelectHost')}</Text>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
{hostId && !cameraId ?
|
||||||
|
<Text size='xl'>{t('pleaseSelectCamera')}</Text>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
{hostId && cameraId && !eventsStore.isPeriodSet() ?
|
||||||
|
<Text size='xl'>{t('pleaseSelectDate')}</Text>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default observer(EventsPage);
|
||||||
@ -20,7 +20,7 @@ import { CameraTag } from '../types/tags';
|
|||||||
const MainPage = () => {
|
const MainPage = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { mainStore } = useContext(Context)
|
const { mainStore } = useContext(Context)
|
||||||
const { setChildrenComponent } = useContext(SideBarContext)
|
const { setRightChildren } = useContext(SideBarContext)
|
||||||
const { selectedHostId, selectedTags } = mainStore
|
const { selectedHostId, selectedTags } = mainStore
|
||||||
const [searchQuery, setSearchQuery] = useState<string>()
|
const [searchQuery, setSearchQuery] = useState<string>()
|
||||||
const [filteredCameras, setFilteredCameras] = useState<GetCameraWHostWConfig[]>()
|
const [filteredCameras, setFilteredCameras] = useState<GetCameraWHostWConfig[]>()
|
||||||
@ -35,8 +35,8 @@ const MainPage = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChildrenComponent(<MainFiltersRightSide />);
|
setRightChildren(<MainFiltersRightSide />);
|
||||||
return () => setChildrenComponent(null);
|
return () => setRightChildren(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import SelectedHostList from '../widgets/SelectedHostList';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SideBarContext } from '../widgets/sidebars/SideBarContext';
|
import { SideBarContext } from '../widgets/sidebars/SideBarContext';
|
||||||
|
|
||||||
|
|
||||||
export const recordingsPageQuery = {
|
export const recordingsPageQuery = {
|
||||||
hostId: 'hostId',
|
hostId: 'hostId',
|
||||||
cameraId: 'cameraId',
|
cameraId: 'cameraId',
|
||||||
@ -41,12 +40,11 @@ const RecordingsPage = () => {
|
|||||||
const [cameraId, setCameraId] = useState<string>('')
|
const [cameraId, setCameraId] = useState<string>('')
|
||||||
const [period, setPeriod] = useState<[Date | null, Date | null]>([null, null])
|
const [period, setPeriod] = useState<[Date | null, Date | null]>([null, null])
|
||||||
|
|
||||||
const { setChildrenComponent } = useContext(SideBarContext)
|
const { setRightChildren } = useContext(SideBarContext)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChildrenComponent(<RecordingsFiltersRightSide />);
|
setRightChildren(<RecordingsFiltersRightSide />);
|
||||||
|
return () => setRightChildren(null);
|
||||||
return () => setChildrenComponent(null);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ export const routesPath = {
|
|||||||
MAIN_PATH: '/',
|
MAIN_PATH: '/',
|
||||||
BIRDSEYE_PATH: '/birdseye',
|
BIRDSEYE_PATH: '/birdseye',
|
||||||
RECORDINGS_PATH: '/recordings',
|
RECORDINGS_PATH: '/recordings',
|
||||||
|
EVENTS_PATH: '/events',
|
||||||
SETTINGS_PATH: '/settings',
|
SETTINGS_PATH: '/settings',
|
||||||
HOSTS_PATH: '/hosts',
|
HOSTS_PATH: '/hosts',
|
||||||
HOST_CONFIG_PATH: '/hosts/:id/config',
|
HOST_CONFIG_PATH: '/hosts/:id/config',
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import RecordingsPage from "../pages/RecordingsPage";
|
|||||||
import AccessSettings from "../pages/AccessSettingsPage";
|
import AccessSettings from "../pages/AccessSettingsPage";
|
||||||
import PlayRecordPage from "../pages/PlayRecordPage";
|
import PlayRecordPage from "../pages/PlayRecordPage";
|
||||||
import EditCameraPage from "../pages/EditCameraPage";
|
import EditCameraPage from "../pages/EditCameraPage";
|
||||||
|
import EventsPage from "../pages/EventsPage";
|
||||||
|
|
||||||
interface IRoute {
|
interface IRoute {
|
||||||
path: string,
|
path: string,
|
||||||
@ -33,6 +34,10 @@ export const routes: IRoute[] = [
|
|||||||
path: routesPath.RECORDINGS_PATH,
|
path: routesPath.RECORDINGS_PATH,
|
||||||
component: <RecordingsPage />,
|
component: <RecordingsPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: routesPath.EVENTS_PATH,
|
||||||
|
component: <EventsPage />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: routesPath.HOST_CONFIG_PATH,
|
path: routesPath.HOST_CONFIG_PATH,
|
||||||
component: <HostConfigPage />,
|
component: <HostConfigPage />,
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { EventFrigate } from "../../types/event";
|
|||||||
import { getResolvedTimeZone } from "../../shared/utils/dateUtil";
|
import { getResolvedTimeZone } from "../../shared/utils/dateUtil";
|
||||||
import { FrigateStats, GetFfprobe, GetHostStorage, GetVaInfo } from "../../types/frigateStats";
|
import { FrigateStats, GetFfprobe, GetHostStorage, GetVaInfo } from "../../types/frigateStats";
|
||||||
import { PostSaveConfig, SaveOption } from "../../types/saveConfig";
|
import { PostSaveConfig, SaveOption } from "../../types/saveConfig";
|
||||||
import keycloak from "../keycloak-config";
|
import keycloakInstance from "../keycloak-config";
|
||||||
import { PutMask } from "../../types/mask";
|
import { PutMask } from "../../types/mask";
|
||||||
import { GetUserTag, PutUserTag } from "../../types/tags";
|
import { GetUserTag, PutUserTag } from "../../types/tags";
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ const instanceApi = axios.create({
|
|||||||
|
|
||||||
instanceApi.interceptors.request.use(
|
instanceApi.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
const accessToken = keycloak.token;
|
const accessToken = keycloakInstance.token;
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
config.headers.Authorization = `Bearer ${accessToken}`
|
config.headers.Authorization = `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
@ -44,6 +44,7 @@ export const frigateApi = {
|
|||||||
putOIDPConfigTest: (config: OIDPConfig) => instanceApi.put('apiv1/config/oidp/test', config).then(res => res.data),
|
putOIDPConfigTest: (config: OIDPConfig) => instanceApi.put('apiv1/config/oidp/test', config).then(res => res.data),
|
||||||
getHosts: () => instanceApi.get<GetFrigateHost[]>('apiv1/frigate-hosts').then(res => res.data),
|
getHosts: () => instanceApi.get<GetFrigateHost[]>('apiv1/frigate-hosts').then(res => res.data),
|
||||||
getHost: (id: string) => instanceApi.get<GetFrigateHost>(`apiv1/frigate-hosts/${id}`).then(res => res.data),
|
getHost: (id: string) => instanceApi.get<GetFrigateHost>(`apiv1/frigate-hosts/${id}`).then(res => res.data),
|
||||||
|
getCameraById: (cameraId: string) => instanceApi.get<GetCameraWHostWConfig>(`apiv1/cameras/${cameraId}`).then(res => res.data),
|
||||||
getCamerasByHostId: (hostId: string) => instanceApi.get<GetCameraWHostWConfig[]>(`apiv1/cameras/host/${hostId}`).then(res => res.data),
|
getCamerasByHostId: (hostId: string) => instanceApi.get<GetCameraWHostWConfig[]>(`apiv1/cameras/host/${hostId}`).then(res => res.data),
|
||||||
getCamerasWHost: () => instanceApi.get<GetCameraWHostWConfig[]>(`apiv1/cameras`).then(res => res.data),
|
getCamerasWHost: () => instanceApi.get<GetCameraWHostWConfig[]>(`apiv1/cameras`).then(res => res.data),
|
||||||
getCameraWHost: (id: string) => instanceApi.get<GetCameraWHostWConfig>(`apiv1/cameras/${id}`).then(res => res.data),
|
getCameraWHost: (id: string) => instanceApi.get<GetCameraWHostWConfig>(`apiv1/cameras/${id}`).then(res => res.data),
|
||||||
@ -227,6 +228,7 @@ export const frigateQueryKeys = {
|
|||||||
getFrigateHost: 'frigate-host',
|
getFrigateHost: 'frigate-host',
|
||||||
getCamerasWHost: 'cameras-frigate-host',
|
getCamerasWHost: 'cameras-frigate-host',
|
||||||
getCameraWHost: 'camera-frigate-host',
|
getCameraWHost: 'camera-frigate-host',
|
||||||
|
getCameraById: 'camera-by-Id',
|
||||||
getCameraByHostId: 'camera-by-hostId',
|
getCameraByHostId: 'camera-by-hostId',
|
||||||
getHostConfig: 'host-config',
|
getHostConfig: 'host-config',
|
||||||
postHostConfig: 'host-config-save',
|
postHostConfig: 'host-config-save',
|
||||||
|
|||||||
@ -2,11 +2,11 @@ import Keycloak from "keycloak-js";
|
|||||||
import { oidpSettings } from "../shared/env.const";
|
import { oidpSettings } from "../shared/env.const";
|
||||||
|
|
||||||
const keycloakConfig = {
|
const keycloakConfig = {
|
||||||
url: oidpSettings.server,
|
url: oidpSettings.server,
|
||||||
realm: oidpSettings.realm,
|
realm: oidpSettings.realm,
|
||||||
clientId: oidpSettings.clientId,
|
clientId: oidpSettings.clientId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const keycloak = new Keycloak(keycloakConfig);
|
const keycloakInstance = new Keycloak(keycloakConfig)
|
||||||
|
|
||||||
export default keycloak;
|
export default keycloakInstance;
|
||||||
@ -32,8 +32,6 @@ const RightSideBar = ({ onChangeHidden, children }: RightSideBarProps) => {
|
|||||||
|
|
||||||
const side = 'right'
|
const side = 'right'
|
||||||
|
|
||||||
const hideSizePx = useMantineSize(dimensions.hideSidebarsSize)
|
|
||||||
|
|
||||||
const [visible, { open, close }] = useDisclosure(true);
|
const [visible, { open, close }] = useDisclosure(true);
|
||||||
|
|
||||||
const { classes } = useStyles({ visible })
|
const { classes } = useStyles({ visible })
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useCallback } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { dimensions } from '../dimensions/dimensions';
|
import { dimensions } from '../dimensions/dimensions';
|
||||||
import ColorSchemeToggle from './buttons/ColorSchemeToggle';
|
import ColorSchemeToggle from './buttons/ColorSchemeToggle';
|
||||||
import keycloak from "../../services/keycloak-config";
|
import { useKeycloak } from "@react-keycloak/web";
|
||||||
|
|
||||||
interface UserMenuProps {
|
interface UserMenuProps {
|
||||||
user: { name: string; image: string }
|
user: { name: string; image: string }
|
||||||
@ -14,6 +14,8 @@ const UserMenu = ({ user }: UserMenuProps) => {
|
|||||||
|
|
||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
|
|
||||||
|
const { keycloak, initialized } = useKeycloak()
|
||||||
|
|
||||||
const languages = [
|
const languages = [
|
||||||
{ lng: 'en', name: 'Eng' },
|
{ lng: 'en', name: 'Eng' },
|
||||||
{ lng: 'ru', name: 'Rus' },
|
{ lng: 'ru', name: 'Rus' },
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Accordion, Center, Loader, Text } from '@mantine/core';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Context } from '../../..';
|
import { Context } from '../../..';
|
||||||
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||||
import { GetCameraWHostWConfig, GetFrigateHost, getEventsQuerySchema } from '../../../services/frigate.proxy/frigate.schema';
|
import { GetCameraWHostWConfig, GetFrigateHost, getEventsQuerySchema } from '../../../services/frigate.proxy/frigate.schema';
|
||||||
@ -16,8 +17,10 @@ import EventsAccordionItem from './EventsAccordionItem';
|
|||||||
* @param hostName proxy format, e.g hostName: localhost:4000
|
* @param hostName proxy format, e.g hostName: localhost:4000
|
||||||
*/
|
*/
|
||||||
interface EventsAccordionProps {
|
interface EventsAccordionProps {
|
||||||
day: string,
|
startTime?: number,
|
||||||
hour: string,
|
endTime?: number,
|
||||||
|
day?: string,
|
||||||
|
hour?: string,
|
||||||
camera: GetCameraWHostWConfig
|
camera: GetCameraWHostWConfig
|
||||||
host: GetFrigateHost
|
host: GetFrigateHost
|
||||||
}
|
}
|
||||||
@ -29,6 +32,8 @@ interface EventsAccordionProps {
|
|||||||
* @param hostName proxy format, e.g hostName: localhost:4000
|
* @param hostName proxy format, e.g hostName: localhost:4000
|
||||||
*/
|
*/
|
||||||
const EventsAccordion = ({
|
const EventsAccordion = ({
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
day,
|
day,
|
||||||
hour,
|
hour,
|
||||||
camera,
|
camera,
|
||||||
@ -37,19 +42,32 @@ const EventsAccordion = ({
|
|||||||
const { recordingsStore: recStore } = useContext(Context)
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
const [openedItem, setOpenedItem] = useState<string>()
|
const [openedItem, setOpenedItem] = useState<string>()
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const [retryCount, setRetryCount] = useState(0)
|
||||||
|
const MAX_RETRY_COUNT = 3
|
||||||
|
|
||||||
const hostName = mapHostToHostname(host)
|
const hostName = mapHostToHostname(host)
|
||||||
const isRequiredParams = host && camera
|
const isRequiredParams = (host && camera) || !(day && hour) || !(startTime && endTime)
|
||||||
|
|
||||||
const { data, isPending, isError, refetch } = useQuery({
|
const { data, isPending, isError, refetch } = useQuery({
|
||||||
queryKey: [frigateQueryKeys.getEvents, host, camera, day, hour],
|
queryKey: [frigateQueryKeys.getEvents, host, camera, day, hour, startTime, endTime],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
if (!isRequiredParams) return null
|
if (!isRequiredParams) return null
|
||||||
const [startTime, endTime] = getUnixTime(day, hour)
|
let queryStartTime: number
|
||||||
|
let queryEndTime: number
|
||||||
|
if (day && hour) {
|
||||||
|
[queryStartTime, queryEndTime] = getUnixTime(day, hour)
|
||||||
|
} else if (startTime && endTime) {
|
||||||
|
queryStartTime = startTime
|
||||||
|
queryEndTime = endTime
|
||||||
|
}
|
||||||
|
else { return null }
|
||||||
const parsed = getEventsQuerySchema.safeParse({
|
const parsed = getEventsQuerySchema.safeParse({
|
||||||
hostName: mapHostToHostname(host),
|
hostName: mapHostToHostname(host),
|
||||||
camerasName: [camera.name],
|
camerasName: [camera.name],
|
||||||
after: startTime,
|
after: queryStartTime,
|
||||||
before: endTime,
|
before: queryEndTime,
|
||||||
hasClip: true,
|
hasClip: true,
|
||||||
includeThumnails: false,
|
includeThumnails: false,
|
||||||
})
|
})
|
||||||
@ -69,12 +87,26 @@ const EventsAccordion = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
},
|
||||||
|
retry: (failureCount, error) => {
|
||||||
|
setRetryCount(failureCount);
|
||||||
|
|
||||||
|
if (failureCount >= MAX_RETRY_COUNT) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isPending) return <Center><Loader /></Center>
|
if (isPending) return <Center><Loader /></Center>
|
||||||
|
if (isError && retryCount >= MAX_RETRY_COUNT) {
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
<Text>{t('maxRetries', { maxRetries: MAX_RETRY_COUNT })}</Text>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (isError) return <RetryError onRetry={refetch} />
|
if (isError) return <RetryError onRetry={refetch} />
|
||||||
if (!data || data.length < 1) return <Center><Text>Not have events at that period</Text></Center>
|
if (!data || data.length < 1) return <Center><Text>{t('notHaveEventsAtThatPeriod')}</Text></Center>
|
||||||
|
|
||||||
const handleOpenPlayer = (value: string | undefined) => {
|
const handleOpenPlayer = (value: string | undefined) => {
|
||||||
if (value !== recStore.playedItem) {
|
if (value !== recStore.playedItem) {
|
||||||
@ -94,7 +126,7 @@ const EventsAccordion = ({
|
|||||||
recStore.playedItem = undefined
|
recStore.playedItem = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hostName) throw Error('EventsAccordion hostName must be exist')
|
if (!hostName) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion
|
<Accordion
|
||||||
@ -105,11 +137,11 @@ const EventsAccordion = ({
|
|||||||
>
|
>
|
||||||
{data.map(event => (
|
{data.map(event => (
|
||||||
<EventsAccordionItem
|
<EventsAccordionItem
|
||||||
key={event.id}
|
key={event.id}
|
||||||
event={event}
|
event={event}
|
||||||
hostName={hostName}
|
hostName={hostName}
|
||||||
played={recStore.playedItem === event.id}
|
played={recStore.playedItem === event.id}
|
||||||
openPlayer={handleOpenPlayer}
|
openPlayer={handleOpenPlayer}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|||||||
65
src/shared/components/filters/CameraSelect.tsx
Normal file
65
src/shared/components/filters/CameraSelect.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { Loader, MantineStyleSystemProps, SpacingValue, SystemProp } from '@mantine/core';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { FC, useEffect } from 'react';
|
||||||
|
import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api';
|
||||||
|
import RetryError from '../RetryError';
|
||||||
|
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
|
||||||
|
|
||||||
|
|
||||||
|
interface CameraSelectProps extends MantineStyleSystemProps {
|
||||||
|
hostId: string
|
||||||
|
label?: string
|
||||||
|
valueId?: string
|
||||||
|
defaultId?: string
|
||||||
|
spaceBetween?: SystemProp<SpacingValue>
|
||||||
|
placeholder?: string
|
||||||
|
onChange?: (value: string) => void
|
||||||
|
onSuccess?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CameraSelect: FC<CameraSelectProps> = ({
|
||||||
|
hostId,
|
||||||
|
label,
|
||||||
|
valueId,
|
||||||
|
defaultId,
|
||||||
|
spaceBetween,
|
||||||
|
placeholder,
|
||||||
|
onChange,
|
||||||
|
onSuccess,
|
||||||
|
...styleProps
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const { data, isError, isPending, isSuccess, refetch } = useQuery({
|
||||||
|
queryKey: [frigateQueryKeys.getCameraByHostId, hostId],
|
||||||
|
queryFn: () => frigateApi.getCamerasByHostId(hostId)
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onSuccess) onSuccess()
|
||||||
|
}, [isSuccess])
|
||||||
|
|
||||||
|
if (isPending) return <Loader />
|
||||||
|
if (isError) return <RetryError onRetry={refetch} />
|
||||||
|
if (!data) return null
|
||||||
|
|
||||||
|
const camerasItems: OneSelectItem[] = data.map(camera => ({ value: camera.id, label: camera.name }))
|
||||||
|
|
||||||
|
const handleSelect = (value: string) => {
|
||||||
|
if (onChange) onChange(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OneSelectFilter
|
||||||
|
id='frigate-cameras'
|
||||||
|
label={label}
|
||||||
|
spaceBetween='1rem'
|
||||||
|
value={valueId || ''}
|
||||||
|
defaultValue={defaultId || ''}
|
||||||
|
data={camerasItems}
|
||||||
|
onChange={handleSelect}
|
||||||
|
{...styleProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CameraSelect;
|
||||||
@ -1,14 +1,11 @@
|
|||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
import React, { useContext, useEffect } from 'react';
|
|
||||||
import { Context } from '../../..';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api';
|
import { observer } from 'mobx-react-lite';
|
||||||
import CogwheelLoader from '../loaders/CogwheelLoader';
|
import { useContext } from 'react';
|
||||||
import { Center, Loader, Text } from '@mantine/core';
|
|
||||||
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
|
|
||||||
import RetryError from '../RetryError';
|
|
||||||
import { isProduction } from '../../env.const';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Context } from '../../..';
|
||||||
|
import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api';
|
||||||
|
import { isProduction } from '../../env.const';
|
||||||
|
import CameraSelect from './CameraSelect';
|
||||||
|
|
||||||
interface CameraSelectFilterProps {
|
interface CameraSelectFilterProps {
|
||||||
selectedHostId: string,
|
selectedHostId: string,
|
||||||
@ -20,28 +17,22 @@ const CameraSelectFilter = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { recordingsStore: recStore } = useContext(Context)
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
|
|
||||||
const { data, isError, isPending, isSuccess, refetch } = useQuery({
|
const { data } = useQuery({
|
||||||
queryKey: [frigateQueryKeys.getCameraByHostId, selectedHostId],
|
queryKey: [frigateQueryKeys.getCameraByHostId, selectedHostId],
|
||||||
queryFn: () => frigateApi.getCamerasByHostId(selectedHostId)
|
queryFn: () => frigateApi.getCamerasByHostId(selectedHostId)
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
const handleSuccess = () => {
|
||||||
if (!data) return
|
if (!data) return
|
||||||
if (recStore.cameraIdParam) {
|
if (recStore.cameraIdParam) {
|
||||||
if (!isProduction) console.log('change camera by param')
|
if (!isProduction) console.log('change camera by param')
|
||||||
recStore.filteredCamera = data.find( camera => camera.id === recStore.cameraIdParam)
|
recStore.filteredCamera = data.find(camera => camera.id === recStore.cameraIdParam)
|
||||||
recStore.cameraIdParam = undefined
|
recStore.cameraIdParam = undefined
|
||||||
}
|
}
|
||||||
}, [isSuccess])
|
}
|
||||||
|
|
||||||
if (isPending) return <Loader />
|
|
||||||
if (isError) return <RetryError onRetry={refetch}/>
|
|
||||||
if (!data) return null
|
|
||||||
|
|
||||||
const camerasItems: OneSelectItem[] = data.map(camera => ({ value: camera.id, label: camera.name }))
|
|
||||||
|
|
||||||
const handleSelect = (value: string) => {
|
const handleSelect = (value: string) => {
|
||||||
const camera = data.find(camera => camera.id === value)
|
const camera = data?.find(camera => camera.id === value)
|
||||||
if (!camera) {
|
if (!camera) {
|
||||||
recStore.filteredCamera = undefined
|
recStore.filteredCamera = undefined
|
||||||
return
|
return
|
||||||
@ -52,14 +43,15 @@ const CameraSelectFilter = ({
|
|||||||
if (!isProduction) console.log('CameraSelectFilter rendered')
|
if (!isProduction) console.log('CameraSelectFilter rendered')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OneSelectFilter
|
|
||||||
id='frigate-cameras'
|
< CameraSelect
|
||||||
|
hostId={selectedHostId}
|
||||||
label={t('selectCamera')}
|
label={t('selectCamera')}
|
||||||
spaceBetween='1rem'
|
spaceBetween='1rem'
|
||||||
value={recStore.filteredCamera?.id || ''}
|
valueId={recStore.filteredCamera?.id || ''}
|
||||||
defaultValue={recStore.filteredCamera?.id || ''}
|
defaultId={recStore.filteredCamera?.id || ''}
|
||||||
data={camerasItems}
|
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
|
onSuccess={handleSuccess}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,24 +1,23 @@
|
|||||||
import { Box, Flex, Indicator, Text } from '@mantine/core';
|
import { Box, Flex, Indicator, Text } from '@mantine/core';
|
||||||
import { DatePickerInput } from '@mantine/dates';
|
import { DatePickerInput } from '@mantine/dates';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { FC } from 'react';
|
||||||
import { useContext } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Context } from '../../..';
|
|
||||||
import { isProduction } from '../../env.const';
|
|
||||||
|
|
||||||
interface DateRangeSelectFilterProps {}
|
interface DateRangeSelectFilterProps {
|
||||||
|
onChange?(value: [Date | null, Date | null]): void
|
||||||
const DateRangeSelectFilter = ({
|
value?: [Date | null, Date | null]
|
||||||
|
}
|
||||||
|
|
||||||
|
const DateRangeSelectFilter: FC<DateRangeSelectFilterProps> = ({
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
}: DateRangeSelectFilterProps) => {
|
}: DateRangeSelectFilterProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { recordingsStore: recStore } = useContext(Context)
|
|
||||||
|
|
||||||
const handlePick = (value: [Date | null, Date | null]) => {
|
const handlePick = (value: [Date | null, Date | null]) => {
|
||||||
recStore.selectedRange = value
|
if (onChange) onChange(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isProduction) console.log('DateRangeSelectFilter rendered')
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Flex
|
<Flex
|
||||||
@ -35,7 +34,7 @@ const DateRangeSelectFilter = ({
|
|||||||
type="range"
|
type="range"
|
||||||
mx="auto"
|
mx="auto"
|
||||||
maw={400}
|
maw={400}
|
||||||
value={recStore.selectedRange}
|
value={value}
|
||||||
onChange={handlePick}
|
onChange={handlePick}
|
||||||
renderDay={(date) => {
|
renderDay={(date) => {
|
||||||
const day = date.getDate();
|
const day = date.getDate();
|
||||||
@ -53,4 +52,4 @@ const DateRangeSelectFilter = ({
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default observer(DateRangeSelectFilter);
|
export default DateRangeSelectFilter;
|
||||||
81
src/shared/components/filters/TimePicker.tsx
Normal file
81
src/shared/components/filters/TimePicker.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { ActionIcon, Box, Flex, Text } from '@mantine/core';
|
||||||
|
import { TimeInput } from '@mantine/dates';
|
||||||
|
import { useDebouncedState, useDebouncedValue } from '@mantine/hooks';
|
||||||
|
import { IconClock, IconX } from '@tabler/icons-react';
|
||||||
|
import React, { FC, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
interface TimePickerProps {
|
||||||
|
value?: string
|
||||||
|
defaultValue?: string
|
||||||
|
label?: string
|
||||||
|
onChange?(value: string | undefined): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const TimePicker: FC<TimePickerProps> = ({
|
||||||
|
defaultValue,
|
||||||
|
label,
|
||||||
|
onChange
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const ref = useRef<HTMLInputElement | null>(null)
|
||||||
|
const [value, setValue] = useState(defaultValue)
|
||||||
|
const [debounced] = useDebouncedValue(value, 1600)
|
||||||
|
|
||||||
|
const [pickerOpened, setPickerOpened] = useState(false)
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const inputValue = e.currentTarget.value
|
||||||
|
setValue(inputValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onChange) onChange(debounced)
|
||||||
|
}, [debounced])
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (!pickerOpened) {
|
||||||
|
ref.current?.showPicker();
|
||||||
|
setPickerOpened(true);
|
||||||
|
} else {
|
||||||
|
setPickerOpened(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Flex
|
||||||
|
mt='1rem'
|
||||||
|
justify='space-between'>
|
||||||
|
<Text>{label}</Text>
|
||||||
|
</Flex>
|
||||||
|
<TimeInput
|
||||||
|
value={value}
|
||||||
|
mt='1rem'
|
||||||
|
ref={ref}
|
||||||
|
rightSection={
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
!value ? null :
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() => setValue('')}
|
||||||
|
ml='-1.7rem'
|
||||||
|
>
|
||||||
|
<IconX size='1rem' />
|
||||||
|
</ActionIcon>
|
||||||
|
}
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() => ref.current?.showPicker()}>
|
||||||
|
<IconClock size="1rem" stroke={1.5} />
|
||||||
|
</ActionIcon>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
maw={400}
|
||||||
|
mx="auto"
|
||||||
|
onChange={handleChange}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TimePicker;
|
||||||
102
src/shared/stores/events.store.ts
Normal file
102
src/shared/stores/events.store.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { makeAutoObservable } from "mobx";
|
||||||
|
|
||||||
|
interface Filters {
|
||||||
|
hostId?: string
|
||||||
|
cameraId?: string
|
||||||
|
period?: [Date | null, Date | null]
|
||||||
|
startTime?: string
|
||||||
|
endTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventsStore {
|
||||||
|
filters: Filters = {}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
this.loadFiltersFromURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFiltersFromURL() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
this.filters.hostId = params.get('hostId') || undefined;
|
||||||
|
this.filters.cameraId = params.get('cameraId') || undefined;
|
||||||
|
const startDate = params.get('startDate');
|
||||||
|
const endDate = params.get('endDate');
|
||||||
|
if (startDate && endDate) {
|
||||||
|
this.filters.period = [new Date(startDate), new Date(endDate)]
|
||||||
|
}
|
||||||
|
this.filters.startTime = params.get('startTime') || undefined
|
||||||
|
this.filters.endTime = params.get('endTime') || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
setHostId(hostId: string, navigate: (path: string) => void) {
|
||||||
|
this.filters.hostId = hostId;
|
||||||
|
this.updateURL(navigate)
|
||||||
|
}
|
||||||
|
|
||||||
|
setCameraId(cameraId: string, navigate: (path: string) => void) {
|
||||||
|
this.filters.cameraId = cameraId;
|
||||||
|
this.updateURL(navigate)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPeriod(period: [Date | null, Date | null], navigate: (path: string) => void) {
|
||||||
|
this.filters.period = period;
|
||||||
|
this.updateURL(navigate)
|
||||||
|
}
|
||||||
|
|
||||||
|
setStartTime(startTime: string, navigate: (path: string) => void) {
|
||||||
|
this.filters.startTime = startTime
|
||||||
|
this.updateURL(navigate)
|
||||||
|
}
|
||||||
|
|
||||||
|
setEndTime(endTime: string, navigate: (path: string) => void) {
|
||||||
|
this.filters.endTime = endTime
|
||||||
|
this.updateURL(navigate)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateURL(navigate: (path: string) => void) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (this.filters.hostId) params.set('hostId', this.filters.hostId);
|
||||||
|
if (this.filters.cameraId) params.set('cameraId', this.filters.cameraId);
|
||||||
|
if (this.filters.period) {
|
||||||
|
const [startDate, endDate] = this.filters.period;
|
||||||
|
if (startDate instanceof Date && !isNaN(startDate.getTime())) {
|
||||||
|
params.set('startDate', startDate.toISOString());
|
||||||
|
}
|
||||||
|
if (endDate instanceof Date && !isNaN(endDate.getTime())) {
|
||||||
|
params.set('endDate', endDate.toISOString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.filters.startTime) params.set('startTime', this.filters.startTime)
|
||||||
|
if (this.filters.endTime) params.set('endTime', this.filters.endTime)
|
||||||
|
|
||||||
|
navigate(`?${params.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
isPeriodSet() {
|
||||||
|
if (this.getStartDay() && this.getEndDay()) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
getStartDay() {
|
||||||
|
if (this.filters.period) {
|
||||||
|
const [startDate, endDate] = this.filters.period;
|
||||||
|
if (startDate instanceof Date && !isNaN(startDate.getTime())) {
|
||||||
|
return startDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
getEndDay() {
|
||||||
|
if (this.filters.period) {
|
||||||
|
const [startDate, endDate] = this.filters.period;
|
||||||
|
if (endDate instanceof Date && !isNaN(endDate.getTime())) {
|
||||||
|
return endDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -61,13 +61,4 @@ export class RecordingsStore {
|
|||||||
public set selectedRange(value: [Date | null, Date | null]) {
|
public set selectedRange(value: [Date | null, Date | null]) {
|
||||||
this._selectedRange = value
|
this._selectedRange = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Delete
|
|
||||||
// private _openedCamera: GetCameraWHostWConfig | undefined
|
|
||||||
// public get openedCamera(): GetCameraWHostWConfig | undefined {
|
|
||||||
// return this._openedCamera
|
|
||||||
// }
|
|
||||||
// public set openedCamera(value: GetCameraWHostWConfig | undefined) {
|
|
||||||
// this._openedCamera = value
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { EventsStore } from "./events.store";
|
||||||
import { MainStore } from "./main.store";
|
import { MainStore } from "./main.store";
|
||||||
import { ModalStore } from "./modal.store";
|
import { ModalStore } from "./modal.store";
|
||||||
import { RecordingsStore } from "./recordings.store";
|
import { RecordingsStore } from "./recordings.store";
|
||||||
@ -8,11 +9,13 @@ class RootStore {
|
|||||||
userStore: UserStore
|
userStore: UserStore
|
||||||
modalStore: ModalStore
|
modalStore: ModalStore
|
||||||
recordingsStore: RecordingsStore
|
recordingsStore: RecordingsStore
|
||||||
|
eventsStore: EventsStore
|
||||||
constructor() {
|
constructor() {
|
||||||
this.mainStore = new MainStore()
|
this.mainStore = new MainStore()
|
||||||
this.userStore = new UserStore()
|
this.userStore = new UserStore()
|
||||||
this.modalStore = new ModalStore(this)
|
this.modalStore = new ModalStore(this)
|
||||||
this.recordingsStore = new RecordingsStore()
|
this.recordingsStore = new RecordingsStore()
|
||||||
|
this.eventsStore = new EventsStore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -133,6 +133,41 @@ export const getUnixTime = (day?: string, hour?: number | string) => {
|
|||||||
return [unixTimeStart, unixTimeEnd];
|
return [unixTimeStart, unixTimeEnd];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param date JS Date
|
||||||
|
* @returns unixTime
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const dateToUnixTime = (date: Date) => {
|
||||||
|
return date.getTime() / 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param period [start: begin of Day, end: end of Day]
|
||||||
|
* @returns [start: unixTimeStart, end: unixTimeEnd]
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const dayRangeToUnixPeriod = (period: [Date, Date]) => {
|
||||||
|
const start = period[0]
|
||||||
|
const end = period[1]
|
||||||
|
|
||||||
|
start.setHours(0, 0, 0, 0)
|
||||||
|
end.setHours(23, 59, 59)
|
||||||
|
|
||||||
|
const startTime = dateToUnixTime(start)
|
||||||
|
const endTime = dateToUnixTime(end)
|
||||||
|
return [startTime, endTime]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dayTimeToUnixTime = (day: Date, time: string) => {
|
||||||
|
const [hours, minutes] = time.split(':').map(Number)
|
||||||
|
day.setHours(hours);
|
||||||
|
day.setMinutes(minutes);
|
||||||
|
day.setSeconds(0);
|
||||||
|
day.setMilliseconds(0);
|
||||||
|
return Math.floor(day.getTime() / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function takes in a Unix timestamp, configuration options for date/time display, and an optional strftime format string,
|
* This function takes in a Unix timestamp, configuration options for date/time display, and an optional strftime format string,
|
||||||
* and returns a formatted date/time string.
|
* and returns a formatted date/time string.
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { Flex } from '@mantine/core';
|
import { Flex } from '@mantine/core';
|
||||||
import { dataTagSymbol, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { notifications } from '@mantine/notifications';
|
||||||
|
import { IconAlertCircle } from '@tabler/icons-react';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||||
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
|
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
|
||||||
import AddBadge from '../shared/components/AddBadge';
|
import AddBadge from '../shared/components/AddBadge';
|
||||||
import TagBadge from '../shared/components/TagBadge';
|
import TagBadge from '../shared/components/TagBadge';
|
||||||
import { CameraTag, PutUserTag } from '../types/tags';
|
import { CameraTag } from '../types/tags';
|
||||||
import { notifications } from '@mantine/notifications';
|
|
||||||
import { IconAlertCircle } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
|
|
||||||
interface CameraTagsListProps {
|
interface CameraTagsListProps {
|
||||||
|
|||||||
52
src/widgets/EventsBody.tsx
Normal file
52
src/widgets/EventsBody.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||||
|
import EventsAccordion from '../shared/components/accordion/EventsAccordion';
|
||||||
|
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||||
|
import RetryError from '../shared/components/RetryError';
|
||||||
|
import { dayTimeToUnixTime } from '../shared/utils/dateUtil';
|
||||||
|
|
||||||
|
interface EventsBodyProps {
|
||||||
|
hostId: string,
|
||||||
|
cameraId: string,
|
||||||
|
period: [Date, Date],
|
||||||
|
startTime?: string,
|
||||||
|
endTime?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventsBody: FC<EventsBodyProps> = ({
|
||||||
|
hostId,
|
||||||
|
cameraId,
|
||||||
|
period,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const startTimeUnix = dayTimeToUnixTime(period[0], startTime ? startTime : '00:00')
|
||||||
|
const endTimeUnix = dayTimeToUnixTime(period[1], endTime ? endTime : '23:59')
|
||||||
|
|
||||||
|
const { data, isError, isPending, refetch } = useQuery({
|
||||||
|
queryKey: [frigateQueryKeys.getCameraById, cameraId, frigateQueryKeys.getFrigateHost, hostId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const host = await frigateApi.getHost(hostId)
|
||||||
|
const camera = await frigateApi.getCameraById(cameraId)
|
||||||
|
return { camera, host }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isPending) return <CenterLoader />
|
||||||
|
if (isError) return <RetryError onRetry={refetch} />
|
||||||
|
if (!data) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EventsAccordion
|
||||||
|
camera={data.camera}
|
||||||
|
host={data.host}
|
||||||
|
startTime={startTimeUnix}
|
||||||
|
endTime={endTimeUnix}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(EventsBody);
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
|
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
|
||||||
import { dateToQueryString, getResolvedTimeZone } from '../shared/utils/dateUtil';
|
import { dateToQueryString, getResolvedTimeZone } from '../shared/utils/dateUtil';
|
||||||
import { Context } from '..';
|
import { Context } from '..';
|
||||||
@ -9,6 +9,7 @@ import CenterLoader from '../shared/components/loaders/CenterLoader';
|
|||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import DayAccordion from '../shared/components/accordion/DayAccordion';
|
import DayAccordion from '../shared/components/accordion/DayAccordion';
|
||||||
import { isProduction } from '../shared/env.const';
|
import { isProduction } from '../shared/env.const';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface SelectedDayListProps {
|
interface SelectedDayListProps {
|
||||||
day: Date
|
day: Date
|
||||||
@ -21,6 +22,12 @@ const SelectedDayList = ({
|
|||||||
const camera = recStore.filteredCamera
|
const camera = recStore.filteredCamera
|
||||||
const host = recStore.filteredHost
|
const host = recStore.filteredHost
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
|
||||||
|
const [retryCount, setRetryCount] = useState(0)
|
||||||
|
const MAX_RETRY_COUNT = 3
|
||||||
|
|
||||||
const { data, isPending, isError, refetch } = useQuery({
|
const { data, isPending, isError, refetch } = useQuery({
|
||||||
queryKey: [frigateQueryKeys.getRecordingsSummary, recStore.filteredCamera?.id, day],
|
queryKey: [frigateQueryKeys.getRecordingsSummary, recStore.filteredCamera?.id, day],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@ -31,6 +38,13 @@ const SelectedDayList = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
},
|
||||||
|
retry: (failureCount, error) => {
|
||||||
|
setRetryCount(failureCount);
|
||||||
|
|
||||||
|
if (failureCount >= MAX_RETRY_COUNT) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -39,6 +53,15 @@ const SelectedDayList = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isPending) return <CenterLoader />
|
if (isPending) return <CenterLoader />
|
||||||
|
|
||||||
|
if (isError && retryCount >= MAX_RETRY_COUNT) {
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
<Text>{t('maxRetries', { maxRetries: MAX_RETRY_COUNT })}</Text>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isError) return <RetryErrorPage onRetry={handleRetry} />
|
if (isError) return <RetryErrorPage onRetry={handleRetry} />
|
||||||
if (!camera || !host) return <Center><Text>Please select host or camera</Text></Center>
|
if (!camera || !host) return <Center><Text>Please select host or camera</Text></Center>
|
||||||
if (!data) return <Text>Not have response from server</Text>
|
if (!data) return <Text>Not have response from server</Text>
|
||||||
@ -56,7 +79,7 @@ const SelectedDayList = ({
|
|||||||
camera={camera}
|
camera={camera}
|
||||||
recordSummary={recordingsDay} />
|
recordSummary={recordingsDay} />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default observer(SelectedDayList);
|
export default observer(SelectedDayList)
|
||||||
@ -6,7 +6,7 @@ import UserMenu from '../../shared/components/UserMenu';
|
|||||||
import ColorSchemeToggle from "../../shared/components/buttons/ColorSchemeToggle";
|
import ColorSchemeToggle from "../../shared/components/buttons/ColorSchemeToggle";
|
||||||
import Logo from "../../shared/components/images/LogoImage";
|
import Logo from "../../shared/components/images/LogoImage";
|
||||||
import DrawerMenu from "../../shared/components/menu/DrawerMenu";
|
import DrawerMenu from "../../shared/components/menu/DrawerMenu";
|
||||||
import keycloak from "../../services/keycloak-config";
|
import { useKeycloak } from "@react-keycloak/web";
|
||||||
|
|
||||||
const HEADER_HEIGHT = rem(60)
|
const HEADER_HEIGHT = rem(60)
|
||||||
|
|
||||||
@ -62,6 +62,7 @@ export const HeaderAction = ({ links }: HeaderActionProps) => {
|
|||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { isAdmin } = useAdminRole()
|
const { isAdmin } = useAdminRole()
|
||||||
|
const { keycloak, initialized } = useKeycloak()
|
||||||
|
|
||||||
const handleNavigate = (link: string) => {
|
const handleNavigate = (link: string) => {
|
||||||
navigate(link)
|
navigate(link)
|
||||||
|
|||||||
93
src/widgets/sidebars/EventsRightFilters.tsx
Normal file
93
src/widgets/sidebars/EventsRightFilters.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Context } from '../..';
|
||||||
|
import CameraSelect from '../../shared/components/filters/CameraSelect';
|
||||||
|
import DateRangeSelect from '../../shared/components/filters/DateRangeSelect';
|
||||||
|
import HostSelect from '../../shared/components/filters/HostSelect';
|
||||||
|
import TimePicker from '../../shared/components/filters/TimePicker';
|
||||||
|
import { isProduction } from '../../shared/env.const';
|
||||||
|
|
||||||
|
const EventsRightFilters = () => {
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { eventsStore } = useContext(Context)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
|
||||||
|
const handleHostSelect = (hostId: string) => {
|
||||||
|
eventsStore.setHostId(hostId, navigate)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCameraSelect = (cameraId: string) => {
|
||||||
|
eventsStore.setCameraId(cameraId, navigate)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePeriodSelect = (value: [Date | null, Date | null]) => {
|
||||||
|
eventsStore.setPeriod(value, navigate)
|
||||||
|
if (!isProduction) console.log('Selected period: ', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectStartTime = (value: string) => {
|
||||||
|
eventsStore.setStartTime(value, navigate)
|
||||||
|
if (!isProduction) console.log('Selected start time: ', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectEndTime = (value: string) => {
|
||||||
|
eventsStore.setEndTime(value, navigate)
|
||||||
|
if (!isProduction) console.log('Selected end time: ', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const validatedStartTime = () => {
|
||||||
|
if (eventsStore.filters.startTime && eventsStore.filters.endTime) {
|
||||||
|
if (eventsStore.filters.startTime > eventsStore.filters.endTime) {
|
||||||
|
return eventsStore.filters.endTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return eventsStore.filters.startTime
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HostSelect
|
||||||
|
label={t('selectHost')}
|
||||||
|
valueId={eventsStore.filters.hostId}
|
||||||
|
onChange={handleHostSelect}
|
||||||
|
/>
|
||||||
|
{!eventsStore.filters.hostId ? null :
|
||||||
|
<CameraSelect
|
||||||
|
label={t('selectCamera')}
|
||||||
|
hostId={eventsStore.filters.hostId}
|
||||||
|
valueId={eventsStore.filters.cameraId}
|
||||||
|
onChange={handleCameraSelect}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{!eventsStore.filters.cameraId ? null :
|
||||||
|
<DateRangeSelect
|
||||||
|
onChange={handlePeriodSelect}
|
||||||
|
value={eventsStore.filters.period}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{!eventsStore.isPeriodSet() ? null :
|
||||||
|
<>
|
||||||
|
<TimePicker
|
||||||
|
defaultValue={eventsStore.filters.startTime}
|
||||||
|
key='startTime'
|
||||||
|
label={t('eventsPage.selectStartTime')}
|
||||||
|
onChange={handleSelectStartTime}
|
||||||
|
/>
|
||||||
|
<TimePicker
|
||||||
|
defaultValue={eventsStore.filters.endTime}
|
||||||
|
key='endTime'
|
||||||
|
label={t('eventsPage.selectEndTime')}
|
||||||
|
onChange={handleSelectEndTime}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(EventsRightFilters);
|
||||||
@ -2,7 +2,7 @@ import React, { useContext } from 'react';
|
|||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { Context } from '../..';
|
import { Context } from '../..';
|
||||||
import CameraSelectFilter from '../../shared/components/filters/CameraSelectFilter';
|
import CameraSelectFilter from '../../shared/components/filters/CameraSelectFilter';
|
||||||
import DateRangeSelectFilter from '../../shared/components/filters/DateRangeSelectFilter';
|
import DateRangeSelectFilter from '../../shared/components/filters/DateRangeSelect';
|
||||||
import RecordingsHostFilter from '../../shared/components/filters/RecordingsHostFilter';
|
import RecordingsHostFilter from '../../shared/components/filters/RecordingsHostFilter';
|
||||||
import { isProduction } from '../../shared/env.const';
|
import { isProduction } from '../../shared/env.const';
|
||||||
|
|
||||||
@ -10,16 +10,23 @@ const RecordingsFiltersRightSide = () => {
|
|||||||
const { recordingsStore: recStore } = useContext(Context)
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
|
|
||||||
if (!isProduction) console.log('RecordingsFiltersRightSide rendered')
|
if (!isProduction) console.log('RecordingsFiltersRightSide rendered')
|
||||||
|
|
||||||
|
const handleDatePick = (value: [Date | null, Date | null]) => {
|
||||||
|
recStore.selectedRange = value
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RecordingsHostFilter />
|
<RecordingsHostFilter />
|
||||||
{recStore.filteredHost ?
|
{recStore.filteredHost ?
|
||||||
<CameraSelectFilter
|
<CameraSelectFilter
|
||||||
selectedHostId={recStore.filteredHost.id} />
|
selectedHostId={recStore.filteredHost.id} />
|
||||||
: <></>
|
: <></>
|
||||||
}
|
}
|
||||||
{recStore.filteredCamera ?
|
{recStore.filteredCamera ?
|
||||||
<DateRangeSelectFilter />
|
<DateRangeSelectFilter
|
||||||
|
onChange={handleDatePick}
|
||||||
|
value={recStore.selectedRange}
|
||||||
|
/>
|
||||||
: <></>
|
: <></>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -2,19 +2,19 @@ import React, { createContext, ReactNode, useState } from 'react';
|
|||||||
|
|
||||||
interface SideBarContextProps {
|
interface SideBarContextProps {
|
||||||
childrenComponent: ReactNode;
|
childrenComponent: ReactNode;
|
||||||
setChildrenComponent: (component: ReactNode) => void;
|
setRightChildren: (component: ReactNode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SideBarContext = createContext<SideBarContextProps>({
|
export const SideBarContext = createContext<SideBarContextProps>({
|
||||||
childrenComponent: null,
|
childrenComponent: null,
|
||||||
setChildrenComponent: () => {},
|
setRightChildren: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SideBarProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
export const SideBarProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
const [childrenComponent, setChildrenComponent] = useState<ReactNode>(null);
|
const [rightChildren, setRightChildren] = useState<ReactNode>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SideBarContext.Provider value={{ childrenComponent, setChildrenComponent }}>
|
<SideBarContext.Provider value={{ childrenComponent: rightChildren, setRightChildren }}>
|
||||||
{children}
|
{children}
|
||||||
</SideBarContext.Provider>
|
</SideBarContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user