Add shareable endpoint components.

This commit is contained in:
kunw 2017-05-11 14:59:12 +08:00
parent c3c7b540f1
commit 5071dcf304
29 changed files with 1375 additions and 18 deletions

View File

@ -0,0 +1,21 @@
export const CONFIRMATION_DIALOG_STYLE: string = `
.confirmation-icon-inline {
display: inline-block;
}
.confirmation-title {
line-height: 24px;
color: #000000;
font-size: 22px;
}
.confirmation-content {
font-size: 14px;
color: #565656;
line-height: 24px;
display: inline-block;
vertical-align: middle;
width: 80%;
white-space: pre-wrap;
}
`;

View File

@ -0,0 +1,28 @@
export const CONFIRMATION_DIALOG_TEMPLATE: string = `
<clr-modal [(clrModalOpen)]="opened" [clrModalClosable]="false" [clrModalStaticBackdrop]="true">
<h3 class="modal-title" class="confirmation-title" style="margin-top: 0px;">{{dialogTitle}}</h3>
<div class="modal-body">
<div class="confirmation-icon-inline">
<clr-icon shape="warning" class="is-warning" size="64"></clr-icon>
</div>
<div class="confirmation-content">{{dialogContent}}</div>
</div>
<div class="modal-footer" [ngSwitch]="buttons">
<ng-template [ngSwitchCase]="0">
<button type="button" class="btn btn-outline" (click)="cancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="confirm()">{{ 'BUTTON.CONFIRM' | translate}}</button>
</ng-template>
<ng-template [ngSwitchCase]="1">
<button type="button" class="btn btn-outline" (click)="cancel()">{{'BUTTON.NO' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="confirm()">{{ 'BUTTON.YES' | translate}}</button>
</ng-template>
<ng-template [ngSwitchCase]="2">
<button type="button" class="btn btn-outline" (click)="cancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-danger" (click)="confirm()">{{ 'BUTTON.DELETE' | translate}}</button>
</ng-template>
<ng-template [ngSwitchCase]="3">
<button type="button" class="btn btn-primary" (click)="cancel()">{{'BUTTON.CLOSE' | translate}}</button>
</ng-template>
</div>
</clr-modal>
`;

View File

@ -0,0 +1,90 @@
// 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, EventEmitter, Output } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { ConfirmationMessage } from './confirmation-message';
import { ConfirmationAcknowledgement } from './confirmation-state-message';
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const';
import { CONFIRMATION_DIALOG_TEMPLATE } from './confirmation-dialog.component.html';
import { CONFIRMATION_DIALOG_STYLE } from './confirmation-dialog.component.css';
@Component({
selector: 'confirmation-dialog',
template: CONFIRMATION_DIALOG_TEMPLATE,
styles: [ CONFIRMATION_DIALOG_STYLE ]
})
export class ConfirmationDialogComponent {
opened: boolean = false;
dialogTitle: string = "";
dialogContent: string = "";
message: ConfirmationMessage;
buttons: ConfirmationButtons;
@Output() confirmAction = new EventEmitter<ConfirmationAcknowledgement>();
@Output() cancelAction = new EventEmitter<ConfirmationAcknowledgement>();
constructor(
private translate: TranslateService) {}
open(msg: ConfirmationMessage): void {
this.dialogTitle = msg.title;
this.dialogContent = msg.message;
this.message = msg;
this.translate.get(this.dialogTitle).subscribe((res: string) => this.dialogTitle = res);
this.translate.get(this.dialogContent, { 'param': msg.param }).subscribe((res: string) => this.dialogContent = res);
//Open dialog
this.buttons = msg.buttons;
this.opened = true;
}
close(): void {
this.opened = false;
}
cancel(): void {
if(!this.message){//Inproper condition
this.close();
return;
}
let data: any = this.message.data ? this.message.data : {};
let target = this.message.targetId ? this.message.targetId : ConfirmationTargets.EMPTY;
this.cancelAction.emit(new ConfirmationAcknowledgement(
ConfirmationState.CANCEL,
data,
target
));
this.close();
}
confirm(): void {
if(!this.message){//Inproper condition
this.close();
return;
}
let data: any = this.message.data ? this.message.data : {};
let target = this.message.targetId ? this.message.targetId : ConfirmationTargets.EMPTY;
let message = new ConfirmationAcknowledgement(
ConfirmationState.CONFIRMED,
data,
target
);
this.confirmAction.emit(message);
this.close();
}
}

View File

@ -0,0 +1,31 @@
// 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 { ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const';
export class ConfirmationMessage {
public constructor(title: string, message: string, param: string, data: any, targetId: ConfirmationTargets, buttons?: ConfirmationButtons) {
this.title = title;
this.message = message;
this.data = data;
this.targetId = targetId;
this.param = param;
this.buttons = buttons ? buttons : ConfirmationButtons.CONFIRM_CANCEL;
}
title: string;
message: string;
data: any = {};//default is empty
targetId: ConfirmationTargets = ConfirmationTargets.EMPTY;
param: string;
buttons: ConfirmationButtons;
}

View File

