summaryrefslogtreecommitdiffstats
path: root/catalog-ui/src/app/ng2/components
diff options
context:
space:
mode:
Diffstat (limited to 'catalog-ui/src/app/ng2/components')
-rw-r--r--catalog-ui/src/app/ng2/components/logic/attributes-table/attribute-table.module.ts49
-rw-r--r--catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.html97
-rw-r--r--catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.less264
-rw-r--r--catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.spec.ts190
-rw-r--r--catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.ts116
-rw-r--r--catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.html102
-rw-r--r--catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.less82
-rw-r--r--catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.ts232
-rw-r--r--catalog-ui/src/app/ng2/components/logic/attributes-table/pipes/filterChildAttributes.pipe.ts38
-rw-r--r--catalog-ui/src/app/ng2/components/logic/hierarchy-navigtion/hierarchy-navigation.module.ts32
-rw-r--r--catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.html97
-rw-r--r--catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.less189
-rw-r--r--catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.ts141
-rw-r--r--catalog-ui/src/app/ng2/components/logic/properties-table/properties-table.component.ts1
14 files changed, 1629 insertions, 1 deletions
diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/attribute-table.module.ts b/catalog-ui/src/app/ng2/components/logic/attributes-table/attribute-table.module.ts
new file mode 100644
index 0000000000..5f4b15781a
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/attribute-table.module.ts
@@ -0,0 +1,49 @@
+/*-
+ * ============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 {NgModule} from "@angular/core";
+import {AttributesTableComponent} from "./attributes-table.component";
+import {DynamicAttributeComponent} from "./dynamic-attribute/dynamic-attribute.component";
+import {FormsModule} from "@angular/forms";
+import {UiElementsModule} from "../../ui/ui-elements.module";
+import {CommonModule} from "@angular/common";
+import {FilterChildAttributesPipe} from "./pipes/filterChildAttributes.pipe";
+import {GlobalPipesModule} from "../../../pipes/global-pipes.module";
+import {MultilineEllipsisModule} from "../../../shared/multiline-ellipsis/multiline-ellipsis.module";
+import {AttributesService} from "../../../services/attributes.service";
+
+@NgModule({
+ imports: [
+ FormsModule,
+ CommonModule,
+ GlobalPipesModule,
+ UiElementsModule,
+ MultilineEllipsisModule
+ ],
+ declarations: [
+ FilterChildAttributesPipe,
+ DynamicAttributeComponent,
+ AttributesTableComponent
+ ],
+ exports: [AttributesTableComponent, DynamicAttributeComponent],
+ providers: [FilterChildAttributesPipe, AttributesService]
+})
+export class AttributeTableModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.html b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.html
new file mode 100644
index 0000000000..1115620594
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.html
@@ -0,0 +1,97 @@
+<!--
+ * ============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-table">
+ <loader [display]="isLoading" [size]="'large'" [relative]="true" [loaderDelay]="500"></loader>
+ <div class="table-header">
+ <div class="table-cell col1" (click)="sort('name')">Attribute Name
+ <span *ngIf="sortBy === 'name'" class="table-header-sort-arrow" [ngClass]="{'down': reverse, 'up':!reverse}">
+ </span>
+ </div>
+ <div class="table-cell col2" (click)="sort('type')" *ngIf="!hideAttributeType">Type
+ <span *ngIf="sortBy === 'type'" class="table-header-sort-arrow" [ngClass]="{'down': reverse, 'up':!reverse}">
+ </span>
+ </div>
+ <div class="table-cell col3" (click)="sort('schema.property.type')" *ngIf="!hideAttributeType">EntrySchema
+ <span *ngIf="sortBy === 'schema.property.type'" class="table-header-sort-arrow" [ngClass]="{'down': reverse, 'up':!reverse}">
+ </span>
+ </div>
+ <div class="table-cell valueCol">Value</div>
+ </div>
+ <div class="table-body" [ngClass]="{'view-mode': readonly}">
+ <div class="no-data" *ngIf="!feAttributesMap || !(feAttributesMap | keys).length">No data to display</div>
+
+ <ng-container *ngFor="let instanceId of feAttributesMap | keys; trackBy:vspId">
+ <!-- Icon & Instance Name -->
+ <div class="table-rows-header white-sub-header" *ngIf="feInstanceNamesMap">
+ <span [ngClass]="['prop-instance-icon', feInstanceNamesMap[instanceId].iconClass, 'small']"></span>
+ {{feInstanceNamesMap[instanceId].name}}
+ <div class="sprite-new archive-label" *ngIf="feInstanceNamesMap[instanceId].originArchived == true"></div>
+ </div>
+
+ <div class="table-row" *ngFor="let property of feAttributesMap[instanceId] | searchFilter:'name':searchTerm | propertiesOrderBy:{path: path, direction: direction}; trackBy:property?.name "
+ (click)="onClickAttributeRow(property, instanceId, $event)" [ngClass]="{'selected': selectedAttributeId && selectedAttributeId === property.name, 'readonly': property.isDisabled || property.isDeclared}">
+
+ <div class="table-cell col1" [ngClass]="{'filtered':property.name === attributeNameSearchText}" [class.round-checkbox]="property.isDeclared">
+ <!-- Attribute Name -->
+ <div class="attribute-name">
+ <checkbox *ngIf="hasDeclareOption" [(checked)]="property.isSelected" [disabled]="property.isDisabled || property.isDeclared || readonly"
+ (checkedChange)="attributeChecked(property)" [attr.data-tests-id]="property.name"></checkbox>
+ <div class="inner-cell-div-multiline" tooltip="{{property.name}}">
+ <multiline-ellipsis className="table-cell-multiline-ellipsis" [lines]="2">{{property.name}}</multiline-ellipsis>
+ </div>
+ </div>
+ <span *ngIf="property.description" class="property-description-icon sprite-new show-desc" tooltip="{{property.description}}"
+ tooltipDelay="0"></span>
+ </div>
+ <!-- Attribute Type -->
+ <div class="table-cell col2" *ngIf="!hideAttributeType">
+ <div class="inner-cell-div" tooltip="{{property.type | contentAfterLastDot}}">
+ <span>{{property.type | contentAfterLastDot}}</span>
+ </div>
+ </div>
+ <!-- Attribute ES (Entry Schema) -->
+ <div class="table-cell col3" *ngIf="!hideAttributeType">
+ <div *ngIf="property.schema && property.schema.property && property.schema.property.type" class="inner-cell-div" tooltip="{{property.schema.property.type | contentAfterLastDot}}">
+ <span>{{property.schema.property.type | contentAfterLastDot}}</span>
+ </div>
+ </div>
+ <!-- Attribute Value -->
+ <div class="table-cell valueCol">
+ <dynamic-property
+ [selectedAttributeId]="selectedAttributeId"
+ [hasDeclareOption]="hasDeclareOption"
+ [canBeDeclared]="hasDeclareOption && true"
+ [attribute]="property"
+ [expandedChildId]="property.expandedChildPropertyId"
+ [attributeNameSearchText]="attributeNameSearchText"
+ [readonly]="readonly"
+ (attributeChanged)="onAttributeChanged(property)"
+ (expandChild)="property.updateExpandedChildPropertyId($event)"
+ (clickOnAttributeRow)="onClickAttributeInnerRow($event, instanceId)"
+ (checkAttribute)="attributeChecked(property, $event)"
+ >
+ </dynamic-property>
+ </div>
+ </div>
+ </ng-container>
+
+ </div>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.less b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.less
new file mode 100644
index 0000000000..26ae0d4d74
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.less
@@ -0,0 +1,264 @@
+@import './../../../../../assets/styles/mixins.less';
+@import '../../../../../assets/styles/sprite';
+@smaller-screen: ~"only screen and (max-width: 1580px)";
+
+:host /deep/ input { width:100%;}
+
+.attributes-table {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ height: 100%;
+ text-align: left;
+
+
+ .inner-cell-div {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ height: 20px;
+ }
+
+ .inner-cell-div-multiline {
+ max-width: 100%;
+ }
+
+ .table-header {
+ display: flex;
+ flex-direction:row;
+ flex: 0 0 auto;
+ font-weight:bold;
+ border-top: #d2d2d2 solid 1px;
+ background-color: #f2f2f2;
+
+ .table-cell {
+ color:#191919;
+ font-size:13px;
+ .table-header-sort-arrow {
+ display: inline-block;
+ background-color: transparent;
+ border: none;
+ color: #AAA;
+ margin: 8px 0 0 5px;
+ &.up {
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-bottom: 5px solid;
+ height:5px;
+ }
+ &.down {
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-top: 5px solid;
+ }
+ }
+ }
+ }
+
+ .table-rows-header {
+ border: #d2d2d2 solid 1px;
+ border-top:none;
+ display: flex;
+ align-items: center;
+ .archive-label{
+ margin-left: 10px;
+ }
+ }
+
+ .table-body {
+ display:flex;
+ flex-direction: column;
+ overflow-y:auto;
+ flex: 1;
+ background-color: @main_color_p;
+
+ .no-data {
+ border: #d2d2d2 solid 1px;
+ border-top:none;
+ text-align: center;
+ height: 100%;
+ padding: 20px;
+ }
+ /deep/.selected{
+ background-color: #e6f6fb;
+ color: #009fdb;
+ }
+ &.view-mode{
+ /deep/ .dynamic-property-row:not(.selected){
+ background-color:#f8f8f8;
+ }
+ }
+ .table-row {
+ display: flex;
+ flex-direction:row;
+ flex: 0 0 auto;
+ &.readonly{
+ background-color: #f8f8f8;
+ cursor: auto;
+ }
+
+ &:hover:not(.selected){
+ background-color:#f8f8f8; cursor:pointer;
+ /deep/ .dynamic-property-row:not(.selected){
+ background-color:#f8f8f8; cursor:pointer;
+ }
+ }
+
+ .selected-row {
+ background-color:#e6f6fb;
+ }
+
+ .table-cell.valueCol {
+ padding:0px;
+
+ }
+ }
+ }
+ .table-cell {
+ font-size:13px;
+ flex:1;
+ border: #d2d2d2 solid 1px;
+ border-right:none;
+ border-top:none;
+ padding:10px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow:hidden;
+ display: flex;
+ min-height:40px;
+
+ &:last-child {
+ border-right:#d2d2d2 solid 1px;
+ }
+
+ // Column: Property Name
+ &.col1 {
+ flex: 1 0 190px;
+ max-width:300px;
+ display: flex;
+ @media @smaller-screen { flex: 0 0 25%;}
+
+ .attribute-name {
+ flex: 1;
+ display: flex;
+ overflow: hidden;
+ //max-width: 90%; fix bug 327139
+ }
+
+ .property-description-icon {
+ float: right;
+ margin-top: 4px;
+ margin-left: 15px;
+ flex: 0 0 auto;
+ }
+ }
+
+ // Column: Type
+ &.col2 {
+ flex: 0 0 150px;
+ max-width:150px;
+ @media @smaller-screen { flex: 0 0 20%;}
+ }
+
+ // Column: ES
+ &.col3 {
+ flex:0 0 120px;
+ max-width:120px;
+ @media @smaller-screen { flex: 0 0 15%;}
+ }
+
+ // Column: Value
+ &.valueCol {
+ flex: 2 0 250px;
+ display: flex;
+ @media @smaller-screen { flex: 1 0 40%;}
+ }
+
+
+ /deep/ .checkbox-container {
+ margin-right: 10px;
+ }
+
+ /deep/ &.round-checkbox {
+ .checkbox-container input[type=checkbox].checkbox-hidden {
+ &:checked ~ .checkbox-icon::before {
+ .sprite-new;
+ .round-checked-icon;
+ }
+ &[disabled] ~ .checkbox-icon::before {
+ .sprite-new;
+ .round-checked-icon.disabled;
+ background-color:inherit;
+ border:none;
+ //animation: addDisabledCheck 4s linear;
+ }
+ }
+ }
+ }
+
+ .delete-button-container {
+ max-height: 24px;
+ cursor: pointer;
+ }
+
+ .filtered {
+ /deep/ .checkbox-label-content{
+ background-color: yellow;
+ }
+ }
+
+ dynamic-property {
+ width:100%;
+ &:last-child /deep/ .dynamic-property-row {
+ border-bottom:none;
+ }
+ }
+
+ .table-row {
+ /deep/ .table-cell-multiline-ellipsis .multiline-ellipsis-dots {
+ background: linear-gradient(to right, transparent 0%, #ffffff 80%);
+ padding-left: 1em;
+ }
+
+ &.selected /deep/ .table-cell-multiline-ellipsis .multiline-ellipsis-dots {
+ background: linear-gradient(to right, transparent 0%, #e6f6fb 80%);
+ padding-left: 1em;
+ }
+
+ &.readonly /deep/ .table-cell-multiline-ellipsis .multiline-ellipsis-dots {
+ background: linear-gradient(to right, transparent 0%, #f8f8f8 80%);
+ padding-left: 1em;
+ }
+
+ &:hover:not(.selected) /deep/ .table-cell-multiline-ellipsis .multiline-ellipsis-dots {
+ background: linear-gradient(to right, transparent 0%, #f8f8f8 80%);
+ padding-left: 1em;
+ }
+ }
+
+ .prop-instance-icon {
+ vertical-align: middle;
+ margin-right: 7px;
+ &.defaulticon.small {
+ background-color: @main_color_q;
+ border-radius:14px;
+ }
+ // square icons
+ &.icon-group {
+ .square-icon();
+ background-color: @main_color_a;
+
+ &::before {
+ content: "G";
+ }
+ }
+ &.icon-policy {
+ .square-icon();
+ background-color: @main_color_r;
+
+ &::before {
+ content: "P";
+ }
+ }
+
+ }
+}
diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.spec.ts b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.spec.ts
new file mode 100644
index 0000000000..e916d788ce
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.spec.ts
@@ -0,0 +1,190 @@
+/*-
+ * ============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 {NO_ERRORS_SCHEMA, SimpleChange} from '@angular/core';
+import {ComponentFixture} from '@angular/core/testing';
+import {ConfigureFn, configureTests} from '../../../../../jest/test-config.helper';
+import {ContentAfterLastDotPipe} from '../../../pipes/contentAfterLastDot.pipe';
+import {KeysPipe} from '../../../pipes/keys.pipe';
+import {SearchFilterPipe} from '../../../pipes/searchFilter.pipe';
+import {ModalService} from '../../../services/modal.service';
+import {AttributeRowSelectedEvent, AttributesTableComponent} from './attributes-table.component';
+import {AttributesService} from "../../../services/attributes.service";
+import {AttributeFEModel} from "../../../../models/attributes-outputs/attribute-fe-model";
+import {AttributeBEModel} from "app/models/attributes-outputs/attribute-be-model";
+import {DerivedFEAttribute} from "../../../../models/attributes-outputs/derived-fe-attribute";
+import {PropertiesOrderByPipe} from "app/ng2/pipes/properties-order-by.pipe";
+
+describe('attributes-table component', () => {
+
+ let fixture: ComponentFixture<AttributesTableComponent>;
+ let attributesServiceMock: Partial<AttributesService>;
+ let modalServiceMock: Partial<ModalService>;
+
+ beforeEach(
+ () => {
+ attributesServiceMock = {
+ undoDisableRelatedAttributes: jest.fn(),
+ disableRelatedAttributes: jest.fn()
+ };
+ modalServiceMock = {};
+
+ const configure: ConfigureFn = (testBed) => {
+ testBed.configureTestingModule({
+ declarations: [
+ AttributesTableComponent,
+ KeysPipe,
+ PropertiesOrderByPipe,
+ SearchFilterPipe,
+ ContentAfterLastDotPipe
+ ],
+ imports: [],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: AttributesService, useValue: attributesServiceMock},
+ {provide: ModalService, useValue: modalServiceMock}
+ ],
+ });
+ };
+
+ configureTests(configure).then((testBed) => {
+ fixture = testBed.createComponent(AttributesTableComponent);
+ });
+ }
+ );
+
+ it('When Properties assignment page is loaded, it is sorted by attribute name (acsending)', () => {
+ const fePropertiesMapValues = new SimpleChange('previousValue', 'currentValue', true);
+ const changes = {
+ fePropertiesMap: fePropertiesMapValues
+ };
+
+ // init values before ngOnChanges was called
+ fixture.componentInstance.sortBy = 'existingValue';
+
+ fixture.componentInstance.ngOnChanges(changes);
+
+ expect(fixture.componentInstance.reverse).toEqual(true);
+ expect(fixture.componentInstance.direction).toEqual(fixture.componentInstance.ascUpperLettersFirst);
+ expect(fixture.componentInstance.sortBy).toEqual('name');
+ expect(fixture.componentInstance.path.length).toEqual(1);
+ expect(fixture.componentInstance.path[0]).toEqual('name');
+ });
+
+ it('When ngOnChanges is called without fePropertiesMap,' +
+ ' sortBy will remain as it was', () => {
+ const fePropertiesMapValues = new SimpleChange('previousValue', 'currentValue', true);
+ const changes = {
+ dummyKey: fePropertiesMapValues
+ };
+
+ // init values before ngOnChanges was called
+ fixture.componentInstance.sortBy = 'existingValue';
+ fixture.componentInstance.sort = jest.fn();
+
+ fixture.componentInstance.ngOnChanges(changes);
+
+ expect(fixture.componentInstance.sortBy).toEqual('existingValue');
+ });
+
+ it('When sort is called init this.direction to 1', () => {
+ // init values
+ fixture.componentInstance.reverse = false;
+ fixture.componentInstance.direction = 0;
+ fixture.componentInstance.sortBy = 'initialize.Value';
+ fixture.componentInstance.path = [];
+
+ // call sore function
+ fixture.componentInstance.sort('initialize.Value');
+
+ // expect that
+ expect(fixture.componentInstance.reverse).toBe(true);
+ expect(fixture.componentInstance.direction).toBe(fixture.componentInstance.ascUpperLettersFirst);
+ expect(fixture.componentInstance.sortBy).toBe('initialize.Value');
+ expect(fixture.componentInstance.path.length).toBe(2);
+ expect(fixture.componentInstance.path[0]).toBe('initialize');
+ expect(fixture.componentInstance.path[1]).toBe('Value');
+ });
+
+ it('When sort is called init this.direction to -1', () => {
+ // init values
+ fixture.componentInstance.reverse = true;
+ fixture.componentInstance.direction = 0;
+ fixture.componentInstance.sortBy = 'initialize.Value';
+ fixture.componentInstance.path = [];
+
+ // call sore function
+ fixture.componentInstance.sort('initialize.Value');
+
+ // expect that
+ expect(fixture.componentInstance.reverse).toBe(false);
+ expect(fixture.componentInstance.direction).toBe(fixture.componentInstance.descLowerLettersFirst);
+ });
+
+ it('When onPropertyChanged is called, event is emitted', () => {
+ spyOn(fixture.componentInstance.emitter, 'emit');
+ fixture.componentInstance.onAttributeChanged('testProperty');
+ expect(fixture.componentInstance.emitter.emit).toHaveBeenCalledWith('testProperty');
+ });
+
+ it('When onClickPropertyRow is called, selectedPropertyId is updated and event is emitted.', () => {
+ const attributeFEModel = new AttributeFEModel(new AttributeBEModel());
+ attributeFEModel.name = 'attributeName';
+ const attributeRowSelectedEvent: AttributeRowSelectedEvent = new AttributeRowSelectedEvent(attributeFEModel, 'instanceName');
+
+ spyOn(fixture.componentInstance.selectAttributeRow, 'emit');
+ fixture.componentInstance.onClickAttributeRow(attributeFEModel, 'instanceName');
+
+ expect(fixture.componentInstance.selectedAttributeId).toBe('attributeName');
+ expect(fixture.componentInstance.selectAttributeRow.emit).toHaveBeenCalledWith(attributeRowSelectedEvent);
+ });
+
+ it('When onClickPropertyInnerRow is called, event is emitted.', () => {
+ const derivedFEProperty = new DerivedFEAttribute(new AttributeBEModel());
+ const attributeRowSelectedEvent: AttributeRowSelectedEvent = new AttributeRowSelectedEvent(derivedFEProperty, 'instanceName');
+ spyOn(fixture.componentInstance.selectAttributeRow, 'emit');
+ fixture.componentInstance.onClickAttributeInnerRow(derivedFEProperty, 'instanceName');
+
+ expect(fixture.componentInstance.selectAttributeRow.emit).toHaveBeenCalledWith(attributeRowSelectedEvent);
+ });
+
+ it('When attributeChecked is called, attributesService.undoDisableRelatedProperties is called and event is emitted.', () => {
+
+ const attributeFEModel = new AttributeFEModel(new AttributeBEModel());
+ attributeFEModel.isSelected = false;
+
+ spyOn(fixture.componentInstance.updateCheckedAttributeCount, 'emit');
+ fixture.componentInstance.attributeChecked(attributeFEModel);
+ expect(attributesServiceMock.undoDisableRelatedAttributes).toHaveBeenCalledWith(attributeFEModel, undefined);
+ expect(fixture.componentInstance.updateCheckedAttributeCount.emit).toHaveBeenCalledWith(false);
+ });
+
+ it('When attributeChecked is called, attributesService.disableRelatedProperties is called and event is emitted.', () => {
+
+ const attributeFEModel = new AttributeFEModel(new AttributeBEModel());
+ attributeFEModel.isSelected = true;
+
+ spyOn(fixture.componentInstance.updateCheckedAttributeCount, 'emit');
+ fixture.componentInstance.attributeChecked(attributeFEModel);
+ expect(attributesServiceMock.disableRelatedAttributes).toHaveBeenCalledWith(attributeFEModel, undefined);
+ expect(fixture.componentInstance.updateCheckedAttributeCount.emit).toHaveBeenCalledWith(true);
+ });
+
+});
diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.ts b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.ts
new file mode 100644
index 0000000000..000e2cc6e9
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/attributes-table.component.ts
@@ -0,0 +1,116 @@
+/*-
+ * ============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, EventEmitter, Input, OnChanges, Output, SimpleChanges} from '@angular/core';
+import {InstanceFeDetails} from '../../../../models/instance-fe-details';
+import {InstanceFeAttributesMap} from "../../../../models/attributes-outputs/attribute-fe-map";
+import {AttributeFEModel} from "../../../../models/attributes-outputs/attribute-fe-model";
+import {AttributesService} from "../../../services/attributes.service";
+import {DerivedFEAttribute} from "../../../../models/attributes-outputs/derived-fe-attribute";
+
+@Component({
+ selector: 'attributes-table',
+ templateUrl: './attributes-table.component.html',
+ styleUrls: ['./attributes-table.component.less']
+})
+export class AttributesTableComponent implements OnChanges {
+
+ @Input() feAttributesMap: InstanceFeAttributesMap;
+ @Input() feInstanceNamesMap: Map<string, InstanceFeDetails>;
+ @Input() selectedAttributeId: string;
+ @Input() attributeNameSearchText: string;
+ @Input() searchTerm: string;
+ @Input() readonly: boolean;
+ @Input() isLoading: boolean;
+ @Input() hasDeclareOption: boolean;
+ @Input() hideAttributeType: boolean;
+ @Input() showDelete: boolean;
+
+ @Output('attributeChanged') emitter: EventEmitter<AttributeFEModel> = new EventEmitter<AttributeFEModel>();
+ @Output() selectAttributeRow: EventEmitter<AttributeRowSelectedEvent> = new EventEmitter<AttributeRowSelectedEvent>();
+ @Output() updateCheckedAttributeCount: EventEmitter<boolean> = new EventEmitter<boolean>(); // only for hasDeclareOption
+ @Output() updateCheckedChildAttributeCount: EventEmitter<boolean> = new EventEmitter<boolean>();//only for hasDeclareListOption
+ @Output() deleteAttribute: EventEmitter<AttributeFEModel> = new EventEmitter<AttributeFEModel>();
+
+ sortBy: string;
+ reverse: boolean;
+ direction: number;
+ path: string[];
+
+ readonly ascUpperLettersFirst = 1;
+ readonly descLowerLettersFirst = -1;
+
+ constructor(private attributesService: AttributesService) {
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.fePropertiesMap) {
+ this.sortBy = '';
+ this.sort('name');
+ }
+ }
+
+ sort(sortBy) {
+ this.reverse = (this.sortBy === sortBy) ? !this.reverse : true;
+ this.direction = this.reverse ? this.ascUpperLettersFirst : this.descLowerLettersFirst;
+ this.sortBy = sortBy;
+ this.path = sortBy.split('.');
+ }
+
+ onAttributeChanged = (attribute) => {
+ this.emitter.emit(attribute);
+ }
+
+ // Click on main row (row of AttributeFEModel)
+ onClickAttributeRow = (attribute: AttributeFEModel, instanceName: string) => {
+ this.selectedAttributeId = attribute.name;
+ const attributeRowSelectedEvent: AttributeRowSelectedEvent = new AttributeRowSelectedEvent(attribute, instanceName);
+ this.selectAttributeRow.emit(attributeRowSelectedEvent);
+ }
+
+ // Click on inner row (row of DerivedFEAttribute)
+ onClickAttributeInnerRow = (attribute: DerivedFEAttribute, instanceName: string) => {
+ const attributeRowSelectedEvent: AttributeRowSelectedEvent = new AttributeRowSelectedEvent(attribute, instanceName);
+ this.selectAttributeRow.emit(attributeRowSelectedEvent);
+ }
+
+ attributeChecked = (attrib: AttributeFEModel, childAttribName?: string) => {
+ const isChecked: boolean = (!childAttribName) ? attrib.isSelected : attrib.flattenedChildren.find((attrib) => attrib.attributesName == childAttribName).isSelected;
+
+ if (isChecked) {
+ this.attributesService.disableRelatedAttributes(attrib, childAttribName);
+ } else {
+ this.attributesService.undoDisableRelatedAttributes(attrib, childAttribName);
+ }
+ this.updateCheckedAttributeCount.emit(isChecked);
+
+ }
+
+}
+
+export class AttributeRowSelectedEvent {
+ attributeModel: AttributeFEModel | DerivedFEAttribute;
+ instanceName: string;
+
+ constructor(attributeModel: AttributeFEModel | DerivedFEAttribute, instanceName: string) {
+ this.attributeModel = attributeModel;
+ this.instanceName = instanceName;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.html b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.html
new file mode 100644
index 0000000000..7f271af4e1
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.html
@@ -0,0 +1,102 @@
+<!--
+ * ============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 *ngIf="!attribute.hidden" class="dynamic-property-row nested-level-{{nestedLevel}}" [@fadeIn]
+ [ngClass]="{'selected': selectedAttributeId && selectedAttributeId === attribute.propertiesName, 'readonly': attribute.isDisabled ||attribute.isDeclared}"
+ [class.with-top-border]="attribute.isChildOfListOrMap"
+ (click)="onClickPropertyRow(attribute, $event)">
+ <!-- LEFT CELL -->
+ <ng-container *ngIf="!isAttributeFEModel">
+ <div class="table-cell" *ngIf="canBeDeclared" [ngClass]="{'filtered':attribute.name === attributeNameSearchText}" [class.round-checkbox]="attribute.isDeclared"> <!-- simple children of complex type [@checkEffect]="property.isDeclared"-->
+ <checkbox *ngIf="hasDeclareOption" [(checked)]="attribute.isSelected" [disabled]="attribute.isDisabled ||attribute.isDeclared || readonly" (checkedChange)="checkAttribute.emit(attribute.propertiesName)" ></checkbox>
+ <div class="inner-cell-div" tooltip="{{attribute.name}}"><span>{{attribute.name}}</span></div>
+ </div>
+ <div class="table-cell" *ngIf="!canBeDeclared && !attribute.isChildOfListOrMap">
+ <div class="inner-cell-div" tooltip="{{attribute.name}}"><span>{{attribute.name}}</span></div>
+ </div> <!-- simple children of complex type within map or list -->
+ <div class="table-cell map-entry" *ngIf="attribute.isChildOfListOrMap && attribType == derivedAttributeType.MAP"><!-- map left cell -->
+ <dynamic-element #mapKeyInput
+ class="value-input"
+ pattern="validationUtils.getValidationPattern(string)"
+ [value]="attribute.mapKey"
+ type="string"
+ [name]="attribute.name"
+ (elementChanged)="mapKeyChanged.emit($event.value)"
+ [readonly]="readonly"
+ [testId]="'prop-key-' + attributeTestsId"
+ ></dynamic-element>
+ </div>
+ </ng-container>
+ <!-- RIGHT CELL OR FULL WIDTH CELL-->
+ <ng-container *ngIf="attribType == derivedAttributeType.SIMPLE || attribute.isDeclared || (attribute.isChildOfListOrMap && attribType == derivedAttributeType.MAP && attribute.schema.property.isSimpleType)">
+ <div class="table-cell">
+ <dynamic-element class="value-input"
+ pattern="validationUtils.getValidationPattern(property.type)"
+ [value]="attribute.isDeclared ? attribute.value : attribute.valueObj"
+ [type]="attribute.isDeclared ? 'string' : attribute.type"
+ [name]="attribute.name"
+ [path]="attribute.propertiesName"
+ (elementChanged)="onElementChanged($event)"
+ [readonly]="readonly || attribute.isDeclared || attribute.isDisabled"
+ [testId]="'prop-' + attributeTestsId"
+ [declared] = "attribute.isDeclared"
+ [constraints] = "constraints"
+ ></dynamic-element>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="!isAttributeFEModel && attribType != derivedAttributeType.SIMPLE && !attribute.isDeclared"> <!-- right cell for complex elements, or list complex -->
+ <div class="table-cell" *ngIf="attribType == derivedAttributeType.COMPLEX">{{attribute.type | contentAfterLastDot }}</div>
+ <div class="table-cell" *ngIf="attribType == derivedAttributeType.MAP && !attribute.schema.property.isSimpleType">{{attribute.schema.property.type | contentAfterLastDot }}</div>
+ </ng-container>
+ <ng-container *ngIf="isAttributeFEModel && (attribType == derivedAttributeType.LIST || attribType == derivedAttributeType.MAP) && !attribute.isDeclared"><!-- empty, full-width table cell - for PropertyFEModel of type list or map -->
+ <div class="table-cell empty"></div>
+ </ng-container>
+ <!-- ICONS: add, delete, and expand -->
+ <ng-container *ngIf="!attribute.isDeclared">
+ <a *ngIf="(attribType == derivedAttributeType.LIST || attribType == derivedAttributeType.MAP) && !attribute.isChildOfListOrMap" class="property-icon add-item" (click)="createNewChildProperty();" [ngClass]="{'disabled':readonly || preventInsertItem(attribute)}" [attr.data-tests-id]="'add-to-list-' + attributeTestsId">Add value to list</a>
+ <span *ngIf="attribute.isChildOfListOrMap" (click)="deleteItem.emit(attribute);" class="property-icon sprite-new delete-item-icon" [ngClass]="{'disabled':readonly}" [attr.data-tests-id]="'delete-from-list-' + attributeTestsId"></span>
+ <span *ngIf="!isAttributeFEModel && (attribType == derivedAttributeType.COMPLEX || ((attribType == derivedAttributeType.LIST || attribType == derivedAttributeType.MAP) && hasChildren))" (click)="expandChildById(attribPath)" class="property-icon sprite-new round-expand-icon" [class.open]="expandedChildId.indexOf(attribPath) == 0" [attr.data-tests-id]="'expand-' + attributeTestsId" ></span>
+ </ng-container>
+
+</div>
+<!-- FLAT CHILDREN -->
+<div class="flat-children-container" *ngIf="isAttributeFEModel && !attribute.isDeclared">
+ <ng-container *ngFor="let prop of attribute.flattenedChildren | filterChildAttributes: expandedChildId; trackBy:prop?.propertiesName">
+ <dynamic-property
+ [selectedAttributeId]="selectedAttributeId"
+ [hasDeclareOption]="hasDeclareOption"
+ [canBeDeclared]="hasDeclareOption && prop.canBeDeclared"
+ [attribute]="prop"
+ [rootAttribute]="rootAttribute || attribute"
+ [expandedChildId]="expandedChildId"
+ [attributeNameSearchText]="attributeNameSearchText"
+ [readonly]="readonly"
+ [hasChildren]="getHasChildren(prop)"
+ (propertyChanged)="childValueChanged(prop)"
+ (mapKeyChanged)="updateChildKeyInParent(prop, $event)"
+ (expandChild)="expandChildById($event)"
+ (deleteItem)="deleteListOrMapItem($event)"
+ (clickOnAttributeRow)="onClickPropertyRow($event)"
+ (checkAttribute)="checkedChange($event)"
+ (addChildAttribsToParent)="addChildProps($event, prop.propertiesName)"
+ >
+ </dynamic-property>
+ </ng-container>
+</div>
diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.less b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.less
new file mode 100644
index 0000000000..fd572b0f2d
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.less
@@ -0,0 +1,82 @@
+@import '../../../../../../assets/styles/variables.less';
+.flat-children-container {
+ .dynamic-property-row {
+ /*create nested left border classes for up to 10 levels of nesting*/
+ .nested-border-loop(@i) when (@i > 0) {
+ @size: (@i - 1) *2;
+ &.nested-level-@{i} .table-cell:first-child {
+ border-left: ~"solid @{size}px #009fdb";
+ }
+ .nested-border-loop(@i - 1)
+ }
+ .nested-border-loop(10);
+ }
+ dynamic-property {
+ &:first-child .dynamic-property-row.with-top-border {
+ border-top:solid 1px #d2d2d2;
+ }
+ &:not(:last-child) .dynamic-property-row {
+ border-bottom:solid 1px #d2d2d2;
+ }
+ }
+}
+.dynamic-property-row {
+ display:flex;
+ flex-direction:row;
+ align-items: stretch;
+
+ &.readonly{
+ background-color: @tlv_color_t;
+ cursor: auto;
+ }
+ //for the case that the parent is disabled but the child is enabled
+ &:not(.readonly){
+ background-color: @main_color_p;
+ }
+
+ .table-cell {
+ flex: 1;
+ padding:9px;
+ justify-content: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ &:first-child {
+ flex: 0 0 50%;
+ border-right:#d2d2d2 solid 1px;
+ &:only-of-type {
+ flex: 1 1 100%;
+ border-right:none;
+ }
+ }
+ &.empty {
+ height:40px;
+ }
+ }
+ .property-icon {
+ flex: 0 0 auto;
+ margin-right:10px;
+ align-self:center;
+ cursor:pointer;
+ }
+}
+
+.filtered {
+ /deep/ .checkbox-label-content{
+ background-color: yellow;
+ }
+}
+.inner-cell-div{
+ max-width: 100%;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ display: inline;
+ padding-left: 8px;
+}
+.error {
+ border: solid 1px @func_color_q;
+ color: @func_color_q;
+ outline: none;
+ box-sizing: border-box;
+}
diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.ts b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.ts
new file mode 100644
index 0000000000..39faac9283
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/dynamic-attribute/dynamic-attribute.component.ts
@@ -0,0 +1,232 @@
+/*-
+ * ============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 {Component, EventEmitter, Input, Output, ViewChild} from "@angular/core";
+import {PROPERTY_TYPES} from 'app/utils';
+import {DataTypeService} from "../../../../services/data-type.service";
+import {animate, style, transition, trigger} from '@angular/animations';
+import {IUiElementChangeEvent} from "../../../ui/form-components/ui-element-base.component";
+import {DynamicElementComponent} from "../../../ui/dynamic-element/dynamic-element.component";
+import {DerivedAttributeType} from "../../../../../models/attributes-outputs/attribute-be-model";
+import {AttributeFEModel} from "app/models/attributes-outputs/attribute-fe-model";
+import {DerivedFEAttribute} from "../../../../../models/attributes-outputs/derived-fe-attribute";
+import {AttributesUtils} from "../../../../pages/attributes-outputs/services/attributes.utils";
+
+@Component({
+ selector: 'dynamic-property',
+ templateUrl: './dynamic-attribute.component.html',
+ styleUrls: ['./dynamic-attribute.component.less'],
+ animations: [trigger('fadeIn', [transition(':enter', [style({opacity: '0'}), animate('.7s ease-out', style({opacity: '1'}))])])]
+})
+export class DynamicAttributeComponent {
+
+ derivedAttributeType = DerivedAttributeType;
+ attribType: DerivedAttributeType;
+ attribPath: string;
+ isAttributeFEModel: boolean;
+ nestedLevel: number;
+ attributeTestsId: string;
+ constraints: string[];
+
+ @Input() canBeDeclared: boolean;
+ @Input() attribute: AttributeFEModel | DerivedFEAttribute;
+ @Input() expandedChildId: string;
+ @Input() selectedAttributeId: string;
+ @Input() attributeNameSearchText: string;
+ @Input() readonly: boolean;
+ @Input() hasChildren: boolean;
+ @Input() hasDeclareOption: boolean;
+ @Input() rootAttribute: AttributeFEModel;
+
+ @Output('attributeChanged') emitter: EventEmitter<void> = new EventEmitter<void>();
+ @Output() expandChild: EventEmitter<string> = new EventEmitter<string>();
+ @Output() checkAttribute: EventEmitter<string> = new EventEmitter<string>();
+ @Output() deleteItem: EventEmitter<string> = new EventEmitter<string>();
+ @Output() clickOnAttributeRow: EventEmitter<AttributeFEModel | DerivedFEAttribute> = new EventEmitter<AttributeFEModel | DerivedFEAttribute>();
+ @Output() mapKeyChanged: EventEmitter<string> = new EventEmitter<string>();
+ @Output() addChildAttribsToParent: EventEmitter<Array<DerivedFEAttribute>> = new EventEmitter<Array<DerivedFEAttribute>>();
+
+ @ViewChild('mapKeyInput') public mapKeyInput: DynamicElementComponent;
+
+ constructor(private attributesUtils: AttributesUtils, private dataTypeService: DataTypeService) {
+ }
+
+ ngOnInit() {
+ this.isAttributeFEModel = this.attribute instanceof AttributeFEModel;
+ this.attribType = this.attribute.derivedDataType;
+ this.attribPath = (this.attribute instanceof AttributeFEModel) ? this.attribute.name : this.attribute.attributesName;
+ this.nestedLevel = (this.attribute.attributesName.match(/#/g) || []).length;
+ this.rootAttribute = (this.rootAttribute) ? this.rootAttribute : <AttributeFEModel>this.attribute;
+ this.attributeTestsId = this.getAttributeTestsId();
+
+ this.initConstraintsValues();
+
+ }
+
+ initConstraintsValues() {
+ let primitiveProperties = ['string', 'integer', 'float', 'boolean'];
+
+ if (this.attribute.constraints) {
+ this.constraints = this.attribute.constraints[0].validValues
+ }
+
+ //Complex Type
+ else if (primitiveProperties.indexOf(this.rootAttribute.type) == -1 && primitiveProperties.indexOf(this.attribute.type) >= 0) {
+ this.constraints = this.dataTypeService.getConstraintsByParentTypeAndUniqueID(this.rootAttribute.type, this.attribute.name);
+ } else {
+ this.constraints = null;
+ }
+
+ }
+
+ onClickPropertyRow = (property, event) => {
+ // Because DynamicAttributeComponent is recursive second time the event is fire event.stopPropagation = undefined
+ event && event.stopPropagation && event.stopPropagation();
+ this.clickOnAttributeRow.emit(property);
+ }
+
+ expandChildById = (id: string) => {
+ this.expandedChildId = id;
+ this.expandChild.emit(id);
+ }
+
+ checkedChange = (propName: string) => {
+ this.checkAttribute.emit(propName);
+ }
+
+ getHasChildren = (property: DerivedFEAttribute): boolean => {// enter to this function only from base property (AttributeFEModel) and check for child property if it has children
+ return _.filter((<AttributeFEModel>this.attribute).flattenedChildren, (prop: DerivedFEAttribute) => {
+ return _.startsWith(prop.attributesName + '#', property.attributesName);
+ }).length > 1;
+ }
+
+ getAttributeTestsId = () => {
+ return [this.rootAttribute.name].concat(this.rootAttribute.getParentNamesArray(this.attribute.attributesName, [], true)).join('.');
+ };
+
+ onElementChanged = (event: IUiElementChangeEvent) => {
+ this.attribute.updateValueObj(event.value, event.isValid);
+ this.emitter.emit();
+ };
+
+ createNewChildProperty = (): void => {
+
+ let newProps: Array<DerivedFEAttribute> = this.attributesUtils.createListOrMapChildren(this.attribute, "", null);
+ this.attributesUtils.assignFlattenedChildrenValues(this.attribute.valueObj, [newProps[0]], this.attribute.attributesName);
+ if (this.attribute instanceof AttributeFEModel) {
+ this.addChildProps(newProps, this.attribute.name);
+ } else {
+ this.addChildAttribsToParent.emit(newProps);
+ }
+ }
+
+ addChildProps = (newProps: Array<DerivedFEAttribute>, childPropName: string) => {
+
+ if (this.attribute instanceof AttributeFEModel) {
+ let insertIndex: number = this.attribute.getIndexOfChild(childPropName) + this.attribute.getCountOfChildren(childPropName); //insert after parent prop and existing children
+ this.attribute.flattenedChildren.splice(insertIndex, 0, ...newProps); //using ES6 spread operator
+ this.expandChildById(newProps[0].attributesName);
+
+ this.updateMapKeyValueOnMainParent(newProps);
+ this.emitter.emit();
+ }
+ }
+
+ updateMapKeyValueOnMainParent(childrenProps: Array<DerivedFEAttribute>) {
+ if (this.attribute instanceof AttributeFEModel) {
+ const attributeFEModel: AttributeFEModel = <AttributeFEModel>this.attribute;
+ //Update only if all this attributeFEModel parents has key name
+ if (attributeFEModel.getParentNamesArray(childrenProps[0].attributesName, []).indexOf('') === -1) {
+ angular.forEach(childrenProps, (prop: DerivedFEAttribute): void => { //Update parent AttributeFEModel with value for each child, including nested props
+ attributeFEModel.childPropUpdated(prop);
+ if (prop.isChildOfListOrMap && prop.mapKey !== undefined) {
+ attributeFEModel.childPropMapKeyUpdated(prop, prop.mapKey, true);
+ }
+ }, this);
+ //grab the cumulative value for the new item from parent AttributeFEModel and assign that value to DerivedFEProp[0] (which is the list or map parent with UUID of the set we just added)
+ let parentNames = (<AttributeFEModel>attributeFEModel).getParentNamesArray(childrenProps[0].attributesName, []);
+ childrenProps[0].valueObj = _.get(attributeFEModel.valueObj, parentNames.join('.'), null);
+ }
+ }
+ }
+
+ childValueChanged = (property: DerivedFEAttribute) => { //value of child property changed
+
+ if (this.attribute instanceof AttributeFEModel) { // will always be the case
+ if (this.attribute.getParentNamesArray(property.attributesName, []).indexOf('') === -1) {//If one of the parents is empty key -don't save
+ this.attribute.childPropUpdated(property);
+ this.emitter.emit();
+ }
+ }
+ }
+
+ deleteListOrMapItem = (item: DerivedFEAttribute) => {
+ if (this.attribute instanceof AttributeFEModel) {
+ this.removeValueFromParent(item);
+ this.attribute.flattenedChildren.splice(this.attribute.getIndexOfChild(item.attributesName), this.attribute.getCountOfChildren(item.attributesName));
+ this.expandChildById(item.attributesName);
+ }
+ }
+
+ removeValueFromParent = (item: DerivedFEAttribute) => {
+ if (this.attribute instanceof AttributeFEModel) {
+ let itemParent = (item.parentName == this.attribute.name)
+ ? this.attribute : this.attribute.flattenedChildren.find(prop => prop.attributesName == item.parentName);
+ if (!itemParent) {
+ return;
+ }
+
+ if (item.derivedDataType == DerivedAttributeType.MAP) {
+ const oldKey = item.getActualMapKey();
+ delete itemParent.valueObj[oldKey];
+ if (itemParent instanceof AttributeFEModel) {
+ delete itemParent.valueObjValidation[oldKey];
+ itemParent.valueObjIsValid = itemParent.calculateValueObjIsValid();
+ }
+ this.attribute.childPropMapKeyUpdated(item, null); // remove map key
+ } else {
+ const itemIndex: number = this.attribute.flattenedChildren.filter(prop => prop.parentName == item.parentName).map(prop => prop.attributesName).indexOf(item.attributesName);
+ itemParent.valueObj.splice(itemIndex, 1);
+ if (itemParent instanceof AttributeFEModel) {
+ itemParent.valueObjValidation.splice(itemIndex, 1);
+ itemParent.valueObjIsValid = itemParent.calculateValueObjIsValid();
+ }
+ }
+ if (itemParent instanceof AttributeFEModel) { //direct child
+ this.emitter.emit();
+ } else { //nested child - need to update parent prop by getting flattened name (recurse through parents and replace map/list keys, etc)
+ this.childValueChanged(itemParent);
+ }
+ }
+ }
+
+ updateChildKeyInParent(childProp: DerivedFEAttribute, newMapKey: string) {
+ if (this.attribute instanceof AttributeFEModel) {
+ this.attribute.childPropMapKeyUpdated(childProp, newMapKey);
+ this.emitter.emit();
+ }
+ }
+
+ preventInsertItem = (property: DerivedFEAttribute): boolean => {
+ return property.type == PROPERTY_TYPES.MAP && Object.keys(property.valueObj).indexOf('') > -1;
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/components/logic/attributes-table/pipes/filterChildAttributes.pipe.ts b/catalog-ui/src/app/ng2/components/logic/attributes-table/pipes/filterChildAttributes.pipe.ts
new file mode 100644
index 0000000000..b2098d8478
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/attributes-table/pipes/filterChildAttributes.pipe.ts
@@ -0,0 +1,38 @@
+/*-
+ * ============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 {Pipe, PipeTransform} from '@angular/core';
+import {DerivedFEAttribute} from "../../../../../models/attributes-outputs/derived-fe-attribute";
+
+@Pipe({
+ name: 'filterChildAttributes',
+})
+export class FilterChildAttributesPipe implements PipeTransform {
+ public transform(childAttributes: Array<DerivedFEAttribute>, parentId: string) {
+ if (!parentId || !childAttributes) return childAttributes;
+
+ let validParents: Array<string> = [parentId];
+ while (parentId.lastIndexOf('#') > 0) {
+ parentId = parentId.substring(0, parentId.lastIndexOf('#'));
+ validParents.push(parentId);
+ }
+ return childAttributes.filter(derivedAttrib => validParents.indexOf(derivedAttrib.parentName) > -1);
+ }
+}
diff --git a/catalog-ui/src/app/ng2/components/logic/hierarchy-navigtion/hierarchy-navigation.module.ts b/catalog-ui/src/app/ng2/components/logic/hierarchy-navigtion/hierarchy-navigation.module.ts
new file mode 100644
index 0000000000..fc6c73f2f6
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/hierarchy-navigtion/hierarchy-navigation.module.ts
@@ -0,0 +1,32 @@
+/*-
+ * ============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 {NgModule} from '@angular/core'
+import {BrowserModule} from '@angular/platform-browser'
+import {HierarchyNavigationComponent} from "./hierarchy-navigation.component";
+
+@NgModule({
+ imports: [BrowserModule],
+ declarations: [HierarchyNavigationComponent],
+ bootstrap: [],
+ exports: [HierarchyNavigationComponent]
+})
+export class HierarchyNavigationModule {
+}
diff --git a/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.html b/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.html
new file mode 100644
index 0000000000..fbae0e45e6
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.html
@@ -0,0 +1,97 @@
+<!--
+============LICENSE_START=======================================================
+* Copyright (C) 2021 Nordix Foundation
+* ================================================================================
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+* ============LICENSE_END=========================================================
+-->
+
+<div class="output-attributes-table">
+ <loader [display]="isLoading" [size]="'large'" [relative]="true"></loader>
+ <div class="table-header">
+ <div class="table-cell col-output-attribute-name" (click)="sort('name')">Output Name
+ <span *ngIf="sortBy === 'name'" class="table-header-sort-arrow"
+ [ngClass]="{'down': reverse, 'up':!reverse}"></span>
+ </div>
+ <div class="table-cell col-output-attribute-instance" (click)="sort('instanceUniqueId')">From
+ Instance
+ <span *ngIf="sortBy === 'instanceUniqueId'" class="table-header-sort-arrow"
+ [ngClass]="{'down': reverse, 'up':!reverse}"></span>
+ </div>
+ <div class="table-cell col-output-attribute-type" (click)="sort('type')">Type
+ <span *ngIf="sortBy === 'type'" class="table-header-sort-arrow"
+ [ngClass]="{'down': reverse, 'up':!reverse}">
+ </span>
+ </div>
+ <div class="table-cell col-output-attribute-required" (click)="sort('required')"
+ *ngIf="componentType == 'SERVICE'">
+ <span tooltip="Required in Runtime" tooltipDelay="400">Req. in RT</span>
+ </div>
+ <div class="table-cell col-output-attribute-value">Value</div>
+ </div>
+ <div class="table-body">
+ <div class="no-data" *ngIf="!outputs || !outputs.length">No data to display</div>
+ <div>
+ <div class="table-row" *ngFor="let output of outputs">
+ <!-- attribute Name -->
+ <div class="table-cell col-output-attribute-name">
+ <div class="output-inner-cell-div">
+ <span class="attribute-name" tooltip="{{output.name}}">{{output.name}}</span>
+ </div>
+ <span *ngIf="output.description"
+ class="attribute-description-icon sprite-new show-desc"
+ tooltip="{{output.description}}" tooltipDelay="0"></span>
+ </div>
+ <!-- From Instance -->
+ <div class="table-cell col-output-attribute-instance">
+ <div class="output-inner-cell-div"
+ tooltip="{{instanceNamesMap[output.instanceUniqueId]?.name}}">
+ <span>{{instanceNamesMap[output.instanceUniqueId]?.name}}</span>
+ </div>
+ </div>
+ <!-- Type -->
+ <div class="table-cell col-output-attribute-type">
+ <div class="output-inner-cell-div" tooltip="{{output.type | contentAfterLastDot}}">
+ <span>{{output.type | contentAfterLastDot}}</span>
+ </div>
+ </div>
+ <!-- Required in runtime -->
+ <div class="table-cell col-output-attribute-required" *ngIf="componentType == 'SERVICE'">
+ <sdc-checkbox [(checked)]="output.required"
+ (checkedChange)="onRequiredChanged(output, $event)"
+ [disabled]="readonly"></sdc-checkbox>
+ </div>
+ <!-- Value -->
+ <div class="table-cell col-output-attribute-value output-value-col"
+ [class.inner-table-container]="!output.isSimpleType">
+ <dynamic-element class="value-output"
+ *ngIf="checkInstanceFeAttributesMapIsFilled() && output.isSimpleType"
+ pattern="null"
+ [value]="output.value"
+ [type]="'string'"
+ [name]="output.name"
+ (elementChanged)="onOutputChanged(output, $event)"
+ [readonly]="true"
+ [testId]="'output-' + output.name"
+ [constraints]="getConstraints(output)">
+ </dynamic-element>
+ <div class="delete-button-container">
+ <span *ngIf="output.instanceUniqueId && !readonly" class="sprite-new delete-btn"
+ (click)="openDeleteModal(output)"></span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.less b/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.less
new file mode 100644
index 0000000000..56ed502943
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.less
@@ -0,0 +1,189 @@
+@import './../../../../../assets/styles/variables.less';
+
+:host /deep/ output {
+ width: 100%;
+}
+
+.output-attributes-table {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ height: 100%;
+ text-align: left;
+
+ .output-inner-cell-div {
+ max-width: 100%;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ height: 20px;
+ }
+
+
+ .table-header {
+ font-weight: bold;
+ border-top: #d2d2d2 solid 1px;
+ background-color: #eaeaea;
+ color: #191919;
+
+ .table-cell {
+ font-size: 13px;
+
+ .table-header-sort-arrow {
+ display: inline-block;
+ background-color: transparent;
+ border: none;
+ color: #AAA;
+ margin: 8px 0 0 5px;
+
+ &.up {
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-bottom: 5px solid;
+ }
+
+ &.down {
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-top: 5px solid;
+ }
+ }
+ }
+
+ .output-value-col {
+ justify-content: flex-start;
+ padding: 10px;
+ }
+ }
+
+ .table-header, .table-row {
+ display: flex;
+ flex-direction: row;
+ flex: 0 0 auto;
+ }
+
+ .table-body {
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ flex: 1;
+
+ .no-data {
+ border: #d2d2d2 solid 1px;
+ border-top: none;
+ text-align: center;
+ height: 100%;
+ padding: 20px;
+ }
+
+ /deep/ .selected {
+ background-color: #e6f6fb;
+ color: #009fdb;
+ }
+ }
+
+ .table-row {
+ &:hover {
+ background-color: #f8f8f8;
+ cursor: pointer;
+ }
+
+ &:last-child {
+ flex: 1 0 auto;
+ }
+
+ .selected-row {
+ background-color: #e6f6fb;
+ }
+ }
+
+ .table-cell {
+ font-size: 13px;
+ flex: 1;
+ border: #d2d2d2 solid 1px;
+ border-right: none;
+ border-top: none;
+ padding: 10px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+
+ &:last-child {
+ border-right: #d2d2d2 solid 1px;
+ }
+
+ &.col-output-attribute-name {
+ flex: 1 0 130px;
+ max-width: 250px;
+
+ justify-content: space-between;
+
+ .property-name {
+ flex: 1;
+ }
+
+ .property-description-icon {
+ float: right;
+ margin-top: 4px;
+ margin-left: 5px;
+ flex: 0 0 auto;
+ }
+ }
+
+ &.col-output-attribute-type {
+ flex: 0 0 140px;
+ max-width: 140px;
+ }
+
+ &.col-output-attribute-instance {
+ flex: 0 0 120px;
+ max-width: 120px;
+ }
+
+ &.col-output-attribute-required {
+ flex: 0 0 80px;
+ max-width: 80px;
+ text-align: center;
+ }
+
+ &.col-output-attribute-value {
+ .value-output {
+ flex: 1;
+ border: none;
+ background-color: inherit;
+
+ &:focus, &:active {
+ border: none;
+ outline: none;
+ }
+ }
+
+ .delete-btn {
+ flex: 0 0 auto;
+ }
+
+ .delete-button-container {
+ max-height: 24px;
+ }
+
+ &.inner-table-container {
+ padding: 0;
+
+ .delete-button-container {
+ padding: 0 8px 0 0;
+ }
+ }
+ }
+
+ &.output-value-col {
+ padding: 8px;
+ }
+
+ }
+
+ .filtered {
+ /deep/ .checkbox-label-content {
+ background-color: yellow;
+ }
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.ts b/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.ts
new file mode 100644
index 0000000000..a7caeaa9fb
--- /dev/null
+++ b/catalog-ui/src/app/ng2/components/logic/outputs-table/outputs-table.component.ts
@@ -0,0 +1,141 @@
+/*
+* ============LICENSE_START=======================================================
+* 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.
+*
+* SPDX-License-Identifier: Apache-2.0
+* ============LICENSE_END=========================================================
+*/
+
+import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
+import {InstanceFeDetails} from "../../../../models/instance-fe-details";
+import {ModalService} from "../../../services/modal.service";
+import {DataTypeService} from "../../../services/data-type.service";
+import {TranslateService} from "../../../shared/translator/translate.service";
+import {InstanceFeAttributesMap} from "app/models/attributes-outputs/attribute-fe-map";
+import {Select} from "@ngxs/store";
+import {WorkspaceState} from "../../../store/states/workspace.state";
+import {OutputFEModel} from "app/models/attributes-outputs/output-fe-model";
+import {InputFEModel} from "../../../../models/properties-inputs/input-fe-model";
+
+@Component({
+ selector: 'outputs-table',
+ templateUrl: './outputs-table.component.html',
+ styleUrls: ['./outputs-table.component.less']
+})
+export class OutputsTableComponent implements OnInit {
+ @Select(WorkspaceState.isViewOnly)
+ isViewOnly$: boolean;
+
+ @ViewChild('componentOutputsTable')
+ private table: any;
+
+ @Input() outputs: Array<OutputFEModel>;
+ @Input() instanceNamesMap: Map<string, InstanceFeDetails>;
+ @Input() readonly: boolean;
+ @Input() isLoading: boolean;
+ @Input() componentType: string;
+ @Output() outputChanged: EventEmitter<any> = new EventEmitter<any>();
+ @Output() deleteOutput: EventEmitter<any> = new EventEmitter<any>();
+ @Input() feAttributesMap: InstanceFeAttributesMap;
+
+ deleteMsgTitle: string;
+ deleteMsgBodyTxt: string;
+ modalDeleteBtn: string;
+ modalCancelBtn: string;
+ sortBy: string;
+ reverse: boolean;
+ selectedOutputToDelete: OutputFEModel;
+
+ constructor(private modalService: ModalService,
+ private dataTypeService: DataTypeService,
+ private translateService: TranslateService) {
+ }
+
+ ngOnInit() {
+ this.translateService.languageChangedObservable.subscribe((lang) => {
+ this.deleteMsgTitle = this.translateService.translate('DELETE_OUTPUT_TITLE');
+ this.modalDeleteBtn = this.translateService.translate('MODAL_DELETE');
+ this.modalCancelBtn = this.translateService.translate('MODAL_CANCEL');
+
+ });
+ }
+
+ sort = (sortBy) => {
+ this.reverse = (this.sortBy === sortBy) ? !this.reverse : true;
+ let reverse = this.reverse ? 1 : -1;
+ this.sortBy = sortBy;
+ let instanceNameMapTemp = this.instanceNamesMap;
+ let itemIdx1Val = "";
+ let itemIdx2Val = "";
+ this.outputs.sort(function (itemIdx1, itemIdx2) {
+ if (sortBy == 'instanceUniqueId') {
+ itemIdx1Val = (itemIdx1[sortBy] && instanceNameMapTemp[itemIdx1[sortBy]] !== undefined) ? instanceNameMapTemp[itemIdx1[sortBy]].name : "";
+ itemIdx2Val = (itemIdx2[sortBy] && instanceNameMapTemp[itemIdx2[sortBy]] !== undefined) ? instanceNameMapTemp[itemIdx2[sortBy]].name : "";
+ } else {
+ itemIdx1Val = itemIdx1[sortBy];
+ itemIdx2Val = itemIdx2[sortBy];
+ }
+ if (itemIdx1Val < itemIdx2Val) {
+ return -1 * reverse;
+ } else if (itemIdx1Val > itemIdx2Val) {
+ return 1 * reverse;
+ } else {
+ return 0;
+ }
+ });
+ };
+
+ onOutputChanged = (output, event) => {
+ output.updateDefaultValueObj(event.value, event.isValid);
+ this.outputChanged.emit(output);
+ };
+
+ onRequiredChanged = (output: OutputFEModel, event) => {
+ this.outputChanged.emit(output);
+ }
+
+ onDeleteOutput = () => {
+ this.deleteOutput.emit(this.selectedOutputToDelete);
+ this.modalService.closeCurrentModal();
+ };
+
+ openDeleteModal = (output: OutputFEModel) => {
+ this.selectedOutputToDelete = output;
+ this.modalService.createActionModal("Delete Output", "Are you sure you want to delete this output?", "Delete", this.onDeleteOutput, "Close").instance.open();
+ }
+
+ getConstraints(output: OutputFEModel): string[] {
+ if (output.outputPath) {
+ const pathValuesName = output.outputPath.split('#');
+ const rootPropertyName = pathValuesName[0];
+ const propertyName = pathValuesName[1];
+ let filteredRootPropertyType = _.values(this.feAttributesMap)[0].filter(property =>
+ property.name == rootPropertyName);
+ if (filteredRootPropertyType.length > 0) {
+ let rootPropertyType = filteredRootPropertyType[0].type;
+ return this.dataTypeService.getConstraintsByParentTypeAndUniqueID(rootPropertyType, propertyName);
+ } else {
+ return null;
+ }
+
+ } else {
+ return null;
+ }
+ }
+
+ checkInstanceFeAttributesMapIsFilled() {
+ return _.keys(this.feAttributesMap).length > 0
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/components/logic/properties-table/properties-table.component.ts b/catalog-ui/src/app/ng2/components/logic/properties-table/properties-table.component.ts
index e499b3786b..b79bec0942 100644
--- a/catalog-ui/src/app/ng2/components/logic/properties-table/properties-table.component.ts
+++ b/catalog-ui/src/app/ng2/components/logic/properties-table/properties-table.component.ts
@@ -81,7 +81,6 @@ export class PropertiesTableComponent implements OnChanges {
// Click on main row (row of propertyFEModel)
onClickPropertyRow = (property: PropertyFEModel, instanceName: string, event?) => {
- // event && event.stopPropagation();
this.selectedPropertyId = property.name;
const propertyRowSelectedEvent: PropertyRowSelectedEvent = new PropertyRowSelectedEvent(property, instanceName);
this.selectPropertyRow.emit(propertyRowSelectedEvent);