From b35be54ed9f23b7fc7859f054902e37cb88cefd8 Mon Sep 17 00:00:00 2001 From: Nizamudeen A Date: Wed, 18 Oct 2023 23:16:09 +0530 Subject: [PATCH] mgr/dashboard: cephfs subvolume list snapshots Added a tab for displaying the subvolume snapshots - this tab will show an info alert when there are no subvolumes present - if the subvolume is present, then it'll be auto-selected by default Implemented a filter to search the groups and subvolumes by its name. Also added a scrollbar when there are too many items in the nav list Modified the REST APIs to fetch only the names of the resources and fetch the info when an API call is requesting for it. Added unit tests Fixes: https://tracker.ceph.com/issues/63237 Signed-off-by: Nizamudeen A --- .../mgr/dashboard/controllers/cephfs.py | 87 +++++++--- .../cephfs-subvolume-group.component.ts | 2 +- .../cephfs-subvolume-list.component.html | 23 +-- .../cephfs-subvolume-list.component.ts | 18 ++- ...fs-subvolume-snapshots-list.component.html | 36 +++++ ...fs-subvolume-snapshots-list.component.scss | 0 ...subvolume-snapshots-list.component.spec.ts | 38 +++++ ...phfs-subvolume-snapshots-list.component.ts | 148 ++++++++++++++++++ .../cephfs-tabs/cephfs-tabs.component.html | 8 + .../src/app/ceph/cephfs/cephfs.module.ts | 4 +- .../api/cephfs-subvolume-group.service.ts | 10 +- .../api/cephfs-subvolume.service.spec.ts | 10 +- .../shared/api/cephfs-subvolume.service.ts | 30 +++- .../shared/components/components.module.ts | 7 +- .../vertical-navigation.component.html | 24 +++ .../vertical-navigation.component.scss | 3 + .../vertical-navigation.component.spec.ts | 60 +++++++ .../vertical-navigation.component.ts | 37 +++++ .../models/cephfs-subvolume-group.model.ts | 2 +- .../shared/models/cephfs-subvolume.model.ts | 10 ++ .../models/cephfs-subvolumegroup.model.ts | 13 -- src/pybind/mgr/dashboard/openapi.yaml | 86 ++++++++++ 22 files changed, 588 insertions(+), 68 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.ts delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolumegroup.model.ts diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index a1066cbae0d..eb1f764c472 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -676,7 +676,7 @@ class CephFsUi(CephFS): @APIDoc('CephFS Subvolume Management API', 'CephFSSubvolume') class CephFSSubvolume(RESTController): - def get(self, vol_name: str, group_name: str = ""): + def get(self, vol_name: str, group_name: str = "", info=True): params = {'vol_name': vol_name} if group_name: params['group_name'] = group_name @@ -687,15 +687,17 @@ class CephFSSubvolume(RESTController): f'Failed to list subvolumes for volume {vol_name}: {err}' ) subvolumes = json.loads(out) - for subvolume in subvolumes: - params['sub_name'] = subvolume['name'] - error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None, - params) - if error_code != 0: - raise DashboardException( - f'Failed to get info for subvolume {subvolume["name"]}: {err}' - ) - subvolume['info'] = json.loads(out) + + if info: + for subvolume in subvolumes: + params['sub_name'] = subvolume['name'] + error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None, + params) + if error_code != 0: + raise DashboardException( + f'Failed to get info for subvolume {subvolume["name"]}: {err}' + ) + subvolume['info'] = json.loads(out) return subvolumes @RESTController.Resource('GET') @@ -752,12 +754,27 @@ class CephFSSubvolume(RESTController): component='cephfs') return f'Subvolume {subvol_name} removed successfully' + @RESTController.Resource('GET') + def exists(self, vol_name: str, group_name=''): + params = {'vol_name': vol_name} + if group_name: + params['group_name'] = group_name + error_code, out, err = mgr.remote( + 'volumes', '_cmd_fs_subvolume_exist', None, params) + if error_code != 0: + raise DashboardException( + f'Failed to check if subvolume exists: {err}' + ) + if out == 'no subvolume exists': + return False + return True + @APIRouter('/cephfs/subvolume/group', Scope.CEPHFS) @APIDoc("Cephfs Subvolume Group Management API", "CephfsSubvolumeGroup") class CephFSSubvolumeGroups(RESTController): - def get(self, vol_name): + def get(self, vol_name, info=True): if not vol_name: raise DashboardException( f'Error listing subvolume groups for {vol_name}') @@ -767,15 +784,17 @@ class CephFSSubvolumeGroups(RESTController): raise DashboardException( f'Error listing subvolume groups for {vol_name}') subvolume_groups = json.loads(out) - for group in subvolume_groups: - error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolumegroup_info', - None, {'vol_name': vol_name, - 'group_name': group['name']}) - if error_code != 0: - raise DashboardException( - f'Failed to get info for subvolume group {group["name"]}: {err}' - ) - group['info'] = json.loads(out) + + if info: + for group in subvolume_groups: + error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolumegroup_info', + None, {'vol_name': vol_name, + 'group_name': group['name']}) + if error_code != 0: + raise DashboardException( + f'Failed to get info for subvolume group {group["name"]}: {err}' + ) + group['info'] = json.loads(out) return subvolume_groups @RESTController.Resource('GET') @@ -816,3 +835,31 @@ class CephFSSubvolumeGroups(RESTController): f'Failed to delete subvolume group {group_name}: {err}' ) return f'Subvolume group {group_name} removed successfully' + + +@APIRouter('/cephfs/subvolume/snapshot', Scope.CEPHFS) +@APIDoc("Cephfs Subvolume Snapshot Management API", "CephfsSubvolumeSnapshot") +class CephFSSubvolumeSnapshots(RESTController): + def get(self, vol_name: str, subvol_name, group_name: str = '', info=True): + params = {'vol_name': vol_name, 'sub_name': subvol_name} + if group_name: + params['group_name'] = group_name + error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_snapshot_ls', None, + params) + if error_code != 0: + raise DashboardException( + f'Failed to list subvolume snapshots for subvolume {subvol_name}: {err}' + ) + snapshots = json.loads(out) + + if info: + for snapshot in snapshots: + params['snap_name'] = snapshot['name'] + error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_snapshot_info', + None, params) + if error_code != 0: + raise DashboardException( + f'Failed to get info for subvolume snapshot {snapshot["name"]}: {err}' + ) + snapshot['info'] = json.loads(out) + return snapshots diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts index 3807ae61b67..0e8768c8577 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts @@ -9,7 +9,6 @@ import { CdTableAction } from '~/app/shared/models/cd-table-action'; 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 { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolumegroup.model'; import { CephfsSubvolumegroupFormComponent } from '../cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component'; import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; @@ -18,6 +17,7 @@ import { Permissions } from '~/app/shared/models/permissions'; import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; import { FinishedTask } from '~/app/shared/models/finished-task'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model'; @Component({ selector: 'cd-cephfs-subvolume-group', 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 index 29731bbbd1b..f840c8dab11 100644 --- 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 @@ -1,21 +1,10 @@
-
-

