mirror of
https://github.com/Telecominfraproject/wlan-cloud-owprov-ui.git
synced 2025-10-29 17:52:25 +00:00
[WIFI-11543] Added API keys support
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
29
package-lock.json
generated
29
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.8.0(12)",
|
||||
"version": "2.8.0(13)",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.8.0(12)",
|
||||
"version": "2.8.0(13)",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.11",
|
||||
@@ -64,6 +64,7 @@
|
||||
"@types/node": "^18.11.2",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-csv": "^1.1.3",
|
||||
"@types/react-datepicker": "4.8.0",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
@@ -4089,6 +4090,18 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-datepicker": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.8.0.tgz",
|
||||
"integrity": "sha512-20uzZsIf4moPAjjHDfPvH8UaOHZBxrkiQZoLS3wgKq8Xhp+95gdercLEdoA7/I8nR9R5Jz2qQkdMIM+Lq4AS1A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"@types/react": "*",
|
||||
"date-fns": "^2.0.1",
|
||||
"react-popper": "^2.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.7.tgz",
|
||||
@@ -14609,6 +14622,18 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-datepicker": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.8.0.tgz",
|
||||
"integrity": "sha512-20uzZsIf4moPAjjHDfPvH8UaOHZBxrkiQZoLS3wgKq8Xhp+95gdercLEdoA7/I8nR9R5Jz2qQkdMIM+Lq4AS1A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"@types/react": "*",
|
||||
"date-fns": "^2.0.1",
|
||||
"react-popper": "^2.2.5"
|
||||
}
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"version": "18.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.7.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.8.0(12)",
|
||||
"version": "2.8.0(13)",
|
||||
"description": "",
|
||||
"main": "index.tsx",
|
||||
"scripts": {
|
||||
@@ -72,6 +72,7 @@
|
||||
"@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-window": "^1.8.5",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"eslint": "8.25.0",
|
||||
|
||||
@@ -245,6 +245,7 @@
|
||||
"identification": "Identifizierung",
|
||||
"inherit": "Erben",
|
||||
"language": "Sprache",
|
||||
"last_use": "Zuletzt verwendeten",
|
||||
"lifetime": "Lebenszeit",
|
||||
"locale": "Gebietsschema",
|
||||
"logout": "Ausloggen",
|
||||
@@ -261,6 +262,7 @@
|
||||
"model": "Modell",
|
||||
"modified": "Geändert",
|
||||
"monthly": "Monatlich",
|
||||
"months": "Monate",
|
||||
"my_account": "Mein Konto",
|
||||
"name": "Name",
|
||||
"name_error": "Der Name muss weniger als 50 Zeichen lang sein",
|
||||
@@ -314,6 +316,7 @@
|
||||
"use_file": "Datei verwenden",
|
||||
"value": "Wert",
|
||||
"variable": "Variable",
|
||||
"view": "Aussicht",
|
||||
"view_details": "Details anzeigen",
|
||||
"view_in_gateway": "Im Controller anzeigen",
|
||||
"view_json": "JSON anzeigen",
|
||||
@@ -745,7 +748,9 @@
|
||||
"keys": {
|
||||
"description_error": "Die Beschreibung muss weniger als 64 Zeichen lang sein",
|
||||
"expire_error": "Der Ablauf darf nicht mehr als ein Jahr in der Zukunft liegen",
|
||||
"name_error": "Der Name sollte eindeutig sein und zwischen 6 und 20 Zeichen lang sein",
|
||||
"expires": "Läuft ab",
|
||||
"max_keys": "Max. Schlüssel erreicht (10)",
|
||||
"name_error": "Der Name sollte eindeutig sein und aus 6 bis 20 alphanumerischen Zeichen bestehen",
|
||||
"one": "API-Schlüssel",
|
||||
"other": "API-Schlüssel"
|
||||
},
|
||||
|
||||
@@ -245,6 +245,7 @@
|
||||
"identification": "Identification",
|
||||
"inherit": "Inherit",
|
||||
"language": "Language",
|
||||
"last_use": "Last Use",
|
||||
"lifetime": "Lifetime",
|
||||
"locale": "Locale",
|
||||
"logout": "Logout",
|
||||
@@ -261,6 +262,7 @@
|
||||
"model": "Model",
|
||||
"modified": "Modified",
|
||||
"monthly": "Monthly",
|
||||
"months": "Months",
|
||||
"my_account": "My Account",
|
||||
"name": "Name",
|
||||
"name_error": "Name must be less than 50 characters long",
|
||||
@@ -314,6 +316,7 @@
|
||||
"use_file": "Use File",
|
||||
"value": "Value",
|
||||
"variable": "Variable",
|
||||
"view": "View",
|
||||
"view_details": "View Details",
|
||||
"view_in_gateway": "View In Controller",
|
||||
"view_json": "View JSON",
|
||||
@@ -745,7 +748,9 @@
|
||||
"keys": {
|
||||
"description_error": "Description needs to be less than 64 characters long",
|
||||
"expire_error": "The expiry cannot be more than one year in the future",
|
||||
"name_error": "Name should be unique and be between 6 and 20 characters",
|
||||
"expires": "Expires",
|
||||
"max_keys": "Max keys reached (10)",
|
||||
"name_error": "Name should be unique and be between 6 and 20 alphanumeric characters",
|
||||
"one": "API Key",
|
||||
"other": "API Keys"
|
||||
},
|
||||
|
||||
@@ -245,6 +245,7 @@
|
||||
"identification": "identificación",
|
||||
"inherit": "Heredar",
|
||||
"language": "idioma",
|
||||
"last_use": "Utilizado por última vez",
|
||||
"lifetime": "Toda la vida",
|
||||
"locale": "lugar",
|
||||
"logout": "Cerrar sesión",
|
||||
@@ -261,6 +262,7 @@
|
||||
"model": "Modelo",
|
||||
"modified": "Modificado",
|
||||
"monthly": "Mensual",
|
||||
"months": "Meses",
|
||||
"my_account": "Mi cuenta",
|
||||
"name": "Nombre",
|
||||
"name_error": "El nombre debe tener menos de 50 caracteres",
|
||||
@@ -314,6 +316,7 @@
|
||||
"use_file": "Usar archivo",
|
||||
"value": "Valor",
|
||||
"variable": "Variable",
|
||||
"view": "Ver",
|
||||
"view_details": "Ver detalles",
|
||||
"view_in_gateway": "Ver en controlador",
|
||||
"view_json": "Ver JSON",
|
||||
@@ -745,7 +748,9 @@
|
||||
"keys": {
|
||||
"description_error": "La descripción debe tener menos de 64 caracteres",
|
||||
"expire_error": "El vencimiento no puede ser más de un año en el futuro",
|
||||
"name_error": "El nombre debe ser único y tener entre 6 y 20 caracteres",
|
||||
"expires": "Vence",
|
||||
"max_keys": "Número máximo de claves alcanzado (10)",
|
||||
"name_error": "El nombre debe ser único y tener entre 6 y 20 caracteres alfanuméricos",
|
||||
"one": "Clave API",
|
||||
"other": "Claves de api"
|
||||
},
|
||||
|
||||
@@ -245,6 +245,7 @@
|
||||
"identification": "Identification",
|
||||
"inherit": "Hériter",
|
||||
"language": "La langue",
|
||||
"last_use": "Dernière utilisation",
|
||||
"lifetime": "durée de vie",
|
||||
"locale": "lieu",
|
||||
"logout": "Connectez - Out",
|
||||
@@ -261,6 +262,7 @@
|
||||
"model": "Modèle",
|
||||
"modified": "Modifié",
|
||||
"monthly": "Mensuel",
|
||||
"months": "mois",
|
||||
"my_account": "Mon compte",
|
||||
"name": "Prénom",
|
||||
"name_error": "Le nom doit comporter moins de 50 caractères",
|
||||
@@ -314,6 +316,7 @@
|
||||
"use_file": "Utiliser le fichier",
|
||||
"value": "Valeur",
|
||||
"variable": "Variable",
|
||||
"view": "Vue",
|
||||
"view_details": "Voir les détails",
|
||||
"view_in_gateway": "Afficher dans le contrôleur",
|
||||
"view_json": "Afficher JSON",
|
||||
@@ -745,7 +748,9 @@
|
||||
"keys": {
|
||||
"description_error": "La description doit comporter moins de 64 caractères",
|
||||
"expire_error": "L'expiration ne peut pas être supérieure à un an dans le futur",
|
||||
"name_error": "Le nom doit être unique et comporter entre 6 et 20 caractères",
|
||||
"expires": "EXPIRÉ",
|
||||
"max_keys": "Max de clés atteint (10)",
|
||||
"name_error": "Le nom doit être unique et comporter entre 6 et 20 caractères alphanumériques",
|
||||
"one": "Clé API",
|
||||
"other": "Clés API"
|
||||
},
|
||||
|
||||
@@ -245,6 +245,7 @@
|
||||
"identification": "Identificação",
|
||||
"inherit": "Herdar",
|
||||
"language": "Língua",
|
||||
"last_use": "Usado por último",
|
||||
"lifetime": "Tempo de vida",
|
||||
"locale": "Localidade",
|
||||
"logout": "Sair",
|
||||
@@ -261,6 +262,7 @@
|
||||
"model": "Modelo",
|
||||
"modified": "Modificado",
|
||||
"monthly": "Por mês",
|
||||
"months": "Meses",
|
||||
"my_account": "Minha conta",
|
||||
"name": "Nome",
|
||||
"name_error": "O nome deve ter menos de 50 caracteres",
|
||||
@@ -314,6 +316,7 @@
|
||||
"use_file": "Usar arquivo",
|
||||
"value": "Valor",
|
||||
"variable": "Variável",
|
||||
"view": "Visão",
|
||||
"view_details": "VER DETALHES",
|
||||
"view_in_gateway": "Ver no controlador",
|
||||
"view_json": "Ver JSON",
|
||||
@@ -745,7 +748,9 @@
|
||||
"keys": {
|
||||
"description_error": "A descrição precisa ter menos de 64 caracteres",
|
||||
"expire_error": "A expiração não pode ser superior a um ano no futuro",
|
||||
"name_error": "O nome deve ser único e ter entre 6 e 20 caracteres",
|
||||
"expires": "expira",
|
||||
"max_keys": "Teclas máximas alcançadas (10)",
|
||||
"name_error": "O nome deve ser único e ter entre 6 e 20 caracteres alfanuméricos",
|
||||
"one": "Chave API",
|
||||
"other": "Chaves de Api"
|
||||
},
|
||||
|
||||
@@ -25,15 +25,28 @@ import {
|
||||
useBreakpoint,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTable, usePagination, useSortBy, Row } from 'react-table';
|
||||
import {
|
||||
useTable,
|
||||
usePagination,
|
||||
useSortBy,
|
||||
Row,
|
||||
TableInstance,
|
||||
UsePaginationInstanceProps,
|
||||
UseSortByInstanceProps,
|
||||
UsePaginationState,
|
||||
} from 'react-table';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
// @ts-ignore
|
||||
import SortIcon from './SortIcon';
|
||||
import LoadingOverlay from 'components/LoadingOverlay';
|
||||
import { Column, PageInfo } from 'models/Table';
|
||||
|
||||
interface Props {
|
||||
columns: Column[];
|
||||
const defaultProps = {
|
||||
sortBy: [],
|
||||
};
|
||||
|
||||
type DataTableProps = {
|
||||
columns: readonly Column<object>[];
|
||||
data: object[];
|
||||
count?: number;
|
||||
setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>;
|
||||
@@ -42,25 +55,19 @@ interface Props {
|
||||
sortBy?: { id: string; desc: boolean }[];
|
||||
hiddenColumns?: string[];
|
||||
hideControls?: boolean;
|
||||
minHeight?: string;
|
||||
minHeight?: string | number;
|
||||
fullScreen?: boolean;
|
||||
isManual?: boolean;
|
||||
saveSettingsId?: string;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
count: undefined,
|
||||
setPageInfo: undefined,
|
||||
isLoading: false,
|
||||
minHeight: undefined,
|
||||
fullScreen: false,
|
||||
sortBy: [],
|
||||
hiddenColumns: [],
|
||||
hideControls: false,
|
||||
isManual: false,
|
||||
saveSettingsId: undefined,
|
||||
showAllRows?: boolean;
|
||||
};
|
||||
|
||||
type TableInstanceWithHooks<T extends object> = TableInstance<T> &
|
||||
UsePaginationInstanceProps<T> &
|
||||
UseSortByInstanceProps<T> & {
|
||||
state: UsePaginationState<T>;
|
||||
};
|
||||
|
||||
const DataTable = ({
|
||||
columns,
|
||||
data,
|
||||
@@ -75,14 +82,31 @@ const DataTable = ({
|
||||
setPageInfo,
|
||||
isManual,
|
||||
saveSettingsId,
|
||||
}: Props) => {
|
||||
showAllRows,
|
||||
}: DataTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
const breakpoint = useBreakpoint();
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
const getPageSize = () => {
|
||||
const saved = saveSettingsId ? localStorage.getItem(saveSettingsId) : undefined;
|
||||
if (saved) return Number.parseInt(saved, 10);
|
||||
return 10;
|
||||
try {
|
||||
if (showAllRows) return 1000000;
|
||||
const saved = saveSettingsId ? localStorage.getItem(saveSettingsId) : undefined;
|
||||
if (saved) return Number.parseInt(saved, 10);
|
||||
return 10;
|
||||
} catch {
|
||||
return 10;
|
||||
}
|
||||
};
|
||||
const getPageIndex = () => {
|
||||
try {
|
||||
if (saveSettingsId) {
|
||||
const saved = localStorage.getItem(`${saveSettingsId}.page`);
|
||||
if (saved) return Number.parseInt(saved, 10);
|
||||
}
|
||||
return 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
const [queryPageSize, setQueryPageSize] = useState(getPageSize());
|
||||
|
||||
@@ -104,25 +128,49 @@ const DataTable = ({
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable(
|
||||
{
|
||||
// @ts-ignore
|
||||
columns,
|
||||
data,
|
||||
// @ts-ignore
|
||||
initialState: { sortBy, pagination: !hideControls, pageSize: queryPageSize },
|
||||
manualPagination: isManual,
|
||||
pageCount: isManual && count !== undefined ? Math.ceil(count / queryPageSize) : undefined,
|
||||
pageCount:
|
||||
isManual && count !== undefined
|
||||
? Math.ceil(count / queryPageSize)
|
||||
: Math.ceil(data?.length ?? 0 / queryPageSize),
|
||||
},
|
||||
useSortBy,
|
||||
usePagination,
|
||||
);
|
||||
) as TableInstanceWithHooks<object>;
|
||||
|
||||
const handleGoToPage = (newPage: number) => {
|
||||
if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(newPage));
|
||||
gotoPage(newPage);
|
||||
};
|
||||
const handleNextPage = () => {
|
||||
nextPage();
|
||||
if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(pageIndex + 1));
|
||||
};
|
||||
const handlePreviousPage = () => {
|
||||
previousPage();
|
||||
if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(pageIndex - 1));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (setPageInfo && pageIndex !== undefined) setPageInfo({ index: pageIndex, limit: queryPageSize });
|
||||
}, [queryPageSize, pageIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
// @ts-ignore
|
||||
if (saveSettingsId) localStorage.setItem(saveSettingsId, pageSize);
|
||||
setQueryPageSize(pageSize);
|
||||
}, [pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isManual && count !== undefined) {
|
||||
gotoPage(getPageIndex());
|
||||
}
|
||||
}, [count]);
|
||||
useEffect(() => {
|
||||
if (hiddenColumns) setHiddenColumns(hiddenColumns);
|
||||
}, [hiddenColumns]);
|
||||
@@ -155,6 +203,7 @@ const DataTable = ({
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
// Render the UI for your table
|
||||
return (
|
||||
<>
|
||||
@@ -186,6 +235,7 @@ const DataTable = ({
|
||||
alignContent: 'center',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
// @ts-ignore
|
||||
paddingTop: column.canSort ? '' : '4px',
|
||||
}}
|
||||
>
|
||||
@@ -264,7 +314,7 @@ const DataTable = ({
|
||||
<Tooltip label={t('table.first_page')}>
|
||||
<IconButton
|
||||
aria-label="Go to first page"
|
||||
onClick={() => gotoPage(0)}
|
||||
onClick={() => handleGoToPage(0)}
|
||||
isDisabled={!canPreviousPage}
|
||||
icon={<ArrowLeftIcon h={3} w={3} />}
|
||||
mr={4}
|
||||
@@ -273,7 +323,7 @@ const DataTable = ({
|
||||
<Tooltip label={t('table.previous_page')}>
|
||||
<IconButton
|
||||
aria-label="Previous page"
|
||||
onClick={previousPage}
|
||||
onClick={handlePreviousPage}
|
||||
isDisabled={!canPreviousPage}
|
||||
icon={<ChevronLeftIcon h={6} w={6} />}
|
||||
/>
|
||||
@@ -302,7 +352,7 @@ const DataTable = ({
|
||||
max={pageOptions.length}
|
||||
onChange={(_: unknown, numberValue: number) => {
|
||||
const newPage = numberValue ? numberValue - 1 : 0;
|
||||
gotoPage(newPage);
|
||||
handleGoToPage(newPage);
|
||||
}}
|
||||
defaultValue={pageIndex + 1}
|
||||
>
|
||||
@@ -333,7 +383,7 @@ const DataTable = ({
|
||||
<Tooltip label={t('table.next_page')}>
|
||||
<IconButton
|
||||
aria-label="Go to next page"
|
||||
onClick={nextPage}
|
||||
onClick={handleNextPage}
|
||||
isDisabled={!canNextPage}
|
||||
icon={<ChevronRightIcon h={6} w={6} />}
|
||||
/>
|
||||
@@ -341,7 +391,7 @@ const DataTable = ({
|
||||
<Tooltip label={t('table.last_page')}>
|
||||
<IconButton
|
||||
aria-label="Go to last page"
|
||||
onClick={() => gotoPage(pageCount - 1)}
|
||||
onClick={() => handleGoToPage(pageCount - 1)}
|
||||
isDisabled={!canNextPage}
|
||||
icon={<ArrowRightIcon h={3} w={3} />}
|
||||
ml={4}
|
||||
@@ -356,4 +406,4 @@ const DataTable = ({
|
||||
|
||||
DataTable.defaultProps = defaultProps;
|
||||
|
||||
export default DataTable;
|
||||
export default React.memo(DataTable);
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const propTypes = {
|
||||
value: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
value: '',
|
||||
onClick: () => {},
|
||||
};
|
||||
|
||||
const DatePickerInput = forwardRef(({ value, onClick }, ref) => (
|
||||
<Button colorScheme="gray" onClick={onClick} ref={ref}>
|
||||
{value}
|
||||
</Button>
|
||||
));
|
||||
|
||||
DatePickerInput.propTypes = propTypes;
|
||||
DatePickerInput.defaultProps = defaultProps;
|
||||
export default DatePickerInput;
|
||||
15
src/components/DatePickers/DatePickerInput/index.tsx
Normal file
15
src/components/DatePickers/DatePickerInput/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
|
||||
type Props = {
|
||||
value?: string;
|
||||
onClick?: () => void;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
const DatePickerInput = forwardRef(({ value, onClick, isDisabled }: Props, ref: React.Ref<HTMLButtonElement>) => (
|
||||
<Button colorScheme="gray" onClick={onClick} ref={ref} isDisabled={isDisabled}>
|
||||
{value}
|
||||
</Button>
|
||||
));
|
||||
|
||||
export default DatePickerInput;
|
||||
@@ -1,27 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import DatePickerInput from '../DatePickerInput';
|
||||
|
||||
const propTypes = {
|
||||
date: PropTypes.instanceOf(Date).isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isStart: PropTypes.bool,
|
||||
isEnd: PropTypes.bool,
|
||||
startDate: PropTypes.instanceOf(Date),
|
||||
endDate: PropTypes.instanceOf(Date),
|
||||
type Props = {
|
||||
date: Date;
|
||||
onChange: (v: Date | null) => void;
|
||||
isStart?: boolean;
|
||||
isEnd?: boolean;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
isStart: false,
|
||||
isEnd: false,
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
};
|
||||
|
||||
const DateTimePicker = ({ date, onChange, isStart, isEnd, startDate, endDate }) => {
|
||||
const DateTimePicker = ({ date, onChange, isStart, isEnd, startDate, endDate, isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -36,11 +29,9 @@ const DateTimePicker = ({ date, onChange, isStart, isEnd, startDate, endDate })
|
||||
dateFormat="dd/MM/yyyy hh:mm aa"
|
||||
timeFormat="p"
|
||||
showTimeSelect
|
||||
customInput={<DatePickerInput />}
|
||||
customInput={<DatePickerInput isDisabled={isDisabled} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
DateTimePicker.propTypes = propTypes;
|
||||
DateTimePicker.defaultProps = defaultProps;
|
||||
export default React.memo(DateTimePicker);
|
||||
84
src/hooks/Network/ApiKeys.ts
Normal file
84
src/hooks/Network/ApiKeys.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { axiosSec } from 'utils/axiosInstances';
|
||||
|
||||
export type ApiKey = {
|
||||
id: string;
|
||||
userUuid: string;
|
||||
name: string;
|
||||
description: string;
|
||||
apiKey: string;
|
||||
expiresOn: number;
|
||||
lastUse: number;
|
||||
};
|
||||
|
||||
export type ApiKeyResponse = {
|
||||
apiKeys: ApiKey[];
|
||||
};
|
||||
|
||||
const getKeys = async (uuid?: string) =>
|
||||
axiosSec
|
||||
.get(`apiKey/${uuid}`)
|
||||
.then(({ data }: { data: ApiKeyResponse }) => data)
|
||||
.catch((e: AxiosError) => {
|
||||
if (e.response?.status === 404) {
|
||||
return {
|
||||
apiKeys: [],
|
||||
} as ApiKeyResponse;
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
|
||||
export const useGetUserApiKeys = ({ userId }: { userId?: string }) =>
|
||||
useQuery(['apiKeys', userId], () => getKeys(userId), {
|
||||
enabled: userId !== undefined && userId !== '',
|
||||
staleTime: 1000 * 60 * 30,
|
||||
});
|
||||
|
||||
const deleteKey = async ({ userId, keyId }: { userId: string; keyId: string }) =>
|
||||
axiosSec.delete(`apiKey/${userId}?keyUuid=${keyId}`, {});
|
||||
export const useDeleteApiKey = ({ userId }: { userId?: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(deleteKey, {
|
||||
onSuccess: () => {
|
||||
if (userId !== undefined && userId.length > 0) {
|
||||
queryClient.invalidateQueries(['apiKeys', userId]);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const updateKey = async ({ description, userId, keyId }: { description: string; userId: string; keyId: string }) =>
|
||||
axiosSec.put(`apiKey/${userId}?keyUuid=${keyId}`, { id: keyId, userUuid: userId, description });
|
||||
export const useUpdateApiKey = ({ userId }: { userId?: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(updateKey, {
|
||||
onSuccess: () => {
|
||||
if (userId !== undefined && userId.length > 0) {
|
||||
queryClient.invalidateQueries(['apiKeys', userId]);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createKey = async ({
|
||||
data,
|
||||
userId,
|
||||
}: {
|
||||
userId: string;
|
||||
data: { userUuid: string; name: string; description: string; expiresOn: number };
|
||||
}) => axiosSec.post(`apiKey/${userId}`, data);
|
||||
|
||||
export const useCreateApiKey = ({ userId }: { userId?: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(createKey, {
|
||||
onSuccess: () => {
|
||||
if (userId !== undefined && userId.length > 0) {
|
||||
queryClient.invalidateQueries(['apiKeys', userId]);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
140
src/pages/Profile/ApiKeys/Table/Actions.tsx
Normal file
140
src/pages/Profile/ApiKeys/Table/Actions.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import { CopyIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverFooter,
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
Center,
|
||||
Box,
|
||||
Button,
|
||||
useDisclosure,
|
||||
HStack,
|
||||
Text,
|
||||
useClipboard,
|
||||
} from '@chakra-ui/react';
|
||||
import { Eye, Trash } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ApiKey, useDeleteApiKey } from 'hooks/Network/ApiKeys';
|
||||
|
||||
interface Props {
|
||||
apiKey: ApiKey;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const ApiKeyActions = ({ apiKey, isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const deleteKey = useDeleteApiKey({ userId: apiKey.userUuid });
|
||||
const { hasCopied, onCopy } = useClipboard(apiKey.apiKey);
|
||||
|
||||
const handleDeleteClick = React.useCallback(() => {
|
||||
deleteKey.mutate(
|
||||
{ userId: apiKey.userUuid, keyId: apiKey.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
},
|
||||
},
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<HStack mx="auto">
|
||||
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
||||
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
||||
<Box>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
aria-label="delete-device"
|
||||
colorScheme="red"
|
||||
icon={<Trash size={20} />}
|
||||
size="sm"
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<PopoverContent w="340px">
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverHeader>
|
||||
{t('crud.delete')} {apiKey.name}
|
||||
</PopoverHeader>
|
||||
<PopoverBody>
|
||||
<Text whiteSpace="break-spaces">{t('crud.delete_confirm', { obj: t('keys.one') })}</Text>
|
||||
</PopoverBody>
|
||||
<PopoverFooter>
|
||||
<Center>
|
||||
<Button colorScheme="gray" mr="1" onClick={onClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={deleteKey.isLoading}>
|
||||
{t('common.yes')}
|
||||
</Button>
|
||||
</Center>
|
||||
</PopoverFooter>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Tooltip
|
||||
label={hasCopied ? `${t('common.copied')}!` : `${t('common.copy')} ${t('keys.one')}`}
|
||||
hasArrow
|
||||
closeOnClick={false}
|
||||
>
|
||||
<IconButton
|
||||
aria-label={t('common.copy')}
|
||||
icon={<CopyIcon h={5} w={5} />}
|
||||
onClick={onCopy}
|
||||
size="sm"
|
||||
colorScheme="teal"
|
||||
mr={2}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popover>
|
||||
<Tooltip label={`${t('common.view')} ${t('keys.one')}`} hasArrow closeOnClick={false}>
|
||||
<Box>
|
||||
<PopoverTrigger>
|
||||
<IconButton aria-label={t('common.view')} icon={<Eye size={20} />} size="sm" colorScheme="purple" />
|
||||
</PopoverTrigger>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<PopoverContent w="560px">
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverHeader>
|
||||
{t('common.view')} {apiKey.name} {t('keys.one')}
|
||||
<Tooltip
|
||||
label={hasCopied ? `${t('common.copied')}!` : `${t('common.copy')} ${t('keys.one')}`}
|
||||
hasArrow
|
||||
closeOnClick={false}
|
||||
>
|
||||
<IconButton
|
||||
aria-label={t('common.copy')}
|
||||
icon={<CopyIcon h={4} w={4} />}
|
||||
onClick={onCopy}
|
||||
size="xs"
|
||||
colorScheme="teal"
|
||||
ml={2}
|
||||
/>
|
||||
</Tooltip>
|
||||
</PopoverHeader>
|
||||
<PopoverBody>
|
||||
<Text whiteSpace="break-spaces">
|
||||
<Center>
|
||||
<pre style={{ fontFamily: 'monospace' }}>{apiKey.apiKey}</pre>
|
||||
</Center>
|
||||
</Text>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyActions;
|
||||
131
src/pages/Profile/ApiKeys/Table/AddButton.tsx
Normal file
131
src/pages/Profile/ApiKeys/Table/AddButton.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormLabel,
|
||||
Input,
|
||||
Textarea,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import axios from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateButton from '../../../../components/Buttons/CreateButton';
|
||||
import { Modal } from '../../../../components/Modals/Modal';
|
||||
import ApiKeyExpiresOnField from './ExpiresOnField';
|
||||
import { ApiKey, useCreateApiKey } from 'hooks/Network/ApiKeys';
|
||||
|
||||
type Props = {
|
||||
apiKeys: ApiKey[];
|
||||
userId: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
const thirtyDaysFromNow = () => Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30;
|
||||
|
||||
const CreateApiKeyButton = ({ apiKeys, userId, isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [name, setName] = React.useState('');
|
||||
const [description, setDescription] = React.useState('');
|
||||
const [expiresOn, setExpiresOn] = React.useState(thirtyDaysFromNow());
|
||||
const createKey = useCreateApiKey({ userId });
|
||||
|
||||
const onNameChange = (e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value);
|
||||
const onDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => setDescription(e.target.value);
|
||||
|
||||
const onCreate = React.useCallback(() => {
|
||||
createKey.mutate(
|
||||
{ data: { name, description, userUuid: userId, expiresOn }, userId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e)) {
|
||||
toast({
|
||||
id: 'create-api-key-error',
|
||||
title: t('common.error'),
|
||||
description: e?.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
}, [name, description, expiresOn]);
|
||||
|
||||
const nameError = React.useMemo(() => {
|
||||
if (
|
||||
name.length === 0 ||
|
||||
name.length > 20 ||
|
||||
apiKeys.map(({ name: n }) => n).includes(name) ||
|
||||
!/^[a-z0-9]+$/i.test(name)
|
||||
) {
|
||||
return t('keys.name_error');
|
||||
}
|
||||
return undefined;
|
||||
}, [name, apiKeys]);
|
||||
|
||||
const handleOpenClick = () => {
|
||||
setName('');
|
||||
setDescription('');
|
||||
setExpiresOn(thirtyDaysFromNow());
|
||||
onOpen();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{apiKeys.length >= 10 && (
|
||||
<Alert status="error">
|
||||
<AlertIcon />
|
||||
<AlertDescription>{t('keys.max_keys')}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<CreateButton onClick={handleOpenClick} isDisabled={isDisabled || apiKeys.length >= 10} isCompact />
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={` ${t('crud.create')} ${t('keys.one')}`}
|
||||
topRightButtons={
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
ml="1"
|
||||
onClick={onCreate}
|
||||
isDisabled={nameError !== undefined || description.length > 64}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
}
|
||||
options={{
|
||||
modalSize: 'sm',
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<FormControl mb={2} isInvalid={nameError !== undefined}>
|
||||
<FormLabel>{t('common.name')}</FormLabel>
|
||||
<Input value={name} onChange={onNameChange} />
|
||||
<FormErrorMessage>{nameError}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl mb={2} isInvalid={description.length > 64}>
|
||||
<FormLabel>{t('common.description')}</FormLabel>
|
||||
<Textarea value={description} onChange={onDescriptionChange} noOfLines={2} />
|
||||
<FormErrorMessage>{t('keys.description_error')}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<ApiKeyExpiresOnField value={expiresOn} setValue={setExpiresOn} />
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateApiKeyButton;
|
||||
123
src/pages/Profile/ApiKeys/Table/DescriptionCell.tsx
Normal file
123
src/pages/Profile/ApiKeys/Table/DescriptionCell.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverFooter,
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
Text,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import axios from 'axios';
|
||||
import { Pen } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ApiKey, useUpdateApiKey } from 'hooks/Network/ApiKeys';
|
||||
|
||||
type Props = {
|
||||
apiKey: ApiKey;
|
||||
};
|
||||
|
||||
const ApiKeyDescriptionCell = ({ apiKey }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const [newDescription, setNewDescription] = React.useState(apiKey.description);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const updateApiKey = useUpdateApiKey({ userId: apiKey.userUuid });
|
||||
|
||||
const onDescriptionChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setNewDescription(e.target.value);
|
||||
}, []);
|
||||
|
||||
const handleSaveClick = () => {
|
||||
updateApiKey.mutate(
|
||||
{
|
||||
userId: apiKey.userUuid,
|
||||
keyId: apiKey.id,
|
||||
description: newDescription,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e)) {
|
||||
toast({
|
||||
id: 'error-save-api-key-description',
|
||||
title: t('common.error'),
|
||||
description: e?.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
setNewDescription(apiKey.description);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Text w="100%" overflowWrap="break-word" whiteSpace="pre-wrap">
|
||||
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
||||
<Tooltip label={t('common.edit')} hasArrow closeOnClick={false} isDisabled={isOpen}>
|
||||
<Box display="unset">
|
||||
<PopoverTrigger>
|
||||
<IconButton aria-label={t('common.edit')} icon={<Pen size={20} />} size="xs" colorScheme="teal" mr={2} />
|
||||
</PopoverTrigger>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<PopoverContent w="340px">
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverHeader>
|
||||
{t('common.edit')} {apiKey.name} {t('common.description')}
|
||||
</PopoverHeader>
|
||||
<PopoverBody>
|
||||
<FormControl mb={2} isInvalid={newDescription.length > 64}>
|
||||
<FormLabel>{t('common.description')}</FormLabel>
|
||||
<Textarea value={newDescription} onChange={onDescriptionChange} noOfLines={2} />
|
||||
<FormErrorMessage>{t('keys.description_error')}</FormErrorMessage>
|
||||
</FormControl>
|
||||
</PopoverBody>
|
||||
<PopoverFooter>
|
||||
<Center>
|
||||
<Button colorScheme="gray" mr="1" onClick={onCancel}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
ml="1"
|
||||
onClick={handleSaveClick}
|
||||
isDisabled={newDescription.length > 64}
|
||||
isLoading={updateApiKey.isLoading}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Center>
|
||||
</PopoverFooter>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{apiKey.description}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyDescriptionCell;
|
||||
81
src/pages/Profile/ApiKeys/Table/ExpiresOnField.tsx
Normal file
81
src/pages/Profile/ApiKeys/Table/ExpiresOnField.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, FormControl, FormErrorMessage, FormLabel, Radio, RadioGroup, Stack, Text } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DateTimePicker from '../../../../components/DatePickers/DateTimePicker';
|
||||
|
||||
type Props = {
|
||||
value: number;
|
||||
setValue: (value: number) => void;
|
||||
};
|
||||
|
||||
const aYearFromNow = () => Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365;
|
||||
|
||||
const ApiKeyExpiresOnField = ({ value, setValue }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [radio, setRadioValue] = React.useState('30');
|
||||
|
||||
const onRadioChange = (v: string) => {
|
||||
setRadioValue(v);
|
||||
const days = parseInt(v, 10);
|
||||
const now = new Date();
|
||||
if (days > 0) {
|
||||
now.setDate(now.getDate() + days);
|
||||
setValue(Math.floor(now.getTime() / 1000));
|
||||
} else {
|
||||
now.setDate(now.getDate() + 30);
|
||||
setValue(Math.floor(now.getTime() / 1000));
|
||||
}
|
||||
};
|
||||
|
||||
const onDateChange = (v: Date | null) => {
|
||||
if (v) setValue(Math.floor(v.getTime() / 1000));
|
||||
};
|
||||
|
||||
const tempDate = () => {
|
||||
if (!value || value === 0) return new Date();
|
||||
|
||||
return new Date(value * 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormControl isInvalid={value > aYearFromNow()} isRequired>
|
||||
<FormLabel ms={0} mb={2} fontSize="md" fontWeight="normal" _disabled={{ opacity: 0.8 }}>
|
||||
{t('certificates.expires_on')}
|
||||
</FormLabel>
|
||||
<Box>
|
||||
<RadioGroup onChange={onRadioChange} defaultValue="30">
|
||||
<Stack spacing={5} direction="column">
|
||||
<Radio colorScheme="blue" value="30">
|
||||
30 {t('common.days')}
|
||||
</Radio>
|
||||
<Radio colorScheme="blue" value="60">
|
||||
60 {t('common.days')}
|
||||
</Radio>
|
||||
<Radio colorScheme="blue" value="90">
|
||||
90 {t('common.days')}
|
||||
</Radio>
|
||||
<Radio colorScheme="blue" value="180">
|
||||
6 {t('common.months')}
|
||||
</Radio>
|
||||
<Radio colorScheme="green" value="0">
|
||||
<Flex>
|
||||
<Text my="auto" mr={2} w="180px">
|
||||
{t('common.custom')}
|
||||
</Text>
|
||||
<DateTimePicker
|
||||
date={tempDate()}
|
||||
isStart
|
||||
onChange={onDateChange}
|
||||
isDisabled={!value || value === 0 || radio !== '0'}
|
||||
/>
|
||||
</Flex>
|
||||
</Radio>
|
||||
</Stack>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
<FormErrorMessage>{t('keys.expire_error')}</FormErrorMessage>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyExpiresOnField;
|
||||
54
src/pages/Profile/ApiKeys/Table/index.tsx
Normal file
54
src/pages/Profile/ApiKeys/Table/index.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Heading, HStack, Spacer } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateApiKeyButton from './AddButton';
|
||||
import useApiKeyTable from './useApiKeyTable';
|
||||
import RefreshButton from 'components/Buttons/RefreshButton';
|
||||
import ColumnPicker from 'components/ColumnPicker';
|
||||
import DataTable from 'components/DataTable';
|
||||
import { Column } from 'models/Table';
|
||||
|
||||
type Props = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
const ApiKeyTable = ({ userId }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { query, columns, hiddenColumns } = useApiKeyTable({ userId });
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Flex mb={2}>
|
||||
<Heading size="md" my="auto">
|
||||
{t('keys.other')} ({query.data?.apiKeys.length})
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
<CreateApiKeyButton userId={userId} apiKeys={query.data?.apiKeys ?? []} />
|
||||
<ColumnPicker
|
||||
columns={columns as Column<unknown>[]}
|
||||
hiddenColumns={hiddenColumns[0]}
|
||||
setHiddenColumns={hiddenColumns[1]}
|
||||
preference="apiKeys.profile.table.hiddenColumns"
|
||||
/>
|
||||
<RefreshButton onClick={query.refetch} isFetching={query.isFetching} isCompact />
|
||||
</HStack>
|
||||
</Flex>
|
||||
<Box>
|
||||
<DataTable
|
||||
columns={columns as Column<object>[]}
|
||||
saveSettingsId="apiKeys.profile.table"
|
||||
data={query.data?.apiKeys ?? []}
|
||||
obj={t('keys.other')}
|
||||
sortBy={[{ id: 'expiresOn', desc: false }]}
|
||||
minHeight="400px"
|
||||
hiddenColumns={hiddenColumns[0]}
|
||||
showAllRows
|
||||
hideControls
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyTable;
|
||||
78
src/pages/Profile/ApiKeys/Table/useApiKeyTable.tsx
Normal file
78
src/pages/Profile/ApiKeys/Table/useApiKeyTable.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ApiKeyActions from './Actions';
|
||||
import ApiKeyDescriptionCell from './DescriptionCell';
|
||||
import FormattedDate from 'components/FormattedDate';
|
||||
import { ApiKey, useGetUserApiKeys } from 'hooks/Network/ApiKeys';
|
||||
import { Column } from 'models/Table';
|
||||
|
||||
type Props = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
const useApiKeyTable = ({ userId }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const hiddenColumns = React.useState<string[]>([]);
|
||||
const query = useGetUserApiKeys({ userId });
|
||||
|
||||
const dateCell = React.useCallback((date: number) => <FormattedDate date={date} />, []);
|
||||
const actionCell = React.useCallback((apiKey: ApiKey) => <ApiKeyActions apiKey={apiKey} />, []);
|
||||
const descriptionCell = React.useCallback((apiKey: ApiKey) => <ApiKeyDescriptionCell apiKey={apiKey} />, []);
|
||||
|
||||
const columns = React.useMemo(
|
||||
(): Column<ApiKey>[] => [
|
||||
{
|
||||
id: 'name',
|
||||
Header: t('common.name'),
|
||||
Footer: '',
|
||||
accessor: 'name',
|
||||
alwaysShow: true,
|
||||
customWidth: '120px',
|
||||
},
|
||||
{
|
||||
id: 'expiresOn',
|
||||
Header: t('keys.expires'),
|
||||
Footer: '',
|
||||
Cell: ({ cell }) => dateCell(cell.row.original.expiresOn),
|
||||
accessor: 'expiresOn',
|
||||
customWidth: '120px',
|
||||
},
|
||||
{
|
||||
id: 'lastUse',
|
||||
Header: t('common.last_use'),
|
||||
Footer: '',
|
||||
Cell: ({ cell }) => dateCell(cell.row.original.lastUse),
|
||||
accessor: 'lastUse',
|
||||
customWidth: '120px',
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
Header: t('common.description'),
|
||||
Footer: '',
|
||||
Cell: ({ cell }) => descriptionCell(cell.row.original),
|
||||
accessor: 'description',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
Header: t('common.actions'),
|
||||
Footer: '',
|
||||
Cell: (v) => actionCell(v.cell.row.original),
|
||||
disableSortBy: true,
|
||||
customWidth: '120px',
|
||||
alwaysShow: true,
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
return React.useMemo(
|
||||
() => ({
|
||||
query,
|
||||
columns,
|
||||
hiddenColumns,
|
||||
}),
|
||||
[query, columns, hiddenColumns],
|
||||
);
|
||||
};
|
||||
|
||||
export default useApiKeyTable;
|
||||
19
src/pages/Profile/ApiKeys/index.tsx
Normal file
19
src/pages/Profile/ApiKeys/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import ApiKeyTable from './Table';
|
||||
import Card from 'components/Card';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
const ApiKeysCard = () => {
|
||||
const { user } = useAuth();
|
||||
|
||||
return (
|
||||
<Card p={4}>
|
||||
<CardBody>
|
||||
<ApiKeyTable userId={user?.id ?? ''} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeysCard;
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import Masonry from 'react-masonry-css';
|
||||
import ApiKeysCard from './ApiKeys';
|
||||
import GeneralInformationProfile from './GeneralInformation';
|
||||
import MultiFactorAuthProfile from './MultiFactorAuth';
|
||||
import ProfileNotes from './Notes';
|
||||
@@ -10,7 +11,7 @@ const ProfileLayout = () => (
|
||||
breakpointCols={{
|
||||
default: 3,
|
||||
1800: 2,
|
||||
1100: 1,
|
||||
1200: 1,
|
||||
}}
|
||||
className="my-masonry-grid"
|
||||
columnClassName="my-masonry-grid_column"
|
||||
@@ -19,6 +20,7 @@ const ProfileLayout = () => (
|
||||
<MultiFactorAuthProfile />
|
||||
<GeneralInformationProfile />
|
||||
<ProfileNotes />
|
||||
<ApiKeysCard />
|
||||
</Masonry>
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user