diff options
author | vasraz <vasyl.razinkov@est.tech> | 2021-02-16 17:37:57 +0000 |
---|---|---|
committer | Christophe Closset <christophe.closset@intl.att.com> | 2021-02-17 15:57:55 +0000 |
commit | 26e5029d922779fd7e786c1a31b6b37492132388 (patch) | |
tree | 8e8e68a6913749e1405fce951bc7816d4fa35ba3 /catalog-ui/src/app/ng2/components | |
parent | f2c0a4118c3c0b6360b639622766543bd754b59c (diff) |
Implement Attributes/Outputs FE
Change-Id: I014bb0ebc07f3fea4266a4f295172eadee546705
Signed-off-by: Vasyl Razinkov <vasyl.razinkov@est.tech>
Issue-ID: SDC-3448
Diffstat (limited to 'catalog-ui/src/app/ng2/components')
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); |