From 43114f9eb2c5415eb2a0f78c10bd0ef284ddcd14 Mon Sep 17 00:00:00 2001 From: Nizamudeen A Date: Wed, 26 Jul 2023 18:41:16 +0530 Subject: [PATCH] mgr/dashboard: cephfs subvolume list Fixes: https://tracker.ceph.com/issues/62182 Signed-off-by: Nizamudeen A --- .../mgr/dashboard/controllers/cephfs.py | 24 +++++ .../cephfs-subvolume-list.component.html | 65 +++++++++++ .../cephfs-subvolume-list.component.scss | 0 .../cephfs-subvolume-list.component.spec.ts | 27 +++++ .../cephfs-subvolume-list.component.ts | 101 ++++++++++++++++++ .../cephfs-tabs/cephfs-tabs.component.html | 7 ++ .../src/app/ceph/cephfs/cephfs.module.ts | 9 +- .../api/cephfs-subvolume.service.spec.ts | 35 ++++++ .../shared/api/cephfs-subvolume.service.ts | 17 +++ .../cd-label/cd-label.component.html | 3 +- .../components/cd-label/cd-label.component.ts | 1 + .../copy2clipboard-button.component.html | 22 ++-- .../copy2clipboard-button.component.ts | 3 + .../usage-bar/usage-bar.component.html | 8 +- .../usage-bar/usage-bar.component.ts | 4 + .../datatable/table/table.component.html | 12 +++ .../shared/datatable/table/table.component.ts | 3 + .../src/app/shared/enum/cell-template.enum.ts | 7 +- .../shared/models/cephfs-subvolume.model.ts | 15 +++ .../octal-to-human-readable.pipe.spec.ts | 32 ++++++ .../pipes/octal-to-human-readable.pipe.ts | 80 ++++++++++++++ .../src/app/shared/pipes/path.pipe.spec.ts | 18 ++++ .../src/app/shared/pipes/path.pipe.ts | 16 +++ .../src/app/shared/pipes/pipes.module.ts | 13 ++- .../app/shared/pipes/relative-date.pipe.ts | 5 +- src/pybind/mgr/dashboard/openapi.yaml | 29 +++++ 26 files changed, 537 insertions(+), 19 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume.model.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.ts diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index 5cfa2b5f800cb..c38cc940fd8b7 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import json import logging import os from collections import defaultdict @@ -630,3 +631,26 @@ class CephFsUi(CephFS): except (cephfs.PermissionError, cephfs.ObjectNotFound): # pragma: no cover paths = [] return paths + + +@APIRouter('/cephfs/subvolume', Scope.CEPHFS) +@APIDoc('CephFS Subvolume Management API', 'CephFSSubvolume') +class CephFSSubvolume(RESTController): + + def get(self, vol_name: str): + error_code, out, err = mgr.remote( + 'volumes', '_cmd_fs_subvolume_ls', None, {'vol_name': vol_name}) + if error_code != 0: + raise RuntimeError( + f'Failed to list subvolumes for volume {vol_name}: {err}' + ) + subvolumes = json.loads(out) + for subvolume in subvolumes: + error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None, { + 'vol_name': vol_name, 'sub_name': subvolume['name']}) + if error_code != 0: + raise RuntimeError( + f'Failed to get info for subvolume {subvolume["name"]}: {err}' + ) + subvolume['info'] = json.loads(out) + return subvolumes diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html new file mode 100644 index 0000000000000..7ecb3faae8013 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html @@ -0,0 +1,65 @@ + + + + + + + + + + + {{row.info.bytes_used | dimlessBinary}} + + + + + + + + + + {{ result.content }} + + + + + {{row.name}} + + + + + + + + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.spec.ts new file mode 100644 index 0000000000000..cafbff71768cc --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CephfsSubvolumeListComponent } from './cephfs-subvolume-list.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { SharedModule } from '~/app/shared/shared.module'; + +describe('CephfsSubvolumeListComponent', () => { + let component: CephfsSubvolumeListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CephfsSubvolumeListComponent], + imports: [HttpClientTestingModule, SharedModule] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CephfsSubvolumeListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts new file mode 100644 index 0000000000000..59fc488890563 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts @@ -0,0 +1,101 @@ +import { Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service'; +import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; +import { Icons } from '~/app/shared/enum/icons.enum'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; +import { CephfsSubvolume } from '~/app/shared/models/cephfs-subvolume.model'; + +@Component({ + selector: 'cd-cephfs-subvolume-list', + templateUrl: './cephfs-subvolume-list.component.html', + styleUrls: ['./cephfs-subvolume-list.component.scss'] +}) +export class CephfsSubvolumeListComponent implements OnInit, OnChanges { + @ViewChild('quotaUsageTpl', { static: true }) + quotaUsageTpl: any; + + @ViewChild('typeTpl', { static: true }) + typeTpl: any; + + @ViewChild('modeToHumanReadableTpl', { static: true }) + modeToHumanReadableTpl: any; + + @ViewChild('nameTpl', { static: true }) + nameTpl: any; + + @ViewChild('quotaSizeTpl', { static: true }) + quotaSizeTpl: any; + + @Input() fsName: string; + + columns: CdTableColumn[] = []; + context: CdTableFetchDataContext; + selection = new CdTableSelection(); + icons = Icons; + + subVolumes$: Observable; + + constructor(private cephfsSubVolume: CephfsSubvolumeService) {} + + ngOnInit(): void { + this.columns = [ + { + name: $localize`Name`, + prop: 'name', + flexGrow: 1, + cellTemplate: this.nameTpl + }, + { + name: $localize`Data Pool`, + prop: 'info.data_pool', + flexGrow: 0.7, + cellTransformation: CellTemplate.badge, + customTemplateConfig: { + class: 'badge-background-primary' + } + }, + { + name: $localize`Usage`, + prop: 'info.bytes_pcent', + flexGrow: 0.7, + cellTemplate: this.quotaUsageTpl, + cellClass: 'text-right' + }, + { + name: $localize`Path`, + prop: 'info.path', + flexGrow: 1, + cellTransformation: CellTemplate.path + }, + { + name: $localize`Mode`, + prop: 'info.mode', + flexGrow: 0.5, + cellTemplate: this.modeToHumanReadableTpl + }, + { + name: $localize`Created`, + prop: 'info.created_at', + flexGrow: 0.5, + cellTransformation: CellTemplate.timeAgo + } + ]; + } + + ngOnChanges() { + this.subVolumes$ = this.cephfsSubVolume.get(this.fsName).pipe( + catchError(() => { + this.context.error(); + return of(null); + }) + ); + } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html index b8db2e6b327be..50545a1ad65ce 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html @@ -12,6 +12,13 @@ + + Subvolumes + + + + Clients diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts index 31398666c53cf..3556345662347 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts @@ -3,7 +3,7 @@ import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { TreeModule } from '@circlon/angular-tree-component'; -import { NgbNavModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbNavModule, NgbTooltipModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap'; import { ChartsModule } from 'ng2-charts'; import { AppRoutingModule } from '~/app/app-routing.module'; @@ -15,6 +15,7 @@ import { CephfsDirectoriesComponent } from './cephfs-directories/cephfs-director import { CephfsVolumeFormComponent } from './cephfs-form/cephfs-form.component'; import { CephfsListComponent } from './cephfs-list/cephfs-list.component'; import { CephfsTabsComponent } from './cephfs-tabs/cephfs-tabs.component'; +import { CephfsSubvolumeListComponent } from './cephfs-subvolume-list/cephfs-subvolume-list.component'; @NgModule({ imports: [ @@ -26,7 +27,8 @@ import { CephfsTabsComponent } from './cephfs-tabs/cephfs-tabs.component'; NgbNavModule, FormsModule, ReactiveFormsModule, - NgbTypeaheadModule + NgbTypeaheadModule, + NgbTooltipModule ], declarations: [ CephfsDetailComponent, @@ -35,7 +37,8 @@ import { CephfsTabsComponent } from './cephfs-tabs/cephfs-tabs.component'; CephfsListComponent, CephfsTabsComponent, CephfsVolumeFormComponent, - CephfsDirectoriesComponent + CephfsDirectoriesComponent, + CephfsSubvolumeListComponent ] }) export class CephfsModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.spec.ts new file mode 100644 index 0000000000000..0e65aa6c0d748 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.spec.ts @@ -0,0 +1,35 @@ +import { TestBed } from '@angular/core/testing'; + +import { CephfsSubvolumeService } from './cephfs-subvolume.service'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { configureTestBed } from '~/testing/unit-test-helper'; + +describe('CephfsSubvolumeService', () => { + let service: CephfsSubvolumeService; + let httpTesting: HttpTestingController; + + configureTestBed({ + imports: [HttpClientTestingModule], + providers: [CephfsSubvolumeService] + }); + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CephfsSubvolumeService); + httpTesting = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpTesting.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should call get', () => { + service.get('testFS').subscribe(); + const req = httpTesting.expectOne('api/cephfs/subvolume/testFS'); + expect(req.request.method).toBe('GET'); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts new file mode 100644 index 0000000000000..a983f7e2c7a84 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts @@ -0,0 +1,17 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { CephfsSubvolume } from '../models/cephfs-subvolume.model'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class CephfsSubvolumeService { + baseURL = 'api/cephfs/subvolume'; + + constructor(private http: HttpClient) {} + + get(fsName: string): Observable { + return this.http.get(`${this.baseURL}/${fsName}`); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.html index 41cfae743f2f6..1e92028823c3e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.html @@ -1,6 +1,7 @@ + ngClass="{{value | colorClassFromText}}" + [ngbTooltip]="tooltipText"> {{ value }} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.ts index ae0471bf6f793..149dbb4ea1db7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.ts @@ -8,4 +8,5 @@ import { Component, Input } from '@angular/core'; export class CdLabelComponent { @Input() key?: string; @Input() value?: string; + @Input() tooltipText?: string; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.html index 25a3f3cfe252f..655364eefc0c6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.html @@ -1,7 +1,15 @@ - + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.ts index 2cc656bfccbe4..80c7acbf28aea 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.ts @@ -17,6 +17,9 @@ export class Copy2ClipboardButtonComponent { @Input() byId = true; + @Input() + showIconOnly = false; + icons = Icons; constructor(private toastr: ToastrService) {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html index 7068744e9f028..10064941f75d6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html @@ -1,13 +1,17 @@ - + - + + + + +
Used: Used: {{ isBinary ? (used | dimlessBinary) : (used | dimless) }}
Free: Free: {{ isBinary ? (total - used | dimlessBinary) : (total - used | dimless) }}
{{ customLegend }}:{{ isBinary ? (customLegendValue | dimlessBinary) : (customLegend[1] | dimless) }}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts index 4877e891e1f12..e41ed4c310957 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts @@ -24,6 +24,10 @@ export class UsageBarComponent implements OnChanges { calculatePerc = true; @Input() title = $localize`usage`; + @Input() + customLegend?: string; + @Input() + customLegendValue?: string; usedPercentage: number; freePercentage: number; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html index fc7b9f6e54ea2..995dfda51c0f2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html @@ -339,3 +339,15 @@ {{ value | relativeDate }} + + + {{ value | path }} + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts index 3fc62a9d67385..1b5e6db946df8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts @@ -72,6 +72,8 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O rowDetailsTpl: TemplateRef; @ViewChild('rowSelectionTpl', { static: true }) rowSelectionTpl: TemplateRef; + @ViewChild('pathTpl', { static: true }) + pathTpl: TemplateRef; // This is the array with the items to be shown. @Input() @@ -608,6 +610,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O this.cellTemplates.map = this.mapTpl; this.cellTemplates.truncate = this.truncateTpl; this.cellTemplates.timeAgo = this.timeAgoTpl; + this.cellTemplates.path = this.pathTpl; } useCustomClass(value: any): string { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts index 066cc9930adce..2790f97497859 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts @@ -55,5 +55,10 @@ export enum CellTemplate { This templace replaces a time, datetime or timestamp with a user-friendly "X {seconds,minutes,hours,days,...} ago", but the tooltip still displays the absolute timestamp */ - timeAgo = 'timeAgo' + timeAgo = 'timeAgo', + /* + This template truncates a path to a shorter format and shows the whole path in a tooltip + eg: /var/lib/ceph/osd/ceph-0 -> /var/.../ceph-0 + */ + path = 'path' } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume.model.ts new file mode 100644 index 0000000000000..bceaf311d5087 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume.model.ts @@ -0,0 +1,15 @@ +export interface CephfsSubvolume { + name: string; + info: CephfsSubvolumeInfo; +} + +export interface CephfsSubvolumeInfo { + mode: number; + type: string; + bytes_pcent: number; + bytes_quota: number; + data_pool: string; + path: string; + state: string; + created_at: string; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.spec.ts new file mode 100644 index 0000000000000..265365b2fbd77 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.spec.ts @@ -0,0 +1,32 @@ +import { OctalToHumanReadablePipe } from './octal-to-human-readable.pipe'; + +describe('OctalToHumanReadablePipe', () => { + const testPipeResults = (value: any, expected: any) => { + // eslint-disable-next-line + for (let r in value) { + expect(value[r].content).toEqual(expected[r].content); + } + }; + + it('create an instance', () => { + const pipe = new OctalToHumanReadablePipe(); + expect(pipe).toBeTruthy(); + }); + + it('should transform decimal values to octal mode human readable', () => { + const values = [16877, 16868, 16804]; + + const expected = [ + [{ content: 'owner: rwx' }, { content: 'group: r-x' }, { content: 'others: r-x' }], + [{ content: 'owner: rwx' }, { content: 'group: r--' }, { content: 'others: r--' }], + [{ content: 'owner: rw-' }, { content: 'group: r--' }, { content: 'others: r--' }] + ]; + + const pipe = new OctalToHumanReadablePipe(); + // eslint-disable-next-line + for (let index in values) { + const summary = pipe.transform(values[index]); + testPipeResults(summary, expected[index]); + } + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.ts new file mode 100644 index 0000000000000..d8455630597b1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.ts @@ -0,0 +1,80 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'octalToHumanReadable' +}) +export class OctalToHumanReadablePipe implements PipeTransform { + transform(value: number): any { + if (!value) { + return []; + } + const permissionSummary = []; + const permissions = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx']; + const octal = value.toString(8).padStart(7, '0'); + const digits = octal.split(''); + + const fileType = this.getFileTypeSymbol(parseInt(digits[1] + digits[2])); + const owner = permissions[parseInt(digits[4])]; + const group = permissions[parseInt(digits[5])]; + const others = permissions[parseInt(digits[6])]; + + if (fileType !== 'directory') { + permissionSummary.push({ + content: fileType, + class: 'badge-primary me-1' + }); + } + + if (owner !== '---') { + permissionSummary.push({ + content: `owner: ${owner}`, + class: 'badge-primary me-1' + }); + } + + if (group !== '---') { + permissionSummary.push({ + content: `group: ${group}`, + class: 'badge-primary me-1' + }); + } + + if (others !== '---') { + permissionSummary.push({ + content: `others: ${others}`, + class: 'badge-primary me-1' + }); + } + + if (permissionSummary.length === 0) { + return [ + { + content: 'no permissions', + class: 'badge-warning me-1', + toolTip: `owner: ${owner}, group: ${group}, others: ${others}` + } + ]; + } + + return permissionSummary; + } + + private getFileTypeSymbol(fileType: number): string { + switch (fileType) { + case 1: + return 'fifo'; + case 2: + return 'character'; + case 4: + return 'directory'; + case 6: + return 'block'; + case 10: + return 'regular'; + case 12: + return 'symbolic-link'; + default: + return '-'; + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.spec.ts new file mode 100644 index 0000000000000..49375f8c68c2a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.spec.ts @@ -0,0 +1,18 @@ +import { PathPipe } from './path.pipe'; + +describe('PathPipe', () => { + it('create an instance', () => { + const pipe = new PathPipe(); + expect(pipe).toBeTruthy(); + }); + + it('should transform the path', () => { + const pipe = new PathPipe(); + expect(pipe.transform('/a/b/c/d')).toBe('/a/.../d'); + }); + + it('should transform the path with no slash at beginning', () => { + const pipe = new PathPipe(); + expect(pipe.transform('a/b/c/d')).toBe('a/.../d'); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.ts new file mode 100644 index 0000000000000..1131b3fc7c066 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'path' +}) +export class PathPipe implements PipeTransform { + transform(value: unknown): string { + const splittedPath = value.toString().split('/'); + + if (splittedPath[0] === '') { + splittedPath.shift(); + return `/${splittedPath[0]}/.../${splittedPath[splittedPath.length - 1]}`; + } + return `${splittedPath[0]}/.../${splittedPath[splittedPath.length - 1]}`; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts index 4abc029533ac3..b5267aa71216d 100755 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts @@ -35,6 +35,8 @@ import { SanitizeHtmlPipe } from './sanitize-html.pipe'; import { SearchHighlightPipe } from './search-highlight.pipe'; import { TruncatePipe } from './truncate.pipe'; import { UpperFirstPipe } from './upper-first.pipe'; +import { OctalToHumanReadablePipe } from './octal-to-human-readable.pipe'; +import { PathPipe } from './path.pipe'; @NgModule({ imports: [CommonModule], @@ -72,7 +74,9 @@ import { UpperFirstPipe } from './upper-first.pipe'; HealthIconPipe, MgrSummaryPipe, MdsSummaryPipe, - OsdSummaryPipe + OsdSummaryPipe, + OctalToHumanReadablePipe, + PathPipe ], exports: [ ArrayPipe, @@ -108,7 +112,9 @@ import { UpperFirstPipe } from './upper-first.pipe'; HealthIconPipe, MgrSummaryPipe, MdsSummaryPipe, - OsdSummaryPipe + OsdSummaryPipe, + OctalToHumanReadablePipe, + PathPipe ], providers: [ ArrayPipe, @@ -139,7 +145,8 @@ import { UpperFirstPipe } from './upper-first.pipe'; HealthIconPipe, MgrSummaryPipe, MdsSummaryPipe, - OsdSummaryPipe + OsdSummaryPipe, + OctalToHumanReadablePipe ] }) export class PipesModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts index f802b6b2add17..251ab055e9516 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts @@ -40,10 +40,11 @@ export class RelativeDatePipe implements PipeTransform { */ transform(value: Date | string | number, upperFirst = true): string { let date: moment.Moment; + const offset = moment().utcOffset(); if (_.isNumber(value)) { - date = moment.unix(value); + date = moment.parseZone(moment.unix(value)).utc().utcOffset(offset).local(); } else { - date = moment(value); + date = moment.parseZone(value).utc().utcOffset(offset).local(); } if (!date.isValid()) { return ''; diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index fb8e3df8ef0dc..23e00468369b6 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -1681,6 +1681,33 @@ paths: - jwt: [] tags: - Cephfs + /api/cephfs/subvolume/{vol_name}: + get: + parameters: + - in: path + name: vol_name + required: true + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - CephFSSubvolume /api/cephfs/{fs_id}: get: parameters: @@ -12258,6 +12285,8 @@ servers: tags: - description: Initiate a session with Ceph name: Auth +- description: CephFS Subvolume Management API + name: CephFSSubvolume - description: Cephfs Management API name: Cephfs - description: Get Cluster Details -- 2.39.5