[WIFI-12286] Add support for new venue upgrade API

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2023-02-10 10:26:41 +01:00
parent 84cadb6876
commit f447005817
10 changed files with 341 additions and 72 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "wlan-cloud-owprov-ui",
"version": "2.9.0(8)",
"version": "2.9.0(10)",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "wlan-cloud-owprov-ui",
"version": "2.9.0(8)",
"version": "2.9.0(10)",
"license": "ISC",
"dependencies": {
"@chakra-ui/icons": "^2.0.11",

View File

@@ -1,6 +1,6 @@
{
"name": "wlan-cloud-owprov-ui",
"version": "2.9.0(8)",
"version": "2.9.0(10)",
"description": "",
"main": "index.tsx",
"scripts": {

View File

@@ -630,6 +630,8 @@
"one": "Gerät",
"reassign_already_owned": "Geräte neu zuweisen, die bereits vorhanden sind und einem anderen Unternehmen/Veranstaltungsort/Abonnenten gehören?",
"restricted": "Beschränkt",
"restricted_overriden": "Dies ist ein eingeschränktes Gerät, aber es befindet sich im Entwicklungsmodus. Alle Einschränkungen werden derzeit ignoriert",
"restrictions_overriden_title": "Dev-Modus",
"sanity": "Gesundheit",
"start_import": "Geräteimport starten",
"test_batch": "Testen Sie Importdaten",
@@ -683,6 +685,13 @@
"update_success": "Entität aktualisiert!",
"venues_under_root": "Veranstaltungsorte können nicht direkt unter der Root-Entität erstellt werden"
},
"firmware": {
"db_update_warning": "Dieser Vorgang wird täglich automatisch durchgeführt, ohne dass dieses manuelle Update verwendet werden muss. Die Aktualisierung dieser Datenbank kann bis zu 25 Minuten dauern",
"last_db_update_modal": "Firmware-Datenbank",
"last_db_update_title": "Datenbank",
"start_db_update": "Datenbankaktualisierung starten",
"started_db_update": "Datenbankaktualisierung gestartet, dieser Vorgang sollte bis zu 25 Minuten dauern"
},
"footer": {
"powered_by": "Unterstützt von",
"version": "Ausführung"
@@ -1123,9 +1132,10 @@
"title": "Veranstaltungsorte",
"update_all_devices": "Alle Gerätekonfigurationen aktualisieren",
"update_success": "Veranstaltungsort aktualisiert!",
"upgrade_all_devices": "Aktualisieren Sie alle Geräte auf die neueste Firmware",
"upgrade_all_devices": "Aktualisieren Sie die Firmware aller Geräte",
"upgrade_all_devices_error": "Fehler beim Aktualisieren von Geräten: {{e}}",
"upgrade_all_devices_success": "Upgrade von Geräten erfolgreich gestartet!",
"upgrade_options_available": "Hier sind alle verfügbaren Revisionen, bitte wählen Sie diejenige aus, auf die ALLE Geräte dieses Veranstaltungsortes aktualisiert werden sollen",
"use_existing": "Benutze existierendes",
"use_existing_contacts": "Verwenden Sie vorhandene Kontakte",
"use_this_contact": "Verwenden Sie diesen Kontakt"

View File

@@ -630,6 +630,8 @@
"one": "Device",
"reassign_already_owned": "Reassign devices which already exist and are owned by another entity/venue/subscriber?",
"restricted": "Restricted",
"restricted_overriden": "This is a restricted device, but it is in development mode. All restrictions are currently ignored",
"restrictions_overriden_title": "Dev Mode",
"sanity": "Sanity",
"start_import": "Start Device Importation",
"test_batch": "Test Import Data",
@@ -683,6 +685,13 @@
"update_success": "Entity updated!",
"venues_under_root": "Venues cannot be created directly under the root entity"
},
"firmware": {
"db_update_warning": "This operation is done daily automatically without need to use this manual update. Updating this database can take up to 25 minutes",
"last_db_update_modal": "Firmware Database",
"last_db_update_title": "Database",
"start_db_update": "Start Database Update",
"started_db_update": "Started database update, this operation should take up to 25 minutes to complete"
},
"footer": {
"powered_by": "Powered By",
"version": "Version"
@@ -1123,9 +1132,10 @@
"title": "Venues",
"update_all_devices": "Update All Device Configurations",
"update_success": "Venue updated!",
"upgrade_all_devices": "Upgrade All Devices to Latest Firmware",
"upgrade_all_devices": "Upgrade All Devices Firmware",
"upgrade_all_devices_error": "Error upgrading devices: {{e}}",
"upgrade_all_devices_success": "Successfully started upgrading devices!",
"upgrade_options_available": "Here are all available revisions, please select the one you want ALL of this venue's devices to be upgrade to",
"use_existing": "Use Existing",
"use_existing_contacts": "Use Existing Contacts",
"use_this_contact": "Use this contact"

View File

@@ -630,6 +630,8 @@
"one": "Dispositivo",
"reassign_already_owned": "¿Reasignar dispositivos que ya existen y son propiedad de otra entidad/lugar/suscriptor?",
"restricted": "Restringido",
"restricted_overriden": "Este es un dispositivo restringido, pero está en modo de desarrollo. Actualmente se ignoran todas las restricciones.",
"restrictions_overriden_title": "MODO DE DESARROLLO",
"sanity": "Cordura",
"start_import": "Iniciar la importación de dispositivos",
"test_batch": "Datos de importación de prueba",
@@ -683,6 +685,13 @@
"update_success": "¡Entidad actualizada!",
"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz"
},
"firmware": {
"db_update_warning": "Esta operación se realiza automáticamente todos los días de forma automática sin necesidad de utilizar esta actualización manual. La actualización de esta base de datos puede tardar hasta 25 minutos",
"last_db_update_modal": "Base de datos de firmware",
"last_db_update_title": "Base de datos",
"start_db_update": "Iniciar actualización de la base de datos",
"started_db_update": "Actualización de la base de datos iniciada, esta operación debería tardar hasta 25 minutos en completarse"
},
"footer": {
"powered_by": "energizado por",
"version": "Versión"
@@ -1123,9 +1132,10 @@
"title": "Sedes",
"update_all_devices": "Actualizar todas las configuraciones de dispositivos",
"update_success": "Lugar actualizado!",
"upgrade_all_devices": "Actualice todos los dispositivos al firmware más reciente",
"upgrade_all_devices": "Actualizar el firmware de todos los dispositivos",
"upgrade_all_devices_error": "Error al actualizar dispositivos: {{e}}",
"upgrade_all_devices_success": "¡Comenzó con éxito la actualización de dispositivos!",
"upgrade_options_available": "Aquí están todas las revisiones disponibles, seleccione la que desea que TODOS los dispositivos de este lugar se actualicen",
"use_existing": "Utilizar existente",
"use_existing_contacts": "Usar contactos existentes",
"use_this_contact": "Usa este contacto"

View File

@@ -630,6 +630,8 @@
"one": "Dispositif",
"reassign_already_owned": "Réattribuer des appareils qui existent déjà et qui appartiennent à une autre entité/salle/abonné ?",
"restricted": "Limité",
"restricted_overriden": "Il s'agit d'un appareil restreint, mais il est en mode développement. Toutes les restrictions sont actuellement ignorées",
"restrictions_overriden_title": "Mode développement",
"sanity": "Santé mentale",
"start_import": "Démarrer l'importation de l'appareil",
"test_batch": "Tester les données d'importation",
@@ -683,6 +685,13 @@
"update_success": "Entité mise à jour !",
"venues_under_root": "Les lieux ne peuvent pas être créés directement sous l'entité racine"
},
"firmware": {
"db_update_warning": "Cette opération se fait automatiquement quotidiennement sans avoir besoin d'utiliser cette mise à jour manuelle. La mise à jour de cette base de données peut prendre jusqu'à 25 minutes",
"last_db_update_modal": "Base de données du micrologiciel",
"last_db_update_title": "Base de données",
"start_db_update": "Démarrer la mise à jour de la base de données",
"started_db_update": "Mise à jour de la base de données démarrée, cette opération devrait prendre jusqu'à 25 minutes"
},
"footer": {
"powered_by": "Alimenté par",
"version": "Version"
@@ -1123,9 +1132,10 @@
"title": "Les lieux",
"update_all_devices": "Mettre à jour toutes les configurations de périphérique",
"update_success": "Lieu mis à jour !",
"upgrade_all_devices": "Mettre à niveau tous les appareils vers le dernier micrologiciel",
"upgrade_all_devices": "Mettre à niveau le micrologiciel de tous les appareils",
"upgrade_all_devices_error": "Erreur lors de la mise à jour des appareils : {{e}}",
"upgrade_all_devices_success": "La mise à niveau des appareils a démarré avec succès !",
"upgrade_options_available": "Voici toutes les révisions disponibles, veuillez sélectionner celle vers laquelle vous souhaitez que TOUS les appareils de ce lieu soient mis à niveau",
"use_existing": "Utiliser l'existant",
"use_existing_contacts": "Utiliser les contacts existants",
"use_this_contact": "Utilisez ce contact"

View File

@@ -630,6 +630,8 @@
"one": "Dispositivo",
"reassign_already_owned": "Reatribuir dispositivos que já existem e são de propriedade de outra entidade/local/assinante?",
"restricted": "Restrito",
"restricted_overriden": "Este é um dispositivo restrito, mas está em modo de desenvolvimento. Todas as restrições são atualmente ignoradas",
"restrictions_overriden_title": "Modo de desenvolvedor",
"sanity": "Sanidade",
"start_import": "Iniciar importação de dispositivos",
"test_batch": "Dados de importação de teste",
@@ -683,6 +685,13 @@
"update_success": "Entidade atualizada!",
"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz"
},
"firmware": {
"db_update_warning": "Esta operação é feita automaticamente diariamente sem necessidade de usar esta atualização manual. A atualização deste banco de dados pode levar até 25 minutos",
"last_db_update_modal": "banco de dados de firmware",
"last_db_update_title": "base de dados",
"start_db_update": "Iniciar atualização do banco de dados",
"started_db_update": "Atualização do banco de dados iniciada, esta operação deve levar até 25 minutos para ser concluída"
},
"footer": {
"powered_by": "Distribuído por",
"version": "Versão"
@@ -1123,9 +1132,10 @@
"title": "Locais",
"update_all_devices": "Atualizar todas as configurações do dispositivo",
"update_success": "Local atualizado!",
"upgrade_all_devices": "Atualize todos os dispositivos para o firmware mais recente",
"upgrade_all_devices": "Atualize o firmware de todos os dispositivos",
"upgrade_all_devices_error": "Erro ao atualizar dispositivos: {{e}}",
"upgrade_all_devices_success": "Atualização de dispositivos iniciada com sucesso!",
"upgrade_options_available": "Aqui estão todas as revisões disponíveis, selecione aquela para a qual você deseja que TODOS os dispositivos deste local sejam atualizados",
"use_existing": "Usar existente",
"use_existing_contacts": "Usar contatos existentes",
"use_this_contact": "Use este contato"

View File

@@ -1,22 +1,43 @@
import { useToast } from '@chakra-ui/react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import useDefaultPage from 'hooks/useDefaultPage';
import useDefaultPage from '../useDefaultPage';
import { AxiosError } from 'models/Axios';
import { Venue } from 'models/Venue';
import { DeviceRules } from 'models/Basic';
import { Note } from 'models/Note';
import { axiosProv } from 'utils/axiosInstances';
export interface VenueApiResponse {
id: string;
name: string;
description: string;
parent: string;
devices: string[];
children: string[];
contacts: string[];
entity: string;
boards: string[];
created: number;
modified: number;
configurations: string[];
notes: Note[];
variables: string[];
location: string;
sourceIP: string[];
deviceRules: DeviceRules;
}
const getVenuesBatch = async (limit: number, offset: number) =>
axiosProv
.get(`venue?withExtendedInfo=true&offset=${offset}&limit=${limit}`)
.then(({ data }) => data.venues as Venue[]);
.then(({ data }) => data.venues as VenueApiResponse[]);
const getAllVenues = async () => {
const limit = 500;
let offset = 0;
let data: Venue[] = [];
let lastResponse: Venue[] = [];
let data: VenueApiResponse[] = [];
let lastResponse: VenueApiResponse[] = [];
do {
// eslint-disable-next-line no-await-in-loop
lastResponse = await getVenuesBatch(limit, offset);
@@ -81,15 +102,16 @@ export const useGetSelectVenues = ({ select }: { select: string[] }) => {
);
};
export const useGetVenue = ({ id }: { id: string }) => {
export const useGetVenue = ({ id }: { id?: string }) => {
const { t } = useTranslation();
const toast = useToast();
const goToDefaultPage = useDefaultPage();
return useQuery(
['get-venue', id],
() => axiosProv.get(`venue/${id}?withExtendedInfo=true`).then(({ data }) => data),
() => axiosProv.get(`venue/${id}?withExtendedInfo=true`).then(({ data }: { data: VenueApiResponse }) => data),
{
enabled: id !== undefined && id !== '',
onError: (e: AxiosError) => {
if (!toast.isActive('venue-fetching-error'))
toast({
@@ -104,7 +126,7 @@ export const useGetVenue = ({ id }: { id: string }) => {
isClosable: true,
position: 'top-right',
});
goToDefaultPage();
if (e.response?.data?.ErrorCode === 404) goToDefaultPage();
},
},
);
@@ -115,10 +137,25 @@ export const useCreateVenue = () =>
axiosProv.post(`venue/0${createObjects ? `?createObjects=${JSON.stringify(createObjects)}` : ''}`, params),
);
export const useUpdateVenue = ({ id }: { id: string }) =>
useMutation(({ params, createObjects }: { params: unknown; createObjects: unknown }) =>
axiosProv.put(`venue/${id}${createObjects ? `?createObjects=${JSON.stringify(createObjects)}` : ''}`, params),
export const useUpdateVenue = ({ id }: { id: string }) => {
const queryClient = useQueryClient();
return useMutation(
({ params, createObjects }: { params: Partial<VenueApiResponse>; createObjects?: unknown }) =>
axiosProv
.put(`venue/${id}${createObjects ? `?createObjects=${JSON.stringify(createObjects)}` : ''}`, params)
.then((res: { data: VenueApiResponse }) => res),
{
onSuccess: ({ data }) => {
queryClient.invalidateQueries(['get-entity-tree']);
queryClient.invalidateQueries(['get-venues']);
queryClient.invalidateQueries(['get-all-locations', id]);
queryClient.setQueryData(['get-venue', id], data);
},
},
);
};
export const useUpdateVenueDevices = ({ id }: { id: string }) => {
const { t } = useTranslation();
const toast = useToast();
@@ -185,36 +222,38 @@ export const useRebootVenueDevices = ({ id }: { id: string }) => {
});
};
export const useUpgradeVenueDevices = ({ id }: { id: string }) => {
export const useUpgradeVenueDevices = () => {
const { t } = useTranslation();
const toast = useToast();
return useMutation(() => axiosProv.put(`venue/${id}?upgradeAllDevices=true`, {}), {
onSuccess: () => {
toast({
id: 'venue-upgrade-devices-success',
title: t('common.success'),
description: t('venues.upgrade_all_devices_success'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
return useMutation(
(data: { id: string; revision: string }) =>
axiosProv.put(`venue/${data.id}?upgradeAllDevices=true&revision=${data.revision}`, {}),
{
onSuccess: () => {
toast({
id: 'venue-upgrade-devices-success',
title: t('common.success'),
description: t('venues.upgrade_all_devices_success'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
},
onError: (e: AxiosError) => {
toast({
id: uuid(),
title: t('common.error'),
description: e?.response?.data?.ErrorDescription,
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
},
},
onError: (e: AxiosError) => {
toast({
id: uuid(),
title: t('common.error'),
description: t('crud.upgrade_all_devices_error', {
e: e?.response?.data?.ErrorDescription,
}),
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
},
});
);
};
export const useDeleteVenue = () => useMutation((id) => axiosProv.delete(`venue/${id}`));
@@ -271,3 +310,21 @@ export const useRemoveVenueContact = ({
},
);
};
type Release = {
date: number;
revision: string;
};
const getVenueUpgradeAvailableFirmware = (id: string) =>
axiosProv.put(`venue/${id}?upgradeAllDevices=true&revisionsAvailable=true`, {}).then(
(res: {
data: {
releases: Release[];
releasesCandidates: Release[];
developmentReleases: Release[];
};
}) => res.data,
);
export const useGetVenueUpgradeAvailableFirmware = ({ id }: { id: string }) =>
useQuery(['venue', id, 'availableFirmware'], () => getVenueUpgradeAvailableFirmware(id));

View File

@@ -1,24 +1,20 @@
import React from 'react';
import { ChevronDownIcon } from '@chakra-ui/icons';
import { Button, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
import { IconButton, Menu, MenuButton, MenuItem, MenuList, Tooltip, useDisclosure } from '@chakra-ui/react';
import { Wrench } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import { useRebootVenueDevices, useUpdateVenueDevices, useUpgradeVenueDevices } from 'hooks/Network/Venues';
import VenueFirmwareUpgradeModal from './VenueFirmwareUpgradeModal';
import { useRebootVenueDevices, useUpdateVenueDevices } from 'hooks/Network/Venues';
interface Props {
type Props = {
venueId: string;
isDisabled: boolean;
}
};
const VenueActions = (
{
venueId,
isDisabled
}: Props
) => {
const VenueActions = ({ venueId, isDisabled }: Props) => {
const { t } = useTranslation();
const { isOpen: isUpgradeOpen, onOpen: onOpenUpgrade, onClose: onCloseUpgrade } = useDisclosure();
const { mutateAsync: rebootDevices } = useRebootVenueDevices({ id: venueId });
const updateDevices = useUpdateVenueDevices({ id: venueId });
const upgradeDevices = useUpgradeVenueDevices({ id: venueId });
const handleUpdateClick = () => {
updateDevices.mutateAsync();
@@ -28,21 +24,22 @@ const VenueActions = (
rebootDevices();
};
const handleUpgradeDevices = () => {
upgradeDevices.mutateAsync();
};
return (
<Menu>
<MenuButton as={Button} rightIcon={<ChevronDownIcon />} ml={2} isDisabled={isDisabled}>
{t('common.actions')}
</MenuButton>
<MenuList>
<MenuItem onClick={handleRebootClick}>{t('venues.reboot_all_devices')}</MenuItem>
<MenuItem onClick={handleUpdateClick}>{t('venues.update_all_devices')}</MenuItem>
<MenuItem onClick={handleUpgradeDevices}>{t('venues.upgrade_all_devices')}</MenuItem>
</MenuList>
</Menu>
<>
<Menu>
<Tooltip label={t('common.actions')} hasArrow>
<MenuButton as={IconButton} icon={<Wrench size={20} />} ml={2} isDisabled={isDisabled}>
{t('common.actions')}
</MenuButton>
</Tooltip>
<MenuList>
<MenuItem onClick={handleRebootClick}>{t('venues.reboot_all_devices')}</MenuItem>
<MenuItem onClick={handleUpdateClick}>{t('venues.update_all_devices')}</MenuItem>
<MenuItem onClick={onOpenUpgrade}>{t('venues.upgrade_all_devices')}</MenuItem>
</MenuList>
</Menu>
<VenueFirmwareUpgradeModal isOpen={isUpgradeOpen} onClose={onCloseUpgrade} venueId={venueId} />
</>
);
};

View File

@@ -0,0 +1,165 @@
import * as React from 'react';
import {
Alert,
AlertDescription,
AlertIcon,
Box,
Button,
Center,
ListItem,
Spinner,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
Text,
UnorderedList,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import FormattedDate from 'components/FormattedDate';
import { Modal } from 'components/Modals/Modal';
import { useGetVenueUpgradeAvailableFirmware, useUpgradeVenueDevices } from 'hooks/Network/Venues';
import { AxiosError } from 'models/Axios';
type Props = {
isOpen: boolean;
onClose: () => void;
venueId: string;
};
const VenueFirmwareUpgradeModal = ({ isOpen, onClose, venueId }: Props) => {
const { t } = useTranslation();
const getAvailableFirmware = useGetVenueUpgradeAvailableFirmware({ id: venueId });
const upgrade = useUpgradeVenueDevices();
const [selectedRevision, setSelectedRevision] = React.useState<string>();
const onRevisionSelect = (revision: string) => () => {
setSelectedRevision(revision);
};
const onUpgradeClick = () => {
if (selectedRevision) {
upgrade.mutateAsync(
{ revision: selectedRevision, id: venueId },
{
onSuccess: () => {
setSelectedRevision(undefined);
onClose();
},
},
);
}
};
const listItemStyle = (revision: string) => ({
cursor: 'pointer',
backgroundColor: revision === selectedRevision ? 'gray.200' : 'white',
});
const displayRevision = (release: { date: number; revision: string }) => (
<ListItem key={release.revision} onClick={onRevisionSelect(release.revision)} {...listItemStyle(release.revision)}>
<FormattedDate date={release.date} />
<Text>{release.revision}</Text>
</ListItem>
);
const placeholder = React.useMemo(() => {
if (getAvailableFirmware.isFetching) {
return (
<Center my={6}>
<Spinner size="xl" />
</Center>
);
}
if (getAvailableFirmware.isError) {
return (
<Center my={6}>
<Alert status="error">
<AlertIcon />
<AlertDescription>
{(getAvailableFirmware.error as AxiosError).response?.data?.ErrorDescription}
</AlertDescription>
</Alert>
</Center>
);
}
return null;
}, [getAvailableFirmware]);
React.useEffect(() => {
if (isOpen) {
setSelectedRevision(undefined);
getAvailableFirmware.refetch();
}
}, [isOpen]);
return (
<Modal isOpen={isOpen} onClose={onClose} title={t('venues.upgrade_all_devices')}>
<Box>
{placeholder || !getAvailableFirmware.data ? (
placeholder
) : (
<>
<Text fontWeight="bold">{t('venues.upgrade_options_available')}</Text>
<Center my={2}>
<Button
colorScheme="yellow"
onClick={onUpgradeClick}
isDisabled={!selectedRevision}
isLoading={upgrade.isLoading}
display="block"
>
<Text>{selectedRevision ? `Upgrade to ` : 'Select a revision to upgrade'}</Text>
{selectedRevision ? <Text mt={1}>{selectedRevision}</Text> : null}
</Button>
</Center>
<Tabs>
<TabList>
<Tab>Official Releases</Tab>
<Tab>Release Candidates</Tab>
<Tab>Dev Releases</Tab>
</TabList>
<TabPanels>
<TabPanel>
<Text fontWeight="bold" textDecor="underline">
Official Releases
</Text>
<UnorderedList>
{getAvailableFirmware.data?.releases
.sort((a, b) => b.date - a.date)
.map((release) => displayRevision(release))}
</UnorderedList>
</TabPanel>
<TabPanel>
<Text fontWeight="bold" textDecor="underline">
Release Candidates
</Text>
<UnorderedList>
{getAvailableFirmware.data?.releasesCandidates
.sort((a, b) => b.date - a.date)
.map((release) => displayRevision(release))}
</UnorderedList>
</TabPanel>
<TabPanel>
<Text fontWeight="bold" textDecor="underline">
Dev Releases
</Text>
<UnorderedList>
{getAvailableFirmware.data?.developmentReleases
.sort((a, b) => b.date - a.date)
.map((release) => displayRevision(release))}
</UnorderedList>
</TabPanel>
</TabPanels>
</Tabs>
</>
)}
</Box>
</Modal>
);
};
export default VenueFirmwareUpgradeModal;