diff --git a/src/portal/lib/src/project-policy-config/project-policy-config.component.html b/src/portal/lib/src/project-policy-config/project-policy-config.component.html index a07c339d2..141901671 100644 --- a/src/portal/lib/src/project-policy-config/project-policy-config.component.html +++ b/src/portal/lib/src/project-policy-config/project-policy-config.component.html @@ -20,8 +20,8 @@ {{ 'PROJECT_CONFIG.CONTENT_TRUST_POLCIY' | translate }} - - + + diff --git a/src/portal/lib/src/project-policy-config/project-policy-config.component.scss b/src/portal/lib/src/project-policy-config/project-policy-config.component.scss index 9d8a6f486..c447ac52b 100644 --- a/src/portal/lib/src/project-policy-config/project-policy-config.component.scss +++ b/src/portal/lib/src/project-policy-config/project-policy-config.component.scss @@ -95,4 +95,7 @@ flex-direction: column-reverse; font-size: 13px; color: #000; -} \ No newline at end of file +} +.margin-top-05 { + margin-top: 0.5rem; +} diff --git a/src/portal/lib/src/service/project.service.ts b/src/portal/lib/src/service/project.service.ts index 5c3dcc4e3..8b1bd4144 100644 --- a/src/portal/lib/src/service/project.service.ts +++ b/src/portal/lib/src/service/project.service.ts @@ -1,5 +1,5 @@ -import {throwError as observableThrowError, Observable } from "rxjs"; +import { throwError as observableThrowError, Observable, of } from "rxjs"; import {Injectable, Inject} from "@angular/core"; import { HttpClient, HttpParams, HttpResponse } from "@angular/common/http"; import { catchError } from "rxjs/operators"; @@ -169,8 +169,13 @@ export class ProjectDefaultService extends ProjectService { public checkProjectExists(projectName: string): Observable { return this.http - .head(`/api/projects/?project_name=${projectName}`).pipe( - catchError(error => observableThrowError(error)), ); + .head(`/api/projects/?project_name=${projectName}`).pipe( + catchError(error => { + if (error && error.status === 404) { + return of(error); + } + return observableThrowError(error); + })); } public checkProjectMember(projectId: number): Observable { diff --git a/src/portal/src/app/base/global-search/global-search.component.ts b/src/portal/src/app/base/global-search/global-search.component.ts index 2aba69e2c..7532efdcd 100644 --- a/src/portal/src/app/base/global-search/global-search.component.ts +++ b/src/portal/src/app/base/global-search/global-search.component.ts @@ -1,5 +1,5 @@ -import {debounceTime} from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; // Copyright (c) 2017 VMware, Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -71,7 +71,8 @@ export class GlobalSearchComponent implements OnInit, OnDestroy { } this.searchSub = this.searchTerms.pipe( - debounceTime(deBounceTime)) + debounceTime(deBounceTime), + distinctUntilChanged()) .subscribe(term => { this.searchTrigger.triggerSearch(term); }); diff --git a/src/portal/src/app/base/global-search/search-result.component.spec.ts b/src/portal/src/app/base/global-search/search-result.component.spec.ts index c25e419fa..c2669b78f 100644 --- a/src/portal/src/app/base/global-search/search-result.component.spec.ts +++ b/src/portal/src/app/base/global-search/search-result.component.spec.ts @@ -18,11 +18,15 @@ describe('SearchResultComponent', () => { let fakeMessageHandlerService = null; let fakeSearchTriggerService = { searchTriggerChan$: { - subscribe: function () { + pipe() { + return { + subscribe() { + } + }; } }, searchCloseChan$: { - subscribe: function () { + subscribe() { } } }; diff --git a/src/portal/src/app/base/global-search/search-result.component.ts b/src/portal/src/app/base/global-search/search-result.component.ts index 40679f3a1..e0e7287c1 100644 --- a/src/portal/src/app/base/global-search/search-result.component.ts +++ b/src/portal/src/app/base/global-search/search-result.component.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. import { Component, OnInit, OnDestroy } from '@angular/core'; -import { Subscription } from "rxjs"; +import { Observable, Subscription } from "rxjs"; import { GlobalSearchService } from './global-search.service'; import { SearchResults } from './search-results'; @@ -20,6 +20,7 @@ import { SearchTriggerService } from './search-trigger.service'; import { AppConfigService } from './../../app-config.service'; import { MessageHandlerService } from '../../shared/message-handler/message-handler.service'; +import { filter, switchMap } from "rxjs/operators"; @Component({ selector: "search-result", @@ -54,9 +55,35 @@ export class SearchResultComponent implements OnInit, OnDestroy { private appConfigService: AppConfigService) { } ngOnInit() { - this.searchSub = this.searchTrigger.searchTriggerChan$.subscribe(term => { - this.doSearch(term); - }); + this.searchSub = this.searchTrigger.searchTriggerChan$ + .pipe(filter(term => { + if (term === "") { + this.searchResults.project = []; + this.searchResults.repository = []; + if (this.withHelmChart) { + this.searchResults.chart = []; + } + } + return !!(term && term.trim()); + }), + switchMap(term => { + // Confirm page is displayed + if (!this.stateIndicator) { + this.show(); + } + this.currentTerm = term; + // Show spinner + this.onGoing = true; + return this.search.doSearch(term); + })) + .subscribe(searchResults => { + this.onGoing = false; + this.originalCopy = searchResults; // Keep the original data + this.searchResults = this.clone(searchResults); + }, error => { + this.onGoing = false; + this.msgHandler.handleError(error); + }); this.closeSearchSub = this.searchTrigger.searchCloseChan$.subscribe(close => { this.close(); }); diff --git a/src/portal/src/app/project/create-project/create-project.component.html b/src/portal/src/app/project/create-project/create-project.component.html index 614206cc1..57748cd1f 100644 --- a/src/portal/src/app/project/create-project/create-project.component.html +++ b/src/portal/src/app/project/create-project/create-project.component.html @@ -8,8 +8,7 @@
+ required pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" #projectName autocomplete="off">
diff --git a/src/portal/src/app/project/create-project/create-project.component.ts b/src/portal/src/app/project/create-project/create-project.component.ts index 5fd36989a..d09f42d91 100644 --- a/src/portal/src/app/project/create-project/create-project.component.ts +++ b/src/portal/src/app/project/create-project/create-project.component.ts @@ -1,5 +1,3 @@ - -import {debounceTime, distinctUntilChanged} from 'rxjs/operators'; // Copyright (c) 2017 VMware, Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,35 +11,41 @@ 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 { debounceTime, distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; import { - Component, - EventEmitter, - Output, - ViewChild, - OnInit, - OnDestroy, - Input, - OnChanges, - SimpleChanges + Component, + EventEmitter, + Output, + ViewChild, + OnDestroy, + Input, + OnChanges, + SimpleChanges, AfterViewInit, ElementRef } from "@angular/core"; -import { NgForm, Validators, AbstractControl } from "@angular/forms"; - -import { Subject } from "rxjs"; +import { NgForm, Validators } from "@angular/forms"; +import { fromEvent, Subscription } from "rxjs"; import { TranslateService } from "@ngx-translate/core"; - import { MessageHandlerService } from "../../shared/message-handler/message-handler.service"; import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component"; - import { Project } from "../project"; -import { ProjectService, QuotaUnits, QuotaHardInterface, QuotaUnlimited, getByte - , GetIntegerAndUnit, clone, validateLimit, validateCountLimit} from "@harbor/ui"; +import { + clone, getByte, + GetIntegerAndUnit, + ProjectService, + QuotaHardInterface, + QuotaUnits, + QuotaUnlimited, validateCountLimit, + validateLimit +} from "@harbor/ui"; + + @Component({ selector: "create-project", templateUrl: "create-project.component.html", styleUrls: ["create-project.scss"] }) -export class CreateProjectComponent implements OnInit, OnChanges, OnDestroy { +export class CreateProjectComponent implements AfterViewInit, OnChanges, OnDestroy { projectForm: NgForm; @@ -64,49 +68,66 @@ export class CreateProjectComponent implements OnInit, OnChanges, OnDestroy { staticBackdrop = true; closable = false; - - isNameValid = true; + isNameExisted: boolean = false; nameTooltipText = "PROJECT.NAME_TOOLTIP"; checkOnGoing = false; - proNameChecker: Subject = new Subject(); - @Output() create = new EventEmitter(); @Input() quotaObj: QuotaHardInterface; @Input() isSystemAdmin: boolean; @ViewChild(InlineAlertComponent, {static: true}) inlineAlert: InlineAlertComponent; - + @ViewChild('projectName', {static: false}) projectNameInput: ElementRef; + checkNameSubscribe: Subscription; constructor(private projectService: ProjectService, private translateService: TranslateService, private messageHandlerService: MessageHandlerService) { } - ngOnInit(): void { - this.proNameChecker.pipe( - debounceTime(300)) - .subscribe((name: string) => { - let cont = this.currentForm.controls["create_project_name"]; - if (cont) { - this.isNameValid = cont.valid; - if (this.isNameValid) { - // Check exiting from backend - this.checkOnGoing = true; - this.projectService - .checkProjectExists(cont.value) - .subscribe(() => { + ngAfterViewInit(): void { + if (!this.checkNameSubscribe) { + this.checkNameSubscribe = fromEvent(this.projectNameInput.nativeElement, 'input').pipe( + map((e: any) => e.target.value), + debounceTime(300), + distinctUntilChanged(), + filter(name => { + return this.currentForm.controls["create_project_name"].valid && name.length > 0; + }), + switchMap(name => { + // Check exiting from backend + this.checkOnGoing = true; + this.isNameExisted = false; + return this.projectService.checkProjectExists(name); + })).subscribe(response => { // Project existing - this.isNameValid = false; - this.nameTooltipText = "PROJECT.NAME_ALREADY_EXISTS"; + if (!(response && response.status === 404)) { + this.isNameExisted = true; + } this.checkOnGoing = false; - }, error => { + }, error => { this.checkOnGoing = false; - }); - } else { - this.nameTooltipText = "PROJECT.NAME_TOOLTIP"; - } + this.isNameExisted = false; + }); } - }); - } - + } + get isNameValid(): boolean { + if (!this.currentForm || !this.currentForm.controls || !this.currentForm.controls["create_project_name"]) { + return true; + } + if (!(this.currentForm.controls["create_project_name"].dirty || this.currentForm.controls["create_project_name"].touched)) { + return true; + } + if (this.checkOnGoing) { + return true; + } + if (this.currentForm.controls["create_project_name"].errors) { + this.nameTooltipText = 'PROJECT.NAME_TOOLTIP'; + return false; + } + if (this.isNameExisted) { + this.nameTooltipText = 'PROJECT.NAME_ALREADY_EXISTS'; + return false; + } + return true; + } ngOnChanges(changes: SimpleChanges): void { if (changes && changes["quotaObj"] && changes["quotaObj"].currentValue) { this.countLimit = this.quotaObj.count_per_project; @@ -143,7 +164,10 @@ export class CreateProjectComponent implements OnInit, OnChanges, OnDestroy { } } ngOnDestroy(): void { - this.proNameChecker.unsubscribe(); + if (this.checkNameSubscribe) { + this.checkNameSubscribe.unsubscribe(); + this.checkNameSubscribe = null; + } } onSubmit() { @@ -175,11 +199,11 @@ export class CreateProjectComponent implements OnInit, OnChanges, OnDestroy { newProject() { this.project = new Project(); this.hasChanged = false; - this.isNameValid = true; - this.createProjectOpened = true; + if (this.currentForm && this.currentForm.controls && this.currentForm.controls["create_project_name"]) { + this.currentForm.controls["create_project_name"].reset(); + } this.inlineAlert.close(); - this.countLimit = this.countDefaultLimit ; this.storageLimit = this.storageDefaultLimit; this.storageLimitUnit = this.storageDefaultLimitUnit; @@ -192,14 +216,5 @@ export class CreateProjectComponent implements OnInit, OnChanges, OnDestroy { this.isNameValid && !this.checkOnGoing; } - - // Handle the form validation - handleValidation(): void { - let cont = this.currentForm.controls["create_project_name"]; - if (cont) { - this.proNameChecker.next(cont.value); - } - - } } diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 184a879ab..55526c7f1 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -732,10 +732,10 @@ }, "SUMMARY": { "QUOTAS": "quotas", - "PROJECT_REPOSITORY": "Project repositories", - "PROJECT_HELM_CHART": "Project Helm Chart", - "PROJECT_MEMBER": "Project members", - "PROJECT_QUOTAS": "Project quotas", + "PROJECT_REPOSITORY": "Repositories", + "PROJECT_HELM_CHART": "Helm Chart", + "PROJECT_MEMBER": "Members", + "PROJECT_QUOTAS": "Quotas", "ARTIFACT_COUNT": "Artifact count", "STORAGE_CONSUMPTION": "Storage consumption", "ADMIN": "Admin(s)", diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index 7cbdc0012..af498a082 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -733,10 +733,10 @@ }, "SUMMARY": { "QUOTAS": "quotas", - "PROJECT_REPOSITORY": "Project repositories", - "PROJECT_HELM_CHART": "Project Helm Chart", - "PROJECT_MEMBER": "Project members", - "PROJECT_QUOTAS": "Project quotas", + "PROJECT_REPOSITORY": "Repositories", + "PROJECT_HELM_CHART": "Helm Chart", + "PROJECT_MEMBER": "Members", + "PROJECT_QUOTAS": "Quotas", "ARTIFACT_COUNT": "Artifact count", "STORAGE_CONSUMPTION": "Storage consumption", "ADMIN": "Admin(s)", diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index d9d0690a1..3dc47f03b 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -719,10 +719,10 @@ }, "SUMMARY": { "QUOTAS": "quotas", - "PROJECT_REPOSITORY": "Project repositories", - "PROJECT_HELM_CHART": "Project Helm Chart", - "PROJECT_MEMBER": "Project members", - "PROJECT_QUOTAS": "Project quotas", + "PROJECT_REPOSITORY": "Repositories", + "PROJECT_HELM_CHART": "Helm Chart", + "PROJECT_MEMBER": "Members", + "PROJECT_QUOTAS": "Quotas", "ARTIFACT_COUNT": "Artifact count", "STORAGE_CONSUMPTION": "Storage consumption", "ADMIN": "Admin(s)", diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 2c935fae7..075d5d94e 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -728,10 +728,10 @@ }, "SUMMARY": { "QUOTAS": "quotas", - "PROJECT_REPOSITORY": "Project repositories", - "PROJECT_HELM_CHART": "Project Helm Chart", - "PROJECT_MEMBER": "Project members", - "PROJECT_QUOTAS": "Project quotas", + "PROJECT_REPOSITORY": "Repositories", + "PROJECT_HELM_CHART": "Helm Chart", + "PROJECT_MEMBER": "Members", + "PROJECT_QUOTAS": "Quotas", "ARTIFACT_COUNT": "Artifact count", "STORAGE_CONSUMPTION": "Storage consumption", "ADMIN": "Admin(s)", diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index a28117e40..421956e3c 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -733,10 +733,10 @@ }, "SUMMARY": { "QUOTAS": "容量", - "PROJECT_REPOSITORY": "项目镜像仓库", - "PROJECT_HELM_CHART": "项目 Helm Chart", - "PROJECT_MEMBER": "项目成员", - "PROJECT_QUOTAS": "项目容量", + "PROJECT_REPOSITORY": "镜像仓库", + "PROJECT_HELM_CHART": "Helm Chart", + "PROJECT_MEMBER": "成员", + "PROJECT_QUOTAS": "容量", "ARTIFACT_COUNT": "Artifact 数量", "STORAGE_CONSUMPTION": "存储消耗", "ADMIN": "管理员",