add setting
add frigate configs
This commit is contained in:
parent
ece046d2fd
commit
d68edcf6f2
1
.gitignore
vendored
1
.gitignore
vendored
@ -21,6 +21,7 @@
|
|||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.idea/*
|
.idea/*
|
||||||
|
.vscode
|
||||||
|
|
||||||
#docker
|
#docker
|
||||||
docker-compose.*
|
docker-compose.*
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
"@mantine/dates": "^6.0.16",
|
"@mantine/dates": "^6.0.16",
|
||||||
"@mantine/hooks": "^6.0.16",
|
"@mantine/hooks": "^6.0.16",
|
||||||
"@tabler/icons-react": "^2.24.0",
|
"@tabler/icons-react": "^2.24.0",
|
||||||
|
"@tanstack/react-query": "^5.21.2",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^13.0.0",
|
"@testing-library/react": "^13.0.0",
|
||||||
"@testing-library/user-event": "^13.2.1",
|
"@testing-library/user-event": "^13.2.1",
|
||||||
|
|||||||
29
src/App.tsx
29
src/App.tsx
@ -9,9 +9,10 @@ import FullImageModal from './shared/components/FullImageModal';
|
|||||||
import AppBody from './AppBody';
|
import AppBody from './AppBody';
|
||||||
import FullProductModal from './shared/components/FullProductModal';
|
import FullProductModal from './shared/components/FullProductModal';
|
||||||
import Forbidden from './pages/403';
|
import Forbidden from './pages/403';
|
||||||
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// const auth = useAuth();
|
|
||||||
const systemColorScheme = useColorScheme()
|
const systemColorScheme = useColorScheme()
|
||||||
const [colorScheme, setColorScheme] = useState<ColorScheme>(getCookie('mantine-color-scheme') as ColorScheme || systemColorScheme);
|
const [colorScheme, setColorScheme] = useState<ColorScheme>(getCookie('mantine-color-scheme') as ColorScheme || systemColorScheme);
|
||||||
const toggleColorScheme = (value?: ColorScheme) => {
|
const toggleColorScheme = (value?: ColorScheme) => {
|
||||||
@ -20,20 +21,22 @@ function App() {
|
|||||||
setCookie('mantine-color-scheme', nextColorScheme, { maxAge: 60 * 60 * 24 * 30 });
|
setCookie('mantine-color-scheme', nextColorScheme, { maxAge: 60 * 60 * 24 * 30 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auth = useAuth();
|
||||||
// automatically sign-in
|
// automatically sign-in
|
||||||
// useEffect(() => {
|
useEffect(() => {
|
||||||
// if (!hasAuthParams() &&
|
if (!hasAuthParams() &&
|
||||||
// !auth.isAuthenticated && !auth.activeNavigator && !auth.isLoading) {
|
!auth.isAuthenticated && !auth.activeNavigator && !auth.isLoading) {
|
||||||
// auth.signinRedirect();
|
auth.signinRedirect();
|
||||||
// }
|
}
|
||||||
// }, [auth, auth.isAuthenticated, auth.activeNavigator, auth.isLoading, auth.signinRedirect]);
|
}, [auth, auth.isAuthenticated, auth.activeNavigator, auth.isLoading, auth.signinRedirect]);
|
||||||
|
|
||||||
// if (auth.activeNavigator || auth.isLoading) {
|
if (auth.activeNavigator || auth.isLoading) {
|
||||||
// return <CenterLoader />
|
return <CenterLoader />
|
||||||
// }
|
}
|
||||||
// if ((!auth.isAuthenticated && !auth.isLoading) || auth.error) {
|
if ((!auth.isAuthenticated && !auth.isLoading) || auth.error) {
|
||||||
// return <Forbidden />
|
console.error(`auth.error:`, auth.error)
|
||||||
// }
|
return <Forbidden />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
|
|||||||
@ -1,28 +1,33 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { AppShell, useMantineTheme, } from "@mantine/core"
|
import { AppShell, useMantineTheme, } from "@mantine/core"
|
||||||
import LeftSideBar from './widgets/LeftSideBar';
|
|
||||||
import RightSideBar from './widgets/RightSideBar';
|
|
||||||
import { HeaderAction } from './widgets/header/HeaderAction';
|
import { HeaderAction } from './widgets/header/HeaderAction';
|
||||||
import { testHeaderLinks } from './widgets/header/header.links';
|
import { testHeaderLinks } from './widgets/header/header.links';
|
||||||
import AppRouter from './router/AppRouter';
|
import AppRouter from './router/AppRouter';
|
||||||
import { SideBar } from './shared/components/SideBar';
|
import { SideBar } from './shared/components/SideBar';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
const AppBody = () => {
|
const AppBody = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("render Main")
|
console.log("render Main")
|
||||||
})
|
})
|
||||||
|
|
||||||
const [leftSideBar, setLeftSidebar] = useState(true)
|
const [leftSideBar, setLeftSidebar] = useState(false)
|
||||||
const [rightSideBar, setRightSidebar] = useState(true)
|
const [rightSideBar, setRightSidebar] = useState(false)
|
||||||
|
|
||||||
|
|
||||||
const leftSideBarIsHidden = (isHidden: boolean) => {
|
const leftSideBarIsHidden = (isHidden: boolean) => {
|
||||||
setLeftSidebar(!isHidden)
|
setLeftSidebar(!isHidden)
|
||||||
}
|
}
|
||||||
|
const rightSideBarIsHidden = (isHidden: boolean) => {
|
||||||
|
setRightSidebar(!isHidden)
|
||||||
|
}
|
||||||
|
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<AppShell
|
<AppShell
|
||||||
styles={{
|
styles={{
|
||||||
main: {
|
main: {
|
||||||
@ -37,12 +42,13 @@ const AppBody = () => {
|
|||||||
header={
|
header={
|
||||||
<HeaderAction links={testHeaderLinks.links} />
|
<HeaderAction links={testHeaderLinks.links} />
|
||||||
}
|
}
|
||||||
navbar={
|
aside={
|
||||||
<SideBar isHidden={leftSideBarIsHidden} side="left" />
|
<SideBar isHidden={rightSideBarIsHidden} side="right" />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</AppShell>
|
</AppShell>
|
||||||
|
</QueryClientProvider>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,13 +3,28 @@ import ReactDOM from 'react-dom/client';
|
|||||||
import App from './App';
|
import App from './App';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
import RootStore from './shared/stores/root.store';
|
import RootStore from './shared/stores/root.store';
|
||||||
import { AuthProvider } from 'react-oidc-context';
|
import { AuthProvider, AuthProviderProps } from 'react-oidc-context';
|
||||||
import { keycloakConfig } from './shared/services/keycloack';
|
import { oidpSettings } from './shared/env.const';
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement
|
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()
|
const rootStore = new RootStore()
|
||||||
export const Context = createContext<RootStore>(rootStore)
|
export const Context = createContext<RootStore>(rootStore)
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const AdminPage = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdminPage;
|
|
||||||
26
src/pages/FrigateHostsPage.tsx
Normal file
26
src/pages/FrigateHostsPage.tsx
Normal 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;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Container, Flex, Group, Text } from '@mantine/core';
|
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 HeadSearch from '../shared/components/HeadSearch';
|
||||||
import ViewSelector, { SelectorViewState } from '../shared/components/ViewSelector';
|
import ViewSelector, { SelectorViewState } from '../shared/components/ViewSelector';
|
||||||
import { useContext, useState, useEffect } from 'react';
|
import { useContext, useState, useEffect } from 'react';
|
||||||
|
|||||||
122
src/pages/SettingsPage.tsx
Normal file
122
src/pages/SettingsPage.tsx
Normal 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;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { hostURL } from "../shared/env.const"
|
import { hostURL } from ".."
|
||||||
|
|
||||||
export const cameraLiveViewURL = (host: string, cameraName: string) => {
|
export const cameraLiveViewURL = (host: string, cameraName: string) => {
|
||||||
return `ws://${hostURL.host}/proxy-ws/live/jsmpeg/${cameraName}?hostName=${host}`
|
return `ws://${hostURL.host}/proxy-ws/live/jsmpeg/${cameraName}?hostName=${host}`
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
export const pathRoutes = {
|
export const pathRoutes = {
|
||||||
MAIN_PATH: '/',
|
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',
|
THANKS_PATH: '/thanks',
|
||||||
USER_DETAILED_PATH: '/user',
|
USER_DETAILED_PATH: '/user',
|
||||||
RETRY_ERROR_PATH: '/retry_error',
|
RETRY_ERROR_PATH: '/retry_error',
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import {pathRoutes} from "./routes.path";
|
|||||||
import RetryError from "../pages/RetryError";
|
import RetryError from "../pages/RetryError";
|
||||||
import Forbidden from "../pages/403";
|
import Forbidden from "../pages/403";
|
||||||
import NotFound from "../pages/404";
|
import NotFound from "../pages/404";
|
||||||
import AdminPage from "../pages/Admin";
|
import SettingsPage from "../pages/SettingsPage";
|
||||||
|
import FrigateHostsPage from "../pages/FrigateHostsPage";
|
||||||
|
|
||||||
interface IRoute {
|
interface IRoute {
|
||||||
path: string,
|
path: string,
|
||||||
@ -18,8 +19,12 @@ export const routes: IRoute[] = [
|
|||||||
component: <Test />,
|
component: <Test />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: pathRoutes.ADMIN_PATH,
|
path: pathRoutes.SETTINGS_PATH,
|
||||||
component: <AdminPage />,
|
component: <SettingsPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: pathRoutes.HOST_CONFIG_PATH,
|
||||||
|
component: <FrigateHostsPage />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: pathRoutes.MAIN_PATH,
|
path: pathRoutes.MAIN_PATH,
|
||||||
|
|||||||
36
src/services/frigate.proxy/frigate.api.ts
Normal file
36
src/services/frigate.proxy/frigate.api.ts
Normal 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
|
||||||
|
}),
|
||||||
|
}
|
||||||
48
src/shared/components/FloatingLabelInput.tsx
Normal file
48
src/shared/components/FloatingLabelInput.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Avatar, createStyles, Group, Menu, UnstyledButton, Text, Button, Flex } from "@mantine/core";
|
import { Avatar, createStyles, Group, Menu, UnstyledButton, Text, Button, Flex } from "@mantine/core";
|
||||||
import { useAuth } from 'react-oidc-context';
|
import { useAuth } from 'react-oidc-context';
|
||||||
import { keycloakConfig } from '../services/keycloack';
|
|
||||||
import { strings } from '../strings/strings';
|
import { strings } from '../strings/strings';
|
||||||
import { useMediaQuery } from '@mantine/hooks';
|
import { useMediaQuery } from '@mantine/hooks';
|
||||||
import { dimensions } from '../dimensions/dimensions';
|
import { dimensions } from '../dimensions/dimensions';
|
||||||
import ColorSchemeToggle from './ColorSchemeToggle';
|
import ColorSchemeToggle from './ColorSchemeToggle';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { keycloakConfig } from '../..';
|
||||||
|
|
||||||
interface UserMenuProps {
|
interface UserMenuProps {
|
||||||
user: { name: string; image: string }
|
user: { name: string; image: string }
|
||||||
|
|||||||
44
src/shared/components/css/FloatingLabelInput.module.css
Normal file
44
src/shared/components/css/FloatingLabelInput.module.css
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,9 +14,12 @@ interface SortedThProps {
|
|||||||
const SortedTh = ({ sortedName, reversed, title, onSort, textProps, sorting=true }: SortedThProps) => {
|
const SortedTh = ({ sortedName, reversed, title, onSort, textProps, sorting=true }: SortedThProps) => {
|
||||||
const sorted = sortedName === title
|
const sorted = sortedName === title
|
||||||
const Icon = sorted ? (reversed ? IconChevronUp : IconChevronDown) : IconSelector;
|
const Icon = sorted ? (reversed ? IconChevronUp : IconChevronDown) : IconSelector;
|
||||||
|
const handleClick = () => {
|
||||||
|
if (sorting) onSort(title)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<th style={{paddingLeft: 5, paddingRight: 5}}>
|
<th style={{paddingLeft: 5, paddingRight: 5}}>
|
||||||
<Center onClick={() => onSort(title)}>
|
<Center onClick={handleClick}>
|
||||||
<Tooltip label={title} transitionProps={{ transition: 'slide-up', duration: 300 }} openDelay={500}>
|
<Tooltip label={title} transitionProps={{ transition: 'slide-up', duration: 300 }} openDelay={500}>
|
||||||
<Text {...textProps}>{title}</Text>
|
<Text {...textProps}>{title}</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
36
src/shared/components/table.aps/SwitchCell.tsx
Normal file
36
src/shared/components/table.aps/SwitchCell.tsx
Normal 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;
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||||
import RowCounter from './RowCounter';
|
import RowCounter from './RowCounter';
|
||||||
import { Badge, Center, Flex, Group, Text, createStyles } from '@mantine/core';
|
import { Badge, Center, Flex, Group, Text, createStyles } from '@mantine/core';
|
||||||
import { TableAdapter } from './ProductTable';
|
import { TableAdapter } from '../../../widgets/ProductTable';
|
||||||
import ImageWithPlaceHolder from '../ImageWithPlaceHolder';
|
import ImageWithPlaceHolder from '../ImageWithPlaceHolder';
|
||||||
import Currency from '../Currency';
|
import Currency from '../Currency';
|
||||||
import { Context } from '../../..';
|
import { Context } from '../../..';
|
||||||
|
|||||||
28
src/shared/components/table.aps/TextInputCell.tsx
Normal file
28
src/shared/components/table.aps/TextInputCell.tsx
Normal 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;
|
||||||
30
src/shared/components/table.aps/useSortedData.ts
Normal file
30
src/shared/components/table.aps/useSortedData.ts
Normal 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;
|
||||||
@ -3,13 +3,12 @@ const isProduction = appMode === "production"
|
|||||||
if (isProduction && typeof process.env.REACT_APP_HOST === 'undefined') {
|
if (isProduction && typeof process.env.REACT_APP_HOST === 'undefined') {
|
||||||
throw new Error('REACT_APP_HOST environment variable is 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') {
|
if (isProduction && typeof process.env.REACT_APP_PORT === 'undefined') {
|
||||||
throw new Error('REACT_APP_PORT environment variable is undefined');
|
throw new Error('REACT_APP_PORT environment variable is undefined');
|
||||||
}
|
}
|
||||||
export const port = process.env.REACT_APP_PORT || '4000'
|
export const port = process.env.REACT_APP_PORT
|
||||||
export const hostURL = new URL('http://' + host + ':' + port)
|
|
||||||
|
|
||||||
if (typeof process.env.REACT_APP_FRIGATE_PROXY === 'undefined') {
|
if (typeof process.env.REACT_APP_FRIGATE_PROXY === 'undefined') {
|
||||||
throw new Error('REACT_APP_FRIGATE_PROXY environment variable is 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') {
|
if (typeof process.env.REACT_APP_OPENID_SERVER === 'undefined') {
|
||||||
throw new Error('REACT_APP_OPENID_SERVER environment variable is 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') {
|
if (typeof process.env.REACT_APP_CLIENT_ID === 'undefined') {
|
||||||
throw new Error('REACT_APP_CLIENT_ID environment variable is 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,
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { makeAutoObservable, runInAction } from "mobx";
|
import { makeAutoObservable, runInAction } from "mobx";
|
||||||
import { Product } from "./product.store";
|
import { Product } from "./product.store";
|
||||||
import { sleep } from "../utils/async.sleep";
|
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 { DeliveryMethod, DeliveryMethods, PaymentMethod, PaymentMethods } from "./orders.store";
|
||||||
import { addItem, removeItemById } from "../utils/array.helper";
|
import { addItem, removeItemById } from "../utils/array.helper";
|
||||||
import { strings } from "../strings/strings";
|
import { strings } from "../strings/strings";
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { makeAutoObservable, runInAction } from "mobx";
|
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 { GridAdapter } from "../components/grid.aps/ProductGrid";
|
||||||
import { sleep } from "../utils/async.sleep";
|
import { sleep } from "../utils/async.sleep";
|
||||||
import { CartProduct } from "./cart.store";
|
import { CartProduct } from "./cart.store";
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { FiltersStore } from "./filters/filters.store";
|
|||||||
import { ModalStore } from "./modal.store";
|
import { ModalStore } from "./modal.store";
|
||||||
import { OrdersStore } from "./orders.store";
|
import { OrdersStore } from "./orders.store";
|
||||||
import { ProductStore } from "./product.store";
|
import { ProductStore } from "./product.store";
|
||||||
|
import { SettingsStore } from "./settings.store";
|
||||||
import { SideBarsStore } from "./sidebars.store";
|
import { SideBarsStore } from "./sidebars.store";
|
||||||
import PostStore from "./test.store";
|
import PostStore from "./test.store";
|
||||||
import { UserStore } from "./user.store";
|
import { UserStore } from "./user.store";
|
||||||
@ -18,6 +19,7 @@ class RootStore {
|
|||||||
filtersStore: FiltersStore
|
filtersStore: FiltersStore
|
||||||
sideBarsStore: SideBarsStore
|
sideBarsStore: SideBarsStore
|
||||||
ordersStore: OrdersStore
|
ordersStore: OrdersStore
|
||||||
|
settingsStore: SettingsStore
|
||||||
constructor() {
|
constructor() {
|
||||||
this.userStore = new UserStore()
|
this.userStore = new UserStore()
|
||||||
this.productStore = new ProductStore(this)
|
this.productStore = new ProductStore(this)
|
||||||
@ -28,6 +30,7 @@ class RootStore {
|
|||||||
this.filtersStore = new FiltersStore()
|
this.filtersStore = new FiltersStore()
|
||||||
this.sideBarsStore = new SideBarsStore()
|
this.sideBarsStore = new SideBarsStore()
|
||||||
this.ordersStore = new OrdersStore()
|
this.ordersStore = new OrdersStore()
|
||||||
|
this.settingsStore = new SettingsStore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
src/shared/stores/settings.store.ts
Normal file
4
src/shared/stores/settings.store.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export class SettingsStore {
|
||||||
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { makeAutoObservable, runInAction } from "mobx"
|
import { makeAutoObservable, runInAction } from "mobx"
|
||||||
import { User } from "oidc-client-ts";
|
import { User } from "oidc-client-ts";
|
||||||
import { keycloakConfig } from "../services/keycloack";
|
|
||||||
import { Resource } from "../utils/resource"
|
import { Resource } from "../utils/resource"
|
||||||
import { sleep } from "../utils/async.sleep";
|
import { sleep } from "../utils/async.sleep";
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { keycloakConfig } from "../..";
|
||||||
|
|
||||||
|
|
||||||
export interface UserServer {
|
export interface UserServer {
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
export const headerMenu = {
|
export const headerMenu = {
|
||||||
home:"На главную",
|
home:"На главную",
|
||||||
test:"Тест",
|
test:"Тест",
|
||||||
|
settings:"Настройки",
|
||||||
|
rolesAcess:"Доступ",
|
||||||
|
hostsConfig:"Серверы Frigate",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
export const strings = {
|
export const strings = {
|
||||||
|
host: {
|
||||||
|
name: 'Имя хоста',
|
||||||
|
url: 'Адрес',
|
||||||
|
enabled: 'Включен',
|
||||||
|
},
|
||||||
// user section
|
// user section
|
||||||
aboutMe: "Обо мне",
|
aboutMe: "Обо мне",
|
||||||
settings: "Настройки",
|
settings: "Настройки",
|
||||||
@ -20,6 +25,7 @@ export const strings = {
|
|||||||
schedule: "Расписание",
|
schedule: "Расписание",
|
||||||
address: "Адрес",
|
address: "Адрес",
|
||||||
edit: "Изменить",
|
edit: "Изменить",
|
||||||
|
delete: "Удалить",
|
||||||
error: "Ошибка",
|
error: "Ошибка",
|
||||||
discounts: "Скидки",
|
discounts: "Скидки",
|
||||||
delivery: "Доставка",
|
delivery: "Доставка",
|
||||||
@ -54,6 +60,7 @@ export const strings = {
|
|||||||
weight: "Вес:",
|
weight: "Вес:",
|
||||||
total: "Итого:",
|
total: "Итого:",
|
||||||
confirm: "Подтвердить",
|
confirm: "Подтвердить",
|
||||||
|
discard: "Отменить",
|
||||||
next: "Далее",
|
next: "Далее",
|
||||||
back: "Назад",
|
back: "Назад",
|
||||||
paymentMethod: "Метод оплаты",
|
paymentMethod: "Метод оплаты",
|
||||||
|
|||||||
13
src/shared/utils/debounce.ts
Normal file
13
src/shared/utils/debounce.ts
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
19
src/shared/utils/sort.array.ts
Normal file
19
src/shared/utils/sort.array.ts
Normal 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)
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { Button, Table } from '@mantine/core';
|
import { Button, Table } from '@mantine/core';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import SortedTh from './SortedTh';
|
import SortedTh from '../shared/components/table.aps/SortedTh';
|
||||||
import { DeliveryPoint } from '../../stores/user.store';
|
import { DeliveryPoint } from '../shared/stores/user.store';
|
||||||
import { strings } from '../../strings/strings';
|
import { strings } from '../shared/strings/strings';
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
|
||||||
@ -20,34 +20,34 @@ const DeliveryPointsTable = ({ data }: DeliveryPointsTableProps) => {
|
|||||||
const handleSort = (headName: string, dataIndex: number) => {
|
const handleSort = (headName: string, dataIndex: number) => {
|
||||||
const reverse = headName === sortedName ? !reversed : false;
|
const reverse = headName === sortedName ? !reversed : false;
|
||||||
setReversed(reverse)
|
setReversed(reverse)
|
||||||
|
const keys = Object.keys(data[0]) as Array<keyof DeliveryPoint>
|
||||||
|
const key = keys[dataIndex]
|
||||||
if (reverse) {
|
if (reverse) {
|
||||||
setData(sortByKey(data, dataIndex).reverse())
|
setData(sortByKey(data, key).reverse())
|
||||||
} else {
|
} else {
|
||||||
setData(sortByKey(data, dataIndex))
|
setData(sortByKey(data, key))
|
||||||
}
|
}
|
||||||
setSortedName(headName)
|
setSortedName(headName)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortByKey = (deliveryPoints: DeliveryPoint[], keyIndex: number): DeliveryPoint[] => {
|
function sortByKey<T, K extends keyof T>(array: T[], key: K): T[] {
|
||||||
const keys = Object.keys(deliveryPoints[0]) as Array<keyof DeliveryPoint>
|
return array.sort((a, b) => {
|
||||||
return deliveryPoints.sort((a, b) => {
|
let valueA = a[key];
|
||||||
const valueA = a[keys[keyIndex]].toLowerCase();
|
let valueB = b[key];
|
||||||
const valueB = b[keys[keyIndex]].toLowerCase();
|
|
||||||
|
|
||||||
if (valueA < valueB) {
|
const stringValueA = String(valueA).toLowerCase();
|
||||||
return -1;
|
const stringValueB = String(valueB).toLowerCase();
|
||||||
}
|
|
||||||
if (valueA > valueB) {
|
if (stringValueA < stringValueB) return -1;
|
||||||
return 1;
|
if (stringValueA > stringValueB) return 1;
|
||||||
}
|
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const headTitle = [
|
const headTitle = [
|
||||||
{ dataIndex:1, title: strings.name },
|
{ propertyIndex: 1, title: strings.name },
|
||||||
{ dataIndex:2, title: strings.schedule },
|
{ propertyIndex: 2, title: strings.schedule },
|
||||||
{ dataIndex:3, title: strings.address },
|
{ propertyIndex: 3, title: strings.address },
|
||||||
{ title: '', sorting: false },
|
{ title: '', sorting: false },
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ const DeliveryPointsTable = ({ data }: DeliveryPointsTableProps) => {
|
|||||||
title={head.title}
|
title={head.title}
|
||||||
reversed={reversed}
|
reversed={reversed}
|
||||||
sortedName={sortedName}
|
sortedName={sortedName}
|
||||||
onSort={() => handleSort(head.title, head.dataIndex ? head.dataIndex : 0)}
|
onSort={() => handleSort(head.title, head.propertyIndex ? head.propertyIndex : 0)}
|
||||||
sorting={head.sorting} />
|
sorting={head.sorting} />
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
144
src/widgets/FrigateHostsTable.tsx
Normal file
144
src/widgets/FrigateHostsTable.tsx
Normal 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;
|
||||||
@ -1,11 +1,11 @@
|
|||||||
import { Table, } from '@mantine/core';
|
import { Table, } from '@mantine/core';
|
||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
import { useDisclosure, useHotkeys, } from '@mantine/hooks';
|
import { useDisclosure, useHotkeys, } from '@mantine/hooks';
|
||||||
import TableRow from './TableRow';
|
import TableRow from '../shared/components/table.aps/TableRow';
|
||||||
import InputModal from '../InputModal';
|
import InputModal from '../shared/components/InputModal';
|
||||||
import { Context } from '../../..';
|
import { Context } from '..';
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import ProductsTableHead from './ProductsTableHead';
|
import ProductsTableHead from '../shared/components/table.aps/ProductsTableHead';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
export type TableAdapter = {
|
export type TableAdapter = {
|
||||||
@ -7,5 +7,8 @@ export const testHeaderLinks: HeaderActionProps =
|
|||||||
links: [
|
links: [
|
||||||
{link: pathRoutes.MAIN_PATH, label: headerMenu.home, links: []},
|
{link: pathRoutes.MAIN_PATH, label: headerMenu.home, links: []},
|
||||||
{link: pathRoutes.TEST_PATH, label: headerMenu.test, 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: []},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
12
yarn.lock
12
yarn.lock
@ -2150,6 +2150,18 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
remove-accents "0.4.2"
|
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":
|
"@tanstack/react-table@8.9.3":
|
||||||
version "8.9.3"
|
version "8.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.9.3.tgz#03a52e9e15f65c82a8c697a445c42bfca0c5cfc4"
|
resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.9.3.tgz#03a52e9e15f65c82a8c697a445c42bfca0c5cfc4"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user