From a34ff61f3428289f59bc8f48a9e728db957c1738 Mon Sep 17 00:00:00 2001 From: AllForNothing Date: Tue, 17 Dec 2019 16:00:57 +0800 Subject: [PATCH] Improve repo datagrid search function Signed-off-by: AllForNothing --- .../repository-gridview.component.html | 6 +- .../repository-gridview.component.spec.ts | 136 +++++++----------- .../repository-gridview.component.ts | 84 ++++++----- 3 files changed, 104 insertions(+), 122 deletions(-) diff --git a/src/portal/src/lib/components/repository-gridview/repository-gridview.component.html b/src/portal/src/lib/components/repository-gridview/repository-gridview.component.html index ed2bce3d0..bcd4b92da 100644 --- a/src/portal/src/lib/components/repository-gridview/repository-gridview.component.html +++ b/src/portal/src/lib/components/repository-gridview/repository-gridview.component.html @@ -8,7 +8,7 @@ {{'CONFIG.REGISTRY_CERTIFICATE' | translate | uppercase}} - + @@ -23,7 +23,7 @@
- + @@ -39,7 +39,7 @@ {{r.pull_count}} - {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} {{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}} diff --git a/src/portal/src/lib/components/repository-gridview/repository-gridview.component.spec.ts b/src/portal/src/lib/components/repository-gridview/repository-gridview.component.spec.ts index e4bba3b39..83642abfc 100644 --- a/src/portal/src/lib/components/repository-gridview/repository-gridview.component.spec.ts +++ b/src/portal/src/lib/components/repository-gridview/repository-gridview.component.spec.ts @@ -1,40 +1,31 @@ import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; - import { RouterTestingModule } from '@angular/router/testing'; - import { SharedModule } from '../../utils/shared/shared.module'; -import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; -import { ImageNameInputComponent } from "../image-name-input/image-name-input.component"; import { RepositoryGridviewComponent } from './repository-gridview.component'; -import { TagComponent } from '../tag/tag.component'; -import { FilterComponent } from '../filter/filter.component'; - import { ErrorHandler } from '../../utils/error-handler/error-handler'; import { Repository, RepositoryItem, SystemInfo } from '../../services/interface'; import { SERVICE_CONFIG, IServiceConfig } from '../../entities/service.config'; -import { RepositoryService, RepositoryDefaultService } from '../../services/repository.service'; +import { RepositoryService } from '../../services/repository.service'; import { TagService, TagDefaultService } from '../../services/tag.service'; -import { SystemInfoService, SystemInfoDefaultService } from '../../services/system-info.service'; -import { LabelPieceComponent } from "../label-piece/label-piece.component"; +import { SystemInfoService } from '../../services/system-info.service'; import { OperationService } from "../operation/operation.service"; -import { ProjectDefaultService, ProjectService, RetagDefaultService, RetagService } from "../../services"; -import { UserPermissionService, UserPermissionDefaultService } from "../../services/permission.service"; -import { USERSTATICPERMISSION } from "../../services/permission-static"; +import { + ProjectDefaultService, + ProjectService, + RequestQueryParams, + RetagDefaultService, + RetagService +} from "../../services"; +import { UserPermissionService } from "../../services/permission.service"; import { of } from "rxjs"; import { HarborLibraryModule } from "../../harbor-library.module"; +import { delay } from 'rxjs/operators'; describe('RepositoryComponentGridview (inline template)', () => { let compRepo: RepositoryGridviewComponent; let fixtureRepo: ComponentFixture; - let repositoryService: RepositoryService; - let systemInfoService: SystemInfoService; - let userPermissionService: UserPermissionService; - - let spyRepos: jasmine.Spy; - let spySystemInfo: jasmine.Spy; - let mockSystemInfo: SystemInfo = { "with_notary": true, "with_admiral": false, @@ -46,7 +37,6 @@ describe('RepositoryComponentGridview (inline template)', () => { "has_ca_root": false, "harbor_version": "v1.1.1-rc1-160-g565110d" }; - let mockRepoData: RepositoryItem[] = [ { "id": 1, @@ -87,28 +77,34 @@ describe('RepositoryComponentGridview (inline template)', () => { metadata: { xTotalCount: 2 }, data: mockRepoNginxData }; - let mockHasCreateRepositoryPermission: boolean = true; - let mockHasDeleteRepositoryPermission: boolean = true; - // let mockTagData: Tag[] = [ - // { - // "digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55", - // "name": "1.11.5", - // "size": "2049", - // "architecture": "amd64", - // "os": "linux", - // "docker_version": "1.12.3", - // "author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"", - // "created": new Date("2016-11-08T22:41:15.912313785Z"), - // "signature": null, - // "labels": [] - // } - // ]; - let config: IServiceConfig = { repositoryBaseEndpoint: '/api/repository/testing', systemInfoEndpoint: '/api/systeminfo/testing', targetBaseEndpoint: '/api/tag/testing' }; + const fakedErrorHandler = { + error() { + return undefined; + } + }; + const fakedSystemInfoService = { + getSystemInfo() { + return of(mockSystemInfo); + } + }; + const fakedRepositoryService = { + getRepositories(projectId: number, name: string, param?: RequestQueryParams) { + if (name === 'nginx') { + return of(mockNginxRepo); + } + return of(mockRepo).pipe(delay(0)); + } + }; + const fakedUserPermissionService = { + getPermission() { + return of(true); + } + }; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -118,15 +114,15 @@ describe('RepositoryComponentGridview (inline template)', () => { HarborLibraryModule ], providers: [ - ErrorHandler, + { provide: ErrorHandler, useValue: fakedErrorHandler }, { provide: SERVICE_CONFIG, useValue: config }, - { provide: RepositoryService, useClass: RepositoryDefaultService }, + { provide: RepositoryService, useValue: fakedRepositoryService }, { provide: TagService, useClass: TagDefaultService }, { provide: ProjectService, useClass: ProjectDefaultService }, { provide: RetagService, useClass: RetagDefaultService }, - { provide: SystemInfoService, useClass: SystemInfoDefaultService }, - { provide: UserPermissionService, useClass: UserPermissionDefaultService }, - { provide: OperationService } + { provide: SystemInfoService, useValue: fakedSystemInfoService }, + { provide: UserPermissionService, useValue: fakedUserPermissionService }, + { provide: OperationService }, ] }); })); @@ -137,34 +133,14 @@ describe('RepositoryComponentGridview (inline template)', () => { compRepo.projectId = 1; compRepo.mode = ''; compRepo.hasProjectAdminRole = true; - - repositoryService = fixtureRepo.debugElement.injector.get(RepositoryService); - systemInfoService = fixtureRepo.debugElement.injector.get(SystemInfoService); - - spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(of(mockSystemInfo)); - spyRepos = spyOn(repositoryService, 'getRepositories') - .and.callFake(function (projectId: number, name: string) { - if (name === 'nginx') { - return of(mockNginxRepo); - } - return of(mockRepo); - }); - userPermissionService = fixtureRepo.debugElement.injector.get(UserPermissionService); - spyOn(userPermissionService, "getPermission") - .withArgs(compRepo.projectId, - USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.CREATE) - .and.returnValue(of(mockHasCreateRepositoryPermission)) - .withArgs(compRepo.projectId, USERSTATICPERMISSION.REPOSITORY.KEY, USERSTATICPERMISSION.REPOSITORY.VALUE.DELETE) - .and.returnValue(of(mockHasDeleteRepositoryPermission)); + compRepo.isCardView = false; fixtureRepo.detectChanges(); }); let originalTimeout; - beforeEach(function () { originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000; }); - afterEach(function () { jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; }); @@ -183,23 +159,19 @@ describe('RepositoryComponentGridview (inline template)', () => { expect(elRepo.textContent).toEqual('library/busybox'); }); })); - // Will fail after upgrade to angular 6. todo: need to fix it. - it('should filter data by keyword', async(() => { - fixtureRepo.whenStable().then(() => { - fixtureRepo.detectChanges(); - compRepo.doSearchRepoNames('nginx'); - fixtureRepo.whenStable().then(() => { - - fixtureRepo.detectChanges(); - let de: DebugElement[] = fixtureRepo.debugElement.queryAll(By.css('.datagrid-cell')); - expect(de).toBeTruthy(); - expect(compRepo.repositories.length).toEqual(1); - expect(de.length).toEqual(1); - let el: HTMLElement = de[0].nativeElement; - expect(el).toBeTruthy(); - expect(el.textContent).toEqual('library/nginx'); - }); - }); - })); + it('should filter data by keyword', async () => { + fixtureRepo.detectChanges(); + await fixtureRepo.whenStable(); + compRepo.doSearchRepoNames('nginx'); + fixtureRepo.detectChanges(); + await fixtureRepo.whenStable(); + let de: DebugElement[] = fixtureRepo.debugElement.queryAll(By.css('.datagrid-cell')); + expect(de).toBeTruthy(); + expect(compRepo.repositories.length).toEqual(1); + expect(de.length).toEqual(4); + let el: HTMLElement = de[1].nativeElement; + expect(el).toBeTruthy(); + expect(el.textContent).toEqual('library/nginx'); + }); }); diff --git a/src/portal/src/lib/components/repository-gridview/repository-gridview.component.ts b/src/portal/src/lib/components/repository-gridview/repository-gridview.component.ts index 612b47876..f861af776 100644 --- a/src/portal/src/lib/components/repository-gridview/repository-gridview.component.ts +++ b/src/portal/src/lib/components/repository-gridview/repository-gridview.component.ts @@ -9,14 +9,12 @@ import { EventEmitter, OnChanges, SimpleChanges, - Inject + Inject, OnDestroy } from "@angular/core"; import { Router } from "@angular/router"; -import { forkJoin } from "rxjs"; -import { finalize } from "rxjs/operators"; +import { forkJoin, Subscription } from "rxjs"; +import { debounceTime, distinctUntilChanged, finalize, switchMap } from "rxjs/operators"; import { TranslateService } from "@ngx-translate/core"; -import { Comparator, State } from "../../services/interface"; - import { Repository, SystemInfo, @@ -26,7 +24,7 @@ import { RepositoryItem, TagService } from '../../services'; -import { ErrorHandler } from '../../utils/error-handler/error-handler'; +import { ErrorHandler } from '../../utils/error-handler'; import { DEFAULT_PAGE_SIZE, calculatePage, clone } from '../../utils/utils'; import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../../entities/shared.const'; import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; @@ -43,13 +41,13 @@ import { map, catchError } from "rxjs/operators"; import { Observable, throwError as observableThrowError } from "rxjs"; import { errorHandler as errorHandFn } from "../../utils/shared/shared.utils"; import { ClrDatagridStateInterface } from "@clr/angular"; +import { FilterComponent } from "../filter/filter.component"; @Component({ selector: "hbr-repository-gridview", templateUrl: "./repository-gridview.component.html", styleUrls: ["./repository-gridview.component.scss"], - changeDetection: ChangeDetectionStrategy.OnPush }) -export class RepositoryGridviewComponent implements OnChanges, OnInit { +export class RepositoryGridviewComponent implements OnChanges, OnInit, OnDestroy { signedCon: { [key: string]: any | string[] } = {}; downloadLink: string; @Input() projectId: number; @@ -76,7 +74,7 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { pageSize: number = DEFAULT_PAGE_SIZE; currentPage = 1; totalCount = 0; - currentState: State; + currentState: ClrDatagridStateInterface; @ViewChild("confirmationDialog", {static: false}) confirmationDialog: ConfirmationDialogComponent; @@ -84,6 +82,9 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { @ViewChild("gridView", {static: false}) gridView: GridViewComponent; hasCreateRepositoryPermission: boolean; hasDeleteRepositoryPermission: boolean; + @ViewChild(FilterComponent, {static: true}) + filterComponent: FilterComponent; + searchSub: Subscription; constructor(@Inject(SERVICE_CONFIG) private configInfo: IServiceConfig, private errorHandler: ErrorHandler, private translateService: TranslateService, @@ -92,8 +93,7 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { private tagService: TagService, private operationService: OperationService, public userPermissionService: UserPermissionService, - private ref: ChangeDetectorRef, - private router: Router) { + ) { if (this.configInfo && this.configInfo.systemInfoEndpoint) { this.downloadLink = this.configInfo.systemInfoEndpoint + "/getcert"; } @@ -129,14 +129,40 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { .subscribe(systemInfo => (this.systemInfo = systemInfo) , error => this.errorHandler.error(error)); - if (this.mode === "admiral") { - this.isCardView = true; - } else { - this.isCardView = false; - } + this.isCardView = this.mode === "admiral"; this.lastFilteredRepoName = ""; this.getHelmChartVersionPermission(this.projectId); + if (!this.searchSub) { + this.searchSub = this.filterComponent.filterTerms.pipe( + debounceTime(500), + distinctUntilChanged(), + switchMap(repoName => { + this.lastFilteredRepoName = repoName; + this.currentPage = 1; + // Pagination + let params: RequestQueryParams = new RequestQueryParams() + .set("page", "" + this.currentPage).set("page_size", "" + this.pageSize); + this.loading = true; + return this.repositoryService.getRepositories(this.projectId, this.lastFilteredRepoName, params); + }) + ).subscribe((repo: Repository) => { + this.totalCount = repo.metadata.xTotalCount; + this.repositories = repo.data; + this.signedCon = {}; + this.loading = false; + }, error => { + this.loading = false; + this.errorHandler.error(error); + }); + } + } + + ngOnDestroy() { + if (this.searchSub) { + this.searchSub.unsubscribe(); + this.searchSub = null; + } } confirmDeletion(message: ConfirmationAcknowledgement) { @@ -160,7 +186,7 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { forkJoin(observableLists).subscribe((item) => { this.selectedRow = []; this.refresh(); - let st: State = this.getStateAfterDeletion(); + let st: ClrDatagridStateInterface = this.getStateAfterDeletion(); if (!st) { this.refresh(); } else { @@ -214,7 +240,7 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { doSearchRepoNames(repoName: string) { this.lastFilteredRepoName = repoName; this.currentPage = 1; - let st: State = this.currentState; + let st: ClrDatagridStateInterface = this.currentState; if (!st || !st.page) { st = { page: {} }; } @@ -264,11 +290,7 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { { repoName: repoName, signedImages: signature, - }).pipe(finalize(() => { - let hnd = setInterval(() => this.ref.markForCheck(), 100); - setTimeout(() => clearInterval(hnd), 5000); - })) - .subscribe((res: string) => { + }).subscribe((res: string) => { summaryKey = res; let message = new ConfirmationMessage( summaryTitle, @@ -324,12 +346,6 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { evt.stopPropagation(); this.deleteRepos([item]); } - - selectedChange(): void { - let hnd = setInterval(() => this.ref.markForCheck(), 100); - setTimeout(() => clearInterval(hnd), 2000); - } - refresh() { this.doSearchRepoNames(""); } @@ -356,8 +372,6 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { this.loading = false; this.errorHandler.error(error); }); - let hnd = setInterval(() => this.ref.markForCheck(), 500); - setTimeout(() => clearInterval(hnd), 5000); } clrLoad(state: ClrDatagridStateInterface): void { @@ -401,13 +415,9 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { this.loading = false; this.errorHandler.error(error); }); - - // Force refresh view - let hnd = setInterval(() => this.ref.markForCheck(), 100); - setTimeout(() => clearInterval(hnd), 5000); } - getStateAfterDeletion(): State { + getStateAfterDeletion(): ClrDatagridStateInterface { let total: number = this.totalCount - 1; if (total <= 0) { return null; @@ -420,7 +430,7 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit { targetPageNumber = totalPages; // Should == currentPage -1 } - let st: State = this.currentState; + let st: ClrDatagridStateInterface = this.currentState; if (!st) { st = { page: {} }; }