]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: subvolume snapshot creation form 55012/head
authorNizamudeen A <nia@redhat.com>
Wed, 27 Dec 2023 09:14:53 +0000 (14:44 +0530)
committerNizamudeen A <nia@redhat.com>
Tue, 9 Jan 2024 14:50:51 +0000 (20:20 +0530)
Fixes: https://tracker.ceph.com/issues/63934
Signed-off-by: Nizamudeen A <nia@redhat.com>
15 files changed:
src/pybind/mgr/dashboard/controllers/cephfs.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/cd-date.pipe.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/cd-date.pipe.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
src/pybind/mgr/dashboard/openapi.yaml

index 61b31eb809fd2c9fdacebb447f78d444a7ab7a40..712efe11b0ddc2e65f9a036a75f86d1f8df5ab9b 100644 (file)
@@ -864,6 +864,33 @@ class CephFSSubvolumeSnapshots(RESTController):
                 snapshot['info'] = json.loads(out)
         return snapshots
 
+    @RESTController.Resource('GET')
+    def info(self, vol_name: str, subvol_name: str, snap_name: str, group_name: str = ''):
+        params = {'vol_name': vol_name, 'sub_name': subvol_name, 'snap_name': snap_name}
+        if group_name:
+            params['group_name'] = group_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 {snap_name}: {err}'
+            )
+        return json.loads(out)
+
+    def create(self, vol_name: str, subvol_name: str, snap_name: str, group_name=''):
+        params = {'vol_name': vol_name, 'sub_name': subvol_name, 'snap_name': snap_name}
+        if group_name:
+            params['group_name'] = group_name
+
+        error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolume_snapshot_create', None,
+                                        params)
+
+        if error_code != 0:
+            raise DashboardException(
+                f'Failed to create subvolume snapshot {snap_name}: {err}'
+            )
+        return f'Subvolume snapshot {snap_name} created successfully'
+
 
 @APIRouter('/cephfs/snaphost/schedule', Scope.CEPHFS)
 @APIDoc("Cephfs Snapshot Scheduling API", "CephFSSnapshotSchedule")
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component.html
new file mode 100644 (file)
index 0000000..867ed1b
--- /dev/null
@@ -0,0 +1,98 @@
+<cd-modal [modalRef]="activeModal">
+  <ng-container i18n="form title"
+                class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+  <ng-container class="modal-content"
+                *cdFormLoading="loading">
+    <form name="snapshotForm"
+          #formDir="ngForm"
+          [formGroup]="snapshotForm"
+          novalidate>
+      <div class="modal-body">
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="snapshotName"
+                 i18n>Name</label>
+          <div class="cd-col-form-input">
+            <input class="form-control"
+                   type="text"
+                   placeholder="Snapshot name..."
+                   id="snapshotName"
+                   name="snapshotName"
+                   formControlName="snapshotName"
+                   autofocus>
+            <span class="invalid-feedback"
+                  *ngIf="snapshotForm.showError('snapshotName', formDir, 'required')"
+                  i18n>This field is required.</span>
+            <span class="invalid-feedback"
+                  *ngIf="snapshotForm.showError('snapshotName', formDir, 'notUnique')"
+                  i18n>The snapshot already exists.</span>
+          </div>
+        </div>
+
+        <!-- Volume name -->
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="volumeName"
+                 i18n>Volume name</label>
+          <div class="cd-col-form-input">
+            <input class="form-control"
+                   id="volumeName"
+                   name="volumeName"
+                   formControlName="volumeName">
+          </div>
+        </div>
+
+        <!--Subvolume Group name -->
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="subvolumeGroupName"
+                 i18n>Subvolume group
+          </label>
+          <div class="cd-col-form-input">
+            <select class="form-select"
+                    id="subvolumeGroupName"
+                    name="subvolumeGroupName"
+                    formControlName="subvolumeGroupName"
+                    #selection
+                    (change)="onSelectionChange(selection.value)"
+                    *ngIf="subVolumeGroups">
+              <ng-container *ngFor="let subvolumegroup of subVolumeGroups">
+                <option *ngIf="subvolumegroup == ''"
+                        value="">_nogroup</option>
+                <option [value]="subvolumegroup"
+                        *ngIf="subvolumegroup !== ''">{{ subvolumegroup }}</option>
+              </ng-container>
+            </select>
+          </div>
+        </div>
+
+        <!--Subvolume name -->
+        <div class="form-group row">
+          <label class="cd-col-form-label"
+                 for="subVolumeName"
+                 i18n>Subvolume
+          </label>
+          <div class="cd-col-form-input">
+            <select class="form-select"
+                    id="subVolumeName"
+                    name="subVolumeName"
+                    formControlName="subVolumeName"
+                    #selection
+                    (change)="resetValidators(selection.value)"
+                    *ngIf="subVolumes$ | async as subVolumes">
+              <option *ngFor="let subVolume of subVolumes"
+                      [value]="subVolume.name">{{ subVolume.name }}</option>
+            </select>
+          </div>
+        </div>
+      </div>
+
+      <div class="modal-footer">
+        <cd-form-button-panel (submitActionEvent)="submit()"
+                              [form]="snapshotForm"
+                              [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+      </div>
+    </form>
+  </ng-container>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component.spec.ts
new file mode 100644 (file)
index 0000000..a6eb923
--- /dev/null
@@ -0,0 +1,41 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CephfsSubvolumeSnapshotsFormComponent } from './cephfs-subvolume-snapshots-form.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ToastrModule } from 'ngx-toastr';
+import { ReactiveFormsModule } from '@angular/forms';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+describe('CephfsSubvolumeSnapshotsFormComponent', () => {
+  let component: CephfsSubvolumeSnapshotsFormComponent;
+  let fixture: ComponentFixture<CephfsSubvolumeSnapshotsFormComponent>;
+
+  configureTestBed({
+    declarations: [CephfsSubvolumeSnapshotsFormComponent],
+    providers: [NgbActiveModal],
+    imports: [
+      SharedModule,
+      ToastrModule.forRoot(),
+      ReactiveFormsModule,
+      HttpClientTestingModule,
+      RouterTestingModule
+    ]
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CephfsSubvolumeSnapshotsFormComponent);
+    component = fixture.componentInstance;
+    component.fsName = 'test_volume';
+    component.subVolumeName = 'test_subvolume';
+    component.subVolumeGroupName = 'test_subvolume_group';
+    component.ngOnInit();
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component.ts
new file mode 100644 (file)
index 0000000..92757d3
--- /dev/null
@@ -0,0 +1,127 @@
+import { Component, OnInit } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import moment from 'moment';
+import { Observable } from 'rxjs';
+import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { CephfsSubvolume } from '~/app/shared/models/cephfs-subvolume.model';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+@Component({
+  selector: 'cd-cephfs-subvolume-snapshots-form',
+  templateUrl: './cephfs-subvolume-snapshots-form.component.html',
+  styleUrls: ['./cephfs-subvolume-snapshots-form.component.scss']
+})
+export class CephfsSubvolumeSnapshotsFormComponent extends CdForm implements OnInit {
+  fsName: string;
+  subVolumeName: string;
+  subVolumeGroupName: string;
+  subVolumeGroups: string[];
+
+  isEdit = false;
+
+  snapshotForm: CdFormGroup;
+
+  action: string;
+  resource: string;
+
+  subVolumes$: Observable<CephfsSubvolume[]>;
+
+  constructor(
+    public activeModal: NgbActiveModal,
+    private actionLabels: ActionLabelsI18n,
+    private taskWrapper: TaskWrapperService,
+    private cephFsSubvolumeService: CephfsSubvolumeService
+  ) {
+    super();
+    this.resource = $localize`snapshot`;
+    this.action = this.actionLabels.CREATE;
+  }
+
+  ngOnInit(): void {
+    this.createForm();
+
+    this.subVolumes$ = this.cephFsSubvolumeService.get(this.fsName, this.subVolumeGroupName, false);
+    this.loadingReady();
+  }
+
+  createForm() {
+    this.snapshotForm = new CdFormGroup({
+      snapshotName: new FormControl(moment().toISOString(true), {
+        validators: [Validators.required],
+        asyncValidators: [
+          CdValidators.unique(
+            this.cephFsSubvolumeService.snapshotExists,
+            this.cephFsSubvolumeService,
+            null,
+            null,
+            this.fsName,
+            this.subVolumeName,
+            this.subVolumeGroupName
+          )
+        ]
+      }),
+      volumeName: new FormControl({ value: this.fsName, disabled: true }),
+      subVolumeName: new FormControl(this.subVolumeName),
+      subvolumeGroupName: new FormControl(this.subVolumeGroupName)
+    });
+  }
+
+  onSelectionChange(groupName: string) {
+    this.subVolumeGroupName = groupName;
+    this.subVolumes$ = this.cephFsSubvolumeService.get(this.fsName, this.subVolumeGroupName, false);
+    this.subVolumes$.subscribe((subVolumes) => {
+      this.subVolumeName = subVolumes[0].name;
+      this.snapshotForm.get('subVolumeName').setValue(this.subVolumeName);
+
+      this.resetValidators();
+    });
+  }
+
+  resetValidators(subVolumeName?: string) {
+    this.subVolumeName = subVolumeName;
+    this.snapshotForm
+      .get('snapshotName')
+      .setAsyncValidators(
+        CdValidators.unique(
+          this.cephFsSubvolumeService.snapshotExists,
+          this.cephFsSubvolumeService,
+          null,
+          null,
+          this.fsName,
+          this.subVolumeName,
+          this.subVolumeGroupName
+        )
+      );
+    this.snapshotForm.get('snapshotName').updateValueAndValidity();
+  }
+
+  submit() {
+    const snapshotName = this.snapshotForm.getValue('snapshotName');
+    const subVolumeName = this.snapshotForm.getValue('subVolumeName');
+    const subVolumeGroupName = this.snapshotForm.getValue('subvolumeGroupName');
+    const volumeName = this.snapshotForm.getValue('volumeName');
+
+    this.taskWrapper
+      .wrapTaskAroundCall({
+        task: new FinishedTask('cephfs/subvolume/snapshot/' + URLVerbs.CREATE, {
+          snapshotName: snapshotName
+        }),
+        call: this.cephFsSubvolumeService.createSnapshot(
+          volumeName,
+          snapshotName,
+          subVolumeName,
+          subVolumeGroupName
+        )
+      })
+      .subscribe({
+        error: () => this.snapshotForm.setErrors({ cdSubmitButton: true }),
+        complete: () => this.activeModal.close()
+      });
+  }
+}
index de311723652414a2572b3f1aea11ddc04c6706cc..190072027bcff9dcb629a6b3fbd36386d024016f 100644 (file)
               [columns]="columns"
               selectionType="single"
               [hasDetails]="false"
-              (fetchData)="fetchData()"></cd-table>
+              (fetchData)="fetchData()"
+              (updateSelection)="updateSelection($event)">
+
+      <div class="table-actions btn-toolbar">
+        <cd-table-actions [permission]="permissions.cephfs"
+                          [selection]="selection"
+                          class="btn-group"
+                          id="cephfs-snapshot-actions"
+                          [tableActions]="tableActions">
+        </cd-table-actions>
+      </div>
+    </cd-table>
   </div>
 </div>
 <ng-template #noGroupsTpl>
index 251314c3e86a80c918994a49aaa8edee37afc264..9970d598887986bd89a65d6463e8d988f2ee02d7 100644 (file)
@@ -3,10 +3,19 @@ import { BehaviorSubject, Observable, 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 { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { Icons } from '~/app/shared/enum/icons.enum';
+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 { CephfsSubvolume, SubvolumeSnapshot } from '~/app/shared/models/cephfs-subvolume.model';
+import { CephfsSubvolumeSnapshotsFormComponent } from './cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { Permissions } from '~/app/shared/models/permissions';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
 
 @Component({
   selector: 'cd-cephfs-subvolume-snapshots-list',
@@ -18,6 +27,9 @@ export class CephfsSubvolumeSnapshotsListComponent implements OnInit, OnChanges
 
   context: CdTableFetchDataContext;
   columns: CdTableColumn[] = [];
+  tableActions: CdTableAction[];
+  selection = new CdTableSelection();
+  permissions: Permissions;
 
   subVolumes$: Observable<CephfsSubvolume[]>;
   snapshots$: Observable<any[]>;
@@ -37,8 +49,14 @@ export class CephfsSubvolumeSnapshotsListComponent implements OnInit, OnChanges
 
   constructor(
     private cephfsSubvolumeGroupService: CephfsSubvolumeGroupService,
-    private cephfsSubvolumeService: CephfsSubvolumeService
-  ) {}
+    private cephfsSubvolumeService: CephfsSubvolumeService,
+    private actionLabels: ActionLabelsI18n,
+    private modalService: ModalService,
+    private authStorageService: AuthStorageService,
+    private cdDatePipe: CdDatePipe
+  ) {
+    this.permissions = this.authStorageService.getPermissions();
+  }
 
   ngOnInit(): void {
     this.columns = [
@@ -51,7 +69,7 @@ export class CephfsSubvolumeSnapshotsListComponent implements OnInit, OnChanges
         name: $localize`Created`,
         prop: 'info.created_at',
         flexGrow: 1,
-        cellTransformation: CellTemplate.timeAgo
+        pipe: this.cdDatePipe
       },
       {
         name: $localize`Pending Clones`,
@@ -67,6 +85,15 @@ export class CephfsSubvolumeSnapshotsListComponent implements OnInit, OnChanges
       }
     ];
 
+    this.tableActions = [
+      {
+        name: this.actionLabels.CREATE,
+        permission: 'create',
+        icon: Icons.add,
+        click: () => this.openModal()
+      }
+    ];
+
     this.cephfsSubvolumeGroupService
       .get(this.fsName)
       .pipe(
@@ -145,4 +172,22 @@ export class CephfsSubvolumeSnapshotsListComponent implements OnInit, OnChanges
   fetchData() {
     this.snapshotSubject.next([]);
   }
+
+  openModal(edit = false) {
+    this.modalService.show(
+      CephfsSubvolumeSnapshotsFormComponent,
+      {
+        fsName: this.fsName,
+        subVolumeName: this.activeSubVolumeName,
+        subVolumeGroupName: this.activeGroupName,
+        subVolumeGroups: this.subvolumeGroupList,
+        isEdit: edit
+      },
+      { size: 'lg' }
+    );
+  }
+
+  updateSelection(selection: CdTableSelection) {
+    this.selection = selection;
+  }
 }
index 53544ccd9ed40536d8b910cf4f90f33ada8c779d..41451d9e3c0cc748a7be6ce359e999f7646e9bf4 100644 (file)
@@ -22,6 +22,7 @@ import { CephfsSubvolumegroupFormComponent } from './cephfs-subvolumegroup-form/
 import { CephfsSubvolumeSnapshotsListComponent } from './cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component';
 import { CephfsSnapshotscheduleListComponent } from './cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component';
 import { DataTableModule } from '../../shared/datatable/datatable.module';
+import { CephfsSubvolumeSnapshotsFormComponent } from './cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component';
 
 @NgModule({
   imports: [
@@ -51,7 +52,8 @@ import { DataTableModule } from '../../shared/datatable/datatable.module';
     CephfsSubvolumeGroupComponent,
     CephfsSubvolumegroupFormComponent,
     CephfsSubvolumeSnapshotsListComponent,
-    CephfsSnapshotscheduleListComponent
+    CephfsSnapshotscheduleListComponent,
+    CephfsSubvolumeSnapshotsFormComponent
   ]
 })
 export class CephfsModule {}
index 2e8448ff1a22de149796f047dce08ff42de20974..10ef5ea00f73a39f4b2d949c298df97c356d1fe5 100644 (file)
@@ -48,4 +48,10 @@ describe('CephfsSubvolumeService', () => {
     );
     expect(req.request.method).toBe('GET');
   });
+
+  it('should call createSnapshot', () => {
+    service.createSnapshot('testFS', 'testSnap', 'testSubvol').subscribe();
+    const req = httpTesting.expectOne('api/cephfs/subvolume/snapshot/');
+    expect(req.request.method).toBe('POST');
+  });
 });
index d76523aafd2adb6824a1789a46c817f89df7cab8..1995fd293bae0c0341f3e29a55a40c5df31787e7 100644 (file)
@@ -117,4 +117,48 @@ export class CephfsSubvolumeService {
       }
     );
   }
+
+  getSnapshotInfo(snapshotName: string, fsName: string, subVolumeName: string, groupName = '') {
+    return this.http.get(`${this.baseURL}/snapshot/${fsName}/${subVolumeName}/info`, {
+      params: {
+        snap_name: snapshotName,
+        group_name: groupName
+      }
+    });
+  }
+
+  snapshotExists(
+    fsName: string,
+    snapshotName: string,
+    subVolumeName: string,
+    groupName: string = ''
+  ): Observable<boolean> {
+    return this.getSnapshotInfo(fsName, snapshotName, subVolumeName, groupName).pipe(
+      mapTo(true),
+      catchError((error: Event) => {
+        if (_.isFunction(error.preventDefault)) {
+          error.preventDefault();
+        }
+        return of(false);
+      })
+    );
+  }
+
+  createSnapshot(
+    fsName: string,
+    snapshotName: string,
+    subVolumeName: string,
+    groupName: string = ''
+  ) {
+    return this.http.post(
+      `${this.baseURL}/snapshot/`,
+      {
+        vol_name: fsName,
+        subvol_name: subVolumeName,
+        snap_name: snapshotName,
+        group_name: groupName
+      },
+      { observe: 'response' }
+    );
+  }
 }
