1 import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
2 import { Validators } from '@angular/forms';
4 import { I18n } from '@ngx-translate/i18n-polyfill';
5 import * as _ from 'lodash';
6 import * as moment from 'moment';
7 import { NodeEvent, Tree, TreeComponent, TreeModel } from 'ng2-tree';
8 import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
10 import { CephfsService } from '../../../shared/api/cephfs.service';
11 import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component';
12 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
13 import { FormModalComponent } from '../../../shared/components/form-modal/form-modal.component';
14 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
15 import { Icons } from '../../../shared/enum/icons.enum';
16 import { NotificationType } from '../../../shared/enum/notification-type.enum';
17 import { CdValidators } from '../../../shared/forms/cd-validators';
18 import { CdFormModalFieldConfig } from '../../../shared/models/cd-form-modal-field-config';
19 import { CdTableAction } from '../../../shared/models/cd-table-action';
20 import { CdTableColumn } from '../../../shared/models/cd-table-column';
21 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
26 } from '../../../shared/models/cephfs-directory-models';
27 import { Permission } from '../../../shared/models/permissions';
28 import { CdDatePipe } from '../../../shared/pipes/cd-date.pipe';
29 import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
30 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
31 import { NotificationService } from '../../../shared/services/notification.service';
35 // Shows quota that is used for current directory
37 value: number | string;
49 selector: 'cd-cephfs-directories',
50 templateUrl: './cephfs-directories.component.html',
51 styleUrls: ['./cephfs-directories.component.scss']
53 export class CephfsDirectoriesComponent implements OnInit, OnChanges {
54 @ViewChild(TreeComponent, { static: true })
55 treeComponent: TreeComponent;
56 @ViewChild('origin', { static: true })
57 originTmpl: TemplateRef<any>;
62 private modalRef: BsModalRef;
63 private dirs: CephfsDir[];
64 private nodeIds: { [path: string]: CephfsDir };
65 private requestedPaths: string[];
66 private selectedNode: Tree;
68 permission: Permission;
69 selectedDir: CephfsDir;
70 settings: QuotaSetting[];
72 columns: CdTableColumn[];
73 selection: CdTableSelection;
74 tableActions: CdTableAction[];
75 updateSelection: Function;
78 columns: CdTableColumn[];
79 selection: CdTableSelection;
80 tableActions: CdTableAction[];
81 updateSelection: Function;
86 private authStorageService: AuthStorageService,
87 private modalService: BsModalService,
88 private cephfsService: CephfsService,
89 private cdDatePipe: CdDatePipe,
91 private actionLabels: ActionLabelsI18n,
92 private notificationService: NotificationService,
93 private dimlessBinaryPipe: DimlessBinaryPipe
97 this.permission = this.authStorageService.getPermissions().cephfs;
102 name: this.i18n('Name'),
107 name: this.i18n('Value'),
112 prop: 'row.originPath',
113 name: this.i18n('Origin'),
115 cellTemplate: this.originTmpl,
119 selection: new CdTableSelection(),
120 updateSelection: (selection: CdTableSelection) => {
121 this.quota.selection = selection;
125 name: this.actionLabels.SET,
127 permission: 'update',
128 visible: (selection) =>
129 !selection.hasSelection || (selection.first() && selection.first().dirValue === 0),
130 click: () => this.updateQuotaModal()
133 name: this.actionLabels.UPDATE,
135 permission: 'update',
136 visible: (selection) => selection.first() && selection.first().dirValue > 0,
137 click: () => this.updateQuotaModal()
140 name: this.actionLabels.UNSET,
142 permission: 'update',
143 disable: (selection) =>
144 !selection.hasSelection || (selection.first() && selection.first().dirValue === 0),
145 click: () => this.unsetQuotaModal()
153 name: this.i18n('Name'),
158 name: this.i18n('Path'),
164 name: this.i18n('Created'),
166 pipe: this.cdDatePipe
169 selection: new CdTableSelection(),
170 updateSelection: (selection: CdTableSelection) => {
171 this.snapshot.selection = selection;
175 name: this.actionLabels.CREATE,
177 permission: 'create',
178 canBePrimary: (selection) => !selection.hasSelection,
179 click: () => this.createSnapshot()
182 name: this.actionLabels.DELETE,
184 permission: 'delete',
185 click: () => this.deleteSnapshotModal(),
186 canBePrimary: (selection) => selection.hasSelection,
187 disable: (selection) => !selection.hasSelection
194 this.selectedDir = undefined;
196 this.requestedPaths = [];
198 if (_.isUndefined(this.id)) {
199 this.setRootNode([]);
205 private setRootNode(nodes: TreeModel[]) {
206 const tree: TreeModel = {
210 selectionAllowed: false,
214 if (nodes.length > 0) {
215 tree.children = nodes;
220 private firstCall() {
221 this.updateDirectory('/', (nodes) => this.setRootNode(nodes));
224 updateDirectory(path: string, callback: (x: any[]) => void) {
226 !this.requestedPaths.includes(path) &&
227 (path === '/' || this.getSubDirectories(path).length > 0)
229 this.requestedPaths.push(path);
231 .lsDir(this.id, path)
232 .subscribe((data) => this.loadDirectory(data, path, callback));
234 this.getChildren(path, callback);
238 private getSubDirectories(path: string, tree: CephfsDir[] = this.dirs): CephfsDir[] {
239 return tree.filter((d) => d.parent === path);
242 private loadDirectory(data: CephfsDir[], path: string, callback: (x: any[]) => void) {
244 // As always to levels are loaded all sub-directories of the current called path are
245 // already loaded, that's why they are filtered out.
246 data = data.filter((dir) => dir.parent !== path);
248 this.dirs = this.dirs.concat(data);
249 this.getChildren(path, callback);
252 private getChildren(path: string, callback: (x: any[]) => void) {
253 const subTree = this.getSubTree(path);
254 const nodes = _.sortBy(this.getSubDirectories(path), 'path').map((d) => {
255 this.nodeIds[d.path] = d;
256 const newNode: TreeModel = {
259 settings: { static: true }
261 if (this.getSubDirectories(d.path, subTree).length > 0) {
262 // LoadChildren will be triggered if a node is expanded
263 newNode.loadChildren = (treeCallback) => this.updateDirectory(d.path, treeCallback);
270 private getSubTree(path: string): CephfsDir[] {
271 return this.dirs.filter((d) => d.parent.startsWith(path));
275 this.treeComponent.getControllerByNodeId(path).select();
278 onNodeSelected(e: NodeEvent) {
280 this.treeComponent.getControllerByNodeId(node.id).expand();
281 this.setSettings(node);
282 this.selectedDir = this.getDirectory(node);
283 this.selectedNode = node;
286 private setSettings(node: Tree) {
287 const readable = (value: number, fn?: (number) => number | string): number | string =>
288 value ? (fn ? fn(value) : value) : '';
291 this.getQuota(node, 'max_files', readable),
292 this.getQuota(node, 'max_bytes', (value) =>
293 readable(value, (v) => this.dimlessBinaryPipe.transform(v))
301 valueConvertFn: (number) => number | string
303 // Get current maximum
304 const currentPath = tree.id;
305 tree = this.getOrigin(tree, quotaKey);
306 const dir = this.getDirectory(tree);
307 const value = dir.quotas[quotaKey];
308 // Get next tree maximum
309 // => The value that isn't changeable through a change of the current directories quota value
310 let nextMaxValue = value;
311 let nextMaxPath = dir.path;
312 if (tree.id === currentPath) {
313 if (tree.parent.value === '/') {
314 // The value will never inherit any other value, so it has no maximum.
317 const nextMaxDir = this.getDirectory(this.getOrigin(tree.parent, quotaKey));
318 nextMaxValue = nextMaxDir.quotas[quotaKey];
319 nextMaxPath = nextMaxDir.path;
324 name: quotaKey === 'max_bytes' ? this.i18n('Max size') : this.i18n('Max files'),
325 value: valueConvertFn(value),
326 originPath: value ? dir.path : ''
329 dirValue: this.nodeIds[currentPath].quotas[quotaKey],
332 path: nextMaxValue ? nextMaxPath : ''
337 private getOrigin(tree: Tree, quotaSetting: string): Tree {
338 if (tree.parent.value !== '/') {
339 const current = this.getQuotaFromTree(tree, quotaSetting);
340 const originTree = this.getOrigin(tree.parent, quotaSetting);
341 const inherited = this.getQuotaFromTree(originTree, quotaSetting);
343 const useOrigin = current === 0 || (inherited !== 0 && inherited < current);
344 return useOrigin ? originTree : tree;
349 private getQuotaFromTree(tree: Tree, quotaSetting: string): number {
350 return this.getDirectory(tree).quotas[quotaSetting];
353 private getDirectory(node: Tree): CephfsDir {
354 const path = node.id as string;
355 return this.nodeIds[path];
359 const path = this.selectedDir.path;
360 const selection: QuotaSetting = this.quota.selection.first();
361 const nextMax = selection.nextTreeMaximum;
362 const key = selection.quotaKey;
363 const value = selection.dirValue;
364 this.modalService.show(FormModalComponent, {
366 titleText: this.getModalQuotaTitle(
367 value === 0 ? this.actionLabels.SET : this.actionLabels.UPDATE,
370 message: nextMax.value
371 ? this.i18n('The inherited {{quotaValue}} is the maximum value to be used.', {
372 quotaValue: this.getQuotaValueFromPathMsg(nextMax.value, nextMax.path)
375 fields: [this.getQuotaFormField(selection.row.name, key, value, nextMax.value)],
376 submitButtonText: 'Save',
377 onSubmit: (values) => this.updateQuota(values)
382 private getModalQuotaTitle(action: string, path: string): string {
383 return this.i18n("{{action}} CephFS {{quotaName}} quota for '{{path}}'", {
385 quotaName: this.getQuotaName(),
390 private getQuotaName(): string {
391 return this.isBytesQuotaSelected() ? this.i18n('size') : this.i18n('files');
394 private isBytesQuotaSelected(): boolean {
395 return this.quota.selection.first().quotaKey === 'max_bytes';
398 private getQuotaValueFromPathMsg(value: number, path: string): string {
399 return this.i18n("{{quotaName}} quota {{value}} from '{{path}}'", {
400 value: this.isBytesQuotaSelected() ? this.dimlessBinaryPipe.transform(value) : value,
401 quotaName: this.getQuotaName(),
406 private getQuotaFormField(
411 ): CdFormModalFieldConfig {
412 const isBinary = name === 'max_bytes';
413 const formValidators = [isBinary ? CdValidators.binaryMin(0) : Validators.min(0)];
415 formValidators.push(isBinary ? CdValidators.binaryMax(maxValue) : Validators.max(maxValue));
417 const field: CdFormModalFieldConfig = {
418 type: isBinary ? 'binary' : 'number',
422 validators: formValidators,
427 min: this.i18n(`Value has to be at least {{value}} or more`, { value: 0 }),
428 max: this.i18n(`Value has to be at most {{value}} or less`, { value: maxValue })
434 private updateQuota(values: CephfsQuotas, onSuccess?: Function) {
435 const path = this.selectedDir.path;
436 const key = this.quota.selection.first().quotaKey;
438 this.selectedDir.quotas[key] === 0
439 ? this.actionLabels.SET
441 ? this.actionLabels.UNSET
442 : this.i18n('Updated');
443 this.cephfsService.updateQuota(this.id, path, values).subscribe(() => {
447 this.notificationService.show(
448 NotificationType.success,
449 this.getModalQuotaTitle(action, path)
451 this.forceDirRefresh();
456 const path = this.selectedDir.path;
457 const selection: QuotaSetting = this.quota.selection.first();
458 const key = selection.quotaKey;
459 const nextMax = selection.nextTreeMaximum;
460 const dirValue = selection.dirValue;
462 this.modalRef = this.modalService.show(ConfirmationModalComponent, {
464 titleText: this.getModalQuotaTitle(this.actionLabels.UNSET, path),
465 buttonText: this.actionLabels.UNSET,
466 description: this.i18n(`{{action}} {{quotaValue}} {{conclusion}}.`, {
467 action: this.actionLabels.UNSET,
468 quotaValue: this.getQuotaValueFromPathMsg(dirValue, path),
471 ? nextMax.value > dirValue
472 ? this.i18n('in order to inherit {{quotaValue}}', {
473 quotaValue: this.getQuotaValueFromPathMsg(nextMax.value, nextMax.path)
475 : this.i18n("which isn't used because of the inheritance of {{quotaValue}}", {
476 quotaValue: this.getQuotaValueFromPathMsg(nextMax.value, nextMax.path)
478 : this.i18n('in order to have no quota on the directory')
480 onSubmit: () => this.updateQuota({ [key]: 0 }, () => this.modalRef.hide())
486 // Create a snapshot. Auto-generate a snapshot name by default.
487 const path = this.selectedDir.path;
488 this.modalService.show(FormModalComponent, {
490 titleText: this.i18n('Create Snapshot'),
491 message: this.i18n('Please enter the name of the snapshot.'),
496 value: `${moment().toISOString(true)}`,
500 submitButtonText: this.i18n('Create Snapshot'),
501 onSubmit: (values) => {
502 this.cephfsService.mkSnapshot(this.id, path, values.name).subscribe((name) => {
503 this.notificationService.show(
504 NotificationType.success,
505 this.i18n('Created snapshot "{{name}}" for "{{path}}"', {
510 this.forceDirRefresh();
518 * Forces an update of the current selected directory
520 * As all nodes point by their path on an directory object, the easiest way is to update
521 * the objects by merge with their latest change.
523 private forceDirRefresh() {
524 const path = this.selectedNode.parent.id as string;
525 this.cephfsService.lsDir(this.id, path).subscribe((data) => {
526 data.forEach((d) => {
527 Object.assign(this.dirs.find((sub) => sub.path === d.path), d);
530 this.setSettings(this.selectedNode);
534 deleteSnapshotModal() {
535 this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
537 itemDescription: this.i18n('CephFs Snapshot'),
538 itemNames: this.snapshot.selection.selected.map(
539 (snapshot: CephfsSnapshot) => snapshot.name
541 submitAction: () => this.deleteSnapshot()
547 const path = this.selectedDir.path;
548 this.snapshot.selection.selected.forEach((snapshot: CephfsSnapshot) => {
549 const name = snapshot.name;
550 this.cephfsService.rmSnapshot(this.id, path, name).subscribe(() => {
551 this.notificationService.show(
552 NotificationType.success,
553 this.i18n('Deleted snapshot "{{name}}" for "{{path}}"', {
560 this.modalRef.hide();
561 this.forceDirRefresh();