diff --git a/example/example.docker-compose.yml b/example/example.docker-compose.yml index c9848eb..7ea73eb 100644 --- a/example/example.docker-compose.yml +++ b/example/example.docker-compose.yml @@ -3,6 +3,9 @@ version: '3.0' services: front: image: oncharterliz/multi-frigate:latest + volumes: + - /etc/timezone:/etc/timezone:ro # for Unix TZ + - /etc/localtime:/etc/localtime:ro # for Unix Time environment: FRIGATE_PROXY: http://localhost:4000 OPENID_SERVER: https://server:port/realms/your-realm diff --git a/src/locales/en.ts b/src/locales/en.ts index 6aaefe2..5b51769 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -18,6 +18,7 @@ const en = { usage: 'Usage', usagePercent: 'Usage %', sreamBandwidth: 'Stream Bandwidth', + total: 'Total', }, cameraStatTable: { process: 'Process', @@ -55,6 +56,7 @@ const en = { doubleClickToFullHint: 'Double click for fullscreen', rating: 'Rating', }, + version: 'Version', uptime: 'Uptime', pleaseSelectRole: 'Please select Role', @@ -92,6 +94,7 @@ const en = { retry: "Retry", youCanRetryOrGoToMain: "You can retry or return to the main page", errors: { + emptyResponse: 'Empty response', somthingGoesWrong: "Something went wrong", 403: "Sorry, you do not have access", 404: "Sorry, we cannot find that page", diff --git a/src/locales/ru.ts b/src/locales/ru.ts index b0f33fc..ec3c7f6 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -18,6 +18,7 @@ const ru = { usage: 'Занято', usagePercent: 'Занято %', sreamBandwidth: 'Скорость потока', + total: 'Итого', }, cameraStatTable: { process: 'Процесс', @@ -92,6 +93,7 @@ const ru = { retry: "Повторить", youCanRetryOrGoToMain: "Вы можете повторить или вернуться на главную", errors: { + emptyResponse: 'Пустой ответ', somthingGoesWrong: "Что-то пошло не так", 403: "Извините, у вас нет доступа", 404: "Извините, мы не можем найти такую страницу", diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 2a09b98..a2318ad 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -11,13 +11,16 @@ import { useTranslation } from 'react-i18next'; import { Context } from '..'; import { useAdminRole } from '../hooks/useAdminRole'; import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api'; -import { GetConfig } from '../services/frigate.proxy/frigate.schema'; +import { GetConfig, PutConfig } from '../services/frigate.proxy/frigate.schema'; import { FloatingLabelInput } from '../shared/components/inputs/FloatingLabelInput'; import CenterLoader from '../shared/components/loaders/CenterLoader'; import { dimensions } from '../shared/dimensions/dimensions'; import { isProduction } from '../shared/env.const'; import Forbidden from './403'; import RetryErrorPage from './RetryErrorPage'; +import { notifications } from '@mantine/notifications'; +import { v4 } from 'uuid'; +import { IconAlertCircle, IconCircleCheck } from '@tabler/icons-react'; const SettingsPage = () => { const { t } = useTranslation() @@ -41,7 +44,7 @@ const SettingsPage = () => { const { isAdmin, isLoading: adminLoading } = useAdminRole() - const ecryptedTemplate = '**********' + const ecryptedTemplate = 'encrypted value is exist' const mapEncryptedToView = (data: GetConfig[] | undefined): GetConfig[] | undefined => { return data?.map(item => { const { value, encrypted, ...rest } = item @@ -54,10 +57,35 @@ const SettingsPage = () => { const isMobile = useMediaQuery(dimensions.mobileSize) const mutation = useMutation({ - mutationFn: frigateApi.putConfig, + mutationFn: (config: PutConfig[]) => frigateApi.putConfig(config).catch(error => { + if (error.response && error.response.data) { + return Promise.reject(error.response.data) + } + return Promise.reject(error) + }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: [frigateQueryKeys.getConfig] }) + notifications.show({ + id: v4(), + withCloseButton: true, + autoClose: 5000, + title: `Sucessfully`, + message: `Sucessfully saved`, + color: 'green', + icon: + }) }, + onError: (e) => { + notifications.show({ + id: e.message, + withCloseButton: true, + autoClose: false, + title: "Error", + message: e.message, + color: 'red', + icon: , + }) + } }) const handleDiscard = () => { @@ -122,6 +150,7 @@ const SettingsPage = () => { label={config.description} value={config.value} placeholder={config.description} + encrypted={config.encrypted} ecryptedValue={ecryptedTemplate} /> ))} diff --git a/src/shared/components/images/AutoUpdatedImage.tsx b/src/shared/components/images/AutoUpdatedImage.tsx index 4cf3b52..331b1f1 100644 --- a/src/shared/components/images/AutoUpdatedImage.tsx +++ b/src/shared/components/images/AutoUpdatedImage.tsx @@ -28,7 +28,7 @@ const AutoUpdatedImage = ({ queryKey: [imageUrl], queryFn: () => proxyApi.getImageFrigate(imageUrl, 522), staleTime: 60 * 1000, - gcTime: Infinity, + gcTime: 5 * 60 * 1000, refetchInterval: isVisible ? 30 * 1000 : undefined, retry: 1, }); diff --git a/src/shared/components/inputs/FloatingLabelInput.module.css b/src/shared/components/inputs/FloatingLabelInput.module.css index cc5cd57..96cde0d 100644 --- a/src/shared/components/inputs/FloatingLabelInput.module.css +++ b/src/shared/components/inputs/FloatingLabelInput.module.css @@ -1,44 +1,57 @@ .root { - position: relative; + position: relative; +} + +.label { + position: absolute; + z-index: 2; + top: 0.5rem; + left: 0.5rem; + pointer-events: none; + color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); + transition: + transform 150ms ease, + font-size 150ms ease, + color 150ms ease; + + &[data-floating] { + transform: translate(-0.5rem, -2rem); + font-size: var(--mantine-font-size-xm); + font-weight: 500; } - - .label { - position: absolute; - z-index: 2; - top: rem(7px); - left: 0.5rem; - pointer-events: none; - color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); - transition: - transform 150ms ease, - font-size 150ms ease, - color 150ms ease; - - &[data-floating] { - transform: translate(-0.5rem, -1.625rem); - font-size: var(--mantine-font-size-xm); - font-weight: 500; - } +} + +.required { + transition: opacity 150ms ease; + opacity: 0; + + [data-floating] & { + opacity: 1; } - - .required { - transition: opacity 150ms ease; - opacity: 0; - - [data-floating] & { - opacity: 1; - } +} + +.input { + &::placeholder { + transition: color 150ms ease; + color: transparent; } - - .input { + + &[data-floating] { &::placeholder { - transition: color 150ms ease; - color: transparent; + color: var(--mantine-color-placeholder); } - - &[data-floating] { - &::placeholder { - color: var(--mantine-color-placeholder); - } + } +} + +.innerInput { + &::placeholder { + transition: color 150ms ease; + color: transparent; + } + + &[data-floating] { + &::placeholder { + color: var(--mantine-color-placeholder); } - } \ No newline at end of file + } +} \ No newline at end of file diff --git a/src/shared/components/inputs/FloatingLabelInput.tsx b/src/shared/components/inputs/FloatingLabelInput.tsx index ef5c3a2..5b0fe22 100644 --- a/src/shared/components/inputs/FloatingLabelInput.tsx +++ b/src/shared/components/inputs/FloatingLabelInput.tsx @@ -1,16 +1,22 @@ -import { TextInput, TextInputProps, createStyles } from '@mantine/core'; +import { PasswordInput, TextInput, TextInputProps, createStyles } from '@mantine/core'; import { useEffect, useState } from 'react'; import classes from './FloatingLabelInput.module.css'; interface FloatingLabelInputProps extends TextInputProps { - value?: string, - ecryptedValue?: string, + value?: string + encrypted?: boolean + ecryptedValue?: string onChangeValue?: (key: string, value: string) => void } -export const FloatingLabelInput = (props: FloatingLabelInputProps) => { - const { value: propVal, onChangeValue, ecryptedValue, ...rest } = props +export const FloatingLabelInput: React.FC = ({ + value: propVal, + onChangeValue, + encrypted, + ecryptedValue, + ...rest +}) => { const [focused, setFocused] = useState(false); const [value, setValue] = useState(propVal || ''); const floating = value?.trim().length !== 0 || focused || undefined; @@ -26,10 +32,26 @@ export const FloatingLabelInput = (props: FloatingLabelInputProps) => { const handleChange = (event: React.ChangeEvent) => { setValue(event.currentTarget.value); - if (onChangeValue && props.name) { - onChangeValue(props.name, event.currentTarget.value); + if (onChangeValue && rest.name) { + onChangeValue(rest.name, event.currentTarget.value); } } + if (encrypted) { + return ( + setFocused(false)} + mt='2rem' + autoComplete="nope" + data-floating={floating} + labelProps={{ 'data-floating': floating }} + {...rest} + /> + ) + } return ( { return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } -export const formatMBytes = (mb: number, decimals?: number): string => { +export const formatMBytes = (mb?: number | null, decimals?: number): string => { + if (!mb) return '0' const bytes = mb * 1024 * 1024; return formatBytes(bytes, decimals); } \ No newline at end of file diff --git a/src/types/frigateStats.ts b/src/types/frigateStats.ts index 7574831..aed7fcc 100644 --- a/src/types/frigateStats.ts +++ b/src/types/frigateStats.ts @@ -3,9 +3,9 @@ export interface GetHostStorage { } export interface CameraStorage { - bandwidth: number // MiB/hr - usage: number // MB - usage_percent: number // Usage / 1024 / Total storage size * 100 + bandwidth?: number // MiB/hr + usage?: number // MB + usage_percent?: number // Usage / 1024 / Total storage size * 100 } export interface GetVaInfo { diff --git a/src/widgets/camera.stat.table/FrigateStorageStateTable.tsx b/src/widgets/camera.stat.table/FrigateStorageStateTable.tsx index bbdf430..2ce0c93 100644 --- a/src/widgets/camera.stat.table/FrigateStorageStateTable.tsx +++ b/src/widgets/camera.stat.table/FrigateStorageStateTable.tsx @@ -46,9 +46,9 @@ const FrigateStorageStateTable: React.FC = ({ return Object.entries(data).map(([name, storage]) => { return { cameraName: name, - usage: storage.usage, - usagePercent: storage.usage_percent, - sreamBandwidth: storage.bandwidth, + usage: storage.usage || 0, + usagePercent: storage.usage_percent || 0, + sreamBandwidth: storage.bandwidth || 0, } }) }, [data]) @@ -71,8 +71,7 @@ const FrigateStorageStateTable: React.FC = ({ if (isPending) return if (isError) return - // if (!tableData || tableData.length < 1) return
Empty response
- + if (!tableData ) return
{t('errors.emptyResponse')}
const headTitle: TableHead[] = [ { propertyName: 'cameraName', title: t('camera') }, @@ -93,18 +92,31 @@ const FrigateStorageStateTable: React.FC = ({ ) }) - const rows = tableData.map(item => { return ( {item.cameraName} {formatMBytes(item.usage)} {item.usagePercent.toFixed(4)} % - {item.sreamBandwidth} MiB/hr + {item.sreamBandwidth.toFixed(2) } MiB/hr ) }) + const totalRow = () => { + const totalUsage = tableData.reduce((acc, curr) => acc + curr.usage, 0) + const totalStreamBandwidth = tableData.reduce((acc, curr) => acc + curr.sreamBandwidth, 0) + const totalUsagePercent = tableData.reduce((acc, curr) => acc + curr.usagePercent, 0) + return ( + + {t('cameraStorageTable.total')} + {formatMBytes(totalUsage)} + {totalUsagePercent.toFixed(4)} % + {totalStreamBandwidth.toFixed(2)} MiB/hr + + ) + } + return ( @@ -114,6 +126,7 @@ const FrigateStorageStateTable: React.FC = ({ {rows} + {totalRow()}
);