refactoring

add host filter to main page
add timer to loader
decompose host filter
filter admin menu from drawer
fix cameratransferlist buttons
This commit is contained in:
NlightN22 2024-03-07 00:55:34 +07:00
parent 5629db199c
commit acaf99c878
21 changed files with 209 additions and 121 deletions

View File

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Build commands: # Build commands:
# - $VERSION=0.5 # - $VERSION=0.6
# - rm build -r -Force ; rm ./node_modules/.cache/babel-loader -r -Force ; yarn build # - rm build -r -Force ; rm ./node_modules/.cache/babel-loader -r -Force ; yarn build
# - docker build --pull --rm -t oncharterliz/multi-frigate:latest -t oncharterliz/multi-frigate:$VERSION "." # - docker build --pull --rm -t oncharterliz/multi-frigate:latest -t oncharterliz/multi-frigate:$VERSION "."
# - docker image push --all-tags oncharterliz/multi-frigate # - docker image push --all-tags oncharterliz/multi-frigate

View File

@ -20,19 +20,12 @@ export const keycloakConfig: AuthProviderProps = {
onSigninCallback: () => { onSigninCallback: () => {
const currentUrl = new URL(window.location.href); const currentUrl = new URL(window.location.href);
const params = currentUrl.searchParams; const params = currentUrl.searchParams;
console.log('params', params.toString())
params.delete('state'); params.delete('state');
params.delete('session_state'); params.delete('session_state');
params.delete('code'); params.delete('code');
const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}` const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`
console.log('newUrl', newUrl)
window.history.replaceState({}, document.title, newUrl) window.history.replaceState({}, document.title, newUrl)
} }
} }
const rootStore = new RootStore() const rootStore = new RootStore()

View File

@ -4,7 +4,7 @@ import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.
import CenterLoader from '../shared/components/loaders/CenterLoader'; import CenterLoader from '../shared/components/loaders/CenterLoader';
import RetryErrorPage from './RetryErrorPage'; import RetryErrorPage from './RetryErrorPage';
import { Flex, Group, Select, Text } from '@mantine/core'; import { Flex, Group, Select, Text } from '@mantine/core';
import { OneSelectItem } from '../shared/components/filters.aps/OneSelectFilter'; import { OneSelectItem } from '../shared/components/filters/OneSelectFilter';
import { useMediaQuery } from '@mantine/hooks'; import { useMediaQuery } from '@mantine/hooks';
import { dimensions } from '../shared/dimensions/dimensions'; import { dimensions } from '../shared/dimensions/dimensions';
import CamerasTransferList from '../shared/components/CamerasTransferList'; import CamerasTransferList from '../shared/components/CamerasTransferList';

View File

