Add ui code about replication enhancement

This commit is contained in:
Fuhui Peng (c) 2017-11-30 11:05:44 +08:00
parent 34f70ff3b6
commit 831f69595a
26 changed files with 937 additions and 65 deletions

View File

@ -2,9 +2,9 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = `
<div>
<clr-datagrid [clrDgLoading]="loading">
<clr-dg-column [clrDgField]="'name'">{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'project_name'" *ngIf="!projectScope">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'projects'" *ngIf="!projectScope">{{'REPLICATION.PROJECT' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'description'">{{'REPLICATION.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'target_name'">{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'targets'">{{'REPLICATION.DESTINATION_NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="startTimeComparator">{{'REPLICATION.LAST_START_TIME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="enabledComparator">{{'REPLICATION.ACTIVATION' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'REPLICATION.PLACEHOLDER' | translate }}</clr-dg-placeholder>
@ -14,17 +14,12 @@ export const LIST_REPLICATION_RULE_TEMPLATE: string = `
<button class="action-item" (click)="toggleRule(p)">{{ (p.enabled === 0 ? 'REPLICATION.ENABLE' : 'REPLICATION.DISABLE') | translate}}</button>
<button class="action-item" (click)="deleteRule(p)">{{'REPLICATION.DELETE_POLICY' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>
<ng-template [ngIf]="!projectScope">
<a href="javascript:void(0)" (click)="redirectTo(p)">{{p.name}}</a>
</ng-template>
<ng-template [ngIf]="projectScope">
{{p.name}}
</ng-template>
<clr-dg-cell>{{p.name}}</clr-dg-cell>
<clr-dg-cell *ngIf="!projectScope">
<a href="javascript:void(0)" (click)="redirectTo(p)">{{p.projects[0].name}}</a>
</clr-dg-cell>
<clr-dg-cell *ngIf="!projectScope">{{p.project_name}}</clr-dg-cell>
<clr-dg-cell>{{p.description ? p.description : '-'}}</clr-dg-cell>
<clr-dg-cell>{{p.target_name}}</clr-dg-cell>
<clr-dg-cell>{{p.targets[0].name}}</clr-dg-cell>
<clr-dg-cell>
<ng-template [ngIf]="p.start_time === nullTime">-</ng-template>
<ng-template [ngIf]="p.start_time !== nullTime">{{p.start_time | date: 'short'}}</ng-template>

View File

@ -3,8 +3,7 @@ export const REPLICATION_TEMPLATE: string = `
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between" style="height:32px;">
<div class="flex-xs-middle option-left">
<button *ngIf="creationAvailable" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
<create-edit-rule [projectId]="projectId" (reload)="reloadRules($event)"></create-edit-rule>
<button *ngIf="!readonly" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
</div>
<div class="flex-xs-middle option-right">
<div class="select" style="float: left; top: 8px;">

View File

@ -88,6 +88,8 @@ export class ReplicationComponent implements OnInit, OnDestroy {
@Input() readonly: boolean;
@Output() redirect = new EventEmitter<ReplicationRule>();
@Output() openCreateRule = new EventEmitter<any>();
@Output() openEdit = new EventEmitter<string | number>();
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);
}
}

View File

@ -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",

View File

@ -18,6 +18,7 @@
<ul class="nav-list">
<li><a class="nav-link nav-link-override" routerLink="/harbor/users" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}</a></li>
<li><a class="nav-link nav-link-override" routerLink="/harbor/replications" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}</a></li>
<li><a class="nav-link nav-link-override" routerLink="/harbor/registry" routerLinkActive="active">{{'APP_TITLE.REG' | translate}}</a></li>
<li><a class="nav-link nav-link-override" routerLink="/harbor/configs" routerLinkActive="active">{{'SIDE_NAV.SYSTEM_MGMT.CONFIG' | translate}}</a></li>
</ul>
</section>

View File

@ -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],
}
]
},

View File

@ -13,7 +13,7 @@
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="logs" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">
<li class="nav-item" *ngIf="isSProjectAdmin || isSystemAdmin">
<a class="nav-link" routerLink="replications" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">

View File

@ -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 {

View File

@ -1,3 +1,4 @@
<h2 class="custom-h2">{{'REPLICATION.ENDPOINTS' | translate}}</h2>
<div style="margin-top: 24px;">
<hbr-endpoint></hbr-endpoint>
</div>

View File

@ -1,3 +1,3 @@
<div style="margin-top: 24px;">
<hbr-replication #replicationView [projectId]="projectIdentify" [withReplicationJob]='true'></hbr-replication>
<hbr-replication [readonly]="true" #replicationView [projectId]="projectIdentify" [withReplicationJob]='true'></hbr-replication>
</div>

View File

@ -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<string> = new Subject<string>();
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<ReplicationRule[]>(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 = <FormArray>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']);
}
}
}

View File

@ -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;}

View File

@ -0,0 +1,122 @@
<div>
<a class="cursor" (click)="backReplication()">< {{'SIDE_NAV.SYSTEM_MGMT.REPLICATION' | translate}}</a>
<h1 class="sub-header-title">New Replication Rule</h1>
<form [formGroup]="ruleForm" (ngSubmit)="onSubmit()" novalidate>
<section class="form-block">
<div class="form-group">
<label class="col-md-4 form-group-label-override">{{'REPLICATION.NAME' | translate}}<span style="color: red">*</span></label>
<label class="col-md-8" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
[class.invalid]='(ruleForm.controls.name.touched && ruleForm.controls.name.invalid) || isRuleNameExist'>
<input type="text" id="ruleName" required 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>
</div>
<!--Description-->
<div class="form-group">
<label class="col-md-4 form-group-label-override">{{'REPLICATION.DESCRIPTION' | translate}}</label>
<textarea type="text" id="ruleDescription" style=" width: 355px;" row= 3; formControlName="description"></textarea>
</div>
<!--Projects-->
<h4>Source</h4>
<div class="form-group">
<label class="col-md-4 form-group-label-override">{{'PROJECT.PROJECTS' | translate}}<span style="color: red">*</span></label>
<div formArrayName="projects">
<div class="select" *ngFor="let project of projects.controls; let i= index" [formGroupName]="i">
<select id="ruleProject" (change)="projectChange($event)" formControlName="project_id">
<option *ngFor="let pro of projectList" value="{{pro.project_id}}">{{pro.name}}</option>
</select>
</div>
</div>
</div>
<!--images/Filter-->
<div class="form-group">
<label class="col-md-4 form-group-label-override">Filter</label>
<div formArrayName="filters">
<div class="filterSelect" *ngFor="let filter of filters.controls; let i=index" [formGroupName]="i">
<div>
<div class="select floatSet">
<select formControlName="kind" (change)="filterChange($event)" id="{{i}}" name="{{filterListData[i].name}}">
<option *ngFor="let filter of filterListData[i].options;" value="{{filter}}">{{filter}}</option>
</select>
</div>
<label aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left"
[class.invalid]='ruleForm.controls.filters.controls[i].controls.pattern.touched && ruleForm.controls.filters.controls[i].controls.pattern.invalid'>
<input type="text" #filterValue required size="14" formControlName="pattern">
<span class="tooltip-content">{{'TOOLTIP.EMPTY' | translate}}</span>
</label>
<clr-icon shape="times-circle" class="is-solid" (click)="deleteFilter(i)"></clr-icon>
</div>
</div>
</div>
<clr-icon shape="plus-circle" class="is-solid" [hidden]="isFilterHide" (click)="addNewFilter()" style="margin-top: 9px;"></clr-icon>
</div>
<!--Targets-->
<h4>Targets</h4>
<div class="form-group">
<label class="col-md-4 form-group-label-override">Endpoint <span style="color: red">*</span></label>
<div formArrayName="targets">
<div class="select endpointSelect" *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>
</select>
</div>
</div>
</div>
<!--Trigger-->
<h4>Trigger</h4>
<div class="form-group">
<label class="col-md-4 form-group-label-override">Replication schedule</label>
<div formGroupName="trigger">
<!--on trigger-->
<div class="select floatSet">
<select id="ruleTrigger" formControlName="kind" (change)="selectTrigger($event)">
<option *ngFor="let triggerName of triggerNames" value="{{triggerName}}">{{triggerName}}</option>
</select>
</div>
<!--on push-->
<div style="float: left;" formGroupName="schedule_param">
<div class="select floatSet" [hidden]="!isScheduleOpt">
<select name="scheduleType" formControlName="type" (change)="selectSchedule($event)">
<option *ngFor="let scheduleName of scheduleNames" value="{{scheduleName}}">{{scheduleName}}</option>
</select>
</div>
<!--weekly-->
<span style="float: left;" [hidden]="!weeklySchedule || !isScheduleOpt">on &nbsp;&nbsp;</span>
<div [hidden]="!weeklySchedule || !isScheduleOpt" class="select floatSet">
<select name="scheduleDay" formControlName="weekday">
<option *ngFor="let filter of weekly; let i = index" [value]="i+1">{{filter}}</option>
</select>
</div>
<!--daily/time-->
<span [hidden]="!isScheduleOpt">at &nbsp;&nbsp;</span>
<input [hidden]="!isScheduleOpt" type="time" formControlName="offtime" required value="08:00" />
</div>
</div>
</div>
<!--Setting-->
<h4>Setting</h4>
<div class="form-group">
<label class="col-md-4 form-group-label-override">Setting</label>
<div class="col-lg-7 padLeft0">
<clr-checkbox [clrChecked]="true" id="ruleDeletion" formControlName="replicate_deletion">
Delete remote images when locally deleted
</clr-checkbox>
</div>
<div class="col-lg-7 padLeft0">
<clr-checkbox [clrChecked]="true" id="ruleExit" formControlName="replicate_existing_image_now">
exiting images immediately
</clr-checkbox>
</div>
</div>
<div class="offset-md-4">
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
<br>
<button type="button" id="ruleBtnCancel" class="btn btn-outline" [disabled]="!ruleForm.dirty" (click)="onCancel()">{{ 'BUTTON.CANCEL' | translate }}</button>
<button type="submit" id="ruleBtnOk" class="btn btn-primary" [disabled]="!ruleForm.valid || isRuleNameExist || !hasFormChange">{{ 'BUTTON.OK' | translate }}</button>
</div><!-- [disabled]="!ruleForm.valid"-->
</section>
</form>
</div>

View File

@ -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<any> | Promise<any> | 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[]> | 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<ReplicationRule> {
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[]> | Target[] {
return this.http
.get(this.targetUrl)
.toPromise()
.then(response => response.json())
.catch(error => Promise.reject(error));
}
public listProjects(): Promise<Project[]> | 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<any> | Promise<any> | 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));
}
}

View File

@ -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;
}

View File

@ -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 { }

View File

@ -1,3 +1,4 @@
<h2 class="custom-h2">{{'REPLICATION.REPLICATION_RULE' | translate}}</h2>
<div style="margin-top: 24px;">
<hbr-replication [withReplicationJob]='false' (redirect)="customRedirect($event)"></hbr-replication>
<hbr-replication [readonly]="false" [withReplicationJob]='true' (openCreateRule)="openCreatePage()" (openEdit)="openEditPage($event)" (redirect)="customRedirect($event)"></hbr-replication>
</div>

View File

@ -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 });
}
}
openEditPage(id: number): void {
this.router.navigate([id, 'rule'], { relativeTo: this.activeRoute });
}
openCreatePage(): void {
this.router.navigate(['new-rule'], { relativeTo: this.activeRoute });
}
}

View File

@ -3,3 +3,6 @@
margin-top: 36px;
margin-bottom: 11px;
}
.custom-h2 {
margin-top: 0px !important;
}

View File

@ -33,12 +33,13 @@ export class SystemAdminGuard implements CanActivate, CanActivateChild {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> | 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]);

View File

@ -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<T> implements Comparator<T> {
}
}
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
*

View File

@ -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",

View File

@ -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",

View File

@ -50,7 +50,9 @@
"NUMBER_REQUIRED": "此项为必填项且为数字。",
"PORT_REQUIRED": "此项为必填项且为合法端口号。",
"EMAIL_EXISTING": "邮件地址已经存在。",
"USER_EXISTING": "用户名已经存在。"
"USER_EXISTING": "用户名已经存在。",
"RULE_USER_EXISTING": "名称已经存在。",
"EMPTY": "名称为必填项"
},
"PLACEHOLDER": {
"CURRENT_PWD": "输入当前密码",

View File

@ -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}

View File

@ -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']