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>
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';
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',
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`;
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();
}
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(
{
NgbTooltipModule,
DataTableModule,
NgbDatepickerModule,
- NgbTimepickerModule
+ NgbTimepickerModule,
+ NgbTypeaheadModule
],
declarations: [
CephfsDetailComponent,
import { cdEncode } from '../decorators/cd-encode';
import { CephfsDir, CephfsQuotas } from '../models/cephfs-directory-models';
+import { shareReplay } from 'rxjs/operators';
@cdEncode
@Injectable({
if (path) {
apiPath += `&path=${encodeURIComponent(path)}`;
}
- return this.http.get<CephfsDir[]>(apiPath);
+ return this.http.get<CephfsDir[]>(apiPath).pipe(shareReplay());
}
getCephfs(id: number) {
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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();
+ }
+}