harbor/src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.ts

899 lines
32 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,
Input,
EventEmitter,
Output,
} from '@angular/core';
import { Filter } from '../../../../../shared/services';
import { forkJoin, Observable, Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, finalize } from 'rxjs/operators';
import {
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
Validators,
UntypedFormControl,
} from '@angular/forms';
import {
clone,
isEmptyObject,
isSameObject,
} from '../../../../../shared/units/utils';
import { InlineAlertComponent } from '../../../../../shared/components/inline-alert/inline-alert.component';
import { ReplicationService } from '../../../../../shared/services';
import { ErrorHandler } from '../../../../../shared/units/error-handler';
import { TranslateService } from '@ngx-translate/core';
import { cronRegex } from '../../../../../shared/units/utils';
import { FilterType } from '../../../../../shared/entities/shared.const';
import { RegistryService } from '../../../../../../../ng-swagger-gen/services/registry.service';
import { Registry } from '../../../../../../../ng-swagger-gen/models/registry';
import { Label } from '../../../../../../../ng-swagger-gen/models/label';
import { LabelService } from '../../../../../../../ng-swagger-gen/services/label.service';
import {
BandwidthUnit,
Decoration,
Flatten_I18n_MAP,
Flatten_Level,
} from '../../replication';
import { errorHandler as errorHandlerFn } from '../../../../../shared/units/shared.utils';
import { ReplicationPolicy } from '../../../../../../../ng-swagger-gen/models/replication-policy';
import { RegistryInfo } from 'ng-swagger-gen/models';
const PREFIX: string = '0 ';
const PAGE_SIZE: number = 100;
export const KB_TO_MB: number = 1024;
@Component({
selector: 'hbr-create-edit-rule',
templateUrl: './create-edit-rule.component.html',
styleUrls: ['./create-edit-rule.component.scss'],
})
export class CreateEditRuleComponent implements OnInit, OnDestroy {
sourceList: Registry[] = [];
targetList: Registry[] = [];
noEndpointInfo = '';
isPushMode = true;
noSelectedEndpoint = true;
TRIGGER_TYPES = {
MANUAL: 'manual',
SCHEDULED: 'scheduled',
EVENT_BASED: 'event_based',
};
ruleNameTooltip = 'REPLICATION.NAME_TOOLTIP';
headerTitle = 'REPLICATION.ADD_POLICY';
createEditRuleOpened: boolean;
inProgress = false;
onGoing = false;
inNameChecking = false;
isRuleNameValid = true;
nameChecker: Subject<string> = new Subject<string>();
policyId: number;
confirmSub: Subscription;
ruleForm: UntypedFormGroup;
copyUpdateForm: ReplicationPolicy;
cronString: string;
supportedTriggers: string[];
supportedFilters: Filter[];
supportedFilterLabels: {
name: string;
color: string;
scope: string;
}[] = [];
@Input() withAdmiral: boolean;
@Output() goToRegistry = new EventEmitter<any>();
@Output() reload = new EventEmitter<boolean>();
@ViewChild(InlineAlertComponent, { static: true })
inlineAlert: InlineAlertComponent;
flattenLevelMap = Flatten_I18n_MAP;
speedUnits = [
{
UNIT: BandwidthUnit.KB,
},
{
UNIT: BandwidthUnit.MB,
},
];
selectedUnit: string = BandwidthUnit.KB;
copySpeedUnit: string = BandwidthUnit.KB;
showChunkOption: boolean = false;
stringForLabelFilter: string = '';
copyStringForLabelFilter: string = '';
constructor(
private fb: UntypedFormBuilder,
private repService: ReplicationService,
private endpointService: RegistryService,
private errorHandler: ErrorHandler,
private translateService: TranslateService,
private labelService: LabelService
) {
this.createForm();
}
initRegistryInfo(id: number): void {
this.inProgress = true;
this.endpointService
.getRegistryInfo({ id: id })
.pipe(finalize(() => (this.inProgress = false)))
.subscribe({
next: adapter => {
// for push mode, if id === 0, what we get is currentRegistryInfo, and then setFilterAndTrigger(currentRegistryInfo)
// for pull mode, always setFilterAndTrigger
if ((this.isPushMode && !id) || !this.isPushMode) {
this.setFilterAndTrigger(adapter);
}
if (id) {
this.checkChunkOption(id, adapter);
}
},
error: error => {
this.inlineAlert.showInlineError(error);
},
});
}
getAllRegistries() {
this.endpointService
.listRegistriesResponse({
page: 1,
pageSize: PAGE_SIZE,
})
.subscribe(
result => {
// Get total count
if (result.headers) {
const xHeader: string =
result.headers.get('X-Total-Count');
const totalCount = parseInt(xHeader, 0);
let arr = result.body || [];
if (totalCount <= PAGE_SIZE) {
// already gotten all Registries
this.targetList = result.body || [];
this.sourceList = result.body || [];
} else {
// get all the registries in specified times
const times: number = Math.ceil(
totalCount / PAGE_SIZE
);
const observableList: Observable<Registry[]>[] = [];
for (let i = 2; i <= times; i++) {
observableList.push(
this.endpointService.listRegistries({
page: i,
pageSize: PAGE_SIZE,
})
);
}
forkJoin(observableList).subscribe(res => {
if (res && res.length) {
res.forEach(item => {
arr = arr.concat(item);
});
this.sourceList = arr;
this.targetList = arr;
}
});
}
}
},
error => {
this.errorHandler.error(error);
}
);
}
ngOnInit(): void {
this.getAllLabels();
this.getAllRegistries();
this.nameChecker
.pipe(debounceTime(300))
.pipe(distinctUntilChanged())
.subscribe((ruleName: string) => {
let cont = this.ruleForm.controls['name'];
if (cont) {
this.isRuleNameValid = cont.valid;
if (this.isRuleNameValid) {
this.inNameChecking = true;
this.repService
.getReplicationRules(0, ruleName)
.subscribe(
response => {
if (
response.some(
rule =>
rule.name === ruleName &&
rule.id !== this.policyId
)
) {
this.ruleNameTooltip =
'TOOLTIP.RULE_USER_EXISTING';
this.isRuleNameValid = false;
}
this.inNameChecking = false;
},
() => {
this.inNameChecking = false;
}
);
} else {
this.ruleNameTooltip = 'REPLICATION.NAME_TOOLTIP';
}
}
});
}
trimText(event) {
if (event.target.value) {
event.target.value = event.target.value.replace(/\s+/g, '');
}
}
equals(c1: any, c2: any): boolean {
return c1 && c2 ? c1.id === c2.id : c1 === c2;
}
pushModeChange(): void {
this.setFilter([]);
this.initRegistryInfo(0);
this.checkChunkOption(this.ruleForm?.get('dest_registry')?.value?.id);
}
pullModeChange(): void {
let selectId = this.ruleForm.get('src_registry').value;
if (selectId) {
this.setFilter([]);
this.initRegistryInfo(selectId.id);
} else {
this.checkChunkOption(0);
}
}
sourceChange($event): void {
this.noSelectedEndpoint = false;
let selectId = this.ruleForm.get('src_registry').value;
this.setFilter([]);
this.initRegistryInfo(selectId.id);
}
ngOnDestroy(): void {
if (this.confirmSub) {
this.confirmSub.unsubscribe();
}
if (this.nameChecker) {
this.nameChecker.unsubscribe();
}
}
get isValid() {
if (this.ruleForm.controls['dest_namespace'].value) {
if (this.ruleForm.controls['dest_namespace'].invalid) {
return false;
}
}
if (this.ruleForm.controls['speed'].invalid) {
return false;
}
let controlName = !!this.ruleForm.controls['name'].value;
let sourceRegistry = !!this.ruleForm.controls['src_registry'].value;
let destRegistry = !!this.ruleForm.controls['dest_registry'].value;
let triggerMode = !!this.ruleForm.controls['trigger'].value.type;
let cron = !!this.ruleForm.value.trigger.trigger_settings.cron;
return !(
!controlName ||
!triggerMode ||
!this.isRuleNameValid ||
(!this.isPushMode && !sourceRegistry) ||
(this.isPushMode && !destRegistry) ||
!(
(!this.isNotSchedule() &&
cron &&
cronRegex(
this.ruleForm.value.trigger.trigger_settings.cron || ''
)) ||
this.isNotSchedule()
)
);
}
createForm() {
this.ruleForm = this.fb.group({
name: ['', Validators.required],
description: '',
src_registry: new UntypedFormControl(),
dest_registry: new UntypedFormControl(),
dest_namespace: '',
dest_namespace_replace_count: -1,
trigger: this.fb.group({
type: '',
trigger_settings: this.fb.group({
cron: '',
}),
}),
filters: this.fb.array([]),
enabled: true,
deletion: false,
override: true,
speed: -1,
copy_by_chunk: false,
});
}
isNotSchedule(): boolean {
return (
this.ruleForm.get('trigger').get('type').value !==
this.TRIGGER_TYPES.SCHEDULED
);
}
isNotEventBased(): boolean {
return (
this.ruleForm.get('trigger').get('type').value !==
this.TRIGGER_TYPES.EVENT_BASED
);
}
formReset(): void {
this.ruleForm.reset({
name: '',
description: '',
trigger: {
type: '',
trigger_settings: {
cron: '',
},
},
deletion: false,
enabled: true,
override: true,
dest_namespace_replace_count: Flatten_Level.FLATTEN_LEVEl_1,
speed: -1,
copy_by_chunk: false,
});
this.isPushMode = true;
this.selectedUnit = BandwidthUnit.KB;
this.stringForLabelFilter = '';
}
updateRuleFormAndCopyUpdateForm(rule: ReplicationPolicy): void {
if (rule?.filters?.length) {
// set stringForLabelFilter
this.stringForLabelFilter = '';
this.copyStringForLabelFilter = '';
rule.filters.forEach(item => {
if (item.type === FilterType.LABEL) {
this.stringForLabelFilter = (item.value as string[]).join(
','
);
this.copyStringForLabelFilter = this.stringForLabelFilter;
}
});
}
this.isPushMode = rule.dest_registry.id !== 0;
this.checkChunkOption(rule.dest_registry.id || rule.src_registry.id);
setTimeout(() => {
// convert speed unit to KB or MB
let speed: number = this.convertToInputValue(rule.speed);
// There is no trigger_setting type when the harbor is upgraded from the old version.
rule.trigger.trigger_settings = rule.trigger.trigger_settings
? rule.trigger.trigger_settings
: { cron: '' };
this.ruleForm.reset({
name: rule.name,
description: rule.description,
dest_namespace: rule.dest_namespace,
dest_namespace_replace_count: rule.dest_namespace_replace_count,
src_registry: rule.src_registry,
dest_registry: rule.dest_registry,
trigger: rule.trigger,
deletion: rule.deletion,
enabled: rule.enabled,
override: rule.override,
speed: speed,
copy_by_chunk: rule.copy_by_chunk,
});
let filtersArray = this.getFilterArray(rule);
this.noSelectedEndpoint = false;
this.setFilter(filtersArray);
this.copyUpdateForm = clone(this.ruleForm.value);
this.copySpeedUnit = this.selectedUnit;
// keep trigger same value
this.copyUpdateForm.trigger = clone(rule.trigger);
this.copyUpdateForm.filters =
this.copyUpdateForm.filters === null
? []
: this.copyUpdateForm.filters;
// set filter value is [] if callback filter value is null.
}, 100);
// end of reset the filter list.
}
get filters(): UntypedFormArray {
return this.ruleForm.get('filters') as UntypedFormArray;
}
setFilter(filters: Filter[]) {
const filterFGs = filters.map(filter => {
if (filter.type === FilterType.LABEL) {
let fbLabel = this.fb.group({
type: FilterType.LABEL,
decoration: filter.decoration || Decoration.MATCHES,
});
let filterLabel = this.fb.array(filter.value);
fbLabel.setControl('value', filterLabel);
return fbLabel;
}
if (filter.type === FilterType.TAG) {
return this.fb.group({
type: FilterType.TAG,
decoration: filter.decoration || Decoration.MATCHES,
value: filter.value,
});
}
return this.fb.group(filter);
});
const filterFormArray = this.fb.array(filterFGs);
this.ruleForm.setControl('filters', filterFormArray);
}
initFilter(name: string) {
if (name === FilterType.LABEL) {
const labelArray = this.fb.array([]);
const labelControl = this.fb.group({
type: name,
decoration: Decoration.MATCHES,
});
labelControl.setControl('value', labelArray);
return labelControl;
}
if (name === FilterType.TAG) {
return this.fb.group({
type: name,
decoration: Decoration.MATCHES,
value: '',
});
}
return this.fb.group({
type: name,
value: '',
});
}
targetChange($event: any) {
if ($event && $event.target) {
if ($event.target['value'] === '-1') {
this.noSelectedEndpoint = true;
return;
}
this.noSelectedEndpoint = false;
this.initRegistryInfo(this.ruleForm.get('dest_registry').value.id);
}
}
checkRuleName(): void {
let ruleName: string = this.ruleForm.controls['name'].value;
if (ruleName) {
this.nameChecker.next(ruleName);
} else {
this.ruleNameTooltip = 'REPLICATION.NAME_TOOLTIP';
}
}
public hasFormChange(): boolean {
if (this.copySpeedUnit !== this.selectedUnit) {
// speed unit has been changed
return true;
}
if (this.copyStringForLabelFilter !== this.stringForLabelFilter) {
// label filter has been changed
return true;
}
return !isEmptyObject(this.hasChanges());
}
onSubmit() {
if (this.ruleForm.value.trigger.type !== 'scheduled') {
this.ruleForm
.get('trigger')
.get('trigger_settings')
.get('cron')
.setValue('');
}
// add new Replication rule
this.inProgress = true;
let copyRuleForm: ReplicationPolicy = this.ruleForm.value;
// need to convert unit to KB for speed property
copyRuleForm.speed = this.convertToKB(copyRuleForm.speed);
copyRuleForm.dest_namespace_replace_count =
copyRuleForm.dest_namespace_replace_count
? parseInt(
copyRuleForm.dest_namespace_replace_count.toString(),
10
)
: 0;
if (this.isPushMode) {
copyRuleForm.src_registry = null;
} else {
copyRuleForm.dest_registry = null;
}
let filters: any = copyRuleForm.filters;
// set label filter
if (this.stringForLabelFilter) {
// set stringForLabelFilter
copyRuleForm.filters.forEach(item => {
if (item.type === FilterType.LABEL) {
item.value = this.stringForLabelFilter
.split(',')
.filter(item => item);
}
});
}
// remove the filters which user not set.
for (let i = filters.length - 1; i >= 0; i--) {
if (
!filters[i].value ||
(filters[i].value instanceof Array &&
filters[i].value.length === 0)
) {
copyRuleForm.filters.splice(i, 1);
}
}
if (!this.showChunkOption) {
delete copyRuleForm?.copy_by_chunk;
delete this.ruleForm?.value?.copy_by_chunk;
}
if (this.policyId < 0) {
this.repService.createReplicationRule(copyRuleForm).subscribe(
() => {
this.translateService
.get('REPLICATION.CREATED_SUCCESS')
.subscribe(res => this.errorHandler.info(res));
this.inProgress = false;
this.reload.emit(true);
this.createForm();
this.close();
},
(error: any) => {
this.inProgress = false;
this.inlineAlert.showInlineError(error);
}
);
} else {
this.repService
.updateReplicationRule(this.policyId, this.ruleForm.value)
.subscribe(
() => {
this.translateService
.get('REPLICATION.UPDATED_SUCCESS')
.subscribe(res => this.errorHandler.info(res));
this.inProgress = false;
this.reload.emit(true);
this.close();
},
(error: any) => {
this.inProgress = false;
this.inlineAlert.showInlineError(error);
}
);
}
}
openCreateEditRule(rule?: ReplicationPolicy): void {
this.formReset();
this.copyUpdateForm = clone(this.ruleForm.value);
this.inlineAlert.close();
this.noSelectedEndpoint = true;
this.isRuleNameValid = true;
this.policyId = -1;
this.createEditRuleOpened = true;
this.noEndpointInfo = '';
this.showChunkOption = false;
if (this.targetList.length === 0) {
this.noEndpointInfo = 'REPLICATION.NO_ENDPOINT_INFO';
}
if (rule) {
this.onGoing = true;
this.policyId = +rule.id;
this.headerTitle = 'REPLICATION.EDIT_POLICY_TITLE';
this.repService
.getRegistryInfo(rule.src_registry.id)
.pipe(finalize(() => (this.onGoing = false)))
.subscribe({
next: adapter => {
this.setFilterAndTrigger(adapter);
this.updateRuleFormAndCopyUpdateForm(rule);
},
error: (error: any) => {
// if error, use default(set registry id to 0) filters and triggers
this.repService.getRegistryInfo(0).subscribe(res => {
this.setFilterAndTrigger(res);
this.updateRuleFormAndCopyUpdateForm(rule);
});
this.translateService
.get('REPLICATION.UNREACHABLE_SOURCE_REGISTRY', {
error: errorHandlerFn(error),
})
.subscribe(translatedResponse => {
this.inlineAlert.showInlineError(
translatedResponse
);
});
},
});
} else {
this.onGoing = true;
let registryObs = this.repService.getRegistryInfo(0);
registryObs.pipe(finalize(() => (this.onGoing = false))).subscribe(
adapter => {
this.setFilterAndTrigger(adapter);
this.copyUpdateForm = clone(this.ruleForm.value);
},
(error: any) => {
this.inlineAlert.showInlineError(error);
}
);
this.headerTitle = 'REPLICATION.ADD_POLICY';
this.copyUpdateForm = clone(this.ruleForm.value);
}
}
setFilterAndTrigger(adapter) {
this.supportedFilters = adapter.supported_resource_filters;
this.filters.clear(); // clear before init
this.supportedFilters.forEach(element => {
this.filters.push(this.initFilter(element.type));
});
this.supportedTriggers = adapter.supported_triggers;
this.ruleForm
.get('trigger')
.get('type')
.setValue(this.supportedTriggers[0]);
}
close(): void {
this.createEditRuleOpened = false;
}
confirmCancel(confirmed: boolean) {
this.inlineAlert.close();
this.createForm();
this.close();
}
onCancel(): void {
if (this.hasFormChange()) {
this.inlineAlert.showInlineConfirmation({
message: 'ALERT.FORM_CHANGE_CONFIRMATION',
});
} else {
this.createForm();
this.close();
}
}
goRegistry(): void {
this.goToRegistry.emit();
}
hasChanges(): boolean {
const formValue = clone(this.ruleForm.value);
const initValue = clone(this.copyUpdateForm);
return !isSameObject(formValue, initValue);
}
getFilterArray(rule): Array<any> {
let filtersArray = [];
for (let i = 0; i < this.supportedFilters.length; i++) {
let findTag: boolean = false;
if (rule.filters) {
rule.filters.forEach(ruleItem => {
if (this.supportedFilters[i].type === ruleItem.type) {
filtersArray.push(ruleItem);
findTag = true;
}
});
}
if (!findTag) {
if (this.supportedFilters[i].type === FilterType.LABEL) {
filtersArray.push({
type: this.supportedFilters[i].type,
value: [],
});
} else {
filtersArray.push({
type: this.supportedFilters[i].type,
value: '',
});
}
}
}
return filtersArray;
}
cronInputShouldShowError(): boolean {
if (
this.ruleForm?.get('trigger')?.get('trigger_settings')?.get('cron')
?.touched ||
this.ruleForm?.get('trigger')?.get('trigger_settings')?.get('cron')
?.dirty
) {
return (
this.ruleForm
?.get('trigger')
?.get('trigger_settings')
?.get('cron')?.invalid ||
!cronRegex(
this.ruleForm
.get('trigger')
.get('trigger_settings')
.get('cron').value
)
);
}
return false;
}
stickLabel(name: string) {
if (this.isSelect(name)) {
let arr: string[] = this.stringForLabelFilter.split(',');
arr = arr.filter(item => {
return item !== name;
});
this.stringForLabelFilter = arr.join(',');
} else {
if (this.stringForLabelFilter) {
this.stringForLabelFilter += `,${name}`;
} else {
this.stringForLabelFilter += `${name}`;
}
}
}
// set prefix '0 ', so user can not set item of 'seconds'
inputInvalid(e: any) {
if (this.headerTitle === 'REPLICATION.ADD_POLICY') {
// adding model
if (e && e.target) {
if (
!e.target.value ||
(e.target.value && e.target.value.indexOf(PREFIX)) !== 0
) {
e.target.value = PREFIX;
}
e.target.value = e.target.value.replace(/\s+/g, ' ');
if (e.target.value && e.target.value.split(/\s+/g).length > 6) {
e.target.value = e.target.value.trim();
}
}
}
}
// when trigger type is scheduled, should set cron prefix to '0 '
changeTrigger(e: any) {
if (this.headerTitle === 'REPLICATION.ADD_POLICY') {
// adding model
if (
e &&
e.target &&
e.target.value === this.TRIGGER_TYPES.SCHEDULED
) {
this.ruleForm
.get('trigger')
.get('trigger_settings')
.get('cron')
.setValue(PREFIX);
}
}
}
getAllLabels(): void {
// get all global labels
this.labelService
.ListLabelsResponse({
pageSize: PAGE_SIZE,
page: 1,
scope: 'g',
})
.subscribe(res => {
if (res.headers) {
const xHeader: string = res.headers.get('X-Total-Count');
const totalCount = parseInt(xHeader, 0);
let arr = res.body || [];
if (totalCount <= PAGE_SIZE) {
// already gotten all global labels
if (arr && arr.length) {
arr.forEach(data => {
this.supportedFilterLabels.push({
name: data.name,
color: data.color ? data.color : '#FFFFFF',
scope: 'g',
});
});
}
} else {
// get all the global labels in specified times
const times: number = Math.ceil(totalCount / PAGE_SIZE);
const observableList: Observable<Label[]>[] = [];
for (let i = 2; i <= times; i++) {
observableList.push(
this.labelService.ListLabels({
page: i,
pageSize: PAGE_SIZE,
scope: 'g',
})
);
}
forkJoin(observableList).subscribe(response => {
if (response && response.length) {
response.forEach(item => {
arr = arr.concat(item);
});
arr.forEach(data => {
this.supportedFilterLabels.push({
name: data.name,
color: data.color
? data.color
: '#FFFFFF',
scope: 'g',
});
});
}
});
}
}
});
}
// convert 'MB' or 'KB' to 'KB'
convertToKB(inputValue: number): number {
if (!inputValue) {
// return default value
return -1;
}
if (this.selectedUnit === BandwidthUnit.KB) {
return +inputValue;
}
return inputValue * KB_TO_MB;
}
// convert 'KB' to 'MB' or 'KB'
convertToInputValue(realSpeed: number): number {
if (realSpeed >= KB_TO_MB && realSpeed % KB_TO_MB === 0) {
this.selectedUnit = BandwidthUnit.MB;
return Math.ceil(realSpeed / KB_TO_MB);
} else {
this.selectedUnit = BandwidthUnit.KB;
return realSpeed ? realSpeed : -1;
}
}
checkChunkOption(id: number, info?: RegistryInfo) {
this.showChunkOption = false;
this.ruleForm.get('copy_by_chunk').reset(false);
if (info) {
this.showChunkOption = info.supported_copy_by_chunk;
} else {
if (id) {
this.endpointService.getRegistryInfo({ id }).subscribe(res => {
if (res) {
this.showChunkOption = res.supported_copy_by_chunk;
}
});
}
}
}
isSelect(v: string) {
if (v && this.stringForLabelFilter) {
return this.stringForLabelFilter.indexOf(v) !== -1;
}
return false;
}
}