]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: added dir search to snap schdl form
authorIvo Almeida <ialmeida@redhat.com>
Mon, 29 Jan 2024 10:58:43 +0000 (10:58 +0000)
committerIvo Almeida <ialmeida@redhat.com>
Wed, 14 Feb 2024 13:45:50 +0000 (13:45 +0000)
Fixes: https://tracker.ceph.com/issues/64246
Signed-off-by: Ivo Almeida <ialmeida@redhat.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/directory-store.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/directory-store.service.ts [new file with mode: 0644]

index 9e9cde86b325011fdf2cfb98897d2ceee24a2e05..e315e8ab7671dc6e259c5482242e6bff4f785caa 100644 (file)
                  i18n>Directory
           </label>
           <div class="cd-col-form-input">
-            <ng-template #loading>
+            <div class="input-group">
+            <input id="typeahead-http"
+                   i18n
+                   type="text"
+                   class="form-control"
+                   disabled="directoryStore.isLoading"
+                   formControlName="directory"
+                   [ngbTypeahead]="search"
+                   [placeholder]="directoryStore.isLoading ? 'Loading directories' : 'Directory search'" />
+            <div *ngIf="directoryStore.isLoading">
               <i [ngClass]="[icons.spinner, icons.spin, 'mt-2', 'me-2']"></i>
-              <span i18n>Loading directories</span>
-            </ng-template>
-            <select class="form-select"
-                    id="directory"
-                    name="directory"
-                    formControlName="directory"
-                    *ngIf="directories$ | async as directories; else loading">
-              <option [ngValue]="null"
-                      i18n>--Select a directory--</option>
-              <option *ngFor="let dir of directories"
-                      [value]="dir.path">{{ dir.path }}</option>
-            </select>
+            </div>
+          </div>
             <span class="invalid-feedback"
                   *ngIf="snapScheduleForm.showError('directory', formDir, 'required')"
                   i18n>This field is required.</span>
