summaryrefslogtreecommitdiffstats
path: root/catalog-ui/src/app/ng2/pages/attributes-outputs
diff options
context:
space:
mode:
authorvasraz <vasyl.razinkov@est.tech>2021-02-16 17:37:57 +0000
committerChristophe Closset <christophe.closset@intl.att.com>2021-02-17 15:57:55 +0000
commit26e5029d922779fd7e786c1a31b6b37492132388 (patch)
tree8e8e68a6913749e1405fce951bc7816d4fa35ba3 /catalog-ui/src/app/ng2/pages/attributes-outputs
parentf2c0a4118c3c0b6360b639622766543bd754b59c (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/attributes-outputs')
-rw-r--r--catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.html66
-rw-r--r--catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.less36
-rw-r--r--catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component.ts98
-rw-r--r--catalog-ui/src/app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.module.ts48
-rw-r--r--catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.module.ts65
-rw-r--r--catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.html103
-rw-r--r--catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.less234
-rw-r--r--catalog-ui/src/app/ng2/pages/attributes-outputs/attributes-outputs.page.component.ts719
-rw-r--r--catalog-ui/src/app/ng2/pages/attributes-outputs/services/attributes.utils.ts198
-rw-r--r--catalog-ui/src/app/ng2/pages/attributes-outputs/services/hierarchy-nav.service.ts84
-rw-r--r--catalog-ui/src/app/ng2/pages/attributes-outputs/services/outputs.utils.ts40
11 files changed, 1691 insertions, 0 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);
+ }
+
+}