import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { Validators } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
PermissionHelper
} from '../../../../testing/unit-test-helper';
import { CephfsService } from '../../../shared/api/cephfs.service';
+import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component';
+import { FormModalComponent } from '../../../shared/components/form-modal/form-modal.component';
+import { NotificationType } from '../../../shared/enum/notification-type.enum';
+import { CdValidators } from '../../../shared/forms/cd-validators';
+import { CdTableAction } from '../../../shared/models/cd-table-action';
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
import {
CephfsDir,
CephfsQuotas,
CephfsSnapshot
} from '../../../shared/models/cephfs-directory-models';
+import { NotificationService } from '../../../shared/services/notification.service';
import { SharedModule } from '../../../shared/shared.module';
import { CephfsDirectoriesComponent } from './cephfs-directories.component';
let fixture: ComponentFixture<CephfsDirectoriesComponent>;
let cephfsService: CephfsService;
let lsDirSpy;
+ let modalShowSpy;
+ let notificationShowSpy;
+ let minValidator;
+ let maxValidator;
+ let minBinaryValidator;
+ let maxBinaryValidator;
let originalDate;
let modal;
parent: Tree;
createdSnaps: CephfsSnapshot[] | any[];
deletedSnaps: CephfsSnapshot[] | any[];
+ updatedQuotas: { [path: string]: CephfsQuotas };
};
// Object contains mock functions
}
return snapshots;
},
- dir: (path: string, name: string, modifier: number): CephfsDir => {
- const dirPath = `${path === '/' ? '' : path}/${name}`;
- let snapshots = mockLib.snapshots(path, modifier);
+ dir: (parentPath: string, name: string, modifier: number): CephfsDir => {
+ const dirPath = `${parentPath === '/' ? '' : parentPath}/${name}`;
+ let snapshots = mockLib.snapshots(parentPath, modifier);
const extraSnapshots = mockData.createdSnaps.filter((s) => s.path === dirPath);
if (extraSnapshots.length > 0) {
snapshots = snapshots.concat(extraSnapshots);
return {
name,
path: dirPath,
- parent: path,
- quotas: mockLib.quotas(1024 * modifier, 10 * modifier),
+ parent: parentPath,
+ quotas: Object.assign(
+ mockLib.quotas(1024 * modifier, 10 * modifier),
+ mockData.updatedQuotas[dirPath] || {}
+ ),
snapshots: snapshots
};
},
});
return of(name);
},
+ updateQuota: (_id, path, updated: CephfsQuotas) => {
+ mockData.updatedQuotas[path] = Object.assign(mockData.updatedQuotas[path] || {}, updated);
+ return of('Response');
+ },
modalShow: (comp, init) => {
modal = modalServiceShow(comp, init);
return modal.ref;
component.snapshot.selection.selected = snapshots;
component.deleteSnapshotModal();
modal.component.callSubmitAction();
+ },
+ updateQuotaThroughModal: (attribute: string, value: number) => {
+ component.quota.selection.selected = component.settings.filter(
+ (q) => q.quotaKey === attribute
+ );
+ component.updateQuotaModal();
+ modal.component.onSubmitForm({ [attribute]: value });
+ },
+ unsetQuotaThroughModal: (attribute: string) => {
+ component.quota.selection.selected = component.settings.filter(
+ (q) => q.quotaKey === attribute
+ );
+ component.unsetQuotaModal();
+ modal.component.onSubmit();
+ },
+ setFourQuotaDirs: (quotas: number[][]) => {
+ expect(quotas.length).toBe(4); // Make sure this function is used correctly
+ let path = '';
+ quotas.forEach((quota, index) => {
+ index += 1;
+ mockLib.mkDir(path, index.toString(), quota[0], quota[1]);
+ path += '/' + index;
+ });
+ mockData.parent = {
+ value: '3',
+ id: '/1/2/3',
+ parent: {
+ value: '2',
+ id: '/1/2',
+ parent: {
+ value: '1',
+ id: '/1',
+ parent: { value: '/', id: '/' }
+ }
+ }
+ } as Tree;
+ mockLib.selectNode('/1/2/3/4');
}
};
requestedPaths: (expected: string[]) => expect(get.requestedPaths()).toEqual(expected),
snapshotsByName: (snaps: string[]) =>
expect(component.selectedDir.snapshots.map((s) => s.name)).toEqual(snaps),
- quotaSettings: (
- fileValue: number | string,
- fileOrigin: string,
- sizeValue: string,
- sizeOrigin: string
- ) =>
- expect(component.settings).toEqual([
- { name: 'Max files', value: fileValue, origin: fileOrigin },
- { name: 'Max size', value: sizeValue, origin: sizeOrigin }
- ])
+ dirQuotas: (bytes: number, files: number) => {
+ expect(component.selectedDir.quotas).toEqual({ max_bytes: bytes, max_files: files });
+ },
+ noQuota: (key: 'bytes' | 'files') => {
+ assert.quotaRow(key, '', 0, '');
+ },
+ quotaIsNotInherited: (key: 'bytes' | 'files', shownValue, nextMaximum) => {
+ const dir = component.selectedDir;
+ const path = dir.path;
+ assert.quotaRow(key, shownValue, nextMaximum, path);
+ },
+ quotaIsInherited: (key: 'bytes' | 'files', shownValue, path) => {
+ const isBytes = key === 'bytes';
+ const nextMaximum = get.nodeIds()[path].quotas[isBytes ? 'max_bytes' : 'max_files'];
+ assert.quotaRow(key, shownValue, nextMaximum, path);
+ },
+ quotaRow: (
+ key: 'bytes' | 'files',
+ shownValue: number | string,
+ nextTreeMaximum: number,
+ originPath: string
+ ) => {
+ const isBytes = key === 'bytes';
+ expect(component.settings[isBytes ? 1 : 0]).toEqual({
+ row: {
+ name: `Max ${isBytes ? 'size' : key}`,
+ value: shownValue,
+ originPath
+ },
+ quotaKey: `max_${key}`,
+ dirValue: expect.any(Number),
+ nextTreeMaximum: {
+ value: nextTreeMaximum,
+ path: expect.any(String)
+ }
+ });
+ },
+ quotaUnsetModalTexts: (titleText, message, notificationMsg) => {
+ expect(modalShowSpy).toHaveBeenCalledWith(ConfirmationModalComponent, {
+ initialState: expect.objectContaining({
+ titleText,
+ description: message,
+ buttonText: 'Unset'
+ })
+ });
+ expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
+ },
+ quotaUpdateModalTexts: (titleText, message, notificationMsg) => {
+ expect(modalShowSpy).toHaveBeenCalledWith(FormModalComponent, {
+ initialState: expect.objectContaining({
+ titleText,
+ message,
+ submitButtonText: 'Save'
+ })
+ });
+ expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
+ },
+ quotaUpdateModalField: (
+ type: string,
+ label: string,
+ key: string,
+ value: number,
+ max: number,
+ errors?: { [key: string]: string }
+ ) => {
+ expect(modalShowSpy).toHaveBeenCalledWith(FormModalComponent, {
+ initialState: expect.objectContaining({
+ fields: [
+ {
+ type,
+ label,
+ errors,
+ name: key,
+ value,
+ validators: expect.anything(),
+ required: true
+ }
+ ]
+ })
+ });
+ if (type === 'binary') {
+ expect(minBinaryValidator).toHaveBeenCalledWith(0);
+ expect(maxBinaryValidator).toHaveBeenCalledWith(max);
+ } else {
+ expect(minValidator).toHaveBeenCalledWith(0);
+ expect(maxValidator).toHaveBeenCalledWith(max);
+ }
+ }
};
configureTestBed({
nodes: undefined,
parent: undefined,
createdSnaps: [],
- deletedSnaps: []
+ deletedSnaps: [],
+ updatedQuotas: {}
};
originalDate = Date;
spyOn(global, 'Date').and.callFake(mockLib.date);
lsDirSpy = spyOn(cephfsService, 'lsDir').and.callFake(mockLib.lsDir);
spyOn(cephfsService, 'mkSnapshot').and.callFake(mockLib.mkSnapshot);
spyOn(cephfsService, 'rmSnapshot').and.callFake(mockLib.rmSnapshot);
+ spyOn(cephfsService, 'updateQuota').and.callFake(mockLib.updateQuota);
- spyOn(TestBed.get(BsModalService), 'show').and.callFake(mockLib.modalShow);
+ modalShowSpy = spyOn(TestBed.get(BsModalService), 'show').and.callFake(mockLib.modalShow);
+ notificationShowSpy = spyOn(TestBed.get(NotificationService), 'show').and.stub();
fixture = TestBed.createComponent(CephfsDirectoriesComponent);
component = fixture.componentInstance;
'/a/a/b'
]);
});
+
+ describe('test quota update mock', () => {
+ const PATH = '/a';
+ const ID = 2;
+
+ const updateQuota = (quotas: CephfsQuotas) => mockLib.updateQuota(ID, PATH, quotas);
+
+ const expectMockUpdate = (max_bytes?: number, max_files?: number) =>
+ expect(mockData.updatedQuotas[PATH]).toEqual({
+ max_bytes,
+ max_files
+ });
+
+ const expectLsUpdate = (max_bytes?: number, max_files?: number) => {
+ let dir: CephfsDir;
+ mockLib.lsDir(ID, '/').subscribe((dirs) => (dir = dirs.find((d) => d.path === PATH)));
+ expect(dir.quotas).toEqual({
+ max_bytes,
+ max_files
+ });
+ };
+
+ it('tests to set quotas', () => {
+ expectLsUpdate(1024, 10);
+
+ updateQuota({ max_bytes: 512 });
+ expectMockUpdate(512);
+ expectLsUpdate(512, 10);
+
+ updateQuota({ max_files: 100 });
+ expectMockUpdate(512, 100);
+ expectLsUpdate(512, 100);
+ });
+
+ it('tests to unset quotas', () => {
+ updateQuota({ max_files: 0 });
+ expectMockUpdate(undefined, 0);
+ expectLsUpdate(1024, 0);
+
+ updateQuota({ max_bytes: 0 });
+ expectMockUpdate(0, 0);
+ expectLsUpdate(0, 0);
+ });
+ });
});
it('calls lsDir only if an id exits', () => {
mockLib.selectNode('/a');
const dir = get.dirs().find((d) => d.path === '/a');
expect(component.selectedDir).toEqual(dir);
- assert.quotaSettings(10, '/a', '1 KiB', '/a');
+ assert.quotaIsNotInherited('files', 10, 0);
+ assert.quotaIsNotInherited('bytes', '1 KiB', 0);
});
it('should extend the list by subdirectories when expanding and omit already called path', () => {
});
describe('used quotas', () => {
- const setUpDirs = (quotas: number[][]) => {
- let path = '';
- quotas.forEach((quota, index) => {
- index += 1;
- mockLib.mkDir(path, index.toString(), quota[0], quota[1]);
- path += '/' + index;
- });
- mockData.parent = {
- value: '3',
- id: '/1/2/3',
- parent: {
- value: '2',
- id: '/1/2',
- parent: {
- value: '1',
- id: '/1',
- parent: { value: '/', id: '/' }
- }
- }
- } as Tree;
- mockLib.selectNode('/1/2/3/4');
- };
-
it('should use no quota if none is set', () => {
- setUpDirs([[0, 0], [0, 0], [0, 0], [0, 0]]);
- assert.quotaSettings('', '', '', '');
+ mockLib.setFourQuotaDirs([[0, 0], [0, 0], [0, 0], [0, 0]]);
+ assert.noQuota('files');
+ assert.noQuota('bytes');
+ assert.dirQuotas(0, 0);
});
it('should use quota from upper parents', () => {
- setUpDirs([[100, 0], [0, 8], [0, 0], [0, 0]]);
- assert.quotaSettings(100, '/1', '8 KiB', '/1/2');
+ mockLib.setFourQuotaDirs([[100, 0], [0, 8], [0, 0], [0, 0]]);
+ assert.quotaIsInherited('files', 100, '/1');
+ assert.quotaIsInherited('bytes', '8 KiB', '/1/2');
+ assert.dirQuotas(0, 0);
});
it('should use quota from the parent with the lowest value (deep inheritance)', () => {
- setUpDirs([[200, 1], [100, 4], [400, 3], [300, 2]]);
- assert.quotaSettings(100, '/1/2', '1 KiB', '/1');
+ mockLib.setFourQuotaDirs([[200, 1], [100, 4], [400, 3], [300, 2]]);
+ assert.quotaIsInherited('files', 100, '/1/2');
+ assert.quotaIsInherited('bytes', '1 KiB', '/1');
+ assert.dirQuotas(2048, 300);
});
it('should use current value', () => {
- setUpDirs([[200, 2], [300, 4], [400, 3], [100, 1]]);
- assert.quotaSettings(100, '/1/2/3/4', '1 KiB', '/1/2/3/4');
+ mockLib.setFourQuotaDirs([[200, 2], [300, 4], [400, 3], [100, 1]]);
+ assert.quotaIsNotInherited('files', 100, 200);
+ assert.quotaIsNotInherited('bytes', '1 KiB', 2048);
+ assert.dirQuotas(1024, 100);
});
});
});
mockLib.deleteSnapshotsThroughModal(component.selectedDir.snapshots);
assert.snapshotsByName([]);
});
-
- afterEach(() => {
- // Makes sure the directory is updated correctly
- expect(component.selectedDir).toEqual(get.nodeIds()[component.selectedDir.path]);
- });
});
it('should test all snapshot table actions combinations', () => {
}
});
});
+
+ describe('quotas', () => {
+ beforeEach(() => {
+ // Spies
+ minValidator = spyOn(Validators, 'min').and.callThrough();
+ maxValidator = spyOn(Validators, 'max').and.callThrough();
+ minBinaryValidator = spyOn(CdValidators, 'binaryMin').and.callThrough();
+ maxBinaryValidator = spyOn(CdValidators, 'binaryMax').and.callThrough();
+ // Select /a/c/b
+ mockLib.changeId(1);
+ mockLib.selectNode('/a');
+ mockLib.selectNode('/a/c');
+ mockLib.selectNode('/a/c/b');
+ // Quotas after selection
+ assert.quotaIsInherited('files', 10, '/a');
+ assert.quotaIsInherited('bytes', '1 KiB', '/a');
+ assert.dirQuotas(2048, 20);
+ });
+
+ describe('update modal', () => {
+ describe('max_files', () => {
+ beforeEach(() => {
+ mockLib.updateQuotaThroughModal('max_files', 5);
+ });
+
+ it('should update max_files correctly', () => {
+ expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 5 });
+ assert.quotaIsNotInherited('files', 5, 10);
+ });
+
+ it('uses the correct form field', () => {
+ assert.quotaUpdateModalField('number', 'Max files', 'max_files', 20, 10, {
+ min: 'Value has to be at least 0 or more',
+ max: 'Value has to be at most 10 or less'
+ });
+ });
+
+ it('shows the right texts', () => {
+ assert.quotaUpdateModalTexts(
+ "Update CephFS files quota for '/a/c/b'",
+ "The inherited files quota 10 from '/a' is the maximum value to be used.",
+ "Updated CephFS files quota for '/a/c/b'"
+ );
+ });
+ });
+
+ describe('max_bytes', () => {
+ beforeEach(() => {
+ mockLib.updateQuotaThroughModal('max_bytes', 512);
+ });
+
+ it('should update max_files correctly', () => {
+ expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 512 });
+ assert.quotaIsNotInherited('bytes', '512 B', 1024);
+ });
+
+ it('uses the correct form field', () => {
+ mockLib.updateQuotaThroughModal('max_bytes', 512);
+ assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 2048, 1024);
+ });
+
+ it('shows the right texts', () => {
+ assert.quotaUpdateModalTexts(
+ "Update CephFS size quota for '/a/c/b'",
+ "The inherited size quota 1 KiB from '/a' is the maximum value to be used.",
+ "Updated CephFS size quota for '/a/c/b'"
+ );
+ });
+ });
+
+ describe('action behaviour', () => {
+ it('opens with next maximum as maximum if directory holds the current maximum', () => {
+ mockLib.updateQuotaThroughModal('max_bytes', 512);
+ mockLib.updateQuotaThroughModal('max_bytes', 888);
+ assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 512, 1024);
+ });
+
+ it("uses 'Set' action instead of 'Update' if the quota is not set (0)", () => {
+ mockLib.updateQuotaThroughModal('max_bytes', 0);
+ mockLib.updateQuotaThroughModal('max_bytes', 200);
+ assert.quotaUpdateModalTexts(
+ "Set CephFS size quota for '/a/c/b'",
+ "The inherited size quota 1 KiB from '/a' is the maximum value to be used.",
+ "Set CephFS size quota for '/a/c/b'"
+ );
+ });
+ });
+ });
+
+ describe('unset modal', () => {
+ describe('max_files', () => {
+ beforeEach(() => {
+ mockLib.updateQuotaThroughModal('max_files', 5); // Sets usable quota
+ mockLib.unsetQuotaThroughModal('max_files');
+ });
+
+ it('should unset max_files correctly', () => {
+ expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 0 });
+ assert.dirQuotas(2048, 0);
+ });
+
+ it('shows the right texts', () => {
+ assert.quotaUnsetModalTexts(
+ "Unset CephFS files quota for '/a/c/b'",
+ "Unset files quota 5 from '/a/c/b' in order to inherit files quota 10 from '/a'.",
+ "Unset CephFS files quota for '/a/c/b'"
+ );
+ });
+ });
+
+ describe('max_bytes', () => {
+ beforeEach(() => {
+ mockLib.updateQuotaThroughModal('max_bytes', 512); // Sets usable quota
+ mockLib.unsetQuotaThroughModal('max_bytes');
+ });
+
+ it('should unset max_files correctly', () => {
+ expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 0 });
+ assert.dirQuotas(0, 20);
+ });
+
+ it('shows the right texts', () => {
+ assert.quotaUnsetModalTexts(
+ "Unset CephFS size quota for '/a/c/b'",
+ "Unset size quota 512 B from '/a/c/b' in order to inherit size quota 1 KiB from '/a'.",
+ "Unset CephFS size quota for '/a/c/b'"
+ );
+ });
+ });
+
+ describe('action behaviour', () => {
+ it('uses different Text if no quota is inherited', () => {
+ mockLib.selectNode('/a');
+ mockLib.unsetQuotaThroughModal('max_bytes');
+ assert.quotaUnsetModalTexts(
+ "Unset CephFS size quota for '/a'",
+ "Unset size quota 1 KiB from '/a' in order to have no quota on the directory.",
+ "Unset CephFS size quota for '/a'"
+ );
+ });
+
+ it('uses different Text if quota is already inherited', () => {
+ mockLib.unsetQuotaThroughModal('max_bytes');
+ assert.quotaUnsetModalTexts(
+ "Unset CephFS size quota for '/a/c/b'",
+ "Unset size quota 2 KiB from '/a/c/b' which isn't used because of the inheritance " +
+ "of size quota 1 KiB from '/a'.",
+ "Unset CephFS size quota for '/a/c/b'"
+ );
+ });
+ });
+ });
+ });
+
+ describe('table actions', () => {
+ let actions: CdTableAction[];
+
+ const empty = (): CdTableSelection => {
+ const selection = new CdTableSelection();
+ return selection;
+ };
+
+ const select = (value: number): CdTableSelection => {
+ const selection = new CdTableSelection();
+ selection.selected = [{ dirValue: value }];
+ return selection;
+ };
+
+ beforeEach(() => {
+ actions = component.quota.tableActions;
+ });
+
+ it("shows 'Set' for empty and not set quotas", () => {
+ const isSetVisible = actions[0].visible;
+ expect(isSetVisible(empty())).toBe(true);
+ expect(isSetVisible(select(0))).toBe(true);
+ expect(isSetVisible(select(1))).toBe(false);
+ });
+
+ it("shows 'Update' for set quotas only", () => {
+ const isUpdateVisible = actions[1].visible;
+ expect(isUpdateVisible(empty())).toBeFalsy();
+ expect(isUpdateVisible(select(0))).toBe(false);
+ expect(isUpdateVisible(select(1))).toBe(true);
+ });
+
+ it("only enables 'Unset' for set quotas only", () => {
+ const isUnsetDisabled = actions[2].disable;
+ expect(isUnsetDisabled(empty())).toBe(true);
+ expect(isUnsetDisabled(select(0))).toBe(true);
+ expect(isUnsetDisabled(select(1))).toBe(false);
+ });
+
+ it('should test all quota table actions permission combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions = permissionHelper.setPermissionsAndGetActions(
+ component.quota.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Set', 'Update', 'Unset'],
+ primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ },
+ 'create,update': {
+ actions: ['Set', 'Update', 'Unset'],
+ primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ },
+ 'create,delete': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ create: {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ 'update,delete': {
+ actions: ['Set', 'Update', 'Unset'],
+ primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ },
+ update: {
+ actions: ['Set', 'Update', 'Unset'],
+ primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ },
+ delete: {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+ });
});
import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Validators } from '@angular/forms';
import { I18n } from '@ngx-translate/i18n-polyfill';
import * as _ from 'lodash';
import * as moment from 'moment';
import { NodeEvent, Tree, TreeComponent, TreeModel } from 'ng2-tree';
-
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
+
import { CephfsService } from '../../../shared/api/cephfs.service';
+import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component';
import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { FormModalComponent } from '../../../shared/components/form-modal/form-modal.component';
+import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
import { Icons } from '../../../shared/enum/icons.enum';
import { NotificationType } from '../../../shared/enum/notification-type.enum';
+import { CdValidators } from '../../../shared/forms/cd-validators';
+import { CdFormModalFieldConfig } from '../../../shared/models/cd-form-modal-field-config';
import { CdTableAction } from '../../../shared/models/cd-table-action';
import { CdTableColumn } from '../../../shared/models/cd-table-column';
import { CdTableSelection } from '../../../shared/models/cd-table-selection';
-import { CephfsDir, CephfsSnapshot } from '../../../shared/models/cephfs-directory-models';
+import {
+ CephfsDir,
+ CephfsQuotas,
+ CephfsSnapshot
+} from '../../../shared/models/cephfs-directory-models';
import { Permission } from '../../../shared/models/permissions';
import { CdDatePipe } from '../../../shared/pipes/cd-date.pipe';
import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
import { NotificationService } from '../../../shared/services/notification.service';
+class QuotaSetting {
+ row: {
+ // Shows quota that is used for current directory
+ name: string;
+ value: number | string;
+ originPath: string;
+ };
+ quotaKey: string;
+ dirValue: number;
+ nextTreeMaximum: {
+ value: number;
+ path: string;
+ };
+}
+
@Component({
selector: 'cd-cephfs-directories',
templateUrl: './cephfs-directories.component.html',
permission: Permission;
selectedDir: CephfsDir;
- settings: {
- name: string;
- value: number | string;
- origin: string;
- }[];
- settingsColumns: CdTableColumn[];
+ settings: QuotaSetting[];
+ quota: {
+ columns: CdTableColumn[];
+ selection: CdTableSelection;
+ tableActions: CdTableAction[];
+ updateSelection: Function;
+ };
snapshot: {
columns: CdTableColumn[];
selection: CdTableSelection;
private cephfsService: CephfsService,
private cdDatePipe: CdDatePipe,
private i18n: I18n,
+ private actionLabels: ActionLabelsI18n,
private notificationService: NotificationService,
private dimlessBinaryPipe: DimlessBinaryPipe
) {}
ngOnInit() {
this.permission = this.authStorageService.getPermissions().cephfs;
- this.settingsColumns = [
- {
- prop: 'name',
- name: this.i18n('Name'),
- flexGrow: 1
- },
- {
- prop: 'value',
- name: this.i18n('Value'),
- sortable: false,
- flexGrow: 1
+ this.quota = {
+ columns: [
+ {
+ prop: 'row.name',
+ name: this.i18n('Name'),
+ flexGrow: 1
+ },
+ {
+ prop: 'row.value',
+ name: this.i18n('Value'),
+ sortable: false,
+ flexGrow: 1
+ },
+ {
+ prop: 'row.originPath',
+ name: this.i18n('Origin'),
+ sortable: false,
+ cellTemplate: this.originTmpl,
+ flexGrow: 1
+ }
+ ],
+ selection: new CdTableSelection(),
+ updateSelection: (selection: CdTableSelection) => {
+ this.quota.selection = selection;
},
- {
- prop: 'origin',
- name: this.i18n('Origin'),
- sortable: false,
- cellTemplate: this.originTmpl,
- flexGrow: 1
- }
- ];
+ tableActions: [
+ {
+ name: this.actionLabels.SET,
+ icon: Icons.edit,
+ permission: 'update',
+ visible: (selection) =>
+ !selection.hasSelection || (selection.first() && selection.first().dirValue === 0),
+ click: () => this.updateQuotaModal()
+ },
+ {
+ name: this.actionLabels.UPDATE,
+ icon: Icons.edit,
+ permission: 'update',
+ visible: (selection) => selection.first() && selection.first().dirValue > 0,
+ click: () => this.updateQuotaModal()
+ },
+ {
+ name: this.actionLabels.UNSET,
+ icon: Icons.destroy,
+ permission: 'update',
+ disable: (selection) =>
+ !selection.hasSelection || (selection.first() && selection.first().dirValue === 0),
+ click: () => this.unsetQuotaModal()
+ }
+ ]
+ };
this.snapshot = {
columns: [
{
},
tableActions: [
{
- name: this.i18n('Create'),
+ name: this.actionLabels.CREATE,
icon: Icons.add,
permission: 'create',
canBePrimary: (selection) => !selection.hasSelection,
click: () => this.createSnapshot()
},
{
- name: this.i18n('Delete'),
+ name: this.actionLabels.DELETE,
icon: Icons.destroy,
permission: 'delete',
click: () => this.deleteSnapshotModal(),
}
private setSettings(node: Tree) {
- const files = this.getQuota(node, 'max_files');
- const size = this.getQuota(node, 'max_bytes');
+ const readable = (value: number, fn?: (number) => number | string): number | string =>
+ value ? (fn ? fn(value) : value) : '';
+
this.settings = [
- {
- name: 'Max files',
- value: files.value,
- origin: files.origin
- },
- {
- name: 'Max size',
- value: size.value !== '' ? this.dimlessBinaryPipe.transform(size.value) : '',
- origin: size.origin
- }
+ this.getQuota(node, 'max_files', readable),
+ this.getQuota(node, 'max_bytes', (value) =>
+ readable(value, (v) => this.dimlessBinaryPipe.transform(v))
+ )
];
}
- private getQuota(tree: Tree, quotaSetting: string): { value: string; origin: string } {
- tree = this.getOrigin(tree, quotaSetting);
+ private getQuota(
+ tree: Tree,
+ quotaKey: string,
+ valueConvertFn: (number) => number | string
+ ): QuotaSetting {
+ // Get current maximum
+ const currentPath = tree.id;
+ tree = this.getOrigin(tree, quotaKey);
const dir = this.getDirectory(tree);
- const value = dir.quotas[quotaSetting];
+ const value = dir.quotas[quotaKey];
+ // Get next tree maximum
+ // => The value that isn't changeable through a change of the current directories quota value
+ let nextMaxValue = value;
+ let nextMaxPath = dir.path;
+ if (tree.id === currentPath) {
+ if (tree.parent.value === '/') {
+ // The value will never inherit any other value, so it has no maximum.
+ nextMaxValue = 0;
+ } else {
+ const nextMaxDir = this.getDirectory(this.getOrigin(tree.parent, quotaKey));
+ nextMaxValue = nextMaxDir.quotas[quotaKey];
+ nextMaxPath = nextMaxDir.path;
+ }
+ }
return {
- value: value ? value : '',
- origin: value ? dir.path : ''
+ row: {
+ name: quotaKey === 'max_bytes' ? this.i18n('Max size') : this.i18n('Max files'),
+ value: valueConvertFn(value),
+ originPath: value ? dir.path : ''
+ },
+ quotaKey,
+ dirValue: this.nodeIds[currentPath].quotas[quotaKey],
+ nextTreeMaximum: {
+ value: nextMaxValue,
+ path: nextMaxValue ? nextMaxPath : ''
+ }
};
}
return this.nodeIds[path];
}
+ updateQuotaModal() {
+ const path = this.selectedDir.path;
+ const selection: QuotaSetting = this.quota.selection.first();
+ const nextMax = selection.nextTreeMaximum;
+ const key = selection.quotaKey;
+ const value = selection.dirValue;
+ this.modalService.show(FormModalComponent, {
+ initialState: {
+ titleText: this.getModalQuotaTitle(
+ value === 0 ? this.actionLabels.SET : this.actionLabels.UPDATE,
+ path
+ ),
+ message: nextMax.value
+ ? this.i18n('The inherited {{quotaValue}} is the maximum value to be used.', {
+ quotaValue: this.getQuotaValueFromPathMsg(nextMax.value, nextMax.path)
+ })
+ : undefined,
+ fields: [this.getQuotaFormField(selection.row.name, key, value, nextMax.value)],
+ submitButtonText: 'Save',
+ onSubmit: (values) => this.updateQuota(values)
+ }
+ });
+ }
+
+ private getModalQuotaTitle(action: string, path: string): string {
+ return this.i18n("{{action}} CephFS {{quotaName}} quota for '{{path}}'", {
+ action,
+ quotaName: this.getQuotaName(),
+ path
+ });
+ }
+
+ private getQuotaName(): string {
+ return this.isBytesQuotaSelected() ? this.i18n('size') : this.i18n('files');
+ }
+
+ private isBytesQuotaSelected(): boolean {
+ return this.quota.selection.first().quotaKey === 'max_bytes';
+ }
+
+ private getQuotaValueFromPathMsg(value: number, path: string): string {
+ return this.i18n("{{quotaName}} quota {{value}} from '{{path}}'", {
+ value: this.isBytesQuotaSelected() ? this.dimlessBinaryPipe.transform(value) : value,
+ quotaName: this.getQuotaName(),
+ path
+ });
+ }
+
+ private getQuotaFormField(
+ label: string,
+ name: string,
+ value: number,
+ maxValue: number
+ ): CdFormModalFieldConfig {
+ const isBinary = name === 'max_bytes';
+ const formValidators = [isBinary ? CdValidators.binaryMin(0) : Validators.min(0)];
+ if (maxValue) {
+ formValidators.push(isBinary ? CdValidators.binaryMax(maxValue) : Validators.max(maxValue));
+ }
+ const field: CdFormModalFieldConfig = {
+ type: isBinary ? 'binary' : 'number',
+ label,
+ name,
+ value,
+ validators: formValidators,
+ required: true
+ };
+ if (!isBinary) {
+ field.errors = {
+ min: this.i18n(`Value has to be at least {{value}} or more`, { value: 0 }),
+ max: this.i18n(`Value has to be at most {{value}} or less`, { value: maxValue })
+ };
+ }
+ return field;
+ }
+
+ private updateQuota(values: CephfsQuotas, onSuccess?: Function) {
+ const path = this.selectedDir.path;
+ const key = this.quota.selection.first().quotaKey;
+ const action =
+ this.selectedDir.quotas[key] === 0
+ ? this.actionLabels.SET
+ : values[key] === 0
+ ? this.actionLabels.UNSET
+ : this.i18n('Updated');
+ this.cephfsService.updateQuota(this.id, path, values).subscribe(() => {
+ if (onSuccess) {
+ onSuccess();
+ }
+ this.notificationService.show(
+ NotificationType.success,
+ this.getModalQuotaTitle(action, path)
+ );
+ this.forceDirRefresh();
+ });
+ }
+
+ unsetQuotaModal() {
+ const path = this.selectedDir.path;
+ const selection: QuotaSetting = this.quota.selection.first();
+ const key = selection.quotaKey;
+ const nextMax = selection.nextTreeMaximum;
+ const dirValue = selection.dirValue;
+
+ this.modalRef = this.modalService.show(ConfirmationModalComponent, {
+ initialState: {
+ titleText: this.getModalQuotaTitle(this.actionLabels.UNSET, path),
+ buttonText: this.actionLabels.UNSET,
+ description: this.i18n(`{{action}} {{quotaValue}} {{conclusion}}.`, {
+ action: this.actionLabels.UNSET,
+ quotaValue: this.getQuotaValueFromPathMsg(dirValue, path),
+ conclusion:
+ nextMax.value > 0
+ ? nextMax.value > dirValue
+ ? this.i18n('in order to inherit {{quotaValue}}', {
+ quotaValue: this.getQuotaValueFromPathMsg(nextMax.value, nextMax.path)
+ })
+ : this.i18n("which isn't used because of the inheritance of {{quotaValue}}", {
+ quotaValue: this.getQuotaValueFromPathMsg(nextMax.value, nextMax.path)
+ })
+ : this.i18n('in order to have no quota on the directory')
+ }),
+ onSubmit: () => this.updateQuota({ [key]: 0 }, () => this.modalRef.hide())
+ }
+ });
+ }
+
createSnapshot() {
// Create a snapshot. Auto-generate a snapshot name by default.
const path = this.selectedDir.path;
message: this.i18n('Please enter the name of the snapshot.'),
fields: [
{
- type: 'inputText',
+ type: 'text',
name: 'name',
value: `${moment().toISOString(true)}`,
required: true
*/
private forceDirRefresh() {
const path = this.selectedNode.parent.id as string;
- this.cephfsService.lsDir(this.id, path).subscribe((data) =>
+ this.cephfsService.lsDir(this.id, path).subscribe((data) => {
data.forEach((d) => {
Object.assign(this.dirs.find((sub) => sub.path === d.path), d);
- })
- );
+ });
+ // Now update quotas
+ this.setSettings(this.selectedNode);
+ });
}
deleteSnapshotModal() {
this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
initialState: {
- itemDescription: 'CephFs Snapshot',
+ itemDescription: this.i18n('CephFs Snapshot'),
itemNames: this.snapshot.selection.selected.map(
(snapshot: CephfsSnapshot) => snapshot.name
),