@ -0,0 +1,26 @@
// 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 { ConfirmationState, ConfirmationTargets } from '../shared/shared.const';
export class ConfirmationAcknowledgement {
constructor(state: ConfirmationState, data: any, source: ConfirmationTargets) {
this.state = state;
this.data = data;
this.source = source;
}
state: ConfirmationState = ConfirmationState.NA;
data: any = {};
source: ConfirmationTargets = ConfirmationTargets.EMPTY;
}

View File

@ -0,0 +1,7 @@
import { Type } from '@angular/core';
import { ConfirmationDialogComponent } from './confirmation-dialog.component';
export const CONFIRMATION_DIALOG_DIRECTIVES: Type<any>[] = [
ConfirmationDialogComponent
];

View File

@ -0,0 +1,6 @@
export const CREATE_EDIT_ENDPOINT_STYLE: string = `
.form-group-label-override {
font-size: 14px;
font-weight: 400;
}
`;

View File

@ -0,0 +1,53 @@
export const CREATE_EDIT_ENDPOINT_TEMPLATE: string = `<clr-modal [(clrModalOpen)]="createEditDestinationOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{modalTitle}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body">
<div class="alert alert-warning" *ngIf="!editable">
<div class="alert-item">
<span class="alert-text">
{{'DESTINATION.CANNOT_EDIT' | translate}}
</span>
</div>
</div>
<form #targetForm="ngForm">
<section class="form-block">
<div class="form-group">
<label for="destination_name" class="col-md-4 form-group-label-override">{{ 'DESTINATION.NAME' | translate }}<span style="color: red">*</span></label>
<label class="col-md-8" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)" [class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
<input type="text" id="destination_name" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.name" name="targetName" size="20" #targetName="ngModel" required (keyup)="changedTargetName($event)">
<span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
{{ 'DESTINATION.NAME_IS_REQUIRED' | translate }}
</span>
</label>
</div>
<div class="form-group">
<label for="destination_url" class="col-md-4 form-group-label-override">{{ 'DESTINATION.URL' | translate }}<span style="color: red">*</span></label>
<label class="col-md-8" for="destination_url" aria-haspopup="true" role="tooltip" [class.invalid]="targetEndpoint.errors && (targetEndpoint.dirty || targetEndpoint.touched)" [class.valid]="targetEndpoint.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-left">
<input type="text" id="destination_url" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.endpoint" size="20" name="endpointUrl" #targetEndpoint="ngModel" required (keyup)="clearPassword($event)">
<span class="tooltip-content" *ngIf="targetEndpoint.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
{{ 'DESTINATION.URL_IS_REQUIRED' | translate }}
</span>
</label>
</div>
<div class="form-group">
<label for="destination_username" class="col-md-4 form-group-label-override">{{ 'DESTINATION.USERNAME' | translate }}</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.username" size="20" name="username" #username="ngModel" (keyup)="clearPassword($event)">
</div>
<div class="form-group">
<label for="destination_password" class="col-md-4 form-group-label-override">{{ 'DESTINATION.PASSWORD' | translate }}</label>
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.password" size="20" name="password" #password="ngModel" (focus)="clearPassword($event)">
</div>
<div class="form-group">
<label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span>
<span [style.color]="!pingStatus ? 'red': ''" class="form-group-label-override">{{ pingTestMessage }}</span>
</div>
</section>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="testOngoing || targetEndpoint.errors">{{ 'DESTINATION.TEST_CONNECTION' | translate }}</button>
<button type="button" class="btn btn-outline" (click)="onCancel()" [disabled]="testOngoing">{{ 'BUTTON.CANCEL' | translate }}</button>
<button type="submit" class="btn btn-primary" (click)="onSubmit()" [disabled]="testOngoing || targetForm.form.invalid || !editable">{{ 'BUTTON.OK' | translate }}</button>
</div>
</clr-modal>`;

View File

@ -0,0 +1,96 @@
import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { SharedModule } from '../shared/shared.module';
import { FilterComponent } from '../filter/filter.component';
import { CreateEditEndpointComponent } from '../create-edit-endpoint/create-edit-endpoint.component';
import { InlineAlertComponent } from '../inline-alert/inline-alert.component';
import { ErrorHandler } from '../error-handler/error-handler';
import { Endpoint } from '../service/interface';
import { EndpointService, EndpointDefaultService } from '../service/endpoint.service';
import { IServiceConfig, SERVICE_CONFIG } from '../service.config';
describe('CreateEditEndpointComponent (inline template)', () => {
let mockData: Endpoint = {
"id": 1,
"endpoint": "https://10.117.4.151",
"name": "target_01",
"username": "admin",
"password": "",
"type": 0
};
let comp: CreateEditEndpointComponent;
let fixture: ComponentFixture<CreateEditEndpointComponent>;
let de: DebugElement;
let el: HTMLElement;
let config: IServiceConfig = {
systemInfoEndpoint: '/api/endpoints/testing'
};
let endpointService: EndpointService;
let spy: jasmine.Spy;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ SharedModule ],
declarations: [
FilterComponent,
CreateEditEndpointComponent,
InlineAlertComponent ],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: EndpointService, useClass: EndpointDefaultService },
{ provide: TranslateService, useClass: TranslateService}
]
});
}));
beforeEach(()=>{
fixture = TestBed.createComponent(CreateEditEndpointComponent);
comp = fixture.componentInstance;
endpointService = fixture.debugElement.injector.get(EndpointService);
spy = spyOn(endpointService, 'getEndpoint').and.returnValue(Promise.resolve(mockData));
fixture.detectChanges();
});
it('should be created', () => {
fixture.detectChanges();
expect(comp).toBeTruthy();
});
it('should get endpoint be called', async(()=>{
fixture.detectChanges();
comp.openCreateEditTarget(true, 1);
comp.createEditDestinationOpened = false;
fixture.whenStable().then(()=>{
fixture.detectChanges();
expect(spy.calls.any()).toBeTruthy();
});
}));
it('should get endpoint to open modal', async(()=>{
fixture.detectChanges();
comp.openCreateEditTarget(true, 1);
comp.createEditDestinationOpened = false;
fixture.whenStable().then(()=>{
fixture.detectChanges();
expect(comp.target.name).toEqual('target_01');
});
}));
it('should endpoint be initialized', () => {
fixture.detectChanges();
expect(config.systemInfoEndpoint).toEqual('/api/endpoints/testing');
});
});

