diff options
Diffstat (limited to 'catalog-ui/src/app/ng2/pages/workspace/attributes')
9 files changed, 961 insertions, 0 deletions
diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.html b/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.html new file mode 100644 index 0000000000..bd30a469e0 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.html @@ -0,0 +1,104 @@ +<form> + <div class="attr-container"> + + <div class="attr-col"> + <!-- ATTRIBUTE NAME - MANDATORY --> + <div> + <sdc-input + #attributeName + label="Name" + [required]="true" + [(value)]="attributeToEdit.name" + [disabled]="isEdit" + name="attributeName" + testId="attributeName" + [maxLength]="255"> + </sdc-input> + <sdc-validation [validateElement]="attributeName" (validityChanged)="onValidityChange($event, 'name')"> + <sdc-required-validator message="{{'VALIDATION_ERROR_REQUIRED' | translate : { 'field' : 'Name' } }}"></sdc-required-validator> + <sdc-regex-validator message="{{'VALIDATION_ERROR_SPECIAL_CHARS_NOT_ALLOWED' | translate }}" [pattern]="validationPatterns.propertyName"></sdc-regex-validator> + </sdc-validation> + </div> + + <!-- ATTRIBUTE DESCRIPTION - OPTIONAL --> + <div> + <sdc-textarea #attributeDescription + [(value)]="attributeToEdit.description" + [required]="false" + testId="description" + [maxLength]="256" + label="Description"> + </sdc-textarea> + </div> + </div> + + <div class="attr-col"> + + <div class="attributeType"> + <!-- ATTRIBUTE TYPE - MANDATORY --> + <sdc-dropdown #attributeType [disabled]="false" label="Type" [required]="true" + [selectedOption]="toDropDownOption(this.attributeToEdit.type)" placeHolder="Choose Type" + [options]="types" (changed)="onTypeSelected($event)"> + <sdc-validation [validateElement]="attributeType" (validityChanged)="onValidityChange($event, 'type')"> + <sdc-required-validator message="'required field'"></sdc-required-validator> + </sdc-validation> + </sdc-dropdown> + </div> + + <!-- ATTRIBUTE DEFAULT VALUE TEXT - OPTIONAL --> + <div *ngIf="attributeToEdit.type != 'boolean'"> + <sdc-input + #defaultValue + [required]="false" + label="Default Value" + [(value)]="attributeToEdit.defaultValue" + [disabled]="false" + name="defaultValue" + testId="defaultValue" + [maxLength]="255" + (valueChange)="defaultValueChanged()"> + </sdc-input> + + <sdc-validation [validateElement]="defaultValue" (validityChanged)="onValidityChange($event, 'defaultValue')"> + <sdc-regex-validator *ngIf="this.attributeToEdit.defaultValue && this.attributeToEdit.defaultValue.length > 0" message="{{ this.defaultValueErrorMessage }}" + [pattern]="defaultValuePattern"></sdc-regex-validator> + <sdc-custom-validator *ngIf="this.attributeToEdit.type == 'map' && this.attributeToEdit.schema.property.type" message="{{ 'PROPERTY_EDIT_MAP_UNIQUE_KEYS' | translate }}" + [callback]="isMapUnique" [disabled]="false"></sdc-custom-validator> + </sdc-validation> + </div> + + <!-- ATTRIBUTE DEFAULT VALUE BOOLEAN- OPTIONAL --> + <div *ngIf="attributeToEdit.type == 'boolean'"> + <sdc-dropdown [disabled]="false" label="Default Value" + [required]="false" + [selectedOption]="toDropDownOption(this.attributeToEdit.defaultValue)" placeHolder="Choose Default Value" + [options]="booleanValues" (changed)="onBooleanDefaultValueSelected($event)"> + + </sdc-dropdown> + </div> + + <div *ngIf="attributeToEdit.type == 'list' || attributeToEdit.type == 'map'"> + <!-- ATTRIBUTE ENTRY SCHEMA - MANDATORY --> + <sdc-dropdown #entrySchema + [disabled]="false" label="Entry Schema" [required]="true" + [selectedOption]="toDropDownOption(this.attributeToEdit.schema.property.type)" placeHolder="Choose Schema Type" + [options]="entrySchemaValues" (changed)="onEntrySchemaTypeSelected($event)"> + <sdc-validation [validateElement]="entrySchema" (validityChanged)="onValidityChange($event, 'entrySchema')"> + <sdc-required-validator message="'required !TODO - CHANGE MESSAGE'"></sdc-required-validator> + </sdc-validation> + </sdc-dropdown> + </div> + + <!-- ATTRIBUTE HIDDEN - OPTIONAL --> + <sdc-checkbox + label="Hidden" + [checked]="attributeToEdit.hidden" + [disabled]="false" + testId="hidden" + (checkedChange)="this.onHiddenCheckboxClicked($event)" + > + </sdc-checkbox> + </div> + </div> + +</form>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts new file mode 100644 index 0000000000..c703869ad2 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts @@ -0,0 +1,138 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { IDropDownOption } from 'onap-ui-angular/dist/form-elements/dropdown/dropdown-models'; +import { InputComponent } from 'onap-ui-angular/dist/form-elements/text-elements/input/input.component'; +import { Subject } from 'rxjs/Subject'; +import { AttributeModel } from '../../../../models/attributes'; +import { ValidationUtils } from '../../../../utils/validation-utils'; +import { CacheService } from '../../../services/cache.service'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { AttributeOptions } from './attributes-options'; + +@Component({ + selector: 'attribute-modal', + templateUrl: './attribute-modal.component.html', + styleUrls: ['./attributes.component.less'] +}) +export class AttributeModalComponent implements OnInit { + + @ViewChild('defaultValue') validatedInput: InputComponent; + + public readonly types = AttributeOptions.types; // integer, string, boolean etc. + + public readonly booleanValues = AttributeOptions.booleanValues; // true / false + + public readonly entrySchemaValues = AttributeOptions.entrySchemaValues; // integer, string, boolean, float + + public onValidationChange: Subject<boolean> = new Subject(); + + public validationPatterns: any; + public readonly listPattern = ValidationUtils.getPropertyListPatterns(); + public readonly mapPattern = ValidationUtils.getPropertyMapPatterns(); + + // The current effective default value pattern + public defaultValuePattern: string; + public defaultValueErrorMessage: string; + + // Attribute being Edited + public attributeToEdit: AttributeModel; + + constructor(private translateService: TranslateService, private cacheService: CacheService) { + this.validationPatterns = this.cacheService.get('validation').validationPatterns; + } + + ngOnInit() { + this.revalidateDefaultValue(); + } + + onHiddenCheckboxClicked(event: boolean) { + this.attributeToEdit.hidden = event; + } + + onTypeSelected(selectedElement: IDropDownOption) { + if (this.attributeToEdit.type !== selectedElement.value && selectedElement.value === 'boolean') { + this.attributeToEdit.defaultValue = ''; // Clean old value in case we choose change type to boolean + } + this.attributeToEdit.type = selectedElement.value; + this.revalidateDefaultValue(); + } + + onBooleanDefaultValueSelected(selectedElement: IDropDownOption) { + if (this.attributeToEdit.type === 'boolean') { + this.attributeToEdit.defaultValue = selectedElement.value; + } + } + + onEntrySchemaTypeSelected(selectedElement: IDropDownOption) { + this.attributeToEdit.schema.property.type = selectedElement.value; + this.revalidateDefaultValue(); + } + + onValidityChange(isValid: boolean, field: string) { + const typeIsValid = this.attributeToEdit.type && this.attributeToEdit.type.length > 0; // Make sure type is defined + + // Make sure name is defined when other fields are changed + let nameIsValid = true; + if (field !== 'name') { + nameIsValid = this.attributeToEdit.name && this.attributeToEdit.name.length > 0; + } + this.onValidationChange.next(isValid && nameIsValid && typeIsValid); + } + + defaultValueChanged() { + this.revalidateDefaultValue(); + } + + /** + * Utility function for UI that converts a simple value to IDropDownOption + * @param val + * @returns {{value: any; label: any}} + */ + toDropDownOption(val: string) { + return { value : val, label: val }; + } + + public isMapUnique = () => { + if (this.attributeToEdit && this.attributeToEdit.type === 'map' && this.attributeToEdit.defaultValue) { + return ValidationUtils.validateUniqueKeys(this.attributeToEdit.defaultValue); + } + return true; + } + + private revalidateDefaultValue() { + this.setDefaultValuePattern(this.attributeToEdit.type); + setTimeout(() => { + if (this.validatedInput) { + this.validatedInput.onKeyPress(this.attributeToEdit.defaultValue); + } }, 250); + } + + private setDefaultValuePattern(valueType: string) { + const selectedSchemaType = this.attributeToEdit.schema.property.type; + this.defaultValuePattern = '.*'; + switch (valueType) { + case 'float': + this.defaultValuePattern = this.validationPatterns.number; + this.defaultValueErrorMessage = this.translateService.translate('VALIDATION_ERROR_TYPE', { type : 'float' }); + break; + case 'integer': + this.defaultValuePattern = this.validationPatterns.integerNoLeadingZero; + this.defaultValueErrorMessage = this.translateService.translate('VALIDATION_ERROR_TYPE', { type : 'integer' }); + break; + case 'list': + if (selectedSchemaType != undefined) { + this.defaultValuePattern = this.listPattern[selectedSchemaType]; + const listTypeStr = `list of ${selectedSchemaType}s (v1, v2, ...) `; + this.defaultValueErrorMessage = this.translateService.translate('VALIDATION_ERROR_TYPE', { type : listTypeStr }); + } + break; + case 'map': + if (selectedSchemaType != undefined) { + this.defaultValuePattern = this.mapPattern[selectedSchemaType]; + const mapTypeStr = `map of ${selectedSchemaType}s (k1:v1, k2:v2, ...)`; + this.defaultValueErrorMessage = this.translateService.translate('VALIDATION_ERROR_TYPE', { type : mapTypeStr }); + } + break; + } + } + +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes-modal.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes-modal.component.spec.ts new file mode 100644 index 0000000000..99aa140dd1 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes-modal.component.spec.ts @@ -0,0 +1,128 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { AttributeModel } from '../../../../models/attributes'; +import { ValidationUtils } from '../../../../utils/validation-utils'; +import { CacheService } from '../../../services/cache.service'; +import { TranslatePipe } from '../../../shared/translator/translate.pipe'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { AttributeModalComponent } from './attribute-modal.component'; + +describe('attributes modal component', () => { + + let fixture: ComponentFixture<AttributeModalComponent>; + + // Mocks + let translateServiceMock: Partial<TranslateService>; + let cacheServiceMock: Partial<CacheService>; + + const validationPatterns = { + integerNoLeadingZero : 'int_regx', + number : 'number_regx' + }; + + const newAttribute = { + uniqueId: '1', name: 'attr1', description: 'description1', type: 'string', hidden: false, defaultValue: 'val1', schema: null + }; + + beforeEach( + async(() => { + + translateServiceMock = { + translate: jest.fn() + }; + + cacheServiceMock = { + get: jest.fn().mockImplementation((k) => { + return { validationPatterns}; + } ) + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [AttributeModalComponent, TranslatePipe], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: TranslateService, useValue: translateServiceMock}, + {provide: CacheService, useValue: cacheServiceMock}, + ] + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(AttributeModalComponent); + }); + }) + ); + + it('test that when hidden is clicked, hidden attribute is set', async () => { + fixture.componentInstance.attributeToEdit = new AttributeModel(); + const hidden = fixture.componentInstance.attributeToEdit.hidden; + fixture.componentInstance.ngOnInit(); + + expect(hidden).toBe(false); + fixture.componentInstance.onHiddenCheckboxClicked(true); + expect(fixture.componentInstance.attributeToEdit.hidden).toBe(true); + }); + + it('test that when type is set to boolean default value is cleared', async () => { + const component = fixture.componentInstance; + component.attributeToEdit = new AttributeModel(); + component.ngOnInit(); + + component.onTypeSelected({ value : 'string', label : 'string'}); + component.attributeToEdit.defaultValue = 'some_value'; + component.onTypeSelected({ value : 'boolean', label : 'boolean'}); + expect(component.attributeToEdit.defaultValue).toBe(''); + + component.onBooleanDefaultValueSelected({ value : 'true', label : 'true'}); + expect(component.attributeToEdit.defaultValue).toBe('true'); + }); + + it('test that when certain type is selected, the correct regex pattern is chosen', async () => { + const component = fixture.componentInstance; + component.attributeToEdit = new AttributeModel(); + component.ngOnInit(); + + // integer + component.onTypeSelected({ value : 'integer', label : 'integer'}); + expect(component.defaultValuePattern).toBe(validationPatterns.integerNoLeadingZero); + + // float + component.onTypeSelected({ value : 'float', label : 'float'}); + expect(component.defaultValuePattern).toBe(validationPatterns.number); + + // list is chosen with no schema, regex pattern is set to default + component.onTypeSelected({ value : 'list', label : 'list'}); + expect(component.defaultValuePattern).toEqual('.*'); + + // schema is set to list of int + component.onEntrySchemaTypeSelected({ value : 'integer', label : 'integer' }); + expect(component.defaultValuePattern).toEqual(ValidationUtils.getPropertyListPatterns().integer); + + // schema is set to list of float + component.onEntrySchemaTypeSelected({ value : 'float', label : 'float' }); + expect(component.defaultValuePattern).toEqual(ValidationUtils.getPropertyListPatterns().float); + + // map is selected (float schema is still selected from previous line) + component.onTypeSelected({ value : 'map', label : 'map'}); + expect(component.defaultValuePattern).toEqual(ValidationUtils.getPropertyMapPatterns().float); + + // change schema type to boolean + component.onEntrySchemaTypeSelected({ value : 'boolean', label : 'boolean' }); + }); + + it('should detect map with non-unique keys', async () => { + const component = fixture.componentInstance; + component.attributeToEdit = new AttributeModel(); + component.ngOnInit(); + expect(component.isMapUnique()).toBe(true); // map is not selected so return true by default + component.onTypeSelected({ value : 'map', label : 'map'}); + component.onEntrySchemaTypeSelected({ value : 'boolean', label : 'boolean' }); + component.attributeToEdit.defaultValue = '"1":true,"2":false'; + expect(component.isMapUnique()).toBe(true); + component.attributeToEdit.defaultValue = '"1":true,"1":false'; + expect(component.isMapUnique()).toBe(false); + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes-options.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes-options.ts new file mode 100644 index 0000000000..2a6924bc5e --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes-options.ts @@ -0,0 +1,60 @@ +import { IDropDownOption } from 'onap-ui-angular/dist/form-elements/dropdown/dropdown-models'; + +export class AttributeOptions { + public static readonly types: IDropDownOption[] = [ + { + label: 'integer', + value: 'integer', + }, + { + label: 'string', + value: 'string', + }, + { + label: 'float', + value: 'float' + }, + { + label: 'boolean', + value: 'boolean' + }, + { + label: 'list', + value: 'list' + }, + { + label: 'map', + value: 'map' + } + ]; + + public static readonly booleanValues: IDropDownOption[] = [ + { + label: 'true', + value: 'true', + }, + { + label: 'false', + value: 'false', + } + ]; + + public static readonly entrySchemaValues: IDropDownOption[] = [ + { + label: 'integer', + value: 'integer', + }, + { + label: 'string', + value: 'string', + }, + { + label: 'float', + value: 'float' + }, + { + label: 'boolean', + value: 'boolean' + } + ]; +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.html b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.html new file mode 100644 index 0000000000..00a7a5cec0 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.html @@ -0,0 +1,93 @@ +<!-- + ~ Copyright (C) 2018 AT&T Intellectual Property. All rights reserved. + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. +--> +<div class="workspace-attributes"> + + <div class="action-bar-wrapper"> + <svg-icon-label + *ngIf="!(this.isViewOnly$ | async)" + class="add-attr-icon" + [name]="'plus'" + [mode]="'primary'" + [size]="'medium'" + [label]="'Add'" + [labelPlacement]="'right'" + [labelClassName]="'externalActionLabel'" + (click)="onAddAttribute()"> + </svg-icon-label> + </div> + + <ngx-datatable + columnMode="flex" + [footerHeight]="0" + [limit]="50" + [headerHeight]="40" + [rowHeight]="35" + [rows]="attributes" + #componentAttributesTable + (activate)="onExpandRow($event)"> + + <ngx-datatable-row-detail [rowHeight]="80"> + <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template> + <div>{{row.description}}</div> + </ng-template> + </ngx-datatable-row-detail> + + <ngx-datatable-column [resizeable]="false" name="Name" [flexGrow]="2"> + + <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded"> + <div class="expand-collapse-cell"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'"></svg-icon> + <span>{{ row.name }}</span> + </div> + </ng-template> + + </ngx-datatable-column> + + <ngx-datatable-column [resizeable]="false" name="Type" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{row.type}} + </ng-template> + </ngx-datatable-column> + + <ngx-datatable-column [resizeable]="false" name="Default Value" [flexGrow]="3"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{row.defaultValue}} + </ng-template> + </ngx-datatable-column> + + <ngx-datatable-column *ngIf="!(this.isViewOnly$ | async)" [resizeable]="false" name="Action" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row" let-rowIndex="rowIndex"> + <div class="actionColumn"> + <svg-icon [clickable]="true" + [mode]="'primary2'" + [name]="'edit-o'" + [size]="'medium'" + (click)="onEditAttribute($event, row)"> + </svg-icon> + <svg-icon [clickable]="true" + [mode]="'primary2'" + [name]="'trash-o'" + (click)="onDeleteAttribute($event, row)" + [size]="'medium'"> + </svg-icon> + </div> + </ng-template> + </ngx-datatable-column> + + </ngx-datatable> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.less b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.less new file mode 100644 index 0000000000..3e91ae4689 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.less @@ -0,0 +1,36 @@ +.action-bar-wrapper { + flex: 0 0 30%; + display: flex; + justify-content: flex-end; + margin-bottom: 10px; +} + +.add-attr-icon{ + cursor: pointer; +} + +.attr-container { + display: flex; + justify-content: space-between; + + .attr-col { + display: flex; + flex-direction: column; + max-width: 275px; + flex-grow: 1; + } + +} + +.attributeType { + margin-bottom: 10px; +} + +sdc-checkbox { + margin-top: 20px; +} + +.actionColumn { + text-align: center; + padding: 5px; +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.spec.ts new file mode 100644 index 0000000000..f676e2b4d9 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.spec.ts @@ -0,0 +1,182 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import 'rxjs/add/observable/of'; +import { Observable } from 'rxjs/Rx'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { ComponentMetadata } from '../../../../models/component-metadata'; +import { ModalsHandler } from '../../../../utils'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { WorkspaceService } from '../workspace.service'; +import { AttributesComponent } from './attributes.component'; + +describe('attributes component', () => { + + let fixture: ComponentFixture<AttributesComponent>; + + // Mocks + let workspaceServiceMock: Partial<WorkspaceService>; + let topologyTemplateServiceMock: Partial<TopologyTemplateService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let componentMetadataMock: ComponentMetadata; + let modalServiceMock: Partial<SdcUiServices.ModalService>; + + const mockAttributesList = [ + { uniqueId: '1', name: 'attr1', description: 'description1', type: 'string', hidden: false, defaultValue: 'val1', schema: null }, + { uniqueId : '2', name : 'attr2', description: 'description2', type : 'int', hidden : false, defaultValue : 1, schema : null}, + { uniqueId : '3', name : 'attr3', description: 'description3', type : 'double', hidden : false, defaultValue : 1.0, schema : null}, + { uniqueId : '4', name : 'attr4', description: 'description4', type : 'boolean', hidden : false, defaultValue : true, schema : null}, + ]; + + const newAttribute = { + uniqueId : '5', name : 'attr5', description: 'description5', type : 'string', hidden : false, defaultValue : 'val5', schema : null + }; + const updatedAttribute = { + uniqueId : '2', name : 'attr2', description: 'description_new', type : 'string', hidden : false, defaultValue : 'new_val2', schema : null + }; + const errorAttribute = { + uniqueId : '99', name : 'attr99', description: 'description_error', type : 'string', hidden : false, defaultValue : 'error', schema : null + }; + + beforeEach( + async(() => { + + componentMetadataMock = new ComponentMetadata(); + componentMetadataMock.uniqueId = 'fake'; + componentMetadataMock.componentType = 'VL'; + + topologyTemplateServiceMock = { + getComponentAttributes: jest.fn().mockResolvedValue({ attributes : mockAttributesList }), + addAttributeAsync: jest.fn().mockImplementation( + (compType, cUid, attr) => { + if (attr === errorAttribute) { + return Observable.throwError('add_error').toPromise(); + } else { + return Observable.of(newAttribute).toPromise(); + } + } + ), + updateAttributeAsync: jest.fn().mockImplementation( + (compType, cUid, attr) => { + if (attr === errorAttribute) { + return Observable.throwError('update_error').toPromise(); + } else { + return Observable.of(updatedAttribute).toPromise(); + } + } + ), + deleteAttributeAsync: jest.fn().mockImplementation((cid, ctype, attr) => Observable.of(attr)) + }; + + workspaceServiceMock = { + metadata: componentMetadataMock + }; + + const customModalInstance = { innerModalContent: { instance: { onValidationChange: { subscribe: jest.fn()}}}}; + + modalServiceMock = { + openInfoModal: jest.fn(), + openCustomModal: jest.fn().mockImplementation(() => customModalInstance) + }; + + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [AttributesComponent], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: TopologyTemplateService, useValue: topologyTemplateServiceMock}, + {provide: ModalsHandler, useValue: {}}, + {provide: TranslateService, useValue: { translate: jest.fn() }}, + {provide: SdcUiServices.ModalService, useValue: modalServiceMock }, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock } + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(AttributesComponent); + }); + }) + ); + + it('should see exactly 1 attributes on init', async () => { + await fixture.componentInstance.asyncInitComponent(); + expect(fixture.componentInstance.getAttributes().length).toEqual(4); + }); + + it('should see exactly 5 attributes when adding', async () => { + await fixture.componentInstance.asyncInitComponent(); + expect(fixture.componentInstance.getAttributes().length).toEqual(4); + + await fixture.componentInstance.addOrUpdateAttribute(newAttribute, false); + expect(fixture.componentInstance.getAttributes().length).toEqual(5); + }); + + it('should see exactly 3 attributes when deleting', async () => { + await fixture.componentInstance.asyncInitComponent(); + expect(fixture.componentInstance.getAttributes().length).toEqual(4); + const attrToDelete = mockAttributesList[0]; + expect(fixture.componentInstance.getAttributes().filter((attr) => attr.uniqueId === attrToDelete.uniqueId).length).toEqual(1); + await fixture.componentInstance.deleteAttribute(attrToDelete); + expect(fixture.componentInstance.getAttributes().length).toEqual(3); + expect(fixture.componentInstance.getAttributes().filter((attr) => attr.uniqueId === attrToDelete.uniqueId).length).toEqual(0); + }); + + it('should see updated attribute', async () => { + await fixture.componentInstance.asyncInitComponent(); + + await fixture.componentInstance.addOrUpdateAttribute(updatedAttribute, true); + expect(fixture.componentInstance.getAttributes().length).toEqual(4); + const attribute = fixture.componentInstance.getAttributes().filter( (attr) => { + return attr.uniqueId === updatedAttribute.uniqueId; + })[0]; + expect(attribute.description).toEqual( 'description_new'); + }); + + it('Add fails, make sure loader is deactivated and attribute is not added', async () => { + await fixture.componentInstance.asyncInitComponent(); + const numAttributes = fixture.componentInstance.getAttributes().length; + await fixture.componentInstance.addOrUpdateAttribute(errorAttribute, false); // Add + expect(loaderServiceMock.deactivate).toHaveBeenCalled(); + expect(fixture.componentInstance.getAttributes().length).toEqual(numAttributes); + }); + + it('Update fails, make sure loader is deactivated', async () => { + await fixture.componentInstance.asyncInitComponent(); + const numAttributes = fixture.componentInstance.getAttributes().length; + await fixture.componentInstance.addOrUpdateAttribute(errorAttribute, true); // Add + expect(loaderServiceMock.deactivate).toHaveBeenCalled(); + expect(fixture.componentInstance.getAttributes().length).toEqual(numAttributes); + }); + + it('on delete modal shell be opened', async () => { + await fixture.componentInstance.asyncInitComponent(); + const event = { stopPropagation: jest.fn() }; + fixture.componentInstance.onDeleteAttribute(event, fixture.componentInstance.getAttributes()[0]); + expect(event.stopPropagation).toHaveBeenCalled(); + expect(modalServiceMock.openInfoModal).toHaveBeenCalled(); + }); + + it('on add modal shell be opened', async () => { + await fixture.componentInstance.asyncInitComponent(); + fixture.componentInstance.onAddAttribute(); + expect(modalServiceMock.openCustomModal).toHaveBeenCalled(); + }); + + it('on edit modal shell be opened', async () => { + await fixture.componentInstance.asyncInitComponent(); + const event = { stopPropagation: jest.fn() }; + fixture.componentInstance.onEditAttribute(event, fixture.componentInstance.getAttributes()[0]); + expect(event.stopPropagation).toHaveBeenCalled(); + expect(modalServiceMock.openCustomModal).toHaveBeenCalled(); + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.ts new file mode 100644 index 0000000000..bc47f1456b --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.ts @@ -0,0 +1,188 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { Select } from '@ngxs/store'; +import { IAttributeModel } from 'app/models'; +import * as _ from 'lodash'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { ModalComponent } from 'onap-ui-angular/dist/modals/modal.component'; +import { AttributeModel } from '../../../../models'; +import { Resource } from '../../../../models'; +import { ModalsHandler } from '../../../../utils'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { WorkspaceState } from '../../../store/states/workspace.state'; +import { WorkspaceService } from '../workspace.service'; +import { AttributeModalComponent } from './attribute-modal.component'; + +@Component({ + selector: 'attributes', + templateUrl: './attributes.component.html', + styleUrls: ['./attributes.component.less', '../../../../../assets/styles/table-style.less'] +}) +export class AttributesComponent implements OnInit { + + @Select(WorkspaceState.isViewOnly) + isViewOnly$: boolean; + + @ViewChild('componentAttributesTable') + private table: any; + + private componentType: string; + private componentUid: string; + + private attributes: IAttributeModel[] = []; + private temp: IAttributeModel[] = []; + private customModalInstance: ModalComponent; + + constructor(private workspaceService: WorkspaceService, + private topologyTemplateService: TopologyTemplateService, + private modalsHandler: ModalsHandler, + private modalService: SdcUiServices.ModalService, + private loaderService: SdcUiServices.LoaderService, + private translateService: TranslateService) { + + this.componentType = this.workspaceService.metadata.componentType; + this.componentUid = this.workspaceService.metadata.uniqueId; + } + + ngOnInit(): void { + this.asyncInitComponent(); + } + + async asyncInitComponent() { + this.loaderService.activate(); + const response = await this.topologyTemplateService.getComponentAttributes(this.componentType, this.componentUid); + this.attributes = response.attributes; + this.temp = [...response.attributes]; + this.loaderService.deactivate(); + } + + getAttributes(): IAttributeModel[] { + return this.attributes; + } + + addOrUpdateAttribute = async (attribute: AttributeModel, isEdit: boolean) => { + this.loaderService.activate(); + let attributeFromServer: AttributeModel; + this.temp = [...this.attributes]; + + const deactivateLoader = () => { + this.loaderService.deactivate(); + return undefined; + }; + + if (isEdit) { + attributeFromServer = await this.topologyTemplateService + .updateAttributeAsync(this.componentType, this.componentUid, attribute) + .catch(deactivateLoader); + if (attributeFromServer) { + const indexOfUpdatedAttribute = _.findIndex(this.temp, (e) => e.uniqueId === attributeFromServer.uniqueId); + this.temp[indexOfUpdatedAttribute] = attributeFromServer; + } + } else { + attributeFromServer = await this.topologyTemplateService + .addAttributeAsync(this.componentType, this.componentUid, attribute) + .catch(deactivateLoader); + if (attributeFromServer) { + this.temp.push(attributeFromServer); + } + } + this.attributes = this.temp; + this.loaderService.deactivate(); + } + + deleteAttribute = async (attributeToDelete: AttributeModel) => { + this.loaderService.activate(); + this.temp = [...this.attributes]; + const res = await this.topologyTemplateService.deleteAttributeAsync(this.componentType, this.componentUid, attributeToDelete); + _.remove(this.temp, (attr) => attr.uniqueId === attributeToDelete.uniqueId); + this.attributes = this.temp; + this.loaderService.deactivate(); + }; + + openAddEditModal(selectedRow: AttributeModel, isEdit: boolean) { + const component = new Resource(undefined, undefined, undefined); + component.componentType = this.componentType; + component.uniqueId = this.componentUid; + + const title: string = this.translateService.translate('ATTRIBUTE_DETAILS_MODAL_TITLE'); + const attributeModalConfig = { + title, + size: 'md', + type: SdcUiCommon.ModalType.custom, + buttons: [ + { + id: 'save', + text: 'Save', + // spinner_position: Placement.left, + size: 'sm', + callback: () => this.modalCallBack(isEdit), + closeModal: true, + disabled: false, + } + ] as SdcUiCommon.IModalButtonComponent[] + }; + + this.customModalInstance = this.modalService.openCustomModal(attributeModalConfig, AttributeModalComponent, { attributeToEdit: selectedRow }); + this.customModalInstance.innerModalContent.instance. + onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('save').disabled = !isValid); + } + + /*********************** + * Call Backs from UI * + ***********************/ + + /** + * Called when 'Add' is clicked + */ + onAddAttribute() { + this.openAddEditModal(new AttributeModel(), false); + } + + /** + * Called when 'Edit' button is clicked + */ + onEditAttribute(event, row) { + event.stopPropagation(); + + const attributeToEdit: AttributeModel = new AttributeModel(row); + this.openAddEditModal(attributeToEdit, true); + } + + /** + * Called when 'Delete' button is clicked + */ + onDeleteAttribute(event, row: AttributeModel) { + event.stopPropagation(); + const onOk = () => { + this.deleteAttribute(row); + }; + + const title: string = this.translateService.translate('ATTRIBUTE_VIEW_DELETE_MODAL_TITLE'); + const message: string = this.translateService.translate('ATTRIBUTE_VIEW_DELETE_MODAL_TEXT'); + const okButton = new SdcUiComponents.ModalButtonComponent(); + okButton.testId = 'OK'; + okButton.text = 'OK'; + okButton.type = SdcUiCommon.ButtonType.info; + okButton.closeModal = true; + okButton.callback = onOk; + + this.modalService.openInfoModal(title, message, 'delete-modal', [okButton]); + } + + onExpandRow(event) { + if (event.type === 'click') { + this.table.rowDetail.toggleExpandRow(event.row); + } + } + + /** + * Callback from Modal after "Save" is clicked + * + * @param {boolean} isEdit - Whether modal is edit or add attribute + */ + modalCallBack = (isEdit: boolean) => { + const attribute: AttributeModel = this.customModalInstance.innerModalContent.instance.attributeToEdit; + this.addOrUpdateAttribute(attribute, isEdit); + } + +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts new file mode 100644 index 0000000000..5abb952e37 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts @@ -0,0 +1,32 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { SdcUiComponentsModule } from 'onap-ui-angular'; +import { GlobalPipesModule } from '../../../pipes/global-pipes.module'; +import { AttributesComponent } from './attributes.component'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; +import { AttributeModalComponent } from './attribute-modal.component'; +import { TranslateModule } from '../../../shared/translator/translate.module'; + +@NgModule({ + declarations: [ + AttributesComponent, + AttributeModalComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + GlobalPipesModule, + NgxDatatableModule, + TranslateModule + ], + exports: [ + AttributesComponent + ], + entryComponents: [ + AttributesComponent, AttributeModalComponent + ], + providers: [TopologyTemplateService] +}) +export class AttributesModule { +} |