diff --git a/static/skywire-manager-src/src/app/components/layout/confirmation/confirmation.component.ts b/static/skywire-manager-src/src/app/components/layout/confirmation/confirmation.component.ts index ff94b7ee30..b627a60be4 100644 --- a/static/skywire-manager-src/src/app/components/layout/confirmation/confirmation.component.ts +++ b/static/skywire-manager-src/src/app/components/layout/confirmation/confirmation.component.ts @@ -39,9 +39,9 @@ export interface ConfirmationData { * is shown. */ enum ConfirmationStates { - Asking, - Processing, - Done, + Asking = 'Asking', + Processing = 'Processing', + Done = 'Done', } /** @@ -97,6 +97,25 @@ export class ConfirmationComponent implements AfterViewInit, OnDestroy { this.operationAccepted.emit(); } + /** + * Puts the modal window in the state in which it is waiting for confirmation. + * @param newData New configuration for the modal window. + */ + showAsking(newData: ConfirmationData | null) { + if (newData) { + this.data = newData; + } + + this.state = ConfirmationStates.Asking; + this.confirmButton.reset(); + this.disableDismiss = false; + this.dialogRef.disableClose = this.disableDismiss; + + if (this.cancelButton) { + this.cancelButton.showEnabled(); + } + } + showProcessing() { this.state = ConfirmationStates.Processing; this.disableDismiss = true; @@ -112,8 +131,12 @@ export class ConfirmationComponent implements AfterViewInit, OnDestroy { * @param newTitle New title for the modal window. * @param newText New main text for the modal window. */ - showDone(newTitle: string, newText: string) { - this.doneTitle = newTitle; + showDone(newTitle: string | null, newText: string) { + if (newTitle) { + this.doneTitle = newTitle; + } else { + this.doneTitle = this.data.headerText; + } this.doneText = newText; this.confirmButton.reset(); diff --git a/static/skywire-manager-src/src/app/components/pages/node/actions/actions.component.ts b/static/skywire-manager-src/src/app/components/pages/node/actions/actions.component.ts index 3dccfd6c93..6df9a24ea3 100644 --- a/static/skywire-manager-src/src/app/components/pages/node/actions/actions.component.ts +++ b/static/skywire-manager-src/src/app/components/pages/node/actions/actions.component.ts @@ -1,10 +1,9 @@ import { Component, AfterViewInit, OnDestroy, Input } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; +import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { Subscription } from 'rxjs'; import { ConfigurationComponent } from './configuration/configuration.component'; -import { UpdateNodeComponent } from './update-node/update-node.component'; import { BasicTerminalComponent } from './basic-terminal/basic-terminal.component'; import { SnackbarService } from '../../../../services/snackbar.service'; import { NodeComponent } from '../node.component'; @@ -12,10 +11,12 @@ import { SidenavService } from 'src/app/services/sidenav.service'; import { Node } from '../../../../app.datatypes'; import GeneralUtils from 'src/app/utils/generalUtils'; import { NodeService } from 'src/app/services/node.service'; -import { TerminalComponent } from './terminal/terminal.component'; import { OperationError } from 'src/app/utils/operation-error'; import { processServiceError } from 'src/app/utils/errors'; import { SelectableOption, SelectOptionComponent } from 'src/app/components/layout/select-option/select-option.component'; +import { ConfirmationData, ConfirmationComponent } from 'src/app/components/layout/confirmation/confirmation.component'; +import { AppConfig } from 'src/app/app.config'; +import { TranslateService } from '@ngx-translate/core'; /** * Component for making the options of the left bar of the nodes page to appear. It does not @@ -42,6 +43,7 @@ export class ActionsComponent implements AfterViewInit, OnDestroy { private menuSubscription: Subscription; private nodeSubscription: Subscription; private rebootSubscription: Subscription; + private updateSubscription: Subscription; constructor( private dialog: MatDialog, @@ -49,6 +51,7 @@ export class ActionsComponent implements AfterViewInit, OnDestroy { private snackbarService: SnackbarService, private sidenavService: SidenavService, private nodeService: NodeService, + private translateService: TranslateService, ) { } ngAfterViewInit() { @@ -116,6 +119,12 @@ export class ActionsComponent implements AfterViewInit, OnDestroy { if (this.menuSubscription) { this.menuSubscription.unsubscribe(); } + if (this.rebootSubscription) { + this.rebootSubscription.unsubscribe(); + } + if (this.updateSubscription) { + this.updateSubscription.unsubscribe(); + } } reboot() { @@ -136,14 +145,65 @@ export class ActionsComponent implements AfterViewInit, OnDestroy { } update() { - const confirmationDialog = GeneralUtils.createConfirmationDialog(this.dialog, 'actions.update.confirmation'); + // Configuration for the confirmation modal window used as the main UI element for the + // updating process. + const confirmationData: ConfirmationData = { + text: 'actions.update.processing', + headerText: 'actions.update.title', + confirmButtonText: 'actions.update.processing-button', + disableDismiss: true, + }; + + // Show the confirmation window in a "loading" state while checking if there are updates. + const config = new MatDialogConfig(); + config.data = confirmationData; + config.autoFocus = false; + config.width = AppConfig.smallModalWidth; + const confirmationDialog = this.dialog.open(ConfirmationComponent, config); + setTimeout(() => confirmationDialog.componentInstance.showProcessing()); + + // Check if there is an update available. + this.updateSubscription = this.nodeService.checkUpdate(NodeComponent.getCurrentNodeKey()).subscribe(response => { + if (response && response.available) { + // New configuration for asking for confirmation. + const newText = this.translateService.instant('actions.update.update-available', + { currentVersion: response.current_version, newVersion: response.available_version } + ); + const newConfirmationData: ConfirmationData = { + text: newText, + headerText: 'actions.update.title', + confirmButtonText: 'actions.update.install', + cancelButtonText: 'common.cancel', + }; + + // Ask for confirmation. + confirmationDialog.componentInstance.showAsking(newConfirmationData); + } else if (response) { + // Inform that there are no updates available. + const newText = this.translateService.instant('actions.update.no-update', { version: response.current_version }); + confirmationDialog.componentInstance.showDone(null, newText); + } else { + // Inform that there was an error. + confirmationDialog.componentInstance.showDone('confirmation.error-header-text', 'common.operation-error'); + } + }, (err: OperationError) => { + err = processServiceError(err); + confirmationDialog.componentInstance.showDone('confirmation.error-header-text', err.translatableErrorMsg); + }); + + // React if the user confirm the update. confirmationDialog.componentInstance.operationAccepted.subscribe(() => { confirmationDialog.componentInstance.showProcessing(); - this.rebootSubscription = this.nodeService.update(NodeComponent.getCurrentNodeKey()).subscribe(() => { - this.snackbarService.showDone('actions.update.done'); - confirmationDialog.close(); + // Update the visor. + this.updateSubscription = this.nodeService.update(NodeComponent.getCurrentNodeKey()).subscribe(response => { + if (response && response.updated) { + this.snackbarService.showDone('actions.update.done'); + confirmationDialog.close(); + } else { + confirmationDialog.componentInstance.showDone('confirmation.error-header-text', 'actions.update.update-error'); + } }, (err: OperationError) => { err = processServiceError(err); diff --git a/static/skywire-manager-src/src/app/components/pages/node/actions/update-node/update-node.component.ts b/static/skywire-manager-src/src/app/components/pages/node/actions/update-node/update-node.component.ts index 1814c84673..3b5a8e3365 100644 --- a/static/skywire-manager-src/src/app/components/pages/node/actions/update-node/update-node.component.ts +++ b/static/skywire-manager-src/src/app/components/pages/node/actions/update-node/update-node.component.ts @@ -1,3 +1,6 @@ +// NOTE: The updater has been implemented without using this component, so it could be +// removed soon. + import { Component, OnInit } from '@angular/core'; import {NodeService} from '../../../../../services/node.service'; import { MatDialogRef, MatDialog, MatDialogConfig } from '@angular/material/dialog'; diff --git a/static/skywire-manager-src/src/app/services/node.service.ts b/static/skywire-manager-src/src/app/services/node.service.ts index 56e693090f..4ca6a54f39 100644 --- a/static/skywire-manager-src/src/app/services/node.service.ts +++ b/static/skywire-manager-src/src/app/services/node.service.ts @@ -138,6 +138,13 @@ export class NodeService { return this.apiService.get(`visors/${nodeKey}/restart`).pipe(); } + /** + * Checks if there are updates available for a node. + */ + checkUpdate(nodeKey: string): Observable { + return this.apiService.get(`visors/${nodeKey}/update/available`).pipe(); + } + /** * Updates a node. */ diff --git a/static/skywire-manager-src/src/assets/i18n/en.json b/static/skywire-manager-src/src/assets/i18n/en.json index e6297f05f1..e87a548089 100644 --- a/static/skywire-manager-src/src/assets/i18n/en.json +++ b/static/skywire-manager-src/src/assets/i18n/en.json @@ -2,6 +2,7 @@ "common": { "save": "Save", "edit": "Edit", + "cancel": "Cancel", "node-key": "Node Key", "app-key": "App Key", "discovery": "Discovery", @@ -172,14 +173,14 @@ "error": "Unexpected error while trying to execute the command." }, "update": { - "title": "Check node update", - "no-update": "Currently, there is no update for the node.", - "update-available": "There is an update available for the node. Click the 'Install update' button to continue.", - "update-error": "Could not install node update. Please, try again later.", - "install": "Install update", - "update-success": "Node updated successfully, reboot the node and refresh this site to apply changes.", - "confirmation": "Are you sure you want to update the visor?", - "done": "The visor is being updated." + "title": "Update", + "processing": "Looking for updates...", + "processing-button": "Please wait", + "no-update": "Currently, there is no update for the visor. The currently installed version is {{ version }}.", + "update-available": "There is an update available for the visor. Click the 'Install update' button to continue. The currently installed version is {{ currentVersion }} and the new version is {{ newVersion }}.", + "done": "The visor is being updated.", + "update-error": "Could not install the update. Please, try again later.", + "install": "Install update" } }, diff --git a/static/skywire-manager-src/src/assets/i18n/es.json b/static/skywire-manager-src/src/assets/i18n/es.json index b181dbade9..44881deff0 100644 --- a/static/skywire-manager-src/src/assets/i18n/es.json +++ b/static/skywire-manager-src/src/assets/i18n/es.json @@ -2,6 +2,7 @@ "common": { "save": "Guardar", "edit": "Editar", + "cancel": "Cancelar", "node-key": "Llave del Nodo", "app-key": "Llave de la App", "discovery": "Discovery", @@ -172,14 +173,14 @@ "error": "Error inesperado mientras se intentaba ejecutar el comando." }, "update": { - "title": "Revisar actualización del nodo", - "no-update": "Actualmente no hay actualizaciones para el nodo.", - "update-available": "Hay una actualización disponible para el nodo. Presione el botón 'Instalar actualización' para continuar.", - "update-error": "No se pudo instalar la actualización del nodo. Por favor inténtelo nuevamente luego.", - "install": "Instalar actualización", - "update-success": "Nodo actualizado exitosamente, reinicie el nodo y refresque este sitio para aplicar los cambios.", - "confirmation": "¿Seguro que desea actualizar el visor?", - "done": "El visor está siendo actualizado." + "title": "Actualizar", + "processing": "Buscando actualizaciones...", + "processing-button": "Por favor espere", + "no-update": "Actualmente no hay actualizaciones para el visor. Actualmente está instalada la versión {{ version }}.", + "update-available": "Hay una actualización disponible para el visor. Presione el botón 'Instalar actualización' para continuar. Actualmente está instalada la versión {{ currentVersion }} y la nueva versión es la {{ newVersion }}.", + "done": "El visor está siendo actualizado.", + "update-error": "No se pudo instalar la actualización. Por favor inténtelo nuevamente luego.", + "install": "Instalar actualización" } }, diff --git a/static/skywire-manager-src/src/assets/i18n/es_base.json b/static/skywire-manager-src/src/assets/i18n/es_base.json index e6297f05f1..e87a548089 100644 --- a/static/skywire-manager-src/src/assets/i18n/es_base.json +++ b/static/skywire-manager-src/src/assets/i18n/es_base.json @@ -2,6 +2,7 @@ "common": { "save": "Save", "edit": "Edit", + "cancel": "Cancel", "node-key": "Node Key", "app-key": "App Key", "discovery": "Discovery", @@ -172,14 +173,14 @@ "error": "Unexpected error while trying to execute the command." }, "update": { - "title": "Check node update", - "no-update": "Currently, there is no update for the node.", - "update-available": "There is an update available for the node. Click the 'Install update' button to continue.", - "update-error": "Could not install node update. Please, try again later.", - "install": "Install update", - "update-success": "Node updated successfully, reboot the node and refresh this site to apply changes.", - "confirmation": "Are you sure you want to update the visor?", - "done": "The visor is being updated." + "title": "Update", + "processing": "Looking for updates...", + "processing-button": "Please wait", + "no-update": "Currently, there is no update for the visor. The currently installed version is {{ version }}.", + "update-available": "There is an update available for the visor. Click the 'Install update' button to continue. The currently installed version is {{ currentVersion }} and the new version is {{ newVersion }}.", + "done": "The visor is being updated.", + "update-error": "Could not install the update. Please, try again later.", + "install": "Install update" } },