mirror of
https://github.com/Telecominfraproject/wlan-cloud-owprov-ui.git
synced 2025-10-29 17:52:25 +00:00
[WIFI-12575] Theme improvements
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
@@ -15,7 +15,6 @@
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"ignorePatterns": ["build/", "dist/"],
|
||||
"plugins": ["import", "react", "@typescript-eslint", "prettier"],
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
@@ -27,6 +26,7 @@
|
||||
"plugin:import/warnings",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
"plugins": ["import", "react", "@typescript-eslint", "prettier"],
|
||||
"rules": {
|
||||
"import/extensions": [
|
||||
"error",
|
||||
@@ -69,6 +69,7 @@
|
||||
],
|
||||
"max-len": ["error", { "code": 150 }],
|
||||
"@typescript-eslint/ban-ts-comment": ["off"],
|
||||
"import/prefer-default-export": ["off"],
|
||||
"react/prop-types": ["warn"],
|
||||
"react/require-default-props": "off",
|
||||
"react/jsx-props-no-spreading": ["off"],
|
||||
|
||||
8277
package-lock.json
generated
8277
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
85
package.json
85
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.10.0(2)",
|
||||
"version": "2.10.0(7)",
|
||||
"description": "",
|
||||
"main": "index.tsx",
|
||||
"scripts": {
|
||||
@@ -14,73 +14,73 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.11",
|
||||
"@chakra-ui/anatomy": "^2.1.1",
|
||||
"@chakra-ui/icons": "^2.0.18",
|
||||
"@chakra-ui/react": "^2.3.6",
|
||||
"@chakra-ui/theme-tools": "^2.0.12",
|
||||
"@chakra-ui/utils": "^2.0.11",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@fontsource/inter": "^4.5.14",
|
||||
"@chakra-ui/utils": "^2.0.14",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@fontsource/inter": "^4.5.15",
|
||||
"@hello-pangea/dnd": "^16.2.0",
|
||||
"@nivo/circle-packing": "^0.80.0",
|
||||
"@nivo/core": "^0.80.0",
|
||||
"@phosphor-icons/react": "^2.0.8",
|
||||
"@react-spring/web": "^9.5.5",
|
||||
"axios": "^1.1.3",
|
||||
"@tanstack/react-query": "^4.29.3",
|
||||
"@tanstack/react-table": "^8.8.5",
|
||||
"axios": "^1.3.5",
|
||||
"buffer": "^6.0.3",
|
||||
"chakra-react-select": "^4.3.0",
|
||||
"cronstrue": "2.14.0",
|
||||
"chakra-react-select": "^4.6.0",
|
||||
"cronstrue": "2.26.0",
|
||||
"currency-codes": "^2.1.0",
|
||||
"dagre": "^0.8.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"formik": "^2.2.9",
|
||||
"framer-motion": "^6.3.6",
|
||||
"i18next": "^22.0.0",
|
||||
"i18next-browser-languagedetector": "^6.1.8",
|
||||
"i18next-http-backend": "^1.4.4",
|
||||
"libphonenumber-js": "^1.10.14",
|
||||
"papaparse": "^5.3.2",
|
||||
"phosphor-react": "^1.4.1",
|
||||
"framer-motion": "^10.12.3",
|
||||
"i18next": "^22.4.14",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"i18next-http-backend": "^2.2.0",
|
||||
"libphonenumber-js": "^1.10.28",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"papaparse": "^5.4.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-app-polyfill": "^3.0.0",
|
||||
"react-country-flag": "^3.0.2",
|
||||
"react-country-flag": "^3.1.0",
|
||||
"react-csv": "^2.2.2",
|
||||
"react-datepicker": "^4.8.0",
|
||||
"react-datepicker": "^4.11.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"react-fast-compare": "^3.2.1",
|
||||
"react-flow-renderer": "^10.3.17",
|
||||
"react-full-screen": "^1.1.1",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-i18next": "^12.2.0",
|
||||
"react-masonry-css": "^1.0.16",
|
||||
"react-papaparse": "^4.1.0",
|
||||
"@tanstack/react-query": "^4.12.0",
|
||||
"react-router-dom": "^6.4.2",
|
||||
"react-router-dom": "^6.10.0",
|
||||
"react-table": "^7.8.0",
|
||||
"react-tooltip": "^4.4.2",
|
||||
"react-virtualized-auto-sizer": "^1.0.7",
|
||||
"react-window": "^1.8.8",
|
||||
"react-virtualized-auto-sizer": "^1.0.15",
|
||||
"react-window": "^1.8.9",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^3.1.8",
|
||||
"typescript": "^4.8.4",
|
||||
"typescript": "^5.0.4",
|
||||
"uuid": "^9.0.0",
|
||||
"vite": "^4.2.2",
|
||||
"yup": "^0.32.11",
|
||||
"zustand": "^4.1.2"
|
||||
"zustand": "^4.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.2",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/react": "^18.0.37",
|
||||
"@types/react-csv": "^1.1.3",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-datepicker": "4.8.0",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-table": "^7.7.14",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"eslint": "8.25.0",
|
||||
"vite-tsconfig-paths": "^3.5.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"vite-plugin-pwa": "^0.13.1",
|
||||
"prettier": "^2.7.1",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"eslint": "8.38.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-airbnb-typescript-prettier": "^5.0.0",
|
||||
@@ -92,7 +92,10 @@
|
||||
"eslint-plugin-no-inline-styles": "^1.0.5",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-react-hooks": "^4.6.0"
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"lint-staged": "^13.2.1",
|
||||
"prettier": "^2.8.7",
|
||||
"vite-tsconfig-paths": "^4.2.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
24
src/@tanstack.react-table.d.ts
vendored
Normal file
24
src/@tanstack.react-table.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { BoxProps } from '@chakra-ui/react';
|
||||
import '@tanstack/react-table';
|
||||
|
||||
declare module '@tanstack/table-core' {
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
stopPropagation?: boolean;
|
||||
alwaysShow?: boolean;
|
||||
anchored?: boolean;
|
||||
hasPopover?: boolean;
|
||||
customMaxWidth?: string;
|
||||
customMinWidth?: string;
|
||||
customWidth?: string;
|
||||
isMonospace?: boolean;
|
||||
isCentered?: boolean;
|
||||
columnSelectorOptions?: {
|
||||
label?: string;
|
||||
};
|
||||
headerOptions?: {
|
||||
tooltip?: string;
|
||||
};
|
||||
headerStyleProps?: BoxProps;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Warning } from 'phosphor-react';
|
||||
import { Warning } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ThemeProps } from 'models/Theme';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { X } from 'phosphor-react';
|
||||
import { X } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint, LayoutProps, SpaceProps } from '@chakra-ui/react';
|
||||
import { Plus } from 'phosphor-react';
|
||||
import { Plus } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props extends LayoutProps, SpaceProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Button, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Pen } from 'phosphor-react';
|
||||
import { Pen } from '@phosphor-icons/react';
|
||||
|
||||
interface Props {
|
||||
onClick: () => void;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { ArrowsClockwise } from 'phosphor-react';
|
||||
import { ArrowsClockwise } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { FloppyDisk } from 'phosphor-react';
|
||||
import { FloppyDisk } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { ArrowRight, FloppyDisk } from 'phosphor-react';
|
||||
import { ArrowRight, FloppyDisk } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint, useDisclosure } from '@chakra-ui/react';
|
||||
import { Pencil, X } from 'phosphor-react';
|
||||
import { Pencil, X } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ConfirmCloseAlert from 'components/Modals/Actions/ConfirmCloseAlert';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Warning } from 'phosphor-react';
|
||||
import { Warning } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ThemeProps } from 'models/Theme';
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Box, LayoutProps, SpaceProps, useStyleConfig } from '@chakra-ui/react';
|
||||
import { Box, FlexProps, LayoutProps, SpaceProps, useStyleConfig } from '@chakra-ui/react';
|
||||
|
||||
interface CardBodyProps extends LayoutProps, SpaceProps {
|
||||
interface CardBodyProps extends LayoutProps, SpaceProps, FlexProps {
|
||||
variant?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const CardBody = ({ variant, children, ...props }: CardBodyProps) => {
|
||||
const CardBody: React.FC<CardBodyProps> = ({ variant, children, ...props }) => {
|
||||
// @ts-ignore
|
||||
const styles = useStyleConfig('CardBody', { variant });
|
||||
// Pass the computed styles into the `__css` prop
|
||||
|
||||
@@ -1,26 +1,60 @@
|
||||
import React from 'react';
|
||||
import { Box, LayoutProps, SpaceProps, useStyleConfig } from '@chakra-ui/react';
|
||||
import React, { DOMAttributes } from 'react';
|
||||
import {
|
||||
BackgroundProps,
|
||||
Box,
|
||||
EffectProps,
|
||||
InteractivityProps,
|
||||
LayoutProps,
|
||||
PositionProps,
|
||||
SpaceProps,
|
||||
useColorModeValue,
|
||||
useStyleConfig,
|
||||
useToken,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
interface Props extends LayoutProps, SpaceProps {
|
||||
variant?: string;
|
||||
interface CardHeaderProps
|
||||
extends LayoutProps,
|
||||
SpaceProps,
|
||||
BackgroundProps,
|
||||
InteractivityProps,
|
||||
PositionProps,
|
||||
EffectProps,
|
||||
DOMAttributes<HTMLDivElement> {
|
||||
variant?: 'panel' | 'unstyled';
|
||||
children: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
headerStyle?: {
|
||||
color: string;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
variant: undefined,
|
||||
};
|
||||
const CardHeader = ({
|
||||
variant,
|
||||
children,
|
||||
icon,
|
||||
headerStyle = {
|
||||
color: 'blue',
|
||||
},
|
||||
...rest
|
||||
}: CardHeaderProps) => {
|
||||
const iconBgcolor = useToken('colors', [`${headerStyle?.color}.500`, `${headerStyle?.color}.300`]);
|
||||
const bgColor = useToken('colors', [`${headerStyle?.color}.50`, `${headerStyle?.color}.700`]);
|
||||
const iconColor = useColorModeValue(iconBgcolor[0], iconBgcolor[1]);
|
||||
const headerBgColor = useColorModeValue(bgColor[0], bgColor[1]);
|
||||
|
||||
const CardHeader = ({ variant, children, ...rest }: Props) => {
|
||||
// @ts-ignore
|
||||
const styles = useStyleConfig('CardHeader', { variant });
|
||||
|
||||
// Pass the computed styles into the `__css` prop
|
||||
return (
|
||||
<Box __css={styles} {...rest}>
|
||||
<Box __css={styles} bgColor={variant === 'unstyled' ? undefined : headerBgColor} {...rest}>
|
||||
{icon ? (
|
||||
<Box mr={2} color={headerStyle ? iconColor : undefined} bgColor="unset">
|
||||
{icon}
|
||||
</Box>
|
||||
) : null}
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
CardHeader.defaultProps = defaultProps;
|
||||
|
||||
export default CardHeader;
|
||||
export default React.memo(CardHeader);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Tooltip,
|
||||
useBreakpoint,
|
||||
} from '@chakra-ui/react';
|
||||
import { FunnelSimple } from 'phosphor-react';
|
||||
import { FunnelSimple } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
HStack,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import EditOverrideForm from './EditForm';
|
||||
import { ConfigurationOverride } from 'hooks/Network/ConfigurationOverride';
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
PopoverTrigger,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { Plus } from 'phosphor-react';
|
||||
import { Plus } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateConfigurationOverrideForm from './CreateForm';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
Tooltip,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { Pen } from 'phosphor-react';
|
||||
import { Pen } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ACCEPTED_CONFIGURATION_OVERRIDES, ConfigurationOverride } from 'hooks/Network/ConfigurationOverride';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
IconButton,
|
||||
Box,
|
||||
} from '@chakra-ui/react';
|
||||
import { ArrowDown, ArrowUp, Plus, Trash } from 'phosphor-react';
|
||||
import { ArrowDown, ArrowUp, Plus, Trash } from '@phosphor-icons/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CloseButton from 'components/Buttons/CloseButton';
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
Textarea,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { areRrmParamsValid } from './helper';
|
||||
|
||||
64
src/components/DataGrid/CellRow.tsx
Normal file
64
src/components/DataGrid/CellRow.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as React from 'react';
|
||||
import { Td, Tr } from '@chakra-ui/react';
|
||||
import { Row, flexRender } from '@tanstack/react-table';
|
||||
|
||||
export type DataGridCellRowProps<TValue extends object> = {
|
||||
row: Row<TValue>;
|
||||
onRowClick: ((row: TValue) => (() => void) | undefined) | undefined;
|
||||
rowStyle: {
|
||||
hoveredRowBg: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const DataGridCellRow = <TValue extends object>({
|
||||
row,
|
||||
rowStyle: { hoveredRowBg },
|
||||
onRowClick,
|
||||
}: DataGridCellRowProps<TValue>) => {
|
||||
const onClick = onRowClick ? onRowClick(row.original) : undefined;
|
||||
|
||||
return (
|
||||
<Tr
|
||||
key={row.id}
|
||||
_hover={{
|
||||
backgroundColor: hoveredRowBg,
|
||||
}}
|
||||
onClick={onClick}
|
||||
borderRight="1px solid gray"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<Td
|
||||
px={1}
|
||||
key={cell.id}
|
||||
textOverflow="ellipsis"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
minWidth={cell.column.columnDef.meta?.customMinWidth ?? undefined}
|
||||
maxWidth={cell.column.columnDef.meta?.customMaxWidth ?? undefined}
|
||||
width={cell.column.columnDef.meta?.customWidth}
|
||||
textAlign={cell.column.columnDef.meta?.isCentered ? 'center' : undefined}
|
||||
fontFamily={
|
||||
cell.column.columnDef.meta?.isMonospace
|
||||
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||
: undefined
|
||||
}
|
||||
onClick={
|
||||
cell.column.columnDef.meta?.stopPropagation || (cell.column.id === 'actions' && onClick)
|
||||
? (e) => {
|
||||
e.stopPropagation();
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
cursor={
|
||||
!cell.column.columnDef.meta?.stopPropagation && cell.column.id !== 'actions' && onClick
|
||||
? 'pointer'
|
||||
: undefined
|
||||
}
|
||||
border="0.5px solid gray"
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</Td>
|
||||
))}
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
54
src/components/DataGrid/DataGridColumnPicker.tsx
Normal file
54
src/components/DataGrid/DataGridColumnPicker.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { Box, Checkbox, IconButton, Menu, MenuButton, MenuItem, MenuList, Tooltip } from '@chakra-ui/react';
|
||||
import { FunnelSimple } from '@phosphor-icons/react';
|
||||
import { VisibilityState } from '@tanstack/react-table';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DataGridColumn } from './useDataGrid';
|
||||
|
||||
export type DataGridColumnPickerProps<TValue extends object> = {
|
||||
columns: DataGridColumn<TValue>[];
|
||||
columnVisibility: VisibilityState;
|
||||
toggleVisibility: (id: string) => void;
|
||||
};
|
||||
|
||||
export const DataGridColumnPicker = <TValue extends object>({
|
||||
columns,
|
||||
columnVisibility,
|
||||
toggleVisibility,
|
||||
}: DataGridColumnPickerProps<TValue>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Menu closeOnSelect={false} isLazy>
|
||||
<Tooltip label={t('common.columns')} hasArrow>
|
||||
<MenuButton as={IconButton} icon={<FunnelSimple />} />
|
||||
</Tooltip>
|
||||
<MenuList maxH="200px" overflowY="auto">
|
||||
{columns
|
||||
.filter((col) => col.id && col.header)
|
||||
.map((column) => {
|
||||
const handleClick =
|
||||
column.id !== undefined ? () => toggleVisibility(column.id as unknown as string) : undefined;
|
||||
const id = column.id ?? uuid();
|
||||
let label = column.header?.toString() ?? 'Unrecognized column';
|
||||
if (column.meta?.columnSelectorOptions?.label) label = column.meta.columnSelectorOptions.label;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={id}
|
||||
as={Checkbox}
|
||||
isChecked={columnVisibility[id] === undefined || columnVisibility[id]}
|
||||
onChange={column.meta?.alwaysShow ? undefined : handleClick}
|
||||
isDisabled={column.meta?.alwaysShow}
|
||||
>
|
||||
{label}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
45
src/components/DataGrid/HeaderRow.tsx
Normal file
45
src/components/DataGrid/HeaderRow.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Th, Tooltip, Tr } from '@chakra-ui/react';
|
||||
import { HeaderGroup, flexRender } from '@tanstack/react-table';
|
||||
import { DataGridSortIcon } from './SortIcon';
|
||||
|
||||
export type DataGridHeaderRowProps<TValue extends object> = {
|
||||
headerGroup: HeaderGroup<TValue>;
|
||||
};
|
||||
|
||||
export const DataGridHeaderRow = <TValue extends object>({ headerGroup }: DataGridHeaderRowProps<TValue>) => (
|
||||
<Tr p={0} borderRight="1px solid gray">
|
||||
{headerGroup.headers.map((header) => (
|
||||
<Th
|
||||
color="gray.400"
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
minWidth={header.column.columnDef.meta?.customMinWidth ?? undefined}
|
||||
maxWidth={header.column.columnDef.meta?.customMaxWidth ?? undefined}
|
||||
width={header.column.columnDef.meta?.customWidth}
|
||||
fontSize="sm"
|
||||
onClick={header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined}
|
||||
cursor={header.column.getCanSort() ? 'pointer' : undefined}
|
||||
border="0.5px solid gray"
|
||||
px={1}
|
||||
>
|
||||
<Flex display="flex" alignItems="center">
|
||||
{header.isPlaceholder ? null : (
|
||||
<Tooltip label={header.column.columnDef.meta?.headerOptions?.tooltip}>
|
||||
<Box
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
alignContent="center"
|
||||
width="100%"
|
||||
{...header.column.columnDef.meta?.headerStyleProps}
|
||||
>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)}
|
||||
<DataGridSortIcon sortInfo={header.column.getIsSorted()} canSort={header.column.getCanSort()} />
|
||||
</Flex>
|
||||
</Th>
|
||||
))}
|
||||
</Tr>
|
||||
);
|
||||
124
src/components/DataGrid/Input.tsx
Normal file
124
src/components/DataGrid/Input.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import * as React from 'react';
|
||||
import { ArrowLeftIcon, ArrowRightIcon, ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Tooltip,
|
||||
Flex,
|
||||
IconButton,
|
||||
Text,
|
||||
Select,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberDecrementStepper,
|
||||
} from '@chakra-ui/react';
|
||||
import { Table } from '@tanstack/react-table';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useContainerDimensions } from 'hooks/useContainerDimensions';
|
||||
|
||||
type Props<T extends object> = {
|
||||
table: Table<T>;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const DataGridControls = <T extends object>({ table, isDisabled }: Props<T>) => {
|
||||
const { t } = useTranslation();
|
||||
const { ref, dimensions } = useContainerDimensions({ precision: 100 });
|
||||
const isCompact = dimensions.width !== 0 && dimensions.width <= 800;
|
||||
|
||||
return (
|
||||
<Flex ref={ref} justifyContent="space-between" m={4} alignItems="center">
|
||||
<Flex>
|
||||
<Tooltip label={t('table.first_page')}>
|
||||
<IconButton
|
||||
aria-label="Go to first page"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
isDisabled={isDisabled || !table.getCanPreviousPage()}
|
||||
icon={<ArrowLeftIcon h={3} w={3} />}
|
||||
mr={4}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('table.previous_page')}>
|
||||
<IconButton
|
||||
aria-label="Previous page"
|
||||
onClick={() => table.previousPage()}
|
||||
isDisabled={isDisabled || !table.getCanPreviousPage()}
|
||||
icon={<ChevronLeftIcon h={6} w={6} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
|
||||
<Flex alignItems="center">
|
||||
{isCompact ? null : (
|
||||
<>
|
||||
<Text flexShrink={0} mr={8}>
|
||||
{t('table.page')}{' '}
|
||||
<Text fontWeight="bold" as="span">
|
||||
{table.getState().pagination.pageIndex + 1}
|
||||
</Text>{' '}
|
||||
{t('common.of')}{' '}
|
||||
<Text fontWeight="bold" as="span">
|
||||
{table.getPageCount()}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text flexShrink={0}>{t('table.go_to_page')}</Text>{' '}
|
||||
<NumberInput
|
||||
ml={2}
|
||||
mr={8}
|
||||
w={28}
|
||||
min={1}
|
||||
max={table.getPageCount()}
|
||||
onChange={(_, numberValue) => {
|
||||
const newPage = numberValue ? numberValue - 1 : 0;
|
||||
table.setPageIndex(newPage);
|
||||
}}
|
||||
value={table.getState().pagination.pageIndex + 1}
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
</>
|
||||
)}
|
||||
<Select
|
||||
w={32}
|
||||
value={table.getState().pagination.pageSize}
|
||||
onChange={(e) => {
|
||||
table.setPageSize(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
{[10, 20, 30, 40, 50].map((opt) => (
|
||||
<option key={uuid()} value={opt}>
|
||||
{t('common.show')} {opt}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Flex>
|
||||
|
||||
<Flex>
|
||||
<Tooltip label={t('table.next_page')}>
|
||||
<IconButton
|
||||
aria-label="Go to next page"
|
||||
onClick={() => table.nextPage()}
|
||||
isDisabled={isDisabled || !table.getCanNextPage()}
|
||||
icon={<ChevronRightIcon h={6} w={6} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('table.last_page')}>
|
||||
<IconButton
|
||||
aria-label="Go to last page"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
isDisabled={isDisabled || !table.getCanNextPage()}
|
||||
icon={<ArrowRightIcon h={3} w={3} />}
|
||||
ml={4}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataGridControls;
|
||||
23
src/components/DataGrid/SortIcon.tsx
Normal file
23
src/components/DataGrid/SortIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { ArrowDown, ArrowUp, Circle } from '@phosphor-icons/react';
|
||||
import { SortDirection } from '@tanstack/react-table';
|
||||
|
||||
export type DataGridSortIconProps = {
|
||||
sortInfo: false | SortDirection;
|
||||
canSort: boolean;
|
||||
};
|
||||
|
||||
export const DataGridSortIcon = ({ sortInfo, canSort }: DataGridSortIconProps) => {
|
||||
if (canSort) {
|
||||
if (sortInfo) {
|
||||
return sortInfo === 'desc' ? (
|
||||
<Icon ml={1} boxSize={3} as={ArrowDown} />
|
||||
) : (
|
||||
<Icon ml={1} boxSize={3} as={ArrowUp} />
|
||||
);
|
||||
}
|
||||
return <Icon ml={1} boxSize={3} as={Circle} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Icon, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
|
||||
import { Draggable } from '@hello-pangea/dnd';
|
||||
import { ArrowsDownUp, Lock } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DataGridColumn } from '../../useDataGrid';
|
||||
|
||||
type Props<TValue> = {
|
||||
draggableId: string;
|
||||
index: number;
|
||||
column: DataGridColumn<TValue>;
|
||||
};
|
||||
|
||||
const DraggableColumn = <TValue extends object>({ draggableId, index, column }: Props<TValue>) => {
|
||||
const { t } = useTranslation();
|
||||
const isDraggingBackground = useColorModeValue('blue.100', 'blue.600');
|
||||
const notDraggingBackground = useColorModeValue('gray.50', 'gray.700');
|
||||
|
||||
let label = column.header?.toString() ?? 'Unrecognized column';
|
||||
if (column.meta?.columnSelectorOptions?.label) label = column.meta.columnSelectorOptions.label;
|
||||
|
||||
const tooltipLabel = () => {
|
||||
if (column.meta?.anchored) return t('table.drag_locked');
|
||||
if (column.meta?.alwaysShow) return t('table.drag_always_show');
|
||||
|
||||
return t('table.drag_explanation');
|
||||
};
|
||||
|
||||
return (
|
||||
<Draggable draggableId={draggableId} index={index} isDragDisabled={column.meta?.anchored}>
|
||||
{(itemProvided, itemSnapshot) => (
|
||||
<Tooltip label={tooltipLabel()}>
|
||||
<Box
|
||||
ref={itemProvided.innerRef}
|
||||
{...itemProvided.draggableProps}
|
||||
{...itemProvided.dragHandleProps}
|
||||
display="flex"
|
||||
backgroundColor={itemSnapshot.isDragging ? isDraggingBackground : notDraggingBackground}
|
||||
px={6}
|
||||
py={2}
|
||||
my={2}
|
||||
borderRadius={15}
|
||||
cursor={column.meta?.anchored ? 'not-allowed' : undefined}
|
||||
>
|
||||
<Icon as={column.meta?.anchored ? Lock : ArrowsDownUp} boxSize={5} ml={0.5} mr={2} my="auto" />
|
||||
<Text my="auto">{label}</Text>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
export default DraggableColumn;
|
||||
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import { Box, useColorModeValue } from '@chakra-ui/react';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { DataGridColumn } from '../../useDataGrid';
|
||||
import DraggableColumn from './DraggableColumn';
|
||||
|
||||
type Props<TValue> = {
|
||||
items: string[];
|
||||
columns: DataGridColumn<TValue>[];
|
||||
droppableId: string;
|
||||
isDropDisabled?: boolean;
|
||||
};
|
||||
const DroppableBox = <TValue extends object>({ items, columns, droppableId, isDropDisabled }: Props<TValue>) => {
|
||||
const notDraggingBackground = useColorModeValue('gray.200', 'gray.600');
|
||||
const isDraggingOverBackground = useColorModeValue('blue.300', 'blue.500');
|
||||
|
||||
return (
|
||||
<Droppable droppableId={droppableId} direction="vertical" isCombineEnabled={false} isDropDisabled={isDropDisabled}>
|
||||
{(provided, snapshot) => (
|
||||
<Box
|
||||
ref={provided.innerRef}
|
||||
backgroundColor={snapshot.isDraggingOver ? isDraggingOverBackground : notDraggingBackground}
|
||||
padding={2}
|
||||
borderRadius={15}
|
||||
>
|
||||
{items.map((item, index) => {
|
||||
const found = columns.find((col) => col.id === item);
|
||||
return found ? <DraggableColumn key={item} draggableId={item} index={index} column={found} /> : null;
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</Box>
|
||||
)}
|
||||
</Droppable>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(DroppableBox);
|
||||
129
src/components/DataGrid/TableSettingsModal/DragDrop/index.tsx
Normal file
129
src/components/DataGrid/TableSettingsModal/DragDrop/index.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Heading } from '@chakra-ui/react';
|
||||
import { DragDropContext, DragStart, DropResult } from '@hello-pangea/dnd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DataGridColumn, UseDataGridReturn } from '../../useDataGrid';
|
||||
import DroppableBox from './DroppableBox';
|
||||
|
||||
const reorder = (list: string[], startIndex: number, endIndex: number) => {
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
if (removed) {
|
||||
result.splice(endIndex, 0, removed);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const getShownColumns = <TValue extends object>(columns: DataGridColumn<TValue>[], columnOrder: string[]) => {
|
||||
const order = [...columnOrder];
|
||||
|
||||
for (const col of columns) {
|
||||
if (!order.includes(col.id)) {
|
||||
order.push(col.id);
|
||||
}
|
||||
}
|
||||
|
||||
return order;
|
||||
};
|
||||
|
||||
type Props<TValue extends object> = {
|
||||
controller: UseDataGridReturn;
|
||||
shownColumns: DataGridColumn<TValue>[];
|
||||
hiddenColumns: DataGridColumn<TValue>[];
|
||||
};
|
||||
|
||||
const TableDragDrop = <TValue extends object>({ controller, shownColumns, hiddenColumns }: Props<TValue>) => {
|
||||
const { t } = useTranslation();
|
||||
const [shownOrder, setShowOrder] = React.useState(getShownColumns(shownColumns, controller.columnOrder));
|
||||
const [hiddenOrder, setHiddenOrder] = React.useState(hiddenColumns.map((col) => col.id));
|
||||
const [currentDraggingColumn, setCurrentDraggingColumn] = React.useState<DataGridColumn<TValue>>();
|
||||
|
||||
const handleDragStart = React.useCallback(
|
||||
(start: DragStart) => {
|
||||
const foundColumn =
|
||||
shownColumns.find(({ id }) => id === start.draggableId) ??
|
||||
hiddenColumns.find(({ id }) => id === start.draggableId);
|
||||
setCurrentDraggingColumn(foundColumn);
|
||||
},
|
||||
[shownColumns, hiddenColumns],
|
||||
);
|
||||
|
||||
const minimumIndex = React.useMemo(() => {
|
||||
let index = 0;
|
||||
for (const [i, col] of shownColumns.entries()) {
|
||||
if (col.meta?.anchored) {
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
return index + 1;
|
||||
}, [shownColumns]);
|
||||
|
||||
const handleDragEnd = React.useCallback(
|
||||
(result: DropResult) => {
|
||||
const { source, destination, draggableId } = result;
|
||||
|
||||
if (destination === null) return;
|
||||
|
||||
if (source.droppableId === destination.droppableId) {
|
||||
const newOrder = reorder(shownOrder, source.index, Math.max(destination.index, minimumIndex));
|
||||
if (destination.droppableId === 'displayed-columns') {
|
||||
controller.setColumnOrder(newOrder);
|
||||
setShowOrder(newOrder);
|
||||
} else setHiddenOrder(newOrder);
|
||||
}
|
||||
// This means we are moving from displayed to hidden
|
||||
else if (source.droppableId === 'displayed-columns') {
|
||||
// Toggle the column visibility in user preferences
|
||||
const results = controller.hideColumn(draggableId);
|
||||
if (results) {
|
||||
setHiddenOrder([...results.hiddenColumns]);
|
||||
setShowOrder([...results.columnOrder]);
|
||||
}
|
||||
}
|
||||
// This means we are moving from hidden to displayed
|
||||
else if (source.droppableId === 'hidden-columns') {
|
||||
const newOrder = Array.from(shownOrder);
|
||||
newOrder.splice(Math.max(destination.index, minimumIndex), 0, draggableId);
|
||||
const results = controller.unhideColumn(draggableId, newOrder);
|
||||
if (results) {
|
||||
setHiddenOrder(results.hiddenColumns);
|
||||
setShowOrder([...results.columnOrder]);
|
||||
setHiddenOrder([...results.hiddenColumns]);
|
||||
}
|
||||
}
|
||||
|
||||
setCurrentDraggingColumn(undefined);
|
||||
},
|
||||
[shownColumns, hiddenColumns, controller.hideColumn, controller.unhideColumn, minimumIndex],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading size="md">{t('table.columns')}</Heading>
|
||||
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||
<Flex mt={4}>
|
||||
<Box w="50%" mr={2}>
|
||||
<Heading size="sm" mb={4}>
|
||||
Visible ({shownOrder.length})
|
||||
</Heading>
|
||||
<DroppableBox droppableId="displayed-columns" items={shownOrder} columns={shownColumns} />
|
||||
</Box>
|
||||
<Box ml={2} w="50%">
|
||||
<Heading size="sm" mb={4}>
|
||||
Hidden ({hiddenColumns.length})
|
||||
</Heading>
|
||||
<DroppableBox
|
||||
droppableId="hidden-columns"
|
||||
items={hiddenOrder}
|
||||
columns={hiddenColumns}
|
||||
isDropDisabled={currentDraggingColumn?.meta?.alwaysShow}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
</DragDropContext>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableDragDrop;
|
||||
57
src/components/DataGrid/TableSettingsModal/index.tsx
Normal file
57
src/components/DataGrid/TableSettingsModal/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as React from 'react';
|
||||
import { SettingsIcon } from '@chakra-ui/icons';
|
||||
import { Box, IconButton, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import { ClockCounterClockwise } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DataGridColumn, UseDataGridReturn } from '../useDataGrid';
|
||||
import TableDragDrop from './DragDrop';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
|
||||
type Props<TValue extends object> = {
|
||||
controller: UseDataGridReturn;
|
||||
columns: DataGridColumn<TValue>[];
|
||||
};
|
||||
|
||||
const TableSettingsModal = <TValue extends object>({ controller, columns }: Props<TValue>) => {
|
||||
const { t } = useTranslation();
|
||||
const modalProps = useDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip label={t('table.preferences')}>
|
||||
<IconButton
|
||||
aria-label={t('table.preferences')}
|
||||
icon={<SettingsIcon weight="bold" />}
|
||||
onClick={modalProps.onOpen}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Modal
|
||||
title={t('table.preferences')}
|
||||
topRightButtons={
|
||||
<Tooltip label={t('table.reset')}>
|
||||
<IconButton
|
||||
aria-label={t('table.reset')}
|
||||
icon={<ClockCounterClockwise size={20} />}
|
||||
onClick={controller.resetPreferences}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
options={{
|
||||
modalSize: 'md',
|
||||
maxWidth: { sm: '600px', md: '600px', lg: '600px', xl: '600px' },
|
||||
}}
|
||||
{...modalProps}
|
||||
>
|
||||
<Box w="100%">
|
||||
<TableDragDrop<TValue>
|
||||
shownColumns={columns.filter((col) => controller.columnVisibility[col.id] !== false)}
|
||||
hiddenColumns={columns.filter((col) => controller.columnVisibility[col.id] === false)}
|
||||
controller={controller}
|
||||
/>
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TableSettingsModal);
|
||||
251
src/components/DataGrid/index.tsx
Normal file
251
src/components/DataGrid/index.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Center,
|
||||
Flex,
|
||||
HStack,
|
||||
Heading,
|
||||
LayoutProps,
|
||||
Spacer,
|
||||
Spinner,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Thead,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DataGridCellRow } from './CellRow';
|
||||
import { DataGridHeaderRow } from './HeaderRow';
|
||||
import DataGridControls from './Input';
|
||||
import TableSettingsModal from './TableSettingsModal';
|
||||
import { DataGridColumn, UseDataGridReturn } from './useDataGrid';
|
||||
import RefreshButton from 'components/Buttons/RefreshButton';
|
||||
import Card from 'components/Card';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
import LoadingOverlay from 'components/LoadingOverlay';
|
||||
|
||||
export type ColumnOptions = {
|
||||
isSortable?: boolean;
|
||||
};
|
||||
|
||||
export type DataGridOptions<TValue extends object> = {
|
||||
count?: number;
|
||||
isFullScreen?: boolean;
|
||||
isHidingControls?: boolean;
|
||||
isManual?: boolean;
|
||||
minimumHeight?: LayoutProps['minH'];
|
||||
onRowClick?: (row: TValue) => (() => void) | undefined;
|
||||
refetch?: () => void;
|
||||
showAsCard?: boolean;
|
||||
};
|
||||
|
||||
export type DataGridProps<TValue extends object> = {
|
||||
controller: UseDataGridReturn;
|
||||
columns: DataGridColumn<TValue>[];
|
||||
header: {
|
||||
title: string;
|
||||
objectListed: string;
|
||||
leftContent?: React.ReactNode;
|
||||
addButton?: React.ReactNode;
|
||||
otherButtons?: React.ReactNode;
|
||||
};
|
||||
data?: TValue[];
|
||||
isLoading?: boolean;
|
||||
options?: DataGridOptions<TValue>;
|
||||
};
|
||||
|
||||
export const DataGrid = <TValue extends object>({
|
||||
controller,
|
||||
columns,
|
||||
header,
|
||||
data = [],
|
||||
options = {},
|
||||
isLoading = false,
|
||||
}: DataGridProps<TValue>) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
/*
|
||||
Table Styling
|
||||
*/
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
const hoveredRowBg = useColorModeValue('gray.100', 'gray.600');
|
||||
|
||||
const minimumHeight: LayoutProps['minH'] = React.useMemo(() => {
|
||||
if (options.isFullScreen) {
|
||||
return { base: 'calc(100vh - 360px)', md: 'calc(100vh - 288px)' };
|
||||
}
|
||||
return options.minimumHeight ?? '300px';
|
||||
}, [options.isFullScreen, options.minimumHeight]);
|
||||
|
||||
/*
|
||||
Table Options
|
||||
*/
|
||||
const onRowClick = React.useMemo(() => options.onRowClick, [options.onRowClick]);
|
||||
|
||||
const pagination = React.useMemo(
|
||||
() => ({
|
||||
pageIndex: controller.pageInfo.pageIndex,
|
||||
pageSize: controller.pageInfo.pageSize,
|
||||
}),
|
||||
[controller.pageInfo.pageIndex, controller.pageInfo.pageSize],
|
||||
);
|
||||
|
||||
const pageCount = React.useMemo(() => {
|
||||
if (options.isManual && options.count) {
|
||||
return Math.ceil(options.count / pagination.pageSize);
|
||||
}
|
||||
return Math.ceil((data?.length ?? 0) / pagination.pageSize);
|
||||
}, [options.count, options.isManual, data?.length, pagination.pageSize]);
|
||||
|
||||
const tableOptions = React.useMemo(
|
||||
() => ({
|
||||
pageCount: pageCount > 0 ? pageCount : 1,
|
||||
initialState: { sorting: controller.sortBy, pagination },
|
||||
manualPagination: options.isManual,
|
||||
manualSorting: options.isManual,
|
||||
autoResetPageIndex: false,
|
||||
}),
|
||||
[options.isManual, controller.sortBy, pageCount],
|
||||
);
|
||||
|
||||
const orderedColumns = React.useMemo(() => {
|
||||
const order = controller.columnOrder.filter((id) => columns.find((col) => col.id === id));
|
||||
if (order.length !== columns.length) {
|
||||
for (const col of columns) {
|
||||
if (!order.includes(col.id)) {
|
||||
order.push(col.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return columns.slice().sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
|
||||
}, [columns, controller.columnOrder]);
|
||||
|
||||
const table = useReactTable<TValue>({
|
||||
// react-table base functions
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
|
||||
// Table State
|
||||
data,
|
||||
columns: orderedColumns,
|
||||
state: {
|
||||
sorting: controller.sortBy,
|
||||
columnVisibility: controller.columnVisibility,
|
||||
pagination,
|
||||
},
|
||||
|
||||
// Change Handlers
|
||||
onSortingChange: controller.setSortBy,
|
||||
onPaginationChange: controller.onPaginationChange,
|
||||
|
||||
// debugTable: true,
|
||||
|
||||
// Table Options
|
||||
...tableOptions,
|
||||
});
|
||||
|
||||
if (isLoading && data.length === 0) {
|
||||
return (
|
||||
<Center>
|
||||
<Spinner size="xl" />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return options.showAsCard ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading size="md" my="auto" mr={2}>
|
||||
{header.title}
|
||||
</Heading>
|
||||
{header.leftContent}
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
{header.otherButtons}
|
||||
{header.addButton}
|
||||
{
|
||||
// @ts-ignore
|
||||
<TableSettingsModal<TValue> controller={controller} columns={columns} />
|
||||
}
|
||||
{options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null}
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody display="flex" flexDirection="column">
|
||||
<LoadingOverlay isLoading={isLoading}>
|
||||
<TableContainer minH={minimumHeight}>
|
||||
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px">
|
||||
<Thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
|
||||
))}
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<DataGridCellRow<TValue> key={row.id} row={row} onRowClick={onRowClick} rowStyle={{ hoveredRowBg }} />
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
{data?.length === 0 ? (
|
||||
<Center mt={8}>
|
||||
<Heading size="md">
|
||||
{header.objectListed
|
||||
? t('common.no_obj_found', { obj: header.objectListed })
|
||||
: t('common.empty_list')}
|
||||
</Heading>
|
||||
</Center>
|
||||
) : null}
|
||||
</TableContainer>
|
||||
</LoadingOverlay>
|
||||
{!options.isHidingControls ? <DataGridControls table={table} isDisabled={isLoading} /> : null}
|
||||
</CardBody>
|
||||
</Card>
|
||||
) : (
|
||||
<Box w="100%">
|
||||
<Flex mb={2}>
|
||||
<Heading size="md" my="auto" mr={2}>
|
||||
{header.title}
|
||||
</Heading>
|
||||
{header.leftContent}
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
{header.otherButtons}
|
||||
{header.addButton}
|
||||
{
|
||||
// @ts-ignore
|
||||
<TableSettingsModal<TValue> controller={controller} columns={columns} />
|
||||
}
|
||||
{options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null}
|
||||
</HStack>
|
||||
</Flex>
|
||||
<LoadingOverlay isLoading={isLoading}>
|
||||
<TableContainer minH={minimumHeight}>
|
||||
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px">
|
||||
<Thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
|
||||
))}
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<DataGridCellRow<TValue> key={row.id} row={row} onRowClick={onRowClick} rowStyle={{ hoveredRowBg }} />
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
{data?.length === 0 ? (
|
||||
<Center mt={8}>
|
||||
<Heading size="md">
|
||||
{header.objectListed ? t('common.no_obj_found', { obj: header.objectListed }) : t('common.empty_list')}
|
||||
</Heading>
|
||||
</Center>
|
||||
) : null}
|
||||
</TableContainer>
|
||||
</LoadingOverlay>
|
||||
{!options.isHidingControls ? <DataGridControls table={table} isDisabled={isLoading} /> : null}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
195
src/components/DataGrid/useDataGrid.ts
Normal file
195
src/components/DataGrid/useDataGrid.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import * as React from 'react';
|
||||
import { ColumnDef, PaginationState, SortingColumnDef, SortingState, VisibilityState } from '@tanstack/react-table';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
const getDefaultSettings = (settings?: string) => {
|
||||
let limit = 10;
|
||||
let index = 0;
|
||||
|
||||
if (settings) {
|
||||
const savedSizeSetting = localStorage.getItem(settings);
|
||||
if (savedSizeSetting) {
|
||||
try {
|
||||
limit = parseInt(savedSizeSetting, 10);
|
||||
} catch (e) {
|
||||
limit = 10;
|
||||
}
|
||||
}
|
||||
|
||||
const savedPageSetting = localStorage.getItem(`${settings}.page`);
|
||||
if (savedPageSetting) {
|
||||
try {
|
||||
index = parseInt(savedPageSetting, 10);
|
||||
} catch (e) {
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pageSize: limit,
|
||||
pageIndex: index,
|
||||
};
|
||||
};
|
||||
|
||||
const getSavedColumnOrder = (defaultValue: string[], settings?: string) => {
|
||||
if (settings) {
|
||||
const savedOrderSetting = localStorage.getItem(`${settings}.order`);
|
||||
if (savedOrderSetting) {
|
||||
try {
|
||||
const savedOrder = JSON.parse(savedOrderSetting);
|
||||
return savedOrder.length > 0 ? savedOrder : defaultValue;
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
export type DataGridColumn<T> = ColumnDef<T> & SortingColumnDef<T> & { id: string };
|
||||
|
||||
export type UseDataGridProps = {
|
||||
tableSettingsId: string;
|
||||
defaultOrder: string[];
|
||||
defaultSortBy?: SortingState;
|
||||
};
|
||||
|
||||
export const useDataGrid = ({ tableSettingsId, defaultSortBy, defaultOrder }: UseDataGridProps) => {
|
||||
const orderSetting = `${tableSettingsId}.order`;
|
||||
const hiddenColumnSetting = `${tableSettingsId}.hiddenColumns`;
|
||||
const pageSetting = `${tableSettingsId}.page`;
|
||||
const { getPref, setPref, setPrefs, deletePref } = useAuth();
|
||||
const [sortBy, setSortBy] = React.useState<SortingState>(defaultSortBy ?? []);
|
||||
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
||||
const [columnOrder, setColumnOrder] = React.useState<string[]>(
|
||||
getSavedColumnOrder(defaultOrder ?? [], tableSettingsId),
|
||||
);
|
||||
const [pageInfo, setPageInfo] = React.useState<PaginationState>(getDefaultSettings(tableSettingsId));
|
||||
|
||||
const setNewColumnOrder = React.useCallback(
|
||||
(newOrder: string[]) => {
|
||||
setColumnOrder(newOrder);
|
||||
if (tableSettingsId) {
|
||||
localStorage.setItem(orderSetting, JSON.stringify(newOrder));
|
||||
setPref({ preference: orderSetting, value: newOrder.join(',') });
|
||||
}
|
||||
},
|
||||
[setPref],
|
||||
);
|
||||
|
||||
const resetPreferences = React.useCallback(async () => {
|
||||
if (tableSettingsId) {
|
||||
localStorage.removeItem(orderSetting);
|
||||
localStorage.removeItem(hiddenColumnSetting);
|
||||
await deletePref([orderSetting, hiddenColumnSetting]);
|
||||
}
|
||||
|
||||
setColumnOrder(defaultOrder ?? []);
|
||||
setColumnVisibility({});
|
||||
}, [deletePref]);
|
||||
|
||||
const hideColumn = React.useCallback(
|
||||
(id: string) => {
|
||||
const newVisibility = { ...columnVisibility };
|
||||
newVisibility[id] = false;
|
||||
let hiddenColumnsArray = Object.entries(newVisibility)
|
||||
.filter(([, value]) => !value)
|
||||
.map(([key]) => key);
|
||||
hiddenColumnsArray = [...new Set(hiddenColumnsArray)]; // Remove duplicates
|
||||
|
||||
// New column order without hidden columns
|
||||
let filteredColumnOrder = columnOrder.filter((columnId) => !hiddenColumnsArray.includes(columnId));
|
||||
filteredColumnOrder = [...new Set(filteredColumnOrder)]; // Remove duplicates
|
||||
|
||||
setPrefs([
|
||||
{ tag: hiddenColumnSetting, value: hiddenColumnsArray.join(',') },
|
||||
{ tag: orderSetting, value: filteredColumnOrder.join(',') },
|
||||
]);
|
||||
setColumnVisibility({ ...newVisibility });
|
||||
setColumnOrder(filteredColumnOrder);
|
||||
localStorage.setItem(orderSetting, JSON.stringify(filteredColumnOrder));
|
||||
localStorage.setItem(hiddenColumnSetting, hiddenColumnsArray.join(','));
|
||||
|
||||
return {
|
||||
hiddenColumns: hiddenColumnsArray,
|
||||
columnOrder: filteredColumnOrder,
|
||||
};
|
||||
},
|
||||
[columnOrder, columnVisibility, setPrefs],
|
||||
);
|
||||
|
||||
const unhideColumn = React.useCallback(
|
||||
(id: string, newOrder: string[]) => {
|
||||
const newVisibility = { ...columnVisibility };
|
||||
newVisibility[id] = true;
|
||||
let hiddenColumnsArray = Object.entries(newVisibility)
|
||||
.filter(([, value]) => !value)
|
||||
.map(([key]) => key);
|
||||
hiddenColumnsArray = [...new Set(hiddenColumnsArray)]; // Remove duplicates
|
||||
|
||||
const newColumnOrder = [...new Set(newOrder)]; // Remove duplicates
|
||||
|
||||
setPrefs([
|
||||
{ tag: hiddenColumnSetting, value: hiddenColumnsArray.join(',') },
|
||||
{ tag: orderSetting, value: newColumnOrder.join(',') },
|
||||
]);
|
||||
setColumnVisibility({ ...newVisibility });
|
||||
setColumnOrder(newColumnOrder);
|
||||
localStorage.setItem(orderSetting, JSON.stringify(newColumnOrder));
|
||||
localStorage.setItem(hiddenColumnSetting, hiddenColumnsArray.join(','));
|
||||
|
||||
return {
|
||||
hiddenColumns: hiddenColumnsArray,
|
||||
columnOrder: newColumnOrder,
|
||||
};
|
||||
},
|
||||
[columnOrder, columnVisibility, setPrefs],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const savedPrefs = getPref(hiddenColumnSetting);
|
||||
|
||||
if (savedPrefs) {
|
||||
const savedHiddenColumns = savedPrefs.split(',');
|
||||
setColumnVisibility(savedHiddenColumns.reduce((acc, curr) => ({ ...acc, [curr]: false }), {}));
|
||||
} else {
|
||||
setColumnVisibility({});
|
||||
}
|
||||
|
||||
const savedOrderSetting = getPref(orderSetting);
|
||||
|
||||
if (savedOrderSetting) {
|
||||
const savedHiddenColumns = savedOrderSetting.split(',');
|
||||
setColumnOrder(savedHiddenColumns);
|
||||
}
|
||||
}, [tableSettingsId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (tableSettingsId) {
|
||||
localStorage.setItem(pageSetting, String(pageInfo.pageIndex));
|
||||
if (tableSettingsId) localStorage.setItem(`${tableSettingsId}`, String(pageInfo.pageSize));
|
||||
}
|
||||
}, [pageInfo.pageIndex, pageInfo.pageSize]);
|
||||
|
||||
return React.useMemo(
|
||||
() => ({
|
||||
tableSettingsId,
|
||||
pageInfo,
|
||||
sortBy,
|
||||
setSortBy,
|
||||
columnOrder,
|
||||
setColumnOrder: setNewColumnOrder,
|
||||
hideColumn,
|
||||
unhideColumn,
|
||||
columnVisibility,
|
||||
setColumnVisibility,
|
||||
onPaginationChange: setPageInfo,
|
||||
resetPreferences,
|
||||
}),
|
||||
[pageInfo, hideColumn, unhideColumn, columnVisibility, sortBy, columnOrder, setNewColumnOrder],
|
||||
);
|
||||
};
|
||||
|
||||
export type UseDataGridReturn = ReturnType<typeof useDataGrid>;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { ArrowDown, ArrowUp, Circle } from 'phosphor-react';
|
||||
import { ArrowDown, ArrowUp, Circle } from '@phosphor-icons/react';
|
||||
|
||||
interface Props {
|
||||
isSorted: boolean;
|
||||
@@ -12,13 +12,7 @@ const defaultProps = {
|
||||
isSortedDesc: false,
|
||||
};
|
||||
|
||||
const SortIcon = (
|
||||
{
|
||||
isSorted,
|
||||
isSortedDesc,
|
||||
canSort
|
||||
}: Props
|
||||
) => {
|
||||
const SortIcon = ({ isSorted, isSortedDesc, canSort }: Props) => {
|
||||
if (canSort) {
|
||||
if (isSorted) {
|
||||
return isSortedDesc ? <Icon pt={2} h={5} w={5} as={ArrowDown} /> : <Icon pt={2} h={5} w={5} as={ArrowUp} />;
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
Tooltip,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { UploadSimple } from 'phosphor-react';
|
||||
import { UploadSimple } from '@phosphor-icons/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { Trash } from '@phosphor-icons/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { IconButton, Input, InputGroup, InputRightElement, Tooltip } from '@chakra-ui/react';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import DataTable from 'components/DataTable';
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
Tooltip,
|
||||
IconButton,
|
||||
} from '@chakra-ui/react';
|
||||
import { Trash } from '@phosphor-icons/react';
|
||||
import { Formik } from 'formik';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CloseButton from 'components/Buttons/CloseButton';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
} from '@chakra-ui/react';
|
||||
import { Question } from 'phosphor-react';
|
||||
import { Question } from '@phosphor-icons/react';
|
||||
|
||||
export type InfoPopoverProps = {
|
||||
title: string;
|
||||
|
||||
@@ -4,6 +4,7 @@ import ReactCountryFlag from 'react-country-flag';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const iconStyle = { width: '24px', height: '24px', borderRadius: '20px' };
|
||||
|
||||
const LanguageSwitcher = () => {
|
||||
const { t } = useTranslation();
|
||||
const { i18n } = useTranslation();
|
||||
@@ -32,7 +33,14 @@ const LanguageSwitcher = () => {
|
||||
return (
|
||||
<Menu>
|
||||
<Tooltip label={t('common.language')}>
|
||||
<MenuButton background="transparent" as={IconButton} aria-label="Commands" icon={languageIcon} size="sm" />
|
||||
<MenuButton
|
||||
background="transparent"
|
||||
variant="ghost"
|
||||
as={IconButton}
|
||||
aria-label="Commands"
|
||||
icon={languageIcon}
|
||||
size="sm"
|
||||
/>
|
||||
</Tooltip>
|
||||
<MenuList>
|
||||
<MenuItem onClick={changeLanguage('de')}>Deutsche</MenuItem>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Flex, HStack, ModalHeader as Header, Spacer } from '@chakra-ui/react';
|
||||
import { HStack, ModalHeader as Header, Spacer, useColorModeValue } from '@chakra-ui/react';
|
||||
|
||||
interface ModalHeaderProps {
|
||||
title: string;
|
||||
@@ -7,17 +7,21 @@ interface ModalHeaderProps {
|
||||
right: React.ReactNode;
|
||||
}
|
||||
|
||||
const ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => (
|
||||
<Header>
|
||||
<Flex justifyContent="center" alignItems="center" maxW="100%" px={1}>
|
||||
const ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => {
|
||||
const bg = useColorModeValue('blue.50', 'blue.700');
|
||||
|
||||
return (
|
||||
<Header bg={bg}>
|
||||
{title}
|
||||
<HStack spacing={2} ml={2}>
|
||||
{left ?? null}
|
||||
</HStack>
|
||||
{left ? (
|
||||
<HStack spacing={2} ml={2}>
|
||||
{left}
|
||||
</HStack>
|
||||
) : null}
|
||||
<Spacer />
|
||||
{right}
|
||||
</Flex>
|
||||
</Header>
|
||||
);
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalHeader;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Flex, IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { MagnifyingGlass } from 'phosphor-react';
|
||||
import { MagnifyingGlass } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import DataTable from 'components/DataTable';
|
||||
@@ -10,11 +10,7 @@ interface Props {
|
||||
subscribers: Subscriber[];
|
||||
}
|
||||
|
||||
const SubscriberSearchDisplayTable = (
|
||||
{
|
||||
subscribers
|
||||
}: Props
|
||||
) => {
|
||||
const SubscriberSearchDisplayTable = ({ subscribers }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Tooltip,
|
||||
IconButton,
|
||||
} from '@chakra-ui/react';
|
||||
import { MagnifyingGlass } from 'phosphor-react';
|
||||
import { MagnifyingGlass } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SubscriberSearchDisplayTable from './Table';
|
||||
import CloseButton from 'components/Buttons/CloseButton';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Modal, ModalOverlay, ModalContent, ModalBody, Center, Spinner } from '@chakra-ui/react';
|
||||
import { ArrowLeft, Download, Gauge } from 'phosphor-react';
|
||||
import { ArrowLeft, Download, Gauge } from '@phosphor-icons/react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import WifiScanForm from './Form';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { ArrowDown, ArrowUp, Circle } from 'phosphor-react';
|
||||
import { ArrowDown, ArrowUp, Circle } from '@phosphor-icons/react';
|
||||
|
||||
interface Props {
|
||||
isSorted: boolean;
|
||||
@@ -12,13 +12,7 @@ const defaultProps = {
|
||||
isSortedDesc: false,
|
||||
};
|
||||
|
||||
const SortIcon = (
|
||||
{
|
||||
isSorted,
|
||||
isSortedDesc,
|
||||
canSort
|
||||
}: Props
|
||||
) => {
|
||||
const SortIcon = ({ isSorted, isSortedDesc, canSort }: Props) => {
|
||||
if (canSort) {
|
||||
if (isSorted) {
|
||||
return isSortedDesc ? <Icon pt={2} h={5} w={5} as={ArrowDown} /> : <Icon pt={2} h={5} w={5} as={ArrowUp} />;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { InfoIcon } from '@chakra-ui/icons';
|
||||
import { Heading, IconButton, LayoutProps, LightMode, SpaceProps, Spacer, Tooltip } from '@chakra-ui/react';
|
||||
import { MagnifyingGlass } from 'phosphor-react';
|
||||
import { MagnifyingGlass } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Card from 'components/Card';
|
||||
|
||||
@@ -32,6 +32,7 @@ const SimpleStatDisplay = ({
|
||||
onClick={openModal}
|
||||
cursor={openModal ? 'pointer' : ''}
|
||||
className="tile-shadow-animate"
|
||||
p={4}
|
||||
{...props}
|
||||
>
|
||||
{title !== '' && (
|
||||
@@ -48,7 +49,6 @@ const SimpleStatDisplay = ({
|
||||
variant="ghost"
|
||||
aria-label={t('common.view_details')}
|
||||
size="sm"
|
||||
colorScheme="purple"
|
||||
icon={<MagnifyingGlass height={20} width={20} />}
|
||||
ml={2}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList, Spinner, Tooltip } from '@chakra-ui/react';
|
||||
import { Wrench } from 'phosphor-react';
|
||||
import { Wrench } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useBlinkDevice, useGetDeviceRtty, useRebootDevice } from 'hooks/Network/GatewayDevices';
|
||||
import useMutationResult from 'hooks/useMutationResult';
|
||||
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { Trash } from '@phosphor-icons/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDeleteConfiguration } from 'hooks/Network/Configurations';
|
||||
import { Configuration } from 'models/Configuration';
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
IconButton,
|
||||
} from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Lock, Plus, Trash } from 'phosphor-react';
|
||||
import { Lock, Plus, Trash } from '@phosphor-icons/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList, Spinner, Tooltip } from '@chakra-ui/react';
|
||||
import { Wrench } from 'phosphor-react';
|
||||
import { Wrench } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useBlinkDevice, useGetDeviceRtty, useRebootDevice } from 'hooks/Network/GatewayDevices';
|
||||
import useMutationResult from 'hooks/useMutationResult';
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { ArrowSquareOut, PaperPlaneTilt, Trash } from 'phosphor-react';
|
||||
import { ArrowSquareOut, PaperPlaneTilt, Trash } from '@phosphor-icons/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DeviceActionDropdown from './ActionDropdown';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useDisclosure, Modal, ModalOverlay, ModalContent, ModalBody, Tooltip, IconButton } from '@chakra-ui/react';
|
||||
import { UploadSimple } from 'phosphor-react';
|
||||
import { UploadSimple } from '@phosphor-icons/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Flex, IconButton, Tooltip, useToast } from '@chakra-ui/react';
|
||||
import { Plus, Trash } from 'phosphor-react';
|
||||
import { Plus, Trash } from '@phosphor-icons/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DataTable from 'components/DataTable';
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { MagnifyingGlass, Trash } from 'phosphor-react';
|
||||
import { MagnifyingGlass, Trash } from '@phosphor-icons/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -89,7 +89,7 @@ export const AuthProvider = ({ token, children }: AuthProviderProps) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const setPref = ({ preference, value }: { preference: string; value: string }) => {
|
||||
const setPref = async ({ preference, value }: { preference: string; value: string }) => {
|
||||
let updated = false;
|
||||
if (preferences) {
|
||||
const newPreferences: Preference[] = preferences.map((pref: Preference) => {
|
||||
@@ -102,15 +102,41 @@ export const AuthProvider = ({ token, children }: AuthProviderProps) => {
|
||||
|
||||
if (!updated) newPreferences.push({ tag: preference, value });
|
||||
|
||||
updatePreferences.mutateAsync(newPreferences);
|
||||
await updatePreferences.mutateAsync(newPreferences);
|
||||
}
|
||||
};
|
||||
|
||||
const deletePref = (preference: string) => {
|
||||
const setPrefs = async (preferencesToUpdate: Preference[]) => {
|
||||
if (preferences) {
|
||||
const newPreferences: Preference[] = preferences.filter((pref: Preference) => pref.tag !== preference);
|
||||
const updatedPreferences: string[] = [];
|
||||
const newPreferences = preferences.map((pref: Preference) => {
|
||||
const preferenceToUpdate = preferencesToUpdate.find(
|
||||
(prefToUpdate: Preference) => prefToUpdate.tag === pref.tag,
|
||||
);
|
||||
if (preferenceToUpdate) {
|
||||
updatedPreferences.push(pref.tag);
|
||||
return { tag: pref.tag, value: preferenceToUpdate.value };
|
||||
}
|
||||
return pref;
|
||||
});
|
||||
|
||||
updatePreferences.mutateAsync(newPreferences);
|
||||
for (const preferenceToUpdate of preferencesToUpdate) {
|
||||
if (!updatedPreferences.includes(preferenceToUpdate.tag)) {
|
||||
newPreferences.push(preferenceToUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
await updatePreferences.mutateAsync(newPreferences);
|
||||
}
|
||||
};
|
||||
|
||||
const deletePref = async (preference: string | string[]) => {
|
||||
if (preferences) {
|
||||
const newPreferences: Preference[] = preferences.filter((pref: Preference) =>
|
||||
typeof preference === 'string' ? pref.tag !== preference : !preference.includes(pref.tag),
|
||||
);
|
||||
|
||||
await updatePreferences.mutateAsync(newPreferences);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -146,6 +172,7 @@ export const AuthProvider = ({ token, children }: AuthProviderProps) => {
|
||||
ref,
|
||||
getPref,
|
||||
setPref,
|
||||
setPrefs,
|
||||
deletePref,
|
||||
endpoints,
|
||||
configurationDescriptions,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { Preference } from 'models/Preference';
|
||||
import { User } from 'models/User';
|
||||
import { axiosProv } from 'utils/axiosInstances';
|
||||
|
||||
@@ -26,7 +27,8 @@ export interface AuthProviderReturn {
|
||||
logout: () => void;
|
||||
getPref: (preference: string) => string | null;
|
||||
setPref: ({ preference, value }: { preference: string; value: string }) => void;
|
||||
deletePref: (preference: string) => void;
|
||||
setPrefs: (preferencesToUpdate: Preference[]) => void;
|
||||
deletePref: (preference: string | string[]) => void;
|
||||
ref: React.MutableRefObject<undefined>;
|
||||
endpoints: { [key: string]: string } | null;
|
||||
configurationDescriptions: Record<string, unknown>;
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { X } from '@phosphor-icons/react';
|
||||
import { TOptions } from 'i18next';
|
||||
import { X } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ProvisioningVenueNotificationMessage } from '../../utils';
|
||||
|
||||
54
src/hooks/useContainerDimensions.ts
Normal file
54
src/hooks/useContainerDimensions.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import * as React from 'react';
|
||||
|
||||
const roundToNearest = (num: number, precision: number) => {
|
||||
const factor = 1 / precision;
|
||||
return Math.round(num * factor) / factor;
|
||||
};
|
||||
|
||||
export type UseContainerDimensionsProps = {
|
||||
precision?: 10 | 100 | 1000;
|
||||
};
|
||||
|
||||
export const useContainerDimensions = ({ precision }: UseContainerDimensionsProps) => {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const [dimensions, setDimensions] = React.useState({ width: 0, height: 0 });
|
||||
|
||||
React.useEffect(() => {
|
||||
const getDimensions = () => ({
|
||||
width: (ref && ref.current?.offsetWidth) || 0,
|
||||
height: (ref && ref.current?.offsetHeight) || 0,
|
||||
});
|
||||
|
||||
const handleResize = () => {
|
||||
const { width, height } = getDimensions();
|
||||
if (!precision) {
|
||||
if (dimensions.width !== width && dimensions.height !== height) setDimensions({ width, height });
|
||||
} else {
|
||||
const newDimensions = { width, height };
|
||||
newDimensions.width = roundToNearest(newDimensions.width, precision);
|
||||
newDimensions.height = roundToNearest(newDimensions.height, precision);
|
||||
if (newDimensions.width !== dimensions.width || newDimensions.height !== dimensions.height) {
|
||||
setDimensions(newDimensions);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (ref.current) {
|
||||
handleResize();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [ref, dimensions]);
|
||||
|
||||
return React.useMemo(
|
||||
() => ({
|
||||
dimensions,
|
||||
ref,
|
||||
}),
|
||||
[dimensions.height, dimensions.width],
|
||||
);
|
||||
};
|
||||
@@ -15,6 +15,7 @@ i18next
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
returnNull: false,
|
||||
// debug: process.env.NODE_ENV === "development",
|
||||
});
|
||||
export default i18next;
|
||||
|
||||
7
src/i18next.d.ts
vendored
Normal file
7
src/i18next.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'i18next';
|
||||
|
||||
declare module 'i18next' {
|
||||
interface CustomTypeOptions {
|
||||
returnNull: false;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
useBreakpoint,
|
||||
Portal,
|
||||
} from '@chakra-ui/react';
|
||||
import { ArrowCircleLeft, MapTrifold } from 'phosphor-react';
|
||||
import { ArrowCircleLeft } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
@@ -73,7 +73,6 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
|
||||
};
|
||||
|
||||
const goToProfile = () => navigate('/account');
|
||||
const goToMap = () => navigate('/map');
|
||||
|
||||
window.addEventListener('scroll', changeNavbar);
|
||||
|
||||
@@ -98,14 +97,22 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
|
||||
ps="12px"
|
||||
pt="8px"
|
||||
top="15px"
|
||||
w={isCompact ? '100%' : 'calc(100vw - 256px)'}
|
||||
border={scrolled ? '0.5px solid' : undefined}
|
||||
w={isCompact ? '100%' : 'calc(100% - 254px)'}
|
||||
>
|
||||
<Flex w="100%" flexDirection="row" alignItems="center">
|
||||
<Flex
|
||||
w="100%"
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
justifyItems="center"
|
||||
alignContent="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{isCompact && <HamburgerIcon w="24px" h="24px" onClick={toggleSidebar} mr={10} mt={1} />}
|
||||
<Heading>{activeRoute}</Heading>
|
||||
<Heading size="lg">{activeRoute}</Heading>
|
||||
<Tooltip label={t('common.go_back')}>
|
||||
<IconButton
|
||||
mt={2}
|
||||
mt={1}
|
||||
ml={4}
|
||||
colorScheme="blue"
|
||||
aria-label={t('common.go_back')}
|
||||
@@ -116,14 +123,6 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
|
||||
</Tooltip>
|
||||
<Box ms="auto" w={{ base: 'unset' }}>
|
||||
<Flex alignItems="center" flexDirection="row">
|
||||
<Tooltip hasArrow label={t('common.go_to_map')}>
|
||||
<IconButton
|
||||
aria-label={t('common.go_to_map')}
|
||||
variant="ghost"
|
||||
icon={<MapTrifold size={24} />}
|
||||
onClick={goToMap}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip hasArrow label={t('common.theme')}>
|
||||
<IconButton
|
||||
aria-label={t('common.theme')}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, Flex, Text, useColorModeValue, useDisclosure } from '@chakra-ui/react';
|
||||
import { ArrowCircleRight } from 'phosphor-react';
|
||||
import { ArrowCircleRight } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import EntityPopover from './EntityPopover';
|
||||
import IconBox from 'components/IconBox';
|
||||
@@ -21,7 +21,7 @@ const EntityNavButton = ({ isActive, route, toggleSidebar }: Props) => {
|
||||
const inactiveArrowColor = useColorModeValue('var(--chakra-colors-gray-600)', 'var(--chakra-colors-gray-200)');
|
||||
const activeTextColor = useColorModeValue('gray.700', 'white');
|
||||
const inactiveTextColor = useColorModeValue('gray.600', 'gray.200');
|
||||
const inactiveIconColor = useColorModeValue('gray.100', 'gray.600');
|
||||
const hoverBg = useColorModeValue('blue.100', 'blue.800');
|
||||
|
||||
return (
|
||||
<EntityPopover isOpen={isOpen} onClose={onClose} toggleSidebar={toggleSidebar}>
|
||||
@@ -35,7 +35,7 @@ const EntityNavButton = ({ isActive, route, toggleSidebar }: Props) => {
|
||||
bg="transparent"
|
||||
transition={variantChange}
|
||||
mx="auto"
|
||||
ps="10px"
|
||||
px={1}
|
||||
py="12px"
|
||||
borderRadius="15px"
|
||||
w="100%"
|
||||
@@ -47,13 +47,17 @@ const EntityNavButton = ({ isActive, route, toggleSidebar }: Props) => {
|
||||
_focus={{
|
||||
boxShadow: '0px 7px 11px rgba(0, 0, 0, 0.04)',
|
||||
}}
|
||||
_hover={{
|
||||
bg: hoverBg,
|
||||
}}
|
||||
borderWidth="0px"
|
||||
rightIcon={<ArrowCircleRight size={24} color={activeArrowColor} />}
|
||||
>
|
||||
<Flex>
|
||||
<IconBox bg="blue.300" color="white" h="42px" w="42px" me="12px" transition={variantChange}>
|
||||
{route.icon(true)}
|
||||
<Flex alignItems="center" w="100%">
|
||||
<IconBox color="blue.300" h="30px" w="30px" me="6px" transition={variantChange} fontWeight="bold">
|
||||
{route.icon(false)}
|
||||
</IconBox>
|
||||
<Text color={activeTextColor} my="auto" fontSize="lg">
|
||||
<Text color={activeTextColor} fontSize="md" fontWeight="bold">
|
||||
{t(route.name)}
|
||||
</Text>
|
||||
</Flex>
|
||||
@@ -67,7 +71,7 @@ const EntityNavButton = ({ isActive, route, toggleSidebar }: Props) => {
|
||||
bg="transparent"
|
||||
mx="auto"
|
||||
py="12px"
|
||||
ps="10px"
|
||||
ps={1}
|
||||
borderRadius="15px"
|
||||
w="100%"
|
||||
_active={{
|
||||
@@ -78,13 +82,17 @@ const EntityNavButton = ({ isActive, route, toggleSidebar }: Props) => {
|
||||
_focus={{
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
_hover={{
|
||||
bg: hoverBg,
|
||||
}}
|
||||
borderWidth="0px"
|
||||
rightIcon={<ArrowCircleRight size={20} color={inactiveArrowColor} />}
|
||||
>
|
||||
<Flex>
|
||||
<IconBox bg={inactiveIconColor} color="blue.300" h="34px" w="34px" me="12px" transition={variantChange}>
|
||||
<Flex alignItems="center" w="100%">
|
||||
<IconBox color="blue.300" h="30px" w="30px" me="6px" transition={variantChange} fontWeight="bold">
|
||||
{route.icon(false)}
|
||||
</IconBox>
|
||||
<Text color={inactiveTextColor} my="auto" fontSize="sm">
|
||||
<Text color={inactiveTextColor} fontSize="md" fontWeight="bold">
|
||||
{t(route.name)}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
useBreakpoint,
|
||||
} from '@chakra-ui/react';
|
||||
import { FocusableElement } from '@chakra-ui/utils';
|
||||
import { TreeStructure, Buildings, X } from 'phosphor-react';
|
||||
import { TreeStructure, Buildings, X } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -119,8 +119,9 @@ const EntityPopover = ({ isOpen, onClose, children, toggleSidebar }: Props) => {
|
||||
const initRef = React.useRef<HTMLButtonElement>();
|
||||
|
||||
const goTo = useCallback(
|
||||
(id: string, type: string) => {
|
||||
(id, type) => {
|
||||
navigate(`/${type}/${id}`);
|
||||
onClose();
|
||||
if (breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md') toggleSidebar();
|
||||
},
|
||||
[breakpoint],
|
||||
|
||||
@@ -1,38 +1,15 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import React from 'react';
|
||||
import { Button, Flex, Text, useColorModeValue } from '@chakra-ui/react';
|
||||
import { AccordionButton, AccordionItem, Flex, Text, useColorModeValue } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import IconBox from 'components/IconBox';
|
||||
import { Route } from 'models/Routes';
|
||||
import { SingleRoute } from 'models/Routes';
|
||||
|
||||
const variantChange = '0.2s linear';
|
||||
|
||||
const commonStyle = {
|
||||
boxSize: 'initial',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
transition: variantChange,
|
||||
bg: 'transparent',
|
||||
ps: '6px',
|
||||
py: '12px',
|
||||
pe: '4px',
|
||||
w: '100%',
|
||||
borderRadius: '15px',
|
||||
_active: {
|
||||
bg: 'inherit',
|
||||
transform: 'none',
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
_focus: {
|
||||
boxShadow: '0px 7px 11px rgba(0, 0, 0, 0.04)',
|
||||
},
|
||||
} as const;
|
||||
|
||||
type Props = {
|
||||
isActive: boolean;
|
||||
route: Route;
|
||||
route: SingleRoute;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
@@ -40,42 +17,64 @@ export const NavLinkButton = ({ isActive, route, toggleSidebar }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const activeTextColor = useColorModeValue('gray.700', 'white');
|
||||
const inactiveTextColor = useColorModeValue('gray.600', 'gray.200');
|
||||
const inactiveIconColor = useColorModeValue('gray.100', 'gray.600');
|
||||
const activeBg = useColorModeValue('blue.50', 'blue.900');
|
||||
const hoverBg = useColorModeValue('blue.100', 'blue.800');
|
||||
|
||||
if (route.navButton) {
|
||||
return route.navButton(isActive, toggleSidebar, route) as JSX.Element;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink to={route.path.replace(':id', '0')} key={uuid()} style={{ width: '100%' }}>
|
||||
<NavLink to={route.path.replace(':id', '0')} style={{ width: '100%' }}>
|
||||
{isActive ? (
|
||||
<Button {...commonStyle} boxShadow="none">
|
||||
<Flex>
|
||||
<IconBox bg="blue.300" color="white" h="38px" w="38px" me="6px" transition={variantChange}>
|
||||
{route.icon(true)}
|
||||
</IconBox>
|
||||
<Text color={activeTextColor} my="auto" fontSize="md">
|
||||
{t(route.name)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Button>
|
||||
<AccordionItem w="152px" borderTop="0px" borderBottom="0px">
|
||||
<AccordionButton
|
||||
px={1}
|
||||
h={{
|
||||
md: '40px',
|
||||
lg: '50px',
|
||||
}}
|
||||
borderRadius="15px"
|
||||
w="100%"
|
||||
bg={activeBg}
|
||||
_hover={{
|
||||
bg: hoverBg,
|
||||
}}
|
||||
>
|
||||
<Flex alignItems="center" w="100%">
|
||||
<IconBox color="blue.300" h="30px" w="30px" me="6px" transition={variantChange} fontWeight="bold">
|
||||
{route.icon(false)}
|
||||
</IconBox>
|
||||
<Text color={activeTextColor} fontSize="md" fontWeight="bold">
|
||||
{t(route.name)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
</AccordionItem>
|
||||
) : (
|
||||
<Button
|
||||
{...commonStyle}
|
||||
ps="6px"
|
||||
_focus={{
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
>
|
||||
<Flex>
|
||||
<IconBox bg={inactiveIconColor} color="blue.300" h="34px" w="34px" me="6px" transition={variantChange}>
|
||||
{route.icon(false)}
|
||||
</IconBox>
|
||||
<Text color={inactiveTextColor} my="auto" fontSize="sm">
|
||||
{t(route.name)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Button>
|
||||
<AccordionItem w="152px" borderTop="0px" borderBottom="0px">
|
||||
<AccordionButton
|
||||
px={1}
|
||||
h={{
|
||||
md: '40px',
|
||||
lg: '50px',
|
||||
}}
|
||||
borderRadius="15px"
|
||||
w="100%"
|
||||
_hover={{
|
||||
bg: hoverBg,
|
||||
}}
|
||||
>
|
||||
<Flex alignItems="center" w="100%">
|
||||
<IconBox color="blue.300" h="30px" w="30px" me="6px" transition={variantChange} fontWeight="bold">
|
||||
{route.icon(false)}
|
||||
</IconBox>
|
||||
<Text color={inactiveTextColor} fontSize="md" fontWeight="bold">
|
||||
{t(route.name)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
</AccordionItem>
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
39
src/layout/Sidebar/NestedNavButton/SubNavigationButton.tsx
Normal file
39
src/layout/Sidebar/NestedNavButton/SubNavigationButton.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import * as React from 'react';
|
||||
import { Button, useColorModeValue } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { SubRoute } from 'models/Routes';
|
||||
|
||||
type Props = {
|
||||
isActive: (path: string) => boolean;
|
||||
route: SubRoute;
|
||||
};
|
||||
|
||||
const SubNavigationButton = ({ isActive, route }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const activeTextColor = useColorModeValue('gray.700', 'white');
|
||||
const inactiveTextColor = useColorModeValue('gray.600', 'gray.200');
|
||||
const activeBg = useColorModeValue('blue.50', 'blue.900');
|
||||
const hoverBg = useColorModeValue('blue.100', 'blue.800');
|
||||
|
||||
const isCurrentlyActive = isActive(route.path);
|
||||
|
||||
return (
|
||||
<NavLink to={route.path.replace(':id', '0')} style={{ width: '100%' }}>
|
||||
<Button
|
||||
w="100%"
|
||||
justifyContent="left"
|
||||
color={isCurrentlyActive ? activeTextColor : inactiveTextColor}
|
||||
bg={isCurrentlyActive ? activeBg : 'transparent'}
|
||||
_hover={{
|
||||
bg: hoverBg,
|
||||
}}
|
||||
border="none"
|
||||
>
|
||||
{t(route.name)}
|
||||
</Button>
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubNavigationButton;
|
||||
64
src/layout/Sidebar/NestedNavButton/index.tsx
Normal file
64
src/layout/Sidebar/NestedNavButton/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Flex,
|
||||
Text,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SubNavigationButton from './SubNavigationButton';
|
||||
import IconBox from 'components/IconBox';
|
||||
import { RouteGroup } from 'models/Routes';
|
||||
|
||||
const variantChange = '0.2s linear';
|
||||
|
||||
type Props = {
|
||||
isActive: (path: string) => boolean;
|
||||
route: RouteGroup;
|
||||
};
|
||||
|
||||
const NestedNavButton = ({ isActive, route }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const inactiveTextColor = useColorModeValue('gray.600', 'gray.200');
|
||||
const hoverBg = useColorModeValue('blue.100', 'blue.800');
|
||||
|
||||
return (
|
||||
<AccordionItem w="152px" borderTop="0px" borderBottom="0px">
|
||||
<AccordionButton
|
||||
px={1}
|
||||
h={{
|
||||
md: '40px',
|
||||
lg: '50px',
|
||||
}}
|
||||
_hover={{
|
||||
bg: hoverBg,
|
||||
}}
|
||||
borderRadius="15px"
|
||||
w="100%"
|
||||
>
|
||||
<Flex alignItems="center" w="100%">
|
||||
<IconBox color="blue.300" h="30px" w="30px" me="6px" transition={variantChange} fontWeight="bold">
|
||||
{route.icon(false)}
|
||||
</IconBox>
|
||||
<Text size="md" fontWeight="bold" color={inactiveTextColor}>
|
||||
{typeof route.name === 'string' ? t(route.name) : route.name(t)}
|
||||
</Text>
|
||||
</Flex>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pl="18px" paddingEnd={0} pr="-18px">
|
||||
<Box pl={1} pr={-1} borderLeft="1px solid #63b3ed">
|
||||
{route.children.map((subRoute) => (
|
||||
<SubNavigationButton key={subRoute.path} route={subRoute} isActive={isActive} />
|
||||
))}
|
||||
</Box>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default NestedNavButton;
|
||||
@@ -12,11 +12,12 @@ import {
|
||||
Spacer,
|
||||
useBreakpoint,
|
||||
VStack,
|
||||
Accordion,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { NavLinkButton } from './NavLinkButton';
|
||||
import NestedNavButton from './NestedNavButton';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { Route } from 'models/Routes';
|
||||
|
||||
@@ -60,14 +61,25 @@ export const Sidebar = ({ routes, isOpen, toggle, logo, version, topNav, childre
|
||||
const sidebarContent = React.useMemo(
|
||||
() => (
|
||||
<>
|
||||
<VStack spacing={2} alignItems="start" w="100%" px={4}>
|
||||
{topNav ? topNav(isRouteActive, toggle) : null}
|
||||
{routes
|
||||
.filter(({ hidden, authorized }) => !hidden && authorized.includes(user?.userRole ?? ''))
|
||||
.map((route) => (
|
||||
<NavLinkButton key={uuid()} isActive={isRouteActive(route.path)} route={route} toggleSidebar={toggle} />
|
||||
))}
|
||||
</VStack>
|
||||
<Accordion allowToggle>
|
||||
<VStack spacing={2} alignItems="start" w="100%" px={4}>
|
||||
{topNav ? topNav(isRouteActive, toggle) : null}
|
||||
{routes
|
||||
.filter(({ hidden, authorized }) => !hidden && authorized.includes(user?.userRole ?? ''))
|
||||
.map((route) =>
|
||||
route.children ? (
|
||||
<NestedNavButton key={route.id} isActive={isRouteActive} route={route} />
|
||||
) : (
|
||||
<NavLinkButton
|
||||
key={route.id}
|
||||
isActive={isRouteActive(route.path)}
|
||||
route={route}
|
||||
toggleSidebar={toggle}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</VStack>
|
||||
</Accordion>
|
||||
<Spacer />
|
||||
<Box mb={2}>{children}</Box>
|
||||
<Box>
|
||||
@@ -117,7 +129,8 @@ export const Sidebar = ({ routes, isOpen, toggle, logo, version, topNav, childre
|
||||
h="calc(100vh - 32px)"
|
||||
my="16px"
|
||||
ml="16px"
|
||||
borderRadius="16px"
|
||||
borderRadius="15px"
|
||||
border="0.5px solid"
|
||||
>
|
||||
{brand}
|
||||
<Flex direction="column" h="calc(100vh - 160px)" alignItems="center" overflowY="auto">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useBoolean, useBreakpoint, useColorMode } from '@chakra-ui/react';
|
||||
import { useBoolean, useColorMode } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -9,31 +9,69 @@ import { Sidebar } from './Sidebar';
|
||||
import darkLogo from 'assets/Logo_Dark_Mode.svg';
|
||||
import lightLogo from 'assets/Logo_Light_Mode.svg';
|
||||
import LanguageSwitcher from 'components/LanguageSwitcher';
|
||||
import { Route as RouteType } from 'models/Routes';
|
||||
import { RouteName } from 'models/Routes';
|
||||
import NotFoundPage from 'pages/NotFound';
|
||||
import routes from 'router/routes';
|
||||
|
||||
const Layout = () => {
|
||||
const { t } = useTranslation();
|
||||
const { colorMode } = useColorMode();
|
||||
const location = useLocation();
|
||||
const breakpoint = useBreakpoint('xl');
|
||||
const [isSidebarOpen, { toggle: toggleSidebar }] = useBoolean(breakpoint !== 'base' && breakpoint !== 'sm');
|
||||
const { colorMode } = useColorMode();
|
||||
const [isSidebarOpen, { toggle: toggleSidebar }] = useBoolean(false);
|
||||
document.documentElement.dir = 'ltr';
|
||||
|
||||
const activeRoute = React.useMemo(() => {
|
||||
const route = routes.find(
|
||||
(r) => r.path === location.pathname || location.pathname.split('/')[1] === r.path.split('/')[1],
|
||||
);
|
||||
let name: RouteName = '';
|
||||
for (const route of routes) {
|
||||
if (!route.children && route.path === location.pathname) {
|
||||
name = route.navName ?? route.name;
|
||||
break;
|
||||
}
|
||||
if (route.path?.includes('/:')) {
|
||||
const routePath = route.path.split('/:')[0];
|
||||
const currPath = location.pathname.split('/');
|
||||
if (routePath && location.pathname.startsWith(routePath) && currPath.length === 3) {
|
||||
name = route.navName ?? route.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (route.children) {
|
||||
for (const child of route.children) {
|
||||
if (child.path === location.pathname) {
|
||||
name = child.navName ?? child.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (route) return route.navName ? t(route.navName) : t(route.name);
|
||||
if (typeof name === 'function') return name(t);
|
||||
|
||||
return '';
|
||||
if (name.includes('PATH')) {
|
||||
name = location.pathname.split('/')[location.pathname.split('/').length - 1] ?? '';
|
||||
}
|
||||
|
||||
if (name.includes('RAW-')) name.replace('RAW-', '');
|
||||
|
||||
return t(name);
|
||||
}, [t, location.pathname]);
|
||||
|
||||
const getRoutes = (r: RouteType[]) =>
|
||||
// @ts-ignore
|
||||
r.map((route: RouteType) => <Route path={route.path} element={<route.component />} key={uuid()} />);
|
||||
const routeInstances = React.useMemo(() => {
|
||||
const instances = [];
|
||||
|
||||
for (const route of routes) {
|
||||
// @ts-ignore
|
||||
if (!route.children) instances.push(<Route path={route.path} element={<route.component />} key={route.id} />);
|
||||
else {
|
||||
for (const child of route.children) {
|
||||
// @ts-ignore
|
||||
instances.push(<Route path={child.path} element={<child.component />} key={child.id} />);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instances;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -57,9 +95,7 @@ const Layout = () => {
|
||||
/>
|
||||
<Navbar toggleSidebar={toggleSidebar} languageSwitcher={<LanguageSwitcher />} activeRoute={activeRoute} />
|
||||
<PageContainer waitForUser>
|
||||
<Routes>
|
||||
{[...getRoutes(routes as RouteType[]), <Route path="*" element={<NotFoundPage />} key={uuid()} />]}
|
||||
</Routes>
|
||||
<Routes>{[...routeInstances, <Route path="*" element={<NotFoundPage />} key={uuid()} />]}</Routes>
|
||||
</PageContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
48
src/models/Inventory.ts
Normal file
48
src/models/Inventory.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Note } from './Note';
|
||||
|
||||
export type InventoryTagApiResponse = {
|
||||
contact: string;
|
||||
created: number;
|
||||
description: string;
|
||||
devClass: string;
|
||||
deviceConfiguration: string;
|
||||
deviceRules: {
|
||||
firmwareUpgrade: 'inherit' | 'on' | 'off';
|
||||
rcOnly: 'inherit' | 'on' | 'off';
|
||||
rrm: 'inherit' | 'on' | 'off';
|
||||
};
|
||||
deviceType: string;
|
||||
entity: string;
|
||||
extendedInfo?: {
|
||||
venue?: {
|
||||
description: string;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
entity?: {
|
||||
description: string;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
deviceConfiguration?: {
|
||||
description: string;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
geoCode: string;
|
||||
id: string;
|
||||
locale: string;
|
||||
location: string;
|
||||
managementPolicy: string;
|
||||
modified: number;
|
||||
name: string;
|
||||
notes: Note[];
|
||||
qrCode: string;
|
||||
realMacAddress: string;
|
||||
serialNumber: string;
|
||||
state: string;
|
||||
subscriber: string;
|
||||
tags: string[];
|
||||
venue: string;
|
||||
};
|
||||
@@ -1,14 +1,53 @@
|
||||
import { ReactNode } from 'react';
|
||||
import React, { LazyExoticComponent } from 'react';
|
||||
|
||||
export type Route = {
|
||||
export type RouteName = string | ((t: (s: string) => string) => string);
|
||||
|
||||
export type SubRoute = {
|
||||
id: string;
|
||||
authorized: string[];
|
||||
path: string;
|
||||
name: string;
|
||||
navName?: string;
|
||||
icon: (active: boolean) => ReactNode;
|
||||
navButton?: (isActive: boolean, toggleSidebar: () => void, route: Route) => React.ReactNode;
|
||||
name: RouteName;
|
||||
component: React.ReactElement | LazyExoticComponent<React.ComponentType<unknown>>;
|
||||
navName?: RouteName;
|
||||
hidden?: boolean;
|
||||
icon?: undefined;
|
||||
navButton?: undefined;
|
||||
isEntity?: undefined;
|
||||
isCustom?: undefined;
|
||||
children?: undefined;
|
||||
};
|
||||
|
||||
export type RouteGroup = {
|
||||
id: string;
|
||||
authorized: string[];
|
||||
name: RouteName;
|
||||
icon: (active: boolean) => React.ReactElement;
|
||||
children: SubRoute[];
|
||||
hidden?: boolean;
|
||||
path?: undefined;
|
||||
navName?: undefined;
|
||||
navButton?: undefined;
|
||||
isEntity?: undefined;
|
||||
isCustom?: undefined;
|
||||
};
|
||||
|
||||
export type SingleRoute = {
|
||||
id: string;
|
||||
authorized: string[];
|
||||
path: string;
|
||||
name: RouteName;
|
||||
navName?: RouteName;
|
||||
icon: (active: boolean) => React.ReactElement;
|
||||
navButton?: (
|
||||
isActive: boolean,
|
||||
toggleSidebar: () => void,
|
||||
route: Route,
|
||||
) => React.ReactElement | LazyExoticComponent<React.ComponentType<unknown>>;
|
||||
isEntity?: boolean;
|
||||
component: unknown;
|
||||
component: React.ReactElement | LazyExoticComponent<React.ComponentType<unknown>>;
|
||||
hidden?: boolean;
|
||||
isCustom?: boolean;
|
||||
children?: undefined;
|
||||
};
|
||||
|
||||
export type Route = SingleRoute | RouteGroup;
|
||||
|
||||
@@ -1,13 +1,42 @@
|
||||
import { Note } from './Note';
|
||||
|
||||
export interface User {
|
||||
name: string;
|
||||
export type UserRole =
|
||||
| 'root'
|
||||
| 'admin'
|
||||
| 'subscriber'
|
||||
| 'partner'
|
||||
| 'csr'
|
||||
| 'system'
|
||||
| 'installer'
|
||||
| 'noc'
|
||||
| 'accounting';
|
||||
|
||||
export type User = {
|
||||
avatar: string;
|
||||
blackListed: boolean;
|
||||
creationDate: number;
|
||||
currentLoginURI: string;
|
||||
currentPassword: string;
|
||||
description: string;
|
||||
currentPassword?: string;
|
||||
id: string;
|
||||
email: string;
|
||||
userRole: string;
|
||||
id: string;
|
||||
lastEmailCheck: number;
|
||||
lastLogin: number;
|
||||
lastPasswordChange: number;
|
||||
lastPasswords: string[];
|
||||
locale: string;
|
||||
location: string;
|
||||
modified: number;
|
||||
name: string;
|
||||
notes: Note[];
|
||||
oauthType: string;
|
||||
oauthUserInfo: string;
|
||||
owner: string;
|
||||
securityPolicy: string;
|
||||
securityPolicyChange: number;
|
||||
signingUp: string;
|
||||
suspended: boolean;
|
||||
userRole: UserRole;
|
||||
userTypeProprietaryInfo: {
|
||||
authenticatorSecret: string;
|
||||
mfa: {
|
||||
@@ -16,6 +45,9 @@ export interface User {
|
||||
};
|
||||
mobiles: { number: string }[];
|
||||
};
|
||||
suspended: boolean;
|
||||
notes: Note[];
|
||||
}
|
||||
validated: boolean;
|
||||
validationDate: number;
|
||||
validationEmail: string;
|
||||
validationURI: string;
|
||||
waitingForEmailCheck: boolean;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
useBoolean,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { Flask } from 'phosphor-react';
|
||||
import { Flask } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Textarea,
|
||||
useBoolean,
|
||||
} from '@chakra-ui/react';
|
||||
import { WarningCircle } from 'phosphor-react';
|
||||
import { WarningCircle } from '@phosphor-icons/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import { UploadSimple } from 'phosphor-react';
|
||||
import { UploadSimple } from '@phosphor-icons/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ImportConfigurationModal from './Modal';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Button, useDisclosure, Modal, ModalBody, ModalContent, ModalOverlay, Box } from '@chakra-ui/react';
|
||||
import { useDisclosure, Modal, ModalBody, ModalContent, ModalOverlay, Box } from '@chakra-ui/react';
|
||||
import { Formik } from 'formik';
|
||||
import { Plus } from 'phosphor-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CREATE_INTERFACE_SCHEMA, SINGLE_INTERFACE_SCHEMA } from './interfacesConstants';
|
||||
import CloseButton from 'components/Buttons/CloseButton';
|
||||
import CreateButton from 'components/Buttons/CreateButton';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import SelectField from 'components/FormFields/SelectField';
|
||||
import StringField from 'components/FormFields/StringField';
|
||||
@@ -35,17 +35,14 @@ const CreateInterfaceButton = ({ editing, arrayHelpers: { push: pushInterface },
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
type="button"
|
||||
<CreateButton
|
||||
label={t('configurations.add_interface')}
|
||||
onClick={onOpen}
|
||||
rightIcon={<Plus size={20} />}
|
||||
isCompact={arrLength !== 0}
|
||||
hidden={!editing}
|
||||
size="lg"
|
||||
borderRadius={0}
|
||||
minWidth={24}
|
||||
>
|
||||
{t('configurations.add_interface')}
|
||||
</Button>
|
||||
/>
|
||||
<Modal onClose={onClose} isOpen={isOpen} size="sm" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Button, Heading, useColorModeValue, useMultiStyleConfig, useTab } from '@chakra-ui/react';
|
||||
import { Tab, useColorMode, useMultiStyleConfig, useTab } from '@chakra-ui/react';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const InterfaceTab = React.forwardRef((
|
||||
{
|
||||
index,
|
||||
...props
|
||||
}: {
|
||||
index: number
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const InterfaceTab: React.FC<{ index: number }> = React.forwardRef(({ index, ...props }, ref) => {
|
||||
const { value } = useFastField({ name: `configuration[${index}]` });
|
||||
|
||||
const bgColorSelected = useColorModeValue('gray.100', 'gray.800');
|
||||
const bgColorUnSelected = useColorModeValue('white', 'gray.700');
|
||||
|
||||
const { colorMode } = useColorMode();
|
||||
const isLight = colorMode === 'light';
|
||||
// @ts-ignore
|
||||
const tabProps = useTab({ ...props, ref });
|
||||
const isSelected = !!tabProps['aria-selected'];
|
||||
|
||||
// 2. Hook into the Tabs `size`, `variant`, props
|
||||
const styles = useMultiStyleConfig('Tabs', tabProps);
|
||||
|
||||
const name = useMemo(() => {
|
||||
@@ -32,16 +20,15 @@ const InterfaceTab = React.forwardRef((
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
__css={styles.tab}
|
||||
{...tabProps}
|
||||
bgColor={isSelected ? bgColorSelected : bgColorUnSelected}
|
||||
_focus={{ outline: 'none !important' }}
|
||||
<Tab
|
||||
_selected={{
|
||||
// @ts-ignore
|
||||
...styles.tab?._selected,
|
||||
borderBottomColor: isLight ? 'gray.100' : 'gray.800',
|
||||
}}
|
||||
>
|
||||
<Heading size="md">
|
||||
{name} ({value?.role})
|
||||
</Heading>
|
||||
</Button>
|
||||
{name} ({value?.role})
|
||||
</Tab>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Center, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||
import { Box, Center, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CreateInterfaceButton from './CreateInterfaceButton';
|
||||
import InterfaceTab from './InterfaceTab';
|
||||
@@ -42,48 +42,38 @@ const Interfaces = ({ editing, arrayHelpers, interfacesLength }) => {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tabs
|
||||
index={tabIndex}
|
||||
onChange={handleTabsChange}
|
||||
isLazy
|
||||
variant="enclosed-colored"
|
||||
w="100%"
|
||||
px={0}
|
||||
colorScheme="blue"
|
||||
>
|
||||
<TabList
|
||||
w="100%"
|
||||
overflowX="auto"
|
||||
style={{
|
||||
overflowY: 'hidden',
|
||||
}}
|
||||
>
|
||||
{Array(interfacesLength)
|
||||
.fill(1)
|
||||
.map((el, i) => (
|
||||
<InterfaceTab key={i} index={i} />
|
||||
))}
|
||||
<CreateInterfaceButton
|
||||
editing={editing}
|
||||
arrayHelpers={arrayHelpers}
|
||||
setTabIndex={setTabIndex}
|
||||
arrLength={interfacesLength}
|
||||
/>
|
||||
</TabList>
|
||||
<Card variant="widget" mb={4} borderRadius={0}>
|
||||
<CardBody display="unset">
|
||||
<TabPanels>
|
||||
{Array(interfacesLength)
|
||||
.fill(1)
|
||||
.map((el, i) => (
|
||||
<TabPanel overflowX="auto" px={0} key={i}>
|
||||
<SingleInterface index={i} remove={handleRemove} editing={editing} />
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Tabs>
|
||||
<Card variant="widget">
|
||||
<CardBody display="block">
|
||||
<Box display="unset" position="unset" w="100%">
|
||||
<Tabs index={tabIndex} onChange={handleTabsChange} variant="enclosed" isLazy w="100%">
|
||||
<Box overflowX="auto" overflowY="auto" pt={1} h="56px">
|
||||
<TabList mt={0}>
|
||||
{Array(interfacesLength)
|
||||
.fill(1)
|
||||
.map((el, i) => (
|
||||
<InterfaceTab key={i} index={i} />
|
||||
))}
|
||||
<CreateInterfaceButton
|
||||
editing={editing}
|
||||
arrayHelpers={arrayHelpers}
|
||||
setTabIndex={setTabIndex}
|
||||
arrLength={interfacesLength}
|
||||
/>
|
||||
</TabList>
|
||||
</Box>
|
||||
<TabPanels w="100%">
|
||||
{Array(interfacesLength)
|
||||
.fill(1)
|
||||
.map((el, i) => (
|
||||
<TabPanel key={i}>
|
||||
<SingleInterface index={i} remove={handleRemove} editing={editing} />
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Center } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { INTERFACE_SSID_SCHEMA } from '../../interfacesConstants';
|
||||
@@ -22,9 +21,14 @@ const CreateSsidButton = ({ editing, pushSsid, setTabIndex, arrLength }) => {
|
||||
if (!editing) return null;
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<CreateButton label={t('configurations.add_ssid')} onClick={createSsid} borderRadius={0} />
|
||||
</Center>
|
||||
<CreateButton
|
||||
label={t('configurations.add_ssid')}
|
||||
onClick={createSsid}
|
||||
borderRadius={0}
|
||||
mt="auto"
|
||||
isCompact={arrLength !== 0}
|
||||
size="lg"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Heading, SimpleGrid, Spacer } from '@chakra-ui/react';
|
||||
import { Box, Flex, Heading, SimpleGrid, Spacer } from '@chakra-ui/react';
|
||||
import { getIn, useFormikContext } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -9,9 +9,7 @@ import Encryption from './Encryption';
|
||||
import LockedSsid from './LockedSsid';
|
||||
import PassPoint from './PassPoint';
|
||||
import DeleteButton from 'components/Buttons/DeleteButton';
|
||||
import Card from 'components/Card';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
import ConfigurationResourcePicker from 'components/CustomFields/ConfigurationResourcePicker';
|
||||
import MultiSelectField from 'components/FormFields/MultiSelectField';
|
||||
import SelectField from 'components/FormFields/SelectField';
|
||||
@@ -39,8 +37,8 @@ const SingleSsid = ({ editing, index, namePrefix, remove }) => {
|
||||
}, [getIn(values, `${namePrefix}`)]);
|
||||
|
||||
return (
|
||||
<Card mb={4} borderRadius={0}>
|
||||
<CardHeader flex="auto">
|
||||
<>
|
||||
<Flex px={4} py={2}>
|
||||
<Heading size="md" mr={2} pt={2}>
|
||||
#{index}
|
||||
</Heading>
|
||||
@@ -52,7 +50,7 @@ const SingleSsid = ({ editing, index, namePrefix, remove }) => {
|
||||
/>
|
||||
<Spacer />
|
||||
<DeleteButton isDisabled={!editing} onClick={removeSsid} label={t('configurations.delete_ssid')} />
|
||||
</CardHeader>
|
||||
</Flex>
|
||||
<CardBody display="unset">
|
||||
{isUsingCustomRadius ? (
|
||||
<>
|
||||
@@ -113,7 +111,7 @@ const SingleSsid = ({ editing, index, namePrefix, remove }) => {
|
||||
<LockedSsid variableBlockId={getIn(values, `${namePrefix}.__variableBlock`)[0]} />
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,76 +1,52 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Button, Heading, useColorModeValue, useMultiStyleConfig, useTab } from '@chakra-ui/react';
|
||||
import { Tab } from '@chakra-ui/react';
|
||||
import { useGetResource } from 'hooks/Network/Resources';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
const SsidTab = React.forwardRef(// eslint-disable-next-line react/prop-types
|
||||
(
|
||||
{
|
||||
index,
|
||||
interIndex,
|
||||
...props
|
||||
}: {
|
||||
index: number
|
||||
interIndex: number
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { value } = useFastField({ name: `configuration[${interIndex}].ssids[${index}]` });
|
||||
const { value: wifiBands } = useFastField({ name: `configuration[${interIndex}].ssids[${index}].wifi-bands` });
|
||||
const { data: resource } = useGetResource({
|
||||
id: value?.__variableBlock,
|
||||
enabled: value?.__variableBlock !== undefined,
|
||||
});
|
||||
const bgColorSelected = useColorModeValue('white', 'gray.700');
|
||||
const bgColorUnSelected = useColorModeValue('gray.100', 'gray.800');
|
||||
const SsidTab: React.FC<{ index: number; interIndex: number }> = React.forwardRef(
|
||||
// eslint-disable-next-line react/prop-types
|
||||
({ index, interIndex }) => {
|
||||
const { value } = useFastField({ name: `configuration[${interIndex}].ssids[${index}]` });
|
||||
const { value: wifiBands } = useFastField({ name: `configuration[${interIndex}].ssids[${index}].wifi-bands` });
|
||||
const { data: resource } = useGetResource({
|
||||
id: value?.__variableBlock,
|
||||
enabled: value?.__variableBlock !== undefined,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
const tabProps = useTab({ ...props, ref });
|
||||
const isSelected = !!tabProps['aria-selected'];
|
||||
|
||||
const styles = useMultiStyleConfig('Tabs', tabProps);
|
||||
|
||||
const name = useMemo(() => {
|
||||
if (value?.name) {
|
||||
return value.name.length <= 15 ? value.name : `${value.name.substring(0, 12)}...`;
|
||||
}
|
||||
if (resource?.variables && resource?.variables[0]?.value) {
|
||||
try {
|
||||
const json = JSON.parse(resource?.variables[0]?.value);
|
||||
return json.name.length <= 15 ? json.name : `${json.name.substring(0, 12)}...`;
|
||||
} catch (e) {
|
||||
return '';
|
||||
const name = useMemo(() => {
|
||||
if (value?.name) {
|
||||
return value.name.length <= 12 ? value.name : `${value.name.substring(0, 9)}...`;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}, [value, resource]);
|
||||
|
||||
const bands = useMemo(() => {
|
||||
if (wifiBands) return wifiBands;
|
||||
if (resource?.variables && resource?.variables[0]?.value) {
|
||||
try {
|
||||
const json = JSON.parse(resource?.variables[0]?.value);
|
||||
return json['wifi-bands'];
|
||||
} catch (e) {
|
||||
return '';
|
||||
if (resource?.variables && resource?.variables[0]?.value) {
|
||||
try {
|
||||
const json = JSON.parse(resource?.variables[0]?.value);
|
||||
return json.name.length <= 12 ? json.name : `${json.name.substring(0, 9)}...`;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}, [wifiBands, resource]);
|
||||
return (
|
||||
// @ts-ignore
|
||||
(<Button
|
||||
__css={styles.tab}
|
||||
{...tabProps}
|
||||
bgColor={isSelected ? bgColorSelected : bgColorUnSelected}
|
||||
_focus={{ outline: 'none !important' }}
|
||||
>
|
||||
<Heading size="md">
|
||||
|
||||
return '';
|
||||
}, [value, resource]);
|
||||
|
||||
const bands = useMemo(() => {
|
||||
if (wifiBands) return wifiBands;
|
||||
if (resource?.variables && resource?.variables[0]?.value) {
|
||||
try {
|
||||
const json = JSON.parse(resource?.variables[0]?.value);
|
||||
return json['wifi-bands'];
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}, [wifiBands, resource]);
|
||||
return (
|
||||
<Tab>
|
||||
{name} ({bands?.join(', ')})
|
||||
</Heading>
|
||||
</Button>)
|
||||
);
|
||||
});
|
||||
</Tab>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default React.memo(SsidTab);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Center, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||
import { Box, Center, Heading, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CreateSsidButton from './CreateSsidButton';
|
||||
import SingleSsid from './SingleSsid';
|
||||
import SsidTab from './SsidTab';
|
||||
import Card from 'components/Card';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
|
||||
const propTypes = {
|
||||
editing: PropTypes.bool.isRequired,
|
||||
@@ -31,46 +34,52 @@ const SsidList = ({ editing, index, arrayHelpers, ssidsLength }) => {
|
||||
if (ssidsLength === 0) {
|
||||
return (
|
||||
<Center>
|
||||
<CreateSsidButton editing={editing} pushSsid={arrayHelpers.push} />
|
||||
<CreateSsidButton editing={editing} pushSsid={arrayHelpers.push} arrLength={0} />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tabs index={tabIndex} onChange={handleTabsChange} isLazy variant="enclosed-colored" w="100%" colorScheme="blue">
|
||||
<TabList
|
||||
w="100%"
|
||||
overflowX="auto"
|
||||
style={{
|
||||
overflowY: 'hidden',
|
||||
}}
|
||||
>
|
||||
{Array(ssidsLength)
|
||||
.fill(1)
|
||||
.map((el, i) => (
|
||||
<SsidTab key={i} index={i} interIndex={index} />
|
||||
))}
|
||||
<CreateSsidButton
|
||||
editing={editing}
|
||||
pushSsid={arrayHelpers.push}
|
||||
setTabIndex={setTabIndex}
|
||||
arrLength={ssidsLength}
|
||||
/>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
{Array(ssidsLength)
|
||||
.fill(1)
|
||||
.map((el, i) => (
|
||||
<TabPanel overflowX="auto" p={0} key={i}>
|
||||
<SingleSsid
|
||||
index={i}
|
||||
namePrefix={`configuration[${index}].ssids[${i}]`}
|
||||
remove={handleRemove}
|
||||
editing={editing}
|
||||
/>
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<Card>
|
||||
<CardHeader mb={0}>
|
||||
<Heading size="md">SSIDs</Heading>
|
||||
</CardHeader>
|
||||
<CardBody display="block">
|
||||
<Box display="unset" position="unset" w="100%">
|
||||
<Tabs index={tabIndex} onChange={handleTabsChange} variant="enclosed" isLazy w="100%">
|
||||
<Box overflowX="auto" overflowY="auto" pt={1} h="56px">
|
||||
<TabList mt={0}>
|
||||
{Array(ssidsLength)
|
||||
.fill(1)
|
||||
.map((el, i) => (
|
||||
<SsidTab key={i} index={i} interIndex={index} />
|
||||
))}
|
||||
<CreateSsidButton
|
||||
editing={editing}
|
||||
pushSsid={arrayHelpers.push}
|
||||
setTabIndex={setTabIndex}
|
||||
arrLength={ssidsLength}
|
||||
/>
|
||||
</TabList>
|
||||
</Box>
|
||||
|
||||
<TabPanels w="100%">
|
||||
{Array(ssidsLength)
|
||||
.fill(1)
|
||||
.map((el, i) => (
|
||||
<TabPanel key={i}>
|
||||
<SingleSsid
|
||||
index={i}
|
||||
namePrefix={`configuration[${index}].ssids[${i}]`}
|
||||
remove={handleRemove}
|
||||
editing={editing}
|
||||
/>
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Flex, Heading, SimpleGrid, Spacer } from '@chakra-ui/react';
|
||||
import { Box, Flex, Heading, SimpleGrid, Spacer } from '@chakra-ui/react';
|
||||
import { FieldArray } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Captive from './Captive';
|
||||
@@ -36,7 +36,7 @@ const SingleInterface: React.FC<Props> = ({ editing, index, remove }) => {
|
||||
[],
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Box w="100%">
|
||||
<Flex>
|
||||
<div>
|
||||
<Heading size="md" borderBottom="1px solid">
|
||||
@@ -155,7 +155,7 @@ const SingleInterface: React.FC<Props> = ({ editing, index, remove }) => {
|
||||
/>
|
||||
)}
|
||||
</FieldArray>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,13 +10,14 @@ import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
} from '@chakra-ui/react';
|
||||
import { Plus } from '@phosphor-icons/react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { Plus } from 'phosphor-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SINGLE_RADIO_SCHEMA } from './radiosConstants';
|
||||
import CloseButton from 'components/Buttons/CloseButton';
|
||||
import CreateButton from 'components/Buttons/CreateButton';
|
||||
import ModalHeader from 'components/Modals/ModalHeader';
|
||||
import { useGetAllResources } from 'hooks/Network/Resources';
|
||||
|
||||
@@ -72,16 +73,14 @@ const RadioPicker = ({ editing, radios, arrayHelpers: { push: pushRadio }, setTa
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
type="button"
|
||||
<CreateButton
|
||||
label={t('configurations.add_radio')}
|
||||
size="lg"
|
||||
onClick={onOpen}
|
||||
rightIcon={<Plus size={20} />}
|
||||
isCompact={radios.length > 0}
|
||||
hidden={!editing}
|
||||
borderRadius={0}
|
||||
>
|
||||
{t('configurations.add_radio')}
|
||||
</Button>
|
||||
/>
|
||||
<Modal onClose={onClose} isOpen={isOpen} size="sm" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Button, Heading, useColorModeValue, useMultiStyleConfig, useTab } from '@chakra-ui/react';
|
||||
import { Tab, useColorMode, useMultiStyleConfig, useTab } from '@chakra-ui/react';
|
||||
import { useGetResource } from 'hooks/Network/Resources';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const RadioTab = React.forwardRef((
|
||||
{
|
||||
index,
|
||||
...props
|
||||
}: {
|
||||
index: number
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const RadioTab: React.FC<{ index: number }> = React.forwardRef(({ index, ...props }, ref) => {
|
||||
const { value } = useFastField({ name: `configuration[${index}]` });
|
||||
const { data: resource } = useGetResource({
|
||||
id: value?.__variableBlock,
|
||||
enabled: value?.__variableBlock !== undefined,
|
||||
});
|
||||
const bgColorSelected = useColorModeValue('gray.100', 'gray.800');
|
||||
const bgColorUnSelected = useColorModeValue('white', 'gray.700');
|
||||
const { colorMode } = useColorMode();
|
||||
const isLight = colorMode === 'light';
|
||||
|
||||
// @ts-ignore
|
||||
const tabProps = useTab({ ...props, ref });
|
||||
const isSelected = !!tabProps['aria-selected'];
|
||||
|
||||
// 2. Hook into the Tabs `size`, `variant`, props
|
||||
const styles = useMultiStyleConfig('Tabs', tabProps);
|
||||
@@ -44,15 +35,15 @@ const RadioTab = React.forwardRef((
|
||||
return '';
|
||||
}, [value, resource]);
|
||||
return (
|
||||
// @ts-ignore
|
||||
(<Button
|
||||
__css={styles.tab}
|
||||
{...tabProps}
|
||||
bgColor={isSelected ? bgColorSelected : bgColorUnSelected}
|
||||
_focus={{ outline: 'none !important' }}
|
||||
<Tab
|
||||
_selected={{
|
||||
// @ts-ignore
|
||||
...styles.tab?._selected,
|
||||
borderBottomColor: isLight ? 'gray.100' : 'gray.800',
|
||||
}}
|
||||
>
|
||||
<Heading size="md">{name} Radio</Heading>
|
||||
</Button>)
|
||||
{name} Radio
|
||||
</Tab>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Center, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||
import { Box, Center, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import RadioPicker from './RadioPicker';
|
||||
@@ -66,37 +66,27 @@ const Radios = ({ editing, arrayHelpers, radioBands, radioBandsLength }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
index={tabIndex}
|
||||
onChange={handleTabsChange}
|
||||
isLazy
|
||||
variant="enclosed-colored"
|
||||
colorScheme="blue"
|
||||
w="100%"
|
||||
px={0}
|
||||
>
|
||||
<TabList
|
||||
w="100%"
|
||||
overflowX="auto"
|
||||
style={{
|
||||
overflowY: 'hidden',
|
||||
}}
|
||||
>
|
||||
{tabs}
|
||||
<RadioPicker
|
||||
radios={radioBands}
|
||||
editing={editing}
|
||||
arrayHelpers={arrayHelpers}
|
||||
setTabIndex={setTabIndex}
|
||||
arrLength={radioBandsLength}
|
||||
/>
|
||||
</TabList>
|
||||
<Card variant="widget" mb={4} borderRadius={0}>
|
||||
<CardBody display="unset">
|
||||
<TabPanels>{panels}</TabPanels>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Tabs>
|
||||
<Card variant="widget">
|
||||
<CardBody display="block">
|
||||
<Box display="unset" position="unset" w="100%">
|
||||
<Tabs index={tabIndex} onChange={handleTabsChange} variant="enclosed" isLazy w="100%">
|
||||
<Box overflowX="auto" overflowY="auto" pt={1} h="56px">
|
||||
<TabList mt={0}>
|
||||
{tabs}
|
||||
<RadioPicker
|
||||
radios={radioBands}
|
||||
editing={editing}
|
||||
arrayHelpers={arrayHelpers}
|
||||
setTabIndex={setTabIndex}
|
||||
arrLength={radioBandsLength}
|
||||
/>
|
||||
</TabList>
|
||||
</Box>
|
||||
<TabPanels>{panels}</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Flex, Heading, IconButton, SimpleGrid, Spacer, Tooltip } from '@chakra-ui/react';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SERVICES_CLASSIFIER_DNS_SCHEMA, SERVICES_CLASSIFIER_PORTS_SCHEMA } from '../servicesConstants';
|
||||
import Card from 'components/Card';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Heading, IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { Plus } from 'phosphor-react';
|
||||
import { Plus } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ClassifierField } from './ClassifierField';
|
||||
import useFastField from 'hooks/useFastField';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Tooltip, useDisclosure, Modal, ModalBody, ModalContent, ModalOverlay } from '@chakra-ui/react';
|
||||
import { CheckCircle, WarningOctagon } from 'phosphor-react';
|
||||
import { CheckCircle, WarningOctagon } from '@phosphor-icons/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Tooltip, useDisclosure, Modal, ModalBody, ModalContent, ModalOverlay } from '@chakra-ui/react';
|
||||
import { CheckCircle, WarningOctagon } from 'phosphor-react';
|
||||
import { CheckCircle, WarningOctagon } from '@phosphor-icons/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Tooltip, useDisclosure, Modal, ModalBody, ModalContent, ModalOverlay } from '@chakra-ui/react';
|
||||
import { ArrowsOut } from 'phosphor-react';
|
||||
import { ArrowsOut } from '@phosphor-icons/react';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useInterfacesJsonDisplay from './useInterfacesJsonDisplay';
|
||||
|
||||
@@ -353,10 +353,8 @@ const ConfigurationSectionsCard = ({ configId, editing, setSections, label, onDe
|
||||
|
||||
return (
|
||||
<Card px={label ? 0 : undefined}>
|
||||
<CardHeader mb="10px" display="flex">
|
||||
<Box pt={1}>
|
||||
<Heading size="md">{label ?? configuration?.name}</Heading>
|
||||
</Box>
|
||||
<CardHeader>
|
||||
<Heading size="md">{label ?? configuration?.name}</Heading>
|
||||
<Spacer />
|
||||
<Box>
|
||||
<ViewConfigWarningsModal
|
||||
|
||||
@@ -140,7 +140,7 @@ const ConfigurationCard = ({ id }) => {
|
||||
return (
|
||||
<>
|
||||
<Card mb={4}>
|
||||
<CardHeader mb="10px" display="flex">
|
||||
<CardHeader>
|
||||
<Box pt={1}>
|
||||
<Heading size="md">{configuration?.name}</Heading>
|
||||
</Box>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Heading,
|
||||
Select,
|
||||
@@ -18,11 +17,11 @@ import {
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import axios from 'axios';
|
||||
import { FloppyDisk } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Modal } from '../../../../components/Modals/Modal';
|
||||
import SaveButton from 'components/Buttons/SaveButton';
|
||||
import LoadingOverlay from 'components/LoadingOverlay';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { useGetSystemLogLevelNames, useGetSystemLogLevels, useUpdateSystemLogLevels } from 'hooks/Network/System';
|
||||
|
||||
type Props = {
|
||||
@@ -138,15 +137,7 @@ const SystemLoggingModal = ({ modalProps, endpoint, token }: Props) => {
|
||||
maxWidth: { sm: '600px', md: '600px', lg: '600px', xl: '600px' },
|
||||
}}
|
||||
topRightButtons={
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
rightIcon={<FloppyDisk size={20} />}
|
||||
onClick={onUpdate}
|
||||
isDisabled={newLevels.length === 0}
|
||||
isLoading={updateLevels.isLoading}
|
||||
>
|
||||
{t('system.update_levels')} {newLevels.length > 0 ? newLevels.length : ''}
|
||||
</Button>
|
||||
<SaveButton onClick={onUpdate} isDisabled={newLevels.length === 0} isLoading={updateLevels.isLoading} />
|
||||
}
|
||||
>
|
||||
<>
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { IconButton, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import { Article } from 'phosphor-react';
|
||||
import { Article } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SystemLoggingModal from './Modal';
|
||||
import { EndpointApiResponse } from 'hooks/Network/Endpoints';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user