add filter and query params

This commit is contained in:
NlightN22 2024-02-25 02:27:13 +07:00
parent f369692133
commit a971ea55a4
19 changed files with 678 additions and 1115 deletions

View File

@ -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

View File

@ -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({
@ -61,23 +62,30 @@ export const proxyApi = {
) =>
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',
}

View File

@ -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>

View File

@ -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)
@ -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)

View File

@ -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,9 +34,9 @@ const CameraAccordion = observer(({
useEffect(() => {
if (openedDay) {
recordingsStore.playedRecord.cameraName = camera.name
recStore.recordToPlay.cameraName = camera.name
const hostName = mapHostToHostname(host)
recordingsStore.playedRecord.hostName = hostName
recStore.recordToPlay.hostName = hostName
}
}, [openedDay])
@ -44,16 +44,13 @@ const CameraAccordion = observer(({
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}

View File

@ -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,
@ -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>
))}

View 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;

View 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;

View File

@ -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)
}} />
}

View 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);

View File

@ -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

View File

@ -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) => {

View File

@ -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) {
@ -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';
}

View File

@ -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 = ''
}

View File

@ -1,4 +1,4 @@
export interface Event {
export interface EventFrigate {
id: string;
label: string;
sub_label?: string;

View 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);

View 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;

View 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;