]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
import ceph quincy 17.2.4
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / block / rbd-list / rbd-list.component.ts
index 0ffb25695c8d2e5e3e54c93e42778ac2cb7764a5..983f39ed9be523e7ef53bad5d6f1841cc1a4f558 100644 (file)
@@ -2,18 +2,19 @@ import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
 
 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
 import _ from 'lodash';
+import { Observable, Subscriber } from 'rxjs';
 
 import { RbdService } from '~/app/shared/api/rbd.service';
 import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
-import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache';
+import { TableStatus } from '~/app/shared/classes/table-status';
 import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { TableComponent } from '~/app/shared/datatable/table/table.component';
 import { Icons } from '~/app/shared/enum/icons.enum';
-import { ViewCacheStatus } from '~/app/shared/enum/view-cache-status.enum';
 import { CdTableAction } from '~/app/shared/models/cd-table-action';
 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
 import { FinishedTask } from '~/app/shared/models/finished-task';
 import { ImageSpec } from '~/app/shared/models/image-spec';
@@ -22,10 +23,12 @@ import { Task } from '~/app/shared/models/task';
 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
 import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { CdTableServerSideService } from '~/app/shared/services/cd-table-server-side.service';
 import { ModalService } from '~/app/shared/services/modal.service';
 import { TaskListService } from '~/app/shared/services/task-list.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { RbdFormEditRequestModel } from '../rbd-form/rbd-form-edit-request.model';
 import { RbdParentModel } from '../rbd-form/rbd-parent.model';
 import { RbdTrashMoveModalComponent } from '../rbd-trash-move-modal/rbd-trash-move-modal.component';
 import { RBDImageFormat, RbdModel } from './rbd-model';
@@ -50,6 +53,8 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
   parentTpl: TemplateRef<any>;
   @ViewChild('nameTpl')
   nameTpl: TemplateRef<any>;
+  @ViewChild('mirroringTpl', { static: true })
+  mirroringTpl: TemplateRef<any>;
   @ViewChild('flattenTpl', { static: true })
   flattenTpl: TemplateRef<any>;
   @ViewChild('deleteTpl', { static: true })
@@ -66,10 +71,11 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
   images: any;
   columns: CdTableColumn[];
   retries: number;
-  tableStatus = new TableStatusViewCache();
+  tableStatus = new TableStatus('light');
   selection = new CdTableSelection();
   icons = Icons;
-
+  count = 0;
+  private tableContext: CdTableFetchDataContext = null;
   modalRef: NgbModalRef;
 
   builders = {
@@ -89,6 +95,7 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
         metadata['dest_image_name']
       )
   };
+  remove_scheduling: boolean;
 
   private createRbdFromTaskImageSpec(imageSpecStr: string): RbdModel {
     const imageSpec = ImageSpec.fromString(imageSpecStr);
@@ -148,6 +155,13 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
       name: this.actionLabels.DELETE,
       disable: (selection: CdTableSelection) => this.getDeleteDisableDesc(selection)
     };
+    const resyncAction: CdTableAction = {
+      permission: 'update',
+      icon: Icons.refresh,
+      click: () => this.resyncRbdModal(),
+      name: this.actionLabels.RESYNC,
+      disable: (selection: CdTableSelection) => this.getResyncDisableDesc(selection)
+    };
     const copyAction: CdTableAction = {
       permission: 'create',
       canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection,
@@ -180,13 +194,41 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
         this.getInvalidNameDisable(selection) ||
         selection.first().image_format === RBDImageFormat.V1
     };
