diff --git a/package-lock.json b/package-lock.json index cd9ce1d..1b8a791 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 5a3e709..c869fa3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index ba1ad90..34a32e5 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -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" }, diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 5abdc9b..1b71a80 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -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" }, diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index 999ae9c..7740c57 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -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" }, diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 17724d0..d07aba4 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -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" }, diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json index 64881ff..16c8740 100644 --- a/public/locales/pt/translation.json +++ b/public/locales/pt/translation.json @@ -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" }, diff --git a/src/components/DataTable/index.tsx b/src/components/DataTable/index.tsx index e2bf271..13682a5 100644 --- a/src/components/DataTable/index.tsx +++ b/src/components/DataTable/index.tsx @@ -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[]; data: object[]; count?: number; setPageInfo?: React.Dispatch>; @@ -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 = TableInstance & + UsePaginationInstanceProps & + UseSortByInstanceProps & { + state: UsePaginationState; + }; + 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; + + 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 = ({ ); } + // 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 = ({ gotoPage(0)} + onClick={() => handleGoToPage(0)} isDisabled={!canPreviousPage} icon={} mr={4} @@ -273,7 +323,7 @@ const DataTable = ({ } /> @@ -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 = ({ } /> @@ -341,7 +391,7 @@ const DataTable = ({ gotoPage(pageCount - 1)} + onClick={() => handleGoToPage(pageCount - 1)} isDisabled={!canNextPage} icon={} ml={4} @@ -356,4 +406,4 @@ const DataTable = ({ DataTable.defaultProps = defaultProps; -export default DataTable; +export default React.memo(DataTable); diff --git a/src/components/DatePickers/DatePickerInput/index.jsx b/src/components/DatePickers/DatePickerInput/index.jsx deleted file mode 100644 index 0ea44a2..0000000 --- a/src/components/DatePickers/DatePickerInput/index.jsx +++ /dev/null @@ -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) => ( - -)); - -DatePickerInput.propTypes = propTypes; -DatePickerInput.defaultProps = defaultProps; -export default DatePickerInput; diff --git a/src/components/DatePickers/DatePickerInput/index.tsx b/src/components/DatePickers/DatePickerInput/index.tsx new file mode 100644 index 0000000..c5b794a --- /dev/null +++ b/src/components/DatePickers/DatePickerInput/index.tsx @@ -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) => ( + +)); + +export default DatePickerInput; diff --git a/src/components/DatePickers/DateTimePicker/index.jsx b/src/components/DatePickers/DateTimePicker/index.tsx similarity index 57% rename from src/components/DatePickers/DateTimePicker/index.jsx rename to src/components/DatePickers/DateTimePicker/index.tsx index aafad57..a1fbdb4 100644 --- a/src/components/DatePickers/DateTimePicker/index.jsx +++ b/src/components/DatePickers/DateTimePicker/index.tsx @@ -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={} + customInput={} /> ); }; -DateTimePicker.propTypes = propTypes; -DateTimePicker.defaultProps = defaultProps; export default React.memo(DateTimePicker); diff --git a/src/hooks/Network/ApiKeys.ts b/src/hooks/Network/ApiKeys.ts new file mode 100644 index 0000000..6407d62 --- /dev/null +++ b/src/hooks/Network/ApiKeys.ts @@ -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]); + } + }, + }); +}; diff --git a/src/pages/Profile/ApiKeys/Table/Actions.tsx b/src/pages/Profile/ApiKeys/Table/Actions.tsx new file mode 100644 index 0000000..70896b5 --- /dev/null +++ b/src/pages/Profile/ApiKeys/Table/Actions.tsx @@ -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 ( + + + + + + } + size="sm" + isDisabled={isDisabled} + /> + + + + + + + + {t('crud.delete')} {apiKey.name} + + + {t('crud.delete_confirm', { obj: t('keys.one') })} + + +
+ + +
+
+
+
+ + } + onClick={onCopy} + size="sm" + colorScheme="teal" + mr={2} + /> + + + + + + } size="sm" colorScheme="purple" /> + + + + + + + + {t('common.view')} {apiKey.name} {t('keys.one')} + + } + onClick={onCopy} + size="xs" + colorScheme="teal" + ml={2} + /> + + + + +
+
{apiKey.apiKey}
+
+
+
+
+
+
+ ); +}; + +export default ApiKeyActions; diff --git a/src/pages/Profile/ApiKeys/Table/AddButton.tsx b/src/pages/Profile/ApiKeys/Table/AddButton.tsx new file mode 100644 index 0000000..6efcb68 --- /dev/null +++ b/src/pages/Profile/ApiKeys/Table/AddButton.tsx @@ -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) => setName(e.target.value); + const onDescriptionChange = (e: React.ChangeEvent) => 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 && ( + + + {t('keys.max_keys')} + + )} + = 10} isCompact /> + 64} + > + {t('common.save')} + + } + options={{ + modalSize: 'sm', + }} + > + + + {t('common.name')} + + {nameError} + + 64}> + {t('common.description')} +