diff --git a/package-lock.json b/package-lock.json index 22a0783..e54ea89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ucentral-client", - "version": "2.8.0(10)", + "version": "2.8.0(11)", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ucentral-client", - "version": "2.8.0(10)", + "version": "2.8.0(11)", "license": "ISC", "dependencies": { "@chakra-ui/icons": "^2.0.11", @@ -57,6 +57,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", @@ -3551,6 +3552,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.6", "dev": true, @@ -11815,6 +11828,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.6", "dev": true, diff --git a/package.json b/package.json index 055ae3f..b60afae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ucentral-client", - "version": "2.8.0(10)", + "version": "2.8.0(11)", "description": "", "private": true, "main": "index.tsx", @@ -65,6 +65,7 @@ "@types/react-csv": "^1.1.3", "@types/react-dom": "^18.0.6", "@types/react-table": "^7.7.12", + "@types/react-datepicker": "4.8.0", "@types/uuid": "^8.3.4", "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index 3d5923f..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", @@ -742,6 +745,15 @@ "successful_macs": "Erfolgreiche MACs", "title": "Arbeitsplätze" }, + "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", + "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" + }, "locations": { "address_line_one": "Adresszeile eins", "address_line_two": "Adresszeile zwei", @@ -802,7 +814,10 @@ "receiving_types": "Typen empfangen", "security": "Sicherheit", "source": "Quelle", - "thread": "Faden" + "thread": "Faden", + "venue_config": "Aufbau", + "venue_reboot": "Starten Sie neu", + "venue_upgrade": "Aktualisierung" }, "map": { "auto_align": "Automatisch ausrichten", @@ -838,6 +853,22 @@ "my_organization": "Meine Organisation", "title": "Organisation" }, + "overrides": { + "delete_source": "Alle Überschreibungen von {{source}}löschen", + "ignore_overrides": "Konfigurationsüberschreibungen ignorieren", + "name_error": "Der Parameter ist bereits von Ihrer Quelle definiert", + "one": "Konfigurationsüberschreibung", + "other": "Konfigurationsüberschreibungen", + "param_name": "Parameter", + "param_value": "Wert", + "parameter": "Parameter", + "reason": "Grund", + "reason_error": "Ihr Grund muss weniger als 64 Zeichen lang sein. lang", + "source": "Quelle", + "tx_power_error": "Die Sendeleistung muss zwischen 1 und 32 liegen", + "update_success": "Aktualisierte Konfigurationsüberschreibungen!", + "value": "Wert" + }, "profile": { "about_me": "Über mich", "activate": "", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index a407c3c..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", @@ -742,6 +745,15 @@ "successful_macs": "Successful MACs", "title": "Jobs" }, + "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", + "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" + }, "locations": { "address_line_one": "Address Line One", "address_line_two": "Address Line Two", @@ -802,7 +814,10 @@ "receiving_types": "Receiving Types", "security": "Security", "source": "Source", - "thread": "Thread" + "thread": "Thread", + "venue_config": "Configuration", + "venue_reboot": "Reboot", + "venue_upgrade": "Upgrade" }, "map": { "auto_align": "Auto Align", @@ -838,6 +853,22 @@ "my_organization": "My Organization", "title": "Organization" }, + "overrides": { + "delete_source": "Delete all overrides from {{source}}", + "ignore_overrides": "Ignore Configuration Overrides", + "name_error": "Parameter is already defined by your source", + "one": "Configuration Override", + "other": "Configuration Overrides", + "param_name": "Parameter", + "param_value": "Value", + "parameter": "Parameter", + "reason": "Reason", + "reason_error": "Your reason needs to be less than 64 chars. long", + "source": "Source", + "tx_power_error": "Tx power needs to be between 1 and 32", + "update_success": "Updated Configuration Overrides!", + "value": "Value" + }, "profile": { "about_me": "About Me", "activate": "Activate", diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index 6d46f3d..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", @@ -742,6 +745,15 @@ "successful_macs": "MAC exitosos", "title": "Trabajos" }, + "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", + "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" + }, "locations": { "address_line_one": "Dirección Línea Uno", "address_line_two": "Dirección línea dos", @@ -802,7 +814,10 @@ "receiving_types": "Tipos de recepción", "security": "SEGURIDAD", "source": "Fuente", - "thread": "Hilo" + "thread": "Hilo", + "venue_config": "Configuración", + "venue_reboot": "Reiniciar", + "venue_upgrade": "Mejorar" }, "map": { "auto_align": "Alineación automática", @@ -838,6 +853,22 @@ "my_organization": "MI ORGANIZACION", "title": "Organización" }, + "overrides": { + "delete_source": "Eliminar todas las anulaciones de {{source}}", + "ignore_overrides": "Ignorar anulaciones de configuración", + "name_error": "El parámetro ya está definido por su fuente", + "one": "Anulación de configuración", + "other": "Anulaciones de configuración", + "param_name": "parámetro", + "param_value": "Valor", + "parameter": "parámetro", + "reason": "Razón", + "reason_error": "Su motivo debe tener menos de 64 caracteres. largo", + "source": "Fuente", + "tx_power_error": "La potencia Tx debe estar entre 1 y 32", + "update_success": "¡Anulaciones de configuración actualizadas!", + "value": "Valor" + }, "profile": { "about_me": "Sobre mí", "activate": "", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 4d4fcb3..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", @@ -742,6 +745,15 @@ "successful_macs": "MAC réussis", "title": "Emplois" }, + "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", + "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" + }, "locations": { "address_line_one": "Adresse Ligne 1", "address_line_two": "Adresse ligne deux", @@ -802,7 +814,10 @@ "receiving_types": "Types de réception", "security": "SÉCURITÉ", "source": "La source", - "thread": "Fil de discussion" + "thread": "Fil de discussion", + "venue_config": "Configuration", + "venue_reboot": "Redémarrer", + "venue_upgrade": "Améliorer" }, "map": { "auto_align": "Alignement automatique", @@ -838,6 +853,22 @@ "my_organization": "Mon organisation", "title": "Organisation" }, + "overrides": { + "delete_source": "Supprimer tous les remplacements de {{source}}", + "ignore_overrides": "Ignorer les remplacements de configuration", + "name_error": "Le paramètre est déjà défini par votre source", + "one": "Remplacement de la configuration", + "other": "Remplacements de configuration", + "param_name": "paramètre", + "param_value": "Valeur", + "parameter": "paramètre", + "reason": "raison", + "reason_error": "Votre raison doit être inférieure à 64 caractères. long", + "source": "La source", + "tx_power_error": "La puissance de transmission doit être comprise entre 1 et 32", + "update_success": "Remplacements de configuration mis à jour !", + "value": "Valeur" + }, "profile": { "about_me": "À propos de moi", "activate": "", diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json index 39f30a3..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", @@ -742,6 +745,15 @@ "successful_macs": "MACs de sucesso", "title": "Empregos" }, + "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", + "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" + }, "locations": { "address_line_one": "Linha de endereço um", "address_line_two": "Linha de endereço dois", @@ -802,7 +814,10 @@ "receiving_types": "Tipos de recebimento", "security": "SEGURANÇA", "source": "Fonte", - "thread": "FIO" + "thread": "FIO", + "venue_config": "Configuração", + "venue_reboot": "Reiniciar", + "venue_upgrade": "Melhorar" }, "map": { "auto_align": "Alinhamento Automático", @@ -838,6 +853,22 @@ "my_organization": "Minha organização", "title": "Organização" }, + "overrides": { + "delete_source": "Excluir todas as substituições de {{source}}", + "ignore_overrides": "Ignorar substituições de configuração", + "name_error": "O parâmetro já está definido pela sua fonte", + "one": "Substituição de configuração", + "other": "Substituições de configuração", + "param_name": "parâmetro", + "param_value": "Valor", + "parameter": "parâmetro", + "reason": "RAZÃO", + "reason_error": "Seu motivo precisa ter menos de 64 caracteres. grandes", + "source": "Fonte", + "tx_power_error": "A potência Tx precisa estar entre 1 e 32", + "update_success": "Substituições de configuração atualizadas!", + "value": "Valor" + }, "profile": { "about_me": "Sobre mim", "activate": "", 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.tsx b/src/components/DatePickers/DateTimePicker/index.tsx new file mode 100644 index 0000000..a1fbdb4 --- /dev/null +++ b/src/components/DatePickers/DateTimePicker/index.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import DatePicker from 'react-datepicker'; +import { useTranslation } from 'react-i18next'; +import 'react-datepicker/dist/react-datepicker.css'; +import DatePickerInput from '../DatePickerInput'; + +type Props = { + date: Date; + onChange: (v: Date | null) => void; + isStart?: boolean; + isEnd?: boolean; + startDate?: Date; + endDate?: Date; + isDisabled?: boolean; +}; + +const DateTimePicker = ({ date, onChange, isStart, isEnd, startDate, endDate, isDisabled }: Props) => { + const { t } = useTranslation(); + + return ( + } + /> + ); +}; + +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..57f73bb --- /dev/null +++ b/src/hooks/Network/ApiKeys.ts @@ -0,0 +1,84 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { axiosSec } from 'constants/axiosInstances'; +import { AxiosError } from 'models/Axios'; + +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..8da6074 --- /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 ApiKeyExpiresOnField from './ExpiresOnField'; +import { CreateButton } from 'components/Buttons/CreateButton'; +import { Modal } from 'components/Modals/Modal'; +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')} +