Add replication rule label filter

This commit is contained in:
pfh 2018-06-15 11:03:47 +08:00
parent 952bba6d6c
commit 963830b9d1
23 changed files with 626 additions and 46 deletions

View File

@ -2,6 +2,14 @@
<h3 class="modal-title">{{headerTitle | translate}}</h3>
<hbr-inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></hbr-inline-alert>
<div class="modal-body" style="max-height: 85vh;">
<clr-alert [hidden]='!deletedLabelCount' [clrAlertType]="'alert-warning'" [clrAlertSizeSmall]="true" [clrAlertClosable]="false" [(clrAlertClosed)]="alertClosed">
<div class="alert-item">
<span class="alert-text">{{deletedLabelInfo}}</span>
<div class="alert-actions">
<a class="alert-action" (click)=" alertClosed = true">{{'REPLICATION.ACKNOWLEDGE' | translate}}</a>
</div>
</div>
</clr-alert>
<form [formGroup]="ruleForm" novalidate>
<section class="form-block">
<div class="form-group form-group-override">
@ -39,23 +47,28 @@
<div class="form-group form-group-override">
<label class="form-group-label-override">{{'REPLICATION.SOURCE_IMAGES_FILTER' | translate}}</label>
<div formArrayName="filters">
<div class="filterSelect" *ngFor="let filter of filters.controls; let i=index" [formGroupName]="i">
<div>
<div class="filterSelect" *ngFor="let filter of filters.controls; let i=index">
<div [formGroupName]="i">
<div class="select floatSetPar">
<select formControlName="kind" (change)="filterChange($event)" id="{{i}}" name="{{filterListData[i]?.name}}">
<option *ngFor="let filter of filterListData[i]?.options;" value="{{filter}}">{{filter}}</option>
<select formControlName="kind" #selectedValue (change)="filterChange($event, selectedValue.value)" id="{{i}}" name="{{filterListData[i]?.name}}">
<option *ngFor="let opt of filterListData[i]?.options;" value="{{opt}}">{{opt}}</option>
</select>
</div>
<label aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
[class.invalid]='ruleForm.controls.filters.controls[i].controls.pattern.touched && ruleForm.controls.filters.controls[i].controls.pattern.invalid'>
<input type="text" #filterValue required size="14" formControlName="pattern">
[class.invalid]='(ruleForm.controls.filters.controls[i].controls.value.dirty || ruleForm.controls.filters.controls[i].controls.value.touched) && ruleForm.controls.filters.controls[i].controls.value.invalid'>
<input type="text" #filterValue required size="14" formControlName="value" [attr.disabled]="(filterListData[i]?.name=='label') ?'' : null">
<span class="tooltip-content">{{'TOOLTIP.EMPTY' | translate}}</span>
</label>
<div class="arrowSet" [hidden]="!(filterListData[i]?.name=='label')" (click)="openLabelList(selectedValue.value, i, $event)"><clr-icon shape="angle"></clr-icon></div>
<clr-icon shape="warning-standard" class="is-solid is-warning" size="14" style="margin-left: -15px;" [hidden]="!deletedLabelCount || !(filterListData[i]?.name=='label')"></clr-icon>
<clr-icon shape="times-circle" class="is-solid" (click)="deleteFilter(i)"></clr-icon>
<div *ngIf="!withAdmiral"><hbr-filter-label [projectId]="ruleForm.controls.projects.controls[0]?.value.project_id" [selectedLabelInfo]="filterLabelInfo" [isOpen]="filterListData[i].isOpen" (selectedLabels)="selectedLabelList($event, i)" (closePanelEvent)="filterListData[i].isOpen = false"></hbr-filter-label></div>
</div>
</div>
</div>
<clr-icon shape="plus-circle" class="is-solid" [hidden]="isFilterHide" (click)="addNewFilter()" style="margin-top: 11px;"></clr-icon>
<clr-icon shape="plus-circle" class="is-solid" [hidden]="isFilterHide" (click)="addNewFilter()" style="margin-top: 11px;"></clr-icon>
</div>
<!--Targets-->
<div class="form-group form-group-override">

View File

@ -44,6 +44,7 @@ h4 {
}
.filterSelect {
position: relative;
width: 315px;
}
@ -156,8 +157,29 @@ h4 {
cursor: pointer;
}
.arrowSet{
display: inline-block;
position: absolute;
right: 50px;
top: 8px;
transform: rotate(180deg);
}
clr-modal {
::ng-deep div.modal-dialog {
width: 25rem;
}
}
.deletedDiv {
text-align: left;
line-height: 14px;
color: red;
}
.deletedDiv a{
margin-left: 10px;
text-decoration: underline;
cursor: pointer;
color: blue;
}

View File

