Merge pull request #7177 from zhoumeina/replication_ng

Merge create replication policy code
This commit is contained in:
Wenkai Yin 2019-03-20 14:29:12 +08:00 committed by GitHub
commit 761004f98d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 270 additions and 604 deletions

View File

@ -21,12 +21,12 @@ import { IServiceConfig, SERVICE_CONFIG } from "../service.config";
describe("CreateEditEndpointComponent (inline template)", () => {
let mockData: Endpoint = {
id: 1,
endpoint: "https://10.117.4.151",
url: "https://10.117.4.151",
name: "target_01",
username: "admin",
password: "",
insecure: false,
type: 0
type: "harbor"
};
let comp: CreateEditEndpointComponent;

View File

@ -98,12 +98,12 @@ export class CreateEditEndpointComponent
initEndpoint(): Endpoint {
return {
endpoint: "",
url: "",
name: "",
username: "",
password: "",
insecure: false,
type: 0
type: ""
};
}

View File

@ -2,8 +2,8 @@
<h3 class="modal-title">{{headerTitle | translate}}</h3>
<hbr-inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></hbr-inline-alert>
<div class="modal-body modal-body-height">
<clr-alert [hidden]='!deletedLabelCount' [clrAlertType]="'alert-warning'" [clrAlertSizeSmall]="true"
[clrAlertClosable]="false" [(clrAlertClosed)]="alertClosed">
<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">
@ -15,10 +15,9 @@
<section class="form-block">
<div class="form-group form-group-override">
<label class="form-group-label-override required">{{'REPLICATION.NAME' | translate}}</label>
<label aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
[class.invalid]='(ruleForm.controls.name.touched && ruleForm.controls.name.invalid) || !isRuleNameValid'>
<input type="text" id="ruleName" pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" class="inputWidth" required
maxlength="255" formControlName="name" #ruleName (keyup)='checkRuleName()' autocomplete="off">
<label aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='(ruleForm.controls.name.touched && ruleForm.controls.name.invalid) || !isRuleNameValid'>
<input type="text" id="ruleName" pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" class="inputWidth" required maxlength="255" formControlName="name"
#ruleName (keyup)='checkRuleName()' autocomplete="off">
<span class="tooltip-content">{{ruleNameTooltip | translate}}</span>
</label>
<span class="spinner spinner-inline spinner-pos" [hidden]="!inNameChecking"></span>
@ -28,29 +27,42 @@
<label class="form-group-label-override">{{'REPLICATION.DESCRIPTION' | translate}}</label>
<textarea type="text" id="ruleDescription" class="inputWidth" row=3 formControlName="description"></textarea>
</div>
<!--Projects-->
<!-- replication mode -->
<div class="form-group form-group-override">
<label class="form-group-label-override required">{{'REPLICATION.SOURCE_PROJECT' | translate}}</label>
<div formArrayName="projects">
<div class="projectInput inputWidth" *ngFor="let project of projects.controls; let i= index"
[formGroupName]="i" (mouseleave)="leaveInput()">
<label aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
[class.invalid]='noProjectInfo'>
<input *ngIf="!projectId" formControlName="name" type="text" class="inputWidth" value="name" required
pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" (keyup)='handleValidation()' (focus)="focusClear($event)"
autocomplete="off">
<input *ngIf="projectId" formControlName="name" type="text" class="inputWidth" value="name" readonly>
<span class="tooltip-content">{{noProjectInfo | translate}}</span>
</label>
<div class="selectBox inputWidth" [style.display]="selectedProjectList.length ? 'block' : 'none'">
<ul>
<li *ngFor="let project of selectedProjectList" (click)="selectedProjectName(project?.name)">{{project?.name}}</li>
</ul>
</div>
</div>
<label class="form-group-label-override">{{'REPLICATION.REPLI_MODE' | translate}}</label>
<div class="radio" style="display:inherit;">
<input type="radio" id="push_base" name="replicationMode" [value]=true [(ngModel)]="isPushMode" [ngModelOptions]="{standalone: true}">
<label for="push_base">Push-based</label>
<input type="radio" id="pull_base" name="replicationMode" [value]=false [(ngModel)]="isPushMode" [ngModelOptions]="{standalone: true}">
<label for="pull_base">Pull-based</label>
</div>
</div>
<!--source registry-->
<div *ngIf="!isPushMode" class="form-group form-group-override">
<label class="form-group-label-override required">{{'REPLICATION.SOURCE_REGISTRY' | translate}}</label>
<div class="form-select">
<div class="select endpointSelect pull-left">
<select id="src_registry_id" (change)="sourceChange($event)" formControlName="src_registry_id">
<option *ngFor="let source of sourceList" [ngValue]="source.id">{{source.name}}-{{source.url}}</option>
</select>
</div>
</div>
<label *ngIf="noEndpointInfo.length != 0" class="colorRed alertLabel">{{noEndpointInfo | translate}}</label>
<span class="alertLabel goLink" *ngIf="noEndpointInfo.length != 0" (click)="goRegistry()">{{'REPLICATION.ENDPOINTS' | translate}}</span>
</div>
<!--source namespace -->
<div class="form-group form-group-override">
<label class="form-group-label-override required">{{'REPLICATION.SOURCE_NAMESPACES' | translate}}</label>
<div formArrayName="src_namespaces">
<div class="width-315" *ngFor="let src_namespace of src_namespaces.controls; let i=index">
<div class="select endpointSelect pull-left">
<input type="text" [formControlName]="i" pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" class="inputWidth" required maxlength="255">
</div>
<clr-icon shape="times-circle" class="is-solid" (click)="deleteNamespace(i)"></clr-icon>
</div>
</div>
<clr-icon id="addSourceNamespace" shape="plus-circle" class="is-solid mr-t-15" (click)="addNewNamespace()"></clr-icon>
</div>
<!--images/Filter-->
<div class="form-group form-group-override">
<label class="form-group-label-override">{{'REPLICATION.SOURCE_IMAGES_FILTER' | translate}}</label>
@ -58,46 +70,52 @@
<div class="filterSelect" *ngFor="let filter of filters.controls; let i=index">
<div [formGroupName]="i">
<div class="select floatSetPar">
<select formControlName="kind" #selectedValue (change)="filterChange($event, selectedValue.value)" id="{{i}}"
name="{{filterListData[i]?.name}}">
<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]='(filter.value.dirty || filter.value.touched) && filter.value.invalid'>
<label aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='(filter.value.dirty || filter.value.touched) && filter.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>
<clr-icon shape="angle"></clr-icon>
</div>
<clr-icon shape="warning-standard" class="is-solid is-warning warning-icon" size="14" [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]="projectId" [selectedLabelInfo]="filterLabelInfo" [isOpen]="filterListData[i].isOpen"
(selectedLabels)="selectedLabelList($event, i)" (closePanelEvent)="filterListData[i].isOpen = false"></hbr-filter-label>
<hbr-filter-label [projectId]="projectId" [selectedLabelInfo]="filterLabelInfo" [isOpen]="filterListData[i].isOpen" (selectedLabels)="selectedLabelList($event, i)"
(closePanelEvent)="filterListData[i].isOpen = false"></hbr-filter-label>
</div>
</div>
</div>
</div>
<clr-icon id="add-label-list" shape="plus-circle" class="is-solid plus-position" [hidden]="isFilterHide" (click)="addNewFilter()"></clr-icon>
<clr-icon id="add-label-list" shape="plus-circle" class="is-solid mr-t-11" [hidden]="isFilterHide" (click)="addNewFilter()"></clr-icon>
</div>
<!--Targets-->
<div class="form-group form-group-override">
<label class="form-group-label-override required">{{'DESTINATION.ENDPOINT' | translate}}</label>
<div formArrayName="targets" class="form-select">
<div class="select endpointSelect pull-left" *ngFor="let target of targets.controls; let i= index"
[formGroupName]="i">
<select id="ruleTarget" (change)="targetChange($event)" formControlName="id">
<option *ngFor="let target of targetList" value="{{target.id}}">{{target.name}}-{{target.endpoint}}</option>
<!--destination registry-->
<div *ngIf="isPushMode" class="form-group form-group-override">
<label class="form-group-label-override required">{{'REPLICATION.DEST_REGISTRY' | translate}}</label>
<div class="form-select">
<div class="select endpointSelect pull-left">
<select id="dest_registry_id" (change)="targetChange($event)" formControlName="dest_registry_id">
<option *ngFor="let target of targetList" [ngValue]="target.id">{{target.name}}-{{target.url}}</option>
</select>
</div>
</div>
<label *ngIf="noEndpointInfo.length != 0" class="colorRed alertLabel">{{noEndpointInfo | translate}}</label>
<span class="alertLabel goLink" *ngIf="noEndpointInfo.length != 0" (click)="goRegistry()">{{'REPLICATION.ENDPOINTS'
| translate}}</span>
<span class="alertLabel goLink" *ngIf="noEndpointInfo.length != 0" (click)="goRegistry()">{{'REPLICATION.ENDPOINTS' | translate}}</span>
</div>
<!--destination namespaces -->
<div class="form-group form-group-override">
<label class="form-group-label-override">{{'REPLICATION.DEST_NAMESPACE' | translate}}</label>
<div class="form-select">
<div class="select endpointSelect pull-left">
<input formControlName="dest_namespace" type="text" id="dest_namespace" pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" class="inputWidth"
maxlength="255">
</div>
</div>
</div>
<!--Trigger-->
@ -113,44 +131,24 @@
</select>
</div>
<!--on push-->
<div formGroupName="schedule_param" class="schedule-style">
<div class="select floatSet" [hidden]="!isScheduleOpt">
<select name="scheduleType" formControlName="type" (change)="selectSchedule($event)">
<option value="Daily">{{'REPLICATION.DAILY' | translate}}</option>
<option value="Weekly">{{'REPLICATION.WEEKLY' | translate}}</option>
</select>
<div formGroupName="schedule_param">
<div [hidden]="!isScheduleOpt">
<label>Cron String</label>
<label for="targetCron" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-right">
<input type="text" name=targetCron id="targetCron" required class="form-control cron-input" formControlName="cron">
<span class="tooltip-content">
{{'TOOLTIP.CRON_REQUIRED' | translate }}
</span>
</label>
</div>
<!--weekly-->
<span [hidden]="!weeklySchedule || !isScheduleOpt">on &nbsp;</span>
<div [hidden]="!weeklySchedule || !isScheduleOpt" class="select floatSet weekday-width">
<select name="scheduleDay" formControlName="weekday">
<option value="1">{{'WEEKLY.MONDAY' | translate}}</option>
<option value="2">{{'WEEKLY.TUESDAY' | translate}}</option>
<option value="3">{{'WEEKLY.WEDNESDAY' | translate}}</option>
<option value="4">{{'WEEKLY.THURSDAY' | translate}}</option>
<option value="5">{{'WEEKLY.FRIDAY' | translate}}</option>
<option value="6">{{'WEEKLY.SATURDAY' | translate}}</option>
<option value="7">{{'WEEKLY.SUNDAY' | translate}}</option>
</select>
</div>
<!--daily/time-->
<span [hidden]="!isScheduleOpt">at &nbsp;</span>
<input [hidden]="!isScheduleOpt" type="time" formControlName="offtime" required value="08:00" />
</div>
</div>
<div [hidden]="!isImmediate" class="clr-form-control rule-width">
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [checked]="false" id="ruleDeletion" formControlName="replicate_deletion" class="clr-checkbox">
<input type="checkbox" clrCheckbox [checked]="false" id="ruleDeletion" formControlName="deletion" class="clr-checkbox">
<label for="ruleDeletion" class="clr-control-label">{{'REPLICATION.DELETE_REMOTE_IMAGES' | translate}}</label>
</clr-checkbox-wrapper>
</div>
<div class="clr-form-control rule-width">
<clr-checkbox-wrapper>
<input type="checkbox" clrCheckbox [checked]="true" id="ruleExit" formControlName="replicate_existing_image_now"
class="clr-checkbox">
<label for="ruleExit" class="clr-control-label">{{'REPLICATION.REPLICATE_IMMEDIATE' | translate}}</label>
</clr-checkbox-wrapper>
</div>
</div>
<div class="loading-center">
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
@ -159,9 +157,7 @@
</form>
</div>
<div class="modal-footer">
<button type="button" id="ruleBtnCancel" class="btn btn-outline" [disabled]="this.inProgress" (click)="onCancel()">{{
'BUTTON.CANCEL' | translate }}</button>
<button type="submit" id="ruleBtnOk" class="btn btn-primary" (click)="onSubmit()" [disabled]="!ruleForm.valid || !isValid || !hasFormChange()">{{
'BUTTON.SAVE' | translate }}</button>
<button type="button" id="ruleBtnCancel" class="btn btn-outline" [disabled]="this.inProgress" (click)="onCancel()">{{ 'BUTTON.CANCEL' | translate }}</button>
<button type="submit" id="ruleBtnOk" class="btn btn-primary" (click)="onSubmit()" [disabled]="!isValid || !hasFormChange()">{{ 'BUTTON.SAVE' | translate }}</button>
</div>
</clr-modal>

View File

@ -146,12 +146,19 @@ h4 {
}
.form-group-override {
padding-left: 170px !important;
padding-left: 200px;
}
.form-group>label:first-child {
font-size: 14px;
width: 6.5rem;
.form-group {
>label:first-child{
font-size: 14px;
width: 7rem;
}
.radio {
label {
margin-right:10px;
}
}
}
.form-select {
@ -199,7 +206,7 @@ clr-modal {
margin-left: -15px;
}
.plus-position {
.mr-t-11{
margin-top: 11px;
}
@ -214,4 +221,14 @@ clr-modal {
.loading-center {
display: block;
text-align: center;
}
.width-315 {
display: flex;
align-items: center;
width:315px;
}
.mr-t-15 {
margin-top: 15px;
}

View File

@ -48,48 +48,14 @@ describe("CreateEditRuleComponent (inline template)", () => {
id: 1,
name: "sync_01",
description: "",
projects: [
{
project_id: 1,
owner_id: 0,
name: "project_01",
creation_time: "",
deleted: 0,
owner_name: "",
togglable: false,
update_time: "",
current_user_role_id: 0,
repo_count: 0,
has_project_admin_role: false,
is_member: false,
role_name: "",
metadata: {
public: "",
enable_content_trust: "",
prevent_vul: "",
severity: "",
auto_scan: ""
}
}
],
targets: [
{
id: 1,
endpoint: "https://10.117.4.151",
name: "target_01",
username: "admin",
password: "",
insecure: false,
type: 0
}
],
src_registry_id: 2,
src_namespaces: ["name1", "name2"],
trigger: {
kind: "Manual",
schedule_param: null
},
filters: [],
replicate_existing_image_now: false,
replicate_deletion: false
deletion: false
}
];
let mockJobs: ReplicationJobItem[] = [
@ -127,39 +93,55 @@ describe("CreateEditRuleComponent (inline template)", () => {
let mockEndpoints: Endpoint[] = [
{
id: 1,
endpoint: "https://10.117.4.151",
name: "target_01",
username: "admin",
password: "",
credential: {
access_key: "admin",
access_secret: "",
type: "basic"
},
description: "test",
insecure: false,
type: 0
name: "target_01",
type: "Harbor",
url: "https://10.117.4.151"
},
{
id: 2,
endpoint: "https://10.117.5.142",
name: "target_02",
username: "AAA",
password: "",
credential: {
access_key: "AAA",
access_secret: "",
type: "basic"
},
description: "test",
insecure: false,
type: 0
name: "target_02",
type: "Harbor",
url: "https://10.117.5.142"
},
{
id: 3,
endpoint: "https://101.1.11.111",
name: "target_03",
username: "admin",
password: "",
credential: {
access_key: "admin",
access_secret: "",
type: "basic"
},
description: "test",
insecure: false,
type: 0
name: "target_03",
type: "Harbor",
url: "https://101.1.11.111"
},
{
id: 4,
endpoint: "http://4.4.4.4",
name: "target_04",
username: "",
password: "",
credential: {
access_key: "admin",
access_secret: "",
type: "basic"
},
description: "test",
insecure: true,
type: 0
name: "target_04",
type: "Harbor",
url: "https://4.4.4.4"
}
];
@ -167,48 +149,14 @@ describe("CreateEditRuleComponent (inline template)", () => {
id: 1,
name: "sync_01",
description: "",
projects: [
{
project_id: 1,
owner_id: 0,
name: "project_01",
creation_time: "",
deleted: 0,
owner_name: "",
togglable: false,
update_time: "",
current_user_role_id: 0,
repo_count: 0,
has_project_admin_role: false,
is_member: false,
role_name: "",
metadata: {
public: "",
enable_content_trust: "",
prevent_vul: "",
severity: "",
auto_scan: ""
}
}
],
targets: [
{
id: 1,
endpoint: "https://10.117.4.151",
name: "target_01",
username: "admin",
password: "",
insecure: false,
type: 0
}
],
src_namespaces: ["namespace1", "namespace2"],
src_registry_id: 10,
trigger: {
kind: "Manual",
schedule_param: null
},
filters: [],
replicate_existing_image_now: false,
replicate_deletion: false
deletion: false
};
let fixture: ComponentFixture<ReplicationComponent>;

View File

@ -24,7 +24,7 @@ import {
import { Filter, ReplicationRule, Endpoint, Label } from "../service/interface";
import { Subject , Subscription } from "rxjs";
import {debounceTime, distinctUntilChanged} from "rxjs/operators";
import { FormArray, FormBuilder, FormGroup, Validators } from "@angular/forms";
import { FormArray, FormBuilder, FormGroup, Validators, FormControl } from "@angular/forms";
import { clone, compareValue, isEmptyObject, toPromise } from "../utils";
import { InlineAlertComponent } from "../inline-alert/inline-alert.component";
import { ReplicationService } from "../service/replication.service";
@ -45,6 +45,7 @@ const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS;
})
export class CreateEditRuleComponent implements OnInit, OnDestroy {
_localTime: Date = new Date();
sourceList: Endpoint[] = [];
targetList: Endpoint[] = [];
projectList: Project[] = [];
selectedProjectList: Project[] = [];
@ -54,22 +55,13 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
isImmediate = false;
noProjectInfo = "";
noEndpointInfo = "";
isPushMode = true;
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"];
filterSelect: string[] = ["type", "repository", "tag", "label"];
ruleNameTooltip = "REPLICATION.NAME_TOOLTIP";
headerTitle = "REPLICATION.ADD_POLICY";
@ -87,10 +79,16 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
deletedLabelCount = 0;
deletedLabelInfo: string;
namespaceList: [any] = [{
id: 1,
name: "namespace1"
}];
confirmSub: Subscription;
ruleForm: FormGroup;
formArrayLabel: FormArray;
copyUpdateForm: ReplicationRule;
cronString: string;
@Input() projectId: number;
@Input() projectName: string;
@ -138,23 +136,16 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
ngOnInit(): void {
if (this.withAdmiral) {
this.filterSelect = ["repository", "tag"];
this.filterSelect = ["type", "repository", "tag"];
}
toPromise<Endpoint[]>(this.endpointService.getEndpoints())
.then(targets => {
this.targetList = targets || [];
.then(endPoints => {
this.targetList = endPoints || [];
this.sourceList = endPoints || [];
})
.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
.pipe(debounceTime(300))
.pipe(distinctUntilChanged())
@ -182,39 +173,19 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
}
});
}
this.proNameChecker
.pipe(debounceTime(500))
.pipe(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;
});
});
sourceChange($event): void {
if ($event && $event.target) {
if ($event.target["value"] === "-1") {
this.noSelectedEndpoint = true;
return;
}
let selecedTarget: Endpoint = this.sourceList.find(
source => source.id === +$event.target["value"]
);
this.noSelectedEndpoint = false;
}
}
ngOnDestroy(): void {
@ -228,11 +199,11 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.proNameChecker.unsubscribe();
}
}
get src_namespaces(): FormArray { return this.ruleForm.get('src_namespaces') as FormArray; }
get isValid() {
return !(
!this.isRuleNameValid ||
this.noSelectedProject ||
this.noSelectedEndpoint ||
this.inProgress
);
@ -240,23 +211,21 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
createForm() {
this.formArrayLabel = this.fb.array([]);
this.ruleForm = this.fb.group({
name: ["", Validators.required],
description: "",
projects: this.fb.array([]),
targets: this.fb.array([]),
src_registry_id: new FormControl(),
src_namespaces: new FormArray([new FormControl('')], Validators.required),
dest_registry_id: new FormControl(),
dest_namespace: "",
trigger: this.fb.group({
kind: this.triggerNames[0],
schedule_param: this.fb.group({
type: this.scheduleNames[0],
weekday: 1,
offtime: "08:00"
cron: ""
})
}),
filters: this.fb.array([]),
replicate_existing_image_now: true,
replicate_deletion: false
deletion: false
});
}
@ -267,33 +236,27 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
trigger: {
kind: this.triggerNames[0],
schedule_param: {
type: this.scheduleNames[0],
weekday: 1,
offtime: "08:00"
cron: ""
}
},
replicate_existing_image_now: true,
replicate_deletion: false
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,
src_namespaces: rule.src_namespaces,
dest_namespace: rule.dest_namespace,
trigger: rule.trigger,
replicate_existing_image_now: rule.replicate_existing_image_now,
replicate_deletion: rule.replicate_deletion
deletion: rule.deletion
});
this.setProject(rule.projects);
this.noSelectedProject = false;
this.setTarget(rule.targets);
this.noSelectedEndpoint = false;
if (rule.filters) {
@ -315,7 +278,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
let delLabel = '';
filterLabels.forEach((data: any) => {
if (data.kind === this.filterSelect[2]) {
if (data.kind === this.filterSelect[3]) {
if (!data.value.deleted) {
count++;
this.filterLabelInfo.push(data.value);
@ -337,7 +300,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
if (delLabel || count) {
let len = filterLabels.length;
for (let i = 0; i < len; i++) {
let lab = filterLabels.find(data => data.kind === this.filterSelect[2]);
let lab = filterLabels.find(data => data.kind === this.filterSelect[3]);
if (lab) { filterLabels.splice(filterLabels.indexOf(lab), 1); }
}
filterLabels.push({ kind: 'label', value: count + ' labels' });
@ -364,19 +327,12 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
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]
value: ''
});
}
@ -398,7 +354,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
// if before select, $event is label
if (!this.withAdmiral && name === this.filterSelect[2] && data.name === value) {
if (!this.withAdmiral && name === this.filterSelect[3] && data.name === value) {
this.labelInputVal = controlArray.controls[index].get('value').value.split(' ')[0];
data.isOpen = false;
controlArray.controls[index].get('value').setValue('');
@ -421,7 +377,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
// when input value is label, then open label panel
openLabelList(labelTag: string, indexId: number, $event: any) {
if (!this.withAdmiral && labelTag === this.filterSelect[2]) {
if (!this.withAdmiral && labelTag === this.filterSelect[3]) {
this.filterListData.forEach((data, index) => {
if (index === indexId) {
data.isOpen = true;
@ -438,10 +394,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.noSelectedEndpoint = true;
return;
}
let selecedTarget: Endpoint = this.targetList.find(
target => target.id === +$event.target["value"]
);
this.setTarget([selecedTarget]);
this.noSelectedEndpoint = false;
}
}
@ -486,6 +438,14 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
}
addNewNamespace(): void {
this.src_namespaces.push(new FormControl());
}
deleteNamespace(index: number): void {
this.src_namespaces.removeAt(index);
}
addNewFilter(): void {
const controlArray = <FormArray>this.ruleForm.get('filters');
if (this.filterCount === 0) {
@ -513,7 +473,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
if (this.filterCount >= this.filterSelect.length) {
this.isFilterHide = true;
}
if (controlArray.controls[this.filterCount - 1].get('kind').value === this.filterSelect[2] && this.labelInputVal) {
if (controlArray.controls[this.filterCount - 1].get('kind').value === this.filterSelect[3] && this.labelInputVal) {
controlArray.controls[this.filterCount - 1].get('value').setValue(this.labelInputVal + ' labels');
}
@ -565,23 +525,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
// 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 {
@ -634,54 +577,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
});
}
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]);
@ -703,7 +598,12 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
// add new Replication rule
this.inProgress = true;
let copyRuleForm: ReplicationRule = this.ruleForm.value;
copyRuleForm.trigger = this.setTriggerVaule(copyRuleForm.trigger);
copyRuleForm.trigger = null;
if (this.isPushMode) {
copyRuleForm.src_registry_id = null;
} else {
copyRuleForm.dest_registry_id = null;
}
// rewrite key name of label when filer contain labels.
if (copyRuleForm.filters) { this.setFilterLabelVal(copyRuleForm.filters); }
@ -787,12 +687,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
});
} 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);
}
}
@ -820,71 +714,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
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);

View File

@ -24,51 +24,51 @@ describe("EndpointComponent (inline template)", () => {
let mockData: Endpoint[] = [
{
id: 1,
endpoint: "https://10.117.4.151",
url: "https://10.117.4.151",
name: "target_01",
username: "admin",
password: "",
insecure: true,
type: 0
type: "Harbor"
},
{
id: 2,
endpoint: "https://10.117.5.142",
url: "https://10.117.5.142",
name: "target_02",
username: "AAA",
password: "",
insecure: false,
type: 0
type: "Harbor"
},
{
id: 3,
endpoint: "https://101.1.11.111",
url: "https://101.1.11.111",
name: "target_03",
username: "admin",
password: "",
insecure: false,
type: 0
type: "Harbor"
},
{
id: 4,
endpoint: "http://4.4.4.4",
url: "http://4.4.4.4",
name: "target_04",
username: "",
password: "",
insecure: false,
type: 0
type: "Harbor"
}
];
let mockOne: Endpoint[] = [
{
id: 1,
endpoint: "https://10.117.4.151",
url: "https://10.117.4.151",
name: "target_01",
username: "admin",
password: "",
insecure: false,
type: 0
type: "Harbor"
}
];

View File

@ -77,12 +77,12 @@ export class EndpointComponent implements OnInit, OnDestroy {
get initEndpoint(): Endpoint {
return {
endpoint: "",
url: "",
name: "",
username: "",
password: "",
insecure: false,
type: 0
type: ""
};
}

View File

@ -20,83 +20,25 @@ describe('ListReplicationRuleComponent (inline template)', () => {
let mockRules: ReplicationRule[] = [
{
"id": 1,
"projects": [{
"project_id": 33,
"owner_id": 1,
"name": "aeas",
"deleted": 0,
"togglable": false,
"current_user_role_id": 0,
"repo_count": 0,
"metadata": {
"public": false,
"enable_content_trust": "",
"prevent_vul": "",
"severity": "",
"auto_scan": ""},
"owner_name": "",
"creation_time": null,
"update_time": null,
"has_project_admin_role": true,
"is_member": true,
"role_name": ""
}],
"targets": [{
"endpoint": "",
"id": 0,
"insecure": false,
"name": "khans3",
"username": "",
"password": "",
"type": 0,
}],
"name": "sync_01",
"description": "",
"filters": null,
"trigger": {"kind": "Manual", "schedule_param": null},
"error_job_count": 2,
"replicate_deletion": false,
"replicate_existing_image_now": false,
"deletion": false,
"src_namespaces": ["name1", "name2"],
"src_registry_id": 3
},
{
"id": 2,
"projects": [{
"project_id": 33,
"owner_id": 1,
"name": "aeas",
"deleted": 0,
"togglable": false,
"current_user_role_id": 0,
"repo_count": 0,
"metadata": {
"public": false,
"enable_content_trust": "",
"prevent_vul": "",
"severity": "",
"auto_scan": ""},
"owner_name": "",
"creation_time": null,
"update_time": null,
"has_project_admin_role": true,
"is_member": true,
"role_name": ""
}],
"targets": [{
"endpoint": "",
"id": 0,
"insecure": false,
"name": "khans3",
"username": "",
"password": "",
"type": 0,
}],
"name": "sync_02",
"description": "",
"filters": null,
"trigger": {"kind": "Manual", "schedule_param": null},
"error_job_count": 2,
"replicate_deletion": false,
"replicate_existing_image_now": false,
"deletion": false,
"src_namespaces": ["name1", "name2"],
"dest_registry_id": 3
},
];

View File

@ -29,83 +29,25 @@ describe('Replication Component (inline template)', () => {
let mockRules: ReplicationRule[] = [
{
"id": 1,
"projects": [{
"project_id": 33,
"owner_id": 1,
"name": "aeas",
"deleted": 0,
"togglable": false,
"current_user_role_id": 0,
"repo_count": 0,
"metadata": {
"public": false,
"enable_content_trust": "",
"prevent_vul": "",
"severity": "",
"auto_scan": ""},
"owner_name": "",
"creation_time": null,
"update_time": null,
"has_project_admin_role": true,
"is_member": true,
"role_name": ""
}],
"targets": [{
"id": 1,
"endpoint": "https://10.117.4.151",
"name": "target_01",
"username": "admin",
"password": "",
"insecure": false,
"type": 0
}],
"name": "sync_01",
"description": "",
"filters": null,
"trigger": {"kind": "Manual", "schedule_param": null},
"error_job_count": 2,
"replicate_deletion": false,
"replicate_existing_image_now": false,
"deletion": false,
"src_registry_id": 3,
"src_namespaces": ["name1"]
},
{
"id": 2,
"projects": [{
"project_id": 33,
"owner_id": 1,
"name": "aeas",
"deleted": 0,
"togglable": false,
"current_user_role_id": 0,
"repo_count": 0,
"metadata": {
"public": false,
"enable_content_trust": "",
"prevent_vul": "",
"severity": "",
"auto_scan": ""},
"owner_name": "",
"creation_time": null,
"update_time": null,
"has_project_admin_role": true,
"is_member": true,
"role_name": ""
}],
"targets": [{
"id": 1,
"endpoint": "https://10.117.4.151",
"name": "target_01",
"username": "admin",
"password": "",
"insecure": false,
"type": 0
}],
"name": "sync_02",
"description": "",
"filters": null,
"trigger": {"kind": "Manual", "schedule_param": null},
"error_job_count": 2,
"replicate_deletion": false,
"replicate_existing_image_now": false,
"deletion": false,
"dest_registry_id": 5,
"src_namespaces": ["name1"]
}
];
@ -142,47 +84,24 @@ describe('Replication Component (inline template)', () => {
let mockEndpoints: Endpoint[] = [
{
"id": 1,
"endpoint": "https://10.117.4.151",
"url": "https://10.117.4.151",
"name": "target_01",
"username": "admin",
"password": "",
"insecure": false,
"type": 0
"type": "Harbor"
},
{
"id": 2,
"endpoint": "https://10.117.5.142",
"url": "https://10.117.5.142",
"name": "target_02",
"username": "AAA",
"password": "",
"insecure": false,
"type": 0
"type": "Harbor"
},
];
// let mockProjects: Project[] = [
// { "project_id": 1,
// "owner_id": 0,
// "name": 'project_01',
// "creation_time": '',
// "deleted": 0,
// "owner_name": '',
// "togglable": false,
// "update_time": '',
// "current_user_role_id": 0,
// "repo_count": 0,
// "has_project_admin_role": false,
// "is_member": false,
// "role_name": '',
// "metadata": {
// "public": '',
// "enable_content_trust": '',
// "prevent_vul": '',
// "severity": '',
// "auto_scan": '',
// }
// }];
let mockJob: ReplicationJob = {
metadata: {xTotalCount: 3},
data: mockJobs

View File

@ -75,12 +75,12 @@ export interface Tag extends Base {
* extends {Base}
*/
export interface Endpoint extends Base {
endpoint: string;
url: string;
name: string;
username?: string;
password?: string;
insecure: boolean;
type: number;
type: string;
[key: string]: any;
}
@ -97,24 +97,13 @@ export interface ReplicationRule extends Base {
id?: number;
name: string;
description: string;
projects: Project[];
targets: Endpoint[];
trigger: Trigger;
filters: Filter[];
replicate_existing_image_now?: boolean;
replicate_deletion?: boolean;
// id?: number;
// name: string;
// description: string;
// src_registry_id: number;
// src_namespaces: [];
// dest_registry_id: number;
// dest_namespace: string;
// trigger: Trigger;
// filter: Filter[];
// deletion: boolean;
// override: boolean;
// enabled: boolean;
deletion?: boolean;
src_registry_id?: number;
dest_registry_id?: number;
src_namespaces: string [];
dest_namespace?: string;
}
export class Filter {

View File

@ -216,7 +216,8 @@ export class ReplicationDefaultService extends ReplicationService {
rule != null &&
rule.name !== undefined &&
rule.name.trim() !== "" &&
rule.targets.length !== 0
rule.src_namespaces && rule.src_namespaces.length > 0 &&
(!!rule.dest_registry_id || !! rule.src_registry_id)
);
}

View File

@ -447,7 +447,12 @@
"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."
"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.",
"REPLI_MODE": "Replication mode",
"SOURCE_REGISTRY":"Source registry",
"SOURCE_NAMESPACES":"Source namespaces",
"DEST_REGISTRY":"Destination registry",
"DEST_NAMESPACE":"Destination namespace"
},
"DESTINATION": {
"NEW_ENDPOINT": "New Endpoint",

View File

@ -448,7 +448,12 @@
"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."
"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.",
"REPLI_MODE": "Replication mode",
"SOURCE_REGISTRY":"Source registry",
"SOURCE_NAMESPACES":"Source namespaces",
"DEST_REGISTRY":"Destination registry",
"DEST_NAMESPACE":"Destination namespace"
},
"DESTINATION": {
"NEW_ENDPOINT": "Nuevo Endpoint",

View File

@ -429,7 +429,12 @@
"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."
"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.",
"REPLI_MODE": "Replication mode",
"SOURCE_REGISTRY":"Source registry",
"SOURCE_NAMESPACES":"Source namespaces",
"DEST_REGISTRY":"Destination registry",
"DEST_NAMESPACE":"Destination namespace"
},
"DESTINATION": {
"NEW_ENDPOINT": "Nouveau Point Final",

View File

@ -447,7 +447,12 @@
"NAME_TOOLTIP": "nome da regra de replicação deve conter ao menos 2 caracteres sendo caracteres minusculos, números e ._- e devem iniciar com letras e números.",
"DELETED_LABEL_INFO": "Removidas as label(s) '{{param}}' referenciadas no filtro, clique 'SALVAR' para atualizar o filtro e habilitar essa regra.",
"ACKNOWLEDGE": "Reconhecer",
"RULE_DISABLED": "Essa regra foi desabilitada pois uma label usada no seu filtro foi removida. \n Edite a regra e atualize seu filtro para habilitá-la."
"RULE_DISABLED": "Essa regra foi desabilitada pois uma label usada no seu filtro foi removida. \n Edite a regra e atualize seu filtro para habilitá-la.",
"REPLI_MODE": "Replication mode",
"SOURCE_REGISTRY":"Source registry",
"SOURCE_NAMESPACES":"Source namespaces",
"DEST_REGISTRY":"Destination registry",
"DEST_NAMESPACE":"Destination namespace"
},
"DESTINATION": {
"NEW_ENDPOINT": "Novo Endpoint",

View File

@ -448,7 +448,12 @@
"NAME_TOOLTIP": "项目名称由小写字符、数字和._-组成且至少2个字符并以字符或者数字开头。",
"DELETED_LABEL_INFO": "过滤项有被删除的标签 {{param}} , 点击保存按钮更新过滤项使规则可用。",
"ACKNOWLEDGE": "确认",
"RULE_DISABLED": "这个规则因为过滤选项中的标签被删除已经不能用了,更新过滤项以便重新启用规则。"
"RULE_DISABLED": "这个规则因为过滤选项中的标签被删除已经不能用了,更新过滤项以便重新启用规则。",
"REPLI_MODE": "复制模式",
"SOURCE_REGISTRY":"源Registry",
"SOURCE_NAMESPACES":"源Namespace",
"DEST_REGISTRY":"目的Registry",
"DEST_NAMESPACE":"目的Namespace"
},
"DESTINATION": {
"NEW_ENDPOINT": "新建目标",