View File

@ -0,0 +1,301 @@
// 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, Output, EventEmitter, ViewChild, AfterViewChecked } from '@angular/core';
import { NgForm } from '@angular/forms';
import { EndpointService } from '../service/endpoint.service';
import { ErrorHandler } from '../error-handler/index';
import { ActionType } from '../shared/shared.const';
import { InlineAlertComponent } from '../inline-alert/inline-alert.component';
import { Endpoint } from '../service/interface';
import { TranslateService } from '@ngx-translate/core';
import { CREATE_EDIT_ENDPOINT_STYLE } from './create-edit-endpoint.component.css';
import { CREATE_EDIT_ENDPOINT_TEMPLATE } from './create-edit-endpoint.component.html';
import { toPromise } from '../utils';
const FAKE_PASSWORD = 'rjGcfuRu';
@Component({
selector: 'create-edit-endpoint',
template: CREATE_EDIT_ENDPOINT_TEMPLATE,
styles: [ CREATE_EDIT_ENDPOINT_STYLE ]
})
export class CreateEditEndpointComponent implements AfterViewChecked {
modalTitle: string;
createEditDestinationOpened: boolean;
editable: boolean;
testOngoing: boolean;
pingTestMessage: string;
pingStatus: boolean;
actionType: ActionType;
target: Endpoint = Object.assign({}, this.initEndpoint);
initVal: Endpoint = Object.assign({}, this.initEndpoint);
targetForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('targetForm')
currentForm: NgForm;
hasChanged: boolean;
endpointHasChanged: boolean;
targetNameHasChanged: boolean;
@ViewChild(InlineAlertComponent)
inlineAlert: InlineAlertComponent;
@Output() reload = new EventEmitter<boolean>();
get initEndpoint(): Endpoint {
return {
endpoint: "",
name: "",
username: "",
password: "",
type: 0
};
}
constructor(
private endpointService: EndpointService,
private errorHandler: ErrorHandler,
private translateService: TranslateService) {}
openCreateEditTarget(editable: boolean, targetId?: number) {
this.target = Object.assign({}, this.initEndpoint);
this.editable = editable;
this.createEditDestinationOpened = true;
this.hasChanged = false;
this.endpointHasChanged = false;
this.targetNameHasChanged = false;
this.pingTestMessage = '';
this.pingStatus = true;
this.testOngoing = false;
if(targetId) {
this.actionType = ActionType.EDIT;
this.translateService.get('DESTINATION.TITLE_EDIT').subscribe(res=>this.modalTitle=res);
toPromise<Endpoint>(this.endpointService
.getEndpoint(targetId))
.then(
target=>{
this.target = target;
this.initVal.name = this.target.name;
this.initVal.endpoint = this.target.endpoint;
this.initVal.username = this.target.username;
this.initVal.password = FAKE_PASSWORD;
this.target.password = this.initVal.password;
})
.catch(error=>this.errorHandler.error(error));
} else {
this.actionType = ActionType.ADD_NEW;
this.translateService.get('DESTINATION.TITLE_ADD').subscribe(res=>this.modalTitle=res);
}
}
testConnection() {
this.translateService.get('DESTINATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res);
this.pingStatus = true;
this.testOngoing = !this.testOngoing;
let payload: Endpoint = Object.assign({}, this.initEndpoint);;
if(this.endpointHasChanged) {
payload.endpoint = this.target.endpoint;
payload.username = this.target.username;
payload.password = this.target.password;
} else {
payload.id = this.target.id;
}
toPromise<Endpoint>(this.endpointService
.pingEndpoint(payload))
.then(
response=>{
this.pingStatus = true;
this.translateService.get('DESTINATION.TEST_CONNECTION_SUCCESS').subscribe(res=>this.pingTestMessage=res);
this.testOngoing = !this.testOngoing;
}).catch(
error=>{
this.pingStatus = false;
this.translateService.get('DESTINATION.TEST_CONNECTION_FAILURE').subscribe(res=>this.pingTestMessage=res);
this.testOngoing = !this.testOngoing;
});
}
changedTargetName($event: any) {
if(this.editable) {
this.targetNameHasChanged = true;
}
}
clearPassword($event: any) {
if(this.editable) {
this.target.password = '';
this.endpointHasChanged = true;
}
}
onSubmit() {
switch(this.actionType) {
case ActionType.ADD_NEW:
toPromise<number>(this.endpointService
.createEndpoint(this.target))
.then(
response=>{
this.errorHandler.info('DESTINATION.CREATED_SUCCESS');
this.createEditDestinationOpened = false;
this.reload.emit(true);
})
.catch(
error=>{
let errorMessageKey = '';
switch(error.status) {
case 409:
errorMessageKey = 'DESTINATION.CONFLICT_NAME';
break;
case 400:
errorMessageKey = 'DESTINATION.INVALID_NAME';
break;
default:
errorMessageKey = 'UNKNOWN_ERROR';
}
this.translateService
.get(errorMessageKey)
.subscribe(res=>{
// if(this.messageHandlerService.isAppLevel(error)) {
// this.messageHandlerService.handleError(error);
// this.createEditDestinationOpened = false;
// } else {
// this.inlineAlert.showInlineError(res);
// }
this.errorHandler.error(res);
});
}
);
break;
case ActionType.EDIT:
if(!(this.targetNameHasChanged || this.endpointHasChanged)) {
this.createEditDestinationOpened = false;
return;
}
let payload: Endpoint = Object.assign({}, this.initEndpoint);
if(this.targetNameHasChanged) {
payload.name = this.target.name;
}
if (this.endpointHasChanged) {
payload.endpoint = this.target.endpoint;
payload.username = this.target.username;
payload.password = this.target.password;
delete payload.name;
}
toPromise<number>(this.endpointService
.updateEndpoint(this.target.id, payload))
.then(
response=>{
this.errorHandler.info('DESTINATION.UPDATED_SUCCESS');
this.createEditDestinationOpened = false;
this.reload.emit(true);
})
.catch(
error=>{
let errorMessageKey = '';
switch(error.status) {
case 409:this
errorMessageKey = 'DESTINATION.CONFLICT_NAME';
break;
case 400:
errorMessageKey = 'DESTINATION.INVALID_NAME';
break;
default:
errorMessageKey = 'UNKNOWN_ERROR';
}
this.translateService
.get(errorMessageKey)
.subscribe(res=>{
// if(this.messageHandlerService.isAppLevel(error)) {
// this.messageHandlerService.handleError(error);
// this.createEditDestinationOpened = false;
// } else {
// this.inlineAlert.showInlineError(res);
// }
this.errorHandler.error(res);
});
}
);
break;
}
}
onCancel() {
if(this.hasChanged) {
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
} else {
this.createEditDestinationOpened = false;
if(this.targetForm)
this.targetForm.reset();
}
}
confirmCancel(confirmed: boolean) {
this.createEditDestinationOpened = false;
this.inlineAlert.close();
}
ngAfterViewChecked(): void {
this.targetForm = this.currentForm;
if(this.targetForm) {
let comparison: {[key: string]: string} = {
targetName: this.initVal.name,
endpointUrl: this.initVal.endpoint,
username: this.initVal.username,
password: this.initVal.password
};
this.targetForm.valueChanges.subscribe(data=>{
for(let key in data) {
let current = data[key];
let origin: string = comparison[key];
if(((this.actionType === ActionType.EDIT && this.editable && !current) || current) &&
current !== origin) {
this.hasChanged = true;
break;
} else {
this.hasChanged = false;
this.inlineAlert.close();
}
}
});
}
}
}

View File

@ -0,0 +1,7 @@
import { Type } from '@angular/core';
import { CreateEditEndpointComponent } from './create-edit-endpoint.component';
export const CREATE_EDIT_ENDPOINT_DIRECTIVES: Type<any>[] = [
CreateEditEndpointComponent
];

View File

@ -0,0 +1,10 @@
export const ENDPOINT_STYLE: string = `
.option-left {
padding-left: 16px;
margin-top: 24px;
}
.option-right {
padding-right: 16px;
margin-top: 36px;
}
`;

View File

@ -0,0 +1,36 @@
export const ENDPOINT_TEMPLATE: string = `
<confirmation-dialog #confirmationDialog (confirmAction)="confirmDeletion($event)" (cancelAction)="cancelDeletion($event)"></confirmation-dialog>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between">
<div class="flex-items-xs-middle option-left">
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'DESTINATION.ENDPOINT' | translate}}</button>
<create-edit-endpoint (reload)="reload($event)"></create-edit-endpoint>
</div>
<div class="flex-items-xs-middle option-right">
<hbr-filter filterPlaceholder='{{"REPLICATION.FILTER_TARGETS_PLACEHOLDER" | translate}}' (filter)="doSearchTargets($event)" [currentValue]="targetName"></hbr-filter>
<a href="javascript:void(0)" (click)="refreshTargets()">
<clr-icon shape="refresh"></clr-icon>
</a>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid>
<clr-dg-column>{{'DESTINATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'DESTINATION.URL' | translate}}</clr-dg-column>
<clr-dg-column>{{'DESTINATION.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let t of targets" [clrDgItem]='t'>
<clr-dg-action-overflow>
<button class="action-item" (click)="editTarget(t)">{{'DESTINATION.TITLE_EDIT' | translate}}</button>
<button class="action-item" (click)="deleteTarget(t)">{{'DESTINATION.DELETE' | translate}}</button>
</clr-dg-action-overflow>
<clr-dg-cell>{{t.name}}</clr-dg-cell>
<clr-dg-cell>{{t.endpoint}}</clr-dg-cell>
<clr-dg-cell>{{t.creation_time | date: 'short'}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{ (targets ? targets.length : 0) }} {{'DESTINATION.ITEMS' | translate}}</clr-dg-footer>
</clr-datagrid>
</div>
</div>
`;

View File

@ -0,0 +1,133 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { SharedModule } from '../shared/shared.module';
import { EndpointComponent } from './endpoint.component';
import { FilterComponent } from '../filter/filter.component';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
import { CreateEditEndpointComponent } from '../create-edit-endpoint/create-edit-endpoint.component';
import { InlineAlertComponent } from '../inline-alert/inline-alert.component';
import { ErrorHandler } from '../error-handler/error-handler';
import { Endpoint } from '../service/interface';
import { EndpointService, EndpointDefaultService } from '../service/endpoint.service';
import { IServiceConfig, SERVICE_CONFIG } from '../service.config';
describe('EndpointComponent (inline template)', () => {
let mockData: Endpoint[] = [
{
"id": 1,
"endpoint": "https://10.117.4.151",
"name": "target_01",
"username": "admin",
"password": "",
"type": 0
},
{
"id": 2,
"endpoint": "https://10.117.5.142",
"name": "target_02",
"username": "AAA",
"password": "",
"type": 0
},
{
"id": 3,
"endpoint": "https://101.1.11.111",
"name": "target_03",
"username": "admin",
"password": "",
"type": 0
},
{
"id": 4,
"endpoint": "http://4.4.4.4",
"name": "target_04",
"username": "",
"password": "",
"type": 0
}
];
let mockOne: Endpoint = {
"id": 1,
"endpoint": "https://10.117.4.151",
"name": "target_01",
"username": "admin",
"password": "",
"type": 0
};
let comp: EndpointComponent;
let fixture: ComponentFixture<EndpointComponent>;
let de: DebugElement;
let el: HTMLElement;
let config: IServiceConfig = {
systemInfoEndpoint: '/api/endpoints/testing'
};
let endpointService: EndpointService;
let spy: jasmine.Spy;
let spyOnRules: jasmine.Spy;
let spyOne: jasmine.Spy;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ SharedModule ],
declarations: [
FilterComponent,
ConfirmationDialogComponent,
CreateEditEndpointComponent,
InlineAlertComponent,
EndpointComponent ],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: EndpointService, useClass: EndpointDefaultService }
]
});
}));
beforeEach(()=>{
fixture = TestBed.createComponent(EndpointComponent);
comp = fixture.componentInstance;
endpointService = fixture.debugElement.injector.get(EndpointService);
spy = spyOn(endpointService, 'getEndpoints').and.returnValues(Promise.resolve(mockData));
spyOnRules = spyOn(endpointService, 'getEndpointWithReplicationRules').and.returnValue([]);
spyOne = spyOn(endpointService, 'getEndpoint').and.returnValue(Promise.resolve(mockOne));
fixture.detectChanges();
});
it('should retrieve endpoint data', () => {
fixture.detectChanges();
expect(spy.calls.any()).toBeTruthy();
});
it('should endpoint be initialized', () => {
fixture.detectChanges();
expect(config.systemInfoEndpoint).toEqual('/api/endpoints/testing');
});
it('should open create endpoint modal', async(() => {
fixture.detectChanges();
comp.editTarget(mockOne);
fixture.whenStable().then(()=>{
fixture.detectChanges();
expect(comp.target.name).toEqual('target_01');
});
}));
it('should filter endpoints by keyword', async(() => {
fixture.detectChanges();
fixture.whenStable().then(()=>{
fixture.detectChanges();
comp.doSearchTargets('target_02');
fixture.detectChanges();
expect(comp.targets.length).toEqual(1);
});
}));
});

