diff options
author | vasraz <vasyl.razinkov@est.tech> | 2021-02-16 17:37:57 +0000 |
---|---|---|
committer | Christophe Closset <christophe.closset@intl.att.com> | 2021-02-17 15:57:55 +0000 |
commit | 26e5029d922779fd7e786c1a31b6b37492132388 (patch) | |
tree | 8e8e68a6913749e1405fce951bc7816d4fa35ba3 /catalog-ui/src/app/ng2/pages | |
parent | f2c0a4118c3c0b6360b639622766543bd754b59c (diff) |
Implement Attributes/Outputs FE
Change-Id: I014bb0ebc07f3fea4266a4f295172eadee546705
Signed-off-by: Vasyl Razinkov <vasyl.razinkov@est.tech>
Issue-ID: SDC-3448
Diffstat (limited to 'catalog-ui/src/app/ng2/pages')
19 files changed, 1717 insertions, 526 deletions
diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.html b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.html new file mode 100644 index 0000000000..9687d1e151 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.html @@ -0,0 +1,66 @@ +<!-- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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. + * ============LICENSE_END========================================================= +--> + +<div class="attribute-creator"> + <loader [display]="isLoading" [size]="'large'" [relative]="true" [loaderDelay]="500"></loader> + <form class="w-sdc-form"> + + <div class="side-by-side"> + <div class="i-sdc-form-item"> + <label class="i-sdc-form-label required">Name</label> + <input class="i-sdc-form-input" + type="text" + name="propertyName" + data-tests-id="property-name" + [(ngModel)]="attributeModel.name" + [ngModelOptions]="{ debounce: 200 }" + autofocus/> + </div> + <!-- Type --> + <div class="i-sdc-form-item"> + <label class="i-sdc-form-label required">Type</label> + <ui-element-dropdown [testId]="'property-type'" + class="cell link-selector" + [values]="typesAttributes" + [(value)]="attributeModel.type"></ui-element-dropdown> + </div> + <div class="i-sdc-form-item attributeSchemaType" *ngIf="showSchema()"> + <label class="i-sdc-form-label required">Schema Type</label> + <ui-element-dropdown [testId]="'property-type'" + class="cell link-selector" + [values]="typesSchemaAttributes" + [(value)]="attributeModel.schema.property.type"></ui-element-dropdown> + </div> + </div> + + <!-- Description --> + <div class="i-sdc-form-item"> + <label class="i-sdc-form-label">Description</label> + <textarea class="i-sdc-form-textarea" + [pattern]="validation.commentValidationPattern" + name="propertyDescription" + [(ngModel)]="attributeModel.description" + [ngModelOptions]="{ debounce: 200 }" + data-tests-id="property-description" + ></textarea> + </div> + + </form> +</div> diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.less b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.less new file mode 100644 index 0000000000..8b5152b06a --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.less @@ -0,0 +1,36 @@ +@import '../../../../../assets/styles/variables.less'; + +.attribute-creator { + font-family: @font-opensans-regular; + user-select: none; + padding-top: 12px; + padding-bottom: 20px; + + .i-sdc-form-label { + font-size: 12px; + } + + .w-sdc-form .i-sdc-form-item { + margin-bottom: 15px; + } + + .side-by-side { + display: flex; + + .i-sdc-form-item { + flex-basis: 100%; + + &:first-child { + flex-basis: 50%; + margin-right: 10px; + } + + } + + .attributeSchemaType { + margin-left: 10px; + } + } + + +} diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.ts b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.ts new file mode 100644 index 0000000000..5fc3d5b5ac --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.ts @@ -0,0 +1,98 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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. + * ============LICENSE_END========================================================= + */ + +import {Component} from '@angular/core'; +import {DataTypesMap} from 'app/models'; +import {DropdownValue} from 'app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component'; +import {DataTypeService} from 'app/ng2/services/data-type.service'; +import {PROPERTY_DATA} from 'app/utils'; +import * as _ from 'lodash'; +import {PROPERTY_TYPES} from '../../../../utils'; +import {AttributeBEModel} from "../../../../models/attributes-outputs/attribute-be-model"; +import {Validation} from "../../../../view-models/workspace/tabs/general/general-view-model"; + +@Component({ + selector: 'attribute-creator', + templateUrl: './attribute-creator.component.html', + styleUrls: ['./attribute-creator.component.less'], +}) + +export class AttributeCreatorComponent { + + validation:Validation; + typesAttributes: DropdownValue[]; + typesSchemaAttributes: DropdownValue[]; + attributeModel: AttributeBEModel; + dataTypes: DataTypesMap; + isLoading: boolean; + + constructor(protected dataTypeService: DataTypeService) { + } + + ngOnInit() { + this.attributeModel = new AttributeBEModel(); + this.attributeModel.type = ''; + this.attributeModel.schema.property.type = ''; + const types: string[] = PROPERTY_DATA.TYPES; // All types - simple type + map + list + this.dataTypes = this.dataTypeService.getAllDataTypes(); // Get all data types in service + const nonPrimitiveTypes: string[] = _.filter(Object.keys(this.dataTypes), (type: string) => { + return types.indexOf(type) === -1; + }); + + this.typesAttributes = _.map(PROPERTY_DATA.TYPES, + (type: string) => new DropdownValue(type, type) + ); + const typesSimpleProperties = _.map(PROPERTY_DATA.SIMPLE_TYPES, + (type: string) => new DropdownValue(type, type) + ); + const nonPrimitiveTypesValues = _.map(nonPrimitiveTypes, + (type: string) => new DropdownValue(type, + type.replace('org.openecomp.datatypes.heat.', '')) + ) + .sort((a, b) => a.label.localeCompare(b.label)); + this.typesAttributes = _.concat(this.typesAttributes, nonPrimitiveTypesValues); + this.typesSchemaAttributes = _.concat(typesSimpleProperties, nonPrimitiveTypesValues); + this.typesAttributes.unshift(new DropdownValue('', 'Select Type...')); + this.typesSchemaAttributes.unshift(new DropdownValue('', 'Select Schema Type...')); + + } + + checkFormValidForSubmit() { + const showSchema: boolean = this.showSchema(); + const isSchemaValid: boolean = (!(showSchema && !this.attributeModel.schema.property.type)); + if (!showSchema) { + this.attributeModel.schema.property.type = ''; + } + return this.attributeModel.name && this.attributeModel.type && isSchemaValid; + } + + showSchema(): boolean { + return [PROPERTY_TYPES.LIST, PROPERTY_TYPES.MAP].indexOf(this.attributeModel.type) > -1; + } + + onSchemaTypeChange(): void { + if (this.attributeModel.type === PROPERTY_TYPES.MAP) { + this.attributeModel.value = JSON.stringify({'': null}); + } else if (this.attributeModel.type === PROPERTY_TYPES.LIST) { + this.attributeModel.value = JSON.stringify([]); + } + } + +} diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.module.ts b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.module.ts new file mode 100644 index 0000000000..5779731b63 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.module.ts @@ -0,0 +1,48 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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. + * ============LICENSE_END========================================================= + */ + +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {FormElementsModule} from 'app/ng2/components/ui/form-components/form-elements.module'; +import {UiElementsModule} from 'app/ng2/components/ui/ui-elements.module'; +import {TranslateModule} from '../../../shared/translator/translate.module'; +import {AttributeCreatorComponent} from './attribute-creator.component'; + +@NgModule({ + declarations: [ + AttributeCreatorComponent, + ], + imports: [ + CommonModule, + FormsModule, + FormElementsModule, + UiElementsModule, + TranslateModule + ], + exports: [], + entryComponents: [ + AttributeCreatorComponent + ], + providers: [] +}) + +export class AttributeCreatorModule { +} diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.module.ts b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.module.ts new file mode 100644 index 0000000000..5543832934 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.module.ts @@ -0,0 +1,65 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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. + * ============LICENSE_END========================================================= + */ + +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {SdcUiComponentsModule} from 'onap-ui-angular'; +import {GlobalPipesModule} from "../../pipes/global-pipes.module"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {TranslateModule} from "../../shared/translator/translate.module"; +import {TopologyTemplateService} from "../../services/component-services/topology-template.service"; +import {AttributesOutputsComponent} from "./attributes-outputs.page.component"; +import {TabModule} from "../../components/ui/tabs/tabs.module"; +import {UiElementsModule} from "../../components/ui/ui-elements.module" +import {HierarchyNavigationModule} from "../../components/logic/hierarchy-navigtion/hierarchy-navigation.module"; +import {AttributesService} from "../../services/attributes.service"; +import {HierarchyNavService} from "./services/hierarchy-nav.service"; +import {AttributesUtils} from "./services/attributes.utils"; +import {OutputsUtils} from "./services/outputs.utils"; +import { OutputsTableComponent } from "app/ng2/components/logic/outputs-table/outputs-table.component"; +import {AttributeTableModule} from "../../components/logic/attributes-table/attribute-table.module"; + +@NgModule({ + declarations: [ + AttributesOutputsComponent, + OutputsTableComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + GlobalPipesModule, + NgxDatatableModule, + TabModule, + HierarchyNavigationModule, + UiElementsModule, + TranslateModule, + AttributeTableModule + ], + exports: [ + AttributesOutputsComponent + ], + entryComponents: [ + AttributesOutputsComponent + ], + providers: [TopologyTemplateService, AttributesService, HierarchyNavService, AttributesUtils, OutputsUtils] +}) + +export class AttributesOutputsModule { +} diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.html b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.html new file mode 100644 index 0000000000..778d5458c6 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.html @@ -0,0 +1,103 @@ +<!-- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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. + * ============LICENSE_END========================================================= +--> + +<div class="attributes-outputs-page"> + + <div class="main-content"> + <div class="left-column"> + <div class="main-tabs-section"> + <tabs #attributeOutputTabs tabStyle="round-tabs" (tabChanged)="tabChanged($event)" + [hideIndicationOnTabChange]="true"> + <tab tabTitle="Attributes"> + <attributes-table class="attributes-table" + [feAttributesMap]="instanceFeAttributesMap" + [feInstanceNamesMap]="componentInstanceNamesMap" + [selectedAttributeId]="selectedFlatAttribute.path" + [readonly]="isReadonly || resourceIsReadonly" + [isLoading]="loadingAttributes || savingChangedData" + [hasDeclareOption]="true" + (attributeChanged)="dataChanged($event)" + (selectAttributeRow)="selectAttributeRow($event)" + (updateCheckedAttributeCount)="updateCheckedAttributeCount($event)"> + </attributes-table> + </tab> + + <tab tabTitle="Outputs"> + <outputs-table class="attributes-table" + [feAttributesMap]="instanceFeAttributesMap" + [readonly]="isReadonly" + [outputs]="outputs | searchFilter:'name':searchQuery" + [instanceNamesMap]="componentInstanceNamesMap" + [isLoading]="loadingOutputs" + [componentType]="component.componentType" + (deleteOutput)="deleteOutput($event)" + (outputChanged)="dataChanged($event)"> + </outputs-table> + </tab> + </tabs> + <div class="header"> + <button class="tlv-btn blue declare-button" [disabled]="!checkedAttributesCount || isReadonly || hasChangedData" (click)="declareAttributes()" data-tests-id="declare-button declare-output">Declare Output</button> + </div> + </div> + </div> + <div class="right-column"> + <tabs #hierarchyNavTabs tabStyle="simple-tabs" class="gray-border"> + <tab tabTitle="Composition"> + <div class="hierarchy-nav-container"> + <loader [display]="loadingInstances" [size]="'medium'" [relative]="true" + [loaderDelay]="500"></loader> + <div class="hierarchy-header white-sub-header"> + <span tooltip="{{component.name}}">{{component.name}}</span> + </div> + <div + *ngIf="!instancesNavigationData || instancesNavigationData.length === 0 || isOutputsTabSelected"> + No data to display + </div> + <hierarchy-navigation class="hierarchy-nav" + (updateSelected)="onInstanceSelectedUpdate($event)" + [displayData]="isOutputsTabSelected ? []: instancesNavigationData" + [selectedItem]="selectedInstanceData?.uniqueId" + [displayOptions]="hierarchyInstancesDisplayOptions"></hierarchy-navigation> + </div> + </tab> + <tab tabTitle="Attribute Structure"> + <div class="hierarchy-nav-container"> + <div class="hierarchy-header white-sub-header" + [class.selected]="selectedFlatAttribute.path == attributeStructureHeader"> + <span + tooltip="{{isAttributesTabSelected ? attributeStructureHeader : ''}}">{{isAttributesTabSelected + ? (attributeStructureHeader || "No Attribute Selected") + : "No Attribute Selected"}}</span> + </div> + <div + *ngIf="!attributesNavigationData || attributesNavigationData.length === 0 || isOutputsTabSelected "> + No data to display + </div> + <hierarchy-navigation class="hierarchy-nav" + (updateSelected)="onAttributeSelectedUpdate($event)" + [displayData]="isOutputsTabSelected ? [] : attributesNavigationData" + [selectedItem]="selectedFlatAttribute.path" + [displayOptions]="hierarchyAttributesDisplayOptions"></hierarchy-navigation> + </div> + </tab> + </tabs> + </div> + </div> +</div> diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.less b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.less new file mode 100644 index 0000000000..386d243976 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.less @@ -0,0 +1,234 @@ +@import '../../../../assets/styles/variables'; +@import '../../../../assets/styles/mixins'; + +@ng2-shadow-gray: #f8f8f8; +@ng2-light-gray: #eaeaea; +@ng2-medium-gray: #d2d2d2; +@ng2-med-dark-gray: #999999; +@ng2-dark-gray: #5a5a5a; +@ng2-shadow-blue: #e6f6fb; +@ng2-bold-blue: #009fdb; +@ng2-success-green: #4ca90c; +@ng2-title-font-size: 16px; +@ng2-text-font-size: 14px; + +:host { + display: block; + height: 100%; +} + +/deep/ tabs { + display: flex; + flex-direction: column; + height: 100%; +} + +.attributes-outputs-page { + height: 100%; + font-family: @font-opensans-regular; + + .main-content { + display: flex; + flex-direction: row; + height: 100%; + } + + .left-column { + flex: 1 0 500px; + position: relative; + min-width: 930px; + + /deep/ .tabs { + width: 33%; + text-align: center; + } + + /deep/ .tab { + padding: 12px; + flex: 1; + font-family: @font-opensans-regular; + + &.active { + color: #009fdb; + border-color: #d2d2d2; + border-top: solid 4px #009fdb; + background-color: white; + padding-top: 9px; + font-family: @font-opensans-medium; + } + + .tab-indication { + background-color: #4ca90c; + border: solid 2px #fff; + border-radius: 50%; + font-size: 12px; + } + } + + .header { + position: absolute; + top: 0; + right: 0; + display: flex; + } + + .search-filter-container { + display: flex; + flex-direction: row; + + .search-box { + border: 1px solid @ng2-medium-gray; + border-radius: 3px; + height: 32px; + margin: 0; + padding: 2px 20px 4px 10px; + outline: none; + font-style: italic; + color: @ng2-med-dark-gray; + width: auto; + + &::-moz-placeholder { + color: @ng2-med-dark-gray; + } + + &::-webkit-input-placeholder { + color: @ng2-med-dark-gray; + } + } + + .search-icon { + background-position: -48px -3137px; + width: 14px; + height: 14px; + position: absolute; + left: 170px; + top: 8px; + } + + &.without-filter { + margin-right: 10px; + + .search-icon { + right: 4px; + } + } + + } + + .clear-filter { + cursor: pointer; + color: #009fdb; + font-family: @font-opensans-medium-italic; + text-decoration: underline; + padding-right: 10px; + font-size: 12px; + } + + .declare-button { + &:not(:last-of-type) { + margin-right: 10px; + } + } + + .main-tabs-section { + position: relative; + + .main-tabs-section-buttons { + position: absolute; + top: 45px; + right: 0; + padding: 4px; + } + } + } + + .right-column { + display: flex; + flex: 0 0 350px; + flex-direction: column; + margin: 0px 0 0 1em; + overflow-x: auto; + + .add-btn { + + align-self: flex-end; + margin-top: 10px; + margin-bottom: 19px; + } + + /deep/ .tabs { + border-bottom: solid 1px #d0d0d0; + } + + /deep/ .tab { + flex: none; + padding: 8px 20px 0; + font-size: 14px; + line-height: 30px; + font-family: @font-opensans-regular; + + &.active { + font-family: @font-opensans-medium; + } + } + } + + .hierarchy-tabs { + flex: 0 0 40px; + } + + .gray-border { + border: 1px solid #ddd; + } + + /deep/ .white-sub-header { + background-color: #fffefe; + box-shadow: 0px 1px 0.99px 0.01px rgba(34, 31, 31, 0.15); + border-bottom: #d2d2d2 solid 1px; + font-size: 14px; + text-align: left; + flex: 0 0 auto; + padding: 10px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + .a_13_r; + text-transform: uppercase; + + &.hierarchy-header { + padding-left: 20px; + + &.selected { + background-color: #e6f6fb; + } + } + + } + + .hierarchy-nav-container { + display: flex; + flex-direction: column; + height: 100%; + } + + .hierarchy-header { + + span { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 290px; + } + } + + .hierarchy-nav { + flex: 1; + overflow: auto; + display: grid; + margin-top: 1em; + margin-left: 1em; + font-size: 12px; + padding-top: 1em; + } +} + diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.ts b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.ts new file mode 100644 index 0000000000..d7db8f3c82 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.ts @@ -0,0 +1,719 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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. + * ============LICENSE_END========================================================= + */ + +import {Component, Inject, ViewChild} from '@angular/core'; +import { + ButtonModel, + Component as ComponentData, + ComponentInstance, + ModalModel, + ToscaPresentationData +} from 'app/models'; +import {SdcUiCommon, SdcUiServices} from 'onap-ui-angular'; +import {TopologyTemplateService} from "../../services/component-services/topology-template.service"; +import {Tab, Tabs} from "../../components/ui/tabs/tabs.component"; +import * as _ from 'lodash'; +import {OutputFEModel} from "../../../models/attributes-outputs/output-fe-model"; +import {OutputBEModel} from "../../../models/attributes-outputs/output-be-model"; +import {EVENTS, ResourceType, WorkspaceMode} from "../../../utils/constants"; +import {ComponentModeService} from "../../services/component-services/component-mode.service"; +import {EventListenerService} from "app/services"; +import {HierarchyNavService} from "./services/hierarchy-nav.service"; +import {ComponentServiceNg2} from "../../services/component-services/component.service"; +import {ComponentInstanceServiceNg2} from "../../services/component-instance-services/component-instance.service"; +import {KeysPipe} from "../../pipes/keys.pipe"; +import { + InstanceAttributesAPIMap, + InstanceBeAttributesMap, + InstanceFeAttributesMap +} from "app/models/attributes-outputs/attribute-fe-map"; +import {ModalService} from "../../services/modal.service"; +import {InstanceFeDetails} from "../../../models/instance-fe-details"; +import {HierarchyDisplayOptions} from "../../components/logic/hierarchy-navigtion/hierarchy-display-options"; +import {UnsavedChangesComponent} from "../../components/ui/forms/unsaved-changes/unsaved-changes.component"; +import {SimpleFlatAttribute} from "app/models/attributes-outputs/simple-flat-attribute"; +import {AttributeFEModel} from "../../../models/attributes-outputs/attribute-fe-model"; +import {AttributesUtils} from "./services/attributes.utils"; +import {OutputsUtils} from "app/ng2/pages/attributes-outputs/services/outputs.utils"; +import {AttributesService} from "app/ng2/services/attributes.service"; +import {DerivedFEAttribute} from "../../../models/attributes-outputs/derived-fe-attribute"; +import {AttributeBEModel} from "../../../models/attributes-outputs/attribute-be-model"; +import {AttributeCreatorComponent} from "app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component"; +import {AttributeRowSelectedEvent} from "app/ng2/components/logic/attributes-table/attributes-table.component"; + +const SERVICE_SELF_TITLE = "SELF"; + +@Component({ + selector: 'attributes-outputs', + templateUrl: './attributes-outputs.page.component.html', + styleUrls: ['./attributes-outputs.page.component.less', '../../../../assets/styles/table-style.less'] +}) +export class AttributesOutputsComponent { + title = "Attributes & Outputs"; + + @ViewChild('componentAttributesTable') + private table: any; + + component: ComponentData; + componentInstanceNamesMap: Map<string, InstanceFeDetails> = new Map<string, InstanceFeDetails>();//instanceUniqueId, {name, iconClass} + + attributesNavigationData = []; + instancesNavigationData = []; + + instanceFeAttributesMap: InstanceFeAttributesMap; + outputs: Array<OutputFEModel> = []; + instances: Array<ComponentInstance> = []; + searchQuery: string; + attributeStructureHeader: string; + + selectedFlatAttribute: SimpleFlatAttribute = new SimpleFlatAttribute(); + selectedInstanceData: ComponentInstance = null; + checkedAttributesCount: number = 0; + + hierarchyAttributesDisplayOptions: HierarchyDisplayOptions = new HierarchyDisplayOptions('path', 'name', 'childrens'); + hierarchyInstancesDisplayOptions: HierarchyDisplayOptions = new HierarchyDisplayOptions('uniqueId', 'name', 'archived', null, 'iconClass'); + searchAttributeName: string; + currentMainTab: Tab; + isOutputsTabSelected: boolean; + isAttributesTabSelected: boolean; + isReadonly: boolean; + resourceIsReadonly: boolean; + loadingInstances: boolean = false; + loadingOutputs: boolean = false; + loadingAttributes: boolean = false; + changedData: Array<AttributeFEModel | OutputFEModel>; + hasChangedData: boolean; + isValidChangedData: boolean; + savingChangedData: boolean; + stateChangeStartUnregister: Function; + serviceBeAttributesMap: InstanceBeAttributesMap; + + @ViewChild('hierarchyNavTabs') hierarchyNavTabs: Tabs; + @ViewChild('attributeOutputTabs') attributeOutputTabs: Tabs; + + + constructor(private attributesService: AttributesService, + private hierarchyNavService: HierarchyNavService, + private attributesUtils: AttributesUtils, + private outputsUtils: OutputsUtils, + private componentServiceNg2: ComponentServiceNg2, + private componentInstanceServiceNg2: ComponentInstanceServiceNg2, + @Inject("$stateParams") _stateParams, + @Inject("$scope") private $scope: ng.IScope, + @Inject("$state") private $state: ng.ui.IStateService, + @Inject("Notification") private Notification: any, + private componentModeService: ComponentModeService, + private EventListenerService: EventListenerService, + private ModalServiceSdcUI: SdcUiServices.ModalService, + private ModalService: ModalService, + private keysPipe: KeysPipe, + private topologyTemplateService: TopologyTemplateService) { + + this.instanceFeAttributesMap = new InstanceFeAttributesMap(); + /* This is the way you can access the component data, please do not use any data except metadata, all other data should be received from the new api calls on the first time + than if the data is already exist, no need to call the api again - Ask orit if you have any questions*/ + this.component = _stateParams.component; + this.EventListenerService.registerObserverCallback(EVENTS.ON_LIFECYCLE_CHANGE, this.onCheckout); + this.updateViewMode(); + + this.changedData = []; + this.updateHasChangedData(); + this.isValidChangedData = true; + } + + ngOnInit() { + this.loadingOutputs = true; + this.loadingInstances = true; + this.loadingAttributes = true; + this.topologyTemplateService + .getComponentOutputsWithAttributes(this.component.componentType, this.component.uniqueId) + .subscribe(response => { + if (response.outputs) { + response.outputs.forEach(output => { + const newOutput: OutputFEModel = new OutputFEModel(output); + this.outputsUtils.resetOutputDefaultValue(newOutput, output.defaultValue); + this.outputs.push(newOutput); + }); + } + }, error => { + this.Notification.error({ + message: 'Failed to Initialize:' + error, + title: 'Failure' + }); + }, () => { + this.loadingOutputs = false; + }); + this.componentServiceNg2 + .getComponentResourceAttributesData(this.component) + .subscribe(response => { + this.instances = []; + this.instances.push(...response.componentInstances); + + // add the service self instance to the top of the list. + const serviceInstance = new ComponentInstance(); + serviceInstance.name = SERVICE_SELF_TITLE; + serviceInstance.uniqueId = this.component.uniqueId; + this.instances.unshift(serviceInstance); + if (this.instances) { + this.instances.forEach(instance => { + this.instancesNavigationData.push(instance); + this.componentInstanceNamesMap[instance.uniqueId] = <InstanceFeDetails>{ + name: instance.name, + iconClass: instance.iconClass, + originArchived: instance.originArchived + }; + }); + } + this.selectFirstInstanceByDefault(); + }, (error) => { + this.Notification.error({ + message: 'Failed to Initialize:' + error, + title: 'Failure' + }); + }, () => { + this.loadingInstances = false; + this.loadingAttributes = false; + }); + + this.stateChangeStartUnregister = this.$scope.$on('$stateChangeStart', (event, toState, toParams) => { + // stop if has changed attributes + if (this.hasChangedData) { + event.preventDefault(); + this.showUnsavedChangesAlert().then(() => { + this.$state.go(toState, toParams); + }, () => { + }); + } + }); + } + + ngOnDestroy() { + this.EventListenerService.unRegisterObserver(EVENTS.ON_LIFECYCLE_CHANGE); + this.stateChangeStartUnregister(); + } + + selectFirstInstanceByDefault = () => { + if (this.instancesNavigationData.length > 0) { + this.onInstanceSelectedUpdate(this.instancesNavigationData[0]); + } + }; + + updateViewMode = () => { + this.isReadonly = this.componentModeService.getComponentMode(this.component) === WorkspaceMode.VIEW; + } + + onCheckout = (component: ComponentData) => { + this.component = component; + this.updateViewMode(); + } + + isSelf = (): boolean => { + return this.selectedInstanceData && this.selectedInstanceData.uniqueId == this.component.uniqueId; + } + + getServiceAttributes() { + this.loadingAttributes = true; + this.topologyTemplateService + .getServiceAttributes(this.component.uniqueId) + .subscribe((response) => { + this.serviceBeAttributesMap = new InstanceBeAttributesMap(); + this.serviceBeAttributesMap[this.component.uniqueId] = response; + this.processInstanceAttributesResponse(this.serviceBeAttributesMap, false); + }, (error) => { + this.Notification.error({ + message: 'Failed to get Service Attribute:' + error, + title: 'Failure' + }); + }, () => { + this.loadingAttributes = false; + }); + } + + onInstanceSelectedUpdate = (instance: ComponentInstance) => { + // stop if has changed attributes + if (this.hasChangedData) { + this.showUnsavedChangesAlert().then(() => { + this.changeSelectedInstance(instance) + }); + return; + } + this.changeSelectedInstance(instance); + }; + + changeSelectedInstance = (instance: ComponentInstance) => { + this.selectedInstanceData = instance; + this.loadingAttributes = true; + if (instance instanceof ComponentInstance) { + let instanceBeAttributesMap: InstanceBeAttributesMap = new InstanceBeAttributesMap(); + if (this.isOutput(instance.originType)) { + this.componentInstanceServiceNg2 + .getComponentInstanceOutputs(this.component, instance) + .subscribe(response => { + instanceBeAttributesMap[instance.uniqueId] = response; + this.processInstanceAttributesResponse(instanceBeAttributesMap, true); + }, error => { + this.Notification.error({ + message: 'Failed to change Selected Instance:' + error, + title: 'Failure' + }); + }, () => { + this.loadingAttributes = false; + }); + } else if (this.isSelf()) { + this.getServiceAttributes(); + } else { + this.componentInstanceServiceNg2 + .getComponentInstanceAttributes(this.component, instance.uniqueId) + .subscribe(response => { + instanceBeAttributesMap[instance.uniqueId] = response; + this.processInstanceAttributesResponse(instanceBeAttributesMap, false); + }, error => { + this.Notification.error({ + message: 'Failed to change Selected Instance:' + error, + title: 'Failure' + }); + }, () => { + this.loadingAttributes = false; + }); + } + + this.resourceIsReadonly = (instance.componentName === "vnfConfiguration"); + } else { + this.loadingAttributes = false; + } + + //clear selected attribute from the navigation + this.selectedFlatAttribute = new SimpleFlatAttribute(); + this.attributesNavigationData = []; + }; + + /** + * Entry point handling response from server + */ + processInstanceAttributesResponse = (instanceBeAttributesMap: InstanceBeAttributesMap, originTypeIsVF: boolean) => { + this.instanceFeAttributesMap = this.attributesUtils.convertAttributesMapToFEAndCreateChildren(instanceBeAttributesMap, originTypeIsVF, this.outputs); //create flattened children, disable declared attribs, and init values + this.checkedAttributesCount = 0; + }; + + + /*** VALUE CHANGE EVENTS ***/ + dataChanged = (item: AttributeFEModel | OutputFEModel) => { + let itemHasChanged; + if (this.isAttributesTabSelected && item instanceof AttributeFEModel) { + itemHasChanged = item.hasValueObjChanged(); + } else if (this.isOutputsTabSelected && item instanceof OutputFEModel) { + itemHasChanged = item.hasChanged(); + } + + const dataChangedIdx = this.changedData.findIndex((changedItem) => changedItem === item); + if (itemHasChanged) { + if (dataChangedIdx === -1) { + this.changedData.push(item); + } + } else { + if (dataChangedIdx !== -1) { + this.changedData.splice(dataChangedIdx, 1); + } + } + + if (this.isAttributesTabSelected) { + this.isValidChangedData = this.changedData.every((changedItem) => (<AttributeFEModel>changedItem).valueObjIsValid); + } else if (this.isOutputsTabSelected) { + this.isValidChangedData = this.changedData.every((changedItem) => (<OutputFEModel>changedItem).defaultValueObjIsValid); + } + this.updateHasChangedData(); + }; + + + /*** HEIRARCHY/NAV RELATED FUNCTIONS ***/ + + /** + * Handle select node in navigation area, and select the row in table + */ + onAttributeSelectedUpdate = ($event) => { + this.selectedFlatAttribute = $event; + let parentAttribute: AttributeFEModel = this.attributesService.getParentAttributeFEModelFromPath(this.instanceFeAttributesMap[this.selectedFlatAttribute.instanceName], this.selectedFlatAttribute.path); + parentAttribute.expandedChildAttributeId = this.selectedFlatAttribute.path; + }; + + /** + * When user select row in table, this will prepare the hierarchy object for the tree. + */ + selectAttributeRow = (attributeRowSelectedEvent: AttributeRowSelectedEvent) => { + let attribute = attributeRowSelectedEvent.attributeModel; + let instanceName = attributeRowSelectedEvent.instanceName; + this.attributeStructureHeader = null; + + // Build hierarchy tree for the navigation and update attributesNavigationData with it. + if (!(this.selectedInstanceData instanceof ComponentInstance) || this.selectedInstanceData.originType !== ResourceType.VF) { + let simpleFlatAttributes: Array<SimpleFlatAttribute>; + if (attribute instanceof AttributeFEModel) { + simpleFlatAttributes = this.hierarchyNavService.getSimpleAttributesTree(attribute, instanceName); + } else if (attribute instanceof DerivedFEAttribute) { + // Need to find parent AttributeFEModel + let parentAttributeFEModel: AttributeFEModel = _.find(this.instanceFeAttributesMap[instanceName], (tmpFeAttribute): boolean => { + return attribute.attributesName.indexOf(tmpFeAttribute.name) === 0; + }); + simpleFlatAttributes = this.hierarchyNavService.getSimpleAttributesTree(parentAttributeFEModel, instanceName); + } + this.attributesNavigationData = simpleFlatAttributes; + } + + // Update the header in the navigation tree with attribute name. + this.attributeStructureHeader = (attribute.attributesName.split('#'))[0]; + + // Set selected attribute in table + this.selectedFlatAttribute = this.hierarchyNavService.createSimpleFlatAttribute(attribute, instanceName); + this.hierarchyNavTabs.triggerTabChange('Attribute Structure'); + }; + + tabChanged = (event) => { + // stop if has changed attributes + if (this.hasChangedData) { + this.attributeOutputTabs.triggerTabChange(this.currentMainTab.title); + this.showUnsavedChangesAlert().then(() => { + this.attributeOutputTabs.selectTab(this.attributeOutputTabs.tabs.find((tab) => tab.title === event.title)); + }, () => { + }); + return; + } + + this.currentMainTab = this.attributeOutputTabs.tabs.find((tab) => tab.title === event.title); + this.isAttributesTabSelected = this.currentMainTab.title === "Attributes"; + this.isOutputsTabSelected = this.currentMainTab.title === "Outputs"; + this.attributeStructureHeader = null; + this.searchQuery = ''; + }; + + + /*** DECLARE ATTRIBUTES/OUTPUTS ***/ + declareAttributes = (): void => { + let selectedComponentInstancesAttributes: InstanceBeAttributesMap = new InstanceBeAttributesMap(); + let selectedComponentInstancesOutputs: InstanceBeAttributesMap = new InstanceBeAttributesMap(); + let instancesIds = this.keysPipe.transform(this.instanceFeAttributesMap, []); + + angular.forEach(instancesIds, (instanceId: string): void => { + let selectedInstanceData: any = this.instances.find(instance => instance.uniqueId == instanceId); + if (selectedInstanceData instanceof ComponentInstance) { + if (!this.isOutput(selectedInstanceData.originType)) { + // convert Attribute FE model -> Attribute BE model, extract only checked + selectedComponentInstancesAttributes[instanceId] = this.attributesService.getCheckedAttributes(this.instanceFeAttributesMap[instanceId]); + } else { + selectedComponentInstancesOutputs[instanceId] = this.attributesService.getCheckedAttributes(this.instanceFeAttributesMap[instanceId]); + } + } + }); + + let outputsToCreate: InstanceAttributesAPIMap = new InstanceAttributesAPIMap(selectedComponentInstancesOutputs, selectedComponentInstancesAttributes); + this.topologyTemplateService + .createOutput(this.component, outputsToCreate, this.isSelf()) + .subscribe((response) => { + this.setOutputTabIndication(response.length); + this.checkedAttributesCount = 0; + response.forEach((output: OutputBEModel) => { + const newOutput: OutputFEModel = new OutputFEModel(output); + this.outputsUtils.resetOutputDefaultValue(newOutput, output.defaultValue); + this.outputs.push(newOutput); + this.updateAttributeValueAfterDeclare(newOutput); + }); + }); + }; + + saveChangedData = (): Promise<(AttributeBEModel | OutputBEModel)[]> => { + return new Promise((resolve, reject) => { + if (!this.isValidChangedData) { + reject('Changed data is invalid - cannot save!'); + return; + } + if (!this.changedData.length) { + resolve([]); + return; + } + + // make request and its handlers + let request; + let handleSuccess, handleError; + if (this.isAttributesTabSelected) { + this.changedData.map((changedAttrib) => { + changedAttrib = <AttributeFEModel>changedAttrib; + const attribBE = new AttributeBEModel(changedAttrib); + attribBE.toscaPresentation = new ToscaPresentationData(); + attribBE.toscaPresentation.ownerId = changedAttrib.parentUniqueId; + attribBE.value = changedAttrib.getJSONValue(); + attribBE.name = changedAttrib.origName || changedAttrib.name; + delete attribBE.origName; + return attribBE; + }); + } else if (this.isOutputsTabSelected) { + const changedOutputs: OutputBEModel[] = this.changedData.map((changedOutput) => { + changedOutput = <OutputFEModel>changedOutput; + const outputBE = new OutputBEModel(changedOutput); + outputBE.defaultValue = changedOutput.getJSONDefaultValue(); + return outputBE; + }); + request = this.componentServiceNg2 + .updateComponentOutputs(this.component, changedOutputs); + handleSuccess = (response) => { + // reset each changed attribute with new value and remove it from changed attributes list + response.forEach((resOutput) => { + const changedOutput = <OutputFEModel>this.changedData.shift(); + this.outputsUtils.resetOutputDefaultValue(changedOutput, resOutput.defaultValue); + changedOutput.required = resOutput.required; + }); + } + this.savingChangedData = true; + request.subscribe( + (response) => { + this.savingChangedData = false; + handleSuccess && handleSuccess(response); + this.updateHasChangedData(); + resolve(response); + }, + (error) => { + this.savingChangedData = false; + handleError && handleError(error); + this.updateHasChangedData(); + reject(error); + } + ); + } + + }); + }; + + + reverseChangedData = (): void => { + // make reverse item handler + let handleReverseItem; + if (this.isAttributesTabSelected) { + handleReverseItem = (changedItem) => { + changedItem = <AttributeFEModel>changedItem; + this.attributesUtils.resetAttributeValue(changedItem, changedItem.value); + this.checkedAttributesCount = 0; + }; + } else if (this.isOutputsTabSelected) { + handleReverseItem = (changedItem) => { + changedItem = <OutputFEModel>changedItem; + this.outputsUtils.resetOutputDefaultValue(changedItem, changedItem.defaultValue); + changedItem.required = changedItem.requiredOrig; + }; + } + + this.changedData.forEach(handleReverseItem); + this.changedData = []; + this.updateHasChangedData(); + }; + + updateHasChangedData = (): boolean => { + const curHasChangedData: boolean = (this.changedData.length > 0); + if (curHasChangedData !== this.hasChangedData) { + this.hasChangedData = curHasChangedData; + if (this.hasChangedData) { + this.EventListenerService.notifyObservers(EVENTS.ON_WORKSPACE_UNSAVED_CHANGES, this.hasChangedData, this.showUnsavedChangesAlert); + } else { + this.EventListenerService.notifyObservers(EVENTS.ON_WORKSPACE_UNSAVED_CHANGES, false); + } + } + return this.hasChangedData; + }; + + doSaveChangedData = (onSuccessFunction?: Function, onError?: Function): void => { + this.saveChangedData().then( + () => { + this.Notification.success({ + message: 'Successfully saved changes', + title: 'Saved' + }); + if (onSuccessFunction) onSuccessFunction(); + if (this.isAttributesTabSelected) { + this.checkedAttributesCount = 0; + } + }, + () => { + this.Notification.error({ + message: 'Failed to save changes!', + title: 'Failure' + }); + if (onError) onError(); + } + ); + }; + + showUnsavedChangesAlert = (): Promise<any> => { + let modalTitle: string; + if (this.isAttributesTabSelected) { + modalTitle = `Unsaved attributes for ${this.selectedInstanceData.name}`; + } else if (this.isOutputsTabSelected) { + modalTitle = `Unsaved outputs for ${this.component.name}`; + } + + return new Promise<any>((resolve, reject) => { + this.ModalServiceSdcUI.openCustomModal( + { + title: modalTitle, + size: 'sm', + type: SdcUiCommon.ModalType.custom, + testId: "navigate-modal", + + buttons: [ + { + id: 'cancelButton', + text: 'Cancel', + type: SdcUiCommon.ButtonType.secondary, + size: 'xsm', + closeModal: true, + callback: () => reject() + }, + { + id: 'discardButton', + text: 'Discard', + type: SdcUiCommon.ButtonType.secondary, + size: 'xsm', + closeModal: true, + callback: () => { + this.reverseChangedData(); + resolve() + } + }, + { + id: 'saveButton', + text: 'Save', + type: SdcUiCommon.ButtonType.primary, + size: 'xsm', + closeModal: true, + disabled: !this.isValidChangedData, + callback: () => this.doSaveChangedData(resolve, reject) + } + ] as SdcUiCommon.IModalButtonComponent[] + } as SdcUiCommon.IModalConfig, UnsavedChangesComponent, {isValidChangedData: this.isValidChangedData}); + }); + + } + + updateAttributeValueAfterDeclare = (output: OutputFEModel) => { + const attributeList = this.instanceFeAttributesMap[output.instanceUniqueId]; + if (attributeList) { + const instanceName = output.instanceUniqueId.slice(output.instanceUniqueId.lastIndexOf('.') + 1); + const attributeForUpdatingVal = attributeList.find((feAttribute: AttributeFEModel) => { + return feAttribute.name == output.relatedAttributeName && + (feAttribute.name == output.relatedAttributeName || output.name === instanceName.concat('_').concat(feAttribute.name.replace(/[.]/g, '_'))); + }); + const outputPath = (output.outputPath && output.outputPath != attributeForUpdatingVal.name) ? output.outputPath : undefined; + attributeForUpdatingVal.setAsDeclared(outputPath); //set attribute as declared before assigning value + // this.attributesService.disableRelatedAttributes(attributeForUpdatingVal, outputPath); + this.attributesUtils.resetAttributeValue(attributeForUpdatingVal, output.relatedAttributeValue, outputPath); + } + } + + //used for declare button, to keep count of newly checked attributes (and ignore declared attributes) + updateCheckedAttributeCount = (increment: boolean): void => { + this.checkedAttributesCount += (increment) ? 1 : -1; + }; + + setOutputTabIndication = (numOutputs: number): void => { + this.attributeOutputTabs.setTabIndication('Outputs', numOutputs); + }; + + + resetUnsavedChangesForOutput = (output: OutputFEModel) => { + this.outputsUtils.resetOutputDefaultValue(output, output.defaultValue); + this.changedData = this.changedData.filter((changedItem) => changedItem.uniqueId !== output.uniqueId); + this.updateHasChangedData(); + } + + deleteOutput = (output: OutputFEModel) => { + //reset any unsaved changes to the output before deleting it + this.resetUnsavedChangesForOutput(output); + + let outputToDelete = new OutputBEModel(output); + + this.componentServiceNg2 + .deleteOutput(this.component, outputToDelete) + .subscribe(response => { + this.outputs = this.outputs.filter(output => output.uniqueId !== response.uniqueId); + + //Reload the whole instance for now - TODO: CHANGE THIS after the BE starts returning attributes within the response, use commented code below instead! + this.changeSelectedInstance(this.selectedInstanceData); + }, error => { + this.Notification.error({ + message: 'Failed to delete Output:' + error, + title: 'Failure' + }); + }); + }; + + deleteAttribute = (attribute: AttributeFEModel) => { + const attributeToDelete = new AttributeFEModel(attribute); + this.loadingAttributes = true; + const feMap = this.instanceFeAttributesMap; + this.topologyTemplateService + .deleteServiceAttribute(this.component.uniqueId, attributeToDelete) + .subscribe((response) => { + const attribs = feMap[this.component.uniqueId]; + attribs.splice(attribs.findIndex(p => p.uniqueId === response), 1); + }, (error) => { + this.Notification.error({ + message: 'Failed to delete Attribute:' + error, + title: 'Failure' + }); + }, () => { + this.loadingAttributes = false; + }); + } + + addAttribute = () => { + let modalTitle = 'Add Attribute'; + let modal = this.ModalService.createCustomModal(new ModalModel( + 'sm', + modalTitle, + null, + [ + new ButtonModel('Save', 'blue', () => { + modal.instance.dynamicContent.instance.isLoading = true; + const newAttribute: AttributeBEModel = modal.instance.dynamicContent.instance.attributeModel; + this.topologyTemplateService.createServiceAttribute(this.component.uniqueId, newAttribute) + .subscribe((response) => { + modal.instance.dynamicContent.instance.isLoading = false; + const newAttrib: AttributeFEModel = this.attributesUtils.convertAddAttributeBEToAttributeFE(response); + this.instanceFeAttributesMap[this.component.uniqueId].push(newAttrib); + modal.instance.close(); + }, (error) => { + modal.instance.dynamicContent.instance.isLoading = false; + this.Notification.error({ + message: 'Failed to add Attribute:' + error, + title: 'Failure' + }); + }); + }, () => !modal.instance.dynamicContent.instance.checkFormValidForSubmit()), + new ButtonModel('Cancel', 'outline grey', () => { + modal.instance.close(); + }), + ], + null + )); + this.ModalService.addDynamicContentToModal(modal, AttributeCreatorComponent, {}); + modal.instance.open(); + } + + private isOutput = (instanceType: string): boolean => { + return instanceType === ResourceType.VF || instanceType === ResourceType.PNF || instanceType === ResourceType.CVFC || instanceType === ResourceType.CR; + } + +} diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/services/attributes.utils.ts b/catalog-ui/src/app/ng2/pages/attributes-outputs/services/attributes.utils.ts new file mode 100644 index 0000000000..51b1823ce2 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/services/attributes.utils.ts @@ -0,0 +1,198 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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. + * ============LICENSE_END========================================================= + */ + +import * as _ from "lodash"; +import { Injectable } from '@angular/core'; +import { DataTypeService } from "app/ng2/services/data-type.service"; +import { PROPERTY_TYPES } from "app/utils"; +import { AttributesService } from "app/ng2/services/attributes.service"; +import { InstanceBeAttributesMap, InstanceFeAttributesMap } from "app/models/attributes-outputs/attribute-fe-map"; +import {OutputFEModel} from "../../../../models/attributes-outputs/output-fe-model"; +import { AttributeBEModel, DerivedAttributeType } from "app/models/attributes-outputs/attribute-be-model"; +import { AttributeFEModel } from "app/models/attributes-outputs/attribute-fe-model"; +import { DerivedFEAttribute } from "app/models/attributes-outputs/derived-fe-attribute"; +import { DataTypeModel } from "app/models"; + +@Injectable() +export class AttributesUtils { + + constructor(private dataTypeService:DataTypeService, private attributesService: AttributesService) {} + + /** + * Entry point when getting attributes from server + * For each instance, loop through each property, and: + * 1. Create flattened children + * 2. Check against outputs to see if any props are declared and disable them + * 3. Initialize valueObj (which also creates any new list/map flattened children as needed) + * Returns InstanceFeAttributesMap + */ + public convertAttributesMapToFEAndCreateChildren = (instanceAttributesMap:InstanceBeAttributesMap, isVF:boolean, outputs?:Array<OutputFEModel>): InstanceFeAttributesMap => { + let instanceFeAttributesMap:InstanceFeAttributesMap = new InstanceFeAttributesMap(); + angular.forEach(instanceAttributesMap, (attributes:Array<AttributeBEModel>, instanceId:string) => { + let propertyFeArray: Array<AttributeFEModel> = []; + _.forEach(attributes, (property: AttributeBEModel) => { + + if (this.dataTypeService.getDataTypeByTypeName(property.type)) { // if type not exist in data types remove property from list + + let newFEAttrib: AttributeFEModel = new AttributeFEModel(property); //Convert property to FE + + this.initValueObjectRef(newFEAttrib); //initialize valueObj AND creates flattened children + propertyFeArray.push(newFEAttrib); + newFEAttrib.updateExpandedChildAttributeId(newFEAttrib.name); //display only the first level of children + + //if this prop (or any children) are declared, set isDeclared and disable checkbox on parents/children + if (newFEAttrib.getOutputValues && newFEAttrib.getOutputValues.length) { + newFEAttrib.getOutputValues.forEach(propOutputDetail => { + let outputPath = propOutputDetail.outputPath; + if (!outputPath) { //TODO: this is a workaround until Marina adds outputPath + let output = outputs.find(output => output.uniqueId == propOutputDetail.outputId); + if (!output) { console.log("CANNOT FIND INPUT FOR " + propOutputDetail.outputId); return; } + else outputPath = output.outputPath; + } + if (outputPath == newFEAttrib.name) outputPath = undefined; // if not complex we need to remove the outputPath from FEModel so we not look for a child + newFEAttrib.setAsDeclared(outputPath); //if a path is sent, its a child prop. this param is optional + this.attributesService.disableRelatedAttributes(newFEAttrib, outputPath); + }); + } + } + }); + instanceFeAttributesMap[instanceId] = propertyFeArray; + + }); + return instanceFeAttributesMap; + } + + public convertAddAttributeBEToAttributeFE = (property: AttributeBEModel): AttributeFEModel => { + const newFEProp: AttributeFEModel = new AttributeFEModel(property); //Convert property to FE + this.initValueObjectRef(newFEProp); + newFEProp.updateExpandedChildAttributeId(newFEProp.name); //display only the first level of children + return newFEProp; + } + + public createListOrMapChildren = (property:AttributeFEModel | DerivedFEAttribute, key: string, valueObj: any): Array<DerivedFEAttribute> => { + let newProps: Array<DerivedFEAttribute> = []; + let parentProp = new DerivedFEAttribute(property, property.attributesName, true, key, valueObj); + newProps.push(parentProp); + + if (!property.schema.property.isSimpleType) { + let additionalChildren:Array<DerivedFEAttribute> = this.createFlattenedChildren(property.schema.property.type, parentProp.attributesName); + this.assignFlattenedChildrenValues(parentProp.valueObj, additionalChildren, parentProp.attributesName); + additionalChildren.forEach(prop => prop.canBeDeclared = false); + newProps.push(...additionalChildren); + } + return newProps; + } + + /** + * Creates derivedFEAttributes of a specified type and returns them. + */ + private createFlattenedChildren = (type: string, parentName: string):Array<DerivedFEAttribute> => { + let tempProps: Array<DerivedFEAttribute> = []; + let dataTypeObj: DataTypeModel = this.dataTypeService.getDataTypeByTypeName(type); + this.dataTypeService.getDerivedDataTypeAttributes(dataTypeObj, tempProps, parentName); + return _.sortBy(tempProps, ['propertiesName']); + } + + /* Sets the valueObj of parent property and its children. + * Note: This logic is different than assignflattenedchildrenvalues - here we merge values, there we pick either the parents value, props value, or default value - without merging. + */ + public initValueObjectRef = (attribute: AttributeFEModel): void => { + attribute.resetValueObjValidation(); + if (attribute.isDeclared) { //if attribute is declared, it gets a simple output instead. List and map values and pseudo-children will be handled in attribute component + attribute.valueObj = attribute.value || attribute.defaultValue || null; // use null for empty value object + if (attribute.valueObj && typeof attribute.valueObj == 'object') { + attribute.valueObj = JSON.stringify(attribute.valueObj); + } + } else { + attribute.valueObj = attribute.getValueObj(); + if (attribute.derivedDataType == DerivedAttributeType.LIST || attribute.derivedDataType == DerivedAttributeType.MAP) { + attribute.flattenedChildren = []; + Object.keys(attribute.valueObj).forEach((key) => { + attribute.flattenedChildren.push(...this.createListOrMapChildren(attribute, key, attribute.valueObj[key])) + }); + } else if (attribute.derivedDataType === DerivedAttributeType.COMPLEX) { + attribute.flattenedChildren = this.createFlattenedChildren(attribute.type, attribute.name); + this.assignFlattenedChildrenValues(attribute.valueObj, attribute.flattenedChildren, attribute.name); + attribute.flattenedChildren.forEach((childProp) => { + attribute.childPropUpdated(childProp); + }); + } + } + attribute.updateValueObjOrig(); + }; + + /* + * Loops through flattened attributes array and to assign values + * Then, convert any neccessary strings to objects, and vis-versa + * For list or map property, creates new children props if valueObj has values + */ + public assignFlattenedChildrenValues = (parentValueJSON: any, derivedPropArray: Array<DerivedFEAttribute>, parentName: string) => { + if (!derivedPropArray || !parentName) return; + let propsToPushMap: Map<number, Array<DerivedFEAttribute>> = new Map<number, Array<DerivedFEAttribute>>(); + derivedPropArray.forEach((prop, index) => { + + let propNameInObj = prop.attributesName.substring(prop.attributesName.indexOf(parentName) + parentName.length + 1).split('#').join('.'); //extract everything after parent name + prop.valueObj = _.get(parentValueJSON, propNameInObj, prop.value || prop.defaultValue || null); //assign value -first value of parent if exists. If not, prop.value if not, prop.defaultvalue + prop.value = (prop.valueObj !== null && (typeof prop.valueObj) != 'string') ? JSON.stringify(prop.valueObj) : prop.valueObj; + + if ((prop.isDeclared || prop.type == PROPERTY_TYPES.STRING || prop.type == PROPERTY_TYPES.JSON)) { //Stringify objects of items that are declared or from type string/json + prop.valueObj = (prop.valueObj !== null && typeof prop.valueObj == 'object') ? JSON.stringify(prop.valueObj) : prop.valueObj; + } else if(prop.type == PROPERTY_TYPES.INTEGER || prop.type == PROPERTY_TYPES.FLOAT || prop.type == PROPERTY_TYPES.BOOLEAN){ //parse ints and non-string simple types + prop.valueObj = (prop.valueObj !== null && typeof prop.valueObj == PROPERTY_TYPES.STRING) ? JSON.parse(prop.valueObj) : prop.valueObj; + } else { //parse strings that should be objects + if (prop.derivedDataType == DerivedAttributeType.COMPLEX) { + prop.valueObj = (prop.valueObj === null || typeof prop.valueObj != 'object') ? JSON.parse(prop.valueObj || '{}') : prop.valueObj; + } else if (prop.derivedDataType == DerivedAttributeType.LIST) { + prop.valueObj = (prop.valueObj === null || typeof prop.valueObj != 'object') ? JSON.parse(prop.valueObj || '[]') : prop.valueObj; + } else if (prop.derivedDataType == DerivedAttributeType.MAP) { + if (!prop.isChildOfListOrMap || !prop.schema.property.isSimpleType) { + prop.valueObj = (prop.valueObj === null || typeof prop.valueObj != 'object') ? JSON.parse(prop.valueObj || '{}') : prop.valueObj; + } + } + if ((prop.derivedDataType == DerivedAttributeType.LIST || prop.derivedDataType == DerivedAttributeType.MAP) && typeof prop.valueObj == 'object' && prop.valueObj !== null && Object.keys(prop.valueObj).length) { + let newProps: Array<DerivedFEAttribute> = []; + Object.keys(prop.valueObj).forEach((key) => { + newProps.push(...this.createListOrMapChildren(prop, key, prop.valueObj[key]));//create new children, assign their values, and then add to array + }); + propsToPushMap[index + 1] = newProps; + } + } + + prop.valueObj = AttributeFEModel.cleanValueObj(prop.valueObj); + }); + + //add props after we're done looping (otherwise our loop gets messed up). Push in reverse order, so we dont mess up indexes. + Object.keys(propsToPushMap).reverse().forEach((indexToInsert) => { + derivedPropArray.splice(+indexToInsert, 0, ...propsToPushMap[indexToInsert]); //slacker parsing + }); + } + + public resetAttributeValue = (attribute: AttributeFEModel, newValue: string, nestedPath?: string): void => { + attribute.value = newValue; + if (nestedPath) { + let newAttrib = attribute.flattenedChildren.find(attrib => attrib.attributesName == nestedPath); + newAttrib && this.assignFlattenedChildrenValues(JSON.parse(newValue), [newAttrib], attribute.name); + attribute.updateValueObjOrig(); + } else { + this.initValueObjectRef(attribute); + } + } + +} diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/services/hierarchy-nav.service.ts b/catalog-ui/src/app/ng2/pages/attributes-outputs/services/hierarchy-nav.service.ts new file mode 100644 index 0000000000..cef2eb2793 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/services/hierarchy-nav.service.ts @@ -0,0 +1,84 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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. + * ============LICENSE_END========================================================= + */ + +import {Injectable} from '@angular/core'; +import {AttributeFEModel} from "../../../../models/attributes-outputs/attribute-fe-model"; +import {SimpleFlatAttribute} from "app/models/attributes-outputs/simple-flat-attribute"; +import {DerivedFEAttribute} from "../../../../models/attributes-outputs/derived-fe-attribute"; + +@Injectable() +export class HierarchyNavService { + /** + * Build hierarchy structure for the tree when user selects on table row. + * First create Array<SimpleFlatAttribute> and insert also the parent (AttributeFEModel) to this array. + * The Array is flat and contains SimpleFlatAttribute that has parentName and uniqueId. + * Now we build hierarchy from this Array (that includes children) and return it for the tree + * + * @argument attribute: AttributeFEModel - attribute contains flattenedChildren array of DerivedFEAttribute + * @returns Array<SimpleFlatAttribute> - containing children Array<SimpleFlatAttribute>, augmantin children to SimpleFlatAttribute. + */ + public getSimpleAttributesTree(attribute: AttributeFEModel, instanceName: string): Array<SimpleFlatAttribute> { + // Build Array of SimpleFlatAttribute before unflatten function + let flatAttributes: Array<SimpleFlatAttribute> = []; + flatAttributes.push(this.createSimpleFlatAttribute(attribute, instanceName)); // Push the root attribute + attribute.flattenedChildren.forEach((child: DerivedFEAttribute): void => { + if (child.isChildOfListOrMap && child.schema.property.isSimpleType) return; //do not display non-complex children of list or map + flatAttributes.push(this.createSimpleFlatAttribute(child, instanceName)); + }); + + let tree = this.unflatten(flatAttributes, '', []); + return tree[0].childrens; // Return the childrens without the root. + } + + public createSimpleFlatAttribute = (attribute: AttributeFEModel | DerivedFEAttribute, instanceName: string): SimpleFlatAttribute => { + if (attribute instanceof AttributeFEModel) { + return new SimpleFlatAttribute(attribute.uniqueId, attribute.name, attribute.name, '', instanceName); + } else { + let attribName: string = (attribute.isChildOfListOrMap) ? attribute.mapKey : attribute.name; + return new SimpleFlatAttribute(attribute.uniqueId, attribute.attributesName, attribName, attribute.parentName, instanceName); + } + + } + + /** + * Unflatten Array<SimpleFlatAttribute> and build hierarchy. + * The result will be Array<SimpleFlatAttribute> that augmantin with children for each SimpleFlatAttribute. + */ + private unflatten(array: Array<SimpleFlatAttribute>, parent: any, tree?: any): any { + tree = typeof tree !== 'undefined' ? tree : []; + parent = typeof parent !== 'undefined' && parent !== '' ? parent : {path: ''}; + + var children = array.filter((child: SimpleFlatAttribute): boolean => { + return child.parentName == parent.path; + }); + + if (children && children.length) { + if (parent.path == '') { + tree = children; + } else { + parent['children'] = children; + } + children.forEach((child): void => { + this.unflatten(array, child); + }); + } + return tree; + } +} diff --git a/catalog-ui/src/app/ng2/pages/attributes-outputs/services/outputs.utils.ts b/catalog-ui/src/app/ng2/pages/attributes-outputs/services/outputs.utils.ts new file mode 100644 index 0000000000..c2df55e6a4 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/attributes-outputs/services/outputs.utils.ts @@ -0,0 +1,40 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2021 Nordix Foundation. 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. + * ============LICENSE_END========================================================= + */ + +import { Injectable } from '@angular/core'; +import {OutputFEModel} from "../../../../models/attributes-outputs/output-fe-model"; + +@Injectable() +export class OutputsUtils { + + constructor() {} + + public initDefaultValueObject = (output: OutputFEModel): void => { + output.resetDefaultValueObjValidation(); + output.defaultValueObj = output.getDefaultValueObj(); + output.updateDefaultValueObjOrig(); + }; + + public resetOutputDefaultValue = (output: OutputFEModel, newDefaultValue: string): void => { + output.defaultValue = newDefaultValue; + this.initDefaultValueObject(output); + } + +} diff --git a/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.module.ts b/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.module.ts index 3def63e0d2..0d676ed950 100644 --- a/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.module.ts +++ b/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.module.ts @@ -17,8 +17,7 @@ * limitations under the License. * ============LICENSE_END========================================================= */ -import { NgModule } from "@angular/core"; -import {HierarchyNavigationComponent} from "../../components/logic/hierarchy-navigtion/hierarchy-navigation.component"; +import {NgModule} from "@angular/core"; import {FormsModule} from "@angular/forms"; import {PropertyTableModule} from "../../components/logic/properties-table/property-table.module"; import {UiElementsModule} from "../../components/ui/ui-elements.module"; @@ -36,29 +35,30 @@ import {InputsUtils} from "./services/inputs.utils"; import {ComponentModeService} from "../../services/component-services/component-mode.service"; import {SdcUiComponentsModule} from "onap-ui-angular"; import {ModalFormsModule} from "app/ng2/components/ui/forms/modal-forms.module"; +import {HierarchyNavigationModule} from "../../components/logic/hierarchy-navigtion/hierarchy-navigation.module"; @NgModule({ - declarations: [ - PropertiesAssignmentComponent, - InputsTableComponent, - HierarchyNavigationComponent, - FilterPropertiesAssignmentComponent - ], - imports: [ - BrowserModule, - FormsModule, - GlobalPipesModule, - PropertyTableModule, - PoliciesTableModule, - UiElementsModule, - SdcUiComponentsModule, - ModalFormsModule], + declarations: [ + PropertiesAssignmentComponent, + InputsTableComponent, + FilterPropertiesAssignmentComponent + ], + imports: [ + BrowserModule, + FormsModule, + GlobalPipesModule, + PropertyTableModule, + PoliciesTableModule, + HierarchyNavigationModule, + UiElementsModule, + SdcUiComponentsModule, + ModalFormsModule], - entryComponents: [PropertiesAssignmentComponent], - exports: [ - PropertiesAssignmentComponent - ], - providers: [PropertiesService, HierarchyNavService, PropertiesUtils, InputsUtils, DataTypeService, ComponentModeService] + entryComponents: [PropertiesAssignmentComponent], + exports: [ + PropertiesAssignmentComponent + ], + providers: [PropertiesService, HierarchyNavService, PropertiesUtils, InputsUtils, DataTypeService, ComponentModeService] }) export class PropertiesAssignmentModule { diff --git a/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.component.ts b/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.component.ts index 5053d52cc8..8167caa959 100644 --- a/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.component.ts +++ b/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.component.ts @@ -6,6 +6,7 @@ import { DataTypeService } from 'app/ng2/services/data-type.service'; import { PROPERTY_DATA } from 'app/utils'; import * as _ from 'lodash'; import { PROPERTY_TYPES } from '../../../../utils'; +import {Validation} from "../../../../view-models/workspace/tabs/general/general-view-model"; @Component({ selector: 'property-creator', @@ -15,12 +16,10 @@ import { PROPERTY_TYPES } from '../../../../utils'; export class PropertyCreatorComponent { + validation:Validation; typesProperties: DropdownValue[]; typesSchemaProperties: DropdownValue[]; propertyModel: PropertyBEModel; - // propertyNameValidationPattern:RegExp = /^[a-zA-Z0-9_:-]{1,50}$/; - // commentValidationPattern:RegExp = /^[\u0000-\u00BF]*$/; - // types:Array<string>; dataTypes: DataTypesMap; isLoading: boolean; diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts index b0a7651809..426ed4063e 100644 --- a/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts @@ -11,7 +11,7 @@ import { AttributeOptions } from './attributes-options'; @Component({ selector: 'attribute-modal', templateUrl: './attribute-modal.component.html', - styleUrls: ['./attributes.component.less'] + styleUrls: ['../../../../view-models/workspace/tabs/attributes/attributes.component.less'] }) export class AttributeModalComponent implements OnInit { diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.html b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.html deleted file mode 100644 index 6d50bbe11b..0000000000 --- a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.html +++ /dev/null @@ -1,93 +0,0 @@ -<!-- - ~ Copyright (C) 2018 AT&T Intellectual Property. 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. ---> -<div class="workspace-attributes"> - - <div class="action-bar-wrapper"> - <svg-icon-label - *ngIf="!(this.isViewOnly$ | async)" - class="add-attr-icon" - [name]="'plus'" - [mode]="'primary'" - [size]="'medium'" - [label]="'Add'" - [labelPlacement]="'right'" - [labelClassName]="'externalActionLabel'" - (click)="onAddAttribute()"> - </svg-icon-label> - </div> - - <ngx-datatable - columnMode="flex" - [footerHeight]="0" - [limit]="50" - [headerHeight]="40" - [rowHeight]="35" - [rows]="attributes" - #componentAttributesTable - (activate)="onExpandRow($event)"> - - <ngx-datatable-row-detail [rowHeight]="80"> - <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template> - <div>{{row.description}}</div> - </ng-template> - </ngx-datatable-row-detail> - - <ngx-datatable-column [resizeable]="false" name="Name" [flexGrow]="2"> - - <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded"> - <div class="expand-collapse-cell"> - <svg-icon [clickable]="true" class="expand-collapse-icon" - [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" - [size]="'medium'"></svg-icon> - <span>{{ row.name }}</span> - </div> - </ng-template> - - </ngx-datatable-column> - - <ngx-datatable-column [resizeable]="false" name="Type" [flexGrow]="1"> - <ng-template ngx-datatable-cell-template let-row="row"> - {{row.type}} - </ng-template> - </ngx-datatable-column> - - <ngx-datatable-column [resizeable]="false" name="Default Value" [flexGrow]="3"> - <ng-template ngx-datatable-cell-template let-row="row"> - {{row._default}} - </ng-template> - </ngx-datatable-column> - - <ngx-datatable-column *ngIf="!(this.isViewOnly$ | async)" [resizeable]="false" name="Action" [flexGrow]="1"> - <ng-template ngx-datatable-cell-template let-row="row" let-rowIndex="rowIndex"> - <div class="actionColumn"> - <svg-icon [clickable]="true" - [mode]="'primary2'" - [name]="'edit-o'" - [size]="'medium'" - (click)="onEditAttribute($event, row)"> - </svg-icon> - <svg-icon [clickable]="true" - [mode]="'primary2'" - [name]="'trash-o'" - (click)="onDeleteAttribute($event, row)" - [size]="'medium'"> - </svg-icon> - </div> - </ng-template> - </ngx-datatable-column> - - </ngx-datatable> -</div> diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.less b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.less deleted file mode 100644 index 3e91ae4689..0000000000 --- a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.less +++ /dev/null @@ -1,36 +0,0 @@ -.action-bar-wrapper { - flex: 0 0 30%; - display: flex; - justify-content: flex-end; - margin-bottom: 10px; -} - -.add-attr-icon{ - cursor: pointer; -} - -.attr-container { - display: flex; - justify-content: space-between; - - .attr-col { - display: flex; - flex-direction: column; - max-width: 275px; - flex-grow: 1; - } - -} - -.attributeType { - margin-bottom: 10px; -} - -sdc-checkbox { - margin-top: 20px; -} - -.actionColumn { - text-align: center; - padding: 5px; -}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.spec.ts deleted file mode 100644 index f676e2b4d9..0000000000 --- a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.spec.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture } from '@angular/core/testing'; -import { NgxDatatableModule } from '@swimlane/ngx-datatable'; -import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; -import 'rxjs/add/observable/of'; -import { Observable } from 'rxjs/Rx'; -import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; -import { ComponentMetadata } from '../../../../models/component-metadata'; -import { ModalsHandler } from '../../../../utils'; -import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; -import { TranslateService } from '../../../shared/translator/translate.service'; -import { WorkspaceService } from '../workspace.service'; -import { AttributesComponent } from './attributes.component'; - -describe('attributes component', () => { - - let fixture: ComponentFixture<AttributesComponent>; - - // Mocks - let workspaceServiceMock: Partial<WorkspaceService>; - let topologyTemplateServiceMock: Partial<TopologyTemplateService>; - let loaderServiceMock: Partial<SdcUiServices.LoaderService>; - let componentMetadataMock: ComponentMetadata; - let modalServiceMock: Partial<SdcUiServices.ModalService>; - - const mockAttributesList = [ - { uniqueId: '1', name: 'attr1', description: 'description1', type: 'string', hidden: false, defaultValue: 'val1', schema: null }, - { uniqueId : '2', name : 'attr2', description: 'description2', type : 'int', hidden : false, defaultValue : 1, schema : null}, - { uniqueId : '3', name : 'attr3', description: 'description3', type : 'double', hidden : false, defaultValue : 1.0, schema : null}, - { uniqueId : '4', name : 'attr4', description: 'description4', type : 'boolean', hidden : false, defaultValue : true, schema : null}, - ]; - - const newAttribute = { - uniqueId : '5', name : 'attr5', description: 'description5', type : 'string', hidden : false, defaultValue : 'val5', schema : null - }; - const updatedAttribute = { - uniqueId : '2', name : 'attr2', description: 'description_new', type : 'string', hidden : false, defaultValue : 'new_val2', schema : null - }; - const errorAttribute = { - uniqueId : '99', name : 'attr99', description: 'description_error', type : 'string', hidden : false, defaultValue : 'error', schema : null - }; - - beforeEach( - async(() => { - - componentMetadataMock = new ComponentMetadata(); - componentMetadataMock.uniqueId = 'fake'; - componentMetadataMock.componentType = 'VL'; - - topologyTemplateServiceMock = { - getComponentAttributes: jest.fn().mockResolvedValue({ attributes : mockAttributesList }), - addAttributeAsync: jest.fn().mockImplementation( - (compType, cUid, attr) => { - if (attr === errorAttribute) { - return Observable.throwError('add_error').toPromise(); - } else { - return Observable.of(newAttribute).toPromise(); - } - } - ), - updateAttributeAsync: jest.fn().mockImplementation( - (compType, cUid, attr) => { - if (attr === errorAttribute) { - return Observable.throwError('update_error').toPromise(); - } else { - return Observable.of(updatedAttribute).toPromise(); - } - } - ), - deleteAttributeAsync: jest.fn().mockImplementation((cid, ctype, attr) => Observable.of(attr)) - }; - - workspaceServiceMock = { - metadata: componentMetadataMock - }; - - const customModalInstance = { innerModalContent: { instance: { onValidationChange: { subscribe: jest.fn()}}}}; - - modalServiceMock = { - openInfoModal: jest.fn(), - openCustomModal: jest.fn().mockImplementation(() => customModalInstance) - }; - - loaderServiceMock = { - activate: jest.fn(), - deactivate: jest.fn() - }; - - const configure: ConfigureFn = (testBed) => { - testBed.configureTestingModule({ - declarations: [AttributesComponent], - imports: [NgxDatatableModule], - schemas: [NO_ERRORS_SCHEMA], - providers: [ - {provide: WorkspaceService, useValue: workspaceServiceMock}, - {provide: TopologyTemplateService, useValue: topologyTemplateServiceMock}, - {provide: ModalsHandler, useValue: {}}, - {provide: TranslateService, useValue: { translate: jest.fn() }}, - {provide: SdcUiServices.ModalService, useValue: modalServiceMock }, - {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock } - ], - }); - }; - - configureTests(configure).then((testBed) => { - fixture = testBed.createComponent(AttributesComponent); - }); - }) - ); - - it('should see exactly 1 attributes on init', async () => { - await fixture.componentInstance.asyncInitComponent(); - expect(fixture.componentInstance.getAttributes().length).toEqual(4); - }); - - it('should see exactly 5 attributes when adding', async () => { - await fixture.componentInstance.asyncInitComponent(); - expect(fixture.componentInstance.getAttributes().length).toEqual(4); - - await fixture.componentInstance.addOrUpdateAttribute(newAttribute, false); - expect(fixture.componentInstance.getAttributes().length).toEqual(5); - }); - - it('should see exactly 3 attributes when deleting', async () => { - await fixture.componentInstance.asyncInitComponent(); - expect(fixture.componentInstance.getAttributes().length).toEqual(4); - const attrToDelete = mockAttributesList[0]; - expect(fixture.componentInstance.getAttributes().filter((attr) => attr.uniqueId === attrToDelete.uniqueId).length).toEqual(1); - await fixture.componentInstance.deleteAttribute(attrToDelete); - expect(fixture.componentInstance.getAttributes().length).toEqual(3); - expect(fixture.componentInstance.getAttributes().filter((attr) => attr.uniqueId === attrToDelete.uniqueId).length).toEqual(0); - }); - - it('should see updated attribute', async () => { - await fixture.componentInstance.asyncInitComponent(); - - await fixture.componentInstance.addOrUpdateAttribute(updatedAttribute, true); - expect(fixture.componentInstance.getAttributes().length).toEqual(4); - const attribute = fixture.componentInstance.getAttributes().filter( (attr) => { - return attr.uniqueId === updatedAttribute.uniqueId; - })[0]; - expect(attribute.description).toEqual( 'description_new'); - }); - - it('Add fails, make sure loader is deactivated and attribute is not added', async () => { - await fixture.componentInstance.asyncInitComponent(); - const numAttributes = fixture.componentInstance.getAttributes().length; - await fixture.componentInstance.addOrUpdateAttribute(errorAttribute, false); // Add - expect(loaderServiceMock.deactivate).toHaveBeenCalled(); - expect(fixture.componentInstance.getAttributes().length).toEqual(numAttributes); - }); - - it('Update fails, make sure loader is deactivated', async () => { - await fixture.componentInstance.asyncInitComponent(); - const numAttributes = fixture.componentInstance.getAttributes().length; - await fixture.componentInstance.addOrUpdateAttribute(errorAttribute, true); // Add - expect(loaderServiceMock.deactivate).toHaveBeenCalled(); - expect(fixture.componentInstance.getAttributes().length).toEqual(numAttributes); - }); - - it('on delete modal shell be opened', async () => { - await fixture.componentInstance.asyncInitComponent(); - const event = { stopPropagation: jest.fn() }; - fixture.componentInstance.onDeleteAttribute(event, fixture.componentInstance.getAttributes()[0]); - expect(event.stopPropagation).toHaveBeenCalled(); - expect(modalServiceMock.openInfoModal).toHaveBeenCalled(); - }); - - it('on add modal shell be opened', async () => { - await fixture.componentInstance.asyncInitComponent(); - fixture.componentInstance.onAddAttribute(); - expect(modalServiceMock.openCustomModal).toHaveBeenCalled(); - }); - - it('on edit modal shell be opened', async () => { - await fixture.componentInstance.asyncInitComponent(); - const event = { stopPropagation: jest.fn() }; - fixture.componentInstance.onEditAttribute(event, fixture.componentInstance.getAttributes()[0]); - expect(event.stopPropagation).toHaveBeenCalled(); - expect(modalServiceMock.openCustomModal).toHaveBeenCalled(); - }); -}); diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.ts deleted file mode 100644 index bc47f1456b..0000000000 --- a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; -import { Select } from '@ngxs/store'; -import { IAttributeModel } from 'app/models'; -import * as _ from 'lodash'; -import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; -import { ModalComponent } from 'onap-ui-angular/dist/modals/modal.component'; -import { AttributeModel } from '../../../../models'; -import { Resource } from '../../../../models'; -import { ModalsHandler } from '../../../../utils'; -import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; -import { TranslateService } from '../../../shared/translator/translate.service'; -import { WorkspaceState } from '../../../store/states/workspace.state'; -import { WorkspaceService } from '../workspace.service'; -import { AttributeModalComponent } from './attribute-modal.component'; - -@Component({ - selector: 'attributes', - templateUrl: './attributes.component.html', - styleUrls: ['./attributes.component.less', '../../../../../assets/styles/table-style.less'] -}) -export class AttributesComponent implements OnInit { - - @Select(WorkspaceState.isViewOnly) - isViewOnly$: boolean; - - @ViewChild('componentAttributesTable') - private table: any; - - private componentType: string; - private componentUid: string; - - private attributes: IAttributeModel[] = []; - private temp: IAttributeModel[] = []; - private customModalInstance: ModalComponent; - - constructor(private workspaceService: WorkspaceService, - private topologyTemplateService: TopologyTemplateService, - private modalsHandler: ModalsHandler, - private modalService: SdcUiServices.ModalService, - private loaderService: SdcUiServices.LoaderService, - private translateService: TranslateService) { - - this.componentType = this.workspaceService.metadata.componentType; - this.componentUid = this.workspaceService.metadata.uniqueId; - } - - ngOnInit(): void { - this.asyncInitComponent(); - } - - async asyncInitComponent() { - this.loaderService.activate(); - const response = await this.topologyTemplateService.getComponentAttributes(this.componentType, this.componentUid); - this.attributes = response.attributes; - this.temp = [...response.attributes]; - this.loaderService.deactivate(); - } - - getAttributes(): IAttributeModel[] { - return this.attributes; - } - - addOrUpdateAttribute = async (attribute: AttributeModel, isEdit: boolean) => { - this.loaderService.activate(); - let attributeFromServer: AttributeModel; - this.temp = [...this.attributes]; - - const deactivateLoader = () => { - this.loaderService.deactivate(); - return undefined; - }; - - if (isEdit) { - attributeFromServer = await this.topologyTemplateService - .updateAttributeAsync(this.componentType, this.componentUid, attribute) - .catch(deactivateLoader); - if (attributeFromServer) { - const indexOfUpdatedAttribute = _.findIndex(this.temp, (e) => e.uniqueId === attributeFromServer.uniqueId); - this.temp[indexOfUpdatedAttribute] = attributeFromServer; - } - } else { - attributeFromServer = await this.topologyTemplateService - .addAttributeAsync(this.componentType, this.componentUid, attribute) - .catch(deactivateLoader); - if (attributeFromServer) { - this.temp.push(attributeFromServer); - } - } - this.attributes = this.temp; - this.loaderService.deactivate(); - } - - deleteAttribute = async (attributeToDelete: AttributeModel) => { - this.loaderService.activate(); - this.temp = [...this.attributes]; - const res = await this.topologyTemplateService.deleteAttributeAsync(this.componentType, this.componentUid, attributeToDelete); - _.remove(this.temp, (attr) => attr.uniqueId === attributeToDelete.uniqueId); - this.attributes = this.temp; - this.loaderService.deactivate(); - }; - - openAddEditModal(selectedRow: AttributeModel, isEdit: boolean) { - const component = new Resource(undefined, undefined, undefined); - component.componentType = this.componentType; - component.uniqueId = this.componentUid; - - const title: string = this.translateService.translate('ATTRIBUTE_DETAILS_MODAL_TITLE'); - const attributeModalConfig = { - title, - size: 'md', - type: SdcUiCommon.ModalType.custom, - buttons: [ - { - id: 'save', - text: 'Save', - // spinner_position: Placement.left, - size: 'sm', - callback: () => this.modalCallBack(isEdit), - closeModal: true, - disabled: false, - } - ] as SdcUiCommon.IModalButtonComponent[] - }; - - this.customModalInstance = this.modalService.openCustomModal(attributeModalConfig, AttributeModalComponent, { attributeToEdit: selectedRow }); - this.customModalInstance.innerModalContent.instance. - onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('save').disabled = !isValid); - } - - /*********************** - * Call Backs from UI * - ***********************/ - - /** - * Called when 'Add' is clicked - */ - onAddAttribute() { - this.openAddEditModal(new AttributeModel(), false); - } - - /** - * Called when 'Edit' button is clicked - */ - onEditAttribute(event, row) { - event.stopPropagation(); - - const attributeToEdit: AttributeModel = new AttributeModel(row); - this.openAddEditModal(attributeToEdit, true); - } - - /** - * Called when 'Delete' button is clicked - */ - onDeleteAttribute(event, row: AttributeModel) { - event.stopPropagation(); - const onOk = () => { - this.deleteAttribute(row); - }; - - const title: string = this.translateService.translate('ATTRIBUTE_VIEW_DELETE_MODAL_TITLE'); - const message: string = this.translateService.translate('ATTRIBUTE_VIEW_DELETE_MODAL_TEXT'); - const okButton = new SdcUiComponents.ModalButtonComponent(); - okButton.testId = 'OK'; - okButton.text = 'OK'; - okButton.type = SdcUiCommon.ButtonType.info; - okButton.closeModal = true; - okButton.callback = onOk; - - this.modalService.openInfoModal(title, message, 'delete-modal', [okButton]); - } - - onExpandRow(event) { - if (event.type === 'click') { - this.table.rowDetail.toggleExpandRow(event.row); - } - } - - /** - * Callback from Modal after "Save" is clicked - * - * @param {boolean} isEdit - Whether modal is edit or add attribute - */ - modalCallBack = (isEdit: boolean) => { - const attribute: AttributeModel = this.customModalInstance.innerModalContent.instance.attributeToEdit; - this.addOrUpdateAttribute(attribute, isEdit); - } - -} diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts index 5abb952e37..f85d5298f9 100644 --- a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts @@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { SdcUiComponentsModule } from 'onap-ui-angular'; import { GlobalPipesModule } from '../../../pipes/global-pipes.module'; -import { AttributesComponent } from './attributes.component'; +import { AttributesComponent } from '../../../../view-models/workspace/tabs/attributes/attributes.component'; import { NgxDatatableModule } from '@swimlane/ngx-datatable'; import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; import { AttributeModalComponent } from './attribute-modal.component'; |