]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
import 15.2.4
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / block / rbd-list / rbd-list.component.ts
index 0ba72c8ef67973f7eef178fb1892f0c4fae3b2c8..f69945b6e7eb465696bd8b084e8a56d8eaaef919 100644 (file)
@@ -5,17 +5,21 @@ import * as _ from 'lodash';
 import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
 
 import { RbdService } from '../../../shared/api/rbd.service';
+import { ListWithDetails } from '../../../shared/classes/list-with-details.class';
 import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component';
 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
 import { TableComponent } from '../../../shared/datatable/table/table.component';
 import { CellTemplate } from '../../../shared/enum/cell-template.enum';
+import { Icons } from '../../../shared/enum/icons.enum';
 import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
 import { CdTableAction } from '../../../shared/models/cd-table-action';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { FinishedTask } from '../../../shared/models/finished-task';
+import { ImageSpec } from '../../../shared/models/image-spec';
 import { Permission } from '../../../shared/models/permissions';
+import { Task } from '../../../shared/models/task';
 import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
 import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
@@ -37,17 +41,19 @@ const BASE_URL = 'block/rbd';
     { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }
   ]
 })
-export class RbdListComponent implements OnInit {
-  @ViewChild(TableComponent)
+export class RbdListComponent extends ListWithDetails implements OnInit {
+  @ViewChild(TableComponent, { static: true })
   table: TableComponent;
-  @ViewChild('usageTpl')
+  @ViewChild('usageTpl', { static: false })
   usageTpl: TemplateRef<any>;
-  @ViewChild('parentTpl')
+  @ViewChild('parentTpl', { static: true })
   parentTpl: TemplateRef<any>;
-  @ViewChild('nameTpl')
+  @ViewChild('nameTpl', { static: false })
   nameTpl: TemplateRef<any>;
-  @ViewChild('flattenTpl')
+  @ViewChild('flattenTpl', { static: true })
   flattenTpl: TemplateRef<any>;
+  @ViewChild('deleteTpl', { static: true })
+  deleteTpl: TemplateRef<any>;
 
   permission: Permission;
   tableActions: CdTableAction[];
@@ -60,18 +66,33 @@ export class RbdListComponent implements OnInit {
   modalRef: BsModalRef;
 
   builders = {
-    'rbd/create': (metadata) =>
-      this.createRbdFromTask(metadata['pool_name'], metadata['image_name']),
-    'rbd/clone': (metadata) =>
-      this.createRbdFromTask(metadata['child_pool_name'], metadata['child_image_name']),
-    'rbd/copy': (metadata) =>
-      this.createRbdFromTask(metadata['dest_pool_name'], metadata['dest_image_name'])
+    'rbd/create': (metadata: object) =>
+      this.createRbdFromTask(metadata['pool_name'], metadata['namespace'], metadata['image_name']),
+    'rbd/delete': (metadata: object) => this.createRbdFromTaskImageSpec(metadata['image_spec']),
+    'rbd/clone': (metadata: object) =>
+      this.createRbdFromTask(
+        metadata['child_pool_name'],
+        metadata['child_namespace'],
+        metadata['child_image_name']
+      ),
+    'rbd/copy': (metadata: object) =>
+      this.createRbdFromTask(
+        metadata['dest_pool_name'],
+        metadata['dest_namespace'],
+        metadata['dest_image_name']
+      )
   };
 
-  private createRbdFromTask(pool: string, name: string): RbdModel {
+  private createRbdFromTaskImageSpec(imageSpecStr: string): RbdModel {
+    const imageSpec = ImageSpec.fromString(imageSpecStr);
+    return this.createRbdFromTask(imageSpec.poolName, imageSpec.namespace, imageSpec.imageName);
+  }
+
+  private createRbdFromTask(pool: string, namespace: string, name: string): RbdModel {
     const model = new RbdModel();
     model.id = '-1';
     model.name = name;
+    model.namespace = namespace;
     model.pool_name = pool;
     return model;
   }
@@ -88,53 +109,61 @@ export class RbdListComponent implements OnInit {
     private urlBuilder: URLBuilderService,
     public actionLabels: ActionLabelsI18n
   ) {
+    super();
     this.permission = this.authStorageService.getPermissions().rbdImage;
     const getImageUri = () =>
       this.selection.first() &&
-      `${encodeURIComponent(this.selection.first().pool_name)}/${encodeURIComponent(
+      new ImageSpec(
+        this.selection.first().pool_name,
+        this.selection.first().namespace,
         this.selection.first().name
-      )}`;
+      ).toStringEncoded();
     const addAction: CdTableAction = {
       permission: 'create',
-      icon: 'fa-plus',
+      icon: Icons.add,
       routerLink: () => this.urlBuilder.getCreate(),
       canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
       name: this.actionLabels.CREATE
     };
     const editAction: CdTableAction = {
       permission: 'update',
-      icon: 'fa-pencil',
+      icon: Icons.edit,
       routerLink: () => this.urlBuilder.getEdit(getImageUri()),
       name: this.actionLabels.EDIT
     };
     const deleteAction: CdTableAction = {
       permission: 'delete',
-      icon: 'fa-times',
+      icon: Icons.destroy,
       click: () => this.deleteRbdModal(),
-      name: this.actionLabels.DELETE
+      name: this.actionLabels.DELETE,
+      disable: (selection: CdTableSelection) =>
+        !this.selection.first() ||
+        !this.selection.hasSingleSelection ||
+        this.hasClonedSnapshots(selection.first()),
+      disableDesc: () => this.getDeleteDisableDesc()
     };
     const copyAction: CdTableAction = {
       permission: 'create',
       canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection,
       disable: (selection: CdTableSelection) =>
         !selection.hasSingleSelection || selection.first().cdExecuting,
-      icon: 'fa-copy',
+      icon: Icons.copy,
       routerLink: () => `/block/rbd/copy/${getImageUri()}`,
-      name: this.i18n('Copy')
+      name: this.actionLabels.COPY
     };
     const flattenAction: CdTableAction = {
       permission: 'update',
       disable: (selection: CdTableSelection) =>
         !selection.hasSingleSelection || selection.first().cdExecuting || !selection.first().parent,
-      icon: 'fa-chain-broken',
+      icon: Icons.flatten,
       click: () => this.flattenRbdModal(),
-      name: this.i18n('Flatten')
+      name: this.actionLabels.FLATTEN
     };
     const moveAction: CdTableAction = {
       permission: 'delete',
-      icon: 'fa-trash-o',
+      icon: Icons.trash,
       click: () => this.trashRbdModal(),
-      name: this.i18n('Move to Trash')
+      name: this.actionLabels.TRASH
     };
     this.tableActions = [
       addAction,
@@ -159,6 +188,11 @@ export class RbdListComponent implements OnInit {
         prop: 'pool_name',
         flexGrow: 2
       },
+      {
+        name: this.i18n('Namespace'),
+        prop: 'namespace',
+        flexGrow: 2
+      },
       {
         name: this.i18n('Size'),
         prop: 'size',
@@ -202,13 +236,58 @@ export class RbdListComponent implements OnInit {
       }
     ];
 
+    const itemFilter = (entry: Record<string, any>, task: Task) => {
+      let taskImageSpec: string;
+      switch (task.name) {
+        case 'rbd/copy':
+          taskImageSpec = new ImageSpec(
+            task.metadata['dest_pool_name'],
+            task.metadata['dest_namespace'],
+            task.metadata['dest_image_name']
+          ).toString();
+          break;
+        case 'rbd/clone':
+          taskImageSpec = new ImageSpec(
+            task.metadata['child_pool_name'],
+            task.metadata['child_namespace'],
+            task.metadata['child_image_name']
+          ).toString();
+          break;
+        case 'rbd/create':
+          taskImageSpec = new ImageSpec(
+            task.metadata['pool_name'],
+            task.metadata['namespace'],
+            task.metadata['image_name']
+          ).toString();
+          break;
+        default:
+          taskImageSpec = task.metadata['image_spec'];
+          break;
+      }
+      return (
+        taskImageSpec === new ImageSpec(entry.pool_name, entry.namespace, entry.name).toString()
+      );
+    };
+
+    const taskFilter = (task: Task) => {
+      return [
+        'rbd/clone',
+        'rbd/copy',
+        'rbd/create',
+        'rbd/delete',
+        'rbd/edit',
+        'rbd/flatten',
+        'rbd/trash/move'
+      ].includes(task.name);
+    };
+
     this.taskListService.init(
       () => this.rbdService.list(),
       (resp) => this.prepareResponse(resp),
       (images) => (this.images = images),
       () => this.onFetchError(),
-      this.taskFilter,
-      this.itemFilter,
+      taskFilter,
+      itemFilter,
       this.builders
     );
   }
@@ -219,7 +298,7 @@ export class RbdListComponent implements OnInit {
   }
 
   prepareResponse(resp: any[]): any[] {
-    let images = [];
+    let images: any[] = [];
     const viewCacheStatusMap = {};
     resp.forEach((pool) => {
       if (_.isUndefined(viewCacheStatusMap[pool.status])) {
@@ -228,7 +307,7 @@ export class RbdListComponent implements OnInit {
       viewCacheStatusMap[pool.status].push(pool.pool_name);
       images = images.concat(pool.value);
     });
-    const viewCacheStatusList = [];
+    const viewCacheStatusList: any[] = [];
     _.forEach(viewCacheStatusMap, (value: any, key) => {
       viewCacheStatusList.push({
         status: parseInt(key, 10),
@@ -243,58 +322,31 @@ export class RbdListComponent implements OnInit {
     return images;
   }
 
-  itemFilter(entry, task) {
-    let pool_name_k: string;
-    let image_name_k: string;
-    switch (task.name) {
-      case 'rbd/copy':
-        pool_name_k = 'dest_pool_name';
-        image_name_k = 'dest_image_name';
-        break;
-      case 'rbd/clone':
-        pool_name_k = 'child_pool_name';
-        image_name_k = 'child_image_name';
-        break;
-      default:
-        pool_name_k = 'pool_name';
-        image_name_k = 'image_name';
-        break;
-    }
-    return (
-      entry.pool_name === task.metadata[pool_name_k] && entry.name === task.metadata[image_name_k]
-    );
-  }
-
-  taskFilter(task) {
-    return [
-      'rbd/clone',
-      'rbd/copy',
-      'rbd/create',
-      'rbd/delete',
-      'rbd/edit',
-      'rbd/flatten',
-      'rbd/trash/move'
-    ].includes(task.name);
-  }
-
   updateSelection(selection: CdTableSelection) {
     this.selection = selection;
   }
 
   deleteRbdModal() {
     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, {
       initialState: {
         itemDescription: 'RBD',
+        itemNames: [imageSpec],
+        bodyTemplate: this.deleteTpl,
+        bodyContext: {
+          hasSnapshots: this.hasSnapshots(),
+          snapshots: this.listProtectedSnapshots()
+        },
         submitActionObservable: () =>
           this.taskWrapper.wrapTaskAroundCall({
             task: new FinishedTask('rbd/delete', {
-              pool_name: poolName,
-              image_name: imageName
+              image_spec: imageSpec.toString()
             }),
-            call: this.rbdService.delete(poolName, imageName)
+            call: this.rbdService.delete(imageSpec)
           })
       }
     });
@@ -302,21 +354,21 @@ export class RbdListComponent implements OnInit {
 
   trashRbdModal() {
     const initialState = {
-      metaType: 'RBD',
       poolName: this.selection.first().pool_name,
-      imageName: this.selection.first().name
+      namespace: this.selection.first().namespace,
+      imageName: this.selection.first().name,
+      hasSnapshots: this.hasSnapshots()
     };
     this.modalRef = this.modalService.show(RbdTrashMoveModalComponent, { initialState });
   }
 
-  flattenRbd(poolName, imageName) {
+  flattenRbd(imageSpec: ImageSpec) {
     this.taskWrapper
       .wrapTaskAroundCall({
         task: new FinishedTask('rbd/flatten', {
-          pool_name: poolName,
-          image_name: imageName
+          image_spec: imageSpec.toString()
         }),
-        call: this.rbdService.flatten(poolName, imageName)
+        call: this.rbdService.flatten(imageSpec)
       })
       .subscribe(undefined, undefined, () => {
         this.modalRef.hide();
@@ -325,22 +377,61 @@ export class RbdListComponent implements OnInit {
 
   flattenRbdModal() {
     const poolName = this.selection.first().pool_name;
+    const namespace = this.selection.first().namespace;
     const imageName = this.selection.first().name;
     const parent: RbdParentModel = this.selection.first().parent;
+    const parentImageSpec = new ImageSpec(
+      parent.pool_name,
+      parent.pool_namespace,
+      parent.image_name
+    );
+    const childImageSpec = new ImageSpec(poolName, namespace, imageName);
 
     const initialState = {
       titleText: 'RBD flatten',
       buttonText: 'Flatten',
       bodyTpl: this.flattenTpl,
       bodyData: {
-        parent: `${parent.pool_name}/${parent.image_name}@${parent.snap_name}`,
-        child: `${poolName}/${imageName}`
+        parent: `${parentImageSpec}@${parent.snap_name}`,
+        child: childImageSpec.toString()
       },
       onSubmit: () => {
-        this.flattenRbd(poolName, imageName);
+        this.flattenRbd(childImageSpec);
       }
     };
 
     this.modalRef = this.modalService.show(ConfirmationModalComponent, { initialState });
   }
+
+  hasSnapshots() {
+    const snapshots = this.selection.first()['snapshots'] || [];
+    return snapshots.length > 0;
+  }
+
+  hasClonedSnapshots(image: object) {
+    const snapshots = image['snapshots'] || [];
+    return snapshots.some((snap: object) => snap['children'] && snap['children'].length > 0);
+  }
+
+  listProtectedSnapshots() {
+    const first = this.selection.first();
+    const snapshots = first['snapshots'];
+    return snapshots.reduce((accumulator: string[], snap: object) => {
+      if (snap['is_protected']) {
+        accumulator.push(snap['name']);
+      }
+      return accumulator;
+    }, []);
+  }
+
+  getDeleteDisableDesc(): string {
+    const first = this.selection.first();
+    if (first && this.hasClonedSnapshots(first)) {
+      return this.i18n(
+        'This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.'
+      );
+    }
+
+    return '';
+  }
 }