add setting

add frigate configs
This commit is contained in:
NlightN22 2024-02-19 02:42:25 +07:00
parent ece046d2fd
commit d68edcf6f2
37 changed files with 699 additions and 108 deletions

1
.gitignore vendored
View File

@ -21,6 +21,7 @@
.env.test.local
.env.production.local
.idea/*
.vscode
#docker
docker-compose.*

View File

@ -10,6 +10,7 @@
"@mantine/dates": "^6.0.16",
"@mantine/hooks": "^6.0.16",
"@tabler/icons-react": "^2.24.0",
"@tanstack/react-query": "^5.21.2",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",

View File

@ -9,9 +9,10 @@ import FullImageModal from './shared/components/FullImageModal';
import AppBody from './AppBody';
import FullProductModal from './shared/components/FullProductModal';
import Forbidden from './pages/403';
import { QueryClient } from '@tanstack/react-query';
function App() {
// const auth = useAuth();
const systemColorScheme = useColorScheme()
const [colorScheme, setColorScheme] = useState<ColorScheme>(getCookie('mantine-color-scheme') as ColorScheme || systemColorScheme);
const toggleColorScheme = (value?: ColorScheme) => {
@ -20,20 +21,22 @@ function App() {
setCookie('mantine-color-scheme', nextColorScheme, { maxAge: 60 * 60 * 24 * 30 });
}
const auth = useAuth();
// automatically sign-in
// useEffect(() => {
// if (!hasAuthParams() &&
// !auth.isAuthenticated && !auth.activeNavigator && !auth.isLoading) {
// auth.signinRedirect();
// }
// }, [auth, auth.isAuthenticated, auth.activeNavigator, auth.isLoading, auth.signinRedirect]);
useEffect(() => {
if (!hasAuthParams() &&
!auth.isAuthenticated && !auth.activeNavigator && !auth.isLoading) {
auth.signinRedirect();
}
}, [auth, auth.isAuthenticated, auth.activeNavigator, auth.isLoading, auth.signinRedirect]);
// if (auth.activeNavigator || auth.isLoading) {
// return <CenterLoader />
// }
// if ((!auth.isAuthenticated && !auth.isLoading) || auth.error) {
// return <Forbidden />
// }
if (auth.activeNavigator || auth.isLoading) {
return <CenterLoader />
}
if ((!auth.isAuthenticated && !auth.isLoading) || auth.error) {
console.error(`auth.error:`, auth.error)
return <Forbidden />
}
return (
<div className="App">

View File

@ -1,48 +1,54 @@
import React, { useEffect, useState } from 'react';
import { AppShell, useMantineTheme, } from "@mantine/core"
import LeftSideBar from './widgets/LeftSideBar';
import RightSideBar from './widgets/RightSideBar';
import { HeaderAction } from './widgets/header/HeaderAction';
import { testHeaderLinks } from './widgets/header/header.links';
import AppRouter from './router/AppRouter';
import { SideBar } from './shared/components/SideBar';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient()
const AppBody = () => {
useEffect(() => {
console.log("render Main")
})
const [leftSideBar, setLeftSidebar] = useState(true)
const [rightSideBar, setRightSidebar] = useState(true)
const [leftSideBar, setLeftSidebar] = useState(false)
const [rightSideBar, setRightSidebar] = useState(false)
const leftSideBarIsHidden = (isHidden: boolean) => {
setLeftSidebar(!isHidden)
}
const rightSideBarIsHidden = (isHidden: boolean) => {
setRightSidebar(!isHidden)
}
const theme = useMantineTheme();
return (
<AppShell
styles={{
main: {
paddingLeft: !leftSideBar ? "3em" : '',
paddingRight: !rightSideBar ? '3em' : '',
background: theme.colorScheme === 'dark' ? theme.colors.dark[8] : undefined,
},
}}
navbarOffsetBreakpoint="sm"
asideOffsetBreakpoint="sm"
<QueryClientProvider client={queryClient}>
<AppShell
styles={{
main: {
paddingLeft: !leftSideBar ? "3em" : '',
paddingRight: !rightSideBar ? '3em' : '',
background: theme.colorScheme === 'dark' ? theme.colors.dark[8] : undefined,
},
}}
navbarOffsetBreakpoint="sm"
asideOffsetBreakpoint="sm"
header={
<HeaderAction links={testHeaderLinks.links} />
}
navbar={
<SideBar isHidden={leftSideBarIsHidden} side="left" />
}
>
<AppRouter />
</AppShell>
header={
<HeaderAction links={testHeaderLinks.links} />
}
aside={
<SideBar isHidden={rightSideBarIsHidden} side="right" />
}
>
<AppRouter />
</AppShell>
</QueryClientProvider>
)
};

View File

@ -3,13 +3,28 @@ import ReactDOM from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
import RootStore from './shared/stores/root.store';
import { AuthProvider } from 'react-oidc-context';
import { keycloakConfig } from './shared/services/keycloack';
import { AuthProvider, AuthProviderProps } from 'react-oidc-context';
import { oidpSettings } from './shared/env.const';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
export const hostURL = new URL(window.location.href)
export const keycloakConfig: AuthProviderProps = {
authority: oidpSettings.server,
client_id: oidpSettings.clientId,
redirect_uri: hostURL.toString(),
onSigninCallback: () => {
window.history.replaceState(
{},
document.title,
window.location.pathname
)
}
}
const rootStore = new RootStore()
export const Context = createContext<RootStore>(rootStore)

View File

@ -1,11 +0,0 @@
import React from 'react';
const AdminPage = () => {
return (
<div>
</div>
);
};
export default AdminPage;

View File

@ -0,0 +1,26 @@
import React from 'react';
import FrigateHostsTable from '../widgets/FrigateHostsTable';
import { useQuery } from '@tanstack/react-query';
import { frigateApi } from '../services/frigate.proxy/frigate.api';
import CenterLoader from '../shared/components/CenterLoader';
import RetryError from './RetryError';
const FrigateHostsPage = () => {
const { isPending: hostsPending, error: hostsError, data, refetch } = useQuery({
queryKey: ['frigate-hosts'],
queryFn: frigateApi.getHosts,
})
if (hostsPending) return <CenterLoader />
if (hostsError) return <RetryError />
return (
<div>
<FrigateHostsTable data={data} showAddButton/>
</div>
);
};
export default FrigateHostsPage;

View File

@ -1,5 +1,5 @@
import { Container, Flex, Group, Text } from '@mantine/core';
import ProductTable, { TableAdapter } from '../shared/components/table.aps/ProductTable';
import ProductTable, { TableAdapter } from '../widgets/ProductTable';
import HeadSearch from '../shared/components/HeadSearch';
import ViewSelector, { SelectorViewState } from '../shared/components/ViewSelector';
import { useContext, useState, useEffect } from 'react';

122
src/pages/SettingsPage.tsx Normal file
View File

@ -0,0 +1,122 @@
import React, { useEffect, useState } from 'react';
import {
useQuery,
useMutation,
useQueryClient,
} from '@tanstack/react-query'
import { Config, frigateApi } from '../services/frigate.proxy/frigate.api';
import CenterLoader from '../shared/components/CenterLoader';
import RetryError from './RetryError';
import { Button, Flex, Space } from '@mantine/core';
import { FloatingLabelInput } from '../shared/components/FloatingLabelInput';
import { strings } from '../shared/strings/strings';
import { dimensions } from '../shared/dimensions/dimensions';
import { useMediaQuery } from '@mantine/hooks';
const SettingsPage = () => {
const queryClient = useQueryClient()
const { isPending: configPending, error: configError, data, refetch } = useQuery({
queryKey: ['config'],
queryFn: frigateApi.getConfig,
})
const ecryptedValue = '**********'
const mapEncryptedToView = (data: Config[] | undefined): Config[] | undefined => {
return data?.map(item => {
const { value, encrypted, ...rest } = item
if (encrypted) return { value: ecryptedValue, encrypted, ...rest }
return item
})
}
const [configs, setConfigs] = useState(data)
const isMobile = useMediaQuery(dimensions.mobileSize)
const mutation = useMutation({
mutationFn: frigateApi.putConfig,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['config'] })
},
})
const handleDiscard = () => {
console.log('Discard changes')
refetch()
setConfigs(data ? mapEncryptedToView(data) : [])
}
useEffect(() => {
console.log('data changed')
setConfigs(mapEncryptedToView(data))
}, [data])
useEffect(() => {
console.log('configs changed')
}, [configs])
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const formData = new FormData(event.currentTarget);
const formDataObj: any = Array.from(formData.entries()).reduce((acc, [key, value]) => ({
...acc,
[key]: value,
}), {});
const configsToUpdate = Object.keys(formDataObj).map(key => {
const value = formDataObj[key]
const currData = data?.find( val => val.key === key)
const isEncrypted = value === ecryptedValue
if (currData && currData.encrypted && isEncrypted) {
return {
key,
value: currData.value
}
}
return {
key,
value: value,
}
});
console.log('configsToUpdate', configsToUpdate)
mutation.mutate(configsToUpdate);
}
if (configPending) return <CenterLoader />
if (configError) return <RetryError />
return (
<Flex h='100%'>
{!isMobile ?
< Space w='20%' />
: <></>
}
<Flex direction='column' h='100%' w='100%' justify='stretch'>
<form onSubmit={handleSubmit}>
{!configs ? <></>
:
configs.map((config) => (
<FloatingLabelInput
key={`${config.key}-${new Date().getTime()}`}
name={config.key}
label={config.description}
value={config.value}
placeholder={config.description}
ecryptedValue={ecryptedValue}
/>
))}
<Space h='2%' />
<Flex w='100%' justify='stretch' wrap='nowrap' align='center'>
<Button w='100%' onClick={handleDiscard} m='0.5rem'>{strings.discard}</Button>
<Button w='100%' type="submit" m='0.5rem'>{strings.confirm}</Button>
</Flex>
</form>
</Flex>
{!isMobile ?
<Space w='20%' />
: <></>
}
</Flex>
);
};
export default SettingsPage;

View File

@ -1,4 +1,4 @@
import { hostURL } from "../shared/env.const"
import { hostURL } from ".."
export const cameraLiveViewURL = (host: string, cameraName: string) => {
return `ws://${hostURL.host}/proxy-ws/live/jsmpeg/${cameraName}?hostName=${host}`

View File

@ -1,6 +1,12 @@
export const pathRoutes = {
MAIN_PATH: '/',
ADMIN_PATH: '/admin',
BIRDSEYE_PATH: '/birdseye',
EVENTS_PATH: '/events',
RECORDINGS_PATH: '/recordings',
SETTINGS_PATH: '/settings',
HOST_CONFIG_PATH: '/host-config',
ROLES_PATH: '/roles',
LIVE_PATH: '/live',
THANKS_PATH: '/thanks',
USER_DETAILED_PATH: '/user',
RETRY_ERROR_PATH: '/retry_error',

View File

@ -5,7 +5,8 @@ import {pathRoutes} from "./routes.path";
import RetryError from "../pages/RetryError";
import Forbidden from "../pages/403";
import NotFound from "../pages/404";
import AdminPage from "../pages/Admin";
import SettingsPage from "../pages/SettingsPage";
import FrigateHostsPage from "../pages/FrigateHostsPage";
interface IRoute {
path: string,
@ -18,8 +19,12 @@ export const routes: IRoute[] = [
component: <Test />,
},
{
path: pathRoutes.ADMIN_PATH,
component: <AdminPage />,
path: pathRoutes.SETTINGS_PATH,
component: <SettingsPage />,
},
{
path: pathRoutes.HOST_CONFIG_PATH,
component: <FrigateHostsPage />,
},
{
path: pathRoutes.MAIN_PATH,

View File

@ -0,0 +1,36 @@
import axios from "axios"
import { proxyURL } from "../../shared/env.const"
export interface Config {
key: string,
value: string,
description: string,
encrypted: boolean,
}
export interface PutConfig {
key: string,
value: string,
}
export interface FrigateHost {
id: string
createAt: string
updateAt: string
name: string
host: string
enabled: boolean
}
const instance = axios.create({
baseURL: proxyURL.toString(),
timeout: 30000,
});
export const frigateApi = {
getConfig: () => instance.get<Config[]>('apiv1/config').then(res => res.data),
putConfig: (config: PutConfig[]) => instance.put('apiv1/config', config).then(res => res.data),
getHosts: () => instance.get<FrigateHost[]>('apiv1/frigate-hosts').then(res => {
// return new Map(res.data.map(item => [item.id, item]))
return res.data
}),
}

View File

@ -0,0 +1,48 @@
import { TextInput, TextInputProps, createStyles } from '@mantine/core';
import { useEffect, useState } from 'react';
import classes from './css/FloatingLabelInput.module.css';
interface FloatingLabelInputProps extends TextInputProps {
value?: string,
ecryptedValue?: string,
onChangeValue?: (key: string, value: string) => void
}
export const FloatingLabelInput = (props: FloatingLabelInputProps) => {
const { value: propVal, onChangeValue, ecryptedValue, ...rest } = props
const [focused, setFocused] = useState(false);
const [value, setValue] = useState(propVal || '');
const floating = value?.trim().length !== 0 || focused || undefined;
useEffect(() => {
setValue(propVal || '')
}, [propVal]);
const handleFocused = (event: React.FocusEvent<HTMLInputElement>) => {
setFocused(true)
event.target.select()
}
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.currentTarget.value);
if (onChangeValue && props.name) {
onChangeValue(props.name, event.currentTarget.value);
}
}
return (
<TextInput
value={value}
classNames={classes}
onChange={handleChange}
onFocus={handleFocused}
onBlur={() => setFocused(false)}
mt='2rem'
autoComplete="nope"
data-floating={floating}
labelProps={{ 'data-floating': floating }}
{...rest}
/>
);
}

View File

@ -1,12 +1,12 @@
import React, { useState } from 'react';
import { Avatar, createStyles, Group, Menu, UnstyledButton, Text, Button, Flex } from "@mantine/core";
import { useAuth } from 'react-oidc-context';
import { keycloakConfig } from '../services/keycloack';
import { strings } from '../strings/strings';
import { useMediaQuery } from '@mantine/hooks';
import { dimensions } from '../dimensions/dimensions';
import ColorSchemeToggle from './ColorSchemeToggle';
import { useNavigate } from 'react-router-dom';
import { keycloakConfig } from '../..';
interface UserMenuProps {
user: { name: string; image: string }

View File

@ -0,0 +1,44 @@
.root {
position: relative;
}
.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 {
&::placeholder {
transition: color 150ms ease;
color: transparent;
}
&[data-floating] {
&::placeholder {
color: var(--mantine-color-placeholder);
}
}
}

View File

@ -14,9 +14,12 @@ interface SortedThProps {
const SortedTh = ({ sortedName, reversed, title, onSort, textProps, sorting=true }: SortedThProps) => {
const sorted = sortedName === title
const Icon = sorted ? (reversed ? IconChevronUp : IconChevronDown) : IconSelector;
const handleClick = () => {
if (sorting) onSort(title)
}
return (
<th style={{paddingLeft: 5, paddingRight: 5}}>
<Center onClick={() => onSort(title)}>
<Center onClick={handleClick}>
<Tooltip label={title} transitionProps={{ transition: 'slide-up', duration: 300 }} openDelay={500}>
<Text {...textProps}>{title}</Text>
</Tooltip>

View File

@ -0,0 +1,36 @@
import { Flex, Switch, useMantineTheme } from '@mantine/core';
import { IconBulbFilled, IconBulbOff } from '@tabler/icons-react';
import React from 'react';
interface SwithCellProps {
value?: boolean,
defaultValue?: boolean,
width?: string,
id?: string | number,
propertyName?: string,
toggle?: (id: string | number, propertyName: string, value: string) => void,
}
export const SwitchCell = ( { value, defaultValue, width, id, propertyName, toggle }: SwithCellProps ) => {
const theme = useMantineTheme();
if (typeof value === undefined && typeof defaultValue !== undefined) value = defaultValue
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (id && toggle && propertyName) toggle(id, propertyName, event.target.value)
}
return (
<td style={{ width: width }}>
<Flex w='100%' justify='center'>
<Switch
checked={value}
onChange={handleChange}
size="lg"
onLabel={<IconBulbFilled color={theme.colors.green[5]} size="1.25rem" stroke={1.5} />}
offLabel={<IconBulbOff color={theme.colors.gray[6]} size="1.25rem" stroke={1.5} />}
/>
</Flex>
</td>
)
}
export default SwitchCell;

View File

@ -1,7 +1,7 @@
import React, { useContext, useEffect, useRef, useState } from 'react';
import RowCounter from './RowCounter';
import { Badge, Center, Flex, Group, Text, createStyles } from '@mantine/core';
import { TableAdapter } from './ProductTable';
import { TableAdapter } from '../../../widgets/ProductTable';
import ImageWithPlaceHolder from '../ImageWithPlaceHolder';
import Currency from '../Currency';
import { Context } from '../../..';

View File

@ -0,0 +1,28 @@
import { TextInput } from '@mantine/core';
import React from 'react';
interface TextImputCellProps {
text?: string | number | boolean,
width?: string,
id?: string | number,
propertyName?: string,
onChange?: (
id: string | number,
propertyName: string,
value: string,
) => void,
}
const TextInputCell = ({ text, width, id, propertyName, onChange }: TextImputCellProps) => {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (id && propertyName && onChange)
onChange(id, propertyName, event.currentTarget.value)
}
return (
<td style={{ width: width, textAlign: 'center' }}>
<TextInput onChange={handleChange} size='sm' value={String(text)} />
</td>
)
}
export default TextInputCell;

View File

@ -0,0 +1,30 @@
import { useState, useMemo } from 'react';
type SortDirection = 'asc' | 'desc' | null;
function useSortedData<T, K extends keyof T>(items: T[], initialSortKey: K | null = null) {
const [sortKey, setSortKey] = useState<K | null>(initialSortKey);
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
const sortedItems = useMemo(() => {
if (!sortKey) return items;
return [...items].sort((a, b) => {
if (a[sortKey] < b[sortKey]) return sortDirection === 'asc' ? -1 : 1;
if (a[sortKey] > b[sortKey]) return sortDirection === 'asc' ? 1 : -1;
return 0;
});
}, [items, sortKey, sortDirection]);
const requestSort = (key: K) => {
if (key === sortKey) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortKey(key);
setSortDirection('asc');
}
};
return { items: sortedItems, requestSort, sortKey, sortDirection };
}
export default useSortedData;

View File

@ -3,13 +3,12 @@ const isProduction = appMode === "production"
if (isProduction && typeof process.env.REACT_APP_HOST === 'undefined') {
throw new Error('REACT_APP_HOST environment variable is undefined');
}
export const host = process.env.REACT_APP_HOST || 'localhost'
export const host = process.env.REACT_APP_HOST
if (isProduction && typeof process.env.REACT_APP_PORT === 'undefined') {
throw new Error('REACT_APP_PORT environment variable is undefined');
}
export const port = process.env.REACT_APP_PORT || '4000'
export const hostURL = new URL('http://' + host + ':' + port)
export const port = process.env.REACT_APP_PORT
if (typeof process.env.REACT_APP_FRIGATE_PROXY === 'undefined') {
throw new Error('REACT_APP_FRIGATE_PROXY environment variable is undefined');
@ -19,11 +18,11 @@ export const proxyURL = new URL(process.env.REACT_APP_FRIGATE_PROXY)
if (typeof process.env.REACT_APP_OPENID_SERVER === 'undefined') {
throw new Error('REACT_APP_OPENID_SERVER environment variable is undefined');
}
export const openIdServer = process.env.REACT_APP_OPENID_SERVER
if (typeof process.env.REACT_APP_CLIENT_ID === 'undefined') {
throw new Error('REACT_APP_CLIENT_ID environment variable is undefined');
}
export const clientId = process.env.REACT_APP_CLIENT_ID
export const redirectURL = hostURL.toString()
export const oidpSettings = {
server: process.env.REACT_APP_OPENID_SERVER,
clientId: process.env.REACT_APP_CLIENT_ID,
}

View File

@ -1,15 +0,0 @@
import { AuthProviderProps } from "react-oidc-context";
import { openIdServer, clientId, redirectURL } from "../env.const";
export const keycloakConfig: AuthProviderProps = {
authority: openIdServer,
client_id: clientId,
redirect_uri: redirectURL,
onSigninCallback: () => {
window.history.replaceState(
{},
document.title,
window.location.pathname
)
}
}

View File

@ -1,7 +1,7 @@
import { makeAutoObservable, runInAction } from "mobx";
import { Product } from "./product.store";
import { sleep } from "../utils/async.sleep";
import { TableAdapter } from "../components/table.aps/ProductTable";
import { TableAdapter } from "../../widgets/ProductTable";
import { DeliveryMethod, DeliveryMethods, PaymentMethod, PaymentMethods } from "./orders.store";
import { addItem, removeItemById } from "../utils/array.helper";
import { strings } from "../strings/strings";

View File

@ -1,5 +1,5 @@
import { makeAutoObservable, runInAction } from "mobx";
import { TableAdapter } from "../components/table.aps/ProductTable";
import { TableAdapter } from "../../widgets/ProductTable";
import { GridAdapter } from "../components/grid.aps/ProductGrid";
import { sleep } from "../utils/async.sleep";
import { CartProduct } from "./cart.store";

View File

@ -4,6 +4,7 @@ import { FiltersStore } from "./filters/filters.store";
import { ModalStore } from "./modal.store";
import { OrdersStore } from "./orders.store";
import { ProductStore } from "./product.store";
import { SettingsStore } from "./settings.store";
import { SideBarsStore } from "./sidebars.store";
import PostStore from "./test.store";
import { UserStore } from "./user.store";
@ -18,6 +19,7 @@ class RootStore {
filtersStore: FiltersStore
sideBarsStore: SideBarsStore
ordersStore: OrdersStore
settingsStore: SettingsStore
constructor() {
this.userStore = new UserStore()
this.productStore = new ProductStore(this)
@ -28,6 +30,7 @@ class RootStore {
this.filtersStore = new FiltersStore()
this.sideBarsStore = new SideBarsStore()
this.ordersStore = new OrdersStore()
this.settingsStore = new SettingsStore()
}
}

View File

@ -0,0 +1,4 @@
export class SettingsStore {
}

View File

@ -1,9 +1,9 @@
import { makeAutoObservable, runInAction } from "mobx"
import { User } from "oidc-client-ts";
import { keycloakConfig } from "../services/keycloack";
import { Resource } from "../utils/resource"
import { sleep } from "../utils/async.sleep";
import { z } from 'zod'
import { keycloakConfig } from "../..";
export interface UserServer {

View File

@ -1,4 +1,7 @@
export const headerMenu = {
home:"На главную",
test:"Тест",
settings:"Настройки",
rolesAcess:"Доступ",
hostsConfig:"Серверы Frigate",
}

View File

@ -1,4 +1,9 @@
export const strings = {
host: {
name: 'Имя хоста',
url: 'Адрес',
enabled: 'Включен',
},
// user section
aboutMe: "Обо мне",
settings: "Настройки",
@ -20,6 +25,7 @@ export const strings = {
schedule: "Расписание",
address: "Адрес",
edit: "Изменить",
delete: "Удалить",
error: "Ошибка",
discounts: "Скидки",
delivery: "Доставка",
@ -54,6 +60,7 @@ export const strings = {
weight: "Вес:",
total: "Итого:",
confirm: "Подтвердить",
discard: "Отменить",
next: "Далее",
back: "Назад",
paymentMethod: "Метод оплаты",

View File

@ -0,0 +1,13 @@
export function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...funcArgs: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | null = null;
return function(...args: Parameters<T>): void {
const later = () => {
timeout = null;
func(...args);
};
if (timeout !== null) {
clearTimeout(timeout);
}
timeout = setTimeout(later, wait);
};
}

View File

@ -0,0 +1,19 @@
/**
* Function get array and sort it by index of key
* @param uniqueValue Name of table head, to change image on it
* @param objectIndex Index of head, must be equal to idex of array object
* @param arrayData Array data
* @param reverse If you need to reverse array
* @returns uniqueValue and sorted array
*/
export function sortArrayByObjectIndex<T extends object>(
objectIndex: number,
arrayData: T[],
callBack: (arrayData: T[], key: string | number | symbol) => void,
reverse?: boolean,
) {
if (arrayData.length === 0) throw Error('handleSort failed, array is empty')
const keys = Object.keys(arrayData[0])
const key = keys[objectIndex]
callBack(arrayData, key as keyof T)
}

