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:
parent
5629db199c
commit
acaf99c878
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build commands:
|
||||
# - $VERSION=0.5
|
||||
# - $VERSION=0.6
|
||||
# - 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 image push --all-tags oncharterliz/multi-frigate
|
||||
|
||||
@ -20,19 +20,12 @@ export const keycloakConfig: AuthProviderProps = {
|
||||
onSigninCallback: () => {
|
||||
const currentUrl = new URL(window.location.href);
|
||||
const params = currentUrl.searchParams;
|
||||
console.log('params', params.toString())
|
||||
|
||||
params.delete('state');
|
||||
params.delete('session_state');
|
||||
params.delete('code');
|
||||
|
||||
const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`
|
||||
console.log('newUrl', newUrl)
|
||||
|
||||
window.history.replaceState({}, document.title, newUrl)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
const rootStore = new RootStore()
|
||||
|
||||
@ -4,7 +4,7 @@ import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
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 { dimensions } from '../shared/dimensions/dimensions';
|
||||
import CamerasTransferList from '../shared/components/CamerasTransferList';
|
||||
@ -6,7 +6,9 @@ import { useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Context } from '..';
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
|
||||
import HostSelect from '../shared/components/filters/HostSelect';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import CameraCard from '../widgets/CameraCard';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
|
||||
@ -14,6 +16,7 @@ const MainPage = () => {
|
||||
const executed = useRef(false)
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
const [searchQuery, setSearchQuery] = useState<string>()
|
||||
const [selectedHostId, setSelectedHostId] = useState<string>()
|
||||
const [filteredCameras, setFilteredCameras] = useState<GetCameraWHostWConfig[]>()
|
||||
|
||||
const { data: cameras, isPending, isError, refetch } = useQuery({
|
||||
@ -22,12 +25,19 @@ const MainPage = () => {
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery && cameras) {
|
||||
setFilteredCameras(cameras.filter(camera => camera.name.toLowerCase().includes(searchQuery.toLowerCase())))
|
||||
} else {
|
||||
if (!cameras) {
|
||||
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(() => {
|
||||
if (!executed.current) {
|
||||
@ -66,6 +76,10 @@ const MainPage = () => {
|
||||
|
||||
if (isError) return <RetryErrorPage onRetry={refetch} />
|
||||
|
||||
const handleSelectHost = (hostId: string) => {
|
||||
setSelectedHostId(hostId)
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex direction='column' h='100%' w='100%' >
|
||||
<Flex justify='space-between' align='center' w='100%'>
|
||||
@ -77,11 +91,18 @@ const MainPage = () => {
|
||||
<TextInput
|
||||
maw={400}
|
||||
style={{ flexGrow: 1 }}
|
||||
placeholder="Search..."
|
||||
placeholder={strings.search}
|
||||
icon={<IconSearch size="0.9rem" stroke={1.5} />}
|
||||
value={searchQuery}
|
||||
onChange={(event) => setSearchQuery(event.currentTarget.value)}
|
||||
/>
|
||||
<HostSelect
|
||||
valueId={selectedHostId}
|
||||
onChange={handleSelectHost}
|
||||
ml='1rem'
|
||||
spaceBetween='0px'
|
||||
placeholder={strings.selectHost}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex justify='center' h='100%' direction='column' w='100%' >
|
||||
|
||||
@ -100,9 +100,9 @@ const SettingsPage = () => {
|
||||
mutation.mutate(configsToUpdate);
|
||||
}
|
||||
|
||||
if (!isAdmin) return <Forbidden />
|
||||
if (configPending || adminLoading) return <CenterLoader />
|
||||
if (configError) return <RetryErrorPage onRetry={refetch} />
|
||||
if (!isAdmin) return <Forbidden />
|
||||
|
||||
return (
|
||||
<Flex h='100%'>
|
||||
|
||||
@ -12,7 +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";
|
||||
import AccessSettings from "../pages/AccessSettingsPage";
|
||||
import PlayRecordPage from "../pages/PlayRecordPage";
|
||||
|
||||
interface IRoute {
|
||||
|
||||
@ -4,7 +4,7 @@ import { frigateApi, frigateQueryKeys } from '../../services/frigate.proxy/friga
|
||||
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';
|
||||
import { OneSelectItem } from './filters/OneSelectFilter';
|
||||
import { strings } from '../strings/strings';
|
||||
import { isProduction } from '../env.const';
|
||||
|
||||
@ -65,8 +65,8 @@ const CamerasTransferList = ({
|
||||
return (
|
||||
<>
|
||||
<Flex w='100%' justify='center'>
|
||||
<Button mt='1rem' w='10%' 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='6rem' mr='1rem' onClick={handleDiscard}>{strings.discard}</Button>
|
||||
<Button mt='1rem' miw='5rem' onClick={handleSave}>{strings.save}</Button>
|
||||
</Flex>
|
||||
<TransferList
|
||||
transferAllMatchingFilter
|
||||
|
||||
@ -30,13 +30,19 @@ const useStyles = createStyles((theme,
|
||||
|
||||
const SideBar = ({ isHidden, side, children }: SideBarProps) => {
|
||||
const hideSizePx = useMantineSize(dimensions.hideSidebarsSize)
|
||||
const [visible, { open, close }] = useDisclosure(window.innerWidth > hideSizePx);
|
||||
const manualVisible: React.MutableRefObject<null | boolean> = useRef(null)
|
||||
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 handleClickVisible = (state: boolean) => {
|
||||
manualVisible.current = state
|
||||
localStorage.setItem(`sidebarVisible_${side}`, String(state))
|
||||
if (state) open()
|
||||
else close()
|
||||
}
|
||||
@ -73,23 +79,14 @@ const SideBar = ({ isHidden, side, children }: SideBarProps) => {
|
||||
isHidden(!visible)
|
||||
}, [visible])
|
||||
|
||||
// resize controller
|
||||
useEffect(() => {
|
||||
const checkWindowSize = () => {
|
||||
if (window.innerWidth <= hideSizePx && visible) {
|
||||
close()
|
||||
}
|
||||
if (window.innerWidth > hideSizePx && !visible && manualVisible.current === null) {
|
||||
const savedVisibility = localStorage.getItem(`sidebarVisible_${side}`);
|
||||
if (savedVisibility === null && window.innerWidth < hideSizePx) {
|
||||
open()
|
||||
} else if (savedVisibility) {
|
||||
savedVisibility === 'true' ? open() : close()
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', checkWindowSize);
|
||||
|
||||
// Cleanup function to remove event listener
|
||||
return () => {
|
||||
window.removeEventListener('resize', checkWindowSize);
|
||||
}
|
||||
}, [visible])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -112,7 +109,6 @@ const SideBar = ({ isHidden, side, children }: SideBarProps) => {
|
||||
{rightChildren}
|
||||
</Aside>
|
||||
}
|
||||
|
||||
<SideButton side={side} hide={visible} onClick={() => handleClickVisible(true)} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -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);
|
||||
@ -1,11 +1,10 @@
|
||||
import { Box, Flex, Indicator, Text } from '@mantine/core';
|
||||
import { DatePickerInput } from '@mantine/dates';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useContext } from 'react';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { Box, Flex, Indicator, Text } from '@mantine/core';
|
||||
import CloseWithTooltip from '../buttons/CloseWithTooltip';
|
||||
import { useContext } from 'react';
|
||||
import { Context } from '../../..';
|
||||
import { isProduction } from '../../env.const';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
interface DateRangeSelectFilterProps {}
|
||||
|
||||
67
src/shared/components/filters/HostSelect.tsx
Normal file
67
src/shared/components/filters/HostSelect.tsx
Normal 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;
|
||||
@ -11,15 +11,15 @@ export interface OneSelectItem {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface OneSelectFilterProps extends SelectProps {
|
||||
export interface OneSelectFilterProps extends SelectProps {
|
||||
id?: string
|
||||
data: OneSelectItem[]
|
||||
spaceBetween?: SystemProp<SpacingValue>
|
||||
label?: string
|
||||
defaultValue?: string
|
||||
defaultValue?: string | null
|
||||
textClassName?: string
|
||||
showClose?: boolean,
|
||||
value?: string,
|
||||
value?: string | null,
|
||||
onChange?: (value: string, id?: string,) => void
|
||||
onClose?: () => void
|
||||
}
|
||||
@ -41,11 +41,13 @@ const OneSelectFilter = ({
|
||||
|
||||
return (
|
||||
<Box mt={spaceBetween}>
|
||||
{!label ? null :
|
||||
<Flex justify='space-between'>
|
||||
<Text className={textClassName}>{label}</Text>
|
||||
{showClose ? <CloseWithTooltip label={strings.hide} onClose={handleOnClose} />
|
||||
: null}
|
||||
</Flex>
|
||||
}
|
||||
<Select
|
||||
mt={spaceBetween}
|
||||
data={data}
|
||||
50
src/shared/components/filters/RecordingsHostFilter.tsx
Normal file
50
src/shared/components/filters/RecordingsHostFilter.tsx
Normal 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);
|
||||
@ -1,9 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Center, DEFAULT_THEME } from '@mantine/core';
|
||||
import { Center } from '@mantine/core';
|
||||
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 (
|
||||
<Center>
|
||||
{CogwheelSVG}
|
||||
|
||||
@ -3,6 +3,7 @@ import { useDisclosure } from '@mantine/hooks';
|
||||
import React from 'react';
|
||||
import { LinkItem } from '../../../widgets/header/HeaderAction';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAdminRole } from '../../../hooks/useAdminRole';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
burger: {
|
||||
@ -38,6 +39,8 @@ const DrawerMenu = ({
|
||||
links
|
||||
}: DrawerMenuProps) => {
|
||||
const navigate = useNavigate()
|
||||
const { isAdmin } = useAdminRole()
|
||||
|
||||
|
||||
const { classes } = useStyles();
|
||||
const [drawerOpened, { toggle: toggleDrawer, close: closeDrawer }] = useDisclosure(false)
|
||||
@ -47,7 +50,7 @@ const DrawerMenu = ({
|
||||
closeDrawer()
|
||||
}
|
||||
|
||||
const items = links.map(item => (
|
||||
const items = links.filter(link => !(link.admin && !isAdmin)).map(item => (
|
||||
<UnstyledButton
|
||||
className={classes.drawerButton}
|
||||
key={item.link}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Context } from '..';
|
||||
import CameraSelectFilter from '../shared/components/filters.aps/CameraSelectFilter';
|
||||
import DateRangeSelectFilter from '../shared/components/filters.aps/DateRangeSelectFilter';
|
||||
import HostSelectFilter from '../shared/components/filters.aps/HostSelectFilter';
|
||||
import CameraSelectFilter from '../shared/components/filters/CameraSelectFilter';
|
||||
import DateRangeSelectFilter from '../shared/components/filters/DateRangeSelectFilter';
|
||||
import RecordingsHostFilter from '../shared/components/filters/RecordingsHostFilter';
|
||||
import { isProduction } from '../shared/env.const';
|
||||
|
||||
const RecordingsFiltersRightSide = () => {
|
||||
@ -12,7 +12,7 @@ const RecordingsFiltersRightSide = () => {
|
||||
if (!isProduction) console.log('RecordingsFiltersRightSide rendered')
|
||||
return (
|
||||
<>
|
||||
<HostSelectFilter />
|
||||
<RecordingsHostFilter />
|
||||
{recStore.filteredHost ?
|
||||
<CameraSelectFilter
|
||||
selectedHostId={recStore.filteredHost.id} />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user