Add user tags filter at main page

This commit is contained in:
NlightN22 2024-09-29 22:21:59 +07:00
parent d39e976053
commit 90114ac7a6
30 changed files with 541 additions and 390 deletions

View File

@ -8,6 +8,7 @@ import { useEffect, useState } from 'react';
import AppBody from './AppBody';
import { FfprobeModal } from './shared/components/modal.windows/FfprobeModal';
import { VaInfoModal } from './shared/components/modal.windows/VaInfoModal';
import { SideBarProvider } from './widgets/sidebars/SideBarContext';
const queryClient = new QueryClient({
defaultOptions: {
@ -67,8 +68,10 @@ function App() {
}}
>
<ModalsProvider modals={modals}>
<SideBarProvider>
<Notifications />
<AppBody />
</SideBarProvider>
</ModalsProvider>
</MantineProvider >
</ColorSchemeProvider>

View File

@ -5,9 +5,10 @@ import { useTranslation } from 'react-i18next';
import { Context } from '.';
import AppRouter from './router/AppRouter';
import { routesPath } from './router/routes.path';
import SideBar from './shared/components/SideBar';
import { isProduction } from './shared/env.const';
import { HeaderAction } from './widgets/header/HeaderAction';
import RightSideBar from "./shared/components/RightSideBar";
import { useLocation } from "react-router-dom";
const AppBody = () => {
const { t } = useTranslation()
@ -20,20 +21,22 @@ const AppBody = () => {
{ link: routesPath.ACCESS_PATH, label: t('header.acessSettings'), admin: true },
]
const { sideBarsStore } = useContext(Context)
const [leftSideBar, setLeftSidebar] = useState(false)
const [rightSideBar, setRightSidebar] = useState(false)
const location = useLocation()
const leftSideBarIsHidden = (isHidden: boolean) => {
setLeftSidebar(!isHidden)
}
const rightSideBarIsHidden = (isHidden: boolean) => {
setRightSidebar(!isHidden)
}
const pathsWithLeftSidebar: string[] = []
const pathsWithRightSidebar: string[] = [routesPath.MAIN_PATH, routesPath.RECORDINGS_PATH]
const [leftSideBar, setLeftSidebar] = useState(pathsWithLeftSidebar.includes(location.pathname))
const [rightSideBar, setRightSidebar] = useState(pathsWithRightSidebar.includes(location.pathname))
const handleRightSidebarChange = (isVisible: boolean) => {
setRightSidebar(isVisible);
};
const theme = useMantineTheme();
if (!isProduction) console.log("render Main")
return (
<AppShell
@ -50,9 +53,10 @@ const AppBody = () => {
header={
<HeaderAction links={headerLinks} />
}
aside={
!sideBarsStore.rightVisible ? <></> :
<SideBar isHidden={rightSideBarIsHidden} side="right" />
!pathsWithRightSidebar.includes(location.pathname) ? <></> :
<RightSideBar onChangeHidden={handleRightSidebarChange}/>
}
>
<AppRouter />

View File

@ -2,7 +2,8 @@ import { error } from "console"
const en = {
mainPage: {
createSelectTags: 'Create\\Select tags'
createSelectTags: 'Create\\Select tags',
tagsError: 'Cannot be more than 10 symbols',
},
editCameraPage: {
notFrigateCamera: 'Not frigate camera',

View File

@ -1,6 +1,7 @@
const ru = {
mainPage: {
createSelectTags: 'Создать\\Выбрать тэги'
createSelectTags: 'Создай\\Выбери тэги',
tagsError: 'Не может быть более 10 символов',
},
editCameraPage: {
notFrigateCamera: 'Не камера Фригата',

View File

@ -10,17 +10,7 @@ import { useNavigate } from 'react-router-dom';
const Forbidden = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const executed = useRef(false)
const { sideBarsStore } = useContext(Context)
useEffect(() => {
if (!executed.current) {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
executed.current = true
}
}, [sideBarsStore])
const handleGoToMain = () => {
navigate(routesPath.MAIN_PATH)

View File

@ -1,24 +1,11 @@
import { Button, Flex, Text } from '@mantine/core';
import React, { useContext, useEffect, useRef } from 'react';
import CogWheelWithText from '../shared/components/loaders/CogWheelWithText';
import { routesPath } from '../router/routes.path';
import { Context } from '..';
import { observer } from 'mobx-react-lite';
import { useTranslation } from 'react-i18next';
import { routesPath } from '../router/routes.path';
import CogWheelWithText from '../shared/components/loaders/CogWheelWithText';
const NotFound = () => {
const { t } = useTranslation()
const executed = useRef(false)
const { sideBarsStore } = useContext(Context)
useEffect(() => {
if (!executed.current) {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
executed.current = true
}
}, [sideBarsStore])
const handleGoToMain = () => {
window.location.replace(routesPath.MAIN_PATH)

View File

@ -2,9 +2,8 @@ import { Flex, Group, Text } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { useQuery } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';
import { useContext, useEffect, useRef, useState } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Context } from '..';
import { useAdminRole } from '../hooks/useAdminRole';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import CamerasTransferList from '../shared/components/CamerasTransferList';
@ -17,22 +16,13 @@ import RetryErrorPage from './RetryErrorPage';
const AccessSettings = () => {
const { t } = useTranslation()
const executed = useRef(false)
const { data, isPending, isError, refetch } = useQuery({
queryKey: [frigateQueryKeys.getRoles],
queryFn: frigateApi.getRoles
})
const { sideBarsStore } = useContext(Context)
const { isAdmin, isLoading: adminLoading } = useAdminRole()
useEffect(() => {
if (!executed.current) {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
executed.current = true
}
}, [sideBarsStore])
const isMobile = useMediaQuery(dimensions.mobileSize)
const [roleId, setRoleId] = useState<string>()

View File

@ -3,10 +3,9 @@ import { notifications } from '@mantine/notifications';
import { IconAlertCircle, IconCircleCheck } from '@tabler/icons-react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';
import { useContext, useEffect, useRef, useState } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { Context } from '..';
import { useAdminRole } from '../hooks/useAdminRole';
import { frigateApi, frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
import MaskSelect, { MaskItem, MaskType } from '../shared/components/filters/MaskSelect';
@ -19,7 +18,6 @@ import RetryErrorPage from './RetryErrorPage';
const EditCameraPage = () => {
const { t } = useTranslation()
const executed = useRef(false)
let { id: cameraId } = useParams<'id'>()
if (!cameraId) throw Error(t('editCameraPage.cameraIdNotExist'))
const [selectedMask, setSelectedMask] = useState<MaskItem>()
@ -105,16 +103,6 @@ const EditCameraPage = () => {
const { isAdmin, isLoading: adminLoading } = useAdminRole()
const { sideBarsStore } = useContext(Context)
useEffect(() => {
if (!executed.current) {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
executed.current = true
}
}, [sideBarsStore])
if (isPending || adminLoading) return <CenterLoader />
if (!isAdmin) return <Forbidden />
if (isError) return <RetryErrorPage onRetry={refetch} />

View File

@ -3,9 +3,8 @@ import { notifications } from '@mantine/notifications';
import { IconAlertCircle } from '@tabler/icons-react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';
import { useContext, useEffect, useRef, useState } from 'react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Context } from '..';
import { useAdminRole } from '../hooks/useAdminRole';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import { GetFrigateHost, deleteFrigateHostSchema, putFrigateHostSchema } from '../services/frigate.proxy/frigate.schema';
@ -19,22 +18,12 @@ import RetryErrorPage from './RetryErrorPage';
const FrigateHostsPage = () => {
const { t } = useTranslation()
const executed = useRef(false)
const queryClient = useQueryClient()
const { isPending: hostsPending, error: hostsError, data } = useQuery({
queryKey: [frigateQueryKeys.getFrigateHosts],
queryFn: frigateApi.getHosts,
})
const { sideBarsStore } = useContext(Context)
useEffect(() => {
if (!executed.current) {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
executed.current = true
}
}, [sideBarsStore])
const { isAdmin, isLoading: adminLoading } = useAdminRole()
const [pageData, setPageData] = useState(data)

View File

@ -1,13 +1,15 @@
import { Button, Flex, Text, useMantineTheme } from '@mantine/core';
import { Button, Flex, useMantineTheme } from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import Editor, { Monaco } from '@monaco-editor/react';
import { IconAlertCircle, IconCircleCheck } from '@tabler/icons-react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';
import * as monaco from "monaco-editor";
import { SchemasSettings, configureMonacoYaml } from 'monaco-yaml';
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useParams } from 'react-router-dom';
import { Context } from '..';
import { useAdminRole } from '../hooks/useAdminRole';
import { frigateApi, frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
import { GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
@ -16,9 +18,6 @@ import { isProduction } from '../shared/env.const';
import { SaveOption } from '../types/saveConfig';
import Forbidden from './403';
import RetryErrorPage from './RetryErrorPage';
import { notifications } from '@mantine/notifications';
import { IconAlertCircle, IconCircleCheck } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
window.MonacoEnvironment = {
@ -42,9 +41,7 @@ const HostConfigPage = () => {
const queryParams = useMemo(() => {
return new URLSearchParams(location.search);
}, [location.search])
const executed = useRef(false)
const host = useRef<GetFrigateHost | undefined>()
const { sideBarsStore } = useContext(Context)
let { id } = useParams<'id'>()
const { isAdmin, isLoading: adminLoading } = useAdminRole()
@ -96,15 +93,6 @@ const HostConfigPage = () => {
}
})
useEffect(() => {
if (!executed.current) {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
executed.current = true
}
}, [sideBarsStore])
const clipboard = useClipboard({ timeout: 500 })
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)

View File

@ -2,10 +2,9 @@ import { Flex, Grid, SegmentedControl, Text } from '@mantine/core';
import { openContextModal } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { Context } from '..';
import { useAdminRole } from '../hooks/useAdminRole';
import { frigateApi, frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
import { GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
@ -16,9 +15,9 @@ import StorageRingStat from '../shared/components/stats/StorageRingStat';
import { isProduction } from '../shared/env.const';
import { formatUptime } from '../shared/utils/dateUtil';
import FrigateCamerasStateTable, { CameraItem, ProcessType } from '../widgets/camera.stat.table/FrigateCameraStateTable';
import FrigateStorageStateTable from '../widgets/camera.stat.table/FrigateStorageStateTable';
import Forbidden from './403';
import RetryErrorPage from './RetryErrorPage';
import FrigateStorageStateTable from '../widgets/camera.stat.table/FrigateStorageStateTable';
export const hostSystemPageQuery = {
hostId: 'hostId',
@ -31,21 +30,10 @@ enum SelectorItems {
const HostSystemPage = () => {
const { t } = useTranslation()
const executed = useRef(false)
const { sideBarsStore } = useContext(Context)
const { isAdmin } = useAdminRole()
const host = useRef<GetFrigateHost | undefined>()
const [selector, setSelector] = useState(SelectorItems.Cameras)
useEffect(() => {
if (!executed.current) {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
executed.current = true
}
}, [sideBarsStore])
let { id: paramHostId } = useParams<'id'>()
const { data, isError, isPending, refetch } = useQuery({

View File

@ -1,10 +1,8 @@
import { Flex } from '@mantine/core';
import { useQuery } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';
import { useContext, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { Context } from '..';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import CenterLoader from '../shared/components/loaders/CenterLoader';
import Player from '../widgets/Player';
@ -13,7 +11,6 @@ import RetryErrorPage from './RetryErrorPage';
const LiveCameraPage = () => {
const { t } = useTranslation()
const executed = useRef(false)
let { id: cameraId } = useParams<'id'>()
if (!cameraId) throw Error('Camera id does not exist')
@ -22,17 +19,6 @@ const LiveCameraPage = () => {
queryFn: () => frigateApi.getCameraWHost(cameraId!)
})
const { sideBarsStore } = useContext(Context)
useEffect(() => {
if (!executed.current) {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
executed.current = true
}
}, [sideBarsStore])
if (isPending) return <CenterLoader />
if (isError) return <RetryErrorPage onRetry={refetch} />

View File

@ -1,25 +1,25 @@
import { Flex, Grid, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { Flex, Grid } from '@mantine/core';
import { useQuery } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';
import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Context } from '..';
import { useRealmUser } from '../hooks/useRealmUser';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
import CenterLoader from '../shared/components/loaders/CenterLoader';
import CameraCard from '../widgets/CameraCard';
import RetryErrorPage from './RetryErrorPage';
import ClearableTextInput from '../shared/components/inputs/ClearableTextInput';
import { useTranslation } from 'react-i18next';
import MainFiltersRightSide from '../widgets/sidebars/MainFiltersRightSide';
import { isProduction } from '../shared/env.const';
import { useKeycloak } from '@react-keycloak/web';
import { useRealmUser } from '../hooks/useRealmUser';
import CameraCard from '../widgets/CameraCard';
import MainFiltersRightSide from '../widgets/sidebars/MainFiltersRightSide';
import { SideBarContext } from '../widgets/sidebars/SideBarContext';
import RetryErrorPage from './RetryErrorPage';
import { IconSearch } from '@tabler/icons-react';
import ClearableTextInput from '../shared/components/inputs/ClearableTextInput';
const MainPage = () => {
const { t } = useTranslation()
const executed = useRef(false)
const { sideBarsStore, mainStore } = useContext(Context)
const { mainStore } = useContext(Context)
const { setChildrenComponent } = useContext(SideBarContext)
const { selectedHostId } = mainStore
const [searchQuery, setSearchQuery] = useState<string>()
const [filteredCameras, setFilteredCameras] = useState<GetCameraWHostWConfig[]>()
@ -32,6 +32,11 @@ const MainPage = () => {
queryFn: frigateApi.getCamerasWHost
})
useEffect(() => {
setChildrenComponent(<MainFiltersRightSide />);
return () => setChildrenComponent(null);
}, []);
useEffect(() => {
if (!cameras) {
setFilteredCameras(undefined)
@ -48,26 +53,6 @@ const MainPage = () => {
}, [searchQuery, cameras, selectedHostId])
useEffect(() => {
sideBarsStore.setLeftChildren(null)
sideBarsStore.leftVisible = false
executed.current = true
if (!isProduction) console.log('MainPage rendered first time')
}, [])
useEffect(() => {
sideBarsStore.setRightChildren(<MainFiltersRightSide />)
sideBarsStore.rightVisible = true
return () => {
sideBarsStore.setRightChildren(null)
sideBarsStore.rightVisible = false
}
}, [sideBarsStore])
//test change
const cards = useMemo(() => {
if (filteredCameras)
return filteredCameras.filter(camera => {
@ -115,7 +100,8 @@ const MainPage = () => {
</Flex>
<Flex justify='center' h='100%' direction='column' w='100%' >
<Grid mt='sm' justify="center" mb='sm' align='stretch'>
{cards}
{/* TODO DELETE SLICE TO WORK */}
{cards.slice(0, 5)}
</Grid>
</Flex>
</Flex>

View File

@ -1,26 +1,14 @@
import { Flex } from '@mantine/core';
import React, { useContext, useEffect, useRef } from 'react';
import { observer } from 'mobx-react-lite';
import { useLocation } from 'react-router-dom';
import VideoPlayer from '../shared/components/players/VideoPlayer';
import NotFound from './404';
import { Context } from '..';
import { observer } from 'mobx-react-lite';
export const playRecordPageQuery = {
link: 'link',
}
const PlayRecordPage = () => {
const executed = useRef(false)
const { sideBarsStore } = useContext(Context)
useEffect(() => {
if (!executed.current) {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
executed.current = true
}
}, [sideBarsStore])
const location = useLocation()
const queryParams = new URLSearchParams(location.search)

View File

@ -11,6 +11,7 @@ import SelectedCameraList from '../widgets/SelectedCameraList';
import SelectedDayList from '../widgets/SelectedDayList';
import SelectedHostList from '../widgets/SelectedHostList';
import { useTranslation } from 'react-i18next';
import { SideBarContext } from '../widgets/sidebars/SideBarContext';
export const recordingsPageQuery = {
@ -24,7 +25,7 @@ export const recordingsPageQuery = {
const RecordingsPage = () => {
const { t } = useTranslation()
const executed = useRef(false)
const { sideBarsStore, recordingsStore: recStore } = useContext(Context)
const { recordingsStore: recStore } = useContext(Context)
const location = useLocation()
const navigate = useNavigate()
@ -40,12 +41,17 @@ const RecordingsPage = () => {
const [cameraId, setCameraId] = useState<string>('')
const [period, setPeriod] = useState<[Date | null, Date | null]>([null, null])
const { setChildrenComponent } = useContext(SideBarContext)
useEffect(() => {
setChildrenComponent(<RecordingsFiltersRightSide />);
return () => setChildrenComponent(null);
}, []);
useEffect(() => {
if (!executed.current) {
sideBarsStore.rightVisible = true
sideBarsStore.setRightChildren(
<RecordingsFiltersRightSide />
)
if (paramHostId) recStore.hostIdParam = paramHostId
if (paramCameraId) recStore.cameraIdParam = paramCameraId
if (paramStartDay && paramEndDay) {
@ -55,10 +61,6 @@ const RecordingsPage = () => {
}
executed.current = true
if (!isProduction) console.log('RecordingsPage rendered first time')
return () => {
sideBarsStore.setRightChildren(null)
sideBarsStore.rightVisible = false
}
}
}, [])

View File

@ -1,11 +1,10 @@
import { Flex, Button, Text } from '@mantine/core';
import React, { useContext, useEffect, useRef } from 'react';
import { routesPath } from '../router/routes.path';
import { useNavigate } from 'react-router-dom';
import { ExclamationCogWheel } from '../shared/components/svg/ExclamationCogWheel';
import { Context } from '..';
import { Button, Flex, Text } from '@mantine/core';
import { observer } from 'mobx-react-lite';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { routesPath } from '../router/routes.path';
import { ExclamationCogWheel } from '../shared/components/svg/ExclamationCogWheel';
interface RetryErrorPageProps {
repeatVisible?: boolean
@ -21,20 +20,9 @@ const RetryErrorPage = ({
onRetry
}: RetryErrorPageProps) => {
const { t } = useTranslation()
const executed = useRef(false)
const navigate = useNavigate()
const { sideBarsStore } = useContext(Context)
useEffect(() => {
if (!executed.current) {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
executed.current = true
}
}, [sideBarsStore])
const handleGoToMain = () => {
navigate(routesPath.MAIN_PATH)
}

View File

@ -2,9 +2,8 @@ import { Flex, Space } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { useMutation } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';
import { useContext, useEffect, useRef, useState } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Context } from '..';
import { useAdminRole } from '../hooks/useAdminRole';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import { GetRole } from '../services/frigate.proxy/frigate.schema';
@ -16,21 +15,10 @@ import Forbidden from './403';
const SettingsPage = () => {
const { t } = useTranslation()
const executed = useRef(false)
const [showRoles, setShowRoles] = useState<boolean>(false)
const [allRoles, setAllRoles] = useState<GetRole[]>()
const { sideBarsStore } = useContext(Context)
useEffect(() => {
if (!executed.current) {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
executed.current = true
}
}, [sideBarsStore])
const { isAdmin, isLoading: adminLoading } = useAdminRole()
const isMobile = useMediaQuery(dimensions.mobileSize)

View File

@ -3,29 +3,13 @@ import { Editor, Monaco } from '@monaco-editor/react';
import { observer } from 'mobx-react-lite';
import * as monaco from 'monaco-editor';
import { SchemasSettings, configureMonacoYaml } from 'monaco-yaml';
import { useContext, useEffect, useRef } from 'react';
import { Context } from '..';
import { useRef } from 'react';
import HeadSearch from '../shared/components/inputs/HeadSearch';
const Test = () => {
const executed = useRef(false)
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
const theme = useMantineTheme();
const { sideBarsStore } = useContext(Context)
sideBarsStore.rightVisible = true
useEffect(() => {
if (!executed.current) {
sideBarsStore.rightVisible = false
sideBarsStore.setLeftChildren(null)
sideBarsStore.setRightChildren(null)
executed.current = true
}
}, [sideBarsStore])
const value = `
# Property descriptions are displayed when hovering over properties using your cursor

View File

@ -21,10 +21,6 @@ interface IRoute {
}
export const routes: IRoute[] = [
{ //todo delete
path: routesPath.TEST_PATH,
component: <Test />,
},
{
path: routesPath.SETTINGS_PATH,
component: <SettingsPage />,

View File

@ -15,6 +15,7 @@ import { FrigateStats, GetFfprobe, GetHostStorage, GetVaInfo } from "../../types
import { PostSaveConfig, SaveOption } from "../../types/saveConfig";
import keycloak from "../keycloak-config";
import { PutMask } from "../../types/mask";
import { GetUserTag, PutUserTag } from "../../types/tags";
const instanceApi = axios.create({
baseURL: proxyURL.toString(),
@ -61,6 +62,9 @@ export const frigateApi = {
cameraIDs: cameraIDs
}).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),
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)
}
export const proxyPrefix = `${proxyURL.protocol}//${proxyURL.host}/proxy/`
@ -242,4 +246,6 @@ export const frigateQueryKeys = {
getRoleWCameras: 'roles-cameras',
getUsersByRole: 'users-role',
getAdminRole: 'admin-role',
getUserTags: 'users-tags',
putUserTag: 'put-user-tag',
}

View File

@ -0,0 +1,62 @@
import { Aside, Button, createStyles } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { t } from 'i18next';
import React, { useContext } from 'react';
import { SideBarContext } from '../../widgets/sidebars/SideBarContext';
import { dimensions } from '../dimensions/dimensions';
import { useMantineSize } from '../utils/mantine.size.convertor';
import { SideButton } from './SideButton';
interface RightSideBarProps {
onChangeHidden?: (isHidden: boolean) => void,
children?: React.ReactNode,
}
const useStyles = createStyles((theme,
{ visible }: { visible: boolean }) => ({
navbar: {
transition: 'transform 0.3s ease-in-out',
transform: visible ? 'translateX(0)' : 'translateX(-100%)',
},
aside: {
transition: 'transform 0.3s ease-in-out',
transform: visible ? 'translateX(0)' : 'translateX(+100%)',
},
}))
const RightSideBar = ({ onChangeHidden, children }: RightSideBarProps) => {
const side = 'right'
const hideSizePx = useMantineSize(dimensions.hideSidebarsSize)
const [visible, { open, close }] = useDisclosure(true);
const { classes } = useStyles({ visible })
const { childrenComponent } = useContext(SideBarContext);
const handleClickVisible = (state: boolean) => {
if (state) open()
else close()
if (onChangeHidden) onChangeHidden(state)
}
return (<div>
<Aside
className={classes.aside}
p={dimensions.hideSidebarsSize}
width={{ sm: 200, lg: 300 }}>
<Button onClick={() => handleClickVisible(false)}>{t('hide')}</Button>
{childrenComponent}
</Aside>
<SideButton side={side} hide={visible} onClick={() => handleClickVisible(true)} />
</div>
);
};
export default RightSideBar;

View File

@ -8,111 +8,113 @@ import { dimensions } from '../dimensions/dimensions';
import { useMantineSize } from '../utils/mantine.size.convertor';
import { SideButton } from './SideButton';
export interface SideBarProps {
isHidden: (isHidden: boolean) => void,
side: 'left' | 'right',
children?: React.ReactNode,
}
// export interface SideBarProps {
// onChangeHidden: (isHidden: boolean) => void,
// side: 'left' | 'right',
// children?: React.ReactNode,
// }
const useStyles = createStyles((theme,
{ visible }: { visible: boolean }) => ({
navbar: {
transition: 'transform 0.3s ease-in-out',
transform: visible ? 'translateX(0)' : 'translateX(-100%)',
},
// const useStyles = createStyles((theme,
// { visible }: { visible: boolean }) => ({
// navbar: {
// transition: 'transform 0.3s ease-in-out',
// transform: visible ? 'translateX(0)' : 'translateX(-100%)',
// },
aside: {
transition: 'transform 0.3s ease-in-out',
transform: visible ? 'translateX(0)' : 'translateX(+100%)',
},
}))
// aside: {
// transition: 'transform 0.3s ease-in-out',
// transform: visible ? 'translateX(0)' : 'translateX(+100%)',
// },
// }))
const SideBar = ({ isHidden, side, children }: SideBarProps) => {
const { t } = useTranslation()
const hideSizePx = useMantineSize(dimensions.hideSidebarsSize)
const initialVisible = () => {
const savedVisibility = localStorage.getItem(`sidebarVisible_${side}`);
if (savedVisibility === null) {
return window.innerWidth < hideSizePx;
}
return savedVisibility === 'true';
}
const [visible, { open, close }] = useDisclosure(initialVisible());
// const SideBar = ({ onChangeHidden: isHidden, side, children }: SideBarProps) => {
// const { t } = useTranslation()
// const hideSizePx = useMantineSize(dimensions.hideSidebarsSize)
// const initialVisible = () => {
// const savedVisibility = localStorage.getItem(`sidebarVisible_${side}`);
// if (savedVisibility === null) {
// return window.innerWidth < hideSizePx;
// }
// return savedVisibility === 'true';
// }
// const [visible, { open, close }] = useDisclosure(initialVisible());
const { classes } = useStyles({ visible })
// const { classes } = useStyles({ visible })
const handleClickVisible = (state: boolean) => {
localStorage.setItem(`sidebarVisible_${side}`, String(state))
if (state) open()
else close()
}
// const handleClickVisible = (state: boolean) => {
// localStorage.setItem(`sidebarVisible_${side}`, String(state))
// if (state) open()
// else close()
// }
const { sideBarsStore } = useContext(Context)
useEffect(() => {
if (sideBarsStore.rightVisible && side === 'right' && !visible) {
open()
} else if (!sideBarsStore.rightVisible && side === 'right' && visible) {
close()
}
}, [sideBarsStore.rightVisible])
// const { sideBarsStore } = useContext(Context)
// useEffect(() => {
// if (sideBarsStore.rightVisible && side === 'right' && !visible) {
// open()
// } else if (!sideBarsStore.rightVisible && side === 'right' && visible) {
// close()
// }
// }, [sideBarsStore.rightVisible])
const [leftChildren, setLeftChildren] = useState<React.ReactNode>(() => {
if (children && side === 'left') return children
else if (sideBarsStore.leftChildren) return sideBarsStore.leftChildren
return null
})
const [rightChildren, setRightChildren] = useState<React.ReactNode>(() => {
if (children && side === 'right') return children
else if (sideBarsStore.rightChildren) return sideBarsStore.rightChildren
return null
})
// const [leftChildren, setLeftChildren] = useState<React.ReactNode>(() => {
// if (children && side === 'left') return children
// else if (sideBarsStore.leftChildren) return sideBarsStore.leftChildren
// return null
// })
// const [rightChildren, setRightChildren] = useState<React.ReactNode>(() => {
// if (children && side === 'right') return children
// else if (sideBarsStore.rightChildren) return sideBarsStore.rightChildren
// return null
// })
useEffect(() => {
setLeftChildren(sideBarsStore.leftChildren)
}, [sideBarsStore.leftChildren])
// useEffect(() => {
// setLeftChildren(sideBarsStore.leftChildren)
// }, [sideBarsStore.leftChildren])
useEffect(() => {
setRightChildren(sideBarsStore.rightChildren)
}, [sideBarsStore.rightChildren])
// useEffect(() => {
// setRightChildren(sideBarsStore.rightChildren)
// }, [sideBarsStore.rightChildren])
useEffect(() => {
isHidden(!visible)
}, [visible])
// useEffect(() => {
// isHidden(!visible)
// }, [visible])
useEffect(() => {
const savedVisibility = localStorage.getItem(`sidebarVisible_${side}`);
if (savedVisibility === null && window.innerWidth < hideSizePx) {
open()
} else if (savedVisibility) {
savedVisibility === 'true' ? open() : close()
}
}, [])
// useEffect(() => {
// const savedVisibility = localStorage.getItem(`sidebarVisible_${side}`);
// if (savedVisibility === null && window.innerWidth < hideSizePx) {
// open()
// } else if (savedVisibility) {
// savedVisibility === 'true' ? open() : close()
// }
// }, [])
return (
<div>
{
side === 'left' ?
<Navbar
className={classes.navbar}
p={dimensions.hideSidebarsSize}
width={{ sm: 200, lg: 300, }}
>
<Button onClick={() => handleClickVisible(false)}>{t('hide')}</Button>
{leftChildren}
</Navbar>
:
<Aside
className={classes.aside}
p={dimensions.hideSidebarsSize}
width={{ sm: 200, lg: 300 }}>
<Button onClick={() => handleClickVisible(false)}>{t('hide')}</Button>
{rightChildren}
</Aside>
}
<SideButton side={side} hide={visible} onClick={() => handleClickVisible(true)} />
</div>
)
}
// return (
// <div>
// {
// side === 'left' ?
// <Navbar
// className={classes.navbar}
// p={dimensions.hideSidebarsSize}
// width={{ sm: 200, lg: 300, }}
// >
// <Button onClick={() => handleClickVisible(false)}>{t('hide')}</Button>
// {leftChildren}
// </Navbar>
// :
// <Aside
// className={classes.aside}
// p={dimensions.hideSidebarsSize}
// width={{ sm: 200, lg: 300 }}>
// <Button onClick={() => handleClickVisible(false)}>{t('hide')}</Button>
// {rightChildren}
// </Aside>
// }
// <SideButton side={side} hide={visible} onClick={() => handleClickVisible(true)} />
// </div>
// )
// }
export default observer(SideBar)
export const toDoDelete = () => ( <></> )
// export default observer(SideBar)

View File

@ -1,53 +1,78 @@
import { Box, Flex, MultiSelect, MultiSelectProps, SelectItem, SpacingValue, SystemProp, Text } from '@mantine/core';
import { t } from 'i18next';
import React, { CSSProperties, FC } from 'react';
import CloseWithTooltip from '../buttons/CloseWithTooltip';
import { Box, Flex, Group, MultiSelect, MultiSelectProps, SelectItem, SpacingValue, SystemProp, Text } from '@mantine/core';
import React, { CSSProperties, forwardRef } from 'react';
import { useTranslation } from 'react-i18next';
import CloseWithTooltip from '../buttons/CloseWithTooltip';
import { IconTrash } from '@tabler/icons-react';
interface CreatableMultiSelectProps {
id?: string
value?: string[]
data: SelectItem[]
spaceBetween?: SystemProp<SpacingValue>
label?: string
defaultValue?: string[]
textClassName?: string
selectProps?: MultiSelectProps,
display?: SystemProp<CSSProperties['display']>
showClose?: boolean,
changedState?(value: string[], id?: string): void
onChange?(value: string[]): void
onClose?(): void
onCreate?(value: string): void
onCreate?(query: string | SelectItem | null | undefined): SelectItem | string | null | undefined
error?: string
onTrashClick?(value: string): void
}
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
label: string,
value: string,
onTrashClick?: (value: string) => void
}
const DeletableItem = forwardRef<HTMLDivElement, ItemProps>(
({ label, value, onTrashClick, ...others }, ref) => (
<div {...others} ref={ref}>
<Flex justify='space-between'>
<Text>{label}</Text>
<IconTrash
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
if (onTrashClick) onTrashClick(value);
}}
/>
</Flex>
</div>
)
);
const CreatableMultiSelect: React.FC<CreatableMultiSelectProps> = ({
id,
value,
data,
spaceBetween,
label,
defaultValue,
textClassName,
selectProps,
display,
showClose,
changedState,
onChange,
onClose,
onCreate
onCreate,
onTrashClick,
error,
}) => {
const { t } = useTranslation()
const handleOnChange = (value: string[]) => {
if (changedState) {
changedState(value, id)
}
if (onChange) onChange(value)
}
const handleOnCreate = (query: string | SelectItem | null | undefined) => {
const item = { value: query, label: query } as SelectItem
if (onCreate && typeof query === 'string') {
onCreate(query)
if (onCreate) return onCreate(query)
}
return item
const handleTrashClick = (value: string) => {
if (onTrashClick) onTrashClick(value)
}
return (
@ -59,17 +84,25 @@ const CreatableMultiSelect: React.FC<CreatableMultiSelectProps> = ({
: null}
</Flex>
<MultiSelect
{...selectProps}
value={value}
mt={spaceBetween}
data={data}
disableSelectedItemFiltering
defaultValue={defaultValue}
itemComponent={forwardRef((itemProps, ref) => (
<DeletableItem
{...itemProps}
ref={ref} // передаем ref в DeletableItem
onTrashClick={handleTrashClick} // передаем коллбэк сюда напрямую
/>
))}
onChange={handleOnChange}
searchable
clearable
creatable
getCreateLabel={(query) => `+ ${t('create') + ' ' + query}`}
onCreate={handleOnCreate}
{...selectProps}
error={error}
/>
</Box>
);

View File

@ -3,7 +3,7 @@ import { CSSProperties } from 'react';
import { useTranslation } from 'react-i18next';
import CloseWithTooltip from '../buttons/CloseWithTooltip';
interface MultiSelectFilterProps {
interface MultiSelectFilterProps extends MultiSelectProps {
id?: string
data: SelectItem[]
spaceBetween?: SystemProp<SpacingValue>

View File

@ -0,0 +1,157 @@
import { SelectItem } from '@mantine/core';
import { t } from 'i18next';
import { useState } from 'react';
import { z } from 'zod';
import CreatableMultiSelect from './CreatableMultiSelect';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api';
import RetryError from '../RetryError';
import CogwheelLoader from '../loaders/CogwheelLoader';
import { mapUserTagsToSelectItems, PutUserTag } from '../../../types/tags';
import { notifications } from '@mantine/notifications';
import { IconAlertCircle } from '@tabler/icons-react';
const UserTagsFilter = () => {
const { t } = useTranslation()
const queryClient = useQueryClient()
const [selectedList, setSelectedList] = useState<string[]>([])
const { data, isPending, isError, refetch } = useQuery({
queryKey: [frigateQueryKeys.getUserTags],
queryFn: frigateApi.getUserTags
})
const SelectItemSchema = z.object({
value: z.string(),
label: z.string().optional(),
selected: z.boolean().optional(),
disabled: z.boolean().optional(),
group: z.string().optional(),
}).passthrough();
const [tagsError, setTagsError] = useState<string>('')
const validateNewTag = (query: string): boolean => {
if (query.length > 10) {
setTagsError(t('mainPage.tagsError'))
return false
}
return true
}
const { mutate } = useMutation({
mutationFn: (newTag: PutUserTag) => frigateApi.putUserTag(newTag)
.catch(error => {
if (error.response && error.response.data) {
return Promise.reject(error.response.data)
}
return Promise.reject(error)
}),
onSuccess: (data) => {
setSelectedList([...selectedList, data.id])
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getUserTags] })
},
onError: (e) => {
if (e && e.message) {
notifications.show({
id: e.message,
withCloseButton: true,
autoClose: 5000,
title: "Error",
message: e.message,
color: 'red',
icon: <IconAlertCircle />,
})
}
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getFrigateHosts] })
}
})
const { mutate: deleteTag } = useMutation({
mutationFn: (tagId: string) => frigateApi.delUserTag(tagId)
.catch(error => {
if (error.response && error.response.data) {
return Promise.reject(error.response.data)
}
return Promise.reject(error)
}),
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getUserTags] })
const updatedList = selectedList.filter(item => data.id === item)
setSelectedList(updatedList)
},
onError: (e) => {
if (e && e.message) {
notifications.show({
id: e.message,
withCloseButton: true,
autoClose: 5000,
title: "Error",
message: e.message,
color: 'red',
icon: <IconAlertCircle />,
})
}
queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getFrigateHosts] })
}
})
const saveNewTag = (value: string) => {
const newTag: PutUserTag = {
value,
cameraIds: []
}
mutate(newTag)
}
const onCreate = (query: string | SelectItem | null | undefined) => {
setTagsError('')
const parseQuery = SelectItemSchema.safeParse(query)
if (typeof query === 'string') {
if (!validateNewTag(query)) return undefined
saveNewTag(query)
// return query
}
else if (parseQuery.success) {
const parsedQuery = parseQuery.data as SelectItem
if (!validateNewTag(parsedQuery.value)) return undefined
saveNewTag(parsedQuery.value)
// return parsedQuery
}
}
if (isPending) return <CogwheelLoader />
if (isError) return <RetryError onRetry={refetch} />
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])
}
const handleTrashClick = (value: string) => {
if (value) {
deleteTag(value)
}
}
return (
<CreatableMultiSelect
value={selectedList}
label={t('mainPage.createSelectTags')}
onChange={handleOnChange}
spaceBetween='1rem'
data={mapUserTagsToSelectItems(data)}
onCreate={onCreate}
error={tagsError}
onTrashClick={handleTrashClick}
/>
);
};
export default UserTagsFilter;

View File

@ -1,20 +1,17 @@
import { MainStore } from "./main.store";
import { ModalStore } from "./modal.store";
import { RecordingsStore } from "./recordings.store";
import { SideBarsStore } from "./sidebars.store";
import { UserStore } from "./user.store";
class RootStore {
mainStore: MainStore
userStore: UserStore
modalStore: ModalStore
sideBarsStore: SideBarsStore
recordingsStore: RecordingsStore
constructor() {
this.mainStore = new MainStore()
this.userStore = new UserStore()
this.modalStore = new ModalStore(this)
this.sideBarsStore = new SideBarsStore()
this.recordingsStore = new RecordingsStore()
}
}

View File

@ -1,45 +1,47 @@
import { action, makeAutoObservable } from "mobx"
// TODO Delete
export class SideBarsStore {
private _rightVisible: boolean = true
public get rightVisible(): boolean {
return this._rightVisible
}
public set rightVisible(visible: boolean) {
this._rightVisible = visible
}
private _leftVisible: boolean = true
public get leftVisible(): boolean {
return this._leftVisible
}
public set leftVisible(visible: boolean) {
this._leftVisible = visible
}
// private _rightVisible: boolean = true
// public get rightVisible(): boolean {
// return this._rightVisible
// }
// public set rightVisible(visible: boolean) {
// this._rightVisible = visible
// }
// private _leftVisible: boolean = true
// public get leftVisible(): boolean {
// return this._leftVisible
// }
// public set leftVisible(visible: boolean) {
// this._leftVisible = visible
// }
private _leftChildren: React.ReactNode = null
public get leftChildren(): React.ReactNode {
return this._leftChildren
}
// private _leftChildren: React.ReactNode = null
// public get leftChildren(): React.ReactNode {
// return this._leftChildren
// }
private _rightChildren: React.ReactNode = null
public get rightChildren(): React.ReactNode {
return this._rightChildren
}
// private _rightChildren: React.ReactNode = null
// public get rightChildren(): React.ReactNode {
// return this._rightChildren
// }
constructor () {
makeAutoObservable(this, {
setRightChildren: action,
setLeftChildren: action,
})
}
// constructor () {
// makeAutoObservable(this, {
// setRightChildren: action,
// setLeftChildren: action,
// })
// }
setRightChildren = (value: React.ReactNode) => {
this._rightChildren = value
}
// setRightChildren = (value: React.ReactNode) => {
// this._rightChildren = value
// }
setLeftChildren = (value: React.ReactNode) => {
this._leftChildren = value
}
// setLeftChildren = (value: React.ReactNode) => {
// this._leftChildren = value
// }
}

26
src/types/tags.ts Normal file
View File

@ -0,0 +1,26 @@
import { SelectItem } from "@mantine/core";
import { z } from "zod";
export const putUserTag = z.object({
value: z.string(),
cameraIds: z.string().array()
})
export const getUserTag = z.object({
id: z.string(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
value: z.string(),
userId: z.string(),
cameraIds: z.string().array(),
})
export type GetUserTag = z.infer<typeof getUserTag>
export type PutUserTag = z.infer<typeof putUserTag>
export const mapUserTagsToSelectItems = (tags: GetUserTag[]): SelectItem[] => {
return tags.map(tag => ({
value: tag.id,
label: tag.value
}))
}

View File

@ -3,10 +3,13 @@ import { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { Context } from '../..';
import HostSelect from '../../shared/components/filters/HostSelect';
import UserTagsFilter from '../../shared/components/filters/UserTagsFilter';
import { isProduction } from '../../shared/env.const';
const MainFiltersRightSide = () => {
const { t } = useTranslation()
@ -26,16 +29,11 @@ const MainFiltersRightSide = () => {
defaultId={selectedHostId}
onChange={handleSelect}
/>
{/* TODO Add tags select */}
{/* <CreatableMultiSelect
label={t('mainPage.createSelectTags')}
spaceBetween='1rem'
data={[]}
/> */}
<UserTagsFilter />
</>
);
};
export default observer(MainFiltersRightSide);

View File

@ -0,0 +1,21 @@
import React, { createContext, ReactNode, useState } from 'react';
interface SideBarContextProps {
childrenComponent: ReactNode;
setChildrenComponent: (component: ReactNode) => void;
}
export const SideBarContext = createContext<SideBarContextProps>({
childrenComponent: null,
setChildrenComponent: () => {},
});
export const SideBarProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [childrenComponent, setChildrenComponent] = useState<ReactNode>(null);
return (
<SideBarContext.Provider value={{ childrenComponent, setChildrenComponent }}>
{children}
</SideBarContext.Provider>
);
};