add access page

This commit is contained in:
NlightN22 2024-02-26 01:17:55 +07:00
parent 7304fe231e
commit 5cfd9155c3
13 changed files with 224 additions and 29 deletions

View File

@ -7,7 +7,13 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Context } from '.';
import SideBar from './shared/components/SideBar';
const queryClient = new QueryClient()
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false
}
}
})
const AppBody = () => {
useEffect(() => {

View File

@ -0,0 +1,63 @@
import { useQuery } from '@tanstack/react-query';
import React, { useContext, useEffect, useState } from 'react';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import CenterLoader from '../shared/components/CenterLoader';
import RetryErrorPage from './RetryErrorPage';
import { Flex, Group, Select, Text } from '@mantine/core';
import OneSelectFilter, { OneSelectItem } from '../shared/components/filters.aps/OneSelectFilter';
import { useMediaQuery } from '@mantine/hooks';
import { dimensions } from '../shared/dimensions/dimensions';
import CamerasTransferList from '../shared/components/CamerasTransferList';
import { Context } from '..';
const AccessSettings = () => {
const { data, isPending, isError, refetch } = useQuery({
queryKey: [frigateQueryKeys.getRoles],
queryFn: frigateApi.getRoles
})
const { sideBarsStore } = useContext(Context)
useEffect(() => {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
}, [])
const isMobile = useMediaQuery(dimensions.mobileSize)
const [roleId, setRoleId] = useState<string>()
if (isPending) return <CenterLoader />
if (isError || !data) return <RetryErrorPage />
const rolesSelect: OneSelectItem[] = data.map(role => ({ value: role.id, label: role.name }))
const handleSelectRole = (value: string) => {
setRoleId(value)
}
console.log('AccessSettings rendered')
return (
<Flex w='100%' h='100%' direction='column'>
<Text align='center' size='xl'>Please select role</Text>
<Flex justify='space-between' align='center' w='100%'>
{!isMobile ? <Group w='40%' /> : <></>}
<Select
w='100%'
mt='1rem'
data={rolesSelect}
value={roleId}
onChange={handleSelectRole}
searchable
clearable
/>
{!isMobile ? <Group w='40%' /> : <></>}
</Flex>
{!roleId ? <></> :
<CamerasTransferList roleId={roleId} />
}
</Flex>
);
};
export default AccessSettings;

View File

@ -104,11 +104,8 @@ const RecordingsPage = observer(() => {
}
if (cameraId && paramCameraId) {
// console.log('cameraId', cameraId)
// console.log('paramCameraId', paramCameraId)
if ((startDay && endDay) || (!startDay && !endDay)) {
return <SelectedCameraList />
// return <SelectedCameraList cameraId={cameraId} />
}
}

View File

@ -7,7 +7,7 @@ export const routesPath = {
HOST_CONFIG_PATH: '/hosts/:id/config',
HOST_SYSTEM_PATH: '/hosts/:id/system',
HOST_STORAGE_PATH: '/hosts/:id/storage',
ROLES_PATH: '/roles',
ACCESS_PATH: '/access',
LIVE_PATH: '/cameras/:id/',
THANKS_PATH: '/thanks',
USER_DETAILED_PATH: '/user',

View File

@ -12,6 +12,7 @@ import HostSystemPage from "../pages/HostSystemPage";
import HostStoragePage from "../pages/HostStoragePage";
import LiveCameraPage from "../pages/LiveCameraPage";
import RecordingsPage from "../pages/RecordingsPage";
import AccessSettings from "../pages/AccessSettings";
interface IRoute {
path: string,
@ -47,6 +48,10 @@ export const routes: IRoute[] = [
path: routesPath.HOST_STORAGE_PATH,
component: <HostStoragePage />,
},
{
path: routesPath.ACCESS_PATH,
component: <AccessSettings />,
},
{
path: routesPath.LIVE_PATH,
component: <LiveCameraPage />,

View File

@ -1,7 +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 } from "./frigate.schema";
import {
GetConfig, DeleteFrigateHost, GetFrigateHost, PutConfig, PutFrigateHost,
GetFrigateHostWithCameras, GetCameraWHost, GetCameraWHostWConfig, GetRole,
GetUserByRole, GetRoleWCameras
} from "./frigate.schema";
import { FrigateConfig } from "../../types/frigateConfig";
import { url } from "inspector";
import { RecordSummary } from "../../types/record";
@ -25,7 +29,7 @@ export const frigateApi = {
getHost: (id: string) => instanceApi.get<GetFrigateHostWithCameras>(`apiv1/frigate-hosts/${id}`).then(res => {
return res.data
}),
getCamerasWHost: () => instanceApi.get<GetCameraWHostWConfig[]>(`apiv1/cameras`).then(res => { return 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 }),
putHosts: (hosts: PutFrigateHost[]) => instanceApi.put<GetFrigateHost[]>('apiv1/frigate-hosts', hosts).then(res => {
return res.data
@ -33,6 +37,13 @@ export const frigateApi = {
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),
getUsersByRole: (roleName: string) => instanceApi.get<GetUserByRole[]>(`apiv1/users/${roleName}`).then(res => res.data),
getRoleWCameras: (roleId: string) => instanceApi.get<GetRoleWCameras>(`apiv1/roles/${roleId}`).then(res => res.data),
putRoleWCameras: (roleId: string, cameraIDs: string[]) => instanceApi.put<GetRoleWCameras>(`apiv1/roles/${roleId}/cameras`,
{
cameraIDs: cameraIDs
}).then(res => res.data)
}
export const proxyApi = {
@ -129,4 +140,7 @@ export const frigateQueryKeys = {
getRecordingsSummary: 'recordings-frigate-summary',
getRecordings: 'recordings-frigate',
getEvents: 'events-frigate',
getRoles: 'roles',
getRoleWCameras: 'roles-cameras',
getUsersByRole: 'users-role',
}

View File

@ -41,8 +41,16 @@ const getCameraSchema = z.object({
state: z.boolean().nullable(),
});
export const getRoleSchema = z.object({
id: z.string(),
name: z.string(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
})
const getCameraWithHostSchema = getCameraSchema.merge(z.object({
frigateHost: getFrigateHostSchema.optional()
frigateHost: getFrigateHostSchema.optional(),
roles: getRoleSchema.array().optional(),
}))
export const getFrigateHostWithCamerasSchema = getFrigateHostSchema.merge(z.object({
@ -72,12 +80,32 @@ export const getEventsQuerySchema = z.object({
maxScore: z.number().optional(),
})
export const getRoleWCamerasSchema = getRoleSchema.merge(z.object({
cameras: getCameraSchema.array()
}))
export const getUserByRoleSchema = z.object({
id: z.string(),
username: z.string(),
enabled: z.boolean(),
emailVerified: z.boolean(),
firstName: z.string(),
lastName: z.string(),
email: z.string(),
})
export type GetConfig = z.infer<typeof getConfigSchema>
export type PutConfig = z.infer<typeof putConfigSchema>
export type GetFrigateHost = z.infer<typeof getFrigateHostSchema>
export type GetFrigateHostWithCameras = z.infer<typeof getFrigateHostWithCamerasSchema>
export type GetFrigateHostWConfig = GetFrigateHost & { config: FrigateConfig}
export type GetFrigateHostWConfig = GetFrigateHost & { config: FrigateConfig }
export type GetCameraWHost = z.infer<typeof getCameraWithHostSchema>
export type GetCameraWHostWConfig = GetCameraWHost & { config?: CameraConfig }
export type PutFrigateHost = z.infer<typeof putFrigateHostSchema>
export type DeleteFrigateHost = z.infer<typeof deleteFrigateHostSchema>
export type GetRole = z.infer<typeof getRoleSchema>
export type GetRoleWCameras = z.infer<typeof getRoleWCamerasSchema>
export type GetUserByRole = z.infer<typeof getUserByRoleSchema>

View File

@ -2,7 +2,6 @@ import { useEffect, useRef } from "react";
import { CameraConfig } from "../../types/frigateConfig";
import { Flex, Text } from "@mantine/core";
import { useQuery } from "@tanstack/react-query";
import CenterLoader from "./CenterLoader";
import { frigateApi, proxyApi } from "../../services/frigate.proxy/frigate.api";
import { useIntersection } from "@mantine/hooks";
import CogwheelLoader from "./loaders/CogwheelLoader";
@ -33,7 +32,6 @@ const AutoUpdatedImage = ({
useEffect(() => {
if (isVisible) {
console.log('imageUrl is visible')
const intervalId = setInterval(() => {
refetch();
}, 60 * 1000);

View File

@ -0,0 +1,84 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import React, { useEffect, useState } from 'react';
import { frigateApi, frigateQueryKeys } from '../../services/frigate.proxy/frigate.api';
import CogwheelLoader from './loaders/CogwheelLoader';
import RetryError from './RetryError';
import { TransferList, Text, TransferListData, TransferListProps, TransferListItem, Button, Flex } from '@mantine/core';
import { OneSelectItem } from './filters.aps/OneSelectFilter';
interface CamerasTransferListProps {
roleId: string
}
const CamerasTransferList = ({
roleId,
}: CamerasTransferListProps) => {
const queryClient = useQueryClient()
const { data: cameras, isPending, isError, refetch } = useQuery({
queryKey: [frigateQueryKeys.getCamerasWHost, roleId],
queryFn: frigateApi.getCamerasWHost
})
const [lists, setLists] = useState<[TransferListItem[], TransferListItem[]]>([[], []])
const handleChange = (value: TransferListData) => {
setLists(value)
}
const mutation = useMutation({
mutationFn: () => {
const toRole = lists[1].map( item => item.value )
return frigateApi.putRoleWCameras(roleId, toRole)
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getCamerasWHost] })
},
})
useEffect(() => {
if (cameras) {
const roleInCameras: TransferListItem[] | undefined = cameras.filter(camera => camera.roles?.some(role => role.id === roleId))
.map(camera => ({ value: camera.id, label: `${camera.name} / ${camera.frigateHost?.name}` }))
const notInCameras: TransferListItem[] | undefined = cameras.filter(camera => !camera.roles?.some(role => role.id === roleId))
.map(camera => ({ value: camera.id, label: `${camera.name} / ${camera.frigateHost?.name}` }))
setLists([notInCameras, roleInCameras])
}
}, [cameras])
if (isPending) return <CogwheelLoader />
if (isError || !cameras) return <RetryError onRetry={refetch} />
if (cameras.length < 1) return <Text>Empty cameras </Text>
const handleSave = () => {
mutation.mutate()
}
const handleDiscard = () => {
refetch()
}
console.log('CamerasTransferListProps rendered')
return (
<>
<Flex w='100%' justify='center'>
<Button mt='1rem' w='10%' miw='6rem' mr='1rem' onClick={handleDiscard}>Discard</Button>
<Button mt='1rem' w='10%' miw='5rem' onClick={handleSave}>Save</Button>
</Flex>
<TransferList
transferAllMatchingFilter
listHeight={500}
mt='1rem'
value={lists}
onChange={handleChange}
searchPlaceholder="Search..."
nothingFound="Nothing here"
titles={['Not allowed', 'Allowed']}
breakpoint="sm"
/>
</>
);
};
export default CamerasTransferList;

View File

@ -38,7 +38,7 @@ const CameraSelectFilter = ({
const camerasItems: OneSelectItem[] = data.cameras.map(camera => ({ value: camera.id, label: camera.name }))
const handleSelect = (id: string, value: string) => {
const handleSelect = (value: string) => {
const camera = data.cameras.find(camera => camera.id === value)
if (!camera) {
recStore.selectedCamera = undefined

View File

@ -34,7 +34,7 @@ const HostSelectFilter = () => {
.filter(host => host.enabled)
.map(host => ({ value: host.id, label: host.name }))
const handleSelect = (id: string, value: string) => {
const handleSelect = (value: string) => {
const host = hosts?.find(host => host.id === value)
if (!host) {
recStore.selectedHost = undefined

View File

@ -11,43 +11,42 @@ export interface OneSelectItem {
disabled?: boolean;
}
interface OneSelectFilterProps {
id: string
interface OneSelectFilterProps extends SelectProps {
id?: string
data: OneSelectItem[]
spaceBetween?: SystemProp<SpacingValue>
label?: string
defaultValue?: string
textClassName?: string
selectProps?: SelectProps,
display?: SystemProp<CSSProperties['display']>
showClose?: boolean,
value?: string,
onChange?(id: string, value: string): void
onClose?(): void
onChange?: (value: string, id?: string,) => void
onClose?: () => void
}
const OneSelectFilter = ({
id, data, spaceBetween,
label, defaultValue, textClassName,
selectProps, display, showClose, value, onChange, onClose
showClose, value, onChange: onChange, onClose, ...selectProps
}: OneSelectFilterProps) => {
const handleOnChange = (value: string) => {
if (onChange) {
onChange(id, value)
}
if (onChange) onChange(value, id,)
}
const handleOnClose = () => {
if (onClose) onClose()
}
return (
<Box display={display} mt={spaceBetween}>
<Box mt={spaceBetween}>
<Flex justify='space-between'>
<Text className={textClassName}>{label}</Text>
{showClose ? <CloseWithTooltip label={strings.hide} onClose={onClose} />
: null}
{showClose ? <CloseWithTooltip label={strings.hide} onClose={handleOnClose} />
: null}
</Flex>
<Select
{...selectProps}
mt={spaceBetween}
data={data}
defaultValue={defaultValue}
@ -55,6 +54,7 @@ const OneSelectFilter = ({
onChange={handleOnChange}
searchable
clearable
{...selectProps}
/>
</Box>
)

View File

@ -10,6 +10,6 @@ export const testHeaderLinks: HeaderActionProps =
{link: routesPath.SETTINGS_PATH, label: headerMenu.settings, links: []},
{link: routesPath.RECORDINGS_PATH, label: headerMenu.recordings, links: []},
{link: routesPath.HOSTS_PATH, label: headerMenu.hostsConfig, links: []},
{link: routesPath.ROLES_PATH, label: headerMenu.acessSettings, links: []},
{link: routesPath.ACCESS_PATH, label: headerMenu.acessSettings, links: []},
]
}