+    const removeSchedulingAction: CdTableAction = {
+      permission: 'update',
+      icon: Icons.edit,
+      click: () => this.removeSchedulingModal(),
+      name: this.actionLabels.REMOVE_SCHEDULING,
+      disable: (selection: CdTableSelection) =>
+        this.getRemovingStatusDesc(selection) ||
+        this.getInvalidNameDisable(selection) ||
+        selection.first().schedule_info === undefined
+    };
+    const promoteAction: CdTableAction = {
+      permission: 'update',
+      icon: Icons.edit,
+      click: () => this.actionPrimary(true),
+      name: this.actionLabels.PROMOTE,
+      visible: () => this.selection.first() != null && !this.selection.first().primary
+    };
+    const demoteAction: CdTableAction = {
+      permission: 'update',
+      icon: Icons.edit,
+      click: () => this.actionPrimary(false),
+      name: this.actionLabels.DEMOTE,
+      visible: () => this.selection.first() != null && this.selection.first().primary
+    };
     this.tableActions = [
       addAction,
       editAction,
       copyAction,
       flattenAction,
+      resyncAction,
       deleteAction,
-      moveAction
+      moveAction,
+      removeSchedulingAction,
+      promoteAction,
+      demoteAction
     ];
   }
 
@@ -213,6 +255,7 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
         prop: 'size',
         flexGrow: 1,
         cellClass: 'text-right',
+        sortable: false,
         pipe: this.dimlessBinaryPipe
       },
       {
@@ -220,6 +263,7 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
         prop: 'num_objs',
         flexGrow: 1,
         cellClass: 'text-right',
+        sortable: false,
         pipe: this.dimlessPipe
       },
       {
@@ -227,6 +271,7 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
         prop: 'obj_size',
         flexGrow: 1,
         cellClass: 'text-right',
+        sortable: false,
         pipe: this.dimlessBinaryPipe
       },
       {
@@ -235,6 +280,7 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
         cellClass: 'text-center',
         flexGrow: 1,
         pipe: this.dimlessBinaryPipe,
+        sortable: false,
         cellTemplate: this.provisionedNotAvailableTooltipTpl
       },
       {
@@ -243,13 +289,22 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
         cellClass: 'text-center',
         flexGrow: 1,
         pipe: this.dimlessBinaryPipe,
+        sortable: false,
         cellTemplate: this.totalProvisionedNotAvailableTooltipTpl
       },
       {
         name: $localize`Parent`,
         prop: 'parent',
         flexGrow: 2,
+        sortable: false,
         cellTemplate: this.parentTpl
+      },
+      {
+        name: $localize`Mirroring`,
+        prop: 'mirror_mode',
+        flexGrow: 3,
+        sortable: false,
+        cellTemplate: this.mirroringTpl
       }
     ];
 
@@ -299,7 +354,7 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
     };
 
     this.taskListService.init(
-      () => this.rbdService.list(),
+      (context) => this.getRbdImages(context),
       (resp) => this.prepareResponse(resp),
       (images) => (this.images = images),
       () => this.onFetchError(),
@@ -311,40 +366,44 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
 
   onFetchError() {
     this.table.reset(); // Disable loading indicator.
-    this.tableStatus = new TableStatusViewCache(ViewCacheStatus.ValueException);
+    this.tableStatus = new TableStatus('danger');
+  }
+
+  getRbdImages(context: CdTableFetchDataContext) {
+    if (context !== null) {
+      this.tableContext = context;
+    }
+    if (this.tableContext == null) {
+      this.tableContext = new CdTableFetchDataContext(() => undefined);
+    }
+    return this.rbdService.list(this.tableContext?.toParams());
   }
 
   prepareResponse(resp: any[]): any[] {
     let images: any[] = [];
-    const viewCacheStatusMap = {};
 
     resp.forEach((pool) => {
-      if (_.isUndefined(viewCacheStatusMap[pool.status])) {
-        viewCacheStatusMap[pool.status] = [];
-      }
-      viewCacheStatusMap[pool.status].push(pool.pool_name);
       images = images.concat(pool.value);
     });
 
-    let status: number;
-    if (viewCacheStatusMap[ViewCacheStatus.ValueException]) {
-      status = ViewCacheStatus.ValueException;
-    } else if (viewCacheStatusMap[ViewCacheStatus.ValueStale]) {
-      status = ViewCacheStatus.ValueStale;
-    } else if (viewCacheStatusMap[ViewCacheStatus.ValueNone]) {
-      status = ViewCacheStatus.ValueNone;
-    }
-
-    if (status) {
-      const statusFor =
-        (viewCacheStatusMap[status].length > 1 ? 'pools ' : 'pool ') +
-        viewCacheStatusMap[status].join();
+    images.forEach((image) => {
+      if (image.schedule_info !== undefined) {
+        let scheduling: any[] = [];
+        const scheduleStatus = 'scheduled';
+        let nextSnapshotDate = +new Date(image.schedule_info.schedule_time);
+        const offset = new Date().getTimezoneOffset();
+        nextSnapshotDate = nextSnapshotDate + Math.abs(offset) * 60000;
+        scheduling.push(image.mirror_mode, scheduleStatus, nextSnapshotDate);
+        image.mirror_mode = scheduling;
+        scheduling = [];
+      }
+    });
 
-      this.tableStatus = new TableStatusViewCache(status, statusFor);
+    if (images.length > 0) {
+      this.count = CdTableServerSideService.getCount(resp[0]);
     } else {
-      this.tableStatus = new TableStatusViewCache();
+      this.count = 0;
     }
-
     return images;
   }
 
@@ -376,6 +435,26 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
     });
   }
 