@ -6,7 +6,9 @@ import { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { Context } from '..'; import { Context } from '..';
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api'; import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema'; import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
import HostSelect from '../shared/components/filters/HostSelect';
import CenterLoader from '../shared/components/loaders/CenterLoader'; import CenterLoader from '../shared/components/loaders/CenterLoader';
import { strings } from '../shared/strings/strings';
import CameraCard from '../widgets/CameraCard'; import CameraCard from '../widgets/CameraCard';
import RetryErrorPage from './RetryErrorPage'; import RetryErrorPage from './RetryErrorPage';
@ -14,6 +16,7 @@ const MainPage = () => {
const executed = useRef(false) const executed = useRef(false)
const { sideBarsStore } = useContext(Context) const { sideBarsStore } = useContext(Context)
const [searchQuery, setSearchQuery] = useState<string>() const [searchQuery, setSearchQuery] = useState<string>()
const [selectedHostId, setSelectedHostId] = useState<string>()
const [filteredCameras, setFilteredCameras] = useState<GetCameraWHostWConfig[]>() const [filteredCameras, setFilteredCameras] = useState<GetCameraWHostWConfig[]>()
const { data: cameras, isPending, isError, refetch } = useQuery({ const { data: cameras, isPending, isError, refetch } = useQuery({
@ -22,12 +25,19 @@ const MainPage = () => {
}) })
useEffect(() => { useEffect(() => {
if (searchQuery && cameras) { if (!cameras) {
setFilteredCameras(cameras.filter(camera => camera.name.toLowerCase().includes(searchQuery.toLowerCase())))
} else {
setFilteredCameras(undefined) setFilteredCameras(undefined)
return
} }
}, [searchQuery, cameras])
const filterCameras = (camera: GetCameraWHostWConfig) => {
const matchesHostId = selectedHostId ? camera.frigateHost?.id === selectedHostId : true
const matchesSearchQuery = searchQuery ? camera.name.toLowerCase().includes(searchQuery.toLowerCase()) : true
return matchesHostId && matchesSearchQuery
}
setFilteredCameras(cameras.filter(filterCameras))
}, [searchQuery, cameras, selectedHostId])
useEffect(() => { useEffect(() => {
if (!executed.current) { if (!executed.current) {
@ -66,6 +76,10 @@ const MainPage = () => {
if (isError) return <RetryErrorPage onRetry={refetch} /> if (isError) return <RetryErrorPage onRetry={refetch} />
const handleSelectHost = (hostId: string) => {
setSelectedHostId(hostId)
}
return ( return (
<Flex direction='column' h='100%' w='100%' > <Flex direction='column' h='100%' w='100%' >
<Flex justify='space-between' align='center' w='100%'> <Flex justify='space-between' align='center' w='100%'>
@ -77,11 +91,18 @@ const MainPage = () => {
<TextInput <TextInput
maw={400} maw={400}
style={{ flexGrow: 1 }} style={{ flexGrow: 1 }}
placeholder="Search..." placeholder={strings.search}
icon={<IconSearch size="0.9rem" stroke={1.5} />} icon={<IconSearch size="0.9rem" stroke={1.5} />}
value={searchQuery} value={searchQuery}
onChange={(event) => setSearchQuery(event.currentTarget.value)} onChange={(event) => setSearchQuery(event.currentTarget.value)}
/> />
<HostSelect
valueId={selectedHostId}
onChange={handleSelectHost}
ml='1rem'
spaceBetween='0px'
placeholder={strings.selectHost}
/>
</Flex> </Flex>
</Flex> </Flex>
<Flex justify='center' h='100%' direction='column' w='100%' > <Flex justify='center' h='100%' direction='column' w='100%' >

View File

@ -100,9 +100,9 @@ const SettingsPage = () => {
mutation.mutate(configsToUpdate); mutation.mutate(configsToUpdate);
} }
if (!isAdmin) return <Forbidden />
if (configPending || adminLoading) return <CenterLoader /> if (configPending || adminLoading) return <CenterLoader />
if (configError) return <RetryErrorPage onRetry={refetch} /> if (configError) return <RetryErrorPage onRetry={refetch} />
if (!isAdmin) return <Forbidden />
return ( return (
<Flex h='100%'> <Flex h='100%'>

View File

@ -12,7 +12,7 @@ import HostSystemPage from "../pages/HostSystemPage";
import HostStoragePage from "../pages/HostStoragePage"; import HostStoragePage from "../pages/HostStoragePage";
import LiveCameraPage from "../pages/LiveCameraPage"; import LiveCameraPage from "../pages/LiveCameraPage";
import RecordingsPage from "../pages/RecordingsPage"; import RecordingsPage from "../pages/RecordingsPage";
import AccessSettings from "../pages/AccessSettings"; import AccessSettings from "../pages/AccessSettingsPage";
import PlayRecordPage from "../pages/PlayRecordPage"; import PlayRecordPage from "../pages/PlayRecordPage";
interface IRoute { interface IRoute {

View File

@ -4,7 +4,7 @@ import { frigateApi, frigateQueryKeys } from '../../services/frigate.proxy/friga
import CogwheelLoader from './loaders/CogwheelLoader'; import CogwheelLoader from './loaders/CogwheelLoader';
import RetryError from './RetryError'; import RetryError from './RetryError';
import { TransferList, Text, TransferListData, TransferListProps, TransferListItem, Button, Flex } from '@mantine/core'; import { TransferList, Text, TransferListData, TransferListProps, TransferListItem, Button, Flex } from '@mantine/core';
import { OneSelectItem } from './filters.aps/OneSelectFilter'; import { OneSelectItem } from './filters/OneSelectFilter';
import { strings } from '../strings/strings'; import { strings } from '../strings/strings';
import { isProduction } from '../env.const'; import { isProduction } from '../env.const';
@ -65,8 +65,8 @@ const CamerasTransferList = ({
return ( return (
<> <>
<Flex w='100%' justify='center'> <Flex w='100%' justify='center'>
<Button mt='1rem' w='10%' miw='6rem' mr='1rem' onClick={handleDiscard}>{strings.discard}</Button> <Button mt='1rem' miw='6rem' mr='1rem' onClick={handleDiscard}>{strings.discard}</Button>
<Button mt='1rem' w='10%' miw='5rem' onClick={handleSave}>{strings.save}</Button> <Button mt='1rem' miw='5rem' onClick={handleSave}>{strings.save}</Button>
</Flex> </Flex>
<TransferList <TransferList
transferAllMatchingFilter transferAllMatchingFilter

View File

@ -30,13 +30,19 @@ const useStyles = createStyles((theme,
const SideBar = ({ isHidden, side, children }: SideBarProps) => { const SideBar = ({ isHidden, side, children }: SideBarProps) => {
const hideSizePx = useMantineSize(dimensions.hideSidebarsSize) const hideSizePx = useMantineSize(dimensions.hideSidebarsSize)
const [visible, { open, close }] = useDisclosure(window.innerWidth > hideSizePx); const initialVisible = () => {
const manualVisible: React.MutableRefObject<null | boolean> = useRef(null) 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) => { const handleClickVisible = (state: boolean) => {
manualVisible.current = state localStorage.setItem(`sidebarVisible_${side}`, String(state))
if (state) open() if (state) open()
else close() else close()
} }
@ -73,23 +79,14 @@ const SideBar = ({ isHidden, side, children }: SideBarProps) => {
isHidden(!visible) isHidden(!visible)
}, [visible]) }, [visible])
// resize controller
useEffect(() => { useEffect(() => {
const checkWindowSize = () => { const savedVisibility = localStorage.getItem(`sidebarVisible_${side}`);
if (window.innerWidth <= hideSizePx && visible) { if (savedVisibility === null && window.innerWidth < hideSizePx) {
close() open()
} } else if (savedVisibility) {
if (window.innerWidth > hideSizePx && !visible && manualVisible.current === null) { savedVisibility === 'true' ? open() : close()
open()
}
} }
window.addEventListener('resize', checkWindowSize); }, [])
// Cleanup function to remove event listener
return () => {
window.removeEventListener('resize', checkWindowSize);
}
}, [visible])
return ( return (
<div> <div>
@ -112,7 +109,6 @@ const SideBar = ({ isHidden, side, children }: SideBarProps) => {
{rightChildren} {rightChildren}
</Aside> </Aside>
} }
<SideButton side={side} hide={visible} onClick={() => handleClickVisible(true)} /> <SideButton side={side} hide={visible} onClick={() => handleClickVisible(true)} />
</div> </div>
) )

View File

@ -1,63 +0,0 @@
import { Center, Text } from '@mantine/core';
import { useQuery } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';
import React, { useContext, useEffect } from 'react';
import { Context } from '../../..';
import { frigateQueryKeys, frigateApi } from '../../../services/frigate.proxy/frigate.api';
import { strings } from '../../strings/strings';
import CogwheelLoader from '../loaders/CogwheelLoader';
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
import RetryError from '../RetryError';
const HostSelectFilter = () => {
const { recordingsStore: recStore } = useContext(Context)
const { data: hosts, isError, isPending, isSuccess, refetch } = useQuery({
queryKey: [frigateQueryKeys.getFrigateHosts],
queryFn: frigateApi.getHosts
})
useEffect(() => {
if (!hosts) return
if (recStore.hostIdParam) {
recStore.filteredHost = hosts.find(host => host.id === recStore.hostIdParam)
recStore.hostIdParam = undefined
}
}, [isSuccess])
if (isPending) return <CogwheelLoader />
if (isError) return <RetryError onRetry={refetch}/>
if (!hosts || hosts.length < 1) return null
const hostItems: OneSelectItem[] = hosts
.filter(host => host.enabled)
.map(host => ({ value: host.id, label: host.name }))
const handleSelect = (value: string) => {
const host = hosts?.find(host => host.id === value)
if (!host) {
recStore.filteredHost = undefined
recStore.filteredCamera = undefined
return
}
if (recStore.filteredHost?.id !== host.id) {
recStore.filteredCamera = undefined
}
recStore.filteredHost = host
}
return (
<OneSelectFilter
id='frigate-hosts'
label={strings.selectHost}
spaceBetween='1rem'
value={recStore.filteredHost?.id || ''}
defaultValue={recStore.filteredHost?.id || ''}
data={hostItems}
onChange={handleSelect}
/>
);
};
export default observer(HostSelectFilter);

View File

@ -1,11 +1,10 @@
import { Box, Flex, Indicator, Text } from '@mantine/core';
import { DatePickerInput } from '@mantine/dates'; import { DatePickerInput } from '@mantine/dates';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import React, { useContext } from 'react'; import { useContext } from 'react';
import { strings } from '../../strings/strings';
import { Box, Flex, Indicator, Text } from '@mantine/core';
import CloseWithTooltip from '../buttons/CloseWithTooltip';
import { Context } from '../../..'; import { Context } from '../../..';
import { isProduction } from '../../env.const'; import { isProduction } from '../../env.const';
import { strings } from '../../strings/strings';
interface DateRangeSelectFilterProps {} interface DateRangeSelectFilterProps {}

View File

@ -0,0 +1,67 @@
import { useQuery } from '@tanstack/react-query';
import React, { useEffect } from 'react';
import { frigateQueryKeys, frigateApi } from '../../../services/frigate.proxy/frigate.api';
import { strings } from '../../strings/strings';
import RetryError from '../RetryError';
import CogwheelLoader from '../loaders/CogwheelLoader';
import OneSelectFilter, { OneSelectItem } from './OneSelectFilter';
import { SystemProp, SpacingValue, MantineStyleSystemProps, Loader, Center } from '@mantine/core';
interface HostSelectProps extends MantineStyleSystemProps {
label?: string
valueId?: string
defaultId?: string
spaceBetween?: SystemProp<SpacingValue>
placeholder?: string
onChange?: (value: string) => void
onSuccess?: () => void
}
const HostSelect = ({
label,
valueId,
defaultId,
spaceBetween,
placeholder,
onChange,
onSuccess,
...styleProps
}: HostSelectProps) => {
const { data: hosts, isError, isPending, isSuccess, refetch } = useQuery({
queryKey: [frigateQueryKeys.getFrigateHosts],
queryFn: frigateApi.getHosts
})
useEffect(() => {
if (onSuccess) onSuccess()
}, [isSuccess])
if (isPending) return <Center><Loader /></Center>
if (isError) return <RetryError onRetry={refetch} />
if (!hosts || hosts.length < 1) return null
const hostItems: OneSelectItem[] = hosts
.filter(host => host.enabled)
.map(host => ({ value: host.id, label: host.name }))
const handleSelect = (value: string) => {
if (onChange) onChange(value)
}
return (
<OneSelectFilter
id='frigate-hosts'
label={label}
placeholder={placeholder}
spaceBetween={spaceBetween ? spaceBetween : '1rem'}
value={valueId || ''}
defaultValue={defaultId || ''}
data={hostItems}
onChange={handleSelect}
{...styleProps}
/>
);
};
export default HostSelect;

View File

@ -11,15 +11,15 @@ export interface OneSelectItem {
disabled?: boolean; disabled?: boolean;
} }
interface OneSelectFilterProps extends SelectProps { export interface OneSelectFilterProps extends SelectProps {
id?: string id?: string
data: OneSelectItem[] data: OneSelectItem[]
spaceBetween?: SystemProp<SpacingValue> spaceBetween?: SystemProp<SpacingValue>
label?: string label?: string
defaultValue?: string defaultValue?: string | null
textClassName?: string textClassName?: string
showClose?: boolean, showClose?: boolean,
value?: string, value?: string | null,
onChange?: (value: string, id?: string,) => void onChange?: (value: string, id?: string,) => void
onClose?: () => void onClose?: () => void
} }
@ -41,11 +41,13 @@ const OneSelectFilter = ({
return ( return (
<Box mt={spaceBetween}> <Box mt={spaceBetween}>
<Flex justify='space-between'> {!label ? null :
<Text className={textClassName}>{label}</Text> <Flex justify='space-between'>
{showClose ? <CloseWithTooltip label={strings.hide} onClose={handleOnClose} /> <Text className={textClassName}>{label}</Text>
: null} {showClose ? <CloseWithTooltip label={strings.hide} onClose={handleOnClose} />
</Flex> : null}
</Flex>
}
<Select <Select
mt={spaceBetween} mt={spaceBetween}
data={data} data={data}

View File

@ -0,0 +1,50 @@
import { useQuery } from '@tanstack/react-query';
import { observer } from 'mobx-react-lite';
import { useContext } from 'react';
import { Context } from '../../..';
import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api';
import { strings } from '../../strings/strings';
import HostSelect from './HostSelect';
const RecordingsHostFilter = () => {
const { recordingsStore: recStore } = useContext(Context)
const { data: hosts } = useQuery({
queryKey: [frigateQueryKeys.getFrigateHosts],
queryFn: frigateApi.getHosts
})
const handleSelect = (value: string) => {
const host = hosts?.find(host => host.id === value)
if (!host) {
recStore.filteredHost = undefined
recStore.filteredCamera = undefined
return
}
if (recStore.filteredHost?.id !== host.id) {
recStore.filteredCamera = undefined
}
recStore.filteredHost = host
}
const handleSuccess = () => {
if (!hosts) return
if (recStore.hostIdParam) {
recStore.filteredHost = hosts.find(host => host.id === recStore.hostIdParam)
recStore.hostIdParam = undefined
}
}
return (
<HostSelect
label={strings.selectHost}
valueId={recStore.filteredHost?.id}
defaultId={recStore.filteredHost?.id}
onChange={handleSelect}
onSuccess={handleSuccess}
/>
);
};
export default observer(RecordingsHostFilter);

View File

@ -1,9 +1,29 @@
import React from 'react'; import { Center } from '@mantine/core';
import { Center, DEFAULT_THEME } from '@mantine/core';
import CogwheelSVG from '../svg/CogwheelSVG'; import CogwheelSVG from '../svg/CogwheelSVG';
import { useState, useEffect } from 'react';
interface CogwheelLoaderProps {
duration?: number;
onTimeout?: () => void;
}
const CogwheelLoader: React.FC<CogwheelLoaderProps> = ({
duration,
onTimeout,
}) => {
const [isTimeout, setIsTimeout] = useState(false)
useEffect(() => {
if (duration && onTimeout) {
const timer = setTimeout(() => {
setIsTimeout(true);
onTimeout();
}, duration);
return () => clearTimeout(timer);
}
}, [duration, onTimeout])
const CogwheelLoader = () => {
return ( return (
<Center> <Center>
{CogwheelSVG} {CogwheelSVG}

View File

@ -3,6 +3,7 @@ import { useDisclosure } from '@mantine/hooks';
import React from 'react'; import React from 'react';
import { LinkItem } from '../../../widgets/header/HeaderAction'; import { LinkItem } from '../../../widgets/header/HeaderAction';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAdminRole } from '../../../hooks/useAdminRole';
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({
burger: { burger: {
@ -38,6 +39,8 @@ const DrawerMenu = ({
links links
}: DrawerMenuProps) => { }: DrawerMenuProps) => {
const navigate = useNavigate() const navigate = useNavigate()
const { isAdmin } = useAdminRole()
const { classes } = useStyles(); const { classes } = useStyles();
const [drawerOpened, { toggle: toggleDrawer, close: closeDrawer }] = useDisclosure(false) const [drawerOpened, { toggle: toggleDrawer, close: closeDrawer }] = useDisclosure(false)
@ -47,7 +50,7 @@ const DrawerMenu = ({
closeDrawer() closeDrawer()
} }
const items = links.map(item => ( const items = links.filter(link => !(link.admin && !isAdmin)).map(item => (
<UnstyledButton <UnstyledButton
className={classes.drawerButton} className={classes.drawerButton}
key={item.link} key={item.link}

View File

@ -1,9 +1,9 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { Context } from '..'; import { Context } from '..';
import CameraSelectFilter from '../shared/components/filters.aps/CameraSelectFilter'; import CameraSelectFilter from '../shared/components/filters/CameraSelectFilter';
import DateRangeSelectFilter from '../shared/components/filters.aps/DateRangeSelectFilter'; import DateRangeSelectFilter from '../shared/components/filters/DateRangeSelectFilter';
import HostSelectFilter from '../shared/components/filters.aps/HostSelectFilter'; import RecordingsHostFilter from '../shared/components/filters/RecordingsHostFilter';
import { isProduction } from '../shared/env.const'; import { isProduction } from '../shared/env.const';
const RecordingsFiltersRightSide = () => { const RecordingsFiltersRightSide = () => {
@ -12,7 +12,7 @@ const RecordingsFiltersRightSide = () => {
if (!isProduction) console.log('RecordingsFiltersRightSide rendered') if (!isProduction) console.log('RecordingsFiltersRightSide rendered')
return ( return (
<> <>
<HostSelectFilter /> <RecordingsHostFilter />
{recStore.filteredHost ? {recStore.filteredHost ?
<CameraSelectFilter <CameraSelectFilter
selectedHostId={recStore.filteredHost.id} /> selectedHostId={recStore.filteredHost.id} />