diff options
17 files changed, 1052 insertions, 355 deletions
diff --git a/catalog-ui/src/app/models/operation.ts b/catalog-ui/src/app/models/operation.ts index 2aa1332f06..6eeccecd88 100644 --- a/catalog-ui/src/app/models/operation.ts +++ b/catalog-ui/src/app/models/operation.ts @@ -3,15 +3,15 @@ export class OperationParameter { name: string; type: String; - property: string; - mandatory: boolean; + inputId: string; + required: boolean; constructor(param?: OperationParameter) { if (param) { this.name = param.name; this.type = param.type; - this.property = param.property; - this.mandatory = param.mandatory; + this.inputId = param.inputId; + this.required = param.required; } } } @@ -26,13 +26,13 @@ export class WORKFLOW_ASSOCIATION_OPTIONS { static EXISTING = 'EXISTING'; } -export class OperationModel { - operationType: string; +export class BEOperationModel { + name: string; description: string; uniqueId: string; - inputParams: IOperationParamsList; - outputParams: IOperationParamsList; + inputs: IOperationParamsList; + outputs: IOperationParamsList; workflowAssociationType: string; workflowId: string; @@ -40,12 +40,12 @@ export class OperationModel { constructor(operation?: any) { if (operation) { - this.operationType = operation.operationType; + this.name = operation.name; this.description = operation.description; this.uniqueId = operation.uniqueId; - this.inputParams = operation.inputParams; - this.outputParams = operation.outputParams; + this.inputs = operation.inputs; + this.outputs = operation.outputs; this.workflowAssociationType = operation.workflowAssociationType; this.workflowId = operation.workflowId; @@ -53,23 +53,66 @@ export class OperationModel { } } - public createInputParamsList(inputParams: Array<OperationParameter>): void { - this.inputParams = { - listToscaDataDefinition: inputParams + public createInputsList(inputs: Array<OperationParameter>): void { + this.inputs = { + listToscaDataDefinition: inputs }; } - public createOutputParamsList(outputParams: Array<OperationParameter>): void { - this.outputParams = { - listToscaDataDefinition: _.map(outputParams, output => { - const newOutput = {...output}; - delete newOutput.property; - return newOutput; + public createOutputsList(outputs: Array<OperationParameter>): void { + this.outputs = { + listToscaDataDefinition: _.map(outputs, output => { + delete output.inputId; + return output; }) }; } } -export interface CreateOperationResponse extends OperationModel { +export class OperationModel extends BEOperationModel { + interfaceType: string; + interfaceId: string; + + constructor(operation?: any) { + super(operation); + if (operation) { + this.interfaceId = operation.interfaceId; + this.interfaceType = operation.interfaceType; + } + } + + public displayName(): string { + const lastDot = this.name ? this.name.lastIndexOf('.') : -1; + return lastDot === -1 ? this.name : this.name.substr(lastDot + 1); + } +} + +export class CreateOperationResponse extends OperationModel { artifactUUID: string; + + constructor(operation?: any) { + super(operation); + if (operation) { + this.artifactUUID = operation.artifactUUID; + } + } +} + +export class InterfaceModel { + type: string; + uniqueId: string; + operations: Array<OperationModel>; + + constructor(interf?: any) { + if (interf) { + this.type = interf.type; + this.uniqueId = interf.uniqueId; + this.operations = interf.operations; + } + } + + public displayType(): string { + const lastDot = this.type ? this.type.lastIndexOf('.') : -1; + return lastDot === -1 ? this.type : this.type.substr(lastDot + 1); + } } diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.module.ts b/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.module.ts index 3414769e62..6292d85422 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.module.ts +++ b/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.module.ts @@ -1,7 +1,9 @@ import {NgModule} from "@angular/core"; import {CommonModule} from "@angular/common"; import {InterfaceOperationComponent} from "./interface-operation.page.component"; +import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; +import {TranslateModule} from "app/ng2/shared/translator/translate.module"; @NgModule({ declarations: [ @@ -9,7 +11,9 @@ import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; ], imports: [ CommonModule, - UiElementsModule + SdcUiComponentsModule, + UiElementsModule, + TranslateModule ], exports: [], entryComponents: [ diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.html b/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.html index c9fdf94fe0..afeff48d91 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.html +++ b/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.html @@ -1,56 +1,142 @@ <!-- - ~ Copyright © 2016-2018 European Support Limited - ~ - ~ 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. - --> - + ~ Copyright © 2016-2018 European Support Limited + ~ + ~ 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. + --> + <div class="interface-operation"> <loader [display]="isLoading" [size]="'large'" [relative]="true"></loader> <div - class="add-btn" + class="top-add-btn add-btn" + *ngIf="!isListEmpty()" [ngClass]="{'disabled': readonly}" data-tests-id="addInterfaceOperation" (click)="onEditOperation()"> - Add Operation + {{ 'INTERFACE_ADD_OPERATION' | translate }} </div> <div class="operation-list"> <div - class="operation-row" - *ngFor="let operation of operationList" - (click)="onEditOperation(operation)"> - - <span class="operation-info-container"> - <span class="operation-title"> - <p class="operation-text" data-tests-id="interfaceOperationType">{{operation.operationType}}</p> - </span> - - <span class="operation-description"> - <p class="operation-text" data-tests-id="interfaceOperationDescription">{{operation.description}}</p> - </span> - </span> - - <span class="operation-dumbo" *ngIf="!readonly"> - <span - class="sprite-new delete-item-icon" - data-tests-id="deleteInterfaceOperation" - (click)="onRemoveOperation($event, operation)"> - </span> - </span> + class="empty-list-container" + *ngIf="isListEmpty() && !isLoading"> + <div + class="empty-list-add-btn" + [ngClass]="{'disabled': readonly}" + (click)="onEditOperation()"> + <svg-icon + name="plus-circle" + mode="primary" + size="x_large" + [disabled]="readonly"> + </svg-icon> + <div class="button-text">{{ 'INTERFACE_ADD_OPERATION' | translate }}</div> + </div> + </div> + + <div *ngIf="!isListEmpty()"> + + <div class="expand-collapse"> + <a + class="link" + [ngClass]="{'disabled': isAllExpanded()}" + (click)="collapseAll(false)"> + {{ 'INTERFACE_EXPAND_ALL' | translate }} + </a> | + <a + class="link" + [ngClass]="{'disabled': isAllCollapsed()}" + (click)="collapseAll()"> + {{ 'INTERFACE_COLLAPSE_ALL' | translate }} + </a> + </div> + + <div + class="interface-row" + *ngFor="let interface of interfaces"> + <div + class="interface-accordion" + (click)="interface.toggleCollapse()"> + <span + class="chevron-container" + [ngClass]="{'isCollapsed': interface.isCollapsed}"> + <svg-icon + name="caret1-down-o" + mode="primary" + size="small"> + </svg-icon> + </span> + <span class="interface-name">{{interface.displayType()}}</span> + </div> + + <div + class="generic-table" + *ngIf="!interface.isCollapsed"> + <div class="header-row table-row"> + <span + class="cell header-cell field-name header-name"> + {{ 'INTERFACE_HEADER_NAME' | translate }} + </span> + <span + class="cell header-cell field-description header-description"> + {{ 'INTERFACE_HEADER_DESCRIPTION' | translate }} + </span> + <span + class="cell header-cell field-actions header-actions"> + â—â—â— + </span> + </div> + + <div + class="data-row" + *ngFor="let operation of interface.operations" + (click)="onEditOperation(operation)"> + <span + class="cell field-name"> + {{operation.name}} + </span> + <span + class="cell field-description" + [ngClass]="{'collapsed': operation.isCollapsed}"> + {{operation.getDescriptionEllipsis()}} + <span + class="more-or-less link" + (click)="operation.toggleCollapsed($event)"> + {{!operation.isEllipsis ? '' : operation.isCollapsed ? 'More' : 'Less'}} + </span> + </span> + <span class="cell field-actions"> + <span + class="delete-action" + data-tests-id="deleteOperation" + (click)="onRemoveOperation($event, operation)"> + <svg-icon + *ngIf="!readonly" + name="trash-o" + mode="info" + size="small" + [clickable]="true"> + </svg-icon> + </span> + </span> + </div> + + </div> + </div> </div> + </div> </div> diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.less b/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.less index 435502066e..b79e2f580c 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.less +++ b/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.less @@ -1,88 +1,196 @@ @import '../../../../assets/styles/variables.less'; +@import '../../../../assets/styles/override.less'; .interface-operation { - .add-btn { + font-size: 14px; + + .top-add-btn { position: relative; top: -31px; text-transform: uppercase; font-size: 14px; + font-family: @font-opensans-medium; } - a:not(.disabled) { - &:hover { - cursor: pointer; + .link { + color: @sdcui_color_blue; + text-decoration: underline; + font-family: @font-opensans-regular; + + &:not(.disabled) { + &:not(.empty-list-add-btn) { + &:hover { + color: @sdcui_color_dark-blue; + cursor: pointer; + } + } } } .operation-list { border-top: 1px solid @main_color_o; - padding-top: 25px; + padding-top: 5px; - .operation-row { + .empty-list-container { width: 100%; - border: 1px solid @main_color_o; display: flex; - justify-content: space-between; - align-items: center; - height: 100px; + justify-content: center; - &:hover { - border-color: @main_color_c; - cursor: pointer; + .empty-list-add-btn { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + border: 1px solid @main_color_o; + margin-top: 50px; + + height: 229px; + width: 480px; + + &.disabled { + pointer-events: none; + } + + &:hover { + &:not(.disabled) { + border: 1px solid @sdcui_color_blue; + cursor: pointer; + } + } + + .button-text { + margin-top: 9px; + font-family: @font-opensans-medium; + font-size: 16px; + text-transform: uppercase; + color: @sdcui_color_blue; + } } + } + + .expand-collapse { + margin-top: 4px; + margin-bottom: 18px; + color: @sdcui_color_light-gray; + } + + .interface-row { + width: 100%; + margin-top: 13px; + border-bottom: 1px solid @main_color_o; + padding-left: 4px; + min-height: 37px; + + + .interface-accordion { + cursor: pointer; - &:not(:first-child) { - margin-top: 20px; + .chevron-container { + position: relative; + margin-right: 5px; + + &.isCollapsed { + right: -6px; + top: 0; + * { + transform: rotate(270deg); + } + } + &:not(.isCollapsed) { + top: 6px; + } + * { + &:hover { + cursor: pointer; + } + } + } + .interface-name { + font-size: 18px; + font-family: @font-opensans-bold; + margin-bottom: 15px; + } } - .operation-info-container { - height: 100%; - display: flex; - align-items: center; - margin-right: 60px; + .generic-table { + margin-bottom: 24px; + margin-top: 10px; + margin-left: 22px; + font-size: 14px; - .operation-title, .operation-description { - display: flex; - align-items: center; + .header-row, .data-row { + .cell { + &.field-description { + flex: 2.5; + } - .operation-text { - overflow: hidden; - margin-bottom: 0; - max-height: 5rem; + &.field-actions { + flex-basis: 72px; + display: flex; + justify-content: center; + align-items: center; + } } } - .operation-title { - flex-shrink: 0; - width: 200px; - height: calc(100% - 13px); - border-right: 1px solid @main_color_o; - margin: 4px 0; - padding: 0 30px; + .header-row { + .cell { + background: @sdcui_color_silver; - .operation-text { - white-space: nowrap; - text-overflow: ellipsis; + &.field-actions { + font-size: 10px; + } } } - .operation-description { - padding-left: 30px; - text-align: left; - font-family: @font-opensans-regular; + .data-row { + cursor: pointer; + + &:hover { + background: @sdcui_color_light-silver; - .operation-text { - word-break: break-word; + .cell { + &.field-name { + color: @sdcui_color_dark-blue; + } + } } + + &:not(:hover) { + .field-actions { + visibility: hidden; + } + } + + .cell { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + + &.field-description { + &:not(.collapsed) { + white-space: normal; + } + &.collapsed { + text-overflow: clip; + } + .more-or-less { + margin-left: 5px; + } + } + + &.field-actions { + .delete-action { + position: relative; + top: 2px; + } + } + } + } } - .operation-dumbo { - padding-right: 20px; - display: flex; - flex-direction: column; - align-items: left; - } } } } diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.ts b/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.ts index e19d3457b3..e9b2001de1 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.ts +++ b/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.ts @@ -3,12 +3,17 @@ import {Component, Input, Output, ComponentRef, Inject} from '@angular/core'; import {Component as IComponent} from 'app/models/components/component'; import {SdcConfigToken, ISdcConfig} from "app/ng2/config/sdc-config.config"; +import {TranslateService} from "app/ng2/shared/translator/translate.service"; import {Observable} from "rxjs/Observable"; import {ModalComponent} from 'app/ng2/components/ui/modal/modal.component'; import {ModalService} from 'app/ng2/services/modal.service'; -import {ModalModel, ButtonModel, InputBEModel, OperationModel, CreateOperationResponse, WORKFLOW_ASSOCIATION_OPTIONS} from 'app/models'; +import {ModalModel, ButtonModel, InputBEModel, OperationModel, InterfaceModel, CreateOperationResponse, WORKFLOW_ASSOCIATION_OPTIONS} from 'app/models'; + +import {IModalConfig, IModalButtonComponent} from "sdc-ui/lib/angular/modals/models/modal-config"; +import {SdcUiComponents} from "sdc-ui/lib/angular"; +import {ModalButtonComponent} from "sdc-ui/lib/angular/components"; import {ComponentServiceNg2} from 'app/ng2/services/component-services/component.service'; import {ComponentGenericResponse} from 'app/ng2/services/responses/component-generic-response'; @@ -16,21 +21,96 @@ import {WorkflowServiceNg2} from 'app/ng2/services/workflow.service'; import {OperationCreatorComponent, OperationCreatorInput} from './operation-creator/operation-creator.component'; +export class UIOperationModel extends OperationModel { + isCollapsed: boolean = true; + isEllipsis: boolean; + MAX_LENGTH = 75; + _description: string; + + constructor(operation: OperationModel) { + super(operation); + + if (!operation.description) { + this.description = ''; + } + + if (this.description.length > this.MAX_LENGTH) { + this.isEllipsis = true; + } else { + this.isEllipsis = false; + } + } + + getDescriptionEllipsis(): string { + if (this.isCollapsed && this.description.length > this.MAX_LENGTH) { + return this.description.substr(0, this.MAX_LENGTH - 3) + '...'; + } + return this.description; + } + + toggleCollapsed(e) { + e.stopPropagation(); + this.isCollapsed = !this.isCollapsed; + } +} + +class ModalTranslation { + CREATE_TITLE: string; + EDIT_TITLE: string; + DELETE_TITLE: string; + CANCEL_BUTTON: string; + SAVE_BUTTON: string; + CREATE_BUTTON: string; + DELETE_BUTTON: string; + deleteText: Function; + + constructor(private TranslateService: TranslateService) { + this.TranslateService.languageChangedObservable.subscribe(lang => { + this.CREATE_TITLE = this.TranslateService.translate("INTERFACE_CREATE_TITLE"); + this.EDIT_TITLE = this.TranslateService.translate("INTERFACE_EDIT_TITLE"); + this.DELETE_TITLE = this.TranslateService.translate("INTERFACE_DELETE_TITLE"); + this.CANCEL_BUTTON = this.TranslateService.translate("INTERFACE_CANCEL_BUTTON"); + this.SAVE_BUTTON = this.TranslateService.translate("INTERFACE_SAVE_BUTTON"); + this.CREATE_BUTTON = this.TranslateService.translate("INTERFACE_CREATE_BUTTON"); + this.DELETE_BUTTON = this.TranslateService.translate("INTERFACE_DELETE_BUTTON"); + this.deleteText = (operationName) => this.TranslateService.translate("INTERFACE_DELETE_TEXT", {operationName}); + }); + } +} + +export class UIInterfaceModel extends InterfaceModel { + isCollapsed: boolean = false; + + constructor(interf?: any) { + super(interf); + this.operations = _.map( + this.operations, + operation => new UIOperationModel(operation) + ); + } + + toggleCollapse() { + this.isCollapsed = !this.isCollapsed; + } +} + @Component({ selector: 'interface-operation', templateUrl: './interface-operation.page.component.html', styleUrls: ['interface-operation.page.component.less'], - providers: [ModalService] + providers: [ModalService, TranslateService] }) export class InterfaceOperationComponent { + interfaces: Array<UIInterfaceModel>; modalInstance: ComponentRef<ModalComponent>; - operationList: Array<OperationModel> = []; openOperation: OperationModel; enableWorkflowAssociation: boolean; inputs: Array<InputBEModel>; isLoading: boolean; + interfaceTypes:{ [interfaceType: string]: Array<string> }; + modalTranslation: ModalTranslation; @Input() component: IComponent; @Input() readonly: boolean; @@ -40,40 +120,79 @@ export class InterfaceOperationComponent { constructor( @Inject(SdcConfigToken) private sdcConfig: ISdcConfig, @Inject("$state") private $state: ng.ui.IStateService, + private TranslateService: TranslateService, private ComponentServiceNg2: ComponentServiceNg2, private WorkflowServiceNg2: WorkflowServiceNg2, private ModalServiceNg2: ModalService, + private ModalServiceSdcUI: SdcUiComponents.ModalService ) { this.enableWorkflowAssociation = sdcConfig.enableWorkflowAssociation; + this.modalTranslation = new ModalTranslation(TranslateService); } ngOnInit(): void { this.isLoading = true; Observable.forkJoin( - this.ComponentServiceNg2.getInterfaceOperations(this.component), - this.ComponentServiceNg2.getComponentInputs(this.component) - ).subscribe((response) => { + this.ComponentServiceNg2.getInterfaces(this.component), + this.ComponentServiceNg2.getComponentInputs(this.component), + this.ComponentServiceNg2.getInterfaceTypes(this.component) + ).subscribe((response: Array<any>) => { this.isLoading = false; - this.component.interfaceOperations = response[0].interfaceOperations; - this.operationList = _.toArray(response[0].interfaceOperations).sort((a, b) => a.operationType.localeCompare(b.operationType)); + this.initInterfaces(response[0].interfaces); + this.sortInterfaces(); this.inputs = response[1].inputs; + this.interfaceTypes = response[2]; + }); + } + + initInterfaces(interfaces: Array<InterfaceModel>): void { + this.interfaces = _.map(interfaces, interf => new UIInterfaceModel(interf)); + } + + sortInterfaces(): void { + this.interfaces = _.filter(this.interfaces, interf => interf.operations && interf.operations.length > 0); // remove empty interfaces + this.interfaces.sort((a, b) => a.type.localeCompare(b.type)); // sort interfaces alphabetically + _.forEach(this.interfaces, interf => { + interf.operations.sort((a, b) => a.name.localeCompare(b.name)); // sort operations alphabetically + }); + } + + collapseAll(value: boolean = true): void { + _.forEach(this.interfaces, interf => { + interf.isCollapsed = value; }); } + isAllCollapsed(): boolean { + return _.every(this.interfaces, interf => interf.isCollapsed); + } + + isAllExpanded(): boolean { + return _.every(this.interfaces, interf => !interf.isCollapsed); + } + + isListEmpty(): boolean { + return _.filter( + this.interfaces, + interf => interf.operations && interf.operations.length > 0 + ).length === 0; + } + getDisabled = (): boolean => { return !this.modalInstance.instance.dynamicContent.instance.checkFormValidForSubmit(); } onEditOperation = (operation?: OperationModel): void => { + const modalMap = { create: { - modalTitle: 'Create a New Operation', - saveBtnText: 'Create', + modalTitle: this.modalTranslation.CREATE_TITLE, + saveBtnText: this.modalTranslation.CREATE_BUTTON, submitCallback: this.createOperation, }, edit: { - modalTitle: 'Edit Operation', - saveBtnText: 'Save', + modalTitle: this.modalTranslation.EDIT_TITLE, + saveBtnText: this.modalTranslation.SAVE_BUTTON, submitCallback: this.updateOperation, } }; @@ -86,23 +205,28 @@ export class InterfaceOperationComponent { } } - const cancelButton: ButtonModel = new ButtonModel( - 'Cancel', - 'outline white', - () => { + const cancelButton: IModalButtonComponent = { + id: 'cancelButton', + text: this.modalTranslation.CANCEL_BUTTON, + type: 'secondary', + size: 'small', + closeModal: true, + callback: () => { this.openOperation = null; - this.ModalServiceNg2.closeCurrentModal(); }, - ); - - const saveButton: ButtonModel = new ButtonModel( - modalData.saveBtnText, - 'blue', - () => { - this.modalInstance.instance.dynamicContent.instance.createParamLists(); - this.ModalServiceNg2.closeCurrentModal(); + }; - const {operation, isUsingExistingWF} = this.modalInstance.instance.dynamicContent.instance; + const saveButton: IModalButtonComponent = { + id: 'saveButton', + text: modalData.saveBtnText, + type: 'primary', + size: 'small', + closeModal: true, + callback: () => { + const modalInstance = this.ModalServiceSdcUI.getCurrentInstance().innerModalContent.instance; + + const {operation, isUsingExistingWF, createParamLists} = modalInstance; + createParamLists(); this.openOperation = {...operation}; if (this.enableWorkflowAssociation && !isUsingExistingWF()) { @@ -111,74 +235,81 @@ export class InterfaceOperationComponent { } modalData.submitCallback(operation); - }, - this.getDisabled, - ); - - const modalModel: ModalModel = new ModalModel( - 'l', - modalData.modalTitle, - '', - [saveButton, cancelButton], - 'standard', - ); - - this.modalInstance = this.ModalServiceNg2.createCustomModal(modalModel); + } + }; - let input: OperationCreatorInput = { - operation, + const input: OperationCreatorInput = { + inputOperation: operation, inputProperties: this.inputs, enableWorkflowAssociation: this.enableWorkflowAssociation, readonly: this.readonly, - isService: this.component.isService() - } + isService: this.component.isService(), + interfaceTypes: this.interfaceTypes, + validityChangedCallback: this.enableOrDisableSaveButton + }; - this.ModalServiceNg2.addDynamicContentToModal( - this.modalInstance, - OperationCreatorComponent, - input, - ); + const modalConfig: IModalConfig = { + title: modalData.modalTitle, + size: 'l', + type: 'custom', + buttons: [saveButton, cancelButton] as IModalButtonComponent[] + }; + + this.ModalServiceSdcUI.openCustomModal(modalConfig, OperationCreatorComponent, input); - this.modalInstance.instance.open(); + } + + private enableOrDisableSaveButton = (shouldEnable: boolean): void => { + let saveButton: ModalButtonComponent = this.ModalServiceSdcUI.getCurrentInstance().getButtonById('saveButton'); + saveButton.disabled = !shouldEnable; } onRemoveOperation = (event: Event, operation: OperationModel): void => { event.stopPropagation(); const confirmCallback = () => { - this.ModalServiceNg2.closeCurrentModal(); this.ComponentServiceNg2 .deleteInterfaceOperation(this.component, operation) .subscribe(() => { - const index = _.findIndex(this.operationList, el => el.uniqueId === operation.uniqueId); - this.operationList.splice(index, 1); - this.component.interfaceOperations = this.operationList; + const curInterf = _.find(this.interfaces, interf => interf.type === operation.interfaceType); + const index = _.findIndex(curInterf.operations, el => el.uniqueId === operation.uniqueId); + curInterf.operations.splice(index, 1); + if (!curInterf.operations.length) { + const interfIndex = _.findIndex(this.interfaces, interf => interf.type === operation.interfaceType); + this.interfaces.splice(interfIndex, 1); + } }); } - this.modalInstance = this.ModalServiceNg2.createActionModal( - operation.operationType, - 'Are you sure you want to delete this operation?', - 'Delete', + this.ModalServiceSdcUI.openAlertModal( + this.modalTranslation.DELETE_TITLE, + this.modalTranslation.deleteText(operation.name), + this.modalTranslation.DELETE_BUTTON, confirmCallback, - 'Cancel', + 'deleteOperationModal' ); - - this.modalInstance.instance.open(); } private createOperation = (operation: OperationModel): void => { this.ComponentServiceNg2.createInterfaceOperation(this.component, operation).subscribe((response: CreateOperationResponse) => { this.openOperation = null; - this.operationList.push(new OperationModel(response)); - this.operationList.sort((a, b) => a.operationType.localeCompare(b.operationType)); + let curInterf = _.find( + this.interfaces, + interf => interf.type === operation.interfaceType + ) + if (!curInterf) { + curInterf = new UIInterfaceModel({ + type: response.interfaceType, + uniqueId: response.uniqueId, + operations: [] + }); + this.interfaces.push(curInterf); + } + curInterf.operations.push(new UIOperationModel(response)); + this.sortInterfaces(); if (response.workflowId && operation.workflowAssociationType === WORKFLOW_ASSOCIATION_OPTIONS.EXISTING) { - const operationId = response.uniqueId; - const workflowId = response.workflowId; - const versionId = response.workflowVersionId; - const artifactId = response.artifactUUID; - this.WorkflowServiceNg2.associateWorkflowArtifact(this.component, operationId, workflowId, versionId, artifactId).subscribe(); + this.WorkflowServiceNg2.associateWorkflowArtifact(this.component, response).subscribe(); } else if (operation.workflowAssociationType === WORKFLOW_ASSOCIATION_OPTIONS.NEW) { this.$state.go('workspace.plugins', { path: 'workflowDesigner' }); } @@ -188,16 +319,22 @@ export class InterfaceOperationComponent { private updateOperation = (operation: OperationModel): void => { this.ComponentServiceNg2.updateInterfaceOperation(this.component, operation).subscribe(newOperation => { this.openOperation = null; - const index = _.findIndex(this.operationList, el => el.uniqueId === operation.uniqueId); - this.operationList.splice(index, 1, newOperation); - this.component.interfaceOperations = this.operationList; + + _.forEach(this.interfaces, interf => { + _.forEach(interf.operations, op => { + if (op.uniqueId === newOperation.uniqueId) { + const oldIndex = _.findIndex(interf.operations, el => el.uniqueId === op.uniqueId); + interf.operations.splice(oldIndex, 1); + } + }) + }); + + const newInterf = _.find(this.interfaces, interf => interf.type === operation.interfaceType); + newInterf.operations.push(new UIOperationModel(newOperation)); + this.sortInterfaces(); if (newOperation.workflowId && operation.workflowAssociationType === WORKFLOW_ASSOCIATION_OPTIONS.EXISTING) { - const operationId = newOperation.uniqueId; - const workflowId = newOperation.workflowId; - const versionId = newOperation.workflowVersionId; - const artifactId = newOperation.artifactUUID; - this.WorkflowServiceNg2.associateWorkflowArtifact(this.component, operationId, workflowId, versionId, artifactId).subscribe(); + this.WorkflowServiceNg2.associateWorkflowArtifact(this.component, newOperation).subscribe(); } }); } diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.html b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.html index 441875b2a9..81a33c4c8a 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.html +++ b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.html @@ -20,66 +20,92 @@ <form class="w-sdc-form"> <div class="side-by-side"> - <div class="i-sdc-form-item"> - <label class="i-sdc-form-label" [ngClass]="{'required': !isEditMode}">{{ 'OPERATION_NAME' | translate }}</label> - <input - type="text" - name="type" - data-tests-id="operationType" - [(ngModel)]="operation.operationType" - [attr.maxLength]="200" - [ngClass]="{'disabled':isEditMode}" /> + <div class="form-item"> + <sdc-dropdown + label="{{ 'OPERATION_INTERFACE_TYPE' | translate }}" + [required]="true" + testId="interface-name" + selectedOption="{{operation.interfaceType}}" + placeHolder="Select..." + [disabled]="readonly" + (changed)="onSelectInterface($event)" + [options]="interfaceNames"> + </sdc-dropdown> </div> - <div class="i-sdc-form-item"> - <label class="i-sdc-form-label">{{ 'OPERATION_DESCRIPTION' | translate }}</label> - <input - type="text" - data-tests-id="operationDescription" - name="description" - [ngClass]="{'disabled': readonly}" - [(ngModel)]="operation.description" - [attr.maxLength]="200" /> + <div class="form-item" *ngIf="!isInterfaceOther()"> + <sdc-dropdown + label="{{ 'OPERATION_NAME' | translate }}" + [required]="true" + testId="operation-name" + selectedOption="{{operation.name}}" + placeHolder="Select..." + [disabled]="readonly" + (changed)="onSelectOperationName($event)" + [options]="operationNames"> + </sdc-dropdown> </div> + <div class="form-item" *ngIf="isInterfaceOther()"> + <sdc-input + label="{{ 'OPERATION_NAME' | translate }}" + [(value)]="operation.name" + testId="operationName"> + </sdc-input> + </div> + </div> - <div class="side-by-side association-options"> - <div class="i-sdc-form-item" *ngIf="enableWorkflowAssociation"> - <label class="i-sdc-form-label">{{ 'OPERATION_WORKFLOW_ASSIGNMENT' | translate }}</label> - <ui-element-dropdown - data-tests-id="association-type" - [(value)]="operation.workflowAssociationType" - [values]="associationOptions" - (valueChange)="toggleAssociateWorkflow()" - [readonly]="readonly"> - </ui-element-dropdown> - </div> - <div></div> + <div class="i-sdc-form-item sdc-input"> + <span class="sdc-input__label">{{ 'OPERATION_DESCRIPTION' | translate }}</span> + <textarea + data-tests-id="operationDescription" + rows="2" + name="description" + [(ngModel)]="descriptionValue" + [ngClass]="{'disabled': readonly}"> + </textarea> </div> - <div class="side-by-side" *ngIf="isUsingExistingWF()"> - <div class="i-sdc-form-item"> - <label class="i-sdc-form-label required">{{ 'OPERATION_WORKFLOW' | translate }} + <div class="side-by-side" *ngIf="enableWorkflowAssociation"> + <div class="form-item"> + <sdc-dropdown + label="{{ 'OPERATION_WORKFLOW_ASSIGNMENT' | translate }}" + placeHolder="Select..." + testId="association-type" + selectedOption="{{workflowAssociationType}}" + [options]="associationOptions" + (changed)="toggleAssociateWorkflow($event)" + [disabled]="readonly"> + </sdc-dropdown> + </div> + + <div class="form-item" *ngIf="!isUsingExistingWF()"></div> + + <div class="form-item sdc-input" *ngIf="isUsingExistingWF()"> + <label class="sdc-input__label required">{{ 'OPERATION_WORKFLOW' | translate }} <span class="archive-warning" *ngIf="archivedWorkflowId === operation.workflowId">({{ 'OPERATION_WORKFLOW_ARCHIVED' | translate }})</span> + <span class="no-workflow-warning" *ngIf="!workflows.length">{{ 'OPERATION_NO_WORKFLOW_ERROR' | translate }}</span> </label> - <ui-element-dropdown - data-tests-id="associated-workflow" - [readonly]="readonly" - [values]="workflows" - [(value)]="operation.workflowId" - (valueChange)="onSelectWorkflow()"> - </ui-element-dropdown> + <sdc-dropdown + placeHolder="Select..." + testId="associated-workflow" + selectedOption="{{operation.workflowId}}" + [options]="workflows" + (changed)="onSelectWorkflow($event)" + [disabled]="readonly || !workflows.length"> + </sdc-dropdown> </div> - <div class="i-sdc-form-item"> - <label class="i-sdc-form-label required">{{ 'OPERATION_WORKFLOW_VERSION' | translate }}</label> - <ui-element-dropdown - data-tests-id="associated-workflow-version" - [readonly]="!operation.workflowId || archivedWorkflowId === operation.workflowId || readonly" - [values]="workflowVersions" - [(value)]="operation.workflowVersionId" - (valueChange)="changeWorkflowVersion()"> - </ui-element-dropdown> + <div class="form-item sdc-input" *ngIf="isUsingExistingWF()"> + <sdc-dropdown + *ngIf="workflows.length" + label="{{ 'OPERATION_WORKFLOW_VERSION' | translate }}" + testId="associated-workflow-version" + selectedOption="{{operation.workflowVersionId}}" + [options]="workflowVersions" + (changed)="changeWorkflowVersion($event)" + [disabled]="!operation.workflowId || archivedWorkflowId === operation.workflowId || readonly"> + </sdc-dropdown> </div> </div> @@ -93,7 +119,7 @@ *ngIf="!isUsingExistingWF() && !readonly" data-tests-id="addInputParameter" [ngClass]="{'disabled':!canAdd()}" - (click)="addParam()">{{ 'OPERATION_ADD_PARAMS' | translate }}</a> + (click)="addParam()">{{ currentTab === TYPE_INPUT ? 'Add Input' : 'Add Output' }}</a> </div> <div class="generic-table"> @@ -120,7 +146,6 @@ <span class="bold-message">{{ 'EMPTY_PARAM_TABLE_NO_SELECTED_WORKFLOW_1' | translate }}</span> <span>{{ 'EMPTY_PARAM_TABLE_NO_SELECTED_WORKFLOW_2' | translate }}</span> </div> - <div *ngIf="!workflows.length" [innerHTML]="'EMPTY_PARAM_TABLE_NO_WORKFLOWS' | translate"></div> </div> </div> @@ -132,7 +157,8 @@ [param]="param" [inputProps]="inputProperties" [onRemoveParam]="onRemoveParam" - [readonly]="readonly"> + [readonly]="readonly" + [validityChanged]="validityChanged"> </param-row> </div> diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.less b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.less index 10976ef473..1d65d98b9c 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.less +++ b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.less @@ -1,4 +1,5 @@ @import '../../../../../assets/styles/variables.less'; +@import '../../../../../assets/styles/override.less'; .operation-creator { font-family: @font-opensans-regular; @@ -10,32 +11,66 @@ font-size: 12px; } - .w-sdc-form .i-sdc-form-item { + .w-sdc-form .form-item { margin-bottom: 15px; } - .side-by-side { - display: flex; + textarea { + min-height: 74px; + margin-bottom: 18px; + } + + /deep/ .sdc-dropdown__component-container { + .sdc-dropdown__header { + height: 38px; + line-height: 35px; + + svg-icon { + margin: 13px 6px; + } + } + } + + /deep/ .sdc-input { + margin-bottom: 0; - &.association-options { - margin-top: 5px; + .sdc-input__input { + height: 38px; } + } + + .side-by-side { + display: flex; - .i-sdc-form-item { - flex-basis: 100%; + .form-item { + flex: 1; &:first-child { - flex-basis: 40%; - margin-right: 10px; + margin-right: 14px; + flex-basis: 37%; + flex-grow: 0; + flex-shrink: 0; } - .archive-warning { - font-family: @font-opensans-bold; - color: @main_color_i; + &:nth-child(3) { + margin-left: 14px; + flex: 0.4; } + } } + .archive-warning { + font-family: @font-opensans-bold; + color: @main_color_i; + } + + .no-workflow-warning { + font-family: @font-opensans-bold; + color: @sdcui_color_red; + float: right; + } + .input-param-title { font-size: 16px; text-transform: uppercase; @@ -51,24 +86,33 @@ cursor: pointer; } } + + .tab { + width: 84px; + text-align: center; + } } .generic-table { - max-height: 233px; + max-height: 244px; min-height: 91px; background: @main_color_p; .header-row .header-cell { - &.remove { - padding: 8px; - } .info-icon { float: right; + position: relative; + top: 2px; } /deep/ .tooltip-inner { - max-width: 280px; + padding: 2px; + max-width: 270px; font-size: 11px; } + &.remove { + padding: 10px; + font-size: 10px; + } } .data-row { diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.ts b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.ts index a304f1a30d..e1b2b4e079 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.ts +++ b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.ts @@ -7,15 +7,37 @@ import {TranslateService} from "app/ng2/shared/translator/translate.service"; import {WorkflowServiceNg2} from 'app/ng2/services/workflow.service'; import {OperationModel, OperationParameter, InputBEModel, RadioButtonModel, WORKFLOW_ASSOCIATION_OPTIONS} from 'app/models'; +import {IDropDownOption} from "sdc-ui/lib/angular/form-elements/dropdown/dropdown-models"; import {Tabs, Tab} from "app/ng2/components/ui/tabs/tabs.component"; import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; +export class DropDownOption implements IDropDownOption { + value: string; + label: string; + + constructor(value: string, label?: string) { + this.value = value; + this.label = label || value; + } +} + +class TypedDropDownOption extends DropDownOption { + type: number; + + constructor(value: string, label?: string, type?: number) { + super(value, label); + this.type = type; + } +} + export interface OperationCreatorInput { - operation: OperationModel, + inputOperation: OperationModel, inputProperties: Array<InputBEModel>, enableWorkflowAssociation: boolean, readonly: boolean, - isService: boolean + isService: boolean, + interfaceTypes: { [interfaceType: string]: Array<string> }, + validityChangedCallback: Function } @Component({ @@ -28,7 +50,12 @@ export interface OperationCreatorInput { export class OperationCreatorComponent { input: OperationCreatorInput; + inputOperation: OperationModel; operation: OperationModel; + interfaceNames: Array<TypedDropDownOption> = []; + interfaceTypes: { [interfaceType: string]: Array<string> }; + operationNames: Array<DropDownOption> = []; + validityChangedCallback: Function; workflows: Array<DropdownValue> = []; workflowVersions: Array<DropdownValue> = []; @@ -45,7 +72,8 @@ export class OperationCreatorComponent { tableParameters: Array<OperationParameter> = []; - associationOptions: Array<DropdownValue>; + associationOptions: Array<DropdownValue> = []; + workflowAssociationType: string; enableWorkflowAssociation: boolean; isEditMode: boolean = false; @@ -57,6 +85,9 @@ export class OperationCreatorComponent { TYPE_INPUT = 'Inputs'; TYPE_OUTPUT = 'Outputs'; + INTERFACE_OTHER_HEADER = 'Local Interfaces'; + INTERFACE_OTHER = 'Local'; + @ViewChild('propertyInputTabs') propertyInputTabs: Tabs; currentTab: String; @@ -65,24 +96,41 @@ export class OperationCreatorComponent { this.propertyTooltipText = this.translateService.translate("OPERATION_PROPERTY_TOOLTIP_TEXT"); this.associationOptions = [ - new DropdownValue(WORKFLOW_ASSOCIATION_OPTIONS.NONE, this.translateService.translate("NO_WORKFLOW_ASSOCIATION")), - new DropdownValue(WORKFLOW_ASSOCIATION_OPTIONS.EXISTING, this.translateService.translate("EXISTING_WORKFLOW_ASSOCIATION")) + new DropDownOption(WORKFLOW_ASSOCIATION_OPTIONS.NONE, this.translateService.translate("NO_WORKFLOW_ASSOCIATION")), + new DropDownOption(WORKFLOW_ASSOCIATION_OPTIONS.EXISTING, this.translateService.translate("EXISTING_WORKFLOW_ASSOCIATION")) ]; + + this.workflowAssociationType = this.operation.workflowAssociationType || WORKFLOW_ASSOCIATION_OPTIONS.NONE; }); this.currentTab = this.TYPE_INPUT; } - ngOnInit() { - this.readonly = this.input.readonly; - this.enableWorkflowAssociation = this.input.enableWorkflowAssociation; - this.inputProperties = this.input.inputProperties; + createInterfaceDropdown(type: string) { + let label = type; + const lastDot = label.lastIndexOf('.'); + if (lastDot > -1) { + label = label.substr(lastDot + 1); + } + return new TypedDropDownOption(type, label); + } - const inputOperation = this.input.operation; + ngOnInit() { + this.interfaceNames = _.map( + _.keys(this.interfaceTypes), + type => this.createInterfaceDropdown(type) + ); + this.interfaceNames.unshift(new TypedDropDownOption('Existing Interfaces', 'Existing Interfaces', 1)); + this.interfaceNames = this.interfaceNames.concat([ + new TypedDropDownOption(' ', ' ', 3), + new TypedDropDownOption(this.INTERFACE_OTHER_HEADER, this.INTERFACE_OTHER_HEADER, 1), + new TypedDropDownOption(this.INTERFACE_OTHER) + ]); + + const inputOperation = this.inputOperation; this.operation = new OperationModel(inputOperation || {}); - if (!inputOperation) { - this.operation.workflowAssociationType = WORKFLOW_ASSOCIATION_OPTIONS.NONE; - } + this.onSelectInterface(new DropDownOption(this.operation.interfaceType)); + this.validityChanged(); if (this.enableWorkflowAssociation) { this.isLoading = true; @@ -113,13 +161,16 @@ export class OperationCreatorComponent { } reconstructOperation = () => { - const inputOperation = this.input.operation; + const inputOperation = this.inputOperation; if (inputOperation) { if (this.enableWorkflowAssociation && inputOperation.workflowVersionId && this.isUsingExistingWF(inputOperation)) { - this.onSelectWorkflow(inputOperation.workflowVersionId).add(() => { - this.buildParams(); - this.updateTable(); - }); + const sub = this.onSelectWorkflow(new DropDownOption(inputOperation.workflowId), inputOperation.workflowVersionId); + if (sub) { + sub.add(() => { + this.buildParams(); + this.updateTable(); + }); + } } else { this.inputParameters = this.noAssignInputParameters; this.outputParameters = this.noAssignOutputParameters; @@ -132,14 +183,15 @@ export class OperationCreatorComponent { } } this.updateTable(); + this.validityChanged(); } buildParams = () => { - if (this.input.operation.outputParams) { + if (this.inputOperation.outputs) { this.currentTab = this.TYPE_OUTPUT; this.updateTable(); _.forEach( - [...this.input.operation.outputParams.listToscaDataDefinition].sort((a, b) => a.name.localeCompare(b.name)), + [...this.inputOperation.outputs.listToscaDataDefinition].sort((a, b) => a.name.localeCompare(b.name)), (output: OperationParameter) => { this.addParam(output); } @@ -147,9 +199,9 @@ export class OperationCreatorComponent { } this.currentTab = this.TYPE_INPUT; this.updateTable(); - if (this.input.operation.inputParams) { + if (this.inputOperation.inputs) { _.forEach( - [...this.input.operation.inputParams.listToscaDataDefinition].sort((a, b) => a.name.localeCompare(b.name)), + [...this.inputOperation.inputs.listToscaDataDefinition].sort((a, b) => a.name.localeCompare(b.name)), (input: OperationParameter) => { this.addParam(input); } @@ -157,15 +209,53 @@ export class OperationCreatorComponent { } } - onSelectWorkflow(selectedVersionId?: string): Subscription { + isInterfaceOther(): boolean { + return this.operation.interfaceType === this.INTERFACE_OTHER; + } + + onSelectInterface(interf: IDropDownOption) { + if (interf && this.operation.interfaceType !== interf.value) { + this.operation.name = undefined; + } + this.operation.interfaceType = interf && interf.value; + this.operationNames = !this.operation.interfaceType ? [] : ( + _.map( + this.interfaceTypes[this.operation.interfaceType], + name => new DropDownOption(name) + ) + ); + this.validityChanged(); + } + + onSelectOperationName(name: IDropDownOption) { + if (name) { + this.operation.name = name.value; + this.validityChanged(); + } + } + + get descriptionValue() { + return this.operation.description; + } + + set descriptionValue(v) { + this.operation.description = v; + this.validityChanged(); + } - this.operation.workflowVersionId = selectedVersionId || null; + onSelectWorkflow(workflowId: DropDownOption, selectedVersionId?: string): Subscription { + + if (_.isUndefined(workflowId) || workflowId.value === this.operation.workflowId) { + return; + } + this.operation.workflowId = workflowId.value; if (!this.assignInputParameters[this.operation.workflowId]) { this.assignInputParameters[this.operation.workflowId] = {}; this.assignOutputParameters[this.operation.workflowId] = {}; } this.isLoading = true; + this.validityChanged(); return this.workflowServiceNg2.getWorkflowVersions(this.operation.workflowId).subscribe((versions: Array<any>) => { this.isLoading = false; @@ -197,18 +287,34 @@ export class OperationCreatorComponent { this.operation.workflowVersionId = _.last(this.workflowVersions).value; } - this.changeWorkflowVersion(); + this.changeWorkflowVersion(new DropDownOption(this.operation.workflowVersionId)); + this.validityChanged(); }); } - changeWorkflowVersion() { + changeWorkflowVersion(versionId: DropDownOption) { + + if (_.isUndefined(versionId)) { + return; + } + + this.operation.workflowVersionId = versionId.value; this.inputParameters = this.assignInputParameters[this.operation.workflowId][this.operation.workflowVersionId]; this.outputParameters = this.assignOutputParameters[this.operation.workflowId][this.operation.workflowVersionId]; this.updateTable(); + this.validityChanged(); + } - toggleAssociateWorkflow() { + toggleAssociateWorkflow(type: DropDownOption) { + + if (_.isUndefined(type)) { + return; + } + + this.operation.workflowAssociationType = type.value; + this.workflowAssociationType = this.operation.workflowAssociationType; if (!this.isUsingExistingWF()) { this.inputParameters = this.noAssignInputParameters; @@ -224,15 +330,19 @@ export class OperationCreatorComponent { } this.updateTable(); + this.validityChanged(); } tabChanged = (event) => { + this.currentTab = event.title; this.updateTable(); + } updateTable() { + switch (this.currentTab) { case this.TYPE_INPUT: this.tableParameters = this.inputParameters; @@ -241,17 +351,20 @@ export class OperationCreatorComponent { this.tableParameters = this.outputParameters; break; } + } addParam(param?: OperationParameter): void { + this.validityChanged(); this.tableParameters.push(new OperationParameter(param)); } - canAdd(): boolean { + canAdd = (): boolean => { + let valid = true; if (this.currentTab === this.TYPE_INPUT) { _.forEach(this.inputParameters, param => { - if (!param.name || !param.property) { + if (!param.name || !param.inputId) { valid = false; } }); @@ -262,13 +375,16 @@ export class OperationCreatorComponent { } }); } + return valid; + } - isParamsValid(): boolean { + isParamsValid = (): boolean => { + let valid = true; _.forEach(this.inputParameters, param => { - if (!param.name || !param.property) { + if (!param.name || !param.inputId) { valid = false; } }); @@ -277,7 +393,9 @@ export class OperationCreatorComponent { valid = false; } }); + return valid; + } onRemoveParam = (param: OperationParameter): void => { @@ -285,9 +403,9 @@ export class OperationCreatorComponent { this.tableParameters.splice(index, 1); } - createParamLists(): void { - this.operation.createInputParamsList(this.inputParameters); - this.operation.createOutputParamsList(this.outputParameters); + createParamLists = () => { + this.operation.createInputsList(this.inputParameters); + this.operation.createOutputsList(this.outputParameters); } isUsingExistingWF = (operation?: OperationModel): boolean => { @@ -295,15 +413,20 @@ export class OperationCreatorComponent { return operation.workflowAssociationType === WORKFLOW_ASSOCIATION_OPTIONS.EXISTING; } - shouldCreateWF(operation?: OperationModel): boolean { + shouldCreateWF = (operation?: OperationModel): boolean => { operation = operation || this.operation; return this.operation.workflowAssociationType === WORKFLOW_ASSOCIATION_OPTIONS.NEW; } - checkFormValidForSubmit(): boolean { - return this.operation.operationType && + checkFormValidForSubmit = (): boolean => { + return this.operation.name && (!this.isUsingExistingWF() || this.operation.workflowVersionId) && this.isParamsValid(); } + validityChanged = () => { + let validState = this.checkFormValidForSubmit(); + this.validityChangedCallback(validState); + } + } diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.module.ts b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.module.ts index 7d881231d5..0b6f8336c3 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.module.ts +++ b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.module.ts @@ -3,8 +3,9 @@ import {CommonModule} from "@angular/common"; import {FormsModule} from "@angular/forms"; import {FormElementsModule} from "app/ng2/components/ui/form-components/form-elements.module"; -import {TranslateModule} from "app/ng2/shared/translator/translate.module"; +import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; +import {TranslateModule} from "app/ng2/shared/translator/translate.module"; import {OperationCreatorComponent} from "./operation-creator.component"; import {ParamRowComponent} from './param-row/param-row.component'; @@ -16,6 +17,7 @@ import {ParamRowComponent} from './param-row/param-row.component'; ], imports: [ CommonModule, + SdcUiComponentsModule, FormsModule, FormElementsModule, TranslateModule, diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.html b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.html index 9a5c101e87..1128d60e04 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.html +++ b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.html @@ -19,6 +19,7 @@ *ngIf="!isAssociateWorkflow" data-tests-id="paramName" [(value)]="param.name" + (valueChange)="onChangeName()" [readonly]="readonly"> </ui-element-input> <span *ngIf="isAssociateWorkflow">{{param.name}}</span> @@ -41,13 +42,14 @@ *ngIf="filteredInputProps.length || !isAssociateWorkflow" data-tests-id="paramProperty" [values]="filteredInputProps" - [(value)]="param.property" + value="paramId" + (valueChange)="onChangeProperty($event)" [readonly]="readonly"> </ui-element-dropdown> <span *ngIf="!filteredInputProps.length && isAssociateWorkflow" class="no-properties-error"> - No available properties of this type. + {{ 'PARAM_NONE_OF_TYPE' | translate }} </span> </div> @@ -55,15 +57,18 @@ <checkbox *ngIf="!isAssociateWorkflow" data-tests-id="paramMandatory" - [(checked)]="param.mandatory" + [(checked)]="param.required" [ngClass]="{'disabled':readonly}"> </checkbox> </div> <div class="cell remove" *ngIf="!isAssociateWorkflow && !readonly"> - <span - class="sprite-new delete-item-icon" - data-tests-id="removeInputParam" - (click)="onRemoveParam(param)"> - </span> + <svg-icon + name="trash-o" + mode="info" + size="small" + testId="removeInputParam" + (click)="onRemoveParam(param)" + [clickable]="true"> + </svg-icon> </div> diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.less b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.less index 2c2625d778..99a54bb65c 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.less +++ b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.less @@ -5,7 +5,10 @@ align-items: center; justify-content: center; - .delete-item-icon { + svg-icon { + position: relative; + right: -3px; + &:hover { cursor: pointer; } diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.ts b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.ts index a12425d6e9..8837a17bd9 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.ts +++ b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.ts @@ -1,6 +1,7 @@ import {Component, Input} from '@angular/core'; import {DataTypeService} from "app/ng2/services/data-type.service"; import {OperationParameter, InputBEModel} from 'app/models'; +import {DropDownOption} from "../operation-creator.component"; import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; @Component({ @@ -16,8 +17,10 @@ export class ParamRowComponent { @Input() isAssociateWorkflow: boolean; @Input() readonly: boolean; @Input() isInputParam: boolean; + @Input() validityChanged: Function; - propTypeEnum: Array<String> = []; + paramId: string; + propTypeEnum: Array<DropDownOption> = []; filteredInputProps: Array<DropdownValue> = []; constructor(private dataTypeService: DataTypeService) {} @@ -26,20 +29,48 @@ export class ParamRowComponent { this.propTypeEnum = _.uniq( _.map( this.getPrimitiveSubtypes(), - prop => prop.type + prop => new DropDownOption(prop.type) ) ); this.onChangeType(); + this.validityChanged(); } - onChangeType() { + onChangeName() { + this.validityChanged(); + } + + onChangeType(paramId?: string) { this.filteredInputProps = _.map( _.filter( this.getPrimitiveSubtypes(), - prop => prop.type === this.param.type + prop => !this.param.type || prop.type === this.param.type ), prop => new DropdownValue(prop.uniqueId, prop.name) ); + if (paramId) { + this.paramId = paramId; + } + } + + onChangeProperty(paramId: string) { + this.param.inputId = paramId; + const newProp = _.find( + this.getPrimitiveSubtypes(), + prop => this.param.inputId === prop.uniqueId + ); + + if (!this.param.type) { + this.param.type = newProp.type; + this.onChangeType(paramId); + } else { + this.paramId = paramId; + } + + if (!this.param.name) { + this.param.name = newProp.name; + } + this.validityChanged(); } getPrimitiveSubtypes(): Array<InputBEModel> { diff --git a/catalog-ui/src/app/ng2/services/component-services/component.service.ts b/catalog-ui/src/app/ng2/services/component-services/component.service.ts index 26b0291156..97e62daba7 100644 --- a/catalog-ui/src/app/ng2/services/component-services/component.service.ts +++ b/catalog-ui/src/app/ng2/services/component-services/component.service.ts @@ -24,9 +24,9 @@ import {Observable} from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/toPromise'; import {Response, URLSearchParams} from '@angular/http'; -import { Component, InputBEModel, InstancePropertiesAPIMap, FilterPropertiesAssignmentData, OperationModel, CreateOperationResponse} from "app/models"; +import { Component, InputBEModel, InstancePropertiesAPIMap, FilterPropertiesAssignmentData, OperationModel, BEOperationModel, CreateOperationResponse} from "app/models"; import {downgradeInjectable} from '@angular/upgrade/static'; -import {COMPONENT_FIELDS} from "app/utils"; +import {COMPONENT_FIELDS, CommonUtils} from "app/utils"; import {ComponentGenericResponse} from "../responses/component-generic-response"; import {InstanceBePropertiesMap} from "../../../models/properties-inputs/property-fe-map"; import {API_QUERY_PARAMS} from "app/utils"; @@ -122,38 +122,79 @@ export class ComponentServiceNg2 { return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_PROPERTIES]); } - getInterfaceOperations(component:Component):Observable<ComponentGenericResponse> { + getInterfaces(component:Component):Observable<ComponentGenericResponse> { return this.getComponentDataByFieldsName(component.componentType, component.uniqueId, [COMPONENT_FIELDS.COMPONENT_INTERFACE_OPERATIONS]); } getInterfaceOperation(component:Component, operation:OperationModel):Observable<OperationModel> { - return this.http.get(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/interfaceOperations/' + operation.uniqueId) - .map((res:Response) => res.json()); + return this.http.get(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/interfaces/' + operation.interfaceId + '/operations/' + operation.uniqueId) + .map((res:Response) => { + return res.json(); + }); } createInterfaceOperation(component:Component, operation:OperationModel):Observable<CreateOperationResponse> { const operationList = { - 'interfaceOperations': { - [operation.operationType]: operation + 'interfaces': { + [operation.interfaceType]: { + 'type': operation.interfaceType, + 'operations': { + [operation.name]: new BEOperationModel(operation) + } + } } }; return this.http.post(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/interfaceOperations', operationList) - .map((res:Response) => res.json()); + .map((res:Response) => { + const interf = _.find(res.json().interfaces, (interf: any) => interf.type === operation.interfaceType); + const newOperation = _.find(interf.operations, (op:OperationModel) => op.name === operation.name); + return new CreateOperationResponse({ + ...newOperation, + interfaceType: interf.type, + interfaceId: interf.uniqueId + }); + }); } updateInterfaceOperation(component:Component, operation:OperationModel):Observable<CreateOperationResponse> { const operationList = { - 'interfaceOperations': { - [operation.operationType]: operation + 'interfaces': { + [operation.interfaceType]: { + 'type': operation.interfaceType, + 'operations': { + [operation.name]: new BEOperationModel(operation) + } + } } }; return this.http.put(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/interfaceOperations', operationList) - .map((res:Response) => res.json()); + .map((res:Response) => { + const interf = _.find(res.json().interfaces, (interf: any) => interf.type === operation.interfaceType); + const newOperation = _.find(interf.operations, (op:OperationModel) => op.name === operation.name); + return new CreateOperationResponse({ + ...newOperation, + interfaceType: interf.type, + interfaceId: interf.uniqueId + }); + }); } deleteInterfaceOperation(component:Component, operation:OperationModel):Observable<OperationModel> { - return this.http.delete(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/interfaceOperations/' + operation.uniqueId) - .map((res:Response) => res.json()); + return this.http.delete(this.baseUrl + component.getTypeUrl() + component.uniqueId + '/interfaces/' + operation.interfaceId + '/operations/' + operation.uniqueId) + .map((res:Response) => { + return res.json(); + }); + } + + getInterfaceTypes(component:Component):Observable<{[id:string]: Array<string>}> { + return this.http.get(this.baseUrl + 'interfaceLifecycleTypes') + .map((res:Response) => { + const interfaceMap = {}; + _.forEach(res.json(), (interf:any) => { + interfaceMap[interf.toscaPresentation.type] = _.keys(interf.toscaPresentation.operations); + }); + return interfaceMap; + }); } getCapabilitiesAndRequirements(componentType: string, componentId:string):Observable<ComponentGenericResponse> { diff --git a/catalog-ui/src/app/ng2/services/responses/component-generic-response.ts b/catalog-ui/src/app/ng2/services/responses/component-generic-response.ts index a77133e09f..647cc927f7 100644 --- a/catalog-ui/src/app/ng2/services/responses/component-generic-response.ts +++ b/catalog-ui/src/app/ng2/services/responses/component-generic-response.ts @@ -92,6 +92,7 @@ export class ComponentGenericResponse implements Serializable<ComponentGenericR this.toscaArtifacts = new ArtifactGroupModel(response.toscaArtifacts); } if(response.interfaces) { + this.interfaces = CommonUtils.initInterfaces(response.interfaces); this.interfaceOperations = CommonUtils.initInterfaceOperations(response.interfaces); } if(response.metadata) { diff --git a/catalog-ui/src/app/ng2/services/workflow.service.ts b/catalog-ui/src/app/ng2/services/workflow.service.ts index 1fc5c7843b..36d56d6fa7 100644 --- a/catalog-ui/src/app/ng2/services/workflow.service.ts +++ b/catalog-ui/src/app/ng2/services/workflow.service.ts @@ -3,7 +3,17 @@ import { Response } from "@angular/http"; import { Observable } from "rxjs/Observable"; import { HttpService } from "./http.service"; import { SdcConfigToken, ISdcConfig } from "../config/sdc-config.config"; -import { Component } from "app/models"; +import { Component, CreateOperationResponse } from "app/models"; + +interface WorkflowOutputParameter { + name: string, + type: string, + mandatory: boolean +} + +interface WorkflowInputParameter extends WorkflowOutputParameter { + property: string; +} @Injectable() export class WorkflowServiceNg2 { @@ -20,9 +30,9 @@ export class WorkflowServiceNg2 { this.catalogBaseUrl = sdcConfig.api.POST_workflow_artifact; } - public associateWorkflowArtifact(component: Component, operationId: string, workflowId: string, workflowVersionId: string, artifactUuid: string): Observable<any> { - return this.http.post(this.baseUrl + '/workflows/' + workflowId + '/versions/' + workflowVersionId + '/artifact-deliveries', { - endpoint: this.catalogBaseUrl + '/' + component.getTypeUrl() + component.uuid + '/interfaces/' + operationId + '/artifacts/' + artifactUuid, + public associateWorkflowArtifact(component: Component, operation: CreateOperationResponse): Observable<any> { + return this.http.post(this.baseUrl + '/workflows/' + operation.workflowId + '/versions/' + operation.workflowVersionId + '/artifact-deliveries', { + endpoint: this.catalogBaseUrl + '/' + component.getTypeUrl() + component.uuid + '/interfaces/' + operation.interfaceId + '/operations/' + operation.uniqueId + '/artifacts/' + operation.artifactUUID, method: 'POST' }) .map((res:Response) => { @@ -40,7 +50,7 @@ export class WorkflowServiceNg2 { public getWorkflowVersions(workflowId: string, filterCertified: boolean = true): Observable<any> { return this.http.get(this.baseUrl + '/workflows/' + workflowId + '/versions' + (filterCertified ? '?state=' + this.VERSION_STATE_CERTIFIED : '')) .map((res:Response) => { - return res.json().items; + return _.map(res.json().items, version => version); }); } diff --git a/catalog-ui/src/app/utils/common-utils.ts b/catalog-ui/src/app/utils/common-utils.ts index 7ba50fdcf1..c5259f0d30 100644 --- a/catalog-ui/src/app/utils/common-utils.ts +++ b/catalog-ui/src/app/utils/common-utils.ts @@ -19,9 +19,9 @@ */ import * as _ from "lodash"; -import {Module, AttributeModel, ResourceInstance, PropertyModel, InputFEModel, OperationModel} from "../models"; -import {ComponentInstanceFactory} from "./component-instance-factory"; -import {InputBEModel, PropertyBEModel, RelationshipModel} from "app/models"; +import { ComponentInstanceFactory } from "./component-instance-factory"; +import { Module, AttributeModel, ResourceInstance, PropertyModel, InputFEModel, InterfaceModel, OperationModel } from "../models"; +import { InputBEModel, PropertyBEModel, RelationshipModel } from "app/models"; import { PolicyInstance } from "app/models/graph/zones/policy-instance"; import { GroupInstance } from "../models/graph/zones/group-instance"; @@ -139,44 +139,55 @@ export class CommonUtils { return groups; } - static initInterfaceOperations(interfaces: any): Array<OperationModel> { + static initInterfaces(interfaces: Array<InterfaceModel>): Array<InterfaceModel> { - return _.reduce(interfaces, (acc, interf: any) => { + return _.map(interfaces, (interf: InterfaceModel) => { + + return new InterfaceModel({ + type: interf.type, + uniqueId: interf.uniqueId, + operations: _.map(interf.operations, + (operation: OperationModel) => { + const newOperation = new OperationModel(operation); + newOperation.interfaceType = interf.type; + newOperation.interfaceId = interf.uniqueId; + + const {inputs, outputs} = operation; + if (inputs) { + newOperation.createInputsList(inputs.listToscaDataDefinition); + } + if (outputs) { + newOperation.createOutputsList(outputs.listToscaDataDefinition); + } + + return newOperation; + } + ) + }); + + }); + } + + static initInterfaceOperations(interfaces: Array<InterfaceModel>): Array<OperationModel> { + + return _.reduce(interfaces, (acc, interf: InterfaceModel) => { return acc.concat( _.map(interf.operations, - ({description, name, uniqueId, inputs, outputs, workflowId, workflowVersionId, workflowAssociationType}) => { - const operation = new OperationModel({ - description, - operationType: name, - uniqueId, - workflowAssociationType, - workflowId, - workflowVersionId - }); + (operation: OperationModel) => { + const newOperation = new OperationModel(operation); + newOperation.interfaceType = interf.type; + newOperation.interfaceId = interf.uniqueId; + + const {inputs, outputs} = operation; if (inputs) { - const inputParams = _.map(inputs.listToscaDataDefinition, (input:any) => { - return { - name: input.name, - property: input.inputId, - type: input.type, - mandatory: input.mandatory - }; - }); - operation.createInputParamsList(inputParams); + newOperation.createInputsList(inputs.listToscaDataDefinition); } if (outputs) { - const outputParams = _.map(outputs.listToscaDataDefinition, (output:any) => { - return { - name: output.name, - property: output.inputId, - type: output.type, - mandatory: output.mandatory - }; - }); - operation.createOutputParamsList(outputParams); + newOperation.createOutputsList(outputs.listToscaDataDefinition); } - return operation; + + return newOperation; } ) ); diff --git a/catalog-ui/src/assets/languages/en_US.json b/catalog-ui/src/assets/languages/en_US.json index 02bc58ffb4..8e43f18bce 100644 --- a/catalog-ui/src/assets/languages/en_US.json +++ b/catalog-ui/src/assets/languages/en_US.json @@ -451,6 +451,23 @@ "SERVICE_PATH_SELECTOR_HIDE_ALL_VALUE" : "⚊ Hide all ⚊", "SERVICE_PATH_SELECTOR_SHOW_ALL_VALUE" : "⚊ Show all ⚊", + "=========== INTERFACE OPERATION ==========": "", + "INTERFACE_ADD_OPERATION": "Add Operation", + "INTERFACE_EXPAND_ALL": "Expand All", + "INTERFACE_COLLAPSE_ALL": "Collapse All", + "INTERFACE_HEADER_NAME": "Name", + "INTERFACE_HEADER_DESCRIPTION": "Description", + + "=========== INTERFACE OPERATION MODAL ATTRIBUTES =========": "", + "INTERFACE_CREATE_TITLE": "Create New Operation", + "INTERFACE_EDIT_TITLE": "Edit Operation", + "INTERFACE_DELETE_TITLE": "Delete Operation", + "INTERFACE_CANCEL_BUTTON": "Cancel", + "INTERFACE_SAVE_BUTTON": "Save", + "INTERFACE_CREATE_BUTTON": "Create", + "INTERFACE_DELETE_BUTTON": "Delete", + "INTERFACE_DELETE_TEXT": "Are you sure you want to delete {{operationName}}?", + "=========== OPERATION CREATOR ============": "", "OPERATION_PROPERTY_TOOLTIP_TEXT": "VNF properties are defined by the input parameter type. In case you can't find a certain parameter, it might be due to a wrong type selection.", "SERVICE_OPERATION_PROPERTY_TOOLTIP_TEXT": "Service properties are defined by the input parameter type. In case you can't find a certain parameter, it might be due to a wrong type selection.", @@ -458,10 +475,12 @@ "NEW_WORKFLOW_ASSOCIATION": "New Workflow", "EXISTING_WORKFLOW_ASSOCIATION": "Existing Workflow", + "OPERATION_INTERFACE_TYPE": "Interface Name", "OPERATION_NAME": "Operation Name", "OPERATION_DESCRIPTION": "Description", "OPERATION_WORKFLOW_ASSIGNMENT": "Workflow Assignment", "OPERATION_WORKFLOW": "Workflow", + "OPERATION_NO_WORKFLOW_ERROR": "No Certified Versions Available", "OPERATION_WORKFLOW_ARCHIVED": "Archived", "OPERATION_WORKFLOW_VERSION": "Workflow Version", "OPERATION_ADD_PARAMS": "Add Paramaters", @@ -469,11 +488,14 @@ "OPERATION_PARAM_TYPE": "Type", "OPERATION_PARAM_PROPERTY": "Property", "OPERATION_PARAM_MANDATORY": "Mandatory", + "EMPTY_PARAM_TABLE_HEADER": "NO PARAMETERS TO SHOW", "EMPTY_PARAM_TABLE_NO_SELECTED_WORKFLOW_1": "Select Workflow and Workflow Version above", "EMPTY_PARAM_TABLE_NO_SELECTED_WORKFLOW_2": "in order to see the parameters", "EMPTY_PARAM_TABLE_NO_WORKFLOWS": "Only <b>certified</b> workflow versions can be assigned to an operation", + "PARAM_NONE_OF_TYPE": "No available properties of this type.", + "=========== PLUGIN NOT CONNECTED ===========": "", "PLUGIN_NOT_CONNECTED_ERROR_MAIN": "The \"{{pluginName}}\" plugin is currently unavailable.", |