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