settings page: add notify, add password input
add total usage percent to storage table
This commit is contained in:
parent
b896c88f6c
commit
b52ea6ff37
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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: "Извините, мы не можем найти такую страницу",
|
||||
|
||||
@ -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: <IconCircleCheck />
|
||||
})
|
||||
},
|
||||
onError: (e) => {
|
||||
notifications.show({
|
||||
id: e.message,
|
||||
withCloseButton: true,
|
||||
autoClose: false,
|
||||
title: "Error",
|
||||
message: e.message,
|
||||
color: 'red',
|
||||
icon: <IconAlertCircle />,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const handleDiscard = () => {
|
||||
@ -122,6 +150,7 @@ const SettingsPage = () => {
|
||||
label={config.description}
|
||||
value={config.value}
|
||||
placeholder={config.description}
|
||||
encrypted={config.encrypted}
|
||||
ecryptedValue={ecryptedTemplate}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
.required {
|
||||
transition: opacity 150ms ease;
|
||||
opacity: 0;
|
||||
|
||||
[data-floating] & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
&::placeholder {
|
||||
transition: color 150ms ease;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
&[data-floating] {
|
||||
&::placeholder {
|
||||
transition: color 150ms ease;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&[data-floating] {
|
||||
&::placeholder {
|
||||
color: var(--mantine-color-placeholder);
|
||||
}
|
||||
color: var(--mantine-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.innerInput {
|
||||
&::placeholder {
|
||||
transition: color 150ms ease;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
&[data-floating] {
|
||||
&::placeholder {
|
||||
color: var(--mantine-color-placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<FloatingLabelInputProps> = ({
|
||||
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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<PasswordInput
|
||||
value={value}
|
||||
classNames={classes}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocused}
|
||||
onBlur={() => setFocused(false)}
|
||||
mt='2rem'
|
||||
autoComplete="nope"
|
||||
data-floating={floating}
|
||||
labelProps={{ 'data-floating': floating }}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
|
||||
@ -7,7 +7,8 @@ export const formatBytes = (bytes: number, decimals = 2): string => {
|
||||
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);
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -46,9 +46,9 @@ const FrigateStorageStateTable: React.FC<TableProps> = ({
|
||||
return Object.entries(data).map<StorageItem>(([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<TableProps> = ({
|
||||
|
||||
if (isPending) return <CogwheelLoader />
|
||||
if (isError) return <RetryError onRetry={refetch} />
|
||||
// if (!tableData || tableData.length < 1) return <Center><Text>Empty response</Text></Center>
|
||||
|
||||
if (!tableData ) return <Center><Text>{t('errors.emptyResponse')}</Text></Center>
|
||||
|
||||
const headTitle: TableHead[] = [
|
||||
{ propertyName: 'cameraName', title: t('camera') },
|
||||
@ -93,18 +92,31 @@ const FrigateStorageStateTable: React.FC<TableProps> = ({
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
const rows = tableData.map(item => {
|
||||
return (
|
||||
<tr key={item.cameraName}>
|
||||
<td><Text align='center'>{item.cameraName}</Text></td>
|
||||
<td><Text align='center'>{formatMBytes(item.usage)}</Text></td>
|
||||
<td><Text align='center'>{item.usagePercent.toFixed(4)} %</Text></td>
|
||||
<td><Text align='center'>{item.sreamBandwidth} MiB/hr</Text></td>
|
||||
<td><Text align='center'>{item.sreamBandwidth.toFixed(2) } MiB/hr</Text></td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
|
||||
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 (
|
||||
<tr key={totalUsage}>
|
||||
<td><Text align='center'>{t('cameraStorageTable.total')}</Text></td>
|
||||
<td><Text align='center'>{formatMBytes(totalUsage)}</Text></td>
|
||||
<td><Text align='center'>{totalUsagePercent.toFixed(4)} %</Text></td>
|
||||
<td><Text align='center'>{totalStreamBandwidth.toFixed(2)} MiB/hr</Text></td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Table >
|
||||
<thead>
|
||||
@ -114,6 +126,7 @@ const FrigateStorageStateTable: React.FC<TableProps> = ({
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
{totalRow()}
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user