From: Nizamudeen A Date: Wed, 18 Oct 2023 17:46:09 +0000 (+0530) Subject: mgr/dashboard: cephfs subvolume list snapshots X-Git-Tag: v18.2.4~342^2~4 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=c8b2bef91ac1f52cf8f3c62025ea869963fe54a8;p=ceph.git 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 (cherry picked from commit b35be54ed9f23b7fc7859f054902e37cb88cefd8) --- diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index 09b2bebfc1df..ac8333b69503 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -623,7 +623,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 @@ -634,15 +634,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') @@ -699,12 +701,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}') @@ -714,15 +731,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') @@ -763,3 +782,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 3807ae61b67c..0e8768c85772 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 29731bbbd1b0..f840c8dab116 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 000000000000..de3117236524 --- /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 000000000000..e69de29bb2d1 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 000000000000..1d03cf2a8bca --- /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 000000000000..ef5c1050513b --- /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 0ad69ccf50a3..6a50ad2e0786 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 a83e0f16870f..cbdb2840867a 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 db7fcfacd597..49d001f04f09 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 e40e9a52f3f3..2e8448ff1a22 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 4c167725007e..d76523aafd2a 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 17f418d1e148..142f19338b01 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 @@ -51,6 +51,7 @@ import { UsageBarComponent } from './usage-bar/usage-bar.component'; import { WizardComponent } from './wizard/wizard.component'; import { CardComponent } from './card/card.component'; import { CardRowComponent } from './card-row/card-row.component'; +import { VerticalNavigationComponent } from './vertical-navigation/vertical-navigation.component'; @NgModule({ imports: [ @@ -105,7 +106,8 @@ import { CardRowComponent } from './card-row/card-row.component'; CdLabelComponent, ColorClassFromTextPipe, CardComponent, - CardRowComponent + CardRowComponent, + VerticalNavigationComponent ], providers: [], exports: [ @@ -137,7 +139,8 @@ import { CardRowComponent } from './card-row/card-row.component'; CustomLoginBannerComponent, CdLabelComponent, CardComponent, - CardRowComponent + CardRowComponent, + 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 000000000000..19628f0d1e1e --- /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 000000000000..569e2d68708a --- /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 000000000000..0d45b339a202 --- /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 000000000000..a46cc4f6c433 --- /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 fc087ab53d00..246e4543eb9d 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 41858be61304..25a2a5acc7f4 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 fc087ab53d00..000000000000 --- 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 aeb5d9464390..04996e80a5d1 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -1859,6 +1859,11 @@ paths: required: true schema: type: string + - default: true + in: query + name: info + schema: + type: boolean responses: '200': content: @@ -1954,6 +1959,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: @@ -2013,6 +2060,11 @@ paths: name: group_name schema: type: string + - default: true + in: query + name: info + schema: + type: boolean responses: '200': content: @@ -2079,6 +2131,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: @@ -12611,6 +12695,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