index bea426724e0736d396dac2f50048b0ca9dc85a5c..602a11e7343cd636903b00a8c7377198bcbc2cca 100644 (file)
@@ -18,7 +18,7 @@ export function isEmptyInputValue(value: any): boolean {
   return value == null || value.length === 0;
 }
 
-export type existsServiceFn = (value: any, args?: any) => Observable<boolean>;
+export type existsServiceFn = (value: any, ...args: any[]) => Observable<boolean>;
 
 export class CdValidators {
   /**
@@ -359,7 +359,7 @@ export class CdValidators {
     serviceFnThis: any = null,
     usernameFn?: Function,
     uidField = false,
-    extraArgs = ''
+    ...extraArgs: any[]
   ): AsyncValidatorFn {
     let uName: string;
     return (control: AbstractControl): Observable<ValidationErrors | null> => {
@@ -378,7 +378,7 @@ export class CdValidators {
       }
 
       return observableTimer().pipe(
-        switchMapTo(serviceFn.call(serviceFnThis, uName, extraArgs)),
+        switchMapTo(serviceFn.call(serviceFnThis, uName, ...extraArgs)),
         map((resp: boolean) => {
           if (!resp) {
             return null;
index b67ed62c8a6c7c60017571e11fa8eb0039ec304f..b711bdbb1cefd4909ad2da94df611c172e97288c 100644 (file)
@@ -1,15 +1,12 @@
-import { DatePipe } from '@angular/common';
-
 import moment from 'moment';
 
 import { CdDatePipe } from './cd-date.pipe';
 
 describe('CdDatePipe', () => {
-  const datePipe = new DatePipe('en-US');
-  let pipe = new CdDatePipe(datePipe);
+  let pipe = new CdDatePipe();
 
   it('create an instance', () => {
-    pipe = new CdDatePipe(datePipe);
+    pipe = new CdDatePipe();
     expect(pipe).toBeTruthy();
   });
 
@@ -18,7 +15,12 @@ describe('CdDatePipe', () => {
   });
 
   it('transforms with some date', () => {
-    const result = moment(1527085564486).format('M/D/YY LTS');
+    const result = moment
+      .parseZone(moment.unix(1527085564486))
+      .utc()
+      .utcOffset(moment().utcOffset())
+      .local()
+      .format('D/M/YY hh:mm A');
     expect(pipe.transform(1527085564486)).toBe(result);
   });
 });
index 911f320410f4b68117d7ac13a45b1550f6600d6e..887d8d6bfb98c52ed7bcfbc36eb268a4e491b4a2 100644 (file)
@@ -1,20 +1,30 @@
-import { DatePipe } from '@angular/common';
 import { Pipe, PipeTransform } from '@angular/core';
+import _ from 'lodash';
+import moment from 'moment';
 
 @Pipe({
   name: 'cdDate'
 })
 export class CdDatePipe implements PipeTransform {
-  constructor(private datePipe: DatePipe) {}
+  constructor() {}
 
   transform(value: any): any {
     if (value === null || value === '') {
       return '';
     }
-    return (
-      this.datePipe.transform(value, 'shortDate') +
-      ' ' +
-      this.datePipe.transform(value, 'mediumTime')
-    );
+    let date: string;
+    const offset = moment().utcOffset();
+    if (_.isNumber(value)) {
+      date = moment
+        .parseZone(moment.unix(value))
+        .utc()
+        .utcOffset(offset)
+        .local()
+        .format('D/M/YY hh:mm A');
+    } else {
+      value = value?.replace('Z', '');
+      date = moment.parseZone(value).utc().utcOffset(offset).local().format('D/M/YY hh:mm A');
+    }
+    return date;
   }
 }
index f6969c2e8e1b48a627f0c933b26f2b8b34289145..c1165d318a364312552448f7d835d8e0c87ddd98 100644 (file)
@@ -379,6 +379,10 @@ export class TaskMessageService {
     ),
     'cephfs/subvolume/group/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
       this.subvolumegroup(metadata)
+    ),
+    'cephfs/subvolume/snapshot/create': this.newTaskMessage(
+      this.commonOperations.create,
+      (metadata) => this.snapshot(metadata)
     )
   };
 
@@ -447,6 +451,10 @@ export class TaskMessageService {
     return $localize`subvolume group '${metadata.subvolumegroupName}'`;
   }
 
+  snapshot(metadata: any) {
+    return $localize`snapshot '${metadata.snapshotName}'`;
+  }
+
   crudMessageId(id: string) {
     return $localize`${id}`;
   }
index 0ed83aab6c8ec9fe9cccef68ad7e049cd10dd2e1..6129321c7df201983fe62fbd0a161754db03088a 100644 (file)
@@ -2019,6 +2019,52 @@ paths:
       - jwt: []
       tags:
       - CephfsSubvolumeGroup
+  /api/cephfs/subvolume/snapshot:
+    post:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                group_name:
+                  default: ''
+                  type: string
+                snap_name:
+                  type: string
+                subvol_name:
+                  type: string
+                vol_name:
+                  type: string
+              required:
+              - vol_name
+              - subvol_name
+              - snap_name
+              type: object
+      responses:
+        '201':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Resource created.
+        '202':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: Operation is still executing. Please check the task queue.
+        '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/snapshot/{vol_name}/{subvol_name}:
     get:
       parameters:
@@ -2061,6 +2107,48 @@ paths:
       - jwt: []
       tags:
       - CephfsSubvolumeSnapshot
+  /api/cephfs/subvolume/snapshot/{vol_name}/{subvol_name}/info:
+    get:
+      parameters:
+      - in: path
+        name: vol_name
+        required: true
+        schema:
+          type: string
+      - in: path
+        name: subvol_name
+        required: true
+        schema:
+          type: string
+      - in: query
+        name: snap_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:
+      - CephfsSubvolumeSnapshot
   /api/cephfs/subvolume/{vol_name}:
     delete:
       parameters: