+
+
+
+
+ {{ 'update.processing' | translate }}
+
+
+ {{ errorText | translate }}
+
+
+ {{ (data.length === 1 ? 'update.no-update' : 'update.no-updates') | translate }}
+
+
+
+
+
+ - {{ currentNodeVersion ? currentNodeVersion : ('common.unknown' | translate) }}
+
+
+
+ 0">
+
+ {{ 'update.already-updating' | translate }}
+
+
+
+ - {{ nodesToUpdate[index].label }}
+
+
+
+
+
+
+ {{ updateAvailableText | translate:{number: nodesForUpdatesFound} }}
+
+
+
+ - {{ 'update.version-change' | translate:update }}
+
+
+
+ {{ 'update.update-instructions' | translate }}
+
+
+
+
+
+
+
+ {{ 'update.updating' | translate }}
+
+
+
+
+
+
+ - {{ node.label }}
+ : {{ node.updateProgressInfo.rawMsg }}
+ ({{ 'update.finished' | translate }})
+
+
+
+
+
+
+ {{ node.label }}
+
+
+
+
+
+
+ {{ 'update.downloaded-file-name-prefix' | translate }} {{ node.updateProgressInfo.fileName }}
+ ({{ node.updateProgressInfo.progress }}%)
+
+ {{ 'update.speed-prefix' | translate }} {{ node.updateProgressInfo.speed }}
+
+ {{ 'update.time-downloading-prefix' | translate }} {{ node.updateProgressInfo.elapsedTime }}
+ /
+ {{ 'update.time-left-prefix' | translate }} {{ node.updateProgressInfo.remainingTime }}
+
+
+ {{ 'update.finished' | translate }}
+
+
+
+
+
+ - {{ node.label }}: {{ node.updateProgressInfo.errorMsg }}
+
+
+
+
+
+
+
+
+
diff --git a/static/skywire-manager-src/src/app/components/layout/update/update.component.scss b/static/skywire-manager-src/src/app/components/layout/update/update.component.scss
new file mode 100644
index 0000000000..5ed87fa701
--- /dev/null
+++ b/static/skywire-manager-src/src/app/components/layout/update/update.component.scss
@@ -0,0 +1,56 @@
+@import 'variables';
+
+.text-container {
+ word-break: break-word;
+}
+
+.list-container {
+ font-size: 14px;
+ margin: 10px;
+ color: $blue-medium;
+ word-break: break-word;
+
+ .details {
+ color: $light-gray;
+ }
+}
+
+.buttons {
+ margin-top: 15px;
+ text-align: right;
+
+ app-button {
+ margin-left: 5px;
+ }
+}
+
+.progress-container {
+ margin: 10px 0;
+
+ .name {
+ font-size: $font-size-mini;
+ color: $blue-medium;
+ }
+
+ ::ng-deep {
+ .mat-progress-bar-fill::after {
+ background-color: $blue-medium !important;
+ }
+ }
+
+ .details {
+ font-size: $font-size-mini;
+ text-align: right;
+ color: $light-gray;
+ }
+}
+
+.closed-indication {
+ color: $yellow;
+}
+
+.loading-indicator {
+ display: inline-block;
+ position: relative;
+ top: 2px;
+}
diff --git a/static/skywire-manager-src/src/app/components/layout/update/update.component.ts b/static/skywire-manager-src/src/app/components/layout/update/update.component.ts
new file mode 100644
index 0000000000..7bca396495
--- /dev/null
+++ b/static/skywire-manager-src/src/app/components/layout/update/update.component.ts
@@ -0,0 +1,460 @@
+import { Component, Inject, OnDestroy, AfterViewInit, ChangeDetectorRef } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA, MatDialog, MatDialogConfig } from '@angular/material/dialog';
+import { TranslateService } from '@ngx-translate/core';
+import { Subscription, forkJoin, interval } from 'rxjs';
+
+import { AppConfig } from 'src/app/app.config';
+import { NodeService } from 'src/app/services/node.service';
+import { StorageService } from 'src/app/services/storage.service';
+import { OperationError } from 'src/app/utils/operation-error';
+import { processServiceError } from 'src/app/utils/errors';
+
+/**
+ * States of the modal window.
+ */
+enum UpdatingStates {
+ /**
+ * Looking for updates.
+ */
+ InitialProcessing = 'InitialProcessing',
+ /**
+ * If no update was found.
+ */
+ NoUpdatesFound = 'NoUpdatesFound',
+ /**
+ * Showing the list of updates found and asking for confirmation before installing them.
+ */
+ Asking = 'Asking',
+ /**
+ * Installing the updates.
+ */
+ Updating = 'Updating',
+ /**
+ * Showing an error msg. Operation cancelled.
+ */
+ Error = 'Error',
+}
+
+/**
+ * Data about a node to update.
+ */
+export interface NodeData {
+ key: string;
+ label: string;
+}
+
+/**
+ * Extended data about a node to update, for internal use.
+ */
+interface NodeToUpdate extends NodeData {
+ /**
+ * If there is an update for the node or it was detected as being updated, so the update
+ * function must be called for it.
+ */
+ update: boolean;
+ /**
+ * Info about the current state of the update procedure.
+ */
+ updateProgressInfo: UpdateProgressInfo;
+}
+
+/**
+ * Info about the current state of the update procedure of a node.
+ */
+export class UpdateProgressInfo {
+ /**
+ * Error found while updating. If it has a valid value, the whole procedure must be
+ * considered as failed.
+ */
+ errorMsg = '';
+ /**
+ * Raw progress text obtained from the backend.
+ */
+ rawMsg = '';
+ /**
+ * If it was posible to parse the raw progress text obtained from the backend and
+ * populate the rest of the vars.
+ */
+ dataParsed = false;
+ /**
+ * Name of the file currently being downloaded (only is dataParsed === true).
+ */
+ fileName = '';
+ /**
+ * Progress downloading the file, in percentage (only is dataParsed === true).
+ */
+ progress = 100;
+ /**
+ * Current download speed (only is dataParsed === true).
+ */
+ speed = '';
+ /**
+ * Time since starting to download the file (only is dataParsed === true).
+ */
+ elapsedTime = '';
+ /**
+ * Expected time for finishing to download the file (only is dataParsed === true).
+ */
+ remainingTime = '';
+ /**
+ * If true, the connection with the backend for getting progress updates has been clo0sed.
+ */
+ closed = false;
+}
+
+/**
+ * Data about an update found.
+ */
+interface UpdateVersion {
+ currentVersion: string;
+ newVersion: string;
+}
+
+/**
+ * Modal window used for updating a list of nodes.
+ */
+@Component({
+ selector: 'app-update',
+ templateUrl: './update.component.html',
+ styleUrls: ['./update.component.scss'],
+})
+export class UpdateComponent implements AfterViewInit, OnDestroy {
+ // Current state of the window.
+ state = UpdatingStates.InitialProcessing;
+
+ // Text to show in the cancel button.
+ cancelButtonText = 'common.cancel';
+ // Text to show in the confirm button.
+ confirmButtonText: string;
+ // Error msg to show if the current state is UpdatingStates.Error.
+ errorText: string;
+ // If it was requested to update only one node and no updates were found, this var contains
+ // the current version of the node.
+ currentNodeVersion: string;
+
+ // List with the names of all updates found for the nodes, without repeated values.
+ updatesFound: UpdateVersion[];
+ // List with all the nodes that should be updated. It includes all requested nodes, so it
+ // may include nodes without updates available and nodes which are already being updated.
+ nodesToUpdate: NodeToUpdate[];
+ // List with the indexes, inside nodesToUpdate, of all nodes which were detected as already
+ // being updated.
+ indexesAlreadyBeingUpdated: number[] = [];
+ // How many nodes inside nodesToUpdate have updates available and are not currently
+ // being updated.
+ nodesForUpdatesFound: number;
+
+ updatingStates = UpdatingStates;
+
+ private subscription: Subscription;
+ private progressSubscriptions: Subscription[];
+ private uiUpdateSubscription: Subscription;
+
+ /**
+ * Opens the modal window. Please use this function instead of opening the window "by hand".
+ */
+ /**
+ * Opens the modal window. Please use this function instead of opening the window "by hand".
+ * @param nodes Nodes to update.
+ */
+ public static openDialog(dialog: MatDialog, nodes: NodeData[]): MatDialogRef