Merge pull request #10143 from AllForNothing/ui-modification

Modify ui to fix some bugs
This commit is contained in:
Will Sun 2019-12-05 18:30:32 +08:00 committed by GitHub
commit d803fd0a72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 140 additions and 90 deletions

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. // Copyright (c) 2017 VMware, Inc. All Rights Reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // 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( this.searchSub = this.searchTerms.pipe(
debounceTime(deBounceTime)) debounceTime(deBounceTime),
distinctUntilChanged())
.subscribe(term => { .subscribe(term => {
this.searchTrigger.triggerSearch(term); this.searchTrigger.triggerSearch(term);
}); });

View File

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

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from "rxjs"; import { Observable, Subscription } from "rxjs";
import { GlobalSearchService } from './global-search.service'; import { GlobalSearchService } from './global-search.service';
import { SearchResults } from './search-results'; import { SearchResults } from './search-results';
@ -20,6 +20,7 @@ import { SearchTriggerService } from './search-trigger.service';
import { AppConfigService } from './../../app-config.service'; import { AppConfigService } from './../../app-config.service';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service'; import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { filter, switchMap } from "rxjs/operators";
@Component({ @Component({
selector: "search-result", selector: "search-result",
@ -54,9 +55,35 @@ export class SearchResultComponent implements OnInit, OnDestroy {
private appConfigService: AppConfigService) { } private appConfigService: AppConfigService) { }
ngOnInit() { ngOnInit() {
this.searchSub = this.searchTrigger.searchTriggerChan$.subscribe(term => { this.searchSub = this.searchTrigger.searchTriggerChan$
this.doSearch(term); .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.closeSearchSub = this.searchTrigger.searchCloseChan$.subscribe(close => {
this.close(); this.close();
}); });
@ -111,7 +138,7 @@ export class SearchResultComponent implements OnInit, OnDestroy {
} }
// Call search service to complete the search request // Call search service to complete the search request
doSearch(term: string): void { doSearch(term: string): void {
// Only search none empty term // Only search none empty term
if (!term || term.trim() === "") { if (!term || term.trim() === "") {
return; return;

View File

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

View File

@ -11,20 +11,20 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import {debounceTime, distinctUntilChanged} from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
import { import {
Component, Component,
EventEmitter, EventEmitter,
Output, Output,
ViewChild, ViewChild,
OnInit, OnInit,
OnDestroy, OnDestroy,
Input, Input,
OnChanges, OnChanges,
SimpleChanges SimpleChanges, AfterViewInit, ElementRef
} from "@angular/core"; } from "@angular/core";
import { NgForm, Validators, AbstractControl } from "@angular/forms"; import { NgForm, Validators, AbstractControl } from "@angular/forms";
import { Subject } from "rxjs"; import { fromEvent, Subject, Subscription } from "rxjs";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
import { MessageHandlerService } from "../../shared/message-handler/message-handler.service"; import { MessageHandlerService } from "../../shared/message-handler/message-handler.service";
import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component"; import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component";
@ -39,7 +39,7 @@ import { clone, getByte, GetIntegerAndUnit, validateCountLimit, validateLimit }
templateUrl: "create-project.component.html", templateUrl: "create-project.component.html",
styleUrls: ["create-project.scss"] styleUrls: ["create-project.scss"]
}) })
export class CreateProjectComponent implements OnInit, OnChanges, OnDestroy { export class CreateProjectComponent implements AfterViewInit, OnChanges, OnDestroy {
projectForm: NgForm; projectForm: NgForm;
@ -62,49 +62,66 @@ export class CreateProjectComponent implements OnInit, OnChanges, OnDestroy {
staticBackdrop = true; staticBackdrop = true;
closable = false; closable = false;
isNameExisted: boolean = false;
isNameValid = true;
nameTooltipText = "PROJECT.NAME_TOOLTIP"; nameTooltipText = "PROJECT.NAME_TOOLTIP";
checkOnGoing = false; checkOnGoing = false;
proNameChecker: Subject<string> = new Subject<string>();
@Output() create = new EventEmitter<boolean>(); @Output() create = new EventEmitter<boolean>();
@Input() quotaObj: QuotaHardInterface; @Input() quotaObj: QuotaHardInterface;
@Input() isSystemAdmin: boolean; @Input() isSystemAdmin: boolean;
@ViewChild(InlineAlertComponent, {static: true}) @ViewChild(InlineAlertComponent, {static: true})
inlineAlert: InlineAlertComponent; inlineAlert: InlineAlertComponent;
@ViewChild('projectName', {static: false}) projectNameInput: ElementRef;
checkNameSubscribe: Subscription;
constructor(private projectService: ProjectService, constructor(private projectService: ProjectService,
private translateService: TranslateService, private translateService: TranslateService,
private messageHandlerService: MessageHandlerService) { } private messageHandlerService: MessageHandlerService) { }
ngOnInit(): void { ngAfterViewInit(): void {
this.proNameChecker.pipe( if (!this.checkNameSubscribe) {
debounceTime(300)) this.checkNameSubscribe = fromEvent(this.projectNameInput.nativeElement, 'input').pipe(
.subscribe((name: string) => { map((e: any) => e.target.value),
let cont = this.currentForm.controls["create_project_name"]; debounceTime(300),
if (cont) { distinctUntilChanged(),
this.isNameValid = cont.valid; filter(name => {
if (this.isNameValid) { return this.currentForm.controls["create_project_name"].valid && name.length > 0;
// Check exiting from backend }),
this.checkOnGoing = true; switchMap(name => {
this.projectService // Check exiting from backend
.checkProjectExists(cont.value) this.checkOnGoing = true;
.subscribe(() => { this.isNameExisted = false;
return this.projectService.checkProjectExists(name);
})).subscribe(response => {
// Project existing // Project existing
this.isNameValid = false; if (!(response && response.status === 404)) {
this.nameTooltipText = "PROJECT.NAME_ALREADY_EXISTS"; this.isNameExisted = true;
}
this.checkOnGoing = false; this.checkOnGoing = false;
}, error => { }, error => {
this.checkOnGoing = false; this.checkOnGoing = false;
}); this.isNameExisted = false;
} else { });
this.nameTooltipText = "PROJECT.NAME_TOOLTIP";
}
} }
}); }
} 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 { ngOnChanges(changes: SimpleChanges): void {
if (changes && changes["quotaObj"] && changes["quotaObj"].currentValue) { if (changes && changes["quotaObj"] && changes["quotaObj"].currentValue) {
this.countLimit = this.quotaObj.count_per_project; this.countLimit = this.quotaObj.count_per_project;
@ -141,7 +158,10 @@ export class CreateProjectComponent implements OnInit, OnChanges, OnDestroy {
} }
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.proNameChecker.unsubscribe(); if (this.checkNameSubscribe) {
this.checkNameSubscribe.unsubscribe();
this.checkNameSubscribe = null;
}
} }
onSubmit() { onSubmit() {
@ -173,11 +193,11 @@ export class CreateProjectComponent implements OnInit, OnChanges, OnDestroy {
newProject() { newProject() {
this.project = new Project(); this.project = new Project();
this.hasChanged = false; this.hasChanged = false;
this.isNameValid = true;
this.createProjectOpened = 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.inlineAlert.close();
this.countLimit = this.countDefaultLimit ; this.countLimit = this.countDefaultLimit ;
this.storageLimit = this.storageDefaultLimit; this.storageLimit = this.storageDefaultLimit;
this.storageLimitUnit = this.storageDefaultLimitUnit; this.storageLimitUnit = this.storageDefaultLimitUnit;
@ -190,14 +210,5 @@ export class CreateProjectComponent implements OnInit, OnChanges, OnDestroy {
this.isNameValid && this.isNameValid &&
!this.checkOnGoing; !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

@ -733,10 +733,10 @@
}, },
"SUMMARY": { "SUMMARY": {
"QUOTAS": "quotas", "QUOTAS": "quotas",
"PROJECT_REPOSITORY": "Project repositories", "PROJECT_REPOSITORY": "Repositories",
"PROJECT_HELM_CHART": "Project Helm Chart", "PROJECT_HELM_CHART": "Helm Chart",
"PROJECT_MEMBER": "Project members", "PROJECT_MEMBER": "Members",
"PROJECT_QUOTAS": "Project quotas", "PROJECT_QUOTAS": "Quotas",
"ARTIFACT_COUNT": "Artifact count", "ARTIFACT_COUNT": "Artifact count",
"STORAGE_CONSUMPTION": "Storage consumption", "STORAGE_CONSUMPTION": "Storage consumption",
"ADMIN": "Admin(s)", "ADMIN": "Admin(s)",

View File

@ -734,10 +734,10 @@
}, },
"SUMMARY": { "SUMMARY": {
"QUOTAS": "quotas", "QUOTAS": "quotas",
"PROJECT_REPOSITORY": "Project repositories", "PROJECT_REPOSITORY": "Repositories",
"PROJECT_HELM_CHART": "Project Helm Chart", "PROJECT_HELM_CHART": "Helm Chart",
"PROJECT_MEMBER": "Project members", "PROJECT_MEMBER": "Members",
"PROJECT_QUOTAS": "Project quotas", "PROJECT_QUOTAS": "Quotas",
"ARTIFACT_COUNT": "Artifact count", "ARTIFACT_COUNT": "Artifact count",
"STORAGE_CONSUMPTION": "Storage consumption", "STORAGE_CONSUMPTION": "Storage consumption",
"ADMIN": "Admin(s)", "ADMIN": "Admin(s)",

View File

@ -720,10 +720,10 @@
}, },
"SUMMARY": { "SUMMARY": {
"QUOTAS": "quotas", "QUOTAS": "quotas",
"PROJECT_REPOSITORY": "Project repositories", "PROJECT_REPOSITORY": "Repositories",
"PROJECT_HELM_CHART": "Project Helm Chart", "PROJECT_HELM_CHART": "Helm Chart",
"PROJECT_MEMBER": "Project members", "PROJECT_MEMBER": "Members",
"PROJECT_QUOTAS": "Project quotas", "PROJECT_QUOTAS": "Quotas",
"ARTIFACT_COUNT": "Artifact count", "ARTIFACT_COUNT": "Artifact count",
"STORAGE_CONSUMPTION": "Storage consumption", "STORAGE_CONSUMPTION": "Storage consumption",
"ADMIN": "Admin(s)", "ADMIN": "Admin(s)",

View File

@ -729,10 +729,10 @@
}, },
"SUMMARY": { "SUMMARY": {
"QUOTAS": "quotas", "QUOTAS": "quotas",
"PROJECT_REPOSITORY": "Project repositories", "PROJECT_REPOSITORY": "Repositories",
"PROJECT_HELM_CHART": "Project Helm Chart", "PROJECT_HELM_CHART": "Helm Chart",
"PROJECT_MEMBER": "Project members", "PROJECT_MEMBER": "Members",
"PROJECT_QUOTAS": "Project quotas", "PROJECT_QUOTAS": "Quotas",
"ARTIFACT_COUNT": "Artifact count", "ARTIFACT_COUNT": "Artifact count",
"STORAGE_CONSUMPTION": "Storage consumption", "STORAGE_CONSUMPTION": "Storage consumption",
"ADMIN": "Admin(s)", "ADMIN": "Admin(s)",

View File

@ -734,10 +734,10 @@
}, },
"SUMMARY": { "SUMMARY": {
"QUOTAS": "容量", "QUOTAS": "容量",
"PROJECT_REPOSITORY": "项目镜像仓库", "PROJECT_REPOSITORY": "镜像仓库",
"PROJECT_HELM_CHART": "项目 Helm Chart", "PROJECT_HELM_CHART": "Helm Chart",
"PROJECT_MEMBER": "项目成员", "PROJECT_MEMBER": "成员",
"PROJECT_QUOTAS": "项目容量", "PROJECT_QUOTAS": "容量",
"ARTIFACT_COUNT": "Artifact 数量", "ARTIFACT_COUNT": "Artifact 数量",
"STORAGE_CONSUMPTION": "存储消耗", "STORAGE_CONSUMPTION": "存储消耗",
"ADMIN": "管理员", "ADMIN": "管理员",

View File

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

View File

@ -95,4 +95,7 @@
flex-direction: column-reverse; flex-direction: column-reverse;
font-size: 13px; font-size: 13px;
color: #000; 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 {Injectable, Inject} from "@angular/core";
import { HttpClient, HttpParams, HttpResponse } from "@angular/common/http"; import { HttpClient, HttpParams, HttpResponse } from "@angular/common/http";
import { catchError } from "rxjs/operators"; import { catchError } from "rxjs/operators";
@ -169,8 +169,13 @@ export class ProjectDefaultService extends ProjectService {
public checkProjectExists(projectName: string): Observable<any> { public checkProjectExists(projectName: string): Observable<any> {
return this.http return this.http
.head(`/api/projects/?project_name=${projectName}`).pipe( .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> { public checkProjectMember(projectId: number): Observable<any> {