]> 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.5
[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 { RBDImageFormat, 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.unique_id = '-1';
95 model.name = name;
96 model.namespace = namespace;
97 model.pool_name = pool;
98 model.image_format = RBDImageFormat.V2;
99 return model;
100 }
101
102 constructor(
103 private authStorageService: AuthStorageService,
104 private rbdService: RbdService,
105 private dimlessBinaryPipe: DimlessBinaryPipe,
106 private dimlessPipe: DimlessPipe,
107 private modalService: BsModalService,
108 private taskWrapper: TaskWrapperService,
109 private taskListService: TaskListService,
110 private i18n: I18n,
111 private urlBuilder: URLBuilderService,
112 public actionLabels: ActionLabelsI18n
113 ) {
114 super();
115 this.permission = this.authStorageService.getPermissions().rbdImage;
116 const getImageUri = () =>
117 this.selection.first() &&
118 new ImageSpec(
119 this.selection.first().pool_name,
120 this.selection.first().namespace,
121 this.selection.first().name
122 ).toStringEncoded();
123 const addAction: CdTableAction = {
124 permission: 'create',
125 icon: Icons.add,
126 routerLink: () => this.urlBuilder.getCreate(),
127 canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
128 name: this.actionLabels.CREATE
129 };
130 const editAction: CdTableAction = {
131 permission: 'update',
132 icon: Icons.edit,
133 routerLink: () => this.urlBuilder.getEdit(getImageUri()),
134 name: this.actionLabels.EDIT
135 };
136 const deleteAction: CdTableAction = {
137 permission: 'delete',
138 icon: Icons.destroy,
139 click: () => this.deleteRbdModal(),
140 name: this.actionLabels.DELETE,
141 disable: (selection: CdTableSelection) =>
142 !this.selection.first() ||
143 !this.selection.hasSingleSelection ||
144 this.hasClonedSnapshots(selection.first()),
145 disableDesc: () => this.getDeleteDisableDesc()
146 };
147 const copyAction: CdTableAction = {
148 permission: 'create',
149 canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection,
150 disable: (selection: CdTableSelection) =>
151 !selection.hasSingleSelection || selection.first().cdExecuting,
152 icon: Icons.copy,
153 routerLink: () => `/block/rbd/copy/${getImageUri()}`,
154 name: this.actionLabels.COPY
155 };
156 const flattenAction: CdTableAction = {
157 permission: 'update',
158 disable: (selection: CdTableSelection) =>
159 !selection.hasSingleSelection || selection.first().cdExecuting || !selection.first().parent,
160 icon: Icons.flatten,
161 click: () => this.flattenRbdModal(),
162 name: this.actionLabels.FLATTEN
163 };
164 const moveAction: CdTableAction = {
165 permission: 'delete',
166 icon: Icons.trash,
167 click: () => this.trashRbdModal(),
168 name: this.actionLabels.TRASH,
169 disable: (selection: CdTableSelection) =>
170 !selection.first() ||
171 !selection.hasSingleSelection ||
172 selection.first().image_format === RBDImageFormat.V1
173 };
174 this.tableActions = [
175 addAction,
176 editAction,
177 copyAction,
178 flattenAction,
179 deleteAction,
180 moveAction
181 ];
182 }
183
184 ngOnInit() {
185 this.columns = [
186 {
187 name: this.i18n('Name'),
188 prop: 'name',
189 flexGrow: 2,
190 cellTransformation: CellTemplate.executing
191 },
192 {
193 name: this.i18n('Pool'),
194 prop: 'pool_name',
195 flexGrow: 2
196 },
197 {
198 name: this.i18n('Namespace'),
199 prop: 'namespace',
200 flexGrow: 2
201 },
202 {
203 name: this.i18n('Size'),
204 prop: 'size',
205 flexGrow: 1,
206 cellClass: 'text-right',
207 pipe: this.dimlessBinaryPipe
208 },
209 {
210 name: this.i18n('Objects'),
211 prop: 'num_objs',
212 flexGrow: 1,
213 cellClass: 'text-right',
214 pipe: this.dimlessPipe
215 },
216 {
217 name: this.i18n('Object size'),
218 prop: 'obj_size',
219 flexGrow: 1,
220 cellClass: 'text-right',
221 pipe: this.dimlessBinaryPipe
222 },
223 {
224 name: this.i18n('Provisioned'),
225 prop: 'disk_usage',
226 cellClass: 'text-center',
227 flexGrow: 1,
228 pipe: this.dimlessBinaryPipe
229 },
230 {
231 name: this.i18n('Total provisioned'),
232 prop: 'total_disk_usage',
233 cellClass: 'text-center',
234 flexGrow: 1,
235 pipe: this.dimlessBinaryPipe
236 },
237 {
238 name: this.i18n('Parent'),
239 prop: 'parent',
240 flexGrow: 2,
241 cellTemplate: this.parentTpl
242 }
243 ];
244
245 const itemFilter = (entry: Record<string, any>, task: Task) => {
246 let taskImageSpec: string;
247 switch (task.name) {
248 case 'rbd/copy':
249 taskImageSpec = new ImageSpec(
250 task.metadata['dest_pool_name'],
251 task.metadata['dest_namespace'],
252 task.metadata['dest_image_name']
253 ).toString();
254 break;
255 case 'rbd/clone':
256 taskImageSpec = new ImageSpec(
257 task.metadata['child_pool_name'],
258 task.metadata['child_namespace'],
259 task.metadata['child_image_name']
260 ).toString();
261 break;
262 case 'rbd/create':
263 taskImageSpec = new ImageSpec(
264 task.metadata['pool_name'],
265 task.metadata['namespace'],
266 task.metadata['image_name']
267 ).toString();
268 break;
269 default:
270 taskImageSpec = task.metadata['image_spec'];
271 break;
272 }
273 return (
274 taskImageSpec === new ImageSpec(entry.pool_name, entry.namespace, entry.name).toString()
275 );
276 };
277
278 const taskFilter = (task: Task) => {
279 return [
280 'rbd/clone',
281 'rbd/copy',
282 'rbd/create',
283 'rbd/delete',
284 'rbd/edit',
285 'rbd/flatten',
286 'rbd/trash/move'
287 ].includes(task.name);
288 };
289
290 this.taskListService.init(
291 () => this.rbdService.list(),
292 (resp) => this.prepareResponse(resp),
293 (images) => (this.images = images),
294 () => this.onFetchError(),
295 taskFilter,
296 itemFilter,
297 this.builders
298 );
299 }
300
301 onFetchError() {
302 this.table.reset(); // Disable loading indicator.
303 this.viewCacheStatusList = [{ status: ViewCacheStatus.ValueException }];
304 }
305
306 prepareResponse(resp: any[]): any[] {
307 let images: any[] = [];
308 const viewCacheStatusMap = {};
309 resp.forEach((pool) => {
310 if (_.isUndefined(viewCacheStatusMap[pool.status])) {
311 viewCacheStatusMap[pool.status] = [];
312 }
313 viewCacheStatusMap[pool.status].push(pool.pool_name);
314 images = images.concat(pool.value);
315 });
316 const viewCacheStatusList: any[] = [];
317 _.forEach(viewCacheStatusMap, (value: any, key) => {
318 viewCacheStatusList.push({
319 status: parseInt(key, 10),
320 statusFor:
321 (value.length > 1 ? 'pools ' : 'pool ') +
322 '<strong>' +
323 value.join('</strong>, <strong>') +
324 '</strong>'
325 });
326 });
327 this.viewCacheStatusList = viewCacheStatusList;
328 return images;
329 }
330
331 updateSelection(selection: CdTableSelection) {
332 this.selection = selection;
333 }
334
335 deleteRbdModal() {
336 const poolName = this.selection.first().pool_name;
337 const namespace = this.selection.first().namespace;
338 const imageName = this.selection.first().name;
339 const imageSpec = new ImageSpec(poolName, namespace, imageName);
340
341 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
342 initialState: {
343 itemDescription: 'RBD',
344 itemNames: [imageSpec],
345 bodyTemplate: this.deleteTpl,
346 bodyContext: {
347 hasSnapshots: this.hasSnapshots(),
348 snapshots: this.listProtectedSnapshots()
349 },
350 submitActionObservable: () =>
351 this.taskWrapper.wrapTaskAroundCall({
352 task: new FinishedTask('rbd/delete', {
353 image_spec: imageSpec.toString()
354 }),
355 call: this.rbdService.delete(imageSpec)
356 })
357 }
358 });
359 }
360
361 trashRbdModal() {
362 const initialState = {
363 poolName: this.selection.first().pool_name,
364 namespace: this.selection.first().namespace,
365 imageName: this.selection.first().name,
366 hasSnapshots: this.hasSnapshots()
367 };
368 this.modalRef = this.modalService.show(RbdTrashMoveModalComponent, { initialState });
369 }
370
371 flattenRbd(imageSpec: ImageSpec) {
372 this.taskWrapper
373 .wrapTaskAroundCall({
374 task: new FinishedTask('rbd/flatten', {
375 image_spec: imageSpec.toString()
376 }),
377 call: this.rbdService.flatten(imageSpec)
378 })
379 .subscribe(undefined, undefined, () => {
380 this.modalRef.hide();
381 });
382 }
383
384 flattenRbdModal() {
385 const poolName = this.selection.first().pool_name;
386 const namespace = this.selection.first().namespace;
387 const imageName = this.selection.first().name;
388 const parent: RbdParentModel = this.selection.first().parent;
389 const parentImageSpec = new ImageSpec(
390 parent.pool_name,
391 parent.pool_namespace,
392 parent.image_name
393 );
394 const childImageSpec = new ImageSpec(poolName, namespace, imageName);
395
396 const initialState = {
397 titleText: 'RBD flatten',
398 buttonText: 'Flatten',
399 bodyTpl: this.flattenTpl,
400 bodyData: {
401 parent: `${parentImageSpec}@${parent.snap_name}`,
402 child: childImageSpec.toString()
403 },
404 onSubmit: () => {
405 this.flattenRbd(childImageSpec);
406 }
407 };
408
409 this.modalRef = this.modalService.show(ConfirmationModalComponent, { initialState });
410 }
411
412 hasSnapshots() {
413 const snapshots = this.selection.first()['snapshots'] || [];
414 return snapshots.length > 0;
415 }
416
417 hasClonedSnapshots(image: object) {
418 const snapshots = image['snapshots'] || [];
419 return snapshots.some((snap: object) => snap['children'] && snap['children'].length > 0);
420 }
421
422 listProtectedSnapshots() {
423 const first = this.selection.first();
424 const snapshots = first['snapshots'];
425 return snapshots.reduce((accumulator: string[], snap: object) => {
426 if (snap['is_protected']) {
427 accumulator.push(snap['name']);
428 }
429 return accumulator;
430 }, []);
431 }
432
433 getDeleteDisableDesc(): string {
434 const first = this.selection.first();
435 if (first && this.hasClonedSnapshots(first)) {
436 return this.i18n(
437 'This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.'
438 );
439 }
440
441 return '';
442 }
443 }