diff --git a/src/hooks/useAdminRole.ts b/src/hooks/useAdminRole.ts index 0ecba78..eb2ebc2 100644 --- a/src/hooks/useAdminRole.ts +++ b/src/hooks/useAdminRole.ts @@ -1,20 +1,31 @@ import { useQuery } from "@tanstack/react-query"; import { frigateQueryKeys, frigateApi } from "../services/frigate.proxy/frigate.api"; import { useRealmAccessRoles } from "./useRealmAccessRoles"; +import { useEffect, useState } from "react"; export const useAdminRole = () => { - const { data: adminConfig, isError, isPending } = useQuery({ + const { data: adminConfig, isError, isFetching } = useQuery({ queryKey: [frigateQueryKeys.getAdminRole], - queryFn: frigateApi.getAdminRole + queryFn: frigateApi.getAdminRole, + staleTime: 1000 * 60 * 60, + gcTime: 1000 * 60 * 60 * 24, }) const roles = useRealmAccessRoles() + const [initialized, setInitialized] = useState(false) + const isLoading = isFetching || roles === undefined - if (isPending) return { isAdmin: undefined, isLoading: true } - if (isError) return { isAdmin: false, isError: true } - if (!adminConfig) return { isAdmin: true } - if (adminConfig && !adminConfig.value) return { isAdmin: true } + useEffect(() => { + if (!isLoading) { + setInitialized(true); + } + }, [isLoading]); + + if (!initialized || isLoading) return { isAdmin: undefined, isLoading: true } + if (isError) return { isAdmin: false, isError: true, isLoading: false } + if (!adminConfig) return { isAdmin: true, isLoading: false } + if (adminConfig && !adminConfig.value) return { isAdmin: true, isLoading: false } const isAdmin = roles.some(role => role === adminConfig.value) - return { isAdmin } + return { isAdmin, isLoading: false } } \ No newline at end of file diff --git a/src/pages/HostConfigPage.tsx b/src/pages/HostConfigPage.tsx index 2992761..2d5102c 100644 --- a/src/pages/HostConfigPage.tsx +++ b/src/pages/HostConfigPage.tsx @@ -1,12 +1,11 @@ import React, { useCallback, useContext, useEffect, useRef } from 'react'; import { useParams } from 'react-router-dom'; import { Context } from '..'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; import { frigateApi, frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api'; -import { Button, Flex, Text, useMantineTheme } from '@mantine/core'; +import { Button, Flex, useMantineTheme } from '@mantine/core'; import { useClipboard } from '@mantine/hooks'; -import { configureMonacoYaml } from "monaco-yaml"; -import Editor, { DiffEditor, useMonaco, loader, Monaco } from '@monaco-editor/react' +import Editor, { Monaco } from '@monaco-editor/react' import * as monaco from "monaco-editor"; import CenterLoader from '../shared/components/loaders/CenterLoader'; import RetryErrorPage from './RetryErrorPage'; diff --git a/src/services/frigate.proxy/frigate.api.ts b/src/services/frigate.proxy/frigate.api.ts index fcbd3f2..93a45cb 100644 --- a/src/services/frigate.proxy/frigate.api.ts +++ b/src/services/frigate.proxy/frigate.api.ts @@ -1,13 +1,11 @@ import axios from "axios" import { proxyURL } from "../../shared/env.const" -import { z } from "zod" import { GetConfig, DeleteFrigateHost, GetFrigateHost, PutConfig, PutFrigateHost, - GetFrigateHostWithCameras, GetCameraWHost, GetCameraWHostWConfig, GetRole, - GetUserByRole, GetRoleWCameras, GetExportedFile + GetCameraWHostWConfig, GetRole, + GetRoleWCameras, GetExportedFile } from "./frigate.schema"; import { FrigateConfig } from "../../types/frigateConfig"; -import { url } from "inspector"; import { RecordSummary } from "../../types/record"; import { EventFrigate } from "../../types/event"; import { keycloakConfig } from "../.."; @@ -42,12 +40,11 @@ export const frigateApi = { getHosts: () => instanceApi.get('apiv1/frigate-hosts').then(res => { return res.data }), - // getHostsWithCameras: () => instanceApi.get('apiv1/frigate-hosts', { params: { include: 'cameras' } }).then(res => { - // return res.data - // }), - getHost: (id: string) => instanceApi.get(`apiv1/frigate-hosts/${id}`).then(res => { + // TODO change request to cameras + getHost: (id: string) => instanceApi.get(`apiv1/frigate-hosts/${id}`).then(res => { return res.data }), + getCamerasByHostId: (hostId: string) => instanceApi.get(`apiv1/cameras/host/${hostId}`).then(res => res.data), getCamerasWHost: () => instanceApi.get(`apiv1/cameras`).then(res => res.data), getCameraWHost: (id: string) => instanceApi.get(`apiv1/cameras/${id}`).then(res => { return res.data }), putHosts: (hosts: PutFrigateHost[]) => instanceApi.put('apiv1/frigate-hosts', hosts).then(res => { @@ -181,6 +178,7 @@ export const frigateQueryKeys = { getFrigateHost: 'frigate-host', getCamerasWHost: 'cameras-frigate-host', getCameraWHost: 'camera-frigate-host', + getCameraByHostId: 'camera-by-hostId', getHostConfig: 'host-config', getRecordingsSummary: 'recordings-frigate-summary', getRecordings: 'recordings-frigate', diff --git a/src/services/frigate.proxy/frigate.schema.ts b/src/services/frigate.proxy/frigate.schema.ts index ef2e9bd..4ec9211 100644 --- a/src/services/frigate.proxy/frigate.schema.ts +++ b/src/services/frigate.proxy/frigate.schema.ts @@ -108,7 +108,7 @@ export const getExpotedFile = z.object({ export type GetConfig = z.infer export type PutConfig = z.infer export type GetFrigateHost = z.infer -export type GetFrigateHostWithCameras = z.infer +// export type GetFrigateHostWithCameras = z.infer export type GetFrigateHostWConfig = GetFrigateHost & { config: FrigateConfig } export type GetCameraWHost = z.infer export type GetCameraWHostWConfig = GetCameraWHost & { config?: CameraConfig } diff --git a/src/shared/components/accordion/EventPanel.tsx b/src/shared/components/accordion/EventPanel.tsx index d91078a..1d39fb6 100644 --- a/src/shared/components/accordion/EventPanel.tsx +++ b/src/shared/components/accordion/EventPanel.tsx @@ -6,6 +6,7 @@ import { strings } from '../../strings/strings'; import { unixTimeToDate, getDurationFromTimestamps } from '../../utils/dateUtil'; import VideoPlayer from '../players/VideoPlayer'; import { EventFrigate } from '../../../types/event'; +import BlobImage from '../images/BlobImage'; interface EventPanelProps { event: EventFrigate @@ -25,7 +26,7 @@ const EventPanel = ({ {playedValue === event.id && playerUrl ? : <>} {!hostName ? <> : - {strings.player.object}: {event.label} {strings.player.startTime}: {unixTimeToDate(event.start_time)} {strings.player.duration}: {getDurationFromTimestamps(event.start_time, event.end_time)} - {strings.player.rating}: {(event.data.score * 100).toFixed(2)}% + {!event.data?.score? <> : + {strings.player.rating}: {(event.data.score * 100).toFixed(2)}% + } diff --git a/src/shared/components/filters.aps/CameraSelectFilter.tsx b/src/shared/components/filters.aps/CameraSelectFilter.tsx index 56d42fa..df76018 100644 --- a/src/shared/components/filters.aps/CameraSelectFilter.tsx +++ b/src/shared/components/filters.aps/CameraSelectFilter.tsx @@ -19,15 +19,15 @@ const CameraSelectFilter = ({ const { recordingsStore: recStore } = useContext(Context) const { data, isError, isPending, isSuccess, refetch } = useQuery({ - queryKey: [frigateQueryKeys.getFrigateHost, selectedHostId], - queryFn: () => frigateApi.getHost(selectedHostId) + queryKey: [frigateQueryKeys.getCameraByHostId, selectedHostId], + queryFn: () => frigateApi.getCamerasByHostId(selectedHostId) }) useEffect(() => { if (!data) return if (recStore.cameraIdParam) { console.log('change camera by param') - recStore.filteredCamera = data.cameras.find( camera => camera.id === recStore.cameraIdParam) + recStore.filteredCamera = data.find( camera => camera.id === recStore.cameraIdParam) recStore.cameraIdParam = undefined } }, [isSuccess]) @@ -36,10 +36,10 @@ const CameraSelectFilter = ({ if (isError) return if (!data) return null - const camerasItems: OneSelectItem[] = data.cameras.map(camera => ({ value: camera.id, label: camera.name })) + const camerasItems: OneSelectItem[] = data.map(camera => ({ value: camera.id, label: camera.name })) const handleSelect = (value: string) => { - const camera = data.cameras.find(camera => camera.id === value) + const camera = data.find(camera => camera.id === value) if (!camera) { recStore.filteredCamera = undefined return diff --git a/src/shared/components/images/BlobImage.tsx b/src/shared/components/images/BlobImage.tsx new file mode 100644 index 0000000..9771b60 --- /dev/null +++ b/src/shared/components/images/BlobImage.tsx @@ -0,0 +1,51 @@ +import { Image, ImageProps, Loader } from '@mantine/core'; +import React, { useEffect, useState } from 'react'; +import { proxyApi } from '../../../services/frigate.proxy/frigate.api'; +import { useIntersection } from '@mantine/hooks'; +import { useQuery } from '@tanstack/react-query'; +import RetryError from '../RetryError'; + +interface BlobImageProps extends ImageProps { + src: string +} + +const BlobImage = ({ + src, + ...rest +}: BlobImageProps) => { + const [imageSrc, setImageSrc] = useState(null); + + const { ref, entry } = useIntersection({ threshold: 0.1, }) + const isVisible = entry?.isIntersecting + + const { data: imageBlob, refetch, isPending, isError } = useQuery({ + queryKey: [src], + queryFn: () => proxyApi.getImageFrigate(src), + staleTime: 60 * 1000, + gcTime: Infinity, + refetchInterval: isVisible ? 30 * 1000 : undefined, + }); + + useEffect(() => { + if (imageBlob && imageBlob instanceof Blob) { + const objectURL = URL.createObjectURL(imageBlob); + setImageSrc(objectURL); + + return () => { + if (objectURL) { + URL.revokeObjectURL(objectURL); + } + } + } + }, [imageBlob]) + + if (isPending || !imageSrc) return + + if (isError) return + + return ( + + ); +}; + +export default BlobImage; \ No newline at end of file diff --git a/src/shared/components/images/ImageWithPlaceHolder.tsx b/src/shared/components/images/ImageWithPlaceHolder.tsx index bbc7cec..3877b0f 100644 --- a/src/shared/components/images/ImageWithPlaceHolder.tsx +++ b/src/shared/components/images/ImageWithPlaceHolder.tsx @@ -2,8 +2,8 @@ import { ImageProps, Image, Center } from '@mantine/core'; import { IconPhotoOff } from '@tabler/icons-react'; import React from 'react'; -const ImageWithPlaceHolder = (props: ImageProps & React.RefAttributes) => { - if (props.src) return ( +const ImageWithPlaceHolder = (props: ImageProps) => { + if (props.src && (typeof props.src === 'string')) return ( ) return ( diff --git a/src/shared/stores/recordings.store.ts b/src/shared/stores/recordings.store.ts index 19a8655..76dbb1f 100644 --- a/src/shared/stores/recordings.store.ts +++ b/src/shared/stores/recordings.store.ts @@ -1,6 +1,6 @@ import { makeAutoObservable } from "mobx" import { z } from "zod" -import { GetCameraWHostWConfig, GetFrigateHost, GetFrigateHostWithCameras } from "../../services/frigate.proxy/frigate.schema" +import { GetCameraWHostWConfig, GetFrigateHost } from "../../services/frigate.proxy/frigate.schema" export type RecordForPlay = { hostName?: string // format 'localhost:4000' diff --git a/src/types/event.ts b/src/types/event.ts index 83a5065..187bec5 100644 --- a/src/types/event.ts +++ b/src/types/event.ts @@ -13,9 +13,9 @@ export interface EventFrigate { retain_indefinitely: boolean; plus_id?: string; model_hash?: string; - data: { - top_score: number; - score: number; + data?: { + top_score?: number; + score?: number; sub_label_score?: number; region: number[]; box: number[]; diff --git a/src/widgets/FrigateHostsTable.tsx b/src/widgets/FrigateHostsTable.tsx index bf3aad1..2e0f22c 100644 --- a/src/widgets/FrigateHostsTable.tsx +++ b/src/widgets/FrigateHostsTable.tsx @@ -26,7 +26,6 @@ const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedC const [sortedName, setSortedName] = useState(null) useEffect(() => { - console.log('data changed') setTableData(data) }, [data]) diff --git a/src/widgets/SelectedHostList.tsx b/src/widgets/SelectedHostList.tsx index 0400267..f4f58fb 100644 --- a/src/widgets/SelectedHostList.tsx +++ b/src/widgets/SelectedHostList.tsx @@ -21,19 +21,19 @@ const SelectedHostList = ({ const { recordingsStore: recStore } = useContext(Context) const [openCameraId, setOpenCameraId] = useState(null) - const { data: host, isPending: hostPending, isError: hostError, refetch: hostRefetch } = useQuery({ - queryKey: [frigateQueryKeys.getFrigateHost, hostId], + const { data: camerasQuery, isPending: hostPending, isError: hostError, refetch: hostRefetch } = useQuery({ + queryKey: [frigateQueryKeys.getCameraByHostId, hostId], queryFn: async () => { if (hostId) { - return frigateApi.getHost(hostId) + return frigateApi.getCamerasByHostId(hostId) } - return null + return [] } }) const handleOnChange = (cameraId: string | null) => { setOpenCameraId(openCameraId === cameraId ? null : cameraId) - recStore.openedCamera = host?.cameras.find( camera => camera.id === cameraId) + recStore.openedCamera = camerasQuery?.find( camera => camera.id === cameraId) } const handleRetry = () => { @@ -43,9 +43,9 @@ const SelectedHostList = ({ if (hostPending) return if (hostError) return - if (!host || host.cameras.length < 1) return null + if (!camerasQuery || camerasQuery.length < 1) return null - const cameras = host.cameras.map(camera => { + const camerasItems = camerasQuery.map(camera => { return ( {strings.camera}: {camera.name} @@ -62,13 +62,13 @@ const SelectedHostList = ({ return ( - {strings.host}: {host.name} + {strings.host}: {camerasQuery[0].frigateHost?.name} handleOnChange(value)}> - {cameras} + {camerasItems} )