1 import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
3 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
4 import _ from 'lodash';
6 import { RbdService } from '~/app/shared/api/rbd.service';
7 import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
8 import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache';
9 import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
10 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
11 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
12 import { TableComponent } from '~/app/shared/datatable/table/table.component';
13 import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
14 import { Icons } from '~/app/shared/enum/icons.enum';
15 import { ViewCacheStatus } from '~/app/shared/enum/view-cache-status.enum';
16 import { CdTableAction } from '~/app/shared/models/cd-table-action';
17 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
18 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
19 import { FinishedTask } from '~/app/shared/models/finished-task';
20 import { ImageSpec } from '~/app/shared/models/image-spec';
21 import { Permission } from '~/app/shared/models/permissions';
22 import { Task } from '~/app/shared/models/task';
23 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
24 import { DimlessPipe } from '~/app/shared/pipes/dimless.pipe';
25 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
26 import { ModalService } from '~/app/shared/services/modal.service';
27 import { TaskListService } from '~/app/shared/services/task-list.service';
28 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
29 import { URLBuilderService } from '~/app/shared/services/url-builder.service';
30 import { RbdParentModel } from '../rbd-form/rbd-parent.model';
31 import { RbdTrashMoveModalComponent } from '../rbd-trash-move-modal/rbd-trash-move-modal.component';
32 import { RBDImageFormat, RbdModel } from './rbd-model';
34 const BASE_URL = 'block/rbd';
37 selector: 'cd-rbd-list',
38 templateUrl: './rbd-list.component.html',
39 styleUrls: ['./rbd-list.component.scss'],
42 { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }
45 export class RbdListComponent extends ListWithDetails implements OnInit {
46 @ViewChild(TableComponent, { static: true })
47 table: TableComponent;
48 @ViewChild('usageTpl')
49 usageTpl: TemplateRef<any>;
50 @ViewChild('parentTpl', { static: true })
51 parentTpl: TemplateRef<any>;
53 nameTpl: TemplateRef<any>;
54 @ViewChild('flattenTpl', { static: true })
55 flattenTpl: TemplateRef<any>;
56 @ViewChild('deleteTpl', { static: true })
57 deleteTpl: TemplateRef<any>;
59 permission: Permission;
60 tableActions: CdTableAction[];
62 columns: CdTableColumn[];
64 tableStatus = new TableStatusViewCache();
65 selection = new CdTableSelection();
67 modalRef: NgbModalRef;
70 'rbd/create': (metadata: object) =>
71 this.createRbdFromTask(metadata['pool_name'], metadata['namespace'], metadata['image_name']),
72 'rbd/delete': (metadata: object) => this.createRbdFromTaskImageSpec(metadata['image_spec']),
73 'rbd/clone': (metadata: object) =>
74 this.createRbdFromTask(
75 metadata['child_pool_name'],
76 metadata['child_namespace'],
77 metadata['child_image_name']
79 'rbd/copy': (metadata: object) =>
80 this.createRbdFromTask(
81 metadata['dest_pool_name'],
82 metadata['dest_namespace'],
83 metadata['dest_image_name']
87 private createRbdFromTaskImageSpec(imageSpecStr: string): RbdModel {
88 const imageSpec = ImageSpec.fromString(imageSpecStr);
89 return this.createRbdFromTask(imageSpec.poolName, imageSpec.namespace, imageSpec.imageName);
92 private createRbdFromTask(pool: string, namespace: string, name: string): RbdModel {
93 const model = new RbdModel();
95 model.unique_id = '-1';
97 model.namespace = namespace;
98 model.pool_name = pool;
99 model.image_format = RBDImageFormat.V2;
104 private authStorageService: AuthStorageService,
105 private rbdService: RbdService,
106 private dimlessBinaryPipe: DimlessBinaryPipe,
107 private dimlessPipe: DimlessPipe,
108 private modalService: ModalService,
109 private taskWrapper: TaskWrapperService,
110 public taskListService: TaskListService,
111 private urlBuilder: URLBuilderService,
112 public actionLabels: ActionLabelsI18n
115 this.permission = this.authStorageService.getPermissions().rbdImage;
116 const getImageUri = () =>
117 this.selection.first() &&
119 this.selection.first().pool_name,
120 this.selection.first().namespace,
121 this.selection.first().name
123 const addAction: CdTableAction = {
124 permission: 'create',
126 routerLink: () => this.urlBuilder.getCreate(),
127 canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
128 name: this.actionLabels.CREATE
130 const editAction: CdTableAction = {
131 permission: 'update',
133 routerLink: () => this.urlBuilder.getEdit(getImageUri()),
134 name: this.actionLabels.EDIT,
135 disable: this.getInvalidNameDisable
137 const deleteAction: CdTableAction = {
138 permission: 'delete',
140 click: () => this.deleteRbdModal(),
141 name: this.actionLabels.DELETE,
142 disable: (selection: CdTableSelection) => this.getDeleteDisableDesc(selection)
144 const copyAction: CdTableAction = {
145 permission: 'create',
146 canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection,
147 disable: (selection: CdTableSelection) =>
148 this.getInvalidNameDisable(selection) || !!selection.first().cdExecuting,
150 routerLink: () => `/block/rbd/copy/${getImageUri()}`,
151 name: this.actionLabels.COPY
153 const flattenAction: CdTableAction = {
154 permission: 'update',
155 disable: (selection: CdTableSelection) =>
156 this.getInvalidNameDisable(selection) ||
157 selection.first().cdExecuting ||
158 !selection.first().parent,
160 click: () => this.flattenRbdModal(),
161 name: this.actionLabels.FLATTEN
163 const moveAction: CdTableAction = {
164 permission: 'delete',
166 click: () => this.trashRbdModal(),
167 name: this.actionLabels.TRASH,
168 disable: (selection: CdTableSelection) =>
169 this.getInvalidNameDisable(selection) ||
170 selection.first().image_format === RBDImageFormat.V1
172 this.tableActions = [
185 name: $localize`Name`,
188 cellTransformation: CellTemplate.executing
191 name: $localize`Pool`,
196 name: $localize`Namespace`,
201 name: $localize`Size`,
204 cellClass: 'text-right',
205 pipe: this.dimlessBinaryPipe
208 name: $localize`Objects`,
211 cellClass: 'text-right',
212 pipe: this.dimlessPipe
215 name: $localize`Object size`,
218 cellClass: 'text-right',
219 pipe: this.dimlessBinaryPipe
222 name: $localize`Provisioned`,
224 cellClass: 'text-center',
226 pipe: this.dimlessBinaryPipe
229 name: $localize`Total provisioned`,
230 prop: 'total_disk_usage',
231 cellClass: 'text-center',
233 pipe: this.dimlessBinaryPipe
236 name: $localize`Parent`,
239 cellTemplate: this.parentTpl
243 const itemFilter = (entry: Record<string, any>, task: Task) => {
244 let taskImageSpec: string;
247 taskImageSpec = new ImageSpec(
248 task.metadata['dest_pool_name'],
249 task.metadata['dest_namespace'],
250 task.metadata['dest_image_name']
254 taskImageSpec = new ImageSpec(
255 task.metadata['child_pool_name'],
256 task.metadata['child_namespace'],
257 task.metadata['child_image_name']
261 taskImageSpec = new ImageSpec(
262 task.metadata['pool_name'],
263 task.metadata['namespace'],
264 task.metadata['image_name']
268 taskImageSpec = task.metadata['image_spec'];
272 taskImageSpec === new ImageSpec(entry.pool_name, entry.namespace, entry.name).toString()
276 const taskFilter = (task: Task) => {
285 ].includes(task.name);
288 this.taskListService.init(
289 () => this.rbdService.list(),
290 (resp) => this.prepareResponse(resp),
291 (images) => (this.images = images),
292 () => this.onFetchError(),
300 this.table.reset(); // Disable loading indicator.
301 this.tableStatus = new TableStatusViewCache(ViewCacheStatus.ValueException);
304 prepareResponse(resp: any[]): any[] {
305 let images: any[] = [];
306 const viewCacheStatusMap = {};
308 resp.forEach((pool) => {
309 if (_.isUndefined(viewCacheStatusMap[pool.status])) {
310 viewCacheStatusMap[pool.status] = [];
312 viewCacheStatusMap[pool.status].push(pool.pool_name);
313 images = images.concat(pool.value);
317 if (viewCacheStatusMap[ViewCacheStatus.ValueException]) {
318 status = ViewCacheStatus.ValueException;
319 } else if (viewCacheStatusMap[ViewCacheStatus.ValueStale]) {
320 status = ViewCacheStatus.ValueStale;
321 } else if (viewCacheStatusMap[ViewCacheStatus.ValueNone]) {
322 status = ViewCacheStatus.ValueNone;
327 (viewCacheStatusMap[status].length > 1 ? 'pools ' : 'pool ') +
328 viewCacheStatusMap[status].join();
330 this.tableStatus = new TableStatusViewCache(status, statusFor);
332 this.tableStatus = new TableStatusViewCache();
338 updateSelection(selection: CdTableSelection) {
339 this.selection = selection;
343 const poolName = this.selection.first().pool_name;
344 const namespace = this.selection.first().namespace;
345 const imageName = this.selection.first().name;
346 const imageSpec = new ImageSpec(poolName, namespace, imageName);
348 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
349 itemDescription: 'RBD',
350 itemNames: [imageSpec],
351 bodyTemplate: this.deleteTpl,
353 hasSnapshots: this.hasSnapshots(),
354 snapshots: this.listProtectedSnapshots()
356 submitActionObservable: () =>
357 this.taskWrapper.wrapTaskAroundCall({
358 task: new FinishedTask('rbd/delete', {
359 image_spec: imageSpec.toString()
361 call: this.rbdService.delete(imageSpec)
367 const initialState = {
368 poolName: this.selection.first().pool_name,
369 namespace: this.selection.first().namespace,
370 imageName: this.selection.first().name,
371 hasSnapshots: this.hasSnapshots()
373 this.modalRef = this.modalService.show(RbdTrashMoveModalComponent, initialState);
376 flattenRbd(imageSpec: ImageSpec) {
378 .wrapTaskAroundCall({
379 task: new FinishedTask('rbd/flatten', {
380 image_spec: imageSpec.toString()
382 call: this.rbdService.flatten(imageSpec)
386 this.modalRef.close();
392 const poolName = this.selection.first().pool_name;
393 const namespace = this.selection.first().namespace;
394 const imageName = this.selection.first().name;
395 const parent: RbdParentModel = this.selection.first().parent;
396 const parentImageSpec = new ImageSpec(
398 parent.pool_namespace,
401 const childImageSpec = new ImageSpec(poolName, namespace, imageName);
403 const initialState = {
404 titleText: 'RBD flatten',
405 buttonText: 'Flatten',
406 bodyTpl: this.flattenTpl,
408 parent: `${parentImageSpec}@${parent.snap_name}`,
409 child: childImageSpec.toString()
412 this.flattenRbd(childImageSpec);
416 this.modalRef = this.modalService.show(ConfirmationModalComponent, initialState);
420 const snapshots = this.selection.first()['snapshots'] || [];
421 return snapshots.length > 0;
424 hasClonedSnapshots(image: object) {
425 const snapshots = image['snapshots'] || [];
426 return snapshots.some((snap: object) => snap['children'] && snap['children'].length > 0);
429 listProtectedSnapshots() {
430 const first = this.selection.first();
431 const snapshots = first['snapshots'];
432 return snapshots.reduce((accumulator: string[], snap: object) => {
433 if (snap['is_protected']) {
434 accumulator.push(snap['name']);
440 getDeleteDisableDesc(selection: CdTableSelection): string | boolean {
441 const first = selection.first();
443 if (first && this.hasClonedSnapshots(first)) {
444 return $localize`This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.`;
447 return this.getInvalidNameDisable(selection) || this.hasClonedSnapshots(selection.first());
450 getInvalidNameDisable(selection: CdTableSelection): string | boolean {
451 const first = selection.first();
453 if (first?.name?.match(/[@/]/)) {
454 return $localize`This RBD image has an invalid name and can't be managed by ceph.`;
457 return !selection.first() || !selection.hasSingleSelection;