diff --git a/src/ui_ng/lib/src/replication/replication.component.ts b/src/ui_ng/lib/src/replication/replication.component.ts
index 9e6ce673f..8427c5ecf 100644
--- a/src/ui_ng/lib/src/replication/replication.component.ts
+++ b/src/ui_ng/lib/src/replication/replication.component.ts
@@ -88,6 +88,8 @@ export class ReplicationComponent implements OnInit, OnDestroy {
@Input() readonly: boolean;
@Output() redirect = new EventEmitter
();
+ @Output() openCreateRule = new EventEmitter();
+ @Output() openEdit = new EventEmitter();
search: SearchOption = new SearchOption();
@@ -111,8 +113,8 @@ export class ReplicationComponent implements OnInit, OnDestroy {
@ViewChild(ListReplicationRuleComponent)
listReplicationRule: ListReplicationRuleComponent;
- @ViewChild(CreateEditRuleComponent)
- createEditPolicyComponent: CreateEditRuleComponent;
+/* @ViewChild(CreateEditRuleComponent)
+ createEditPolicyComponent: CreateEditRuleComponent;*/
@ViewChild("replicationLogViewer")
replicationLogViewer: JobLogViewerComponent;
@@ -134,9 +136,9 @@ export class ReplicationComponent implements OnInit, OnDestroy {
private translateService: TranslateService) {
}
- public get creationAvailable(): boolean {
+ /*public get creationAvailable(): boolean {
return !this.readonly && this.projectId ? true : false;
- }
+ }*/
public get showPaginationIndex(): boolean {
return this.totalCount > 0;
@@ -146,6 +148,7 @@ export class ReplicationComponent implements OnInit, OnDestroy {
this.currentRuleStatus = this.ruleStatus[0];
this.currentJobStatus = this.jobStatus[0];
this.currentJobSearchOption = 0;
+ console.log('readonly', this.readonly);
}
ngOnDestroy() {
@@ -155,7 +158,8 @@ export class ReplicationComponent implements OnInit, OnDestroy {
}
openModal(): void {
- this.createEditPolicyComponent.openCreateEditRule(true);
+ this.openCreateRule.emit();
+ // this.createEditPolicyComponent.openCreateEditRule(true);
}
openEditRule(rule: ReplicationRule) {
@@ -164,7 +168,8 @@ export class ReplicationComponent implements OnInit, OnDestroy {
if (rule.enabled === 1) {
editable = false;
}
- this.createEditPolicyComponent.openCreateEditRule(editable, rule.id);
+ this.openEdit.emit(rule.id);
+ // this.createEditPolicyComponent.openCreateEditRule(editable, rule.id);
}
}
diff --git a/src/ui_ng/package.json b/src/ui_ng/package.json
index f8f2b596a..b91e5fd80 100644
--- a/src/ui_ng/package.json
+++ b/src/ui_ng/package.json
@@ -31,7 +31,7 @@
"clarity-icons": "^0.9.8",
"clarity-ui": "^0.9.8",
"core-js": "^2.4.1",
- "harbor-ui": "0.4.91",
+ "harbor-ui": "^0.5.9-test-31",
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0",
diff --git a/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html b/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html
index efe767bf4..7ce4a0cc0 100644
--- a/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html
+++ b/src/ui_ng/src/app/base/harbor-shell/harbor-shell.component.html
@@ -18,6 +18,7 @@
diff --git a/src/ui_ng/src/app/harbor-routing.module.ts b/src/ui_ng/src/app/harbor-routing.module.ts
index ed9f876ba..57711c35b 100644
--- a/src/ui_ng/src/app/harbor-routing.module.ts
+++ b/src/ui_ng/src/app/harbor-routing.module.ts
@@ -50,6 +50,7 @@ import { LeavingConfigRouteDeactivate } from './shared/route/leaving-config-deac
import { MemberGuard } from './shared/route/member-guard-activate.service';
import { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component';
+import { ReplicationRuleComponent} from "./replication/replication-rule/replication-rule.component";
const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
@@ -80,23 +81,23 @@ const harborRoutes: Routes = [
},
{
path: 'replications',
- component: ReplicationManagementComponent,
+ component: TotalReplicationPageComponent,
canActivate: [SystemAdminGuard],
canActivateChild: [SystemAdminGuard],
- children: [
- {
- path: 'rules',
- component: TotalReplicationPageComponent
- },
- {
- path: 'endpoints',
- component: DestinationPageComponent
- },
- {
- path: '**',
- redirectTo: 'endpoints'
- }
- ]
+ },
+ {
+ path: 'replications/:id/rule',
+ component: ReplicationRuleComponent,
+ canActivate: [SystemAdminGuard],
+ canActivateChild: [SystemAdminGuard],
+
+ },
+ {
+ path: 'replications/new-rule',
+ component: ReplicationRuleComponent,
+ canActivate: [SystemAdminGuard],
+ canActivateChild: [SystemAdminGuard],
+
},
{
path: 'tags/:id/:repo',
@@ -146,6 +147,12 @@ const harborRoutes: Routes = [
component: ConfigurationComponent,
canActivate: [SystemAdminGuard],
canDeactivate: [LeavingConfigRouteDeactivate]
+ },
+ {
+ path: 'registry',
+ component: DestinationPageComponent,
+ canActivate: [SystemAdminGuard],
+ canActivateChild: [SystemAdminGuard],
}
]
},
diff --git a/src/ui_ng/src/app/project/project-detail/project-detail.component.html b/src/ui_ng/src/app/project/project-detail/project-detail.component.html
index 317f03b8a..e4d291458 100644
--- a/src/ui_ng/src/app/project/project-detail/project-detail.component.html
+++ b/src/ui_ng/src/app/project/project-detail/project-detail.component.html
@@ -13,7 +13,7 @@
{{'PROJECT_DETAIL.LOGS' | translate}}
-
+
{{'PROJECT_DETAIL.REPLICATION' | translate}}
diff --git a/src/ui_ng/src/app/project/project-detail/project-detail.component.ts b/src/ui_ng/src/app/project/project-detail/project-detail.component.ts
index 942aedcb0..eb7703297 100644
--- a/src/ui_ng/src/app/project/project-detail/project-detail.component.ts
+++ b/src/ui_ng/src/app/project/project-detail/project-detail.component.ts
@@ -50,7 +50,12 @@ export class ProjectDetailComponent {
public get isSystemAdmin(): boolean {
let account = this.sessionService.getCurrentUser();
- return account != null && account.has_admin_role > 0;
+ return account && account.has_admin_role > 0;
+ }
+
+ public get isSProjectAdmin(): boolean {
+ let account = this.sessionService.projectMembers;
+ return account && account[0].role_name === 'projectAdmin';
}
public get isSessionValid(): boolean {
diff --git a/src/ui_ng/src/app/replication/destination/destination-page.component.html b/src/ui_ng/src/app/replication/destination/destination-page.component.html
index 3d0c138d2..b47208702 100644
--- a/src/ui_ng/src/app/replication/destination/destination-page.component.html
+++ b/src/ui_ng/src/app/replication/destination/destination-page.component.html
@@ -1,3 +1,4 @@
+{{'REPLICATION.ENDPOINTS' | translate}}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/replication/replication-page.component.html b/src/ui_ng/src/app/replication/replication-page.component.html
index 667be81c6..b2ebb01ba 100644
--- a/src/ui_ng/src/app/replication/replication-page.component.html
+++ b/src/ui_ng/src/app/replication/replication-page.component.html
@@ -1,3 +1,3 @@
-
+
\ No newline at end of file
diff --git a/src/ui_ng/src/app/replication/replication-rule/replication-rule.component.ts b/src/ui_ng/src/app/replication/replication-rule/replication-rule.component.ts
new file mode 100644
index 000000000..2d705b6c6
--- /dev/null
+++ b/src/ui_ng/src/app/replication/replication-rule/replication-rule.component.ts
@@ -0,0 +1,528 @@
+import {Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef, AfterViewInit} from '@angular/core';
+import {ProjectService} from '../../project/project.service';
+import {Project} from '../../project/project';
+import {compareValue, toPromise} from 'harbor-ui/src/utils';
+import {ActivatedRoute, Router} from '@angular/router';
+import {FormArray, FormBuilder, FormGroup, Validators} from "@angular/forms";
+import {ReplicationRuleServie} from "./replication-rule.service";
+import {MessageHandlerService} from "../../shared/message-handler/message-handler.service";
+import {Target, Filter, ReplicationRule} from "./replication-rule";
+import {ConfirmationDialogService} from "../../shared/confirmation-dialog/confirmation-dialog.service";
+import { ConfirmationTargets, ConfirmationState } from '../../shared/shared.const';
+import {Subscription} from "rxjs/Subscription";
+import {ConfirmationMessage} from "../../shared/confirmation-dialog/confirmation-message";
+import {Subject} from "rxjs/Subject";
+
+
+const ONE_HOUR_SECONDS: number = 3600;
+const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS;
+
+@Component ({
+ selector: 'repliction-rule',
+ templateUrl: 'replication-rule.html',
+ styleUrls: ['replication-rule.css']
+
+})
+export class ReplicationRuleComponent implements OnInit, AfterViewInit, OnDestroy {
+ timerHandler: any;
+ _localTime: Date = new Date();
+ policyId: number;
+ projectList: Project[] = [];
+ targetList: Target[] = [];
+ isFilterHide: boolean = false;
+ weeklySchedule: boolean;
+ isScheduleOpt: boolean;
+ filterCount: number = 0;
+ triggerNames: string[] = ['immediate', 'schedule', 'manual'];
+ scheduleNames: string[] = ['daily', 'weekly'];
+ weekly: string[] = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
+ filterSelect: string[] = ['repository', 'tag'];
+ ruleNameTooltip: string = 'TOOLTIP.EMPTY';
+
+ filterListData: {[key: string]: any}[] = [];
+ inProgress: boolean = false;
+ inNameChecking: boolean = false;
+ isBackReplication: boolean = false;
+ isRuleNameExist: boolean = false;
+ nameChecker: Subject = new Subject();
+
+ confirmSub: Subscription;
+ ruleForm: FormGroup;
+ copyUpdateForm: ReplicationRule;
+
+ baseFilterData(name: string, option: string[], state: boolean) {
+ return {
+ name: name,
+ options: option,
+ state: state,
+ isValid: true
+ };
+ }
+
+ constructor(public projectService: ProjectService,
+ private router: Router,
+ private fb: FormBuilder,
+ private repService: ReplicationRuleServie,
+ private route: ActivatedRoute,
+ private msgHandler: MessageHandlerService,
+ private confirmService: ConfirmationDialogService,
+ public ref: ChangeDetectorRef) {
+ this.createForm();
+
+ Promise.all([this.repService.getEndpoints(), this.repService.listProjects()])
+ .then(res => {
+ if (!res[0].length || !res[1].length) {
+ this.msgHandler.error('should have project and target first');
+ this.router.navigate(['/harbor/replications']);
+ };
+ if (res[0].length && res[1].length) {
+ this.projectList = res[1];
+ this.setProject([this.projectList[0]]);
+ this.targetList = res[0];
+ this.setTarget([this.targetList[0]]);
+ }
+ });
+ }
+
+ ngOnInit(): void {
+ this.policyId = +this.route.snapshot.params['id'];
+ if (this.policyId) {
+ this.repService.getReplicationRule(this.policyId)
+ .then((response) => {
+ this.copyUpdateForm = Object.assign({}, response);
+ this.updateForm(response);
+ }).catch(error => {
+ this.msgHandler.handleError(error);
+ });
+ }
+
+ this.nameChecker.debounceTime(500).distinctUntilChanged().subscribe((ruleName: string) => {
+ this.isRuleNameExist = false;
+ this.inNameChecking = true;
+ toPromise(this.repService.getReplicationRules(0, ruleName))
+ .then(response => {
+ if (response.some(rule => rule.name === ruleName)) {
+ this.ruleNameTooltip = 'TOOLTIP.RULE_USER_EXISTING';
+ this.isRuleNameExist = true;
+ }
+ this.inNameChecking = false;
+ }).catch(() => {
+ this.inNameChecking = false;
+ });
+ });
+ this.confirmSub = this.confirmService.confirmationConfirm$.subscribe(confirmation => {
+ if (confirmation &&
+ confirmation.state === ConfirmationState.CONFIRMED) {
+ if (confirmation.source === ConfirmationTargets.CONFIG) {
+ if (this.policyId) {
+ this.updateForm(this.copyUpdateForm);
+ } else {
+ this.initFom();
+ }
+ if (this.isBackReplication) {
+ this.router.navigate(['/harbor/replications']);
+ }
+ }
+ }
+ });
+ }
+
+ get hasFormChange() {
+ if (this.copyUpdateForm) {
+ return !compareValue(this.copyUpdateForm, this.ruleForm.value);
+ }
+ return this.ruleForm.touched && this.ruleForm.dirty;
+ }
+
+ ngAfterViewInit(): void {
+ }
+
+ ngOnDestroy(): void {
+ if (this.confirmSub) {
+ this.confirmSub.unsubscribe();
+ }
+ if (this.nameChecker) {
+ this.nameChecker.unsubscribe();
+ }
+ }
+
+ createForm() {
+ 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: true
+ });
+ }
+
+ 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.setTarget(rule.targets);
+ if (rule.filters) {
+ this.setFilter(rule.filters);
+ this.updateFilter(rule.filters);
+ }
+ }
+
+ initFom(): 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: true
+ });
+ this.setProject([this.projectList[0]]);
+ this.setTarget([this.targetList[0]]);
+ this.setFilter([]);
+
+ this.isFilterHide = false;
+ this.filterListData = [];
+ this.isScheduleOpt = false;
+ this.weeklySchedule = false;
+ this.isRuleNameExist = true;
+ this.ruleNameTooltip = 'TOOLTIP.EMPTY';
+ }
+
+
+ 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: Target[]) {
+ 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,
+ pattern: ['', Validators.required]
+ });
+ }
+
+ filterChange($event: any) {
+ if ($event && $event.target['value']) {
+ let id: number = $event.target.id;
+ let name: string = $event.target.name;
+ let value: string = $event.target['value'];
+
+ 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);
+ }
+ });
+ }
+ }
+
+ projectChange($event: any) {
+ if ($event && $event.target && event.target['value']) {
+ let selecedProject: Project = this.projectList.find(project => project.project_id === +$event.target['value']);
+ this.setProject([selecedProject]);
+ }
+ }
+
+ targetChange($event: any) {
+ if ($event && $event.target && event.target['value']) {
+ let selecedTarget: Target = this.targetList.find(target => target.id === +$event.target['value']);
+ this.setTarget([selecedTarget]);
+ }
+ }
+
+ addNewFilter(): void {
+ if (this.filterCount === 0) {
+ this.filterListData.push(this.baseFilterData(this.filterSelect[0], this.filterSelect.slice(), true));
+ this.filters.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));
+ this.filters.push(this.initFilter(nameArr[0]));
+ }
+ this.filterCount += 1;
+ if (this.filterCount >= this.filterSelect.length) {
+ this.isFilterHide = true;
+ }
+ }
+
+ // delete a filter
+ deleteFilter(i: number): void {
+ if (i || 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 = this.ruleForm.controls['filters'];
+ control.removeAt(i);
+ }
+ }
+
+ selectTrigger($event: any): void {
+ if ($event && $event.target && $event.target['value']) {
+ if ($event.target['value'] === this.triggerNames[1]) {
+ this.isScheduleOpt = true;
+ } else {
+ this.isScheduleOpt = false;
+ }
+ }
+ }
+
+ // Replication Schedule select exchange
+ selectSchedule($event: any): void {
+ if ($event && $event.target && $event.target['value']) {
+ switch ($event.target['value']) {
+ case this.scheduleNames[1]:
+ this.weeklySchedule = true;
+ break;
+ case this.scheduleNames[0]:
+/* this.dailySchedule = true;*/
+ this.weeklySchedule = false;
+ break;
+ }
+ }
+ }
+
+ checkRuleName(): void {
+ let ruleName: string = this.ruleForm.controls['name'].value;
+ if (ruleName) {
+ this.nameChecker.next(ruleName);
+ } else {
+ this.ruleNameTooltip = 'TOOLTIP.EMPTY';
+ }
+ }
+
+ 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;
+ }
+ }
+
+ updateTrigger(trigger: any) {
+ if (trigger['schedule_param']) {
+ this.isScheduleOpt = true;
+ 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 {
+ 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;
+ }
+ }
+
+
+ onSubmit() {
+ // add new Replication rule
+ let copyRuleForm: ReplicationRule = this.ruleForm.value;
+ copyRuleForm.trigger = this.setTriggerVaule(copyRuleForm.trigger);
+ if (!this.policyId) {
+ this.repService.createReplicationRule(copyRuleForm)
+ .then(() => {
+ this.msgHandler.showSuccess('REPLICATION.CREATED_SUCCESS');
+ this.inProgress = false;
+ setTimeout(() => {
+ this.router.navigate(['/harbor/replications']);
+ }, 2000);
+
+ }).catch((error: any) => {
+ this.inProgress = false;
+ this.msgHandler.handleError(error);
+ });
+ } else {
+ this.repService.updateReplicationRule(this.policyId, this.ruleForm.value)
+ .then(() => {
+ this.msgHandler.showSuccess('REPLICATION.CREATED_SUCCESS');
+ this.inProgress = false;
+ setTimeout(() => {
+ this.router.navigate(['/harbor/replications']);
+ }, 2000);
+
+ }).catch((error: any) => {
+ this.inProgress = false;
+ this.msgHandler.handleError(error);
+ });
+ }
+ this.inProgress = true;
+ }
+
+ onCancel(): void {
+
+ console.log(this.ruleForm.valid, this.isRuleNameExist , !this.hasFormChange)
+ if (this.ruleForm.dirty) {
+ let msg = new ConfirmationMessage(
+ 'CONFIG.CONFIRM_TITLE',
+ 'CONFIG.CONFIRM_SUMMARY',
+ '',
+ null,
+ ConfirmationTargets.CONFIG
+ );
+
+ this.confirmService.openComfirmDialog(msg);
+ }
+ }
+ // UTC time
+ public getOfftime(daily_time: any): string {
+
+ let timeOffset: number = 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;
+ }
+
+ backReplication(): void {
+ this.isBackReplication = true;
+ if (this.ruleForm.dirty) {
+ this.onCancel();
+ } else {
+ this.router.navigate(['/harbor/replications']);
+ }
+ }
+}
diff --git a/src/ui_ng/src/app/replication/replication-rule/replication-rule.css b/src/ui_ng/src/app/replication/replication-rule/replication-rule.css
new file mode 100644
index 000000000..a785e2ab0
--- /dev/null
+++ b/src/ui_ng/src/app/replication/replication-rule/replication-rule.css
@@ -0,0 +1,30 @@
+/**
+ * Created by pengf on 9/28/2017.
+ */
+
+.select{
+ width: 186px;
+}
+.select .optionMore{
+ background-color: #bfbaba;
+ height: 1.6em;
+ font-size: 1.2em;
+ cursor: pointer;
+ text-align: center;
+}
+.hideFilter{ display: none;}
+h4{
+ color: #666;
+}
+label:first-child {
+ font-size: 15px;
+ left: -10px !important;
+}
+.endpointSelect{ width: 290px;}
+.filterSelect{width: 320px;}
+.filterSelect label{width: 160px;}
+.filterSelect label input{width: 100%;}
+.cursor{cursor: pointer;}
+.padLeft0{padding-left: 0;}
+.floatSet {display: inline-block; float: left; width: 120px;margin-right: 10px;}
+.form-group{ min-height: 36px;}
\ No newline at end of file
diff --git a/src/ui_ng/src/app/replication/replication-rule/replication-rule.html b/src/ui_ng/src/app/replication/replication-rule/replication-rule.html
new file mode 100644
index 000000000..41a7d72cb
--- /dev/null
+++ b/src/ui_ng/src/app/replication/replication-rule/replication-rule.html
@@ -0,0 +1,122 @@
+
\ No newline at end of file
diff --git a/src/ui_ng/src/app/replication/replication-rule/replication-rule.service.ts b/src/ui_ng/src/app/replication/replication-rule/replication-rule.service.ts
new file mode 100644
index 000000000..fbe5a1c4c
--- /dev/null
+++ b/src/ui_ng/src/app/replication/replication-rule/replication-rule.service.ts
@@ -0,0 +1,75 @@
+/**
+ * Created by pengf on 12/5/2017.
+ */
+
+import {Injectable} from "@angular/core";
+import {Http, RequestOptions, Headers, URLSearchParams} from "@angular/http";
+import {Observable} from "rxjs/Observable";
+import {ReplicationRule, Target} from "./replication-rule";
+import {HTTP_GET_OPTIONS, HTTP_JSON_OPTIONS} from "../../shared/shared.utils";
+import {Project} from "../../project/project";
+
+@Injectable()
+export class ReplicationRuleServie {
+ headers = new Headers({'Content-type': 'application/json'});
+ options = new RequestOptions({'headers': this.headers});
+ baseurl = '/api/policies/replication';
+ targetUrl= '/api/targets';
+
+ constructor(private http: Http) {}
+
+ public createReplicationRule(replicationRule: ReplicationRule): Observable | Promise | any {
+ /*if (!this._isValidRule(replicationRule)) {
+ return Promise.reject('Bad argument');
+ }*/
+
+ return this.http.post(this.baseurl, JSON.stringify(replicationRule), this.options).toPromise()
+ .then(response => response)
+ .catch(error => Promise.reject(error));
+ }
+
+ public getReplicationRules(projectId?: number | string, ruleName?: string): Promise | ReplicationRule[] {
+ let queryParams = new URLSearchParams();
+ if (projectId) {
+ queryParams.set('project_id', '' + projectId);
+ }
+
+ if (ruleName) {
+ queryParams.set('name', ruleName);
+ }
+
+ return this.http.get(this.baseurl, {search: queryParams}).toPromise()
+ .then(response => response.json() as ReplicationRule[])
+ .catch(error => Promise.reject(error));
+ }
+
+ public getReplicationRule(policyId: number): Promise {
+ let url: string = `${this.baseurl}/${policyId}`;
+ return this.http.get(url, HTTP_GET_OPTIONS).toPromise()
+ .then(response => response.json() as ReplicationRule)
+ .catch(error => Promise.reject(error));
+ }
+
+
+ public getEndpoints(): Promise | Target[] {
+ return this.http
+ .get(this.targetUrl)
+ .toPromise()
+ .then(response => response.json())
+ .catch(error => Promise.reject(error));
+ }
+
+ public listProjects(): Promise | Project[] {
+ return this.http.get(`/api/projects`, HTTP_GET_OPTIONS).toPromise()
+ .then(response => response.json())
+ .catch(error => Promise.reject(error));
+ }
+
+ public updateReplicationRule(id: number, rep: {[key: string]: any | any[] }): Observable | Promise | any {
+ let url: string = `${this.baseurl}/${id}`;
+ return this.http.put(url, JSON.stringify(rep), HTTP_JSON_OPTIONS).toPromise()
+ .then(response => response)
+ .catch(error => Promise.reject(error));
+ }
+
+}
diff --git a/src/ui_ng/src/app/replication/replication-rule/replication-rule.ts b/src/ui_ng/src/app/replication/replication-rule/replication-rule.ts
new file mode 100644
index 000000000..0167be8d4
--- /dev/null
+++ b/src/ui_ng/src/app/replication/replication-rule/replication-rule.ts
@@ -0,0 +1,48 @@
+import {Project} from "../../project/project";
+/**
+ * Created by pengf on 12/7/2017.
+ */
+
+export class Target {
+ id: 0;
+ endpoint: 'string';
+ name: 'string';
+ username: 'string';
+ password: 'string';
+ type: 0;
+ insecure: true;
+ creation_time: 'string';
+ update_time: 'string';
+}
+
+export class Filter {
+ kind: string;
+ pattern: string;
+ constructor(kind: string, pattern: string) {
+ this.kind = kind;
+ this.pattern = pattern;
+ }
+}
+
+export class Trigger {
+ kind: string;
+ schedule_param: any | {
+ [key: string]: any | any[];
+ };
+ constructor(kind: string, param: any | { [key: string]: any | any[]; }) {
+ this.kind = kind;
+ this.schedule_param = param;
+ }
+}
+
+export interface ReplicationRule {
+ name: string;
+ description: string;
+ projects: Project[];
+ targets: Target[] ;
+ trigger: Trigger ;
+ filters: Filter[] ;
+ replicate_existing_image_now?: boolean;
+ replicate_deletion?: boolean;
+}
+
diff --git a/src/ui_ng/src/app/replication/replication.module.ts b/src/ui_ng/src/app/replication/replication.module.ts
index cd20fada9..b6da1c6f4 100644
--- a/src/ui_ng/src/app/replication/replication.module.ts
+++ b/src/ui_ng/src/app/replication/replication.module.ts
@@ -20,22 +20,29 @@ import { TotalReplicationPageComponent } from './total-replication/total-replica
import { DestinationPageComponent } from './destination/destination-page.component';
import { SharedModule } from '../shared/shared.module';
+import {ReplicationRuleComponent} from "./replication-rule/replication-rule.component";
+import {ReactiveFormsModule} from "@angular/forms";
+import {ReplicationRuleServie} from "./replication-rule/replication-rule.service";
@NgModule({
imports: [
SharedModule,
- RouterModule
+ RouterModule,
+ ReactiveFormsModule
],
declarations: [
ReplicationPageComponent,
ReplicationManagementComponent,
TotalReplicationPageComponent,
- DestinationPageComponent
+ DestinationPageComponent,
+ ReplicationRuleComponent,
],
exports: [
ReplicationPageComponent,
DestinationPageComponent,
- TotalReplicationPageComponent
- ]
+ TotalReplicationPageComponent,
+ ReplicationRuleComponent,
+ ],
+ providers: [ReplicationRuleServie]
})
export class ReplicationModule { }
\ No newline at end of file
diff --git a/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.html b/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.html
index b93ec2ca3..df2a97bfd 100644
--- a/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.html
+++ b/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.html
@@ -1,3 +1,4 @@
+{{'REPLICATION.REPLICATION_RULE' | translate}}
-
+
\ No newline at end of file
diff --git a/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.ts b/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.ts
index 9716c07d7..5da340b1d 100644
--- a/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.ts
+++ b/src/ui_ng/src/app/replication/total-replication/total-replication-page.component.ts
@@ -14,7 +14,7 @@
import { Component } from '@angular/core';
import {Router,ActivatedRoute} from "@angular/router";
-import {ReplicationRule} from "harbor-ui";
+import {ReplicationRule} from "../replication-rule/replication-rule";
@Component({
selector: 'total-replication',
@@ -26,7 +26,15 @@ export class TotalReplicationPageComponent {
private activeRoute: ActivatedRoute){}
customRedirect(rule: ReplicationRule): void {
if (rule) {
- this.router.navigate(['../../projects', rule.project_id, "replications"], { relativeTo: this.activeRoute });
+ this.router.navigate(['../projects', rule.projects[0].project_id, "replications"], { relativeTo: this.activeRoute });
}
}
-}
\ No newline at end of file
+
+ openEditPage(id: number): void {
+ this.router.navigate([id, 'rule'], { relativeTo: this.activeRoute });
+ }
+
+ openCreatePage(): void {
+ this.router.navigate(['new-rule'], { relativeTo: this.activeRoute });
+ }
+}
diff --git a/src/ui_ng/src/app/replication/total-replication/total-replication.component.css b/src/ui_ng/src/app/replication/total-replication/total-replication.component.css
index 23904eade..8d38880dd 100644
--- a/src/ui_ng/src/app/replication/total-replication/total-replication.component.css
+++ b/src/ui_ng/src/app/replication/total-replication/total-replication.component.css
@@ -2,4 +2,7 @@
padding-right: 16px;
margin-top: 36px;
margin-bottom: 11px;
-}
\ No newline at end of file
+}
+.custom-h2 {
+ margin-top: 0px !important;
+}
diff --git a/src/ui_ng/src/app/shared/route/system-admin-activate.service.ts b/src/ui_ng/src/app/shared/route/system-admin-activate.service.ts
index 5b1e9300b..c38c391ad 100644
--- a/src/ui_ng/src/app/shared/route/system-admin-activate.service.ts
+++ b/src/ui_ng/src/app/shared/route/system-admin-activate.service.ts
@@ -33,12 +33,13 @@ export class SystemAdminGuard implements CanActivate, CanActivateChild {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise | boolean {
return new Promise((resolve, reject) => {
let user = this.authService.getCurrentUser();
+ let projectMem = this.authService.projectMembers;
if (!user) {
this.authService.retrieveUser()
.then(() => {
//updated user
user = this.authService.getCurrentUser();
- if (user.has_admin_role > 0) {
+ if (user.has_admin_role > 0 || projectMem[0].role_name === 'projectAdmin') {
return resolve(true);
} else {
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
@@ -60,7 +61,7 @@ export class SystemAdminGuard implements CanActivate, CanActivateChild {
}
});
} else {
- if (user.has_admin_role > 0) {
+ if (user.has_admin_role > 0 || projectMem[0].role_name === 'projectAdmin') {
return resolve(true);
} else {
this.router.navigate([CommonRoutes.HARBOR_DEFAULT]);
diff --git a/src/ui_ng/src/app/shared/shared.utils.ts b/src/ui_ng/src/app/shared/shared.utils.ts
index 6f04d8542..d38f3848a 100644
--- a/src/ui_ng/src/app/shared/shared.utils.ts
+++ b/src/ui_ng/src/app/shared/shared.utils.ts
@@ -15,6 +15,7 @@ import { NgForm } from '@angular/forms';
import { httpStatusCode, AlertType } from './shared.const';
import { MessageService } from '../global-message/message.service';
import { Comparator, State } from 'clarity-angular';
+import {RequestOptions, Headers} from "@angular/http";
/**
* To handle the error message body
@@ -155,6 +156,27 @@ export class CustomComparator implements Comparator {
}
}
+export const HTTP_JSON_OPTIONS: RequestOptions = new RequestOptions({
+ headers: new Headers({
+ "Content-Type": 'application/json',
+ "Accept": 'application/json',
+ })
+});
+export const HTTP_GET_OPTIONS: RequestOptions = new RequestOptions({
+ headers: new Headers({
+ "Content-Type": 'application/json',
+ "Accept": 'application/json',
+ "Cache-Control": 'no-cache',
+ "Pragma": 'no-cache'
+ })
+});
+
+export const HTTP_FORM_OPTIONS: RequestOptions = new RequestOptions({
+ headers: new Headers({
+ "Content-Type": 'application/x-www-form-urlencoded'
+ })
+});
+
/**
* Filter columns via RegExp
*
diff --git a/src/ui_ng/src/i18n/lang/en-us-lang.json b/src/ui_ng/src/i18n/lang/en-us-lang.json
index 9089c6c01..0b32c0cb6 100644
--- a/src/ui_ng/src/i18n/lang/en-us-lang.json
+++ b/src/ui_ng/src/i18n/lang/en-us-lang.json
@@ -50,7 +50,9 @@
"NUMBER_REQUIRED": "Field is required and should be numbers.",
"PORT_REQUIRED": "Field is required and should be valid port number.",
"EMAIL_EXISTING": "Email address already exists.",
- "USER_EXISTING": "Username is already in use."
+ "USER_EXISTING": "Username is already in use.",
+ "RULE_USER_EXISTING": "Name is already in use.",
+ "EMPTY": "Name is required"
},
"PLACEHOLDER": {
"CURRENT_PWD": "Enter current password",
diff --git a/src/ui_ng/src/i18n/lang/es-es-lang.json b/src/ui_ng/src/i18n/lang/es-es-lang.json
index 8b1b40078..01fb4bf39 100644
--- a/src/ui_ng/src/i18n/lang/es-es-lang.json
+++ b/src/ui_ng/src/i18n/lang/es-es-lang.json
@@ -50,7 +50,9 @@
"NUMBER_REQUIRED": "El campo es obligatorio y debería ser un número.",
"PORT_REQUIRED": "El campo es obligatorio y debería ser un número de puerto válido.",
"EMAIL_EXISTING": "Esa dirección de email ya existe.",
- "USER_EXISTING": "Ese nombre de usuario ya existe."
+ "USER_EXISTING": "Ese nombre de usuario ya existe.",
+ "RULE_USER_EXISTING": "Name is already in use.",
+ "EMPTY": "Name is required"
},
"PLACEHOLDER": {
"CURRENT_PWD": "Introduzca la contraseña actual",
diff --git a/src/ui_ng/src/i18n/lang/zh-cn-lang.json b/src/ui_ng/src/i18n/lang/zh-cn-lang.json
index af643351b..dc1347f37 100644
--- a/src/ui_ng/src/i18n/lang/zh-cn-lang.json
+++ b/src/ui_ng/src/i18n/lang/zh-cn-lang.json
@@ -50,7 +50,9 @@
"NUMBER_REQUIRED": "此项为必填项且为数字。",
"PORT_REQUIRED": "此项为必填项且为合法端口号。",
"EMAIL_EXISTING": "邮件地址已经存在。",
- "USER_EXISTING": "用户名已经存在。"
+ "USER_EXISTING": "用户名已经存在。",
+ "RULE_USER_EXISTING": "名称已经存在。",
+ "EMPTY": "名称为必填项"
},
"PLACEHOLDER": {
"CURRENT_PWD": "输入当前密码",
diff --git a/tests/resources/Harbor-Pages/Replication.robot b/tests/resources/Harbor-Pages/Replication.robot
index 7c1eb174e..9e1d2355e 100644
--- a/tests/resources/Harbor-Pages/Replication.robot
+++ b/tests/resources/Harbor-Pages/Replication.robot
@@ -22,22 +22,32 @@ ${HARBOR_VERSION} v1.1.1
*** Keywords ***
Create An New Rule With New Endpoint
[Arguments] ${policy_name} ${policy_description} ${destination_name} ${destination_url} ${destination_username} ${destination_password}
- Click element xpath=${new_name_xpath}
+ Click element xpath=${new_rule_xpath}
Sleep 2
Input Text xpath=${policy_name_xpath} ${policy_name}
Input Text xpath=${policy_description_xpath} ${policy_description}
+ Click Element xpath=//select[@id="ruleProject"]
+ Click Element xpath=//select[@id="ruleProject"]//option[1]
+
+ Click Element xpath=//select[@id="ruleTarget"]
+ Click Element xpath=//select[@id="ruleTarget"]//option[1]
+
+ Click Element xpath=//select[@id="ruleTrigger"]
+ Click Element xpath=//select[@id="ruleTrigger"]//option[@value='immediate']
+
+ Mouse down xpath=//*[@id="clr-checkbox-ruleDeletion"]
+ Mouse up xpath=//*[@id="clr-checkbox-ruleDeletion"]
+
+ Mouse down xpath=//*[@id="clr-checkbox-ruleExit"]
+ Mouse up xpath=//*[@id="clr-checkbox-ruleExit"]
+
Click element xpath=${policy_enable_checkbox}
Click element xpath=${policy_endpoint_checkbox}
- Input text xpath=${destination_name_xpath} ${destination_name}
- Input text xpath=${destination_url_xpath} ${destination_url}
- Input text xpath=${destination_username_xpath} ${destination_username}
- Input text xpath=${destination_password_xpath} ${destination_password}
- Click element xpath=${replicaton_save_xpath}
+ Click element xpath=//*[@id="ruleBtnOk"]
Sleep 5
Capture Page Screenshot rule_${policy_name}.png
Wait Until Page Contains ${policy_name}
- Wait Until Page Contains ${policy_description}
- Wait Until Page Contains ${destination_name}
\ No newline at end of file
+ Wait Until Page Contains ${policy_description}
\ No newline at end of file
diff --git a/tests/resources/Harbor-Pages/Replication_Elements.robot b/tests/resources/Harbor-Pages/Replication_Elements.robot
index a7a3fe98d..e3bd4292b 100644
--- a/tests/resources/Harbor-Pages/Replication_Elements.robot
+++ b/tests/resources/Harbor-Pages/Replication_Elements.robot
@@ -16,11 +16,9 @@
Documentation This resource provides any keywords related to the Harbor private registry appliance
*** Variables ***
-${new_name_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/project-detail/replicaton/div/hbr-replication/div/div[1]/div/div[1]/button/clr-icon
-${policy_name_xpath} //*[@id="policy_name"]
-${policy_description_xpath} //*[@id="policy_description"]
-${policy_enable_checkbox} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/project-detail/replicaton/div/hbr-replication/div/div[1]/div/div[1]/create-edit-rule/clr-modal/div/div[1]/div/div[1]/div/div[2]/form/section/div[3]/div/label
-${policy_endpoint_checkbox} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/project-detail/replicaton/div/hbr-replication/div/div[1]/div/div[1]/create-edit-rule/clr-modal/div/div[1]/div/div[1]/div/div[2]/form/section/div[4]/div[2]/label
+${new_rule_xpath} /html/body/harbor-app/harbor-shell/clr-main-container/div/div/total-replication/div/hbr-replication/div/div[1]/div/div[1]/button
+${policy_name_xpath} //*[@id="ruleName"]
+${policy_description_xpath} //*[@id="ruleDescription"]
${destination_name_xpath} //*[@id='destination_name']
${destination_url_xpath} //*[@id='destination_url']
${destination_username_xpath} //*[@id='destination_username']