index 5b6d900e7520ab58b43959efb69a19b759d61722..41e55c72599ac62ee02e52e4f4c4bdddfe9733b6 100644 (file)
@@ -2,10 +2,10 @@ import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
 import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
 import { NgbActiveModal, NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap';
 import { uniq } from 'lodash';
-import { Observable, timer } from 'rxjs';
-import { map, switchMap } from 'rxjs/operators';
+import { Observable, OperatorFunction, of, timer } from 'rxjs';
+import { catchError, debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
 import { CephfsSnapshotScheduleService } from '~/app/shared/api/cephfs-snapshot-schedule.service';
-import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { DirectoryStoreService } from '~/app/shared/api/directory-store.service';
 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
 import { Icons } from '~/app/shared/enum/icons.enum';
 import { RepeatFrequency } from '~/app/shared/enum/repeat-frequency.enum';
@@ -13,12 +13,12 @@ import { RetentionFrequency } from '~/app/shared/enum/retention-frequency.enum';
 import { CdForm } from '~/app/shared/forms/cd-form';
 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
-import { CephfsDir } from '~/app/shared/models/cephfs-directory-models';
 import { FinishedTask } from '~/app/shared/models/finished-task';
 import { RetentionPolicy, SnapshotScheduleFormValue } from '~/app/shared/models/snapshot-schedule';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 
 const VALIDATON_TIMER = 300;
+const DEBOUNCE_TIMER = 300;
 
 @Component({
   selector: 'cd-cephfs-snapshotschedule-form',
@@ -42,15 +42,14 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
   resource!: string;
 
   columns!: CdTableColumn[];
-  directories$!: Observable<CephfsDir[]>;
 
   constructor(
     public activeModal: NgbActiveModal,
     private actionLabels: ActionLabelsI18n,
-    private cephfsService: CephfsService,
     private snapScheduleService: CephfsSnapshotScheduleService,
     private taskWrapper: TaskWrapperService,
-    private cd: ChangeDetectorRef
+    private cd: ChangeDetectorRef,
+    public directoryStore: DirectoryStoreService
   ) {
     super();
     this.resource = $localize`Snapshot schedule`;
@@ -70,7 +69,7 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
 
   ngOnInit(): void {
     this.action = this.actionLabels.CREATE;
-    this.directories$ = this.cephfsService.lsDir(this.id, '/', 3);
+    this.directoryStore.loadDirectories(this.id, '/', 3);
     this.createForm();
     this.loadingReady();
   }
@@ -79,6 +78,19 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
     return this.snapScheduleForm.get('retentionPolicies') as FormArray;
   }
 
+  search: OperatorFunction<string, readonly string[]> = (input: Observable<string>) =>
+    input.pipe(
+      debounceTime(DEBOUNCE_TIMER),
+      distinctUntilChanged(),
+      switchMap((term) =>
+        this.directoryStore.search(term, this.id).pipe(
+          catchError(() => {
+            return of([]);
+          })
+        )
+      )
+    );
+
   createForm() {
     this.snapScheduleForm = new CdFormGroup(
       {
index b13273dc4a21e3fcfc0a85795d99b7370864359b..687dd0b93ee933ac39cbd1248cb11047d424a029 100644 (file)
@@ -45,7 +45,8 @@ import { CephfsSnapshotscheduleFormComponent } from './cephfs-snapshotschedule-f
     NgbTooltipModule,
     DataTableModule,
     NgbDatepickerModule,
-    NgbTimepickerModule
+    NgbTimepickerModule,
+    NgbTypeaheadModule
   ],
   declarations: [
     CephfsDetailComponent,
index a265ae7a265864816bdb8dedea3bdfffc1a34f69..d2dfbc0e2a763b79da80640896a6c22fde597939 100644 (file)
@@ -6,6 +6,7 @@ import { Observable } from 'rxjs';
 
 import { cdEncode } from '../decorators/cd-encode';
 import { CephfsDir, CephfsQuotas } from '../models/cephfs-directory-models';
+import { shareReplay } from 'rxjs/operators';
 
 @cdEncode
 @Injectable({
@@ -26,7 +27,7 @@ export class CephfsService {
     if (path) {
       apiPath += `&path=${encodeURIComponent(path)}`;
     }
-    return this.http.get<CephfsDir[]>(apiPath);
+    return this.http.get<CephfsDir[]>(apiPath).pipe(shareReplay());
   }
 
   getCephfs(id: number) {
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/directory-store.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/directory-store.service.spec.ts
new file mode 100644 (file)
index 0000000..78590c8
--- /dev/null
@@ -0,0 +1,24 @@
+import { TestBed } from '@angular/core/testing';
+
+import { DirectoryStoreService } from './directory-store.service';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CephfsService } from './cephfs.service';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+
+describe('DirectoryStoreService', () => {
+  let service: DirectoryStoreService;
+
+  configureTestBed({
+    imports: [HttpClientTestingModule],
+    providers: [CephfsService]
+  });
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+    service = TestBed.inject(DirectoryStoreService);
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/directory-store.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/directory-store.service.ts
new file mode 100644 (file)
index 0000000..cdc5337
--- /dev/null
@@ -0,0 +1,64 @@
+import { Injectable } from '@angular/core';
+import { CephfsService } from './cephfs.service';
+import { BehaviorSubject, Observable, Subject, timer } from 'rxjs';
+import { CephfsDir } from '../models/cephfs-directory-models';
+import { filter, map, retry, share, switchMap, takeUntil, tap } from 'rxjs/operators';
+
+type DirectoryStore = Record<number, CephfsDir[]>;
+
+const POLLING_INTERVAL = 600 * 1000;
+
+@Injectable({
+  providedIn: 'root'
+})
+export class DirectoryStoreService {
+  private _directoryStoreSubject = new BehaviorSubject<DirectoryStore>({});
+
+  readonly directoryStore$: Observable<DirectoryStore> = this._directoryStoreSubject.asObservable();
+
+  stopDirectoryPolling = new Subject();
+
+  isLoading = true;
+
+  constructor(private cephFsService: CephfsService) {}
+
+  loadDirectories(id: number, path = '/', depth = 3) {
+    this.directoryStore$
+      .pipe(
+        filter((store: DirectoryStore) => !Boolean(store[id])),
+        switchMap(() =>
+          timer(0, POLLING_INTERVAL).pipe(
+            switchMap(() =>
+              this.cephFsService.lsDir(id, path, depth).pipe(
+                tap((response) => {
+                  this.isLoading = false;
+                  this._directoryStoreSubject.next({ [id]: response });
+                })
+              )
+            ),
+            retry(),
+            share(),
+            takeUntil(this.stopDirectoryPolling)
+          )
+        )
+      )
+      .subscribe();
+  }
+
+  search(term: string, id: number, limit = 5) {
+    return this.directoryStore$.pipe(
+      map((store: DirectoryStore) => {
+        const regEx = new RegExp(term, 'gi');
+        const results = store[id]
+          .filter((x) => regEx.test(x.path))
+          .map((x) => x.path)
+          .slice(0, limit);
+        return results;
+      })
+    );
+  }
+
+  stopPollingDictories() {
+    this.stopDirectoryPolling.next();
+  }
+}