add access page
This commit is contained in:
parent
7304fe231e
commit
5cfd9155c3
@ -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(() => {
|
||||
|
||||
63
src/pages/AccessSettings.tsx
Normal file
63
src/pages/AccessSettings.tsx
Normal 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;
|
||||
@ -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} />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 />,
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
@ -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>
|
||||
@ -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);
|
||||
|
||||
84
src/shared/components/CamerasTransferList.tsx
Normal file
84
src/shared/components/CamerasTransferList.tsx
Normal 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;
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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: []},
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user