]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
aaea7eb89055490059fb0cc5cf50b0f51cad9f41
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / block / rbd-list / rbd-list.component.ts
1 import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
2
3 import { I18n } from '@ngx-translate/i18n-polyfill';
4 import * as _ from 'lodash';
5 import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
6
7 import { RbdService } from '../../../shared/api/rbd.service';
8 import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component';
9 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
10 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
11 import { TableComponent } from '../../../shared/datatable/table/table.component';
12 import { CellTemplate } from '../../../shared/enum/cell-template.enum';
13 import { Icons } from '../../../shared/enum/icons.enum';
14 import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum';
15 import { CdTableAction } from '../../../shared/models/cd-table-action';
16 import { CdTableColumn } from '../../../shared/models/cd-table-column';
17 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
18 import { FinishedTask } from '../../../shared/models/finished-task';
19 import { ImageSpec } from '../../../shared/models/image-spec';
20 import { Permission } from '../../../shared/models/permissions';
21 import { Task } from '../../../shared/models/task';
22 import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
23 import { DimlessPipe } from '../../../shared/pipes/dimless.pipe';
24 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
25 import { TaskListService } from '../../../shared/services/task-list.service';
26 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
27 import { URLBuilderService } from '../../../shared/services/url-builder.service';
28 import { RbdParentModel } from '../rbd-form/rbd-parent.model';
29 import { RbdTrashMoveModalComponent } from '../rbd-trash-move-modal/rbd-trash-move-modal.component';
30 import { RbdModel } from './rbd-model';
31
32 const BASE_URL = 'block/rbd';
33
34 @Component({
35 selector: 'cd-rbd-list',
36 templateUrl: './rbd-list.component.html',
37 styleUrls: ['./rbd-list.component.scss'],
38 providers: [
39 TaskListService,
40 { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }
41 ]
42 })
43 export class RbdListComponent implements OnInit {
44 @ViewChild(TableComponent, { static: true })
45 table: TableComponent;
46 @ViewChild('usageTpl', { static: false })
47 usageTpl: TemplateRef<any>;
48 @ViewChild('parentTpl', { static: true })
49 parentTpl: TemplateRef<any>;
50 @ViewChild('nameTpl', { static: false })
51 nameTpl: TemplateRef<any>;
52 @ViewChild('flattenTpl', { static: true })
53 flattenTpl: TemplateRef<any>;
54 @ViewChild('deleteTpl', { static: true })
55 deleteTpl: TemplateRef<any>;
56
57 permission: Permission;
58 tableActions: CdTableAction[];
59 images: any;
60 columns: CdTableColumn[];
61 retries: number;
62 viewCacheStatusList: any[];
63 selection = new CdTableSelection();
64
65 modalRef: BsModalRef;
66
67 builders = {
68 'rbd/create': (metadata: object) =>
69 this.createRbdFromTask(metadata['pool_name'], metadata['namespace'], metadata['image_name']),
70 'rbd/delete': (metadata: object) => this.createRbdFromTaskImageSpec(metadata['image_spec']),
71 'rbd/clone': (metadata: object) =>
72 this.createRbdFromTask(
73 metadata['child_pool_name'],
74 metadata['child_namespace'],
75 metadata['child_image_name']
76 ),
77 'rbd/copy': (metadata: object) =>
78 this.createRbdFromTask(
79 metadata['dest_pool_name'],
80 metadata['dest_namespace'],
81 metadata['dest_image_name']
82 )
83 };
84
85 private createRbdFromTaskImageSpec(imageSpecStr: string): RbdModel {
86 const imageSpec = ImageSpec.fromString(imageSpecStr);
87 return this.createRbdFromTask(imageSpec.poolName, imageSpec.namespace, imageSpec.imageName);
88 }
89
90 private createRbdFromTask(pool: string, namespace: string, name: string): RbdModel {
91 const model = new RbdModel();
92 model.id = '-1';
93 model.name = name;
94 model.namespace = namespace;
95 model.pool_name = pool;
96 return model;
97 }
98
99 constructor(
100 private authStorageService: AuthStorageService,
101 private rbdService: RbdService,
102 private dimlessBinaryPipe: DimlessBinaryPipe,
103 private dimlessPipe: DimlessPipe,
104 private modalService: BsModalService,
105 private taskWrapper: TaskWrapperService,
106 private taskListService: TaskListService,
107 private i18n: I18n,
108 private urlBuilder: URLBuilderService,
109 public actionLabels: ActionLabelsI18n
110 ) {
111 this.permission = this.authStorageService.getPermissions().rbdImage;
112 const getImageUri = () =>
113 this.selection.first() &&
114 new ImageSpec(
115 this.selection.first().pool_name,
116 this.selection.first().namespace,
117 this.selection.first().name
118 ).toStringEncoded();
119 const addAction: CdTableAction = {
120 permission: 'create',
121 icon: Icons.add,
122 routerLink: () => this.urlBuilder.getCreate(),
123 canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
124 name: this.actionLabels.CREATE
125 };
126 const editAction: CdTableAction = {
127 permission: 'update',
128 icon: Icons.edit,
129 routerLink: () => this.urlBuilder.getEdit(getImageUri()),
130 name: this.actionLabels.EDIT
131 };
132 const deleteAction: CdTableAction = {
133 permission: 'delete',
134 icon: Icons.destroy,
135 click: () => this.deleteRbdModal(),
136 name: this.actionLabels.DELETE,
137 disable: (selection: CdTableSelection) =>
138 !this.selection.first() ||
139 !this.selection.hasSingleSelection ||
140 this.hasClonedSnapshots(selection.first()),
141 disableDesc: () => this.getDeleteDisableDesc()
142 };
143 const copyAction: CdTableAction = {
144 permission: 'create',
145 canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection,
146 disable: (selection: CdTableSelection) =>
147 !selection.hasSingleSelection || selection.first().cdExecuting,
148 icon: Icons.copy,
149 routerLink: () => `/block/rbd/copy/${getImageUri()}`,
150 name: this.actionLabels.COPY
151 };
152 const flattenAction: CdTableAction = {
153 permission: 'update',
154 disable: (selection: CdTableSelection) =>
155 !selection.hasSingleSelection || selection.first().cdExecuting || !selection.first().parent,
156 icon: Icons.flatten,
157 click: () => this.flattenRbdModal(),
158 name: this.actionLabels.FLATTEN
159 };
160 const moveAction: CdTableAction = {
161 permission: 'delete',
162 icon: Icons.trash,
163 click: () => this.trashRbdModal(),
164 name: this.actionLabels.TRASH
165 };
166 this.tableActions = [
167 addAction,
168 editAction,
169 copyAction,
170 flattenAction,
171 deleteAction,
172 moveAction
173 ];
174 }
175
176 ngOnInit() {
177 this.columns = [
178 {
179 name: this.i18n('Name'),
180 prop: 'name',
181 flexGrow: 2,
182 cellTransformation: CellTemplate.executing
183 },
184 {
185 name: this.i18n('Pool'),
186 prop: 'pool_name',
187 flexGrow: 2
188 },
189 {
190 name: this.i18n('Namespace'),
191 prop: 'namespace',
192 flexGrow: 2
193 },
194 {
195 name: this.i18n('Size'),
196 prop: 'size',
197 flexGrow: 1,
198 cellClass: 'text-right',
199 pipe: this.dimlessBinaryPipe
200 },
201 {
202 name: this.i18n('Objects'),
203 prop: 'num_objs',
204 flexGrow: 1,
205 cellClass: 'text-right',
206 pipe: this.dimlessPipe
207 },
208 {
209 name: this.i18n('Object size'),
210 prop: 'obj_size',
211 flexGrow: 1,
212 cellClass: 'text-right',
213 pipe: this.dimlessBinaryPipe
214 },
215 {
216 name: this.i18n('Provisioned'),
217 prop: 'disk_usage',
218 cellClass: 'text-center',
219 flexGrow: 1,
220 pipe: this.dimlessBinaryPipe
221 },
222 {
223 name: this.i18n('Total provisioned'),
224 prop: 'total_disk_usage',
225 cellClass: 'text-center',
226 flexGrow: 1,
227 pipe: this.dimlessBinaryPipe
228 },
229 {
230 name: this.i18n('Parent'),
231 prop: 'parent',
232 flexGrow: 2,
233 cellTemplate: this.parentTpl
234 }
235 ];
236
237 const itemFilter = (entry: Record<string, any>, task: Task) => {
238 let taskImageSpec: string;
239 switch (task.name) {
240 case 'rbd/copy':
241 taskImageSpec = new ImageSpec(
242 task.metadata['dest_pool_name'],
243 task.metadata['dest_namespace'],
244 task.metadata['dest_image_name']
245 ).toString();
246 break;
247 case 'rbd/clone':
248 taskImageSpec = new ImageSpec(
249 task.metadata['child_pool_name'],
250 task.metadata['child_namespace'],
251 task.metadata['child_image_name']
252 ).toString();
253 break;
254 case 'rbd/create':
255 taskImageSpec = new ImageSpec(
256 task.metadata['pool_name'],
257 task.metadata['namespace'],
258 task.metadata['image_name']
259 ).toString();
260 break;
261 default:
262 taskImageSpec = task.metadata['image_spec'];
263 break;
264 }
265 return (
266 taskImageSpec === new ImageSpec(entry.pool_name, entry.namespace, entry.name).toString()
267 );
268 };
269
270 const taskFilter = (task: Task) => {
271 return [
272 'rbd/clone',
273 'rbd/copy',
274 'rbd/create',
275 'rbd/delete',
276 'rbd/edit',
277 'rbd/flatten',
278 'rbd/trash/move'
279 ].includes(task.name);
280 };
281
282 this.taskListService.init(
283 () => this.rbdService.list(),
284 (resp) => this.prepareResponse(resp),
285 (images) => (this.images = images),
286 () => this.onFetchError(),
287 taskFilter,
288 itemFilter,
289 this.builders
290 );
291 }
292
293 onFetchError() {
294 this.table.reset(); // Disable loading indicator.
295 this.viewCacheStatusList = [{ status: ViewCacheStatus.ValueException }];
296 }
297
298 prepareResponse(resp: any[]): any[] {
299 let images: any[] = [];
300 const viewCacheStatusMap = {};
301 resp.forEach((pool) => {
302 if (_.isUndefined(viewCacheStatusMap[pool.status])) {
303 viewCacheStatusMap[pool.status] = [];
304 }
305 viewCacheStatusMap[pool.status].push(pool.pool_name);
306 images = images.concat(pool.value);
307 });
308 const viewCacheStatusList: any[] = [];
309 _.forEach(viewCacheStatusMap, (value: any, key) => {
310 viewCacheStatusList.push({
311 status: parseInt(key, 10),
312 statusFor:
313 (value.length > 1 ? 'pools ' : 'pool ') +
314 '<strong>' +
315 value.join('</strong>, <strong>') +
316 '</strong>'
317 });
318 });
319 this.viewCacheStatusList = viewCacheStatusList;
320 return images;
321 }
322
323 updateSelection(selection: CdTableSelection) {
324 this.selection = selection;
325 }
326
327 deleteRbdModal() {
328 const poolName = this.selection.first().pool_name;
329 const namespace = this.selection.first().namespace;
330 const imageName = this.selection.first().name;
331 const imageSpec = new ImageSpec(poolName, namespace, imageName);
332
333 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
334 initialState: {
335 itemDescription: 'RBD',
336 itemNames: [imageSpec],
337 bodyTemplate: this.deleteTpl,
338 bodyContext: {
339 hasSnapshots: this.hasSnapshots(),
340 snapshots: this.listProtectedSnapshots()
341 },
342 submitActionObservable: () =>
343 this.taskWrapper.wrapTaskAroundCall({
344 task: new FinishedTask('rbd/delete', {
345 image_spec: imageSpec.toString()
346 }),
347 call: this.rbdService.delete(imageSpec)
348 })
349 }
350 });
351 }
352
353 trashRbdModal() {
354 const initialState = {
355 poolName: this.selection.first().pool_name,
356 namespace: this.selection.first().namespace,
357 imageName: this.selection.first().name,
358 hasSnapshots: this.hasSnapshots()
359 };
360 this.modalRef = this.modalService.show(RbdTrashMoveModalComponent, { initialState });
361 }
362
363 flattenRbd(imageSpec: ImageSpec) {
364 this.taskWrapper
365 .wrapTaskAroundCall({
366 task: new FinishedTask('rbd/flatten', {
367 image_spec: imageSpec.toString()
368 }),
369 call: this.rbdService.flatten(imageSpec)
370 })
371 .subscribe(undefined, undefined, () => {
372 this.modalRef.hide();
373 });
374 }
375
376 flattenRbdModal() {
377 const poolName = this.selection.first().pool_name;
378 const namespace = this.selection.first().namespace;
379 const imageName = this.selection.first().name;
380 const parent: RbdParentModel = this.selection.first().parent;
381 const parentImageSpec = new ImageSpec(
382 parent.pool_name,
383 parent.pool_namespace,
384 parent.image_name
385 );
386 const childImageSpec = new ImageSpec(poolName, namespace, imageName);
387
388 const initialState = {
389 titleText: 'RBD flatten',
390 buttonText: 'Flatten',
391 bodyTpl: this.flattenTpl,
392 bodyData: {
393 parent: `${parentImageSpec}@${parent.snap_name}`,
394 child: childImageSpec.toString()
395 },
396 onSubmit: () => {
397 this.flattenRbd(childImageSpec);
398 }
399 };
400
401 this.modalRef = this.modalService.show(ConfirmationModalComponent, { initialState });
402 }
403
404 hasSnapshots() {
405 const snapshots = this.selection.first()['snapshots'] || [];
406 return snapshots.length > 0;
407 }
408
409 hasClonedSnapshots(image: object) {
410 const snapshots = image['snapshots'] || [];
411 return snapshots.some((snap: object) => snap['children'] && snap['children'].length > 0);
412 }
413
414 listProtectedSnapshots() {
415 const first = this.selection.first();
416 const snapshots = first['snapshots'];
417 return snapshots.reduce((accumulator: string[], snap: object) => {
418 if (snap['is_protected']) {
419 accumulator.push(snap['name']);
420 }
421 return accumulator;
422 }, []);
423 }
424
425 getDeleteDisableDesc(): string {
426 const first = this.selection.first();
427 if (first && this.hasClonedSnapshots(first)) {
428 return this.i18n(
429 'This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.'
430 );
431 }
432
433 return '';
434 }
435 }