add events page
This commit is contained in:
parent
12b508661a
commit
f935de1eeb
@ -1,14 +1,13 @@
|
||||
import { AppShell, useMantineTheme, } from "@mantine/core";
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Context } from '.';
|
||||
import { useLocation } from "react-router-dom";
|
||||
import AppRouter from './router/AppRouter';
|
||||
import { routesPath } from './router/routes.path';
|
||||
import RightSideBar from "./shared/components/RightSideBar";
|
||||
import { isProduction } from './shared/env.const';
|
||||
import { HeaderAction } from './widgets/header/HeaderAction';
|
||||
import RightSideBar from "./shared/components/RightSideBar";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const AppBody = () => {
|
||||
const { t } = useTranslation()
|
||||
@ -17,6 +16,7 @@ const AppBody = () => {
|
||||
{ link: routesPath.MAIN_PATH, label: t('header.home') },
|
||||
{ link: routesPath.SETTINGS_PATH, label: t('header.settings'), admin: true },
|
||||
{ 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.ACCESS_PATH, label: t('header.acessSettings'), admin: true },
|
||||
]
|
||||
@ -25,7 +25,7 @@ const AppBody = () => {
|
||||
const location = useLocation()
|
||||
|
||||
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 [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 { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import RootStore from './shared/stores/root.store';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import './services/i18n';
|
||||
import { ReactKeycloakProvider } from '@react-keycloak/web';
|
||||
import keycloak from './services/keycloak-config';
|
||||
import keycloakInstance from './services/keycloak-config';
|
||||
import CenterLoader from './shared/components/loaders/CenterLoader';
|
||||
import RootStore from './shared/stores/root.store';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
@ -15,7 +15,6 @@ const root = ReactDOM.createRoot(
|
||||
|
||||
export const hostURL = new URL(window.location.href)
|
||||
|
||||
|
||||
const rootStore = new RootStore()
|
||||
export const Context = createContext<RootStore>(rootStore)
|
||||
|
||||
@ -25,11 +24,11 @@ const eventLogger = (event: string, error?: any) => {
|
||||
|
||||
const tokenLogger = (tokens: any) => {
|
||||
console.log('onKeycloakTokens', tokens);
|
||||
};
|
||||
}
|
||||
|
||||
root.render(
|
||||
<ReactKeycloakProvider
|
||||
authClient={keycloak}
|
||||
authClient={keycloakInstance}
|
||||
LoadingComponent={<CenterLoader />}
|
||||
onEvent={eventLogger}
|
||||
onTokens={tokenLogger}
|
||||
|
||||
@ -14,6 +14,10 @@ const en = {
|
||||
height: 'Height',
|
||||
points: 'Points',
|
||||
},
|
||||
eventsPage: {
|
||||
selectStartTime: 'Select start time:',
|
||||
selectEndTime: 'Select end time:',
|
||||
},
|
||||
frigateConfigPage: {
|
||||
copyConfig: 'Copy Config',
|
||||
saveOnly: 'Save Only',
|
||||
@ -74,6 +78,7 @@ const en = {
|
||||
home: 'Main',
|
||||
settings: 'Settings',
|
||||
recordings: 'Recordings',
|
||||
events: 'Events',
|
||||
hostsConfig: 'Frigate servers',
|
||||
acessSettings: 'Access settings',
|
||||
},
|
||||
@ -93,6 +98,7 @@ const en = {
|
||||
doubleClickToFullHint: 'Double click for fullscreen',
|
||||
rating: 'Rating',
|
||||
},
|
||||
maxRetries: 'Error: Unable to fetch data after {{maxRetries}} retries. Please try again later or change period to smaller.',
|
||||
config: 'Config',
|
||||
create: 'Create',
|
||||
clear: 'Clear',
|
||||
@ -116,6 +122,7 @@ const en = {
|
||||
second: 'Second',
|
||||
events: 'Events',
|
||||
notHaveEvents: 'No events',
|
||||
notHaveEventsAtThatPeriod: 'Not have events at that period',
|
||||
selectHost: 'Select host',
|
||||
selectCamera: 'Select Camera',
|
||||
selectRange: 'Select period',
|
||||
|
||||
@ -12,6 +12,11 @@ const ru = {
|
||||
height: 'Высота',
|
||||
points: 'Точки',
|
||||
},
|
||||
eventsPage: {
|
||||
selectStartTime: 'Выбери время начала:',
|
||||
selectEndTime: 'Выбери время окончания:',
|
||||
maxEventsFetches: 'Ошибка: Невозможно получить события после {{maxRetries}} попыток. Пожалуйста попробуйте позже или установите меньший период.',
|
||||
},
|
||||
frigateConfigPage: {
|
||||
copyConfig: 'Копировать Конфиг.',
|
||||
saveOnly: 'Только Сохранить',
|
||||
@ -72,6 +77,7 @@ const ru = {
|
||||
home: 'Главная',
|
||||
settings: 'Настройки',
|
||||
recordings: 'Записи',
|
||||
events: 'События',
|
||||
hostsConfig: 'Серверы Frigate',
|
||||
acessSettings: 'Настройка доступа',
|
||||
},
|
||||
@ -114,6 +120,7 @@ const ru = {
|
||||
second: 'Час',
|
||||
events: 'События',
|
||||
notHaveEvents: 'Событий нет',
|
||||
notHaveEventsAtThatPeriod: 'Нет событий за этот период',
|
||||
selectHost: 'Выбери хост',
|
||||
selectCamera: 'Выбери камеру',
|
||||
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 { t } = useTranslation()
|
||||
const { mainStore } = useContext(Context)
|
||||
const { setChildrenComponent } = useContext(SideBarContext)
|
||||
const { setRightChildren } = useContext(SideBarContext)
|
||||
const { selectedHostId, selectedTags } = mainStore
|
||||
const [searchQuery, setSearchQuery] = useState<string>()
|
||||
const [filteredCameras, setFilteredCameras] = useState<GetCameraWHostWConfig[]>()
|
||||
@ -35,8 +35,8 @@ const MainPage = () => {
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setChildrenComponent(<MainFiltersRightSide />);
|
||||
return () => setChildrenComponent(null);
|
||||
setRightChildren(<MainFiltersRightSide />);
|
||||
return () => setRightChildren(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -13,7 +13,6 @@ import SelectedHostList from '../widgets/SelectedHostList';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SideBarContext } from '../widgets/sidebars/SideBarContext';
|
||||
|
||||
|
||||
export const recordingsPageQuery = {
|
||||
hostId: 'hostId',
|
||||
cameraId: 'cameraId',
|
||||
@ -41,12 +40,11 @@ const RecordingsPage = () => {
|
||||
const [cameraId, setCameraId] = useState<string>('')
|
||||
const [period, setPeriod] = useState<[Date | null, Date | null]>([null, null])
|
||||
|
||||
const { setChildrenComponent } = useContext(SideBarContext)
|
||||
const { setRightChildren } = useContext(SideBarContext)
|
||||
|
||||
useEffect(() => {
|
||||
setChildrenComponent(<RecordingsFiltersRightSide />);
|
||||
|
||||
return () => setChildrenComponent(null);
|
||||
setRightChildren(<RecordingsFiltersRightSide />);
|
||||
return () => setRightChildren(null);
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ export const routesPath = {
|
||||
MAIN_PATH: '/',
|
||||
BIRDSEYE_PATH: '/birdseye',
|
||||
RECORDINGS_PATH: '/recordings',
|
||||
EVENTS_PATH: '/events',
|
||||
SETTINGS_PATH: '/settings',
|
||||
HOSTS_PATH: '/hosts',
|
||||
HOST_CONFIG_PATH: '/hosts/:id/config',
|
||||
|
||||
@ -14,6 +14,7 @@ import RecordingsPage from "../pages/RecordingsPage";
|
||||
import AccessSettings from "../pages/AccessSettingsPage";
|
||||
import PlayRecordPage from "../pages/PlayRecordPage";
|
||||
import EditCameraPage from "../pages/EditCameraPage";
|
||||
import EventsPage from "../pages/EventsPage";
|
||||
|
||||
interface IRoute {
|
||||
path: string,
|
||||
@ -33,6 +34,10 @@ export const routes: IRoute[] = [
|
||||
path: routesPath.RECORDINGS_PATH,
|
||||
component: <RecordingsPage />,
|
||||
},
|
||||
{
|
||||
path: routesPath.EVENTS_PATH,
|
||||
component: <EventsPage />,
|
||||
},
|
||||
{
|
||||
path: routesPath.HOST_CONFIG_PATH,
|
||||
component: <HostConfigPage />,
|
||||
|
||||
@ -15,7 +15,7 @@ import { EventFrigate } from "../../types/event";
|
||||
import { getResolvedTimeZone } from "../../shared/utils/dateUtil";
|
||||
import { FrigateStats, GetFfprobe, GetHostStorage, GetVaInfo } from "../../types/frigateStats";
|
||||
import { PostSaveConfig, SaveOption } from "../../types/saveConfig";
|
||||
import keycloak from "../keycloak-config";
|
||||
import keycloakInstance from "../keycloak-config";
|
||||
import { PutMask } from "../../types/mask";
|
||||
import { GetUserTag, PutUserTag } from "../../types/tags";
|
||||
|
||||
@ -26,7 +26,7 @@ const instanceApi = axios.create({
|
||||
|
||||
instanceApi.interceptors.request.use(
|
||||
config => {
|
||||
const accessToken = keycloak.token;
|
||||
const accessToken = keycloakInstance.token;
|
||||
if (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),
|
||||
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),
|
||||
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),
|
||||
getCamerasWHost: () => instanceApi.get<GetCameraWHostWConfig[]>(`apiv1/cameras`).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',
|
||||
getCamerasWHost: 'cameras-frigate-host',
|
||||
getCameraWHost: 'camera-frigate-host',
|
||||
getCameraById: 'camera-by-Id',
|
||||
getCameraByHostId: 'camera-by-hostId',
|
||||
getHostConfig: 'host-config',
|
||||
postHostConfig: 'host-config-save',
|
||||
|
||||
@ -2,11 +2,11 @@ import Keycloak from "keycloak-js";
|
||||
import { oidpSettings } from "../shared/env.const";
|
||||
|
||||
const keycloakConfig = {
|
||||
url: oidpSettings.server,
|
||||
realm: oidpSettings.realm,
|
||||
clientId: oidpSettings.clientId,
|
||||
};
|
||||
url: oidpSettings.server,
|
||||
realm: oidpSettings.realm,
|
||||
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 hideSizePx = useMantineSize(dimensions.hideSidebarsSize)
|
||||
|
||||
const [visible, { open, close }] = useDisclosure(true);
|
||||
|
||||
const { classes } = useStyles({ visible })
|
||||
|
||||
@ -4,7 +4,7 @@ import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { dimensions } from '../dimensions/dimensions';
|
||||
import ColorSchemeToggle from './buttons/ColorSchemeToggle';
|
||||
import keycloak from "../../services/keycloak-config";
|
||||
import { useKeycloak } from "@react-keycloak/web";
|
||||
|
||||
interface UserMenuProps {
|
||||
user: { name: string; image: string }
|
||||
@ -14,6 +14,8 @@ const UserMenu = ({ user }: UserMenuProps) => {
|
||||
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const { keycloak, initialized } = useKeycloak()
|
||||
|
||||
const languages = [
|
||||
{ lng: 'en', name: 'Eng' },
|
||||
{ lng: 'ru', name: 'Rus' },
|
||||
|
||||
@ -2,6 +2,7 @@ import { Accordion, Center, Loader, Text } from '@mantine/core';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Context } from '../../..';
|
||||
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
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
|
||||
*/
|
||||
interface EventsAccordionProps {
|
||||
day: string,
|
||||
hour: string,
|
||||
startTime?: number,
|
||||
endTime?: number,
|
||||
day?: string,
|
||||
hour?: string,
|
||||
camera: GetCameraWHostWConfig
|
||||
host: GetFrigateHost
|
||||
}
|
||||
@ -29,6 +32,8 @@ interface EventsAccordionProps {
|
||||
* @param hostName proxy format, e.g hostName: localhost:4000
|
||||
*/
|
||||
const EventsAccordion = ({
|
||||
startTime,
|
||||
endTime,
|
||||
day,
|
||||
hour,
|
||||
camera,
|
||||
@ -37,19 +42,32 @@ const EventsAccordion = ({
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
const [openedItem, setOpenedItem] = useState<string>()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [retryCount, setRetryCount] = useState(0)
|
||||
const MAX_RETRY_COUNT = 3
|
||||
|
||||
const hostName = mapHostToHostname(host)
|
||||
const isRequiredParams = host && camera
|
||||
const isRequiredParams = (host && camera) || !(day && hour) || !(startTime && endTime)
|
||||
|
||||
const { data, isPending, isError, refetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getEvents, host, camera, day, hour],
|
||||
queryKey: [frigateQueryKeys.getEvents, host, camera, day, hour, startTime, endTime],
|
||||
queryFn: () => {
|
||||
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({
|
||||
hostName: mapHostToHostname(host),
|
||||
camerasName: [camera.name],
|
||||
after: startTime,
|
||||
before: endTime,
|
||||
after: queryStartTime,
|
||||
before: queryEndTime,
|
||||
hasClip: true,
|
||||
includeThumnails: false,
|
||||
})
|
||||
@ -69,12 +87,26 @@ const EventsAccordion = ({
|
||||
)
|
||||
}
|
||||
return null
|
||||
},
|
||||
retry: (failureCount, error) => {
|
||||
setRetryCount(failureCount);
|
||||
|
||||
if (failureCount >= MAX_RETRY_COUNT) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
})
|
||||
|
||||
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 (!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) => {
|
||||
if (value !== recStore.playedItem) {
|
||||
@ -94,7 +126,7 @@ const EventsAccordion = ({
|
||||
recStore.playedItem = undefined
|
||||
}
|
||||
|
||||
if (!hostName) throw Error('EventsAccordion hostName must be exist')
|
||||
if (!hostName) return null
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
@ -105,11 +137,11 @@ const EventsAccordion = ({
|
||||
>
|
||||
{data.map(event => (
|
||||
<EventsAccordionItem
|
||||
key={event.id}
|
||||
event={event}
|
||||
hostName={hostName}
|
||||
played={recStore.playedItem === event.id}
|
||||
openPlayer={handleOpenPlayer}
|
||||
key={event.id}
|
||||
event={event}
|
||||
hostName={hostName}
|
||||
played={recStore.playedItem === event.id}
|
||||
openPlayer={handleOpenPlayer}
|
||||
/>
|
||||
))}
|
||||
</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 { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api';
|
||||
import CogwheelLoader from '../loaders/CogwheelLoader';
|
||||
import { Center, Loader, Text } from '@mantine/core';
|
||||
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
|
||||
import RetryError from '../RetryError';
|
||||
import { isProduction } from '../../env.const';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
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 {
|
||||
selectedHostId: string,
|
||||
@ -20,28 +17,22 @@ const CameraSelectFilter = ({
|
||||
const { t } = useTranslation()
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const { data, isError, isPending, isSuccess, refetch } = useQuery({
|
||||
const { data } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getCameraByHostId, selectedHostId],
|
||||
queryFn: () => frigateApi.getCamerasByHostId(selectedHostId)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const handleSuccess = () => {
|
||||
if (!data) return
|
||||
if (recStore.cameraIdParam) {
|
||||
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
|
||||
}
|
||||
}, [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 camera = data.find(camera => camera.id === value)
|
||||
const camera = data?.find(camera => camera.id === value)
|
||||
if (!camera) {
|
||||
recStore.filteredCamera = undefined
|
||||
return
|
||||
@ -52,14 +43,15 @@ const CameraSelectFilter = ({
|
||||
if (!isProduction) console.log('CameraSelectFilter rendered')
|
||||
|
||||
return (
|
||||
<OneSelectFilter
|
||||
id='frigate-cameras'
|
||||
|
||||
< CameraSelect
|
||||
hostId={selectedHostId}
|
||||
label={t('selectCamera')}
|
||||
spaceBetween='1rem'
|
||||
value={recStore.filteredCamera?.id || ''}
|
||||
defaultValue={recStore.filteredCamera?.id || ''}
|
||||
data={camerasItems}
|
||||
valueId={recStore.filteredCamera?.id || ''}
|
||||
defaultId={recStore.filteredCamera?.id || ''}
|
||||
onChange={handleSelect}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,24 +1,23 @@
|
||||
import { Box, Flex, Indicator, Text } from '@mantine/core';
|
||||
import { DatePickerInput } from '@mantine/dates';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Context } from '../../..';
|
||||
import { isProduction } from '../../env.const';
|
||||
|
||||
interface DateRangeSelectFilterProps {}
|
||||
|
||||
const DateRangeSelectFilter = ({
|
||||
interface DateRangeSelectFilterProps {
|
||||
onChange?(value: [Date | null, Date | null]): void
|
||||
value?: [Date | null, Date | null]
|
||||
}
|
||||
|
||||
const DateRangeSelectFilter: FC<DateRangeSelectFilterProps> = ({
|
||||
onChange,
|
||||
value,
|
||||
}: DateRangeSelectFilterProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const handlePick = (value: [Date | null, Date | null]) => {
|
||||
recStore.selectedRange = value
|
||||
if (onChange) onChange(value)
|
||||
}
|
||||
|
||||
if (!isProduction) console.log('DateRangeSelectFilter rendered')
|
||||
return (
|
||||
<Box>
|
||||
<Flex
|
||||
@ -35,7 +34,7 @@ const DateRangeSelectFilter = ({
|
||||
type="range"
|
||||
mx="auto"
|
||||
maw={400}
|
||||
value={recStore.selectedRange}
|
||||
value={value}
|
||||
onChange={handlePick}
|
||||
renderDay={(date) => {
|
||||
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]) {
|
||||
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 { ModalStore } from "./modal.store";
|
||||
import { RecordingsStore } from "./recordings.store";
|
||||
@ -8,11 +9,13 @@ class RootStore {
|
||||
userStore: UserStore
|
||||
modalStore: ModalStore
|
||||
recordingsStore: RecordingsStore
|
||||
eventsStore: EventsStore
|
||||
constructor() {
|
||||
this.mainStore = new MainStore()
|
||||
this.userStore = new UserStore()
|
||||
this.modalStore = new ModalStore(this)
|
||||
this.recordingsStore = new RecordingsStore()
|
||||
this.eventsStore = new EventsStore()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -133,6 +133,41 @@ export const getUnixTime = (day?: string, hour?: number | string) => {
|
||||
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,
|
||||
* and returns a formatted date/time string.
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
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 { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
|
||||
import AddBadge from '../shared/components/AddBadge';
|
||||
import TagBadge from '../shared/components/TagBadge';
|
||||
import { CameraTag, PutUserTag } from '../types/tags';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconAlertCircle } from '@tabler/icons-react';
|
||||
import { CameraTag } from '../types/tags';
|
||||
|
||||
|
||||
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 React, { useContext } from 'react';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
|
||||
import { dateToQueryString, getResolvedTimeZone } from '../shared/utils/dateUtil';
|
||||
import { Context } from '..';
|
||||
@ -9,6 +9,7 @@ import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import DayAccordion from '../shared/components/accordion/DayAccordion';
|
||||
import { isProduction } from '../shared/env.const';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SelectedDayListProps {
|
||||
day: Date
|
||||
@ -21,6 +22,12 @@ const SelectedDayList = ({
|
||||
const camera = recStore.filteredCamera
|
||||
const host = recStore.filteredHost
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
||||
const [retryCount, setRetryCount] = useState(0)
|
||||
const MAX_RETRY_COUNT = 3
|
||||
|
||||
const { data, isPending, isError, refetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getRecordingsSummary, recStore.filteredCamera?.id, day],
|
||||
queryFn: async () => {
|
||||
@ -31,6 +38,13 @@ const SelectedDayList = ({
|
||||
}
|
||||
}
|
||||
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 (isError && retryCount >= MAX_RETRY_COUNT) {
|
||||
return (
|
||||
<Center>
|
||||
<Text>{t('maxRetries', { maxRetries: MAX_RETRY_COUNT })}</Text>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) return <RetryErrorPage onRetry={handleRetry} />
|
||||
if (!camera || !host) return <Center><Text>Please select host or camera</Text></Center>
|
||||
if (!data) return <Text>Not have response from server</Text>
|
||||
@ -56,7 +79,7 @@ const SelectedDayList = ({
|
||||
camera={camera}
|
||||
recordSummary={recordingsDay} />
|
||||
</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 Logo from "../../shared/components/images/LogoImage";
|
||||
import DrawerMenu from "../../shared/components/menu/DrawerMenu";
|
||||
import keycloak from "../../services/keycloak-config";
|
||||
import { useKeycloak } from "@react-keycloak/web";
|
||||
|
||||
const HEADER_HEIGHT = rem(60)
|
||||
|
||||
@ -62,6 +62,7 @@ export const HeaderAction = ({ links }: HeaderActionProps) => {
|
||||
const { classes } = useStyles();
|
||||
const navigate = useNavigate()
|
||||
const { isAdmin } = useAdminRole()
|
||||
const { keycloak, initialized } = useKeycloak()
|
||||
|
||||
const handleNavigate = (link: string) => {
|
||||
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 { Context } from '../..';
|
||||
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 { isProduction } from '../../shared/env.const';
|
||||
|
||||
@ -10,16 +10,23 @@ const RecordingsFiltersRightSide = () => {
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
if (!isProduction) console.log('RecordingsFiltersRightSide rendered')
|
||||
|
||||
const handleDatePick = (value: [Date | null, Date | null]) => {
|
||||
recStore.selectedRange = value
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<RecordingsHostFilter />
|
||||
{recStore.filteredHost ?
|
||||
<CameraSelectFilter
|
||||
selectedHostId={recStore.filteredHost.id} />
|
||||
selectedHostId={recStore.filteredHost.id} />
|
||||
: <></>
|
||||
}
|
||||
{recStore.filteredCamera ?
|
||||
<DateRangeSelectFilter />
|
||||
<DateRangeSelectFilter
|
||||
onChange={handleDatePick}
|
||||
value={recStore.selectedRange}
|
||||
/>
|
||||
: <></>
|
||||
}
|
||||
</>
|
||||
|
||||
@ -2,19 +2,19 @@ import React, { createContext, ReactNode, useState } from 'react';
|
||||
|
||||
interface SideBarContextProps {
|
||||
childrenComponent: ReactNode;
|
||||
setChildrenComponent: (component: ReactNode) => void;
|
||||
setRightChildren: (component: ReactNode) => void;
|
||||
}
|
||||
|
||||
export const SideBarContext = createContext<SideBarContextProps>({
|
||||
childrenComponent: null,
|
||||
setChildrenComponent: () => {},
|
||||
setRightChildren: () => {},
|
||||
});
|
||||
|
||||
export const SideBarProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [childrenComponent, setChildrenComponent] = useState<ReactNode>(null);
|
||||
const [rightChildren, setRightChildren] = useState<ReactNode>(null);
|
||||
|
||||
return (
|
||||
<SideBarContext.Provider value={{ childrenComponent, setChildrenComponent }}>
|
||||
<SideBarContext.Provider value={{ childrenComponent: rightChildren, setRightChildren }}>
|
||||
{children}
|
||||
</SideBarContext.Provider>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user