diff --git a/src/ui_ng/lib/src/create-edit-endpoint/create-edit-endpoint.component.ts b/src/ui_ng/lib/src/create-edit-endpoint/create-edit-endpoint.component.ts index bdc87b1ce..9ad2728ff 100644 --- a/src/ui_ng/lib/src/create-edit-endpoint/create-edit-endpoint.component.ts +++ b/src/ui_ng/lib/src/create-edit-endpoint/create-edit-endpoint.component.ts @@ -62,7 +62,6 @@ export class CreateEditEndpointComponent implements AfterViewChecked { currentForm: NgForm; hasChanged: boolean; - endpointHasChanged: boolean; targetNameHasChanged: boolean; @@ -263,7 +262,7 @@ export class CreateEditEndpointComponent implements AfterViewChecked { ngAfterViewChecked(): void { this.targetForm = this.currentForm; if(this.targetForm) { - let comparison: {[key: string]: string} = { + let comparison: {[key: string]: any} = { targetName: this.initVal.name, endpointUrl: this.initVal.endpoint, username: this.initVal.username, diff --git a/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.css.ts b/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.css.ts new file mode 100644 index 000000000..83aaea83c --- /dev/null +++ b/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.css.ts @@ -0,0 +1,5 @@ +export const CREATE_EDIT_RULE_STYLE: string = ` +.form-group-label-override { + font-size: 14px; + font-weight: 400; +}`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.html.ts b/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.html.ts new file mode 100644 index 000000000..5e5b5e9c7 --- /dev/null +++ b/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.html.ts @@ -0,0 +1,88 @@ +export const CREATE_EDIT_RULE_TEMPLATE: string = ` + + + + + +`; diff --git a/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.ts b/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.ts new file mode 100644 index 000000000..6fe2aedd9 --- /dev/null +++ b/src/ui_ng/lib/src/create-edit-rule/create-edit-rule.component.ts @@ -0,0 +1,417 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { Component, Input, Output, EventEmitter, OnInit, ViewChild, AfterViewChecked } from '@angular/core'; + +import { NgForm } from '@angular/forms'; + +import { ReplicationService } from '../service/replication.service'; +import { EndpointService } from '../service/endpoint.service'; + +import { ErrorHandler } from '../error-handler/error-handler'; +import { ActionType } from '../shared/shared.const'; + +import { InlineAlertComponent } from '../inline-alert/inline-alert.component'; + +import { ReplicationRule } from '../service/interface'; +import { Endpoint } from '../service/interface'; + +import { TranslateService } from '@ngx-translate/core'; + +import { CREATE_EDIT_RULE_STYLE } from './create-edit-rule.component.css'; +import { CREATE_EDIT_RULE_TEMPLATE } from './create-edit-rule.component.html'; + +import { toPromise } from '../utils'; + +/** + * Rule form model. + */ +export interface CreateEditRule { + ruleId?: number | string; + name?: string; + description?: string; + enable?: boolean; + endpointId?: number | string; + endpointName?: string; + endpointUrl?: string; + username?: string; + password?: string; +} + +const FAKE_PASSWORD: string = 'ywJZnDTM'; + +@Component({ + selector: 'create-edit-rule', + template: CREATE_EDIT_RULE_TEMPLATE, + styles: [ CREATE_EDIT_RULE_STYLE ] +}) +export class CreateEditRuleComponent implements OnInit, AfterViewChecked { + + modalTitle: string; + createEditRuleOpened: boolean; + createEditRule: CreateEditRule = this.initCreateEditRule; + initVal: CreateEditRule = this.initCreateEditRule; + + actionType: ActionType; + + isCreateEndpoint: boolean; + @Input() projectId: number; + + @Output() reload = new EventEmitter(); + + endpoints: Endpoint[]; + + pingTestMessage: string; + testOngoing: boolean; + pingStatus: boolean; + + ruleForm: NgForm; + + staticBackdrop: boolean = true; + closable: boolean = false; + + @ViewChild('ruleForm') + currentForm: NgForm; + + hasChanged: boolean; + + editable: boolean; + + get initCreateEditRule(): CreateEditRule { + return { + endpointId: '', + name: '', + enable: false, + description: '', + endpointName: '', + endpointUrl: '', + username: '', + password: '' + }; + } + + get initReplicationRule(): ReplicationRule { + return { + project_id: '', + project_name: '', + target_id: '', + target_name: '', + enabled: 0, + description: '', + cron_str: '', + error_job_count: 0, + deleted: 0 + }; + } + + get initEndpoint(): Endpoint { + return { + endpoint: '', + name: '', + username: '', + password: '', + type: 0 + }; + } + + @ViewChild(InlineAlertComponent) + inlineAlert: InlineAlertComponent; + + get readonly(): boolean { + return this.actionType === ActionType.EDIT && (this.createEditRule.enable || false); + } + + get untoggleable(): boolean { + return this.actionType === ActionType.EDIT && (this.initVal.enable || false); + } + + get showNewDestination(): boolean { + return this.actionType === ActionType.ADD_NEW || (!this.createEditRule.enable || false); + } + + constructor( + private replicationService: ReplicationService, + private endpointService: EndpointService, + private errorHandler: ErrorHandler, + private translateService: TranslateService) {} + + prepareTargets(endpointId?: number | string) { + toPromise(this.endpointService + .getEndpoints()) + .then(endpoints=>{ + this.endpoints = endpoints; + if(this.endpoints && this.endpoints.length > 0) { + let initialEndpoint: Endpoint | undefined; + (endpointId) ? initialEndpoint = this.endpoints.find(t=>t.id===endpointId) : initialEndpoint = this.endpoints[0]; + if(!initialEndpoint) { + return; + } + this.createEditRule.endpointId = initialEndpoint.id; + this.createEditRule.endpointName = initialEndpoint.name; + this.createEditRule.endpointUrl = initialEndpoint.endpoint; + this.createEditRule.username = initialEndpoint.username; + this.createEditRule.password = FAKE_PASSWORD; + + this.initVal.endpointId = this.createEditRule.endpointId; + this.initVal.endpointUrl = this.createEditRule.endpointUrl; + this.initVal.username = this.createEditRule.username; + this.initVal.password = this.createEditRule.password; + } + }) + .catch(error=>{ + this.errorHandler.error(error); + this.createEditRuleOpened = false; + }); + } + + ngOnInit(): void {} + + openCreateEditRule(editable: boolean, ruleId?: number | string): void { + this.createEditRuleOpened = true; + this.createEditRule = this.initCreateEditRule; + + if(!this.createEditRule) { + return; + } + + this.editable = editable; + + this.isCreateEndpoint = false; + + this.hasChanged = false; + + this.pingTestMessage = ''; + this.pingStatus = true; + this.testOngoing = false; + + if(ruleId) { + this.actionType = ActionType.EDIT; + this.translateService.get('REPLICATION.EDIT_POLICY_TITLE').subscribe(res=>this.modalTitle=res); + toPromise(this.replicationService + .getReplicationRule(ruleId)) + .then(rule=>{ + this.createEditRule.ruleId = ruleId; + this.createEditRule.name = rule.name; + this.createEditRule.description = rule.description; + this.createEditRule.enable = rule.enabled === 1? true : false; + this.prepareTargets(rule.target_id); + + this.initVal.name = this.createEditRule.name; + this.initVal.description = this.createEditRule.description; + this.initVal.enable = this.createEditRule.enable; + } + ) + } else { + this.actionType = ActionType.ADD_NEW; + this.translateService.get('REPLICATION.ADD_POLICY').subscribe(res=>this.modalTitle=res); + this.prepareTargets(); + } + } + + newEndpoint(checkedAddNew: boolean): void { + this.isCreateEndpoint = checkedAddNew; + if(this.isCreateEndpoint) { + this.createEditRule.endpointName = ''; + this.createEditRule.endpointUrl = ''; + this.createEditRule.username = ''; + this.createEditRule.password = ''; + } else { + this.prepareTargets(); + } + } + + selectEndpoint(): void { + let result: Endpoint | undefined = this.endpoints.find(target=>target.id == this.createEditRule.endpointId); + if(result) { + this.createEditRule.endpointId = result.id; + this.createEditRule.endpointUrl = result.endpoint; + this.createEditRule.username = result.username; + this.createEditRule.password = FAKE_PASSWORD; + } + } + + getRuleByForm(): ReplicationRule { + let rule: ReplicationRule = this.initReplicationRule; + rule.project_id = this.projectId; + rule.id = this.createEditRule.endpointId; + rule.name = this.createEditRule.name; + rule.description = this.createEditRule.description; + rule.enabled = this.createEditRule.enable ? 1 : 0; + rule.target_id = this.createEditRule.endpointId || ''; + return rule; + } + + getEndpointByForm(): Endpoint { + let endpoint: Endpoint = this.initEndpoint; + endpoint.id = this.createEditRule.ruleId; + endpoint.name = this.createEditRule.endpointName || ''; + endpoint.endpoint = this.createEditRule.endpointUrl || ''; + endpoint.username = this.createEditRule.username; + endpoint.password = this.createEditRule.password; + return endpoint; + } + + createReplicationRule(): void { + toPromise(this.replicationService + .createReplicationRule(this.getRuleByForm())) + .then(response=>{ + this.translateService.get('REPLICATION.CREATED_SUCCESS') + .subscribe(res=>this.errorHandler.info(res)); + this.createEditRuleOpened = false; + this.reload.emit(true); + }) + .catch(error=>{ + if (error.status === 409) { + this.inlineAlert.showInlineError('REPLICATION.POLICY_ALREADY_EXISTS'); + } else { + this.inlineAlert.showInlineError(error); + } + console.error('Failed to create policy:' + error.status + ', error message:' + JSON.stringify(error['_body'])); + }); + } + + updateReplicationRule(): void { + toPromise(this.replicationService + .updateReplicationRule(this.getRuleByForm())) + .then(()=>{ + this.translateService.get('REPLICATION.UPDATED_SUCCESS') + .subscribe(res=>this.errorHandler.info(res)); + this.createEditRuleOpened = false; + this.reload.emit(true); + }) + .catch(error=>{ + if (error.status === 409) { + this.inlineAlert.showInlineError('REPLICATION.POLICY_ALREADY_EXISTS'); + } else { + this.inlineAlert.showInlineError(error); + } + console.error('Failed to create policy and target:' + error.status + ', error message:' + JSON.stringify(error['_body'])); + } + ); + } + + createWithEndpoint(actionType: ActionType): void { + toPromise(this.endpointService + .createEndpoint(this.getEndpointByForm())) + .then(()=>{ + toPromise(this.endpointService + .getEndpoints(this.createEditRule.endpointName)) + .then(endpoints=>{ + if(endpoints && endpoints.length > 0) { + let addedEndpoint: Endpoint = endpoints[0]; + this.createEditRule.endpointId = addedEndpoint.id; + switch(actionType) { + case ActionType.ADD_NEW: + this.createReplicationRule(); + break; + case ActionType.EDIT: + this.updateReplicationRule(); + break; + } + } + }) + .catch(error=>{ + this.inlineAlert.showInlineError(error); + this.errorHandler.error(error); + }); + }) + .catch(error=>{ + this.inlineAlert.showInlineError(error); + this.errorHandler.error(error); + }); + } + + onSubmit() { + if(this.isCreateEndpoint) { + this.createWithEndpoint(this.actionType); + } else { + switch(this.actionType) { + case ActionType.ADD_NEW: + this.createReplicationRule(); + break; + case ActionType.EDIT: + this.updateReplicationRule(); + break; + } + } + } + + onCancel() { + if(this.hasChanged) { + this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'}); + } else { + this.createEditRuleOpened = false; + this.ruleForm.reset(); + } + } + + confirmCancel(confirmed: boolean) { + this.createEditRuleOpened = false; + this.inlineAlert.close(); + this.ruleForm.reset(); + } + + ngAfterViewChecked(): void { + this.ruleForm = this.currentForm; + if(this.ruleForm) { + let comparison: {[key: string]: any} = { + targetName: this.initVal.name, + endpointUrl: this.initVal.endpointUrl, + username: this.initVal.username, + password: this.initVal.password + }; + let self: CreateEditRuleComponent | any = this; + if(self) { + self.ruleForm.valueChanges.subscribe((data: any)=>{ + for(let key in data) { + let origin = data[key]; + let current = comparison[key]; + if(((this.actionType === ActionType.EDIT && !this.readonly && !current ) || current) && current !== origin) { + this.hasChanged = true; + break; + } else { + this.hasChanged = false; + this.inlineAlert.close(); + } + } + }); + } + } + } + + testConnection() { + this.pingStatus = true; + this.translateService.get('REPLICATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res); + this.testOngoing = !this.testOngoing; + let pingTarget: Endpoint = this.initEndpoint; + if(this.isCreateEndpoint) { + pingTarget.endpoint = this.createEditRule.endpointUrl || ''; + pingTarget.username = this.createEditRule.username; + pingTarget.password = this.createEditRule.password; + } else { + pingTarget.id = this.createEditRule.endpointId; + } + toPromise(this.endpointService + .pingEndpoint(pingTarget)) + .then(()=>{ + this.testOngoing = !this.testOngoing; + this.translateService.get('REPLICATION.TEST_CONNECTION_SUCCESS').subscribe(res=>this.pingTestMessage=res); + this.pingStatus = true; + }) + .catch(error=>{ + this.testOngoing = !this.testOngoing; + this.translateService.get('REPLICATION.TEST_CONNECTION_FAILURE').subscribe(res=>this.pingTestMessage=res); + this.pingStatus = false; + }); + } +} \ No newline at end of file diff --git a/src/ui_ng/lib/src/create-edit-rule/index.ts b/src/ui_ng/lib/src/create-edit-rule/index.ts new file mode 100644 index 000000000..8703750fd --- /dev/null +++ b/src/ui_ng/lib/src/create-edit-rule/index.ts @@ -0,0 +1,7 @@ +import { Type } from '@angular/core'; + +import { CreateEditRuleComponent } from './create-edit-rule.component'; + +export const CREATE_EDIT_RULE_DIRECTIVES: Type[] = [ + CreateEditRuleComponent +]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/endpoint/endpoint.component.ts b/src/ui_ng/lib/src/endpoint/endpoint.component.ts index f6dad4a11..cb5b4b282 100644 --- a/src/ui_ng/lib/src/endpoint/endpoint.component.ts +++ b/src/ui_ng/lib/src/endpoint/endpoint.component.ts @@ -163,13 +163,12 @@ export class EndpointComponent implements OnInit { } deleteTarget(target: Endpoint) { - console.log('Endpoint:' + JSON.stringify(target)); if (target) { let targetId = target.id; let deletionMessage = new ConfirmationMessage( 'REPLICATION.DELETION_TITLE_TARGET', 'REPLICATION.DELETION_SUMMARY_TARGET', - target.name, + target.name || '', target.id, ConfirmationTargets.TARGET, ConfirmationButtons.DELETE_CANCEL); diff --git a/src/ui_ng/lib/src/harbor-library.module.ts b/src/ui_ng/lib/src/harbor-library.module.ts index 9757acfdc..5e76f193d 100644 --- a/src/ui_ng/lib/src/harbor-library.module.ts +++ b/src/ui_ng/lib/src/harbor-library.module.ts @@ -7,6 +7,11 @@ import { REPOSITORY_DIRECTIVES } from './repository/index'; import { LIST_REPOSITORY_DIRECTIVES } from './list-repository/index'; import { TAG_DIRECTIVES } from './tag/index'; +import { REPLICATION_DIRECTIVES } from './replication/index'; +import { CREATE_EDIT_RULE_DIRECTIVES } from './create-edit-rule/index'; +import { LIST_REPLICATION_RULE_DIRECTIVES } from './list-replication-rule/index'; +import { LIST_REPLICATION_JOB_DIRECTIVES } from './list-replication-job/index'; + import { CREATE_EDIT_ENDPOINT_DIRECTIVES } from './create-edit-endpoint/index'; import { SERVICE_CONFIG, IServiceConfig } from './service.config'; @@ -128,7 +133,11 @@ export function initConfig(translateService: TranslateService, config: IServiceC TAG_DIRECTIVES, CREATE_EDIT_ENDPOINT_DIRECTIVES, CONFIRMATION_DIALOG_DIRECTIVES, - INLINE_ALERT_DIRECTIVES + INLINE_ALERT_DIRECTIVES, + REPLICATION_DIRECTIVES, + LIST_REPLICATION_RULE_DIRECTIVES, + LIST_REPLICATION_JOB_DIRECTIVES, + CREATE_EDIT_RULE_DIRECTIVES ], exports: [ LOG_DIRECTIVES, @@ -139,7 +148,11 @@ export function initConfig(translateService: TranslateService, config: IServiceC TAG_DIRECTIVES, CREATE_EDIT_ENDPOINT_DIRECTIVES, CONFIRMATION_DIALOG_DIRECTIVES, - INLINE_ALERT_DIRECTIVES + INLINE_ALERT_DIRECTIVES, + REPLICATION_DIRECTIVES, + LIST_REPLICATION_RULE_DIRECTIVES, + LIST_REPLICATION_JOB_DIRECTIVES, + CREATE_EDIT_RULE_DIRECTIVES ], providers: [] }) diff --git a/src/ui_ng/lib/src/index.ts b/src/ui_ng/lib/src/index.ts index 5a0611ab2..38f33349a 100644 --- a/src/ui_ng/lib/src/index.ts +++ b/src/ui_ng/lib/src/index.ts @@ -5,4 +5,7 @@ export * from './error-handler/index'; //export * from './utils'; export * from './log/index'; export * from './filter/index'; -export * from './endpoint/index'; \ No newline at end of file +export * from './endpoint/index'; +export * from './repository/index'; +export * from './tag/index'; +export * from './replication/index'; \ No newline at end of file diff --git a/src/ui_ng/lib/src/list-replication-job/index.ts b/src/ui_ng/lib/src/list-replication-job/index.ts new file mode 100644 index 000000000..5b04492b9 --- /dev/null +++ b/src/ui_ng/lib/src/list-replication-job/index.ts @@ -0,0 +1,6 @@ +import { Type } from '@angular/core'; +import { ListReplicationJobComponent } from './list-replication-job.component'; + +export const LIST_REPLICATION_JOB_DIRECTIVES: Type[] = [ + ListReplicationJobComponent +]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/list-replication-job/list-replication-job.component.css.ts b/src/ui_ng/lib/src/list-replication-job/list-replication-job.component.css.ts new file mode 100644 index 000000000..a7e32c880 --- /dev/null +++ b/src/ui_ng/lib/src/list-replication-job/list-replication-job.component.css.ts @@ -0,0 +1 @@ +export const REPLICATION_JOB_STYLE: string = ``; \ No newline at end of file diff --git a/src/ui_ng/lib/src/list-replication-job/list-replication-job.component.html.ts b/src/ui_ng/lib/src/list-replication-job/list-replication-job.component.html.ts new file mode 100644 index 000000000..d48ddc659 --- /dev/null +++ b/src/ui_ng/lib/src/list-replication-job/list-replication-job.component.html.ts @@ -0,0 +1,25 @@ +export const REPLICATION_JOB_TEMPLATE: string = ` + + {{'REPLICATION.NAME' | translate}} + {{'REPLICATION.STATUS' | translate}} + {{'REPLICATION.OPERATION' | translate}} + {{'REPLICATION.CREATION_TIME' | translate}} + {{'REPLICATION.END_TIME' | translate}} + {{'REPLICATION.LOGS' | translate}} + + {{j.repository}} + {{j.status}} + {{j.operation}} + {{j.creation_time | date: 'short'}} + {{j.update_time | date: 'short'}} + + + + + + + + {{ totalRecordCount }} {{'REPLICATION.ITEMS' | translate}} + + +`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/list-replication-job/list-replication-job.component.ts b/src/ui_ng/lib/src/list-replication-job/list-replication-job.component.ts new file mode 100644 index 000000000..e2fc7cabb --- /dev/null +++ b/src/ui_ng/lib/src/list-replication-job/list-replication-job.component.ts @@ -0,0 +1,47 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; +import { ReplicationJob } from '../service/interface'; +import { State } from 'clarity-angular'; +import { ErrorHandler } from '../error-handler/error-handler'; + +import { REPLICATION_JOB_STYLE } from './list-replication-job.component.css'; +import { REPLICATION_JOB_TEMPLATE } from './list-replication-job.component.html'; + +@Component({ + selector: 'list-replication-job', + template: REPLICATION_JOB_TEMPLATE, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ListReplicationJobComponent { + @Input() jobs: ReplicationJob[]; + @Input() totalRecordCount: number; + @Input() totalPage: number; + @Output() paginate = new EventEmitter(); + + constructor( + private errorHandler: ErrorHandler, + private ref: ChangeDetectorRef) { + let hnd = setInterval(()=>ref.markForCheck(), 100); + setTimeout(()=>clearInterval(hnd), 1000); + } + + pageOffset: number = 1; + + refresh(state: State) { + if(this.jobs) { + this.paginate.emit(state); + } + } +} \ No newline at end of file diff --git a/src/ui_ng/lib/src/list-replication-rule/index.ts b/src/ui_ng/lib/src/list-replication-rule/index.ts new file mode 100644 index 000000000..3287654d6 --- /dev/null +++ b/src/ui_ng/lib/src/list-replication-rule/index.ts @@ -0,0 +1,7 @@ +import { Type } from '@angular/core'; + +import { ListReplicationRuleComponent } from './list-replication-rule.component'; + +export const LIST_REPLICATION_RULE_DIRECTIVES: Type[] = [ + ListReplicationRuleComponent +]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.css.ts b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.css.ts new file mode 100644 index 000000000..7ed174a4e --- /dev/null +++ b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.css.ts @@ -0,0 +1 @@ +export const LIST_REPLICATION_RULE_STYLE: string = ``; \ No newline at end of file diff --git a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts new file mode 100644 index 000000000..313c8fa41 --- /dev/null +++ b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.html.ts @@ -0,0 +1,37 @@ +export const LIST_REPLICATION_RULE_TEMPLATE: string = ` + + + + {{'REPLICATION.NAME' | translate}} + {{'REPLICATION.PROJECT' | translate}} + {{'REPLICATION.DESCRIPTION' | translate}} + {{'REPLICATION.DESTINATION_NAME' | translate}} + {{'REPLICATION.LAST_START_TIME' | translate}} + {{'REPLICATION.ACTIVATION' | translate}} + + + + + + + + + {{p.name}} + + + {{p.name}} + + + {{p.project_name}} + {{p.description ? p.description : '-'}} + {{p.target_name}} + + - + {{p.start_time | date: 'short'}} + + + {{ (p.enabled === 1 ? 'REPLICATION.ENABLED' : 'REPLICATION.DISABLED') | translate}} + + + {{ (rules ? rules.length : 0) }} {{'REPLICATION.ITEMS' | translate}} +`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts new file mode 100644 index 000000000..74628bd27 --- /dev/null +++ b/src/ui_ng/lib/src/list-replication-rule/list-replication-rule.component.ts @@ -0,0 +1,135 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { Component, Input, Output, EventEmitter, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; + +import { ReplicationService } from '../service/replication.service'; +import { ReplicationRule } from '../service/interface'; + +import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; +import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message'; +import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message'; + +import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const'; + +import { TranslateService } from '@ngx-translate/core'; + +import { ErrorHandler } from '../error-handler/error-handler'; +import { toPromise } from '../utils'; + +import { LIST_REPLICATION_RULE_STYLE } from './list-replication-rule.component.css'; +import { LIST_REPLICATION_RULE_TEMPLATE } from './list-replication-rule.component.html'; + +@Component({ + selector: 'list-replication-rule', + template: LIST_REPLICATION_RULE_TEMPLATE, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ListReplicationRuleComponent { + + nullTime: string = '0001-01-01T00:00:00Z'; + + @Input() rules: ReplicationRule[]; + @Input() projectless: boolean; + @Input() selectedId: number | string; + + @Output() reload = new EventEmitter(); + @Output() selectOne = new EventEmitter(); + @Output() editOne = new EventEmitter(); + @Output() toggleOne = new EventEmitter(); + + @ViewChild('toggleConfirmDialog') + toggleConfirmDialog: ConfirmationDialogComponent; + + @ViewChild('deletionConfirmDialog') + deletionConfirmDialog: ConfirmationDialogComponent; + + constructor( + private replicationService: ReplicationService, + private translateService: TranslateService, + private errorHandler: ErrorHandler, + private ref: ChangeDetectorRef) { + setInterval(()=>ref.markForCheck(), 500); + } + + toggleConfirm(message: ConfirmationAcknowledgement) { + if(message && + message.source === ConfirmationTargets.TOGGLE_CONFIRM && + message.state === ConfirmationState.CONFIRMED) { + let rule: ReplicationRule = message.data; + if(rule) { + rule.enabled = rule.enabled === 0 ? 1 : 0; + toPromise(this.replicationService + .enableReplicationRule(rule.id || '', rule.enabled)) + .then(() => + this.translateService.get('REPLICATION.TOGGLED_SUCCESS') + .subscribe(res=>this.errorHandler.info(res))) + .catch(error => this.errorHandler.error(error)); + } + } + } + + deletionConfirm(message: ConfirmationAcknowledgement) { + if (message && + message.source === ConfirmationTargets.POLICY && + message.state === ConfirmationState.CONFIRMED) { + toPromise(this.replicationService + .deleteReplicationRule(message.data)) + .then(() => { + this.translateService.get('REPLICATION.DELETED_SUCCESS') + .subscribe(res=>this.errorHandler.info(res)); + this.reload.emit(true); + }) + .catch(error => { + if(error && error.status === 412) { + this.translateService.get('REPLICATION.FAILED_TO_DELETE_POLICY_ENABLED') + .subscribe(res=>this.errorHandler.error(res)); + } else { + this.errorHandler.error(error); + } + }); + } + } + + selectRule(rule: ReplicationRule): void { + this.selectedId = rule.id || ''; + this.selectOne.emit(rule); + } + + editRule(rule: ReplicationRule) { + this.editOne.emit(rule); + } + + toggleRule(rule: ReplicationRule) { + let toggleConfirmMessage: ConfirmationMessage = new ConfirmationMessage( + rule.enabled === 1 ? 'REPLICATION.TOGGLE_DISABLE_TITLE' : 'REPLICATION.TOGGLE_ENABLE_TITLE', + rule.enabled === 1 ? 'REPLICATION.CONFIRM_TOGGLE_DISABLE_POLICY': 'REPLICATION.CONFIRM_TOGGLE_ENABLE_POLICY', + rule.name || '', + rule, + ConfirmationTargets.TOGGLE_CONFIRM + ); + this.toggleConfirmDialog.open(toggleConfirmMessage); + } + + deleteRule(rule: ReplicationRule) { + let deletionMessage: ConfirmationMessage = new ConfirmationMessage( + 'REPLICATION.DELETION_TITLE', + 'REPLICATION.DELETION_SUMMARY', + rule.name || '', + rule.id, + ConfirmationTargets.POLICY, + ConfirmationButtons.DELETE_CANCEL); + this.deletionConfirmDialog.open(deletionMessage); + } + +} \ No newline at end of file diff --git a/src/ui_ng/lib/src/replication/index.ts b/src/ui_ng/lib/src/replication/index.ts new file mode 100644 index 000000000..715aedf76 --- /dev/null +++ b/src/ui_ng/lib/src/replication/index.ts @@ -0,0 +1,6 @@ +import { Type } from '@angular/core'; +import { ReplicationComponent } from './replication.component'; + +export const REPLICATION_DIRECTIVES: Type[] = [ + ReplicationComponent +]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/replication/replication.component.css.ts b/src/ui_ng/lib/src/replication/replication.component.css.ts new file mode 100644 index 000000000..7e5276d15 --- /dev/null +++ b/src/ui_ng/lib/src/replication/replication.component.css.ts @@ -0,0 +1,18 @@ +export const REPLICATION_STYLE: string = ` +.option-left { + padding-left: 16px; + margin-top: 24px; +} +.option-right { + padding-right: 16px; + margin-top: 18px; +} + +.option-left-down { + margin-top: 36px; +} + +.option-right-down { + padding-right: 16px; + margin-top: 24px; +}`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/replication/replication.component.html.ts b/src/ui_ng/lib/src/replication/replication.component.html.ts new file mode 100644 index 000000000..654708044 --- /dev/null +++ b/src/ui_ng/lib/src/replication/replication.component.html.ts @@ -0,0 +1,63 @@ +export const REPLICATION_TEMPLATE: string = ` +
+
+
+
+ + +
+
+
+ +
+ + + + +
+
+
+
+ +
+
+
+
{{'REPLICATION.REPLICATION_JOBS' | translate}}
+
+ + + + + +
+
+
+
+ +
+
+ + + + +
+
+
+
+ +
+
`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/replication/replication.component.ts b/src/ui_ng/lib/src/replication/replication.component.ts new file mode 100644 index 000000000..b71531180 --- /dev/null +++ b/src/ui_ng/lib/src/replication/replication.component.ts @@ -0,0 +1,287 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import { Component, OnInit, ViewChild, Input } from '@angular/core'; +// import { ActivatedRoute } from '@angular/router'; +import { ResponseOptions } from '@angular/http'; +import { NgModel } from '@angular/forms'; + +import { CreateEditRuleComponent } from '../create-edit-rule/create-edit-rule.component'; + +import { ErrorHandler } from '../error-handler/error-handler'; + +import { ReplicationService } from '../service/replication.service'; + +import { RequestQueryParams } from '../service/RequestQueryParams'; +// import { SessionUser } from '../shared/session-user'; +import { ReplicationRule, ReplicationJob, Endpoint } from '../service/interface'; + +import { State } from 'clarity-angular'; + +import { toPromise } from '../utils'; + +import { TranslateService } from '@ngx-translate/core'; + +import { REPLICATION_STYLE } from './replication.component.css'; +import { REPLICATION_TEMPLATE } from './replication.component.html'; + +const ruleStatus: {[key: string]: any} = [ + { 'key': 'all', 'description': 'REPLICATION.ALL_STATUS'}, + { 'key': '1', 'description': 'REPLICATION.ENABLED'}, + { 'key': '0', 'description': 'REPLICATION.DISABLED'} +]; + +const jobStatus: {[key: string]: any} = [ + { 'key': 'all', 'description': 'REPLICATION.ALL' }, + { 'key': 'pending', 'description': 'REPLICATION.PENDING' }, + { 'key': 'running', 'description': 'REPLICATION.RUNNING' }, + { 'key': 'error', 'description': 'REPLICATION.ERROR' }, + { 'key': 'retrying', 'description': 'REPLICATION.RETRYING' }, + { 'key': 'stopped' , 'description': 'REPLICATION.STOPPED' }, + { 'key': 'finished', 'description': 'REPLICATION.FINISHED' }, + { 'key': 'canceled', 'description': 'REPLICATION.CANCELED' } +]; + +const optionalSearch: {} = {0: 'REPLICATION.ADVANCED', 1: 'REPLICATION.SIMPLE'}; + +export class SearchOption { + ruleId: number | string; + ruleName: string = ''; + repoName: string = ''; + status: string = ''; + startTime: string = ''; + startTimestamp: string = ''; + endTime: string = ''; + endTimestamp: string = ''; + page: number = 1; + pageSize: number = 5; +} + +@Component({ + selector: 'hbr-replication', + template: REPLICATION_TEMPLATE +}) +export class ReplicationComponent implements OnInit { + + @Input() projectId: number | string; + + search: SearchOption = new SearchOption(); + + ruleStatus = ruleStatus; + currentRuleStatus: {key: string, description: string}; + + jobStatus = jobStatus; + currentJobStatus: {key: string, description: string}; + + changedRules: ReplicationRule[]; + changedJobs: ReplicationJob[]; + initSelectedId: number | string; + + rules: ReplicationRule[]; + jobs: ReplicationJob[]; + + jobsTotalRecordCount: number; + jobsTotalPage: number; + + toggleJobSearchOption = optionalSearch; + currentJobSearchOption: number; + + @ViewChild(CreateEditRuleComponent) + createEditPolicyComponent: CreateEditRuleComponent; + + @ViewChild('fromTime') fromTimeInput: NgModel; + @ViewChild('toTime') toTimeInput: NgModel; + + get fromTimeInvalid(): boolean { + return (this.fromTimeInput.errors && this.fromTimeInput.errors.dateValidator && (this.fromTimeInput.dirty || this.fromTimeInput.touched)) || false; + } + + get toTimeInvalid(): boolean { + return (this.toTimeInput.errors && this.toTimeInput.errors.dateValidator && (this.toTimeInput.dirty || this.toTimeInput.touched)) || false; + } + + constructor( + private errorHandler: ErrorHandler, + private replicationService: ReplicationService, + private translateService: TranslateService) { + } + + ngOnInit(): void { + this.projectId = 1; + this.currentRuleStatus = this.ruleStatus[0]; + this.currentJobStatus = this.jobStatus[0]; + this.currentJobSearchOption = 0; + this.retrievePolicies(); + + // let isCreate = this.route.snapshot.parent.queryParams['is_create']; + // if (isCreate && isCreate) { + // this.openModal(); + // } + } + + retrievePolicies(): void { + toPromise(this.replicationService + .getReplicationRules(this.projectId, this.search.ruleName)) + .then(response=>{ + this.changedRules = response || []; + if(this.changedRules && this.changedRules.length > 0) { + this.initSelectedId = this.changedRules[0].id || ''; + } + this.rules = this.changedRules; + if(this.changedRules && this.changedRules.length > 0) { + this.search.ruleId = this.changedRules[0].id || ''; + this.fetchReplicationJobs(); + } + }, + error=>this.errorHandler.error(error) + ); + } + + openModal(): void { + this.createEditPolicyComponent.openCreateEditRule(true); + } + + openEditRule(rule: ReplicationRule) { + if(rule) { + let editable = true; + if(rule.enabled === 1) { + editable = false; + } + this.createEditPolicyComponent.openCreateEditRule(editable, rule.id); + } + } + + fetchReplicationJobs(state?: State) { + if(state && state.page && state.page.to) { + this.search.page = state.page.to + 1; + } + let params: RequestQueryParams = new RequestQueryParams(); + params.set('status', this.search.status); + params.set('repository', this.search.repoName); + params.set('start_time', this.search.startTimestamp); + params.set('end_time', this.search.endTimestamp); + params.set('page', this.search.page + ''); + params.set('page_size', this.search.pageSize + ''); + + toPromise(this.replicationService + .getJobs(this.search.ruleId, params)) + .then( + response=>{ + this.jobsTotalRecordCount = response.headers.get('x-total-count'); + this.jobsTotalPage = Math.ceil(this.jobsTotalRecordCount / this.search.pageSize); + this.changedJobs = response.json(); + this.jobs = this.changedJobs; + for(let i = 0; i < this.jobs.length; i++) { + let j = this.jobs[i]; + if(j.status == 'retrying' || j.status == 'error') { + this.translateService.get('REPLICATION.FOUND_ERROR_IN_JOBS') + .subscribe(res=>this.errorHandler.error(res)); + break; + } + } + }, + error=>this.errorHandler.error(error) + ); + } + + selectOneRule(rule: ReplicationRule) { + if(rule) { + this.search.ruleId = rule.id || ''; + this.search.repoName = ''; + this.search.status = ''; + this.currentJobSearchOption = 0; + this.currentJobStatus = { 'key': 'all', 'description': 'REPLICATION.ALL' }; + this.fetchReplicationJobs(); + } + } + + doSearchRules(ruleName: string) { + this.search.ruleName = ruleName; + this.retrievePolicies(); + } + + doFilterRuleStatus($event: any) { + if ($event && $event.target && $event.target["value"]) { + let status = $event.target["value"]; + this.currentRuleStatus = this.ruleStatus.find((r: any)=>r.key === status); + if(this.currentRuleStatus.key === 'all') { + this.changedRules = this.rules; + } else { + this.changedRules = this.rules.filter(policy=>policy.enabled === +this.currentRuleStatus.key); + } + } + } + + doFilterJobStatus($event: any) { + if ($event && $event.target && $event.target["value"]) { + let status = $event.target["value"]; + this.currentJobStatus = this.jobStatus.find((r: any)=>r.key === status); + if(this.currentJobStatus.key === 'all') { + status = ''; + } + this.search.status = status; + this.doSearchJobs(this.search.repoName); + } + } + + doSearchJobs(repoName: string) { + this.search.repoName = repoName; + this.fetchReplicationJobs(); + } + + reloadRules(isReady: boolean) { + if(isReady) { + this.search.ruleName = ''; + this.retrievePolicies(); + } + } + + refreshRules() { + this.retrievePolicies(); + } + + refreshJobs() { + this.fetchReplicationJobs(); + } + + toggleSearchJobOptionalName(option: number) { + (option === 1) ? this.currentJobSearchOption = 0 : this.currentJobSearchOption = 1; + } + + convertDate(strDate: string): string { + if(/^(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d$/.test(strDate)) { + let parts = strDate.split(/[-\/]/); + strDate = parts[2] /*Year*/ + '-' +parts[1] /*Month*/ + '-' + parts[0] /*Date*/; + } + return strDate; + } + + doJobSearchByStartTime(strDate: string) { + this.search.startTimestamp = ''; + if(this.fromTimeInput.valid && strDate) { + strDate = this.convertDate(strDate); + this.search.startTimestamp = new Date(strDate).getTime() / 1000 + ''; + } + this.fetchReplicationJobs(); + } + + doJobSearchByEndTime(strDate: string) { + this.search.endTimestamp = ''; + if(this.toTimeInput.valid && strDate) { + strDate = this.convertDate(strDate); + let oneDayOffset = 3600 * 24; + this.search.endTimestamp = (new Date(strDate).getTime() / 1000 + oneDayOffset) + ''; + } + this.fetchReplicationJobs(); + } +} \ No newline at end of file diff --git a/src/ui_ng/lib/src/service/interface.ts b/src/ui_ng/lib/src/service/interface.ts index ac76ccd2b..f814ed57c 100644 --- a/src/ui_ng/lib/src/service/interface.ts +++ b/src/ui_ng/lib/src/service/interface.ts @@ -75,8 +75,8 @@ export interface Tag extends Base { export interface Endpoint extends Base { endpoint: string; name: string; - username: string; - password: string; + username?: string; + password?: string; type: number; } @@ -87,9 +87,9 @@ export interface Endpoint extends Base { * @interface ReplicationRule */ export interface ReplicationRule extends Base { - project_id: number; + project_id: number | string; project_name: string; - target_id: number; + target_id: number | string; target_name: string; enabled: number; description?: string; diff --git a/src/ui_ng/lib/src/service/replication.service.ts b/src/ui_ng/lib/src/service/replication.service.ts index 6c2b52222..fda8d4975 100644 --- a/src/ui_ng/lib/src/service/replication.service.ts +++ b/src/ui_ng/lib/src/service/replication.service.ts @@ -84,7 +84,7 @@ export abstract class ReplicationService { * * @memberOf ReplicationService */ - abstract enableReplicationRule(ruleId: number | string): Observable | Promise | any; + abstract enableReplicationRule(ruleId: number | string, enablement: number): Observable | Promise | any; /** * Disable the specified replication rule. @@ -113,7 +113,7 @@ export abstract class ReplicationService { * * @memberOf ReplicationService */ - abstract getJobs(ruleId: number | string, queryParams?: RequestQueryParams): Observable | Promise | ReplicationJob[]; + abstract getJobs(ruleId: number | string, queryParams?: RequestQueryParams): Observable | Promise | ReplicationJob[] | Promise; } /** @@ -169,7 +169,7 @@ export class ReplicationDefaultService extends ReplicationService { } let url: string = `${this._ruleBaseUrl}/${ruleId}`; - return this.http.get(url, HTTP_JSON_OPTIONS).toPromise() + return this.http.get(url).toPromise() .then(response => response.json() as ReplicationRule) .catch(error => Promise.reject(error)); } @@ -206,13 +206,13 @@ export class ReplicationDefaultService extends ReplicationService { .catch(error => Promise.reject(error)); } - public enableReplicationRule(ruleId: number | string): Observable | Promise | any { + public enableReplicationRule(ruleId: number | string, enablement: number): Observable | Promise | any { if (!ruleId || ruleId <= 0) { return Promise.reject('Bad argument'); } let url: string = `${this._ruleBaseUrl}/${ruleId}/enablement`; - return this.http.put(url, { enabled: 1 }, HTTP_JSON_OPTIONS).toPromise() + return this.http.put(url, { enabled: enablement }, HTTP_JSON_OPTIONS).toPromise() .then(response => response) .catch(error => Promise.reject(error)); } @@ -228,7 +228,7 @@ export class ReplicationDefaultService extends ReplicationService { .catch(error => Promise.reject(error)); } - public getJobs(ruleId: number | string, queryParams?: RequestQueryParams): Observable | Promise | ReplicationJob[] { + public getJobs(ruleId: number | string, queryParams?: RequestQueryParams): Observable | Promise | ReplicationJob[] | Promise { if (!ruleId || ruleId <= 0) { return Promise.reject('Bad argument'); } @@ -239,7 +239,7 @@ export class ReplicationDefaultService extends ReplicationService { queryParams.set('policy_id', '' + ruleId); return this.http.get(this._jobBaseUrl, buildHttpRequestOptions(queryParams)).toPromise() - .then(response => response.json() as ReplicationJob[]) + .then(response => response) .catch(error => Promise.reject(error)); } } \ No newline at end of file