Merge pull request #10144 from AllForNothing/release-1.10.0

Modify ui to fix some bugs(cherry-pick #10143)
This commit is contained in:
Will Sun 2019-12-06 09:09:39 +08:00 committed by GitHub
commit 46e004b167
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 150 additions and 96 deletions

View File

@ -20,8 +20,8 @@
<clr-control-helper class="config-subtext"> {{ 'PROJECT_CONFIG.CONTENT_TRUST_POLCIY' | translate }}
</clr-control-helper>
</clr-checkbox-container>
<clr-checkbox-container id="prevent-vulenrability-image">
<label><span>{{ 'PROJECT_CONFIG.SECURITY' | translate }}</span></label>
<clr-checkbox-container id="prevent-vulenrability-image" class="margin-top-05">
<label></label>
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [(ngModel)]="projectPolicy.PreventVulImg"
name="prevent-vulenrability-image-input" [disabled]="!hasChangeConfigRole" />

View File

@ -96,3 +96,6 @@
font-size: 13px;
color: #000;
}
.margin-top-05 {
margin-top: 0.5rem;
}

View File

@ -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";
@ -170,7 +170,12 @@ export class ProjectDefaultService extends ProjectService {
public checkProjectExists(projectName: string): Observable<any> {
return this.http
.head(`/api/projects/?project_name=${projectName}`).pipe(
catchError(error => observableThrowError(error)), );
catchError(error => {
if (error && error.status === 404) {
return of(error);
}
return observableThrowError(error);
}));
}
public checkProjectMember(projectId: number): Observable<any> {

View File

@ -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);
});

View File

@ -18,11 +18,15 @@ describe('SearchResultComponent', () => {
let fakeMessageHandlerService = null;
let fakeSearchTriggerService = {
searchTriggerChan$: {
subscribe: function () {
pipe() {
return {
subscribe() {
}
};
}
},
searchCloseChan$: {
subscribe: function () {
subscribe() {
}
}
};

View File

@ -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,8 +55,34 @@ 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();

View File

@ -8,8 +8,7 @@
<div class="clr-control-container" [class.clr-error]="!isNameValid">
<div class="clr-input-wrapper">
<input type="text" id="create_project_name" [(ngModel)]="project.name" name="create_project_name" class="clr-input input-width"
required pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" #projectName="ngModel" autocomplete="off"
(keyup)='handleValidation()'>
required pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" #projectName autocomplete="off">
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
<span class="spinner spinner-inline" [hidden]="!checkOnGoing"></span>
</div>

View File

@ -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
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<string> = new Subject<string>();
@Output() create = new EventEmitter<boolean>();
@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) {
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.projectService
.checkProjectExists(cont.value)
.subscribe(() => {
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 => {
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);
}
}
}

View File

@ -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)",

View File

@ -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)",

View File

@ -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)",

View File

@ -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)",

View File

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