View File

@ -0,0 +1,181 @@
// 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, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Endpoint, ReplicationRule } from '../service/interface';
import { EndpointService } from '../service/endpoint.service';
import { ErrorHandler } from '../error-handler/index';
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../shared/shared.const';
import { Subscription } from 'rxjs/Subscription';
import { CreateEditEndpointComponent } from '../create-edit-endpoint/create-edit-endpoint.component';
import { ENDPOINT_STYLE } from './endpoint.component.css';
import { ENDPOINT_TEMPLATE } from './endpoint.component.html';
import { toPromise } from '../utils';
@Component({
selector: 'hbr-endpoint',
template: ENDPOINT_TEMPLATE,
styles: [ ENDPOINT_STYLE ],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EndpointComponent implements OnInit {
@ViewChild(CreateEditEndpointComponent)
createEditEndpointComponent: CreateEditEndpointComponent;
@ViewChild('confirmationDialog')
confirmationDialogComponent: ConfirmationDialogComponent;
targets: Endpoint[];
target: Endpoint;
targetName: string;
subscription: Subscription;
get initEndpoint(): Endpoint {
return {
endpoint: "",
name: "",
username: "",
password: "",
type: 0
};
}
constructor(
private endpointService: EndpointService,
private errorHandler: ErrorHandler,
private ref: ChangeDetectorRef) {
let hnd = setInterval(()=>ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000);
}
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.TARGET &&
message.state === ConfirmationState.CONFIRMED) {
let targetId = message.data;
toPromise<number>(this.endpointService
.deleteEndpoint(targetId))
.then(
response => {
this.errorHandler.error('DESTINATION.DELETED_SUCCESS');
this.reload(true);
}).catch(
error => {
if(error && error.status === 412) {
this.errorHandler.error('DESTINATION.FAILED_TO_DELETE_TARGET_IN_USED');
} else {
this.errorHandler.error(error);
}
});
}
}
cancelDeletion(message: ConfirmationAcknowledgement) {
console.log('Received message from cancelAction:' + JSON.stringify(message));
}
ngOnInit(): void {
this.targetName = '';
this.retrieve('');
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
retrieve(targetName: string): void {
toPromise<Endpoint[]>(this.endpointService
.getEndpoints(targetName))
.then(
targets => {
this.targets = targets || [];
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000);
}).catch(error => this.errorHandler.error(error));
}
doSearchTargets(targetName: string) {
this.targetName = targetName;
this.retrieve(targetName);
}
refreshTargets() {
this.retrieve('');
}
reload($event: any) {
this.targetName = '';
this.retrieve('');
}
openModal() {
this.createEditEndpointComponent.openCreateEditTarget(true);
this.target = this.initEndpoint;
}
editTarget(target: Endpoint) {
if (target) {
let editable = true;
toPromise<ReplicationRule[]>(this.endpointService
.getEndpointWithReplicationRules(target.id))
.then(
rules=>{
if(rules && rules.length > 0) {
for(let i = 0; i < rules.length; i++){
let p: ReplicationRule = rules[i];
if(p.enabled === 1) {
editable = false;
break;
}
}
}
this.createEditEndpointComponent.openCreateEditTarget(editable, +target.id);
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 1000);
})
.catch(error=>this.errorHandler.error(error));
}
}
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.id,
ConfirmationTargets.TARGET,
ConfirmationButtons.DELETE_CANCEL);
this.confirmationDialogComponent.open(deletionMessage);
}
}
}

