Improve global search component (#15462)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
孙世军 2021-08-24 17:04:37 +08:00 committed by GitHub
parent a8562b2934
commit eca3d82d9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 135 additions and 147 deletions

View File

@ -1,5 +1,5 @@
<form class="search">
<label for="search_input">
<input #globalSearchBox name="globalSearchBox" [(ngModel)]="searchTerm" id="search_input" type="text" (keyup)="search(globalSearchBox.value)" placeholder='{{placeholderText | translate}}'>
<input autocomplete="off" #globalSearchBox name="globalSearchBox" [(ngModel)]="searchTerm" id="search_input" type="text" (keyup)="search(globalSearchBox.value)" placeholder='{{placeholderText | translate}}'>
</label>
</form>

View File

@ -1,4 +1,3 @@
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
@ -13,20 +12,17 @@ import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Router } from '@angular/router';
import { Subject , Subscription } from "rxjs";
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subject, Subscription } from "rxjs";
import { SearchTriggerService } from './search-trigger.service';
import { AppConfigService } from '../../../services/app-config.service';
import {TranslateService} from "@ngx-translate/core";
import {SkinableConfig} from "../../../services/skinable-config.service";
import { TranslateService } from "@ngx-translate/core";
import { SkinableConfig } from "../../../services/skinable-config.service";
import { Location } from '@angular/common';
const deBounceTime = 500; // ms
const SEARCH_KEY: string = 'globalSearch';
@Component({
selector: 'global-search',
@ -43,13 +39,13 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
// To indicate if the result panel is opened
isResPanelOpened: boolean = false;
searchTerm: string = "";
placeholderText: string;
private _searchTerm = "";
constructor(
private searchTrigger: SearchTriggerService,
private router: Router,
private activatedRoute: ActivatedRoute,
private location: Location,
private appConfigService: AppConfigService,
private translate: TranslateService,
private skinableConfig: SkinableConfig) {
@ -71,8 +67,7 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
}
this.searchSub = this.searchTerms.pipe(
debounceTime(deBounceTime),
distinctUntilChanged())
debounceTime(deBounceTime))
.subscribe(term => {
this.searchTrigger.triggerSearch(term);
});
@ -83,6 +78,11 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
if (this.appConfigService.isIntegrationMode()) {
this.placeholderText = "GLOBAL_SEARCH.PLACEHOLDER_VIC";
}
// init _searchTerm from queryParams
this._searchTerm = this.activatedRoute.snapshot.queryParams[SEARCH_KEY];
if (this._searchTerm) {
this.searchTerms.next(this._searchTerm);
}
}
ngOnDestroy(): void {
@ -98,7 +98,24 @@ export class GlobalSearchComponent implements OnInit, OnDestroy {
// Handle the term inputting event
search(term: string): void {
// Send event even term is empty
this.searchTerms.next(term.trim());
}
get searchTerm(): string {
return this._searchTerm;
}
set searchTerm(s) {
let url: string;
if (s) {
url = this.router.createUrlTree([], {
relativeTo: this.activatedRoute,
queryParams: {[SEARCH_KEY]: s}
}).toString();
} else {
url = this.router.createUrlTree([], {
relativeTo: this.activatedRoute,
}).toString();
}
this.location.replaceState(url);
this._searchTerm = s;
}
}

View File

@ -1,5 +1,5 @@
import { Router } from '@angular/router';
import { Component, OnInit, Input } from '@angular/core';
import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
import { HelmChartSearchResultItem, HelmChartVersion, HelmChartMaintainer } from '../../../base/project/helm-chart/helm-chart-detail/helm-chart.interface.service';
import { SearchTriggerService } from '../global-search/search-trigger.service';
import { ProjectService } from "../../services";
@ -7,11 +7,10 @@ import { ProjectService } from "../../services";
@Component({
selector: 'list-chart-version-ro',
templateUrl: './list-chart-version-ro.component.html'
templateUrl: './list-chart-version-ro.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListChartVersionRoComponent implements OnInit {
@Input() projectId: number;
@Input() charts: HelmChartSearchResultItem[];
constructor(

View File

@ -1,14 +1,15 @@
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-datagrid>
<clr-dg-column>{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.ACCESS_LEVEL' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let p of projects" [clrDgItem]="p">
<clr-dg-cell><a href="javascript:void(0)" (click)="goToLink(p.project_id)">{{p.name}}</a></clr-dg-cell>
<clr-dg-cell><a href="javascript:void(0)" [routerLink]="getLink(p.project_id)">{{p.name}}</a></clr-dg-cell>
<clr-dg-cell>{{ (p.metadata.public === 'true' ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}}</clr-dg-cell>
<clr-dg-cell>{{p.repo_count}}</clr-dg-cell>
<clr-dg-cell>{{p.creation_time | harborDatetime: 'short'}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-placeholder>{{'PROJECT.NO_PROJECT' | translate }}</clr-dg-placeholder>
<clr-dg-footer>
<span *ngIf="projects?.length">{{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'PROJECT.OF' | translate}} </span> {{projects?.length}} {{'PROJECT.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="5"></clr-dg-pagination>

View File

@ -1,47 +1,50 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { ListProjectROComponent } from './list-project-ro.component';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ClarityModule } from '@clr/angular';
import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { SearchTriggerService } from '../global-search/search-trigger.service';
import { SharedTestingModule } from "../../shared.module";
import { Project } from "../../../../../ng-swagger-gen/models/project";
import { Component } from '@angular/core';
// mock a TestHostComponent for ListProjectROComponent
@Component({
template: `
<list-project-ro [projects]="projects">
</list-project-ro>`
})
class TestHostComponent {
projects: Project[] = [];
}
describe('ListProjectROComponent', () => {
let component: ListProjectROComponent;
let fixture: ComponentFixture<ListProjectROComponent>;
const mockSearchTriggerService = {
closeSearch: () => { }
};
let component: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;
const mockedProjects: Project[] = [
{
chart_count: 0,
name: "test1",
metadata: {},
project_id: 1,
repo_count: 1
},
{
chart_count: 0,
name: "test2",
metadata: {},
project_id: 2,
repo_count: 1
}
];
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
imports: [
BrowserAnimationsModule,
ClarityModule,
TranslateModule.forRoot(),
FormsModule,
RouterTestingModule,
NoopAnimationsModule,
HttpClientTestingModule
SharedTestingModule
],
declarations: [ListProjectROComponent],
providers: [
TranslateService,
{ provide: SearchTriggerService, useValue: mockSearchTriggerService }
]
})
.compileComponents();
declarations: [ListProjectROComponent,
TestHostComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ListProjectROComponent);
fixture = TestBed.createComponent(TestHostComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@ -49,4 +52,12 @@ describe('ListProjectROComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render project list', async () => {
component.projects = mockedProjects;
fixture.detectChanges();
await fixture.whenStable();
const rows = fixture.nativeElement.querySelectorAll('clr-dg-row');
expect(rows.length).toEqual(2);
});
});

View File

@ -11,12 +11,8 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, EventEmitter, Output, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Router } from '@angular/router';
import { State } from '../../services/interface';
import { SearchTriggerService } from '../global-search/search-trigger.service';
import { Project } from '../../../base/project/project';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Project } from "../../../../../ng-swagger-gen/models/project";
@Component({
selector: 'list-project-ro',
@ -25,21 +21,11 @@ import { Project } from '../../../base/project/project';
})
export class ListProjectROComponent {
@Input() projects: Project[];
@Output() paginate = new EventEmitter<State>();
constructor(
private searchTrigger: SearchTriggerService,
private router: Router) {}
goToLink(proId: number): void {
this.searchTrigger.closeSearch(true);
let linkUrl = ['harbor', 'projects', proId, 'repositories'];
this.router.navigate(linkUrl);
constructor() {
}
refresh(state: State) {
this.paginate.emit(state);
getLink(proId: number) {
return `/harbor/projects/${proId}/repositories`;
}
}

View File

@ -1,12 +1,13 @@
<clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-datagrid>
<clr-dg-column>{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.ARTIFACTS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let r of repositories" [clrDgItem]='r'>
<clr-dg-cell><a href="javascript:void(0)" (click)="gotoLink(projectId || r.project_id, r.name || r.repository_name)">{{r.name || r.repository_name}}</a></clr-dg-cell>
<clr-dg-cell><a href="javascript:void(0)" [routerLink]="getLink(r.project_id, r.name || r.repository_name)" [queryParams]="getQueryParams()">{{r.name || r.repository_name}}</a></clr-dg-cell>
<clr-dg-cell>{{r.artifact_count}}</clr-dg-cell>
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-placeholder>{{'REPOSITORY.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-footer>
<span *ngIf="repositories?.length">{{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'REPOSITORY.OF' | translate}} </span> {{repositories?.length }} {{'REPOSITORY.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="5"></clr-dg-pagination>

View File

@ -1,5 +1,3 @@
import {filter} from 'rxjs/operators';
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
@ -13,72 +11,39 @@ import {filter} from 'rxjs/operators';
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, Output, OnDestroy, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef, OnInit } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { State } from '../../services/interface';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Router } from '@angular/router';
import { Repository } from '../../../../../ng-swagger-gen/models/repository';
import { SearchTriggerService } from '../global-search/search-trigger.service';
import {Subscription} from "rxjs";
import { SessionService } from "../../services/session.service";
import { UN_LOGGED_PARAM } from "../../../account/sign-in/sign-in.service";
const YES: string = 'yes';
@Component({
selector: 'list-repository-ro',
templateUrl: 'list-repository-ro.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListRepositoryROComponent implements OnInit, OnDestroy {
export class ListRepositoryROComponent {
@Input() projectId: number;
@Input() repositories: Repository[];
@Output() paginate = new EventEmitter<State>();
routerSubscription: Subscription;
constructor(
private router: Router,
private searchTrigger: SearchTriggerService,
private ref: ChangeDetectorRef,
private sessionService: SessionService) {
this.router.routeReuseStrategy.shouldReuseRoute = function() {
return false;
};
this.routerSubscription = this.router.events.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event) => {
// trick the Router into believing it's last link wasn't previously loaded
this.router.navigated = false;
// if you need to scroll back to top, here is the right place
window.scrollTo(0, 0);
});
}
ngOnInit(): void {
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 1000);
}
ngOnDestroy(): void {
this.routerSubscription.unsubscribe();
}
refresh(state: State) {
if (this.repositories) {
this.paginate.emit(state);
}
}
public gotoLink(projectId: number, repoName: string): void {
this.searchTrigger.closeSearch(true);
getLink(projectId: number, repoName: string) {
let projectName = repoName.split('/')[0];
let repositorieName = projectName ? repoName.substr(projectName.length + 1) : repoName;
let linkUrl = ['harbor', 'projects', projectId, 'repositories', repositorieName ];
return `/harbor/projects/${projectId}/repositories/${repositorieName}`;
}
getQueryParams() {
if (this.sessionService.getCurrentUser()) {
this.router.navigate(linkUrl);
} else {// if not logged in and it's a public project, add param 'publicAndNotLogged'
this.router.navigate(linkUrl, {queryParams: {[UN_LOGGED_PARAM]: YES}});
return null;
}
return {[UN_LOGGED_PARAM]: YES};
}
}

View File

@ -254,7 +254,8 @@
"PROXY_CACHE": "Proxy Cache",
"PROXY_CACHE_TOOLTIP": "Die Aktivierung der Funktion erlaubt es dem Projekt, als Cache für eine andere Registry Instanz zu dienen. Harbor unterstützt die Proxy Funktion nur für DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Quay und Google GCR.",
"ENDPOINT": "Endpunkt",
"PROXY_CACHE_ENDPOINT": "Proxy Cache Endpunkt"
"PROXY_CACHE_ENDPOINT": "Proxy Cache Endpunkt",
"NO_PROJECT": "We couldn't find any projects"
},
"PROJECT_DETAIL": {
"SUMMARY": "Zusammenfassung",

View File

@ -254,7 +254,8 @@
"PROXY_CACHE": "Proxy Cache",
"PROXY_CACHE_TOOLTIP": "Enable this to allow this project to act as a pull-through cache for a particular target registry instance. Harbor can only act a proxy for DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Quay and Google GCR registries.",
"ENDPOINT": "Endpoint",
"PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint"
"PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint",
"NO_PROJECT": "We couldn't find any projects"
},
"PROJECT_DETAIL": {
"SUMMARY": "Summary",

View File

@ -255,7 +255,8 @@
"PROXY_CACHE": "Proxy Cache",
"PROXY_CACHE_TOOLTIP": "Enable this to allow this project to act as a pull-through cache for a particular target registry instance. Harbor can only act a proxy for DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Quay and Google GCR registries.",
"ENDPOINT": "Endpoint",
"PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint"
"PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint",
"NO_PROJECT": "We couldn't find any projects"
},
"PROJECT_DETAIL": {
"SUMMARY": "Summary",

View File

@ -248,7 +248,8 @@
"PROXY_CACHE": "Proxy Cache",
"PROXY_CACHE_TOOLTIP": "Enable this to allow this project to act as a pull-through cache for a particular target registry instance. Harbor can only act a proxy for DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Quay and Google GCR registries.",
"ENDPOINT": "Endpoint",
"PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint"
"PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint",
"NO_PROJECT": "We couldn't find any projects"
},
"PROJECT_DETAIL": {
"SUMMARY": "Summary",

View File

@ -252,7 +252,8 @@
"PROXY_CACHE": "Proxy Cache",
"PROXY_CACHE_TOOLTIP": "Enable this to allow this project to act as a pull-through cache for a particular target registry instance. Harbor can only act a proxy for DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Quay and Google GCR registries.",
"ENDPOINT": "Endpoint",
"PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint"
"PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint",
"NO_PROJECT": "We couldn't find any projects"
},
"PROJECT_DETAIL": {
"SUMMARY": "Summary",

View File

@ -254,7 +254,8 @@
"PROXY_CACHE": "Proxy Cache",
"PROXY_CACHE_TOOLTIP": "Enable this to allow this project to act as a pull-through cache for a particular target registry instance. Harbor can only act a proxy for DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Quay and Google GCR registries.",
"ENDPOINT": "Endpoint",
"PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint"
"PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint",
"NO_PROJECT": "We couldn't find any projects"
},
"PROJECT_DETAIL": {
"SUMMARY": "Özet",

View File

@ -253,7 +253,8 @@
"PROXY_CACHE": "镜像代理",
"PROXY_CACHE_TOOLTIP": "开启此项,以使得该项目成为目标仓库的镜像代理.仅支持 DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Quay 和 Google GCR 类型的仓库",
"ENDPOINT": "地址",
"PROXY_CACHE_ENDPOINT": "镜像代理地址"
"PROXY_CACHE_ENDPOINT": "镜像代理地址",
"NO_PROJECT": "未发现任何项目"
},
"PROJECT_DETAIL": {
"SUMMARY": "概要",

View File

@ -251,7 +251,8 @@
"PROXY_CACHE": "Proxy Cache",
"PROXY_CACHE_TOOLTIP": "Enable this to allow this project to act as a pull-through cache for a particular target registry instance. Harbor can only act a proxy for DockerHub, Docker Registry, Harbor, Aws ECR, Azure ACR, Quay and Google GCR registries.",
"ENDPOINT": "Endpoint",
"PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint"
"PROXY_CACHE_ENDPOINT": "Proxy Cache Endpoint",
"NO_PROJECT": "We couldn't find any projects"
},
"PROJECT_DETAIL":{
"SUMMARY": "概要",