self.assertIn(key, data)
self.assertIsNotNone(data[key])
+ def get_fs_id(self):
+ return self.fs.get_namespace_id()
+
+ def mk_dirs(self, path, expectedStatus=200):
+ self._post("/api/cephfs/{}/mk_dirs".format(self.get_fs_id()),
+ params={'path': path})
+ self.assertStatus(expectedStatus)
+
+ def rm_dir(self, path, expectedStatus=200):
+ self._post("/api/cephfs/{}/rm_dir".format(self.get_fs_id()),
+ params={'path': path})
+ self.assertStatus(expectedStatus)
+
+ def ls_dir(self, path, expectedLength, depth = None):
+ params = {'path': path}
+ if depth is not None:
+ params['depth'] = depth
+ data = self._get("/api/cephfs/{}/ls_dir".format(self.get_fs_id()),
+ params=params)
+ self.assertStatus(200)
+ self.assertIsInstance(data, list)
+ self.assertEqual(len(data), expectedLength)
+ return data
+
+
@DashboardTestCase.RunAs('test', 'test', ['block-manager'])
def test_access_permissions(self):
- fs_id = self.fs.get_namespace_id()
+ fs_id = self.get_fs_id()
self._get("/api/cephfs/{}/clients".format(fs_id))
self.assertStatus(403)
self._get("/api/cephfs/{}".format(fs_id))
self.assertStatus(403)
def test_cephfs_clients(self):
- fs_id = self.fs.get_namespace_id()
+ fs_id = self.get_fs_id()
data = self._get("/api/cephfs/{}/clients".format(fs_id))
self.assertStatus(200)
self.assertIn('data', data)
def test_cephfs_evict_client_does_not_exist(self):
- fs_id = self.fs.get_namespace_id()
+ fs_id = self.get_fs_id()
data = self._delete("/api/cephfs/{}/client/1234".format(fs_id))
self.assertStatus(404)
def test_cephfs_get(self):
- fs_id = self.fs.get_namespace_id()
+ fs_id = self.get_fs_id()
data = self._get("/api/cephfs/{}/".format(fs_id))
self.assertStatus(200)
self.assertToHave(data, 'versions')
def test_cephfs_mds_counters(self):
- fs_id = self.fs.get_namespace_id()
+ fs_id = self.get_fs_id()
data = self._get("/api/cephfs/{}/mds_counters".format(fs_id))
self.assertStatus(200)
self.assertToHave(cephfs, 'mdsmap')
def test_cephfs_tabs(self):
- fs_id = self.fs.get_namespace_id()
+ fs_id = self.get_fs_id()
data = self._get("/ui-api/cephfs/{}/tabs".format(fs_id))
self.assertStatus(200)
self.assertIsInstance(data, dict)
self.assertIsInstance(clients['status'], int)
def test_ls_mk_rm_dir(self):
- fs_id = self.fs.get_namespace_id()
- data = self._get("/api/cephfs/{}/ls_dir".format(fs_id),
- params={'path': '/'})
- self.assertStatus(200)
- self.assertIsInstance(data, list)
- self.assertEqual(len(data), 0)
+ self.ls_dir('/', 0)
- self._post("/api/cephfs/{}/mk_dirs".format(fs_id),
- params={'path': '/pictures/birds'})
- self.assertStatus(200)
+ self.mk_dirs('/pictures/birds')
+ self.ls_dir('/', 2, 3)
+ self.ls_dir('/pictures', 1)
- data = self._get("/api/cephfs/{}/ls_dir".format(fs_id),
- params={'path': '/pictures'})
- self.assertStatus(200)
- self.assertIsInstance(data, list)
- self.assertEqual(len(data), 1)
-
- self._post("/api/cephfs/{}/rm_dir".format(fs_id),
- params={'path': '/pictures'})
- self.assertStatus(500)
- self._post("/api/cephfs/{}/rm_dir".format(fs_id),
- params={'path': '/pictures/birds'})
- self.assertStatus(200)
- self._post("/api/cephfs/{}/rm_dir".format(fs_id),
- params={'path': '/pictures'})
- self.assertStatus(200)
+ self.rm_dir('/pictures', 500)
+ self.rm_dir('/pictures/birds')
+ self.rm_dir('/pictures')
- data = self._get("/api/cephfs/{}/ls_dir".format(fs_id),
- params={'path': '/'})
- self.assertStatus(200)
- self.assertIsInstance(data, list)
- self.assertEqual(len(data), 0)
+ self.ls_dir('/', 0)
def test_snapshots(self):
- fs_id = self.fs.get_namespace_id()
- self._post("/api/cephfs/{}/mk_dirs".format(fs_id),
- params={'path': '/movies/dune'})
- self.assertStatus(200)
+ fs_id = self.get_fs_id()
+ self.mk_dirs('/movies/dune/extended_version')
self._post("/api/cephfs/{}/mk_snapshot".format(fs_id),
params={'path': '/movies/dune', 'name': 'test'})
self.assertStatus(200)
- data = self._get("/api/cephfs/{}/ls_dir".format(fs_id),
- params={'path': '/movies'})
- self.assertStatus(200)
- self.assertIsInstance(data, list)
- self.assertEqual(len(data), 1)
+ data = self.ls_dir('/movies', 1)
self.assertSchema(data[0], JObj(sub_elems={
'name': JLeaf(str),
'path': JLeaf(str),
+ 'parent': JLeaf(str),
'snapshots': JList(JObj(sub_elems={
'name': JLeaf(str),
'path': JLeaf(str),
self.assertEqual(snapshot['name'], "test")
self.assertEqual(snapshot['path'], "/movies/dune/.snap/test")
+ # Should have filtered out "_test_$timestamp"
+ data = self.ls_dir('/movies/dune', 1)
+ snapshots = data[0]['snapshots']
+ self.assertEqual(len(snapshots), 0)
+
self._post("/api/cephfs/{}/rm_snapshot".format(fs_id),
params={'path': '/movies/dune', 'name': 'test'})
self.assertStatus(200)
- data = self._get("/api/cephfs/{}/ls_dir".format(fs_id),
- params={'path': '/movies'})
- self.assertStatus(200)
+ data = self.ls_dir('/movies', 1)
self.assertEqual(len(data[0]['snapshots']), 0)
- # Cleanup. Note, the CephFS Python extension (and therefor the Dashoard
+ # Cleanup. Note, the CephFS Python extension (and therefor the Dashboard
# REST API) does not support recursive deletion of a directory.
- self._post("/api/cephfs/{}/rm_dir".format(fs_id),
- params={'path': '/movies/dune'})
- self.assertStatus(200)
- self._post("/api/cephfs/{}/rm_dir".format(fs_id),
- params={'path': '/movies'})
- self.assertStatus(200)
+ self.rm_dir('/movies/dune/extended_version')
+ self.rm_dir('/movies/dune')
+ self.rm_dir('/movies')
return CephFS_(fs_name)
@RESTController.Resource('GET')
- def ls_dir(self, fs_id, path=None):
+ def ls_dir(self, fs_id, path=None, depth=1):
"""
List directories of specified path.
:param fs_id: The filesystem identifier.
path = os.path.normpath(path)
try:
cfs = self._cephfs_instance(fs_id)
- paths = cfs.ls_dir(path, 1)
+ paths = cfs.ls_dir(path, int(depth))
# Convert (bytes => string), prettify paths (strip slashes)
# and append additional information.
paths = [{
'name': os.path.basename(p.decode()),
'path': p.decode(),
+ 'parent': os.path.dirname(p.decode()),
'snapshots': cfs.ls_snapshots(p.decode()),
'quotas': cfs.get_quotas(p.decode())
} for p in paths if p != path.encode()]
--- /dev/null
+<div class="row">
+ <div class="col-sm-4">
+ <tree [tree]="tree"
+ (nodeSelected)="onNodeSelected($event)">
+ <ng-template let-node>
+ <span class="node-name"
+ [innerHTML]="node.value"></span>
+ </ng-template>
+ </tree>
+ </div>
+ <!-- Selection details -->
+ <div class="col-sm-8 metadata"
+ *ngIf="selectedDir">
+ <div class="card">
+ <div class="card-header">
+ {{ selectedDir.path }}
+ </div>
+ <div class="card-body">
+ <legend i18n>Quotas</legend>
+ <cd-table [data]="settings"
+ [columns]="settingsColumns"
+ [limit]="0"
+ [footer]="false"
+ [toolHeader]="false">
+ </cd-table>
+
+ <legend i18n>Snapshots</legend>
+ <cd-table [sorts]="snapshot.sortProperties"
+ [data]="selectedDir.snapshots"
+ [columns]="snapshot.columns">
+ </cd-table>
+ </div>
+ </div>
+ </div>
+</div>
+
+<ng-template #origin
+ let-row="row"
+ let-value="value">
+ <span class="quota-origin" (click)="selectOrigin(value)">{{value}}</span>
+</ng-template>
--- /dev/null
+@import 'ng2-tree.scss';
+
+.quota-origin {
+ &:hover {
+ color: #212121;
+ }
+ cursor: pointer;
+ color: #2b99a8;
+}
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NodeEvent, Tree, TreeModel, TreeModule } from 'ng2-tree';
+import { of } from 'rxjs';
+
+import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
+import { CephfsService } from '../../../shared/api/cephfs.service';
+import {
+ CephfsDir,
+ CephfsQuotas,
+ CephfsSnapshot
+} from '../../../shared/models/cephfs-directory-models';
+import { SharedModule } from '../../../shared/shared.module';
+import { CephfsDirectoriesComponent } from './cephfs-directories.component';
+
+describe('CephfsDirectoriesComponent', () => {
+ let component: CephfsDirectoriesComponent;
+ let fixture: ComponentFixture<CephfsDirectoriesComponent>;
+ let lsDirSpy;
+ let originalDate;
+
+ // Get's private attributes or functions
+ const get = {
+ nodeIds: (): { [path: string]: CephfsDir } => component['nodeIds'],
+ dirs: (): CephfsDir[] => component['dirs'],
+ requestedPaths: (): string[] => component['requestedPaths']
+ };
+
+ // Object contains mock data that will be reset before each test.
+ let mockData: {
+ nodes: TreeModel[];
+ parent: Tree;
+ };
+
+ // Object contains mock functions
+ const mockLib = {
+ quotas: (max_bytes: number, max_files: number): CephfsQuotas => ({ max_bytes, max_files }),
+ snapshots: (dirPath: string, howMany: number): CephfsSnapshot[] => {
+ const name = 'someSnapshot';
+ const snapshots = [];
+ for (let i = 0; i < howMany; i++) {
+ const path = `${dirPath}/.snap/${name}${i}`;
+ const created = new Date(
+ +new Date() - 3600 * 24 * 1000 * howMany * (howMany - i)
+ ).toString();
+ snapshots.push({ name, path, created });
+ }
+ return snapshots;
+ },
+ dir: (path: string, name: string, modifier: number): CephfsDir => {
+ const dirPath = `${path === '/' ? '' : path}/${name}`;
+ return {
+ name,
+ path: dirPath,
+ parent: path,
+ quotas: mockLib.quotas(1024 * modifier, 10 * modifier),
+ snapshots: mockLib.snapshots(path, modifier)
+ };
+ },
+ // Only used inside other mocks
+ lsSingleDir: (path = ''): CephfsDir[] => {
+ if (path.includes('b')) {
+ // 'b' has no sub directories
+ return [];
+ }
+ return [
+ // Directories are not sorted!
+ mockLib.dir(path, 'c', 3),
+ mockLib.dir(path, 'a', 1),
+ mockLib.dir(path, 'b', 2)
+ ];
+ },
+ lsDir: (_id: number, path = '') => {
+ // will return 2 levels deep
+ let data = mockLib.lsSingleDir(path);
+ const paths = data.map((dir) => dir.path);
+ paths.forEach((pathL2) => {
+ data = data.concat(mockLib.lsSingleDir(pathL2));
+ });
+ return of(data);
+ },
+ date: (arg) => (arg ? new originalDate(arg) : new Date('2022-02-22T00:00:00')),
+ getControllerByPath: (path: string) => {
+ return {
+ expand: () => mockLib.expand(path),
+ select: () => component.onNodeSelected(mockLib.getNodeEvent(path))
+ };
+ },
+ // Only used inside other mocks to mock "tree.expand" of every node
+ expand: (path: string) => {
+ component.updateDirectory(path, (nodes) => (mockData.nodes = mockData.nodes.concat(nodes)));
+ },
+ getNodeEvent: (path: string): NodeEvent => {
+ const tree = mockData.nodes.find((n) => n.id === path) as Tree;
+ if (mockData.parent) {
+ tree.parent = mockData.parent;
+ } else {
+ const dir = get.nodeIds()[path];
+ const parentNode = mockData.nodes.find((n) => n.id === dir.parent);
+ tree.parent = parentNode as Tree;
+ }
+ return { node: tree } as NodeEvent;
+ },
+ changeId: (id: number) => {
+ component.id = id;
+ component.ngOnChanges();
+ mockData.nodes = [component.tree].concat(component.tree.children);
+ },
+ selectNode: (path: string) => {
+ mockLib.getControllerByPath(path).select();
+ },
+ mkDir: (path: string, name: string, maxFiles: number, maxBytes: number) => {
+ const dir = mockLib.dir(path, name, 3);
+ dir.quotas.max_bytes = maxBytes * 1024;
+ dir.quotas.max_files = maxFiles;
+ get.nodeIds()[dir.path] = dir;
+ mockData.nodes.push({
+ id: dir.path,
+ value: name
+ });
+ }
+ };
+
+ // Expects that are used frequently
+ const assert = {
+ dirLength: (n: number) => expect(get.dirs().length).toBe(n),
+ nodeLength: (n: number) => expect(mockData.nodes.length).toBe(n),
+ lsDirCalledTimes: (n: number) => expect(lsDirSpy).toHaveBeenCalledTimes(n),
+ requestedPaths: (expected: string[]) => expect(get.requestedPaths()).toEqual(expected),
+ 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 }
+ ])
+ };
+
+ configureTestBed({
+ imports: [HttpClientTestingModule, SharedModule, TreeModule],
+ declarations: [CephfsDirectoriesComponent],
+ providers: [i18nProviders]
+ });
+
+ beforeEach(() => {
+ mockData = {
+ nodes: undefined,
+ parent: undefined
+ };
+ originalDate = Date;
+ spyOn(global, 'Date').and.callFake(mockLib.date);
+
+ lsDirSpy = spyOn(TestBed.get(CephfsService), 'lsDir').and.callFake(mockLib.lsDir);
+
+ fixture = TestBed.createComponent(CephfsDirectoriesComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+
+ spyOn(component.treeComponent, 'getControllerByNodeId').and.callFake((id) =>
+ mockLib.getControllerByPath(id)
+ );
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('calls lsDir only if an id exits', () => {
+ component.ngOnChanges();
+ assert.lsDirCalledTimes(0);
+
+ mockLib.changeId(1);
+ assert.lsDirCalledTimes(1);
+ expect(lsDirSpy).toHaveBeenCalledWith(1, '/');
+
+ mockLib.changeId(2);
+ assert.lsDirCalledTimes(2);
+ expect(lsDirSpy).toHaveBeenCalledWith(2, '/');
+ });
+
+ describe('listing sub directories', () => {
+ beforeEach(() => {
+ mockLib.changeId(1);
+ /**
+ * Tree looks like this:
+ * v /
+ * > a
+ * * b
+ * > c
+ * */
+ });
+
+ it('expands first level', () => {
+ // Tree will only show '*' if nor 'loadChildren' or 'children' are defined
+ expect(
+ mockData.nodes.map((node) => ({ [node.id]: Boolean(node.loadChildren || node.children) }))
+ ).toEqual([{ '/': true }, { '/a': true }, { '/b': false }, { '/c': true }]);
+ });
+
+ it('resets all dynamic content on id change', () => {
+ mockLib.selectNode('/a');
+ /**
+ * Tree looks like this:
+ * v /
+ * v a <- Selected
+ * > a
+ * * b
+ * > c
+ * * b
+ * > c
+ * */
+ assert.requestedPaths(['/', '/a']);
+ assert.nodeLength(7);
+ assert.dirLength(15);
+ expect(component.selectedDir).toBeDefined();
+
+ mockLib.changeId(undefined);
+ assert.dirLength(0);
+ assert.requestedPaths([]);
+ expect(component.selectedDir).not.toBeDefined();
+ });
+
+ it('should select a node and show the directory contents', () => {
+ mockLib.selectNode('/a');
+ const dir = get.dirs().find((d) => d.path === '/a');
+ expect(component.selectedDir).toEqual(dir);
+ assert.quotaSettings(10, '/a', '1 KiB', '/a');
+ });
+
+ it('should extend the list by subdirectories when expanding and omit already called path', () => {
+ mockLib.selectNode('/a');
+ mockLib.selectNode('/a/c');
+ /**
+ * Tree looks like this:
+ * v /
+ * v a
+ * > a
+ * * b
+ * v c <- Selected
+ * > a
+ * * b
+ * > c
+ * * b
+ * > c
+ * */
+ assert.lsDirCalledTimes(3);
+ assert.requestedPaths(['/', '/a', '/a/c']);
+ assert.dirLength(21);
+ assert.nodeLength(10);
+ });
+
+ it('should select parent by path', () => {
+ mockLib.selectNode('/a');
+ mockLib.selectNode('/a/c');
+ mockLib.selectNode('/a/c/a');
+ component.selectOrigin('/a');
+ expect(component.selectedDir.path).toBe('/a');
+ });
+
+ it('should omit call for directories that have no sub directories', () => {
+ mockLib.selectNode('/b');
+ /**
+ * Tree looks like this:
+ * v /
+ * > a
+ * * b <- Selected
+ * > c
+ * */
+ assert.lsDirCalledTimes(1);
+ assert.requestedPaths(['/']);
+ assert.nodeLength(4);
+ });
+
+ 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('', '', '', '');
+ });
+
+ it('should use quota from upper parents', () => {
+ setUpDirs([[100, 0], [0, 8], [0, 0], [0, 0]]);
+ assert.quotaSettings(100, '/1', '8 KiB', '/1/2');
+ });
+
+ 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');
+ });
+
+ 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');
+ });
+ });
+ });
+});
--- /dev/null
+import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
+
+import { I18n } from '@ngx-translate/i18n-polyfill';
+import { SortDirection, SortPropDir } from '@swimlane/ngx-datatable';
+import * as _ from 'lodash';
+import { NodeEvent, Tree, TreeComponent, TreeModel } from 'ng2-tree';
+
+import { CephfsService } from '../../../shared/api/cephfs.service';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
+import { CephfsDir } from '../../../shared/models/cephfs-directory-models';
+import { CdDatePipe } from '../../../shared/pipes/cd-date.pipe';
+import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
+
+@Component({
+ selector: 'cd-cephfs-directories',
+ templateUrl: './cephfs-directories.component.html',
+ styleUrls: ['./cephfs-directories.component.scss']
+})
+export class CephfsDirectoriesComponent implements OnInit, OnChanges {
+ @ViewChild(TreeComponent, { static: true })
+ treeComponent: TreeComponent;
+ @ViewChild('origin', { static: true })
+ originTmpl: TemplateRef<any>;
+
+ @Input()
+ id: number;
+
+ private dirs: CephfsDir[];
+ private nodeIds: { [path: string]: CephfsDir };
+ private requestedPaths: string[];
+
+ selectedDir: CephfsDir;
+ tree: TreeModel;
+ settings: {
+ name: string;
+ value: number | string;
+ origin: string;
+ }[];
+
+ settingsColumns: CdTableColumn[];
+ snapshot: { columns: CdTableColumn[]; sortProperties: SortPropDir[] };
+
+ constructor(
+ private cephfsService: CephfsService,
+ private cdDatePipe: CdDatePipe,
+ private i18n: I18n,
+ private dimlessBinaryPipe: DimlessBinaryPipe
+ ) {}
+
+ ngOnInit() {
+ this.settingsColumns = [
+ {
+ prop: 'name',
+ name: this.i18n('Name'),
+ flexGrow: 1
+ },
+ {
+ prop: 'value',
+ name: this.i18n('Value'),
+ sortable: false,
+ flexGrow: 1
+ },
+ {
+ prop: 'origin',
+ name: this.i18n('Origin'),
+ sortable: false,
+ cellTemplate: this.originTmpl,
+ flexGrow: 1
+ }
+ ];
+ this.snapshot = {
+ columns: [
+ {
+ prop: 'name',
+ name: this.i18n('Name'),
+ flexGrow: 1
+ },
+ {
+ prop: 'path',
+ name: this.i18n('Path'),
+ isHidden: true,
+ flexGrow: 2
+ },
+ {
+ prop: 'created',
+ name: this.i18n('Created'),
+ flexGrow: 1,
+ pipe: this.cdDatePipe
+ }
+ ],
+ sortProperties: [
+ {
+ dir: SortDirection.desc,
+ prop: 'created'
+ }
+ ]
+ };
+ }
+
+ ngOnChanges() {
+ this.selectedDir = undefined;
+ this.dirs = [];
+ this.requestedPaths = [];
+ this.nodeIds = {};
+ if (_.isUndefined(this.id)) {
+ this.setRootNode([]);
+ } else {
+ this.firstCall();
+ }
+ }
+
+ private setRootNode(nodes: TreeModel[]) {
+ const tree: TreeModel = {
+ value: '/',
+ id: '/',
+ settings: {
+ selectionAllowed: false,
+ static: true
+ }
+ };
+ if (nodes.length > 0) {
+ tree.children = nodes;
+ }
+ this.tree = tree;
+ }
+
+ private firstCall() {
+ this.updateDirectory('/', (nodes) => this.setRootNode(nodes));
+ }
+
+ updateDirectory(path: string, callback: (x: any[]) => void) {
+ if (
+ !this.requestedPaths.includes(path) &&
+ (path === '/' || this.getSubDirectories(path).length > 0)
+ ) {
+ this.requestedPaths.push(path);
+ this.cephfsService
+ .lsDir(this.id, path)
+ .subscribe((data) => this.loadDirectory(data, path, callback));
+ } else {
+ this.getChildren(path, callback);
+ }
+ }
+
+ private getSubDirectories(path: string, tree: CephfsDir[] = this.dirs): CephfsDir[] {
+ return tree.filter((d) => d.parent === path);
+ }
+
+ private loadDirectory(data: CephfsDir[], path: string, callback: (x: any[]) => void) {
+ if (path !== '/') {
+ // Removes duplicate directories
+ data = data.filter((dir) => dir.parent !== path);
+ }
+ const dirs = this.dirs.concat(data);
+ this.dirs = dirs;
+ this.getChildren(path, callback);
+ }
+
+ private getChildren(path: string, callback: (x: any[]) => void) {
+ const subTree = this.getSubTree(path);
+ const nodes = _.sortBy(this.getSubDirectories(path), 'path').map((d) => {
+ this.nodeIds[d.path] = d;
+ const newNode: TreeModel = {
+ value: d.name,
+ id: d.path,
+ settings: { static: true }
+ };
+ if (this.getSubDirectories(d.path, subTree).length > 0) {
+ // LoadChildren will be triggered if a node is expanded
+ newNode.loadChildren = (treeCallback) => this.updateDirectory(d.path, treeCallback);
+ }
+ return newNode;
+ });
+ callback(nodes);
+ }
+
+ private getSubTree(path: string): CephfsDir[] {
+ return this.dirs.filter((d) => d.parent.startsWith(path));
+ }
+
+ selectOrigin(path) {
+ this.treeComponent.getControllerByNodeId(path).select();
+ }
+
+ onNodeSelected(e: NodeEvent) {
+ const node = e.node;
+ this.treeComponent.getControllerByNodeId(node.id).expand();
+ this.setSettings(node);
+ this.selectedDir = this.getDirectory(node);
+ }
+
+ private setSettings(node: Tree) {
+ const files = this.getQuota(node, 'max_files');
+ const size = this.getQuota(node, 'max_bytes');
+ 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
+ }
+ ];
+ }
+
+ private getQuota(tree: Tree, quotaSetting: string): { value: string; origin: string } {
+ tree = this.getOrigin(tree, quotaSetting);
+ const dir = this.getDirectory(tree);
+ const value = dir.quotas[quotaSetting];
+ return {
+ value: value ? value : '',
+ origin: value ? dir.path : ''
+ };
+ }
+
+ private getOrigin(tree: Tree, quotaSetting: string): Tree {
+ if (tree.parent.value !== '/') {
+ const current = this.getQuotaFromTree(tree, quotaSetting);
+ const originTree = this.getOrigin(tree.parent, quotaSetting);
+ const inherited = this.getQuotaFromTree(originTree, quotaSetting);
+
+ const useOrigin = current === 0 || (inherited !== 0 && inherited < current);
+ return useOrigin ? originTree : tree;
+ }
+ return tree;
+ }
+
+ private getQuotaFromTree(tree: Tree, quotaSetting: string): number {
+ return this.getDirectory(tree).quotas[quotaSetting];
+ }
+
+ private getDirectory(node: Tree): CephfsDir {
+ const path = node.id as string;
+ return this.nodeIds[path];
+ }
+}
(triggerApiUpdate)="refresh()">
</cd-cephfs-clients>
</tab>
+ <tab i18n-heading
+ heading="Directories">
+ <cd-cephfs-directories [id]="id">
+ </cd-cephfs-directories>
+ </tab>
<tab i18n-heading
*ngIf="grafanaPermission.read && grafanaId"
heading="Performance Details">
import { ComponentFixture, TestBed } from '@angular/core/testing';
import * as _ from 'lodash';
+import { TreeModule } from 'ng2-tree';
import { TabsModule } from 'ngx-bootstrap/tabs';
import { ToastrModule } from 'ngx-toastr';
import { of } from 'rxjs';
import { SharedModule } from '../../../shared/shared.module';
import { CephfsClientsComponent } from '../cephfs-clients/cephfs-clients.component';
import { CephfsDetailComponent } from '../cephfs-detail/cephfs-detail.component';
+import { CephfsDirectoriesComponent } from '../cephfs-directories/cephfs-directories.component';
import { CephfsTabsComponent } from './cephfs-tabs.component';
describe('CephfsTabsComponent', () => {
}
configureTestBed({
- imports: [SharedModule, TabsModule.forRoot(), HttpClientTestingModule, ToastrModule.forRoot()],
+ imports: [
+ SharedModule,
+ TabsModule.forRoot(),
+ HttpClientTestingModule,
+ TreeModule,
+ ToastrModule.forRoot()
+ ],
declarations: [
CephfsTabsComponent,
CephfsChartStubComponent,
CephfsDetailComponent,
+ CephfsDirectoriesComponent,
CephfsClientsComponent
],
providers: [i18nProviders]
import { NgModule } from '@angular/core';
import { ChartsModule } from 'ng2-charts';
+import { TreeModule } from 'ng2-tree';
import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
import { TabsModule } from 'ngx-bootstrap/tabs';
import { CephfsChartComponent } from './cephfs-chart/cephfs-chart.component';
import { CephfsClientsComponent } from './cephfs-clients/cephfs-clients.component';
import { CephfsDetailComponent } from './cephfs-detail/cephfs-detail.component';
+import { CephfsDirectoriesComponent } from './cephfs-directories/cephfs-directories.component';
import { CephfsListComponent } from './cephfs-list/cephfs-list.component';
import { CephfsTabsComponent } from './cephfs-tabs/cephfs-tabs.component';
SharedModule,
AppRoutingModule,
ChartsModule,
+ TreeModule,
ProgressbarModule.forRoot(),
TabsModule.forRoot()
],
CephfsClientsComponent,
CephfsChartComponent,
CephfsListComponent,
- CephfsTabsComponent
+ CephfsTabsComponent,
+ CephfsDirectoriesComponent
]
})
export class CephfsModule {}
const req = httpTesting.expectOne('api/cephfs/1/mds_counters');
expect(req.request.method).toBe('GET');
});
+
+ it('should call lsDir', () => {
+ service.lsDir(1).subscribe();
+ const req = httpTesting.expectOne('api/cephfs/1/ls_dir?depth=2');
+ expect(req.request.method).toBe('GET');
+ service.lsDir(2, '/some/path').subscribe();
+ httpTesting.expectOne('api/cephfs/2/ls_dir?depth=2&path=%2Fsome%2Fpath');
+ });
});
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+
+import { CephfsDir } from '../models/cephfs-directory-models';
import { ApiModule } from './api.module';
@Injectable({
return this.http.get(`${this.baseURL}`);
}
+ lsDir(id, path?): Observable<CephfsDir[]> {
+ let apiPath = `${this.baseURL}/${id}/ls_dir?depth=2`;
+ if (path) {
+ apiPath += `&path=${encodeURIComponent(path)}`;
+ }
+ return this.http.get<CephfsDir[]>(apiPath);
+ }
+
getCephfs(id) {
return this.http.get(`${this.baseURL}/${id}`);
}
--- /dev/null
+import { TreeStatus } from '@swimlane/ngx-datatable';
+
+export class CephfsSnapshot {
+ name: string;
+ path: string;
+ created: string;
+}
+
+export class CephfsQuotas {
+ max_bytes: number;
+ max_files: number;
+}
+
+export class CephfsDir {
+ name: string;
+ path: string;
+ quotas: CephfsQuotas;
+ snapshots: CephfsSnapshot[];
+ parent: string;
+ treeStatus?: TreeStatus; // Needed for table tree view
+}
dent = self.cfs.readdir(d)
while dent:
if dent.is_dir():
- if dent.d_name not in [b'.', b'..']:
+ if dent.d_name not in [b'.', b'..'] and not dent.d_name.startswith(b'_'):
snapshot_path = os.path.join(path, dent.d_name)
stat = self.cfs.stat(snapshot_path)
result.append({