]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: cephfs subvolume list 52657/head
authorNizamudeen A <nia@redhat.com>
Wed, 26 Jul 2023 13:11:16 +0000 (18:41 +0530)
committerNizamudeen A <nia@redhat.com>
Thu, 3 Aug 2023 09:24:33 +0000 (14:54 +0530)
Fixes: https://tracker.ceph.com/issues/62182
Signed-off-by: Nizamudeen A <nia@redhat.com>
26 files changed:
src/pybind/mgr/dashboard/controllers/cephfs.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/cd-label/cd-label.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/copy2clipboard-button/copy2clipboard-button.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/octal-to-human-readable.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/path.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts
src/pybind/mgr/dashboard/openapi.yaml

index 5cfa2b5f800cb4f7c75011357fb91450a3ac5c59..c38cc940fd8b794fd68f6a0018533363c4a23f7f 100644 (file)
@@ -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 (file)
index 0000000..7ecb3fa
--- /dev/null
@@ -0,0 +1,65 @@
+<ng-container *ngIf="subVolumes$ | async as subVolumes">
+  <cd-table [data]="subVolumes"
+            columnMode="flex"
+            [columns]="columns"
+            selectionType="single"
+            [hasDetails]="false">
+  </cd-table>
+</ng-container>
+
+<ng-template #quotaUsageTpl
+             let-row="row">
+  <cd-usage-bar *ngIf="row.info.bytes_pcent && row.info.bytes_pcent !== 'undefined'; else noLimitTpl"
+                [total]="row.info.bytes_quota"
+                [used]="row.info.bytes_pcent"
+                [title]="row.name"
+                [calculatePerc]="false"
+                customLegend="Quota"
+                [customLegendValue]="row.info.bytes_quota"
+                decimals="2"></cd-usage-bar>
+
+  <ng-template #noLimitTpl>
+    <span ngbTooltip="Quota limit is not set"
+          *ngIf="row.info.bytes_pcent === 'undefined'"
+          i18n-ngbTooltip>
+      {{row.info.bytes_used | dimlessBinary}}</span>
+  </ng-template>
+</ng-template>
+
+<ng-template #typeTpl
+             let-value="value">
+  <cd-label [value]="value"></cd-label>
+</ng-template>
+
+<ng-template #modeToHumanReadableTpl
+             let-value="value">
+  <span *ngFor="let result of (value | octalToHumanReadable)"
+        [ngClass]="result.class"
+        [ngbTooltip]="result.toolTip">
+    {{ result.content }}
+  </span>
+</ng-template>
+
+<ng-template #nameTpl
+             let-row="row">
+  <span class="fw-bold">{{row.name}}</span>
+
+  <span *ngIf="row.info.state === 'complete'; else snapshotRetainedTpl">
+    <i [ngClass]="[icons.success, icons.large]"
+       ngbTooltip="{{row.name}} is ready to use"
+       class="text-success"></i>
+  </span>
+
+  <ng-template #snapshotRetainedTpl>
+    <i [ngClass]="[icons.warning, icons.large]"
+       class="text-warning"
+       ngbTooltip="{{row.name}} is removed after retaining the snapshots"></i>
+  </ng-template>
+
+  <cd-label [value]="row.info.type"
+            *ngIf="row.info.type !== 'subvolume'"></cd-label>
+
+  <cd-label value="namespaced"
+            *ngIf="row.info.pool_namespace"
+            [tooltipText]="row.info.pool_namespace"></cd-label>
+</ng-template>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..cafbff7
--- /dev/null
@@ -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<CephfsSubvolumeListComponent>;
+
+  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 (file)
index 0000000..59fc488
--- /dev/null
@@ -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<CephfsSubvolume[]>;
+
+  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;
+  }
+}
index b8db2e6b327be19a9908de9da6fb8d3894a24349..50545a1ad65cea20a68305213afdd7c0aaed5b37 100644 (file)
         </cd-cephfs-detail>
       </ng-template>
     </ng-container>
+    <ng-container ngbNavItem="subvolumes">
+      <a ngbNavLink
+         i18n>Subvolumes</a>
+      <ng-template ngbNavContent>
+        <cd-cephfs-subvolume-list [fsName]="selection.mdsmap.fs_name"></cd-cephfs-subvolume-list>
+      </ng-template>
+    </ng-container>
     <ng-container ngbNavItem="clients">
       <a ngbNavLink>
         <ng-container i18n>Clients</ng-container>
index 31398666c53cf51dfa40df4f41c15842f190e4c0..35563456623478ac25e55b3e5da763f6f6029109 100644 (file)
@@ -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 (file)
index 0000000..0e65aa6
--- /dev/null
@@ -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 (file)
index 0000000..a983f7e
--- /dev/null
@@ -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<CephfsSubvolume[]> {
+    return this.http.get<CephfsSubvolume[]>(`${this.baseURL}/${fsName}`);
+  }
+}
index 41cfae743f2f64c21febd40832664b8b470ed7df..1e92028823c3eb0e4b449dd8836811c704ec44b4 100644 (file)
@@ -1,6 +1,7 @@
 <span *ngIf="!key; else key_value"
       class="badge badge-{{value}}"
-      ngClass="{{value | colorClassFromText}}">
+      ngClass="{{value | colorClassFromText}}"
+      [ngbTooltip]="tooltipText">
   {{ value }}
 </span>
 
