aboutsummaryrefslogtreecommitdiffstats
path: root/catalog-ui/src/app/ng2/pages/workspace/attributes
diff options
context:
space:
mode:
Diffstat (limited to 'catalog-ui/src/app/ng2/pages/workspace/attributes')
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.html104
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts138
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/attributes/attributes-modal.component.spec.ts128
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/attributes/attributes-options.ts60
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.html93
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.less36
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.spec.ts182
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.ts188
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts32
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 {
+}