summaryrefslogtreecommitdiffstats
path: root/catalog-ui/src/app/ng2/pages/workspace
diff options
context:
space:
mode:
authorys9693 <ys9693@att.com>2020-01-19 13:50:02 +0200
committerOfir Sonsino <ofir.sonsino@intl.att.com>2020-01-22 12:33:31 +0000
commit16a9fce0e104a38371a9e5a567ec611ae3fc7f33 (patch)
tree03a2aff3060ddb5bc26a90115805a04becbaffc9 /catalog-ui/src/app/ng2/pages/workspace
parentaa83a2da4f911c3ac89318b8e9e8403b072942e1 (diff)
Catalog alignment
Issue-ID: SDC-2724 Signed-off-by: ys9693 <ys9693@att.com> Change-Id: I52b4aacb58cbd432ca0e1ff7ff1f7dd52099c6fe
Diffstat (limited to 'catalog-ui/src/app/ng2/pages/workspace')
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.html68
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.less8
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.spec.ts84
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.ts48
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.module.ts28
-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
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/__snapshots__/deployment-artifacts-page.spec.ts.snap24
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.html73
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.less55
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.ts155
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.module.ts35
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.spec.ts86
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.html11
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.less24
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.ts78
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.module.ts30
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.html29
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.less20
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.ts24
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.html119
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.less222
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.spec.ts133
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.ts139
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.module.ts24
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.html62
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.less78
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.spec.ts90
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.ts68
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.html47
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.less66
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.spec.ts47
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.ts104
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.html80
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.less92
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.spec.ts92
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.ts117
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.module.ts34
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.service.ts233
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/information-artifact/__snapshots__/informational-artifact-page.spec.ts.snap40
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.html82
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.less29
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.ts69
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.module.ts30
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/information-artifact/informational-artifact-page.spec.ts77
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.html22
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.less9
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.ts33
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.html59
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.less16
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.ts79
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.html93
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.less38
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.ts81
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.module.ts29
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.html21
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.less19
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.spec.ts127
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.ts229
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.module.ts49
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.service.ts80
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.html91
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.less35
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.ts90
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.module.ts28
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirements.component.less4
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirments.components.html38
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirments.components.ts103
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/__snapshots__/tosca-artifact-page.spec.ts.snap35
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.html50
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.less7
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.ts46
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.module.ts28
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.spec.ts71
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/workspace-ng1-bridge-service.ts37
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/workspace.component.ts3
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/workspace.module.ts50
-rw-r--r--catalog-ui/src/app/ng2/pages/workspace/workspace.service.ts70
85 files changed, 5785 insertions, 0 deletions
diff --git a/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.html b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.html
new file mode 100644
index 0000000000..d7cf2f930a
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.html
@@ -0,0 +1,68 @@
+<!--
+ ~ 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="activity-log">
+ <div class="sdc-filter-bar-wrapper">
+ <sdc-filter-bar
+ [placeHolder]="'Search...'"
+ [testId]="activityLogSearchBar"
+ (keyup)="updateFilter($event)">
+ </sdc-filter-bar>
+ </div>
+ <ngx-datatable
+ columnMode="flex"
+ [footerHeight]="0"
+ [limit]="50"
+ [headerHeight]="40"
+ [rowHeight]="35"
+ #activityLogTable
+ [rows]="activities">
+
+ <ngx-datatable-column name="Time" [flexGrow]="2" [prop]="'TIMESTAMP'">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ {{row.TIMESTAMP | date }} | {{row.TIMESTAMP | date:"HH:mm O"}}
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column name="Action" [flexGrow]="3" [prop]="'ACTION'">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ {{row.ACTION}}
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column name="Comment" [flexGrow]="5" [prop]="'COMMENT'">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <span sdc-tooltip [tooltip-text]="row.COMMENT">{{ row.COMMENT }}</span>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column name="Modifier" [flexGrow]="3" [prop]="'MODIFIER'">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ {{ row.MODIFIER }}
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column name="Status" [flexGrow]="1" [prop]="'STATUS'">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <svg-icon-label
+ [name]="row.STATUS <= 399 ? 'success' : 'icons_close'"
+ [mode]="row.STATUS <= 399 ? 'success' : 'error'"
+ [size]="'medium'"
+ [label]="row.STATUS"
+ [labelPlacement]="'left'"
+ [labelClassName]="'label'"
+ >
+ </svg-icon-label>
+ </ng-template>
+ </ngx-datatable-column>
+ </ngx-datatable>
+
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.less b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.less
new file mode 100644
index 0000000000..4845f4f606
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.less
@@ -0,0 +1,8 @@
+.sdc-filter-bar-wrapper {
+ sdc-filter-bar {
+ flex: 0 0 30%;
+ }
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 10px;
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.spec.ts
new file mode 100644
index 0000000000..25651e0c1f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.spec.ts
@@ -0,0 +1,84 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture } from '@angular/core/testing';
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import { SdcUiServices } from 'onap-ui-angular';
+import 'rxjs/add/observable/of';
+import { Observable } from 'rxjs/Observable';
+import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper';
+import { ComponentMetadata } from '../../../../models/component-metadata';
+import { ActivityLogService } from '../../../services/activity-log.service';
+import { WorkspaceService } from '../workspace.service';
+import { ActivityLogComponent } from './activity-log.component';
+
+describe('activity log component', () => {
+
+ let fixture: ComponentFixture<ActivityLogComponent>;
+ let activityLogServiceMock: Partial<ActivityLogService>;
+ let workspaceServiceMock: Partial<WorkspaceService>;
+ let loaderServiceMock: Partial<SdcUiServices.LoaderService>;
+ let componentMetadataMock: ComponentMetadata;
+
+ const mockLogs = '[' +
+ '{"MODIFIER":"Carlos Santana(m08740)","COMMENT":"comment","STATUS":"200","ACTION":"Checkout","TIMESTAMP":"2018-11-19 13:00:02.388 UTC"},' +
+ '{"MODIFIER":"John Doe(m08741)","COMMENT":"comment","STATUS":"200","ACTION":"Checkin","TIMESTAMP":"2018-11-20 13:00:02.388 UTC"},' +
+ '{"MODIFIER":"Jane Doe(m08742)","COMMENT":"comment","STATUS":"200","ACTION":"Checkout","TIMESTAMP":"2018-11-21 13:00:02.388 UTC"}' +
+ ']';
+
+ beforeEach(
+ async(() => {
+
+ componentMetadataMock = new ComponentMetadata();
+ componentMetadataMock.uniqueId = 'fake';
+ componentMetadataMock.componentType = 'SERVICE';
+
+ activityLogServiceMock = {
+ getActivityLog : jest.fn().mockImplementation((type, id) => Observable.of(JSON.parse(mockLogs)) )
+ };
+
+ workspaceServiceMock = {
+ metadata : componentMetadataMock
+ };
+
+ loaderServiceMock = {
+ activate : jest.fn(),
+ deactivate: jest.fn()
+ };
+
+ const configure: ConfigureFn = (testBed) => {
+ testBed.configureTestingModule({
+ declarations: [ActivityLogComponent],
+ imports: [NgxDatatableModule],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ { provide: WorkspaceService, useValue: workspaceServiceMock },
+ { provide: ActivityLogService, useValue: activityLogServiceMock },
+ { provide: SdcUiServices.LoaderService, useValue: loaderServiceMock }
+ ],
+ });
+ };
+
+ configureTests(configure).then((testBed) => {
+ fixture = testBed.createComponent(ActivityLogComponent);
+ });
+ })
+ );
+
+ it('should see exactly 3 activity logs', () => {
+ fixture.componentInstance.ngOnInit();
+ expect(fixture.componentInstance.activities.length).toBe(3);
+ });
+
+ it('should filter out 1 element when searching', () => {
+ fixture.componentInstance.ngOnInit();
+
+ const event = {
+ target : {
+ value : 'Checkin'
+ }
+ };
+
+ expect(fixture.componentInstance.activities.length).toBe(3);
+ fixture.componentInstance.updateFilter(event);
+ expect(fixture.componentInstance.activities.length).toBe(1);
+ });
+});
diff --git a/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.ts b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.ts
new file mode 100644
index 0000000000..84fb81a1ef
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.ts
@@ -0,0 +1,48 @@
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { SdcUiServices } from 'onap-ui-angular';
+import { Activity } from '../../../../models/activity';
+import { ActivityLogService } from '../../../services/activity-log.service';
+import { WorkspaceService } from '../workspace.service';
+
+@Component({
+ selector: 'activity-log',
+ templateUrl: './activity-log.component.html',
+ styleUrls: ['./activity-log.component.less', '../../../../../assets/styles/table-style.less']
+})
+export class ActivityLogComponent implements OnInit {
+
+ activities: Activity[] = [];
+ temp: Activity[] = [];
+
+ constructor(private workspaceService: WorkspaceService,
+ private activityLogService: ActivityLogService,
+ private loaderService: SdcUiServices.LoaderService) {
+ }
+
+ ngOnInit(): void {
+ this.loaderService.activate();
+ const componentId: string = this.workspaceService.metadata.uniqueId;
+ const componentType: string = this.workspaceService.metadata.componentType;
+ this.activityLogService.getActivityLog(componentType, componentId).subscribe((logs) => {
+ this.activities = logs;
+ this.temp = [...logs];
+ this.loaderService.deactivate();
+ }, (error) => { this.loaderService.deactivate(); });
+ }
+
+ updateFilter(event) {
+ const val = event.target.value.toLowerCase();
+
+ // filter our data
+ const temp = this.temp.filter((activity: Activity) => {
+ return !val ||
+ activity.COMMENT.toLowerCase().indexOf(val) !== -1 ||
+ activity.STATUS.toLowerCase().indexOf(val) !== -1 ||
+ activity.ACTION.toLowerCase().indexOf(val) !== -1 ||
+ activity.MODIFIER.toLowerCase().indexOf(val) !== -1;
+ });
+
+ // update the rows
+ this.activities = temp;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.module.ts b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.module.ts
new file mode 100644
index 0000000000..39334d8cde
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.module.ts
@@ -0,0 +1,28 @@
+import {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {SdcUiComponentsModule} from "onap-ui-angular";
+import {GlobalPipesModule} from "../../../pipes/global-pipes.module";
+import {ActivityLogComponent} from "./activity-log.component";
+import {ActivityLogService} from "../../../services/activity-log.service";
+import {NgxDatatableModule} from "@swimlane/ngx-datatable";
+
+@NgModule({
+ declarations: [
+ ActivityLogComponent
+ ],
+ imports: [
+ CommonModule,
+ SdcUiComponentsModule,
+ GlobalPipesModule,
+ NgxDatatableModule
+ ],
+ exports: [
+ ActivityLogComponent
+ ],
+ entryComponents: [
+ ActivityLogComponent
+ ],
+ providers: [ ActivityLogService ]
+})
+export class ActivityLogModule {
+}
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 {
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/__snapshots__/deployment-artifacts-page.spec.ts.snap b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/__snapshots__/deployment-artifacts-page.spec.ts.snap
new file mode 100644
index 0000000000..b53674497c
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/__snapshots__/deployment-artifacts-page.spec.ts.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`deployment artifacts page should match current snapshot of informational artifact pages component 1`] = `
+<deployment-artifact-page
+ addOrUpdateArtifact={[Function Function]}
+ artifactsService={[Function Object]}
+ cacheService={[Function Object]}
+ deleteArtifact={[Function Function]}
+ getEnvArtifact={[Function Function]}
+ modalService={[Function Object]}
+ openGenericArtifactBrowserModal={[Function Function]}
+ openPopOver={[Function Function]}
+ popoverContentComponent="undefined"
+ popoverService={[Function Object]}
+ sortArtifacts={[Function Function]}
+ store={[Function Store]}
+ table="undefined"
+ translateService={[Function Object]}
+ updateEnvParams={[Function Function]}
+ workspaceService={[Function Object]}
+>
+
+</deployment-artifact-page>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.html b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.html
new file mode 100644
index 0000000000..35592d846a
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.html
@@ -0,0 +1,73 @@
+<div class="deployment-artifact-page" *ngIf="(workspaceState$ | async) as state">
+ <svg-icon-label *ngIf="!state.isViewOnly" class="add-artifact-btn" [clickable]="true" [mode]="'primary'" [labelPlacement]="'right'"
+ [label]="'Add'" [name]="'plus'"
+ (click)="addOrUpdateArtifact()"></svg-icon-label>
+ <ngx-datatable
+ columnMode="flex"
+ [headerHeight]="40"
+ [footerHeight]="'undefined'"
+ [reorderable]="false"
+ [swapColumns]="false"
+ [rows]="deploymentArtifacts$ | async"
+ #deploymentArtifactsTable>
+ <ngx-datatable-column [resizeable]="false" name="Name" [flexGrow]="1">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <div *ngIf="row.generatedFromId" class="env-artifact-container">
+ <div class="env-artifact"></div>
+ </div>
+ <span sdc-tooltip [tooltip-text]="row.artifactDisplayName" [tooltip-placement]="3" [attr.data-tests-id]="'artifactDisplayName_' + row.artifactDisplayName">{{row.artifactDisplayName}}</span>
+ <span *ngIf="row.description.length > 0" class="info">
+ <svg-icon [clickable]="true" [name]="'comment'" [mode]="'primary2'" (click)="openPopOver('Description',row.description,{x:$event.pageX , y:$event.pageY },'bottom')"></svg-icon>
+ </span>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false" name="Type" [flexGrow]="0.6">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <span sdc-tooltip [tooltip-text]="row.artifactType" [tooltip-placement]="3" [attr.data-tests-id]="'artifactType_' + row.artifactDisplayName">{{row.artifactType}}</span>
+ </ng-template>
+ </ngx-datatable-column> exactly 2 tosca artifacts
+ <ngx-datatable-column [resizeable]="false" name="Version" [flexGrow]="0.3">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <span [attr.data-tests-id]="'artifactVersion_' + row.artifactDisplayName">{{ row.artifactVersion }}</span>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false" name="UUID" [flexGrow]="1">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <span sdc-tooltip [tooltip-text]="row.artifactUUID" [tooltip-placement]="3">{{ row.artifactUUID }}</span>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false" [flexGrow]="0.6">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <div class="download-artifact-button">
+ <svg-icon *ngIf="!row.heatParameters?.length && !state.isViewOnly" class="action-icon" [mode]="'primary2'" [name]="'edit-o'"
+ testId="edit_{{row.artifactDisplayName}}" clickable="true" size="medium"
+ (click)="addOrUpdateArtifact(row, state.isViewOnly)"></svg-icon>
+ <svg-icon *ngIf="row.heatParameters?.length && !state.isViewOnly" class="action-icon" [mode]="'primary2'" [name]="'indesign_status'"
+ testId="update_heat_params_{{row.artifactDisplayName}}" clickable="true" size="medium"
+ (click)="updateEnvParams(row, state.isViewOnly)"></svg-icon>
+ <svg-icon *ngIf="!row.isFromCsar && !state.isViewOnly" class="action-icon" [mode]="'primary2'" [name]="'trash-o'"
+ testId="delete_{{row.artifactDisplayName}}" clickable="true" size="medium" (click)="deleteArtifact(row)"></svg-icon>
+ <svg-icon *ngIf="row.isGenericBrowseable()" class="action-icon" [mode]="'primary2'" [name]="'search-o'"
+ testId="gab-{{row.artifactDisplayName}}" clickable="true" size="medium" (click)="openGenericArtifactBrowserModal(row)"></svg-icon>
+
+ <!--Download-->
+ </div>
+ </ng-template>
+ </ngx-datatable-column>
+
+ <ngx-datatable-footer>
+ <ng-template ngx-datatable-footer-template>
+ <div class="table-footer-container">
+ <sdc-button *ngIf="!state.isViewOnly" [type]="'secondary'"
+ [testId]="'add_artifact_btn'"
+ [text]="'DEPLOYMENT_ARTIFACT_BUTTON_ADD_OTHER' | translate"
+ [icon_name]="'plus-circle-o'"
+ [icon_mode] = "'secondary'"
+ [icon_position]="'left'"
+ (click)="addOrUpdateArtifact()">
+ </sdc-button>
+ </div>
+ </ng-template>
+ </ngx-datatable-footer>
+ </ngx-datatable>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.less b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.less
new file mode 100644
index 0000000000..22ceb96653
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.less
@@ -0,0 +1,55 @@
+.deployment-artifact-page {
+
+
+ .env-artifact-container {
+ margin-left: -25px;
+ margin-top: -21px;
+ padding-left: 10px;
+ position: absolute;
+ background-color: white;
+ .env-artifact {
+ border-left: 1px #848586 solid;
+ height: 33px;
+
+ border-top: 1px #848586 solid;
+ border-bottom: 1px #848586 solid;
+ width: 10px;
+ float: left;
+
+ }
+ }
+ .add-artifact-btn {
+ display: flex;
+ cursor: pointer;
+ justify-content: flex-end;
+ margin-bottom: 10px;
+ }
+ .download-artifact-button {
+ display: flex;
+ justify-content: center;
+
+ .action-icon {
+ margin-right: 10px;
+ }
+ }
+
+ .table-footer-container {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ justify-content: center;
+ margin: 20px 0px;
+ }
+}
+
+:host ::ng-deep {
+
+ .ngx-datatable {
+ //border: 1px solid red;
+ .datatable-body-cell {
+ .info {
+ float: right;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.ts
new file mode 100644
index 0000000000..53b21b34b6
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.ts
@@ -0,0 +1,155 @@
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { Select, Store } from '@ngxs/store';
+import { ArtifactModel } from 'app/models';
+import * as _ from 'lodash';
+import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular';
+import { Observable } from 'rxjs/index';
+import { map } from 'rxjs/operators';
+import { GabConfig } from '../../../../models/gab-config';
+import { PathsAndNamesDefinition } from '../../../../models/paths-and-names';
+import { GenericArtifactBrowserComponent } from '../../../../ng2/components/logic/generic-artifact-browser/generic-artifact-browser.component';
+import { ArtifactGroupType, ArtifactType } from '../../../../utils/constants';
+import { ArtifactsService } from '../../../components/forms/artifacts-form/artifacts.service';
+import { PopoverContentComponent } from '../../../components/ui/popover/popover-content.component';
+import { CacheService } from '../../../services/cache.service';
+import { TranslateService } from '../../../shared/translator/translate.service';
+import { GetArtifactsByTypeAction } from '../../../store/actions/artifacts.action';
+import { ArtifactsState } from '../../../store/states/artifacts.state';
+import { WorkspaceState, WorkspaceStateModel } from '../../../store/states/workspace.state';
+import { WorkspaceService } from '../workspace.service';
+import { ModalService } from 'app/ng2/services/modal.service';
+
+export interface IPoint {
+ x: number;
+ y: number;
+}
+
+@Component({
+ selector: 'deployment-artifact-page',
+ templateUrl: './deployment-artifacts-page.component.html',
+ styleUrls: ['./deployment-artifacts-page.component.less', '../../../../../assets/styles/table-style.less']
+})
+export class DeploymentArtifactsPageComponent implements OnInit {
+
+ public componentId: string;
+ public componentType: string;
+ public deploymentArtifacts$: Observable<ArtifactModel[]>;
+ public isComponentInstanceSelected: boolean;
+
+ @Select(WorkspaceState) workspaceState$: Observable<WorkspaceStateModel>;
+ @ViewChild('informationArtifactsTable') table: any;
+ @ViewChild('popoverForm') popoverContentComponent: PopoverContentComponent;
+
+ constructor(private workspaceService: WorkspaceService,
+ private artifactsService: ArtifactsService,
+ private store: Store,
+ private popoverService: SdcUiServices.PopoverService,
+ private cacheService: CacheService,
+ private modalService: SdcUiServices.ModalService,
+ private translateService: TranslateService) {
+ }
+
+ private getEnvArtifact = (heatArtifact: ArtifactModel, artifacts: ArtifactModel[]): ArtifactModel => {
+ return _.find(artifacts, (item: ArtifactModel) => {
+ return item.generatedFromId === heatArtifact.uniqueId;
+ });
+ };
+
+ // we need to sort the artifact in a way that the env artifact is always under the artifact he is connected to- this is cause of the way the ngx databale work
+ private sortArtifacts = ((artifacts) => {
+ const sortedArtifacts = [];
+ _.forEach(artifacts, (artifact: ArtifactModel): void => {
+ const envArtifact = this.getEnvArtifact(artifact, artifacts);
+ if (!artifact.generatedFromId) {
+ sortedArtifacts.push(artifact);
+ }
+ if (envArtifact) {
+ sortedArtifacts.push(envArtifact);
+ }
+ });
+ return sortedArtifacts;
+ })
+
+ ngOnInit(): void {
+ this.componentId = this.workspaceService.metadata.uniqueId;
+ this.componentType = this.workspaceService.metadata.componentType;
+
+ this.store.dispatch(new GetArtifactsByTypeAction({
+ componentType: this.componentType,
+ componentId: this.componentId,
+ artifactType: ArtifactGroupType.DEPLOYMENT
+ }));
+ this.deploymentArtifacts$ = this.store.select(ArtifactsState.getArtifactsByType).pipe(map((filterFn) => filterFn(ArtifactType.DEPLOYMENT))).pipe(map(artifacts => {
+ return this.sortArtifacts(artifacts);
+ }));
+ }
+
+ onActivate(event) {
+ if (event.type === 'click') {
+ this.table.rowDetail.toggleExpandRow(event.row);
+ }
+ }
+
+ public addOrUpdateArtifact = (artifact: ArtifactModel, isViewOnly: boolean) => {
+ this.artifactsService.openArtifactModal(this.componentId, this.componentType, artifact, ArtifactGroupType.DEPLOYMENT, isViewOnly);
+ }
+
+ public deleteArtifact = (artifactToDelete) => {
+ this.artifactsService.deleteArtifact(this.componentType, this.componentId, artifactToDelete);
+ }
+
+ private openPopOver = (title: string, content: string, positionOnPage: IPoint, location: string) => {
+ this.popoverService.createPopOver(title, content, positionOnPage, location);
+ }
+
+ public updateEnvParams = (artifact: ArtifactModel, isViewOnly: boolean) => {
+ this.artifactsService.openUpdateEnvParams(this.componentType, this.componentId, artifact );
+ }
+
+ private openGenericArtifactBrowserModal = (artifact: ArtifactModel): void => {
+ const titleStr = 'Generic Artifact Browser';
+ const modalConfig = {
+ size: 'sdc-xl',
+ title: titleStr,
+ type: SdcUiCommon.ModalType.custom,
+ buttons: [{
+ id: 'closeGABButton',
+ text: 'Close',
+ size: 'sm',
+ closeModal: true
+ }] as SdcUiCommon.IModalButtonComponent[]
+ };
+
+ const uiConfiguration: any = this.cacheService.get('UIConfiguration');
+ let noConfig: boolean = false;
+ let pathsandnamesArr: PathsAndNamesDefinition[] = [];
+
+ if (typeof uiConfiguration.gab === 'undefined') {
+ noConfig = true;
+ } else {
+ const gabConfig: GabConfig = uiConfiguration.gab
+ .find((config) => config.artifactType === artifact.artifactType);
+ if (typeof gabConfig === 'undefined') {
+ noConfig = true;
+ } else {
+ pathsandnamesArr = gabConfig.pathsAndNamesDefinitions;
+ }
+ }
+
+
+ if (noConfig) {
+ const msg = this.translateService.translate('DEPLOYMENT_ARTIFACT_GAB_NO_CONFIG');
+ this.modalService.openAlertModal(titleStr, msg);
+ }
+
+ const modalInputs = {
+ pathsandnames: pathsandnamesArr,
+ artifactid: artifact.esId,
+ resourceid: this.componentId
+ };
+
+ this.modalService.openCustomModal(modalConfig, GenericArtifactBrowserComponent, modalInputs);
+
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.module.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.module.ts
new file mode 100644
index 0000000000..398e9d3f4d
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.module.ts
@@ -0,0 +1,35 @@
+import {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {SdcUiComponentsModule} from "onap-ui-angular";
+import {NgxDatatableModule} from "@swimlane/ngx-datatable";
+import {UiElementsModule} from "../../../components/ui/ui-elements.module";
+import {ArtifactFormModule} from "../../../components/forms/artifacts-form/artifact-form.module";
+import {ArtifactsService} from "../../../components/forms/artifacts-form/artifacts.service";
+import {DeploymentArtifactsPageComponent} from "./deployment-artifacts-page.component";
+import {TranslatePipe} from "../../../shared/translator/translate.pipe";
+import {TranslateModule} from "../../../shared/translator/translate.module";
+import {GenericArtifactBrowserModule} from "../../../components/logic/generic-artifact-browser/generic-artifact-browser.module";
+
+@NgModule({
+ declarations: [
+ DeploymentArtifactsPageComponent
+ ],
+ imports: [
+ TranslateModule,
+ CommonModule,
+ SdcUiComponentsModule,
+ NgxDatatableModule,
+ UiElementsModule,
+ ArtifactFormModule,
+ GenericArtifactBrowserModule
+ ],
+ exports: [
+ DeploymentArtifactsPageComponent
+ ],
+ entryComponents: [
+ DeploymentArtifactsPageComponent
+ ],
+ providers:[ArtifactsService]
+})
+export class DeploymentArtifactsPageModule {
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.spec.ts
new file mode 100644
index 0000000000..056efdc5d4
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.spec.ts
@@ -0,0 +1,86 @@
+// import ' rxjs/add/observable/of';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture } from '@angular/core/testing';
+import { NgxsModule, Store } from '@ngxs/store';
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import { SdcUiServices } from 'onap-ui-angular';
+import { Observable } from 'rxjs/Observable';
+import { deploymentArtifactMock } from '../../../../../jest/mocks/artifacts-mock';
+import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper';
+import { ComponentMetadata } from '../../../../models/component-metadata';
+import { ArtifactsService } from '../../../components/forms/artifacts-form/artifacts.service';
+import { CacheService } from '../../../services/cache.service';
+import { TopologyTemplateService } from '../../../services/component-services/topology-template.service';
+import { TranslateModule } from '../../../shared/translator/translate.module';
+import { TranslateService } from '../../../shared/translator/translate.service';
+import { ArtifactsState } from '../../../store/states/artifacts.state';
+import { WorkspaceService } from '../workspace.service';
+import { DeploymentArtifactsPageComponent } from './deployment-artifacts-page.component';
+import {ModalService} from "../../../services/modal.service";
+
+describe('deployment artifacts page', () => {
+
+ let fixture: ComponentFixture<DeploymentArtifactsPageComponent>;
+ let topologyTemplateServiceMock: Partial<TopologyTemplateService>;
+ let workspaceServiceMock: Partial<WorkspaceService>;
+ let loaderServiceMock: Partial<SdcUiServices.LoaderService>;
+ let store: Store;
+
+ beforeEach(
+ async(() => {
+
+ topologyTemplateServiceMock = {
+ getArtifactsByType: jest.fn().mockImplementation((componentType, id, artifactType) => Observable.of(deploymentArtifactMock))
+ };
+ workspaceServiceMock = {
+ metadata: <ComponentMetadata>{
+ uniqueId: 'service_unique_id',
+ componentType: 'SERVICE'
+ }
+ }
+
+ loaderServiceMock = {
+ activate: jest.fn(),
+ deactivate: jest.fn()
+ }
+ const configure: ConfigureFn = (testBed) => {
+ testBed.configureTestingModule({
+ declarations: [DeploymentArtifactsPageComponent],
+ imports: [NgxDatatableModule, TranslateModule, NgxsModule.forRoot([ArtifactsState])],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: WorkspaceService, useValue: workspaceServiceMock},
+ {provide: TopologyTemplateService, useValue: topologyTemplateServiceMock},
+ {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock},
+ {provide: ArtifactsService, useValue: {}},
+ {provide: SdcUiServices.PopoverService, useValue: {}},
+ {provide: CacheService, useValue: {}},
+ {provide: SdcUiServices.ModalService, useValue: {}},
+ {provide: ModalService, useValue: {}},
+ {provide: TranslateService, useValue: {}}
+ ],
+ });
+ };
+
+ configureTests(configure).then((testBed) => {
+ fixture = testBed.createComponent(DeploymentArtifactsPageComponent);
+ store = testBed.get(Store);
+ });
+ })
+ );
+
+ it('should match current snapshot of informational artifact pages component', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+ it('should see exactly 2 tosca artifacts', () => {
+ fixture.componentInstance.ngOnInit();
+ fixture.componentInstance.deploymentArtifacts$.subscribe((artifacts) => {
+ expect(artifacts.length).toEqual(8);
+ })
+ store.selectOnce((state) => state.artifacts.deploymentArtifacts).subscribe((artifacts) => {
+ expect(artifacts.length).toEqual(8);
+ });
+ });
+
+});
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.html b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.html
new file mode 100644
index 0000000000..885277217d
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.html
@@ -0,0 +1,11 @@
+<div class="deployment-page">
+ <deployment-graph></deployment-graph>
+ <panel-wrapper-component>
+ <sdc-tabs class="deployment-tabs" [iconsSize]="'large'" [isVertical]="true">
+ <sdc-tab *ngFor="let tab of tabs" [titleIcon]="tab.titleIcon" [active]="tab.isActive"
+ [tooltipText]="tab.tooltipText">
+ <hierarchy-tab *ngIf="isDataAvailable" [isViewOnly]="(isViewOnly$ | async)"></hierarchy-tab>
+ </sdc-tab>
+ </sdc-tabs>
+ </panel-wrapper-component>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.less b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.less
new file mode 100644
index 0000000000..4b7a1e7e9f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.less
@@ -0,0 +1,24 @@
+@import './../../../../../assets/styles/variables.less';
+@import './../../../../../assets/styles/override.less';
+.deployment-page {
+ width: 100%;
+ height: 100%;
+
+ /deep/ .sdc-tabs {
+ height: 100%;
+ }
+ /deep/ .sdc-tabs-list {
+ position: absolute;
+ top: 22px;
+ right: 303px;
+ background-color: @sdcui_color_silver;
+
+ svg-icon-label {
+ vertical-align: middle;
+ }
+ }
+ /deep/ .sdc-tab-content {
+ height: 100%;
+ }
+}
+
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.ts
new file mode 100644
index 0000000000..12bd5369c7
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.ts
@@ -0,0 +1,78 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 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.
+ * ============LICENSE_END=========================================================
+ */
+
+import {Component} from "@angular/core";
+import {HierarchyTabComponent} from "./panel/panel-tabs/hierarchy-tab/hierarchy-tab.component";
+import {ComponentGenericResponse} from "../../../services/responses/component-generic-response";
+import {TopologyTemplateService} from "../../../services/component-services/topology-template.service";
+import {WorkspaceService} from "../workspace.service";
+import {Module} from "app/models";
+import {SdcUiServices} from "onap-ui-angular";
+import {Select} from "@ngxs/store";
+import {WorkspaceState} from "../../../store/states/workspace.state";
+import {DeploymentGraphService} from "../../composition/deployment/deployment-graph.service";
+
+const tabs =
+ {
+ hierarchyTab: {
+ titleIcon: 'composition-o',
+ component: HierarchyTabComponent,
+ input: {},
+ isActive: true,
+ tooltipText: 'Hierarchy'
+ }
+ };
+
+@Component({
+ selector: 'deployment-page',
+ templateUrl: './deployment-page.component.html',
+ styleUrls: ['deployment-page.component.less']
+})
+
+export class DeploymentPageComponent {
+ public tabs: Array<any>;
+ public resourceType: string;
+ public modules: Array<Module>;
+ public isDataAvailable: boolean;
+
+ @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean;
+
+ constructor(private topologyTemplateService: TopologyTemplateService,
+ private workspaceService: WorkspaceService,
+ private deploymentService: DeploymentGraphService,
+ private loaderService: SdcUiServices.LoaderService) {
+ this.tabs = [];
+ this.isDataAvailable = false;
+ }
+
+ ngOnInit(): void {
+ this.topologyTemplateService.getDeploymentGraphData(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId).subscribe((response: ComponentGenericResponse) => {
+ this.deploymentService.componentInstances = response.componentInstances;
+ this.deploymentService.componentInstancesRelations = response.componentInstancesRelations;
+ this.deploymentService.modules = response.modules;
+ this.isDataAvailable = true;
+ this.loaderService.deactivate();
+ });
+
+ this.loaderService.activate();
+ this.resourceType = this.workspaceService.getMetadataType();
+ this.tabs.push(tabs.hierarchyTab);
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.module.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.module.ts
new file mode 100644
index 0000000000..3635e8f2cf
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.module.ts
@@ -0,0 +1,30 @@
+/**
+ * Created by ob0695 on 6/4/2018.
+ */
+import {NgModule} from "@angular/core";
+import {CommonModule} from "@angular/common";
+import {DeploymentPageComponent} from "./deployment-page.component";
+import {SdcUiComponentsModule} from "onap-ui-angular";
+import {UiElementsModule} from "../../../components/ui/ui-elements.module";
+import {TranslateModule} from "../../../shared/translator/translate.module";
+import {GlobalPipesModule} from "../../../pipes/global-pipes.module";
+import {HierarchyTabModule} from "./panel/panel-tabs/hierarchy-tab/hierarchy-tab.module";
+import {DeploymentGraphService} from "../../composition/deployment/deployment-graph.service";
+import {DeploymentGraphModule} from "../../composition/deployment/deployment-graph.module";
+
+@NgModule({
+ declarations: [DeploymentPageComponent],
+ imports: [CommonModule,
+ DeploymentGraphModule,
+ SdcUiComponentsModule,
+ UiElementsModule,
+ TranslateModule,
+ GlobalPipesModule,
+ HierarchyTabModule
+ ],
+ exports: [DeploymentPageComponent],
+ entryComponents: [DeploymentPageComponent],
+ providers: [DeploymentGraphService]
+})
+export class DeploymentPageModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.html b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.html
new file mode 100644
index 0000000000..d5b9d9e9b2
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.html
@@ -0,0 +1,29 @@
+<div class="edit-module-name">
+ <div class="edit-module-name-label vfInstance-name" data-tests-id="popover-vfinstance-name" sdc-tooltip [tooltip-text]="selectModule.vfInstanceName">{{selectModule.vfInstanceName}}</div>
+ <div class="edit-module-name-heatName">
+ <sdc-input #heatName [maxLength]="50"
+ [(value)]="selectModule.heatName"
+ [testId]="'popover-heat-name'"
+ [placeHolder]="'Enter Name'">
+ </sdc-input>
+ <sdc-validation [validateElement]="heatName">
+ <sdc-regex-validator [message]="'Special characters not allowed.'" [pattern]="pattern"></sdc-regex-validator>
+ </sdc-validation>
+ </div>
+ <div class="edit-module-name-label module-name" data-tests-id="'popover-module-name'" sdc-tooltip [tooltip-text]="selectModule.moduleName">{{selectModule.moduleName}}</div>
+ <sdc-button class="edit-module-name-btn cancel-button"
+ [text]="'Cancel'"
+ [testId]="'popover-close-button'"
+ [type]="'primary'"
+ [size] = "'small'"
+ (click)="clickButton(false)">
+ </sdc-button>
+ <sdc-button class="edit-module-name-btn save-button"
+ [text]="'Save'"
+ [testId]="'popover-save-button'"
+ [type]="'primary'"
+ [size] = "'small'"
+ (click)="clickButton(true)"
+ [disabled]="selectModule.heatName == originalName">
+ </sdc-button>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.less b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.less
new file mode 100644
index 0000000000..721ad53bc3
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.less
@@ -0,0 +1,20 @@
+.edit-module-name-btn{
+ float:right;
+ margin-left: 10px;
+ margin-bottom: 20px;
+}
+.save-button {
+ margin-left: 30px;
+}
+.cancel-button {
+ margin-left: 20px;
+}
+.edit-module-name-heatName {
+ margin-bottom: 15px;
+}
+.edit-module-name-label {
+ text-overflow: ellipsis;
+ display: block;
+ white-space: nowrap;
+ margin-bottom: 10px;
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.ts
new file mode 100644
index 0000000000..819182c75f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.ts
@@ -0,0 +1,24 @@
+import { Component, Input, Output, OnInit } from "@angular/core";
+import { EventEmitter } from "@angular/core";
+import { DisplayModule } from "../../../../../../../models/modules/base-module";
+import { ValidationConfiguration } from "../../../../../../../models/validation-config";
+
+@Component({
+ selector: 'edit-module-name',
+ templateUrl: './edit-module-name.component.html',
+ styleUrls: ['edit-module-name.component.less']
+})
+export class EditModuleName implements OnInit{
+ @Input() selectModule:DisplayModule;
+ @Output() clickButtonEvent: EventEmitter<String> = new EventEmitter();
+ private pattern = ValidationConfiguration.validation.validationPatterns.stringOrEmpty;
+ private originalName: string;
+ constructor(){}
+ public ngOnInit(): void {
+ this.originalName = this.selectModule.heatName;
+ }
+
+ private clickButton(saveOrCancel: boolean) : void {
+ this.clickButtonEvent.emit(saveOrCancel ? this.selectModule.heatName : null);
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.html b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.html
new file mode 100644
index 0000000000..7c0e60b814
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.html
@@ -0,0 +1,119 @@
+<div class="sdc-hierarchy-tab" ng-class="">
+ <sdc-loader [global]="false" [testId]="'hierarchy-tab-loader'" [active]="isLoading" [relative]="true" [size]="'medium'"></sdc-loader>
+ <div class="sdc-hierarchy-tab-title"
+ [attr.data-tests-id]="'tab-header'">{{'DEPLOYMENT_TAB_TITLE' | translate }}</div>
+ <div [ngClass]="{'scroll-module-list': selectedModule}">
+ <div *ngIf="topologyTemplateType != 'SERVICE'; else isService" class="modules-list">
+ <div>
+ <div class="sdc-hierarchy-tab-sub-title" data-tests-id="tab-sub-header">{{topologyTemplateName}}</div>
+ <div *ngFor="let module of modules; index as i">
+ <sdc-accordion [title]="module.name" [arrow-direction]="'left'"
+ [css-class]="'expand-collapse-container'"
+ [ngClass]="{'selected': selectedModule !== undefined && selectedModule.uniqueId === module.uniqueId}"
+ [testId]="'hierarchy-module-' + i + '-title'" tooltip="{{module.name}}"
+ (click)="onModuleSelected(module)">
+ <div *ngFor="let memberId of getKeys(module.members)">
+ <div class="expand-collapse-sub-title" tooltip="{{memberId}}">{{memberId}}</div>
+ </div>
+ </sdc-accordion>
+ </div>
+ </div>
+ </div>
+
+ <ng-template #isService>
+ <div class="module-list">
+ <div *ngFor="let instance of componentInstances; index as instanceIndex">
+ <sdc-accordion [title]="instance.name" [arrow-direction]="'left'"
+ [css-class]="'expand-collapse-container outer-container'"
+ [testId]="'hierarchy-instance-' + instanceIndex + '-title'"
+ tooltip="{{instance.name}}">
+ <div *ngFor="let module of instance.groupInstances; index as moduleIndex">
+ <sdc-accordion [title]="module.name" [arrow-direction]="'left'"
+ [css-class]="'expand-collapse-container inner-container'"
+ [ngClass]="{'selected': selectedModule && selectedModule.groupInstanceUniqueId === module.uniqueId}"
+ [testId]="'hierarchy-module-' + moduleIndex + '-title'"
+ tooltip="{{module.uniqueId}}"
+ (click)="onModuleSelected(module, instance.uniqueId)">
+ <div *ngFor="let memberId of getKeys(module.members)">
+ <div class="expand-collapse-sub-title" tooltip="{{memberId}}">{{memberId}}</div>
+ </div>
+ </sdc-accordion>
+ </div>
+ </sdc-accordion>
+ </div>
+ </div>
+ </ng-template>
+
+ <!--TODO: Add Resizable-->
+ <div *ngIf="selectedModule"class="module-data-container" [attr.data-tests-id]="'selected-module-data'">
+ <div class="module-data">
+ <div class="module-name-container">
+ <div class="module-name module-text-overflow" [attr.data-tests-id]="'selected-module-name'"
+ tooltip="{{selectedModule.name}}">{{selectedModule.name}}</div>
+ <div class="edit-name-container" *ngIf="topologyTemplateType != 'SERVICE'">
+ <svg-icon name="edit-o" [size]="'medium'" [ngClass]="{'hand-pointer': !isViewOnly}" (click)="openEditModuleNamePopup($event)"></svg-icon>
+ </div>
+ </div>
+ <div [attr.data-tests-id]="'selected-module-group-uuid'" tooltip="{{selectedModule.groupUUID}}">
+ <div class="selected-module-property-header">Module ID:</div>
+ <div class="selected-module-property-value small-font">{{selectedModule.groupUUID}}</div>
+ </div>
+ <div [attr.data-tests-id]="'selected-module-group-customization-uuid'"
+ *ngIf="topologyTemplateType == 'SERVICE' && isViewOnly"
+ tooltip="{{selectedModule.customizationUUID}}">
+ <div class="selected-module-property-header">Customization ID:</div>
+ <div class="selected-module-property-value small-font">{{selectedModule.customizationUUID}}</div>
+ </div>
+ <div [attr.data-tests-id]="'selected-module-group-invariant-uuid'"
+ tooltip="{{selectedModule.invariantUUID}}">
+ <div class="selected-module-property-header">Invariant UUID:</div>
+ <div class="selected-module-property-value small-font">{{selectedModule.invariantUUID}}</div>
+ </div>
+ <div [attr.data-tests-id]="'selected-module-version'" class="selected-module-property-container">
+ <div class="selected-module-property-header">Version:</div>
+ <div class="selected-module-property-value same-row">{{selectedModule.version}}</div>
+ </div>
+ <div data-tests-id="selected-module-is-base" class="selected-module-property-container">
+ <div class="selected-module-property-header">IsBase:</div>
+ <div class="selected-module-property-value same-row">{{selectedModule.isBase}}</div>
+ </div>
+
+ </div>
+ <sdc-accordion [title]="'Properties'" [arrow-direction]="'right'"
+ [css-class]="'expand-collapse-module-data-container'">
+ <div *ngFor="let property of selectedModule.properties | orderBy:['name']:['asc']">
+ <div class="module-data-list-item">
+ <div class="module-data-list-item-value property-name"
+ [attr.data-tests-id]="'selected-module-property-name'">
+ <span tooltip="{{property.name}}" [ngClass]="{'hand-pointer': !isViewOnly}"
+ (click)="!isViewOnly && openEditPropertyModal(property)">{{property.name}}</span>
+ </div>
+ <div class="module-data-list-item-value property-info"
+ [attr.data-tests-id]="'selected-module-property-type'"> Type: {{property.type}}</div>
+ <div class="module-data-list-item-value property-info"
+ [attr.data-tests-id]="'selected-module-property-schema-type'">
+ Value: {{property.value}}</div>
+ </div>
+ </div>
+ </sdc-accordion>
+ <sdc-accordion [title]="'Artifacts'" [arrow-direction]="'right'"
+ [css-class]="'expand-collapse-module-data-container'">
+ <div *ngFor="let artifact of selectedModule.artifacts | orderBy:['artifactName']:['asc']">
+ <div class="module-data-list-item">
+ <div class="artifact-list-item">
+ <div class="module-data-list-item-value"
+ [attr.data-tests-id]="'selected-module-artifact-name'"
+ tooltip="{{artifact.artifactName}}">{{artifact.artifactName}}</div>
+ <div class="module-data-list-item-value artifact-info"
+ [attr.data-tests-id]="'selected-module-artifact-uuid'"
+ tooltip="{{artifact.artifactUUID}}">UUID: {{artifact.artifactUUID}}</div>
+ <div class="module-data-list-item-value artifact-info"
+ [attr.data-tests-id]="'selected-module-artifact-version'">
+ Version: {{artifact.artifactVersion}}</div>
+ </div>
+ </div>
+ </div>
+ </sdc-accordion>
+ </div>
+ </div>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.less b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.less
new file mode 100644
index 0000000000..269ca0aee0
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.less
@@ -0,0 +1,222 @@
+@import './../../../../../../../../assets/styles/variables.less';
+.sdc-hierarchy-tab {
+ padding: 15px 0 0 0;
+ background-color: #f8f8f8;
+ height: 100%;
+ box-shadow: 0.3px 1px 3px rgba(24, 24, 25, 0.42);
+ display: flex;
+ flex-flow: column;
+
+ .sdc-hierarchy-tab-title {
+ color: @main_color_a;
+ padding: 0 0 15px 20px;
+ border-bottom: 1px solid #d2d2d2;
+ }
+
+ .sdc-hierarchy-tab-sub-title {
+ color: @main_color_a;
+ padding: 15px 20px 15px 20px;
+ }
+
+ .scroll-module-list {
+ overflow-y: auto;
+ display: flex;
+ height: 100%;
+ flex-direction: column;
+ }
+
+ /deep/ .expand-collapse-container {
+ margin-bottom: 0;
+
+ .sdc-accordion-header {
+ white-space: nowrap;
+ line-height: 22px;
+ background-color: @tlv_color_u;
+ padding: 8px 20px 8px 8px;
+ box-shadow: inset 0px -1px 0px 0px rgba(255, 255, 255, 0.7);
+ height: 40px;
+
+ .title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 215px;
+ }
+ }
+
+ .sdc-accordion-body.open {
+ padding: 0 0 5px 0;
+ }
+
+ .sdc-accordion-header:hover {
+ background-color: @main_color_o;
+ }
+
+ &.outer-container {
+ .sdc-accordion-body {
+ padding-left: 0;
+ }
+ }
+
+ &.inner-container {
+ margin-bottom: 0;
+
+ .sdc-accordion-header {
+ padding: 8px 20px 8px 30px;
+ background-color: @tlv_color_t
+ }
+ }
+ }
+
+ sdc-accordion.selected {
+ /deep/ .expand-collapse-container {
+ .sdc-accordion-header {
+ background-color: @main_color_a;
+ color: @main_color_p;
+
+ .svg-icon {
+ fill: @main_color_p;
+ }
+ }
+ }
+ }
+
+ .expand-collapse-sub-title {
+ max-width: 225px;
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding: 10px 0 0 33px;
+ }
+
+ .expand-collapse-content {
+ .expand-collapse-title {
+ padding: 0 10px 0 30px;
+ }
+ }
+
+ .module-data-container {
+ width: 100%;
+ overflow-y: overlay;
+ background-color: @tlv_color_v;
+ border: 1px solid @main_color_a;
+ border-top: 4px solid @main_color_a;
+ box-shadow: 0.3px 1px 2px rgba(24, 24, 25, 0.32);
+ .module-data {
+ color: @main_color_a;
+ padding: 10px 0 10px 0;
+ margin: 0 20px 0 20px;
+
+ .selected-module-property-header {
+ font-weight: bold;
+ }
+
+ .selected-module-property-value {
+ font-family: @font-opensans-regular;
+
+ &.small-font {
+ font-size: 12px;
+ }
+ }
+
+ .module-name-container {
+
+ display: flex;
+ flex-direction: row;
+
+ .module-name {
+ font-size: 14px;
+ width: 75%;
+ max-width: 240px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .edit-name-container {
+ float: right;
+ border-left: 1px solid @main_color_a;
+ height: 20px;
+ padding-left: 12px;
+
+ svg-icon {
+ padding-top: 3px;
+ fill: @main_color_s;
+
+ &.hand-pointer {
+ cursor: pointer;
+ }
+
+ }
+ }
+ }
+ }
+
+ .selected-module-property-container {
+ flex-direction: row;
+ display: flex;
+
+ .selected-module-property-value {
+ text-indent: 2px;
+ }
+ }
+
+ /deep/ .expand-collapse-module-data-container {
+ margin-bottom: 0;
+
+ .sdc-accordion-header {
+ white-space: nowrap;
+ line-height: 22px;
+ padding: 8px 20px 8px 16px;
+ height: 40px;
+ background-color: @tlv_color_w;
+ color: @main_color_l;
+ border-top: 1px solid @main_color_a;
+ border-bottom: 1px solid @main_color_a;
+ width: 100%;
+ }
+
+ }
+
+ .module-data-list-item {
+ padding-bottom: 10px;
+ margin: 0 20px 0 20px;
+
+ .artifact-list-item {
+ color: @main_color_m;
+ }
+
+ .module-data-list-item-value {
+ width: 100%;
+ max-width: 240px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ &.artifact-info {
+ font-family: @font-opensans-regular;
+ font-size: 12px;
+ }
+
+ &.property-name {
+ font-weight: 400;
+ color: @main_color_a;
+
+ .hand-pointer {
+ cursor: pointer;
+ }
+ }
+
+ &.property-info {
+ color: @func_color_s;
+ font-family: @font-opensans-regular;
+ }
+ }
+ }
+ }
+}
+
+.modules-list {
+ overflow-y: overlay;
+ flex-grow: 1;
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.spec.ts
new file mode 100644
index 0000000000..ab88867cc0
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.spec.ts
@@ -0,0 +1,133 @@
+import {async, ComponentFixture} from "@angular/core/testing";
+import {HierarchyTabComponent} from "./hierarchy-tab.component";
+import {ConfigureFn, configureTests} from "../../../../../../../../jest/test-config.helper";
+import {NO_ERRORS_SCHEMA} from "@angular/core";
+import {TranslateModule} from "../../../../../../shared/translator/translate.module";
+import {TopologyTemplateService} from "../../../../../../services/component-services/topology-template.service";
+import {WorkspaceService} from "../../../../workspace.service";
+import {ModulesService} from "../../../../../../services/modules.service";
+import {GlobalPipesModule} from "../../../../../../pipes/global-pipes.module";
+import {TranslateService} from "../../../../../../shared/translator/translate.service";
+import {ModalsHandler} from "../../../../../../../utils/modals-handler";
+import {ComponentFactory} from "../../../../../../../utils/component-factory";
+import {NgxsModule} from "@ngxs/store";
+import { SdcUiServices } from "onap-ui-angular";
+import {Observable} from "rxjs";
+import {DisplayModule, Module} from "../../../../../../../models/modules/base-module";
+import {DeploymentGraphService} from "../../../../../composition/deployment/deployment-graph.service";
+import {ComponentMetadata} from "../../../../../../../models/component-metadata";
+
+describe('HierarchyTabComponent', () => {
+
+ let fixture: ComponentFixture<HierarchyTabComponent>;
+ let workspaceService: Partial<WorkspaceService>;
+ let popoverServiceMock: Partial<SdcUiServices.PopoverService>;
+ let modulesServiceMock: Partial<ModulesService>;
+
+ let editModuleNameInstanceMock = {innerPopoverContent:{instance: { clickButtonEvent: Observable.of("new heat name")}},
+ closePopover: jest.fn()};
+ let eventMock = {x: 1650, y: 350};
+ let moduleMock: Array<Module> = [{name: "NewVf2..base_vepdg..module-0", uniqueId: '1'}];
+ let selectedModuleMock: DisplayModule = {name: "NewVf2..base_vepdg..module-0", vfInstanceName: "NewVf2", moduleName:"module-0",
+ heatName: "base_vepdg", uniqueId: '1', updateName: jest.fn().mockImplementation(() => {
+ selectedModuleMock.name = selectedModuleMock.vfInstanceName + '..' + selectedModuleMock.heatName + '..' +
+ selectedModuleMock.moduleName;})}
+ let updateSelectedModuleMock = () => {
+ selectedModuleMock.heatName = "base_vepdg";
+ selectedModuleMock.name = "NewVf2..base_vepdg..module-0";
+ fixture.componentInstance.selectedModule = selectedModuleMock;
+ fixture.componentInstance.modules = moduleMock;
+ }
+ beforeEach(
+ async(() => {
+
+ workspaceService ={
+ metadata: <ComponentMetadata> {
+ name: '',
+ componentType: ''
+ }
+ }
+ popoverServiceMock = {
+ createPopOverWithInnerComponent: jest.fn().mockImplementation(() => {return editModuleNameInstanceMock})
+ }
+ modulesServiceMock = {
+ updateModuleMetadata: jest.fn().mockReturnValue(Observable.of({}))
+ }
+
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [HierarchyTabComponent],
+ schemas: [NO_ERRORS_SCHEMA],
+ imports: [TranslateModule, NgxsModule.forRoot([]), GlobalPipesModule],
+ providers: [
+ {provide: DeploymentGraphService, useValue: {}},
+ {provide: ComponentFactory, useValue: {}},
+ {provide: TopologyTemplateService, useValue: {}},
+ {provide: WorkspaceService, useValue: workspaceService},
+ {provide: ModulesService, useValue: modulesServiceMock},
+ {provide: TranslateService, useValue: {}},
+ {provide: ModalsHandler, useValue: {}},
+ {provide: SdcUiServices.PopoverService, useValue: popoverServiceMock}
+ ]
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(HierarchyTabComponent);
+ });
+ })
+ );
+
+ it('expected heirarchy component to be defined', () => {
+ expect(fixture).toBeDefined();
+ });
+
+ it('Update heat name and name sucessfully', () => {
+ updateSelectedModuleMock();
+ fixture.componentInstance.openEditModuleNamePopup(eventMock);
+ expect(fixture.componentInstance.selectedModule.updateName).toHaveBeenCalled();
+ expect(modulesServiceMock.updateModuleMetadata).toHaveBeenCalled();
+ expect(fixture.componentInstance.selectedModule.name).toEqual('NewVf2..new heat name..module-0');
+ expect(fixture.componentInstance.modules[0].name).toEqual('NewVf2..new heat name..module-0');
+ expect(fixture.componentInstance.selectedModule.heatName).toEqual('new heat name');
+ })
+ it('Try to update heat name and name and get error from server', () => {
+ updateSelectedModuleMock();
+ modulesServiceMock.updateModuleMetadata.mockImplementation(() => Observable.throwError({}));
+ fixture.componentInstance.openEditModuleNamePopup(eventMock);
+ expect(fixture.componentInstance.selectedModule.updateName).toHaveBeenCalled();
+ expect(modulesServiceMock.updateModuleMetadata).toHaveBeenCalled();
+ expect(fixture.componentInstance.modules[0].name).toEqual('NewVf2..base_vepdg..module-0');
+ expect(fixture.componentInstance.selectedModule.heatName).toEqual('base_vepdg');
+ expect(fixture.componentInstance.selectedModule.name).toEqual('NewVf2..base_vepdg..module-0');
+ })
+ it('Try to update heat name and name but not find the module with the same uniqueId', () => {
+ selectedModuleMock.uniqueId = '2'
+ updateSelectedModuleMock();
+ fixture.componentInstance.openEditModuleNamePopup(eventMock);
+ expect(fixture.componentInstance.selectedModule.updateName).toHaveBeenCalled();
+ expect(modulesServiceMock.updateModuleMetadata).not.toHaveBeenCalled();
+ expect(fixture.componentInstance.modules[0].name).toEqual('NewVf2..base_vepdg..module-0');
+ expect(fixture.componentInstance.selectedModule.heatName).toEqual('base_vepdg');
+ expect(fixture.componentInstance.selectedModule.name).toEqual('NewVf2..base_vepdg..module-0');
+ selectedModuleMock.uniqueId = '1'
+ })
+ it('Open edit module name popover and change the heat name', () => {
+ updateSelectedModuleMock();
+ spyOn(fixture.componentInstance, 'updateHeatName');
+ spyOn(fixture.componentInstance, 'updateOriginalHeatName');
+ fixture.componentInstance.openEditModuleNamePopup(eventMock);
+ expect(popoverServiceMock.createPopOverWithInnerComponent).toHaveBeenCalled();
+ expect(fixture.componentInstance.selectedModule.heatName).toEqual("new heat name");
+ expect(fixture.componentInstance.updateHeatName).toHaveBeenCalled();
+ })
+
+
+ it('Open edit module name popover and not change the heat name', () => {
+ updateSelectedModuleMock();
+ editModuleNameInstanceMock.innerPopoverContent.instance.clickButtonEvent = Observable.of(null);
+ fixture.componentInstance.openEditModuleNamePopup(eventMock);
+ expect(popoverServiceMock.createPopOverWithInnerComponent).toHaveBeenCalled();
+ expect(fixture.componentInstance.selectedModule.heatName).toEqual("base_vepdg");
+ })
+}); \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.ts
new file mode 100644
index 0000000000..604b194283
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.ts
@@ -0,0 +1,139 @@
+import {Component, Input} from "@angular/core";
+import {Component as TopologyTemplate, ComponentInstance, DisplayModule, Module, PropertyModel} from "app/models";
+import {TranslateService} from "app/ng2/shared/translator/translate.service";
+import {ComponentType} from "app/utils/constants";
+import {WorkspaceService} from "../../../../workspace.service";
+import {ModulesService} from "../../../../../../services/modules.service";
+import * as _ from "lodash";
+import {ModalsHandler} from "../../../../../../../utils/modals-handler";
+import {ComponentFactory} from "../../../../../../../utils/component-factory";
+import {Select, Store} from "@ngxs/store";
+import { SdcUiServices } from "onap-ui-angular";
+import { EditModuleName } from "../edit-module-name/edit-module-name.component";
+import {GraphState} from "../../../../../composition/common/store/graph.state";
+import {DeploymentGraphService} from "../../../../../composition/deployment/deployment-graph.service";
+import {OnSidebarOpenOrCloseAction} from "../../../../../composition/common/store/graph.actions";
+
+@ Component({
+ selector: 'hierarchy-tab',
+ templateUrl: './hierarchy-tab.component.html',
+ styleUrls: ['./hierarchy-tab.component.less'],
+})
+export class HierarchyTabComponent {
+
+ @Select(GraphState.withSidebar) withSidebar$: boolean;
+ @Input() isViewOnly: boolean;
+ public selectedIndex: number;
+ public selectedModule: DisplayModule;
+ public isLoading: boolean;
+ public topologyTemplateName: string;
+ public topologyTemplateType: string;
+ public modules: Array<Module> = [];
+ public componentInstances: Array<ComponentInstance> = [];
+ private editPropertyModalTopologyTemplate: TopologyTemplate;
+
+ constructor(private translateService: TranslateService,
+ private workspaceService: WorkspaceService,
+ private deploymentService: DeploymentGraphService,
+ private modulesService: ModulesService,
+ private ModalsHandler: ModalsHandler,
+ private componentFactory: ComponentFactory,
+ private store: Store,
+ private popoverService: SdcUiServices.PopoverService) {
+ this.isLoading = false;
+ this.topologyTemplateName = this.workspaceService.metadata.name;
+ this.topologyTemplateType = this.workspaceService.metadata.componentType;
+ }
+
+ ngOnInit() {
+ this.modules = this.deploymentService.modules;
+ this.componentInstances = this.deploymentService.componentInstances;
+ this.editPropertyModalTopologyTemplate = this.componentFactory.createEmptyComponent(this.topologyTemplateType);
+ this.editPropertyModalTopologyTemplate.componentInstances = this.deploymentService.componentInstances;
+ }
+
+ onModuleSelected(module: Module, componentInstanceId?: string): void {
+
+ let onSuccess = (module: DisplayModule) => {
+ console.log("Module Loaded: ", module);
+ this.selectedModule = module;
+ this.isLoading = false;
+ };
+
+ let onFailed = () => {
+ this.isLoading = false;
+ };
+
+ if (!this.selectedModule || (this.selectedModule && this.selectedModule.uniqueId != module.uniqueId)) {
+ this.isLoading = true;
+ if (this.topologyTemplateType == ComponentType.SERVICE) {
+ // this.selectedInstanceId = componentInstanceId;
+ this.modulesService.getComponentInstanceModule(this.topologyTemplateType, this.workspaceService.metadata.uniqueId, componentInstanceId, module.uniqueId).subscribe((resultModule: DisplayModule) => {
+ onSuccess(resultModule);
+ }, () => {
+ onFailed();
+ });
+ } else {
+ this.modulesService.getModuleForDisplay(this.topologyTemplateType, this.workspaceService.metadata.uniqueId, module.uniqueId).subscribe((resultModule: DisplayModule) => {
+ onSuccess(resultModule);
+ }, () => {
+ onFailed();
+ });
+ }
+ }
+ }
+
+ updateHeatName(): void {
+ this.isLoading = true;
+ let originalName: string = this.selectedModule.name;
+
+ this.selectedModule.updateName();
+ let moduleIndex: number = _.indexOf(this.modules, _.find(this.modules, (module: Module) => {
+ return module.uniqueId === this.selectedModule.uniqueId;
+ }));
+
+ if (moduleIndex !== -1) {
+ this.modules[moduleIndex].name = this.selectedModule.name;
+ this.modulesService.updateModuleMetadata(this.topologyTemplateType, this.workspaceService.metadata.uniqueId, this.modules[moduleIndex]).subscribe(() => {
+ this.isLoading = false;
+ }, () => {
+ this.updateOriginalHeatName(originalName, moduleIndex);
+ this.modules[moduleIndex].name = originalName;
+ });
+ } else {
+ this.updateOriginalHeatName(originalName, moduleIndex);
+ }
+ };
+
+ private updateOriginalHeatName(originalName: string, moduleIndex: number){
+ this.isLoading = false;
+ this.selectedModule.name = originalName;
+ this.selectedModule.heatName = this.selectedModule.name.split('..')[1];
+ }
+
+ openEditPropertyModal(property: PropertyModel): void {
+ this.editPropertyModalTopologyTemplate.setComponentMetadata(this.workspaceService.metadata);
+ this.ModalsHandler.openEditModulePropertyModal(property, this.editPropertyModalTopologyTemplate, this.selectedModule, this.selectedModule.properties).then(() => {
+ });
+ }
+
+ private getKeys(map: Map<any, any>) {
+ return _.keys(map);
+ }
+
+ private toggleSidebarDisplay = () => {
+ // this.withSidebar = !this.withSidebar;
+ this.store.dispatch(new OnSidebarOpenOrCloseAction());
+ }
+
+ public openEditModuleNamePopup($event) {
+ const editModuleNameInstance = this.popoverService.createPopOverWithInnerComponent('Edit Module Name', '', {x:$event.x , y:$event.y }, EditModuleName, {selectModule: _.cloneDeep(this.selectedModule)}, 'top');
+ editModuleNameInstance.innerPopoverContent.instance.clickButtonEvent.subscribe((newHeatName) => {
+ if(newHeatName != null){
+ this.selectedModule.heatName = newHeatName;
+ this.updateHeatName();
+ }
+ editModuleNameInstance.closePopover();
+ })
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.module.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.module.ts
new file mode 100644
index 0000000000..048ca0c65f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.module.ts
@@ -0,0 +1,24 @@
+/**
+ * Created by ob0695 on 6/4/2018.
+ */
+import {NgModule} from "@angular/core";
+import {SdcUiComponentsModule} from "onap-ui-angular";
+import {HierarchyTabComponent} from "./hierarchy-tab.component";
+import {UiElementsModule} from "../../../../../../components/ui/ui-elements.module";
+import {TranslateModule} from "../../../../../../shared/translator/translate.module";
+import {CommonModule} from "@angular/common";
+import {GlobalPipesModule} from "../../../../../../pipes/global-pipes.module";
+import { EditModuleName } from "../edit-module-name/edit-module-name.component";
+
+@NgModule({
+ declarations: [HierarchyTabComponent, EditModuleName],
+ imports: [CommonModule,
+ UiElementsModule,
+ SdcUiComponentsModule,
+ TranslateModule,
+ GlobalPipesModule],
+ entryComponents: [HierarchyTabComponent, EditModuleName],
+ exports: [HierarchyTabComponent, EditModuleName],
+})
+export class HierarchyTabModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.html b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.html
new file mode 100644
index 0000000000..574f2d1bb4
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.html
@@ -0,0 +1,62 @@
+<div class="status-page">
+ <ngx-datatable
+ class="material"
+ [columnMode]="'standard'"
+ [rowHeight]="'auto'"
+ [reorderable]="false"
+ [swapColumns]="false"
+ [rows]="artifacts"
+ [scrollbarH]="true"
+ #statusTable>
+ <ngx-datatable-row-detail [rowHeight]="'auto'">
+ <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template>
+ <div *ngFor="let status of row.statuses">
+ <span class = "status" [attr.data-tests-id]="generateDataTestID('statusTimeStamp_',componentName, row.name, status.status)">{{ status.timeStamp | date:'short':'UTC'}}</span>
+ <span class = "status" [attr.data-tests-id]="generateDataTestID('statusValue_',componentName, row.name, status.status)">{{ status.status }}</span>
+ </div>
+ </ng-template>
+ </ngx-datatable-row-detail>
+ <ngx-datatable-column name="Component ID" [resizeable]="false" [width]="250">
+ <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded" >
+ <div>
+ <span class="urlValue">
+ <svg-icon [clickable]="true" class="expand-collapse-icon"
+ [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'"
+ [size]="'medium'" (click)="expandRow(row)" [attr.data-tests-id]="generateDataTestID('expandIcon_compID_', componentName, row.name)"></svg-icon>
+ </span>
+ <span class="urlValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('compID_',componentName, row.name)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="componentName">
+ {{ componentName }}
+ </span>
+ </div>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false" [width]="280" name="Artifact Name">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <div class = "distributionRowValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('artName_',componentName, row.name)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="row.name">{{ row.name }}</div>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false" [width]="380" name="URL">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <div>
+ <span class="urlValue ellipsisCell" id="urlCell" [attr.data-tests-id]="generateDataTestID('url_',componentName, row.name)">{{ row.url }}</span>
+ <span class="urlCopyIcon" title="Copy URL">
+ <svg-icon-label [clickable]="true" [mode]="'primary'" [labelPlacement]="'right'"
+ [label]="" [name]="'copy-o'" [testId]="'copyToClipboard'"
+ (click)="copyToClipboard(row.url)">
+ </svg-icon-label>
+ </span>
+ </div>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false" [width]="180" name="Time(UTC)">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <div class = "distributionRowValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('time_',componentName, row.name)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="getLatestArtifact(row.name).timeStamp | date:'short':'UTC'">{{ getLatestArtifact(row.name).timeStamp | date:'short':'UTC'}}</div>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false" [width]="280" name="Status">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <div class = "distributionRowValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('status_',componentName, row.name)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="getLatestArtifact(row.name).status">{{ getLatestArtifact(row.name).status }}</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/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.less b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.less
new file mode 100644
index 0000000000..81b8805792
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.less
@@ -0,0 +1,78 @@
+:host ::ng-deep {
+ .ngx-datatable {
+ > div {
+ min-height: 5px;
+ }
+ }
+}
+
+.datatable-header-cell {
+ text-align: left;
+ color: red;
+}
+
+.statusHeaderTable {
+ color: #000000;
+ font-family: OpenSans-Bold, sans-serif;
+ font-size: 12px;
+ font-weight: bold;
+ float: left;
+}
+
+.status {
+ padding-right: 30px;
+ color: #5a5a5a;
+ font-family: OpenSans-Regular, sans-serif;
+ font-size: 12px;
+}
+
+.distributionIDBlock {
+ display: inline-block;
+}
+
+.distributionRowContainer{
+ background-color: #eaeaea;
+ text-align: center;
+}
+
+.distributionRowLabel {
+ overflow: hidden;
+ padding-top: 10px;
+ color: #000000;
+ font-family: OpenSans-Semibold, sans-serif;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.distributionRowValue {
+ color: #263d4d;
+ font-family: OpenSans-Regular, sans-serif;
+ font-size: 14px;
+}
+
+.urlValue {
+ float: left;
+ color: #263d4d;
+ font-family: OpenSans-Regular, sans-serif;
+ font-size: 14px;
+}
+
+.urlCopyIcon {
+ float: right;
+ width: 8%;
+}
+
+.ellipsisCell {
+ width: 92%;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+
+
+
+
+
+
+
diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.spec.ts
new file mode 100644
index 0000000000..72b930b6b8
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.spec.ts
@@ -0,0 +1,90 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture } from '@angular/core/testing';
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import { SdcUiServices } from 'onap-ui-angular';
+import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper';
+import { DistributionService } from '../../distribution.service';
+import { DistributionComponentArtifactTableComponent } from './distribution-component-artifact-table.component';
+
+describe('DistributionComponentArtifactTableComponent', () => {
+ let fixture: ComponentFixture<DistributionComponentArtifactTableComponent>;
+ let distibutionServiceMock: Partial<DistributionService>;
+
+ const mockArtifactsForDistributionAndComponentName = [
+ {
+ name: 'Artifact1',
+ statuses: [
+ {timeStamp: '7/25/2019 12:48AM', status: 'DEPLOY_OK'},
+ {timeStamp: '7/25/2019 12:48AM', status: 'DOWNLOAD_OK'},
+ {timeStamp: '7/25/2019 12:48AM', status: 'NOTIFIED'}
+ ],
+ url: 'URL1',
+ },
+ {
+ name: 'Artifact2',
+ statuses: [
+ {timeStamp: '7/26/2019 12:48AM', status: 'STATUS_TO_DISPLAY'},
+ {timeStamp: '7/25/2019 12:48AM', status: 'DOWNLOAD_OK'},
+ {timeStamp: '7/25/2019 12:48AM', status: 'NOTIFIED'}
+ ],
+ url: 'URL2',
+ },
+ {
+ name: 'ArtifactWithNoStatuses',
+ url: 'URL2',
+ }
+ ];
+
+ beforeEach(() => {
+
+ distibutionServiceMock = {
+ getArtifactstByDistributionIDAndComponentsName: jest.fn().mockReturnValue(mockArtifactsForDistributionAndComponentName),
+ };
+
+ const configure: ConfigureFn = (testBed) => {
+ testBed.configureTestingModule({
+ declarations: [DistributionComponentArtifactTableComponent],
+ imports: [NgxDatatableModule],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: DistributionService, useValue: distibutionServiceMock}
+ ],
+ });
+ };
+
+ configureTests(configure).then((testBed) => {
+ fixture = testBed.createComponent(DistributionComponentArtifactTableComponent);
+ });
+
+ });
+
+ it('Get Latest Artifact (status and timeStamp) - So the Component Table will display the last time stamp of the notification', async () => {
+ await fixture.componentInstance.ngOnInit();
+ expect(fixture.componentInstance.getLatestArtifact('Artifact2')).toEqual({status: 'STATUS_TO_DISPLAY', timeStamp: '7/26/2019 12:48AM'});
+ expect(fixture.componentInstance.getLatestArtifact('ArtifactWithNoStatuses')).toEqual(null);
+ });
+
+ it('Once the Distribution Component Artifact Table Component is created - artifacts will keep the relevant artifacts for a specific distributionID and Component Name', async () => {
+ await fixture.componentInstance.ngOnInit();
+ // tslint:disable:no-string-literal
+ expect(fixture.componentInstance.artifacts.length).toBe(3);
+ expect(fixture.componentInstance.artifacts[0].name).toBe('Artifact1');
+ expect(fixture.componentInstance.artifacts[0].url).toBe('URL1');
+ expect(fixture.componentInstance.artifacts[0].statuses.length).toBe(3);
+
+ expect(fixture.componentInstance.artifacts[1].name).toBe('Artifact2');
+ });
+
+ it('Once the Distribution Component Artifact Table Component is created for Modal- artifacts will keep the relevant artifacts for a ' +
+ 'specific distributionID and Component Name filtered by Status', async () => {
+ fixture.componentInstance.statusFilter = 'DOWNLOAD_OK';
+ await fixture.componentInstance.ngOnInit();
+ expect(fixture.componentInstance.artifacts.length).toBe(3);
+ expect(fixture.componentInstance.artifacts[0].name).toBe('Artifact1');
+ expect(fixture.componentInstance.artifacts[0].url).toBe('URL1');
+
+ expect(fixture.componentInstance.artifacts[0].statuses.length).toBe(1);
+ expect(fixture.componentInstance.artifacts[0].statuses[0]).toEqual({status: 'DOWNLOAD_OK', timeStamp: '7/25/2019 12:48AM'});
+
+ });
+});
diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.ts
new file mode 100644
index 0000000000..af9aef5c64
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.ts
@@ -0,0 +1,68 @@
+import { Component, Input, OnInit, ViewChild } from '@angular/core';
+import * as _ from 'lodash';
+import { DistributionService } from '../../distribution.service';
+
+// tslint:disable:no-string-literal
+
+@Component({
+ selector: 'app-distribution-component-artifact-table',
+ templateUrl: './distribution-component-artifact-table.component.html',
+ styleUrls: ['./distribution-component-artifact-table.component.less']
+})
+export class DistributionComponentArtifactTableComponent implements OnInit {
+
+ @ViewChild('statusTable', {}) table: any;
+
+ @Input() componentName: string;
+ @Input() rowDistributionID: string;
+ @Input() statusFilter: string;
+
+ public artifacts = [];
+
+ constructor(private distributionService: DistributionService) {
+ }
+
+ ngOnInit() {
+ this.artifacts = this.distributionService.getArtifactstByDistributionIDAndComponentsName(this.rowDistributionID, this.componentName);
+ if (this.statusFilter) {
+ this.artifacts.forEach(
+ (artifact) => {
+ artifact.statuses = _.filter(artifact.statuses, {status: this.statusFilter});
+ });
+ }
+ }
+
+ public getLatestArtifact(artifactName: string) {
+ const selectedArtifact = this.artifacts.filter((artifact) => artifact.name === artifactName);
+ if (selectedArtifact && selectedArtifact[0] && selectedArtifact[0]['statuses'] && selectedArtifact[0]['statuses'][0]) {
+ return selectedArtifact[0]['statuses'][0];
+ } else {
+ return null;
+ }
+ }
+
+ private copyToClipboard(urlToCopy: any) {
+
+ const inputForCopyToClipboard = document.getElementById('inputForCopyToClipboard') as HTMLInputElement;
+ inputForCopyToClipboard.value = urlToCopy;
+ /* Select the text field */
+ inputForCopyToClipboard.select();
+
+ /* Copy the text inside the text field */
+ document.execCommand('copy');
+
+ }
+
+ private generateDataTestID(preFix: string, componentName: string, artifactName: string, status?: string) {
+ if (!status) {
+ return preFix + componentName + '_' + artifactName;
+ } else {
+ return preFix + status + '_' + componentName + '_' + artifactName;
+ }
+ }
+
+ private expandRow(row: any) {
+ this.table.rowDetail.toggleExpandRow(row);
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.html b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.html
new file mode 100644
index 0000000000..fa5a9ad7fb
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.html
@@ -0,0 +1,47 @@
+<div >
+ <div class="distributionSummary" *ngIf="!isModal">
+ <span class= "rightVerticalSeperator titleSummaryFontSettings" data-tests-id="totalDistributionArtifactsLabel">Total Artifacts <span class="blue" data-tests-id="totalDistributionArtifactsValue">{{ getTotalArtifactsForDistributionID(rowDistributionID) }} </span></span>
+ <span class="blue rightVerticalSeperator" (click)="openModal(rowDistributionID,'NOTIFIED')" data-tests-id="totalDistributionNotifiedArtifactsLabel">Notified <span data-tests-id="totalDistributionNotifiedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'NOTIFIED') }}</span></span>
+ <span class="blue rightVerticalSeperator" (click)="openModal(rowDistributionID,'DOWNLOAD_OK')" data-tests-id="totalDistributionDownloadedArtifactsLabel">Downloaded <span data-tests-id="totalDistributionDownloadedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DOWNLOAD_OK') }}</span></span>
+ <span class="blue rightVerticalSeperator" (click)="openModal(rowDistributionID,'DEPLOY_OK')" data-tests-id="totalDistributionDeployedArtifactsLabel">Deployed <span data-tests-id="totalDistributionDeployedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DEPLOY_OK') }}</span></span>
+ <span class="blue rightVerticalSeperator" (click)="openModal(rowDistributionID,'NOT_NOTIFIED')" data-tests-id="totalDistributionNotNotifiedArtifactsLabel">Not Notified <span data-tests-id="totalDistributionNotNotifiedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'NOT_NOTIFIED') }}</span></span>
+ <span class="blue rightVerticalSeperator floatRight" (click)="openModal(rowDistributionID,'DEPLOY_ERROR')" data-tests-id="totalDistributionDeployErrorArtifactsLabel">Deploy Errors <span class="red" data-tests-id="totalDistributionDeployErrorArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DEPLOY_ERROR') }}</span></span>
+ <span class="blue rightVerticalSeperator floatRight" (click)="openModal(rowDistributionID,'DOWNLOAD_ERROR ')" data-tests-id="totalDistributionDownloadErrorArtifactsLabel">Download Errors <span class="red" data-tests-id="totalDistributionDownloadErrorArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DOWNLOAD_ERROR') }}</span></span>
+ </div>
+
+ <div class="distributionSummary" *ngIf="isModal">
+ <span data-tests-id="modalStatusLabel"><a>Status {{ statusFilter }} <span class="blue" data-tests-id="statusValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, statusFilter) }}</span></a></span>
+ </div>
+
+
+ <div class="componentShiftLeft" *ngFor="let component of components">
+ <div class="componentSummary" *ngIf="!isModal">
+ <svg-icon [clickable]="true" class="expand-collapse-icon"
+ [name]="isExpanded(component) ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'"
+ [size]="'medium'" [attr.data-tests-id]="generateExpandDataTestID(component)" (click)="expandRow(component)"></svg-icon>
+
+
+ <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, '')">{{ component }} <span class="blue" data-tests-id="totalComponentArtifactsValue">{{ getTotalArtifactsForDistributionID(rowDistributionID, component) }}</span></span>
+ <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'Notified')">Notified <span class="blue" data-tests-id="totalComponentNotifiedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'NOTIFIED', component) }}</span></span>
+ <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'Downloaded')">Downloaded <span class="blue" data-tests-id="totalComponentDownloadedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DOWNLOAD_OK', component) }}</span></span>
+ <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'Deployed')">Deployed <span class="blue" data-tests-id="totalComponentDeployedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DEPLOY_OK', component) }}</span></span>
+ <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'NotNotified')">Not Notified <span class="blue" data-tests-id="totalComponentNotNotifiedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'NOT_NOTIFIED', component) }}</span></span>
+ <span class="msoStatus" [ngClass]="{'red': getMSOStatus (rowDistributionID, component) === 'COMPONENT_DONE_ERROR', 'green': getMSOStatus (rowDistributionID, component) === 'COMPONENT_DONE_OK'}">{{ getMSOStatus (rowDistributionID, component) }}</span>
+ <span class="rightVerticalSeperatorComponent floatRight titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'DeployErrors')">Deploy Errors <span class="red" data-tests-id="totalComponentDeployErrorArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DEPLOY_ERROR', component) }}</span></span>
+ <span class="rightVerticalSeperatorComponent floatRight titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'DownloadErrors')">Download Errors <span class="red" data-tests-id="totalComponentDownloadErrorArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DOWNLOAD_ERROR', component) }}</span></span>
+ </div>
+
+ <div class="componentSummary" *ngIf="isModal">
+ <svg-icon [clickable]="true" class="expand-collapse-icon"
+ [name]="isExpanded(component) ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'"
+ [size]="'medium'" [attr.data-tests-id]="generateExpandDataTestID(component+'_ForModal')" (click)="expandRow(component)"></svg-icon>
+ <span data-tests-id="modalComponentLabel"><a>{{ component }} <span class="blue" data-tests-id="modalComponentValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, statusFilter, component) }} </span></a></span>
+ </div>
+
+ <div *ngIf="isExpanded(component)">
+ <app-distribution-component-artifact-table [rowDistributionID]= rowDistributionID [componentName]=component
+ [statusFilter]="statusFilter"></app-distribution-component-artifact-table>
+ </div>
+ </div>
+</div>
+
diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.less b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.less
new file mode 100644
index 0000000000..3eab18ca14
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.less
@@ -0,0 +1,66 @@
+.red {
+ color: red;
+}
+
+.green {
+ color: green;
+}
+
+.msoStatus {
+ padding-left: 5px;
+}
+
+.blue {
+ color: #009fdb;
+ font-family: OpenSans-Semibold, sans-serif;
+ font-size: 14px;
+}
+
+.rightVerticalSeperator {
+ border-right: 1px solid #d2d2d2;
+ padding-left: 5px;
+ padding-right: 5px;
+ cursor: pointer;
+}
+
+.rightVerticalSeperatorComponent {
+ border-right: 1px solid #d2d2d2;
+ padding-left: 5px;
+ padding-right: 5px;
+}
+
+.floatRight{
+ float: right;
+}
+
+.distributionSummary {
+ padding-top: 5px;
+ padding-bottom: 5px;
+ background-color: #eaeaea;
+ padding-left: 25px;
+ padding-right: 25px;
+}
+
+.componentSummary {
+ margin-top: 5px;
+ margin-bottom: 5px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ background-color: #eaeaea;
+ padding-left: 25px;
+ padding-right: 25px;
+}
+
+.componentShiftLeft {
+ margin-left: 15px;
+}
+
+.titleSummaryFontSettings {
+ color: #191919;
+ font-family: OpenSans-Regular, sans-serif;
+ font-size: 14px;
+}
+
+
+
+
diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.spec.ts
new file mode 100644
index 0000000000..ff89b92fd8
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.spec.ts
@@ -0,0 +1,47 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture } from '@angular/core/testing';
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import { SdcUiServices } from 'onap-ui-angular';
+import { ConfigureFn, configureTests } from '../../../../../../jest/test-config.helper';
+import { DistributionService } from '../distribution.service';
+import { DistributionComponentTableComponent } from './distribution-component-table.component';
+
+describe('DistributionComponentTableComponent', () => {
+ let fixture: ComponentFixture<DistributionComponentTableComponent>;
+ let distibutionServiceMock: Partial<DistributionService>;
+
+ const mockComponentsForDistribution = ['Consumer1', 'Consumer2'];
+
+ beforeEach(() => {
+
+ distibutionServiceMock = {
+ getComponentsByDistributionID: jest.fn().mockReturnValue(mockComponentsForDistribution),
+ getArtifactstByDistributionIDAndComponentsName: jest.fn(),
+ getArtifactsForDistributionIDAndComponentByStatus: jest.fn()
+ };
+
+ const configure: ConfigureFn = (testBed) => {
+ testBed.configureTestingModule({
+ declarations: [DistributionComponentTableComponent],
+ imports: [NgxDatatableModule],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: DistributionService, useValue: distibutionServiceMock},
+ {provide: SdcUiServices.ModalService, useValue: {}}
+ ],
+ });
+ };
+
+ configureTests(configure).then((testBed) => {
+ fixture = testBed.createComponent(DistributionComponentTableComponent);
+ });
+
+ });
+
+ it('Once the Distribution Component Table Component is created - components will keep the relevant components for a specific distributionID', async () => {
+ await fixture.componentInstance.ngOnInit();
+ expect(fixture.componentInstance.components.length).toBe(2);
+ expect(fixture.componentInstance.components[0]).toBe('Consumer1');
+ expect(fixture.componentInstance.components[1]).toBe('Consumer2');
+ });
+});
diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.ts
new file mode 100644
index 0000000000..e3aaf9d639
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.ts
@@ -0,0 +1,104 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular';
+import { ModalComponent } from 'onap-ui-angular/dist/modals/modal.component';
+import { DistributionComponent } from '../distribution.component';
+import { DistributionService } from '../distribution.service';
+
+@Component({
+ selector: 'app-distribution-component-table',
+ templateUrl: './distribution-component-table.component.html',
+ styleUrls: ['./distribution-component-table.component.less']
+})
+export class DistributionComponentTableComponent implements OnInit {
+
+ @Input() rowDistributionID: string;
+ @Input() isModal: boolean = false;
+ @Input() statusFilter: string;
+ public components = [];
+ private customModalInstance: ModalComponent;
+ private expanded = [];
+ constructor(private distributionService: DistributionService,
+ private modalService: SdcUiServices.ModalService) {
+ }
+
+ ngOnInit() {
+ this.initComponents();
+ }
+
+ private generateTotalComponentArtifactsLabel(componentName: any, status: string): string {
+ return 'total' + componentName + status + 'ArtifactsLabel';
+ }
+
+ private generateExpandDataTestID(componentName: string) {
+ return 'expandIcon_' + componentName;
+ }
+
+ private initComponents() {
+ this.components = this.distributionService.getComponentsByDistributionID(this.rowDistributionID);
+ this.components.map((component) => this.expanded.push({componentName: component, expanded: false}));
+ }
+
+ private getTotalArtifactsForDistributionID(distributionID: string, componentName?: string): number {
+ return this.distributionService.getArtifactstByDistributionIDAndComponentsName(distributionID, componentName).length;
+ }
+
+ private getLengthArtifactsForDistributionIDByStatus(distributionID: string, statusToSerach: string, componentName?: string): number {
+ if (componentName) {
+ return this.distributionService.getArtifactsForDistributionIDAndComponentByStatus(distributionID, statusToSerach, componentName).length;
+ } else {
+ return this.distributionService.getArtifactsForDistributionIDAndComponentByStatus(distributionID, statusToSerach).length;
+ }
+ }
+
+ private openModal(rowDistributionID: string, statusFilter: string) {
+
+ const title: string = 'Distribution by Status';
+ const attributeModalConfig = {
+ title,
+ size: 'sdc-xl',
+ type: SdcUiCommon.ModalType.custom,
+ buttons: [
+ {
+ id: 'close',
+ text: 'Close',
+ size: 'sm',
+ closeModal: true,
+ disabled: false,
+ }
+ ] as SdcUiCommon.IModalButtonComponent[]
+ };
+
+ this.customModalInstance = this.modalService.openCustomModal(attributeModalConfig, DistributionComponent, {
+ // inputs
+ rowDistributionID,
+ statusFilter,
+ isModal: true,
+ });
+ }
+
+ private expandRow(componentName: string) {
+ console.log('Should expand componentSummary for componentName = ' + componentName);
+ const selectedComponent = this.expanded.find((component) => component.componentName === componentName);
+ // tslint:disable:no-string-literal
+ const selectedComponentExpandedVal = selectedComponent['expanded'];
+ // this.expanded = !this.expanded;
+ for (const i in this.expanded) {
+ if (this.expanded[i].componentName === componentName) {
+ this.expanded[i].expanded = !this.expanded[i].expanded;
+ break; //Stop this loop, we found it!
+ }
+ }
+ const selectedComponentAfter = this.expanded.find((component) => component.componentName === componentName);
+ const selectedComponentExpandedValAfter = selectedComponentAfter['expanded'];
+ }
+
+ private isExpanded(componentName: string) {
+ const selectedComponent = this.expanded.find((component) => component.componentName === componentName);
+ return selectedComponent['expanded'];
+ }
+
+
+ private getMSOStatus(rowDistributionID: string, componentName: string): string {
+ return this.distributionService.getMSOStatus(rowDistributionID, componentName);
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.html b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.html
new file mode 100644
index 0000000000..d0cacb054e
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.html
@@ -0,0 +1,80 @@
+<div *ngIf="!isModal">
+ <div *ngIf="serviceHasDistibutions" class="w-sdc-distribution-view-header">
+ <div class="w-sdc-distribution-view-title" data-tests-id="DistributionsLabel">DISTRIBUTION <span class="blue-font" data-tests-id="totalArtifacts">[{{distributions.length}}]</span></div>
+ <div class="header-spacer"></div>
+ <input type="text" value="GeeksForGeeks" id="inputForCopyToClipboard" [ngStyle]="{'z-index': '-2', 'width': '25px'}">
+ <div class="top-search">
+ <input type="text"
+ style="width: auto;"
+ class="search-text"
+ data-tests-id="searchTextbox"
+ placeholder="Search"
+ data-ng-model="searchBind"
+ ng-model-options="{ debounce: 500 }"
+ (keyup)="updateFilter($event)"/>
+ </div>
+ <div class="sprite-new refresh-btn" data-tests-id="refreshButton" (click)="refreshDistributions()" title="Refresh"></div>
+ </div>
+ <div class="w-sdc-distribution-view-header w-sdc-distribution-view-title" data-tests-id="noDistributionsLabel" *ngIf="!serviceHasDistibutions">No Distributions To Present</div>
+</div>
+
+<div *ngIf="serviceHasDistibutions">
+ <ngx-datatable
+ [columnMode]="'flex'"
+ [rowHeight]="'auto'"
+ [reorderable]="false"
+ [swapColumns]="false"
+ [scrollbarV]="false"
+ [rows]="distributions"
+ [sorts]="[{prop: 'timestamp', dir: 'desc'}]"
+
+ #distributionTable>
+ <ngx-datatable-row-detail [rowHeight]="'auto'">
+ <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template>
+ <app-distribution-component-table [rowDistributionID]=row.distributionID [isModal]="isModal"
+ [statusFilter]="statusFilter"></app-distribution-component-table>
+ </ng-template>
+ </ngx-datatable-row-detail>
+ <ngx-datatable-column [resizeable]="false" [flexGrow]="2" name="Distribution ID">
+ <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded" >
+ <div class="expand-collapse-cell">
+ <a><svg-icon [clickable]="true" class="expand-collapse-icon"
+ [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'"
+ [size]="'medium'" (click)="expandRow(row, expanded)" [attr.data-tests-id]="generateDataTestID('expandIcon_', row.distributionID, isModal)"></svg-icon></a>
+
+ </div>
+ <div class="distributionIDBlock">
+ <div class = "distributionRowValue" [attr.data-tests-id]="generateDataTestID('distID_', row.distributionID, isModal)">{{ row.distributionID }}</div>
+ </div>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false" [flexGrow]="1" name="User id">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <div class = "distributionRowValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('userID_', row.distributionID)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="row.userId">{{ row.userId }}</div>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false" [flexGrow]="1" name="Time[UTC]">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <div class = "distributionRowValue" [attr.data-tests-id]="generateDataTestID('timeStamp_', row.distributionID)">{{ row.timestamp }} </div>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false"[flexGrow]="1" name="Status" >
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <div>
+ <span class="statusIcon">
+ <svg-icon [clickable]="true" class="expand-collapse-icon"
+ [name]= "getIconName(row.deployementStatus)" [mode]="'primary'"
+ [size]="'medium'"></svg-icon>
+ </span>
+ <span class = "distributionRowValue" [attr.data-tests-id]="generateDataTestID('status_', row.distributionID)">
+ {{ row.deployementStatus }}
+ </span>
+ <span class="btnMarkAsDistributed" (click)="markDeploy(row.distributionID, row.deployementStatus)">
+ <svg-icon [clickable]="true" [name]= "'success'" [mode]="getIconMode(row.deployementStatus)"
+ [size]="'medium'"></svg-icon>
+ </span>
+ </div>
+ </ng-template>
+ </ngx-datatable-column>
+ </ngx-datatable>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.less b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.less
new file mode 100644
index 0000000000..b630881fdc
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.less
@@ -0,0 +1,92 @@
+:host ::ng-deep {
+ .ngx-datatable {
+ > div {
+ min-height: 500px;
+ datatable-body {
+ max-height: max-content;
+ }
+ }
+ }
+}
+
+.w-sdc-distribution-view-header {
+ display: flex;
+ -webkit-justify-content: space-between;
+ margin: 0 25px 5px 40px;
+
+ .header-spacer {
+ flex-grow: 5;
+ }
+
+ .w-sdc-distribution-view-title{
+ color: #191919;
+ font-family: OpenSans-Regular, sans-serif;
+ font-size: 14px;
+ line-height: 30px;
+
+
+ .blue-font {
+ color: #009fdb;
+ font-family: OpenSans-Semibold, sans-serif;
+ font-size: 14px;
+ }
+ }
+
+}
+
+.distribution-page {
+ max-height: 150px;
+}
+
+ .distributionIDBlock {
+ display: inline-block;
+ }
+
+ .expand-collapse-cell {
+ display: inline-block;
+ }
+
+ .statusIcon {
+ display: inline-block;
+ margin-right: 10px;
+ }
+
+ .btnMarkAsDistributed {
+ float: right;
+ background-color: #E5F3FF;
+ border: 1px solid #8DCCD5;
+ width: 55px;
+ height: 21px;
+ text-align: center;
+ }
+
+ .distributionRowContainer{
+ background-color: #eaeaea;
+ text-align: center;
+ }
+
+ .distributionRowLabel {
+ overflow: hidden;
+ padding-top: 10px;
+ color: #000000;
+ font-family: OpenSans-Semibold, sans-serif;
+ font-size: 12px;
+ font-weight: bold;
+ }
+
+ .distributionRowValue {
+ color: #263d4d;
+ font-family: OpenSans-Regular, sans-serif;
+ font-size: 14px;
+ }
+
+.ellipsisCell {
+ width: 92%;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+
+
+
diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.spec.ts
new file mode 100644
index 0000000000..e6c9c239e1
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.spec.ts
@@ -0,0 +1,92 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture } from '@angular/core/testing';
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import { SdcUiServices } from 'onap-ui-angular';
+import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper';
+import { ComponentMetadata } from '../../../../models/component-metadata';
+import { AuthenticationService } from '../../../services/authentication.service';
+import { WorkspaceService } from '../workspace.service';
+import { DistributionComponent } from './distribution.component';
+import { DistributionService } from './distribution.service';
+import {EventListenerService} from "../../../../services/event-listener-service";
+
+describe('DistributionComponent', () => {
+ let fixture: ComponentFixture<DistributionComponent>;
+ let distibutionServiceMock: Partial<DistributionService>;
+ let workspaceServiceMock: Partial<WorkspaceService>;
+ let loaderServiceMock: Partial<SdcUiServices.LoaderService>;
+ let authenticationServiceMock: Partial <AuthenticationService>;
+ let eventListenerService: Partial <EventListenerService>;
+
+ const mockDistributionListFromService = [
+ {
+ deployementStatus: 'Distributed',
+ distributionID: '1',
+ timestamp: '2019-07-21 08:37:02.834 UTC',
+ userId: 'Aretha Franklin(op0001)'
+ }, {
+ deployementStatus: 'Distributed',
+ distributionID: '2',
+ timestamp: '2019-07-21 09:37:02.834 UTC',
+ userId: 'Aretha Franklin(op0001)'
+ }];
+
+ beforeEach(() => {
+
+ distibutionServiceMock = {
+ initDistributionsList: jest.fn(),
+ getDistributionList: jest.fn().mockReturnValue(mockDistributionListFromService),
+ initDistributionsStatusForDistributionID: jest.fn()
+ };
+
+ const componentMetadata = new ComponentMetadata();
+ componentMetadata.uuid = '111';
+
+ workspaceServiceMock = {
+ metadata : componentMetadata
+ };
+
+ authenticationServiceMock = {
+ getLoggedinUser: jest.fn().mockReturnValue({role: 'designer'})
+ };
+
+ eventListenerService = {
+ registerObserverCallback: jest.fn(),
+ unRegisterObserver: jest.fn()
+ }
+
+ loaderServiceMock = {
+ activate: jest.fn(),
+ deactivate: jest.fn()
+ };
+
+ const configure: ConfigureFn = (testBed) => {
+ testBed.configureTestingModule({
+ declarations: [DistributionComponent],
+ imports: [NgxDatatableModule],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: DistributionService, useValue: distibutionServiceMock},
+ {provide: WorkspaceService, useValue: workspaceServiceMock},
+ {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock},
+ {provide: AuthenticationService, useValue: authenticationServiceMock},
+ {provide: EventListenerService, useValue: eventListenerService}
+ ],
+ });
+ };
+
+ configureTests(configure).then((testBed) => {
+ fixture = testBed.createComponent(DistributionComponent);
+ });
+
+ });
+
+ it('Once the Distribution Component is created - distributionsResponseFromServer save all the distributions from the Service', async () => {
+ fixture.componentInstance.componentUuid = 'componentUid';
+ fixture.componentInstance.rowDistributionID = null;
+ fixture.componentInstance.isModal = false;
+
+ await fixture.componentInstance.ngOnInit();
+ expect(fixture.componentInstance.distributions.length).toBe(2);
+ });
+});
diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.ts
new file mode 100644
index 0000000000..ca1b6292d3
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.ts
@@ -0,0 +1,117 @@
+import { Component, Input, OnInit, ViewChild } from '@angular/core';
+import { SdcUiCommon, SdcUiServices } from 'onap-ui-angular';
+import { EventListenerService } from '../../../../services/event-listener-service';
+import { AuthenticationService } from '../../../services/authentication.service';
+import { WorkspaceService } from '../workspace.service';
+import { DistributionService } from './distribution.service';
+import { EVENTS } from '../../../../utils/constants';
+
+@Component({
+ selector: 'distribution',
+ templateUrl: './distribution.component.html',
+ styleUrls: ['../../../../../assets/styles/table-style.less', './distribution.component.less']
+})
+export class DistributionComponent implements OnInit {
+
+ @ViewChild('distributionTable', { }) table: any;
+
+ @Input() isModal: boolean = false;
+ @Input() statusFilter: string;
+ @Input() rowDistributionID: string;
+ public componentUuid: string;
+ public distributions = [];
+ private expanded: any = {};
+ private serviceHasDistibutions: boolean = false;
+ private readonly uniqueId: string;
+ private userRole: string;
+
+ constructor(private workspaceService: WorkspaceService,
+ private distributionService: DistributionService,
+ private loaderService: SdcUiServices.LoaderService,
+ private authService: AuthenticationService,
+ private eventListenerService: EventListenerService) {
+ this.componentUuid = this.workspaceService.metadata.uuid;
+ this.uniqueId = this.workspaceService.metadata.uniqueId;
+ }
+
+
+
+ async ngOnInit() {
+ this.userRole = this.authService.getLoggedinUser().role;
+ this.eventListenerService.registerObserverCallback(EVENTS.ON_DISTRIBUTION_SUCCESS, async () => {
+ await this.refreshDistributions();
+ });
+ await this.initDistributions(this.componentUuid, this.rowDistributionID);
+ }
+
+ ngOnDestroy(): void {
+ this.eventListenerService.unRegisterObserver(EVENTS.ON_DISTRIBUTION_SUCCESS);
+ }
+
+ async initDistributions(componentUuid: string, specificDistributionID?: string) {
+ this.loaderService.activate();
+ await this.distributionService.initDistributionsList(componentUuid);
+ this.distributions = this.distributionService.getDistributionList();
+ this.distributions.length > 0 ? this.serviceHasDistibutions = true : this.serviceHasDistibutions = false;
+ if (specificDistributionID) {
+ this.distributions = this.distributionService.getDistributionList(specificDistributionID);
+ }
+ this.loaderService.deactivate();
+ }
+
+ getIconName(rowStatus: string ) {
+ if (rowStatus === 'Distributed') {
+ return 'distributed';
+ }
+ if (rowStatus === 'Deployed') {
+ return 'v-circle';
+ }
+ }
+
+ getIconMode(rowStatus: string) {
+ if (rowStatus === 'Distributed') {
+ return 'primary';
+ }
+ if (rowStatus === 'Deployed') {
+ return 'secondary';
+ }
+ }
+
+ private async markDeploy(distributionID: string, status: string) {
+ if (status === 'Distributed') {
+ console.log('Should send MarkDeploy POST Request ServiceID:' + this.uniqueId + ' DISTID:' + distributionID);
+ await this.distributionService.markDeploy(this.uniqueId, distributionID);
+ this.refreshDistributions();
+ }
+ }
+
+ private async refreshDistributions() {
+ await this.initDistributions(this.componentUuid);
+ }
+
+ private updateFilter(event) {
+ const val = event.target.value.toLowerCase();
+
+ // filter our data
+ this.distributions = this.distributionService.getDistributionList().filter((distribution: any[]) => {
+ return !val ||
+ // tslint:disable:no-string-literal
+ distribution['distributionID'].toLowerCase().indexOf(val) !== -1;
+ });
+ }
+
+ private generateDataTestID(preFix: string, distributionID: string, isModal?: boolean ): string {
+ if (isModal) {
+ return preFix + distributionID.substring(0, 5) + '_Modal';
+ } else {
+ return preFix + distributionID.substring(0, 5);
+ }
+ }
+
+ private async expandRow(row: any, expanded: boolean) {
+ if (!expanded) {
+ await this.distributionService.initDistributionsStatusForDistributionID(row.distributionID);
+ }
+ this.table.rowDetail.toggleExpandRow(row);
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.module.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.module.ts
new file mode 100644
index 0000000000..723a6d8c0a
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.module.ts
@@ -0,0 +1,34 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { NgxDatatableModule } from '@swimlane/ngx-datatable';
+import { SdcUiComponentsModule } from 'onap-ui-angular';
+import { DistributionComponentArtifactTableComponent } from './distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component';
+import { DistributionComponentTableComponent } from './distribution-component-table/distribution-component-table.component';
+import { DistributionComponent } from './distribution.component';
+import { DistributionService } from './distribution.service';
+
+@NgModule({
+ declarations: [
+ DistributionComponent,
+ DistributionComponentTableComponent,
+ DistributionComponentArtifactTableComponent,
+ ],
+ imports: [
+ // TranslateModule,
+ CommonModule,
+ SdcUiComponentsModule,
+ NgxDatatableModule,
+ ],
+ exports: [
+ DistributionComponent,
+ DistributionComponentTableComponent
+ ],
+ entryComponents: [
+ DistributionComponent,
+ DistributionComponentTableComponent,
+ DistributionComponentArtifactTableComponent
+ ],
+ providers: [DistributionService]
+})
+export class DistributionModule {
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.service.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.service.ts
new file mode 100644
index 0000000000..ed6791c5c1
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.service.ts
@@ -0,0 +1,233 @@
+import { HttpClient } from '@angular/common/http';
+import { Inject, Injectable } from '@angular/core';
+import { tap } from 'rxjs/operators';
+import { Distribution } from '../../../../models/distribution';
+import { ISdcConfig, SdcConfigToken } from '../../../config/sdc-config.config';
+
+@Injectable()
+export class DistributionService {
+ protected baseUrl;
+ private distributionList = [];
+ private distributionStatusesMap = {};
+
+ // tslint:disable:no-string-literal
+
+ constructor(protected http: HttpClient, @Inject(SdcConfigToken) sdcConfig: ISdcConfig) {
+ this.baseUrl = sdcConfig.api.root + sdcConfig.api.component_api_root;
+ }
+
+ // Once the distribution page is loaded or when the user wants to refresh the list
+ async initDistributionsList(componentUuid: string): Promise<object> {
+ const distributionsListURL = this.baseUrl + 'services/' + componentUuid + '/distribution';
+ const res = this.http.get<Distribution[]>(distributionsListURL).pipe(tap( (result) => {
+ this.distributionList = result['distributionStatusOfServiceList'];
+ this.insertDistrbutionsToMap();
+ } ));
+ return res.toPromise();
+ }
+
+ // Once the user click on the relevant distribution ID in the distribution table (open and close)
+ async initDistributionsStatusForDistributionID(distributionID: string): Promise<object> {
+ const distributionStatus = this.baseUrl + 'services/distribution/' + distributionID;
+ const res = this.http.get<object>(distributionStatus).pipe(tap( (result) => {
+ this.insertDistributionStatusToDistributionsMap(distributionID, result['distributionStatusList']);
+ } ));
+ return res.toPromise();
+ }
+
+ public getDistributionList(specificDistributionID?: string) {
+ if (specificDistributionID) {
+ return this.distributionList.filter((distribution) => {
+ return distribution['distributionID'] === specificDistributionID;
+ });
+ } else {
+ return this.distributionList;
+ }
+ }
+
+ public getComponentsByDistributionID(distributionID: string) {
+ const components = [];
+ const distributionStatusMap = this.getStatusMapForDistributionID(distributionID);
+ if (distributionStatusMap) {
+ distributionStatusMap.forEach((component) => components.push(component.componentID));
+ }
+ return components;
+ }
+
+ // get array of artifacts per distributionID w/o componentName, sliced by artifact status
+ public getArtifactsForDistributionIDAndComponentByStatus(distributionID: string, statusToSearch: string, componentName?: string) {
+ const filteredArtifactsByStatus = [];
+
+ if (componentName) {
+ this.getArtifactstByDistributionIDAndComponentsName(distributionID, componentName).forEach ( (artifact) => {
+ if (this.artifactStatusHasMatch(artifact, statusToSearch)) {
+ filteredArtifactsByStatus.push(artifact);
+ }
+ } );
+ } else {
+ this.getArtifactstByDistributionIDAndComponentsName(distributionID).forEach ( (artifact) => {
+ if (this.artifactStatusHasMatch(artifact, statusToSearch)) {
+ filteredArtifactsByStatus.push(artifact);
+ }
+ } );
+ }
+ return filteredArtifactsByStatus;
+ }
+
+ public getArtifactstByDistributionIDAndComponentsName(distributionID: string, componentName?: string): any[] {
+ const artifacts = [];
+ if (this.getStatusMapForDistributionID(distributionID)) {
+ if (componentName) {
+ if (this.getStatusMapForDistributionID(distributionID).filter((component) => component.componentID === componentName).length > 0) {
+ const artifactsArr = this.getStatusMapForDistributionID(distributionID).filter((component) => component.componentID === componentName)[0]['artifacts']
+ if (artifactsArr.length > 0) {
+ artifactsArr.forEach((artifact) => {
+ const artifactObj = {
+ url: artifact.artifactUrl,
+ name: artifact.artifactName,
+ statuses: artifact.statuses
+ };
+ artifacts.push(artifactObj);
+ });
+ }
+ }
+ } else {
+ const components = this.getComponentsByDistributionID(distributionID);
+ components.forEach((componentName) => {
+ if (this.getStatusMapForDistributionID(distributionID).filter((component) => component.componentID === componentName).length > 0) {
+ const artifactsArr = this.getStatusMapForDistributionID(distributionID).filter((component) => component.componentID === componentName)[0]['artifacts']
+ if (artifactsArr.length > 0) {
+ artifactsArr.forEach((artifact) => {
+ const artifactObj = {
+ url: artifact.artifactUrl,
+ name: artifact.artifactName,
+ statuses: artifact.statuses
+ };
+ artifacts.push(artifactObj);
+ });
+ }
+ }
+ });
+ }
+ }
+ return artifacts;
+ }
+
+ public getStatusMapForDistributionID(distributionID: string) {
+ return this.distributionStatusesMap[distributionID];
+ }
+
+ public markDeploy(uniqueId: string, distributionID: string): Promise<object> {
+ const distributionStatus = this.baseUrl + 'services/' + uniqueId + '/distribution/' + distributionID + '/markDeployed';
+ const res = this.http.post<object>(distributionStatus, {}).pipe(tap( (result) => {
+ console.log(result);
+ } ));
+ return res.toPromise();
+ }
+
+ public getMSOStatus(distributionID: string, componentName: string): string {
+ const msoStatus = this.distributionStatusesMap[distributionID].filter((component) => component.componentID === componentName)[0].msoStatus;
+ return msoStatus ? msoStatus : '';
+ }
+
+ private artifactStatusHasMatch(artifact: any, statusToSerach: string) {
+ for (let i = 0; i < artifact.statuses.length; i++) {
+ if (artifact.statuses[i].status === statusToSerach) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private insertDistributionStatusToDistributionsMap(distributionID: string, distributionStatusMapResponseFromServer: object[]) {
+
+ // // Clear the Distribution ID array - to avoid statuses duplications
+ const distribution = this.distributionStatusesMap[distributionID];
+ distribution.length = 0;
+
+ // Sort the response of statuses from Server, so it will be easy to pop the latest status when it will be required
+ const sortedResponseByTimeStamp = distributionStatusMapResponseFromServer.sort((a, b) => b['timestamp'] - a['timestamp'])
+
+ sortedResponseByTimeStamp.map((distributionStatus) => {
+ const formattedDate = this.formatDate(distributionStatus['timestamp']);
+
+ // if (distributionStatus['url'] === null) {
+ // distributionStatus['url'] = "";
+ // }
+
+ const detailedArtifactStatus = {
+ componentID: distributionStatus['omfComponentID'],
+ artifactName: distributionStatus['url']? distributionStatus['url'].split('/').pop() : '',
+ url: distributionStatus['url'],
+ time: distributionStatus['timestamp'],
+ status: distributionStatus['status'],
+ };
+
+
+
+ // Add Component to this.distributionStatusesMap in case not exist.
+ let componentPosition = _.findIndex(distribution, {componentID: detailedArtifactStatus.componentID})
+
+ if (componentPosition === -1) {
+ this.addComponentIdToDistributionStatusMap(distributionID, detailedArtifactStatus.componentID);
+ componentPosition = distribution.length - 1;
+ }
+
+ const component = distribution[componentPosition];
+
+
+ // Add Artifact to this.distributionStatusesMap[componentID] in case not exist.
+ let artifactPosition = _.findIndex(component.artifacts, {artifactUrl: detailedArtifactStatus.url})
+
+ if (artifactPosition === -1) {
+ this.addArtifactToComponentId(distributionID, componentPosition, detailedArtifactStatus.artifactName, detailedArtifactStatus.url);
+ artifactPosition = component.artifacts.length - 1;
+ }
+
+
+ // Add status to relevat artifact in relevent componentID.
+ if (detailedArtifactStatus.url) {
+ // Case where there is a url -> should add its status
+ component.artifacts[artifactPosition].statuses.push({
+ timeStamp: detailedArtifactStatus.time,
+ status: detailedArtifactStatus.status
+ });
+ } else {
+ // Should update the Component -> status from MSO
+ this.distributionStatusesMap[distributionID][componentPosition].msoStatus = detailedArtifactStatus.status;
+ }
+
+
+ });
+ }
+
+ private addComponentIdToDistributionStatusMap(distributionID: string, componentIDValue: string) {
+ this.distributionStatusesMap[distributionID].push({
+ componentID: componentIDValue,
+ msoStatus: null,
+ artifacts: []
+ });
+ }
+
+ private addArtifactToComponentId(distributionID: string, componentPosition: number, artifactNameValue: string, artifactURLValue: any) {
+ if (artifactNameValue) {
+ this.distributionStatusesMap[distributionID][componentPosition].artifacts.push({
+ artifactName: artifactNameValue,
+ artifactUrl: artifactURLValue,
+ statuses: []
+ });
+ }
+ }
+
+ private insertDistrbutionsToMap() {
+ this.distributionList.map((distribution) => this.distributionStatusesMap[distribution.distributionID] = []);
+ }
+
+ private formatDate(epochTime: string) {
+ const intEpochTime = new Date(parseInt(epochTime, 10));
+ const amOrPm = (intEpochTime.getHours() + 24) % 24 > 12 ? 'PM' : 'AM';
+ const formattedDate = (intEpochTime.getMonth() + 1) + '/' + intEpochTime.getDate() + '/' + intEpochTime.getFullYear() + ' ' + intEpochTime.getHours() + ':' +
+ intEpochTime.getMinutes() + amOrPm;
+ return formattedDate;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/information-artifact/__snapshots__/informational-artifact-page.spec.ts.snap b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/__snapshots__/informational-artifact-page.spec.ts.snap
new file mode 100644
index 0000000000..1a19b36cfb
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/__snapshots__/informational-artifact-page.spec.ts.snap
@@ -0,0 +1,40 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`informational artifacts page should match current snapshot of informational artifact pages component 1`] = `
+<information-artifact-page
+ addOrUpdateArtifact={[Function Function]}
+ artifactsService={[Function Object]}
+ deleteArtifact={[Function Function]}
+ store={[Function Store]}
+ table={[Function DatatableComponent]}
+ workspaceService={[Function Object]}
+>
+ <div
+ class="information-artifact-page"
+ >
+ <svg-icon-label
+ class="add-artifact-btn"
+ />
+ <ngx-datatable
+ class="ngx-datatable"
+ columnmode="flex"
+ >
+ <div
+ visibilityobserver=""
+ >
+
+ <datatable-body
+ class="datatable-body"
+ >
+ <datatable-selection>
+
+
+
+ </datatable-selection>
+ </datatable-body>
+
+ </div>
+ </ngx-datatable>
+ </div>
+</information-artifact-page>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.html b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.html
new file mode 100644
index 0000000000..cff33258ae
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.html
@@ -0,0 +1,82 @@
+<div class="information-artifact-page">
+ <svg-icon-label class="add-artifact-btn" [clickable]="true" [mode]="'primary'" [labelPlacement]="'right'"
+ [label]="'Add'" [name]="'plus'" [testId]="'add-information-artifact-button'"
+ (click)="addOrUpdateArtifact()"></svg-icon-label>
+ <ngx-datatable
+ columnMode="flex"
+ [headerHeight]="40"
+ [reorderable]="false"
+ [swapColumns]="false"
+ [rows]="informationArtifacts$ | async"
+ [footerHeight]="'undefined'"
+ [sorts]="[{prop: 'artifactDisplayName', dir: 'desc'}]"
+ #informationArtifactsTable
+ (activate)="onActivate($event)">
+ <ngx-datatable-row-detail [rowHeight]="80">
+ <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template>
+ <div [attr.data-tests-id]="row.artifactDisplayName+'Description'">{{row.description}}</div>
+ </ng-template>
+ </ngx-datatable-row-detail>
+ <ngx-datatable-column [resizeable]="false" name="Name" [flexGrow]="3"
+ [prop]="'artifactDisplayName'">
+ <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 [attr.data-tests-id]="'artifactDisplayName_' + row.artifactDisplayName">{{row.artifactDisplayName }}</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.artifactType}}
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false" name="Version" [flexGrow]="1">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ {{ row.artifactVersion }}
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false" name="UUID" [flexGrow]="2">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ {{ row.artifactUUID }}
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false" [flexGrow]="1">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <div class="download-artifact-button">
+ <svg-icon class="action-icon" *ngIf="!row.isThirdParty()" [mode]="'primary2'"
+ [disabled]="isViewOnly$ | async" [name]="'edit-o'"
+ testId="edit_{{row.artifactDisplayName}}" clickable="true" size="medium"
+ (click)="addOrUpdateArtifact(row)"></svg-icon>
+ <svg-icon class="action-icon" *ngIf="!row.isThirdParty()" [mode]="'primary2'"
+ [disabled]="isViewOnly$ | async" [name]="'trash-o'"
+ testId="delete_{{row.artifactDisplayName}}" clickable="true" size="medium" (click)="deleteArtifact(row)"></svg-icon>
+ <download-artifact class="action-icon" [disabled]="isViewOnly$ | async" [artifact]="row"
+ [componentId]="componentId"
+ [componentType]="componentType"
+ testId="download_{{row.artifactDisplayName}}"></download-artifact>
+ </div>
+ </ng-template>
+ </ngx-datatable-column>
+
+ <ngx-datatable-footer>
+ <ng-template ngx-datatable-footer-template>
+ <div class="add-artifacts-dynamic-btn-list">
+ <sdc-button *ngFor="let artifact of informationArtifactsAsButtons$ | async"
+ class="add-artifacts-dynamic-btn"
+ testId="add_artifact_{{artifact.artifactDisplayName}}"
+ [type]="'secondary'"
+ [size]="'xx-large'"
+ [text]="'ADD ' + artifact.artifactDisplayName"
+ [icon_name]="'plus-circle-o'"
+ [icon_mode] = "'secondary'"
+ [icon_position]="'left'"
+ (click)="addOrUpdateArtifact(artifact)">
+ </sdc-button>
+ </div>
+ </ng-template>
+ </ngx-datatable-footer>
+ </ngx-datatable>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.less b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.less
new file mode 100644
index 0000000000..b69e511f70
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.less
@@ -0,0 +1,29 @@
+.information-artifact-page {
+
+ .add-artifact-btn {
+ display: flex;
+ cursor: pointer;
+ justify-content: flex-end;
+ margin-bottom: 10px;
+ }
+ .download-artifact-button {
+ display: flex;
+ justify-content: center;
+
+ .action-icon{
+ margin-right: 10px;
+ }
+ }
+
+ .add-artifacts-dynamic-btn-list {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ margin: 20px 0px;
+ .add-artifacts-dynamic-btn{
+ width: 350px;
+ margin-top: 15px;
+ }
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.ts b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.ts
new file mode 100644
index 0000000000..a6804a43c6
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.ts
@@ -0,0 +1,69 @@
+import {Component, OnInit, ViewChild} from "@angular/core";
+import {WorkspaceService} from "../workspace.service";
+import {SdcUiCommon, SdcUiComponents, SdcUiServices} from "onap-ui-angular";
+import {TopologyTemplateService} from "../../../services/component-services/topology-template.service";
+import * as _ from "lodash";
+import {ArtifactGroupType, ArtifactType} from "../../../../utils/constants";
+import {ArtifactsService} from "../../../components/forms/artifacts-form/artifacts.service";
+import {DeleteArtifactAction, GetArtifactsByTypeAction} from "../../../store/actions/artifacts.action";
+import {Select, Store} from "@ngxs/store";
+import {Observable} from "rxjs/index";
+import {ArtifactsState} from "../../../store/states/artifacts.state";
+import {map} from "rxjs/operators";
+import {WorkspaceState} from "../../../store/states/workspace.state";
+import {ArtifactModel} from "../../../../models/artifacts";
+
+@Component({
+ selector: 'information-artifact-page',
+ templateUrl: './information-artifact-page.component.html',
+ styleUrls: ['./information-artifact-page.component.less', '../../../../../assets/styles/table-style.less']
+})
+export class InformationArtifactPageComponent implements OnInit {
+
+ public componentId: string;
+ public componentType: string;
+ public informationArtifacts$: Observable<ArtifactModel[]>;
+ public informationArtifactsAsButtons$: Observable<ArtifactModel[]>;
+ @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean;
+ @ViewChild('informationArtifactsTable') table: any;
+
+ constructor(private workspaceService: WorkspaceService,
+ private artifactsService: ArtifactsService,
+ private store: Store) {
+ }
+
+ ngOnInit(): void {
+ this.componentId = this.workspaceService.metadata.uniqueId;
+ this.componentType = this.workspaceService.metadata.componentType;
+
+ this.store.dispatch(new GetArtifactsByTypeAction({
+ componentType: this.componentType,
+ componentId: this.componentId,
+ artifactType: ArtifactGroupType.INFORMATION
+ }));
+
+ let artifacts = this.store.select(ArtifactsState.getArtifactsByType).pipe(map(filterFn => filterFn(ArtifactType.INFORMATION)));
+ this.informationArtifacts$ = artifacts.pipe(map(artifacts => _.filter(artifacts, (artifact) => {
+ return artifact.esId;
+ })));
+
+ this.informationArtifactsAsButtons$ = artifacts.pipe(map(artifacts => _.filter(artifacts, (artifact) => {
+ return !artifact.esId;
+ })));
+ }
+
+ onActivate(event) {
+ if (event.type === 'click') {
+ this.table.rowDetail.toggleExpandRow(event.row);
+ }
+ }
+
+ public addOrUpdateArtifact = (artifact: ArtifactModel, isViewOnly?: boolean) => {
+ this.artifactsService.openArtifactModal(this.componentId, this.componentType, artifact, ArtifactGroupType.INFORMATION, isViewOnly);
+ }
+
+ public deleteArtifact = (artifactToDelete) => {
+ this.artifactsService.deleteArtifact(this.componentType, this.componentId, artifactToDelete)
+ }
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.module.ts b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.module.ts
new file mode 100644
index 0000000000..5eb9e5851b
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.module.ts
@@ -0,0 +1,30 @@
+import {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {SdcUiComponentsModule} from "onap-ui-angular";
+import {NgxDatatableModule} from "@swimlane/ngx-datatable";
+import {UiElementsModule} from "../../../components/ui/ui-elements.module";
+import {InformationArtifactPageComponent} from "./information-artifact-page.component";
+import {ArtifactFormModule} from "../../../components/forms/artifacts-form/artifact-form.module";
+import {ArtifactsService} from "../../../components/forms/artifacts-form/artifacts.service";
+
+@NgModule({
+ declarations: [
+ InformationArtifactPageComponent
+ ],
+ imports: [
+ CommonModule,
+ SdcUiComponentsModule,
+ NgxDatatableModule,
+ UiElementsModule,
+ ArtifactFormModule
+ ],
+ exports: [
+ InformationArtifactPageComponent
+ ],
+ entryComponents: [
+ InformationArtifactPageComponent
+ ],
+ providers:[ArtifactsService]
+})
+export class InformationArtifactPageModule {
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/information-artifact/informational-artifact-page.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/informational-artifact-page.spec.ts
new file mode 100644
index 0000000000..10fd14739b
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/informational-artifact-page.spec.ts
@@ -0,0 +1,77 @@
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {NO_ERRORS_SCHEMA} from "@angular/core";
+import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper";
+import {NgxDatatableModule} from "@swimlane/ngx-datatable";
+import {WorkspaceService} from "../workspace.service";
+import {SdcUiServices} from "onap-ui-angular";
+import {TopologyTemplateService} from "../../../services/component-services/topology-template.service";
+import {Observable} from "rxjs/Observable";
+import {ComponentMetadata} from "../../../../models/component-metadata";
+import 'rxjs/add/observable/of';
+import {NgxsModule, Store} from "@ngxs/store";
+import {ArtifactsState} from "../../../store/states/artifacts.state";
+import {InformationArtifactPageComponent} from "./information-artifact-page.component";
+import { informationalArtifactsMock} from "../../../../../jest/mocks/artifacts-mock";
+import {ArtifactsService} from "../../../components/forms/artifacts-form/artifacts.service";
+
+describe('informational artifacts page', () => {
+
+ let fixture: ComponentFixture<InformationArtifactPageComponent>;
+ let topologyTemplateServiceMock: Partial<TopologyTemplateService>;
+ let workspaceServiceMock: Partial<WorkspaceService>;
+ let loaderServiceMock: Partial<SdcUiServices.LoaderService>;
+ let store: Store;
+
+ beforeEach(
+ async(() => {
+
+ topologyTemplateServiceMock = {
+ getArtifactsByType: jest.fn().mockImplementation((componentType, id, artifactType) => Observable.of(informationalArtifactsMock))
+ };
+ workspaceServiceMock = {metadata: <ComponentMetadata>{uniqueId: 'service_unique_id', componentType: 'SERVICE'}}
+
+ loaderServiceMock = {
+ activate : jest.fn(),
+ deactivate: jest.fn()
+ }
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [InformationArtifactPageComponent],
+ imports: [NgxDatatableModule, NgxsModule.forRoot([ArtifactsState])],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: WorkspaceService, useValue: workspaceServiceMock},
+ {provide: TopologyTemplateService, useValue: topologyTemplateServiceMock},
+ {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock },
+ {provide: ArtifactsService, useValue: {}},
+ ],
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(InformationArtifactPageComponent);
+ store = testBed.get(Store);
+ });
+ })
+ );
+
+ it('should match current snapshot of informational artifact pages component', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+ it('should see exactly 3 informational artifacts and six buttons to add artifact by template', () => {
+ fixture.componentInstance.ngOnInit();
+ fixture.componentInstance.informationArtifacts$.subscribe((artifacts)=> {
+ expect(artifacts.length).toEqual(3);
+ })
+ fixture.componentInstance.informationArtifactsAsButtons$.subscribe((artifacts)=> {
+ expect(artifacts.length).toEqual(6);
+ })
+
+ store.selectOnce(state => state.artifacts.artifacts).subscribe(artifacts => {
+ expect(artifacts.length).toEqual(9);
+ });
+ })
+
+
+}); \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.html b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.html
new file mode 100644
index 0000000000..f496e64c17
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.html
@@ -0,0 +1,22 @@
+<div class="capabilities-properties-table">
+ <ngx-datatable #componentsMetadataTable
+ columnMode="flex"
+ [headerHeight]="40"
+ [rowHeight]="35"
+ [rows]="capabilitiesProperties"
+ [sorts]="[{prop: 'name', dir: 'desc'}]">
+ <ngx-datatable-column *ngFor="let column of capabilityPropertiesColumns" [ngSwitch]="column.prop" [resizeable]="false"
+ [draggable]="false" name={{column.name}} [flexGrow]="column.flexGrow">
+ <ng-template ngx-datatable-cell-template let-row="row" *ngSwitchCase="'name'">
+ <a data-tests-id="row[column.prop]" sdc-tooltip [tooltip-text]="row[column.prop]" (click)="updateProperty(row)">{{row[column.prop]}}</a>
+ </ng-template>
+ <ng-template ngx-datatable-cell-template let-row="row" *ngSwitchCase="'schema'">
+ <span *ngIf="row[column.prop] && row[column.prop].property" data-tests-id="row[column.prop].property.type"
+ sdc-tooltip [tooltip-text]="row[column.prop].property.type">{{row[column.prop].property.type}}</span>
+ </ng-template>
+ <ng-template ngx-datatable-cell-template let-row="row" *ngSwitchDefault>
+ <span data-tests-id="row[column.prop]" sdc-tooltip [tooltip-text]="row[column.prop]" [tooltip-placement]="3">{{row[column.prop]}}</span>
+ </ng-template>
+ </ngx-datatable-column>
+ </ngx-datatable>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.less b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.less
new file mode 100644
index 0000000000..007f509538
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.less
@@ -0,0 +1,9 @@
+
+:host ::ng-deep {
+ .ngx-datatable {
+ > div {
+ min-height: auto !important;
+ }
+
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.ts
new file mode 100644
index 0000000000..2a1a16e265
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.ts
@@ -0,0 +1,33 @@
+
+import { ViewChild, Input, OnInit, Component } from "@angular/core";
+import {SdcUiServices} from "onap-ui-angular";
+import { ModalsHandler } from "../../../../../../utils/modals-handler";
+import { WorkspaceService } from "../../../workspace.service";
+import { PropertyModel } from "../../../../../../models/properties";
+
+
+@Component({
+ selector: 'capabilities-properties',
+ templateUrl: './capabilities-properties.html',
+ styleUrls: ['./capabilities-properties.less', '../../../../../../../assets/styles/table-style.less']
+})
+export class CapabilitiesPropertiesComponent {
+ @Input() public capabilitiesProperties: Array<PropertyModel> = [];
+
+ private capabilityPropertiesColumns = [
+ {name: 'Name', prop: 'name', flexGrow: 1},
+ {name: 'Type', prop: 'type', flexGrow: 1},
+ {name: 'Schema', prop: 'schema', flexGrow: 1},
+ {name: 'Description', prop: 'description', flexGrow: 1},
+ ];
+ constructor(private modalsHandler: ModalsHandler,
+ private workspaceService: WorkspaceService) {}
+
+ private updateProperty(property: PropertyModel): void {
+ _.forEach(this.capabilitiesProperties, (prop: PropertyModel) => {
+ prop.readonly = true;
+ });
+ this.modalsHandler.openEditPropertyModal(property, this.workspaceService.metadata, this.capabilitiesProperties, false, 'component',
+ this.workspaceService.metadata.uniqueId);
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.html b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.html
new file mode 100644
index 0000000000..819eb84849
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.html
@@ -0,0 +1,59 @@
+<div class="capabilities-table">
+ <div class="expand-collapse-all-rows">
+ <svg-icon class="selected-all-capabilities"
+ [mode]="'primary'" [clickable]="true" [name]="'expand-o'"
+ [size]="'medium'" (click)="capabilitiesTable.rowDetail.expandAllRows()">
+ </svg-icon>
+ <svg-icon class="unselected-all-capabilities"
+ [mode]="'primary'" [clickable]="true" [name]="'minimize-o'"
+ [size]="'medium'" (click)="capabilitiesTable.rowDetail.collapseAllRows()">
+ </svg-icon>
+ </div>
+ <ngx-datatable #capabilitiesTable
+ columnMode="flex"
+ [headerHeight]="40"
+ [rowHeight]="35"
+ [rows]="capabilities"
+ (select)="onSelectCapabilities($event)"
+ [selectionType]="'single'">
+ <ngx-datatable-row-detail [rowHeight]="undefiend">
+ <ng-template let-row="row" ngx-datatable-row-detail-template>
+ <div class="properties-title">Properties</div>
+ <capabilities-properties [capabilitiesProperties]="row.properties"></capabilities-properties>
+ </ng-template>
+ </ngx-datatable-row-detail>
+ <ngx-datatable-column name="Name" [flexGrow]="1" [resizeable]="false">
+ <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'" (click)="expendRow(row)"></svg-icon>
+ <span data-tests-id="row.name" sdc-tooltip [tooltip-text]="row.name" [tooltip-placement]="3" (click)="editCapability(row)">{{row.name}}</span>
+ </div>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column name="Type" [flexGrow]="1" [resizeable]="false">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <span data-tests-id="row.type" sdc-tooltip [tooltip-text]="row.type" [tooltip-placement]="3">{{row.type ? row.type.replace("tosca.capabilities.",""): ''}}</span>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column name="Description" [flexGrow]="1" [resizeable]="false">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <span data-tests-id="row.description" sdc-tooltip [tooltip-text]="row.description" [tooltip-placement]="3">{{row.description}}</span>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column name="Valid Source" [flexGrow]="1" [resizeable]="false">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <span data-tests-id="row.validSourceTypes.join(',')" sdc-tooltip [tooltip-text]="row.validSourceTypes ? row.validSourceTypes.join(',') : null" [tooltip-placement]="3">
+ {{row.validSourceTypes ? row.validSourceTypes.join(','): ''}}</span>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column name="Occurrences" [flexGrow]="1" [prop]="'minOccurrences'" [resizeable]="false">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <span data-tests-id="row.minOccurrences+','+row.maxOccurrences" sdc-tooltip
+ [tooltip-text]="row.minOccurrences+','+row.maxOccurrences" [tooltip-placement]="3">
+ {{row.minOccurrences}},{{row.maxOccurrences}}</span>
+ </ng-template>
+ </ngx-datatable-column>
+ </ngx-datatable>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.less b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.less
new file mode 100644
index 0000000000..0c520a8135
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.less
@@ -0,0 +1,16 @@
+:host ::ng-deep {
+ .datatable-row-detail {
+ width: 1260px;
+ }
+ .datatable-body-row {
+ cursor: pointer;
+ }
+}
+.expand-collapse-all-rows {
+ position: absolute;
+ top: 172px;
+ left: 890px;
+}
+.properties-title {
+ padding-bottom: 10px;
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.ts
new file mode 100644
index 0000000000..02db5d3aee
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.ts
@@ -0,0 +1,79 @@
+import {Capability, CapabilityUI} from "../../../../../models/capability";
+import { ViewChild, Input, OnInit, Component } from "@angular/core";
+import {SdcUiServices} from "onap-ui-angular";
+import {CapabilitiesEditorComponent} from "./capabilityEditor/capabilities-editor.component";
+import {WorkspaceService} from "../../workspace.service";
+import {TopologyTemplateService} from "../../../../services/component-services/topology-template.service";
+import {ReqAndCapabilitiesService} from "../req-and-capabilities.service";
+import {ModalComponent} from "onap-ui-angular/dist/modals/modal.component";
+import {EventListenerService} from "../../../../../services/event-listener-service";
+
+
+@Component({
+ selector: 'capabilities',
+ templateUrl: './capabilities.component.html',
+ styleUrls: ['./capabilities.component.less','../../../../../../assets/styles/table-style.less']
+})
+export class CapabilitiesComponent {
+ @Input() public capabilities: Array<Capability>;
+ @ViewChild('capabilitiesTable') capabilitiesTable: any;
+ private customModalInstance: ModalComponent;
+
+ constructor(
+ private workspaceService: WorkspaceService,
+ private loaderService: SdcUiServices.LoaderService,
+ private topologyTemplateService: TopologyTemplateService,
+ private reqAndCapabilitiesService : ReqAndCapabilitiesService,
+ private modalService: SdcUiServices.ModalService,
+ private eventListenerService: EventListenerService) {
+ }
+
+ private onSelectCapabilities({ selected }) {
+ }
+
+ editCapability(cap: CapabilityUI) {
+ let modalConfig = {
+ size: 'md',
+ title: 'Update Capability',
+ type: 'custom',
+ buttons: [
+ {
+ id: 'saveButton',
+ text: ('Update'),
+ size: "'x-small'",
+ callback: () => this.updateCapability(),
+ closeModal: true
+ },
+ {text: "Cancel", size: "'x-small'", closeModal: true}]
+ };
+ let modalInputs = {
+ capability: cap,
+ capabilityTypesList: this.reqAndCapabilitiesService.getCapabilityTypesList(),
+ };
+
+ this.customModalInstance = this.modalService.openCustomModal(modalConfig, CapabilitiesEditorComponent, {input: modalInputs});
+ this.customModalInstance.innerModalContent.instance.
+ onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('saveButton').disabled = !isValid);
+ }
+
+ expendRow(row) {
+ this.capabilitiesTable.rowDetail.toggleExpandRow(row);
+ }
+
+ private updateCapability() {
+ const capability = this.customModalInstance.innerModalContent.instance.capabilityData;
+ this.loaderService.activate();
+ if (capability.uniqueId) {
+ this.topologyTemplateService.updateCapability(this.workspaceService.metadata.getTypeUrl(), this.workspaceService.metadata.uniqueId, capability).subscribe((result) => {
+ let index = this.capabilities.findIndex((cap) => result[0].uniqueId === cap.uniqueId);
+ this.capabilities[index] = new CapabilityUI(result[0], this.workspaceService.metadata.uniqueId);
+ this.loaderService.deactivate();
+ this.eventListenerService.notifyObservers('CAPABILITIES_UPDATED');
+ }, () => {
+ this.loaderService.deactivate();
+ });
+ }
+ }
+
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.html b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.html
new file mode 100644
index 0000000000..bc15d4d228
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.html
@@ -0,0 +1,93 @@
+<div class="capability-editor">
+ <form class="w-sdc-form">
+ <div class="i-sdc-form-content-capability-content">
+ <div class="content-row">
+ <div class="i-sdc-form-item">
+ <sdc-input
+ label="{{ 'CAP_NAME' | translate }}"
+ required="true"
+ class="i-sdc-form-input"
+ testId="capName"
+ [disabled]="isReadonly"
+ [(value)]="capabilityData.name"
+ (valueChange)="validityChanged()">
+ </sdc-input>
+ </div>
+ </div>
+
+ <div class="group-with-border">
+ <div class="content-row i-sdc-form-item">
+ <sdc-dropdown
+ label="{{ 'CAP_TYPE' | translate }}"
+ required="true"
+ class="i-sdc-form-select"
+ testId="capType"
+ [disabled]="isReadonly"
+ [options]="capabilityTypesMappedList"
+ selectedOption="{{ capabilityData.type }}"
+ [placeHolder] = "capabilityData.type"
+ (changed)="onSelectCapabilityType($event)">
+ </sdc-dropdown>
+ </div>
+ <div class="content-row i-sdc-form-item">
+ <label class="i-sdc-form-label"> {{ 'CAP_DESCRIPTION' | translate }} </label>
+ <textarea
+ rows="3"
+ class="i-sdc-form-input description"
+ data-tests-id="capDesc"
+ disabled
+ value="{{capabilityData.description}}">
+ </textarea>
+ </div>
+ <div class="content-row i-sdc-form-item">
+ <label class="i-sdc-form-label valid-source-label"> {{ 'CAP_VALID_SOURCE' | translate }} </label>
+ <textarea
+ rows="2"
+ class="i-sdc-form-input"
+ data-tests-id="capValidSrc"
+ disabled
+ value="{{capabilityData.validSourceTypes}}">
+ </textarea>
+ </div>
+ </div>
+
+ <label class="i-sdc-form-label occurrences-label"> {{ 'REQ_CAP_OCCURRENCES' | translate }} </label>
+ <div class="content-row occurrences-section">
+ <div class="min-occurrences-value">
+ <sdc-input
+ label="{{ 'REQ_CAP_OCCURRENCES_MIN' | translate }}"
+ class="i-sdc-form-input"
+ testId="capOccurrencesMin"
+ [disabled]="isReadonly"
+ [(value)]="capabilityData.minOccurrences"
+ (valueChange)="validityChanged()"
+ type="number">
+ </sdc-input>
+ </div>
+ <div class="sdc-input">
+ <label class="sdc-input__label"> {{ 'REQ_CAP_OCCURRENCES_MAX' | translate }} </label>
+ <div class="max-occurrences-value">
+ <sdc-checkbox
+ class="checkbox-label unbounded-value"
+ testId="capOccurrencesMaxUnbounded"
+ label="{{translatedUnboundTxt.toLowerCase()}}"
+ (checkedChange)="onUnboundedChanged()"
+ [checked]="isUnboundedChecked"
+ [disabled]="isReadonly">
+ </sdc-checkbox>
+
+ <sdc-input
+ *ngIf="!isUnboundedChecked"
+ class="i-sdc-form-input"
+ testId="capOccurrencesMax"
+ [disabled]="isReadonly"
+ [(value)]="capabilityData.maxOccurrences"
+ (valueChange)="validityChanged()"
+ type="number">
+ </sdc-input>
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.less b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.less
new file mode 100644
index 0000000000..324dc6c4d2
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.less
@@ -0,0 +1,38 @@
+@import '../../../../../../../assets/styles/variables.less';
+
+.capability-editor {
+ .i-sdc-form-content-capability-content {
+ padding: 10px 25px;
+ .group-with-border {
+ margin: 25px 0;
+ padding: 15px 0;
+ border-top: 1px solid @tlv_color_u;
+ border-bottom: 1px solid @tlv_color_u;
+ .content-row:not(:last-of-type) {
+ padding-bottom: 13px;
+ }
+ }
+
+ .occurrences-label {
+ font-family: @font-opensans-bold;
+ margin-bottom: 19px;
+ }
+ .occurrences-section, /deep/ .max-occurrences-value {
+ display: flex;
+ .min-occurrences-value {
+ padding-right: 30px;
+ }
+ .unbounded-value {
+ padding-top: 7px;
+ padding-right: 20px;
+ .sdc-checkbox__label {
+ text-transform: capitalize;
+ }
+ }
+ }
+ textarea {
+ min-height: unset;
+ height: unset;
+ }
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.ts
new file mode 100644
index 0000000000..3bafa42e0f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.ts
@@ -0,0 +1,81 @@
+import {Component} from '@angular/core';
+import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service";
+import {Capability, CapabilityTypeModel} from 'app/models';
+import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component";
+import {TranslateService} from 'app/ng2/shared/translator/translate.service';
+import {Subject} from "rxjs";
+
+@Component({
+ selector: 'capabilities-editor',
+ templateUrl: './capabilities-editor.component.html',
+ styleUrls: ['./capabilities-editor.component.less'],
+ providers: [ServiceServiceNg2]
+})
+
+export class CapabilitiesEditorComponent {
+ input: {
+ test: string,
+ capability: Capability,
+ capabilityTypesList: Array<CapabilityTypeModel>,
+ isReadonly: boolean;
+ };
+ capabilityData: Capability;
+ capabilityTypesMappedList: Array<DropdownValue>;
+ isUnboundedChecked: boolean;
+ isReadonly: boolean;
+ translatedUnboundTxt: string;
+
+ public onValidationChange: Subject<boolean> = new Subject();
+
+ constructor(private translateService: TranslateService) {
+ }
+
+ ngOnInit() {
+ this.capabilityData = new Capability(this.input.capability);
+ this.translatedUnboundTxt = '';
+ this.capabilityData.minOccurrences = this.capabilityData.minOccurrences || 0;
+ this.translateService.languageChangedObservable.subscribe(lang => {
+ this.translatedUnboundTxt = this.translateService.translate('REQ_CAP_OCCURRENCES_UNBOUNDED');
+ this.capabilityData.maxOccurrences = this.capabilityData.maxOccurrences || this.translatedUnboundTxt;
+ this.isUnboundedChecked = this.capabilityData.maxOccurrences === this.translatedUnboundTxt;
+ });
+ this.capabilityTypesMappedList = _.map(this.input.capabilityTypesList, capType => new DropdownValue(capType.toscaPresentation.type, capType.toscaPresentation.type));
+ this.isReadonly = this.input.isReadonly;
+ this.validityChanged();
+ }
+
+ onUnboundedChanged() {
+ this.isUnboundedChecked = !this.isUnboundedChecked;
+ this.capabilityData.maxOccurrences = this.isUnboundedChecked ? this.translatedUnboundTxt : null;
+ this.validityChanged();
+ }
+
+ checkFormValidForSubmit() {
+ return this.capabilityData.name && this.capabilityData.name.length &&
+ this.capabilityData.type && this.capabilityData.type.length && !_.isEqual(this.capabilityData.minOccurrences, "") && this.capabilityData.minOccurrences >= 0 &&
+ (
+ this.isUnboundedChecked ||
+ (this.capabilityData.maxOccurrences && (this.capabilityData.minOccurrences <= parseInt(this.capabilityData.maxOccurrences)))
+ );
+ }
+
+ onSelectCapabilityType(selectedCapType: DropdownValue) {
+ this.capabilityData.type = selectedCapType && selectedCapType.value;
+ if (selectedCapType && selectedCapType.value) {
+ let selectedCapabilityTypeObj: CapabilityTypeModel = this.input.capabilityTypesList.find(capType => capType.toscaPresentation.type === selectedCapType.value);
+ this.capabilityData.description = selectedCapabilityTypeObj.toscaPresentation.description;
+ this.capabilityData.validSourceTypes = selectedCapabilityTypeObj.toscaPresentation.validTargetTypes;
+ this.capabilityData.properties = _.forEach(
+ _.toArray(selectedCapabilityTypeObj.properties),
+ prop => prop.uniqueId = null //a requirement for the BE
+ );
+ }
+ this.validityChanged();
+ }
+
+ validityChanged = () => {
+ let validState = this.checkFormValidForSubmit();
+ this.onValidationChange.next(validState);
+ }
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.module.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.module.ts
new file mode 100644
index 0000000000..38b104a0f6
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.module.ts
@@ -0,0 +1,29 @@
+import {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {CapabilitiesEditorComponent} from './capabilities-editor.component';
+import {FormsModule} from '@angular/forms';
+import {FormElementsModule} from 'app/ng2/components/ui/form-components/form-elements.module';
+import {UiElementsModule} from 'app/ng2/components/ui/ui-elements.module';
+import {TranslateModule} from 'app/ng2/shared/translator/translate.module';
+import {SdcUiComponentsModule} from 'onap-ui-angular';
+// import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index";
+
+@NgModule({
+ declarations: [
+ CapabilitiesEditorComponent
+ ],
+ imports: [CommonModule,
+ FormsModule,
+ FormElementsModule,
+ UiElementsModule,
+ TranslateModule,
+ SdcUiComponentsModule
+ ],
+ exports: [],
+ entryComponents: [
+ CapabilitiesEditorComponent
+ ],
+ providers: []
+})
+export class CapabilitiesEditorModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.html b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.html
new file mode 100644
index 0000000000..73e0ae52ae
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.html
@@ -0,0 +1,21 @@
+<div class="workspace-req-and-cap">
+ <div>
+ <span class="addTitle" *ngIf="selectTabName === 'REQUIREMENTS'" (click)="addRequiremnet()">Add Requirement</span>
+ <span class="addTitle" *ngIf="selectTabName !== 'REQUIREMENTS'" (click)="addCapability()">Add Capability</span>
+ <span class="req-and-cap-filter" *ngIf="notEmptyTable">
+ <sdc-filter-bar
+ [placeHolder]="'Search'"
+ (keyup)="updateFilter($event)"
+ [testId]="'search-box'">
+ </sdc-filter-bar>
+ </span>
+ </div>
+ <sdc-tabs (selectedTab)="selectTab($event)" [tabStyle]="'sdc-table-tab'">
+ <sdc-tab [title]="'Requirements('+(requirements.length||'0')+')'" [active]="true" [testId]="'req-tab'">
+ <div #requirmentsContainer></div>
+ </sdc-tab>
+ <sdc-tab [title]="'Capabilities('+(capabilities.length||'0')+')'" [active]="false" [testId]="'cap-tab'">
+ <div #capabilitiesContainer></div>
+ </sdc-tab>
+ </sdc-tabs>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.less b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.less
new file mode 100644
index 0000000000..f3d39cacd6
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.less
@@ -0,0 +1,19 @@
+.req-and-cap-filter {
+ width: 336px;
+ float: right;
+ margin-right: 10px;
+}
+
+.addTitle {
+ float: right;
+ text-transform: uppercase;
+ font-family: OpenSans-Semibold, sans-serif;
+ color: #009fdb;
+ cursor: pointer;
+}
+
+:host ::ng-deep .sdc-tabs {
+ .sdc-tab-content {
+ margin-top: 0;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.spec.ts
new file mode 100644
index 0000000000..b7fad045d3
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.spec.ts
@@ -0,0 +1,127 @@
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import { NO_ERRORS_SCHEMA} from "@angular/core";
+import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper";
+
+import {Observable} from "rxjs/Observable";
+import {NgxDatatableModule} from "@swimlane/ngx-datatable";
+import {SdcUiServices, SdcUiCommon} from "onap-ui-angular";
+import 'rxjs/add/observable/of';
+import {ReqAndCapabilitiesComponent} from "./req-and-capabilities.component";
+import {ReqAndCapabilitiesService} from "./req-and-capabilities.service";
+import {WorkspaceService} from "../workspace.service";
+import {
+ capabilitiesMock,
+ filterRequirmentsMock,
+ requirementMock
+} from "../../../../../jest/mocks/req-and-capabilities.mock";
+import {ComponentMetadata} from "../../../../models/component-metadata";
+import { TopologyTemplateService } from "../../../services/component-services/topology-template.service";
+import {EventListenerService} from "../../../../services/event-listener-service";
+
+describe('req and capabilities component', () => {
+
+ let fixture: ComponentFixture<ReqAndCapabilitiesComponent>;
+ let workspaceServiceMock: Partial<WorkspaceService>;
+ let loaderServiceMock: Partial<SdcUiServices.LoaderService>;
+ let topologyTemplateServiceMock: Partial<TopologyTemplateService>;
+ let createDynamicComponentServiceMock: Partial<SdcUiServices.CreateDynamicComponentService>
+ let reqAndCapabilitiesService: Partial<ReqAndCapabilitiesService>;
+ let modalService: Partial<SdcUiServices.ModalService>;
+ let eventListenerService: Partial<EventListenerService>;
+
+
+
+ beforeEach(
+ async(() => {
+
+ workspaceServiceMock = {
+ metadata: new ComponentMetadata()
+ };
+
+ topologyTemplateServiceMock = {
+ getRequirementsAndCapabilitiesWithProperties: jest.fn().mockImplementation(() =>
+ Observable.of({requirements: {'tosca.requirements.Node': requirementMock},
+ capabilities: {'tosca.capabilities.Node': capabilitiesMock}}))
+ };
+
+ loaderServiceMock = {
+ activate : jest.fn(),
+ deactivate: jest.fn()
+ }
+ createDynamicComponentServiceMock = {
+ insertComponentDynamically: jest.fn()
+ }
+
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [ReqAndCapabilitiesComponent],
+ imports: [NgxDatatableModule],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ { provide: WorkspaceService, useValue: workspaceServiceMock },
+ { provide: SdcUiServices.LoaderService, useValue: loaderServiceMock },
+ { provide: TopologyTemplateService, useValue: topologyTemplateServiceMock },
+ { provide: SdcUiServices.CreateDynamicComponentService, useValue: createDynamicComponentServiceMock },
+ { provide: ReqAndCapabilitiesService, useValue: reqAndCapabilitiesService },
+ { provide: SdcUiServices.ModalService, useValue: modalService },
+ { provide: EventListenerService, useValue: eventListenerService }
+ ],
+ });
+ };
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(ReqAndCapabilitiesComponent);
+ });
+ })
+ );
+
+ it('should see exactly 2 requirement in requirements table when call initCapabilitiesAndRequirements and meta data requirements null', () => {
+ workspaceServiceMock.metadata.requirements = null;
+ fixture.componentInstance.initCapabilitiesAndRequirements();
+ expect(workspaceServiceMock.metadata.requirements["tosca.requirements.Node"].length).toBe(3);
+ });
+ it('should see exactly 2 capabilities in capabilities table when call initCapabilitiesAndRequirements and meta data capabilities null', () => {
+ workspaceServiceMock.metadata.capabilities = null;
+ fixture.componentInstance.initCapabilitiesAndRequirements();
+ expect(workspaceServiceMock.metadata.capabilities["tosca.capabilities.Node"].length).toBe(2);
+ });
+
+ it('capabilities array papulated when call populateReqOrCap with capabilities', () => {
+ workspaceServiceMock.metadata.capabilities = {"tosca.capabilities.Node": capabilitiesMock, "tosca.capabilities.Scalable": capabilitiesMock};
+ fixture.componentInstance.populateReqOrCap("capabilities");
+ expect(fixture.componentInstance.capabilities.length).toBe(4);
+ });
+
+ it('create requirements component when call loadReqOrCap with true', () => {
+ createDynamicComponentServiceMock.insertComponentDynamically.mockImplementation(() => { return {instance: {requirements: requirementMock}}});
+ fixture.componentInstance.requirements = requirementMock;
+ fixture.componentInstance.loadReqOrCap(true);
+ expect(fixture.componentInstance.instanceRef.instance.requirements.length).toEqual(3);
+ });
+
+ it('create capabilities component when call loadReqOrCap with false', () => {
+ fixture.componentInstance.instanceRef = {instance: {requirements: null}};
+ createDynamicComponentServiceMock.insertComponentDynamically.mockImplementation(() => { return {instance: {capabilities: capabilitiesMock}}});
+ fixture.componentInstance.capabilities = capabilitiesMock;
+ fixture.componentInstance.requirementsUI = filterRequirmentsMock;
+ let event = {
+ target : {
+ value : 'root'
+ }
+ }
+ fixture.componentInstance.updateFilter(event);
+ expect(fixture.componentInstance.instanceRef.instance.requirements.length).toBe(1);
+ });
+
+ it('should filter 1 capabilities when searching and call updateFilter function and instanceRef is capabilities component', () => {
+ fixture.componentInstance.instanceRef = {instance: {capabilities: null}};
+ fixture.componentInstance.capabilities = capabilitiesMock;
+ fixture.componentInstance.selectTabName = 'CAPABILITIES';
+ let event = {
+ target : {
+ value : '1source'
+ }
+ }
+ fixture.componentInstance.updateFilter(event);
+ expect(fixture.componentInstance.instanceRef.instance.capabilities[0].type).toBe("tosca.capabilities.Node");
+ });
+});
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.ts
new file mode 100644
index 0000000000..69999bfb86
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.ts
@@ -0,0 +1,229 @@
+import { Component, ComponentRef, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
+import * as _ from 'lodash';
+import { SdcUiServices } from 'onap-ui-angular';
+import { Capability, CapabilityUI } from '../../../../models/capability';
+import { Requirement, RequirementUI } from '../../../../models/requirement';
+import { TopologyTemplateService } from '../../../services/component-services/topology-template.service';
+import { ComponentGenericResponse } from '../../../services/responses/component-generic-response';
+import { WorkspaceService } from '../workspace.service';
+import { CapabilitiesComponent } from './capabilities/capabilities.component';
+import { RequirmentsComponent } from './requirements/requirments.components';
+import {ReqAndCapabilitiesService} from "./req-and-capabilities.service";
+import {CapabilitiesEditorComponent} from "./capabilities/capabilityEditor/capabilities-editor.component";
+import {ModalComponent} from "onap-ui-angular/dist/modals/modal.component";
+import {EventListenerService} from "../../../../services/event-listener-service";
+import {RequirementsEditorComponent} from "./requirements/requirementEditor/requirements-editor.component";
+
+@Component({
+ selector: 'req-and-capabilities',
+ templateUrl: './req-and-capabilities.component.html',
+ styleUrls: ['./req-and-capabilities.component.less']
+})
+export class ReqAndCapabilitiesComponent implements OnInit {
+
+ @ViewChild('requirmentsContainer', { read: ViewContainerRef }) requirmentsContainer: ViewContainerRef;
+ @ViewChild('capabilitiesContainer', { read: ViewContainerRef }) capabilitiesContainer: ViewContainerRef;
+ private requirements: Requirement[] = [];
+ private requirementsUI: RequirementUI[] = [];
+ private capabilities: Capability[] = [];
+ private selectTabName: string = 'REQUIREMENTS';
+ private notEmptyTable: boolean = true;
+ private instanceRef: ComponentRef<any>;
+ private customModalInstance: ModalComponent;
+ readonly INPUTS_FOR_CAPABILITIES: string = 'INPUTS_FOR_CAPABILITIES';
+ readonly INPUTS_FOR_REQUIREMENTS: string = 'INPUTS_FOR_REQUIREMENTS';
+
+ constructor(private workspaceService: WorkspaceService,
+ private loaderService: SdcUiServices.LoaderService,
+ private topologyTemplateService: TopologyTemplateService,
+ private createDynamicComponentService: SdcUiServices.CreateDynamicComponentService,
+ private reqAndCapabilitiesService : ReqAndCapabilitiesService,
+ private modalService: SdcUiServices.ModalService,
+ private eventListenerService: EventListenerService) {
+ }
+
+ ngOnInit(): void {
+ this.initCapabilitiesAndRequirements();
+
+ this.eventListenerService.registerObserverCallback('CAPABILITIES_UPDATED', () => {
+ this.loadReqOrCap();
+ });
+
+ this.eventListenerService.registerObserverCallback('REQUIREMENTS_UPDATED', () => {
+ this.loadReqOrCap();
+ });
+ }
+
+
+
+ private initCapabilitiesAndRequirements(): void {
+ if (!this.workspaceService.metadata.capabilities || !this.workspaceService.metadata.requirements) {
+ this.loaderService.activate();
+ this.topologyTemplateService.getRequirementsAndCapabilitiesWithProperties
+ (this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId)
+ .subscribe((response: ComponentGenericResponse) => {
+ this.workspaceService.metadata.capabilities = response.capabilities;
+ this.workspaceService.metadata.requirements = response.requirements;
+ this.initReqOrCap();
+ this.loaderService.deactivate();
+ }, (error) => {
+ this.loaderService.deactivate();
+ });
+ } else {
+ this.initReqOrCap();
+ }
+ }
+
+ private initReqOrCap() {
+ this.populateReqOrCap('requirements');
+ this.extendRequirementsToRequiremnetsUI(this.requirements);
+ this.populateReqOrCap('capabilities');
+ this.loadReqOrCap();
+ }
+
+ private populateReqOrCap(instanceName: string) {
+ _.forEach(this.workspaceService.metadata[instanceName], (concatArray: any[], name) => {
+ this[instanceName] = this[instanceName].concat(concatArray);
+ });
+ }
+
+ private updateFilter(event) {
+ const val = event.target.value.toLowerCase();
+ if (this.selectTabName === 'REQUIREMENTS') {
+ this.instanceRef.instance.requirements = this.requirementsUI.filter((req: Requirement) => {
+ return !val || this.filterRequirments(req, val);
+ });
+ } else {
+ this.instanceRef.instance.capabilities = this.capabilities.filter((cap: Capability) => {
+ return !val || this.filterCapabilities(cap, val);
+ });
+ }
+
+ }
+
+ private selectTab($event) {
+ this.selectTabName = $event.title.contains('Requirement') ? 'REQUIREMENTS' : 'CATPABILITIES';
+ this.loadReqOrCap();
+ }
+
+ private async loadReqOrCap() {
+ if (this.instanceRef) {
+ this.instanceRef.destroy();
+ }
+
+ if (this.selectTabName === 'REQUIREMENTS') {
+ this.notEmptyTable = this.requirementsUI.length !== 0;
+ this.instanceRef = this.createDynamicComponentService.
+ insertComponentDynamically(RequirmentsComponent, {requirements: this.requirementsUI}, this.requirmentsContainer);
+ // TODO - Keep the initInputs, so it will be called only for the first time - no need to wait to thse API's every time that a user switches tab
+ await this.reqAndCapabilitiesService.initInputs(this.INPUTS_FOR_REQUIREMENTS);
+ } else {
+ this.notEmptyTable = this.capabilities.length !== 0;
+ this.instanceRef = this.createDynamicComponentService.
+ insertComponentDynamically(CapabilitiesComponent, {capabilities: this.capabilities}, this.capabilitiesContainer);
+ // TODO - Keep the initInputs, so it will be called only for the first time - no need to wait to thse API's every time that a user switches tab
+ await this.reqAndCapabilitiesService.initInputs(this.INPUTS_FOR_CAPABILITIES);
+ }
+ }
+
+ private filterCapabilities(capability: Capability, val: string): boolean {
+ return _.includes([capability.name, capability.description, capability.validSourceTypes.join(),
+ capability.minOccurrences, capability.maxOccurrences].join('').toLowerCase(), val) ||
+ (capability.type && capability.type.replace('tosca.capabilities.', '').toLowerCase().indexOf(val) !== -1);
+ }
+
+ private filterRequirments(requirement: Requirement, val: string): boolean {
+ return _.includes([requirement.name, requirement.minOccurrences, requirement.maxOccurrences].join('').toLowerCase(), val) ||
+ (requirement.capability && requirement.capability.substring('tosca.capabilities.'.length).toLowerCase().indexOf(val) !== -1) ||
+ (requirement.node && requirement.node.substring('tosca.node.'.length).toLowerCase().indexOf(val) !== -1) ||
+ (requirement.relationship && requirement.relationship.substring('tosca.relationship.'.length)
+ .toLowerCase().indexOf(val) !== -1);
+ }
+
+ private addCapability() {
+ let modalConfig = {
+ size: 'md',
+ title: 'Add Capability',
+ type: 'custom',
+ buttons: [
+ {
+ id: 'saveButton',
+ text: ('Create'),
+ size: "'x-small'",
+ callback: () => this.createCapability(),
+ closeModal: true
+ },
+ {text: "Cancel", size: "'x-small'", closeModal: true}]
+ };
+ let modalInputs = {
+ capabilityTypesList: this.reqAndCapabilitiesService.getCapabilityTypesList(),
+ };
+
+ this.customModalInstance = this.modalService.openCustomModal(modalConfig, CapabilitiesEditorComponent, {input: modalInputs});
+ this.customModalInstance.innerModalContent.instance.
+ onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('saveButton').disabled = !isValid);
+ }
+
+ private createCapability() {
+ const capability = this.customModalInstance.innerModalContent.instance.capabilityData;
+ this.loaderService.activate();
+ if (!capability.uniqueId) {
+ this.topologyTemplateService.createCapability(this.workspaceService.metadata.getTypeUrl(), this.workspaceService.metadata.uniqueId, capability).subscribe((result) => {
+ this.capabilities.unshift(new CapabilityUI(result[0], this.workspaceService.metadata.uniqueId));
+ this.loadReqOrCap();
+ this.loaderService.deactivate();
+ }, () => {
+ this.loaderService.deactivate();
+ });
+ }
+ }
+
+ private addRequiremnet () {
+ let modalConfig = {
+ size: 'md',
+ title: 'Add Requirement',
+ type: 'custom',
+ buttons: [
+ {
+ id: 'saveButton',
+ text: ('Create'),
+ size: "'x-small'",
+ callback: () => this.createRequirement(),
+ closeModal: true
+ },
+ {text: "Cancel", size: "'x-small'", closeModal: true}]
+ };
+ let modalInputs = {
+ // requirement: req,
+ relationshipTypesList: this.reqAndCapabilitiesService.getRelationsShipeTypeList(),
+ nodeTypesList: this.reqAndCapabilitiesService.getNodeTypesList(),
+ capabilityTypesList: this.reqAndCapabilitiesService.getCapabilityTypesList(),
+ // isReadonly: this.$scope.isViewMode() || !this.$scope.isDesigner(),
+ };
+
+ this.customModalInstance = this.modalService.openCustomModal(modalConfig, RequirementsEditorComponent, {input: modalInputs});
+ this.customModalInstance.innerModalContent.instance.
+ onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('saveButton').disabled = !isValid);
+ }
+
+
+ private createRequirement() {
+ const requirement = this.customModalInstance.innerModalContent.instance.requirementData;
+ this.loaderService.activate();
+ if (!requirement.uniqueId) {
+ this.topologyTemplateService.createRequirement(this.workspaceService.metadata.getTypeUrl(), this.workspaceService.metadata.uniqueId, requirement).subscribe(result => {
+ this.requirementsUI.unshift(new RequirementUI(result[0], this.workspaceService.metadata.uniqueId));
+ this.loadReqOrCap();
+ this.loaderService.deactivate();
+ }, () => {
+ this.loaderService.deactivate();
+ });
+ }
+ }
+
+ private extendRequirementsToRequiremnetsUI(requirements: Requirement[]) {
+ this.requirements.map((requirement) => {
+ this.requirementsUI.push(new RequirementUI(requirement, this.workspaceService.metadata.uniqueId));
+ });
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.module.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.module.ts
new file mode 100644
index 0000000000..aacb3a5bd1
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.module.ts
@@ -0,0 +1,49 @@
+import {NgModule} from "@angular/core";
+import {SdcUiComponentsModule} from "onap-ui-angular";
+
+import {NgxDatatableModule} from "@swimlane/ngx-datatable";
+import { ReqAndCapabilitiesComponent } from "./req-and-capabilities.component";
+import { CommonModule } from "@angular/common";
+
+import {RequirmentsComponent } from "./requirements/requirments.components";
+import { CapabilitiesComponent } from "./capabilities/capabilities.component";
+import { CapabilitiesPropertiesComponent } from "./capabilities/capabilities-properties/capabilities-properties";
+import {ReqAndCapabilitiesService} from "./req-and-capabilities.service";
+import {RequirementsEditorComponent} from "./requirements/requirementEditor/requirements-editor.component";
+import {CapabilitiesEditorComponent} from "./capabilities/capabilityEditor/capabilities-editor.component";
+import {TranslateModule} from "../../../shared/translator/translate.module";
+import {ToscaTypesServiceNg2} from "../../../services/tosca-types.service";
+
+@NgModule({
+ declarations: [
+ ReqAndCapabilitiesComponent,
+ CapabilitiesComponent,
+ RequirmentsComponent,
+ CapabilitiesPropertiesComponent,
+ RequirementsEditorComponent,
+ CapabilitiesEditorComponent
+ ],
+ imports: [
+ CommonModule,
+ SdcUiComponentsModule,
+ NgxDatatableModule,
+ TranslateModule
+ ],
+ exports: [
+ ReqAndCapabilitiesComponent,
+ CapabilitiesComponent,
+ RequirmentsComponent,
+ CapabilitiesPropertiesComponent
+ ],
+ entryComponents: [
+ ReqAndCapabilitiesComponent,
+ CapabilitiesComponent,
+ RequirmentsComponent,
+ CapabilitiesPropertiesComponent,
+ RequirementsEditorComponent,
+ CapabilitiesEditorComponent
+ ],
+ providers: [ ReqAndCapabilitiesService]
+})
+export class reqAndCapabilitiesModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.service.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.service.ts
new file mode 100644
index 0000000000..470aac75a6
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.service.ts
@@ -0,0 +1,80 @@
+import { Injectable } from "@angular/core";
+import { TopologyTemplateService } from "../../../services/component-services/topology-template.service";
+import { Store } from "@ngxs/store";
+import { SdcUiServices } from "onap-ui-angular";
+import { CapabilityTypeModel } from "../../../../models/capability-types";
+import { RelationshipTypeModel } from "../../../../models/relationship-types";
+import { NodeTypeModel } from "../../../../models/node-types";
+import { WorkspaceService } from "../workspace.service";
+import { ToscaTypesServiceNg2 } from "../../../services/tosca-types.service";
+
+
+
+@Injectable()
+export class ReqAndCapabilitiesService {
+
+ private capabilityTypesList: CapabilityTypeModel[];
+ private relationshipTypesList: RelationshipTypeModel[];
+ private nodeTypesList: NodeTypeModel[];
+ private capabilitiesListUpdated: boolean = false;
+ private requirementsListUpdated: boolean = false;
+ private nodeTypeListUpdated: boolean = false;
+
+ readonly INPUTS_FOR_REQUIREMENTS: string = 'INPUTS_FOR_REQUIREMENTS';
+ readonly INPUTS_FOR_CAPABILITIES: string = 'INPUTS_FOR_CAPABILITIES';
+
+ constructor(
+ private workspaceService: WorkspaceService,
+ private modalService: SdcUiServices.ModalService,
+ private loaderService: SdcUiServices.LoaderService,
+ private topologyTemplateService: TopologyTemplateService,
+ private store: Store,
+ private toscaTypesServiceNg2: ToscaTypesServiceNg2){}
+
+ public isViewOnly = (): boolean => {
+ return this.store.selectSnapshot((state) => state.workspace.isViewOnly);
+ }
+
+ public isDesigner = (): boolean => {
+ return this.store.selectSnapshot((state) => state.workspace.isDesigner);
+ }
+
+ public async initInputs(initInputsFor: string) {
+
+ if (!this.capabilitiesListUpdated){
+ // -- COMMON for both --
+ this.capabilityTypesList = [];
+ let capabilityTypesResult = await this.toscaTypesServiceNg2.fetchCapabilityTypes();
+ Object.keys(capabilityTypesResult).forEach(key => {this.capabilityTypesList.push(capabilityTypesResult[key])})
+ this.capabilitiesListUpdated = true;
+ }
+
+ if (initInputsFor === 'INPUTS_FOR_REQUIREMENTS') {
+ if (!this.requirementsListUpdated){
+ this.relationshipTypesList = [];
+ let relationshipTypesResult = await this.toscaTypesServiceNg2.fetchRelationshipTypes();
+ Object.keys(relationshipTypesResult).forEach(key => {this.relationshipTypesList.push(relationshipTypesResult[key])});
+ this.requirementsListUpdated = true;
+ }
+
+ if (!this.nodeTypeListUpdated){
+ this.nodeTypesList = [];
+ let nodeTypesResult = await this.toscaTypesServiceNg2.fetchNodeTypes();
+ Object.keys(nodeTypesResult).forEach(key => {this.nodeTypesList.push(nodeTypesResult[key])})
+ this.nodeTypeListUpdated = true;
+ }
+ }
+ }
+
+ getCapabilityTypesList() {
+ return this.capabilityTypesList;
+ }
+
+ getRelationsShipeTypeList() {
+ return this.relationshipTypesList;
+ }
+
+ getNodeTypesList() {
+ return this.nodeTypesList;
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.html b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.html
new file mode 100644
index 0000000000..330680d3ed
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.html
@@ -0,0 +1,91 @@
+<div class="requirement-editor">
+ <form class="w-sdc-form">
+ <div class="i-sdc-form-content-requirement-content">
+ <div class="content-row">
+ <div class="i-sdc-form-item">
+ <sdc-input
+ label="{{ 'REQ_NAME' | translate}}"
+ required="true"
+ testId="reqName"
+ [disabled]="isReadonly"
+ [(value)]="requirementData.name"
+ (valueChange)="validityChanged()">
+ </sdc-input>
+ </div>
+ </div>
+
+ <div class="group-with-border">
+ <div class="content-row i-sdc-form-item">
+ <sdc-dropdown
+ label="{{ 'REQ_RELATED_CAPABILITY' | translate }}"
+ testId="reqRelatedCapability"
+ required="true"
+ [disabled]="isReadonly"
+ [options]="capabilityTypesMappedList"
+ selectedOption="{{requirementData.capability}}"
+ [placeHolder] = "requirementData.capability"
+ (changed)="onCapabilityChanged($event)">
+ </sdc-dropdown>
+ </div>
+ <div class="content-row i-sdc-form-item">
+ <sdc-dropdown
+ label="{{ 'REQ_NODE' | translate }}"
+ testId="reqNode"
+ [disabled]="isReadonly"
+ [options]="nodeTypesMappedList"
+ selectedOption="{{requirementData.node}}"
+ [placeHolder] = "requirementData.node"
+ (changed)="onNodeChanged($event)">
+ </sdc-dropdown>
+ </div>
+ <div class="content-row i-sdc-form-item">
+ <sdc-dropdown
+ label="{{ 'REQ_RELATIONSHIP' | translate }}"
+ testId="reqRelationship"
+ [disabled]="isReadonly"
+ [options]="relationshipTypesMappedList"
+ selectedOption="{{requirementData.relationship}}"
+ [placeHolder] = "requirementData.relationship"
+ (changed)="onRelationshipChanged($event)">
+ </sdc-dropdown>
+ </div>
+ </div>
+
+ <label class="i-sdc-form-label occurrences-label"> {{ 'REQ_CAP_OCCURRENCES' | translate}} </label>
+ <div class="content-row occurrences-section">
+ <div class="min-occurrences-value">
+ <sdc-input
+ label="{{ 'REQ_CAP_OCCURRENCES_MIN' | translate}}"
+ testId="reqOccurrencesMin"
+ [disabled]="isReadonly"
+ [(value)]="requirementData.minOccurrences"
+ (valueChange)="validityChanged()"
+ type="number">
+ </sdc-input>
+ </div>
+ <div class="sdc-input">
+ <label class="sdc-input__label"> {{ 'REQ_CAP_OCCURRENCES_MAX' | translate}} </label>
+ <div class="max-occurrences-value">
+ <sdc-checkbox
+ class="checkbox-label unbounded-value"
+ testId="reqOccurrencesMaxUnbounded"
+ label="{{translatedUnboundTxt.toLowerCase()}}"
+ (checkedChange)="onUnboundedChanged()"
+ [checked]="isUnboundedChecked"
+ [disabled]="isReadonly">
+ </sdc-checkbox>
+ <sdc-input
+ *ngIf="!isUnboundedChecked"
+ testId="reqOccurrencesMax"
+ [disabled]="isReadonly"
+ [(value)]="requirementData.maxOccurrences"
+ (valueChange)="validityChanged()"
+ type="number">
+ </sdc-input>
+ </div>
+
+ </div>
+ </div>
+ </div>
+ </form>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.less b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.less
new file mode 100644
index 0000000000..6e50eb79f5
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.less
@@ -0,0 +1,35 @@
+@import '../../../../../../../assets/styles/variables.less';
+
+.requirement-editor {
+ .i-sdc-form-content-requirement-content {
+ padding: 10px 25px;
+
+ .group-with-border {
+ margin: 25px 0;
+ padding: 15px 0;
+ border-top: 1px solid @tlv_color_u;
+ border-bottom: 1px solid @tlv_color_u;
+ .content-row:not(:last-of-type) {
+ padding-bottom: 13px;
+ }
+ }
+
+ .occurrences-label {
+ font-family: @font-opensans-bold;
+ margin-bottom: 19px;
+ }
+ .occurrences-section, /deep/ .max-occurrences-value {
+ display: flex;
+ .min-occurrences-value {
+ padding-right: 30px;
+ }
+ .unbounded-value {
+ padding-top: 7px;
+ padding-right: 20px;
+ .sdc-checkbox__label {
+ text-transform: capitalize;
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.ts
new file mode 100644
index 0000000000..2c5c96f3da
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.ts
@@ -0,0 +1,90 @@
+import {Component} from '@angular/core';
+import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service";
+import {Requirement, RelationshipTypeModel, NodeTypeModel, CapabilityTypeModel} from 'app/models';
+import {TranslateService} from 'app/ng2/shared/translator/translate.service';
+import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component";
+import {Subject} from "rxjs";
+
+@Component({
+ selector: 'requirements-editor',
+ templateUrl: 'requirements-editor.component.html',
+ styleUrls: ['requirements-editor.component.less'],
+ providers: [ServiceServiceNg2, TranslateService]
+})
+
+export class RequirementsEditorComponent {
+
+ input: {
+ requirement: Requirement,
+ relationshipTypesList: Array<RelationshipTypeModel>;
+ nodeTypesList: Array<NodeTypeModel>;
+ capabilityTypesList: Array<CapabilityTypeModel>;
+ isReadonly: boolean;
+ };
+ requirementData: Requirement;
+ capabilityTypesMappedList: Array<DropdownValue>;
+ relationshipTypesMappedList: Array<DropdownValue>;
+ nodeTypesMappedList: Array<DropdownValue>;
+ isUnboundedChecked: boolean;
+ isReadonly: boolean;
+ translatedUnboundTxt: string;
+
+ public onValidationChange: Subject<boolean> = new Subject();
+
+ constructor(private translateService: TranslateService) {
+ }
+
+ ngOnInit() {
+ this.requirementData = new Requirement(this.input.requirement);
+ this.requirementData.minOccurrences = this.requirementData.minOccurrences || 0;
+ this.translatedUnboundTxt = '';
+ this.capabilityTypesMappedList = _.map(this.input.capabilityTypesList, capType => new DropdownValue(capType.toscaPresentation.type, capType.toscaPresentation.type));
+ this.relationshipTypesMappedList = _.map(this.input.relationshipTypesList, rType => new DropdownValue(rType.toscaPresentation.type, rType.toscaPresentation.type));
+ this.nodeTypesMappedList = _.map(this.input.nodeTypesList, nodeType => {
+ return new DropdownValue(
+ nodeType.componentMetadataDefinition.componentMetadataDataDefinition.toscaResourceName,
+ nodeType.componentMetadataDefinition.componentMetadataDataDefinition.toscaResourceName)
+ });
+ this.translateService.languageChangedObservable.subscribe(lang => {
+ this.translatedUnboundTxt = this.translateService.translate('REQ_CAP_OCCURRENCES_UNBOUNDED');
+ this.requirementData.maxOccurrences = this.requirementData.maxOccurrences || this.translatedUnboundTxt;
+ this.isUnboundedChecked = this.requirementData.maxOccurrences === this.translatedUnboundTxt;
+ });
+ this.isReadonly = this.input.isReadonly;
+ this.validityChanged();
+ }
+
+ onUnboundedChanged() {
+ this.isUnboundedChecked = !this.isUnboundedChecked;
+ this.requirementData.maxOccurrences = this.isUnboundedChecked ? this.translatedUnboundTxt : null;
+ this.validityChanged();
+ }
+
+ onCapabilityChanged(selectedCapability: DropdownValue) {
+ this.requirementData.capability = selectedCapability && selectedCapability.value;
+ this.validityChanged();
+ }
+
+ onNodeChanged(selectedNode: DropdownValue) {
+ this.requirementData.node = selectedNode && selectedNode.value;
+ }
+
+ onRelationshipChanged(selectedRelationship: DropdownValue) {
+ this.requirementData.relationship = selectedRelationship && selectedRelationship.value;
+ }
+
+ checkFormValidForSubmit() {
+ return this.requirementData.name && this.requirementData.name.length &&
+ this.requirementData.capability && this.requirementData.capability.length && !_.isEqual(this.requirementData.minOccurrences, "") && this.requirementData.minOccurrences >= 0 &&
+ (
+ this.isUnboundedChecked ||
+ (this.requirementData.maxOccurrences && (this.requirementData.minOccurrences <= parseInt(this.requirementData.maxOccurrences)))
+ );
+ }
+
+ validityChanged = () => {
+ let validState = this.checkFormValidForSubmit();
+ this.onValidationChange.next(validState);
+ }
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.module.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.module.ts
new file mode 100644
index 0000000000..b1d8db54aa
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.module.ts
@@ -0,0 +1,28 @@
+import {NgModule} from "@angular/core";
+import {CommonModule} from "@angular/common";
+import {RequirementsEditorComponent} from "./requirements-editor.component";
+import {FormsModule} from "@angular/forms";
+// import {FormElementsModule} from "../../../components/ui/form-components/form-elements.module";
+import {TranslateModule} from 'app/ng2/shared/translator/translate.module';
+import {SdcUiComponentsModule} from "onap-ui-angular/";
+import {FormElementsModule} from 'app/ng2/components/ui/form-components/form-elements.module';
+// import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index";
+
+@NgModule({
+ declarations: [
+ RequirementsEditorComponent
+ ],
+ imports: [CommonModule,
+ FormsModule,
+ FormElementsModule,
+ TranslateModule,
+ SdcUiComponentsModule
+ ],
+ exports: [],
+ entryComponents: [
+ RequirementsEditorComponent
+ ],
+ providers: []
+})
+export class RequirementsEditorModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirements.component.less b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirements.component.less
new file mode 100644
index 0000000000..19f1c9b55a
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirements.component.less
@@ -0,0 +1,4 @@
+/deep/ .importedFromFile {
+ background-color: #f8f8f8;
+ color: #959595;
+ } \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirments.components.html b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirments.components.html
new file mode 100644
index 0000000000..7606ed189a
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirments.components.html
@@ -0,0 +1,38 @@
+<div class="requirements-table">
+ <ngx-datatable #capabilitiesTable
+ columnMode="flex"
+ [headerHeight]="40"
+ [rowHeight]="35"
+ [rowClass]="getRowClass"
+ [rows]="requirements">
+ <ngx-datatable-column name="Name" [flexGrow]="1" [resizeable]="false" >
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <span [ngStyle]="{'cursor':row.isCreatedManually ? 'pointer' : 'null' }" data-tests-id="row.name" sdc-tooltip [tooltip-text]="row.name" [tooltip-placement]="3" (click)="editRequirement(row)">{{row.name}}</span>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column name="Capability" [flexGrow]="1" [resizeable]="false">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <span data-tests-id="row.capability" sdc-tooltip [tooltip-text]="row.capability" [tooltip-placement]="3">{{row.capability ? row.capability.substring("tosca.capabilities.".length) : ''}}</span>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column name="Node" [flexGrow]="1" [resizeable]="false">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <span data-tests-id="row.node" sdc-tooltip [tooltip-text]="row.node" [tooltip-placement]="3">{{row.node ? row.node.substring("tosca.nodes.".length) : ''}}</span>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column name="Relationship" [flexGrow]="1" [resizeable]="false">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <span data-tests-id="row.relationship" sdc-tooltip [tooltip-text]="row.relationship" [tooltip-placement]="3">{{row.relationship ? row.relationship.substring("tosca.relationships.".length): ''}}</span>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column name="Connected To" [flexGrow]="1" [resizeable]="false">
+ </ngx-datatable-column>
+ <ngx-datatable-column name="Occurrences" [flexGrow]="1" [prop]="'minOccurrences'" [resizeable]="false">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <span data-tests-id="row.minOccurrences+','+row.maxOccurrences" sdc-tooltip
+ [tooltip-text]="row.minOccurrences+','+row.maxOccurrences" [tooltip-placement]="3">
+ {{row.minOccurrences}},{{row.maxOccurrences}}</span>
+ </ng-template>
+ </ngx-datatable-column>
+ </ngx-datatable>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirments.components.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirments.components.ts
new file mode 100644
index 0000000000..b65489ce4e
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirments.components.ts
@@ -0,0 +1,103 @@
+import {Input, Component, OnInit} from "@angular/core";
+import {Requirement, RequirementUI} from "../../../../../models/requirement";
+import {RequirementsEditorComponent} from "./requirementEditor/requirements-editor.component";
+import {WorkspaceService} from "../../workspace.service";
+import {TopologyTemplateService} from "../../../../services/component-services/topology-template.service";
+import {ReqAndCapabilitiesService} from "../req-and-capabilities.service";
+import {EventListenerService} from "../../../../../services/event-listener-service";
+import {ModalComponent} from "onap-ui-angular/dist/modals/modal.component";
+import {SdcUiServices} from "onap-ui-angular";
+import sortedIndexBy = require("lodash/sortedIndexBy");
+
+@Component({
+ selector: 'requirments',
+ templateUrl: './requirments.components.html',
+ styleUrls: ['../../../../../../assets/styles/table-style.less', './requirements.component.less']
+})
+
+
+
+export class RequirmentsComponent implements OnInit {
+ @Input() public requirements: Array<RequirementUI>;
+ private customModalInstance: ModalComponent;
+
+ constructor(
+ private workspaceService: WorkspaceService,
+ private loaderService: SdcUiServices.LoaderService,
+ private topologyTemplateService: TopologyTemplateService,
+ private reqAndCapabilitiesService : ReqAndCapabilitiesService,
+ private modalService: SdcUiServices.ModalService,
+ private eventListenerService: EventListenerService) {
+ }
+
+
+ ngOnInit(): void {
+ let isCreatedManually: RequirementUI[] = [];
+ let isImportedFromFile: RequirementUI[] = [];
+
+ isCreatedManually = this.requirements.filter((requirement) => requirement.isCreatedManually);
+ isImportedFromFile = this.requirements.filter((requirement) => !requirement.isCreatedManually);
+
+ this.requirements = [];
+
+ isCreatedManually.map((requirement) => this.requirements.push(requirement));
+ isImportedFromFile.map((requirement) => this.requirements.push(requirement));
+
+ }
+
+
+
+ editRequirement(req) {
+
+ let modalConfig = {
+ size: 'md',
+ title: 'Update Requirement',
+ type: 'custom',
+ buttons: [
+ {
+ id: 'saveButton',
+ text: ('Update'),
+ size: "'x-small'",
+ callback: () => this.updateRequirement(),
+ closeModal: true
+ },
+ {text: "Cancel", size: "'x-small'", closeModal: true}]
+ };
+ let modalInputs = {
+ requirement: req,
+ relationshipTypesList: this.reqAndCapabilitiesService.getRelationsShipeTypeList(),
+ nodeTypesList: this.reqAndCapabilitiesService.getNodeTypesList(),
+ capabilityTypesList: this.reqAndCapabilitiesService.getCapabilityTypesList(),
+ // isReadonly: this.$scope.isViewMode() || !this.$scope.isDesigner(),
+ };
+
+ this.customModalInstance = this.modalService.openCustomModal(modalConfig, RequirementsEditorComponent, {input: modalInputs});
+ this.customModalInstance.innerModalContent.instance.
+ onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('saveButton').disabled = !isValid);
+
+ }
+
+ private updateRequirement() {
+ const requirement = this.customModalInstance.innerModalContent.instance.requirementData;
+ this.loaderService.activate();
+ if (requirement.uniqueId) {
+ this.topologyTemplateService.updateRequirement(this.workspaceService.metadata.getTypeUrl(), this.workspaceService.metadata.uniqueId, requirement).subscribe(result => {
+ let index = this.requirements.findIndex(req => result[0].uniqueId === req.uniqueId);
+ this.requirements[index] = new RequirementUI(result[0], this.workspaceService.metadata.uniqueId);
+ this.eventListenerService.notifyObservers('REQUIREMENTS_UPDATED');
+ this.loaderService.deactivate();
+ }, () => {
+ this.loaderService.deactivate();
+ });
+ }
+ }
+
+ getRowClass(row) {
+ if (!row.isCreatedManually) {
+ return {
+ 'importedFromFile': true
+ };
+ }
+ }
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/__snapshots__/tosca-artifact-page.spec.ts.snap b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/__snapshots__/tosca-artifact-page.spec.ts.snap
new file mode 100644
index 0000000000..14146d51d2
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/__snapshots__/tosca-artifact-page.spec.ts.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`tosca artifacts page should match current snapshot of tosca artifact pages component 1`] = `
+<tosca-artifact-page
+ serviceLoader={[Function Object]}
+ store={[Function Store]}
+ table={[Function DatatableComponent]}
+ workspaceService={[Function Object]}
+>
+ <div
+ class="tosca-artifact-page"
+ >
+ <ngx-datatable
+ class="ngx-datatable"
+ columnmode="flex"
+ >
+ <div
+ visibilityobserver=""
+ >
+
+ <datatable-body
+ class="datatable-body"
+ >
+ <datatable-selection>
+
+
+
+ </datatable-selection>
+ </datatable-body>
+
+ </div>
+ </ngx-datatable>
+ </div>
+</tosca-artifact-page>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.html b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.html
new file mode 100644
index 0000000000..fece92ee37
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.html
@@ -0,0 +1,50 @@
+<div class="tosca-artifact-page">
+ <ngx-datatable
+ columnMode="flex"
+ [headerHeight]="40"
+ [rowHeight]="35"
+ [reorderable]="false"
+ [swapColumns]="false"
+ [rows]="toscaArtifacts$ | async"
+ [sorts]="[{prop: 'artifactDisplayName', dir: 'desc'}]"
+ #toscaArtifactsTable
+ (activate)="onActivate($event)">
+ <ngx-datatable-row-detail [rowHeight]="80">
+ <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template>
+ <div>Label: {{row.artifactLabel}}</div>
+ <div>UUID: {{row.artifactUUID}}</div>
+ <div>Description: {{row.description}}</div>
+ </ng-template>
+ </ngx-datatable-row-detail>
+ <ngx-datatable-column [resizeable]="false" name="Name" [flexGrow]="3"
+ [prop]="'artifactDisplayName'">
+ <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.artifactDisplayName }}</span>
+ </div>
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false"name="Type" [flexGrow]="3">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ {{row.artifactType}}
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false" name="Version" [flexGrow]="1">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ {{ row.artifactVersion }}
+ </ng-template>
+ </ngx-datatable-column>
+ <ngx-datatable-column [resizeable]="false"[flexGrow]="1">
+ <ng-template ngx-datatable-cell-template let-row="row">
+ <div class="download-artifact-button">
+ <download-artifact [artifact]="row" [componentId]="componentId"
+ [componentType]="componentType"
+ testId="download_{{row.artifactDisplayName}}"></download-artifact>
+ </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/tosca-artifacts/tosca-artifact-page.component.less b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.less
new file mode 100644
index 0000000000..9c5dd47585
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.less
@@ -0,0 +1,7 @@
+.tosca-artifact-page {
+ .download-artifact-button {
+ text-align: center;
+ padding-top: 4px;
+
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.ts b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.ts
new file mode 100644
index 0000000000..e74e5db668
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.ts
@@ -0,0 +1,46 @@
+import {Component, OnInit, ViewChild} from "@angular/core";
+import {WorkspaceService} from "../workspace.service";
+import {SdcUiServices} from "onap-ui-angular";
+import {ArtifactModel} from "../../../../models";
+import {Select, Store} from "@ngxs/store";
+import {WorkspaceState} from "../../../store/states/workspace.state";
+import * as _ from "lodash";
+import {ArtifactGroupType, COMPONENT_FIELDS} from "../../../../utils";
+import {GetArtifactsByTypeAction} from "../../../store/actions/artifacts.action";
+import {Observable} from "rxjs/index";
+import {ArtifactsState} from "../../../store/states/artifacts.state";
+import {ArtifactType} from "../../../../utils/constants";
+import {map} from "rxjs/operators";
+
+@Component({
+ selector: 'tosca-artifact-page',
+
+ templateUrl: './tosca-artifact-page.component.html',
+ styleUrls: ['./tosca-artifact-page.component.less', '../../../../../assets/styles/table-style.less']
+})
+export class ToscaArtifactPageComponent implements OnInit {
+
+ @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean;
+ @ViewChild('toscaArtifactsTable') table: any;
+ public toscaArtifacts$: Observable<ArtifactModel[]>;
+ public componentId: string;
+ public componentType:string;
+
+ constructor(private serviceLoader: SdcUiServices.LoaderService, private workspaceService: WorkspaceService, private store: Store) {
+ }
+
+
+ ngOnInit(): void {
+ this.componentId = this.workspaceService.metadata.uniqueId;
+ this.componentType = this.workspaceService.metadata.componentType;
+
+ this.store.dispatch(new GetArtifactsByTypeAction({componentType:this.componentType, componentId:this.componentId, artifactType:ArtifactGroupType.TOSCA}));
+ this.toscaArtifacts$ = this.store.select(ArtifactsState.getArtifactsByType).pipe(map(filterFn => filterFn(ArtifactGroupType.TOSCA)));
+ }
+
+ onActivate(event) {
+ if(event.type === 'click'){
+ this.table.rowDetail.toggleExpandRow(event.row);
+ }
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.module.ts b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.module.ts
new file mode 100644
index 0000000000..00c7b0b371
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.module.ts
@@ -0,0 +1,28 @@
+import {CommonModule} from "@angular/common";
+import {NgModule} from "@angular/core";
+import {SdcUiComponentsModule} from "onap-ui-angular";
+import {GlobalPipesModule} from "../../../pipes/global-pipes.module";
+import {NgxDatatableModule} from "@swimlane/ngx-datatable";
+import {ToscaArtifactPageComponent} from "./tosca-artifact-page.component";
+import {UiElementsModule} from "../../../components/ui/ui-elements.module";
+
+@NgModule({
+ declarations: [
+ ToscaArtifactPageComponent
+ ],
+ imports: [
+ CommonModule,
+ SdcUiComponentsModule,
+ NgxDatatableModule,
+ UiElementsModule
+ ],
+ exports: [
+ ToscaArtifactPageComponent
+ ],
+ entryComponents: [
+ ToscaArtifactPageComponent
+ ],
+
+})
+export class ToscaArtifactPageModule {
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.spec.ts
new file mode 100644
index 0000000000..af3558e15b
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.spec.ts
@@ -0,0 +1,71 @@
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {NO_ERRORS_SCHEMA} from "@angular/core";
+import {ToscaArtifactPageComponent} from "./tosca-artifact-page.component";
+import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper";
+import {NgxDatatableModule} from "@swimlane/ngx-datatable";
+import {WorkspaceService} from "../workspace.service";
+import {SdcUiServices} from "onap-ui-angular";
+import {TopologyTemplateService} from "../../../services/component-services/topology-template.service";
+import {Observable} from "rxjs/Observable";
+import {ComponentMetadata} from "../../../../models/component-metadata";
+import 'rxjs/add/observable/of';
+import {NgxsModule, Store} from "@ngxs/store";
+import {ArtifactsState} from "../../../store/states/artifacts.state";
+import {toscaArtifactMock} from "../../../../../jest/mocks/artifacts-mock";
+
+describe('tosca artifacts page', () => {
+
+ let fixture: ComponentFixture<ToscaArtifactPageComponent>;
+ let topologyTemplateServiceMock: Partial<TopologyTemplateService>;
+ let workspaceServiceMock: Partial<WorkspaceService>;
+ let loaderServiceMock: Partial<SdcUiServices.LoaderService>;
+ let store: Store;
+
+
+ beforeEach(
+ async(() => {
+
+ topologyTemplateServiceMock = {
+ getArtifactsByType: jest.fn().mockImplementation((componentType, id, artifactType) => Observable.of(toscaArtifactMock))
+ };
+ workspaceServiceMock = {metadata: <ComponentMetadata>{uniqueId: 'service_unique_id', componentType: 'SERVICE'}}
+
+ loaderServiceMock = {
+ activate : jest.fn(),
+ deactivate: jest.fn()
+ }
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [ToscaArtifactPageComponent],
+ imports: [NgxDatatableModule, NgxsModule.forRoot([ArtifactsState])],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: WorkspaceService, useValue: workspaceServiceMock},
+ {provide: TopologyTemplateService, useValue: topologyTemplateServiceMock},
+ {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock }
+ ],
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(ToscaArtifactPageComponent);
+ store = testBed.get(Store);
+ });
+ })
+ );
+
+ it('should match current snapshot of tosca artifact pages component', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+ it('should see exactly 2 tosca artifacts', () => {
+ fixture.componentInstance.ngOnInit();
+ fixture.componentInstance.toscaArtifacts$.subscribe((artifacts)=> {
+ expect(artifacts.length).toEqual(2);
+ })
+ store.selectOnce(state => state.artifacts.toscaArtifacts).subscribe(artifacts => {
+ expect(artifacts.length).toEqual(9);
+ });
+ })
+
+}); \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/workspace/workspace-ng1-bridge-service.ts b/catalog-ui/src/app/ng2/pages/workspace/workspace-ng1-bridge-service.ts
new file mode 100644
index 0000000000..3d93b459a2
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/workspace-ng1-bridge-service.ts
@@ -0,0 +1,37 @@
+/**
+ * Created by ob0695 on 6/24/2018.
+ */
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 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.
+ * ============LICENSE_END=========================================================
+ */
+import {Store} from "@ngxs/store";
+import {Injectable} from "@angular/core";
+import {UpdateIsViewOnly} from "../../store/actions/workspace.action";
+
+@Injectable()
+export class WorkspaceNg1BridgeService {
+
+ constructor(private store: Store) {
+ };
+
+ public updateIsViewOnly = (isViewOnly: boolean):void => {
+ this.store.dispatch(new UpdateIsViewOnly(isViewOnly));
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/workspace.component.ts b/catalog-ui/src/app/ng2/pages/workspace/workspace.component.ts
new file mode 100644
index 0000000000..a209406a53
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/workspace.component.ts
@@ -0,0 +1,3 @@
+/**
+ * Created by ob0695 on 6/11/2018.
+ */
diff --git a/catalog-ui/src/app/ng2/pages/workspace/workspace.module.ts b/catalog-ui/src/app/ng2/pages/workspace/workspace.module.ts
new file mode 100644
index 0000000000..cb646379d2
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/workspace.module.ts
@@ -0,0 +1,50 @@
+/**
+ * Created by ob0695 on 6/4/2018.
+ */
+/**
+ * Created by ob0695 on 6/4/2018.
+ */
+import {NgModule} from "@angular/core";
+import {CompositionPageModule} from "../composition/composition-page.module";
+
+import {NgxsModule} from "@ngxs/store";
+import {TopologyTemplateService} from "../../services/component-services/topology-template.service";
+import {WorkspaceState} from "../../store/states/workspace.state";
+import {WorkspaceService} from "./workspace.service";
+import {DeploymentPageModule} from "./deployment/deployment-page.module";
+import {ToscaArtifactPageModule} from "./tosca-artifacts/tosca-artifact-page.module";
+import {InformationArtifactPageModule} from "./information-artifact/information-artifact-page.module";
+import { reqAndCapabilitiesModule } from "./req-and-capabilities/req-and-capabilities.module";
+import {AttributesModule} from "./attributes/attributes.module";
+import {ArtifactsState} from "../../store/states/artifacts.state";
+import {InstanceArtifactsState} from "../../store/states/instance-artifacts.state";
+import {DeploymentArtifactsPageModule} from "./deployment-artifacts/deployment-artifacts-page.module";
+import { DistributionModule } from './disribution/distribution.module';
+import { ActivityLogModule } from './activity-log/activity-log.module';
+
+@NgModule({
+ declarations: [],
+ imports: [
+ DeploymentPageModule,
+ CompositionPageModule,
+ AttributesModule,
+ reqAndCapabilitiesModule,
+ ToscaArtifactPageModule,
+ DeploymentArtifactsPageModule,
+ InformationArtifactPageModule,
+ DistributionModule,
+ ActivityLogModule,
+ NgxsModule.forFeature([WorkspaceState, ArtifactsState, InstanceArtifactsState])
+ ],
+
+ exports: [],
+ entryComponents: [],
+ providers: [TopologyTemplateService, WorkspaceService]
+})
+
+export class WorkspaceModule {
+
+ constructor() {
+
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/workspace/workspace.service.ts b/catalog-ui/src/app/ng2/pages/workspace/workspace.service.ts
new file mode 100644
index 0000000000..9f985016ec
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/workspace/workspace.service.ts
@@ -0,0 +1,70 @@
+/**
+ * Created by ob0695 on 6/5/2018.
+ */
+/*-
+ * ============LICENSE_START=======================================================
+ * SDC
+ * ================================================================================
+ * Copyright (C) 2017 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.
+ * ============LICENSE_END=========================================================
+ */
+
+/**
+ * Created by rc2122 on 5/23/2017.
+ */
+import { Injectable } from '@angular/core';
+import {WorkspaceMode, ComponentState, Role} from "../../../utils/constants";
+import {Component as TopologyTemplate, ComponentMetadata} from "app/models";
+import {CacheService} from "../../services/cache.service";
+import {IComponentMetadata} from "../../../models/component-metadata";
+import {ComponentType} from "../../../utils";
+
+@Injectable()
+export class WorkspaceService {
+
+ public metadata:ComponentMetadata;
+
+ constructor(private cacheService:CacheService) {
+
+ }
+
+ public setComponentMetadata = (metadata: ComponentMetadata) => {
+ this.metadata = metadata;
+ }
+
+ public getMetadataType(): string {
+ switch (this.metadata.componentType) {
+ case ComponentType.SERVICE:
+ return ComponentType.SERVICE;
+ default:
+ return this.metadata.resourceType;
+ }
+ }
+
+ public getComponentMode = (component:TopologyTemplate):WorkspaceMode => {//return if is edit or view for resource or service
+ let mode = WorkspaceMode.VIEW;
+
+ let user = this.cacheService.get("user");
+ if (component.lifecycleState === ComponentState.NOT_CERTIFIED_CHECKOUT &&
+ component.lastUpdaterUserId === user.userId) {
+ if ((component.isService() || component.isResource()) && user.role == Role.DESIGNER) {
+ mode = WorkspaceMode.EDIT;
+ }
+ }
+ return mode;
+ }
+}
+
+ \ No newline at end of file