From f50bfa6cb67b9003951b6cf47ee6f12805553a7b Mon Sep 17 00:00:00 2001 From: NlightN22 Date: Mon, 30 Sep 2024 15:41:11 +0700 Subject: [PATCH] fix tags handlers and schemas --- src/pages/EditCameraPage.tsx | 2 - src/services/frigate.proxy/frigate.api.ts | 26 ++--- src/services/frigate.proxy/frigate.schema.ts | 3 + src/shared/components/AddBadge.tsx | 71 ++++++++++++ src/shared/components/TagBadge.tsx | 49 ++++++++ .../components/filters/UserTagsFilter.tsx | 10 +- src/types/tags.ts | 11 +- src/widgets/CameraCard.tsx | 10 +- src/widgets/CameraTagsList.tsx | 107 ++++++++++++++++++ 9 files changed, 260 insertions(+), 29 deletions(-) create mode 100644 src/shared/components/AddBadge.tsx create mode 100644 src/shared/components/TagBadge.tsx create mode 100644 src/widgets/CameraTagsList.tsx diff --git a/src/pages/EditCameraPage.tsx b/src/pages/EditCameraPage.tsx index 81e23dd..8e88b67 100644 --- a/src/pages/EditCameraPage.tsx +++ b/src/pages/EditCameraPage.tsx @@ -132,8 +132,6 @@ const EditCameraPage = () => { const handleSave = () => { if (!selectedMask || !points) return - console.log('type', selectedMask?.type) - console.log('save', points) mutate() } diff --git a/src/services/frigate.proxy/frigate.api.ts b/src/services/frigate.proxy/frigate.api.ts index dfd918a..892c3ec 100644 --- a/src/services/frigate.proxy/frigate.api.ts +++ b/src/services/frigate.proxy/frigate.api.ts @@ -5,7 +5,9 @@ import { GetCameraWHostWConfig, GetRole, GetRoleWCameras, GetExportedFile, recordingSchema, oidpConfig, - OIDPConfig + OIDPConfig, + GetCameraWHost, + GetCamera } from "./frigate.schema"; import { FrigateConfig } from "../../types/frigateConfig"; import { RecordSummary } from "../../types/record"; @@ -40,21 +42,13 @@ export const frigateApi = { putConfigs: (config: PutConfig[]) => instanceApi.put('apiv1/config', config).then(res => res.data), putOIDPConfig: (config: OIDPConfig) => instanceApi.put('apiv1/config/oidp', config).then(res => res.data), putOIDPConfigTest: (config: OIDPConfig) => instanceApi.put('apiv1/config/oidp/test', config).then(res => res.data), - getHosts: () => instanceApi.get('apiv1/frigate-hosts').then(res => { - return res.data - }), - getHost: (id: string) => instanceApi.get(`apiv1/frigate-hosts/${id}`).then(res => { - return res.data - }), + getHosts: () => instanceApi.get('apiv1/frigate-hosts').then(res => res.data), + getHost: (id: string) => instanceApi.get(`apiv1/frigate-hosts/${id}`).then(res => 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 => { - return res.data - }), - deleteHosts: (hosts: DeleteFrigateHost[]) => instanceApi.delete('apiv1/frigate-hosts', { data: hosts }).then(res => { - return res.data - }), + getCameraWHost: (id: string) => instanceApi.get(`apiv1/cameras/${id}`).then(res => res.data), + putHosts: (hosts: PutFrigateHost[]) => instanceApi.put('apiv1/frigate-hosts', hosts).then(res => res.data), + deleteHosts: (hosts: DeleteFrigateHost[]) => instanceApi.delete('apiv1/frigate-hosts', { data: hosts }).then(res => res.data), getRoles: () => instanceApi.get('apiv1/roles').then(res => res.data), putRoles: () => instanceApi.put('apiv1/roles').then(res => res.data), putRoleWCameras: (roleId: string, cameraIDs: string[]) => instanceApi.put(`apiv1/roles/${roleId}/cameras`, @@ -64,7 +58,9 @@ export const frigateApi = { getAdminRole: () => instanceApi.get('apiv1/config/admin').then(res => res.data), getUserTags: () => instanceApi.get('apiv1/tags').then(res => res.data), putUserTag: (tag: PutUserTag) => instanceApi.put('apiv1/tags', tag).then(res => res.data), - delUserTag: (tagId: string) => instanceApi.delete(`apiv1/tags/${tagId}`).then(res => res.data) + delUserTag: (tagId: string) => instanceApi.delete(`apiv1/tags/${tagId}`).then(res => res.data), + putTagToCamera: (cameraId: string, tagId: string) => instanceApi.put(`apiv1/cameras/${cameraId}/tag/${tagId}`).then(res => res.data), + deleteTagFromCamera: (cameraId: string, tagId: string) => instanceApi.delete(`apiv1/cameras/${cameraId}/tag/${tagId}`).then(res => res.data), } export const proxyPrefix = `${proxyURL.protocol}//${proxyURL.host}/proxy/` diff --git a/src/services/frigate.proxy/frigate.schema.ts b/src/services/frigate.proxy/frigate.schema.ts index ce5860a..68c7d74 100644 --- a/src/services/frigate.proxy/frigate.schema.ts +++ b/src/services/frigate.proxy/frigate.schema.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { CameraConfig, FrigateConfig } from "../../types/frigateConfig"; +import { cameraTag } from "../../types/tags"; export const putConfigSchema = z.object({ key: z.string(), @@ -48,6 +49,7 @@ const getCameraSchema = z.object({ name: z.string(), url: z.string(), state: z.boolean().nullable(), + tags: cameraTag.array() }); export const getRoleSchema = z.object({ @@ -127,6 +129,7 @@ export type OIDPConfig = z.infer export type GetFrigateHost = z.infer // export type GetFrigateHostWithCameras = z.infer export type GetFrigateHostWConfig = GetFrigateHost & { config: FrigateConfig } +export type GetCamera = z.infer export type GetCameraWHost = z.infer export type GetCameraWHostWConfig = GetCameraWHost & { config?: CameraConfig } export type PutFrigateHost = z.infer diff --git a/src/shared/components/AddBadge.tsx b/src/shared/components/AddBadge.tsx new file mode 100644 index 0000000..e0d1d5d --- /dev/null +++ b/src/shared/components/AddBadge.tsx @@ -0,0 +1,71 @@ +import { ActionIcon, Badge, Button, Menu, rem } from '@mantine/core'; +import { IconPlus } from '@tabler/icons-react'; +import { useQuery } from '@tanstack/react-query'; +import React from 'react'; +import { frigateQueryKeys, frigateApi } from '../../services/frigate.proxy/frigate.api'; +import CogwheelLoader from './loaders/CogwheelLoader'; +import RetryError from './RetryError'; + +interface AddBadgeProps { + onClick?(tagId: string): void, +} + + +const AddBadge: React.FC = ({ + onClick +}) => { + + const { data, isPending, isError, refetch } = useQuery({ + queryKey: [frigateQueryKeys.getUserTags], + queryFn: frigateApi.getUserTags + }) + + const handleClick = (tagId: string) => { + if (onClick) onClick(tagId) + } + + if (isPending) return + if (isError) return + + if (!data || data.length < 1) return ( + + + + + + ) + + + return ( + + + + + + + + + + {data.map((item) => ( + handleClick(item.id)}> + {item.value} + + ))} + + + ); +}; + +export default AddBadge; \ No newline at end of file diff --git a/src/shared/components/TagBadge.tsx b/src/shared/components/TagBadge.tsx new file mode 100644 index 0000000..fa3cd55 --- /dev/null +++ b/src/shared/components/TagBadge.tsx @@ -0,0 +1,49 @@ +import { ActionIcon, rem, Badge } from '@mantine/core'; +import { IconX } from '@tabler/icons-react'; +import React from 'react'; + +const RemoveButton = ({ onClick }: { onClick(): void }) => { + return ( + + + + ); +}; + +interface TagBadgeProps { + value: string, + label: string, + onClick?(value: string): void, + onClose?(value: string): void, +} + +const TagBadge: React.FC = ({ + value, + label, + onClick, + onClose, +}) => { + + const handleClick = (value: string) => { + if (onClick) onClick(value) + } + + const handleClose = () => { + if (onClose) onClose(value) + } + + return ( + } + onClick={() => handleClick} + > + {label} + + ); +}; + +export default TagBadge; \ No newline at end of file diff --git a/src/shared/components/filters/UserTagsFilter.tsx b/src/shared/components/filters/UserTagsFilter.tsx index c0b5e84..057116a 100644 --- a/src/shared/components/filters/UserTagsFilter.tsx +++ b/src/shared/components/filters/UserTagsFilter.tsx @@ -43,7 +43,7 @@ const UserTagsFilter = () => { return true } - const { mutate } = useMutation({ + const { mutate: addTag } = useMutation({ mutationFn: (newTag: PutUserTag) => frigateApi.putUserTag(newTag) .catch(error => { if (error.response && error.response.data) { @@ -52,8 +52,8 @@ const UserTagsFilter = () => { return Promise.reject(error) }), onSuccess: (data) => { - setSelectedList([...selectedList, data.id]) queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getUserTags] }) + queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getCamerasWHost] }) }, onError: (e) => { if (e && e.message) { @@ -67,7 +67,6 @@ const UserTagsFilter = () => { icon: , }) } - queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getFrigateHosts] }) } }) @@ -81,6 +80,7 @@ const UserTagsFilter = () => { }), onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getUserTags] }) + queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getCamerasWHost] }) const updatedList = selectedList.filter(item => data.id === item) setSelectedList(updatedList) }, @@ -103,9 +103,8 @@ const UserTagsFilter = () => { const saveNewTag = (value: string) => { const newTag: PutUserTag = { value, - cameraIds: [] } - mutate(newTag) + addTag(newTag) } const onCreate = (query: string | SelectItem | null | undefined) => { @@ -128,7 +127,6 @@ const UserTagsFilter = () => { if (isError) return const handleOnChange = (value: string[]) => { - console.log('cahnged:', value) const updatedList = selectedList.filter(item => value.includes(item)) const newItems = value.filter(item => !selectedList.includes(item)) setSelectedList([...updatedList, ...newItems]) diff --git a/src/types/tags.ts b/src/types/tags.ts index 72db97b..ec5d4f6 100644 --- a/src/types/tags.ts +++ b/src/types/tags.ts @@ -3,7 +3,6 @@ import { z } from "zod"; export const putUserTag = z.object({ value: z.string(), - cameraIds: z.string().array() }) export const getUserTag = z.object({ @@ -12,9 +11,14 @@ export const getUserTag = z.object({ updatedAt: z.string().datetime(), value: z.string(), userId: z.string(), - cameraIds: z.string().array(), }) +export const cameraTag = z.object({ + id: z.string(), + value: z.string(), +}) + +export type CameraTag = z.infer export type GetUserTag = z.infer export type PutUserTag = z.infer @@ -23,4 +27,5 @@ export const mapUserTagsToSelectItems = (tags: GetUserTag[]): SelectItem[] => { value: tag.id, label: tag.value })) -} \ No newline at end of file +} + diff --git a/src/widgets/CameraCard.tsx b/src/widgets/CameraCard.tsx index bb81a8c..c4a521e 100644 --- a/src/widgets/CameraCard.tsx +++ b/src/widgets/CameraCard.tsx @@ -1,14 +1,15 @@ import { Button, Card, Flex, Grid, Group, Text, createStyles } from '@mantine/core'; import { useIntersection } from '@mantine/hooks'; import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; +import { useAdminRole } from '../hooks/useAdminRole'; import { recordingsPageQuery } from '../pages/RecordingsPage'; import { routesPath } from '../router/routes.path'; import { mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api'; import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema'; import AutoUpdatedImage from '../shared/components/images/AutoUpdatedImage'; -import { useTranslation } from 'react-i18next'; -import { useAdminRole } from '../hooks/useAdminRole'; +import CameraTagsList from './CameraTagsList'; const useStyles = createStyles((theme) => ({ mainCard: { @@ -55,7 +56,7 @@ const CameraCard = ({ useEffect(() => { if (entry?.isIntersecting) setRenderImage(true) - }, [entry?.isIntersecting]) + }, [entry?.isIntersecting]) const handleOpenLiveView = () => { const url = routesPath.LIVE_PATH.replace(':id', camera.id) @@ -73,6 +74,8 @@ const CameraCard = ({ } } + + return ( @@ -87,6 +90,7 @@ const CameraCard = ({ {!isAdmin ? null : } + ); diff --git a/src/widgets/CameraTagsList.tsx b/src/widgets/CameraTagsList.tsx new file mode 100644 index 0000000..6cfec66 --- /dev/null +++ b/src/widgets/CameraTagsList.tsx @@ -0,0 +1,107 @@ +import { Flex } from '@mantine/core'; +import { dataTagSymbol, useMutation, useQueryClient } from '@tanstack/react-query'; +import React, { useEffect, useState } from 'react'; +import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api'; +import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema'; +import AddBadge from '../shared/components/AddBadge'; +import TagBadge from '../shared/components/TagBadge'; +import { CameraTag, PutUserTag } from '../types/tags'; +import { notifications } from '@mantine/notifications'; +import { IconAlertCircle } from '@tabler/icons-react'; + + +interface CameraTagsListProps { + camera: GetCameraWHostWConfig +} + +const CameraTagsList: React.FC = ({ + camera +}) => { + + const [tagsList, setTagsList] = useState(camera.tags) + + useEffect(()=>{ + setTagsList(camera.tags) + },[camera]) + + + const { mutate: addTagToCamera } = useMutation({ + mutationFn: async (tagId: string) => frigateApi.putTagToCamera(camera.id, tagId) + .catch(error => { + if (error.response && error.response.data) { + return Promise.reject(error.response.data) + } + return Promise.reject(error) + }), + onSuccess: (data) => { + setTagsList(data.tags) + }, + onError: (e) => { + if (e && e.message) { + notifications.show({ + id: e.message, + withCloseButton: true, + autoClose: 5000, + title: "Error", + message: e.message, + color: 'red', + icon: , + }) + } + } + }) + + + const { mutate: deleteTagFromCamera } = useMutation({ + mutationFn: (tagId: string) => frigateApi.deleteTagFromCamera(camera.id, tagId) + .catch(error => { + if (error.response && error.response.data) { + return Promise.reject(error.response.data) + } + return Promise.reject(error) + }), + onSuccess: (data) => setTagsList(data.tags), + onError: (e) => { + if (e && e.message) { + notifications.show({ + id: e.message, + withCloseButton: true, + autoClose: 5000, + title: "Error", + message: e.message, + color: 'red', + icon: , + }) + } + } + }) + + const handleAddTagClick = (tagId: string) => { + if (tagId) addTagToCamera(tagId) + } + + const handleDeleteTagClick = (tagId: string) => { + if (tagId) deleteTagFromCamera(tagId) + } + + return ( + + { + tagsList && tagsList.length > 0 ? tagsList.map(tag => ( + + )) + : <> + } + + + ); +}; + +export default CameraTagsList; \ No newline at end of file