+ 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": "管理员",