diff --git a/package-lock.json b/package-lock.json index f765c17..3119607 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ucentral-client", - "version": "2.11.0(16)", + "version": "3.0.0(1)", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ucentral-client", - "version": "2.11.0(16)", + "version": "3.0.0(1)", "license": "ISC", "dependencies": { "@chakra-ui/anatomy": "^2.1.1", diff --git a/package.json b/package.json index 45f25e5..e629192 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ucentral-client", - "version": "2.11.0(16)", + "version": "3.0.0(1)", "description": "", "private": true, "main": "index.tsx", diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index 0453bd6..85680fd 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -269,6 +269,7 @@ "map": "Karte", "max": "Max", "min": "MINDEST", + "miscellaneous": "Verschiedenes", "mode": "Modus", "model": "Modell", "modified": "Geändert", @@ -737,6 +738,7 @@ "form": { "captive_web_root_explanation": "Bitte verwenden Sie nur .tar-Dateien (keine komprimierten Dateien wie z. B. .targz)", "certificate_file_explanation": "Bitte verwenden Sie eine .pem-Datei, die mit „-----BEGIN CERTIFICATE-----“ beginnt und mit „-----END CERTIFICATE-----“ endet.", + "invalid_alphanumeric_with_dash": "Akzeptierte Zeichen. sind nur alphanumerisch (Buchstaben & Zahlen)", "invalid_cidr": "Ungültige CIDR-IPv4-Adresse. Beispiel: 192.168.0.1/12", "invalid_email": "Ungültige E-Mail", "invalid_file_content": "Ungültiger Dateiinhalt, bitte bestätigen Sie, dass es sich um ein gültiges Format handelt", @@ -763,7 +765,11 @@ "invalid_static_ipv4_e": "Ungültige Adresse, dieser Bereich ist für Experimente reserviert (Klasse E). Das erste Oktett sollte 223 oder niedriger sein", "invalid_third_party": "Ungültige Drittanbieter-JSON-Zeichenfolge. Bitte bestätigen Sie, dass Ihr Wert ein gültiges JSON ist", "key_file_explanation": "Bitte verwenden Sie eine .pem-Datei, die mit „-----BEGIN PRIVATE KEY-----“ beginnt und mit „-----END PRIVATE KEY-----“ endet.", + "max_length": "Maximale Länge von {{max}} Zeichen.", + "max_value": "Maximalwert von {{max}}", + "min_length": "Mindestlänge von {{min}} Zeichen.", "min_max_string": "Der Wert muss eine Länge zwischen {{min}} (einschließlich) und {{max}} (einschließlich) haben.", + "min_value": "Mindestwert von {{min}}", "missing_interface_upstream": "Sie müssen mindestens eine Upstream-Schnittstelle haben. Im Moment sind alle Ihre Schnittstellen nachgelagert", "new_email_to_notify": "Neue E-Mail zur Benachrichtigung", "new_phone_to_notify": "Neues Telefon zu benachrichtigen", @@ -905,6 +911,11 @@ "one": "Benachrichtigung", "other": "Benachrichtigungen" }, + "openroaming": { + "pool_strategy": "Pool-Strategie", + "radius_endpoint_one": "Radiusendpunkt", + "radius_endpoint_other": "Radiusendpunkte" + }, "operator": { "delete_explanation": "Möchten Sie diesen Operator wirklich löschen? Dieser Vorgang ist nicht umkehrbar", "delete_operator": "Betreiber löschen", @@ -970,6 +981,27 @@ "title": "Beschränkungen", "tty": "TTY-Zugriff" }, + "roaming": { + "account_created": "Neues Konto erstellt!", + "account_deleted": "Konto gelöscht!", + "account_one": "Konto", + "account_other": "Konten", + "certificate_deleted": "Zertifikat gelöscht!", + "certificate_one": "Zertifikat", + "certificate_other": "Zertifikate", + "city": "Stadt", + "common_name": "Gemeinsamen Namen", + "country": "Land", + "global_reach": "Globale Reichweite", + "global_reach_account_id": "Konto-ID", + "invalid_certificate": "Ungültiges Zertifikat", + "invalid_key": "Ungültiger privater Schlüssel", + "location_details_title": "Ort", + "organization": "Organisation", + "private_key": "Privat Schlüssel", + "province": "Provinz", + "state": "Zustand" + }, "rrm": { "algorithm": "Algorithmus", "algorithm_other": "Algorithmen", @@ -1091,6 +1123,7 @@ "title": "Abonnenten" }, "system": { + "advanced": "Erweitert", "backend_logs": "Back-End-Protokolle", "configuration": "Aufbau", "could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 358f4fa..976d162 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -269,6 +269,7 @@ "map": "Map", "max": "Max", "min": "Min", + "miscellaneous": "Miscellaneous", "mode": "Mode", "model": "Model", "modified": "Modified", @@ -737,6 +738,7 @@ "form": { "captive_web_root_explanation": "Please use .tar files only (no compressed files like .targz, for example)", "certificate_file_explanation": "Please use a .pem file that starts with \"-----BEGIN CERTIFICATE-----\" and ends with \"-----END CERTIFICATE-----\"", + "invalid_alphanumeric_with_dash": "Accepted chars. are only alphanumeric (letters & numbers)", "invalid_cidr": "Invalid CIDR IPv4 address. Example: 192.168.0.1/12", "invalid_email": "Invalid Email", "invalid_file_content": "Invalid file content, please confirm that it is of the valid format", @@ -763,7 +765,11 @@ "invalid_static_ipv4_e": "Invalid address, this range reserved for experiments (class E). The first octet should be 223 or lower", "invalid_third_party": "Invalid Third Party JSON string. Please confirm that your value is a valid JSON", "key_file_explanation": "Please use a .pem file that starts with \"-----BEGIN PRIVATE KEY-----\" and ends with \"-----END PRIVATE KEY-----\"", + "max_length": "Maximum length of {{max}} chars.", + "max_value": "Maximum value of {{max}}", + "min_length": "Minimum length of {{min}} chars.", "min_max_string": "Value needs to be of a length between {{min}} (inclusive) and {{max}} (inclusive)", + "min_value": "Minimum value of {{min}}", "missing_interface_upstream": "You need to have at least one upstream interface. At the moment, all your interfaces are downstream", "new_email_to_notify": "New email to notify", "new_phone_to_notify": "New phone to notify", @@ -905,6 +911,11 @@ "one": "Notification", "other": "Notifications" }, + "openroaming": { + "pool_strategy": "Pool Strategy", + "radius_endpoint_one": "Radius Endpoint", + "radius_endpoint_other": "Radius Endpoints" + }, "operator": { "delete_explanation": "Are you sure you want to delete this operator? This operation is not reversible", "delete_operator": "Delete Operator", @@ -970,6 +981,27 @@ "title": "Restrictions", "tty": "TTY Access" }, + "roaming": { + "account_created": "New account created!", + "account_deleted": "Deleted account!", + "account_one": "Account", + "account_other": "Accounts", + "certificate_deleted": "Deleted certificate!", + "certificate_one": "Certificate", + "certificate_other": "Certificates", + "city": "City", + "common_name": "Common Name", + "country": "Country", + "global_reach": "GlobalReach", + "global_reach_account_id": " Account ID", + "invalid_certificate": "Invalid certificate", + "invalid_key": "Invalid private key", + "location_details_title": "Location", + "organization": "Organization", + "private_key": "Private Key", + "province": "Province", + "state": "State" + }, "rrm": { "algorithm": "Algorithm", "algorithm_other": "Algorithms", @@ -1091,6 +1123,7 @@ "title": "Subscribers" }, "system": { + "advanced": "Advanced", "backend_logs": "Back-End Logs", "configuration": "Configuration", "could_not_retrieve": "Error: could not retrieve {{name}} system information", diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index be646c9..fd72403 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -269,6 +269,7 @@ "map": "Mapa", "max": "Max", "min": "Min", + "miscellaneous": "Diverso", "mode": "Modo", "model": "Modelo", "modified": "Modificado", @@ -737,6 +738,7 @@ "form": { "captive_web_root_explanation": "Utilice únicamente archivos .tar (no archivos comprimidos como .targz, por ejemplo)", "certificate_file_explanation": "Utilice un archivo .pem que comience con \"-----BEGIN CERTIFICATE-----\" y termine con \"-----END CERTIFICATE-----\"", + "invalid_alphanumeric_with_dash": "Caracteres aceptados. son solo alfanuméricos (letras y números)", "invalid_cidr": "Dirección IPv4 CIDR no válida. Ejemplo: 192.168.0.1/12", "invalid_email": "Email inválido", "invalid_file_content": "Contenido de archivo no válido, confirme que tiene un formato válido", @@ -763,7 +765,11 @@ "invalid_static_ipv4_e": "Dirección no válida, este rango reservado para experimentos (clase E). El primer octeto debe ser 223 o inferior", "invalid_third_party": "Cadena JSON de terceros no válida. Confirme que su valor es un JSON válido", "key_file_explanation": "Utilice un archivo .pem que comience con \"-----BEGIN PRIVATE KEY-----\" y termine con \"-----END PRIVATE KEY-----\"", + "max_length": "Longitud máxima de {{max}} caracteres.", + "max_value": "Valor máximo de {{max}}", + "min_length": "Longitud mínima de {{min}} caracteres.", "min_max_string": "El valor debe tener una longitud entre {{min}} (inclusive) y {{max}} (inclusive)", + "min_value": "Valor mínimo de {{min}}", "missing_interface_upstream": "Debe tener al menos una interfaz ascendente. Por el momento, todas sus interfaces están en sentido descendente", "new_email_to_notify": "Nuevo correo electrónico para notificar", "new_phone_to_notify": "Nuevo teléfono para avisar", @@ -905,6 +911,11 @@ "one": "Notificación", "other": "Notificaciones" }, + "openroaming": { + "pool_strategy": "Estrategia de piscina", + "radius_endpoint_one": "Punto final del radio", + "radius_endpoint_other": "Puntos finales de radio" + }, "operator": { "delete_explanation": "¿Está seguro de que desea eliminar este operador? Esta operación no es reversible.", "delete_operator": "Eliminar operador", @@ -970,6 +981,27 @@ "title": "Las restricciones", "tty": "Acceso TTY" }, + "roaming": { + "account_created": "¡Nueva cuenta creada!", + "account_deleted": "¡Cuenta eliminada!", + "account_one": "Cuenta", + "account_other": "Cuentas", + "certificate_deleted": "Certificado eliminado!", + "certificate_one": "Certificado", + "certificate_other": "Certificados", + "city": "ciudad", + "common_name": "Nombre común", + "country": "País", + "global_reach": "Alcance global", + "global_reach_account_id": "ID de cuenta ", + "invalid_certificate": "Certificado inválido", + "invalid_key": "Clave privada no válida", + "location_details_title": "Ubicación", + "organization": "Organización", + "private_key": "Llave privada", + "province": "Provincia", + "state": "Estado" + }, "rrm": { "algorithm": "Algoritmo", "algorithm_other": "Algoritmos", @@ -1091,6 +1123,7 @@ "title": "Suscriptores" }, "system": { + "advanced": "Avanzado", "backend_logs": "Registros de back-end", "configuration": "Configuración", "could_not_retrieve": "Error: no se pudo recuperar la información del sistema {{name}} ", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 3155c2b..daca560 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -269,6 +269,7 @@ "map": "Carte", "max": "Max", "min": "Min", + "miscellaneous": "Divers", "mode": "Mode", "model": "Modèle", "modified": "Modifié", @@ -737,6 +738,7 @@ "form": { "captive_web_root_explanation": "Veuillez utiliser uniquement des fichiers .tar (pas de fichiers compressés comme .targz, par exemple)", "certificate_file_explanation": "Veuillez utiliser un fichier .pem qui commence par \"-----BEGIN CERTIFICATE-----\" et se termine par \"-----END CERTIFICATE-----\"", + "invalid_alphanumeric_with_dash": "Caractères acceptés. sont uniquement alphanumériques (lettres et chiffres)", "invalid_cidr": "Adresse IPv4 CIDR non valide. Exemple : 192.168.0.1/12", "invalid_email": "Email Invalide", "invalid_file_content": "Contenu de fichier non valide, veuillez confirmer qu'il est au format valide", @@ -763,7 +765,11 @@ "invalid_static_ipv4_e": "Adresse invalide, cette plage est réservée aux expérimentations (classe E). Le premier octet doit être 223 ou moins", "invalid_third_party": "Chaîne JSON tierce non valide. Veuillez confirmer que votre valeur est un JSON valide", "key_file_explanation": "Veuillez utiliser un fichier .pem qui commence par \"-----BEGIN PRIVATE KEY-----\" et se termine par \"-----END PRIVATE KEY-----\"", + "max_length": "Longueur maximale de {{max}} caractères.", + "max_value": "Valeur maximale de {{max}}", + "min_length": "Longueur minimale de {{min}} caractères.", "min_max_string": "La valeur doit être d'une longueur comprise entre {{min}} (inclus) et {{max}} (inclus)", + "min_value": "Valeur minimale de {{min}}", "missing_interface_upstream": "Vous devez avoir au moins une interface en amont. Pour le moment, toutes vos interfaces sont en aval", "new_email_to_notify": "Nouvel e-mail à notifier", "new_phone_to_notify": "Nouveau téléphone à notifier", @@ -905,6 +911,11 @@ "one": "Notification", "other": "Les notifications" }, + "openroaming": { + "pool_strategy": "Stratégie de pool", + "radius_endpoint_one": "Point final de rayon", + "radius_endpoint_other": "Points de terminaison du rayon" + }, "operator": { "delete_explanation": "Voulez-vous vraiment supprimer cet opérateur ? Cette opération n'est pas réversible", "delete_operator": "Supprimer l'opérateur", @@ -970,6 +981,27 @@ "title": "Restrictions", "tty": "Accès ATS" }, + "roaming": { + "account_created": "Nouveau compte créé !", + "account_deleted": "Compte supprimé !", + "account_one": "Compte", + "account_other": "Comptes", + "certificate_deleted": "Certificat supprimé !", + "certificate_one": "Certificat", + "certificate_other": "Certificats", + "city": "Ville", + "common_name": "Nom commun", + "country": "Pays", + "global_reach": "Portée mondiale", + "global_reach_account_id": "ID de compte ", + "invalid_certificate": "certificat invalide", + "invalid_key": "Clé privée invalide", + "location_details_title": "Emplacement", + "organization": "Organisation", + "private_key": "Clé privée", + "province": "province", + "state": "Etat" + }, "rrm": { "algorithm": "Algorithme", "algorithm_other": "Algorithmes", @@ -1091,6 +1123,7 @@ "title": "Les abonnés" }, "system": { + "advanced": "Avancée", "backend_logs": "Journaux principaux", "configuration": "Configuration", "could_not_retrieve": "Erreur : impossible de récupérer les informations système {{name}} ", diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json index 71a31f3..33fb469 100644 --- a/public/locales/pt/translation.json +++ b/public/locales/pt/translation.json @@ -269,6 +269,7 @@ "map": "Mapa", "max": "máximo", "min": "minuto", + "miscellaneous": "Diversos", "mode": "Modo", "model": "Modelo", "modified": "Modificado", @@ -737,6 +738,7 @@ "form": { "captive_web_root_explanation": "Por favor, use apenas arquivos .tar (sem arquivos compactados como .targz, por exemplo)", "certificate_file_explanation": "Use um arquivo .pem que comece com \"-----BEGIN CERTIFICATE-----\" e termine com \"-----END CERTIFICATE-----\"", + "invalid_alphanumeric_with_dash": "Caracteres aceitos. são apenas alfanuméricos (letras e números)", "invalid_cidr": "Endereço CIDR IPv4 inválido. Exemplo: 192.168.0.1/12", "invalid_email": "E-mail inválido", "invalid_file_content": "Conteúdo de arquivo inválido. Confirme se está no formato válido", @@ -763,7 +765,11 @@ "invalid_static_ipv4_e": "Endereço inválido, este intervalo é reservado para experimentos (classe E). O primeiro octeto deve ser 223 ou inferior", "invalid_third_party": "String JSON de terceiros inválida. Confirme se seu valor é um JSON válido", "key_file_explanation": "Use um arquivo .pem que comece com \"-----BEGIN PRIVATE KEY-----\" e termine com \"-----END PRIVATE KEY-----\"", + "max_length": "Comprimento máximo de {{max}} caracteres.", + "max_value": "Valor máximo de {{max}}", + "min_length": "Comprimento mínimo de {{min}} caracteres.", "min_max_string": "O valor precisa ter um comprimento entre {{min}} (inclusive) e {{max}} (inclusive)", + "min_value": "Valor mínimo de {{min}}", "missing_interface_upstream": "Você precisa ter pelo menos uma interface upstream. No momento, todas as suas interfaces estão downstream", "new_email_to_notify": "Novo e-mail para notificar", "new_phone_to_notify": "Novo telefone para notificar", @@ -905,6 +911,11 @@ "one": "Notificação", "other": "Notificações" }, + "openroaming": { + "pool_strategy": "Estratégia de pool", + "radius_endpoint_one": "Ponto final do raio", + "radius_endpoint_other": "Pontos finais de raio" + }, "operator": { "delete_explanation": "Tem certeza de que deseja excluir este operador? Esta operação não é reversível", "delete_operator": "Excluir operador", @@ -970,6 +981,27 @@ "title": "RESTRIÇÕES", "tty": "Acesso TTY" }, + "roaming": { + "account_created": "Nova conta criada!", + "account_deleted": "Conta excluída!", + "account_one": "Conta", + "account_other": "Contas", + "certificate_deleted": "Certificado excluído!", + "certificate_one": "Certificado", + "certificate_other": "Certificados", + "city": "Cidade", + "common_name": "Nome comum", + "country": "País", + "global_reach": "Alcance global", + "global_reach_account_id": "ID da conta", + "invalid_certificate": "Certificado inválido", + "invalid_key": "Chave privada inválida", + "location_details_title": "Localização", + "organization": "Organização", + "private_key": "Chave privada", + "province": "província", + "state": "Estado" + }, "rrm": { "algorithm": "Algoritmo", "algorithm_other": "Algoritmos", @@ -1091,6 +1123,7 @@ "title": "Inscritos" }, "system": { + "advanced": "Avançado", "backend_logs": "Registros de back-end", "configuration": "Configuração", "could_not_retrieve": "Erro: não foi possível recuperar {{name}} informações do sistema", diff --git a/src/hooks/Network/Simulations.ts b/src/hooks/Network/Simulations.ts new file mode 100644 index 0000000..7676022 --- /dev/null +++ b/src/hooks/Network/Simulations.ts @@ -0,0 +1,173 @@ +import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { axiosGw, axiosOwls } from 'constants/axiosInstances'; +import { AtLeast } from 'models/General'; + +export type Simulation = { + clientInterval: number; + concurrentDeviceS: number; + deviceType: string; + devices: number; + gateway: string; + healthCheckInterval: number; + id: string; + keepAlive: number; + key: string; + macPrefix: string; + minAssociations: number; + maxAssociations: number; + minClients: number; + maxClients: number; + name: string; + reconnectionInterval: number; + simulationLength: number; + stateInterval: number; + threads: number; +}; + +const getSimulations = () => async () => + axiosOwls.get(`simulation/*`).then((response) => response.data as { list: Simulation[] }); + +export const useGetSimulations = () => + useQuery(['simulations'], getSimulations(), { + keepPreviousData: true, + staleTime: 30000, + }); + +const getSimulation = (id?: string) => async () => + axiosOwls.get(`simulation/${id}`).then((response) => response.data as { list: Simulation[] }); +export const useGetSimulation = ({ id }: { id?: string }) => + useQuery(['simulation', id], getSimulation(id), { + keepPreviousData: true, + enabled: id !== undefined, + staleTime: 30000, + }); + +const createSimulation = async (newSimulation: Partial) => axiosOwls.post(`simulation/0`, newSimulation); +export const useCreateSimulation = () => { + const queryClient = useQueryClient(); + + return useMutation(createSimulation, { + onSuccess: () => { + queryClient.invalidateQueries(['simulations']); + }, + }); +}; + +const updateSimulation = async (newSimulation: AtLeast) => + axiosOwls.put(`simulation/${newSimulation.id}`, newSimulation).then((response) => response.data as Simulation); +export const useUpdateSimulation = () => { + const queryClient = useQueryClient(); + + return useMutation(updateSimulation, { + onSuccess: (newSimulation) => { + queryClient.setQueryData(['simulation'], newSimulation); + queryClient.invalidateQueries(['simulations']); + }, + }); +}; + +const deleteSimulation = async ({ id }: { id: string }) => axiosOwls.delete(`simulation/${id}`); +export const useDeleteSimulation = () => { + const queryClient = useQueryClient(); + + return useMutation(deleteSimulation, { + onSuccess: () => { + queryClient.invalidateQueries(['simulations']); + }, + }); +}; + +const startSimulation = async ({ id }: { id: string }) => axiosOwls.post(`operation/${id}?operation=start`); +export const useStartSimulation = () => { + const queryClient = useQueryClient(); + + return useMutation(startSimulation, { + onSuccess: () => { + queryClient.invalidateQueries(['simulations', 'status']); + }, + }); +}; +const stopSimulation = async ({ runId, simulationId }: { simulationId: string; runId: string }) => + axiosOwls.post(`operation/${simulationId}?runningId=${runId}&operation=stop`); +export const useStopSimulation = () => { + const queryClient = useQueryClient(); + + return useMutation(stopSimulation, { + onSuccess: () => { + queryClient.invalidateQueries(['simulations', 'status']); + }, + }); +}; +const cancelSimulation = async ({ runId, simulationId }: { simulationId: string; runId: string }) => + axiosOwls.post(`operation/${simulationId}?runningId=${runId}&operation=cancel`); +export const useCancelSimulation = () => { + const queryClient = useQueryClient(); + + return useMutation(cancelSimulation, { + onSuccess: () => { + queryClient.invalidateQueries(['simulations', 'status']); + }, + }); +}; + +export type SimulationStatus = { + endTime: number; + errorDevices: number; + id: string; + liveDevices: number; + msgsRx: number; + msgsTx: number; + owner: string; + rx: number; + simulationId: string; + startTime: number; + state: 'running' | 'completed' | 'cancelled' | 'none'; + timeToFullDevices: number; + tx: number; +}; +const getSimulationsStatus = async () => + axiosOwls.get(`status/*`).then((response) => response.data as SimulationStatus[]); +export const useGetSimulationsStatus = () => + useQuery(['simulations', 'status'], getSimulationsStatus, { + keepPreviousData: true, + staleTime: Infinity, + }); + +const getSimulationStatus = async (context: QueryFunctionContext<[string, string, string]>) => + axiosOwls.get(`status/${context.queryKey[2]}`).then((response) => response.data as SimulationStatus); +export const useGetSimulationStatus = ({ id }: { id: string }) => + useQuery(['simulations', 'status', id], getSimulationStatus, { + keepPreviousData: true, + staleTime: Infinity, + }); + +const getSimulationHistory = async (context: QueryFunctionContext<[string, string, string]>) => + axiosOwls.get(`results/${context.queryKey[2]}`).then((response) => response.data.list as SimulationStatus[]); +export const useGetSimulationHistory = ({ id }: { id: string }) => + useQuery(['simulations', 'history', id], getSimulationHistory, { + keepPreviousData: true, + enabled: !!id, + }); + +const deleteSimulationResult = async ({ id }: { id: string }) => axiosOwls.delete(`results/${id}`); +export const useDeleteSimulationResult = () => { + const queryClient = useQueryClient(); + + return useMutation(deleteSimulationResult, { + onSuccess: () => { + queryClient.invalidateQueries(['simulations', 'history']); + }, + }); +}; + +const deleteSimulatedDevices = async () => axiosGw.delete('devices?simulatedDevices=true'); + +export const useDeleteSimulatedDevices = () => { + const queryClient = useQueryClient(); + + return useMutation(deleteSimulatedDevices, { + onSuccess: () => { + queryClient.invalidateQueries(['devices']); + }, + }); +}; diff --git a/src/hooks/useNotification.ts b/src/hooks/useNotification.ts new file mode 100644 index 0000000..fcc1f30 --- /dev/null +++ b/src/hooks/useNotification.ts @@ -0,0 +1,80 @@ +import * as React from 'react'; +import { useToast } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { v4 as uuid } from 'uuid'; +import { isApiError } from 'models/Axios'; + +export type SuccessNotificationProps = { + description: string; + id?: string; +}; + +export type ApiErrorNotificationProps = { + e: unknown; + fallbackMessage?: string; + id?: string; +}; + +export const useNotification = () => { + const { t } = useTranslation(); + const toast = useToast(); + + const successToast = ({ description, id }: SuccessNotificationProps) => { + toast({ + id: id ?? uuid(), + title: t('common.success'), + description, + status: 'success', + duration: 3000, + isClosable: true, + position: 'top-right', + }); + }; + + const apiErrorToast = ({ e, id, fallbackMessage }: ApiErrorNotificationProps) => { + if (isApiError(e)) { + toast({ + id: id ?? uuid(), + title: t('common.error'), + description: e.response?.data.ErrorDescription, + status: 'error', + duration: 5000, + isClosable: true, + position: 'top-right', + }); + } else { + toast({ + id: id ?? uuid(), + title: t('common.error'), + description: fallbackMessage, + status: 'error', + duration: 5000, + isClosable: true, + position: 'top-right', + }); + } + }; + + const errorToast = ({ description, id }: SuccessNotificationProps) => { + toast({ + id: id ?? uuid(), + title: t('common.error'), + description, + status: 'error', + duration: 5000, + isClosable: true, + position: 'top-right', + }); + }; + + return React.useMemo( + () => ({ + successToast, + errorToast, + apiErrorToast, + }), + [t], + ); +}; + +export type UseNotificationReturn = ReturnType; diff --git a/src/models/Axios.ts b/src/models/Axios.ts index 5e0567f..c8f5cfd 100644 --- a/src/models/Axios.ts +++ b/src/models/Axios.ts @@ -1,3 +1,6 @@ -import { AxiosError as Err } from 'axios'; +import { AxiosError as Err, isAxiosError } from 'axios'; export type AxiosError = Err<{ ErrorDescription: string; ErrorCode: number }>; + +export const isApiError = (e: unknown): e is AxiosError => + isAxiosError(e) && (e as AxiosError).response?.data?.ErrorDescription !== undefined; diff --git a/src/pages/AdvancedSystemPage/index.tsx b/src/pages/AdvancedSystemPage/index.tsx new file mode 100644 index 0000000..26b3f67 --- /dev/null +++ b/src/pages/AdvancedSystemPage/index.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; +import { + Box, + Button, + Center, + Heading, + Popover, + PopoverArrow, + PopoverBody, + PopoverCloseButton, + PopoverContent, + PopoverHeader, + PopoverTrigger, + Text, +} from '@chakra-ui/react'; +import { Trash } from '@phosphor-icons/react'; +import { Card } from 'components/Containers/Card'; +import { CardHeader } from 'components/Containers/Card/CardHeader'; +import { CardBody } from 'components/Containers/Card/CardBody'; +import { DeleteButton } from 'components/Buttons/DeleteButton'; +import { useNotification } from 'hooks/useNotification'; +import { useDeleteSimulatedDevices } from 'hooks/Network/Simulations'; + +const AdvancedSystemPage = () => { + const { successToast, apiErrorToast } = useNotification(); + const deleteSimulatedDevices = useDeleteSimulatedDevices(); + + const handleDeleteSimulatedDevices = async () => + deleteSimulatedDevices.mutateAsync(undefined, { + onSuccess: () => { + successToast({ + id: 'delete-simulated-devices', + description: 'Simulated devices deleted!', + }); + }, + onError: (e) => { + apiErrorToast({ + id: 'delete-simulated-devices', + e, + fallbackMessage: 'Error deleting simulated devices', + }); + }, + }); + + return ( + + + Operations + + + + Delete Simulated Devices + Delete all simulated devices from the database. This action cannot be undone. + + {({ onClose }) => ( + <> + + + + + + + Confirm + + Are you sure you want to delete all simulated devices? +
+ + { + await handleDeleteSimulatedDevices(); + onClose(); + }} + isCompact={false} + /> +
+
+
+ + )} +
+
+
+
+ ); +}; + +export default AdvancedSystemPage; diff --git a/src/router/routes.tsx b/src/router/routes.tsx index 7cc0c00..3defd69 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Barcode, FloppyDisk, Info, ListBullets, TerminalWindow, UsersThree, WifiHigh } from '@phosphor-icons/react'; import { Route } from 'models/Routes'; +const AdvancedSystemPage = React.lazy(() => import('pages/AdvancedSystemPage')); const DefaultConfigurationsPage = React.lazy(() => import('pages/DefaultConfigurations')); const DefaultFirmwarePage = React.lazy(() => import('pages/DefaultFirmware')); const DevicePage = React.lazy(() => import('pages/Device')); @@ -178,6 +179,13 @@ const routes: Route[] = [ name: 'system.title', icon: () => , children: [ + { + id: 'system-advanced', + authorized: ['root', 'partner', 'admin', 'csr', 'system'], + path: '/systemAdvanced', + name: 'system.advanced', + component: AdvancedSystemPage, + }, { id: 'system-configuration', authorized: ['root', 'partner', 'admin', 'csr', 'system'],