@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
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')
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}')
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')
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
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';
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',
<div class="row">
- <div class="col-sm-1">
- <h3 i18n>Groups</h3>
- <ng-container *ngIf="subVolumeGroups$ | async as subVolumeGroups">
- <ul class="nav flex-column nav-pills">
- <li class="nav-item">
- <a class="nav-link"
- [class.active]="!activeGroupName"
- (click)="selectSubVolumeGroup()">Default</a>
- </li>
- <li class="nav-item"
- *ngFor="let subVolumeGroup of subVolumeGroups">
- <a class="nav-link text-decoration-none text-break"
- [class.active]="subVolumeGroup.name === activeGroupName"
- (click)="selectSubVolumeGroup(subVolumeGroup.name)">{{subVolumeGroup.name}}</a>
- </li>
- </ul>
- </ng-container>
+ <div class="col-sm-1"
+ *ngIf="subVolumeGroups$ | async as subVolumeGroups">
+ <cd-vertical-navigation title="Groups"
+ [items]="subvolumeGroupList"
+ inputIdentifier="group-filter"
+ (emitActiveItem)="selectSubVolumeGroup($event)"></cd-vertical-navigation>
</div>
<div class="col-11 vertical-line">
<cd-table [data]="subVolumes$ | async"
import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Observable, ReplaySubject, of } from 'rxjs';
-import { catchError, shareReplay, switchMap } from 'rxjs/operators';
+import { catchError, shareReplay, switchMap, tap } from 'rxjs/operators';
import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
import { CdForm } from '~/app/shared/forms/cd-form';
import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service';
-import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolumegroup.model';
+import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model';
@Component({
selector: 'cd-cephfs-subvolume-list',
subject = new ReplaySubject<CephfsSubvolume[]>();
groupsSubject = new ReplaySubject<CephfsSubvolume[]>();
+ subvolumeGroupList: string[] = [];
+
activeGroupName: string = '';
constructor(
- private cephfsSubVolume: CephfsSubvolumeService,
+ private cephfsSubVolumeService: CephfsSubvolumeService,
private actionLabels: ActionLabelsI18n,
private modalService: ModalService,
private authStorageService: AuthStorageService,
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);
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,
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);
--- /dev/null
+<ng-container *ngIf="isLoading">
+ <cd-loading-panel>
+ <span i18n>Loading snapshots...</span>
+ </cd-loading-panel>
+</ng-container>
+
+<div class="row"
+ *ngIf="isSubVolumesAvailable; else noGroupsTpl">
+ <div class="col-sm-2">
+ <cd-vertical-navigation title="Groups"
+ [items]="subvolumeGroupList"
+ inputIdentifier="group-filter"
+ (emitActiveItem)="selectSubVolumeGroup($event)"></cd-vertical-navigation>
+ </div>
+ <div class="col-sm-2 vertical-line"
+ *ngIf="subVolumes$ | async">
+ <cd-vertical-navigation title="Subvolumes"
+ [items]="subVolumesList"
+ (emitActiveItem)="selectSubVolume($event)"
+ inputIdentifier="subvol-filter"></cd-vertical-navigation>
+ </div>
+ <div class="col-8 vertical-line"
+ *ngIf="isSubVolumesAvailable">
+ <cd-table [data]="snapshots$ | async"
+ columnMode="flex"
+ [columns]="columns"
+ selectionType="single"
+ [hasDetails]="false"
+ (fetchData)="fetchData()"></cd-table>
+ </div>
+</div>
+<ng-template #noGroupsTpl>
+ <cd-alert-panel type="info"
+ i18n
+ *ngIf="!isLoading">No subvolumes are present. Please create subvolumes to manage snapshots.</cd-alert-panel>
+</ng-template>
--- /dev/null
+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<CephfsSubvolumeSnapshotsListComponent>;
+
+ 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();
+ });
+});
--- /dev/null
+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<CephfsSubvolume[]>;
+ snapshots$: Observable<any[]>;
+ snapshotSubject = new ReplaySubject<SubvolumeSnapshot[]>();
+ subVolumeSubject = new ReplaySubject<CephfsSubvolume[]>();
+
+ 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();
+ }
+}
</cd-cephfs-subvolume-group>
</ng-template>
</ng-container>
+ <ng-container ngbNavItem="snapshots">
+ <a ngbNavLink
+ i18n>Snapshots</a>
+ <ng-template ngbNavContent>
+ <cd-cephfs-subvolume-snapshots-list [fsName]="selection.mdsmap.fs_name">
+ </cd-cephfs-subvolume-snapshots-list>
+ </ng-template>
+ </ng-container>
<ng-container ngbNavItem="clients">
<a ngbNavLink>
<ng-container i18n>Clients</ng-container>
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: [
CephfsSubvolumeFormComponent,
CephfsDirectoriesComponent,
CephfsSubvolumeGroupComponent,
- CephfsSubvolumegroupFormComponent
+ CephfsSubvolumegroupFormComponent,
+ CephfsSubvolumeSnapshotsListComponent
]
})
export class CephfsModule {}
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'
constructor(private http: HttpClient) {}
- get(volName: string): Observable<CephfsSubvolumeGroup[]> {
- return this.http.get<CephfsSubvolumeGroup[]>(`${this.baseURL}/${volName}`);
+ get(volName: string, info = true): Observable<CephfsSubvolumeGroup[]> {
+ return this.http.get<CephfsSubvolumeGroup[]>(`${this.baseURL}/${volName}`, {
+ params: {
+ info: info
+ }
+ });
}
create(
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');
});
);
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');
+ });
});
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';
constructor(private http: HttpClient) {}
- get(fsName: string, subVolumeGroupName: string = ''): Observable<CephfsSubvolume[]> {
+ get(fsName: string, subVolumeGroupName: string = '', info = true): Observable<CephfsSubvolume[]> {
return this.http.get<CephfsSubvolume[]>(`${this.baseURL}/${fsName}`, {
params: {
- group_name: subVolumeGroupName
+ group_name: subVolumeGroupName,
+ info: info
}
});
}
);
}
+ existsInFs(fsName: string, groupName = ''): Observable<boolean> {
+ return this.http.get<boolean>(`${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,
group_name: subVolumeGroupName
});
}
+
+ getSnapshots(
+ fsName: string,
+ subVolumeName: string,
+ groupName = ''
+ ): Observable<SubvolumeSnapshot[]> {
+ return this.http.get<SubvolumeSnapshot[]>(
+ `${this.baseURL}/snapshot/${fsName}/${subVolumeName}`,
+ {
+ params: {
+ group_name: groupName
+ }
+ }
+ );
+ }
}
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: [
CdLabelComponent,
ColorClassFromTextPipe,
CardComponent,
- CardRowComponent
+ CardRowComponent,
+ VerticalNavigationComponent
],
providers: [],
exports: [
CustomLoginBannerComponent,
CdLabelComponent,
CardComponent,
- CardRowComponent
+ CardRowComponent,
+ VerticalNavigationComponent
]
})
export class ComponentsModule {}
--- /dev/null
+<ng-container *ngIf="items.length">
+ <h3 i18n
+ *ngIf="title">{{title}}</h3>
+ <input type="text"
+ placeholder="Filter by name..."
+ (keyup)="updateFilter()"
+ [id]="inputIdentifier"
+ class="form-control text-center mb-2">
+ <div class="overflow-auto">
+ <ul class="nav flex-column nav-pills">
+ <li class="nav-item"
+ *ngFor="let item of filteredItems; trackBy: trackByFn">
+ <a class="nav-link"
+ [class.active]="!activeItem"
+ (click)="selectItem()"
+ *ngIf="item === ''">Default</a>
+ <a class="nav-link text-decoration-none text-break"
+ [class.active]="item === activeItem"
+ (click)="selectItem(item)"
+ *ngIf="item !== ''">{{item}}</a>
+ </li>
+ </ul>
+ </div>
+</ng-container>
--- /dev/null
+.overflow-auto {
+ max-height: 50vh;
+}
--- /dev/null
+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<VerticalNavigationComponent>;
+
+ 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');
+ });
+});
--- /dev/null
+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<string[]> = new EventEmitter();
+ @Output() emitActiveItem: EventEmitter<string> = 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;
+ }
+}
export interface CephfsSubvolumeGroup {
name: string;
- info: CephfsSubvolumeGroupInfo;
+ info?: CephfsSubvolumeGroupInfo;
}
export interface CephfsSubvolumeGroupInfo {
gid: number;
pool_namespace: string;
}
+
+export interface SubvolumeSnapshot {
+ name: string;
+ info: SubvolumeSnapshotInfo;
+}
+
+export interface SubvolumeSnapshotInfo {
+ created_at: string;
+ has_pending_clones: string;
+}
+++ /dev/null
-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;
-}
required: true
schema:
type: string
+ - default: true
+ in: query
+ name: info
+ schema:
+ type: boolean
responses:
'200':
content:
- 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:
name: group_name
schema:
type: string
+ - default: true
+ in: query
+ name: info
+ schema:
+ type: boolean
responses:
'200':
content:
- 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:
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