[WIFI-12575] Theme improvements

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2023-05-03 12:16:21 +02:00
parent e22ac04459
commit 3d2966739e
201 changed files with 10985 additions and 13230 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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;
}
}

View File

@@ -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';

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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> {

View File

@@ -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 {

View File

@@ -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';

View File

@@ -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';

View File

@@ -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

View File

@@ -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);

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View 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>
);
};

View 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>
);
};

View 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>
);

View 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;

View 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;
};

View File

@@ -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;

View File

@@ -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);

View 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;

View 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);

View 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>
);
};

View 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>;

View File

@@ -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} />;

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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();

View File

@@ -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';

View File

@@ -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';

View File

@@ -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} />;

View File

@@ -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}
/>

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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,

View File

@@ -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>;

View File

@@ -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';

View 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],
);
};

View File

@@ -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
View File

@@ -0,0 +1,7 @@
import 'i18next';
declare module 'i18next' {
interface CustomTypeOptions {
returnNull: false;
}
}

View File

@@ -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')}

View File

@@ -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>

View File

@@ -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],

View File

@@ -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>
);

View 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;

View 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;

View File

@@ -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">

View File

@@ -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
View 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;
};

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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>

View File

@@ -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>
);
});

View File

@@ -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>
);
};

View File

@@ -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"
/>
);
};

View File

@@ -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>
</>
);
};

View File

@@ -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);

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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>
);
});

View File

@@ -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>
);
};

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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

View File

@@ -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>

View File

@@ -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} />
}
>
<>

View File

@@ -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