harbor/src/portal/lib/src/create-edit-rule/create-edit-rule.component.ts

914 lines
26 KiB
TypeScript

// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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 {
Component,
OnInit,
OnDestroy,
ViewChild,
ChangeDetectorRef,
Input,
EventEmitter,
Output
} from "@angular/core";
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 { 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";
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;
@Component({
selector: "hbr-create-edit-rule",
templateUrl: "./create-edit-rule.component.html",
styleUrls: ["./create-edit-rule.component.scss"]
})
export class CreateEditRuleComponent implements OnInit, OnDestroy {
_localTime: Date = new Date();
targetList: Endpoint[] = [];
projectList: Project[] = [];
selectedProjectList: Project[] = [];
isFilterHide = false;
weeklySchedule: boolean;
isScheduleOpt: boolean;
isImmediate = false;
noProjectInfo = "";
noEndpointInfo = "";
noSelectedProject = true;
noSelectedEndpoint = true;
filterCount = 0;
alertClosed = false;
triggerNames: string[] = ["Manual", "Immediate", "Scheduled"];
scheduleNames: string[] = ["Daily", "Weekly"];
weekly: string[] = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday"
];
filterSelect: string[] = ["repository", "tag", "label"];
ruleNameTooltip = "REPLICATION.NAME_TOOLTIP";
headerTitle = "REPLICATION.ADD_POLICY";
createEditRuleOpened: boolean;
filterListData: { [key: string]: any }[] = [];
inProgress = false;
inNameChecking = false;
isRuleNameValid = true;
nameChecker: Subject<string> = new Subject<string>();
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>();
@ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent;
emptyProject = {
project_id: -1,
name: ""
};
emptyEndpoint = {
id: -1,
endpoint: "",
name: "",
username: "",
password: "",
insecure: true,
type: 0
};
constructor(
private fb: FormBuilder,
private repService: ReplicationService,
private endpointService: EndpointService,
private errorHandler: ErrorHandler,
private proService: ProjectService,
private translateService: TranslateService,
private ref: ChangeDetectorRef
) {
this.createForm();
}
baseFilterData(name: string, option: string[], state: boolean) {
return {
name: name,
options: option,
state: state,
isValid: true,
isOpen: false // label list
};
}
ngOnInit(): void {
if (this.withAdmiral) {
this.filterSelect = ["repository", "tag"];
}
toPromise<Endpoint[]>(this.endpointService.getEndpoints())
.then(targets => {
this.targetList = targets || [];
})
.catch((error: any) => this.errorHandler.error(error));
if (!this.projectId) {
toPromise<Project[]>(this.proService.listProjects("", undefined))
.then(targets => {
this.projectList = targets || [];
})
.catch(error => this.errorHandler.error(error));
}
this.nameChecker
.debounceTime(300)
.distinctUntilChanged()
.subscribe((ruleName: string) => {
let cont = this.ruleForm.controls["name"];
if (cont) {
this.isRuleNameValid = cont.valid;
if (this.isRuleNameValid) {
this.inNameChecking = true;
toPromise<ReplicationRule[]>(
this.repService.getReplicationRules(0, ruleName)
)
.then(response => {
if (response.some(rule => rule.name === ruleName)) {
this.ruleNameTooltip = "TOOLTIP.RULE_USER_EXISTING";
this.isRuleNameValid = false;
}
this.inNameChecking = false;
})
.catch(() => {
this.inNameChecking = false;
});
} else {
this.ruleNameTooltip = "REPLICATION.NAME_TOOLTIP";
}
}
});
this.proNameChecker
.debounceTime(500)
.distinctUntilChanged()
.subscribe((resp: string) => {
let name = this.ruleForm.controls["projects"].value[0].name;
this.noProjectInfo = "";
this.selectedProjectList = [];
toPromise<Project[]>(this.proService.listProjects(name, undefined))
.then((res: any) => {
if (res) {
this.selectedProjectList = res.slice(0, 10);
// if input value exit in project list
let pro = res.find((data: any) => data.name === name);
if (!pro) {
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
this.noSelectedProject = true;
} else {
this.noProjectInfo = "";
this.noSelectedProject = false;
this.setProject([pro]);
}
} else {
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
this.noSelectedProject = true;
}
})
.catch((error: any) => {
this.errorHandler.error(error);
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
this.noSelectedProject = true;
});
});
}
ngOnDestroy(): void {
if (this.confirmSub) {
this.confirmSub.unsubscribe();
}
if (this.nameChecker) {
this.nameChecker.unsubscribe();
}
if (this.proNameChecker) {
this.proNameChecker.unsubscribe();
}
}
get isValid() {
return !(
!this.isRuleNameValid ||
this.noSelectedProject ||
this.noSelectedEndpoint ||
this.inProgress
);
}
createForm() {
this.formArrayLabel = this.fb.array([]);
this.ruleForm = this.fb.group({
name: ["", Validators.required],
description: "",
projects: this.fb.array([]),
targets: this.fb.array([]),
trigger: this.fb.group({
kind: this.triggerNames[0],
schedule_param: this.fb.group({
type: this.scheduleNames[0],
weekday: 1,
offtime: "08:00"
})
}),
filters: this.fb.array([]),
replicate_existing_image_now: true,
replicate_deletion: false
});
}
initForm(): void {
this.ruleForm.reset({
name: "",
description: "",
trigger: {
kind: this.triggerNames[0],
schedule_param: {
type: this.scheduleNames[0],
weekday: 1,
offtime: "08:00"
}
},
replicate_existing_image_now: true,
replicate_deletion: false
});
this.setProject([this.emptyProject]);
this.setTarget([this.emptyEndpoint]);
this.setFilter([]);
this.copyUpdateForm = clone(this.ruleForm.value);
}
updateForm(rule: ReplicationRule): void {
rule.trigger = this.updateTrigger(rule.trigger);
this.ruleForm.reset({
name: rule.name,
description: rule.description,
trigger: rule.trigger,
replicate_existing_image_now: rule.replicate_existing_image_now,
replicate_deletion: rule.replicate_deletion
});
this.setProject(rule.projects);
this.noSelectedProject = false;
this.setTarget(rule.targets);
this.noSelectedEndpoint = false;
if (rule.filters) {
this.reOrganizeLabel(rule.filters);
this.setFilter(rule.filters);
this.updateFilter(rule.filters);
}
// Force refresh view
let hnd = setInterval(() => this.ref.markForCheck(), 100);
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 === this.filterSelect[2]) {
if (!data.value.deleted) {
count++;
this.filterLabelInfo.push(data.value);
} else {
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: 'label', value: count + ' labels' });
this.labelInputVal = count.toString();
}
}
}
get projects(): FormArray {
return this.ruleForm.get("projects") as FormArray;
}
setProject(projects: Project[]) {
const projectFGs = projects.map(project => this.fb.group(project));
const projectFormArray = this.fb.array(projectFGs);
this.ruleForm.setControl("projects", projectFormArray);
}
get filters(): FormArray {
return this.ruleForm.get("filters") as FormArray;
}
setFilter(filters: Filter[]) {
const filterFGs = filters.map(filter => this.fb.group(filter));
const filterFormArray = this.fb.array(filterFGs);
this.ruleForm.setControl("filters", filterFormArray);
}
get targets(): FormArray {
return this.ruleForm.get("targets") as FormArray;
}
setTarget(targets: Endpoint[]) {
const targetFGs = targets.map(target => this.fb.group(target));
const targetFormArray = this.fb.array(targetFGs);
this.ruleForm.setControl("targets", targetFormArray);
}
initFilter(name: string) {
return this.fb.group({
kind: name,
value: ['', Validators.required]
});
}
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;
} else {
data.options.splice(data.options.indexOf(value), 1);
}
if (data.options.indexOf(name) === -1) {
data.options.push(name);
}
// if before select, $event is label
if (!this.withAdmiral && 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
if (!this.withAdmiral && 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.withAdmiral && labelTag === this.filterSelect[2]) {
this.filterListData.forEach((data, index) => {
if (index === indexId) {
data.isOpen = true;
} else {
data.isOpen = false;
}
});
}
}
targetChange($event: any) {
if ($event && $event.target) {
if ($event.target["value"] === "-1") {
this.noSelectedEndpoint = true;
return;
}
let selecedTarget: Endpoint = this.targetList.find(
target => target.id === +$event.target["value"]
);
this.setTarget([selecedTarget]);
this.noSelectedEndpoint = false;
}
}
// Handle the form validation
handleValidation(): void {
let cont = this.ruleForm.controls["projects"];
if (cont && cont.valid) {
this.proNameChecker.next(cont.value[0].name);
}
}
focusClear($event: any): void {
if (this.policyId < 0 && this.firstClick === 0) {
if ($event && $event.target && $event.target["value"]) {
$event.target["value"] = "";
}
this.firstClick++;
}
}
leaveInput() {
this.selectedProjectList = [];
}
selectedProjectName(projectName: string) {
this.noSelectedProject = false;
let pro: Project = this.selectedProjectList.find(
data => data.name === projectName
);
this.setProject([pro]);
this.selectedProjectList = [];
this.noProjectInfo = "";
}
selectedProject(project: Project): void {
if (!project) {
this.noSelectedProject = true;
} else {
this.noSelectedProject = false;
this.setProject([project]);
}
}
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,
)
);
controlArray.push(this.initFilter(this.filterSelect[0]));
} else {
let nameArr: string[] = this.filterSelect.slice();
this.filterListData.forEach(data => {
nameArr.splice(nameArr.indexOf(data.name), 1);
});
// when add a new filter,the filterListData should change the options
this.filterListData.filter(data => {
data.options.splice(data.options.indexOf(nameArr[0]), 1);
});
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 === this.filterSelect[2] && this.labelInputVal) {
controlArray.controls[this.filterCount - 1].get('value').setValue(this.labelInputVal + ' labels');
}
}
// delete a filter
deleteFilter(i: number): void {
if (i >= 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;
this.filterListData.filter(data => {
if (data.options.indexOf(optionVal) === -1) {
data.options.push(optionVal);
}
});
}
const control = <FormArray>this.ruleForm.get('filters');
if (control.controls[i].get('kind').value === this.filterSelect[2]) {
this.filterLabelInfo = [];
this.labelInputVal = "";
}
control.removeAt(i);
this.setFilter(control.value);
}
}
selectTrigger($event: any): void {
if ($event && $event.target && $event.target["value"]) {
let val: string = $event.target["value"];
if (val === this.triggerNames[2]) {
this.isScheduleOpt = true;
this.isImmediate = false;
}
if (val === this.triggerNames[1]) {
this.isScheduleOpt = false;
this.isImmediate = true;
}
if (val === this.triggerNames[0]) {
this.isScheduleOpt = false;
this.isImmediate = false;
}
}
}
// Replication Schedule select value exchange
selectSchedule($event: any): void {
if ($event && $event.target && $event.target["value"]) {
switch ($event.target["value"]) {
case this.scheduleNames[1]:
this.weeklySchedule = true;
this.ruleForm.patchValue({
trigger: {
schedule_param: {
weekday: 1
}
}
});
break;
case this.scheduleNames[0]:
this.weeklySchedule = false;
break;
}
}
}
checkRuleName(): void {
let ruleName: string = this.ruleForm.controls["name"].value;
if (ruleName) {
this.nameChecker.next(ruleName);
} else {
this.ruleNameTooltip = "REPLICATION.NAME_TOOLTIP";
}
}
updateFilter(filters: any) {
let opt: string[] = this.filterSelect.slice();
filters.forEach((filter: any) => {
opt.splice(opt.indexOf(filter.kind), 1);
});
filters.forEach((filter: any) => {
let option: string[] = opt.slice();
option.unshift(filter.kind);
this.filterListData.push(this.baseFilterData(filter.kind, option, true));
});
this.filterCount = filters.length;
if (filters.length === this.filterSelect.length) {
this.isFilterHide = true;
}
}
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 === this.filterSelect[2]) {
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;
this.isImmediate = false;
trigger["schedule_param"]["offtime"] = this.getOfftime(
trigger["schedule_param"]["offtime"]
);
if (trigger["schedule_param"]["weekday"]) {
this.weeklySchedule = true;
} else {
// set default
trigger["schedule_param"]["weekday"] = 1;
}
} else {
if (trigger["kind"] === this.triggerNames[0]) {
this.isImmediate = false;
}
if (trigger["kind"] === this.triggerNames[1]) {
this.isImmediate = true;
}
trigger["schedule_param"] = {
type: this.scheduleNames[0],
weekday: this.weekly[0],
offtime: "08:00"
};
}
return trigger;
}
setTriggerVaule(trigger: any) {
if (!this.isScheduleOpt) {
delete trigger["schedule_param"];
return trigger;
} else {
if (!this.weeklySchedule) {
delete trigger["schedule_param"]["weekday"];
} else {
trigger["schedule_param"]["weekday"] = +trigger["schedule_param"][
"weekday"
];
}
trigger["schedule_param"]["offtime"] = this.setOfftime(
trigger["schedule_param"]["offtime"]
);
return trigger;
}
}
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.hasChanges());
}
onSubmit() {
// add new Replication rule
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)
.then(() => {
this.translateService
.get("REPLICATION.CREATED_SUCCESS")
.subscribe(res => this.errorHandler.info(res));
this.inProgress = false;
this.reload.emit(true);
this.close();
})
.catch((error: any) => {
this.inProgress = false;
this.inlineAlert.showInlineError(error);
});
} else {
this.repService
.updateReplicationRule(this.policyId, this.ruleForm.value)
.then(() => {
this.translateService
.get("REPLICATION.UPDATED_SUCCESS")
.subscribe(res => this.errorHandler.info(res));
this.inProgress = false;
this.reload.emit(true);
this.close();
})
.catch((error: any) => {
this.inProgress = false;
this.inlineAlert.showInlineError(error);
});
}
}
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 = "";
if (this.targetList.length === 0) {
this.noEndpointInfo = "REPLICATION.NO_ENDPOINT_INFO";
}
if (this.projectList.length === 0 && !this.projectName) {
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
}
if (ruleId) {
this.policyId = +ruleId;
this.headerTitle = "REPLICATION.EDIT_POLICY_TITLE";
toPromise(this.repService.getReplicationRule(ruleId))
.then(response => {
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);
});
} else {
this.headerTitle = "REPLICATION.ADD_POLICY";
if (this.projectId) {
this.setProject([
{ project_id: this.projectId, name: this.projectName }
]);
this.noSelectedProject = false;
}
this.copyUpdateForm = clone(this.ruleForm.value);
}
}
close(): void {
this.createEditRuleOpened = false;
}
confirmCancel(confirmed: boolean) {
this.inlineAlert.close();
this.close();
}
onCancel(): void {
if (this.hasFormChange()) {
this.inlineAlert.showInlineConfirmation({
message: "ALERT.FORM_CHANGE_CONFIRMATION"
});
} else {
this.close();
}
}
goRegistry(): void {
this.goToRegistry.emit();
}
// UTC time
public getOfftime(daily_time: any): string {
let timeOffset = 0; // seconds
if (daily_time && typeof daily_time === "number") {
timeOffset = +daily_time;
}
// Convert to current time
let timezoneOffset: number = this._localTime.getTimezoneOffset();
// Local time
timeOffset = timeOffset - timezoneOffset * 60;
if (timeOffset < 0) {
timeOffset = timeOffset + ONE_DAY_SECONDS;
}
if (timeOffset >= ONE_DAY_SECONDS) {
timeOffset -= ONE_DAY_SECONDS;
}
// To time string
let hours: number = Math.floor(timeOffset / ONE_HOUR_SECONDS);
let minutes: number = Math.floor(
(timeOffset - hours * ONE_HOUR_SECONDS) / 60
);
let timeStr: string = "" + hours;
if (hours < 10) {
timeStr = "0" + timeStr;
}
if (minutes < 10) {
timeStr += ":0";
} else {
timeStr += ":";
}
timeStr += minutes;
return timeStr;
}
public setOfftime(v: string) {
if (!v || v === "") {
return;
}
let values: string[] = v.split(":");
if (!values || values.length !== 2) {
return;
}
let hours: number = +values[0];
let minutes: number = +values[1];
// Convert to UTC time
let timezoneOffset: number = this._localTime.getTimezoneOffset();
let utcTimes: number = hours * ONE_HOUR_SECONDS + minutes * 60;
utcTimes += timezoneOffset * 60;
if (utcTimes < 0) {
utcTimes += ONE_DAY_SECONDS;
}
if (utcTimes >= ONE_DAY_SECONDS) {
utcTimes -= ONE_DAY_SECONDS;
}
return utcTimes;
}
hasChanges(): boolean {
let formValue = clone(this.ruleForm.value);
let initValue = clone(this.copyUpdateForm);
let initValueCopy: any = {};
for (let key of Object.keys(formValue)) {
initValueCopy[key] = initValue[key];
}
if (formValue.filters && formValue.filters.length > 0) {
formValue.filters.forEach((data, index) => {
if (data.kind === this.filterSelect[2]) {
formValue.filters.splice(index, 1);
}
});
// rewrite filter label
this.filterLabelInfo.forEach(data => {
formValue.filters.push({ kind: "label", pattern: "", value: data });
});
}
if (!compareValue(formValue, initValueCopy)) {
return true;
}
return false;
}
}