Merge pull request #7216 from zhoumeina/replication_ng

Create replication rule -- remove project logic and add dynamic trigger logic.
This commit is contained in:
Wenkai Yin 2019-03-26 10:39:05 +08:00 committed by GitHub
commit 690f868d15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 134 additions and 183 deletions

View File

@ -34,13 +34,16 @@ describe("CreateEditEndpointComponent (inline template)", () => {
url: "https://10.117.4.151"
};
let mockAdapter: [Adapter] = [{
let mockAdapters: Adapter[] = [{
type: "Harbor",
description: "test",
supported_resource_types: [
"repository"
supported_resource_filters: [
{
"type": "Name",
"style": "input"
}
],
supported_resource_filters: null
supported_triggers: []
}];
let comp: CreateEditEndpointComponent;
@ -77,7 +80,7 @@ describe("CreateEditEndpointComponent (inline template)", () => {
endpointService = fixture.debugElement.injector.get(EndpointService);
spyAdapter = spyOn(endpointService, "getAdapters").and.returnValue(
of(mockAdapter)
of(mockAdapters)
);
spy = spyOn(endpointService, "getEndpoint").and.returnValue(

View File

@ -31,7 +31,7 @@
<div class="form-group form-group-override">
<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}">
<input type="radio" id="push_base" name="replicationMode" [value]=true [(ngModel)]="isPushMode" (change)="modeChange()" [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>
@ -127,14 +127,12 @@
<!--on trigger-->
<div class="select floatSetPar">
<select id="ruleTrigger" formControlName="kind" (change)="selectTrigger($event)">
<option value="Manual">{{'REPLICATION.MANUAL' | translate}}</option>
<option value="Immediate">{{'REPLICATION.IMMEDIATE' | translate}}</option>
<option value="Scheduled">{{'REPLICATION.SCHEDULE' | translate}}</option>
<option *ngFor="let trigger of supportedTriggers" [value]="trigger">{{trigger }}</option>
</select>
</div>
<!--on push-->
<div formGroupName="schedule_param">
<div [hidden]="!isScheduleOpt">
<div [hidden]="isNotSchedule()">
<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">
@ -145,7 +143,7 @@
</div>
</div>
</div>
<div [hidden]="!isImmediate" class="clr-form-control rule-width">
<div [hidden]="isNotEventBased()" class="clr-form-control rule-width">
<clr-checkbox-wrapper>
<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>

View File

@ -17,7 +17,8 @@ import {
ReplicationRule,
ReplicationJob,
Endpoint,
ReplicationJobItem
ReplicationJobItem,
Adapter
} from "../service/interface";
import { ErrorHandler } from "../error-handler/error-handler";
@ -160,6 +161,38 @@ describe("CreateEditRuleComponent (inline template)", () => {
deletion: false
};
let mockAdapter: Adapter = {
"type": "harbor",
"description": "",
"supported_resource_filters": [
{
"type": "Name",
"style": "input"
},
{
"type": "Version",
"style": "input"
},
{
"type": "Label",
"style": "input"
},
{
"type": "Resource",
"style": "radio",
"values": [
"repository",
"chart"
]
}
],
"supported_triggers": [
"Manual",
"Scheduled",
"EventBased"
]
};
let fixture: ComponentFixture<ReplicationComponent>;
let fixtureCreate: ComponentFixture<CreateEditRuleComponent>;
@ -173,10 +206,11 @@ describe("CreateEditRuleComponent (inline template)", () => {
let spyOneRule: jasmine.Spy;
let spyJobs: jasmine.Spy;
let spyAdapter: jasmine.Spy;
let spyEndpoint: jasmine.Spy;
let config: IServiceConfig = {
replicationBaseEndpoint: "/api/replication/executions/testing",
replicationBaseEndpoint: "/api/replication/testing",
targetBaseEndpoint: "/api/registries/testing"
};
@ -230,6 +264,8 @@ describe("CreateEditRuleComponent (inline template)", () => {
spyJobs = spyOn(replicationService, "getExecutions").and.returnValues(
of(mockJob));
spyAdapter = spyOn(replicationService, "getReplicationAdapter").and.returnValues(
of(mockAdapter));
spyEndpoint = spyOn(endpointService, "getEndpoints").and.returnValues(
of(mockEndpoints)
);
@ -239,6 +275,7 @@ describe("CreateEditRuleComponent (inline template)", () => {
it("Should open creation modal and load endpoints", async(() => {
fixture.detectChanges();
compCreate.initAdapter("harbor");
compCreate.openCreateEditRule();
fixture.whenStable().then(() => {
fixture.detectChanges();
@ -254,6 +291,7 @@ describe("CreateEditRuleComponent (inline template)", () => {
it("Should open modal to edit replication rule", async(() => {
fixture.detectChanges();
compCreate.initAdapter("harbor");
compCreate.openCreateEditRule(mockRule.id);
fixture.whenStable().then(() => {
fixture.detectChanges();

View File

@ -21,7 +21,7 @@ import {
EventEmitter,
Output
} from "@angular/core";
import { Filter, ReplicationRule, Endpoint, Label } from "../service/interface";
import { Filter, ReplicationRule, Endpoint, Label, Adapter } from "../service/interface";
import { Subject, Subscription } from "rxjs";
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { FormArray, FormBuilder, FormGroup, Validators, FormControl } from "@angular/forms";
@ -51,8 +51,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
selectedProjectList: Project[] = [];
isFilterHide = false;
weeklySchedule: boolean;
isScheduleOpt: boolean;
isImmediate = false;
noProjectInfo = "";
noEndpointInfo = "";
isPushMode = true;
@ -60,7 +58,11 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
noSelectedEndpoint = true;
filterCount = 0;
alertClosed = false;
triggerNames: string[] = ["Manual", "Immediate", "Scheduled"];
TRIGGER_TYPES = {
MANUAL: "Manual",
SCHEDULED: "Scheduled",
EVENT_BASED: "EventBased"
};
filterSelect: string[] = ["type", "repository", "tag", "label"];
ruleNameTooltip = "REPLICATION.NAME_TOOLTIP";
headerTitle = "REPLICATION.ADD_POLICY";
@ -71,24 +73,19 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
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;
namespaceList: [any] = [{
id: 1,
name: "namespace1"
}];
confirmSub: Subscription;
ruleForm: FormGroup;
formArrayLabel: FormArray;
copyUpdateForm: ReplicationRule;
cronString: string;
supportedTriggers: string[];
supportedFilters: Filter[];
@Input() projectId: number;
@Input() projectName: string;
@ -98,24 +95,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
@Output() reload = new EventEmitter<boolean>();
@ViewChild(InlineAlertComponent) inlineAlert: InlineAlertComponent;
emptyProject = {
project_id: -1,
name: ""
};
emptyEndpoint = {
id: -1,
credential: {
access_key: "",
access_secret: "",
type: ""
},
description: "",
insecure: false,
name: "",
type: "",
url: "",
};
constructor(
private fb: FormBuilder,
private repService: ReplicationService,
@ -138,20 +117,21 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
};
}
initAdapter(type: string): void {
this.repService.getReplicationAdapter(type).subscribe(adapter => {
this.supportedFilters = adapter.supported_resource_filters;
this.supportedTriggers = adapter.supported_triggers;
});
}
ngOnInit(): void {
if (this.withAdmiral) {
this.filterSelect = ["type", "repository", "tag"];
}
this.endpointService.getEndpoints().subscribe(endPoints => {
this.targetList = endPoints || [];
this.sourceList = endPoints || [];
}, error => {
this.errorHandler.error(error);
});
this.initAdapter("harbor");
this.nameChecker
.pipe(debounceTime(300))
.pipe(distinctUntilChanged())
@ -178,50 +158,22 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
});
}
modeChange(): void {
if (this.isPushMode) {
this.initAdapter("harbor");
}
}
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;
}
this.noSelectedEndpoint = false;
let selectId = this.ruleForm.get('src_registry_id').value;
let selecedTarget: Endpoint = this.sourceList.find(
source => source.id === selectId
);
this.initAdapter(selecedTarget.type);
this.proNameChecker
.pipe(debounceTime(500))
.pipe(distinctUntilChanged())
.subscribe((resp: string) => {
let name = this.ruleForm.controls["projects"].value[0].name;
this.noProjectInfo = "";
this.selectedProjectList = [];
this.proService.listProjects(name, undefined)
.subscribe((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;
}
}, (error: any) => {
this.errorHandler.error(error);
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
this.noSelectedProject = true;
});
});
}
ngOnDestroy(): void {
@ -231,9 +183,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
if (this.nameChecker) {
this.nameChecker.unsubscribe();
}
if (this.proNameChecker) {
this.proNameChecker.unsubscribe();
}
}
get src_namespaces(): FormArray { return this.ruleForm.get('src_namespaces') as FormArray; }
@ -255,7 +204,7 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
dest_registry_id: new FormControl(),
dest_namespace: "",
trigger: this.fb.group({
kind: this.triggerNames[0],
kind: '',
schedule_param: this.fb.group({
cron: ""
})
@ -265,12 +214,24 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
});
}
selectTrigger($event: any): void {
}
isNotSchedule(): boolean {
return this.ruleForm.get("trigger").get("kind").value !== this.TRIGGER_TYPES.SCHEDULED;
}
isNotEventBased(): boolean {
return this.ruleForm.get("trigger").get("kind").value !== this.TRIGGER_TYPES.EVENT_BASED;
}
initForm(): void {
this.ruleForm.reset({
name: "",
description: "",
trigger: {
kind: this.triggerNames[0],
kind: this.supportedTriggers[0],
schedule_param: {
cron: ""
}
@ -278,7 +239,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
deletion: false
});
this.setFilter([]);
this.copyUpdateForm = clone(this.ruleForm.value);
}
@ -345,15 +305,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
}
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;
}
@ -363,8 +314,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.ruleForm.setControl("filters", filterFormArray);
}
initFilter(name: string) {
return this.fb.group({
kind: name,
@ -434,14 +383,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
}
// 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"]) {
@ -455,25 +396,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
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]);
}
}
addNewNamespace(): void {
this.src_namespaces.push(new FormControl());
}
@ -541,26 +463,9 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
}
}
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 {
}
checkRuleName(): void {
@ -634,7 +539,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
// add new Replication rule
this.inProgress = true;
let copyRuleForm: ReplicationRule = this.ruleForm.value;
copyRuleForm.trigger = null;
if (this.isPushMode) {
copyRuleForm.src_registry_id = null;
} else {
@ -688,8 +592,6 @@ export class CreateEditRuleComponent implements OnInit, OnDestroy {
this.deletedLabelCount = 0;
this.weeklySchedule = false;
this.isScheduleOpt = false;
this.isImmediate = false;
this.policyId = -1;
this.createEditRuleOpened = true;
this.filterLabelInfo = [];

View File

@ -93,13 +93,16 @@ describe("EndpointComponent (inline template)", () => {
}
];
let mockAdapter: [Adapter] = [{
let mockAdapters: Adapter[] = [{
type: "Harbor",
description: "test",
supported_resource_types: [
"repository"
supported_resource_filters: [
{
"type": "Name",
"style": "input"
}
],
supported_resource_filters: null
supported_triggers: []
}];
let comp: EndpointComponent;
@ -143,7 +146,7 @@ describe("EndpointComponent (inline template)", () => {
);
spyAdapter = spyOn(endpointService, "getAdapters").and.returnValue(
of(mockAdapter)
of(mockAdapters)
);
spyOnRules = spyOn(

View File

@ -81,9 +81,8 @@ export const DefaultServiceConfig: IServiceConfig = {
repositoryBaseEndpoint: "/api/repositories",
logBaseEndpoint: "/api/logs",
targetBaseEndpoint: "/api/registries",
replicationBaseEndpoint: "/api/replication/executions",
replicationBaseEndpoint: "/api/replication",
replicationRuleEndpoint: "/api/replication/policies",
adapterEndpoint: "api/replication/adapters",
vulnerabilityScanningBaseEndpoint: "/api/repositories",
projectPolicyEndpoint: "/api/projects/configs",
projectBaseEndpoint: "/api/projects",

View File

@ -138,7 +138,7 @@ describe('Replication Component (inline template)', () => {
let config: IServiceConfig = {
replicationRuleEndpoint: '/api/policies/replication/testing',
replicationBaseEndpoint: '/api/replication/executions/testing'
replicationBaseEndpoint: '/api/replication/testing'
};
beforeEach(async(() => {

View File

@ -54,11 +54,6 @@ export interface IServiceConfig {
*/
replicationBaseEndpoint?: string;
/**
* The base endpoint of the service used to handle the adapters.
*/
adapterEndpoint?: string;
/**
* The base endpoint of the service used to handle the replication rules.
* Replication rule related endpoints will be built based on this endpoint.

View File

@ -87,15 +87,17 @@ export interface Endpoint extends Base {
url: string;
}
export interface Filter {
type: string;
style: string;
values ?: string[];
}
export interface Adapter extends Base {
type: string;
description: string;
supported_resource_types: [
string
];
supported_resource_filters: [
string
];
supported_triggers: string [];
supported_resource_filters: Filter [];
}
/**

View File

@ -6,7 +6,7 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
describe('JobLogService', () => {
const mockConfig: IServiceConfig = {
replicationBaseEndpoint: "/api/replication/executions/testing",
replicationBaseEndpoint: "/api/replication/testing",
scanJobEndpoint: "/api/jobs/scan/testing"
};
@ -33,7 +33,7 @@ describe('JobLogService', () => {
it('should be initialized', inject([JobLogDefaultService], (service: JobLogService) => {
expect(service).toBeTruthy();
expect(config.replicationBaseEndpoint).toEqual("/api/replication/executions/testing");
expect(config.replicationBaseEndpoint).toEqual("/api/replication/testing");
expect(config.scanJobEndpoint).toEqual("/api/jobs/scan/testing");
}));
});

View File

@ -49,7 +49,7 @@ export class JobLogDefaultService extends JobLogService {
super();
this._replicationJobBaseUrl = config.replicationBaseEndpoint
? config.replicationBaseEndpoint
: "/api/replication/executions";
: "/api/replication";
this._scanningJobBaseUrl = config.scanJobEndpoint
? config.scanJobEndpoint
: "/api/jobs/scan";

View File

@ -7,7 +7,7 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
describe('ReplicationService', () => {
const mockConfig: IServiceConfig = {
replicationRuleEndpoint: "/api/policies/replication/testing",
replicationBaseEndpoint: "/api/replication/executions/testing"
replicationBaseEndpoint: "/api/replication/testing"
};
let config: IServiceConfig;
@ -38,6 +38,6 @@ describe('ReplicationService', () => {
it('should inject the right config', () => {
expect(config).toBeTruthy();
expect(config.replicationRuleEndpoint).toEqual("/api/policies/replication/testing");
expect(config.replicationBaseEndpoint).toEqual("/api/replication/executions/testing");
expect(config.replicationBaseEndpoint).toEqual("/api/replication/testing");
});
});

View File

@ -10,7 +10,8 @@ import {
ReplicationJob,
ReplicationRule,
ReplicationJobItem,
ReplicationTasks
ReplicationTasks,
Adapter
} from "./interface";
import { RequestQueryParams } from "./RequestQueryParams";
import { map, catchError } from "rxjs/operators";
@ -140,6 +141,9 @@ export abstract class ReplicationService {
ruleId: number | string
): Observable<any>;
abstract getReplicationAdapter(type: string): Observable<Adapter>;
/**
* Get the jobs for the specified replication rule.
* Set query parameters through 'queryParams', support:
@ -202,7 +206,7 @@ export class ReplicationDefaultService extends ReplicationService {
: "/api/replication/policies";
this._replicateUrl = config.replicationBaseEndpoint
? config.replicationBaseEndpoint
: "/api/replication/executions";
: "/api/replication";
}
// Private methods
@ -218,6 +222,14 @@ export class ReplicationDefaultService extends ReplicationService {
);
}
public getReplicationAdapter(type): Observable<Adapter> {
let requestUrl: string = `${this._replicateUrl}/adapters/${type}`;
return this.http
.get(requestUrl)
.pipe(map(response => response.json() as Adapter)
, catchError(error => observableThrowError(error)));
}
public getJobBaseUrl() {
return this._replicateUrl;
}

View File

@ -63,9 +63,8 @@ const uiLibConfig: IServiceConfig = {
repositoryBaseEndpoint: "/api/repositories",
logBaseEndpoint: "/api/logs",
targetBaseEndpoint: "/api/registries",
replicationBaseEndpoint: "/api/replication/executions",
replicationBaseEndpoint: "/api/replication",
replicationRuleEndpoint: "/api/replication/policies",
adapterEndpoint: "api/replication/adapters",
vulnerabilityScanningBaseEndpoint: "/api/repositories",
projectPolicyEndpoint: "/api/projects/configs",
projectBaseEndpoint: "/api/projects",