@ -38,6 +38,9 @@ import {
} from "../service/project.service";
import { JobLogViewerComponent } from "../job-log-viewer/job-log-viewer.component";
import { OperationService } from "../operation/operation.service";
import {FilterLabelComponent} from "./filter-label.component";
import {LabelService} from "../service/label.service";
import {LabelPieceComponent} from "../label-piece/label-piece.component";
describe("CreateEditRuleComponent (inline template)", () => {
let mockRules: ReplicationRule[] = [
@ -239,7 +242,9 @@ describe("CreateEditRuleComponent (inline template)", () => {
DatePickerComponent,
FilterComponent,
InlineAlertComponent,
JobLogViewerComponent
JobLogViewerComponent,
FilterLabelComponent,
LabelPieceComponent
],
providers: [
ErrorHandler,
@ -248,7 +253,8 @@ describe("CreateEditRuleComponent (inline template)", () => {
{ provide: EndpointService, useClass: EndpointDefaultService },
{ provide: ProjectService, useClass: ProjectDefaultService },
{ provide: JobLogService, useClass: JobLogDefaultService },
{ provide: OperationService }
{ provide: OperationService },
{ provide: LabelService }
]
});
}));

View File

@ -21,11 +21,11 @@ import {
EventEmitter,
Output
} from "@angular/core";
import { Filter, ReplicationRule, Endpoint } from "../service/interface";
import {Filter, ReplicationRule, Endpoint, Label} from "../service/interface";
import { Subject } from "rxjs/Subject";
import { Subscription } from "rxjs/Subscription";
import { FormArray, FormBuilder, FormGroup, Validators } from "@angular/forms";
import { compareValue, isEmptyObject, toPromise } from "../utils";
import {clone, compareValue, isEmptyObject, toPromise} from "../utils";
import { InlineAlertComponent } from "../inline-alert/inline-alert.component";
import { ReplicationService } from "../service/replication.service";
import { ErrorHandler } from "../error-handler/error-handler";
@ -33,6 +33,7 @@ import { TranslateService } from "@ngx-translate/core";
import { EndpointService } from "../service/endpoint.service";
import { ProjectService } from "../service/project.service";
import { Project } from "../project-policy-config/project";
import {LabelState} from "../tag/tag.component";
const ONE_HOUR_SECONDS = 3600;
const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS;
@ -56,6 +57,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
noSelectedProject = true;
noSelectedEndpoint = true;
filterCount = 0;
alertClosed = false;
triggerNames: string[] = ["Manual", "Immediate", "Scheduled"];
scheduleNames: string[] = ["Daily", "Weekly"];
weekly: string[] = [
@ -67,7 +69,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
"Saturday",
"Sunday"
];
filterSelect: string[] = ["repository", "tag"];
filterSelect: string[] = ["repository", "tag", "label"];
ruleNameTooltip = "REPLICATION.NAME_TOOLTIP";
headerTitle = "REPLICATION.ADD_POLICY";
@ -80,13 +82,19 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
proNameChecker: Subject<string> = new Subject<string>();
firstClick = 0;
policyId: number;
labelInputVal = '';
filterLabelInfo: Label[] = []; // store filter selected labels` id
deletedLabelCount = 0;
deletedLabelInfo: string;
confirmSub: Subscription;
ruleForm: FormGroup;
formArrayLabel: FormArray;
copyUpdateForm: ReplicationRule;
@Input() projectId: number;
@Input() projectName: string;
@Input() withAdmiral: boolean;
@Output() goToRegistry = new EventEmitter<any>();
@Output() reload = new EventEmitter<boolean>();
@ -118,16 +126,22 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.createForm();
}
baseFilterData(name: string, option: string[], state: boolean) {
baseFilterData(name: string, option: string[], state: boolean, inputValue: string) {
return {
name: name,
options: option,
state: state,
isValid: true
isValid: true,
isOpen: false, // label list
inputValue: inputValue
};
}
ngOnInit(): void {
if (this.withAdmiral) {
this.filterSelect = ["repository", "tag"];
}
toPromise<Endpoint[]>(this.endpointService.getEndpoints())
.then(targets => {
this.targetList = targets || [];
@ -173,7 +187,8 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.proNameChecker
.debounceTime(500)
.distinctUntilChanged()
.subscribe((name: string) => {
.subscribe((resp: string) => {
let name = this.ruleForm.controls["projects"].value[0].name;
this.noProjectInfo = "";
this.selectedProjectList = [];
toPromise<Project[]>(this.proService.listProjects(name, undefined))
@ -225,6 +240,8 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
createForm() {
this.formArrayLabel = this.fb.array([]);
this.ruleForm = this.fb.group({
name: ["", Validators.required],
description: "",
@ -263,7 +280,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.setTarget([this.emptyEndpoint]);
this.setFilter([]);
this.copyUpdateForm = Object.assign({}, this.ruleForm.value);
this.copyUpdateForm = clone(this.ruleForm.value);
}
updateForm(rule: ReplicationRule): void {
@ -281,6 +298,9 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.noSelectedEndpoint = false;
if (rule.filters) {
this.reOrganizeLabel(rule.filters);
this.setFilter(rule.filters);
this.updateFilter(rule.filters);
}
@ -290,6 +310,46 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
setTimeout(() => clearInterval(hnd), 2000);
}
// reorganize filter structure
reOrganizeLabel(filterLabels: any[]): void {
let count = 0;
if (filterLabels.length) {
this.filterLabelInfo = [];
let delLabel = '';
filterLabels.forEach((data: any) => {
if (data.kind === 'label') {
if (data.value.deleted !== true) {
count++;
this.filterLabelInfo.push(data.value);
}
if (data.value.deleted === true) {
this.deletedLabelCount++;
delLabel += data.value.name + ',';
}
}
});
this.translateService.get('REPLICATION.DELETED_LABEL_INFO', {
param: delLabel
}).subscribe((res: string) => {
this.deletedLabelInfo = res;
this.alertClosed = false;
});
// delete api return label info, replace with label count
if (delLabel || count) {
let len = filterLabels.length;
for (let i = 0 ; i < len; i ++) {
let lab = filterLabels.find(data => data.kind === this.filterSelect[2]);
if (lab) {filterLabels.splice(filterLabels.indexOf(lab), 1); }
}
filterLabels.push({kind: this.filterSelect[2], value: count + ' labels'});
this.labelInputVal = count.toString();
}
}
}
get projects(): FormArray {
return this.ruleForm.get("projects") as FormArray;
}
@ -320,16 +380,17 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
initFilter(name: string) {
return this.fb.group({
kind: name,
pattern: ["", Validators.required]
value: ['', Validators.required]
});
}
filterChange($event: any) {
filterChange($event: any, selectedValue: string) {
if ($event && $event.target["value"]) {
let id: number = $event.target.id;
let name: string = $event.target.name;
let value: string = $event.target["value"];
const controlArray = <FormArray> this.ruleForm.get('filters');
this.filterListData.forEach((data, index) => {
if (index === +id) {
data.name = $event.target.name = value;
@ -339,6 +400,38 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
if (data.options.indexOf(name) === -1) {
data.options.push(name);
}
// if before select, $event is label, then after select set inputValue is empty and isOpen is false
if (this.filterSelect.length === 3 && name === this.filterSelect[2] && data.name === value) {
this.labelInputVal = controlArray.controls[index].get('value').value.split(' ')[0];
data.isOpen = false;
controlArray.controls[index].get('value').setValue('');
}
// if before select, $event is not label, then after select set inputValue is labelInputVal and isOpen is true
if (this.filterSelect.length === 3 && data.name === this.filterSelect[2]) {
if (this.labelInputVal) {
controlArray.controls[index].get('value').setValue(this.labelInputVal + ' labels');
} else {
controlArray.controls[index].get('value').setValue('');
}
// this.labelInputVal = '';
data.isOpen = false;
}
});
}
}
// when input value is label, then open label panel
openLabelList(labelTag: string, indexId: number, $event: any) {
if (this.filterSelect.length === 3 && labelTag === this.filterSelect[2]) {
this.filterListData.forEach((data, index) => {
if (index === indexId) {
data.isOpen = true;
}else {
data.isOpen = false;
}
});
}
}
@ -398,15 +491,17 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
addNewFilter(): void {
const controlArray = <FormArray> this.ruleForm.get('filters');
if (this.filterCount === 0) {
this.filterListData.push(
this.baseFilterData(
this.filterSelect[0],
this.filterSelect.slice(),
true
true,
''
)
);
this.filters.push(this.initFilter(this.filterSelect[0]));
controlArray.push(this.initFilter(this.filterSelect[0]));
} else {
let nameArr: string[] = this.filterSelect.slice();
this.filterListData.forEach(data => {
@ -416,33 +511,41 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.filterListData.filter(data => {
data.options.splice(data.options.indexOf(nameArr[0]), 1);
});
this.filterListData.push(this.baseFilterData(nameArr[0], nameArr, true));
this.filters.push(this.initFilter(nameArr[0]));
this.filterListData.push(this.baseFilterData(nameArr[0], nameArr, true, ''));
controlArray.push(this.initFilter(nameArr[0]));
}
this.filterCount += 1;
if (this.filterCount >= this.filterSelect.length) {
this.isFilterHide = true;
}
if (controlArray.controls[this.filterCount - 1].get('kind').value === 'label' && this.labelInputVal) {
controlArray.controls[this.filterCount - 1].get('value').setValue(this.labelInputVal + ' labels');
}
}
// delete a filter
deleteFilter(i: number): void {
if (i || i === 0) {
let delfilter = this.filterListData.splice(i, 1)[0];
let delFilter = this.filterListData.splice(i, 1)[0];
if (this.filterCount === this.filterSelect.length) {
this.isFilterHide = false;
}
this.filterCount -= 1;
if (this.filterListData.length) {
let optionVal = delfilter.name;
let optionVal = delFilter.name;
this.filterListData.filter(data => {
if (data.options.indexOf(optionVal) === -1) {
data.options.push(optionVal);
}
});
}
const control = <FormArray>this.ruleForm.controls["filters"];
const control = <FormArray>this.ruleForm.get('filters');
if (control.controls[i].get('kind').value === 'label') {
this.labelInputVal = control.controls[i].get('value').value.split(' ')[0];
}
control.removeAt(i);
this.setFilter(control.value);
}
}
@ -502,7 +605,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
filters.forEach((filter: any) => {
let option: string[] = opt.slice();
option.unshift(filter.kind);
this.filterListData.push(this.baseFilterData(filter.kind, option, true));
this.filterListData.push(this.baseFilterData(filter.kind, option, true, ''));
});
this.filterCount = filters.length;
if (filters.length === this.filterSelect.length) {
@ -510,6 +613,31 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
}
selectedLabelList(selectedLabels: LabelState[], indexId: number) {
// set input value of filter label
const controlArray = <FormArray> this.ruleForm.get('filters');
this.filterListData.forEach((data, index) => {
if (data.name === 'label') {
let labelsLength = selectedLabels.filter(lab => lab.iconsShow === true).length;
if (labelsLength > 0) {
controlArray.controls[index].get('value').setValue(labelsLength + ' labels');
this.labelInputVal = labelsLength.toString();
}else {
controlArray.controls[index].get('value').setValue('');
}
};
});
// store filter label info
this.filterLabelInfo = [];
selectedLabels.forEach(data => {
if (data.iconsShow === true) {
this.filterLabelInfo.push(data.label);
}
});
}
updateTrigger(trigger: any) {
if (trigger["schedule_param"]) {
this.isScheduleOpt = true;
@ -558,6 +686,19 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
}
setFilterLabelVal(filters: any[]) {
let labels: any = filters.find(data => data.kind === this.filterSelect[2]);
if (labels) {
filters.splice(filters.indexOf(labels), 1);
let info: any[] = [];
this.filterLabelInfo.forEach(data => {
info.push({kind: 'label', value: data.id});
});
filters.push.apply(filters, info);
}
}
public hasFormChange(): boolean {
return !isEmptyObject(this.getChanges());
}
@ -567,6 +708,9 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.inProgress = true;
let copyRuleForm: ReplicationRule = this.ruleForm.value;
copyRuleForm.trigger = this.setTriggerVaule(copyRuleForm.trigger);
// rewrite key name of label when filer contain labels.
if (copyRuleForm.filters) { this.setFilterLabelVal(copyRuleForm.filters); };
if (this.policyId < 0) {
this.repService
.createReplicationRule(copyRuleForm)
@ -602,19 +746,24 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
openCreateEditRule(ruleId?: number | string): void {
this.initForm();
this.inlineAlert.close();
this.selectedProjectList = [];
this.filterCount = 0;
this.isFilterHide = false;
this.filterListData = [];
this.firstClick = 0;
this.noSelectedProject = true;
this.noSelectedEndpoint = true;
this.isRuleNameValid = true;
this.deletedLabelCount = 0;
this.weeklySchedule = false;
this.isScheduleOpt = false;
this.isImmediate = false;
this.policyId = -1;
this.createEditRuleOpened = true;
this.filterLabelInfo = [];
this.labelInputVal = '';
this.noProjectInfo = "";
this.noEndpointInfo = "";
@ -630,12 +779,12 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.headerTitle = "REPLICATION.EDIT_POLICY_TITLE";
toPromise(this.repService.getReplicationRule(ruleId))
.then(response => {
this.copyUpdateForm = Object.assign({}, response);
// set filter value is [] if callback fiter value is null.
this.copyUpdateForm.filters = response.filters
? response.filters
: [];
this.copyUpdateForm = clone(response);
// set filter value is [] if callback filter value is null.
this.updateForm(response);
// keep trigger same value
this.copyUpdateForm.trigger = clone(response.trigger);
this.copyUpdateForm.filters = this.copyUpdateForm.filters === null ? [] : this.copyUpdateForm.filters;
})
.catch((error: any) => {
this.inlineAlert.showInlineError(error);
@ -648,8 +797,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
]);
this.noSelectedProject = false;
}
this.copyUpdateForm = Object.assign({}, this.ruleForm.value);
this.copyUpdateForm = clone(this.ruleForm.value);
}
}
@ -743,7 +891,20 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
getChanges(): { [key: string]: any | any[] } {
let changes: { [key: string]: any | any[] } = {};
let ruleValue: { [key: string]: any | any[] } = this.ruleForm.value;
let ruleValue: { [key: string]: any | any[] } = clone(this.ruleForm.value);
if (ruleValue.filters && ruleValue.filters.length) {
ruleValue.filters.forEach((data, index) => {
if (data.kind === 'label') {
ruleValue.filters.splice(index, 1);
}
});
// rewrite filter label
this.filterLabelInfo.forEach(data => {
ruleValue.filters.push({kind: "label", pattern: "", value: data});
});
}
if (!ruleValue || !this.copyUpdateForm) {
return changes;
}

View File

@ -0,0 +1,13 @@
<div class="filterLabelPanel" [hidden]="!openFilterLabelPanel">
<a class="filterClose" (click)="closeFilter()">&times;</a>
<label class="filterLabelHeader">{{'REPOSITORY.FILTER_BY_LABEL' | translate}}</label>
<div><input class="filterInput" type="text" placeholder="Filter labels" [(ngModel)]= "filterLabelName" (keyup)="handleInputFilter()"></div>
<div [hidden]='labelLists.length' style="padding-left:10px;">{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!labelLists.length' style='max-height:300px;overflow-y: auto;'>
<button type="button" class="labelBtn" *ngFor='let label of labelLists' [hidden]='!label.show' (click)="selectLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="label.label" [labelWidth]="118"></hbr-label-piece></div>
</button>
</div>
</div>

View File

@ -0,0 +1,132 @@
.filterLabelPanel {
display: flex;
position: absolute;
left:135px;
-ms-flex-direction: column;
flex-direction: column;
background: #fff;
padding: .5rem 0;
border: 1px solid #ccc;
box-shadow: 0 1px 0.125rem hsla(0, 0%, 45%, .25);
min-width: 5rem;
max-width: 15rem;
border-radius: .125rem;
z-index: 100;
}
.filterLabelPanel .filterInput{margin-left: .5rem; margin-right: .5rem;}
.filterLabelHeader {
font-size: .5rem;
font-weight: 600;
letter-spacing: normal;
padding: 0 .5rem;
line-height: .75rem;
margin: 0;
color: #313131;
}
.filterLabelPanel .form-group input {
position: relative;
margin-left: .5rem;
margin-right: .5rem;
}
.filterClose {
position: absolute;
right: 8px;
top: 5px;
cursor: pointer;
font-size: 20px;
}
.pull-left {
display: inline-block;
float: left;
}
.pull-right {
display: inline-block;
float: right;
}
.btn-link {
display: inline-flex;
width: 15px;
min-width: 15px;
color: black;
vertical-align: super;
}
.trigger-item,
.signpost-item {
display: inline;
}
.signpost-content-body .label {
margin: .3rem;
}
.labelDiv {
position: absolute;
left: 28px;
top: 5px;
}
.datagrid-action-bar {
z-index: 10;
}
.trigger-item hbr-label-piece {
display: flex !important;
margin: 6px 0;
}
:host>>>.signpost-content {
min-width: 4rem;
}
:host>>>.signpost-content-body {
padding: 0 .4rem;
}
:host>>>.signpost-content-header {
display: none;
}
.filterLabelPiece {
position: absolute;
top: 4px;
z-index: 1;
}
.dropdown .dropdown-toggle.btn {
margin: .25rem .5rem .25rem 0;
}
.labelBtn {
position: relative;
overflow: hidden;
font-size: .58333rem;
letter-spacing: normal;
font-weight: 400;
background: transparent;
border: 0;
color: #565656;
cursor: pointer;
display: block;
margin: 0;
width: 100%;
height: 30px;
text-transform: none;
}
.labelBtn:hover {
background-color: #eee;
}
.labelBtn:focus {
outline: none;
}
.labelDiv label{margin: 0;}

View File

@ -0,0 +1,170 @@
import {Component, Input, OnInit, OnChanges, Output, EventEmitter, ChangeDetectorRef, SimpleChanges} from "@angular/core";
import {LabelService} from "../service/label.service";
import {toPromise} from "../utils";
import {Label} from "../service/interface";
import {ErrorHandler} from "../error-handler/error-handler";
import {Subject} from "rxjs/Subject";
export interface LabelState {
iconsShow: boolean;
label: Label;
show: boolean;
}
@Component({
selector: "hbr-filter-label",
templateUrl: "./filter-label.component.html",
styleUrls: ["./filter-label.component.scss"]
})
export class FilterLabelComponent implements OnInit, OnChanges {
openFilterLabelPanel: boolean;
labelLists: LabelState[] = [];
filterLabelName = '';
labelNameFilter: Subject<string> = new Subject<string> ();
@Input() isOpen: boolean;
@Input() projectId: number;
@Input() selectedLabelInfo: Label[];
@Output() selectedLabels = new EventEmitter<LabelState[]>();
@Output() closePanelEvent = new EventEmitter();
constructor(private labelService: LabelService,
private ref: ChangeDetectorRef,
private errorHandler: ErrorHandler) {}
ngOnInit(): void {
Promise.all([this.getGLabels(), this.getPLabels()]).then(() => {
this.selectedLabelInfo.forEach(info => {
if (this.labelLists.length) {
let lab = this.labelLists.find(data => data.label.id === info.id);
if (lab) {this.selectOper(lab); }
}
});
});
this.labelNameFilter
.debounceTime(500)
.distinctUntilChanged()
.subscribe((name: string) => {
if (this.filterLabelName.length) {
this.labelLists.forEach(data => {
if (data.label.name.indexOf(this.filterLabelName) !== -1) {
data.show = true;
} else {
data.show = false;
}
});
setTimeout(() => {
setInterval(() => this.ref.markForCheck(), 200);
}, 1000);
}
});
}
ngOnChanges(changes: SimpleChanges) {
if (changes['isOpen']) {this.openFilterLabelPanel = changes['isOpen'].currentValue; }
}
getGLabels() {
return toPromise<Label[]>(this.labelService.getGLabels()).then((res: Label[]) => {
if (res.length) {
res.forEach(data => {
this.labelLists.push({'iconsShow': false, 'label': data, 'show': true});
});
}
}).catch(error => {
this.errorHandler.error(error);
});
}
getPLabels() {
if (this.projectId && this.projectId > 0) {
return toPromise<Label[]>(this.labelService.getPLabels(this.projectId)).then((res1: Label[]) => {
if (res1.length) {
res1.forEach(data => {
this.labelLists.push({'iconsShow': false, 'label': data, 'show': true});
});
}
}).catch(error => {
this.errorHandler.error(error);
});
}
}
handleInputFilter(): void {
if (this.filterLabelName.length) {
this.labelNameFilter.next(this.filterLabelName);
}else {
this.labelLists.every(data => data.show = true);
}
}
selectLabel(labelInfo: LabelState): void {
if (labelInfo) {
let isClick = true;
if (!labelInfo.iconsShow) {
this.selectOper(labelInfo, isClick);
} else {
this.unSelectOper(labelInfo, isClick);
}
}
}
selectOper(labelInfo: LabelState, isClick?: boolean): void {
// set the selected label in front
this.labelLists.splice(this.labelLists.indexOf(labelInfo), 1);
this.labelLists.some((data, i) => {
if (!data.iconsShow) {
this.labelLists.splice(i, 0, labelInfo);
return true;
}
});
// when is the last one
if (this.labelLists.every(data => data.iconsShow === true)) {
this.labelLists.push(labelInfo);
}
labelInfo.iconsShow = true;
if (isClick) {
this.selectedLabels.emit(this.labelLists);
}
}
unSelectOper(labelInfo: LabelState, isClick?: boolean): void {
this.sortOperation(this.labelLists, labelInfo);
labelInfo.iconsShow = false;
if (isClick) {
this.selectedLabels.emit(this.labelLists);
}
}
// insert the unselected label to groups with the same icons
sortOperation(labelList: LabelState[], labelInfo: LabelState): void {
labelList.some((data, i) => {
if (!data.iconsShow) {
if (data.label.scope === labelInfo.label.scope) {
labelList.splice(i, 0, labelInfo);
labelList.splice(labelList.indexOf(labelInfo, 0), 1);
return true;
}
if (data.label.scope !== labelInfo.label.scope && i === labelList.length - 1) {
labelList.push(labelInfo);
labelList.splice(labelList.indexOf(labelInfo), 1);
return true;
}
}
if (data.iconsShow && i === labelList.length - 1) {
labelList.push(labelInfo);
labelList.splice(labelList.indexOf(labelInfo), 1);
return true;
}
});
}
closeFilter(): void {
this.closePanelEvent.emit();
}
}

View File

@ -1,7 +1,9 @@
import { Type } from "@angular/core";
import { CreateEditRuleComponent } from "./create-edit-rule.component";
import {FilterLabelComponent} from "./filter-label.component";
export const CREATE_EDIT_RULE_DIRECTIVES: Type<any>[] = [
CreateEditRuleComponent
CreateEditRuleComponent,
FilterLabelComponent
];

View File

@ -3,6 +3,7 @@
color: #222;
display: inline-block;
justify-content: flex-start;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
line-height: .875rem;

View File

@ -1,12 +1,13 @@
<div style="padding-bottom: 15px;">
<clr-datagrid [clrDgLoading]="loading" [(clrDgSingleSelected)]="selectedRow" [clDgRowSelection]="true">
<clr-dg-action-bar style="height: 24px;">
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" (click)="openModal()"><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'REPLICATION.NEW_REPLICATION_RULE' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" [disabled]="!selectedRow" (click)="editRule(selectedRow)"><clr-icon shape="pencil" size="16"></clr-icon>&nbsp;{{'REPLICATION.EDIT_POLICY' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" [disabled]="!selectedRow" (click)="deleteRule(selectedRow)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPLICATION.DELETE_POLICY' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" [disabled]="!selectedRow" (click)="replicateRule(selectedRow)"><clr-icon shape="export" size="16"></clr-icon>&nbsp;{{'REPLICATION.REPLICATE' | translate}}</button>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'name'">{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'status'">{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'projects'" *ngIf="!projectScope">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'description'">{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'targets'">{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
@ -14,6 +15,17 @@
<clr-dg-placeholder>{{'REPLICATION.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let p of changedRules" [clrDgItem]="p" (click)="selectRule(p)" [style.backgroundColor]="(projectScope && withReplicationJob && selectedId === p.id) ? '#eee' : ''">
<clr-dg-cell>{{p.name}}</clr-dg-cell>
<clr-dg-cell>
<div [ngSwitch]="hasDeletedLabel(p)">
<clr-tooltip *ngSwitchCase="'disabled'" class="tooltip-lg">
<clr-icon clrTooltipTrigger shape="exclamation-triangle" style="vertical-align: text-bottom;" class="is-warning" size="22"></clr-icon>Disabled
<clr-tooltip-content clrPosition="top-right" clrSize="xs" *clrIfOpen>
<span>{{'REPLICATION.RULE_DISABLED' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
<div *ngSwitchCase="'enabled'" ><clr-icon shape="success-standard" style="vertical-align: text-bottom;" class="is-success" size="18"></clr-icon> Enabled</div>
</div>
</clr-dg-cell>
<clr-dg-cell *ngIf="!projectScope">
<a href="javascript:void(0)" (click)="$event.stopPropagation(); redirectTo(p)">{{p.projects?.length>0 ? p.projects[0].name : ''}}</a>
</clr-dg-cell>

View File

@ -152,6 +152,22 @@ export class ListReplicationRuleComponent implements OnInit, OnChanges {
this.replicateManual.emit(rules);
}
hasDeletedLabel(rule: any) {
if (rule.filters) {
let count = 0;
rule.filters.forEach((data: any) => {
if (data.kind === 'label' && data.value.deleted) {
count ++;
}
});
if (count === 0) {
return 'enabled';
}else { return 'disabled'; }
}else {
return 'enabled';
}
}
deletionConfirm(message: ConfirmationAcknowledgement) {
if (
message &&

View File

@ -72,7 +72,7 @@
</div>
</div>
<job-log-viewer #replicationLogViewer></job-log-viewer>
<hbr-create-edit-rule *ngIf="isSystemAdmin" [projectId]="projectId" [projectName]="projectName" (goToRegistry)="goRegistry()" (reload)="reloadRules($event)"></hbr-create-edit-rule>
<hbr-create-edit-rule *ngIf="isSystemAdmin" [withAdmiral]="withAdmiral" [projectId]="projectId" [projectName]="projectName" (goToRegistry)="goRegistry()" (reload)="reloadRules($event)"></hbr-create-edit-rule>
<confirmation-dialog #replicationConfirmDialog (confirmAction)="confirmReplication($event)"></confirmation-dialog>
</div>

View File

@ -21,6 +21,8 @@ import { JobLogViewerComponent } from '../job-log-viewer/job-log-viewer.componen
import { JobLogService, JobLogDefaultService, ReplicationJobItem } from '../service/index';
import {ProjectDefaultService, ProjectService} from "../service/project.service";
import {OperationService} from "../operation/operation.service";
import {FilterLabelComponent} from "../create-edit-rule/filter-label.component";
import {LabelPieceComponent} from "../label-piece/label-piece.component";
describe('Replication Component (inline template)', () => {
@ -224,7 +226,9 @@ describe('Replication Component (inline template)', () => {
DatePickerComponent,
FilterComponent,
InlineAlertComponent,
JobLogViewerComponent
JobLogViewerComponent,
FilterLabelComponent,
LabelPieceComponent
],
providers: [
ErrorHandler,

View File

@ -105,6 +105,7 @@ export class ReplicationComponent implements OnInit, OnDestroy {
@Input() projectId: number | string;
@Input() projectName: string;
@Input() isSystemAdmin: boolean;
@Input() withAdmiral: boolean;
@Input() withReplicationJob: boolean;
@Output() redirect = new EventEmitter<ReplicationRule>();

View File

@ -20,7 +20,7 @@ export abstract class LabelService {
abstract getLabels(
scope: string,
projectId: number,
projectId?: number,
name?: string,
queryParams?: RequestQueryParams
): Observable<Label[]> | Promise<Label[]>;
@ -55,7 +55,7 @@ export class LabelDefaultService extends LabelService {
getLabels(
scope: string,
projectId: number,
projectId?: number,
name?: string,
queryParams?: RequestQueryParams
): Observable<Label[]> | Promise<Label[]> {

View File

@ -350,6 +350,11 @@ export class TagComponent implements OnInit, AfterViewInit {
}
});
// when is the last one
if (this.imageStickLabels.every(data => data.iconsShow === true)) {
this.imageStickLabels.push(labelInfo);
}
labelInfo.iconsShow = true;
this.inprogress = false;
}).catch(err => {

View File

@ -49,7 +49,7 @@
"bootstrap": "4.0.0-alpha.5",
"codelyzer": "~2.0.0-beta.4",
"enhanced-resolve": "^3.0.0",
"harbor-ui": "0.7.19-test-14",
"harbor-ui": "0.7.19-test-16",
"jasmine-core": "2.4.1",
"jasmine-spec-reporter": "2.5.0",
"karma": "~1.7.0",

View File

@ -1,4 +1,4 @@
<h2 class="custom-h2">{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}</h2>
<div style="margin-top: 4px;">
<hbr-replication [withReplicationJob]='true' [isSystemAdmin]="isSystemAdmin" (goToRegistry)="goRegistry()" (redirect)="customRedirect($event)"></hbr-replication>
<hbr-replication [withReplicationJob]='true' [isSystemAdmin]="isSystemAdmin" [withAdmiral]="withAdmiral" (goToRegistry)="goRegistry()" (redirect)="customRedirect($event)"></hbr-replication>
</div>

View File

@ -17,6 +17,7 @@ import {Router, ActivatedRoute} from "@angular/router";
import {ReplicationRule} from "harbor-ui";
import {SessionService} from "../../shared/session.service";
import {AppConfigService} from "../../app-config.service";
@Component({
selector: 'total-replication',
@ -26,6 +27,7 @@ export class TotalReplicationPageComponent {
constructor(private router: Router,
private session: SessionService,
private appConfigService: AppConfigService,
private activeRoute: ActivatedRoute) {}
customRedirect(rule: ReplicationRule): void {
if (rule) {
@ -40,4 +42,8 @@ export class TotalReplicationPageComponent {
let account = this.session.getCurrentUser();
return account != null && account.has_admin_role;
}
get withAdmiral(): boolean {
return this.appConfigService.getConfig().with_admiral;
}
}

View File

@ -272,6 +272,7 @@
"TEST_CONNECTION_SUCCESS": "Connection tested successfully.",
"TEST_CONNECTION_FAILURE": "Failed to ping endpoint.",
"NAME": "Name",
"STATUS": "Status",
"PROJECT": "Project",
"NAME_IS_REQUIRED": "Name is required.",
"DESCRIPTION": "Description",
@ -344,7 +345,10 @@
"DELETE_REMOTE_IMAGES":"Delete remote images when locally deleted",
"REPLICATE_IMMEDIATE":"Replicate existing images immediately",
"NEW": "New",
"NAME_TOOLTIP": "replication rule name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers."
"NAME_TOOLTIP": "replication rule name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.",
"DELETED_LABEL_INFO": "Deleted label(s) '{{param}}' referenced in the filter, click 'SAVE' to update the filter to enable this rule.",
"ACKNOWLEDGE": "Acknowledge",
"RULE_DISABLED": "This rule has been disabled because a label used in its filter has been deleted. \n Edit the rule and update its filter to enable it."
},
"DESTINATION": {
"NEW_ENDPOINT": "New Endpoint",

View File

@ -271,6 +271,7 @@
"TEST_CONNECTION_SUCCESS": "Conexión comprobada satisfactoriamente.",
"TEST_CONNECTION_FAILURE": "Fallo al conectar con el endpoint.",
"NAME": "Nombre",
"STATUS": "Status",
"PROJECT": "Proyecto",
"NAME_IS_REQUIRED": "El nombre es obligatorio.",
"DESCRIPTION": "Descripción",
@ -343,7 +344,10 @@
"DELETE_REMOTE_IMAGES":"Delete remote images when locally deleted",
"REPLICATE_IMMEDIATE":"Replicate existing images immediately",
"NEW": "New",
"NAME_TOOLTIP": "replication rule name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers."
"NAME_TOOLTIP": "replication rule name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.",
"DELETED_LABEL_INFO": "Deleted label(s) '{{param}}' referenced in the filter, click 'SAVE' to update the filter to enable this rule.",
"ACKNOWLEDGE": "Acknowledge",
"RULE_DISABLED": "This rule has been disabled because a label used in its filter has been deleted. \n Edit the rule and update its filter to enable it."
},
"DESTINATION": {
"NEW_ENDPOINT": "Nuevo Endpoint",

View File

@ -250,6 +250,7 @@
"TEST_CONNECTION_SUCCESS": "Connexion testée avec succès.",
"TEST_CONNECTION_FAILURE": "Echec du ping du point final.",
"NAME": "Nom",
"STATUS": "Status",
"PROJECT": "Projet",
"NAME_IS_REQUIRED": "Le nom est obligatoire.",
"DESCRIPTION": "Description",
@ -321,7 +322,10 @@
"DELETE_REMOTE_IMAGES":"Delete remote images when locally deleted",
"REPLICATE_IMMEDIATE":"Replicate existing images immediately",
"NEW": "New",
"NAME_TOOLTIP": "replication rule name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers."
"NAME_TOOLTIP": "replication rule name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.",
"DELETED_LABEL_INFO": "Deleted label(s) '{{param}}' referenced in the filter, click 'SAVE' to update the filter to enable this rule.",
"ACKNOWLEDGE": "Acknowledge",
"RULE_DISABLED": "This rule has been disabled because a label used in its filter has been deleted. \n Edit the rule and update its filter to enable it."
},
"DESTINATION": {
"NEW_ENDPOINT": "Nouveau Point Final",

View File

@ -271,6 +271,7 @@
"TEST_CONNECTION_SUCCESS": "测试连接成功。",
"TEST_CONNECTION_FAILURE": "测试连接失败。",
"NAME": "名称",
"STATUS": "状态",
"PROJECT": "项目",
"NAME_IS_REQUIRED": "名称为必填项。",
"DESCRIPTION": "描述",
@ -343,7 +344,10 @@
"DELETE_REMOTE_IMAGES":"删除本地镜像时同时也删除远程的镜像。",
"REPLICATE_IMMEDIATE":"立即复制现有的镜像。",
"NEW": "新增",
"NAME_TOOLTIP": "项目名称由小写字符、数字和._-组成且至少2个字符并以字符或者数字开头。"
"NAME_TOOLTIP": "项目名称由小写字符、数字和._-组成且至少2个字符并以字符或者数字开头。",
"DELETED_LABEL_INFO": "过滤项有被删除的标签 {{param}} , 点击保存按钮更新过滤项使规则可用。",
"ACKNOWLEDGE": "确认",
"RULE_DISABLED": "这个规则因为过滤选项中的标签被删除已经不能用了,更新过滤项以便重新启用规则。"
},
"DESTINATION": {
"NEW_ENDPOINT": "新建目标",