+  resyncRbdModal() {
+    const poolName = this.selection.first().pool_name;
+    const namespace = this.selection.first().namespace;
+    const imageName = this.selection.first().name;
+    const imageSpec = new ImageSpec(poolName, namespace, imageName);
+
+    this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+      itemDescription: 'RBD',
+      itemNames: [imageSpec],
+      actionDescription: 'resync',
+      submitActionObservable: () =>
+        this.taskWrapper.wrapTaskAroundCall({
+          task: new FinishedTask('rbd/edit', {
+            image_spec: imageSpec.toString()
+          }),
+          call: this.rbdService.update(imageSpec, { resync: true })
+        })
+    });
+  }
+
   trashRbdModal() {
     const initialState = {
       poolName: this.selection.first().pool_name,
@@ -429,6 +508,62 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
     this.modalRef = this.modalService.show(ConfirmationModalComponent, initialState);
   }
 
+  editRequest() {
+    const request = new RbdFormEditRequestModel();
+    request.remove_scheduling = !request.remove_scheduling;
+    return request;
+  }
+
+  removeSchedulingModal() {
+    const imageName = this.selection.first().name;
+
+    const imageSpec = new ImageSpec(
+      this.selection.first().pool_name,
+      this.selection.first().namespace,
+      this.selection.first().name
+    );
+
+    this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+      actionDescription: 'remove scheduling on',
+      itemDescription: $localize`image`,
+      itemNames: [`${imageName}`],
+      submitActionObservable: () =>
+        new Observable((observer: Subscriber<any>) => {
+          this.taskWrapper
+            .wrapTaskAroundCall({
+              task: new FinishedTask('rbd/edit', {
+                image_spec: imageSpec.toString()
+              }),
+              call: this.rbdService.update(imageSpec, this.editRequest())
+            })
+            .subscribe({
+              error: (resp) => observer.error(resp),
+              complete: () => {
+                this.modalRef.close();
+              }
+            });
+        })
+    });
+  }
+
+  actionPrimary(primary: boolean) {
+    const request = new RbdFormEditRequestModel();
+    request.primary = primary;
+    const imageSpec = new ImageSpec(
+      this.selection.first().pool_name,
+      this.selection.first().namespace,
+      this.selection.first().name
+    );
+    this.taskWrapper
+      .wrapTaskAroundCall({
+        task: new FinishedTask('rbd/edit', {
+          image_spec: imageSpec.toString()
+        }),
+        call: this.rbdService.update(imageSpec, request)
+      })
+      .subscribe();
+  }
+
   hasSnapshots() {
     const snapshots = this.selection.first()['snapshots'] || [];
     return snapshots.length > 0;
@@ -460,6 +595,19 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
     return this.getInvalidNameDisable(selection) || this.hasClonedSnapshots(selection.first());
   }
 
+  getResyncDisableDesc(selection: CdTableSelection): string | boolean {
+    const first = selection.first();
+
+    if (first && this.imageIsPrimary(first)) {
+      return $localize`Primary RBD images cannot be resynced`;
+    }
+
+    return this.getInvalidNameDisable(selection);
+  }
+
+  imageIsPrimary(image: object) {
+    return image['primary'];
+  }
   getInvalidNameDisable(selection: CdTableSelection): string | boolean {
     const first = selection.first();