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 { testHeaderLinks } from './widgets/header/header.links';
|
||||
import AppRouter from './router/AppRouter';
|
||||
import { SideBar } from './shared/components/SideBar';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Context } from '.';
|
||||
import SideBar from './shared/components/SideBar';
|
||||
|
||||
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 { url } from "inspector";
|
||||
import { RecordSummary } from "../../types/record";
|
||||
import { EventFrigate } from "../../types/event";
|
||||
|
||||
|
||||
const instanceApi = axios.create({
|
||||
@ -59,25 +60,32 @@ export const proxyApi = {
|
||||
cameraName: 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: (
|
||||
hostName: string,
|
||||
camerasName: string[],
|
||||
timezone: string,
|
||||
minScore?: number,
|
||||
maxScore?: number,
|
||||
timezone?: string,
|
||||
hasClip?: boolean,
|
||||
after?: number,
|
||||
before?: number,
|
||||
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: {
|
||||
cameras: camerasName,
|
||||
after: after,
|
||||
cameras: camerasName.join(','), // frigate format
|
||||
timezone: timezone,
|
||||
after: after,
|
||||
before: before, // @before the last event start_time in list
|
||||
has_clip: hasClip,
|
||||
include_thumbnails: includeThumnails,
|
||||
labels: labels,
|
||||
limit: limit,
|
||||
min_score: minScore,
|
||||
max_score: maxScore,
|
||||
}
|
||||
@ -120,4 +128,5 @@ export const frigateQueryKeys = {
|
||||
getHostConfig: 'host-config',
|
||||
getRecordingsSummary: 'recordings-frigate-summary',
|
||||
getRecordings: 'recordings-frigate',
|
||||
getEvents: 'events-frigate',
|
||||
}
|
||||
|
||||
@ -58,6 +58,20 @@ export const deleteFrigateHostSchema = putFrigateHostSchema.pick({
|
||||
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 PutConfig = z.infer<typeof putConfigSchema>
|
||||
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 [visible, { open, close }] = useDisclosure(window.innerWidth > hideSizePx);
|
||||
const manualVisible: React.MutableRefObject<null | boolean> = useRef(null)
|
||||
@ -43,7 +43,7 @@ export const SideBar = observer(({ isHidden, side, children }: SideBarProps) =>
|
||||
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
|
||||
useEffect( () => {
|
||||
useEffect(() => {
|
||||
if (sideBarsStore.rightVisible && side === 'right' && !visible) {
|
||||
open()
|
||||
} 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)} />
|
||||
</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 { useQuery } from '@tanstack/react-query';
|
||||
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import CogwheelLoader from '../CogwheelLoader';
|
||||
import DayAccordion from './DayAccordion';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Context } from '../../..';
|
||||
import { getResolvedTimeZone } from '../frigate/dateUtil';
|
||||
|
||||
interface CameraAccordionProps {
|
||||
camera: GetCameraWHostWConfig,
|
||||
@ -17,14 +17,14 @@ const CameraAccordion = observer(({
|
||||
camera,
|
||||
host
|
||||
}: CameraAccordionProps) => {
|
||||
const { recordingsStore } = useContext(Context)
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const { data, isPending, isError } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getRecordingsSummary, camera?.id],
|
||||
queryFn: () => {
|
||||
if (camera && host) {
|
||||
const hostName = mapHostToHostname(host)
|
||||
return proxyApi.getRecordingsSummary(hostName, camera.name, 'Asia/Krasnoyarsk')
|
||||
return proxyApi.getRecordingsSummary(hostName, camera.name, getResolvedTimeZone())
|
||||
}
|
||||
return null
|
||||
}
|
||||
@ -34,26 +34,23 @@ const CameraAccordion = observer(({
|
||||
|
||||
useEffect(() => {
|
||||
if (openedDay) {
|
||||
recordingsStore.playedRecord.cameraName = camera.name
|
||||
const hostName = mapHostToHostname(host)
|
||||
recordingsStore.playedRecord.hostName = hostName
|
||||
recStore.recordToPlay.cameraName = camera.name
|
||||
const hostName = mapHostToHostname(host)
|
||||
recStore.recordToPlay.hostName = hostName
|
||||
}
|
||||
}, [openedDay])
|
||||
}, [openedDay])
|
||||
|
||||
const handleClick = (value: string | null) => {
|
||||
setOpenedDay(value)
|
||||
}
|
||||
|
||||
if (isPending) return (
|
||||
<Center>
|
||||
<Text>Loading...</Text>
|
||||
</Center>
|
||||
)
|
||||
if (isError) return <Text>Loading error</Text>
|
||||
if (isPending) return <Center><Text>Loading...</Text></Center>
|
||||
if (isError) return <Center><Text>Loading error</Text></Center>
|
||||
|
||||
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.Control key={rec.day + 'control'}>{rec.day}</Accordion.Control>
|
||||
<Accordion.Panel key={rec.day + 'panel'}>
|
||||
@ -62,6 +59,9 @@ const CameraAccordion = observer(({
|
||||
</Accordion.Item>
|
||||
|
||||
))
|
||||
|
||||
console.log('CameraAccordion rendered')
|
||||
|
||||
return (
|
||||
<Accordion variant='separated' radius="md" w='100%' onChange={handleClick}>
|
||||
{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 { RecordHour, RecordSummary, Recording } from '../../../types/record';
|
||||
import Button from '../frigate/Button';
|
||||
@ -8,6 +8,9 @@ import PlayControl from './PlayControl';
|
||||
import { frigateApi, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { Context } from '../../..';
|
||||
import VideoPlayer from '../frigate/VideoPlayer';
|
||||
import { getResolvedTimeZone } from '../frigate/dateUtil';
|
||||
import EventsAccordion from './EventsAccordion';
|
||||
import DayEventsAccordion from './DayEventsAccordion';
|
||||
|
||||
interface RecordingAccordionProps {
|
||||
recordSummary?: RecordSummary
|
||||
@ -25,11 +28,11 @@ const DayAccordion = observer(({
|
||||
if (openVideoPlayer) {
|
||||
console.log('openVideoPlayer', openVideoPlayer)
|
||||
if (openVideoPlayer) {
|
||||
recordingsStore.playedRecord.day = recordSummary?.day
|
||||
recordingsStore.playedRecord.hour = openVideoPlayer
|
||||
recordingsStore.playedRecord.timezone = 'Asia,Krasnoyarsk'
|
||||
const parsed = recordingsStore.getFullPlayedRecord(recordingsStore.playedRecord)
|
||||
console.log('recordingsStore.playedRecord: ', recordingsStore.playedRecord)
|
||||
recordingsStore.recordToPlay.day = recordSummary?.day
|
||||
recordingsStore.recordToPlay.hour = openVideoPlayer
|
||||
recordingsStore.recordToPlay.timezone = getResolvedTimeZone().replace('/', ',')
|
||||
const parsed = recordingsStore.getFullRecordForPlay(recordingsStore.recordToPlay)
|
||||
console.log('recordingsStore.playedRecord: ', recordingsStore.recordToPlay)
|
||||
if (parsed.success) {
|
||||
const url = proxyApi.recordingURL(
|
||||
parsed.data.hostName,
|
||||
@ -42,7 +45,7 @@ const DayAccordion = observer(({
|
||||
setPlayerUrl(url)
|
||||
}
|
||||
}
|
||||
}else {
|
||||
} else {
|
||||
setPlayerUrl(undefined)
|
||||
}
|
||||
}, [openVideoPlayer])
|
||||
@ -68,6 +71,8 @@ const DayAccordion = observer(({
|
||||
setOpenVideoPlayer(undefined)
|
||||
}
|
||||
|
||||
console.log('DayAccordion rendered')
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
key={recordSummary.day}
|
||||
@ -76,14 +81,18 @@ const DayAccordion = observer(({
|
||||
value={openedValue}
|
||||
onChange={handleClick}
|
||||
>
|
||||
{recordSummary.hours.map(hour => (
|
||||
{recordSummary.hours.slice(0, 5).map(hour => (
|
||||
<Accordion.Item key={hour.hour + 'Item'} value={hour.hour}>
|
||||
<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.Panel key={hour.hour + 'Panel'}>
|
||||
{openVideoPlayer === hour.hour ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
||||
Events
|
||||
{openVideoPlayer === hour.hour && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
||||
{hour.events > 0 ?
|
||||
<DayEventsAccordion day={recordSummary.day} hour={hour.hour} qty={hour.events} />
|
||||
:
|
||||
<Center><Text>Not have events</Text></Center>
|
||||
}
|
||||
</Accordion.Panel>
|
||||
</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';
|
||||
|
||||
interface PlayControlProps {
|
||||
hour: string,
|
||||
label: string,
|
||||
value: string,
|
||||
openVideoPlayer?: string,
|
||||
onClick?: (value: string) => void
|
||||
}
|
||||
|
||||
const PlayControl = ({
|
||||
hour,
|
||||
label,
|
||||
value,
|
||||
openVideoPlayer,
|
||||
onClick
|
||||
}: PlayControlProps) => {
|
||||
@ -18,23 +20,23 @@ const PlayControl = ({
|
||||
}
|
||||
return (
|
||||
<Flex justify='space-between'>
|
||||
Hour: {hour}
|
||||
{label}
|
||||
<Group>
|
||||
<Text onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleClick(hour)
|
||||
handleClick(value)
|
||||
}}>
|
||||
{openVideoPlayer === hour ? 'Stop Video' : 'Play Video'}
|
||||
{openVideoPlayer === value ? 'Stop Video' : 'Play Video'}
|
||||
</Text>
|
||||
{openVideoPlayer === hour ?
|
||||
{openVideoPlayer === value ?
|
||||
<IconPlayerStop onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleClick(hour)
|
||||
handleClick(value)
|
||||
}} />
|
||||
:
|
||||
<IconPlayerPlay onClick={(event) => {
|
||||
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,
|
||||
display?: SystemProp<CSSProperties['display']>
|
||||
showClose?: boolean,
|
||||
changedState?(id: string, value: string): void
|
||||
value?: string,
|
||||
onChange?(id: string, value: string): void
|
||||
onClose?(): void
|
||||
}
|
||||
|
||||
@ -29,12 +30,12 @@ interface OneSelectFilterProps {
|
||||
const OneSelectFilter = ({
|
||||
id, data, spaceBetween,
|
||||
label, defaultValue, textClassName,
|
||||
selectProps, display, showClose, changedState, onClose
|
||||
selectProps, display, showClose, value, onChange, onClose
|
||||
}: OneSelectFilterProps) => {
|
||||
|
||||
const handleOnChange = (value: string) => {
|
||||
if (changedState) {
|
||||
changedState(id, value)
|
||||
if (onChange) {
|
||||
onChange(id, value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +51,7 @@ const OneSelectFilter = ({
|
||||
mt={spaceBetween}
|
||||
data={data}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
onChange={handleOnChange}
|
||||
searchable
|
||||
clearable
|
||||
|
||||
@ -4,7 +4,7 @@ import Player from 'video.js/dist/types/player';
|
||||
import 'video.js/dist/video-js.css'
|
||||
|
||||
interface VideoPlayerProps {
|
||||
videoUrl?: string
|
||||
videoUrl: string
|
||||
}
|
||||
|
||||
const VideoPlayer = ({ videoUrl }: VideoPlayerProps) => {
|
||||
|
||||
@ -17,6 +17,40 @@ export const getNowYesterdayInLong = (): number => {
|
||||
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,
|
||||
* 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 format "UTC±HH:MM".
|
||||
*/
|
||||
const getResolvedTimeZone = () => {
|
||||
export const getResolvedTimeZone = () => {
|
||||
try {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
} catch (error) {
|
||||
@ -88,8 +122,8 @@ const getResolvedTimeZone = () => {
|
||||
return `UTC${offsetMinutes < 0 ? '+' : '-'}${Math.abs(offsetMinutes / 60)
|
||||
.toString()
|
||||
.padStart(2, '0')}:${Math.abs(offsetMinutes % 60)
|
||||
.toString()
|
||||
.padStart(2, '0')}`;
|
||||
.toString()
|
||||
.padStart(2, '0')}`;
|
||||
}
|
||||
};
|
||||
|
||||
@ -176,12 +210,12 @@ interface DurationToken {
|
||||
* @param end_time: number|null - Unix timestamp for end time
|
||||
* @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)) {
|
||||
return 'Invalid start time';
|
||||
}
|
||||
let duration = 'In Progress';
|
||||
if (end_time !== null) {
|
||||
if (end_time) {
|
||||
if (isNaN(end_time)) {
|
||||
return 'Invalid end time';
|
||||
}
|
||||
|
||||
@ -1,16 +1,20 @@
|
||||
import { makeAutoObservable } from "mobx"
|
||||
import { z } from "zod"
|
||||
import { GetCameraWHostWConfig, GetFrigateHost, GetFrigateHostWithCameras } from "../../services/frigate.proxy/frigate.schema"
|
||||
|
||||
export type RecordingPlay = {
|
||||
hostName?: string
|
||||
export type RecordForPlay = {
|
||||
hostName?: string // format 'localhost:4000'
|
||||
cameraName?: string
|
||||
hour?: string
|
||||
day?: string
|
||||
timezone?: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class RecordingsStore {
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
recordingSchema = z.object({
|
||||
hostName: z.string(),
|
||||
cameraName: z.string(),
|
||||
@ -19,15 +23,46 @@ export class RecordingsStore {
|
||||
timezone: z.string(),
|
||||
})
|
||||
|
||||
private _playedRecord: RecordingPlay = {}
|
||||
public get playedRecord(): RecordingPlay {
|
||||
return this._playedRecord
|
||||
private _recordToPlay: RecordForPlay = {}
|
||||
public get recordToPlay(): RecordForPlay {
|
||||
return this._recordToPlay
|
||||
}
|
||||
public set playedRecord(value: RecordingPlay) {
|
||||
this._playedRecord = value
|
||||
public set recordToPlay(value: RecordForPlay) {
|
||||
this._recordToPlay = value
|
||||
}
|
||||
|
||||
getFullPlayedRecord(value: RecordingPlay) {
|
||||
getFullRecordForPlay(value: RecordForPlay) {
|
||||
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;
|
||||
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