View File

@ -1,8 +1,8 @@
import { Button, Table } from '@mantine/core';
import React, { useState } from 'react';
import SortedTh from './SortedTh';
import { DeliveryPoint } from '../../stores/user.store';
import { strings } from '../../strings/strings';
import SortedTh from '../shared/components/table.aps/SortedTh';
import { DeliveryPoint } from '../shared/stores/user.store';
import { strings } from '../shared/strings/strings';
import { v4 as uuidv4 } from 'uuid'
@ -20,34 +20,34 @@ const DeliveryPointsTable = ({ data }: DeliveryPointsTableProps) => {
const handleSort = (headName: string, dataIndex: number) => {
const reverse = headName === sortedName ? !reversed : false;
setReversed(reverse)
const keys = Object.keys(data[0]) as Array<keyof DeliveryPoint>
const key = keys[dataIndex]
if (reverse) {
setData(sortByKey(data, dataIndex).reverse())
setData(sortByKey(data, key).reverse())
} else {
setData(sortByKey(data, dataIndex))
setData(sortByKey(data, key))
}
setSortedName(headName)
}
const sortByKey = (deliveryPoints: DeliveryPoint[], keyIndex: number): DeliveryPoint[] => {
const keys = Object.keys(deliveryPoints[0]) as Array<keyof DeliveryPoint>
return deliveryPoints.sort((a, b) => {
const valueA = a[keys[keyIndex]].toLowerCase();
const valueB = b[keys[keyIndex]].toLowerCase();
function sortByKey<T, K extends keyof T>(array: T[], key: K): T[] {
return array.sort((a, b) => {
let valueA = a[key];
let valueB = b[key];
if (valueA < valueB) {
return -1;
}
if (valueA > valueB) {
return 1;
}
const stringValueA = String(valueA).toLowerCase();
const stringValueB = String(valueB).toLowerCase();
if (stringValueA < stringValueB) return -1;
if (stringValueA > stringValueB) return 1;
return 0;
});
}
const headTitle = [
{ dataIndex:1, title: strings.name },
{ dataIndex:2, title: strings.schedule },
{ dataIndex:3, title: strings.address },
{ propertyIndex: 1, title: strings.name },
{ propertyIndex: 2, title: strings.schedule },
{ propertyIndex: 3, title: strings.address },
{ title: '', sorting: false },
]
@ -58,7 +58,7 @@ const DeliveryPointsTable = ({ data }: DeliveryPointsTableProps) => {
title={head.title}
reversed={reversed}
sortedName={sortedName}
onSort={() => handleSort(head.title, head.dataIndex ? head.dataIndex : 0)}
onSort={() => handleSort(head.title, head.propertyIndex ? head.propertyIndex : 0)}
sorting={head.sorting} />
)
})

View File

@ -0,0 +1,144 @@
import { Button, Flex, Switch, Table, Text, TextInput, useMantineTheme } from '@mantine/core';
import React, { useState } from 'react';
import SortedTh from '../shared/components/table.aps/SortedTh';
import { strings } from '../shared/strings/strings';
import { v4 as uuidv4 } from 'uuid'
import { FrigateHost } from '../services/frigate.proxy/frigate.api';
import { IconBulbFilled, IconBulbOff, IconDeviceFloppy, IconPencil, IconPlus, IconSettings, IconTrash } from '@tabler/icons-react';
import SwitchCell from '../shared/components/table.aps/SwitchCell';
import TextInputCell from '../shared/components/table.aps/TextInputCell';
interface FrigateHostsTableProps {
data: FrigateHost[],
showAddButton?: boolean,
handleInputChange?: () => void,
handleSwtitchToggle?: () => void,
}
const FrigateHostsTable = ({ data, showAddButton = false, handleInputChange, handleSwtitchToggle }: FrigateHostsTableProps) => {
const [tableData, setData] = useState(data)
const [reversed, setReversed] = useState(false)
const [sortedName, setSortedName] = useState<string | null>(null)
function sortByKey<T, K extends keyof T>(array: T[], key: K): T[] {
return array.sort((a, b) => {
let valueA = a[key];
let valueB = b[key];
const stringValueA = String(valueA).toLowerCase();
const stringValueB = String(valueB).toLowerCase();
if (stringValueA < stringValueB) return -1;
if (stringValueA > stringValueB) return 1;
return 0;
});
}
const handleSort = (headName: string, propertyName: string,) => {
const reverse = headName === sortedName ? !reversed : false;
setReversed(reverse)
const arr = sortByKey(tableData, propertyName as keyof FrigateHost)
if (reverse) arr.reverse()
setData(arr)
setSortedName(headName)
}
const headTitle = [
{ propertyName: 'name', title: strings.host.name },
{ propertyName: 'host', title: strings.host.url },
{ propertyName: 'enabled', title: strings.host.enabled },
{ title: '', sorting: false },
]
const tableHead = headTitle.map(head => {
return (
<SortedTh
key={uuidv4()}
title={head.title}
reversed={reversed}
sortedName={sortedName}
onSort={() => handleSort(head.title, head.propertyName ? head.propertyName : '')}
sorting={head.sorting} />
)
})
const handleTextChange = (id: string | number, propertyName: string, value: string,) => {
setData(tableData.map(item => {
if (item.id === id) {
return {
...item,
[propertyName]: value,
};
}
return item;
}));
}
const handleSwitchChange = (id: string | number, propertyName: string, value: string,) => {
setData(tableData.map(item => {
if (item.id === id) {
return {
...item,
[propertyName]: !item.enabled,
};
}
return item;
}));
}
const handleDeleteRow = (id: string | number) => {
setData(tableData.filter(item => item.id !== id))
}
const handleAddRow = () => {
const newHost: FrigateHost = {
id: String(Math.random()),
createAt: '',
updateAt: '',
host: '',
name: '',
enabled: true
}
setData(prevTableData => [...prevTableData, newHost])
}
const rows = tableData.map(item => {
return (
<tr key={item.id}>
<TextInputCell text={item.name} width='40%' id={item.id} propertyName='name' onChange={handleTextChange} />
<TextInputCell text={item.host} width='40%' id={item.id} propertyName='host' onChange={handleTextChange} />
{/* {textInputCell(item.host, '40%', item.id, 'host', handleTextChange)} */}
<SwitchCell value={item.enabled} width='10%' id={item.id} propertyName='enabled' toggle={handleSwitchChange} />
<td align='right' style={{ width: '10%', padding: '0', }}>
<Flex justify='center'>
<Button size='xs' ><IconSettings /></Button>
<Button size='xs' ><IconDeviceFloppy /></Button>
<Button size='xs' onClick={() => handleDeleteRow(item.id)}><IconTrash /></Button>
</Flex>
</td>
</tr>
)
})
return (
<div>
<Table >
<thead>
<tr>
{tableHead}
</tr>
</thead>
<tbody>
{rows}
</tbody>
</Table>
{showAddButton ?
<Flex w='100%' justify='end'>
<Button size='xs' onClick={() => handleAddRow()}><IconPlus /></Button>
</Flex>
: <></>
}
</div>
);
};
export default FrigateHostsTable;

View File

@ -1,11 +1,11 @@
import { Table, } from '@mantine/core';
import { useContext, useEffect, useState } from 'react';
import { useDisclosure, useHotkeys, } from '@mantine/hooks';
import TableRow from './TableRow';
import InputModal from '../InputModal';
import { Context } from '../../..';
import TableRow from '../shared/components/table.aps/TableRow';
import InputModal from '../shared/components/InputModal';
import { Context } from '..';
import { v4 as uuidv4 } from 'uuid'
import ProductsTableHead from './ProductsTableHead';
import ProductsTableHead from '../shared/components/table.aps/ProductsTableHead';
import { observer } from 'mobx-react-lite';
export type TableAdapter = {

View File

@ -7,5 +7,8 @@ export const testHeaderLinks: HeaderActionProps =
links: [
{link: pathRoutes.MAIN_PATH, label: headerMenu.home, links: []},
{link: pathRoutes.TEST_PATH, label: headerMenu.test, links: []},
{link: pathRoutes.SETTINGS_PATH, label: headerMenu.settings, links: []},
{link: pathRoutes.HOST_CONFIG_PATH, label: headerMenu.hostsConfig, links: []},
{link: pathRoutes.ROLES_PATH, label: headerMenu.rolesAcess, links: []},
]
}

View File

@ -2150,6 +2150,18 @@
dependencies:
remove-accents "0.4.2"
"@tanstack/query-core@5.21.2":
version "5.21.2"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.21.2.tgz#543bc44f09274d965b54640567de4cd8ccbe2831"
integrity sha512-jg7OcDG44oLT3uuGQQ9BM65ZBIdAq9xNZXPEk7Gr6w1oM1wo2/95H3dPDjLVs0yZKwrmE/ORUOC2Pyi1sp2fDA==
"@tanstack/react-query@^5.21.2":
version "5.21.2"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.21.2.tgz#6bdf30f8afa7623989ce6b7bfda1a9fefe6b1811"
integrity sha512-/Vv1qTumNDDVA5EYk40kivHZ2kICs1w38GBLRvV6A/lrixUJR5bfqZMKqHA1S6ND3gR9hvSyAAYejBbjLrQnSA==
dependencies:
"@tanstack/query-core" "5.21.2"
"@tanstack/react-table@8.9.3":
version "8.9.3"
resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.9.3.tgz#03a52e9e15f65c82a8c697a445c42bfca0c5cfc4"