View File

@ -0,0 +1,7 @@
import { Type } from '@angular/core';
import { EndpointComponent } from './endpoint.component';
export const ENDPOINT_DIRECTIVES: Type<any>[] = [
EndpointComponent
];

View File

@ -2,7 +2,14 @@ import { NgModule, ModuleWithProviders, Provider, APP_INITIALIZER, Inject } from
import { LOG_DIRECTIVES } from './log/index';
import { FILTER_DIRECTIVES } from './filter/index';
import { ENDPOINT_DIRECTIVES } from './endpoint/index';
import { CREATE_EDIT_ENDPOINT_DIRECTIVES } from './create-edit-endpoint/index';
import { SERVICE_CONFIG, IServiceConfig } from './service.config';
import { CONFIRMATION_DIALOG_DIRECTIVES } from './confirmation-dialog/index';
import { INLINE_ALERT_DIRECTIVES } from './inline-alert/index';
import {
AccessLogService,
AccessLogDefaultService,
@ -110,12 +117,21 @@ export function initConfig(translateService: TranslateService, config: IServiceC
],
declarations: [
LOG_DIRECTIVES,
FILTER_DIRECTIVES
FILTER_DIRECTIVES,
ENDPOINT_DIRECTIVES,
CREATE_EDIT_ENDPOINT_DIRECTIVES,
CONFIRMATION_DIALOG_DIRECTIVES,
INLINE_ALERT_DIRECTIVES
],
exports: [
LOG_DIRECTIVES,
FILTER_DIRECTIVES
]
FILTER_DIRECTIVES,
ENDPOINT_DIRECTIVES,
CREATE_EDIT_ENDPOINT_DIRECTIVES,
CONFIRMATION_DIALOG_DIRECTIVES,
INLINE_ALERT_DIRECTIVES
],
providers: []
})
export class HarborLibraryModule {

View File

@ -5,3 +5,4 @@ export * from './error-handler/index';
//export * from './utils';
export * from './log/index';
export * from './filter/index';
export * from './endpoint/index';

View File

@ -0,0 +1,7 @@
import { Type } from '@angular/core';
import { InlineAlertComponent } from './inline-alert.component';
export const INLINE_ALERT_DIRECTIVES: Type<any>[] = [
InlineAlertComponent
];

View File

@ -0,0 +1,10 @@
export const INLINE_ALERT_STYLE: string = `
.alert-text-blink {
color: red;
font-weight: bolder;
}
.alert-btn-link {
padding: 0px !important;
min-width: 30px !important;
}
`;

View File

@ -0,0 +1,13 @@
export const INLINE_ALERT_TEMPLATE: string = `
<clr-alert [clrAlertType]="inlineAlertType" [clrAlertClosable]="inlineAlertClosable" [(clrAlertClosed)]="alertClose" [clrAlertAppLevel]="useAppLevelStyle">
<div class="alert-item">
<span class="alert-text" [class.alert-text-blink]="blinking">
{{errorMessage}}
</span>
<div class="alert-actions" *ngIf="showCancelAction">
<button class="btn btn-sm btn-link alert-btn-link" (click)="close()">{{'BUTTON.NO' | translate}}</button>
<button class="btn btn-sm btn-link alert-btn-link" (click)="confirmCancel()">{{'BUTTON.YES' | translate}}</button>
</div>
</div>
</clr-alert>
`;

View File

@ -0,0 +1,99 @@
// 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 } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { errorHandler } from '../shared/shared.utils';
import { Observable } from 'rxjs/Rx';
import { Subscription } from "rxjs";
import { INLINE_ALERT_STYLE } from './inline-alert.component.css';
import { INLINE_ALERT_TEMPLATE } from './inline-alert.component.html';
@Component({
selector: 'inline-alert',
template: INLINE_ALERT_TEMPLATE,
styles: [ INLINE_ALERT_STYLE ]
})
export class InlineAlertComponent {
inlineAlertType: string = 'alert-danger';
inlineAlertClosable: boolean = false;
alertClose: boolean = true;
displayedText: string = "";
showCancelAction: boolean = false;
useAppLevelStyle: boolean = false;
timer: Subscription | null = null;
count: number = 0;
blinking: boolean = false;
@Output() confirmEvt = new EventEmitter<boolean>();
constructor(private translate: TranslateService) { }
public get errorMessage(): string {
return this.displayedText;
}
//Show error message inline
public showInlineError(error: any): void {
this.displayedText = errorHandler(error);
if (this.displayedText) {
this.translate.get(this.displayedText).subscribe((res: string) => this.displayedText = res);
}
this.inlineAlertType = 'alert-danger';
this.showCancelAction = false;
this.inlineAlertClosable = true;
this.alertClose = false;
this.useAppLevelStyle = false;
}
//Show confirmation info with action button
public showInlineConfirmation(warning: any): void {
this.displayedText = "";
if (warning && warning.message) {
this.translate.get(warning.message).subscribe((res: string) => this.displayedText = res);
}
this.inlineAlertType = 'alert-warning';
this.showCancelAction = true;
this.inlineAlertClosable = false;
this.alertClose = false;
this.useAppLevelStyle = false;
}
//Show inline sccess info
public showInlineSuccess(info: any): void {
this.displayedText = "";
if (info && info.message) {
this.translate.get(info.message).subscribe((res: string) => this.displayedText = res);
}
this.inlineAlertType = 'alert-success';
this.showCancelAction = false;
this.inlineAlertClosable = true;
this.alertClose = false;
this.useAppLevelStyle = false;
}
//Close alert
public close(): void {
this.alertClose = true;
}
public blink() {
}
confirmCancel(): void {
this.confirmEvt.emit(true);
}
}

View File

@ -1,10 +1,13 @@
import { TestBed, inject } from '@angular/core/testing';
import { SharedModule } from '../shared/shared.module';
import { EndpointService, EndpointDefaultService } from './endpoint.service';
describe('EndpointService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
SharedModule
],
providers: [
EndpointDefaultService,
{

View File

@ -1,7 +1,8 @@
import { Observable } from 'rxjs/Observable';
import { RequestQueryParams } from './RequestQueryParams';
import { Endpoint } from './interface';
import { Endpoint, ReplicationRule } from './interface';
import { Injectable } from "@angular/core";
import { Http } from '@angular/http';
import 'rxjs/add/observable/of';
/**
@ -80,6 +81,15 @@ export abstract class EndpointService {
* @memberOf EndpointService
*/
abstract pingEndpoint(endpoint: Endpoint): Observable<any> | Promise<any> | any;
/**
* Check endpoint whether in used with specific replication rule.
*
* @abstract
* @param {{number | string}} endpointId
* @returns {{Observable<any> | any}}
*/
abstract getEndpointWithReplicationRules(endpointId: number | string): Observable<any> | Promise<any> | any;
}
/**
@ -91,28 +101,71 @@ export abstract class EndpointService {
*/
@Injectable()
export class EndpointDefaultService extends EndpointService {
constructor(private http: Http){
super();
}
public getEndpoints(endpointName?: string, queryParams?: RequestQueryParams): Observable<Endpoint[]> | Promise<Endpoint[]> | Endpoint[] {
return Observable.of([]);
return this.http
.get(`/api/targets?name=${endpointName}`)
.toPromise()
.then(response=>response.json())
.catch(error=>Promise.reject(error));
}
public getEndpoint(endpointId: number | string): Observable<Endpoint> | Promise<Endpoint> | Endpoint {
return Observable.of({});
return this.http
.get(`/api/targets/${endpointId}`)
.toPromise()
.then(response=>response.json() as Endpoint)
.catch(error=>Promise.reject(error));
}
public createEndpoint(endpoint: Endpoint): Observable<any> | Promise<any> | any {
return Observable.of({});
return this.http
.post(`/api/targets`, JSON.stringify(endpoint))
.toPromise()
.then(response=>response.status)
.catch(error=>Promise.reject(error));
}
public updateEndpoint(endpointId: number | string, endpoint: Endpoint): Observable<any> | Promise<any> | any {
return Observable.of({});
return this.http
.put(`/api/targets/${endpointId}`, JSON.stringify(endpoint))
.toPromise()
.then(response=>response.status)
.catch(error=>Promise.reject(error));
}
public deleteEndpoint(endpointId: number | string): Observable<any> | Promise<any> | any {
return Observable.of({});
return this.http
.delete(`/api/targets/${endpointId}`)
.toPromise()
.then(response=>response.status)
.catch(error=>Promise.reject(error));
}
public pingEndpoint(endpoint: Endpoint): Observable<any> | Promise<any> | any {
return Observable.of({});
if(endpoint.id) {
return this.http
.post(`/api/targets/${endpoint.id}/ping`, {})
.toPromise()
.then(response=>response.status)
.catch(error=>Promise.reject(error));
}
return this.http
.post(`/api/targets/ping`, endpoint)
.toPromise()
.then(response=>response.status)
.catch(error=>Observable.throw(error));
}
public getEndpointWithReplicationRules(endpointId: number | string): Observable<any> | Promise<any> | any {
return this.http
.get(`/api/targets/${endpointId}/policies`)
.toPromise()
.then(response=>response.json() as ReplicationRule[])
.catch(error=>Promise.reject(error));
}
}

View File

@ -72,7 +72,13 @@ export interface Tag extends Base {
* @interface Endpoint
* @extends {Base}
*/
export interface Endpoint extends Base { }
export interface Endpoint extends Base {
endpoint: string;
name: string;
username: string;
password: string;
type: number;
}
/**
* Interface for replication rule.

View File

@ -114,7 +114,6 @@ export abstract class ReplicationService {
* @memberOf ReplicationService
*/
abstract getJobs(ruleId: number | string, queryParams?: RequestQueryParams): Observable<ReplicationJob[]> | Promise<ReplicationJob[]> | ReplicationJob[];
}
/**

View File

@ -0,0 +1,68 @@
// 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.
export const supportedLangs = ['en-us', 'zh-cn', 'es-es'];
export const enLang = "en-us";
export const languageNames = {
"en-us": "English",
"zh-cn": "中文简体",
"es-es": "Español"
};
export const enum AlertType {
DANGER, WARNING, INFO, SUCCESS
};
export const dismissInterval = 10 * 1000;
export const httpStatusCode = {
"Unauthorized": 401,
"Forbidden": 403
};
export const enum ConfirmationTargets {
EMPTY,
PROJECT,
PROJECT_MEMBER,
USER,
POLICY,
TOGGLE_CONFIRM,
TARGET,
REPOSITORY,
TAG,
CONFIG,
CONFIG_ROUTE,
CONFIG_TAB
};
export const enum ActionType {
ADD_NEW, EDIT
};
export const ListMode = {
READONLY: "readonly",
FULL: "full"
};
export const CommonRoutes = {
SIGN_IN: "/sign-in",
EMBEDDED_SIGN_IN: "/harbor/sign-in",
SIGN_UP: "/sign-in?sign_up=true",
EMBEDDED_SIGN_UP: "/harbor/sign-in?sign_up=true",
HARBOR_ROOT: "/harbor",
HARBOR_DEFAULT: "/harbor/projects"
};
export const enum ConfirmationState {
NA, CONFIRMED, CANCEL
}
export const enum ConfirmationButtons {
CONFIRM_CANCEL, YES_NO, DELETE_CANCEL, CLOSE
}

View File

@ -1,14 +1,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpModule } from '@angular/http';
import { HttpModule, Http } from '@angular/http';
import { ClarityModule } from 'clarity-angular';
import { FormsModule } from '@angular/forms';
import { TranslateModule, TranslateLoader, TranslateService, MissingTranslationHandler } from "@ngx-translate/core";
import { MyMissingTranslationHandler } from '../i18n/missing-trans.handler';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { Http } from '@angular/http';
import { TranslatorJsonLoader } from '../i18n/local-json.loader';
/*export function HttpLoaderFactory(http: Http) {
return new TranslateHttpLoader(http, 'i18n/lang/', '-lang.json');
}*/

View File

@ -0,0 +1,49 @@
// 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 { NgForm } from '@angular/forms';
import { httpStatusCode, AlertType } from './shared.const';
/**
* To handle the error message body
*
* @export
* @returns {string}
*/
export const errorHandler = function (error: any): string {
if (!error) {
return "UNKNOWN_ERROR";
}
if (!(error.statusCode || error.status)) {
//treat as string message
return '' + error;
} else {
switch (error.statusCode || error.status) {
case 400:
return "BAD_REQUEST_ERROR";
case 401:
return "UNAUTHORIZED_ERROR";
case 403:
return "FORBIDDEN_ERROR";
case 404:
return "NOT_FOUND_ERROR";
case 412:
return "PRECONDITION_FAILED";
case 409:
return "CONFLICT_ERROR";
case 500:
return "SERVER_ERROR";
default:
return "UNKNOWN_ERROR";
}
}
}