Groups

- - - +
+
(); groupsSubject = new ReplaySubject(); + subvolumeGroupList: string[] = []; + activeGroupName: string = ''; constructor( - private cephfsSubVolume: CephfsSubvolumeService, + private cephfsSubVolumeService: CephfsSubvolumeService, private actionLabels: ActionLabelsI18n, private modalService: ModalService, private authStorageService: AuthStorageService, @@ -150,7 +152,11 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh this.subVolumeGroups$ = this.groupsSubject.pipe( switchMap(() => - this.cephfsSubvolumeGroupService.get(this.fsName).pipe( + this.cephfsSubvolumeGroupService.get(this.fsName, false).pipe( + tap((groups) => { + this.subvolumeGroupList = groups.map((group) => group.name); + this.subvolumeGroupList.unshift(''); + }), catchError(() => { this.context.error(); return of(null); @@ -203,7 +209,7 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh this.taskWrapper .wrapTaskAroundCall({ task: new FinishedTask('cephfs/subvolume/remove', { subVolumeName: this.selectedName }), - call: this.cephfsSubVolume.remove( + call: this.cephfsSubVolumeService.remove( this.fsName, this.selectedName, this.activeGroupName, @@ -228,7 +234,7 @@ export class CephfsSubvolumeListComponent extends CdForm implements OnInit, OnCh getSubVolumes(subVolumeGroupName = '') { this.subVolumes$ = this.subject.pipe( switchMap(() => - this.cephfsSubVolume.get(this.fsName, subVolumeGroupName).pipe( + this.cephfsSubVolumeService.get(this.fsName, subVolumeGroupName).pipe( catchError(() => { this.context.error(); return of(null); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.html new file mode 100644 index 00000000000..de311723652 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.html @@ -0,0 +1,36 @@ + + + Loading snapshots... + + + +
+
+ +
+
+ +
+
+ +
+
+ + No subvolumes are present. Please create subvolumes to manage snapshots. + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.spec.ts new file mode 100644 index 00000000000..1d03cf2a8bc --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CephfsSubvolumeSnapshotsListComponent } from './cephfs-subvolume-snapshots-list.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { SharedModule } from '~/app/shared/shared.module'; + +describe('CephfsSubvolumeSnapshotsListComponent', () => { + let component: CephfsSubvolumeSnapshotsListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CephfsSubvolumeSnapshotsListComponent], + imports: [HttpClientTestingModule, SharedModule] + }).compileComponents(); + + fixture = TestBed.createComponent(CephfsSubvolumeSnapshotsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show loading when the items are loading', () => { + component.isLoading = true; + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('cd-loading-panel')).toBeTruthy(); + }); + + it('should show the alert panel when there are no subvolumes', () => { + component.isLoading = false; + component.subvolumeGroupList = []; + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('cd-alert-panel')).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.ts new file mode 100644 index 00000000000..ef5c1050513 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.ts @@ -0,0 +1,148 @@ +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { Observable, ReplaySubject, forkJoin, of } from 'rxjs'; +import { catchError, shareReplay, switchMap, tap } from 'rxjs/operators'; +import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service'; +import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service'; +import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; +import { CephfsSubvolume, SubvolumeSnapshot } from '~/app/shared/models/cephfs-subvolume.model'; + +@Component({ + selector: 'cd-cephfs-subvolume-snapshots-list', + templateUrl: './cephfs-subvolume-snapshots-list.component.html', + styleUrls: ['./cephfs-subvolume-snapshots-list.component.scss'] +}) +export class CephfsSubvolumeSnapshotsListComponent implements OnInit, OnChanges { + @Input() fsName: string; + + context: CdTableFetchDataContext; + columns: CdTableColumn[] = []; + + subVolumes$: Observable; + snapshots$: Observable; + snapshotSubject = new ReplaySubject(); + subVolumeSubject = new ReplaySubject(); + + subvolumeGroupList: string[] = []; + subVolumesList: string[]; + + activeGroupName = ''; + activeSubVolumeName = ''; + + isSubVolumesAvailable = false; + isLoading = true; + + observables: any = []; + + constructor( + private cephfsSubvolumeGroupService: CephfsSubvolumeGroupService, + private cephfsSubvolumeService: CephfsSubvolumeService + ) {} + + ngOnInit(): void { + this.columns = [ + { + name: $localize`Name`, + prop: 'name', + flexGrow: 1 + }, + { + name: $localize`Created`, + prop: 'info.created_at', + flexGrow: 1, + cellTransformation: CellTemplate.timeAgo + }, + { + name: $localize`Pending Clones`, + prop: 'info.has_pending_clones', + flexGrow: 0.5, + cellTransformation: CellTemplate.badge, + customTemplateConfig: { + map: { + no: { class: 'badge-success' }, + yes: { class: 'badge-info' } + } + } + } + ]; + + this.cephfsSubvolumeGroupService + .get(this.fsName) + .pipe( + switchMap((groups) => { + // manually adding the group 'default' to the list. + groups.unshift({ name: '' }); + + const observables = groups.map((group) => + this.cephfsSubvolumeService.existsInFs(this.fsName, group.name).pipe( + switchMap((resp) => { + if (resp) { + this.subvolumeGroupList.push(group.name); + } + return of(resp); // Emit the response + }) + ) + ); + + return forkJoin(observables); + }) + ) + .subscribe(() => { + if (this.subvolumeGroupList.length) { + this.isSubVolumesAvailable = true; + } + this.isLoading = false; + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.fsName) { + this.subVolumeSubject.next(); + } + } + + selectSubVolumeGroup(subVolumeGroupName: string) { + this.activeGroupName = subVolumeGroupName; + this.getSubVolumes(); + } + + selectSubVolume(subVolumeName: string) { + this.activeSubVolumeName = subVolumeName; + this.getSubVolumesSnapshot(); + } + + getSubVolumes() { + this.subVolumes$ = this.subVolumeSubject.pipe( + switchMap(() => + this.cephfsSubvolumeService.get(this.fsName, this.activeGroupName, false).pipe( + tap((resp) => { + this.subVolumesList = resp.map((subVolume) => subVolume.name); + this.activeSubVolumeName = resp[0].name; + this.getSubVolumesSnapshot(); + }) + ) + ) + ); + } + + getSubVolumesSnapshot() { + this.snapshots$ = this.snapshotSubject.pipe( + switchMap(() => + this.cephfsSubvolumeService + .getSnapshots(this.fsName, this.activeSubVolumeName, this.activeGroupName) + .pipe( + catchError(() => { + this.context.error(); + return of(null); + }) + ) + ), + shareReplay(1) + ); + } + + fetchData() { + this.snapshotSubject.next(); + } +} 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 0ad69ccf50a..6a50ad2e078 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 @@ -29,6 +29,14 @@ + + Snapshots + + + + + 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 a83e0f16870..cbdb2840867 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 @@ -19,6 +19,7 @@ import { CephfsSubvolumeListComponent } from './cephfs-subvolume-list/cephfs-sub import { CephfsSubvolumeFormComponent } from './cephfs-subvolume-form/cephfs-subvolume-form.component'; import { CephfsSubvolumeGroupComponent } from './cephfs-subvolume-group/cephfs-subvolume-group.component'; import { CephfsSubvolumegroupFormComponent } from './cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component'; +import { CephfsSubvolumeSnapshotsListComponent } from './cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component'; @NgModule({ imports: [ @@ -45,7 +46,8 @@ import { CephfsSubvolumegroupFormComponent } from './cephfs-subvolumegroup-form/ CephfsSubvolumeFormComponent, CephfsDirectoriesComponent, CephfsSubvolumeGroupComponent, - CephfsSubvolumegroupFormComponent + CephfsSubvolumegroupFormComponent, + CephfsSubvolumeSnapshotsListComponent ] }) export class CephfsModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.ts index db7fcfacd59..49d001f04f0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.ts @@ -1,9 +1,9 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; -import { CephfsSubvolumeGroup } from '../models/cephfs-subvolumegroup.model'; import _ from 'lodash'; import { mapTo, catchError } from 'rxjs/operators'; +import { CephfsSubvolumeGroup } from '../models/cephfs-subvolume-group.model'; @Injectable({ providedIn: 'root' @@ -13,8 +13,12 @@ export class CephfsSubvolumeGroupService { constructor(private http: HttpClient) {} - get(volName: string): Observable { - return this.http.get(`${this.baseURL}/${volName}`); + get(volName: string, info = true): Observable { + return this.http.get(`${this.baseURL}/${volName}`, { + params: { + info: info + } + }); } create( 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 index e40e9a52f3f..2e8448ff1a2 100644 --- 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 @@ -29,7 +29,7 @@ describe('CephfsSubvolumeService', () => { it('should call get', () => { service.get('testFS').subscribe(); - const req = httpTesting.expectOne('api/cephfs/subvolume/testFS?group_name='); + const req = httpTesting.expectOne('api/cephfs/subvolume/testFS?group_name=&info=true'); expect(req.request.method).toBe('GET'); }); @@ -40,4 +40,12 @@ describe('CephfsSubvolumeService', () => { ); expect(req.request.method).toBe('DELETE'); }); + + it('should call getSnapshots', () => { + service.getSnapshots('testFS', 'testSubvol').subscribe(); + const req = httpTesting.expectOne( + 'api/cephfs/subvolume/snapshot/testFS/testSubvol?group_name=' + ); + 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 index 4c167725007..d76523aafd2 100644 --- 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 @@ -1,6 +1,6 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { CephfsSubvolume } from '../models/cephfs-subvolume.model'; +import { CephfsSubvolume, SubvolumeSnapshot } from '../models/cephfs-subvolume.model'; import { Observable, of } from 'rxjs'; import { catchError, mapTo } from 'rxjs/operators'; import _ from 'lodash'; @@ -13,10 +13,11 @@ export class CephfsSubvolumeService { constructor(private http: HttpClient) {} - get(fsName: string, subVolumeGroupName: string = ''): Observable { + get(fsName: string, subVolumeGroupName: string = '', info = true): Observable { return this.http.get(`${this.baseURL}/${fsName}`, { params: { - group_name: subVolumeGroupName + group_name: subVolumeGroupName, + info: info } }); } @@ -86,6 +87,14 @@ export class CephfsSubvolumeService { ); } + existsInFs(fsName: string, groupName = ''): Observable { + return this.http.get(`${this.baseURL}/${fsName}/exists`, { + params: { + group_name: groupName + } + }); + } + update(fsName: string, subVolumeName: string, size: string, subVolumeGroupName: string = '') { return this.http.put(`${this.baseURL}/${fsName}`, { subvol_name: subVolumeName, @@ -93,4 +102,19 @@ export class CephfsSubvolumeService { group_name: subVolumeGroupName }); } + + getSnapshots( + fsName: string, + subVolumeName: string, + groupName = '' + ): Observable { + return this.http.get( + `${this.baseURL}/snapshot/${fsName}/${subVolumeName}`, + { + params: { + group_name: groupName + } + } + ); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts index 327d208ef38..1f31374c18b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts @@ -52,6 +52,7 @@ import { WizardComponent } from './wizard/wizard.component'; import { CardComponent } from './card/card.component'; import { CardRowComponent } from './card-row/card-row.component'; import { CodeBlockComponent } from './code-block/code-block.component'; +import { VerticalNavigationComponent } from './vertical-navigation/vertical-navigation.component'; @NgModule({ imports: [ @@ -107,7 +108,8 @@ import { CodeBlockComponent } from './code-block/code-block.component'; ColorClassFromTextPipe, CardComponent, CardRowComponent, - CodeBlockComponent + CodeBlockComponent, + VerticalNavigationComponent ], providers: [], exports: [ @@ -140,7 +142,8 @@ import { CodeBlockComponent } from './code-block/code-block.component'; CdLabelComponent, CardComponent, CardRowComponent, - CodeBlockComponent + CodeBlockComponent, + VerticalNavigationComponent ] }) export class ComponentsModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.html new file mode 100644 index 00000000000..19628f0d1e1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.html @@ -0,0 +1,24 @@ + +

{{title}}

+ +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.scss new file mode 100644 index 00000000000..569e2d68708 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.scss @@ -0,0 +1,3 @@ +.overflow-auto { + max-height: 50vh; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.spec.ts new file mode 100644 index 00000000000..0d45b339a20 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.spec.ts @@ -0,0 +1,60 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { VerticalNavigationComponent } from './vertical-navigation.component'; +import { By } from '@angular/platform-browser'; + +describe('VerticalNavigationComponent', () => { + let component: VerticalNavigationComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [VerticalNavigationComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(VerticalNavigationComponent); + component = fixture.componentInstance; + component.items = ['item1', 'item2', 'item3']; + component.inputIdentifier = 'filter'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have a title', () => { + component.title = 'testTitle'; + fixture.detectChanges(); + const title = fixture.debugElement.query(By.css('h3')); + expect(title.nativeElement.textContent).toEqual('testTitle'); + }); + + it('should select the first item as active if no item is selected', () => { + expect(component.activeItem).toEqual('item1'); + }); + + it('should filter the items by the keyword in filter input', () => { + const event = new KeyboardEvent('keyup'); + const filterInput = fixture.debugElement.query(By.css('#filter')); + filterInput.nativeElement.value = 'item1'; + filterInput.nativeElement.dispatchEvent(event); + fixture.detectChanges(); + expect(component.filteredItems).toEqual(['item1']); + + filterInput.nativeElement.value = 'item2'; + filterInput.nativeElement.dispatchEvent(event); + fixture.detectChanges(); + expect(component.filteredItems).toEqual(['item2']); + }); + + it('should select the item when clicked', () => { + component.activeItem = ''; + + // click on the first item in the nav list + const item = fixture.debugElement.query(By.css('.nav-link')); + item.nativeElement.click(); + fixture.detectChanges(); + expect(component.activeItem).toEqual('item1'); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.ts new file mode 100644 index 00000000000..a46cc4f6c43 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/vertical-navigation/vertical-navigation.component.ts @@ -0,0 +1,37 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +@Component({ + selector: 'cd-vertical-navigation', + templateUrl: './vertical-navigation.component.html', + styleUrls: ['./vertical-navigation.component.scss'] +}) +export class VerticalNavigationComponent implements OnInit { + @Input() items: string[]; + @Input() title: string; + @Input() inputIdentifier: string; + + @Output() emitFilteredItems: EventEmitter = new EventEmitter(); + @Output() emitActiveItem: EventEmitter = new EventEmitter(); + + activeItem = ''; + filteredItems: string[]; + + ngOnInit(): void { + this.filteredItems = this.items; + if (!this.activeItem && this.items.length) this.selectItem(this.items[0]); + } + + updateFilter() { + const filterInput = document.getElementById(this.inputIdentifier) as HTMLInputElement; + this.filteredItems = this.items.filter((item) => item.includes(filterInput.value)); + } + + selectItem(item = '') { + this.activeItem = item; + this.emitActiveItem.emit(item); + } + + trackByFn(item: number) { + return item; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume-group.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume-group.model.ts index fc087ab53d0..246e4543eb9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume-group.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolume-group.model.ts @@ -1,6 +1,6 @@ export interface CephfsSubvolumeGroup { name: string; - info: CephfsSubvolumeGroupInfo; + info?: CephfsSubvolumeGroupInfo; } export interface CephfsSubvolumeGroupInfo { 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 index 41858be6130..25a2a5acc7f 100644 --- 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 @@ -16,3 +16,13 @@ export interface CephfsSubvolumeInfo { gid: number; pool_namespace: string; } + +export interface SubvolumeSnapshot { + name: string; + info: SubvolumeSnapshotInfo; +} + +export interface SubvolumeSnapshotInfo { + created_at: string; + has_pending_clones: string; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolumegroup.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolumegroup.model.ts deleted file mode 100644 index fc087ab53d0..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs-subvolumegroup.model.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface CephfsSubvolumeGroup { - name: string; - info: CephfsSubvolumeGroupInfo; -} - -export interface CephfsSubvolumeGroupInfo { - mode: number; - bytes_pcent: number; - bytes_quota: number; - data_pool: string; - state: string; - created_at: string; -} diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 3081d60b16f..758ed13f9c5 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -1882,6 +1882,11 @@ paths: required: true schema: type: string + - default: true + in: query + name: info + schema: + type: boolean responses: '200': content: @@ -1977,6 +1982,48 @@ paths: - jwt: [] tags: - CephfsSubvolumeGroup + /api/cephfs/subvolume/snapshot/{vol_name}/{subvol_name}: + get: + parameters: + - in: path + name: vol_name + required: true + schema: + type: string + - in: path + name: subvol_name + required: true + schema: + type: string + - default: '' + in: query + name: group_name + schema: + type: string + - default: true + in: query + name: info + schema: + type: boolean + 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: + - CephfsSubvolumeSnapshot /api/cephfs/subvolume/{vol_name}: delete: parameters: @@ -2036,6 +2083,11 @@ paths: name: group_name schema: type: string + - default: true + in: query + name: info + schema: + type: boolean responses: '200': content: @@ -2102,6 +2154,38 @@ paths: - jwt: [] tags: - CephFSSubvolume + /api/cephfs/subvolume/{vol_name}/exists: + get: + parameters: + - in: path + name: vol_name + required: true + schema: + type: string + - default: '' + in: query + name: group_name + 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/subvolume/{vol_name}/info: get: parameters: @@ -12781,6 +12865,8 @@ tags: name: Cephfs - description: Cephfs Subvolume Group Management API name: CephfsSubvolumeGroup +- description: Cephfs Subvolume Snapshot Management API + name: CephfsSubvolumeSnapshot - description: Get Cluster Details name: Cluster - description: Manage Cluster Configurations -- 2.39.5