mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-24 01:27:49 +01:00
Improve search function for replication and tags
Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
parent
26905baca2
commit
5d12423f74
@ -67,7 +67,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="cron-selection">
|
||||
<cron-selection [disabled]="!(retention?.rules?.length > 0)" #cronScheduleComponent [labelCurrent]="label" [labelEdit]='label' [originCron]='originCron()' (inputvalue)="openConfirm($event)"></cron-selection>
|
||||
<cron-selection [buttonMarginLeft]="'150px'" [disabled]="!(retention?.rules?.length > 0)" #cronScheduleComponent [labelCurrent]="label" [labelEdit]='label' [originCron]='originCron()' (inputvalue)="openConfirm($event)"></cron-selection>
|
||||
</div>
|
||||
<div class="clr-row pt-1">
|
||||
<div class="clr-col-2 pt-2 flex-150"><label class="label-left font-size-54">{{'TAG_RETENTION.RETENTION_RUNS' | translate}}</label></div>
|
||||
|
@ -58,18 +58,6 @@
|
||||
.font-size-54 {
|
||||
font-size: .541667rem;
|
||||
}
|
||||
:host::ng-deep {
|
||||
.normal-wrapper-box {
|
||||
.normal-wrapper {
|
||||
.font-style {
|
||||
width: 150px!important;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
margin-left: 150px!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.flex-150 {
|
||||
flex: 0 0 150px;
|
||||
max-width: 150px;
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="normal-wrapper-box flex-layout" *ngIf="!isEditMode">
|
||||
<div class="normal-wrapper">
|
||||
<span class="font-style">{{ labelCurrent | translate }}</span>
|
||||
<span [style.width]="buttonMarginLeft" class="font-style">{{ labelCurrent | translate }}</span>
|
||||
<span>{{(originScheduleType ? 'SCHEDULE.'+ originScheduleType.toUpperCase(): "") | translate}}</span>
|
||||
<a [hidden]="originScheduleType!==SCHEDULE_TYPE.HOURLY" href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
|
||||
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
|
||||
@ -17,12 +17,12 @@
|
||||
<span [hidden]="originScheduleType!==SCHEDULE_TYPE.CUSTOM">{{ "SCHEDULE.CRON" | translate }} :</span>
|
||||
<span [hidden]="originScheduleType!==SCHEDULE_TYPE.CUSTOM">{{ oriCron }}</span>
|
||||
</div>
|
||||
<button [disabled]="disabled" class="btn btn-primary btn-sm" (click)="editSchedule()" id="editSchedule">
|
||||
<button [style.margin-left]="buttonMarginLeft" [disabled]="disabled" class="btn btn-primary btn-sm" (click)="editSchedule()" id="editSchedule">
|
||||
{{ "BUTTON.EDIT" | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="setting-wrapper flex-layout" *ngIf="isEditMode">
|
||||
<span class="font-style">{{ labelEdit | translate }}</span>
|
||||
<span [style.width]="buttonMarginLeft" class="font-style">{{ labelEdit | translate }}</span>
|
||||
<div class="select select-schedule clr-select-wrapper">
|
||||
<select name="selectPolicy" id="selectPolicy" [(ngModel)]="scheduleType">
|
||||
<option [value]="SCHEDULE_TYPE.NONE">{{'SCHEDULE.NONE' | translate}}</option>
|
||||
@ -49,7 +49,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="confirm-button">
|
||||
<button class="btn btn-primary btn-sm"
|
||||
<button [style.margin-left]="buttonMarginLeft" class="btn btn-primary btn-sm"
|
||||
(click)="save()" id="config-save">
|
||||
{{ "BUTTON.SAVE" | translate }}
|
||||
</button>
|
||||
|
@ -8,10 +8,6 @@
|
||||
.normal-wrapper {
|
||||
position: absolute;
|
||||
width: 700px;
|
||||
> span:first-child {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
> span:not(:first-child) {
|
||||
margin-right: 18px;
|
||||
}
|
||||
@ -20,18 +16,14 @@
|
||||
}
|
||||
}
|
||||
button {
|
||||
margin: 35px 20px 0 200px;
|
||||
margin: 35px 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-wrapper {
|
||||
position: relative;
|
||||
> span:first-child {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.confirm-button {
|
||||
margin: 20px 0px 0 200px;
|
||||
margin: 20px 0 0;
|
||||
}
|
||||
|
||||
*:not(:first-child) {
|
||||
@ -65,7 +57,6 @@
|
||||
display: inline-block;
|
||||
color: #000;
|
||||
font-size: .541667rem;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
span.required {
|
||||
|
@ -28,6 +28,7 @@ export class CronScheduleComponent implements OnChanges {
|
||||
@Input() labelEdit: string;
|
||||
@Input() labelCurrent: string;
|
||||
@Input() disabled: boolean;
|
||||
@Input() buttonMarginLeft: string = '200px';
|
||||
dateInvalid: boolean;
|
||||
originScheduleType: string;
|
||||
oriCron: string;
|
||||
|
@ -1,6 +1,6 @@
|
||||
<span>
|
||||
<clr-icon shape="search" size="20" class="search-btn" [class.filter-icon]="isShowSearchBox" (click)="onClick()"></clr-icon>
|
||||
<input [hidden]="!isShowSearchBox" type="text" class="filter-input clr-input" autofocus (keyup)="valueChange()" (focus)="inputFocus()"
|
||||
<input [attr.readOnly]="readonly" [hidden]="!isShowSearchBox" type="text" class="filter-input clr-input" autofocus (keyup)="valueChange()" (focus)="inputFocus()"
|
||||
placeholder="{{placeHolder}}" [(ngModel)]="currentValue" />
|
||||
<span class="filter-divider" *ngIf="withDivider"></span>
|
||||
</span>
|
@ -27,7 +27,7 @@ export class FilterComponent implements OnInit {
|
||||
|
||||
@Output() private filterEvt = new EventEmitter<string>();
|
||||
@Output() private openFlag = new EventEmitter<boolean>();
|
||||
|
||||
@Input() readonly: string = null;
|
||||
@Input() currentValue: string;
|
||||
@Input("filterPlaceholder")
|
||||
public set flPlaceholder(placeHolder: string) {
|
||||
|
@ -1,45 +1,51 @@
|
||||
<form [formGroup]="imageNameForm" class="clr-form clr-form-compact" (mouseleave)="leaveProjectInput()">
|
||||
<div class="clr-form-control clr-row">
|
||||
<label for="project-name" class="required clr-control-label clr-col-xs-12 clr-col-md-4">{{ 'PROJECT.NAME' | translate }}</label>
|
||||
<div class="clr-control-container clr-col-xs-12 clr-col-md-8">
|
||||
<form [formGroup]="imageNameForm" class="clr-form clr-form-horizontal" (mouseleave)="leaveProjectInput()">
|
||||
<div class="clr-form-control">
|
||||
<label class="required clr-control-label">{{ 'PROJECT.NAME' | translate }}</label>
|
||||
<div class="clr-control-container"
|
||||
[class.clr-error]="noProjectInfo && (projectName.dirty || projectName.touched)">
|
||||
<div class="clr-input-wrapper">
|
||||
<label aria-haspopup="true" role="tooltip" class="wrap-label tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]='noProjectInfo'>
|
||||
<input type="text"
|
||||
id="project-name"
|
||||
class="clr-input"
|
||||
(keyup)='validateProjectName()'
|
||||
(blur)='blurProjectInput()'
|
||||
formControlName="projectName"/>
|
||||
<span *ngIf="noProjectInfo && (projectName.dirty || projectName.touched)" class="tooltip-content">{{noProjectInfo | translate}}</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="project-name"
|
||||
class="clr-input w-90"
|
||||
(keyup)='validateProjectName()'
|
||||
(blur)='blurProjectInput()'
|
||||
formControlName="projectName" autocomplete="off" />
|
||||
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
|
||||
<div class="select-box" [style.display]="selectedProjectList.length ? 'block' : 'none'">
|
||||
<ul>
|
||||
<li *ngFor="let project of selectedProjectList" (click)="selectedProjectName(project?.name)">{{project?.name}}</li>
|
||||
<li *ngFor="let project of selectedProjectList"
|
||||
(click)="selectedProjectName(project?.name)">{{project?.name}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<clr-control-error *ngIf="noProjectInfo && (projectName.dirty || projectName.touched)"
|
||||
class="tooltip-content">
|
||||
{{noProjectInfo | translate}}
|
||||
</clr-control-error>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control clr-row">
|
||||
<label for="repo-name" class="required clr-control-label clr-col-xs-12 clr-col-md-4">{{ 'REPOSITORY.REPO_NAME' | translate }}</label>
|
||||
<div class="clr-control-container clr-col-xs-12 clr-col-md-8">
|
||||
<div class="clr-form-control">
|
||||
<label class="required clr-control-label">{{ 'REPOSITORY.REPO_NAME' | translate }}</label>
|
||||
<div class="clr-control-container" [class.clr-error]="repoName.invalid && (repoName.dirty || repoName.touched)">
|
||||
<div class="clr-input-wrapper">
|
||||
<label aria-haspopup="true" role="tooltip" class="wrap-label tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='repoName.invalid && (repoName.dirty || repoName.touched)'>
|
||||
<input type="text" id="repo-name" class="clr-input" formControlName="repoName" />
|
||||
<span *ngIf="repoName.invalid && (repoName.dirty || repoName.touched)" class="tooltip-content">{{ 'RETAG.TIP_REPO' | translate }}</span>
|
||||
</label>
|
||||
<input type="text" id="repo-name" class="clr-input w-90" formControlName="repoName" autocomplete="off" />
|
||||
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
|
||||
</div>
|
||||
<clr-control-error *ngIf="repoName.invalid && (repoName.dirty || repoName.touched)" class="tooltip-content">
|
||||
{{ 'RETAG.TIP_REPO' | translate }}
|
||||
</clr-control-error>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control clr-row">
|
||||
<label for="tag-name" class="required clr-control-label clr-col-xs-12 clr-col-md-4">{{ 'REPOSITORY.TAG' | translate }}</label>
|
||||
<div class="clr-control-container clr-col-xs-12 clr-col-md-8">
|
||||
<div class="clr-form-control">
|
||||
<label class="required clr-control-label">{{ 'REPOSITORY.TAG' | translate }}</label>
|
||||
<div class="clr-control-container" [class.clr-error]="tagName.invalid && (tagName.dirty || tagName.touched)">
|
||||
<div class="clr-input-wrapper">
|
||||
<label aria-haspopup="true" role="tooltip" class="wrap-label tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='tagName.invalid && (tagName.dirty || tagName.touched)'>
|
||||
<input type="text" id="tag-name" class="clr-input" formControlName="tagName" />
|
||||
<span *ngIf="tagName.invalid && (tagName.dirty || tagName.touched)" class="tooltip-content">{{ 'RETAG.TIP_TAG' | translate }}</span>
|
||||
</label>
|
||||
<input type="text" id="tag-name" class="clr-input w-90" formControlName="tagName" autocomplete="off" />
|
||||
<clr-icon class="clr-validate-icon" shape="exclamation-circle"></clr-icon>
|
||||
</div>
|
||||
<clr-control-error *ngIf="tagName.invalid && (tagName.dirty || tagName.touched)" class="tooltip-content">
|
||||
{{ 'RETAG.TIP_TAG' | translate }}
|
||||
</clr-control-error>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@ -42,13 +42,9 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
label.required {
|
||||
&:after {
|
||||
content: '*';
|
||||
font-size: .58479532rem;
|
||||
line-height: .5rem;
|
||||
color: #c92100;
|
||||
margin-left: .25rem;
|
||||
}
|
||||
.clr-control-container {
|
||||
width: 60%;
|
||||
}
|
||||
.w-90 {
|
||||
width: 90%;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Project } from "../project-policy-config/project";
|
||||
import { Subject } from "rxjs/index";
|
||||
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
|
||||
import { debounceTime, distinctUntilChanged, switchMap } from "rxjs/operators";
|
||||
import { ProjectService } from "../../services/project.service";
|
||||
import { AbstractControl, FormBuilder, FormGroup, Validators } from "@angular/forms";
|
||||
import { ErrorHandler } from "../../utils/error-handler/error-handler";
|
||||
@ -42,36 +42,33 @@ export class ImageNameInputComponent implements OnInit, OnDestroy {
|
||||
])],
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.proNameChecker
|
||||
.pipe(debounceTime(200))
|
||||
.pipe(distinctUntilChanged())
|
||||
.subscribe((name: string) => {
|
||||
this.noProjectInfo = "";
|
||||
this.selectedProjectList = [];
|
||||
const prolist: any = this.proService.listProjects(name, undefined);
|
||||
if (prolist.subscribe) {
|
||||
prolist.subscribe(response => {
|
||||
if (response.body) {
|
||||
this.selectedProjectList = response.body.slice(0, 10);
|
||||
// if input project name exist in the project list
|
||||
let exist = response.body.find((data: any) => data.name === name);
|
||||
if (!exist) {
|
||||
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
|
||||
} else {
|
||||
this.noProjectInfo = "";
|
||||
}
|
||||
} else {
|
||||
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
|
||||
}
|
||||
}, (error: any) => {
|
||||
this.errorHandler.error(error);
|
||||
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
|
||||
});
|
||||
.pipe(distinctUntilChanged(),
|
||||
switchMap(name => {
|
||||
this.noProjectInfo = "";
|
||||
this.selectedProjectList = [];
|
||||
return this.proService.listProjects(name, undefined);
|
||||
})
|
||||
).subscribe(response => {
|
||||
if (response.body) {
|
||||
this.selectedProjectList = response.body.slice(0, 10);
|
||||
// if input project name exist in the project list
|
||||
let exist = response.body.find((data: any) => data.name === this.imageNameForm.controls["projectName"].value);
|
||||
if (!exist) {
|
||||
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
|
||||
} else {
|
||||
this.errorHandler.error("not Observable type");
|
||||
this.noProjectInfo = "";
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
|
||||
}
|
||||
}, (error: any) => {
|
||||
this.errorHandler.error(error);
|
||||
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
|
||||
});
|
||||
}
|
||||
|
||||
validateProjectName(): void {
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<div class="row flex-items-xs-between rightPos">
|
||||
<div class="flex-xs-middle option-right">
|
||||
<hbr-filter id="filter-rules" [withDivider]="true" filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filterEvt)="doSearchRules($event)"
|
||||
<hbr-filter id="filter-rules" [withDivider]="true" filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}'
|
||||
[currentValue]="search.ruleName"></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refreshRules()">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
|
@ -21,7 +21,7 @@ import {
|
||||
EventEmitter
|
||||
} from "@angular/core";
|
||||
import { Comparator, State } from "../../services/interface";
|
||||
import { finalize, catchError, map } from "rxjs/operators";
|
||||
import { finalize, catchError, map, debounceTime, distinctUntilChanged, switchMap, delay } from "rxjs/operators";
|
||||
import { Subscription, forkJoin, timer, Observable, throwError as observableThrowError, observable } from "rxjs";
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
|
||||
@ -62,6 +62,7 @@ import {
|
||||
import { OperationService } from "../operation/operation.service";
|
||||
import { Router } from "@angular/router";
|
||||
import { errorHandler as errorHandFn } from "../../utils/shared/shared.utils";
|
||||
import { FilterComponent } from "../filter/filter.component";
|
||||
const ONE_HOUR_SECONDS: number = 3600;
|
||||
const ONE_MINUTE_SECONDS: number = 60;
|
||||
const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS;
|
||||
@ -146,6 +147,9 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
||||
currentState: State;
|
||||
jobsLoading: boolean = false;
|
||||
timerDelay: Subscription;
|
||||
@ViewChild(FilterComponent, {static: true})
|
||||
filterComponent: FilterComponent;
|
||||
searchSub: Subscription;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
@ -160,6 +164,23 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (!this.searchSub) {
|
||||
this.searchSub = this.filterComponent.filterTerms.pipe(
|
||||
debounceTime(500),
|
||||
distinctUntilChanged(),
|
||||
switchMap( ruleName => {
|
||||
this.loading = true;
|
||||
return this.replicationService.getReplicationRules(this.projectId, ruleName);
|
||||
})
|
||||
).subscribe(rules => {
|
||||
this.hideJobs();
|
||||
this.listReplicationRule.changedRules = rules || [];
|
||||
this.loading = false;
|
||||
}, error => {
|
||||
this.errorHandler.error(error);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
this.currentRuleStatus = this.ruleStatus[0];
|
||||
}
|
||||
|
||||
@ -167,6 +188,10 @@ export class ReplicationComponent implements OnInit, OnDestroy {
|
||||
if (this.timerDelay) {
|
||||
this.timerDelay.unsubscribe();
|
||||
}
|
||||
if (this.searchSub) {
|
||||
this.searchSub.unsubscribe();
|
||||
this.searchSub = null;
|
||||
}
|
||||
}
|
||||
|
||||
// open replication rule
|
||||
|
@ -26,11 +26,11 @@
|
||||
<div>
|
||||
<div class="row flex-items-xs-right rightPos">
|
||||
<div id="filterArea">
|
||||
<div class='filterLabelPiece' *ngIf="!withAdmiral" [hidden]="!openLabelFilterPiece" [style.left.px]='filterLabelPieceWidth'>
|
||||
<div class='filterLabelPiece' *ngIf="!withAdmiral" [hidden]="!openLabelFilterPiece" [style.left.px]='32'>
|
||||
<hbr-label-piece *ngIf="showlabel" [hidden]='!filterOneLabel' [label]="filterOneLabel" [labelWidth]="130"></hbr-label-piece>
|
||||
</div>
|
||||
<div class="flex-xs-middle">
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder="{{'TAG.FILTER_FOR_TAGS' | translate}}" (filterEvt)="doSearchTagNames($event)" (openFlag)="openFlagEvent($event)" [currentValue]="lastFilteredTagName"></hbr-filter>
|
||||
<hbr-filter [readonly]="'readonly'" [withDivider]="true" filterPlaceholder="{{'TAG.FILTER_FOR_TAGS' | translate}}" (filterEvt)="doSearchTagNames($event)" (openFlag)="openFlagEvent($event)" [currentValue]="lastFilteredTagName"></hbr-filter>
|
||||
<div class="labelFilterPanel" *ngIf="!withAdmiral" [hidden]="!openLabelFilterPanel">
|
||||
<a class="filterClose" (click)="closeFilter()">×</a>
|
||||
<label class="filterLabelHeader">{{'REPOSITORY.FILTER_BY_LABEL' | translate}}</label>
|
||||
|
@ -126,7 +126,7 @@
|
||||
|
||||
.filterLabelPiece {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
top: 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user