fix sidebar rendering
add sidebar to main page add creatable multiselect component
This commit is contained in:
parent
2ca9195fbf
commit
76481ba12e
@ -8,7 +8,6 @@ import { useEffect, useState } from 'react';
|
|||||||
import AppBody from './AppBody';
|
import AppBody from './AppBody';
|
||||||
import { FfprobeModal } from './shared/components/modal.windows/FfprobeModal';
|
import { FfprobeModal } from './shared/components/modal.windows/FfprobeModal';
|
||||||
import { VaInfoModal } from './shared/components/modal.windows/VaInfoModal';
|
import { VaInfoModal } from './shared/components/modal.windows/VaInfoModal';
|
||||||
import { isProduction } from './shared/env.const';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
|
|||||||
@ -8,8 +8,6 @@ import { routesPath } from './router/routes.path';
|
|||||||
import SideBar from './shared/components/SideBar';
|
import SideBar from './shared/components/SideBar';
|
||||||
import { isProduction } from './shared/env.const';
|
import { isProduction } from './shared/env.const';
|
||||||
import { HeaderAction } from './widgets/header/HeaderAction';
|
import { HeaderAction } from './widgets/header/HeaderAction';
|
||||||
import { useAdminRole } from "./hooks/useAdminRole";
|
|
||||||
import { useRealmAccessRoles } from "./hooks/useRealmAccessRoles";
|
|
||||||
|
|
||||||
const AppBody = () => {
|
const AppBody = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|||||||
37
src/hooks/useRealmUser.ts
Normal file
37
src/hooks/useRealmUser.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { useKeycloak } from "@react-keycloak/web"
|
||||||
|
import { z } from "zod"
|
||||||
|
import { isProduction } from "../shared/env.const"
|
||||||
|
|
||||||
|
const keycloakUser = z.object({
|
||||||
|
sub: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
preferred_username: z.string(),
|
||||||
|
given_name: z.string(),
|
||||||
|
family_name: z.string()
|
||||||
|
})
|
||||||
|
|
||||||
|
interface RealmUser {
|
||||||
|
id: string // sub
|
||||||
|
username: string // preferred_username
|
||||||
|
givenName?: string // given_name
|
||||||
|
familyName?: string // family_name
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRealmUser = () => {
|
||||||
|
const { keycloak } = useKeycloak()
|
||||||
|
if (!isProduction) console.log('Authenticated:', keycloak.authenticated)
|
||||||
|
if (!isProduction) console.log('userInfo:', keycloak.userInfo)
|
||||||
|
if (keycloak.authenticated && keycloak.tokenParsed) {
|
||||||
|
const parsedUser = keycloakUser.safeParse(keycloak.tokenParsed)
|
||||||
|
if (parsedUser.success) {
|
||||||
|
const realmUser: RealmUser = {
|
||||||
|
id: parsedUser.data.sub,
|
||||||
|
username: parsedUser.data.preferred_username,
|
||||||
|
givenName: parsedUser.data.given_name,
|
||||||
|
familyName: parsedUser.data.family_name
|
||||||
|
}
|
||||||
|
return realmUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
@ -1,6 +1,9 @@
|
|||||||
import { error } from "console"
|
import { error } from "console"
|
||||||
|
|
||||||
const en = {
|
const en = {
|
||||||
|
mainPage: {
|
||||||
|
createSelectTags: 'Create\\Select tags'
|
||||||
|
},
|
||||||
editCameraPage: {
|
editCameraPage: {
|
||||||
notFrigateCamera: 'Not frigate camera',
|
notFrigateCamera: 'Not frigate camera',
|
||||||
errorAtPut: 'Error at sending mask',
|
errorAtPut: 'Error at sending mask',
|
||||||
@ -90,6 +93,7 @@ const en = {
|
|||||||
rating: 'Rating',
|
rating: 'Rating',
|
||||||
},
|
},
|
||||||
config: 'Config',
|
config: 'Config',
|
||||||
|
create: 'Create',
|
||||||
clear: 'Clear',
|
clear: 'Clear',
|
||||||
edit: 'Edit',
|
edit: 'Edit',
|
||||||
version: 'Version',
|
version: 'Version',
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
const ru = {
|
const ru = {
|
||||||
|
mainPage: {
|
||||||
|
createSelectTags: 'Создать\\Выбрать тэги'
|
||||||
|
},
|
||||||
editCameraPage: {
|
editCameraPage: {
|
||||||
notFrigateCamera: 'Не камера Фригата',
|
notFrigateCamera: 'Не камера Фригата',
|
||||||
errorAtPut: 'Ошибка при отправке маски',
|
errorAtPut: 'Ошибка при отправке маски',
|
||||||
@ -88,6 +91,7 @@ const ru = {
|
|||||||
rating: 'Рейтинг',
|
rating: 'Рейтинг',
|
||||||
},
|
},
|
||||||
config: 'Конфиг.',
|
config: 'Конфиг.',
|
||||||
|
create: 'Создать',
|
||||||
clear: 'Очистить',
|
clear: 'Очистить',
|
||||||
edit: 'Изменить',
|
edit: 'Изменить',
|
||||||
version: 'Версия',
|
version: 'Версия',
|
||||||
|
|||||||
@ -6,12 +6,15 @@ 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 CameraCard from '../widgets/CameraCard';
|
import CameraCard from '../widgets/CameraCard';
|
||||||
import RetryErrorPage from './RetryErrorPage';
|
import RetryErrorPage from './RetryErrorPage';
|
||||||
import ClearableTextInput from '../shared/components/inputs/ClearableTextInput';
|
import ClearableTextInput from '../shared/components/inputs/ClearableTextInput';
|
||||||
import { useTranslation } from 'react-i18next';
|
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';
|
||||||
|
|
||||||
const MainPage = () => {
|
const MainPage = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -21,6 +24,9 @@ const MainPage = () => {
|
|||||||
const [searchQuery, setSearchQuery] = useState<string>()
|
const [searchQuery, setSearchQuery] = useState<string>()
|
||||||
const [filteredCameras, setFilteredCameras] = useState<GetCameraWHostWConfig[]>()
|
const [filteredCameras, setFilteredCameras] = useState<GetCameraWHostWConfig[]>()
|
||||||
|
|
||||||
|
const realmUser = useRealmUser()
|
||||||
|
if (!isProduction) console.log('Realmuser:', realmUser)
|
||||||
|
|
||||||
const { data: cameras, isPending, isError, refetch } = useQuery({
|
const { data: cameras, isPending, isError, refetch } = useQuery({
|
||||||
queryKey: [frigateQueryKeys.getCamerasWHost],
|
queryKey: [frigateQueryKeys.getCamerasWHost],
|
||||||
queryFn: frigateApi.getCamerasWHost
|
queryFn: frigateApi.getCamerasWHost
|
||||||
@ -41,15 +47,27 @@ const MainPage = () => {
|
|||||||
setFilteredCameras(cameras.filter(filterCameras))
|
setFilteredCameras(cameras.filter(filterCameras))
|
||||||
}, [searchQuery, cameras, selectedHostId])
|
}, [searchQuery, cameras, selectedHostId])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!executed.current) {
|
|
||||||
sideBarsStore.rightVisible = false
|
|
||||||
sideBarsStore.setLeftChildren(null)
|
sideBarsStore.setLeftChildren(null)
|
||||||
sideBarsStore.setRightChildren(null)
|
sideBarsStore.leftVisible = false
|
||||||
executed.current = true
|
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])
|
}, [sideBarsStore])
|
||||||
|
|
||||||
|
//test change
|
||||||
|
|
||||||
const cards = useMemo(() => {
|
const cards = useMemo(() => {
|
||||||
if (filteredCameras)
|
if (filteredCameras)
|
||||||
return filteredCameras.filter(camera => {
|
return filteredCameras.filter(camera => {
|
||||||
@ -78,13 +96,10 @@ const MainPage = () => {
|
|||||||
|
|
||||||
if (isError) return <RetryErrorPage onRetry={refetch} />
|
if (isError) return <RetryErrorPage onRetry={refetch} />
|
||||||
|
|
||||||
const handleSelectHost = (hostId: string) => {
|
if (!isProduction) console.log('MainPage rendered')
|
||||||
mainStore.selectedHostId = 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 w='100%'
|
<Flex w='100%'
|
||||||
justify='center'
|
justify='center'
|
||||||
>
|
>
|
||||||
@ -97,14 +112,6 @@ const MainPage = () => {
|
|||||||
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={t('selectHost')}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex justify='center' h='100%' direction='column' w='100%' >
|
<Flex justify='center' h='100%' direction='column' w='100%' >
|
||||||
<Grid mt='sm' justify="center" mb='sm' align='stretch'>
|
<Grid mt='sm' justify="center" mb='sm' align='stretch'>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|||||||
import { Context } from '..';
|
import { Context } from '..';
|
||||||
import { isProduction } from '../shared/env.const';
|
import { isProduction } from '../shared/env.const';
|
||||||
import { dateToQueryString, parseQueryDateToDate } from '../shared/utils/dateUtil';
|
import { dateToQueryString, parseQueryDateToDate } from '../shared/utils/dateUtil';
|
||||||
import RecordingsFiltersRightSide from '../widgets/RecordingsFiltersRightSide';
|
import RecordingsFiltersRightSide from '../widgets/sidebars/RecordingsFiltersRightSide';
|
||||||
import SelectedCameraList from '../widgets/SelectedCameraList';
|
import SelectedCameraList from '../widgets/SelectedCameraList';
|
||||||
import SelectedDayList from '../widgets/SelectedDayList';
|
import SelectedDayList from '../widgets/SelectedDayList';
|
||||||
import SelectedHostList from '../widgets/SelectedHostList';
|
import SelectedHostList from '../widgets/SelectedHostList';
|
||||||
|
|||||||
78
src/shared/components/filters/CreatableMultiSelect.tsx
Normal file
78
src/shared/components/filters/CreatableMultiSelect.tsx
Normal file
@ -0,0 +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 { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface CreatableMultiSelectProps {
|
||||||
|
id?: 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
|
||||||
|
onClose?(): void
|
||||||
|
onCreate?(value: string): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreatableMultiSelect: React.FC<CreatableMultiSelectProps> = ({
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
spaceBetween,
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
textClassName,
|
||||||
|
selectProps,
|
||||||
|
display,
|
||||||
|
showClose,
|
||||||
|
changedState,
|
||||||
|
onClose,
|
||||||
|
onCreate
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleOnChange = (value: string[]) => {
|
||||||
|
if (changedState) {
|
||||||
|
changedState(value, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOnCreate = (query: string | SelectItem | null | undefined) => {
|
||||||
|
const item = { value: query, label: query } as SelectItem
|
||||||
|
if (onCreate && typeof query === 'string') {
|
||||||
|
onCreate(query)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display={display} mt={spaceBetween}>
|
||||||
|
<Flex justify='space-between'>
|
||||||
|
<Text className={textClassName}>{label}</Text>
|
||||||
|
{showClose ?
|
||||||
|
<CloseWithTooltip label={t('hide')} onClose={onClose} />
|
||||||
|
: null}
|
||||||
|
</Flex>
|
||||||
|
<MultiSelect
|
||||||
|
{...selectProps}
|
||||||
|
mt={spaceBetween}
|
||||||
|
data={data}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
searchable
|
||||||
|
clearable
|
||||||
|
creatable
|
||||||
|
getCreateLabel={(query) => `+ ${t('create') + ' ' + query}`}
|
||||||
|
onCreate={handleOnCreate}
|
||||||
|
{...selectProps}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreatableMultiSelect;
|
||||||
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import CloseWithTooltip from '../buttons/CloseWithTooltip';
|
import CloseWithTooltip from '../buttons/CloseWithTooltip';
|
||||||
|
|
||||||
interface MultiSelectFilterProps {
|
interface MultiSelectFilterProps {
|
||||||
id: string
|
id?: string
|
||||||
data: SelectItem[]
|
data: SelectItem[]
|
||||||
spaceBetween?: SystemProp<SpacingValue>
|
spaceBetween?: SystemProp<SpacingValue>
|
||||||
label?: string
|
label?: string
|
||||||
@ -13,7 +13,7 @@ interface MultiSelectFilterProps {
|
|||||||
selectProps?: MultiSelectProps,
|
selectProps?: MultiSelectProps,
|
||||||
display?: SystemProp<CSSProperties['display']>
|
display?: SystemProp<CSSProperties['display']>
|
||||||
showClose?: boolean,
|
showClose?: boolean,
|
||||||
changedState?(id: string, value: string[]): void
|
changedState?(value: string[], id?: string): void
|
||||||
onClose?(): void
|
onClose?(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ const MultiSelectFilter = ({
|
|||||||
|
|
||||||
const handleOnChange = (value: string[]) => {
|
const handleOnChange = (value: string[]) => {
|
||||||
if (changedState) {
|
if (changedState) {
|
||||||
changedState(id, value)
|
changedState(value, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,13 +39,13 @@ const MultiSelectFilter = ({
|
|||||||
: null}
|
: null}
|
||||||
</Flex>
|
</Flex>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
{...selectProps}
|
|
||||||
mt={spaceBetween}
|
mt={spaceBetween}
|
||||||
data={data}
|
data={data}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
onChange={handleOnChange}
|
onChange={handleOnChange}
|
||||||
searchable
|
searchable
|
||||||
clearable
|
clearable
|
||||||
|
{...selectProps}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { makeAutoObservable } from "mobx"
|
import { action, makeAutoObservable } from "mobx"
|
||||||
|
|
||||||
export class SideBarsStore {
|
export class SideBarsStore {
|
||||||
|
|
||||||
@ -29,7 +29,10 @@ export class SideBarsStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
makeAutoObservable(this)
|
makeAutoObservable(this, {
|
||||||
|
setRightChildren: action,
|
||||||
|
setLeftChildren: action,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setRightChildren = (value: React.ReactNode) => {
|
setRightChildren = (value: React.ReactNode) => {
|
||||||
|
|||||||
42
src/widgets/sidebars/MainFiltersRightSide.tsx
Normal file
42
src/widgets/sidebars/MainFiltersRightSide.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import HostSelect from '../../shared/components/filters/HostSelect';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { Context } from '../..';
|
||||||
|
import { isProduction } from '../../shared/env.const';
|
||||||
|
import CreatableMultiSelect from '../../shared/components/filters/CreatableMultiSelect';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const MainFiltersRightSide = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { mainStore } = useContext(Context)
|
||||||
|
const { selectedHostId } = mainStore
|
||||||
|
|
||||||
|
const handleSelect = (value: string) => {
|
||||||
|
if (!isProduction) console.log('handleSelect value', value)
|
||||||
|
mainStore.selectedHostId = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HostSelect
|
||||||
|
label={t('selectHost')}
|
||||||
|
valueId={selectedHostId}
|
||||||
|
defaultId={selectedHostId}
|
||||||
|
onChange={handleSelect}
|
||||||
|
/>
|
||||||
|
{/* TODO Add tags select */}
|
||||||
|
{/* <CreatableMultiSelect
|
||||||
|
label={t('mainPage.createSelectTags')}
|
||||||
|
spaceBetween='1rem'
|
||||||
|
data={[]}
|
||||||
|
/> */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default observer(MainFiltersRightSide);
|
||||||
@ -1,10 +1,10 @@
|
|||||||
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/CameraSelectFilter';
|
import CameraSelectFilter from '../../shared/components/filters/CameraSelectFilter';
|
||||||
import DateRangeSelectFilter from '../shared/components/filters/DateRangeSelectFilter';
|
import DateRangeSelectFilter from '../../shared/components/filters/DateRangeSelectFilter';
|
||||||
import RecordingsHostFilter from '../shared/components/filters/RecordingsHostFilter';
|
import RecordingsHostFilter from '../../shared/components/filters/RecordingsHostFilter';
|
||||||
import { isProduction } from '../shared/env.const';
|
import { isProduction } from '../../shared/env.const';
|
||||||
|
|
||||||
const RecordingsFiltersRightSide = () => {
|
const RecordingsFiltersRightSide = () => {
|
||||||
const { recordingsStore: recStore } = useContext(Context)
|
const { recordingsStore: recStore } = useContext(Context)
|
||||||
Loading…
Reference in New Issue
Block a user