index ae0471bf6f7938edd75b0b91b7844b8e4220beb2..149dbb4ea1db77761669eb8512cd4efb01b9e378 100644 (file)
@@ -8,4 +8,5 @@ import { Component, Input } from '@angular/core';
 export class CdLabelComponent {
   @Input() key?: string;
   @Input() value?: string;
+  @Input() tooltipText?: string;
 }
index 25a3f3cfe252ff7fa700e0ed53c966819101cc9c..655364eefc0c6c6a935f3862f9a20339d1afc815 100644 (file)
@@ -1,7 +1,15 @@
-<button (click)="onClick()"
-        type="button"
-        class="btn btn-light"
-        i18n-title
-        title="Copy to Clipboard">
-  <i [ngClass]="[icons.clipboard]"></i>
-</button>
+<i [ngClass]="[icons.clipboard, icons.large]"
+   (click)="onClick()"
+   class="text-primary ms-2"
+   title="Copy to Clipboard"
+   *ngIf="showIconOnly; else withButtonTpl"></i>
+
+<ng-template #withButtonTpl>
+  <button (click)="onClick()"
+          type="button"
+          class="btn btn-light"
+          i18n-title
+          title="Copy to Clipboard">
+    <i [ngClass]="[icons.clipboard]"></i>
+  </button>
+</ng-template>
index 2cc656bfccbe48e2703a1c6df3f3e72793806c89..80c7acbf28aeae227a012cd1ede7b5ab90dcadad 100644 (file)
@@ -17,6 +17,9 @@ export class Copy2ClipboardButtonComponent {
   @Input()
   byId = true;
 
+  @Input()
+  showIconOnly = false;
+
   icons = Icons;
 
   constructor(private toastr: ToastrService) {}
index 7068744e9f02882f04fa79c36a8d13371d58ad54..10064941f75d6a594c33cea7658f49e4543b2071 100644 (file)
@@ -1,13 +1,17 @@
 <ng-template #usageTooltipTpl>
   <table>
     <tr>
-      <td class="text-left">Used:&nbsp;</td>
+      <td class="text-left me-1">Used:</td>
       <td class="text-right"><strong> {{ isBinary ? (used | dimlessBinary) : (used | dimless) }}</strong></td>
     </tr>
     <tr *ngIf="calculatePerc">
-      <td class="text-left">Free:&nbsp;</td>
+      <td class="text-left me-1">Free:</td>
       <td class="'text-right"><strong>{{ isBinary ? (total - used | dimlessBinary) : (total - used | dimless) }}</strong></td>
     </tr>
+    <tr *ngIf="customLegend">
+      <td class="text-left me-1">{{ customLegend }}:</td>
+      <td class="text-right"><strong>{{ isBinary ? (customLegendValue | dimlessBinary) : (customLegend[1] | dimless) }}</strong></td>
+    </tr>
   </table>
 </ng-template>
 
index 4877e891e1f124d015b47430e7eff0226c8f13a7..e41ed4c31095720288a1c319d6559b7a3fbf830b 100644 (file)
@@ -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;
index fc7b9f6e54ea291b86d3410521a8621dc6a6a1a9..995dfda51c0f20aa9a01aee1d0237b76f48c1d06 100644 (file)
   <span data-toggle="tooltip"
         [title]="value | cdDate">{{ value | relativeDate }}</span>
 </ng-template>
+
+<ng-template #pathTpl
+             let-value="value">
+  <span data-toggle="tooltip"
+        [title]="value"
+        class="font-monospace">{{ value | path }}
+    <cd-copy-2-clipboard-button [source]="value"
+                                [byId]="false"
+                                [showIconOnly]="true">
+    </cd-copy-2-clipboard-button>
+  </span>
+</ng-template>
index 3fc62a9d673857f68f506f1ee5635a279ddafbbc..1b5e6db946df8319bf6931cd68b008666d676c49 100644 (file)
@@ -72,6 +72,8 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   rowDetailsTpl: TemplateRef<any>;
   @ViewChild('rowSelectionTpl', { static: true })
   rowSelectionTpl: TemplateRef<any>;
+  @ViewChild('pathTpl', { static: true })
+  pathTpl: TemplateRef<any>;
 
   // 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 {
index 066cc9930adce977811161e45d708f33c84bad14..2790f974978597c799b97c28e9837b2583bcf65f 100644 (file)
@@ -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 (file)
index 0000000..bceaf31
--- /dev/null
@@ -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 (file)
index 0000000..265365b
--- /dev/null
@@ -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 (file)
index 0000000..d845563
--- /dev/null
@@ -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 (file)
index 0000000..49375f8
--- /dev/null
@@ -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 (file)
index 0000000..1131b3f
--- /dev/null
@@ -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]}`;
+  }
+}
index 4abc029533ac39f993d63c397f49c29bcc4c4477..b5267aa71216d2c516baa8cd9a6708cd341ae9ec 100755 (executable)
@@ -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 {}
index f802b6b2add174a0124dad8eb1a20c3bc9b7b24c..251ab055e951609a68952f31865f2707cc0eec88 100644 (file)
@@ -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 '';
index fb8e3df8ef0dc82c4b7d8aaed4f149344a8c6500..23e00468369b66b05b4571a72f80c4c8b3a5d64f 100644 (file)
@@ -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