]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: cluster upgrade progress UI 52919/head
authoravanthakkar <avanjohn@gmail.com>
Wed, 9 Aug 2023 08:23:09 +0000 (13:53 +0530)
committerNizamudeen A <nia@redhat.com>
Mon, 14 Aug 2023 13:47:43 +0000 (19:17 +0530)
Fixes: https://tracker.ceph.com/issues/62343
Signed-off-by: avanthakkar <avanjohn@gmail.com>
Co-Authored-By: Nizamudeen A <nia@redhat.com>
16 files changed:
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/upgrade.interface.ts

index d57fb9855a77a3714c908f8e82ed01189bdf05a7..43ebb5964d3117b0b72d23fb45f7cbea5cd3216a 100644 (file)
@@ -47,6 +47,7 @@ import { ModuleStatusGuardService } from './shared/services/module-status-guard.
 import { NoSsoGuardService } from './shared/services/no-sso-guard.service';
 import { UpgradeComponent } from './ceph/cluster/upgrade/upgrade.component';
 import { CephfsVolumeFormComponent } from './ceph/cephfs/cephfs-form/cephfs-form.component';
+import { UpgradeProgressComponent } from './ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component';
 
 @Injectable()
 export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver {
@@ -287,7 +288,6 @@ const routes: Routes = [
       {
         path: 'upgrade',
         canActivate: [ModuleStatusGuardService],
-        component: UpgradeComponent,
         data: {
           moduleStatusGuardConfig: {
             uiApiPath: 'orchestrator',
@@ -298,7 +298,18 @@ const routes: Routes = [
             header: 'Orchestrator is not available'
           },
           breadcrumbs: 'Cluster/Upgrade'
-        }
+        },
+        children: [
+          {
+            path: '',
+            component: UpgradeComponent
+          },
+          {
+            path: 'progress',
+            component: UpgradeProgressComponent,
+            data: { breadcrumbs: 'Progress' }
+          }
+        ]
       },
       {
         path: 'perf_counters/:type/:id',
index c94244e0682cdd401cc06d352439bc75db7ec166..74657ec4010f0ef58c20cf9a9b0732ab86e9cbc8 100644 (file)
@@ -10,6 +10,7 @@ import {
   NgbDropdownModule,
   NgbNavModule,
   NgbPopoverModule,
+  NgbProgressbarModule,
   NgbTimepickerModule,
   NgbTooltipModule,
   NgbTypeaheadModule
@@ -59,6 +60,7 @@ import { ServicesComponent } from './services/services.component';
 import { TelemetryComponent } from './telemetry/telemetry.component';
 import { UpgradeComponent } from './upgrade/upgrade.component';
 import { UpgradeStartModalComponent } from './upgrade/upgrade-form/upgrade-start-modal.component';
+import { UpgradeProgressComponent } from './upgrade/upgrade-progress/upgrade-progress.component';
 
 @NgModule({
   imports: [
@@ -78,7 +80,8 @@ import { UpgradeStartModalComponent } from './upgrade/upgrade-form/upgrade-start
     NgbDatepickerModule,
     NgbPopoverModule,
     NgbDropdownModule,
-    NgxPipeFunctionModule
+    NgxPipeFunctionModule,
+    NgbProgressbarModule
   ],
   declarations: [
     HostsComponent,
@@ -120,7 +123,8 @@ import { UpgradeStartModalComponent } from './upgrade/upgrade-form/upgrade-start
     CreateClusterComponent,
     CreateClusterReviewComponent,
     UpgradeComponent,
-    UpgradeStartModalComponent
+    UpgradeStartModalComponent,
+    UpgradeProgressComponent
   ],
   providers: [NgbActiveModal]
 })
index be12716f0e660fe22d3eb077a3c475148d7ba2ec..27b7bc2e8169c94baf3a5292a68f124caed19764 100644 (file)
           [formGroup]="upgradeForm"
           novalidate>
       <div class="modal-body">
+        <cd-alert-panel type="warning"
+                        spacingClass="mb-3"
+                        *ngIf="showImageField"
+                        i18n>Make sure to put the correct image. Passing an incorrect image can lead the cluster into an undesired state.</cd-alert-panel>
         <div class="form-group row">
-          <label class="cd-col-form-label required"
+          <label class="cd-col-form-label"
+                 [ngClass]="{'required': !showImageField}"
                  for="availableVersions"
                  i18n>New Version</label>
           <div class="cd-col-form-input">
                   i18n>This field is required!</span>
           </div>
         </div>
+
+        <div class="form-group row">
+          <div class="cd-col-form-offset">
+            <div class="custom-control custom-checkbox">
+              <input type="checkbox"
+                     class="custom-control-input"
+                     id="useImage"
+                     name="useImage"
+                     formControlName="useImage"
+                     (click)="useImage()">
+              <label class="custom-control-label"
+                     for="useImage"
+                     i18n>Use image</label>
+            </div>
+          </div>
+        </div>
+
+        <!-- Custom image name input-->
+        <div class="form-group row"
+             *ngIf="showImageField">
+          <label class="cd-col-form-label required"
+                 for="customImageName"
+                 i18n>Image</label>
+          <div class="cd-col-form-input">
+            <input type="text"
+                   class="form-control"
+                   id="customImageName"
+                   name="customImageName"
+                   formControlName="customImageName">
+            <span class="invalid-feedback"
+                  *ngIf="upgradeForm.showError('customImageName', formDir, 'required')"
+                  i18n>This field is required!</span>
+          </div>
+        </div>
       </div>
 
       <div class="modal-footer">
-        <cd-form-button-panel *ngIf="versions"
-                              (submitActionEvent)="startUpgrade()"
+        <cd-form-button-panel (submitActionEvent)="startUpgrade()"
                               [form]="upgradeForm"
                               [submitText]="actionLabels.START_UPGRADE"></cd-form-button-panel>
       </div>
index a6232ee082aad003d5ed5292ac267ec214ea26ff..1fe7ffbf809544b8df9940149f679e11fc7f7076 100644 (file)
@@ -7,13 +7,14 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { NO_ERRORS_SCHEMA } from '@angular/core';
 import { SharedModule } from '~/app/shared/shared.module';
 import { ToastrModule } from 'ngx-toastr';
+import { RouterTestingModule } from '@angular/router/testing';
 
 describe('UpgradeComponent', () => {
   let component: UpgradeComponent;
   let fixture: ComponentFixture<UpgradeComponent>;
 
   configureTestBed({
-    imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot()],
+    imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot(), RouterTestingModule],
     schemas: [NO_ERRORS_SCHEMA],
     declarations: [UpgradeComponent],
     providers: [UpgradeService]
index c601b776f5672285f2b7422afc35ba2160996944..6ea38bfc323a1c92701c2de5ea219bc0260fe743 100644 (file)
@@ -26,6 +26,8 @@ export class UpgradeStartModalComponent implements OnInit {
   icons = Icons;
   versions: string[];
 
+  showImageField = false;
+
   constructor(
     public actionLabels: ActionLabelsI18n,
     private authStorageService: AuthStorageService,
@@ -38,12 +40,16 @@ export class UpgradeStartModalComponent implements OnInit {
 
   ngOnInit() {
     this.upgradeForm = new CdFormGroup({
-      availableVersions: new FormControl(null, [Validators.required])
+      availableVersions: new FormControl(null, [Validators.required]),
+      useImage: new FormControl(false),
+      customImageName: new FormControl(null)
     });
   }
 
   startUpgrade() {
-    this.upgradeService.start(this.upgradeForm.getValue('availableVersions')).subscribe({
+    const version = this.upgradeForm.getValue('availableVersions');
+    const image = this.upgradeForm.getValue('customImageName');
+    this.upgradeService.start(version, image).subscribe({
       next: () => {
         this.notificationService.show(
           NotificationType.success,
@@ -63,4 +69,24 @@ export class UpgradeStartModalComponent implements OnInit {
       }
     });
   }
+
+  useImage() {
+    this.showImageField = !this.showImageField;
+    const availableVersionsControl = this.upgradeForm.get('availableVersions');
+    const customImageNameControl = this.upgradeForm.get('customImageName');
+
+    if (this.showImageField) {
+      availableVersionsControl.disable();
+      availableVersionsControl.clearValidators();
+
+      customImageNameControl.setValidators(Validators.required);
+      customImageNameControl.updateValueAndValidity();
+    } else {
+      availableVersionsControl.enable();
+      availableVersionsControl.setValidators(Validators.required);
+      availableVersionsControl.updateValueAndValidity();
+
+      customImageNameControl.clearValidators();
+    }
+  }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.html
new file mode 100644 (file)
index 0000000..c683eee
--- /dev/null
@@ -0,0 +1,89 @@
+<div class="d-flex flex-column justify-content-center align-items-center bold"
+     *ngIf="upgradeStatus$ | async as upgradeStatus">
+  <ng-container *ngIf="upgradeStatus.in_progress && !upgradeStatus.is_paused; else upgradePaused">
+    <h3 class="text-center"
+        i18n>
+    <i [ngClass]="[icons.large, icons.spin, icons.spinner]"></i>
+  </h3>
+
+  <h3 class="text-center mt-2">
+    {{ executingTask?.description }}
+  </h3>
+
+  <h5 class="text-center mt-3"
+      i18n>{{ upgradeStatus.which }}</h5>
+  </ng-container>
+
+  <div class="w-50 row h-100 d-flex justify-content-center align-items-center mt-4">
+    <div class="text-center w-75">
+      <ng-container *ngIf="upgradeStatus.services_complete.length > 0">
+        Finished upgrading:
+        <span class="text-success">
+          {{ upgradeStatus.services_complete }}
+        </span>
+      </ng-container>
+      <div class="mt-2">
+        <ngb-progressbar type="info"
+                         [value]="executingTask?.progress"
+                         [striped]="true"
+                         [animated]="!upgradeStatus.is_paused"></ngb-progressbar>
+      </div>
+
+    <p class="card-text text-muted">
+      <span class="float-end">
+        {{ executingTask?.progress || 0 }} %
+      </span>
+    </p>
+    </div>
+    <h4 class="text-center m-2"
+        i18n>{{ upgradeStatus.progress}}</h4>
+
+    <h5 *ngIf="upgradeStatus.in_progress"
+        class="text-center mt-2"
+        i18n>
+    {{ upgradeStatus.message }}
+    </h5>
+
+    <div class="text-center mt-3">
+      <button class="btn btn-light"
+              aria-label="Go back"
+              routerLink="/upgrade"
+              i18n>Back</button>
+      <button *ngIf="upgradeStatus.in_progress && !upgradeStatus.is_paused"
+              (click)="pauseUpgrade()"
+              class="btn btn-light m-2"
+              aria-label="Pause Upgrade"
+              i18n>Pause</button>
+      <button *ngIf="upgradeStatus.in_progress && upgradeStatus.is_paused"
+              (click)="resumeUpgrade()"
+              class="btn btn-light m-2"
+              aria-label="Resume Upgrade"
+              i18n>Resume</button>
+      <button *ngIf="upgradeStatus.in_progress"
+              (click)="stopUpgradeModal()"
+              class="btn btn-danger"
+              aria-label="Stop Upgrade"
+              i18n>Stop</button>
+    </div>
+  </div>
+</div>
+
+<legend class="cd-header"
+        i18n>Cluster logs</legend>
+  <cd-logs [showAuditLogs]="false"
+           [showDaemonLogs]="false"
+           [showNavLinks]="false"
+           [showFilterTools]="false"
+           [showDownloadCopyButton]="false"
+           defaultTab="cluster-logs"
+           [scrollable]="true"></cd-logs>
+
+<ng-template #upgradePaused>
+  <h3 class="text-center mt-3">
+    <i [ngClass]="[icons.large, icons.spinner]"></i>
+  </h3>
+
+  <h3 class="text-center mt-3 mb-4">
+    {{ executingTask?.description }}
+  </h3>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.spec.ts
new file mode 100644 (file)
index 0000000..c4ec882
--- /dev/null
@@ -0,0 +1,30 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UpgradeProgressComponent } from './upgrade-progress.component';
+import { ToastrModule } from 'ngx-toastr';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { SharedModule } from '~/app/shared/shared.module';
+import { RouterTestingModule } from '@angular/router/testing';
+import { LogsComponent } from '../../logs/logs.component';
+
+describe('UpgradeProgressComponent', () => {
+  let component: UpgradeProgressComponent;
+  let fixture: ComponentFixture<UpgradeProgressComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [UpgradeProgressComponent, LogsComponent],
+      imports: [ToastrModule.forRoot(), HttpClientTestingModule, SharedModule, RouterTestingModule]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(UpgradeProgressComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-progress/upgrade-progress.component.ts
new file mode 100644 (file)
index 0000000..03bb6ed
--- /dev/null
@@ -0,0 +1,140 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+
+import { Observable, ReplaySubject, Subscription } from 'rxjs';
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { UpgradeService } from '~/app/shared/api/upgrade.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { shareReplay, switchMap, tap } from 'rxjs/operators';
+import { Router } from '@angular/router';
+import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
+import { UpgradeStatusInterface } from '~/app/shared/models/upgrade.interface';
+
+@Component({
+  selector: 'cd-upgrade-progress',
+  templateUrl: './upgrade-progress.component.html',
+  styleUrls: ['./upgrade-progress.component.scss']
+})
+export class UpgradeProgressComponent implements OnInit, OnDestroy {
+  permission: Permission;
+  icons = Icons;
+  modalRef: NgbModalRef;
+  interval = new Subscription();
+  executingTask: ExecutingTask;
+
+  upgradeStatus$: Observable<UpgradeStatusInterface>;
+  subject = new ReplaySubject<UpgradeStatusInterface>();
+
+  constructor(
+    private authStorageService: AuthStorageService,
+    private upgradeService: UpgradeService,
+    private notificationService: NotificationService,
+    private modalService: ModalService,
+    private summaryService: SummaryService,
+    private router: Router,
+    private refreshIntervalService: RefreshIntervalService
+  ) {
+    this.permission = this.authStorageService.getPermissions().configOpt;
+  }
+
+  ngOnInit() {
+    this.upgradeStatus$ = this.subject.pipe(
+      switchMap(() => this.upgradeService.status()),
+      tap((status: UpgradeStatusInterface) => {
+        if (!status.in_progress) {
+          this.router.navigate(['/upgrade']);
+        }
+      }),
+      shareReplay(1)
+    );
+
+    this.interval = this.refreshIntervalService.intervalData$.subscribe(() => {
+      this.fetchStatus();
+    });
+
+    this.summaryService.subscribe((summary) => {
+      this.executingTask = summary.executing_tasks.filter((tasks) =>
+        tasks.name.includes('progress/Upgrade')
+      )[0];
+    });
+  }
+
+  pauseUpgrade() {
+    this.upgradeService.pause().subscribe({
+      error: (error) => {
+        this.notificationService.show(
+          NotificationType.error,
+          $localize`Failed to pause the upgrade`,
+          error
+        );
+      },
+      complete: () => {
+        this.notificationService.show(NotificationType.success, $localize`The upgrade is paused`);
+        this.fetchStatus();
+      }
+    });
+  }
+
+  fetchStatus() {
+    this.subject.next();
+  }
+
+  resumeUpgrade(modal = false) {
+    this.upgradeService.resume().subscribe({
+      error: (error) => {
+        this.notificationService.show(
+          NotificationType.error,
+          $localize`Failed to resume the upgrade`,
+          error
+        );
+      },
+      complete: () => {
+        this.fetchStatus();
+        this.notificationService.show(NotificationType.success, $localize`Upgrade is resumed`);
+        if (modal) {
+          this.modalRef.close();
+        }
+      }
+    });
+  }
+
+  stopUpgradeModal() {
+    // pause the upgrade meanwhile we get stop confirmation from user
+    this.pauseUpgrade();
+    this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+      itemDescription: 'Upgrade',
+      actionDescription: 'stop',
+      submitAction: () => this.stopUpgrade(),
+      callBackAtionObservable: () => this.resumeUpgrade(true)
+    });
+  }
+
+  stopUpgrade() {
+    this.modalRef.close();
+    this.upgradeService.stop().subscribe({
+      error: (error) => {
+        this.notificationService.show(
+          NotificationType.error,
+          $localize`Failed to stop the upgrade`,
+          error
+        );
+      },
+      complete: () => {
+        this.notificationService.show(NotificationType.success, $localize`The upgrade is stopped`);
+        this.router.navigate(['/upgrade']);
+      }
+    });
+  }
+
+  ngOnDestroy() {
+    this.interval?.unsubscribe();
+  }
+}
index c91af1cdcce05b78c78dca80539616d1c2d402c2..e5252cbe4df7f15a6d60ae52667ada662a0aeb38 100644 (file)
@@ -6,24 +6,26 @@
              i18n-cardTitle
              aria-label="New Version"
              i18n-aria-label
-             id="newVersionAvailable">
-      <div class="d-flex flex-column justify-content-center align-items-center"
-           *ngIf="info$ | async as info; else checkingForUpgradeStatus">
-        <ng-container *ngIf="info.versions.length > 0; else noUpgradesAvailable">
-          <div i18n-ngbTooltip
-               [ngbTooltip]="(healthData.mgr_map | mgrSummary).total <= 1 ? 'To upgrade, you need minimum 2 mgr daemons.' : ''">
-            <button class="btn btn-accent mt-2"
-                    id="upgrade"
-                    aria-label="Upgrade now"
-                    (click)="upgradeNow(info.versions[info.versions.length - 1])"
-                    [disabled]="(healthData.mgr_map | mgrSummary).total <= 1"
-                    i18n>Upgrade to {{ info.versions[info.versions.length - 1] }}</button>
-          </div>
+             id="newVersionAvailable"
+             *ngIf="upgradeStatus$ | async as status">
+      <ng-container *ngIf="status.in_progress; else upgradeStatusTpl">
+        <div class="d-flex flex-column justify-content-center align-items-center mt-2">
+          <h5 i18n
+              *ngIf="status.is_paused; else inProgress">
+            <i [ngClass]="[icons.spinner]"></i>
+            Upgrade is paused {{executingTasks?.progress}}%</h5>
           <a class="mt-2 link-primary mb-2"
-             (click)="startUpgradeModal()"
-             i18n>Select another version...</a>
-        </ng-container>
-      </div>
+             routerLink="/upgrade/progress"
+             i18n>View Details...</a>
+        </div>
+
+        <ng-template #inProgress>
+          <h5 i18n>
+            <i [ngClass]="[icons.spin, icons.spinner]"></i>
+              Upgrade in progress {{executingTasks?.progress}}%
+          </h5>
+        </ng-template>
+      </ng-container>
     </cd-card>
 
     <cd-card class="col-sm-3 px-3 d-flex"
              [showDownloadCopyButton]="false"
              defaultTab="cluster-logs"
              [scrollable]="true"></cd-logs>
+
+
+    <ng-template #upgradeStatusTpl>
+      <div class="d-flex flex-column justify-content-center align-items-center"
+           *ngIf="info$ | async as info; else checkingForUpgradeStatus">
+        <ng-container *ngIf="info.versions.length > 0; else noUpgradesAvailable">
+          <div i18n-ngbTooltip
+               [ngbTooltip]="(healthData.mgr_map | mgrSummary).total <= 1 ? 'To upgrade, you need minimum 2 mgr daemons.' : ''">
+            <button class="btn btn-accent mt-2"
+                    id="upgrade"
+                    aria-label="Upgrade now"
+                    (click)="upgradeNow(info.versions[info.versions.length - 1])"
+                    [disabled]="(healthData.mgr_map | mgrSummary).total <= 1"
+                    i18n>Upgrade to {{ info.versions[info.versions.length - 1] }}</button>
+          </div>
+          <a class="mt-2 link-primary mb-2"
+             (click)="startUpgradeModal()"
+             i18n>Select another version...</a>
+        </ng-container>
+      </div>
+    </ng-template>
   </ng-container>
 </div>
 
     Failed to fetch registry informations
   </span>
 </ng-template>
+
+<ng-template #upgradeProgress>
+  <div class="d-flex flex-column justify-content-center align-items-center mt-2">
+    <h5 i18n>
+      <i [ngClass]="[icons.spin, icons.spinner]"></i>
+      Upgrade in progress {{executingTasks?.progress}}%</h5>
+    <a class="mt-2 link-primary mb-2"
+       routerLink="/upgrade/progress"
+       i18n>View Details...</a>
+  </div>
+</ng-template>
index 407a7da58e12de01a31d672ea0f831b94399dc9a..574d3c8bb4c87b878c02770b020fbd069d30abf6 100644 (file)
@@ -14,6 +14,7 @@ import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
 import { NO_ERRORS_SCHEMA } from '@angular/core';
 import { ToastrModule } from 'ngx-toastr';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { RouterTestingModule } from '@angular/router/testing';
 
 export class SummaryServiceMock {
   summaryDataSource = new BehaviorSubject({
@@ -33,6 +34,7 @@ describe('UpgradeComponent', () => {
   let fixture: ComponentFixture<UpgradeComponent>;
   let upgradeInfoSpy: jasmine.Spy;
   let getHealthSpy: jasmine.Spy;
+  let upgradeStatusSpy: jasmine.Spy;
 
   const healthPayload: Record<string, any> = {
     health: { status: 'HEALTH_OK' },
@@ -51,7 +53,13 @@ describe('UpgradeComponent', () => {
   };
 
   configureTestBed({
-    imports: [HttpClientTestingModule, SharedModule, NgbNavModule, ToastrModule.forRoot()],
+    imports: [
+      HttpClientTestingModule,
+      SharedModule,
+      NgbNavModule,
+      ToastrModule.forRoot(),
+      RouterTestingModule
+    ],
     declarations: [UpgradeComponent, LogsComponent],
     schemas: [NO_ERRORS_SCHEMA],
     providers: [UpgradeService, { provide: SummaryService, useClass: SummaryServiceMock }]
@@ -62,6 +70,7 @@ describe('UpgradeComponent', () => {
     component = fixture.componentInstance;
     upgradeInfoSpy = spyOn(TestBed.inject(UpgradeService), 'list').and.callFake(() => of(null));
     getHealthSpy = spyOn(TestBed.inject(HealthService), 'getMinimalHealth');
+    upgradeStatusSpy = spyOn(TestBed.inject(UpgradeService), 'status');
     getHealthSpy.and.returnValue(of(healthPayload));
     const upgradeInfoPayload = {
       image: 'quay.io/ceph-test/ceph',
@@ -69,6 +78,8 @@ describe('UpgradeComponent', () => {
       versions: ['18.1.0', '18.1.1', '18.1.2']
     };
     upgradeInfoSpy.and.returnValue(of(upgradeInfoPayload));
+    upgradeStatusSpy.and.returnValue(of({}));
+    component.fetchStatus();
     spyOn(TestBed.inject(AuthStorageService), 'getPermissions').and.callFake(() => ({
       configOpt: { read: true }
     }));
index a271a24202d72e479f37ebd8884b06c060ffe033..0f1f2318a5e0423abc7612b4efb29c62353b0666 100644 (file)
@@ -1,6 +1,6 @@
-import { Component, OnInit } from '@angular/core';
-import { Observable, of } from 'rxjs';
-import { catchError, publishReplay, refCount, tap } from 'rxjs/operators';
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Observable, ReplaySubject, Subscription, of } from 'rxjs';
+import { catchError, publishReplay, refCount, shareReplay, switchMap, tap } from 'rxjs/operators';
 import { DaemonService } from '~/app/shared/api/daemon.service';
 import { HealthService } from '~/app/shared/api/health.service';
 import { UpgradeService } from '~/app/shared/api/upgrade.service';
@@ -15,13 +15,16 @@ import { NotificationService } from '~/app/shared/services/notification.service'
 import { SummaryService } from '~/app/shared/services/summary.service';
 import { ModalService } from '~/app/shared/services/modal.service';
 import { UpgradeStartModalComponent } from './upgrade-form/upgrade-start-modal.component';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { Router } from '@angular/router';
+import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service';
 
 @Component({
   selector: 'cd-upgrade',
   templateUrl: './upgrade.component.html',
   styleUrls: ['./upgrade.component.scss']
 })
-export class UpgradeComponent implements OnInit {
+export class UpgradeComponent implements OnInit, OnDestroy {
   version: string;
   info$: Observable<UpgradeInfoInterface>;
   permission: Permission;
@@ -31,21 +34,33 @@ export class UpgradeComponent implements OnInit {
   modalRef: NgbModalRef;
   upgradableVersions: string[];
   errorMessage: string;
+  executingTasks: ExecutingTask;
+  interval = new Subscription();
 
   columns: CdTableColumn[] = [];
 
   icons = Icons;
 
+  upgradeStatus$: Observable<any>;
+  subject = new ReplaySubject<any>();
+
   constructor(
     private modalService: ModalService,
     private summaryService: SummaryService,
     private upgradeService: UpgradeService,
     private healthService: HealthService,
     private daemonService: DaemonService,
-    private notificationService: NotificationService
+    private notificationService: NotificationService,
+    private router: Router,
+    private refreshIntervalService: RefreshIntervalService
   ) {}
 
   ngOnInit(): void {
+    this.upgradeStatus$ = this.subject.pipe(
+      switchMap(() => this.upgradeService.status()),
+      shareReplay(1)
+    );
+
     this.columns = [
       {
         name: $localize`Daemon name`,
@@ -64,7 +79,15 @@ export class UpgradeComponent implements OnInit {
     this.summaryService.subscribe((summary) => {
       const version = summary.version.replace('ceph version ', '').split('-');
       this.version = version[0];
+      this.executingTasks = summary.executing_tasks.filter((tasks) =>
+        tasks.name.includes('progress/Upgrade')
+      )[0];
     });
+
+    this.interval = this.refreshIntervalService.intervalData$.subscribe(() => {
+      this.fetchStatus();
+    });
+
     this.info$ = this.upgradeService.list().pipe(
       tap((upgradeInfo: UpgradeInfoInterface) => (this.upgradableVersions = upgradeInfo.versions)),
       publishReplay(1),
@@ -80,6 +103,7 @@ export class UpgradeComponent implements OnInit {
         return of(null);
       })
     );
+
     this.healthData$ = this.healthService.getMinimalHealth();
     this.daemons$ = this.daemonService.list(this.upgradeService.upgradableServiceTypes);
     this.fsid$ = this.healthService.getClusterFsid();
@@ -91,6 +115,10 @@ export class UpgradeComponent implements OnInit {
     });
   }
 
+  fetchStatus() {
+    this.subject.next();
+  }
+
   upgradeNow(version: string) {
     this.upgradeService.start(version).subscribe({
       error: (error) => {
@@ -105,7 +133,13 @@ export class UpgradeComponent implements OnInit {
           NotificationType.success,
           $localize`Started upgrading the cluster`
         );
+        this.fetchStatus();
+        this.router.navigate(['/upgrade/progress']);
       }
     });
   }
+
+  ngOnDestroy() {
+    this.interval?.unsubscribe();
+  }
 }
index 8421fc57f37c96c3737a72128bb6136fb513ff7a..9aa25aa1614787c4e96e1db36bb89276d9adb92b 100644 (file)
@@ -3,7 +3,8 @@ import { Injectable } from '@angular/core';
 import { ApiClient } from './api-client';
 import { map } from 'rxjs/operators';
 import { SummaryService } from '../services/summary.service';
-import { UpgradeInfoInterface } from '../models/upgrade.interface';
+import { UpgradeInfoInterface, UpgradeStatusInterface } from '../models/upgrade.interface';
+import { Observable } from 'rxjs';
 
 @Injectable({
   providedIn: 'root'
@@ -55,7 +56,23 @@ export class UpgradeService extends ApiClient {
     return upgradeInfo;
   }
 
-  start(version: string) {
-    return this.http.post(`${this.baseURL}/start`, { version: version });
+  start(version?: string, image?: string) {
+    return this.http.post(`${this.baseURL}/start`, { image: image, version: version });
+  }
+
+  pause() {
+    return this.http.put(`${this.baseURL}/pause`, null);
+  }
+
+  resume() {
+    return this.http.put(`${this.baseURL}/resume`, null);
+  }
+
+  stop() {
+    return this.http.put(`${this.baseURL}/stop`, null);
+  }
+
+  status(): Observable<UpgradeStatusInterface> {
+    return this.http.get<UpgradeStatusInterface>(`${this.baseURL}/status`);
   }
 }
index 29b669b141f3bf5e78f2b179f80c5779d5e57300..01c0e2ca5ccd4cc06d3d1f28ed602521ca673ab7 100644 (file)
@@ -43,6 +43,7 @@
       </div>
       <div class="modal-footer">
         <cd-form-button-panel (submitActionEvent)="callSubmitAction()"
+                              (backActionEvent)="callBackAction()"
                               [form]="deletionForm"
                               [submitText]="(actionDescription | titlecase) + ' ' + itemDescription"></cd-form-button-panel>
       </div>
index 4c634f8ca2559b3be7423104f3c32ede94484eb2..b2f482db062d330e65ef094b0952ec07eb05affd 100644 (file)
@@ -18,7 +18,9 @@ export class CriticalConfirmationModalComponent implements OnInit {
   bodyTemplate: TemplateRef<any>;
   bodyContext: object;
   submitActionObservable: () => Observable<any>;
+  callBackAtionObservable: () => Observable<any>;
   submitAction: Function;
+  backAction: Function;
   deletionForm: CdFormGroup;
   itemDescription: 'entry';
   itemNames: string[];
@@ -53,6 +55,17 @@ export class CriticalConfirmationModalComponent implements OnInit {
     }
   }
 
+  callBackAction() {
+    if (this.callBackAtionObservable) {
+      this.callBackAtionObservable().subscribe({
+        error: this.stopLoadingSpinner.bind(this),
+        complete: this.hideModal.bind(this)
+      });
+    } else {
+      this.backAction();
+    }
+  }
+
   hideModal() {
     this.activeModal.close();
   }
index ada46bcd6b8d1e7925a591bd2a64a5361f148214..2a853d59a94282c881b01e17c8245f8a0a51c822 100644 (file)
@@ -3,3 +3,13 @@ export interface UpgradeInfoInterface {
   registry: string;
   versions: string[];
 }
+
+export interface UpgradeStatusInterface {
+  target_image: string;
+  in_progress: boolean;
+  which: string;
+  services_complete: string;
+  progress: string;
+  message: string;
+  is_paused: boolean;
+}