fix tags handlers and schemas

This commit is contained in:
NlightN22 2024-09-30 15:41:11 +07:00
parent 90114ac7a6
commit f50bfa6cb6
9 changed files with 260 additions and 29 deletions

View File

@ -132,8 +132,6 @@ const EditCameraPage = () => {
const handleSave = () => { const handleSave = () => {
if (!selectedMask || !points) return if (!selectedMask || !points) return
console.log('type', selectedMask?.type)
console.log('save', points)
mutate() mutate()
} }

View File

@ -5,7 +5,9 @@ import {
GetCameraWHostWConfig, GetRole, GetCameraWHostWConfig, GetRole,
GetRoleWCameras, GetExportedFile, recordingSchema, GetRoleWCameras, GetExportedFile, recordingSchema,
oidpConfig, oidpConfig,
OIDPConfig OIDPConfig,
GetCameraWHost,
GetCamera
} from "./frigate.schema"; } from "./frigate.schema";
import { FrigateConfig } from "../../types/frigateConfig"; import { FrigateConfig } from "../../types/frigateConfig";
import { RecordSummary } from "../../types/record"; import { RecordSummary } from "../../types/record";
@ -40,21 +42,13 @@ export const frigateApi = {
putConfigs: (config: PutConfig[]) => instanceApi.put('apiv1/config', config).then(res => res.data), 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), 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), putOIDPConfigTest: (config: OIDPConfig) => instanceApi.put('apiv1/config/oidp/test', config).then(res => res.data),
getHosts: () => instanceApi.get<GetFrigateHost[]>('apiv1/frigate-hosts').then(res => { getHosts: () => instanceApi.get<GetFrigateHost[]>('apiv1/frigate-hosts').then(res => res.data),
return res.data getHost: (id: string) => instanceApi.get<GetFrigateHost>(`apiv1/frigate-hosts/${id}`).then(res => res.data),
}),
getHost: (id: string) => instanceApi.get<GetFrigateHost>(`apiv1/frigate-hosts/${id}`).then(res => {
return res.data
}),
getCamerasByHostId: (hostId: string) => instanceApi.get<GetCameraWHostWConfig[]>(`apiv1/cameras/host/${hostId}`).then(res => res.data), getCamerasByHostId: (hostId: string) => instanceApi.get<GetCameraWHostWConfig[]>(`apiv1/cameras/host/${hostId}`).then(res => res.data),
getCamerasWHost: () => instanceApi.get<GetCameraWHostWConfig[]>(`apiv1/cameras`).then(res => res.data), getCamerasWHost: () => instanceApi.get<GetCameraWHostWConfig[]>(`apiv1/cameras`).then(res => res.data),
getCameraWHost: (id: string) => instanceApi.get<GetCameraWHostWConfig>(`apiv1/cameras/${id}`).then(res => { return res.data }), getCameraWHost: (id: string) => instanceApi.get<GetCameraWHostWConfig>(`apiv1/cameras/${id}`).then(res => res.data),
putHosts: (hosts: PutFrigateHost[]) => instanceApi.put<GetFrigateHost[]>('apiv1/frigate-hosts', hosts).then(res => { putHosts: (hosts: PutFrigateHost[]) => instanceApi.put<GetFrigateHost[]>('apiv1/frigate-hosts', hosts).then(res => res.data),
return res.data deleteHosts: (hosts: DeleteFrigateHost[]) => instanceApi.delete<GetFrigateHost[]>('apiv1/frigate-hosts', { data: hosts }).then(res => res.data),
}),
deleteHosts: (hosts: DeleteFrigateHost[]) => instanceApi.delete<GetFrigateHost[]>('apiv1/frigate-hosts', { data: hosts }).then(res => {
return res.data
}),
getRoles: () => instanceApi.get<GetRole[]>('apiv1/roles').then(res => res.data), getRoles: () => instanceApi.get<GetRole[]>('apiv1/roles').then(res => res.data),
putRoles: () => instanceApi.put<GetRole[]>('apiv1/roles').then(res => res.data), putRoles: () => instanceApi.put<GetRole[]>('apiv1/roles').then(res => res.data),
putRoleWCameras: (roleId: string, cameraIDs: string[]) => instanceApi.put<GetRoleWCameras>(`apiv1/roles/${roleId}/cameras`, putRoleWCameras: (roleId: string, cameraIDs: string[]) => instanceApi.put<GetRoleWCameras>(`apiv1/roles/${roleId}/cameras`,
@ -64,7 +58,9 @@ export const frigateApi = {
getAdminRole: () => instanceApi.get<GetConfig>('apiv1/config/admin').then(res => res.data), getAdminRole: () => instanceApi.get<GetConfig>('apiv1/config/admin').then(res => res.data),
getUserTags: () => instanceApi.get<GetUserTag[]>('apiv1/tags').then(res => res.data), getUserTags: () => instanceApi.get<GetUserTag[]>('apiv1/tags').then(res => res.data),
putUserTag: (tag: PutUserTag) => instanceApi.put<GetUserTag>('apiv1/tags', tag).then(res => res.data), putUserTag: (tag: PutUserTag) => instanceApi.put<GetUserTag>('apiv1/tags', tag).then(res => res.data),
delUserTag: (tagId: string) => instanceApi.delete<GetUserTag>(`apiv1/tags/${tagId}`).then(res => res.data) delUserTag: (tagId: string) => instanceApi.delete<GetUserTag>(`apiv1/tags/${tagId}`).then(res => res.data),
putTagToCamera: (cameraId: string, tagId: string) => instanceApi.put<GetCamera>(`apiv1/cameras/${cameraId}/tag/${tagId}`).then(res => res.data),
deleteTagFromCamera: (cameraId: string, tagId: string) => instanceApi.delete<GetCamera>(`apiv1/cameras/${cameraId}/tag/${tagId}`).then(res => res.data),
} }
export const proxyPrefix = `${proxyURL.protocol}//${proxyURL.host}/proxy/` export const proxyPrefix = `${proxyURL.protocol}//${proxyURL.host}/proxy/`

View File

@ -1,5 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { CameraConfig, FrigateConfig } from "../../types/frigateConfig"; import { CameraConfig, FrigateConfig } from "../../types/frigateConfig";
import { cameraTag } from "../../types/tags";
export const putConfigSchema = z.object({ export const putConfigSchema = z.object({
key: z.string(), key: z.string(),
@ -48,6 +49,7 @@ const getCameraSchema = z.object({
name: z.string(), name: z.string(),
url: z.string(), url: z.string(),
state: z.boolean().nullable(), state: z.boolean().nullable(),
tags: cameraTag.array()
}); });
export const getRoleSchema = z.object({ export const getRoleSchema = z.object({
@ -127,6 +129,7 @@ export type OIDPConfig = z.infer<typeof oidpConfig>
export type GetFrigateHost = z.infer<typeof getFrigateHostSchema> export type GetFrigateHost = z.infer<typeof getFrigateHostSchema>
// export type GetFrigateHostWithCameras = z.infer<typeof getFrigateHostWithCamerasSchema> // export type GetFrigateHostWithCameras = z.infer<typeof getFrigateHostWithCamerasSchema>
export type GetFrigateHostWConfig = GetFrigateHost & { config: FrigateConfig } export type GetFrigateHostWConfig = GetFrigateHost & { config: FrigateConfig }
export type GetCamera = z.infer<typeof getCameraSchema>
export type GetCameraWHost = z.infer<typeof getCameraWithHostSchema> export type GetCameraWHost = z.infer<typeof getCameraWithHostSchema>
export type GetCameraWHostWConfig = GetCameraWHost & { config?: CameraConfig } export type GetCameraWHostWConfig = GetCameraWHost & { config?: CameraConfig }
export type PutFrigateHost = z.infer<typeof putFrigateHostSchema> export type PutFrigateHost = z.infer<typeof putFrigateHostSchema>

View File

@ -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<AddBadgeProps> = ({
onClick
}) => {
const { data, isPending, isError, refetch } = useQuery({
queryKey: [frigateQueryKeys.getUserTags],
queryFn: frigateApi.getUserTags
})
const handleClick = (tagId: string) => {
if (onClick) onClick(tagId)
}
if (isPending) return <CogwheelLoader />
if (isError) return <RetryError onRetry={refetch} />
if (!data || data.length < 1) return (
<Badge
mt='0.2rem'
variant="outline"
>
<ActionIcon size="xs" color="blue" radius="xl" variant="transparent">
<IconPlus size={rem(20)} />
</ActionIcon>
</Badge>
)
return (
<Menu
shadow="md"
width={200}
transitionProps={{ transition: 'pop-top-right' }}
position="top-end"
withinPortal
>
<Menu.Target>
<Badge
mt='0.2rem'
variant="outline"
>
<ActionIcon size="xs" color="blue" radius="xl" variant="transparent">
<IconPlus size={rem(20)} />
</ActionIcon>
</Badge>
</Menu.Target>
<Menu.Dropdown>
{data.map((item) => (
<Menu.Item key={item.id} onClick={() => handleClick(item.id)}>
{item.value}
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
);
};
export default AddBadge;

View File

@ -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 (
<ActionIcon onClick={onClick} size="xs" color="blue" radius="xl" variant="transparent">
<IconX size={rem(10)} />
</ActionIcon>
);
};
interface TagBadgeProps {
value: string,
label: string,
onClick?(value: string): void,
onClose?(value: string): void,
}
const TagBadge: React.FC<TagBadgeProps> = ({
value,
label,
onClick,
onClose,
}) => {
const handleClick = (value: string) => {
if (onClick) onClick(value)
}
const handleClose = () => {
if (onClose) onClose(value)
}
return (
<Badge
mt='0.2rem'
mr='0.3rem'
variant="outline"
pr={3}
rightSection={<RemoveButton onClick={handleClose} />}
onClick={() => handleClick}
>
{label}
</Badge>
);
};
export default TagBadge;

View File

@ -43,7 +43,7 @@ const UserTagsFilter = () => {
return true return true
} }
const { mutate } = useMutation({ const { mutate: addTag } = useMutation({
mutationFn: (newTag: PutUserTag) => frigateApi.putUserTag(newTag) mutationFn: (newTag: PutUserTag) => frigateApi.putUserTag(newTag)
.catch(error => { .catch(error => {
if (error.response && error.response.data) { if (error.response && error.response.data) {
@ -52,8 +52,8 @@ const UserTagsFilter = () => {
return Promise.reject(error) return Promise.reject(error)
}), }),
onSuccess: (data) => { onSuccess: (data) => {
setSelectedList([...selectedList, data.id])
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getUserTags] }) queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getUserTags] })
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getCamerasWHost] })
}, },
onError: (e) => { onError: (e) => {
if (e && e.message) { if (e && e.message) {
@ -67,7 +67,6 @@ const UserTagsFilter = () => {
icon: <IconAlertCircle />, icon: <IconAlertCircle />,
}) })
} }
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getFrigateHosts] })
} }
}) })
@ -81,6 +80,7 @@ const UserTagsFilter = () => {
}), }),
onSuccess: (data) => { onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getUserTags] }) queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getUserTags] })
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getCamerasWHost] })
const updatedList = selectedList.filter(item => data.id === item) const updatedList = selectedList.filter(item => data.id === item)
setSelectedList(updatedList) setSelectedList(updatedList)
}, },
@ -103,9 +103,8 @@ const UserTagsFilter = () => {
const saveNewTag = (value: string) => { const saveNewTag = (value: string) => {
const newTag: PutUserTag = { const newTag: PutUserTag = {
value, value,
cameraIds: []
} }
mutate(newTag) addTag(newTag)
} }
const onCreate = (query: string | SelectItem | null | undefined) => { const onCreate = (query: string | SelectItem | null | undefined) => {
@ -128,7 +127,6 @@ const UserTagsFilter = () => {
if (isError) return <RetryError onRetry={refetch} /> if (isError) return <RetryError onRetry={refetch} />
const handleOnChange = (value: string[]) => { const handleOnChange = (value: string[]) => {
console.log('cahnged:', value)
const updatedList = selectedList.filter(item => value.includes(item)) const updatedList = selectedList.filter(item => value.includes(item))
const newItems = value.filter(item => !selectedList.includes(item)) const newItems = value.filter(item => !selectedList.includes(item))
setSelectedList([...updatedList, ...newItems]) setSelectedList([...updatedList, ...newItems])

View File

@ -3,7 +3,6 @@ import { z } from "zod";
export const putUserTag = z.object({ export const putUserTag = z.object({
value: z.string(), value: z.string(),
cameraIds: z.string().array()
}) })
export const getUserTag = z.object({ export const getUserTag = z.object({
@ -12,9 +11,14 @@ export const getUserTag = z.object({
updatedAt: z.string().datetime(), updatedAt: z.string().datetime(),
value: z.string(), value: z.string(),
userId: 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<typeof cameraTag>
export type GetUserTag = z.infer<typeof getUserTag> export type GetUserTag = z.infer<typeof getUserTag>
export type PutUserTag = z.infer<typeof putUserTag> export type PutUserTag = z.infer<typeof putUserTag>
@ -24,3 +28,4 @@ export const mapUserTagsToSelectItems = (tags: GetUserTag[]): SelectItem[] => {
label: tag.value label: tag.value
})) }))
} }

View File

@ -1,14 +1,15 @@
import { Button, Card, Flex, Grid, Group, Text, createStyles } from '@mantine/core'; import { Button, Card, Flex, Grid, Group, Text, createStyles } from '@mantine/core';
import { useIntersection } from '@mantine/hooks'; import { useIntersection } from '@mantine/hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAdminRole } from '../hooks/useAdminRole';
import { recordingsPageQuery } from '../pages/RecordingsPage'; import { recordingsPageQuery } from '../pages/RecordingsPage';
import { routesPath } from '../router/routes.path'; import { routesPath } from '../router/routes.path';
import { mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api'; import { mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema'; import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
import AutoUpdatedImage from '../shared/components/images/AutoUpdatedImage'; import AutoUpdatedImage from '../shared/components/images/AutoUpdatedImage';
import { useTranslation } from 'react-i18next'; import CameraTagsList from './CameraTagsList';
import { useAdminRole } from '../hooks/useAdminRole';
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({
mainCard: { mainCard: {
@ -55,7 +56,7 @@ const CameraCard = ({
useEffect(() => { useEffect(() => {
if (entry?.isIntersecting) if (entry?.isIntersecting)
setRenderImage(true) setRenderImage(true)
}, [entry?.isIntersecting]) }, [entry?.isIntersecting])
const handleOpenLiveView = () => { const handleOpenLiveView = () => {
const url = routesPath.LIVE_PATH.replace(':id', camera.id) const url = routesPath.LIVE_PATH.replace(':id', camera.id)
@ -73,6 +74,8 @@ const CameraCard = ({
} }
} }
return ( return (
<Grid.Col md={6} lg={3} p='0.2rem'> <Grid.Col md={6} lg={3} p='0.2rem'>
<Card ref={ref} h='100%' radius="lg" padding='0.5rem' className={classes.mainCard}> <Card ref={ref} h='100%' radius="lg" padding='0.5rem' className={classes.mainCard}>
@ -87,6 +90,7 @@ const CameraCard = ({
{!isAdmin ? null : <Button size='sm' onClick={handleOpenEditCamera}>{t('edit')}</Button>} {!isAdmin ? null : <Button size='sm' onClick={handleOpenEditCamera}>{t('edit')}</Button>}
</Flex> </Flex>
</Group> </Group>
<CameraTagsList camera={camera} />
</Card> </Card>
</Grid.Col > </Grid.Col >
); );

View File

@ -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<CameraTagsListProps> = ({
camera
}) => {
const [tagsList, setTagsList] = useState<CameraTag[]>(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: <IconAlertCircle />,
})
}
}
})
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: <IconAlertCircle />,
})
}
}
})
const handleAddTagClick = (tagId: string) => {
if (tagId) addTagToCamera(tagId)
}
const handleDeleteTagClick = (tagId: string) => {
if (tagId) deleteTagFromCamera(tagId)
}
return (
<Flex justify='end' w='100%' wrap="wrap">
{
tagsList && tagsList.length > 0 ? tagsList.map(tag => (
<TagBadge
key={tag.id}
value={tag.id}
label={tag.value}
onClose={handleDeleteTagClick}
/>
))
: <></>
}
<AddBadge
onClick={handleAddTagClick}
/>
</Flex>
);
};
export default CameraTagsList;