add filter and query params
This commit is contained in:
parent
f369692133
commit
a971ea55a4
@ -3,9 +3,9 @@ import { AppShell, useMantineTheme, } from "@mantine/core"
|
|||||||
import { HeaderAction } from './widgets/header/HeaderAction';
|
import { HeaderAction } from './widgets/header/HeaderAction';
|
||||||
import { testHeaderLinks } from './widgets/header/header.links';
|
import { testHeaderLinks } from './widgets/header/header.links';
|
||||||
import AppRouter from './router/AppRouter';
|
import AppRouter from './router/AppRouter';
|
||||||
import { SideBar } from './shared/components/SideBar';
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { Context } from '.';
|
import { Context } from '.';
|
||||||
|
import SideBar from './shared/components/SideBar';
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,7 @@ import { GetConfig, DeleteFrigateHost, GetFrigateHost, PutConfig, PutFrigateHost
|
|||||||
import { FrigateConfig } from "../../types/frigateConfig";
|
import { FrigateConfig } from "../../types/frigateConfig";
|
||||||
import { url } from "inspector";
|
import { url } from "inspector";
|
||||||
import { RecordSummary } from "../../types/record";
|
import { RecordSummary } from "../../types/record";
|
||||||
|
import { EventFrigate } from "../../types/event";
|
||||||
|
|
||||||
|
|
||||||
const instanceApi = axios.create({
|
const instanceApi = axios.create({
|
||||||
@ -59,25 +60,32 @@ export const proxyApi = {
|
|||||||
cameraName: string,
|
cameraName: string,
|
||||||
timezone: string,
|
timezone: string,
|
||||||
) =>
|
) =>
|
||||||
instanceApi.get<RecordSummary[]>(`proxy/${hostName}/api/${cameraName}/recordings/summary`, {params: { timezone}}).then(res => res.data),
|
instanceApi.get<RecordSummary[]>(`proxy/${hostName}/api/${cameraName}/recordings/summary`, { params: { timezone } }).then(res => res.data),
|
||||||
|
|
||||||
|
// E.g. http://127.0.0.1:5000/api/events?before=1708534799&after=1708448400&camera=CameraName&has_clip=1&include_thumbnails=0&limit=5000
|
||||||
getEvents: (
|
getEvents: (
|
||||||
hostName: string,
|
hostName: string,
|
||||||
camerasName: string[],
|
camerasName: string[],
|
||||||
timezone: string,
|
timezone?: string,
|
||||||
minScore?: number,
|
hasClip?: boolean,
|
||||||
maxScore?: number,
|
|
||||||
after?: number,
|
after?: number,
|
||||||
before?: number,
|
before?: number,
|
||||||
labels?: string[],
|
labels?: string[],
|
||||||
|
limit: number = 5000,
|
||||||
|
includeThumnails?: boolean,
|
||||||
|
minScore?: number,
|
||||||
|
maxScore?: number,
|
||||||
) =>
|
) =>
|
||||||
instanceApi.get(`proxy/${hostName}/api/events`, {
|
instanceApi.get<EventFrigate[]>(`proxy/${hostName}/api/events`, {
|
||||||
params: {
|
params: {
|
||||||
cameras: camerasName,
|
cameras: camerasName.join(','), // frigate format
|
||||||
after: after,
|
|
||||||
timezone: timezone,
|
timezone: timezone,
|
||||||
|
after: after,
|
||||||
before: before, // @before the last event start_time in list
|
before: before, // @before the last event start_time in list
|
||||||
|
has_clip: hasClip,
|
||||||
|
include_thumbnails: includeThumnails,
|
||||||
labels: labels,
|
labels: labels,
|
||||||
|
limit: limit,
|
||||||
min_score: minScore,
|
min_score: minScore,
|
||||||
max_score: maxScore,
|
max_score: maxScore,
|
||||||
}
|
}
|
||||||
@ -120,4 +128,5 @@ export const frigateQueryKeys = {
|
|||||||
getHostConfig: 'host-config',
|
getHostConfig: 'host-config',
|
||||||
getRecordingsSummary: 'recordings-frigate-summary',
|
getRecordingsSummary: 'recordings-frigate-summary',
|
||||||
getRecordings: 'recordings-frigate',
|
getRecordings: 'recordings-frigate',
|
||||||
|
getEvents: 'events-frigate',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,6 +58,20 @@ export const deleteFrigateHostSchema = putFrigateHostSchema.pick({
|
|||||||
id: true,
|
id: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getEventsQuerySchema = z.object({
|
||||||
|
hostName: z.string(),
|
||||||
|
camerasName: z.string().array(),
|
||||||
|
timezone: z.string().optional(),
|
||||||
|
hasClip: z.boolean().optional(),
|
||||||
|
after: z.number().optional(),
|
||||||
|
before: z.number().optional(),
|
||||||
|
labels: z.string().array().optional(),
|
||||||
|
limit: z.number().optional(),
|
||||||
|
includeThumnails: z.boolean().optional(),
|
||||||
|
minScore: z.number().optional(),
|
||||||
|
maxScore: z.number().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
export type GetConfig = z.infer<typeof getConfigSchema>
|
export type GetConfig = z.infer<typeof getConfigSchema>
|
||||||
export type PutConfig = z.infer<typeof putConfigSchema>
|
export type PutConfig = z.infer<typeof putConfigSchema>
|
||||||
export type GetFrigateHost = z.infer<typeof getFrigateHostSchema>
|
export type GetFrigateHost = z.infer<typeof getFrigateHostSchema>
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const useStyles = createStyles((theme,
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
export const SideBar = observer(({ isHidden, side, children }: SideBarProps) => {
|
const SideBar = ({ isHidden, side, children }: SideBarProps) => {
|
||||||
const hideSizePx = useMantineSize(dimensions.hideSidebarsSize)
|
const hideSizePx = useMantineSize(dimensions.hideSidebarsSize)
|
||||||
const [visible, { open, close }] = useDisclosure(window.innerWidth > hideSizePx);
|
const [visible, { open, close }] = useDisclosure(window.innerWidth > hideSizePx);
|
||||||
const manualVisible: React.MutableRefObject<null | boolean> = useRef(null)
|
const manualVisible: React.MutableRefObject<null | boolean> = useRef(null)
|
||||||
@ -43,7 +43,7 @@ export const SideBar = observer(({ isHidden, side, children }: SideBarProps) =>
|
|||||||
|
|
||||||
const { sideBarsStore } = useContext(Context)
|
const { sideBarsStore } = useContext(Context)
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect(() => {
|
||||||
if (sideBarsStore.rightVisible && side === 'right' && !visible) {
|
if (sideBarsStore.rightVisible && side === 'right' && !visible) {
|
||||||
open()
|
open()
|
||||||
} else if (!sideBarsStore.rightVisible && side === 'right' && visible) {
|
} else if (!sideBarsStore.rightVisible && side === 'right' && visible) {
|
||||||
@ -117,5 +117,6 @@ export const SideBar = observer(({ isHidden, side, children }: SideBarProps) =>
|
|||||||
<SideButton side={side} hide={visible} onClick={() => handleClickVisible(true)} />
|
<SideButton side={side} hide={visible} onClick={() => handleClickVisible(true)} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
export default observer(SideBar)
|
||||||
@ -3,10 +3,10 @@ import React, { useContext, useEffect, useState } from 'react';
|
|||||||
import { GetCameraWHostWConfig, GetFrigateHost } from '../../../services/frigate.proxy/frigate.schema';
|
import { GetCameraWHostWConfig, GetFrigateHost } from '../../../services/frigate.proxy/frigate.schema';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||||
import CogwheelLoader from '../CogwheelLoader';
|
|
||||||
import DayAccordion from './DayAccordion';
|
import DayAccordion from './DayAccordion';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { Context } from '../../..';
|
import { Context } from '../../..';
|
||||||
|
import { getResolvedTimeZone } from '../frigate/dateUtil';
|
||||||
|
|
||||||
interface CameraAccordionProps {
|
interface CameraAccordionProps {
|
||||||
camera: GetCameraWHostWConfig,
|
camera: GetCameraWHostWConfig,
|
||||||
@ -17,14 +17,14 @@ const CameraAccordion = observer(({
|
|||||||
camera,
|
camera,
|
||||||
host
|
host
|
||||||
}: CameraAccordionProps) => {
|
}: CameraAccordionProps) => {
|
||||||
const { recordingsStore } = useContext(Context)
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
|
|
||||||
const { data, isPending, isError } = useQuery({
|
const { data, isPending, isError } = useQuery({
|
||||||
queryKey: [frigateQueryKeys.getRecordingsSummary, camera?.id],
|
queryKey: [frigateQueryKeys.getRecordingsSummary, camera?.id],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
if (camera && host) {
|
if (camera && host) {
|
||||||
const hostName = mapHostToHostname(host)
|
const hostName = mapHostToHostname(host)
|
||||||
return proxyApi.getRecordingsSummary(hostName, camera.name, 'Asia/Krasnoyarsk')
|
return proxyApi.getRecordingsSummary(hostName, camera.name, getResolvedTimeZone())
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -34,26 +34,23 @@ const CameraAccordion = observer(({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openedDay) {
|
if (openedDay) {
|
||||||
recordingsStore.playedRecord.cameraName = camera.name
|
recStore.recordToPlay.cameraName = camera.name
|
||||||
const hostName = mapHostToHostname(host)
|
const hostName = mapHostToHostname(host)
|
||||||
recordingsStore.playedRecord.hostName = hostName
|
recStore.recordToPlay.hostName = hostName
|
||||||
}
|
}
|
||||||
}, [openedDay])
|
}, [openedDay])
|
||||||
|
|
||||||
const handleClick = (value: string | null) => {
|
const handleClick = (value: string | null) => {
|
||||||
setOpenedDay(value)
|
setOpenedDay(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPending) return (
|
if (isPending) return <Center><Text>Loading...</Text></Center>
|
||||||
<Center>
|
if (isError) return <Center><Text>Loading error</Text></Center>
|
||||||
<Text>Loading...</Text>
|
|
||||||
</Center>
|
|
||||||
)
|
|
||||||
if (isError) return <Text>Loading error</Text>
|
|
||||||
|
|
||||||
if (!data || !camera) return null
|
if (!data || !camera) return null
|
||||||
|
|
||||||
const days = data.map(rec => (
|
|
||||||
|
const days = data.slice(0, 2).map(rec => (
|
||||||
<Accordion.Item key={rec.day} value={rec.day}>
|
<Accordion.Item key={rec.day} value={rec.day}>
|
||||||
<Accordion.Control key={rec.day + 'control'}>{rec.day}</Accordion.Control>
|
<Accordion.Control key={rec.day + 'control'}>{rec.day}</Accordion.Control>
|
||||||
<Accordion.Panel key={rec.day + 'panel'}>
|
<Accordion.Panel key={rec.day + 'panel'}>
|
||||||
@ -62,6 +59,9 @@ const CameraAccordion = observer(({
|
|||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
|
|
||||||
))
|
))
|
||||||
|
|
||||||
|
console.log('CameraAccordion rendered')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion variant='separated' radius="md" w='100%' onChange={handleClick}>
|
<Accordion variant='separated' radius="md" w='100%' onChange={handleClick}>
|
||||||
{days}
|
{days}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Accordion, Flex, Group, Text } from '@mantine/core';
|
import { Accordion, Center, Flex, Group, Text } from '@mantine/core';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { RecordHour, RecordSummary, Recording } from '../../../types/record';
|
import { RecordHour, RecordSummary, Recording } from '../../../types/record';
|
||||||
import Button from '../frigate/Button';
|
import Button from '../frigate/Button';
|
||||||
@ -8,6 +8,9 @@ import PlayControl from './PlayControl';
|
|||||||
import { frigateApi, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
import { frigateApi, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||||
import { Context } from '../../..';
|
import { Context } from '../../..';
|
||||||
import VideoPlayer from '../frigate/VideoPlayer';
|
import VideoPlayer from '../frigate/VideoPlayer';
|
||||||
|
import { getResolvedTimeZone } from '../frigate/dateUtil';
|
||||||
|
import EventsAccordion from './EventsAccordion';
|
||||||
|
import DayEventsAccordion from './DayEventsAccordion';
|
||||||
|
|
||||||
interface RecordingAccordionProps {
|
interface RecordingAccordionProps {
|
||||||
recordSummary?: RecordSummary
|
recordSummary?: RecordSummary
|
||||||
@ -25,11 +28,11 @@ const DayAccordion = observer(({
|
|||||||
if (openVideoPlayer) {
|
if (openVideoPlayer) {
|
||||||
console.log('openVideoPlayer', openVideoPlayer)
|
console.log('openVideoPlayer', openVideoPlayer)
|
||||||
if (openVideoPlayer) {
|
if (openVideoPlayer) {
|
||||||
recordingsStore.playedRecord.day = recordSummary?.day
|
recordingsStore.recordToPlay.day = recordSummary?.day
|
||||||
recordingsStore.playedRecord.hour = openVideoPlayer
|
recordingsStore.recordToPlay.hour = openVideoPlayer
|
||||||
recordingsStore.playedRecord.timezone = 'Asia,Krasnoyarsk'
|
recordingsStore.recordToPlay.timezone = getResolvedTimeZone().replace('/', ',')
|
||||||
const parsed = recordingsStore.getFullPlayedRecord(recordingsStore.playedRecord)
|
const parsed = recordingsStore.getFullRecordForPlay(recordingsStore.recordToPlay)
|
||||||
console.log('recordingsStore.playedRecord: ', recordingsStore.playedRecord)
|
console.log('recordingsStore.playedRecord: ', recordingsStore.recordToPlay)
|
||||||
if (parsed.success) {
|
if (parsed.success) {
|
||||||
const url = proxyApi.recordingURL(
|
const url = proxyApi.recordingURL(
|
||||||
parsed.data.hostName,
|
parsed.data.hostName,
|
||||||
@ -42,7 +45,7 @@ const DayAccordion = observer(({
|
|||||||
setPlayerUrl(url)
|
setPlayerUrl(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}else {
|
} else {
|
||||||
setPlayerUrl(undefined)
|
setPlayerUrl(undefined)
|
||||||
}
|
}
|
||||||
}, [openVideoPlayer])
|
}, [openVideoPlayer])
|
||||||
@ -68,6 +71,8 @@ const DayAccordion = observer(({
|
|||||||
setOpenVideoPlayer(undefined)
|
setOpenVideoPlayer(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('DayAccordion rendered')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion
|
<Accordion
|
||||||
key={recordSummary.day}
|
key={recordSummary.day}
|
||||||
@ -76,14 +81,18 @@ const DayAccordion = observer(({
|
|||||||
value={openedValue}
|
value={openedValue}
|
||||||
onChange={handleClick}
|
onChange={handleClick}
|
||||||
>
|
>
|
||||||
{recordSummary.hours.map(hour => (
|
{recordSummary.hours.slice(0, 5).map(hour => (
|
||||||
<Accordion.Item key={hour.hour + 'Item'} value={hour.hour}>
|
<Accordion.Item key={hour.hour + 'Item'} value={hour.hour}>
|
||||||
<Accordion.Control key={hour.hour + 'Control'}>
|
<Accordion.Control key={hour.hour + 'Control'}>
|
||||||
<PlayControl hour={hour.hour} openVideoPlayer={openVideoPlayer} onClick={handleOpenPlayer}/>
|
<PlayControl label={`Hour ${hour.hour}`} value={hour.hour} openVideoPlayer={openVideoPlayer} onClick={handleOpenPlayer} />
|
||||||
</Accordion.Control>
|
</Accordion.Control>
|
||||||
<Accordion.Panel key={hour.hour + 'Panel'}>
|
<Accordion.Panel key={hour.hour + 'Panel'}>
|
||||||
{openVideoPlayer === hour.hour ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
{openVideoPlayer === hour.hour && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
||||||
Events
|
{hour.events > 0 ?
|
||||||
|
<DayEventsAccordion day={recordSummary.day} hour={hour.hour} qty={hour.events} />
|
||||||
|
:
|
||||||
|
<Center><Text>Not have events</Text></Center>
|
||||||
|
}
|
||||||
</Accordion.Panel>
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
))}
|
))}
|
||||||
|
|||||||
38
src/shared/components/accordion/DayEventsAccordion.tsx
Normal file
38
src/shared/components/accordion/DayEventsAccordion.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { Accordion, Text } from '@mantine/core';
|
||||||
|
import React, { Suspense, lazy, useState } from 'react';
|
||||||
|
const EventsAccordion = lazy(() => import('./EventsAccordion'))
|
||||||
|
|
||||||
|
interface DayEventsAccordionProps {
|
||||||
|
day: string,
|
||||||
|
hour: string,
|
||||||
|
qty?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DayEventsAccordion = ({
|
||||||
|
day,
|
||||||
|
hour,
|
||||||
|
qty,
|
||||||
|
}: DayEventsAccordionProps) => {
|
||||||
|
const [openedItem, setOpenedItem] = useState<string>()
|
||||||
|
|
||||||
|
const handleClick = (value: string | null) => {
|
||||||
|
setOpenedItem(hour)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Accordion onChange={handleClick}>
|
||||||
|
<Accordion.Item value={hour}>
|
||||||
|
<Accordion.Control><Text>Events {qty}</Text></Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
|
{openedItem === hour ?
|
||||||
|
<Suspense>
|
||||||
|
<EventsAccordion day={day} hour={hour} />
|
||||||
|
</Suspense>
|
||||||
|
: <></>
|
||||||
|
}
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DayEventsAccordion;
|
||||||
143
src/shared/components/accordion/EventsAccordion.tsx
Normal file
143
src/shared/components/accordion/EventsAccordion.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { Accordion, Center, Text } from '@mantine/core';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { Context } from '../../..';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { frigateQueryKeys, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||||
|
import { getEventsQuerySchema } from '../../../services/frigate.proxy/frigate.schema';
|
||||||
|
import PlayControl from './PlayControl';
|
||||||
|
import VideoPlayer from '../frigate/VideoPlayer';
|
||||||
|
import { formatUnixTimestampToDateTime, getDurationFromTimestamps, getUnixTime, unixTimeToDate } from '../frigate/dateUtil';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param day frigate format, e.g day: 2024-02-23
|
||||||
|
* @param hour frigate format, e.g hour: 22
|
||||||
|
* @param cameraName e.g Backyard
|
||||||
|
* @param hostName proxy format, e.g hostName: localhost:4000
|
||||||
|
*/
|
||||||
|
interface EventsAccordionProps {
|
||||||
|
day?: string,
|
||||||
|
hour?: string,
|
||||||
|
cameraName?: string
|
||||||
|
hostName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param day frigate format, e.g day: 2024-02-23
|
||||||
|
* @param hour frigate format, e.g hour: 22
|
||||||
|
* @param cameraName e.g Backyard
|
||||||
|
* @param hostName proxy format, e.g hostName: localhost:4000
|
||||||
|
*/
|
||||||
|
const EventsAccordion = observer(({
|
||||||
|
day,
|
||||||
|
hour,
|
||||||
|
cameraName,
|
||||||
|
hostName,
|
||||||
|
// TODO labels, score
|
||||||
|
}: EventsAccordionProps) => {
|
||||||
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
|
const [openVideoPlayer, setOpenVideoPlayer] = useState<string>()
|
||||||
|
const [openedValue, setOpenedValue] = useState<string>()
|
||||||
|
const [playerUrl, setPlayerUrl] = useState<string>()
|
||||||
|
|
||||||
|
const inHostName = hostName || recStore.recordToPlay.hostName
|
||||||
|
const inCameraName = cameraName || recStore.recordToPlay.cameraName
|
||||||
|
const isRequiredParams = inCameraName && inHostName
|
||||||
|
const { data, isPending, isError, refetch } = useQuery({
|
||||||
|
queryKey: [frigateQueryKeys.getEvents, day, hour, inCameraName, inHostName],
|
||||||
|
queryFn: () => {
|
||||||
|
if (!isRequiredParams) return null
|
||||||
|
const [startTime, endTime] = getUnixTime(day, hour)
|
||||||
|
const parsed = getEventsQuerySchema.safeParse({
|
||||||
|
hostName: inHostName,
|
||||||
|
camerasName: [inCameraName],
|
||||||
|
after: startTime,
|
||||||
|
before: endTime,
|
||||||
|
hasClip: true,
|
||||||
|
includeThumnails: false,
|
||||||
|
})
|
||||||
|
if (parsed.success) {
|
||||||
|
return proxyApi.getEvents(
|
||||||
|
parsed.data.hostName,
|
||||||
|
parsed.data.camerasName,
|
||||||
|
parsed.data.timezone,
|
||||||
|
parsed.data.hasClip,
|
||||||
|
parsed.data.after,
|
||||||
|
parsed.data.before,
|
||||||
|
parsed.data.labels,
|
||||||
|
parsed.data.limit,
|
||||||
|
parsed.data.includeThumnails,
|
||||||
|
parsed.data.minScore,
|
||||||
|
parsed.data.maxScore
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (openVideoPlayer) {
|
||||||
|
console.log('openVideoPlayer', openVideoPlayer)
|
||||||
|
if (openVideoPlayer && inHostName) {
|
||||||
|
const url = proxyApi.eventURL(inHostName, openVideoPlayer)
|
||||||
|
console.log('GET EVENT URL: ', url)
|
||||||
|
setPlayerUrl(url)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setPlayerUrl(undefined)
|
||||||
|
}
|
||||||
|
}, [openVideoPlayer])
|
||||||
|
|
||||||
|
if (isPending) return <Center><Text>Loading...</Text></Center>
|
||||||
|
if (isError) return <Center><Text>Loading error</Text></Center>
|
||||||
|
if (!data || data.length < 1) return <Center><Text>Not have events at that period</Text></Center>
|
||||||
|
|
||||||
|
const handleOpenPlayer = (eventId: string) => {
|
||||||
|
// console.log(`openVideoPlayer day:${recordSummary.day} hour:${hour}`)
|
||||||
|
if (openVideoPlayer !== eventId) {
|
||||||
|
setOpenedValue(eventId)
|
||||||
|
setOpenVideoPlayer(eventId)
|
||||||
|
} else if (openedValue === eventId && openVideoPlayer === eventId) {
|
||||||
|
setOpenVideoPlayer(undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = (value: string) => {
|
||||||
|
if (openedValue === value) {
|
||||||
|
setOpenedValue(undefined)
|
||||||
|
} else {
|
||||||
|
setOpenedValue(value)
|
||||||
|
}
|
||||||
|
setOpenVideoPlayer(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion
|
||||||
|
variant='separated'
|
||||||
|
radius="md" w='100%'
|
||||||
|
value={openedValue}
|
||||||
|
onChange={handleClick}
|
||||||
|
>
|
||||||
|
{data.slice(0, 5).map(event => (
|
||||||
|
<Accordion.Item key={event.id + 'Item'} value={event.id}>
|
||||||
|
<Accordion.Control key={event.id + 'Control'}>
|
||||||
|
<PlayControl
|
||||||
|
label={unixTimeToDate(event.start_time)}
|
||||||
|
value={event.id}
|
||||||
|
openVideoPlayer={openVideoPlayer}
|
||||||
|
onClick={handleOpenPlayer} />
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel key={event.id + 'Panel'}>
|
||||||
|
{openVideoPlayer === event.id && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
||||||
|
<Text>Camera: {event.camera}</Text>
|
||||||
|
<Text>Label: {event.label}</Text>
|
||||||
|
<Text>Start: {unixTimeToDate(event.start_time)}</Text>
|
||||||
|
<Text>Duration: {getDurationFromTimestamps(event.start_time, event.end_time)}</Text>
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
export default EventsAccordion;
|
||||||
@ -3,13 +3,15 @@ import { IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface PlayControlProps {
|
interface PlayControlProps {
|
||||||
hour: string,
|
label: string,
|
||||||
|
value: string,
|
||||||
openVideoPlayer?: string,
|
openVideoPlayer?: string,
|
||||||
onClick?: (value: string) => void
|
onClick?: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlayControl = ({
|
const PlayControl = ({
|
||||||
hour,
|
label,
|
||||||
|
value,
|
||||||
openVideoPlayer,
|
openVideoPlayer,
|
||||||
onClick
|
onClick
|
||||||
}: PlayControlProps) => {
|
}: PlayControlProps) => {
|
||||||
@ -18,23 +20,23 @@ const PlayControl = ({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Flex justify='space-between'>
|
<Flex justify='space-between'>
|
||||||
Hour: {hour}
|
{label}
|
||||||
<Group>
|
<Group>
|
||||||
<Text onClick={(event) => {
|
<Text onClick={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
handleClick(hour)
|
handleClick(value)
|
||||||
}}>
|
}}>
|
||||||
{openVideoPlayer === hour ? 'Stop Video' : 'Play Video'}
|
{openVideoPlayer === value ? 'Stop Video' : 'Play Video'}
|
||||||
</Text>
|
</Text>
|
||||||
{openVideoPlayer === hour ?
|
{openVideoPlayer === value ?
|
||||||
<IconPlayerStop onClick={(event) => {
|
<IconPlayerStop onClick={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
handleClick(hour)
|
handleClick(value)
|
||||||
}} />
|
}} />
|
||||||
:
|
:
|
||||||
<IconPlayerPlay onClick={(event) => {
|
<IconPlayerPlay onClick={(event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
handleClick(hour)
|
handleClick(value)
|
||||||
}} />
|
}} />
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/shared/components/filters.aps/CameraSelectFilter.tsx
Normal file
64
src/shared/components/filters.aps/CameraSelectFilter.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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 '../CogwheelLoader';
|
||||||
|
import { Center, Text } from '@mantine/core';
|
||||||
|
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
|
||||||
|
|
||||||
|
interface CameraSelectFilterProps {
|
||||||
|
selectedHostId: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const CameraSelectFilter = ({
|
||||||
|
selectedHostId,
|
||||||
|
}: CameraSelectFilterProps) => {
|
||||||
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
|
|
||||||
|
const { data, isError, isPending, isSuccess } = useQuery({
|
||||||
|
queryKey: [frigateQueryKeys.getFrigateHost, selectedHostId],
|
||||||
|
queryFn: () => frigateApi.getHost(selectedHostId)
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data) return
|
||||||
|
if (recStore.cameraIdParam) {
|
||||||
|
console.log('change camera by param')
|
||||||
|
recStore.selectedCamera = data.cameras.find( camera => camera.id === recStore.cameraIdParam)
|
||||||
|
recStore.cameraIdParam = undefined
|
||||||
|
}
|
||||||
|
}, [isSuccess])
|
||||||
|
|
||||||
|
if (isPending) return <CogwheelLoader />
|
||||||
|
if (isError) return <Center><Text>Loading error!</Text></Center>
|
||||||
|
if (!data) return null
|
||||||
|
|
||||||
|
const camerasItems: OneSelectItem[] = data.cameras.map(camera => ({ value: camera.id, label: camera.name }))
|
||||||
|
|
||||||
|
const handleSelect = (id: string, value: string) => {
|
||||||
|
const camera = data.cameras.find(camera => camera.id === value)
|
||||||
|
if (!camera) {
|
||||||
|
recStore.selectedCamera = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recStore.selectedCamera = camera
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('CameraSelectFilter rendered')
|
||||||
|
// console.log('recStore.selectedCameraId', recStore.selectedCameraId)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OneSelectFilter
|
||||||
|
id='frigate-cameras'
|
||||||
|
label='Select camera:'
|
||||||
|
spaceBetween='1rem'
|
||||||
|
value={recStore.selectedCamera?.id || ''}
|
||||||
|
defaultValue={recStore.selectedCamera?.id || ''}
|
||||||
|
data={camerasItems}
|
||||||
|
onChange={handleSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default observer(CameraSelectFilter);
|
||||||
@ -21,7 +21,8 @@ interface OneSelectFilterProps {
|
|||||||
selectProps?: SelectProps,
|
selectProps?: SelectProps,
|
||||||
display?: SystemProp<CSSProperties['display']>
|
display?: SystemProp<CSSProperties['display']>
|
||||||
showClose?: boolean,
|
showClose?: boolean,
|
||||||
changedState?(id: string, value: string): void
|
value?: string,
|
||||||
|
onChange?(id: string, value: string): void
|
||||||
onClose?(): void
|
onClose?(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,12 +30,12 @@ interface OneSelectFilterProps {
|
|||||||
const OneSelectFilter = ({
|
const OneSelectFilter = ({
|
||||||
id, data, spaceBetween,
|
id, data, spaceBetween,
|
||||||
label, defaultValue, textClassName,
|
label, defaultValue, textClassName,
|
||||||
selectProps, display, showClose, changedState, onClose
|
selectProps, display, showClose, value, onChange, onClose
|
||||||
}: OneSelectFilterProps) => {
|
}: OneSelectFilterProps) => {
|
||||||
|
|
||||||
const handleOnChange = (value: string) => {
|
const handleOnChange = (value: string) => {
|
||||||
if (changedState) {
|
if (onChange) {
|
||||||
changedState(id, value)
|
onChange(id, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,6 +51,7 @@ const OneSelectFilter = ({
|
|||||||
mt={spaceBetween}
|
mt={spaceBetween}
|
||||||
data={data}
|
data={data}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
|
value={value}
|
||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
searchable
|
searchable
|
||||||
clearable
|
clearable
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import Player from 'video.js/dist/types/player';
|
|||||||
import 'video.js/dist/video-js.css'
|
import 'video.js/dist/video-js.css'
|
||||||
|
|
||||||
interface VideoPlayerProps {
|
interface VideoPlayerProps {
|
||||||
videoUrl?: string
|
videoUrl: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => {
|
const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => {
|
||||||
|
|||||||
@ -17,6 +17,40 @@ export const getNowYesterdayInLong = (): number => {
|
|||||||
return dateToLong(getNowYesterday());
|
return dateToLong(getNowYesterday());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const unixTimeToDate = (unixTime: number) => {
|
||||||
|
const date = new Date(unixTime * 1000);
|
||||||
|
|
||||||
|
const formattedDate = date.getFullYear() +
|
||||||
|
'-' + ('0' + (date.getMonth() + 1)).slice(-2) +
|
||||||
|
'-' + ('0' + date.getDate()).slice(-2) +
|
||||||
|
' ' + ('0' + date.getHours()).slice(-2) +
|
||||||
|
':' + ('0' + date.getMinutes()).slice(-2) +
|
||||||
|
':' + ('0' + date.getSeconds()).slice(-2);
|
||||||
|
|
||||||
|
return formattedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param day frigate format, e.g day: 2024-02-23
|
||||||
|
* @param hour frigate format, e.g hour: 22
|
||||||
|
* @returns [start: unixTimeStart, end: unixTimeEnd]
|
||||||
|
*/
|
||||||
|
export const getUnixTime = (day?: string, hour?: number | string) => {
|
||||||
|
if (!day) return []
|
||||||
|
let startHour: Date
|
||||||
|
let endHour: Date
|
||||||
|
if (!hour || hour === 0) {
|
||||||
|
startHour = new Date(`${day}T00:00:00`);
|
||||||
|
endHour = new Date(`${day}T23:59:59`);
|
||||||
|
} else {
|
||||||
|
startHour = new Date(`${day}T${hour}:00:00`);
|
||||||
|
endHour = new Date(`${day}T${hour}:59:59`);
|
||||||
|
}
|
||||||
|
const unixTimeStart = startHour.getTime() / 1000;
|
||||||
|
const unixTimeEnd = endHour.getTime() / 1000;
|
||||||
|
return [unixTimeStart, unixTimeEnd];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@ -80,7 +114,7 @@ const formatMap: {
|
|||||||
* The returned string will either be a named time zone (e.g., "America/Los_Angeles"), or it will follow
|
* The returned string will either be a named time zone (e.g., "America/Los_Angeles"), or it will follow
|
||||||
* the format "UTC±HH:MM".
|
* the format "UTC±HH:MM".
|
||||||
*/
|
*/
|
||||||
const getResolvedTimeZone = () => {
|
export const getResolvedTimeZone = () => {
|
||||||
try {
|
try {
|
||||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -88,8 +122,8 @@ const getResolvedTimeZone = () => {
|
|||||||
return `UTC${offsetMinutes < 0 ? '+' : '-'}${Math.abs(offsetMinutes / 60)
|
return `UTC${offsetMinutes < 0 ? '+' : '-'}${Math.abs(offsetMinutes / 60)
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, '0')}:${Math.abs(offsetMinutes % 60)
|
.padStart(2, '0')}:${Math.abs(offsetMinutes % 60)
|
||||||
.toString()
|
.toString()
|
||||||
.padStart(2, '0')}`;
|
.padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -176,12 +210,12 @@ interface DurationToken {
|
|||||||
* @param end_time: number|null - Unix timestamp for end time
|
* @param end_time: number|null - Unix timestamp for end time
|
||||||
* @returns string - duration or 'In Progress' if end time is not provided
|
* @returns string - duration or 'In Progress' if end time is not provided
|
||||||
*/
|
*/
|
||||||
export const getDurationFromTimestamps = (start_time: number, end_time: number | null): string => {
|
export const getDurationFromTimestamps = (start_time: number, end_time: number | undefined): string => {
|
||||||
if (isNaN(start_time)) {
|
if (isNaN(start_time)) {
|
||||||
return 'Invalid start time';
|
return 'Invalid start time';
|
||||||
}
|
}
|
||||||
let duration = 'In Progress';
|
let duration = 'In Progress';
|
||||||
if (end_time !== null) {
|
if (end_time) {
|
||||||
if (isNaN(end_time)) {
|
if (isNaN(end_time)) {
|
||||||
return 'Invalid end time';
|
return 'Invalid end time';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
|
import { makeAutoObservable } from "mobx"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
import { GetCameraWHostWConfig, GetFrigateHost, GetFrigateHostWithCameras } from "../../services/frigate.proxy/frigate.schema"
|
||||||
|
|
||||||
export type RecordingPlay = {
|
export type RecordForPlay = {
|
||||||
hostName?: string
|
hostName?: string // format 'localhost:4000'
|
||||||
cameraName?: string
|
cameraName?: string
|
||||||
hour?: string
|
hour?: string
|
||||||
day?: string
|
day?: string
|
||||||
timezone?: string
|
timezone?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class RecordingsStore {
|
export class RecordingsStore {
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this)
|
||||||
|
}
|
||||||
|
|
||||||
recordingSchema = z.object({
|
recordingSchema = z.object({
|
||||||
hostName: z.string(),
|
hostName: z.string(),
|
||||||
cameraName: z.string(),
|
cameraName: z.string(),
|
||||||
@ -19,15 +23,46 @@ export class RecordingsStore {
|
|||||||
timezone: z.string(),
|
timezone: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
private _playedRecord: RecordingPlay = {}
|
private _recordToPlay: RecordForPlay = {}
|
||||||
public get playedRecord(): RecordingPlay {
|
public get recordToPlay(): RecordForPlay {
|
||||||
return this._playedRecord
|
return this._recordToPlay
|
||||||
}
|
}
|
||||||
public set playedRecord(value: RecordingPlay) {
|
public set recordToPlay(value: RecordForPlay) {
|
||||||
this._playedRecord = value
|
this._recordToPlay = value
|
||||||
}
|
}
|
||||||
|
getFullRecordForPlay(value: RecordForPlay) {
|
||||||
getFullPlayedRecord(value: RecordingPlay) {
|
|
||||||
return this.recordingSchema.safeParse(value)
|
return this.recordingSchema.safeParse(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _hostIdParam: string | undefined
|
||||||
|
public get hostIdParam(): string | undefined {
|
||||||
|
return this._hostIdParam
|
||||||
|
}
|
||||||
|
public set hostIdParam(value: string | undefined) {
|
||||||
|
this._hostIdParam = value
|
||||||
|
}
|
||||||
|
private _cameraIdParam: string | undefined
|
||||||
|
public get cameraIdParam(): string | undefined {
|
||||||
|
return this._cameraIdParam
|
||||||
|
}
|
||||||
|
public set cameraIdParam(value: string | undefined) {
|
||||||
|
this._cameraIdParam = value
|
||||||
|
}
|
||||||
|
|
||||||
|
private _selectedHost: GetFrigateHost | undefined
|
||||||
|
public get selectedHost(): GetFrigateHost | undefined {
|
||||||
|
return this._selectedHost
|
||||||
|
}
|
||||||
|
public set selectedHost(value: GetFrigateHost | undefined) {
|
||||||
|
this._selectedHost = value
|
||||||
|
}
|
||||||
|
private _selectedCamera: GetCameraWHostWConfig | undefined
|
||||||
|
public get selectedCamera(): GetCameraWHostWConfig | undefined {
|
||||||
|
return this._selectedCamera
|
||||||
|
}
|
||||||
|
public set selectedCamera(value: GetCameraWHostWConfig | undefined) {
|
||||||
|
this._selectedCamera = value
|
||||||
|
}
|
||||||
|
selectedStartDay: string = ''
|
||||||
|
selectedEndDay: string = ''
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
export interface Event {
|
export interface EventFrigate {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
sub_label?: string;
|
sub_label?: string;
|
||||||
|
|||||||
76
src/widgets/RecordingsFiltersRightSide.tsx
Normal file
76
src/widgets/RecordingsFiltersRightSide.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import React, { useContext, useEffect } from 'react';
|
||||||
|
import OneSelectFilter, { OneSelectItem } from '../shared/components/filters.aps/OneSelectFilter';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { Context } from '..';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||||
|
import { Center, Text } from '@mantine/core';
|
||||||
|
import CogwheelLoader from '../shared/components/CogwheelLoader';
|
||||||
|
import CameraSelectFilter from '../shared/components/filters.aps/CameraSelectFilter';
|
||||||
|
|
||||||
|
interface RecordingsFiltersRightSideProps {
|
||||||
|
}
|
||||||
|
|
||||||
|
const RecordingsFiltersRightSide = ({
|
||||||
|
}: RecordingsFiltersRightSideProps) => {
|
||||||
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
|
|
||||||
|
const { data: hosts, isError, isPending, isSuccess } = useQuery({
|
||||||
|
queryKey: [frigateQueryKeys.getFrigateHosts],
|
||||||
|
queryFn: frigateApi.getHosts
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hosts) return
|
||||||
|
if (recStore.hostIdParam) {
|
||||||
|
recStore.selectedHost = hosts.find(host => host.id === recStore.hostIdParam)
|
||||||
|
recStore.hostIdParam = undefined
|
||||||
|
}
|
||||||
|
}, [isSuccess])
|
||||||
|
|
||||||
|
if (isPending) return <CogwheelLoader />
|
||||||
|
if (isError) return <Center><Text>Loading error!</Text></Center>
|
||||||
|
|
||||||
|
if (!hosts || hosts.length < 1) return null
|
||||||
|
|
||||||
|
const hostItems: OneSelectItem[] = hosts
|
||||||
|
.filter(host => host.enabled)
|
||||||
|
.map(host => ({ value: host.id, label: host.name }))
|
||||||
|
|
||||||
|
const handleSelect = (id: string, value: string) => {
|
||||||
|
const host = hosts?.find(host => host.id === value)
|
||||||
|
if (!host) {
|
||||||
|
recStore.selectedHost = undefined
|
||||||
|
recStore.selectedCamera = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (recStore.selectedHost?.id !== host.id) {
|
||||||
|
recStore.selectedCamera = undefined
|
||||||
|
}
|
||||||
|
recStore.selectedHost = host
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('RecordingsFiltersRightSide rendered')
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<OneSelectFilter
|
||||||
|
id='frigate-hosts'
|
||||||
|
label='Select host:'
|
||||||
|
spaceBetween='1rem'
|
||||||
|
value={recStore.selectedHost?.id || ''}
|
||||||
|
defaultValue={recStore.selectedHost?.id || ''}
|
||||||
|
data={hostItems}
|
||||||
|
onChange={handleSelect}
|
||||||
|
/>
|
||||||
|
{recStore.selectedHost ?
|
||||||
|
<CameraSelectFilter
|
||||||
|
selectedHostId={recStore.selectedHost.id} />
|
||||||
|
:
|
||||||
|
<></>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(RecordingsFiltersRightSide);
|
||||||
51
src/widgets/SelectedCameraList.tsx
Normal file
51
src/widgets/SelectedCameraList.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Center, Flex, Text } from '@mantine/core';
|
||||||
|
import React, { Suspense, useContext } from 'react';
|
||||||
|
import CameraAccordion from '../shared/components/accordion/CameraAccordion';
|
||||||
|
import { GetCameraWHostWConfig, GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Context } from '..';
|
||||||
|
import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.api';
|
||||||
|
import { host } from '../shared/env.const';
|
||||||
|
import CogwheelLoader from '../shared/components/CogwheelLoader';
|
||||||
|
import RetryError from '../pages/RetryError';
|
||||||
|
|
||||||
|
interface SelectedCameraListProps {
|
||||||
|
cameraId: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectedCameraList = ({
|
||||||
|
cameraId,
|
||||||
|
}: SelectedCameraListProps) => {
|
||||||
|
|
||||||
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
|
|
||||||
|
const { data: camera, isPending: cameraPending, isError: cameraError, refetch: cameraRefetch } = useQuery({
|
||||||
|
queryKey: [frigateQueryKeys.getCameraWHost, cameraId],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (cameraId) {
|
||||||
|
return frigateApi.getCameraWHost(cameraId)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleRetry = () => {
|
||||||
|
cameraRefetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cameraPending) return <CogwheelLoader />
|
||||||
|
if (cameraError) return <RetryError onRetry={handleRetry} />
|
||||||
|
|
||||||
|
if (!camera?.frigateHost) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex w='100%' h='100%' direction='column' align='center'>
|
||||||
|
<Text>{camera.frigateHost.name} / {camera.name}</Text>
|
||||||
|
<Suspense>
|
||||||
|
<CameraAccordion camera={camera} host={camera.frigateHost} />
|
||||||
|
</Suspense>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectedCameraList;
|
||||||
75
src/widgets/SelectedHostList.tsx
Normal file
75
src/widgets/SelectedHostList.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { Accordion, Flex, Text } from '@mantine/core';
|
||||||
|
import React, { Suspense, lazy, useContext, useState } from 'react';
|
||||||
|
import { host } from '../shared/env.const';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.api';
|
||||||
|
import { Context } from '..';
|
||||||
|
import CenterLoader from '../shared/components/CenterLoader';
|
||||||
|
import RetryError from '../pages/RetryError';
|
||||||
|
const CameraAccordion = lazy(() => import('../shared/components/accordion/CameraAccordion'));
|
||||||
|
|
||||||
|
|
||||||
|
interface SelectedHostListProps {
|
||||||
|
hostId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectedHostList = ({
|
||||||
|
hostId
|
||||||
|
}: SelectedHostListProps) => {
|
||||||
|
|
||||||
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
|
const [openCameraId, setOpenCameraId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const { data: host, isPending: hostPending, isError: hostError, refetch: hostRefetch } = useQuery({
|
||||||
|
queryKey: [frigateQueryKeys.getFrigateHost, hostId],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (hostId) {
|
||||||
|
return frigateApi.getHost(hostId)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleOnChange = (cameraId: string | null) => {
|
||||||
|
setOpenCameraId(openCameraId === cameraId ? null : cameraId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRetry = () => {
|
||||||
|
if (recStore.selectedHost) hostRefetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hostPending) return <CenterLoader />
|
||||||
|
if (hostError) return <RetryError onRetry={handleRetry} />
|
||||||
|
|
||||||
|
if (!host || host.cameras.length < 1) return null
|
||||||
|
|
||||||
|
const cameras = host.cameras.slice(0, 2).map(camera => {
|
||||||
|
return (
|
||||||
|
<Accordion.Item key={camera.id + 'Item'} value={camera.id}>
|
||||||
|
<Accordion.Control key={camera.id + 'Control'}>{camera.name}</Accordion.Control>
|
||||||
|
<Accordion.Panel key={camera.id + 'Panel'}>
|
||||||
|
{openCameraId === camera.id && (
|
||||||
|
<Suspense>
|
||||||
|
<CameraAccordion camera={camera} host={host} />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex w='100%' h='100%' direction='column' align='center'>
|
||||||
|
<Text>{host.name}</Text>
|
||||||
|
<Accordion
|
||||||
|
mt='1rem'
|
||||||
|
variant='separated'
|
||||||
|
radius="md" w='100%'
|
||||||
|
onChange={(value) => handleOnChange(value)}>
|
||||||
|
{cameras}
|
||||||
|
</Accordion>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectedHostList;
|
||||||
Loading…
Reference in New Issue
Block a user