summaryrefslogtreecommitdiffstats
path: root/catalog-ui/src/app/ng2/pages/composition
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/composition
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/composition')
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/common/common-graph-data.service.ts64
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/common/store/graph.actions.ts33
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/common/store/graph.state.ts170
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/composition-page.component.html8
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/composition-page.component.less26
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/composition-page.component.ts47
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/composition-page.module.ts30
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/composition.service.ts59
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.html1
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.less13
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.spec.ts92
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.ts127
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.module.ts15
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.service.ts8
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.html23
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.less42
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.ts25
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.module.ts30
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/__snapshots__/zone-container.component.spec.ts.snap35
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.html30
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.less62
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.spec.ts46
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.ts35
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.html27
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.less135
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.spec.ts132
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.ts128
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zones-module.ts15
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/common/common-graph-utils.ts304
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/common/image-creator.service.ts92
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.spec.ts37
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.ts362
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/common/style/module-node-style.ts103
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.html57
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.less93
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.spec.ts354
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts768
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.module.ts55
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.html (renamed from catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.component.html)15
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.less4
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.ts10
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.html52
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.less53
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.ts37
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.module.ts43
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.spec.ts85
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.ts58
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/__snapshots__/from-node-step.component.spec.ts.snap12
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.html22
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.spec.ts114
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.ts44
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.html28
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.less15
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.ts68
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/__snapshots__/to-node-step.component.spec.ts.snap14
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.html22
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.spec.ts71
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.ts65
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/__snapshots__/link-row.component.spec.ts.snap29
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.html61
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.less21
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.spec.ts478
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.ts104
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link.model.ts (renamed from catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-information-tab.component.ts)29
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.html55
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.less45
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.ts149
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.module.ts25
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.html27
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.less24
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.ts142
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.module.ts22
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.html21
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.less24
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.ts70
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.module.ts17
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-general-utils.ts268
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-links-utils.ts342
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.spec.ts158
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.ts202
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-palette-utils.ts233
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-service-path-utils.ts148
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-zone-utils.ts204
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/index.ts29
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.spec.ts342
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.ts196
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/__snapshots__/palette.component.spec.ts.snap51
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.html (renamed from catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.component.html)16
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.less5
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.ts71
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.module.ts16
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette-element/__snapshots__/palette-element.component.spec.ts.snap29
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.html11
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.less32
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.spec.ts30
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.ts (renamed from catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-information-tab.component.ts)24
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.html30
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.less37
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.ts98
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette.component.html41
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette.component.less84
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette.component.spec.ts102
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette.component.ts172
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/palette.module.ts25
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/services/palette.service.spec.ts41
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/palette/services/palette.service.ts98
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/__snapshots__/composition-panel.component.spec.ts.snap18
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.html21
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.less27
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.spec.ts228
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.ts171
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.module.ts106
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.html28
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.less3
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.ts25
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.html43
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.less14
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.spec.ts123
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.ts184
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.module.ts15
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/__snapshots__/artifact-tab.component.spec.ts.snap50
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifact-tab.component.spec.ts303
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.html119
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.less169
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.ts204
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.html (renamed from catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.html)6
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.spec.ts127
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.ts158
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component.html (renamed from catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.html)4
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component.ts (renamed from catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.ts)30
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-information-tab.component.html47
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.less13
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.ts133
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.html39
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.ts64
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.component.ts67
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.module.ts71
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/__snapshots__/info-tab.component.spec.ts.snap66
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.html174
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.less51
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.spec.ts98
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.ts189
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/panel-tab.component.ts55
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/panel-tabs.less65
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-information-tab.component.html50
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.less0
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.component.ts72
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.module.ts68
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.less12
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.html (renamed from catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.html)6
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.less (renamed from catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/base/base-tab.component.less)49
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.spec.ts113
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.ts (renamed from catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.ts)105
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.html97
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.less66
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.ts212
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.html36
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.less57
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.ts165
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component.html20
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component.ts40
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.html15
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.less (renamed from catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.less)0
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.ts89
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.html18
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.less3
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.ts95
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel.component.html50
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel.component.less11
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel.component.ts60
-rw-r--r--catalog-ui/src/app/ng2/pages/composition/panel/panel.module.ts54
171 files changed, 12800 insertions, 1064 deletions
diff --git a/catalog-ui/src/app/ng2/pages/composition/common/common-graph-data.service.ts b/catalog-ui/src/app/ng2/pages/composition/common/common-graph-data.service.ts
new file mode 100644
index 0000000000..d4caa5e9ed
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/common/common-graph-data.service.ts
@@ -0,0 +1,64 @@
+import {Injectable} from "@angular/core";
+import 'rxjs/add/observable/forkJoin';
+import {ComponentInstance} from "../../../../models/componentsInstances/componentInstance";
+import {SelectedComponentType} from "./store/graph.actions";
+import {RelationshipModel} from "../../../../models/graph/relationship";
+
+@Injectable()
+export class CommonGraphDataService {
+
+ public componentInstances: Array<ComponentInstance>;
+ public componentInstancesRelations: RelationshipModel[];
+ public selectedComponentType: SelectedComponentType;
+
+ constructor() {
+ }
+
+ //------------------------ RELATIONS ---------------------------------//
+ public setRelations = (componentInstancesRelations: RelationshipModel[]) => {
+ this.componentInstancesRelations = this.componentInstancesRelations;
+ }
+
+ public getRelations = (): RelationshipModel[] => {
+ return this.componentInstancesRelations;
+ }
+
+ public addRelation = (componentInstancesRelations: RelationshipModel) => {
+ this.componentInstancesRelations.push(componentInstancesRelations);
+ }
+
+ public deleteRelation(relationToDelete: RelationshipModel) {
+ this.componentInstancesRelations = _.filter(this.componentInstancesRelations, (relationship: RelationshipModel) => {
+ return relationship.relationships[0].relation.id !== relationToDelete.relationships[0].relation.id;
+ });
+ }
+
+ //---------------------------- COMPONENT INSTANCES ------------------------------------//
+ public getComponentInstances = (): Array<ComponentInstance> => {
+ return this.componentInstances;
+ }
+
+ public addComponentInstance = (instance: ComponentInstance) => {
+ return this.componentInstances.push(instance);
+ }
+
+ public updateComponentInstances = (componentInstances: ComponentInstance[]) => {
+ _.unionBy(this.componentInstances, componentInstances, 'uniqueId');
+ }
+
+ public updateInstance = (instance: ComponentInstance) => {
+ this.componentInstances = this.componentInstances.map(componentInstance => instance.uniqueId === componentInstance.uniqueId? instance : componentInstance);
+ }
+
+ public deleteComponentInstance(instanceToDelete: string) {
+ this.componentInstances = _.filter(this.componentInstances, (instance: ComponentInstance) => {
+ return instance.uniqueId !== instanceToDelete;
+ });
+ }
+
+ //----------------------------SELECTED COMPONENT -----------------------//
+
+ public setSelectedComponentType = (selectedType: SelectedComponentType) => {
+ this.selectedComponentType = selectedType;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/common/store/graph.actions.ts b/catalog-ui/src/app/ng2/pages/composition/common/store/graph.actions.ts
new file mode 100644
index 0000000000..9bd5d0db62
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/common/store/graph.actions.ts
@@ -0,0 +1,33 @@
+export enum SelectedComponentType {
+ COMPONENT_INSTANCE = "COMPONENT_INSTANCE",
+ GROUP = "GROUP",
+ POLICY = "POLICY",
+ TOPOLOGY_TEMPLATE = "TOPOLOGY_TEMPLATE"
+}
+
+export class UpdateSelectedComponentAction {
+ static readonly type = '[COMPOSITION] UpdateSelectedComponent';
+
+ constructor(public payload: {uniqueId?: string, type?: string}) {
+ }
+}
+
+export class SetSelectedComponentAction {
+ static readonly type = '[COMPOSITION] SetSelectedComponent';
+
+ constructor(public payload: {component?: any, type?: SelectedComponentType}) {
+ }
+}
+
+export class OnSidebarOpenOrCloseAction {
+ static readonly type = '[COMPOSITION] OnSidebarOpenOrCloseAction';
+
+ constructor() {
+ }
+}
+
+export class TogglePanelLoadingAction {
+ static readonly type = '[COMPOSITION] TogglePanelLoading';
+ constructor(public payload: { isLoading: boolean}) {
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/common/store/graph.state.ts b/catalog-ui/src/app/ng2/pages/composition/common/store/graph.state.ts
new file mode 100644
index 0000000000..d58bb446df
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/common/store/graph.state.ts
@@ -0,0 +1,170 @@
+import { Action, Selector, State, StateContext} from '@ngxs/store';
+import {
+ OnSidebarOpenOrCloseAction,
+ SelectedComponentType,
+ SetSelectedComponentAction,
+ TogglePanelLoadingAction
+} from "./graph.actions";
+import { PolicyInstance, GroupInstance, Component as TopologyTemplate, ComponentInstance, LeftPaletteComponent, FullComponentInstance} from "app/models";
+import { TopologyTemplateService } from "app/ng2/services/component-services/topology-template.service";
+import { tap } from "rxjs/operators";
+import { CompositionService } from "app/ng2/pages/composition/composition.service";
+import {GroupsService} from "../../../../services/groups.service";
+import {PoliciesService} from "../../../../services/policies.service";
+import {WorkspaceService} from "../../../workspace/workspace.service";
+
+export class CompositionStateModel {
+
+ isViewOnly?: boolean;
+ panelLoading?: boolean;
+ selectedComponentType?: SelectedComponentType;
+ selectedComponent?: PolicyInstance | GroupInstance | TopologyTemplate | ComponentInstance;
+ withSidebar?: boolean;
+}
+
+@State<CompositionStateModel>({
+ name: 'composition',
+ defaults: {
+ withSidebar: true
+ }
+})
+export class GraphState {
+
+ constructor(private topologyTemplateService: TopologyTemplateService,
+ private compositionService: CompositionService,
+ private policiesService:PoliciesService, private groupsService:GroupsService,
+ private workspaceService: WorkspaceService) {}
+
+ @Action(SetSelectedComponentAction)
+ setSelectedComponent({dispatch, getState, patchState}:StateContext<CompositionStateModel>, action: SetSelectedComponentAction) {
+
+ const state:CompositionStateModel = getState();
+
+ patchState({ panelLoading: true });
+
+ if(action.payload.component instanceof ComponentInstance){
+ let originComponent = this.compositionService.getOriginComponentById(action.payload.component.getComponentUid());
+ if(!originComponent) {
+ return this.topologyTemplateService.getFullComponent(action.payload.component.originType, action.payload.component.getComponentUid())
+ .pipe(tap(resp => {
+ this.compositionService.addOriginComponent(resp);
+ this.compositionService.setSelectedComponentType(SelectedComponentType.COMPONENT_INSTANCE);
+ patchState({
+ selectedComponent: new FullComponentInstance(action.payload.component, resp),
+ selectedComponentType: action.payload.type,
+ panelLoading: false
+ });
+ }, err => {
+ patchState({
+ panelLoading: false
+ })
+ }
+ ));
+ } else {
+ patchState({
+ selectedComponent: new FullComponentInstance(action.payload.component, originComponent),
+ selectedComponentType: action.payload.type,
+ panelLoading: false
+ });
+ }
+ } else if (action.payload.component instanceof PolicyInstance) {
+ let topologyTemplate = this.workspaceService.metadata;
+ return this.policiesService.getSpecificPolicy(topologyTemplate.componentType, topologyTemplate.uniqueId, action.payload.component.uniqueId).pipe(tap(resp =>
+ {
+ this.compositionService.updatePolicy(resp);
+ patchState({
+ selectedComponent: resp,
+ selectedComponentType: action.payload.type,
+ panelLoading: false
+ })
+ }, err => {
+ patchState({
+ panelLoading: false
+ })
+ }
+ ));
+
+ } else if (action.payload.component instanceof GroupInstance) {
+ let topologyTemplate = this.workspaceService.metadata;
+ return this.groupsService.getSpecificGroup(topologyTemplate.componentType, topologyTemplate.uniqueId, action.payload.component.uniqueId).pipe(tap(resp => {
+ this.compositionService.updateGroup(resp);
+ patchState({
+ selectedComponent: resp,
+ selectedComponentType: action.payload.type,
+ panelLoading: false
+ });
+ }, err => {
+ patchState({
+ panelLoading: false
+ })
+ }
+ ));
+ } else { //TopologyTemplate
+ patchState({
+ selectedComponent: action.payload.component,
+ selectedComponentType: action.payload.type,
+ panelLoading: false
+ })
+ }
+ }
+
+
+ // @Action(UpdateSelectedComponentNameAction)
+ // UpdateSelectedComponentNameAction({patchState}:StateContext<CompositionStateModel>, action: UpdateSelectedComponentNameAction) {
+
+ // switch(action.payload.type){
+ // case SelectedComponentType.COMPONENT_INSTANCE:
+ // this.store.dispatch(new UpdateComponentInstancesAction([action.payload.component]));
+ // break;
+ // case SelectedComponentType.POLICY:
+ // this.store.dispatch(new UpdatePolicyNameAction(action.payload.uniqueId, action.payload.newName));
+ // break;
+ // case SelectedComponentType.GROUP:
+ // this.store.dispatch(new UpdateGroupInstancesAction)
+
+ // }
+ // if(action.payload.type === SelectedComponentType.COMPONENT_INSTANCE){
+
+ // }
+
+ // }
+
+ @Selector()
+ static getSelectedComponent(state:CompositionStateModel) {
+ return state.selectedComponent;
+ }
+
+ @Selector()
+ static getSelectedComponentId(state:CompositionStateModel) {
+ return state.selectedComponent.uniqueId;
+ }
+
+ @Selector()
+ static getSelectedComponentType(state:CompositionStateModel) {
+ return state.selectedComponentType;
+ }
+
+
+ @Action(OnSidebarOpenOrCloseAction)
+ onSidebarOpenOrCloseAction({getState, setState}:StateContext<CompositionStateModel>) {
+ const state:CompositionStateModel = getState();
+
+ setState({
+ ...state,
+ withSidebar: !state.withSidebar
+ });
+ }
+
+ @Action(TogglePanelLoadingAction)
+ TogglePanelLoading({patchState}:StateContext<CompositionStateModel>, action: TogglePanelLoadingAction) {
+
+ patchState({
+ panelLoading: action.payload.isLoading
+ });
+ }
+
+ @Selector() static withSidebar(state):boolean {
+ return state.withSidebar;
+ }
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/composition-page.component.html b/catalog-ui/src/app/ng2/pages/composition/composition-page.component.html
new file mode 100644
index 0000000000..e1851d5c0c
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/composition-page.component.html
@@ -0,0 +1,8 @@
+<div class="workspace-composition-page">
+ <div class="composition-graph">
+ <composition-palette></composition-palette>
+ <app-palette-popup-panel></app-palette-popup-panel>
+ <composition-graph dndDropzone [dndAllowExternal]=true [topologyTemplate]="topologyTemplate" [testId]="'canvas'"></composition-graph>
+ <ng2-composition-panel [topologyTemplate]="topologyTemplate"></ng2-composition-panel>
+ </div>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/composition-page.component.less b/catalog-ui/src/app/ng2/pages/composition/composition-page.component.less
new file mode 100644
index 0000000000..a80333e2be
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/composition-page.component.less
@@ -0,0 +1,26 @@
+@import "./../../../../assets/styles/override";
+.workspace-composition-page {
+ height:100%;
+ display: block;
+ text-align: left;
+ align-items: left;
+ padding: 0;
+
+ .composition-graph {
+ height:100%;
+ background-color: @sdcui_color_white;
+ bottom: 0;
+ display:flex;
+ flex-direction: row;
+
+ .view-mode{
+ background-color: #f8f8f8;
+ border:0;
+ }
+ }
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/composition-page.component.ts b/catalog-ui/src/app/ng2/pages/composition/composition-page.component.ts
new file mode 100644
index 0000000000..ed1b82e1df
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/composition-page.component.ts
@@ -0,0 +1,47 @@
+/*-
+ * ============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, Inject, OnDestroy, OnInit } from '@angular/core';
+import { Component as TopologyTemplate } from 'app/models';
+import * as Constants from 'constants';
+import { EventListenerService } from '../../../services/event-listener-service';
+import { EVENTS } from '../../../utils';
+
+@Component({
+ templateUrl: './composition-page.component.html',
+ styleUrls: ['composition-page.component.less']
+})
+export class CompositionPageComponent implements OnInit, OnDestroy {
+
+ private topologyTemplate: TopologyTemplate;
+
+ constructor(@Inject('$stateParams') private stateParams, private eventListenerService: EventListenerService) {
+ this.topologyTemplate = stateParams.component;
+ }
+
+ ngOnInit(): void {
+ this.eventListenerService.registerObserverCallback(EVENTS.ON_CHECKOUT, (comp) => {
+ this.topologyTemplate = comp;
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.eventListenerService.unRegisterObserver(EVENTS.ON_CHECKOUT);
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/composition-page.module.ts b/catalog-ui/src/app/ng2/pages/composition/composition-page.module.ts
new file mode 100644
index 0000000000..d0ca05b2be
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/composition-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 {CompositionGraphModule} from "./graph/composition-graph.module";
+import {CompositionPageComponent} from "./composition-page.component";
+import {NgxsModule} from "@ngxs/store";
+import {PaletteModule} from "./palette/palette.module";
+import {PalettePopupPanelComponent} from "./palette/palette-popup-panel/palette-popup-panel.component";
+import { CompositionPanelModule } from "app/ng2/pages/composition/panel/composition-panel.module";
+import {CompositionService} from "./composition.service";
+import {DndModule} from "ngx-drag-drop";
+import {GraphState} from "./common/store/graph.state";
+
+@NgModule({
+ declarations: [CompositionPageComponent, PalettePopupPanelComponent],
+ imports: [CommonModule,
+ CompositionGraphModule,
+ CompositionPanelModule,
+ PaletteModule,
+ DndModule,
+ NgxsModule.forFeature([
+ GraphState])],
+ exports: [CompositionPageComponent],
+ entryComponents: [CompositionPageComponent],
+ providers: [CompositionService]
+})
+export class CompositionPageModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/composition.service.ts b/catalog-ui/src/app/ng2/pages/composition/composition.service.ts
new file mode 100644
index 0000000000..e5e9d2dca8
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/composition.service.ts
@@ -0,0 +1,59 @@
+import {Injectable} from "@angular/core";
+import 'rxjs/add/observable/forkJoin';
+import {Component, PropertiesGroup, AttributesGroup, PolicyInstance} from "app/models";
+import {GroupInstance} from "app/models/graph/zones/group-instance";
+import {CommonGraphDataService} from "./common/common-graph-data.service";
+import {ForwardingPath} from "../../../models/forwarding-path";
+import {SelectedComponentType} from "./common/store/graph.actions";
+
+@Injectable()
+export class CompositionService extends CommonGraphDataService{
+
+ public originComponents: Array<Component>; //This contains the full data set after specifically requesting it. The uniqueId matches the 'componentUid' in the componentInstances array
+ public componentInstancesProperties:PropertiesGroup;
+ public componentInstancesAttributes:AttributesGroup;
+ public groupInstances: GroupInstance[];
+ public policies: PolicyInstance[];
+ public forwardingPaths: { [key:string]:ForwardingPath };
+ public selectedComponentType: SelectedComponentType;
+
+ //---------------------------- COMPONENT INSTANCES ------------------------------------//
+
+ public getOriginComponentById = (uniqueId:string):Component => {
+ return this.originComponents && this.originComponents.find(instance => instance.uniqueId === uniqueId);
+ }
+
+ public addOriginComponent = (originComponent:Component) => {
+ if(!this.originComponents) this.originComponents = [];
+ if(!this.getOriginComponentById(originComponent.uniqueId)){
+ this.originComponents.push(originComponent);
+ }
+ }
+
+
+ public updateGroup = (instance: GroupInstance) => {
+ this.groupInstances = this.groupInstances.map(group => instance.uniqueId === group.uniqueId? instance : group);
+ }
+
+ public updatePolicy = (instance: PolicyInstance) => {
+ this.policies = this.policies.map(policy => instance.uniqueId === policy.uniqueId? instance : policy);
+ }
+
+ //---------------------------- POLICIES---------------------------------//
+ public addPolicyInstance = (instance: PolicyInstance) => {
+ return this.policies.push(instance);
+ }
+
+
+ //---------------------------- POLICIES---------------------------------//
+ public addGroupInstance = (instance: GroupInstance) => {
+ return this.groupInstances.push(instance);
+ }
+
+
+ //----------------------------SELECTED COMPONENT -----------------------//
+
+ public setSelectedComponentType = (selectedType: SelectedComponentType) => {
+ this.selectedComponentType = selectedType;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.html b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.html
new file mode 100644
index 0000000000..4a163ee24b
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.html
@@ -0,0 +1 @@
+<div class="sdc-deployment-graph-wrapper"></div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.less b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.less
new file mode 100644
index 0000000000..9b80fcd651
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.less
@@ -0,0 +1,13 @@
+.sdc-deployment-graph-wrapper {
+ height: 100%;
+ width: 100%;
+
+ ::ng-deep canvas {
+ /*canvas z-index is initialized to 999 while top-nav z-Index is 10, which makes top-nav disappear, so z-Index must be overwritten here*/
+ z-index: 10 !important;
+ }
+ }
+
+::ng-deep .sdc-workspace-container .w-sdc-main-right-container .w-sdc-main-container-body-content.deploy-body-content{
+ padding: 0px;
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.spec.ts
new file mode 100644
index 0000000000..823086fbbf
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.spec.ts
@@ -0,0 +1,92 @@
+import {async, ComponentFixture} from '@angular/core/testing';
+import 'jest-dom/extend-expect';
+import {DeploymentGraphComponent} from "./deployment-graph.component";
+import {DeploymentGraphService} from "./deployment-graph.service";
+import {NO_ERRORS_SCHEMA} from "@angular/core";
+import * as cytoscape from "cytoscape/dist/cytoscape"
+import {AngularJSBridge} from "../../../../services/angular-js-bridge-service";
+import {NodesFactory} from "../../../../models/graph/nodes/nodes-factory";
+import {CommonGraphUtils} from "../graph/common/common-graph-utils";
+import {groupsMock} from "../../../../../jest/mocks/groups.mock";
+import {Module} from "../../../../models/modules/base-module";
+import {ComponentInstance} from "../../../../models/componentsInstances/componentInstance";
+import {componentInstancesMock} from "../../../../../jest/mocks/component-instance.mock";
+import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper";
+import {TopologyTemplateService} from "../../../services/component-services/topology-template.service";
+import {WorkspaceService} from "../../workspace/workspace.service";
+import {SdcConfigToken} from "../../../config/sdc-config.config";
+import {CompositionGraphLinkUtils} from "../graph/utils";
+
+describe('DeploymentGraphComponent', () => {
+
+ let fixture: ComponentFixture<DeploymentGraphComponent>;
+ let deploymentGraphServiceMock: Partial<DeploymentGraphService>;
+ let nodeFactoryServiceMock: Partial<NodesFactory>;
+ let commonGraphUtilsServiceMock: Partial<CommonGraphUtils>;
+ let angularJsBridgeServiceMock: Partial<AngularJSBridge>;
+ let sdcConfigTokenMock: Partial<AngularJSBridge>;
+
+ beforeEach(
+ async(() => {
+
+ deploymentGraphServiceMock = {
+ modules: <Array<Module>>groupsMock,
+ componentInstances: <Array<ComponentInstance>>componentInstancesMock
+ }
+
+ nodeFactoryServiceMock = {
+ createModuleNode: jest.fn().mockResolvedValue(() => {
+ }),
+ createNode: jest.fn().mockResolvedValue(() => {
+ })
+ }
+
+ commonGraphUtilsServiceMock = {
+ addNodeToGraph: jest.fn(),
+ addComponentInstanceNodeToGraph: jest.fn()
+ }
+
+ sdcConfigTokenMock = {
+ imagePath: ''
+ }
+
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [DeploymentGraphComponent],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: DeploymentGraphService, useValue: deploymentGraphServiceMock},
+ {provide: NodesFactory, useValue: nodeFactoryServiceMock},
+ {provide: TopologyTemplateService, useValue: {}},
+ {provide: WorkspaceService, useValue: {}},
+ {provide: CommonGraphUtils, useValue: commonGraphUtilsServiceMock},
+ {provide: CompositionGraphLinkUtils, useValue: {}},
+ {provide: AngularJSBridge, useValue: angularJsBridgeServiceMock},
+ {provide: SdcConfigToken, useValue: SdcConfigToken}
+ ]
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(DeploymentGraphComponent);
+ });
+ })
+ );
+
+ it('expected deployment graph component to be defined', () => {
+ expect(fixture).toBeDefined();
+ });
+
+
+ it('expected to addNodeToGraph to haveBeenCalled 6 times out of 7 cause one of the instances have no parent module', () => {
+ fixture.componentInstance._cy = cytoscape({
+ zoomingEnabled: false,
+ selectionType: 'single',
+ });
+ jest.spyOn(fixture.componentInstance, 'findInstanceModule');
+ fixture.componentInstance.initGraphComponentInstances();
+ expect(fixture.componentInstance.findInstanceModule).toHaveBeenCalledTimes(7);
+ expect(commonGraphUtilsServiceMock.addComponentInstanceNodeToGraph).toHaveBeenCalledTimes(6);
+ });
+
+}); \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.ts b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.ts
new file mode 100644
index 0000000000..143a759960
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.ts
@@ -0,0 +1,127 @@
+import {Component, ElementRef, Inject, OnInit} from "@angular/core";
+import {DeploymentGraphService} from "./deployment-graph.service";
+import '@bardit/cytoscape-expand-collapse';
+import * as _ from "lodash";
+import {TopologyTemplateService} from "../../../services/component-services/topology-template.service";
+import {WorkspaceService} from "../../workspace/workspace.service";
+import {NodesFactory} from "../../../../models/graph/nodes/nodes-factory";
+import {CommonGraphUtils} from "../graph/common/common-graph-utils";
+import {ISdcConfig, SdcConfigToken} from "../../../config/sdc-config.config";
+import {Module} from "../../../../models/modules/base-module";
+import {ComponentInstance} from "../../../../models/componentsInstances/componentInstance";
+import {ComponentGenericResponse} from "../../../services/responses/component-generic-response";
+import {ComponentInstanceFactory} from "../../../../utils/component-instance-factory";
+import {ModulesNodesStyle} from "../graph/common/style/module-node-style";
+import {ComponentInstanceNodesStyle} from "../graph/common/style/component-instances-nodes-style";
+import {CompositionGraphLinkUtils} from "../graph/utils/composition-graph-links-utils";
+
+@Component({
+ selector: 'deployment-graph',
+ templateUrl: './deployment-graph.component.html',
+ styleUrls: ['./deployment-graph.component.less']
+})
+
+export class DeploymentGraphComponent implements OnInit {
+ constructor(private elRef: ElementRef,
+ private topologyTemplateService: TopologyTemplateService,
+ private workspaceService: WorkspaceService,
+ private deploymentService: DeploymentGraphService,
+ private commonGraphUtils: CommonGraphUtils,
+ private nodeFactory: NodesFactory,
+ private commonGraphLinkUtils: CompositionGraphLinkUtils,
+ @Inject(SdcConfigToken) private sdcConfig: ISdcConfig) {
+
+ }
+
+ public _cy: Cy.Instance;
+
+ 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.loadGraph();
+ });
+ }
+
+ public findInstanceModule = (groupsArray: Array<Module>, componentInstanceId: string): string => {
+ let parentGroup: Module = _.find(groupsArray, (group: Module) => {
+ return _.find(_.values(group.members), (member: string) => {
+ return member === componentInstanceId;
+ });
+ });
+ return parentGroup ? parentGroup.uniqueId : "";
+ };
+
+ public initGraphModules = () => {
+ if (this.deploymentService.modules) { // Init module nodes
+ _.each(this.deploymentService.modules, (groupModule: Module) => {
+ let moduleNode = this.nodeFactory.createModuleNode(groupModule);
+ this.commonGraphUtils.addNodeToGraph(this._cy, moduleNode);
+ });
+ }
+ }
+
+ public initGraphComponentInstances = () => {
+ _.each(this.deploymentService.componentInstances, (instance: ComponentInstance) => { // Init component instance nodes
+ let componentInstanceNode = this.nodeFactory.createNode(instance);
+ componentInstanceNode.parent = this.findInstanceModule(this.deploymentService.modules, instance.uniqueId);
+ if (componentInstanceNode.parent) { // we are not drawing instances that are not a part of a module
+ this.commonGraphUtils.addComponentInstanceNodeToGraph(this._cy, componentInstanceNode);
+ }
+ });
+ }
+
+ public handleEmptyModule = () => {
+ // This is a special functionality to pass the cytoscape default behavior - we can't create Parent module node without children's
+ // so we must add an empty dummy child node
+ _.each(this._cy.nodes('[?isGroup]'), (moduleNode: Cy.CollectionFirstNode) => {
+ if (!moduleNode.isParent()) {
+ let dummyInstance = ComponentInstanceFactory.createEmptyComponentInstance();
+ let componentInstanceNode = this.nodeFactory.createNode(dummyInstance);
+ componentInstanceNode.parent = moduleNode.id();
+ let dummyNode = this.commonGraphUtils.addNodeToGraph(this._cy, componentInstanceNode, moduleNode.position());
+ dummyNode.addClass('dummy-node');
+ }
+ })
+ }
+
+ public initGraphNodes = (): void => {
+ this.initGraphModules();
+ this.initGraphComponentInstances();
+ this.handleEmptyModule();
+ };
+
+ private loadGraph = () => {
+
+ let graphEl = this.elRef.nativeElement.querySelector('.sdc-deployment-graph-wrapper');
+ this._cy = cytoscape({
+ container: graphEl,
+ style: ComponentInstanceNodesStyle.getCompositionGraphStyle().concat(ModulesNodesStyle.getModuleGraphStyle()),
+ zoomingEnabled: false,
+ selectionType: 'single',
+
+ });
+
+ //adding expand collapse extension
+ this._cy.expandCollapse({
+ layoutBy: {
+ name: "grid",
+ animate: true,
+ randomize: false,
+ fit: true
+ },
+ fisheye: false,
+ undoable: false,
+ expandCollapseCueSize: 18,
+ expandCueImage: this.sdcConfig.imagesPath + '/assets/styles/images/resource-icons/' + 'closeModule.png',
+ collapseCueImage: this.sdcConfig.imagesPath + '/assets/styles/images/resource-icons/' + 'openModule.png',
+ expandCollapseCueSensitivity: 2,
+ cueOffset: -20
+ });
+
+ this.initGraphNodes(); //creating instances nodes
+ this.commonGraphLinkUtils.initGraphLinks(this._cy, this.deploymentService.componentInstancesRelations);
+ this._cy.collapseAll();
+ };
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.module.ts b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.module.ts
new file mode 100644
index 0000000000..91f97db8c3
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.module.ts
@@ -0,0 +1,15 @@
+import {NgModule} from "@angular/core";
+import {CommonModule} from "@angular/common";
+import {DeploymentGraphComponent} from "./deployment-graph.component";
+
+@NgModule({
+ declarations: [DeploymentGraphComponent],
+ imports: [CommonModule],
+ exports: [DeploymentGraphComponent],
+ entryComponents: [DeploymentGraphComponent],
+ providers: [
+
+ ]
+})
+export class DeploymentGraphModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.service.ts b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.service.ts
new file mode 100644
index 0000000000..7ec346c20b
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.service.ts
@@ -0,0 +1,8 @@
+import {Injectable} from "@angular/core";
+import 'rxjs/add/observable/forkJoin';
+import {CommonGraphDataService} from "../common/common-graph-data.service";
+import {Module} from "../../../../models/modules/base-module";
+@Injectable()
+export class DeploymentGraphService extends CommonGraphDataService {
+ public modules:Array<Module>;
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.html
new file mode 100644
index 0000000000..a8645dc5f0
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.html
@@ -0,0 +1,23 @@
+<div class="canvas-search-component" [ngClass]="{'results-shown': autoCompleteResults.length}"
+ [class.canvas-search-visible]="autoCompleteValues && autoCompleteValues.length" [attr.data-tests-id]="testId">
+ <div class="canvas-search-bar-container" [attr.data-tests-id]="testId"
+ [class.active]="searchQuery && searchQuery.length">
+ <sdc-search-bar class="canvas-search-bar"
+ [placeHolder]="placeholder"
+ (onSearchClicked)="onSearchClicked($event)"
+ [size]="'medium'"
+ [value]="searchQuery"
+ (valueChange)="onSearchQueryChanged($event)">
+ </sdc-search-bar>
+ <svg-icon class="canvas-clear-search"
+ [name]="'close'"
+ [clickable]="true"
+ [mode]="'secondary'"
+ [size]="'small'"
+ (click)="onClearSearch()">
+ </svg-icon>
+ </div>
+ <dropdown-results *ngIf="autoCompleteResults && autoCompleteResults.length" [options]="autoCompleteResults"
+ (onItemSelected)="onItemSelected($event)"></dropdown-results>
+</div>
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.less
new file mode 100644
index 0000000000..247f2a3913
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.less
@@ -0,0 +1,42 @@
+.canvas-search-component {
+
+ .canvas-search-bar-container {
+ display:flex;
+ border-radius: 4px;
+ align-items: center;
+ box-shadow: 0px 2px 3.88px 0.12px rgba(0, 0, 0, 0.29);
+
+ /deep/.sdc-search-bar .search-bar-container .search-button {
+ border: solid 1px #d2d2d2;
+ }
+
+ /deep/.sdc-input__input {
+ width: 250px;
+ transition: all 0.4s;
+ }
+
+ .canvas-clear-search {
+ position: absolute;
+ right: 45px;
+ }
+ }
+
+ &:not(:hover):not(.canvas-search-visible):not(.active) {
+ border-radius: 0;
+ box-shadow: none;
+
+ /deep/.sdc-input__input:not(:focus) {
+ border: none;
+ padding: 0px;
+ width: 0px;
+ }
+ .canvas-clear-search {
+ display: none;
+ }
+ }
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.ts
new file mode 100644
index 0000000000..c1a45a9a4b
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.ts
@@ -0,0 +1,25 @@
+import {Component, EventEmitter, Input, Output} from '@angular/core';
+import {AutoCompleteComponent} from "onap-ui-angular/dist/autocomplete/autocomplete.component";
+
+@Component({
+ selector: 'canvas-search',
+ templateUrl: './canvas-search.component.html',
+ styleUrls: ['./canvas-search.component.less']
+})
+export class CanvasSearchComponent extends AutoCompleteComponent {
+
+ @Output() public searchButtonClicked: EventEmitter<string> = new EventEmitter<string>();
+ @Output() public onSelectedItem: EventEmitter<string> = new EventEmitter<string>();
+
+ public onSearchClicked = (searchText:string)=> {
+ this.searchButtonClicked.emit(searchText);
+ }
+
+ public onItemSelected = (selectedItem) => {
+ this.searchQuery = selectedItem.value;
+ this.autoCompleteResults = [];
+ this.searchButtonClicked.emit(this.searchQuery);
+ this.onSelectedItem.emit(selectedItem);
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.module.ts
new file mode 100644
index 0000000000..6df06067a6
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.module.ts
@@ -0,0 +1,30 @@
+import {SdcUiComponentsModule} from "onap-ui-angular";
+import { NgModule } from "@angular/core";
+import {CanvasSearchComponent} from "./canvas-search.component";
+import {CommonModule} from "@angular/common";
+import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
+import {HttpClientModule} from "@angular/common/http";
+import {BrowserModule} from "@angular/platform-browser";
+import {AutocompletePipe} from "onap-ui-angular/dist/autocomplete/autocomplete.pipe";
+
+@NgModule({
+ declarations: [
+ CanvasSearchComponent
+ ],
+ imports: [
+ CommonModule,
+ BrowserModule,
+ HttpClientModule,
+ BrowserAnimationsModule,
+ SdcUiComponentsModule,
+ ],
+ exports: [
+ CanvasSearchComponent
+ ],
+ entryComponents: [
+ CanvasSearchComponent
+ ],
+ providers: [AutocompletePipe]
+})
+export class CanvasSearchModule {
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/__snapshots__/zone-container.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/__snapshots__/zone-container.component.spec.ts.snap
new file mode 100644
index 0000000000..d4e2a7a359
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/__snapshots__/zone-container.component.spec.ts.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ZoneContainerComponent should match current snapshot of palette element component 1`] = `
+<zone-container
+ backgroundClick={[Function EventEmitter]}
+ backgroundClicked={[Function Function]}
+ minimize={[Function EventEmitter]}
+ unminifyZone={[Function Function]}
+>
+ <div>
+ <div
+ class="sdc-canvas-zone__header"
+ >
+ <div
+ class="sdc-canvas-zone__title"
+ >
+
+ <span
+ class="sdc-canvas-zone__counter"
+ >
+
+ </span>
+ </div>
+ <span
+ class="sdc-canvas-zone__state-button"
+ >
+ –
+ </span>
+ </div>
+ <div
+ class="sdc-canvas-zone__container"
+ />
+ </div>
+</zone-container>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.html
new file mode 100644
index 0000000000..d6343a4a4f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.html
@@ -0,0 +1,30 @@
+<!--
+ ~ 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="sdc-canvas-zone {{class}}-zone" [class.minimized]="minimized" [class.hidden]="!visible"
+ (click)="backgroundClicked()">
+ <div class="sdc-canvas-zone__header" (click)="unminifyZone(); $event.stopPropagation();">
+ <div class="sdc-canvas-zone__title">{{title}}
+ <span class="sdc-canvas-zone__counter">{{count}}</span>
+ </div>
+ <span class="sdc-canvas-zone__state-button">&ndash;</span>
+ </div>
+ <div class="sdc-canvas-zone__container" #scrollDiv>
+ <ng-content></ng-content>
+ </div>
+</div>
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.less
new file mode 100644
index 0000000000..827786cc49
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.less
@@ -0,0 +1,62 @@
+.sdc-canvas-zone {
+ width: 285px;
+ max-height:186px;
+ display:flex;
+ flex-direction:column;
+ color:white;
+ font-family:OpenSans-Regular, sans-serif;
+ transition: width .2s ease-in-out, max-height .2s ease-in-out .1s;
+ position:relative;
+ bottom:0px;
+ margin-right: 5px;
+
+ .sdc-canvas-zone__header {
+ background: #5A5A5A;
+ border-radius: 2px 2px 0 0;
+ padding: 5px 10px;
+ display:flex;
+ justify-content: space-between;
+ font-size: 14px;
+ text-transform:uppercase;
+ .sdc-canvas-zone__state-button {
+ font-weight:bold;
+ cursor:pointer;
+ }
+ }
+
+ .sdc-canvas-zone__container {
+ padding:5px;
+ background-color: #5A5A5A;
+ opacity:0.9;
+ flex: 1;
+ display:flex;
+ flex-direction: row;
+ align-items: flex-start;
+ flex-wrap:wrap;
+ overflow-y:auto;
+ min-height: 80px;
+ max-height: 170px;
+ }
+
+
+ &.minimized {
+ max-height:30px;
+ width:120px;
+ cursor:pointer;
+
+ .sdc-canvas-zone__state-button {
+ display:none;
+ }
+ .sdc-canvas-zone__container {
+ flex: 0 0 0;
+ min-height: 0;
+ padding: 0;
+ overflow-y:hidden;
+ transition: min-height .2s ease-in-out .2s;
+ transition: padding .1s ease-in-out 0s;
+ }
+ }
+ &.hidden {
+ display:none;
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.spec.ts
new file mode 100644
index 0000000000..c432054492
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.spec.ts
@@ -0,0 +1,46 @@
+import {async, ComponentFixture} from '@angular/core/testing';
+import {By} from '@angular/platform-browser';
+import {ConfigureFn, configureTests} from '../../../../../../jest/test-config.helper';
+import 'jest-dom/extend-expect';
+import {ZoneInstanceType} from '../../../../../../app/models/graph/zones/zone-instance';
+import {ZoneContainerComponent} from './zone-container.component';
+
+
+describe('ZoneContainerComponent', () => {
+ let fixture: ComponentFixture<ZoneContainerComponent>;
+
+ beforeEach(
+ async(() => {
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [ZoneContainerComponent]
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(ZoneContainerComponent);
+ });
+ })
+ );
+
+
+ it('should match current snapshot of palette element component', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+ it('should have a group-zone class when the ZoneInstanceType is GROUP',
+ () => {
+ fixture.componentInstance.type = ZoneInstanceType.GROUP;
+ fixture.detectChanges();
+ const compiled = fixture.debugElement.query(By.css('.sdc-canvas-zone'));
+ expect(compiled.nativeElement).toHaveClass('group-zone');
+ });
+
+ it('should have a policy-zone class when the ZoneInstanceType is POLICY',
+ () => {
+ fixture.componentInstance.type = ZoneInstanceType.POLICY;
+ fixture.detectChanges();
+ const compiled = fixture.debugElement.query(By.css('.sdc-canvas-zone'));
+ expect(compiled.nativeElement).toHaveClass('policy-zone');
+ });
+}); \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.ts
new file mode 100644
index 0000000000..4757c1f36d
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.ts
@@ -0,0 +1,35 @@
+import { Component, Input, Output, ViewEncapsulation, EventEmitter, OnInit } from '@angular/core';
+import { ZoneInstanceType } from 'app/models/graph/zones/zone-instance';
+
+@Component({
+ selector: 'zone-container',
+ templateUrl: './zone-container.component.html',
+ styleUrls: ['./zone-container.component.less'],
+ encapsulation: ViewEncapsulation.None
+})
+
+export class ZoneContainerComponent implements OnInit {
+ @Input() title:string;
+ @Input() type:ZoneInstanceType;
+ @Input() count:number;
+ @Input() visible:boolean;
+ @Input() minimized:boolean;
+ @Output() minimize: EventEmitter<any> = new EventEmitter<any>();
+ @Output() backgroundClick: EventEmitter<void> = new EventEmitter<void>();
+ private class:string;
+
+ constructor() {}
+
+ ngOnInit() {
+ this.class = ZoneInstanceType[this.type].toLowerCase();
+ }
+
+ private unminifyZone = () => {
+ this.minimize.emit();
+ }
+
+ private backgroundClicked = () => {
+ this.backgroundClick.emit();
+ }
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.html
new file mode 100644
index 0000000000..d97be69e34
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.html
@@ -0,0 +1,27 @@
+<!--
+ ~ 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 #currentComponent class="zone-instance mode-{{zoneInstance.mode}}" [class.locked]="activeInstanceMode > MODE.HOVER"
+ [class.hiding]="hidden"
+ (mouseenter)="setMode(MODE.HOVER)" (mouseleave)="setMode(MODE.NONE)" (click)="setMode(MODE.SELECTED, $event)">
+ <div class="zone-instance__body" sdc-tooltip tooltip-text="{{zoneInstance.instanceData.name}}" [attr.data-tests-id]="zoneInstance.instanceData.name">
+ <div *ngIf="zoneInstance.handle" class="target-handle {{zoneInstance.handle}}"
+ (click)="tagHandleClicked($event)"></div>
+ <div *ngIf="!isViewOnly" class="zone-instance__handle" (click)="setMode(MODE.TAG, $event)">+</div>
+ <div class="zone-instance__body-content">{{zoneInstance.assignments.length || defaultIconText}}</div>
+ </div>
+ <div class="zone-instance__name">{{zoneInstance.instanceData.name}}</div>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.less
new file mode 100644
index 0000000000..c34b8e149a
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.less
@@ -0,0 +1,135 @@
+@import '../../../../../../../assets/styles/variables';
+
+.zone-instance {
+
+ width:76px;
+ margin:5px;
+ opacity:1;
+
+ .zone-instance__handle {
+ display:none;
+ position:absolute;
+ left: 31px;
+ top: 8px;
+ width:22px;
+ height:22px;
+ cursor:pointer;
+ border: solid @main_color_p 1px;
+ border-radius: 2px;
+ text-align: center;
+ font-weight:bold;
+ }
+
+ .zone-instance__body {
+ position:relative;
+ margin:0 auto;
+ width:43px;
+ height:43px;
+ display:flex;
+ padding:3px;
+ }
+
+ .zone-instance__body-content {
+ border-radius: 2px;
+ flex:1;
+ color:@main_color_p;
+ font-size:16px;
+ text-align:center;
+ display:flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow:none;
+ transition:box-shadow 5s;
+ }
+
+ .zone-instance__name {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ text-align:center;
+ }
+ /* Dynamic classes below */
+
+ .target-handle {
+ position:absolute;
+ width:18px;
+ height:18px;
+ display:block;
+ top: -4px;
+ right: -6px;
+ background-size: 100% 100%;
+ cursor: url("../../../../../../../assets/styles/images/canvas-tagging-icons/policy_2.svg"), pointer;
+
+ &.tagged-policy {
+ background-image: url('../../../../../../../assets/styles/images/canvas-tagging-icons/policy_added.svg');
+ }
+
+ &.tag-available {
+ background-image: url('../../../../../../../assets/styles/images/canvas-tagging-icons/indication.svg');
+ }
+ }
+
+
+ &.mode-1, &.mode-2, &.mode-3 { //hover, selected, tag
+ .zone-instance__body {
+ border:solid 2px;
+ border-radius: 2px;
+ padding:2px;
+ cursor:pointer;
+ }
+ }
+
+ &.mode-1, &.mode-2:hover{
+ .zone-instance__handle{
+ display:block;
+ }
+ }
+
+ &.locked {
+ cursor: inherit;
+ }
+
+ &.hiding {
+ opacity:0;
+ .zone-instance__body-content {
+ box-shadow: #CCC 0px 0px 15px;
+ }
+ }
+
+
+ &.mode-3 .zone-instance__handle {
+ width:24px;
+ height:24px;
+ right:-6px;
+ top:7px;
+ display:block;
+ background-image: linear-gradient(-140deg, #009E98 0%, #97D648 100%);
+ border: 2px solid @main_color_p;
+ border-radius: 2px;
+ box-shadow: inset 2px -2px 3px 0 #007A3E;
+ }
+
+}
+.sdc-canvas-zone.group-zone {
+ .zone-instance__handle {
+ background-color:@main_color_a;
+ }
+ .zone-instance__body {
+ border-color:@main_color_a;
+ .zone-instance__body-content {
+ background: @main_color_a;
+ }
+ }
+}
+
+.sdc-canvas-zone.policy-zone {
+ .zone-instance__handle {
+ background-color:@main_color_r;
+ }
+ .zone-instance__body {
+ border-color:@main_color_r;
+ .zone-instance__body-content {
+ background: @main_color_r;
+ }
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.spec.ts
new file mode 100644
index 0000000000..f5a5f6f546
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.spec.ts
@@ -0,0 +1,132 @@
+import { ComponentFixture, TestBed, async } from '@angular/core/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { SimpleChanges } from '@angular/core';
+import { PoliciesService } from 'app/ng2/services/policies.service';
+import { GroupsService } from 'app/ng2/services/groups.service';
+import { EventListenerService } from 'app/services';
+import { Store } from '@ngxs/store';
+import { CompositionService } from 'app/ng2/pages/composition/composition.service';
+import { ZoneInstanceComponent } from './zone-instance.component';
+import { ZoneInstanceType, ZoneInstance, ZoneInstanceMode, ZoneInstanceAssignmentType, IZoneInstanceAssignment } from "app/models";
+import { PolicyInstance } from "app/models/graph/zones/policy-instance";
+import { Subject, of } from 'rxjs';
+import { _throw } from 'rxjs/observable/throw';
+
+describe('ZoneInstanceComponent', () => {
+ let component: ZoneInstanceComponent;
+ let fixture: ComponentFixture<ZoneInstanceComponent>;
+
+ let createPolicyInstance = () => {
+ let policy = new PolicyInstance();
+ policy.targets = {COMPONENT_INSTANCES: [], GROUPS: []};
+ return new ZoneInstance(policy, '', '');
+ }
+
+ beforeEach(() => {
+ const policiesServiceStub = {updateZoneInstanceAssignments : jest.fn()};
+ const groupsServiceStub = {};
+ const eventListenerServiceStub = {};
+ const storeStub = {};
+ const compositionServiceStub = {};
+ TestBed.configureTestingModule({
+ schemas: [NO_ERRORS_SCHEMA],
+ declarations: [ZoneInstanceComponent],
+ providers: [
+ { provide: PoliciesService, useValue: policiesServiceStub },
+ { provide: GroupsService, useValue: groupsServiceStub },
+ { provide: EventListenerService, useValue: eventListenerServiceStub },
+ { provide: Store, useValue: storeStub },
+ { provide: CompositionService, useValue: compositionServiceStub }
+ ]
+ }).compileComponents().then(() => {
+ fixture = TestBed.createComponent(ZoneInstanceComponent);
+ component = fixture.componentInstance;
+ });
+ });
+
+ it('can load instance', async((done) => {
+ component.zoneInstance = <ZoneInstance>{type : ZoneInstanceType.POLICY, instanceData: {name: 'test policy'}, assignments: []};
+ component.forceSave = new Subject<Function>();
+ fixture.detectChanges();
+ expect(component).toBeTruthy();
+ }));
+
+
+ it('if another instance is already tagging, i cannot change my mode', ()=> {
+ component.zoneInstance = <ZoneInstance>{ mode: ZoneInstanceMode.NONE };
+ component.isActive = false;
+ component.activeInstanceMode = ZoneInstanceMode.TAG;
+ component.setMode(ZoneInstanceMode.SELECTED);
+ expect(component.zoneInstance.mode).toBe(ZoneInstanceMode.NONE);
+ });
+
+ it('if i am active(selected) and NOT in tag mode, I can set another mode', ()=> {
+ component.isActive = true;
+ component.zoneInstance = <ZoneInstance>{ mode: ZoneInstanceMode.SELECTED };
+ jest.spyOn(component.modeChange, 'emit');
+ component.setMode(ZoneInstanceMode.NONE);
+ expect(component.modeChange.emit).toHaveBeenCalledWith({instance: component.zoneInstance, newMode: ZoneInstanceMode.NONE });
+ });
+
+ it('if i am active and in tag mode and i try to set mode other than tag, I am not allowed', ()=> {
+ component.isActive = true;
+ component.zoneInstance = <ZoneInstance>{ mode: ZoneInstanceMode.TAG };
+ component.setMode(ZoneInstanceMode.SELECTED);
+ expect(component.zoneInstance.mode).toBe(ZoneInstanceMode.TAG);
+ });
+
+ it('if i am active and in tag mode and click tag again and no changes, does NOT call save, but DOES turn tagging off', ()=> {
+ component.isActive = true;
+ component.zoneInstance = createPolicyInstance();
+ component.zoneService = component.policiesService;
+ component.zoneInstance.mode = ZoneInstanceMode.TAG;
+ jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments');
+ jest.spyOn(component.modeChange, 'emit');
+
+ component.setMode(ZoneInstanceMode.TAG);
+
+ expect(component.zoneService.updateZoneInstanceAssignments).not.toHaveBeenCalled();
+ expect(component.modeChange.emit).toHaveBeenCalledWith({instance: component.zoneInstance, newMode: ZoneInstanceMode.NONE });
+
+ });
+ it('if i am active and in tag mode and click tag again and HAVE changes, calls save AND turns tagging off', ()=> {
+ component.isActive = true;
+ component.zoneInstance = createPolicyInstance();
+ component.zoneService = component.policiesService;
+ component.zoneInstance.mode = ZoneInstanceMode.TAG;
+ component.zoneInstance.assignments.push(<IZoneInstanceAssignment>{uniqueId: '123', type: ZoneInstanceAssignmentType.COMPONENT_INSTANCES});
+ jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments').mockReturnValue(of(true));
+ jest.spyOn(component.modeChange, 'emit');
+
+ component.setMode(ZoneInstanceMode.TAG);
+
+ expect(component.zoneService.updateZoneInstanceAssignments).toHaveBeenCalled();
+ expect(component.modeChange.emit).toHaveBeenCalledWith({instance: component.zoneInstance, newMode: ZoneInstanceMode.NONE });
+ });
+
+ it('on save error, temporary assignment list is reverted to saved assignments', ()=> {
+ component.isActive = true;
+ component.zoneInstance = createPolicyInstance();
+ component.zoneService = component.policiesService;
+ component.zoneInstance.mode = ZoneInstanceMode.TAG;
+ component.zoneInstance.assignments.push(<IZoneInstanceAssignment>{uniqueId: '123', type: ZoneInstanceAssignmentType.COMPONENT_INSTANCES});
+ jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments').mockReturnValue(_throw({status: 404}));
+
+ component.setMode(ZoneInstanceMode.TAG);
+
+ expect(component.zoneInstance.assignments.length).toEqual(0);
+ });
+
+ it('on save success, all changes are saved to zoneInstance.savedAssignments', ()=> {
+ component.isActive = true;
+ component.zoneInstance = createPolicyInstance();
+ component.zoneService = component.policiesService;
+ component.zoneInstance.mode = ZoneInstanceMode.TAG;
+ component.zoneInstance.assignments.push(<IZoneInstanceAssignment>{uniqueId: '123', type: ZoneInstanceAssignmentType.COMPONENT_INSTANCES});
+ jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments').mockReturnValue(of(true));
+
+ component.setMode(ZoneInstanceMode.TAG);
+
+ expect(component.zoneInstance.instanceData.getSavedAssignments().length).toEqual(1);
+ });
+});
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.ts
new file mode 100644
index 0000000000..1b1363e576
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.ts
@@ -0,0 +1,128 @@
+import { Component, Input, Output, EventEmitter, ViewEncapsulation, OnInit, SimpleChange, ElementRef, ViewChild, SimpleChanges } from '@angular/core';
+import {
+ ZoneInstance, ZoneInstanceMode, ZoneInstanceType,
+ IZoneInstanceAssignment
+} from 'app/models/graph/zones/zone-instance';
+import { PoliciesService } from 'app/ng2/services/policies.service';
+import { GroupsService } from 'app/ng2/services/groups.service';
+import { IZoneService } from "app/models/graph/zones/zone";
+import { EventListenerService } from 'app/services';
+import { GRAPH_EVENTS } from 'app/utils';
+import { Subject } from 'rxjs';
+import { Store } from "@ngxs/store";
+import { CompositionService } from "app/ng2/pages/composition/composition.service";
+import { PolicyInstance } from "app/models";
+import {SelectedComponentType, SetSelectedComponentAction} from "../../../common/store/graph.actions";
+
+
+@Component({
+ selector: 'zone-instance',
+ templateUrl: './zone-instance.component.html',
+ styleUrls: ['./zone-instance.component.less'],
+ encapsulation: ViewEncapsulation.None
+})
+export class ZoneInstanceComponent implements OnInit {
+
+ @Input() zoneInstance:ZoneInstance;
+ @Input() defaultIconText:string;
+ @Input() isActive:boolean;
+ @Input() isViewOnly:boolean;
+ @Input() activeInstanceMode: ZoneInstanceMode;
+ @Input() hidden:boolean;
+ @Input() forceSave:Subject<Function>;
+ @Output() modeChange: EventEmitter<any> = new EventEmitter<any>();
+ @Output() assignmentSaveStart: EventEmitter<void> = new EventEmitter<void>();
+ @Output() assignmentSaveComplete: EventEmitter<boolean> = new EventEmitter<boolean>();
+ @Output() tagHandleClick: EventEmitter<ZoneInstance> = new EventEmitter<ZoneInstance>();
+ @ViewChild('currentComponent') currentComponent: ElementRef;
+ private MODE = ZoneInstanceMode;
+ private zoneService:IZoneService;
+
+ constructor(private policiesService:PoliciesService, private groupsService:GroupsService, private eventListenerService:EventListenerService, private compositionService:CompositionService, private store:Store){}
+
+ ngOnInit(){
+ if(this.zoneInstance.type == ZoneInstanceType.POLICY){
+ this.zoneService = this.policiesService;
+ } else {
+ this.zoneService = this.groupsService;
+ }
+ if(this.forceSave) {
+ this.forceSave.subscribe((afterSaveFunction:Function) => {
+ this.setMode(ZoneInstanceMode.TAG, null, afterSaveFunction);
+ })
+ }
+ }
+
+ ngOnChanges(changes:SimpleChanges) {
+ if(changes.hidden){
+ this.currentComponent.nativeElement.scrollIntoView({behavior: "smooth", block: "nearest", inline:"end"});
+ }
+ }
+
+ ngOnDestroy() {
+ if(this.forceSave) {
+ this.forceSave.unsubscribe();
+ }
+ }
+
+ private setMode = (mode:ZoneInstanceMode, event?:any, afterSaveCallback?:Function):void => {
+
+ if(event){ //prevent event from handle and then repeat event from zone instance
+ event.stopPropagation();
+ }
+
+ if(!this.isActive && this.activeInstanceMode === ZoneInstanceMode.TAG) {
+ return; //someone else is tagging. No events allowed
+ }
+
+ if(this.isActive && this.zoneInstance.mode === ZoneInstanceMode.TAG){
+ if(mode !== ZoneInstanceMode.TAG) {
+ return; //ignore all other events. The only valid option is saving changes.
+ }
+
+ let oldAssignments:Array<IZoneInstanceAssignment> = this.zoneInstance.instanceData.getSavedAssignments();
+ if(this.zoneInstance.isZoneAssignmentChanged(oldAssignments, this.zoneInstance.assignments)) {
+
+ this.assignmentSaveStart.emit();
+
+ this.zoneService.updateZoneInstanceAssignments(this.zoneInstance.parentComponentType, this.zoneInstance.parentComponentID, this.zoneInstance.instanceData.uniqueId, this.zoneInstance.assignments).subscribe(
+ (success) => {
+ this.zoneInstance.instanceData.setSavedAssignments(this.zoneInstance.assignments);
+
+ if(this.zoneInstance.instanceData instanceof PolicyInstance){
+ this.compositionService.updatePolicy(this.zoneInstance.instanceData);
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, this.zoneInstance.instanceData);
+ this.store.dispatch(new SetSelectedComponentAction({component: this.zoneInstance.instanceData, type: SelectedComponentType.POLICY}));
+ } else {
+ this.compositionService.updateGroup(this.zoneInstance.instanceData);
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.zoneInstance.instanceData);
+ this.store.dispatch(new SetSelectedComponentAction({component: this.zoneInstance.instanceData, type: SelectedComponentType.GROUP}));
+ }
+
+ this.assignmentSaveComplete.emit(true);
+ if(afterSaveCallback) afterSaveCallback();
+ }, (error) => {
+ this.zoneInstance.assignments = oldAssignments;
+ this.assignmentSaveComplete.emit(false);
+ });
+ } else {
+ if(afterSaveCallback) afterSaveCallback();
+ }
+ this.modeChange.emit({newMode: ZoneInstanceMode.NONE, instance: this.zoneInstance});
+ // this.store.dispatch(new unsavedChangesActions.RemoveUnsavedChange(this.zoneInstance.instanceData.uniqueId));
+
+
+ } else {
+ this.modeChange.emit({newMode: mode, instance: this.zoneInstance});
+ if(mode == ZoneInstanceMode.TAG){
+ // this.store.dispatch(new unsavedChangesActions.AddUnsavedChange(this.zoneInstance.instanceData.uniqueId));
+ }
+ }
+ }
+
+ private tagHandleClicked = (event:Event) => {
+ this.tagHandleClick.emit(this.zoneInstance);
+ event.stopPropagation();
+ };
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zones-module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zones-module.ts
new file mode 100644
index 0000000000..3287c01f5a
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zones-module.ts
@@ -0,0 +1,15 @@
+import { NgModule } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { ZoneContainerComponent } from "./zone-container.component";
+import { ZoneInstanceComponent } from "./zone-instance/zone-instance.component";
+import { SdcUiComponentsModule } from "onap-ui-angular";
+
+@NgModule({
+ declarations: [ZoneContainerComponent, ZoneInstanceComponent],
+ imports: [CommonModule, SdcUiComponentsModule],
+ entryComponents: [ZoneContainerComponent, ZoneInstanceComponent],
+ exports: [ZoneContainerComponent, ZoneInstanceComponent],
+ providers: []
+})
+export class ZoneModules {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/common-graph-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/common-graph-utils.ts
new file mode 100644
index 0000000000..bfc540e97e
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/common-graph-utils.ts
@@ -0,0 +1,304 @@
+/*-
+ * ============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 * as _ from "lodash";
+import {
+ CommonNodeBase,
+ Relationship,
+ CompositionCiNodeBase
+} from "app/models";
+import {CompositionCiServicePathLink} from "app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link";
+import {Requirement, Capability} from "app/models";
+import {Injectable} from "@angular/core";
+
+
+
+@Injectable()
+export class CommonGraphUtils {
+
+ constructor() {
+
+ }
+
+ public safeApply = (scope:ng.IScope, fn:any) => { //todo remove to general utils
+ let phase = scope.$root.$$phase;
+ if (phase == '$apply' || phase == '$digest') {
+ if (fn && (typeof(fn) === 'function')) {
+ fn();
+ }
+ } else {
+ scope.$apply(fn);
+ }
+ };
+
+ /**
+ * Draw node on the graph
+ * @param cy
+ * @param compositionGraphNode
+ * @param position
+ * @returns {CollectionElements}
+ */
+ public addNodeToGraph(cy:Cy.Instance, compositionGraphNode:CommonNodeBase, position?:Cy.Position):Cy.CollectionElements {
+
+ let node = cy.add(<Cy.ElementDefinition> {
+ group: 'nodes',
+ position: position,
+ data: compositionGraphNode,
+ classes: compositionGraphNode.classes
+ });
+
+ this.initNodeTooltip(node);
+ return node;
+ };
+
+ /**
+ * The function will create a component instance node by the componentInstance position.
+ * If the node is UCPE the function will create all cp lan&wan for the ucpe
+ * @param cy
+ * @param compositionGraphNode
+ * @returns {Cy.CollectionElements}
+ */
+ public addComponentInstanceNodeToGraph(cy:Cy.Instance, compositionGraphNode:CompositionCiNodeBase):Cy.CollectionElements {
+
+ let nodePosition = {
+ x: +compositionGraphNode.componentInstance.posX,
+ y: +compositionGraphNode.componentInstance.posY
+ };
+
+ let node = this.addNodeToGraph(cy, compositionGraphNode, nodePosition);
+ return node;
+ };
+
+ /**
+ * Add service path link to graph - only draw the link
+ * @param cy
+ * @param link
+ */
+ public insertServicePathLinkToGraph = (cy:Cy.Instance, link:CompositionCiServicePathLink) => {
+ let linkElement = cy.add({
+ group: 'edges',
+ data: link,
+ classes: link.classes
+ });
+ this.initServicePathTooltip(linkElement, link);
+ };
+
+ /**
+ * Returns function for the link tooltip content
+ * @param {Relationship} linkRelation
+ * @param {Requirement} requirement
+ * @param {Capability} capability
+ * @returns {() => string}
+ * @private
+ */
+ private _getLinkTooltipContent(linkRelation:Relationship, requirement?:Requirement, capability?:Capability):string {
+ return '<div class="line">' +
+ '<span class="req-cap-label">R: </span>' +
+ '<span>' + (requirement ? requirement.getTitle() : linkRelation.relation.requirement) + '</span>' +
+ '</div>' +
+ '<div class="line">' +
+ '<div class="sprite-new link-tooltip-arrow"></div>' +
+ '<span class="req-cap-label">C: </span>' +
+ '<span>' + (capability ? capability.getTitle() : linkRelation.relation.capability) + '</span>' +
+ '</div>';
+ }
+
+ /**
+ * This function will init qtip tooltip on the link
+ * @param linkElement - the link we want the tooltip to apply on,
+ * @param link
+ * @param getLinkRequirementCapability
+ * link - the link obj
+ */
+ public initLinkTooltip(linkElement:Cy.CollectionElements, link:Relationship, getLinkRequirementCapability:Function) {
+ const content = () => this._getLinkTooltipContent(link); // base tooltip content without owner names
+ const render = (event, api) => {
+ // on render (called once at first show), get the link requirement and capability and change to full tooltip content (with owner names)
+ getLinkRequirementCapability().then((linkReqCap) => {
+ const fullContent = () => this._getLinkTooltipContent(link, linkReqCap.requirement, linkReqCap.capability);
+ api.set('content.text', fullContent);
+ });
+ };
+ linkElement.qtip(this.prepareInitTooltipData({content, events: {render}}));
+ };
+
+ /**
+ *
+ * @param linkElement
+ * @param link
+ */
+ public initServicePathTooltip(linkElement:Cy.CollectionElements, link:CompositionCiServicePathLink) {
+ let content = function () {
+ return '<div class="line">' +
+ '<div>' + link.pathName + '</div>' +
+ '</div>';
+ };
+ linkElement.qtip(this.prepareInitTooltipData({content}));
+ };
+
+ private prepareInitTooltipData(options?:Object) {
+ return _.merge({
+ position: {
+ my: 'top center',
+ at: 'bottom center',
+ adjust: {x: 0, y: 0},
+ effect: false
+ },
+ style: {
+ classes: 'qtip-dark qtip-rounded qtip-custom link-qtip',
+ tip: {
+ width: 16,
+ height: 8
+ }
+ },
+ show: {
+ event: 'mouseover',
+ delay: 1000
+ },
+ hide: {event: 'mouseout mousedown'},
+ includeLabels: true,
+ events: {}
+ }, options);
+ }
+
+ public HTMLCoordsToCytoscapeCoords(cytoscapeBoundingBox:Cy.Extent, mousePos:Cy.Position):Cy.Position {
+ return {x: mousePos.x + cytoscapeBoundingBox.x1, y: mousePos.y + cytoscapeBoundingBox.y1}
+ };
+
+
+ public getCytoscapeNodePosition = (cy:Cy.Instance, event:DragEvent | MouseEvent):Cy.Position => {
+ let targetOffset = $(event.target).offset();
+ if(event instanceof DragEvent) {
+ targetOffset = $('canvas').offset();
+ }
+
+ let x = (event.pageX - targetOffset.left) / cy.zoom();
+ let y = (event.pageY - targetOffset.top) / cy.zoom();
+
+ return this.HTMLCoordsToCytoscapeCoords(cy.extent(), {
+ x: x,
+ y: y
+ });
+ };
+
+
+ public getNodePosition(node:Cy.CollectionFirstNode):Cy.Position {
+ let nodePosition = node.relativePoint();
+ if (node.data().isUcpe) { //UCPEs use bounding box and not relative point.
+ nodePosition = {x: node.boundingbox().x1, y: node.boundingbox().y1};
+ }
+
+ return nodePosition;
+ }
+
+ /**
+ * Generic function that can be used for any html elements overlaid on canvas
+ * Returns the html position of a node on canvas, including left palette and header offsets. Option to pass in additional offset to add to return position.
+ * @param node
+ * @param additionalOffset
+ * @returns {Cy.Position}
+
+ public getNodePositionWithOffset = (node:Cy.CollectionFirstNode, additionalOffset?:Cy.Position): Cy.Position => {
+ if(!additionalOffset) additionalOffset = {x: 0, y:0};
+
+ let nodePosition = node.renderedPosition();
+ let posWithOffset:Cy.Position = {
+ x: nodePosition.x + GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET + additionalOffset.x,
+ y: nodePosition.y + GraphUIObjects.COMPOSITION_HEADER_OFFSET + additionalOffset.y
+ };
+ return posWithOffset;
+ };*/
+
+ /**
+ * return true/false if first node contains in second - this used in order to verify is node is entirely inside ucpe
+ * @param firstBox
+ * @param secondBox
+ * @returns {boolean}
+ */
+ public isFirstBoxContainsInSecondBox(firstBox:Cy.BoundingBox, secondBox:Cy.BoundingBox) {
+
+ return firstBox.x1 > secondBox.x1 && firstBox.x2 < secondBox.x2 && firstBox.y1 > secondBox.y1 && firstBox.y2 < secondBox.y2;
+
+ };
+
+ /**
+ *
+ * @param cy
+ * @param node
+ * @returns {Array}
+ */
+ public getLinkableNodes(cy:Cy.Instance, node:Cy.CollectionFirstNode):Array<CompositionCiNodeBase> {
+ let compatibleNodes = [];
+ _.each(cy.nodes(), (tempNode)=> {
+ if (this.nodeLocationsCompatible(node, tempNode)) {
+ compatibleNodes.push(tempNode.data());
+ }
+ });
+ return compatibleNodes;
+ }
+
+ /**
+ * Checks whether node locations are compatible in reference to UCPEs.
+ * Returns true if both nodes are in UCPE or both nodes out, or one node is UCPEpart.
+ * @param node1
+ * @param node2
+ */
+ public nodeLocationsCompatible(node1:Cy.CollectionFirstNode, node2:Cy.CollectionFirstNode) {
+ return (this.isFirstBoxContainsInSecondBox(node1.boundingbox(), node2.boundingbox()));
+ }
+
+ /**
+ * This function will init qtip tooltip on the node
+ * @param node - the node we want the tooltip to apply on
+ */
+ public initNodeTooltip(node:Cy.CollectionNodes) {
+
+ let opts = {
+ content: function () {
+ return this.data('name');
+ },
+ position: {
+ my: 'top center',
+ at: 'bottom center',
+ adjust: {x: 0, y: -5}
+ },
+ style: {
+ classes: 'qtip-dark qtip-rounded qtip-custom',
+ tip: {
+ width: 16,
+ height: 8
+ }
+ },
+ show: {
+ event: 'mouseover',
+ delay: 1000
+ },
+ hide: {event: 'mouseout mousedown'},
+ includeLabels: true
+ };
+
+ if (node.data().isUcpePart) { //fix tooltip positioning for UCPE-cps
+ opts.position.adjust = {x: 0, y: 20};
+ }
+
+ node.qtip(opts);
+ };
+}
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/image-creator.service.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/image-creator.service.ts
new file mode 100644
index 0000000000..2be92c782b
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/image-creator.service.ts
@@ -0,0 +1,92 @@
+/*-
+ * ============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=========================================================
+ */
+'use strict';
+import {Injectable} from "@angular/core";
+
+export interface ICanvasImage {
+ src: string;
+ width: number
+ height: number;
+ x: number;
+ y: number;
+}
+
+@Injectable()
+export class ImageCreatorService {
+
+ private _canvas:HTMLCanvasElement;
+
+ constructor() {
+ this._canvas = <HTMLCanvasElement>$('<canvas>')[0];
+ this._canvas.setAttribute('style', 'display:none');
+
+ let body = document.getElementsByTagName('body')[0];
+ body.appendChild(this._canvas);
+ }
+
+ /**
+ * Create an image composed of different image layers
+ * @param canvasImages
+ * @param canvasWidth
+ * @param canvasHeight
+ * returns a PROMISE
+ */
+ getMultiLayerBase64Image(canvasImages: ICanvasImage[], canvasWidth?:number, canvasHeight?:number):Promise<string> {
+
+ var promise = new Promise<string>((resolve, reject) => {
+ if(canvasImages && canvasImages.length === 0){
+ return null;
+ }
+
+ //If only width was set, use it for height, otherwise use first canvasImage height
+ canvasHeight = canvasHeight || canvasImages[0].height;
+ canvasWidth = canvasWidth || canvasImages[0].width;
+
+ const images = [];
+ let imagesLoaded = 0;
+ const onImageLoaded = () => {
+ imagesLoaded++;
+ if(imagesLoaded < canvasImages.length){
+ return;
+ }
+ this._canvas.setAttribute('width', (canvasWidth * 4).toString());
+ this._canvas.setAttribute('height', (canvasHeight * 4).toString());
+ const canvasCtx = this._canvas.getContext('2d');
+ canvasCtx.scale(4,4);
+ canvasCtx.clearRect(0, 0, this._canvas.width, this._canvas.height);
+ images.forEach((image, index) => {
+ const canvasImage = canvasImages[index];
+ canvasCtx.drawImage(image, canvasImage.x, canvasImage.y, canvasImage.width, canvasImage.height);
+ });
+
+ let base64Image = this._canvas.toDataURL();
+ resolve(base64Image)
+ };
+ canvasImages.forEach(canvasImage => {
+ let image = new Image();
+ image.onload = onImageLoaded;
+ image.src = canvasImage.src;
+ images.push(image);
+ });
+ });
+
+ return promise;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.spec.ts
new file mode 100644
index 0000000000..54b3dbed24
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.spec.ts
@@ -0,0 +1,37 @@
+import {async} from "@angular/core/testing";
+import {ComponentInstanceNodesStyle} from "./component-instances-nodes-style";
+
+
+describe('component instance nodes style component', () => {
+
+ beforeEach(
+ async(() => {
+ const createElement = document.createElement.bind(document);
+ document.createElement = (tagName) => {
+ if (tagName === 'canvas') {
+ return {
+ getContext: () => ({
+ font: "",
+ measureText: (x) => ({width: x.length})
+ }),
+ };
+ }
+ return createElement(tagName);
+ };
+ })
+ );
+
+ it('verify getGraphDisplayName for String.length smaller than 67 chars', () => {
+ let inputString = 'SomeText';
+ let expectedRes = inputString;
+ let res = ComponentInstanceNodesStyle.getGraphDisplayName(inputString);
+ expect(res).toBe(expectedRes);
+ });
+
+ it('verify getGraphDisplayName for String.length greater than 67 chars', () => {
+ let inputString = 'AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFGGGGGGGGGG12345678';
+ let expectedRes = 'AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFF...';
+ let res = ComponentInstanceNodesStyle.getGraphDisplayName(inputString);
+ expect(res).toBe(expectedRes);
+ });
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.ts
new file mode 100644
index 0000000000..cc9cac16e6
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.ts
@@ -0,0 +1,362 @@
+/*-
+ * ============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 { GraphColors, GraphUIObjects} from "app/utils/constants";
+import constant = require("lodash/constant");
+import {ImagesUrl} from "app/utils/constants";
+import {AngularJSBridge} from "app/services/angular-js-bridge-service";
+import { CanvasHandleTypes } from "app/utils";
+/**
+ * Created by obarda on 12/18/2016.
+ */
+export class ComponentInstanceNodesStyle {
+
+ public static getCompositionGraphStyle = ():Array<Cy.Stylesheet> => {
+ return [
+ {
+ selector: 'core',
+ css: {
+ 'shape': 'rectangle',
+ 'active-bg-size': 0,
+ 'selection-box-color': 'rgb(0, 159, 219)',
+ 'selection-box-opacity': 0.2,
+ 'selection-box-border-color': '#009fdb',
+ 'selection-box-border-width': 1
+
+ }
+ },
+ {
+ selector: 'node',
+ css: {
+ 'font-family': 'OpenSans-Regular,sans-serif',
+
+ 'font-size': 14,
+ 'events': 'yes',
+ 'text-events': 'yes',
+ 'text-border-width': 15,
+ 'text-border-color': GraphColors.NODE_UCPE,
+ 'text-margin-y': 5
+ }
+ },
+ {
+ selector: '.vf-node',
+ css: {
+ 'background-color': 'transparent',
+ 'shape': 'rectangle',
+ 'label': 'data(displayName)',
+ 'background-image': 'data(img)',
+ 'width': GraphUIObjects.DEFAULT_RESOURCE_WIDTH,
+ 'height': GraphUIObjects.DEFAULT_RESOURCE_WIDTH,
+ 'background-opacity': 0,
+ "background-width": GraphUIObjects.DEFAULT_RESOURCE_WIDTH,
+ "background-height": GraphUIObjects.DEFAULT_RESOURCE_WIDTH,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'background-fit': 'cover',
+ 'background-clip': 'node',
+ 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR,
+ 'overlay-opacity': 0
+ }
+ },
+
+ {
+ selector: '.service-node',
+ css: {
+ 'background-color': 'transparent',
+ 'label': 'data(displayName)',
+ 'events': 'yes',
+ 'text-events': 'yes',
+ 'background-image': 'data(img)',
+ 'width': 64,
+ 'height': 64,
+ "border-width": 0,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'background-opacity': 0,
+ 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR,
+ 'overlay-opacity': 0
+ }
+ },
+ {
+ selector: '.cp-node',
+ css: {
+ 'background-color': 'rgb(255,255,255)',
+ 'shape': 'rectangle',
+ 'label': 'data(displayName)',
+ 'background-image': 'data(img)',
+ 'background-width': GraphUIObjects.SMALL_RESOURCE_WIDTH,
+ 'background-height': GraphUIObjects.SMALL_RESOURCE_WIDTH,
+ 'width': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE,
+ 'height': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE/2,
+ 'background-position-x': GraphUIObjects.HANDLE_SIZE / 2,
+ 'background-position-y': GraphUIObjects.HANDLE_SIZE / 2,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'background-opacity': 0,
+ 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR,
+ 'overlay-opacity': 0
+ }
+ },
+ {
+ selector: '.vl-node',
+ css: {
+ 'background-color': 'rgb(255,255,255)',
+ 'shape': 'rectangle',
+ 'label': 'data(displayName)',
+ 'background-image': 'data(img)',
+ 'background-width': GraphUIObjects.SMALL_RESOURCE_WIDTH,
+ 'background-height': GraphUIObjects.SMALL_RESOURCE_WIDTH,
+ 'background-position-x': GraphUIObjects.HANDLE_SIZE / 2,
+ 'background-position-y': GraphUIObjects.HANDLE_SIZE / 2,
+ 'width': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE,
+ 'height': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE / 2,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'background-opacity': 0,
+ 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR,
+ 'overlay-opacity': 0
+ }
+ },
+ {
+ selector: '.ucpe-cp',
+ css: {
+ 'background-color': GraphColors.NODE_UCPE_CP,
+ 'background-width': 15,
+ 'background-height': 15,
+ 'width': 15,
+ 'height': 15,
+ 'text-halign': 'center',
+ 'overlay-opacity': 0,
+ 'label': 'data(displayName)',
+ 'text-valign': 'data(textPosition)',
+ 'text-margin-y': (ele:Cy.Collection) => {
+ return (ele.data('textPosition') == 'top') ? -5 : 5;
+ },
+ 'font-size': 12
+ }
+ },
+ {
+ selector: '.ucpe-node',
+ css: {
+ 'background-fit': 'cover',
+ 'padding-bottom': 0,
+ 'padding-top': 0
+ }
+ },
+ {
+ selector: '.simple-link',
+ css: {
+ 'width': 1,
+ 'line-color': GraphColors.BASE_LINK,
+ 'target-arrow-color': '#3b7b9b',
+ 'target-arrow-shape': 'triangle',
+ 'curve-style': 'bezier',
+ 'control-point-step-size': 30
+ }
+ },
+ {
+ selector: '.vl-link',
+ css: {
+ 'width': 3,
+ 'line-color': GraphColors.VL_LINK,
+ 'curve-style': 'bezier',
+ 'control-point-step-size': 30
+ }
+ },
+ {
+ selector: '.vl-link-1',
+ css: {
+ 'width': 3,
+ 'line-color': GraphColors.ACTIVE_LINK,
+ 'curve-style': 'unbundled-bezier',
+ 'target-arrow-color': '#3b7b9b',
+ 'target-arrow-shape': 'triangle',
+ 'control-point-step-size': 30
+ }
+ },
+ {
+ selector: '.ucpe-host-link',
+ css: {
+ 'width': 0
+ }
+ },
+ {
+ selector: '.not-certified-link',
+ css: {
+ 'width': 1,
+ 'line-color': GraphColors.NOT_CERTIFIED_LINK,
+ 'curve-style': 'bezier',
+ 'control-point-step-size': 30,
+ 'line-style': 'dashed',
+ 'target-arrow-color': '#3b7b9b',
+ 'target-arrow-shape': 'triangle'
+
+ }
+ },
+
+ {
+ selector: '.service-path-link',
+ css: {
+ 'width': 2,
+ 'line-color': GraphColors.SERVICE_PATH_LINK,
+ 'target-arrow-color': GraphColors.SERVICE_PATH_LINK,
+ 'target-arrow-shape': 'triangle',
+ 'curve-style': 'bezier',
+ 'control-point-step-size': 30
+ }
+ },
+ {
+ selector: '.not-certified',
+ css: {
+ 'shape': 'rectangle',
+ 'background-image': (ele:Cy.Collection) => {
+ // return ele.data().setUncertifiedImageBgStyle(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE);//Change name to setUncertifiedImageBgStyle??
+ return ele.data().initUncertifiedImage(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE);
+ },
+ 'border-width': 0
+ }
+ },
+ {
+ selector: '.dependent',
+ css: {
+ 'shape': 'rectangle',
+ 'background-image': (ele:Cy.Collection) => {
+ return ele.data().initDependentImage(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE)
+ },
+ 'border-width': 0
+ }
+ },
+ {
+ selector: '.dependent.not-certified',
+ css: {
+ 'shape': 'rectangle',
+ 'background-image': (ele:Cy.Collection) => {
+ return ele.data().initUncertifiedDependentImage(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE)
+ },
+ 'border-width': 0
+ }
+ },
+ {
+ selector: 'node:selected',
+ css: {
+ "border-width": 2,
+ "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR,
+ 'shape': 'rectangle'
+ }
+ },
+ {
+ selector: 'edge:selected',
+ css: {
+ 'line-color': GraphColors.ACTIVE_LINK
+
+ }
+ },
+ {
+ selector: 'edge:active',
+ css: {
+ 'overlay-opacity': 0
+ }
+ }, {
+ selector: '.configuration-node',
+ css: {
+ 'background-color': 'rgb(255,255,255)',
+ 'shape': 'rectangle',
+ 'label': 'data(displayName)',
+ 'background-image': 'data(img)',
+ 'background-width': GraphUIObjects.SMALL_RESOURCE_WIDTH,
+ 'background-height': GraphUIObjects.SMALL_RESOURCE_WIDTH,
+ 'background-position-x': GraphUIObjects.HANDLE_SIZE / 2,
+ 'background-position-y': GraphUIObjects.HANDLE_SIZE / 2,
+ 'width': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE,
+ 'height': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE/2,
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'background-opacity': 0,
+ 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR,
+ 'overlay-opacity': 0
+ }
+ },
+ {
+ selector: '.archived',
+ css: {
+ 'shape': 'rectangle',
+ 'background-image': (ele:Cy.Collection) => {
+ return ele.data().setArchivedImageBgStyle(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE); //Change name to setArchivedImageBgStyle ??
+ },
+ "border-width": 0
+ }
+ }
+ ]
+ }
+
+ public static getAddEdgeHandle = () => {
+ return {
+
+ single: false,
+ type: CanvasHandleTypes.ADD_EDGE,
+ imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_PLUS_ICON,
+ lineColor: '#27a337',
+ lineWidth: 2,
+ lineStyle: 'dashed'
+
+ }
+ }
+
+ public static getTagHandle = () => {
+ return {
+ single: false,
+ type: CanvasHandleTypes.TAG_AVAILABLE,
+ imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_TAG_ICON,
+ }
+ }
+
+ public static getTaggedPolicyHandle = () => {
+ return {
+ single: false,
+ type: CanvasHandleTypes.TAGGED_POLICY,
+ imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_POLICY_TAGGED_ICON,
+ }
+ }
+
+ public static getTaggedGroupHandle = () => {
+ return {
+ single: false,
+ type: CanvasHandleTypes.TAGGED_GROUP,
+ imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_GROUP_TAGGED_ICON,
+ }
+ }
+
+ public static getGraphDisplayName(name:string):string {
+ let context = document.createElement("canvas").getContext("2d");
+ context.font = "13px Arial";
+
+ if (67 < context.measureText(name).width) {
+ let newLen = name.length - 3;
+ let newName = name.substring(0, newLen);
+
+ while (59 < (context.measureText(newName).width)) {
+ newName = newName.substring(0, (--newLen));
+ }
+ return newName + '...';
+ }
+ return name;
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/style/module-node-style.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/module-node-style.ts
new file mode 100644
index 0000000000..bf71e1c868
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/module-node-style.ts
@@ -0,0 +1,103 @@
+/*-
+ * ============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 {GraphColors} from "app/utils";
+export class ModulesNodesStyle {
+
+ public static getModuleGraphStyle = ():Array<Cy.Stylesheet> => {
+
+ return [
+ {
+ selector: '.cy-expand-collapse-collapsed-node',
+ css: {
+ 'background-image': 'data(img)',
+ 'width': 34,
+ 'height': 32,
+ 'background-opacity': 0,
+ 'shape': 'rectangle',
+ 'label': 'data(displayName)',
+ 'events': 'yes',
+ 'text-events': 'yes',
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'text-margin-y': 5,
+ 'border-opacity': 0
+ }
+ },
+ {
+ selector: '.module-node',
+ css: {
+ 'background-color': 'transparent',
+ 'background-opacity': 0,
+ "border-width": 2,
+ "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR,
+ 'border-style': 'dashed',
+ 'label': 'data(displayName)',
+ 'events': 'yes',
+ 'text-events': 'yes',
+ 'text-valign': 'bottom',
+ 'text-halign': 'center',
+ 'text-margin-y': 8
+ }
+ },
+ {
+ selector: 'node:selected',
+ css: {
+ "border-opacity": 0
+ }
+ },
+ {
+ selector: '.simple-link:selected',
+ css: {
+ 'line-color': GraphColors.BASE_LINK,
+ }
+ },
+ {
+ selector: '.vl-link:selected',
+ css: {
+ 'line-color': GraphColors.VL_LINK,
+ }
+ },
+ {
+ selector: '.cy-expand-collapse-collapsed-node:selected',
+ css: {
+ "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR,
+ 'border-opacity': 1,
+ 'border-style': 'solid',
+ 'border-width': 2
+ }
+ },
+ {
+ selector: '.module-node:selected',
+ css: {
+ "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR,
+ 'border-opacity': 1
+ }
+ },
+ {
+ selector: '.dummy-node',
+ css: {
+ 'width': 20,
+ 'height': 20
+ }
+ },
+ ]
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.html
new file mode 100644
index 0000000000..5a0ca3e43f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.html
@@ -0,0 +1,57 @@
+<div class="sdc-composition-graph-wrapper {{zoneTagMode}}"
+ [ngClass]="{'with-sidebar': withSidebar$ | async, 'view-only':isViewOnly$ | async}">
+</div>
+
+<div class="sdc-composition-menu" [ngClass]="{'with-sidebar': withSidebar$ | async}">
+
+ <service-path-selector
+ *ngIf="topologyTemplate.isService() && compositionService.forwardingPaths"
+ [drawPath]="drawPathOnCy"
+ [deletePaths]="deletePathsOnCy"
+ [selectedPathId]="selectedPathId">
+ </service-path-selector>
+
+ <canvas-search *ngIf="componentInstanceNames" class="composition-search"
+ [placeholder]="'Type to search'"
+ [data]="componentInstanceNames"
+ (searchChanged)="getAutoCompleteValues($event)"
+ (searchButtonClicked)="highlightSearchMatches($event)">
+ </canvas-search>
+
+ <!--<service-path class="zoom-icons"-->
+ <!--*ngIf="!(isViewOnly$ | async) && topologyTemplate.isService()"-->
+ <!--[service]="topologyTemplate"-->
+ <!--[onCreate]="createOrUpdateServicePath">-->
+ <!--</service-path>-->
+
+ <svg-icon *ngIf="!(isViewOnly$ | async) && topologyTemplate.isService()" class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'"
+ [backgroundColor]="'silver'" [name]="'browse'" [clickable]="true" [testId]="'pathsMenuBtn'"
+ (click)="openServicePathMenu($event)"></svg-icon>
+ <svg-icon class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'"
+ [backgroundColor]="'silver'" [name]="'expand-o'" [clickable]="true"
+ (click)="zoomAllWithoutSidebar()"></svg-icon>
+ <svg-icon class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'"
+ [backgroundColor]="'silver'" [name]="'plus'" [clickable]="true"
+ (click)="zoom(true)"></svg-icon>
+ <svg-icon class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'"
+ [backgroundColor]="'silver'" [name]="'minus'" [clickable]="true"
+ (click)="zoom(false)"></svg-icon>
+</div>
+
+<div class="sdc-canvas-zones__wrapper {{zoneTagMode}}" [ngClass]="{'with-sidebar': withSidebar$ | async}">
+ <zone-container *ngFor="let zone of zones" [title]="zone.title" [type]="zone.type" [count]="zone.instances.length"
+ [visible]="zone.visible" [minimized]="zone.minimized" (minimize)="zoneMinimizeToggle(zone.type)"
+ (backgroundClick)="zoneBackgroundClicked()">
+ <zone-instance *ngFor="let instance of zone.instances" [hidden]="instance.hidden"
+ [zoneInstance]="instance" [defaultIconText]="zone.defaultIconText"
+ [isActive]="activeZoneInstance == instance"
+ [activeInstanceMode]="activeZoneInstance && activeZoneInstance.mode"
+ [isViewOnly]="isViewOnly$ | async"
+ [forceSave]="instance.forceSave"
+ (modeChange)="zoneInstanceModeChanged($event.newMode, $event.instance, zone.type)"
+ (tagHandleClick)="zoneInstanceTagged($event)"
+ (assignmentSaveStart)="zoneAssignmentSaveStart()"
+ (assignmentSaveComplete)="zoneAssignmentSaveComplete($event)">
+ </zone-instance>
+ </zone-container>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.less
new file mode 100644
index 0000000000..b3e5ef3a0c
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.less
@@ -0,0 +1,93 @@
+:host(composition-graph) {
+ flex: 1;
+ padding-top: 53px;
+}
+
+.composition {
+ .custom-modal {
+ /* Hack solution to hide canvas tooltips under modals */
+ z-index: 20000 !important;
+ }
+}
+
+.sdc-composition-graph-wrapper {
+ height: 100%;
+ width: 100%;
+
+ &.with-sidebar {
+ width: calc(~'100% - 300px');
+ }
+}
+
+.view-only {
+ background-color: rgb(248, 248, 248);
+}
+
+.sdc-canvas-zones__wrapper {
+ position: absolute;
+ bottom: 10px;
+ right: 12px;
+ display: flex;
+ transition: right 0.2s;
+
+ &.with-sidebar {
+ right: 310px;
+ }
+
+ ng2-zone-container {
+ display: flex;
+ margin-left: 10px;
+ }
+}
+
+.group-tagging {
+ cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/group_1.svg"), pointer;
+}
+
+.group-tagging-hover {
+ cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/group_2.svg"), pointer;
+}
+
+.policy-tagging {
+ cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/policy_1.svg"), pointer;
+}
+
+.policy-tagging-hover {
+ cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/policy_2.svg"), pointer;
+}
+
+//Canvas menu
+.sdc-composition-menu {
+ position: absolute;
+ right: 18px;
+ top: 53px;
+ transition: right 0.2s;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ margin-right: 10px;
+ pointer-events: none;
+
+ & > * {
+ pointer-events: all;
+ }
+
+ &.with-sidebar {
+ right: 320px;
+ }
+
+ .composition-search {
+ margin-top: 12px;
+ }
+
+ .zoom-icons {
+ border: solid 1px #d2d2d2;
+ border-radius: 2px;
+ box-shadow: 0px 2px 3.88px 0.12px rgba(0, 0, 0, 0.29);
+ margin-top: 10px;
+
+ /deep/ .svg-icon {
+ box-sizing: content-box;
+ }
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.spec.ts
new file mode 100644
index 0000000000..9a15ecba69
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.spec.ts
@@ -0,0 +1,354 @@
+import {NO_ERRORS_SCHEMA} from '@angular/core';
+import {async, ComponentFixture} from '@angular/core/testing';
+import {SdcUiServices} from 'onap-ui-angular';
+import 'rxjs/add/observable/of';
+import {ConfigureFn, configureTests} from '../../../../../jest/test-config.helper';
+import {CompositionGraphComponent} from "./composition-graph.component";
+import {WorkspaceService} from "../../workspace/workspace.service";
+import {ComponentInstance, GroupInstance, NodesFactory, ZoneInstance, ZoneInstanceMode} from "../../../../models";
+import {EventListenerService} from "../../../../services";
+import {
+ CompositionGraphGeneralUtils,
+ CompositionGraphNodesUtils,
+ CompositionGraphZoneUtils,
+ MatchCapabilitiesRequirementsUtils, ServicePathGraphUtils
+} from "./utils";
+import {CompositionGraphLinkUtils} from "./utils/composition-graph-links-utils";
+import {ConnectionWizardService} from "./connection-wizard/connection-wizard.service";
+import {CommonGraphUtils} from "./common/common-graph-utils";
+import {CompositionGraphPaletteUtils} from "./utils/composition-graph-palette-utils";
+import {TopologyTemplateService} from "../../../services/component-services/topology-template.service";
+import {ComponentInstanceServiceNg2} from "../../../services/component-instance-services/component-instance.service";
+import {CompositionService} from "../composition.service";
+import {ModalService} from '../../../services/modal.service';
+import {Store} from '@ngxs/store';
+import {PoliciesService} from '../../../services/policies.service';
+import {GroupsService} from '../../../services/groups.service';
+import {PolicyInstance} from "../../../../models/graph/zones/policy-instance";
+import {ZoneInstanceType} from "../../../../models/graph/zones/zone-instance";
+import {GRAPH_EVENTS} from "../../../../utils/constants";
+import * as cytoscape from "cytoscape";
+import {ComponentMetadata} from "../../../../models/component-metadata";
+import {Zone} from "../../../../models/graph/zones/zone";
+import {SelectedComponentType, SetSelectedComponentAction} from "../common/store/graph.actions";
+
+describe('composition graph component', () => {
+
+ let fixture: ComponentFixture<CompositionGraphComponent>;
+ let instance: CompositionGraphComponent;
+ let eventServiceMock: Partial<EventListenerService>;
+ let compositionGraphZoneUtils: Partial<CompositionGraphZoneUtils>;
+ let generalGraphUtils: Partial<CompositionGraphGeneralUtils>;
+ let workspaceServiceMock: Partial<WorkspaceService>;
+ let policyService: Partial<PoliciesService>;
+ let storeStub;
+ let compositionGraphLinkUtils: Partial<CompositionGraphLinkUtils>;
+ let nodesGraphUtils: Partial<CompositionGraphNodesUtils>;
+
+ let createPolicyInstance = () => {
+ let policy = new PolicyInstance();
+ policy.targets = {COMPONENT_INSTANCES: [], GROUPS: []};
+ return new ZoneInstance(policy, '', '');
+ }
+
+ beforeEach(
+ async(() => {
+
+ eventServiceMock = {
+ notifyObservers: jest.fn(),
+ unRegisterObserver: jest.fn()
+ }
+
+ compositionGraphZoneUtils = {
+ endCyTagMode: jest.fn(),
+ showZoneTagIndications: jest.fn(),
+ hideZoneTagIndications: jest.fn(),
+ hideGroupZoneIndications: jest.fn(),
+ showGroupZoneIndications: jest.fn(),
+ startCyTagMode: jest.fn()
+ }
+
+ workspaceServiceMock = {
+ metadata: <ComponentMetadata>{
+ uniqueId: 'service_unique_id',
+ componentType: 'SERVICE'
+ }
+ }
+
+ compositionGraphLinkUtils = {
+ handleLinkClick: jest.fn(),
+ getModifyLinkMenu: jest.fn()
+ }
+
+ storeStub = {
+ dispatch: jest.fn()
+ }
+ policyService = {
+ getSpecificPolicy: jest.fn()
+ }
+
+ generalGraphUtils = {
+ zoomGraphTo: jest.fn()
+ }
+
+ nodesGraphUtils = {
+ onNodesPositionChanged: jest.fn()
+ }
+
+ const configure: ConfigureFn = (testBed) => {
+ testBed.configureTestingModule({
+ declarations: [CompositionGraphComponent],
+ imports: [],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: NodesFactory, useValue: {}},
+ {provide: EventListenerService, useValue: eventServiceMock},
+ {provide: CompositionGraphZoneUtils, useValue: compositionGraphZoneUtils},
+ {provide: CompositionGraphGeneralUtils, useValue: generalGraphUtils},
+ {provide: CompositionGraphLinkUtils, useValue: compositionGraphLinkUtils},
+ {provide: CompositionGraphNodesUtils, useValue: nodesGraphUtils},
+ {provide: ConnectionWizardService, useValue: {}},
+ {provide: CommonGraphUtils, useValue: {}},
+ {provide: CompositionGraphPaletteUtils, useValue: {}},
+ {provide: TopologyTemplateService, useValue: {}},
+ {provide: ComponentInstanceServiceNg2, useValue: {}},
+ {provide: MatchCapabilitiesRequirementsUtils, useValue: {}},
+ {provide: CompositionService, useValue: {}},
+ {provide: SdcUiServices.LoaderService, useValue: {}},
+ {provide: WorkspaceService, useValue: workspaceServiceMock},
+ {provide: SdcUiServices.NotificationsService, useValue: {}},
+ {provide: SdcUiServices.simplePopupMenuService, useValue: {}},
+ {provide: ServicePathGraphUtils, useValue: {}},
+ {provide: ModalService, useValue: {}},
+ {provide: PoliciesService, useValue: policyService},
+ {provide: GroupsService, useValue: {}},
+ {provide: Store, useValue: storeStub},
+ ],
+ });
+ };
+
+ configureTests(configure).then((testBed) => {
+ fixture = testBed.createComponent(CompositionGraphComponent);
+ instance = fixture.componentInstance;
+ instance._cy = cytoscape({});
+ });
+ })
+ );
+
+ it('composition graph component should be defined', () => {
+ expect(fixture).toBeDefined();
+ });
+
+ describe('on zone instance mode changed', () => {
+ let newZoneInstance: ZoneInstance;
+
+ beforeEach(
+ async(() => {
+ newZoneInstance = createPolicyInstance();
+ instance.zoneTagMode = null;
+ instance.zones = [];
+ instance.zones[ZoneInstanceType.POLICY] = new Zone('Policies', 'P', ZoneInstanceType.POLICY);
+ instance.zones[ZoneInstanceType.GROUP] = new Zone('Groups', 'G', ZoneInstanceType.GROUP);
+ instance.activeZoneInstance = createPolicyInstance();
+ }))
+
+ it('zone instance in tag mode and we want to turn tag mode off', () => {
+ instance.zoneTagMode = 'some_zone_id';
+ instance.activeZoneInstance = newZoneInstance;
+ instance.zoneInstanceModeChanged(ZoneInstanceMode.NONE, newZoneInstance, ZoneInstanceType.POLICY);
+ expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_CANVAS_TAG_END, newZoneInstance);
+ expect(instance.activeZoneInstance.mode).toBe(ZoneInstanceMode.SELECTED)
+ })
+
+ it('we are not in tag mode and policy instance mode changed to NONE - group and zone tag indication need to be removed', () => {
+ instance.zoneInstanceModeChanged(ZoneInstanceMode.NONE, newZoneInstance, ZoneInstanceType.POLICY);
+ expect(instance.compositionGraphZoneUtils.hideZoneTagIndications).toHaveBeenCalledWith(instance._cy);
+ expect(instance.compositionGraphZoneUtils.hideGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances);
+ })
+
+ it('we are not in tag mode and active zone instance gets hover/none - we dont actually change mode', () => {
+ let newMode = ZoneInstanceMode.SELECTED;
+ instance.zoneInstanceModeChanged(newMode, newZoneInstance, ZoneInstanceType.POLICY);
+ expect(newZoneInstance.mode).toBe(newMode);
+ })
+
+ it('we are not in tag mode and zone instance mode changed to HOVER mode', () => {
+ instance.zoneInstanceModeChanged(ZoneInstanceMode.HOVER, newZoneInstance, ZoneInstanceType.POLICY);
+ expect(instance.compositionGraphZoneUtils.showZoneTagIndications).toHaveBeenCalledWith(instance._cy, newZoneInstance);
+ expect(instance.compositionGraphZoneUtils.showGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances, newZoneInstance);
+ expect(instance.eventListenerService.notifyObservers).not.toHaveBeenCalled();
+ })
+
+ it('we are not in tag mode and mode changed to SELECTED', () => {
+ instance.zoneInstanceModeChanged(ZoneInstanceMode.SELECTED, newZoneInstance, ZoneInstanceType.POLICY);
+ expect(instance.compositionGraphZoneUtils.showZoneTagIndications).toHaveBeenCalledWith(instance._cy, newZoneInstance);
+ expect(instance.compositionGraphZoneUtils.showGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances, newZoneInstance);
+ expect(instance.activeZoneInstance).toBe(newZoneInstance);
+ expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_ZONE_INSTANCE_SELECTED, newZoneInstance);
+ expect(instance.store.dispatch).toHaveBeenCalledWith(new SetSelectedComponentAction({
+ component: newZoneInstance.instanceData,
+ type: SelectedComponentType[ZoneInstanceType[newZoneInstance.type]]
+ }));
+ expect(instance.eventListenerService.notifyObservers).not.toHaveBeenCalledWith(GRAPH_EVENTS.ON_CANVAS_TAG_START, ZoneInstanceType.POLICY);
+ })
+
+
+ it('we are not in tag mode and and zone instance mode changed to TAG', () => {
+ instance.zoneInstanceModeChanged(ZoneInstanceMode.TAG, newZoneInstance, ZoneInstanceType.POLICY);
+ expect(instance.compositionGraphZoneUtils.showZoneTagIndications).toHaveBeenCalledWith(instance._cy, newZoneInstance);
+ expect(instance.compositionGraphZoneUtils.showGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances, newZoneInstance);
+ expect(instance.activeZoneInstance).toBe(newZoneInstance);
+ expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_ZONE_INSTANCE_SELECTED, newZoneInstance);
+ expect(instance.store.dispatch).toHaveBeenCalledWith(new SetSelectedComponentAction({
+ component: newZoneInstance.instanceData,
+ type: SelectedComponentType[ZoneInstanceType[newZoneInstance.type]]
+ }));
+ expect(instance.compositionGraphZoneUtils.startCyTagMode).toHaveBeenCalledWith(instance._cy);
+ expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_CANVAS_TAG_START, ZoneInstanceType.POLICY);
+ })
+ })
+
+ it('unset active zone instance', () => {
+ instance.activeZoneInstance = createPolicyInstance();
+ instance.unsetActiveZoneInstance();
+ expect(instance.activeZoneInstance).toBeNull();
+ expect(instance.zoneTagMode).toBeNull();
+ })
+
+ it('zone background clicked - we are not in tag mode and active zone instance exist', () => {
+ instance.activeZoneInstance = createPolicyInstance();
+ jest.spyOn(instance, 'unsetActiveZoneInstance');
+ jest.spyOn(instance, 'selectTopologyTemplate');
+ instance.zoneBackgroundClicked();
+ expect(instance.unsetActiveZoneInstance).toHaveBeenCalled();
+ expect(instance.selectTopologyTemplate).toHaveBeenCalled();
+ })
+
+ it('zone background clicked - we are not in tag mode and no active zone instance exist', () => {
+ jest.spyOn(instance, 'unsetActiveZoneInstance');
+ jest.spyOn(instance, 'selectTopologyTemplate');
+ instance.zoneBackgroundClicked();
+ expect(instance.unsetActiveZoneInstance).not.toHaveBeenCalled();
+ expect(instance.selectTopologyTemplate).not.toHaveBeenCalled();
+ })
+
+ it('on zoom in', () => {
+ jest.spyOn(instance, 'zoom');
+ instance.zoom(true);
+ expect(instance.generalGraphUtils.zoomGraphTo).toHaveBeenCalledWith(instance._cy, instance._cy.zoom() + .1);
+ })
+
+ it('on zoom out', () => {
+ jest.spyOn(instance, 'zoom');
+ instance.zoom(false);
+ expect(instance.generalGraphUtils.zoomGraphTo).toHaveBeenCalledWith(instance._cy, instance._cy.zoom() - .1);
+ })
+
+ describe('cytoscape tap end event have been called', () => {
+
+ it('canvas background was clicked while zone instance in tag mode, zone instance still selected in tag mode)', () => {
+ let event = <Cy.EventObject>{cyTarget: instance._cy};
+ instance.zoneTagMode = 'instance_in_tag'
+ instance.onTapEnd(event);
+ expect(instance.zoneTagMode).toBe('instance_in_tag');
+ })
+
+ it('canvas background was clicked and no zone instance selected, topology template is now selected', () => {
+ let event = <Cy.EventObject>{cyTarget: instance._cy};
+ jest.spyOn(instance, 'selectTopologyTemplate');
+ instance.onTapEnd(event);
+ expect(instance.selectTopologyTemplate).toHaveBeenCalled();
+ expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED);
+ })
+
+ it('canvas background was clicked and zone instance was selected, topology template is now selected and zone instance is unselected', () => {
+ let event = <Cy.EventObject>{cyTarget: instance._cy};
+ instance.activeZoneInstance = createPolicyInstance();
+ jest.spyOn(instance, 'selectTopologyTemplate');
+ jest.spyOn(instance, 'unsetActiveZoneInstance');
+ instance.onTapEnd(event);
+ expect(instance.selectTopologyTemplate).toHaveBeenCalled();
+ expect(instance.unsetActiveZoneInstance).toHaveBeenCalled();
+ })
+
+
+ it('canvas background was clicked and zone instance was selected, topology template is now selected and zone instance is unselected', () => {
+ let event = <Cy.EventObject>{cyTarget: instance._cy};
+ instance.activeZoneInstance = createPolicyInstance();
+ jest.spyOn(instance, 'selectTopologyTemplate');
+ jest.spyOn(instance, 'unsetActiveZoneInstance');
+ instance.onTapEnd(event);
+ expect(instance.selectTopologyTemplate).toHaveBeenCalled();
+ expect(instance.unsetActiveZoneInstance).toHaveBeenCalled();
+ })
+
+ it('on simple edge clicked, open link menu and handle link click', () => {
+ let event = <Cy.EventObject>{
+ cyTarget: [{
+ isEdge: jest.fn().mockReturnValue(true),
+ data: jest.fn().mockReturnValue({type: 'simple'})
+ }
+ }];
+ instance.openModifyLinkMenu = jest.fn();
+ instance.onTapEnd(event);
+ expect(instance.compositionGraphLinkUtils.handleLinkClick).toHaveBeenCalledWith(instance._cy, event);
+ expect(instance.openModifyLinkMenu).toHaveBeenCalled();
+ })
+
+ it('on service path edge clicked, no menu is opened', () => {
+ let event = <Cy.EventObject>{
+ cyTarget: [{
+ isEdge: jest.fn().mockReturnValue(true),
+ data: jest.fn().mockReturnValue({type: 'service-path-link'})
+ }]
+ };
+ instance.openModifyLinkMenu = jest.fn();
+ instance.onTapEnd(event);
+ expect(instance.compositionGraphLinkUtils.handleLinkClick).toHaveBeenCalledWith(instance._cy, event);
+ expect(instance.openModifyLinkMenu).not.toHaveBeenCalled();
+ })
+
+ it('on drop after drag event (position has changed), call onNodesPositionChanged to update node position', () => {
+ let event = <Cy.EventObject>{
+ cyTarget: [{
+ isEdge: jest.fn().mockReturnValue(false),
+ position: jest.fn().mockReturnValue({x:2.11, y:2.44})
+ }]
+ };
+ instance.currentlyClickedNodePosition = <Cy.Position>{x:2.33, y:2.44};
+ instance.onTapEnd(event);
+ let nodesMoved: Cy.CollectionNodes = instance._cy.$(':grabbed');
+ expect(instance.nodesGraphUtils.onNodesPositionChanged).toHaveBeenCalledWith(instance._cy, instance.topologyTemplate, nodesMoved);
+
+ })
+
+ it('on node clicked (position not changed) while zone instance selected, unset active zone and call set selected instance', () => {
+ let event = <Cy.EventObject>{
+ cyTarget: [{
+ isEdge: jest.fn().mockReturnValue(false),
+ position: jest.fn().mockReturnValue({x:2.11, y:2.44}),
+ data: jest.fn().mockReturnValue({componentInstance: new ComponentInstance()})
+ }],
+ };
+ instance.currentlyClickedNodePosition = <Cy.Position>{x:2.11, y:2.44};
+ instance.activeZoneInstance = createPolicyInstance();
+ jest.spyOn(instance, 'unsetActiveZoneInstance');
+ jest.spyOn(instance, 'selectComponentInstance');
+ instance.onTapEnd(event);
+ expect(instance.unsetActiveZoneInstance).toHaveBeenCalled();
+ expect(instance.selectComponentInstance).toHaveBeenCalledWith(event.cyTarget[0].data().componentInstance);
+ })
+ })
+
+ it('initial view mode will turn off all cytoscape events', () => {
+ jest.spyOn(instance, 'isViewOnly').mockReturnValue(true);
+ jest.spyOn(instance._cy, 'off');
+ instance.initViewMode();
+ expect(instance._cy.off).toHaveBeenCalledWith('drag');
+ expect(instance._cy.off).toHaveBeenCalledWith('handlemouseout');
+ expect(instance._cy.off).toHaveBeenCalledWith('handlemouseover');
+ expect(instance._cy.off).toHaveBeenCalledWith('canvasredraw');
+ expect(instance._cy.off).toHaveBeenCalledWith('handletagclick');
+
+ })
+});
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts
new file mode 100644
index 0000000000..69ca3faaf5
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts
@@ -0,0 +1,768 @@
+/**
+ * Created by ob0695 on 4/24/2018.
+ */
+import { AfterViewInit, Component, ElementRef, HostBinding, Input } from '@angular/core';
+import { Select, Store } from '@ngxs/store';
+import {
+ ButtonModel,
+ Component as TopologyTemplate,
+ ComponentInstance,
+ CompositionCiNodeBase,
+ ConnectRelationModel,
+ GroupInstance,
+ LeftPaletteComponent,
+ LinkMenu,
+ Match,
+ ModalModel,
+ NodesFactory,
+ Point,
+ PolicyInstance,
+ PropertyBEModel,
+ Relationship,
+ StepModel,
+ Zone,
+ ZoneInstance,
+ ZoneInstanceAssignmentType,
+ ZoneInstanceMode,
+ ZoneInstanceType
+} from 'app/models';
+import { ForwardingPath } from 'app/models/forwarding-path';
+import { CompositionCiServicePathLink } from 'app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link';
+import { UIZoneInstanceObject } from 'app/models/ui-models/ui-zone-instance-object';
+import { CompositionService } from 'app/ng2/pages/composition/composition.service';
+import { CommonGraphUtils } from 'app/ng2/pages/composition/graph/common/common-graph-utils';
+import { ComponentInstanceNodesStyle } from 'app/ng2/pages/composition/graph/common/style/component-instances-nodes-style';
+import { ConnectionPropertiesViewComponent } from 'app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component';
+import { ConnectionWizardHeaderComponent } from 'app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component';
+import { ConnectionWizardService } from 'app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service';
+import { FromNodeStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component';
+import { PropertiesStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component';
+import { ToNodeStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component';
+import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service';
+import { ComponentInstanceServiceNg2 } from 'app/ng2/services/component-instance-services/component-instance.service';
+import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service';
+import { ModalService } from 'app/ng2/services/modal.service';
+import { ComponentGenericResponse } from 'app/ng2/services/responses/component-generic-response';
+import { ServiceGenericResponse } from 'app/ng2/services/responses/service-generic-response';
+import { WorkspaceState } from 'app/ng2/store/states/workspace.state';
+import { EventListenerService } from 'app/services';
+import { ComponentInstanceFactory, EVENTS, SdcElementType } from 'app/utils';
+import { ComponentType, GRAPH_EVENTS, GraphColors, DEPENDENCY_EVENTS } from 'app/utils/constants';
+import * as _ from 'lodash';
+import { DndDropEvent } from 'ngx-drag-drop/ngx-drag-drop';
+import { SdcUiServices } from 'onap-ui-angular';
+import { NotificationSettings } from 'onap-ui-angular/dist/notifications/utilities/notification.config';
+import { menuItem } from 'onap-ui-angular/dist/simple-popup-menu/menu-data.interface';
+import { CytoscapeEdgeEditation } from '../../../../../third-party/cytoscape.js-edge-editation/CytoscapeEdgeEditation.js';
+import { SelectedComponentType, SetSelectedComponentAction } from '../common/store/graph.actions';
+import { GraphState } from '../common/store/graph.state';
+import {
+ CompositionGraphGeneralUtils,
+ CompositionGraphNodesUtils,
+ CompositionGraphZoneUtils,
+ MatchCapabilitiesRequirementsUtils
+} from './utils';
+import { CompositionGraphLinkUtils } from './utils/composition-graph-links-utils';
+import { CompositionGraphPaletteUtils } from './utils/composition-graph-palette-utils';
+import { ServicePathGraphUtils } from './utils/composition-graph-service-path-utils';
+
+declare const window: any;
+
+@Component({
+ selector: 'composition-graph',
+ templateUrl: './composition-graph.component.html',
+ styleUrls: ['./composition-graph.component.less']
+})
+
+export class CompositionGraphComponent implements AfterViewInit {
+
+ @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean;
+ @Select(GraphState.withSidebar) withSidebar$: boolean;
+ @Input() topologyTemplate: TopologyTemplate;
+ @HostBinding('attr.data-tests-id') dataTestId: string;
+ @Input() testId: string;
+
+ // tslint:disable:variable-name
+ private _cy: Cy.Instance;
+ private zoneTagMode: string;
+ private activeZoneInstance: ZoneInstance;
+ private zones: Zone[];
+ private currentlyClickedNodePosition: Cy.Position;
+ private dragElement: JQuery;
+ private dragComponent: ComponentInstance;
+ private componentInstanceNames: string[];
+ private topologyTemplateId: string;
+ private topologyTemplateType: string;
+
+ constructor(private elRef: ElementRef,
+ private nodesFactory: NodesFactory,
+ private eventListenerService: EventListenerService,
+ private compositionGraphZoneUtils: CompositionGraphZoneUtils,
+ private generalGraphUtils: CompositionGraphGeneralUtils,
+ private compositionGraphLinkUtils: CompositionGraphLinkUtils,
+ private nodesGraphUtils: CompositionGraphNodesUtils,
+ private connectionWizardService: ConnectionWizardService,
+ private commonGraphUtils: CommonGraphUtils,
+ private modalService: ModalService,
+ private compositionGraphPaletteUtils: CompositionGraphPaletteUtils,
+ private topologyTemplateService: TopologyTemplateService,
+ private componentInstanceService: ComponentInstanceServiceNg2,
+ private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils,
+ private store: Store,
+ private compositionService: CompositionService,
+ private loaderService: SdcUiServices.LoaderService,
+ private workspaceService: WorkspaceService,
+ private notificationService: SdcUiServices.NotificationsService,
+ private simplePopupMenuService: SdcUiServices.simplePopupMenuService,
+ private servicePathGraphUtils: ServicePathGraphUtils) {
+ }
+
+ ngOnInit() {
+ this.dataTestId = this.testId;
+ this.topologyTemplateId = this.workspaceService.metadata.uniqueId;
+ this.topologyTemplateType = this.workspaceService.metadata.componentType;
+
+ this.store.dispatch(new SetSelectedComponentAction({
+ component: this.topologyTemplate,
+ type: SelectedComponentType.TOPOLOGY_TEMPLATE
+ }));
+ this.eventListenerService.registerObserverCallback(EVENTS.ON_CHECKOUT, () => {
+ this.loadGraphData();
+ });
+ this.loadCompositionData();
+ }
+
+ ngAfterViewInit() {
+ this.loadGraph();
+ }
+
+ ngOnDestroy() {
+ this._cy.destroy();
+ _.forEach(GRAPH_EVENTS, (event) => {
+ this.eventListenerService.unRegisterObserver(event);
+ });
+ this.eventListenerService.unRegisterObserver(EVENTS.ON_CHECKOUT);
+ this.eventListenerService.unRegisterObserver(DEPENDENCY_EVENTS.ON_DEPENDENCY_CHANGE);
+ }
+
+ public isViewOnly = (): boolean => {
+ return this.store.selectSnapshot((state) => state.workspace.isViewOnly);
+ }
+
+ public zoom = (zoomIn: boolean): void => {
+ const currentZoom: number = this._cy.zoom();
+ if (zoomIn) {
+ this.generalGraphUtils.zoomGraphTo(this._cy, currentZoom + .1);
+ } else {
+ this.generalGraphUtils.zoomGraphTo(this._cy, currentZoom - .1);
+ }
+ }
+
+ public zoomAllWithoutSidebar = () => {
+ setTimeout(() => { // wait for sidebar changes to take effect before zooming
+ this.generalGraphUtils.zoomAll(this._cy);
+ });
+ }
+
+ public getAutoCompleteValues = (searchTerm: string) => {
+ if (searchTerm.length > 1) { // US requirement: only display search results after 2nd letter typed.
+ const nodes: Cy.CollectionNodes = this.nodesGraphUtils.getMatchingNodesByName(this._cy, searchTerm);
+ this.componentInstanceNames = _.map(nodes, (node) => node.data('name'));
+ } else {
+ this.componentInstanceNames = [];
+ }
+ }
+
+ public highlightSearchMatches = (searchTerm: string) => {
+ this.nodesGraphUtils.highlightMatchingNodesByName(this._cy, searchTerm);
+ const matchingNodes: Cy.CollectionNodes = this.nodesGraphUtils.getMatchingNodesByName(this._cy, searchTerm);
+ this.generalGraphUtils.zoomAll(this._cy, matchingNodes);
+ }
+
+ public onDrop = (dndEvent: DndDropEvent) => {
+ this.compositionGraphPaletteUtils.addNodeFromPalette(this._cy, dndEvent);
+ }
+
+ public openServicePathMenu = ($event): void => {
+
+ const menuConfig: menuItem[] = [];
+ if (!this.isViewOnly()) {
+ menuConfig.push({
+ text: 'Create Service Flow',
+ action: () => this.servicePathGraphUtils.onCreateServicePath()
+ });
+ }
+ menuConfig.push({
+ text: 'Service Flows List',
+ type: '',
+ action: () => this.servicePathGraphUtils.onListServicePath()
+ });
+ const popup = this.simplePopupMenuService.openBaseMenu(menuConfig, {
+ x: $event.x,
+ y: $event.y
+ });
+
+ }
+
+ public deletePathsOnCy = () => {
+ this.servicePathGraphUtils.deletePathsFromGraph(this._cy);
+ }
+
+ public drawPathOnCy = (data: ForwardingPath) => {
+ this.servicePathGraphUtils.drawPath(this._cy, data);
+ }
+
+ public onTapEnd = (event: Cy.EventObject) => {
+ if (this.zoneTagMode) {
+ return;
+ }
+ if (event.cyTarget === this._cy) { // On Background clicked
+ if (this._cy.$('node:selected').length === 0) { // if the background click but not dragged
+ if (this.activeZoneInstance) {
+ this.unsetActiveZoneInstance();
+ this.selectTopologyTemplate();
+ } else {
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED);
+ this.selectTopologyTemplate();
+ }
+
+ }
+ } else if (event.cyTarget[0].isEdge()) { // and Edge clicked
+ this.compositionGraphLinkUtils.handleLinkClick(this._cy, event);
+ if (event.cyTarget[0].data().type === CompositionCiServicePathLink.LINK_TYPE) {
+ return;
+ }
+ this.openModifyLinkMenu(this.compositionGraphLinkUtils.getModifyLinkMenu(event.cyTarget[0], event), event);
+ } else { // On Node clicked
+
+ this._cy.nodes(':grabbed').style({'overlay-opacity': 0});
+
+ const newPosition = event.cyTarget[0].position();
+ // node position changed (drop after drag event) - we need to update position
+ if (this.currentlyClickedNodePosition.x !== newPosition.x || this.currentlyClickedNodePosition.y !== newPosition.y) {
+ const nodesMoved: Cy.CollectionNodes = this._cy.$(':grabbed');
+ this.nodesGraphUtils.onNodesPositionChanged(this._cy, this.topologyTemplate, nodesMoved);
+ } else {
+ if (this.activeZoneInstance) {
+ this.unsetActiveZoneInstance();
+ }
+ this.selectComponentInstance(event.cyTarget[0].data().componentInstance);
+ }
+ }
+ }
+
+ private registerCytoscapeGraphEvents() {
+
+ this._cy.on('addedgemouseup', (event, data) => {
+ const connectRelationModel: ConnectRelationModel = this.compositionGraphLinkUtils.onLinkDrawn(this._cy, data.source, data.target);
+ if (connectRelationModel != null) {
+ this.connectionWizardService.setRelationMenuDirectiveObj(connectRelationModel);
+ this.connectionWizardService.selectedMatch = null;
+
+ const steps: StepModel[] = [];
+ const fromNodeName: string = connectRelationModel.fromNode.componentInstance.name;
+ const toNodeName: string = connectRelationModel.toNode.componentInstance.name;
+ steps.push(new StepModel(fromNodeName, FromNodeStepComponent));
+ steps.push(new StepModel(toNodeName, ToNodeStepComponent));
+ steps.push(new StepModel('Properties', PropertiesStepComponent));
+ const wizardTitle = 'Connect: ' + fromNodeName + ' to ' + toNodeName;
+ const modalInstance = this.modalService.createMultiStepsWizard(wizardTitle, steps, this.createLinkFromMenu, ConnectionWizardHeaderComponent);
+ modalInstance.instance.open();
+ }
+ });
+
+ this._cy.on('tapstart', 'node', (event: Cy.EventObject) => {
+ this.currentlyClickedNodePosition = angular.copy(event.cyTarget[0].position()); // update node position on drag
+ });
+
+ this._cy.on('drag', 'node', (event: Cy.EventObject) => {
+ if (event.cyTarget.data().componentSubType !== SdcElementType.POLICY && event.cyTarget.data().componentSubType !== SdcElementType.GROUP) {
+ event.cyTarget.style({'overlay-opacity': 0.24});
+ if (this.generalGraphUtils.isValidDrop(this._cy, event.cyTarget)) {
+ event.cyTarget.style({'overlay-color': GraphColors.NODE_BACKGROUND_COLOR});
+ } else {
+ event.cyTarget.style({'overlay-color': GraphColors.NODE_OVERLAPPING_BACKGROUND_COLOR});
+ }
+ }
+ });
+
+ this._cy.on('handlemouseover', (event, payload) => {
+ // no need to add opacity while we are dragging and hovering othe nodes- or if opacity was already calculated for these nodes
+ if (payload.node.grabbed() || this._cy.scratch('_edge_editation_highlights') === true) {
+ return;
+ }
+
+ if (this.zoneTagMode) {
+ this.zoneTagMode = this.zones[this.activeZoneInstance.type].getHoverTagModeId();
+ return;
+ }
+
+ const nodesData = this.nodesGraphUtils.getAllNodesData(this._cy.nodes());
+ const nodesLinks = this.generalGraphUtils.getAllCompositionCiLinks(this._cy);
+ const instance = payload.node.data().componentInstance;
+ const filteredNodesData = this.matchCapabilitiesRequirementsUtils.findMatchingNodesToComponentInstance(instance, nodesData, nodesLinks);
+ this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, this._cy);
+ this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, this._cy, payload.node.data());
+
+ this._cy.scratch()._edge_editation_highlights = true;
+ });
+
+ this._cy.on('handlemouseout', () => {
+ if (this.zoneTagMode) {
+ this.zoneTagMode = this.zones[this.activeZoneInstance.type].getTagModeId();
+ return;
+ }
+ if (this._cy.scratch('_edge_editation_highlights') === true) {
+ this._cy.removeScratch('_edge_editation_highlights');
+ this._cy.emit('hidehandles');
+ this.matchCapabilitiesRequirementsUtils.resetFadedNodes(this._cy);
+ }
+ });
+
+ this._cy.on('tapend', (event: Cy.EventObject) => {
+ this.onTapEnd(event);
+ });
+
+ this._cy.on('boxselect', 'node', (event: Cy.EventObject) => {
+ this.unsetActiveZoneInstance();
+ this.selectComponentInstance(event.cyTarget.data().componentInstance);
+ });
+
+ this._cy.on('canvasredraw', (event: Cy.EventObject) => {
+ if (this.zoneTagMode) {
+ this.compositionGraphZoneUtils.showZoneTagIndications(this._cy, this.activeZoneInstance);
+ }
+ });
+
+ this._cy.on('handletagclick', (event: Cy.EventObject, eventData: any) => {
+ this.compositionGraphZoneUtils.handleTagClick(this._cy, this.activeZoneInstance, eventData.nodeId);
+ });
+ }
+
+ private initViewMode() {
+
+ if (this.isViewOnly()) {
+ // remove event listeners
+ this._cy.off('drag');
+ this._cy.off('handlemouseout');
+ this._cy.off('handlemouseover');
+ this._cy.off('canvasredraw');
+ this._cy.off('handletagclick');
+ this._cy.edges().unselectify();
+ }
+ }
+
+ private saveChangedCapabilityProperties = (): Promise<PropertyBEModel[]> => {
+ return new Promise<PropertyBEModel[]>((resolve) => {
+ const capabilityPropertiesBE: PropertyBEModel[] = this.connectionWizardService.changedCapabilityProperties.map((prop) => {
+ prop.value = prop.getJSONValue();
+ const propBE = new PropertyBEModel(prop);
+ propBE.parentUniqueId = this.connectionWizardService.selectedMatch.relationship.relation.capabilityOwnerId;
+ return propBE;
+ });
+ if (capabilityPropertiesBE.length > 0) {
+ // if there are capability properties to update, then first update capability properties and then resolve promise
+ this.componentInstanceService
+ .updateInstanceCapabilityProperties(
+ this.topologyTemplate,
+ this.connectionWizardService.selectedMatch.toNode,
+ this.connectionWizardService.selectedMatch.capability,
+ capabilityPropertiesBE
+ )
+ .subscribe((response) => {
+ console.log('Update resource instance capability properties response: ', response);
+ this.connectionWizardService.changedCapabilityProperties = [];
+ resolve(capabilityPropertiesBE);
+ });
+ } else {
+ // no capability properties to update, immediately resolve promise
+ resolve(capabilityPropertiesBE);
+ }
+ });
+ }
+
+ private loadCompositionData = () => {
+ this.loaderService.activate();
+ this.topologyTemplateService.getComponentCompositionData(this.topologyTemplateId, this.topologyTemplateType).subscribe((response: ComponentGenericResponse) => {
+ if (this.topologyTemplateType === ComponentType.SERVICE) {
+ this.compositionService.forwardingPaths = (response as ServiceGenericResponse).forwardingPaths;
+ }
+ this.compositionService.componentInstances = response.componentInstances;
+ this.compositionService.componentInstancesRelations = response.componentInstancesRelations;
+ this.compositionService.groupInstances = response.groupInstances;
+ this.compositionService.policies = response.policies;
+ this.loadGraphData();
+ this.loaderService.deactivate();
+ }, (error) => { this.loaderService.deactivate(); });
+ }
+
+ private loadGraph = () => {
+ const graphEl = this.elRef.nativeElement.querySelector('.sdc-composition-graph-wrapper');
+ this.initGraph(graphEl);
+ this.zones = this.compositionGraphZoneUtils.createCompositionZones();
+ this.registerCytoscapeGraphEvents();
+ this.registerCustomEvents();
+ this.initViewMode();
+ }
+
+ private initGraphNodes() {
+
+ setTimeout(() => {
+ const handles = new CytoscapeEdgeEditation();
+ handles.init(this._cy);
+ if (!this.isViewOnly()) { // Init nodes handle extension - enable dynamic links
+ handles.initNodeEvents();
+ handles.registerHandle(ComponentInstanceNodesStyle.getAddEdgeHandle());
+ }
+ handles.registerHandle(ComponentInstanceNodesStyle.getTagHandle());
+ handles.registerHandle(ComponentInstanceNodesStyle.getTaggedPolicyHandle());
+ handles.registerHandle(ComponentInstanceNodesStyle.getTaggedGroupHandle());
+ }, 0);
+
+ _.each(this.compositionService.componentInstances, (instance) => {
+ const compositionGraphNode: CompositionCiNodeBase = this.nodesFactory.createNode(instance);
+ this.commonGraphUtils.addComponentInstanceNodeToGraph(this._cy, compositionGraphNode);
+ });
+
+ }
+
+ private loadGraphData = () => {
+ this.initGraphNodes();
+ this.compositionGraphLinkUtils.initGraphLinks(this._cy, this.compositionService.componentInstancesRelations);
+ this.compositionGraphZoneUtils.initZoneInstances(this.zones);
+ setTimeout(() => { // Need setTimeout so that angular canvas changes will take effect before resize & center
+ this.generalGraphUtils.zoomAllWithMax(this._cy, 1);
+ });
+ this.componentInstanceNames = _.map(this._cy.nodes(), (node) => node.data('name'));
+ }
+
+ private initGraph(graphEl: JQuery) {
+
+ this._cy = cytoscape({
+ container: graphEl,
+ style: ComponentInstanceNodesStyle.getCompositionGraphStyle(),
+ zoomingEnabled: true,
+ maxZoom: 1.2,
+ minZoom: .1,
+ userZoomingEnabled: false,
+ userPanningEnabled: true,
+ selectionType: 'single',
+ boxSelectionEnabled: true,
+ autolock: this.isViewOnly(),
+ autoungrabify: this.isViewOnly()
+ });
+
+ // Testing Bridge that allows Cypress tests to select a component on canvas not via DOM
+ if (window.Cypress) {
+ window.testBridge = this.createCanvasTestBridge();
+ }
+ }
+
+ private createCanvasTestBridge(): any {
+ return {
+ selectComponentInstance: (componentName: string) => {
+ const matchingNodesByName = this.nodesGraphUtils.getMatchingNodesByName(this._cy, componentName);
+ const component = new ComponentInstance(matchingNodesByName.first().data().componentInstance);
+ this.selectComponentInstance(component);
+ }
+ };
+ }
+
+ // -------------------------------------------- ZONES---------------------------------------------------------//
+ private zoneMinimizeToggle = (zoneType: ZoneInstanceType): void => {
+ this.zones[zoneType].minimized = !this.zones[zoneType].minimized;
+ }
+
+ private zoneInstanceModeChanged = (newMode: ZoneInstanceMode, instance: ZoneInstance, zoneId: ZoneInstanceType): void => {
+ if (this.zoneTagMode) { // we're in tag mode.
+ if (instance === this.activeZoneInstance && newMode === ZoneInstanceMode.NONE) { // we want to turn tag mode off.
+ this.zoneTagMode = null;
+ this.activeZoneInstance.mode = ZoneInstanceMode.SELECTED;
+ this.compositionGraphZoneUtils.endCyTagMode(this._cy);
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CANVAS_TAG_END, instance);
+
+ }
+ } else {
+ // when active zone instance gets hover/none, don't actually change mode, just show/hide indications
+ if (instance !== this.activeZoneInstance || (instance === this.activeZoneInstance && newMode > ZoneInstanceMode.HOVER)) {
+ instance.mode = newMode;
+ }
+
+ if (newMode === ZoneInstanceMode.NONE) {
+ this.compositionGraphZoneUtils.hideZoneTagIndications(this._cy);
+ if (this.zones[ZoneInstanceType.GROUP]) {
+ this.compositionGraphZoneUtils.hideGroupZoneIndications(this.zones[ZoneInstanceType.GROUP].instances);
+ }
+ }
+ if (newMode >= ZoneInstanceMode.HOVER) {
+ this.compositionGraphZoneUtils.showZoneTagIndications(this._cy, instance);
+ if (instance.type === ZoneInstanceType.POLICY && this.zones[ZoneInstanceType.GROUP]) {
+ this.compositionGraphZoneUtils.showGroupZoneIndications(this.zones[ZoneInstanceType.GROUP].instances, instance);
+ }
+ }
+ if (newMode >= ZoneInstanceMode.SELECTED) {
+ this._cy.$('node:selected').unselect();
+ if (this.activeZoneInstance && this.activeZoneInstance !== instance && newMode >= ZoneInstanceMode.SELECTED) {
+ this.activeZoneInstance.mode = ZoneInstanceMode.NONE;
+ }
+ this.activeZoneInstance = instance;
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_ZONE_INSTANCE_SELECTED, instance);
+ this.store.dispatch(new SetSelectedComponentAction({
+ component: instance.instanceData,
+ type: SelectedComponentType[ZoneInstanceType[instance.type]]
+ }));
+ }
+ if (newMode === ZoneInstanceMode.TAG) {
+ this.compositionGraphZoneUtils.startCyTagMode(this._cy);
+ this.zoneTagMode = this.zones[zoneId].getTagModeId();
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CANVAS_TAG_START, zoneId);
+ }
+ }
+ }
+
+ private zoneInstanceTagged = (taggedInstance: ZoneInstance) => {
+ this.activeZoneInstance.addOrRemoveAssignment(taggedInstance.instanceData.uniqueId, ZoneInstanceAssignmentType.GROUPS);
+ const newHandle: string = this.compositionGraphZoneUtils.getCorrectHandleForNode(taggedInstance.instanceData.uniqueId, this.activeZoneInstance);
+ taggedInstance.showHandle(newHandle);
+ }
+
+ private unsetActiveZoneInstance = (): void => {
+ if (this.activeZoneInstance) {
+ this.activeZoneInstance.mode = ZoneInstanceMode.NONE;
+ this.activeZoneInstance = null;
+ this.zoneTagMode = null;
+ }
+ }
+
+ private selectComponentInstance = (componentInstance: ComponentInstance) => {
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_NODE_SELECTED, componentInstance);
+ this.store.dispatch(new SetSelectedComponentAction({
+ component: componentInstance,
+ type: SelectedComponentType.COMPONENT_INSTANCE
+ }));
+ }
+
+ private selectTopologyTemplate = () => {
+ this.store.dispatch(new SetSelectedComponentAction({
+ component: this.topologyTemplate,
+ type: SelectedComponentType.TOPOLOGY_TEMPLATE
+ }));
+ }
+
+ private zoneBackgroundClicked = (): void => {
+ if (!this.zoneTagMode && this.activeZoneInstance) {
+ this.unsetActiveZoneInstance();
+ this.selectTopologyTemplate();
+ }
+ }
+
+ private zoneAssignmentSaveStart = () => {
+ this.loaderService.activate();
+ }
+
+ private zoneAssignmentSaveComplete = (success: boolean) => {
+ this.loaderService.deactivate();
+ if (!success) {
+ this.notificationService.push(new NotificationSettings('error', 'Update Failed', 'Error'));
+ }
+ }
+
+ private deleteZoneInstance = (deletedInstance: UIZoneInstanceObject) => {
+ if (deletedInstance.type === ZoneInstanceType.POLICY) {
+ this.compositionService.policies = this.compositionService.policies.filter((policy) => policy.uniqueId !== deletedInstance.uniqueId);
+ } else if (deletedInstance.type === ZoneInstanceType.GROUP) {
+ this.compositionService.groupInstances = this.compositionService.groupInstances.filter((group) => group.uniqueId !== deletedInstance.uniqueId);
+ }
+ // remove it from zones
+ this.zones[deletedInstance.type].removeInstance(deletedInstance.uniqueId);
+ if (deletedInstance.type === ZoneInstanceType.GROUP && !_.isEmpty(this.zones[ZoneInstanceType.POLICY])) {
+ this.compositionGraphZoneUtils.updateTargetsOrMembersOnCanvasDelete(deletedInstance.uniqueId, [this.zones[ZoneInstanceType.POLICY]], ZoneInstanceAssignmentType.GROUPS);
+ }
+ this.selectTopologyTemplate();
+ }
+ // -------------------------------------------------------------------------------------------------------------//
+
+ private registerCustomEvents() {
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, (groupInstance: GroupInstance) => {
+ this.compositionGraphZoneUtils.findAndUpdateZoneInstanceData(this.zones, groupInstance);
+ this.notificationService.push(new NotificationSettings('success', 'Group Updated', 'Success'));
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, (policyInstance: PolicyInstance) => {
+ this.compositionGraphZoneUtils.findAndUpdateZoneInstanceData(this.zones, policyInstance);
+ this.notificationService.push(new NotificationSettings('success', 'Policy Updated', 'Success'));
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, (leftPaletteComponent: LeftPaletteComponent) => {
+ this.compositionGraphPaletteUtils.onComponentHoverIn(leftPaletteComponent, this._cy);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_ADD_ZONE_INSTANCE_FROM_PALETTE,
+ (component: TopologyTemplate, paletteComponent: LeftPaletteComponent, startPosition: Point) => {
+
+ const zoneType: ZoneInstanceType = this.compositionGraphZoneUtils.getZoneTypeForPaletteComponent(paletteComponent.categoryType);
+ this.compositionGraphZoneUtils.showZone(this.zones[zoneType]);
+
+ this.loaderService.activate();
+ this.compositionGraphZoneUtils.createZoneInstanceFromLeftPalette(zoneType, paletteComponent.type).subscribe((zoneInstance: ZoneInstance) => {
+ this.loaderService.deactivate();
+ this.compositionGraphZoneUtils.addInstanceToZone(this.zones[zoneInstance.type], zoneInstance, true);
+ this.compositionGraphZoneUtils.createPaletteToZoneAnimation(startPosition, zoneType, zoneInstance);
+ }, (error) => {
+ this.loaderService.deactivate();
+ });
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_OUT, () => {
+ this._cy.emit('hidehandles');
+ this.matchCapabilitiesRequirementsUtils.resetFadedNodes(this._cy);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_START, (dragElement, dragComponent) => {
+ this.dragElement = dragElement;
+ this.dragComponent = ComponentInstanceFactory.createComponentInstanceFromComponent(dragComponent);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_ACTION, (position: Point) => {
+ const draggedElement = document.getElementById('draggable_element');
+ draggedElement.className = this.compositionGraphPaletteUtils.isDragValid(this._cy, position) ? 'valid-drag' : 'invalid-drag';
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DROP, (event: DndDropEvent) => {
+ this.onDrop(event);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_COMPONENT_INSTANCE_NAME_CHANGED, (component: ComponentInstance) => {
+ const selectedNode = this._cy.getElementById(component.uniqueId);
+ selectedNode.data().componentInstance.name = component.name;
+ selectedNode.data('name', component.name); // used for tooltip
+ selectedNode.data('displayName', selectedNode.data().getDisplayName()); // abbreviated
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, (componentInstanceId: string) => {
+ const nodeToDelete = this._cy.getElementById(componentInstanceId);
+ this.nodesGraphUtils.deleteNode(this._cy, this.topologyTemplate, nodeToDelete);
+ this.selectTopologyTemplate();
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_ZONE_INSTANCE, (deletedInstance: UIZoneInstanceObject) => {
+ this.deleteZoneInstance(deletedInstance);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, (componentInstanceId: string) => {
+ if (!_.isEmpty(this.zones)) {
+ this.compositionGraphZoneUtils.updateTargetsOrMembersOnCanvasDelete(componentInstanceId, this.zones, ZoneInstanceAssignmentType.COMPONENT_INSTANCES);
+ }
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_EDGE, (releaseLoading: boolean, linksToDelete: Cy.CollectionEdges) => {
+ this.compositionGraphLinkUtils.deleteLink(this._cy, this.topologyTemplate, releaseLoading, linksToDelete);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_VERSION_CHANGED, (component: ComponentInstance) => {
+ // Remove everything from graph and reload it all
+ this._cy.elements().remove();
+ this.loadCompositionData();
+ setTimeout(() => { this._cy.getElementById(component.uniqueId).select(); }, 1000);
+ this.selectComponentInstance(component);
+ });
+ this.eventListenerService.registerObserverCallback(DEPENDENCY_EVENTS.ON_DEPENDENCY_CHANGE, (ischecked: boolean) => {
+ if (ischecked) {
+ this._cy.$('node:selected').addClass('dependent');
+ } else {
+ // due to defect in cytoscape, just changing the class does not replace the icon, and i need to revert to original icon with no markings.
+ this._cy.$('node:selected').removeClass('dependent');
+ this._cy.$('node:selected').style({'background-image': this._cy.$('node:selected').data('originalImg')});
+ }
+ });
+ }
+ private createLinkFromMenu = (): void => {
+ this.saveChangedCapabilityProperties().then(() => {
+ this.compositionGraphLinkUtils.createLinkFromMenu(this._cy, this.connectionWizardService.selectedMatch);
+ });
+ }
+
+ private deleteRelation = (link: Cy.CollectionEdges) => {
+ // if multiple edges selected, delete the VL itself so edges get deleted automatically
+ if (this._cy.$('edge:selected').length > 1) {
+ this.nodesGraphUtils.deleteNode(this._cy, this.topologyTemplate, this._cy.$('node:selected'));
+ } else {
+ this.compositionGraphLinkUtils.deleteLink(this._cy, this.topologyTemplate, true, link);
+ }
+ }
+
+ private viewRelation = (link: Cy.CollectionEdges) => {
+
+ const linkData = link.data();
+ const sourceNode: CompositionCiNodeBase = link.source().data();
+ const targetNode: CompositionCiNodeBase = link.target().data();
+ const relationship: Relationship = linkData.relation.relationships[0];
+
+ this.compositionGraphLinkUtils.getRelationRequirementCapability(relationship, sourceNode.componentInstance, targetNode.componentInstance).then((objReqCap) => {
+ const capability = objReqCap.capability;
+ const requirement = objReqCap.requirement;
+
+ this.connectionWizardService.connectRelationModel = new ConnectRelationModel(sourceNode, targetNode, []);
+ this.connectionWizardService.selectedMatch = new Match(requirement, capability, true, linkData.source, linkData.target);
+ this.connectionWizardService.selectedMatch.relationship = relationship;
+
+ const title = `Connection Properties`;
+ const saveButton: ButtonModel = new ButtonModel('Save', 'blue', () => {
+ this.saveChangedCapabilityProperties().then(() => {
+ this.modalService.closeCurrentModal();
+ });
+ });
+ const cancelButton: ButtonModel = new ButtonModel('Cancel', 'white', () => {
+ this.modalService.closeCurrentModal();
+ });
+ const modal = new ModalModel('xl', title, '', [saveButton, cancelButton]);
+ const modalInstance = this.modalService.createCustomModal(modal);
+ this.modalService.addDynamicContentToModal(modalInstance, ConnectionPropertiesViewComponent);
+ modalInstance.instance.open();
+
+ new Promise((resolve) => {
+ if (!this.connectionWizardService.selectedMatch.capability.properties) {
+ this.componentInstanceService.getInstanceCapabilityProperties(this.topologyTemplateType, this.topologyTemplateId, linkData.target, capability)
+ .subscribe(() => {
+ resolve();
+ }, () => { /* do nothing */ });
+ } else {
+ resolve();
+ }
+ }).then(() => {
+ this.modalService.addDynamicContentToModal(modalInstance, ConnectionPropertiesViewComponent);
+ });
+ }, () => { /* do nothing */ });
+ }
+
+ private openModifyLinkMenu = (linkMenuObject: LinkMenu, $event) => {
+
+ const menuConfig: menuItem[] = [{
+ text: 'View',
+ iconName: 'eye-o',
+ iconType: 'common',
+ iconMode: 'secondary',
+ iconSize: 'small',
+ type: '',
+ action: () => this.viewRelation(linkMenuObject.link as Cy.CollectionEdges)
+ }];
+
+ if (!this.isViewOnly()) {
+ menuConfig.push({
+ text: 'Delete',
+ iconName: 'trash-o',
+ iconType: 'common',
+ iconMode: 'secondary',
+ iconSize: 'small',
+ type: '',
+ action: () => this.deleteRelation(linkMenuObject.link as Cy.CollectionEdges)
+ });
+ }
+ this.simplePopupMenuService.openBaseMenu(menuConfig, {
+ x: $event.originalEvent.x,
+ y: $event.originalEvent.y
+ });
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.module.ts
new file mode 100644
index 0000000000..e58d160c4d
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.module.ts
@@ -0,0 +1,55 @@
+import {NgModule} from "@angular/core";
+import {CommonModule} from "@angular/common";
+import {CompositionGraphComponent} from "./composition-graph.component";
+import {ZoneModules} from "./canvas-zone/zones-module";
+import {CompositionGraphZoneUtils} from "./utils/composition-graph-zone-utils";
+import {CompositionGraphGeneralUtils} from "./utils/composition-graph-general-utils";
+import {CommonGraphUtils} from "./common/common-graph-utils";
+import {LinksFactory} from "app/models/graph/graph-links/links-factory";
+import {NodesFactory} from "app/models/graph/nodes/nodes-factory";
+import {ImageCreatorService} from "./common/image-creator.service";
+import {MatchCapabilitiesRequirementsUtils} from "./utils/match-capability-requierment-utils";
+import {CompositionGraphNodesUtils} from "./utils/composition-graph-nodes-utils";
+import {ConnectionWizardService} from "app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service";
+import {CompositionGraphPaletteUtils} from "./utils/composition-graph-palette-utils";
+import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils";
+import {DndModule} from "ngx-drag-drop";
+import { MenuListNg2Module } from "app/ng2/components/downgrade-wrappers/menu-list-ng2/menu-list-ng2.module";
+import { UiElementsModule } from "app/ng2/components/ui/ui-elements.module";
+import {ServicePathSelectorModule} from "./service-path-selector/service-path-selector.module";
+import {SdcUiComponentsModule, SdcUiServices} from "onap-ui-angular";
+import {CanvasSearchModule} from "./canvas-search/canvas-search.module";
+import {CompositionGraphLinkUtils, ServicePathGraphUtils} from "./utils";
+
+
+@NgModule({
+ declarations: [CompositionGraphComponent],
+ imports: [CommonModule,
+ ServicePathSelectorModule,
+ SdcUiComponentsModule,
+ MenuListNg2Module,
+ UiElementsModule,
+ ZoneModules,
+ CanvasSearchModule,
+ DndModule],
+ exports: [CompositionGraphComponent],
+ entryComponents: [CompositionGraphComponent],
+ providers: [
+ CompositionGraphZoneUtils,
+ CompositionGraphGeneralUtils,
+ MatchCapabilitiesRequirementsUtils,
+ CompositionGraphNodesUtils,
+ CompositionGraphLinkUtils,
+ CommonGraphUtils,
+ NodesFactory,
+ LinksFactory,
+ ImageCreatorService,
+ ConnectionWizardService,
+ CompositionGraphPaletteUtils,
+ QueueServiceUtils,
+ SdcUiServices.simplePopupMenuService,
+ ServicePathGraphUtils
+ ]
+})
+export class CompositionGraphModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.html
index 482de5eacf..b24e469554 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.component.html
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.html
@@ -14,14 +14,7 @@
~ limitations under the License.
-->
-<sdc-tabs>
- <sdc-tab titleIcon="info-circle">
- <group-information-tab [group]="group" [isViewOnly]="isViewOnly" *ngIf="group"></group-information-tab>
- </sdc-tab>
- <sdc-tab titleIcon="inputs-o">
- <group-members-tab [group]="group" [topologyTemplate]="topologyTemplate" [isViewOnly]="isViewOnly" (isLoading)="setIsLoading($event)" *ngIf="group"></group-members-tab>
- </sdc-tab>
- <sdc-tab titleIcon="settings-o">
- <group-properties-tab [group]="group" [topologyTemplate]="topologyTemplate" [isViewOnly]="isViewOnly" *ngIf="group"></group-properties-tab>
- </sdc-tab>
-</sdc-tabs>
+<div>
+ <connection-wizard-header currentStepIndex="2"></connection-wizard-header>
+ <properties-step></properties-step>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.less
new file mode 100644
index 0000000000..07f9aa2135
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.less
@@ -0,0 +1,4 @@
+connection-wizard-header {
+ display: block;
+ margin-bottom: 15px;
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.ts
new file mode 100644
index 0000000000..5abb879013
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.ts
@@ -0,0 +1,10 @@
+import {Component} from "@angular/core";
+
+
+@Component({
+ selector: 'connection-properties-view',
+ templateUrl: './connection-properties-view.component.html',
+ styleUrls:['./connection-properties-view.component.less']
+})
+export class ConnectionPropertiesViewComponent {
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.html
new file mode 100644
index 0000000000..7e7e82d85f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.html
@@ -0,0 +1,52 @@
+<!--
+ ~ 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="header-main-container">
+ <div class="inner-container">
+ <div class="node from-node" [ngClass]="{'selected':currentStepIndex == 0}">
+ <div class="text">
+ <div class="node-name">
+ {{connectWizardService.connectRelationModel.fromNode.componentInstance.name}}
+ </div>
+ <div class="selected-req-or-cap" [ngClass]="{'selected': currentStepIndex == 2 && !connectWizardService.selectedMatch.isFromTo}">
+ {{getSelectedReqOrCapName(true)}}
+ </div>
+ </div>
+ <div class="icon">
+ <div class="small medium {{connectWizardService.connectRelationModel.fromNode.componentInstance.iconSprite}} {{connectWizardService.connectRelationModel.fromNode.componentInstance.icon}}">
+ </div>
+ </div>
+ </div>
+ <div class="connection">
+
+ </div>
+ <div class="node to-node" [ngClass]="{'selected':currentStepIndex == 1}">
+ <div class="icon">
+ <div class="small medium {{connectWizardService.connectRelationModel.toNode.componentInstance.iconSprite}} {{connectWizardService.connectRelationModel.toNode.componentInstance.icon}}">
+ </div>
+ </div>
+
+ <div class="text">
+ <div class="node-name">
+ {{connectWizardService.connectRelationModel.toNode.componentInstance.name}}
+ </div>
+ <div class="selected-req-or-cap" [ngClass]="{'selected': currentStepIndex == 2 && connectWizardService.selectedMatch.isFromTo}">
+ {{getSelectedReqOrCapName(false)}}
+ </div>
+ </div>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.less
new file mode 100644
index 0000000000..d8bab288d3
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.less
@@ -0,0 +1,53 @@
+@import '../../../../../../../assets/styles/sprite-proxy-services-icons';
+@import '../../../../../../../assets/styles/variables';
+.header-main-container{
+ background-color: #f8f8f8;
+ width: 100%;
+ height: 100px;
+ display: flex;
+ .inner-container{
+ margin: 0 auto;
+ display: flex;
+ }
+}
+.selected {
+ color: @main_color_a;
+}
+.node{
+ display: flex;
+ &.from-node{
+ text-align: right;
+ }
+ &.to-node{
+ text-align: left;
+ }
+ &.selected{
+ .icon{
+ border: solid 3px @main_color_a;
+ padding: 4px;
+ border-radius: 50%;
+ background-color: @main_color_p;
+ }
+ }
+ .icon{
+ margin: auto 0;
+ display: flex;
+ }
+ .text{
+ font-family: @font-opensans-medium;
+ margin: auto 10px;
+ min-width: 450px;
+ .node-name{
+ font-size: 11px;
+ }
+ .selected-req-or-cap{
+ font-size: 14px;
+ }
+ }
+}
+.connection{
+ width: 67px;
+ height: 0px;
+ border-bottom: dashed 2px #979797;
+ margin: auto 0;
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.ts
new file mode 100644
index 0000000000..f5bc3b7ca4
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.ts
@@ -0,0 +1,37 @@
+/**
+ * Created by rc2122 on 9/27/2017.
+ */
+import {Component, Inject, forwardRef} from "@angular/core";
+import {ConnectionWizardService} from "../connection-wizard.service";
+import {WizardHeaderBaseComponent} from "app/ng2/components/ui/multi-steps-wizard/multi-steps-wizard-header-base.component";
+
+@Component({
+ selector: 'connection-wizard-header',
+ templateUrl: './connection-wizard-header.component.html',
+ styleUrls:['./connection-wizard-header.component.less']
+})
+
+export class ConnectionWizardHeaderComponent extends WizardHeaderBaseComponent{
+
+ constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService) {
+ super();
+ }
+
+ private _getReqOrCapName(isFromNode:boolean) {
+ const attributeReqOrCap:string = isFromNode ? 'requirement' : 'capability';
+ if (this.connectWizardService.selectedMatch[attributeReqOrCap]) {
+ return this.connectWizardService.selectedMatch[attributeReqOrCap].getTitle();
+ } else if (this.connectWizardService.selectedMatch.relationship) {
+ return this.connectWizardService.selectedMatch.relationship.relation[attributeReqOrCap];
+ }
+ return '';
+ }
+
+ private getSelectedReqOrCapName = (isFromNode:boolean):string => {
+ if(!this.connectWizardService.selectedMatch){
+ return '';
+ }
+ return this._getReqOrCapName(this.connectWizardService.selectedMatch.isFromTo ? isFromNode : !isFromNode);
+ }
+}
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.module.ts
new file mode 100644
index 0000000000..80464dc970
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.module.ts
@@ -0,0 +1,43 @@
+import {ToNodeStepComponent} from "./to-node-step/to-node-step.component";
+import {NgModule} from "@angular/core";
+import {FromNodeStepComponent} from "./from-node-step/from-node-step.component";
+import {PropertiesStepComponent} from "./properties-step/properties-step.component";
+import {ConnectionWizardService} from "./connection-wizard.service";
+import {SelectRequirementOrCapabilityModule} from "../../../../components/logic/select-requirement-or-capability/select-requirement-or-capability.module";
+import {PropertyTableModule} from "../../../../components/logic/properties-table/property-table.module";
+import {FormElementsModule} from "../../../../components/ui/form-components/form-elements.module";
+import {ConnectionWizardHeaderComponent} from "./connection-wizard-header/connection-wizard-header.component";
+import {ConnectionPropertiesViewComponent} from "./connection-properties-view/connection-properties-view.component";
+import {BrowserModule} from "@angular/platform-browser";
+
+@NgModule({
+ declarations: [
+ FromNodeStepComponent,
+ ToNodeStepComponent,
+ PropertiesStepComponent,
+ ConnectionWizardHeaderComponent,
+ ConnectionPropertiesViewComponent
+ ],
+ imports: [
+ FormElementsModule,
+ PropertyTableModule,
+ SelectRequirementOrCapabilityModule,
+ BrowserModule
+ ],
+ exports: [
+ FromNodeStepComponent,
+ ToNodeStepComponent,
+ PropertiesStepComponent,
+ ConnectionWizardHeaderComponent,
+ ConnectionPropertiesViewComponent
+ ],
+ entryComponents: [FromNodeStepComponent,
+ ToNodeStepComponent,
+ PropertiesStepComponent,
+ ConnectionWizardHeaderComponent,
+ ConnectionPropertiesViewComponent
+ ],
+ providers: [ConnectionWizardService]
+})
+export class ConnectionWizardModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.spec.ts
new file mode 100644
index 0000000000..8a5c5fcefb
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.spec.ts
@@ -0,0 +1,85 @@
+import {TestBed} from "@angular/core/testing";
+import {WorkspaceService} from "../../../../pages/workspace/workspace.service";
+import { ConnectionWizardService } from "app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service";
+import { ConnectRelationModel, Match, Requirement, Capability } from "app/models";
+import { Mock } from "ts-mockery/dist";
+
+describe('Connection Wizard Service', () => {
+
+ let service: ConnectionWizardService;
+
+ const connectRelationModelMock = Mock.of<ConnectRelationModel>({
+ possibleRelations: [
+ Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement1', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability1', type: 'othertype'})}),
+ Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement2', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability2', type: 'tosca'})}),
+ Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement3', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability3', type: 'tosca'})}),
+ Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement4', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability2', type: 'tosca'})}),
+ Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement5', capability: "cap2"}), capability: Mock.of<Capability>({uniqueId: 'capability1', type: 'tosca'})}),
+ Mock.of<Match>({isFromTo: false, requirement: Mock.of<Requirement>({uniqueId: 'requirement6', capability: "cap2"}), capability: Mock.of<Capability>({uniqueId: 'capability2', type: 'tosca'})}),
+ Mock.of<Match>({isFromTo: false, requirement: Mock.of<Requirement>({uniqueId: 'requirement7', capability: "cap2"}), capability: Mock.of<Capability>({uniqueId: 'capability1', type: 'othertype'})})
+ ]
+ });
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [],
+ providers: [ConnectionWizardService,
+ {provide: WorkspaceService, useValue: {}}
+ ]
+ });
+
+ service = TestBed.get(ConnectionWizardService);
+ service.connectRelationModel = connectRelationModelMock;
+ });
+
+ describe('getOptionalRequirementsByInstanceUniqueId', () => {
+ it('if no capability to match is sent in and isFromTo is true, ALL isFromTo==true requirements are returned', () => {
+ const requirements = service.getOptionalRequirementsByInstanceUniqueId(true);
+ expect(requirements['cap1'].length).toBe(4);
+ expect(requirements['cap2'].length).toBe(1);
+ });
+
+ it('if no capability to match is sent in and isFromTo is false, ALL isFromTo==false requirements are returned', () => {
+ const requirements = service.getOptionalRequirementsByInstanceUniqueId(false);
+ expect(requirements['cap1']).toBeUndefined();
+ expect(requirements['cap2'].length).toBe(2);
+ });
+
+ it('if capability to match IS sent in and isFromTo is true, matches with the same uniqueID and isFromTo==true are returned', () => {
+ const capability = Mock.of<Capability>({uniqueId: 'capability1'});
+ const requirements = service.getOptionalRequirementsByInstanceUniqueId(true, capability);
+ expect(requirements['cap1'].length).toBe(1);
+ expect(requirements['cap2'].length).toBe(1);
+ });
+
+ it('if capability to match IS sent in and isFromTo is false, requirements with the same uniqueID and isFromTo==false are returned', () => {
+ const capability = Mock.of<Capability>({uniqueId: 'capability1'});
+ const requirements = service.getOptionalRequirementsByInstanceUniqueId(false, capability);
+ expect(requirements['cap1']).toBeUndefined();
+ expect(requirements['cap2'].length).toBe(1);
+ });
+ })
+
+ describe('getOptionalCapabilitiesByInstanceUniqueId', () => {
+ it('if requirement to match IS sent in and isFromTo is true, matches with the same uniqueID and isFromTo==true are returned', () => {
+ const requirement = Mock.of<Requirement>({uniqueId: 'requirement1'});
+ const capabilities = service.getOptionalCapabilitiesByInstanceUniqueId(true, requirement);
+ expect(capabilities['othertype'].length).toBe(1);
+ expect(capabilities['tosca']).toBeUndefined();
+ });
+
+ it('if no requirement to match is sent in and isFromTo is true, a UNIQUE list of all capabilities with isFromTo==true are returned', () => {
+ const capabilities = service.getOptionalCapabilitiesByInstanceUniqueId(true);
+ expect(capabilities['othertype'].length).toBe(1);
+ expect(capabilities['tosca'].length).toBe(2);
+ });
+
+ it('if no requirement to match is sent in and isFromTo is false, all capabilities with isFromTo==false are returned', () => {
+ const capabilities = service.getOptionalCapabilitiesByInstanceUniqueId(false);
+ expect(capabilities['othertype'].length).toBe(1);
+ expect(capabilities['tosca'].length).toBe(1);
+ });
+ });
+
+});
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.ts
new file mode 100644
index 0000000000..2eb5428f61
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.ts
@@ -0,0 +1,58 @@
+import * as _ from "lodash";
+import {ConnectRelationModel} from "app/models/graph/connectRelationModel";
+import {Injectable} from "@angular/core";
+import { Requirement, Capability} from "app/models";
+import {Dictionary} from "lodash";
+import {Match, Component, PropertyFEModel} from "app/models";
+import {Store} from "@ngxs/store";
+import {WorkspaceService} from "../../../workspace/workspace.service";
+
+@Injectable()
+export class ConnectionWizardService {
+
+ connectRelationModel:ConnectRelationModel;
+ selectedMatch:Match;
+ changedCapabilityProperties:PropertyFEModel[];
+
+
+ constructor(private workspaceService: WorkspaceService) {
+ this.changedCapabilityProperties = [];
+
+ }
+
+ public setRelationMenuDirectiveObj = (connectRelationModel:ConnectRelationModel) => {
+ this.connectRelationModel = connectRelationModel;
+ // this.selectedCapability = rel
+ }
+
+ getOptionalRequirementsByInstanceUniqueId = (isFromTo: boolean, matchWith?:Capability): Dictionary<Requirement[]> => {
+ let requirements: Array<Requirement> = [];
+ _.forEach(this.connectRelationModel.possibleRelations, (match: Match) => {
+ if(!matchWith || match.capability.uniqueId == matchWith.uniqueId){
+ if(match.isFromTo == isFromTo){
+ requirements.push(match.requirement);
+ }
+ }
+ });
+ requirements = _.uniqBy(requirements, (req:Requirement)=>{
+ return req.ownerId + req.uniqueId + req.name;
+ });
+ return _.groupBy(requirements, 'capability');
+ }
+
+ getOptionalCapabilitiesByInstanceUniqueId = (isFromTo: boolean, matchWith?:Requirement): Dictionary<Capability[]> => {
+ let capabilities: Array<Capability> = [];
+ _.forEach(this.connectRelationModel.possibleRelations, (match: Match) => {
+ if(!matchWith || match.requirement.uniqueId == matchWith.uniqueId){
+ if(match.isFromTo == isFromTo){
+ capabilities.push(match.capability);
+ }
+ }
+ });
+ capabilities = _.uniqBy(capabilities, (cap:Capability)=>{
+ return cap.ownerId + cap.uniqueId + cap.name;
+ });
+ return _.groupBy(capabilities, 'type');
+ }
+}
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/__snapshots__/from-node-step.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/__snapshots__/from-node-step.component.spec.ts.snap
new file mode 100644
index 0000000000..739ce3d8fe
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/__snapshots__/from-node-step.component.spec.ts.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`from-node-step component should match current snapshot 1`] = `
+<from-node-step
+ connectWizardService={[Function Object]}
+ preventBack={[Function Function]}
+ preventNext={[Function Function]}
+ updateSelectedReqOrCap={[Function Function]}
+>
+ <select-requirement-or-capability />
+</from-node-step>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.html
new file mode 100644
index 0000000000..0a70069748
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.html
@@ -0,0 +1,22 @@
+<!--
+ ~ 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.
+ -->
+
+<select-requirement-or-capability [optionalRequirementsMap]="optionalRequirementsMap"
+ [optionalCapabilitiesMap]="optionalCapabilitiesMap"
+ [selectedReqOrCapModel]="connectWizardService.selectedMatch && (connectWizardService.selectedMatch.isFromTo ? connectWizardService.selectedMatch.requirement : connectWizardService.selectedMatch.capability)"
+ [componentInstanceId]="connectWizardService.connectRelationModel.fromNode.componentInstance.uniqueId"
+ (updateSelectedReqOrCap)="updateSelectedReqOrCap($event)">
+</select-requirement-or-capability> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.spec.ts
new file mode 100644
index 0000000000..59ff72adda
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.spec.ts
@@ -0,0 +1,114 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { Capability, Match } from 'app/models';
+import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper';
+import { Requirement } from '../../../../../../models/requirement';
+import { ConnectionWizardService } from '../connection-wizard.service';
+import { FromNodeStepComponent } from './from-node-step.component';
+
+describe('from-node-step component', () => {
+
+ let fixture: ComponentFixture<FromNodeStepComponent>;
+ let connectionWizardServiceMockWithoutSelectedMatch: Partial<ConnectionWizardService>;
+ let connectionWizardServiceMockWithSelectedMatch: Partial<ConnectionWizardService>;
+
+ const connectionWizardServiceMockSelectedMatchWithRequirements = {requirement: 'val'};
+
+ connectionWizardServiceMockWithoutSelectedMatch = {
+ getOptionalRequirementsByInstanceUniqueId: jest.fn().mockReturnValue(5),
+ getOptionalCapabilitiesByInstanceUniqueId: jest.fn().mockReturnValue(10),
+
+ connectRelationModel: {
+ fromNode: {
+ componentInstance: {
+ uniqueId : 'testUniqueID'
+ }
+ }
+ }
+ };
+
+ connectionWizardServiceMockWithSelectedMatch = {
+ selectedMatch: connectionWizardServiceMockSelectedMatchWithRequirements,
+ getOptionalRequirementsByInstanceUniqueId: jest.fn().mockReturnValue(5),
+ getOptionalCapabilitiesByInstanceUniqueId: jest.fn().mockReturnValue(10)
+ };
+
+ let expectedConnectionWizardServiceMock = connectionWizardServiceMockWithoutSelectedMatch;
+
+ beforeEach(
+ async(() => {
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [FromNodeStepComponent],
+ imports: [],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: ConnectionWizardService, useValue: expectedConnectionWizardServiceMock}
+ ],
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(FromNodeStepComponent);
+ });
+ })
+ );
+
+
+ it('should match current snapshot', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+ it('preventBack return true - always', () => {
+ fixture.componentInstance.ngOnInit();
+ const result = fixture.componentInstance.preventBack();
+ expect(result).toEqual(true);
+ });
+
+ it('preventNext return true since selectedMatch does not exist in connectionWizardServiceMock', () => {
+ fixture.componentInstance.ngOnInit();
+ const result = fixture.componentInstance.preventNext();
+ expect(result).toEqual(true);
+ });
+
+ it('preventNext return false since to selectedMatch or selectedMatch.capability & selectedMatch.requirement does exist in connectionWizardServiceMock', () => {
+ fixture.componentInstance.connectWizardService = connectionWizardServiceMockWithSelectedMatch;
+ fixture.componentInstance.ngOnInit();
+ const result = fixture.componentInstance.preventNext();
+ expect(result).toEqual(false);
+ });
+
+ it('updateSelectedReqOrCap is called with instance of requirement, the selectMatch will be set to an Instance of Match of type Requirement', () => {
+ const requirement = new Requirement();
+ fixture.componentInstance.updateSelectedReqOrCap(requirement);
+ const expectedSelectedMatch = fixture.componentInstance.connectWizardService.selectedMatch;
+
+ expect(expectedSelectedMatch).toBeInstanceOf(Match);
+ expect(expectedSelectedMatch.capability).toBe(null);
+ expect(expectedSelectedMatch.fromNode).toBe('testUniqueID');
+ expect(expectedSelectedMatch.isFromTo).toBe(true);
+ expect(expectedSelectedMatch.toNode).toBe(null);
+ expect(expectedSelectedMatch.requirement).toBeInstanceOf(Requirement);
+ });
+
+ it('updateSelectedReqOrCap is called with instance of capability, the selectMatch will be set to an Instance of Match of type Capability', () => {
+ const capability = new Capability();
+ fixture.componentInstance.updateSelectedReqOrCap(capability);
+ const expectedSelectedMatch = fixture.componentInstance.connectWizardService.selectedMatch;
+
+ expect(expectedSelectedMatch).toBeInstanceOf(Match);
+ expect(expectedSelectedMatch.requirement).toBe(null);
+ expect(expectedSelectedMatch.fromNode).toBe(null);
+ expect(expectedSelectedMatch.isFromTo).toBe(false);
+ expect(expectedSelectedMatch.toNode).toBe('testUniqueID');
+ expect(expectedSelectedMatch.capability).toBeInstanceOf(Capability);
+ });
+
+ it('updateSelectedReqOrCap is called with null, the selectMatch will be set to null', () => {
+ fixture.componentInstance.updateSelectedReqOrCap(null);
+ const expectedSelectedMatch = fixture.componentInstance.connectWizardService.selectedMatch;
+
+ expect(expectedSelectedMatch).toBe(null);
+ });
+
+}); \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.ts
new file mode 100644
index 0000000000..cffd58c9ea
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.ts
@@ -0,0 +1,44 @@
+import { Component, forwardRef, Inject, OnInit } from '@angular/core';
+import { Match } from 'app/models';
+import { Capability } from 'app/models/capability';
+import { Requirement } from 'app/models/requirement';
+import { IStepComponent } from 'app/models/wizard-step';
+import { Dictionary } from 'lodash';
+import { ConnectionWizardService } from '../connection-wizard.service';
+
+@Component({
+ selector: 'from-node-step',
+ templateUrl: './from-node-step.component.html'
+})
+
+export class FromNodeStepComponent implements IStepComponent, OnInit{
+
+ optionalRequirementsMap: Dictionary<Requirement[]>;
+ optionalCapabilitiesMap: Dictionary<Capability[]>;
+
+ constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService) {}
+
+ ngOnInit() {
+ this.optionalRequirementsMap = this.connectWizardService.getOptionalRequirementsByInstanceUniqueId(true);
+ this.optionalCapabilitiesMap = this.connectWizardService.getOptionalCapabilitiesByInstanceUniqueId(false);
+ }
+
+ preventNext = (): boolean => {
+ return !this.connectWizardService.selectedMatch || (!this.connectWizardService.selectedMatch.capability && !this.connectWizardService.selectedMatch.requirement);
+ }
+
+ preventBack = (): boolean => {
+ return true;
+ }
+
+ private updateSelectedReqOrCap = (selected: Requirement|Capability): void => {
+ if (!selected) {
+ this.connectWizardService.selectedMatch = null;
+ } else if (selected instanceof Requirement) {
+ this.connectWizardService.selectedMatch = new Match(<Requirement>selected, null, true, this.connectWizardService.connectRelationModel.fromNode.componentInstance.uniqueId, null);
+ } else {
+ this.connectWizardService.selectedMatch = new Match(null, <Capability>selected , false, null, this.connectWizardService.connectRelationModel.fromNode.componentInstance.uniqueId);
+ }
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.html
new file mode 100644
index 0000000000..a8177595a5
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.html
@@ -0,0 +1,28 @@
+<!--
+ ~ 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="title">
+ <span class="capability-name">
+ {{(connectWizardService.selectedMatch.capability && connectWizardService.selectedMatch.capability.getTitle()) || connectWizardService.selectedMatch.relationship.relation.capability}}
+ </span>
+</div>
+<div class="properties-table-container">
+ <properties-table class="properties-table"
+ (propertyChanged)="propertyValueChanged($event)"
+ [fePropertiesMap]="capabilityPropertiesMap"
+ [selectedPropertyId]="''"
+ [hidePropertyType]="true">
+ </properties-table>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.less
new file mode 100644
index 0000000000..c8ad4d38d2
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.less
@@ -0,0 +1,15 @@
+@import '../../../../../../../assets/styles/variables';
+.title{
+ margin-bottom: 20px;
+ .capability-name-label{
+ font-size: 13px;
+ }
+ .capability-name{
+ font-family: @font-opensans-medium;
+ color: @main_color_a;
+ }
+}
+.properties-table-container{
+ height: 362px;
+ overflow-y: auto;
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.ts
new file mode 100644
index 0000000000..2c12e0daed
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.ts
@@ -0,0 +1,68 @@
+/**
+ * Created by ob0695 on 9/4/2017.
+ */
+/**
+ * Created by rc2122 on 9/4/2017.
+ */
+import {Component, Inject, forwardRef} from '@angular/core';
+import {IStepComponent} from "app/models"
+import {ConnectionWizardService} from "../connection-wizard.service";
+import {PropertyFEModel} from "app/models/properties-inputs/property-fe-model";
+import {InstanceFePropertiesMap} from "app/models/properties-inputs/property-fe-map";
+import {PropertiesUtils} from "app/ng2/pages/properties-assignment/services/properties.utils";
+import { ComponentInstanceServiceNg2 } from "app/ng2/services/component-instance-services/component-instance.service";
+
+@Component({
+ selector: 'properties-step',
+ templateUrl: './properties-step.component.html',
+ styleUrls: ['./properties-step.component.less']
+})
+
+export class PropertiesStepComponent implements IStepComponent{
+
+ capabilityPropertiesMap: InstanceFePropertiesMap;
+ savingProperty:boolean = false;
+
+ constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService, private componentInstanceServiceNg2:ComponentInstanceServiceNg2, private propertiesUtils:PropertiesUtils) {
+
+ this.capabilityPropertiesMap = this.propertiesUtils.convertPropertiesMapToFEAndCreateChildren({'capability' : connectWizardService.selectedMatch.capability.properties}, false);
+ }
+
+ ngOnInit() {
+ this.connectWizardService.changedCapabilityProperties = [];
+ }
+
+ onPropertySelectedUpdate = ($event) => {
+ console.log("==>" + 'PROPERTY VALUE SELECTED');
+ // this.selectedFlatProperty = $event;
+ // let parentProperty:PropertyFEModel = this.propertiesService.getParentPropertyFEModelFromPath(this.instanceFePropertiesMap[this.selectedFlatProperty.instanceName], this.selectedFlatProperty.path);
+ // parentProperty.expandedChildPropertyId = this.selectedFlatProperty.path;
+ };
+
+ propertyValueChanged = (property: PropertyFEModel) => {
+ if (!property.isDeclared) {
+ const propChangedIdx = this.connectWizardService.changedCapabilityProperties.indexOf(property);
+ if (property.hasValueObjChanged()) {
+ // if (this.componentInstanceServiceNg2.hasPropertyChanged(property)) {
+ console.log("==>" + this.constructor.name + ": propertyValueChanged " + property);
+ if (propChangedIdx === -1) {
+ this.connectWizardService.changedCapabilityProperties.push(property);
+ }
+ }
+ else {
+ if (propChangedIdx !== -1) {
+ console.log("==>" + this.constructor.name + ": propertyValueChanged (reset to original) " + property);
+ this.connectWizardService.changedCapabilityProperties.splice(propChangedIdx, 1);
+ }
+ }
+ }
+ };
+
+ preventNext = ():boolean => {
+ return false;
+ }
+
+ preventBack = ():boolean => {
+ return this.savingProperty;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/__snapshots__/to-node-step.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/__snapshots__/to-node-step.component.spec.ts.snap
new file mode 100644
index 0000000000..ea587bce71
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/__snapshots__/to-node-step.component.spec.ts.snap
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`to-node-step component should match current snapshot 1`] = `
+<to-node-step
+ connectWizardService={[Function Object]}
+ optionalCapabilitiesMap={[Function Object]}
+ optionalRequirementsMap={[Function Object]}
+ preventBack={[Function Function]}
+ preventNext={[Function Function]}
+ updateSelectedReqOrCap={[Function Function]}
+>
+ <select-requirement-or-capability />
+</to-node-step>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.html
new file mode 100644
index 0000000000..4892b7fadc
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.html
@@ -0,0 +1,22 @@
+<!--
+ ~ 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.
+ -->
+<select-requirement-or-capability [optionalRequirementsMap]="optionalRequirementsMap"
+ [optionalCapabilitiesMap]="optionalCapabilitiesMap"
+ [selectedReqOrCapModel]="connectWizardService.selectedMatch.isFromTo ? connectWizardService.selectedMatch.capability : connectWizardService.selectedMatch.requirement"
+ [selectedReqOrCapOption]="displayRequirementsOrCapabilities"
+ [componentInstanceId]="connectWizardService.connectRelationModel.toNode.componentInstance.uniqueId"
+ (updateSelectedReqOrCap)="updateSelectedReqOrCap($event)">
+</select-requirement-or-capability> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.spec.ts
new file mode 100644
index 0000000000..9d453f21dd
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.spec.ts
@@ -0,0 +1,71 @@
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {NO_ERRORS_SCHEMA} from "@angular/core";
+import {ToNodeStepComponent} from "./to-node-step.component";
+import {ConnectionWizardService} from "../connection-wizard.service";
+import {ConfigureFn, configureTests} from "../../../../../../../jest/test-config.helper";
+import {Match} from "../../../../../../models/graph/match-relation";
+
+
+describe('to-node-step component', () => {
+
+ let fixture: ComponentFixture<ToNodeStepComponent>;
+ let connectionWizardServiceMock: Partial<ConnectionWizardService>;
+
+ beforeEach(
+ async(() => {
+
+ connectionWizardServiceMock = {
+ // selectedMatch: new Match(null, null, true, '',''),
+ selectedMatch: {
+ isFromTo: false
+ },
+ getOptionalRequirementsByInstanceUniqueId: jest.fn().mockReturnValue(5),
+ getOptionalCapabilitiesByInstanceUniqueId: jest.fn().mockReturnValue(10)
+ }
+
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [ToNodeStepComponent],
+ imports: [],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: ConnectionWizardService, useValue: connectionWizardServiceMock}
+ ],
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(ToNodeStepComponent);
+ });
+ })
+ );
+
+
+ it('should match current snapshot', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+ it('should test the ngOnInit with isFromTo = false', () => {
+ const component = TestBed.createComponent(ToNodeStepComponent);
+ let service = TestBed.get(ConnectionWizardService);
+ service.selectedMatch.isFromTo = false;
+ component.componentInstance.ngOnInit();
+ expect(component.componentInstance.displayRequirementsOrCapabilities).toEqual("Requirement");
+ expect(connectionWizardServiceMock.getOptionalRequirementsByInstanceUniqueId).toHaveBeenCalledWith(false, connectionWizardServiceMock.selectedMatch.capability);
+ expect(component.componentInstance.optionalRequirementsMap).toEqual(5);
+ expect(component.componentInstance.optionalCapabilitiesMap).toEqual({});
+ });
+
+
+ it('should test the ngOnInit with isFromTo = true', () => {
+ const component = TestBed.createComponent(ToNodeStepComponent);
+ let service = TestBed.get(ConnectionWizardService);
+ service.selectedMatch.isFromTo = true;
+ component.componentInstance.ngOnInit();
+ expect(component.componentInstance.displayRequirementsOrCapabilities).toEqual("Capability");
+ expect(connectionWizardServiceMock.getOptionalCapabilitiesByInstanceUniqueId).toHaveBeenCalledWith(true, connectionWizardServiceMock.selectedMatch.requirement);
+ expect(component.componentInstance.optionalCapabilitiesMap).toEqual(10);
+ expect(component.componentInstance.optionalRequirementsMap).toEqual({});
+ });
+
+}); \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.ts
new file mode 100644
index 0000000000..67dc381284
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.ts
@@ -0,0 +1,65 @@
+import {Component, forwardRef, Inject} from '@angular/core';
+import {IStepComponent} from "app/models"
+import {Dictionary} from "lodash";
+import {ConnectionWizardService} from "../connection-wizard.service";
+import {Match} from "app/models/graph/match-relation";
+import {Requirement} from "app/models/requirement";
+import {Capability} from "app/models/capability";
+import {PropertyModel} from "app/models/properties";
+
+@Component({
+ selector: 'to-node-step',
+ templateUrl: './to-node-step.component.html'
+})
+
+export class ToNodeStepComponent implements IStepComponent{
+
+ displayRequirementsOrCapabilities:string; //get 'Requirement' or 'Capability'
+ optionalRequirementsMap: Dictionary<Requirement[]> = {};
+ optionalCapabilitiesMap: Dictionary<Capability[]> ={};
+
+ constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService) {
+ }
+
+ ngOnInit(){
+ if(this.connectWizardService.selectedMatch.isFromTo){
+ this.displayRequirementsOrCapabilities = 'Capability';
+ this.optionalRequirementsMap = {};
+ this.optionalCapabilitiesMap = this.connectWizardService.getOptionalCapabilitiesByInstanceUniqueId(true, this.connectWizardService.selectedMatch.requirement);
+ }else{
+ this.displayRequirementsOrCapabilities = 'Requirement';
+ this.optionalRequirementsMap = this.connectWizardService.getOptionalRequirementsByInstanceUniqueId(false, this.connectWizardService.selectedMatch.capability);
+ this.optionalCapabilitiesMap = {}
+ }
+
+
+ }
+
+ preventNext = ():boolean => {
+ return !this.connectWizardService.selectedMatch.capability || !this.connectWizardService.selectedMatch.requirement;
+ }
+
+ preventBack = ():boolean => {
+ return false;
+ }
+
+ private updateSelectedReqOrCap = (selected:Requirement|Capability):void => {
+ if (!selected) {
+ if (this.connectWizardService.selectedMatch.isFromTo) {
+ this.connectWizardService.selectedMatch.capability = undefined;
+ this.connectWizardService.selectedMatch.toNode = undefined;
+ } else {
+ this.connectWizardService.selectedMatch.requirement = undefined;
+ this.connectWizardService.selectedMatch.fromNode = undefined;
+ }
+ } else if (selected instanceof Requirement) {
+ this.connectWizardService.selectedMatch.requirement = <Requirement>selected;
+ this.connectWizardService.selectedMatch.fromNode = this.connectWizardService.connectRelationModel.toNode.componentInstance.uniqueId;
+ } else {
+ this.connectWizardService.selectedMatch.capability = <Capability>selected;
+ this.connectWizardService.selectedMatch.toNode = this.connectWizardService.connectRelationModel.toNode.componentInstance.uniqueId;
+ }
+ this.connectWizardService.selectedMatch.relationship = undefined;
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/__snapshots__/link-row.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/__snapshots__/link-row.component.spec.ts.snap
new file mode 100644
index 0000000000..094f41bd84
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/__snapshots__/link-row.component.spec.ts.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`artifact form component should match current snapshot of artifact form component 1`] = `
+<link-row
+ source={[Function Array]}
+ srcCP={[Function Array]}
+ target={[Function Array]}
+ targetCP={[Function Array]}
+>
+ <ui-element-dropdown
+ class="cell link-selector"
+ data-tests-id="linkSrc"
+ /><ui-element-dropdown
+ class="cell link-selector"
+ data-tests-id="linkSrcCP"
+ /><ui-element-dropdown
+ class="cell link-selector"
+ data-tests-id="linkTarget"
+ /><ui-element-dropdown
+ class="cell link-selector"
+ data-tests-id="linkTargetCP"
+ /><div
+ class="cell remove"
+ data-tests-id="removeLnk"
+ >
+
+ </div>
+</link-row>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.html
new file mode 100644
index 0000000000..0abdda1cc6
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.html
@@ -0,0 +1,61 @@
+<!--
+ ~ 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.
+ -->
+
+<ui-element-dropdown
+ data-tests-id="linkSrc"
+ [readonly]="!link.isFirst || (link.isFirst && !link.canEdit)"
+ class="cell link-selector"
+ [values]="source"
+ [(value)]="link.fromNode"
+ (valueChange)="onSourceSelected($event)">
+</ui-element-dropdown>
+
+<ui-element-dropdown
+ data-tests-id="linkSrcCP"
+ [readonly]="!link.isFirst || (link.isFirst && !link.canEdit)"
+ class="cell link-selector"
+ [values]="srcCP"
+ [(value)]="link.fromCP"
+ (valueChange)="onSrcCPSelected($event)">
+</ui-element-dropdown>
+
+<ui-element-dropdown
+ data-tests-id="linkTarget"
+ [readonly]="!link.canEdit"
+ class="cell link-selector"
+ [values]="target"
+ [(value)]="link.toNode"
+ (valueChange)="onTargetSelected($event)">
+</ui-element-dropdown>
+
+<ui-element-dropdown
+ data-tests-id="linkTargetCP"
+ [readonly]="!link.canEdit"
+ class="cell link-selector"
+ [values]="targetCP"
+ [(value)]="link.toCP"
+ (valueChange)="onTargetCPSelected($event)">
+</ui-element-dropdown>
+
+<div
+ class="cell remove"
+ data-tests-id="removeLnk">
+ <span
+ *ngIf="link.canRemove"
+ class="sprite-new delete-item-icon"
+ (click)="removeRow()">
+ </span>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.less
new file mode 100644
index 0000000000..2a1d0d98c8
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.less
@@ -0,0 +1,21 @@
+@import './../../../../../../../assets/styles/variables.less';
+.remove {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.cell {
+ padding: 0;
+}
+
+/deep/ .link-selector {
+ select {
+ height: 30px;
+ border: none;
+ stroke: none;
+ }
+
+}
+
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.spec.ts
new file mode 100644
index 0000000000..5cbad6ea5d
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.spec.ts
@@ -0,0 +1,478 @@
+import {async, ComponentFixture} from "@angular/core/testing";
+import {CacheService} from "../../../../../services/cache.service";
+import {ConfigureFn, configureTests} from "../../../../../../../jest/test-config.helper";
+import {NO_ERRORS_SCHEMA} from "@angular/core";
+import {LinkRowComponent} from "./link-row.component";
+import {DropdownValue} from "../../../../../components/ui/form-components/dropdown/ui-element-dropdown.component";
+import {MapItemData, ServicePathMapItem} from "../../../../../../models/graph/nodes-and-links-map";
+
+describe('artifact form component', () => {
+
+ let fixture: ComponentFixture<LinkRowComponent>;
+ let cacheServiceMock: Partial<CacheService>;
+
+ beforeEach(
+ async(() => {
+
+
+ cacheServiceMock = {
+ contains: jest.fn(),
+ remove: jest.fn(),
+ set: jest.fn(),
+ get: jest.fn()
+ }
+
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [LinkRowComponent],
+ imports: [],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: []
+ ,
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(LinkRowComponent);
+ });
+ })
+ );
+
+
+ it('should match current snapshot of artifact form component', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+
+ it('ngOnChanges() -> in case data exist -> call to parseInitialData()' ,() => {
+ // init values / mock functions
+ let data = 'something';
+ fixture.componentInstance.parseInitialData = jest.fn();
+ fixture.componentInstance.data = data;
+
+ // call to the tested function
+ fixture.componentInstance.ngOnChanges();
+
+ // expect that
+ expect(fixture.componentInstance.parseInitialData).toHaveBeenCalledWith(data);
+ });
+
+ it('onSourceSelected() -> in case id -> srcCP, link.fromCP, link.toNode, link.toCP, target, targetCP should be updated accordingly' ,() => {
+ // init values / mock functions
+ let id = 'id';
+ let data = 'data';
+ let link = {
+ fromCP:'testVal',
+ toNode:'testVal',
+ toCP:'testVal'
+ }
+ let target = ['val1', 'val2'];
+ let targetCP = ['val1', 'val2'];
+
+ fixture.componentInstance.findOptions = jest.fn();
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal');
+ fixture.componentInstance.data = data;
+ fixture.componentInstance.link = link;
+ fixture.componentInstance.target = target;
+ fixture.componentInstance.targetCP = targetCP;
+
+ // call to the tested function
+ fixture.componentInstance.onSourceSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.findOptions).toHaveBeenCalledWith(data, id);
+ expect(fixture.componentInstance.srcCP).toBe('dummyConvertedVal');
+ expect(fixture.componentInstance.link.fromCP).toBe('');
+ expect(fixture.componentInstance.link.toNode).toBe('');
+ expect(fixture.componentInstance.link.toCP).toBe('');
+ expect(fixture.componentInstance.target.length).toBe(0);
+ expect(fixture.componentInstance.targetCP.length).toBe(0);
+ });
+
+ it('onSourceSelected() -> in case id undefined -> No Change to srcCP, link.fromCP, link.toNode, link.toCP, target, targetCP' ,() => {
+ // init values / mock functions
+ let id;
+ let data = 'data';
+ let link = {
+ fromCP:'testVal',
+ toNode:'testVal',
+ toCP:'testVal'
+ }
+ let target = ['val1', 'val2'];
+ let targetCP = ['val1', 'val2'];
+
+ fixture.componentInstance.findOptions = jest.fn();
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal');
+ fixture.componentInstance.data = data;
+ fixture.componentInstance.link = link;
+ fixture.componentInstance.target = target;
+ fixture.componentInstance.targetCP = targetCP;
+
+ // call to the tested function
+ fixture.componentInstance.onSourceSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.link.fromCP).toBe(link.fromCP);
+ expect(fixture.componentInstance.link.toNode).toBe(link.toNode);
+ expect(fixture.componentInstance.link.toCP).toBe(link.toCP);
+ expect(fixture.componentInstance.target.length).toBe(2);
+ expect(fixture.componentInstance.target[0]).toBe('val1')
+ expect(fixture.componentInstance.targetCP.length).toBe(2);
+ expect(fixture.componentInstance.targetCP[1]).toBe('val2');
+ });
+
+ it('onSrcCPSelected() -> in case id -> Verify target, link.fromCPOriginId, link.toNode, link.toCP, targetCP.length' ,() => {
+ // init values / mock functions
+ let id = 'id';
+ let link = {
+ fromNode:'testVal',
+ toCPOriginId: 'initValue_ShouldBeChanged'
+ };
+ let option1 = {
+ id: 'something'
+ };
+ let option2 = {
+ id: 'id',
+ data: {"ownerId":1}
+ };
+
+ fixture.componentInstance.link = link;
+ fixture.componentInstance.findOptions = jest.fn(() => [option1, option2]);
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal');
+
+ // call to the tested function
+ fixture.componentInstance.onSrcCPSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.target).toBe('dummyConvertedVal');
+ expect(fixture.componentInstance.link.fromCPOriginId).toBe(option2.data.ownerId);
+ expect(fixture.componentInstance.link.toNode).toBe('');
+ expect(fixture.componentInstance.link.toCP).toBe('');
+ expect(fixture.componentInstance.targetCP.length).toBe(0);
+
+ });
+
+ it('onSrcCPSelected() -> in case id undefined -> Verify target, link.fromCPOriginId, link.toNode, link.toCP, targetCP.length' ,() => {
+ // init values / mock functions
+ let id;
+
+ let targetInput:Array<DropdownValue> = [{value:'Value', label:'Label', hidden:true, selected:true}];
+
+ let linkInput = {
+ fromCPOriginId:'expectedLinkFromCPOriginId',
+ toNode:'expectedLinkToNode',
+ toCP:'expectedLinkToCP',
+ // Link Object
+ canEdit:true,
+ canRemove:true,
+ isFirst:true,
+ // ForwardingPathLink Object
+ ownerId:'',
+ fromNode:'',
+ fromCP:'',
+ toCPOriginId:''
+ }
+
+ fixture.componentInstance.target = targetInput;
+ fixture.componentInstance.link = linkInput;
+ fixture.componentInstance.targetCP = targetInput;
+
+
+ // call to the tested function
+ fixture.componentInstance.onSrcCPSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.target).toBe(targetInput);
+ expect(fixture.componentInstance.link.fromCPOriginId).toBe('expectedLinkFromCPOriginId');
+ expect(fixture.componentInstance.link.toNode).toBe('expectedLinkToNode');
+ expect(fixture.componentInstance.link.toCP).toBe('expectedLinkToCP');
+ expect(fixture.componentInstance.targetCP.length).toBe(1);
+ });
+
+ it('onTargetSelected() -> in case id -> Verify targetCP & link.toCP' ,() => {
+ // init values / mock functions
+ let id = 'id';
+ let link = {
+ toCP:'testVal'
+ }
+ let targetCP = ['val1', 'val2'];
+
+ fixture.componentInstance.findOptions = jest.fn();
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal');
+ fixture.componentInstance.link = link;
+ fixture.componentInstance.targetCP = targetCP;
+
+ // call to the tested function
+ fixture.componentInstance.onTargetSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.targetCP).toBe('dummyConvertedVal');
+ expect(fixture.componentInstance.link.toCP).toBe('');
+
+ });
+
+ it('onTargetSelected() -> in case id undefined -> Verify targetCP & link.toCP' ,() => {
+ // init values / mock functions
+ let id;
+ let link = {
+ toCP:'toCP_testVal'
+ }
+ let targetCP = ['val1', 'val2'];
+
+ fixture.componentInstance.findOptions = jest.fn();
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal');
+ fixture.componentInstance.link = link;
+ fixture.componentInstance.targetCP = targetCP;
+
+ // call to the tested function
+ fixture.componentInstance.onTargetSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.targetCP.length).toBe(2);
+ expect(fixture.componentInstance.targetCP).toEqual(['val1', 'val2']);
+ expect(fixture.componentInstance.link.toCP).toBe('toCP_testVal');
+ });
+
+ it('onTargetCPSelected() -> in case id -> Validate toCPOriginId' ,() => {
+ // init values / mock functions
+ let id = 'id';
+ let link = {
+ toNode:'testVal',
+ toCPOriginId: 'initValue_ShouldBeChanged'
+ };
+ let option1 = {
+ id: 'something'
+ };
+ let option2 = {
+ id: 'id',
+ data: {"ownerId":1}
+ };
+ fixture.componentInstance.link = link;
+ fixture.componentInstance.findOptions = jest.fn(() => [option1, option2]);
+
+ // call to the tested function
+ fixture.componentInstance.onTargetCPSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.link.toCPOriginId).toBe(option2.data.ownerId);
+ });
+
+ it('onTargetCPSelected() -> in case id undefined -> Validate toCPOriginId' ,() => {
+ // init values / mock functions
+ let id;
+ let link = {
+ toNode:'testVal',
+ toCPOriginId: 'initValue_ShouldRemain'
+ };
+ let option1 = {
+ id: 'something'
+ };
+ let option2 = {
+ id: 'id',
+ data: {"ownerId":1}
+ };
+ fixture.componentInstance.link = link;
+ fixture.componentInstance.findOptions = jest.fn(() => [option1, option2]);
+
+ // call to the tested function
+ fixture.componentInstance.onTargetCPSelected(id);
+
+ // expect that
+ expect(fixture.componentInstance.link.toCPOriginId).toBe('initValue_ShouldRemain');
+ });
+
+
+ it('findOptions() -> in case item.data.options -> Validate return item.data.options' ,() => {
+ // init values / mock functions
+ const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []};
+ const innerServicePathItem: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 };
+ const mapItemData1: MapItemData = { id: 'mapItemData1_id', name: 'mapItemData1_name', options: [innerServicePathItem]};
+
+ const servicePathItem: ServicePathMapItem = { id: 'servicePathItem_id', data: mapItemData1 };
+ const arrServicePathItems: ServicePathMapItem[] = [servicePathItem];
+
+ let nodeOrCPId: string = servicePathItem.id;
+
+ // call to the tested function
+ let res = fixture.componentInstance.findOptions(arrServicePathItems, nodeOrCPId);
+
+ // expect that
+ expect(res).toEqual([innerServicePathItem]);
+ });
+
+ it('findOptions() -> in case NOT item || item.data || item.data.options -> Validate return null' ,() => {
+ // init values / mock functions
+ let item = [{
+ // data: {
+ data:{
+ name:'data_name',
+ id: 'data_id'
+ },
+ name:'name',
+ id: 'id'
+ // }
+ }];
+ let items: Array<ServicePathMapItem> = item;
+ let nodeOrCPId: string = 'someString';
+
+ // call to the tested function
+ let res = fixture.componentInstance.findOptions(items, nodeOrCPId);
+
+ // expect that
+ expect(res).toBe(null);
+ });
+
+ it('convertValuesToDropDownOptions() -> Verify that the result is sorted' ,() => {
+ // init values / mock functions
+ const mapItemData1: MapItemData = { id: 'Z_ID', name: 'Z_NAME'};
+ const servicePathItem1: ServicePathMapItem = { id: 'Z_servicePathItem_id', data: mapItemData1 };
+
+ const mapItemData2: MapItemData = { id: 'A_ID', name: 'A_NAME'};
+ const servicePathItem2: ServicePathMapItem = { id: 'A_servicePathItem_id', data: mapItemData2 };
+
+ const mapItemData3: MapItemData = { id: 'M_ID', name: 'M_NAME'};
+ const servicePathItem3: ServicePathMapItem = { id: 'M_servicePathItem_id', data: mapItemData3 };
+
+ const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1, servicePathItem2, servicePathItem3];
+
+ // call to the tested function
+ let res = fixture.componentInstance.convertValuesToDropDownOptions(arrServicePathItems);
+
+ // expect that
+ expect(res.length).toBe(3);
+ expect(res[0].value).toBe("A_servicePathItem_id");
+ expect(res[0].label).toBe("A_NAME");
+ expect(res[1].value).toBe("M_servicePathItem_id");
+ expect(res[1].label).toBe("M_NAME");
+ expect(res[2].value).toBe("Z_servicePathItem_id");
+ expect(res[2].label).toBe("Z_NAME");
+
+ });
+
+ it('parseInitialData() -> link.fromNode Exist => Verify srcCP' ,() => {
+ // init values / mock functions
+
+ //Simulate Array<ServicePathMapItem to pass to the function
+ const mapItemData1: MapItemData = { id: 'mapItemID', name: 'mapItemName'};
+ const servicePathItem1: ServicePathMapItem = { id: 'servicePathItemId', data: mapItemData1 };
+ const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1];
+
+ //Simulate link
+ let link = {
+ fromNode:'testVal'
+ };
+ fixture.componentInstance.link = link;
+
+ //Simulate the response from convertValuesToDropDownOptions()
+ const value = "expected_id_fromNode";
+ const label = "expected_label_fromNode"
+ let result:Array<DropdownValue> = [];
+ result[0] = new DropdownValue(value, label);
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => result);
+
+ //Simulate the response from findOptions()
+ const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []};
+ const options: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 };
+ fixture.componentInstance.findOptions = jest.fn(() => options);
+
+
+ // call to the tested function
+ fixture.componentInstance.parseInitialData(arrServicePathItems);
+
+ // expect that
+ expect(fixture.componentInstance.srcCP.length).toBe(1);
+ expect(fixture.componentInstance.srcCP[0]).toEqual({
+ "value": value,
+ "label": label,
+ "hidden": false,
+ "selected": false
+ });
+ });
+
+ it('parseInitialData() -> link.fromNode & link.fromCP Exist => Verify srcCP' ,() => {
+ // init values / mock functions
+
+ //Simulate Array<ServicePathMapItem to pass to the function
+ const mapItemData1: MapItemData = { id: 'mapItemID', name: 'mapItemName'};
+ const servicePathItem1: ServicePathMapItem = { id: 'servicePathItemId', data: mapItemData1 };
+ const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1];
+
+ //Simulate link
+ let link = {
+ fromNode:'testVal',
+ fromCP: 'testVal'
+ };
+ fixture.componentInstance.link = link;
+
+ //Simulate the response from convertValuesToDropDownOptions()
+ const value = "expected_id_fromNode_and_fromCP";
+ const label = "expected_label_fromNode_and_fromCP"
+ let result:Array<DropdownValue> = [];
+ result[0] = new DropdownValue(value, label);
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => result);
+
+ //Simulate the response from findOptions()
+ const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []};
+ const options: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 };
+ fixture.componentInstance.findOptions = jest.fn(() => options);
+
+
+ // call to the tested function
+ fixture.componentInstance.parseInitialData(arrServicePathItems);
+
+ // expect that
+ expect(fixture.componentInstance.srcCP.length).toBe(1);
+ expect(fixture.componentInstance.srcCP[0]).toEqual({
+ "value": value,
+ "label": label,
+ "hidden": false,
+ "selected": false
+ });
+ });
+
+
+ it('parseInitialData() -> link.fromNode & link.fromCP & link.toNode Exist => Verify srcCP' ,() => {
+ // init values / mock functions
+
+ //Simulate Array<ServicePathMapItem to pass to the function
+ const mapItemData1: MapItemData = { id: 'mapItemID', name: 'mapItemName'};
+ const servicePathItem1: ServicePathMapItem = { id: 'servicePathItemId', data: mapItemData1 };
+ const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1];
+
+ //Simulate link
+ let link = {
+ fromNode:'testVal',
+ fromCP: 'testVal',
+ toNode: 'testVal'
+ };
+ fixture.componentInstance.link = link;
+
+ //Simulate the response from convertValuesToDropDownOptions()
+ const value = "expected_id_fromNode_and_fromCP_and_toNode";
+ const label = "expected_label_fromNode_and_fromCP_and_toNode"
+ let result:Array<DropdownValue> = [];
+ result[0] = new DropdownValue(value, label);
+ fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => result);
+
+ //Simulate the response from findOptions()
+ const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []};
+ const options: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 };
+ fixture.componentInstance.findOptions = jest.fn(() => options);
+
+
+ // call to the tested function
+ fixture.componentInstance.parseInitialData(arrServicePathItems);
+
+ // expect that
+ expect(fixture.componentInstance.srcCP.length).toBe(1);
+ expect(fixture.componentInstance.srcCP[0]).toEqual({
+ "value": value,
+ "label": label,
+ "hidden": false,
+ "selected": false
+ });
+ });
+
+
+
+}); \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.ts
new file mode 100644
index 0000000000..83c30b1a60
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.ts
@@ -0,0 +1,104 @@
+import {Component, Input} from '@angular/core';
+import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component";
+import {Link} from './link.model';
+import {ServicePathMapItem} from "app/models/graph/nodes-and-links-map";
+import * as _ from "lodash";
+
+@Component({
+ selector: 'link-row',
+ templateUrl: './link-row.component.html',
+ styleUrls: ['./link-row.component.less']
+})
+
+
+export class LinkRowComponent {
+ @Input() data:Array<ServicePathMapItem>;
+ @Input() link:Link;
+ @Input() removeRow:Function;
+ source: Array<DropdownValue> = [];
+ target: Array<DropdownValue> = [];
+ srcCP: Array<DropdownValue> = [];
+ targetCP: Array<DropdownValue> = [];
+
+ ngOnChanges() {
+ if (this.data) {
+ this.parseInitialData(this.data);
+ }
+ }
+
+ parseInitialData(data: Array<ServicePathMapItem>) {
+ this.source = this.convertValuesToDropDownOptions(data);
+ if (this.link.fromNode) {
+ let srcCPOptions = this.findOptions(data, this.link.fromNode);
+ if (!srcCPOptions) { return; }
+ this.srcCP = this.convertValuesToDropDownOptions(srcCPOptions);
+ if (this.link.fromCP) {
+ this.target = this.convertValuesToDropDownOptions(data);
+ if (this.link.toNode) {
+ let targetCPOptions = this.findOptions(data, this.link.toNode);
+ if (!targetCPOptions) { return; }
+ this.targetCP = this.convertValuesToDropDownOptions(targetCPOptions);
+ }
+ }
+ }
+ }
+
+ private findOptions(items: Array<ServicePathMapItem>, nodeOrCPId: string) {
+ let item = _.find(items, (dataItem) => nodeOrCPId === dataItem.id);
+ if (item && item.data && item.data.options) {
+ return item.data.options;
+ }
+ console.warn('no option was found to match selection of Node/CP with id:' + nodeOrCPId);
+ return null;
+ }
+
+ private convertValuesToDropDownOptions(values: Array<ServicePathMapItem>): Array<DropdownValue> {
+ let result:Array<DropdownValue> = [];
+ for (let i = 0; i < values.length ; i++) {
+ result[result.length] = new DropdownValue(values[i].id, values[i].data.name);
+ }
+ return result.sort((a, b) => a.label.localeCompare(b.label));
+ }
+
+ onSourceSelected(id) {
+ if (id) {
+ let srcCPOptions = this.findOptions(this.data, id);
+ this.srcCP = this.convertValuesToDropDownOptions(srcCPOptions);
+ this.link.fromCP = '';
+ this.link.toNode = '';
+ this.link.toCP = '';
+ this.target = [];
+ this.targetCP = [];
+ }
+ }
+
+ onSrcCPSelected (id) {
+ if (id) {
+ let srcCPOptions = this.findOptions(this.data, this.link.fromNode);
+ let srcCPData = srcCPOptions.find(option => id === option.id).data;
+ this.target = this.convertValuesToDropDownOptions(this.data);
+ this.link.fromCPOriginId = srcCPData.ownerId;
+ this.link.toNode = '';
+ this.link.toCP = '';
+ this.targetCP = [];
+ }
+
+ }
+
+ onTargetSelected(id) {
+ if (id) {
+ let targetCPOptions = this.findOptions(this.data, id);
+ this.targetCP = this.convertValuesToDropDownOptions(targetCPOptions);
+ this.link.toCP = '';
+ }
+
+ }
+
+ onTargetCPSelected(id) {
+ if (id) {
+ let targetCPOptions = this.findOptions(this.data, this.link.toNode);
+ let targetCPDataObj = targetCPOptions.find(option => id === option.id).data;
+ this.link.toCPOriginId = targetCPDataObj.ownerId;
+ }
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-information-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link.model.ts
index 3639639c88..80128eb42e 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-information-tab.component.ts
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link.model.ts
@@ -17,23 +17,20 @@
* limitations under the License.
* ============LICENSE_END=========================================================
*/
+'use strict';
+import {ForwardingPathLink} from "app/models/forwarding-path-link";
-import * as _ from "lodash";
-import { Component, Inject, Input, Output, EventEmitter } from "@angular/core";
-import { TranslateService } from './../../../../../shared/translator/translate.service';
-import { PolicyInstance } from 'app/models/graph/zones/policy-instance';
+export class Link extends ForwardingPathLink {
+ public canEdit:boolean = false;
+ public canRemove:boolean = false;
+ public isFirst:boolean = false;
-@Component({
- selector: 'policy-information-tab',
- templateUrl: './policy-information-tab.component.html',
- styleUrls: ['./../base/base-tab.component.less']
-})
-export class PolicyInformationTabComponent {
-
- @Input() policy:PolicyInstance;
- @Input() isViewOnly: boolean;
-
- constructor(private translateService:TranslateService) {
+ constructor(link: ForwardingPathLink, canEdit: boolean, canRemove: boolean, isFirst: boolean) {
+ super(link.fromNode,link.fromCP, link.toNode, link.toCP, link.fromCPOriginId, link.toCPOriginId);
+ this.canEdit = canEdit;
+ this.canRemove = canRemove;
+ this.isFirst = isFirst;
}
-
}
+
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.html
new file mode 100644
index 0000000000..db0d912934
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.html
@@ -0,0 +1,55 @@
+<!--
+ ~ 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="service-path-creator">
+ <form class="w-sdc-form">
+ <div class="i-sdc-form-item" >
+ <label class="i-sdc-form-label required">Flow Name</label>
+ <input type="text" data-tests-id="pathName" name="pathName" [(ngModel)]="forwardingPath.name" [attr.maxLength]="200" />
+ </div>
+
+ <div class="side-by-side">
+ <div class="i-sdc-form-item" >
+ <label class="i-sdc-form-label">Protocol</label>
+ <input type="text" data-tests-id="pathProtocol" name="protocol" [(ngModel)]="forwardingPath.protocol" [attr.maxLength]="200" />
+ </div>
+ <div class="i-sdc-form-item" >
+ <label class="i-sdc-form-label">Destination Port Numbers</label>
+ <input type="text" data-tests-id="pathPortNumbers" name="portNumbers" [(ngModel)]="forwardingPath.destinationPortNumber" pattern="[0-9,]*" />
+ </div>
+ </div>
+
+ <div class="separator-buttons">
+ <span class="based-on-title">Based On</span>
+ <a (click)="addRow()" [ngClass]="{'disabled':!isExtendAllowed()}" data-tests-id="extendPathlnk">Extend Flow</a>
+ </div>
+
+ <div class="generic-table">
+ <div class="header-row">
+ <div class="cell header-cell" *ngFor="let header of headers">
+ {{header}}
+ </div>
+ </div>
+ <div *ngIf="links && links.length === 0" class="no-row-text" >
+ There is no data to display
+ </div>
+ <div>
+ <link-row *ngFor="let link of links" [data]="linksMap" [link]="link" [removeRow]="removeRow" class="data-row" ></link-row>
+ </div>
+ </div>
+
+
+ </form>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.less
new file mode 100644
index 0000000000..2a3efbdd3c
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.less
@@ -0,0 +1,45 @@
+@import './../../../../../../assets/styles/variables.less';
+.service-path-creator {
+ font-family: @font-opensans-regular;
+ .separator-buttons {
+ margin: 10px 0;
+ display: flex;
+ justify-content: space-between;
+ }
+ .i-sdc-form-label {
+ font-size: 12px;
+ }
+ .w-sdc-form .i-sdc-form-item {
+ margin-bottom: 15px;
+ }
+
+ .side-by-side {
+ display: flex;
+ .i-sdc-form-item {
+ flex-basis: 100%;
+ &:first-child {
+ margin-right: 10px;
+ }
+ }
+ }
+
+ .generic-table {
+ max-height: 233px;
+ .header-row .header-cell {
+ &:last-child {
+ padding: 0;
+ }
+ }
+ /deep/ .cell {
+ &:last-child {
+ min-width: 30px;
+ }
+ }
+ }
+
+ .based-on-title {
+ text-transform: uppercase;
+ font-size: 18px;
+ font-family: @font-opensans-regular;
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.ts
new file mode 100644
index 0000000000..17c2081a75
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.ts
@@ -0,0 +1,149 @@
+/*-
+ * ============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 * as _ from "lodash";
+import { Component, ElementRef, forwardRef, Inject } from '@angular/core';
+import {Link} from './link-row/link.model';
+import {ForwardingPath} from 'app/models/forwarding-path';
+import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service";
+import {ForwardingPathLink} from "app/models/forwarding-path-link";
+import {ServicePathMapItem} from "app/models/graph/nodes-and-links-map";
+import {CompositionService} from "app/ng2/pages/composition/composition.service";
+
+@Component({
+ selector: 'service-path-creator',
+ templateUrl: './service-path-creator.component.html',
+ styleUrls:['./service-path-creator.component.less'],
+ providers: [ServiceServiceNg2]
+})
+
+export class ServicePathCreatorComponent {
+
+ linksMap:Array<ServicePathMapItem>;
+ links:Array<Link> = [];
+ input:any;
+ headers: Array<string> = [];
+ removeRow: Function;
+ forwardingPath:ForwardingPath;
+ //isExtendAllowed:boolean = false;
+
+ constructor(private serviceService: ServiceServiceNg2,
+ private compositionService: CompositionService) {
+ this.forwardingPath = new ForwardingPath();
+ this.links = [new Link(new ForwardingPathLink('', '', '', '', '', ''), true, false, true)];
+ this.headers = ['Source', 'Source Connection Point', 'Target', 'Target Connection Point', ' '];
+ this.removeRow = () => {
+ if (this.links.length === 1) {
+ return;
+ }
+ this.links.splice(this.links.length-1, 1);
+ this.enableCurrentRow();
+ };
+ }
+
+ ngOnInit() {
+ this.serviceService.getNodesAndLinksMap(this.input.serviceId).subscribe((res:any) => {
+ this.linksMap = res;
+ });
+ this.processExistingPath();
+
+ }
+
+ private processExistingPath() {
+ if (this.input.pathId) {
+ let forwardingPath = <ForwardingPath>{...this.compositionService.forwardingPaths[this.input.pathId]};
+ this.forwardingPath.name = forwardingPath.name;
+ this.forwardingPath.destinationPortNumber = forwardingPath.destinationPortNumber;
+ this.forwardingPath.protocol = forwardingPath.protocol;
+ this.forwardingPath.uniqueId = forwardingPath.uniqueId;
+ this.links = [];
+ _.forEach(forwardingPath.pathElements.listToscaDataDefinition, (link:ForwardingPathLink) => {
+ this.links[this.links.length] = new Link(link, false, false, false);
+ });
+ this.links[this.links.length - 1].canEdit = true;
+ this.links[this.links.length - 1].canRemove = true;
+ this.links[0].isFirst = true;
+ }
+ }
+
+ isExtendAllowed():boolean {
+ if (this.links[this.links.length-1].toCP) {
+ return true;
+ }
+ return false;
+ }
+
+ enableCurrentRow() {
+ this.links[this.links.length-1].canEdit = true;
+ if (this.links.length !== 1) {
+ this.links[this.links.length-1].canRemove = true;
+ }
+ }
+
+ addRow() {
+ this.disableRows();
+ this.links[this.links.length] = new Link(
+ new ForwardingPathLink(this.links[this.links.length-1].toNode,
+ this.links[this.links.length-1].toCP,
+ '',
+ '',
+ this.links[this.links.length-1].toCPOriginId,
+ ''
+ ),
+ true,
+ true,
+ false
+ );
+ }
+
+ disableRows() {
+ for (let i = 0 ; i < this.links.length ; i++) {
+ this.links[i].canEdit = false;
+ this.links[i].canRemove = false;
+ }
+ }
+
+ createPathLinksObject() {
+ for (let i = 0 ; i < this.links.length ; i++) {
+ let link = this.links[i];
+ this.forwardingPath.addPathLink(link.fromNode, link.fromCP, link.toNode, link.toCP, link.fromCPOriginId, link.toCPOriginId);
+ }
+ }
+
+ createServicePathData() {
+ this.createPathLinksObject();
+ return this.forwardingPath;
+ }
+
+ checkFormValidForSubmit():boolean {
+ if (this.forwardingPath.name && this.isPathValid() ) {
+ return true;
+ }
+ return false;
+ }
+
+ isPathValid():boolean {
+ let lastLink = this.links[this.links.length -1] ;
+ if (lastLink.toNode && lastLink.toCP && lastLink.fromNode && lastLink.fromCP) {
+ return true;
+ }
+ return false;
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.module.ts
new file mode 100644
index 0000000000..78005317a2
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.module.ts
@@ -0,0 +1,25 @@
+import { NgModule } from "@angular/core";
+import {CommonModule} from "@angular/common";
+import {ServicePathCreatorComponent} from "./service-path-creator.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 {LinkRowComponent} from './link-row/link-row.component'
+@NgModule({
+ declarations: [
+ ServicePathCreatorComponent,
+ LinkRowComponent
+ ],
+ imports: [CommonModule,
+ FormsModule,
+ FormElementsModule,
+ UiElementsModule
+ ],
+ exports: [],
+ entryComponents: [
+ ServicePathCreatorComponent
+ ],
+ providers: []
+})
+export class ServicePathCreatorModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.html
new file mode 100644
index 0000000000..e1a4f68a9b
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.html
@@ -0,0 +1,27 @@
+<!--
+ ~ 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="service-path-selector">
+ <label>Service Flows:</label>
+ <ui-element-dropdown
+ class="path-dropdown"
+ data-tests-id="service-path-selector"
+ [readonly]="dropdownOptions.length < 3"
+ [(value)]="selectedPathId"
+ [values]="dropdownOptions"
+ (valueChange)="onSelectPath()">
+ </ui-element-dropdown>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.less
new file mode 100644
index 0000000000..f618d6b6f4
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.less
@@ -0,0 +1,24 @@
+@import './../../../../../../assets/styles/variables.less';
+.service-path-selector {
+ margin: 10px 35px 10px 0;
+ display: flex;
+ font-size: 12px;
+
+ /deep/ .path-dropdown {
+ width: 150px;
+ select {
+ font-size: 14px;
+ font-family: @font-opensans-regular;
+ padding: 4px 10px;
+ }
+ }
+
+ label {
+ margin-right: 10px;
+ align-self: center;
+ font-size: 14px;
+ font-family: @font-opensans-regular;
+ font-weight: normal;
+ margin-bottom: initial;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.ts
new file mode 100644
index 0000000000..0dba906f64
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.ts
@@ -0,0 +1,142 @@
+import {Component, Input, KeyValueDiffer, IterableDiffers, KeyValueDiffers, DoCheck} from '@angular/core';
+import {Service} from "app/models/components/service";
+import {TranslateService} from "app/ng2/shared/translator/translate.service";
+import {ForwardingPath} from "app/models/forwarding-path";
+import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component";
+import {CompositionService} from "app/ng2/pages/composition/composition.service";
+import {EventListenerService} from "app/services/event-listener-service";
+import {GRAPH_EVENTS} from "app/utils/constants";
+
+@Component({
+ selector: 'service-path-selector',
+ templateUrl: './service-path-selector.component.html',
+ styleUrls: ['service-path-selector.component.less']
+})
+
+export class ServicePathSelectorComponent {
+
+ defaultSelectedId: string;
+ hideAllValue: string;
+ hideAllId: string = '0';
+ showAllValue: string;
+ showAllId: string = '1';
+
+ paths: Array<ForwardingPath> = [];
+ dropdownOptions: Array<DropdownValue>;
+ differ: KeyValueDiffer<string, ForwardingPath>;
+
+ @Input() drawPath: Function;
+ @Input() deletePaths: Function;
+ @Input() selectedPathId: string;
+
+ constructor(private differs: KeyValueDiffers,
+ private translateService: TranslateService,
+ private compositionService: CompositionService,
+ private eventListenerService: EventListenerService
+ ) {
+
+ this.defaultSelectedId = this.hideAllId;
+ this.convertPathsToDropdownOptions();
+
+ this.translateService.languageChangedObservable.subscribe(lang => {
+ this.hideAllValue = this.translateService.translate("SERVICE_PATH_SELECTOR_HIDE_ALL_VALUE");
+ this.showAllValue = this.translateService.translate("SERVICE_PATH_SELECTOR_SHOW_ALL_VALUE");
+ this.convertPathsToDropdownOptions();
+ });
+
+ }
+
+ ngOnInit(): void {
+
+ this.selectedPathId = this.defaultSelectedId;
+ this.differ = this.differs.find(this.compositionService.forwardingPaths).create();
+ this.updatePaths();
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_SERVICE_PATH_CREATED, (createdId) => {
+ this.selectedPathId = createdId;
+ this.updatePaths();
+ } )
+
+ }
+
+ updatePaths(): void {
+
+ const pathsChanged = this.differ.diff(this.compositionService.forwardingPaths);
+
+ if (pathsChanged) {
+ let oldPaths = _.cloneDeep(this.paths);
+ this.populatePathsFromService();
+
+ if (!(_.isEqual(oldPaths, this.paths))) {
+ this.convertPathsToDropdownOptions();
+
+ let temp = this.selectedPathId;
+ this.selectedPathId = '-1';
+
+ setTimeout(() => {
+ this.selectedPathId = temp;
+ this.onSelectPath();
+ }, 0);
+ }
+ }
+
+ }
+
+ populatePathsFromService(): void {
+
+ this.paths = [];
+
+ _.forEach(this.compositionService.forwardingPaths, path => {
+ this.paths.push(path);
+ });
+ this.paths.sort((a: ForwardingPath, b: ForwardingPath) => {
+ return a.name.localeCompare(b.name);
+ });
+
+ }
+
+ convertPathsToDropdownOptions(): void {
+
+ let result = [
+ new DropdownValue(this.hideAllId, this.hideAllValue),
+ new DropdownValue(this.showAllId, this.showAllValue)
+ ];
+
+ _.forEach(this.paths, (value: ForwardingPath) => {
+ result[result.length] = new DropdownValue(value.uniqueId, value.name);
+ });
+
+ this.dropdownOptions = result;
+
+ }
+
+ onSelectPath = (): void => {
+
+ if (this.selectedPathId !== '-1') {
+ this.deletePaths();
+
+ switch (this.selectedPathId) {
+ case this.hideAllId:
+ break;
+
+ case this.showAllId:
+ _.forEach(this.paths, path =>
+ this.drawPath(path)
+ );
+ break;
+
+ default:
+ let path = this.paths.find(path =>
+ path.uniqueId === this.selectedPathId
+ );
+ if (!path) {
+ this.selectedPathId = this.defaultSelectedId;
+ this.onSelectPath(); // currently does nothing in default case, but if one day it does, we want the selection to behave accordingly.
+ break;
+ }
+ this.drawPath(path);
+ break;
+ }
+ }
+
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.module.ts
new file mode 100644
index 0000000000..6782c88b76
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.module.ts
@@ -0,0 +1,22 @@
+import { NgModule } from "@angular/core";
+import {CommonModule} from "@angular/common";
+import {ServicePathSelectorComponent} from "./service-path-selector.component";
+import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module";
+import {CompositionService} from "app/ng2/pages/composition/composition.service";
+
+@NgModule({
+ declarations: [
+ ServicePathSelectorComponent
+ ],
+ imports: [
+ CommonModule,
+ UiElementsModule
+ ],
+ exports: [ServicePathSelectorComponent],
+ entryComponents: [
+ ServicePathSelectorComponent
+ ],
+ providers: [CompositionService]
+})
+export class ServicePathSelectorModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.html
new file mode 100644
index 0000000000..39c41916a2
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.html
@@ -0,0 +1,21 @@
+<div class="service-path-list">
+ <div class="add-path-link" *ngIf="!isViewOnly"><a (click)="onAddServicePath()" data-tests-id="add-service-path-lnk" >+ Add Flow</a></div>
+ <div class="generic-table table-container" >
+ <div class="header-row">
+ <div class="cell header-cell" *ngFor="let header of headers">
+ {{header}}
+ </div>
+ </div>
+ <div *ngFor="let path of paths" class="data-row" >
+ <div class="cell" data-tests-id="path-name" >{{path.name}}</div>
+ <div class="cell path-action-buttons">
+ <span class="sprite-new update-component-icon" (click)="onEditServicePath(path.uniqueId)" data-tests-id="update-service-path-btn" ></span>
+ <span class="sprite-new delete-item-icon" *ngIf="!isViewOnly" (click)="deletePath(path.uniqueId)" data-tests-id="delete-service-path-btn"></span>
+ </div>
+ </div>
+ <div *ngIf="paths && paths.length === 0" class="no-row-text" >
+ No flows have been added yet.
+ </div>
+ </div>
+
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.less
new file mode 100644
index 0000000000..17f70926ff
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.less
@@ -0,0 +1,24 @@
+@import './../../../../../../assets/styles/variables.less';
+
+.add-path-link {
+ display: flex;
+ align-items: flex-end;
+ flex-direction: column;
+ padding-bottom: 10px;
+}
+
+.generic-table {
+ max-height: 233px;
+}
+
+.path-action-buttons {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ .sprite-new {
+ cursor: pointer;
+ }
+ & > span:only-child {
+ margin: auto;
+}
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.ts
new file mode 100644
index 0000000000..81abe42cb3
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.ts
@@ -0,0 +1,70 @@
+/*-
+ * ============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 * as _ from "lodash";
+import {Component, ComponentRef} from '@angular/core';
+import {ForwardingPath} from "app/models/forwarding-path";
+import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service";
+import {ModalService} from "app/ng2/services/modal.service";
+import {ModalComponent} from "app/ng2/components/ui/modal/modal.component";
+import {CompositionService} from "app/ng2/pages/composition/composition.service";
+
+@Component({
+ selector: 'service-paths-list',
+ templateUrl: './service-paths-list.component.html',
+ styleUrls:['service-paths-list.component.less'],
+ providers: [ServiceServiceNg2, ModalService]
+})
+export class ServicePathsListComponent {
+ modalInstance: ComponentRef<ModalComponent>;
+ headers: Array<string> = [];
+ paths: Array<ForwardingPath> = [];
+ input:any;
+ onAddServicePath: Function;
+ onEditServicePath: Function;
+ isViewOnly: boolean;
+
+ constructor(private serviceService:ServiceServiceNg2,
+ private compositionService: CompositionService) {
+ this.headers = ['Flow Name','Actions'];
+ }
+
+ ngOnInit() {
+ _.forEach(this.compositionService.forwardingPaths, (path: ForwardingPath)=> {
+ this.paths[this.paths.length] = path;
+ });
+ this.paths.sort((a:ForwardingPath, b:ForwardingPath)=> {
+ return a.name.localeCompare(b.name);
+ });
+ this.onAddServicePath = this.input.onCreateServicePath;
+ this.onEditServicePath = this.input.onEditServicePath;
+ this.isViewOnly = this.input.isViewOnly;
+ }
+
+ deletePath = (id:string):void => {
+ this.serviceService.deleteServicePath(this.input.serviceId, id).subscribe((res:any) => {
+ delete this.compositionService.forwardingPaths[id];
+ this.paths = this.paths.filter(function(path){
+ return path.uniqueId !== id;
+ });
+ });
+ };
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.module.ts
new file mode 100644
index 0000000000..5121627a9d
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.module.ts
@@ -0,0 +1,17 @@
+import { NgModule } from "@angular/core";
+import {CommonModule} from "@angular/common";
+import { ServicePathsListComponent } from "./service-paths-list.component";
+
+@NgModule({
+ declarations: [
+ ServicePathsListComponent
+ ],
+ imports: [CommonModule],
+ exports: [],
+ entryComponents: [
+ ServicePathsListComponent
+ ],
+ providers: []
+})
+export class ServicePathsListModule {
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-general-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-general-utils.ts
new file mode 100644
index 0000000000..bc8bd691c9
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-general-utils.ts
@@ -0,0 +1,268 @@
+/*-
+ * ============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 * as _ from "lodash";
+import {ComponentInstance, Match, CompositionCiLinkBase, CompositionCiNodeUcpeCp} from "app/models";
+import {Dictionary, GraphUIObjects} from "app/utils";
+import {MatchCapabilitiesRequirementsUtils} from "./match-capability-requierment-utils";
+import {CommonGraphUtils} from "../common/common-graph-utils";
+import {Injectable} from "@angular/core";
+import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils";
+import {ComponentServiceNg2} from "app/ng2/services/component-services/component.service";
+import {RequirementsGroup} from "app/models/requirement";
+import {CapabilitiesGroup} from "app/models/capability";
+import {TopologyTemplateService} from "app/ng2/services/component-services/topology-template.service";
+import {CompositionService} from "../../composition.service";
+import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service";
+import {NotificationsService} from "onap-ui-angular/dist/notifications/services/notifications.service";
+import {NotificationSettings} from "onap-ui-angular/dist/notifications/utilities/notification.config";
+
+export interface RequirementAndCapabilities {
+ capabilities: CapabilitiesGroup;
+ requirements: RequirementsGroup;
+}
+
+@Injectable()
+export class CompositionGraphGeneralUtils {
+
+ public componentRequirementsAndCapabilitiesCaching = new Dictionary<string, RequirementAndCapabilities>();
+
+ constructor(private commonGraphUtils: CommonGraphUtils,
+ private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils,
+ private queueServiceUtils: QueueServiceUtils,
+ private componentService: ComponentServiceNg2,
+ private topologyTemplateService: TopologyTemplateService,
+ private compositionService: CompositionService,
+ private workspaceService: WorkspaceService) {
+ }
+
+ /**
+ * Get the offset for the link creation Menu
+ * @param point
+ * @returns {Cy.Position}
+ */
+ public calcMenuOffset: Function = (point: Cy.Position): Cy.Position => {
+ point.x = point.x + 60;
+ point.y = point.y + 105;
+ return point;
+ };
+
+ /**
+ * return the top left position of the link menu
+ * @param cy
+ * @param targetNodePosition
+ * @returns {Cy.Position}
+ */
+ public getLinkMenuPosition = (cy: Cy.Instance, targetNodePosition: Cy.Position) => {
+ let menuPosition: Cy.Position = this.calcMenuOffset(targetNodePosition); //get the link mid point
+ if ($(document.body).height() < menuPosition.y + GraphUIObjects.LINK_MENU_HEIGHT + $(document.getElementsByClassName('sdc-composition-graph-wrapper')).offset().top) { // if position menu is overflow bottom
+ menuPosition.y = $(document.body).height() - GraphUIObjects.TOP_HEADER_HEIGHT - GraphUIObjects.LINK_MENU_HEIGHT;
+ }
+ return menuPosition;
+ };
+
+ public zoomGraphTo = (cy: Cy.Instance, zoomLevel: number): void => {
+ let zy = cy.height() / 2;
+ let zx = cy.width() / 2;
+ cy.zoom({
+ level: zoomLevel,
+ renderedPosition: {x: zx, y: zy}
+ });
+ }
+
+ //saves the current zoom, and then sets a temporary maximum zoom for zoomAll, and then reverts to old value
+ public zoomAllWithMax = (cy: Cy.Instance, maxZoom: number): void => {
+
+ let oldMaxZoom: number = cy.maxZoom();
+
+ cy.maxZoom(maxZoom);
+ this.zoomAll(cy);
+ cy.maxZoom(oldMaxZoom);
+
+ };
+
+ //Zooms to fit all of the nodes in the collection passed in. If no nodes are passed in, will zoom to fit all nodes on graph
+ public zoomAll = (cy: Cy.Instance, nodes?: Cy.CollectionNodes): void => {
+
+ if (!nodes || !nodes.length) {
+ nodes = cy.nodes();
+ }
+
+ cy.resize();
+ cy.animate({
+ fit: {eles: nodes, padding: 20},
+ center: {eles: nodes}
+ }, {duration: 400});
+ };
+
+ /**
+ * will return true/false if two nodes overlapping
+ *
+ * @param graph node
+ */
+ private isNodesOverlapping(node: Cy.CollectionFirstNode, draggedNode: Cy.CollectionFirstNode): boolean {
+
+ let nodeBoundingBox: Cy.BoundingBox = node.renderedBoundingBox();
+ let secondNodeBoundingBox: Cy.BoundingBox = draggedNode.renderedBoundingBox();
+
+ return this.isBBoxOverlapping(nodeBoundingBox, secondNodeBoundingBox);
+ }
+
+ /**
+ * Checks whether the bounding boxes of two nodes are overlapping on any side
+ * @param nodeOneBBox
+ * @param nodeTwoBBox
+ * @returns {boolean}
+ */
+ private isBBoxOverlapping(nodeOneBBox: Cy.BoundingBox, nodeTwoBBox: Cy.BoundingBox) {
+ return (((nodeOneBBox.x1 < nodeTwoBBox.x1 && nodeOneBBox.x2 > nodeTwoBBox.x1) ||
+ (nodeOneBBox.x1 < nodeTwoBBox.x2 && nodeOneBBox.x2 > nodeTwoBBox.x2) ||
+ (nodeTwoBBox.x1 < nodeOneBBox.x1 && nodeTwoBBox.x2 > nodeOneBBox.x2)) &&
+ ((nodeOneBBox.y1 < nodeTwoBBox.y1 && nodeOneBBox.y2 > nodeTwoBBox.y1) ||
+ (nodeOneBBox.y1 < nodeTwoBBox.y2 && nodeOneBBox.y2 > nodeTwoBBox.y2) ||
+ (nodeTwoBBox.y1 < nodeOneBBox.y1 && nodeTwoBBox.y2 > nodeOneBBox.y2)))
+ }
+
+ /**
+ * Checks whether a specific topologyTemplate instance can be hosted on the UCPE instance
+ * @param cy - Cytoscape instance
+ * @param fromUcpeInstance
+ * @param toComponentInstance
+ * @returns {Match}
+ */
+ public canBeHostedOn(cy: Cy.Instance, fromUcpeInstance: ComponentInstance, toComponentInstance: ComponentInstance): Match {
+
+ let matches: Array<Match> = this.matchCapabilitiesRequirementsUtils.getMatchedRequirementsCapabilities(fromUcpeInstance, toComponentInstance, this.getAllCompositionCiLinks(cy));
+ let hostedOnMatch: Match = _.find(matches, (match: Match) => {
+ return match.requirement.capability.toLowerCase() === 'tosca.capabilities.container';
+ });
+
+ return hostedOnMatch;
+ };
+
+ /**
+ * Checks whether node can be dropped into UCPE
+ * @param cy
+ * @param nodeToInsert
+ * @param ucpeNode
+ * @returns {boolean}
+ */
+ private isValidDropInsideUCPE(cy: Cy.Instance, nodeToInsert: ComponentInstance, ucpeNode: ComponentInstance): boolean {
+
+ let hostedOnMatch: Match = this.canBeHostedOn(cy, ucpeNode, nodeToInsert);
+ let result: boolean = !angular.isUndefined(hostedOnMatch) || nodeToInsert.isVl(); //group validation
+ return result;
+
+ };
+
+ /**
+ * For drops from palette, checks whether the node can be dropped. If node is being held over another node, check if capable of hosting
+ * @param cy
+ * @param pseudoNodeBBox
+ * @param paletteComponentInstance
+ * @returns {boolean}
+ */
+ public isPaletteDropValid(cy: Cy.Instance, pseudoNodeBBox: Cy.BoundingBox) {
+
+ let illegalOverlappingNodes = _.filter(cy.nodes("[isSdcElement]"), (graphNode: Cy.CollectionFirstNode) => {
+ if (this.isBBoxOverlapping(pseudoNodeBBox, graphNode.renderedBoundingBox())) {
+ return true;
+ }
+ return false;
+ });
+
+ return illegalOverlappingNodes.length === 0;
+ }
+
+ /**
+ * will return true/false if a drop of a single node is valid
+ *
+ * @param graph node
+ */
+ public isValidDrop(cy: Cy.Instance, draggedNode: Cy.CollectionFirstNode): boolean {
+
+ let illegalOverlappingNodes = _.filter(cy.nodes("[isSdcElement]"), (graphNode: Cy.CollectionFirstNode) => { //all sdc nodes, removing child nodes (childe node allways collaps
+
+ if (draggedNode.data().isUcpe && (graphNode.isChild() || graphNode.data().isInsideGroup)) { //ucpe cps always inside ucpe, no overlapping
+ return false;
+ }
+ if (draggedNode.data().isInsideGroup && (!draggedNode.active() || graphNode.data().isUcpe)) {
+ return false;
+ }
+
+ if (!draggedNode.data().isUcpe && !(draggedNode.data() instanceof CompositionCiNodeUcpeCp) && graphNode.data().isUcpe) { //case we are dragging a node into UCPE
+ let isEntirelyInUCPE: boolean = this.commonGraphUtils.isFirstBoxContainsInSecondBox(draggedNode.renderedBoundingBox(), graphNode.renderedBoundingBox());
+ if (isEntirelyInUCPE) {
+ if (this.isValidDropInsideUCPE(cy, draggedNode.data().componentInstance, graphNode.data().componentInstance)) { //if this is valid insert into ucpe, we return false - no illegal overlapping nodes
+ return false;
+ }
+ }
+ }
+ return graphNode.data().id !== draggedNode.data().id && this.isNodesOverlapping(draggedNode, graphNode);
+
+ });
+ // return false;
+ return illegalOverlappingNodes.length === 0;
+ };
+
+ /**
+ * will return true/false if the move of the nodes is valid (no node overlapping and verifying if insert into UCPE is valid)
+ *
+ * @param nodesArray - the selected drags nodes
+ */
+ public isGroupValidDrop(cy: Cy.Instance, nodesArray: Cy.CollectionNodes): boolean {
+ let filterDraggedNodes = nodesArray.filter('[?isDraggable]');
+ let isValidDrop = _.every(filterDraggedNodes, (node: Cy.CollectionFirstNode) => {
+ return this.isValidDrop(cy, node);
+
+ });
+ return isValidDrop;
+ };
+
+ /**
+ * get all links in diagram
+ * @param cy
+ * @returns {any[]|boolean[]}
+ */
+ public getAllCompositionCiLinks = (cy: Cy.Instance): Array<CompositionCiLinkBase> => {
+ return _.map(cy.edges("[isSdcElement]"), (edge: Cy.CollectionEdges) => {
+ return edge.data();
+ });
+ };
+
+ /**
+ *
+ * @param blockAction - true/false if this is a block action
+ * @param instances
+ * @param component
+ */
+ public pushMultipleUpdateComponentInstancesRequestToQueue = (instances: Array<ComponentInstance>): void => {
+ this.queueServiceUtils.addNonBlockingUIAction(() => {
+ return new Promise<boolean>((resolve, reject) => {
+ let uniqueId = this.workspaceService.metadata.uniqueId;
+ let topologyType = this.workspaceService.metadata.componentType;
+ this.topologyTemplateService.updateMultipleComponentInstances(uniqueId, topologyType, instances).subscribe(instancesResult => {
+ this.compositionService.updateComponentInstances(instancesResult);
+ resolve(true);
+ });
+ });
+ });
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-links-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-links-utils.ts
new file mode 100644
index 0000000000..6035d05b7f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-links-utils.ts
@@ -0,0 +1,342 @@
+/*-
+ * ============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 obarda on 6/28/2016.
+ */
+import * as _ from "lodash";
+import {GraphUIObjects} from "app/utils";
+import {
+ Match,
+ CompositionCiNodeBase,
+ RelationshipModel,
+ ConnectRelationModel,
+ LinksFactory,
+ Component,
+ LinkMenu,
+ Point,
+ CompositionCiLinkBase,
+ Requirement,
+ Capability,
+ Relationship,
+ ComponentInstance
+} from "app/models";
+import {CommonGraphUtils} from "../common/common-graph-utils";
+import {CompositionGraphGeneralUtils} from "./composition-graph-general-utils";
+import {MatchCapabilitiesRequirementsUtils} from "./match-capability-requierment-utils";
+import {CompositionCiServicePathLink} from "app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link";
+import {Injectable} from "@angular/core";
+import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils";
+import {TopologyTemplateService} from "app/ng2/services/component-services/topology-template.service";
+import {SdcUiServices} from "onap-ui-angular";
+import {CompositionService} from "../../composition.service";
+import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service";
+
+@Injectable()
+export class CompositionGraphLinkUtils {
+
+ constructor(private linksFactory: LinksFactory,
+ private generalGraphUtils: CompositionGraphGeneralUtils,
+ private commonGraphUtils: CommonGraphUtils,
+ private queueServiceUtils: QueueServiceUtils,
+ private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils,
+ private topologyTemplateService: TopologyTemplateService,
+ private loaderService: SdcUiServices.LoaderService,
+ private compositionService: CompositionService,
+ private workspaceService: WorkspaceService) {
+
+
+ }
+
+ /**
+ * Delete the link on server and then remove it from graph
+ * @param component
+ * @param releaseLoading - true/false release the loader when finished
+ * @param link - the link to delete
+ */
+ public deleteLink = (cy: Cy.Instance, component: Component, releaseLoading: boolean, link: Cy.CollectionEdges) => {
+
+ this.loaderService.activate();
+ this.queueServiceUtils.addBlockingUIAction(() => {
+ this.topologyTemplateService.deleteRelation(this.workspaceService.metadata.uniqueId, this.workspaceService.metadata.componentType, link.data().relation).subscribe((deletedRelation) => {
+ this.compositionService.deleteRelation(deletedRelation);
+ cy.remove(link);
+ this.loaderService.deactivate();
+ }, (error) => {this.loaderService.deactivate()});
+ });
+ };
+
+ /**
+ * create the link on server and than draw it on graph
+ * @param link - the link to create
+ * @param cy
+ * @param component
+ */
+ public createLink = (link: CompositionCiLinkBase, cy: Cy.Instance): void => {
+
+ this.loaderService.activate();
+ link.updateLinkDirection();
+
+ this.queueServiceUtils.addBlockingUIAction(() => {
+ this.topologyTemplateService.createRelation(this.workspaceService.metadata.uniqueId, this.workspaceService.metadata.componentType, link.relation).subscribe((relation) => {
+ link.setRelation(relation);
+ this.insertLinkToGraph(cy, link);
+ this.compositionService.addRelation(relation);
+ this.loaderService.deactivate();
+ }, (error) => {this.loaderService.deactivate()})
+ });
+ };
+
+ private createSimpleLink = (match: Match, cy: Cy.Instance): void => {
+ let newRelation: RelationshipModel = match.matchToRelationModel();
+ let linkObg: CompositionCiLinkBase = this.linksFactory.createGraphLink(cy, newRelation, newRelation.relationships[0]);
+ this.createLink(linkObg, cy);
+ };
+
+ public createLinkFromMenu = (cy: Cy.Instance, chosenMatch: Match): void => {
+
+ if (chosenMatch) {
+ if (chosenMatch && chosenMatch instanceof Match) {
+ this.createSimpleLink(chosenMatch, cy);
+ }
+ }
+ }
+
+ /**
+ * open the connect link menu if the link drawn is valid - match requirements & capabilities
+ * @param cy
+ * @param fromNode
+ * @param toNode
+ * @returns {any}
+ */
+ public onLinkDrawn(cy: Cy.Instance, fromNode: Cy.CollectionFirstNode, toNode: Cy.CollectionFirstNode): ConnectRelationModel {
+
+ let linkModel: Array<CompositionCiLinkBase> = this.generalGraphUtils.getAllCompositionCiLinks(cy);
+
+ let possibleRelations: Array<Match> = this.matchCapabilitiesRequirementsUtils.getMatchedRequirementsCapabilities(fromNode.data().componentInstance,
+ toNode.data().componentInstance, linkModel);
+
+ //if found possibleRelations between the nodes we create relation menu directive and open the link menu
+ if (possibleRelations.length) {
+ // let menuPosition = this.generalGraphUtils.getLinkMenuPosition(cy, toNode.renderedPoint());
+ return new ConnectRelationModel(fromNode.data(), toNode.data(), possibleRelations);
+ }
+ return null;
+ };
+
+ private handlePathLink(cy: Cy.Instance, event: Cy.EventObject) {
+ let linkData = event.cyTarget.data();
+ let selectedPathId = linkData.pathId;
+ let pathEdges = cy.collection(`[pathId='${selectedPathId}']`);
+ if (pathEdges.length > 1) {
+ setTimeout(() => {
+ pathEdges.select();
+ }, 0);
+ }
+ }
+
+ private handleVLLink(event: Cy.EventObject) {
+ let vl: Cy.CollectionNodes = event.cyTarget[0].target('.vl-node');
+ let connectedEdges: Cy.CollectionEdges = vl.connectedEdges(`[type!="${CompositionCiServicePathLink.LINK_TYPE}"]`);
+ if (vl.length && connectedEdges.length > 1) {
+ setTimeout(() => {
+ vl.select();
+ connectedEdges.select();
+ }, 0);
+ }
+ }
+
+
+ /**
+ * Handles click event on links.
+ * If one edge selected: do nothing.
+ * Two or more edges: first click - select all, secondary click - select single.
+ * @param cy
+ * @param event
+ */
+ public handleLinkClick(cy: Cy.Instance, event: Cy.EventObject) {
+ if (cy.$('edge:selected').length > 1 && event.cyTarget[0].selected()) {
+ cy.$(':selected').unselect();
+ } else {
+ if (event.cyTarget[0].data().type === CompositionCiServicePathLink.LINK_TYPE) {
+ this.handlePathLink(cy, event);
+ }
+ else {
+ this.handleVLLink(event);
+ }
+ }
+ }
+
+
+ /**
+ * Calculates the position for the menu that modifies an existing link
+ * @param event
+ * @param elementWidth
+ * @param elementHeight
+ * @returns {Point}
+ */
+ public calculateLinkMenuPosition(event, elementWidth, elementHeight): Point {
+ let point: Point = new Point(event.originalEvent.clientX, event.originalEvent.clientY);
+ if (event.originalEvent.view.screen.height - elementHeight < point.y) {
+ point.y = event.originalEvent.view.screen.height - elementHeight;
+ }
+ if (event.originalEvent.view.screen.width - elementWidth < point.x) {
+ point.x = event.originalEvent.view.screen.width - elementWidth;
+ }
+ return point;
+ };
+
+
+ /**
+ * Gets the menu that is displayed when you click an existing link.
+ * @param link
+ * @param event
+ * @returns {LinkMenu}
+ */
+ public getModifyLinkMenu(link: Cy.CollectionFirstEdge, event: Cy.EventObject): LinkMenu {
+ let point: Point = this.calculateLinkMenuPosition(event, GraphUIObjects.MENU_LINK_VL_WIDTH_OFFSET, GraphUIObjects.MENU_LINK_VL_HEIGHT_OFFSET);
+ let menu: LinkMenu = new LinkMenu(point, true, link);
+ return menu;
+ };
+
+ /**
+ * Returns relation source and target nodes.
+ * @param nodes - all nodes in graph in order to find the edge connecting the two nodes
+ * @param fromNodeId
+ * @param toNodeId
+ * @returns [source, target] array of source node and target node.
+ */
+ public getRelationNodes(nodes: Cy.CollectionNodes, fromNodeId: string, toNodeId: string) {
+ return [
+ _.find(nodes, (node: Cy.CollectionFirst) => node.data().id === fromNodeId),
+ _.find(nodes, (node: Cy.CollectionFirst) => node.data().id === toNodeId)
+ ];
+ }
+
+
+ /**
+ * go over the relations and draw links on the graph
+ * @param cy
+ * @param getRelationRequirementCapability - function to get requirement and capability of a relation
+ */
+ public initGraphLinks(cy: Cy.Instance, relations: RelationshipModel[]) {
+ if (relations) {
+ _.forEach(relations, (relationshipModel: RelationshipModel) => {
+ _.forEach(relationshipModel.relationships, (relationship: Relationship) => {
+ let linkToCreate = this.linksFactory.createGraphLink(cy, relationshipModel, relationship);
+ this.insertLinkToGraph(cy, linkToCreate);
+ });
+ });
+ }
+ }
+
+ /**
+ * Add link to graph - only draw the link
+ * @param cy
+ * @param link
+ * @param getRelationRequirementCapability
+ */
+ public insertLinkToGraph = (cy: Cy.Instance, link: CompositionCiLinkBase) => {
+ const relationNodes = this.getRelationNodes(cy.nodes(), link.source, link.target);
+ const sourceNode: CompositionCiNodeBase = relationNodes[0] && relationNodes[0].data();
+ const targetNode: CompositionCiNodeBase = relationNodes[1] && relationNodes[1].data();
+ if ((sourceNode && !sourceNode.certified) || (targetNode && !targetNode.certified)) {
+ link.classes = 'not-certified-link';
+ }
+ let linkElement = cy.add({
+ group: 'edges',
+ data: link,
+ classes: link.classes
+ });
+
+ const getLinkRequirementCapability = () =>
+ this.getRelationRequirementCapability(link.relation.relationships[0], sourceNode.componentInstance, targetNode.componentInstance);
+ this.commonGraphUtils.initLinkTooltip(linkElement, link.relation.relationships[0], getLinkRequirementCapability);
+ };
+
+ public syncComponentByRelation(relation: RelationshipModel) {
+ let componentInstances = this.compositionService.getComponentInstances();
+ relation.relationships.forEach((rel) => {
+ if (rel.capability) {
+ const toComponentInstance: ComponentInstance = componentInstances.find((inst) => inst.uniqueId === relation.toNode);
+ const toComponentInstanceCapability: Capability = toComponentInstance.findCapability(
+ rel.capability.type, rel.capability.uniqueId, rel.capability.ownerId, rel.capability.name);
+ const isCapabilityFulfilled: boolean = rel.capability.isFulfilled();
+ if (isCapabilityFulfilled && toComponentInstanceCapability) {
+ // if capability is fulfilled and in component, then remove it
+ console.log('Capability is fulfilled', rel.capability.getFullTitle(), rel.capability.leftOccurrences);
+ toComponentInstance.capabilities[rel.capability.type].splice(
+ toComponentInstance.capabilities[rel.capability.type].findIndex((cap) => cap === toComponentInstanceCapability), 1
+ )
+ } else if (!isCapabilityFulfilled && !toComponentInstanceCapability) {
+ // if capability is unfulfilled and not in component, then add it
+ console.log('Capability is unfulfilled', rel.capability.getFullTitle(), rel.capability.leftOccurrences);
+ toComponentInstance.capabilities[rel.capability.type].push(rel.capability);
+ }
+ }
+ if (rel.requirement) {
+ const fromComponentInstance: ComponentInstance = componentInstances.find((inst) => inst.uniqueId === relation.fromNode);
+ const fromComponentInstanceRequirement: Requirement = fromComponentInstance.findRequirement(
+ rel.requirement.capability, rel.requirement.uniqueId, rel.requirement.ownerId, rel.requirement.name);
+ const isRequirementFulfilled: boolean = rel.requirement.isFulfilled();
+ if (isRequirementFulfilled && fromComponentInstanceRequirement) {
+ // if requirement is fulfilled and in component, then remove it
+ console.log('Requirement is fulfilled', rel.requirement.getFullTitle(), rel.requirement.leftOccurrences);
+ fromComponentInstance.requirements[rel.requirement.capability].splice(
+ fromComponentInstance.requirements[rel.requirement.capability].findIndex((req) => req === fromComponentInstanceRequirement), 1
+ )
+ } else if (!isRequirementFulfilled && !fromComponentInstanceRequirement) {
+ // if requirement is unfulfilled and not in component, then add it
+ console.log('Requirement is unfulfilled', rel.requirement.getFullTitle(), rel.requirement.leftOccurrences);
+ fromComponentInstance.requirements[rel.requirement.capability].push(rel.requirement);
+ }
+ }
+ });
+ }
+
+ public getRelationRequirementCapability(relationship: Relationship, sourceNode: ComponentInstance, targetNode: ComponentInstance): Promise<{ requirement: Requirement, capability: Capability }> {
+ // try find the requirement and capability in the source and target component instances:
+ let capability: Capability = targetNode.findCapability(undefined,
+ relationship.relation.capabilityUid,
+ relationship.relation.capabilityOwnerId,
+ relationship.relation.capability);
+ let requirement: Requirement = sourceNode.findRequirement(undefined,
+ relationship.relation.requirementUid,
+ relationship.relation.requirementOwnerId,
+ relationship.relation.requirement);
+
+ return new Promise<{ requirement: Requirement, capability: Capability }>((resolve, reject) => {
+ if (capability && requirement) {
+ resolve({capability, requirement});
+ }
+ else {
+ // if requirement and/or capability is missing, then fetch the full relation with its requirement and capability:
+ this.topologyTemplateService.fetchRelation(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, relationship.relation.id).subscribe((fetchedRelation) => {
+ this.syncComponentByRelation(fetchedRelation);
+ resolve({
+ capability: capability || fetchedRelation.relationships[0].capability,
+ requirement: requirement || fetchedRelation.relationships[0].requirement
+ });
+ }, reject);
+ }
+ });
+ }
+}
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.spec.ts
new file mode 100644
index 0000000000..9dcc47f7cc
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.spec.ts
@@ -0,0 +1,158 @@
+import { TestBed } from '@angular/core/testing';
+import { SdcUiServices } from 'onap-ui-angular';
+import { Observable } from 'rxjs/Rx';
+import CollectionNodes = Cy.CollectionNodes;
+import { Mock } from 'ts-mockery';
+import { ComponentInstance } from '../../../../../models';
+import { ComponentMetadata } from '../../../../../models/component-metadata';
+import { Resource } from '../../../../../models/components/resource';
+import { CompositionCiNodeCp } from '../../../../../models/graph/nodes/composition-graph-nodes/composition-ci-node-cp';
+import { CompositionCiNodeVl } from '../../../../../models/graph/nodes/composition-graph-nodes/composition-ci-node-vl';
+import { EventListenerService } from '../../../../../services';
+import CollectionEdges = Cy.CollectionEdges;
+import { GRAPH_EVENTS } from '../../../../../utils/constants';
+import { ServiceServiceNg2 } from '../../../../services/component-services/service.service';
+import { TopologyTemplateService } from '../../../../services/component-services/topology-template.service';
+import { ComponentGenericResponse } from '../../../../services/responses/component-generic-response';
+import { QueueServiceUtils } from '../../../../utils/queue-service-utils';
+import { WorkspaceService } from '../../../workspace/workspace.service';
+import { CompositionService } from '../../composition.service';
+import { CommonGraphUtils } from '../common/common-graph-utils';
+import { CompositionGraphGeneralUtils } from './composition-graph-general-utils';
+import { CompositionGraphNodesUtils } from './composition-graph-nodes-utils';
+
+describe('composition graph nodes utils', () => {
+
+ const CP_TO_DELETE_ID = 'cp1';
+ const VL_TO_DELETE_ID = 'vl';
+ const CP2_ID = 'cp2';
+
+ let loaderServiceMock: Partial<SdcUiServices.LoaderService>;
+ let service: CompositionGraphNodesUtils;
+ let topologyServiceMock: TopologyTemplateService;
+ let queueServiceMock: QueueServiceUtils;
+ let workspaceServiceMock: WorkspaceService;
+ let compositionServiceMock: CompositionService;
+ let eventListenerServiceMock: EventListenerService;
+ const cpInstanceMock: ComponentInstance = Mock.of<ComponentInstance>({
+ uniqueId: CP_TO_DELETE_ID,
+ isVl: () => false
+ });
+ const vlInstanceMock: ComponentInstance = Mock.of<ComponentInstance>({
+ uniqueId: VL_TO_DELETE_ID,
+ isVl: () => true
+ });
+ const cp2InstanceMock: ComponentInstance = Mock.of<ComponentInstance>({
+ uniqueId: CP2_ID,
+ isVl: () => false
+ });
+
+ const cyMock = Mock.of<Cy.Instance>({
+ remove: jest.fn(),
+ collection: jest.fn()
+ });
+
+ const serviceServiceMock = Mock.of<ServiceServiceNg2>({
+ getComponentCompositionData : () => Observable.of(Mock.of<ComponentGenericResponse>())
+ });
+
+ // Instances on the graph cp, vl, cp2
+ const cp = Mock.from<CompositionCiNodeCp>({ id: CP_TO_DELETE_ID, componentInstance: cpInstanceMock });
+ const vl = Mock.from<CompositionCiNodeVl>({ id: VL_TO_DELETE_ID, componentInstance: vlInstanceMock });
+ const cp2 = Mock.from<CompositionCiNodeCp>({ id: CP2_ID, componentInstance: cp2InstanceMock });
+
+ beforeEach(() => {
+
+ loaderServiceMock = {
+ activate: jest.fn(),
+ deactivate: jest.fn()
+ };
+
+ topologyServiceMock = Mock.of<TopologyTemplateService>({
+ deleteComponentInstance : () => Observable.of(cpInstanceMock)
+ });
+
+ queueServiceMock = Mock.of<QueueServiceUtils>({
+ addBlockingUIAction : ( (f) => f() )
+ });
+
+ workspaceServiceMock = Mock.of<WorkspaceService>({
+ metadata: Mock.of<ComponentMetadata>( { uniqueId: 'topologyTemplateUniqueId' } )
+ });
+
+ compositionServiceMock = Mock.of<CompositionService>({
+ deleteComponentInstance : jest.fn()
+ });
+
+ eventListenerServiceMock = Mock.of<EventListenerService>({
+ notifyObservers : jest.fn()
+ });
+
+ TestBed.configureTestingModule({
+ imports: [],
+ providers: [
+ CompositionGraphNodesUtils,
+ {provide: WorkspaceService, useValue: workspaceServiceMock},
+ {provide: TopologyTemplateService, useValue: topologyServiceMock},
+ {provide: CompositionService, useValue: compositionServiceMock},
+ {provide: CompositionGraphGeneralUtils, useValue: {}},
+ {provide: CommonGraphUtils, useValue: {}},
+ {provide: EventListenerService, useValue: eventListenerServiceMock},
+ {provide: QueueServiceUtils, useValue: queueServiceMock},
+ {provide: ServiceServiceNg2, useValue: serviceServiceMock},
+ {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock}
+ ]
+ });
+ service = TestBed.get(CompositionGraphNodesUtils);
+ });
+
+ it('When a CP is deleted which is connected to a VL that has another leg to another CP, the VL is deleted as well', () => {
+ // Prepare a VL that is connected to both CP and CP2
+ const vlToDelete = Mock.of<CollectionNodes>({
+ data: () => vl,
+ connectedEdges: () => Mock.of<CollectionEdges>({
+ length: 2,
+ connectedNodes: () => [cp, cp2] as CollectionNodes
+ })
+ });
+
+ // Prepare a CP which is connected to a VL
+ const cpToDelete = Mock.of<CollectionNodes>({
+ data: () => cp,
+ connectedEdges: () => Mock.of<CollectionEdges>({
+ length: 1,
+ connectedNodes: () => [vlToDelete] as CollectionNodes
+ })
+ });
+ service.deleteNode(cyMock, Mock.of<Resource>(), cpToDelete);
+ expect(compositionServiceMock.deleteComponentInstance).toHaveBeenCalledWith(CP_TO_DELETE_ID);
+ expect(eventListenerServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, VL_TO_DELETE_ID);
+ expect(eventListenerServiceMock.notifyObservers).toHaveBeenLastCalledWith(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, CP_TO_DELETE_ID);
+ expect(cyMock.remove).toHaveBeenCalled();
+ });
+
+ it('When a CP is deleted which is solely connected to another VL the VL is not deleted', () => {
+ // Prepare a VL that is connected only to 1 CP
+ const vlToDelete = Mock.of<CollectionNodes>({
+ data: () => vl,
+ connectedEdges: () => Mock.of<CollectionEdges>({
+ length: 1,
+ connectedNodes: () => [cp] as CollectionNodes
+ })
+ });
+
+ // Prepare a CP which is connected to a VL
+ const cpToDelete = Mock.of<CollectionNodes>({
+ data: () => cp,
+ connectedEdges: () => Mock.of<CollectionEdges>({
+ length: 1,
+ connectedNodes: () => [vlToDelete] as CollectionNodes
+ })
+ });
+ service.deleteNode(cyMock, Mock.of<Resource>(), cpToDelete);
+ expect(compositionServiceMock.deleteComponentInstance).toHaveBeenCalledWith(CP_TO_DELETE_ID);
+ expect(eventListenerServiceMock.notifyObservers).toHaveBeenLastCalledWith(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, CP_TO_DELETE_ID);
+ expect(eventListenerServiceMock.notifyObservers).toHaveBeenCalledTimes(1);
+ expect(cyMock.remove).toHaveBeenCalled();
+ });
+});
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.ts
new file mode 100644
index 0000000000..ea876c6d1a
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.ts
@@ -0,0 +1,202 @@
+/*-
+ * ============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 { Injectable } from '@angular/core';
+import { Component as TopologyTemplate } from 'app/models';
+import {
+ ComponentInstance,
+ CompositionCiNodeVl, Service
+} from 'app/models';
+import { CompositionCiServicePathLink } from 'app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link';
+import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service';
+import { ServiceServiceNg2 } from 'app/ng2/services/component-services/service.service';
+import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service';
+import { ServiceGenericResponse } from 'app/ng2/services/responses/service-generic-response';
+import { QueueServiceUtils } from 'app/ng2/utils/queue-service-utils';
+import { EventListenerService } from 'app/services';
+import { GRAPH_EVENTS } from 'app/utils';
+import * as _ from 'lodash';
+import { SdcUiServices } from 'onap-ui-angular';
+import { CompositionService } from '../../composition.service';
+import { CommonGraphUtils } from '../common/common-graph-utils';
+import { CompositionGraphGeneralUtils } from './composition-graph-general-utils';
+
+/**
+ * Created by obarda on 11/9/2016.
+ */
+@Injectable()
+export class CompositionGraphNodesUtils {
+ constructor(private generalGraphUtils: CompositionGraphGeneralUtils,
+ private commonGraphUtils: CommonGraphUtils,
+ private eventListenerService: EventListenerService,
+ private queueServiceUtils: QueueServiceUtils,
+ private serviceService: ServiceServiceNg2,
+ private loaderService: SdcUiServices.LoaderService,
+ private compositionService: CompositionService,
+ private topologyTemplateService: TopologyTemplateService,
+ private workspaceService: WorkspaceService) {
+ }
+
+ /**
+ * Returns component instances for all nodes passed in
+ * @param nodes - Cy nodes
+ * @returns {any[]}
+ */
+ public getAllNodesData(nodes: Cy.CollectionNodes) {
+ return _.map(nodes, (node: Cy.CollectionFirstNode) => {
+ return node.data();
+ });
+ }
+
+ public highlightMatchingNodesByName = (cy: Cy.Instance, nameToMatch: string) => {
+
+ cy.batch(() => {
+ cy.nodes("[name !@^= '" + nameToMatch + "']").style({'background-image-opacity': 0.4});
+ cy.nodes("[name @^= '" + nameToMatch + "']").style({'background-image-opacity': 1});
+ });
+
+ }
+
+ // Returns all nodes whose name starts with searchTerm
+ public getMatchingNodesByName = (cy: Cy.Instance, nameToMatch: string): Cy.CollectionNodes => {
+ return cy.nodes("[name @^= '" + nameToMatch + "']");
+ }
+
+ /**
+ * Deletes component instances on server and then removes it from the graph as well
+ * @param cy
+ * @param component
+ * @param nodeToDelete
+ */
+ public deleteNode(cy: Cy.Instance, component: TopologyTemplate, nodeToDelete: Cy.CollectionNodes): void {
+
+ this.loaderService.activate();
+ const onSuccess: (response: ComponentInstance) => void = (response: ComponentInstance) => {
+ // check whether the node is connected to any VLs that only have one other connection. If so, delete that VL as well
+ this.loaderService.deactivate();
+ this.compositionService.deleteComponentInstance(response.uniqueId);
+
+ const nodeToDeleteIsNotVl = nodeToDelete.data().componentInstance && !(nodeToDelete.data().componentInstance.isVl());
+ if (nodeToDeleteIsNotVl) {
+ const connectedVls: Cy.CollectionFirstNode[] = this.getConnectedVlToNode(nodeToDelete);
+ this.handleConnectedVlsToDelete(connectedVls);
+ }
+
+ // check whether there is a service path going through this node, and if so clean it from the graph.
+ const nodeId = nodeToDelete.data().id;
+ const connectedPathLinks = cy.collection(`[type="${CompositionCiServicePathLink.LINK_TYPE}"][source="${nodeId}"], [type="${CompositionCiServicePathLink.LINK_TYPE}"][target="${nodeId}"]`);
+ _.forEach(connectedPathLinks, (link, key) => {
+ cy.remove(`[pathId="${link.data().pathId}"]`);
+ });
+
+ // update service path list
+ this.serviceService.getComponentCompositionData(component).subscribe((serviceResponse: ServiceGenericResponse) => {
+ (component as Service).forwardingPaths = serviceResponse.forwardingPaths;
+ });
+
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, nodeId);
+
+ // update UI
+ cy.remove(nodeToDelete);
+ };
+
+ const onFailed: (response: any) => void = (response: any) => {
+ this.loaderService.deactivate();
+ };
+
+ this.queueServiceUtils.addBlockingUIAction(
+ () => {
+ const uniqueId = this.workspaceService.metadata.uniqueId;
+ const componentType = this.workspaceService.metadata.componentType;
+ this.topologyTemplateService.deleteComponentInstance(componentType, uniqueId, nodeToDelete.data().componentInstance.uniqueId).subscribe(onSuccess, onFailed);
+ }
+ );
+ }
+
+ /**
+ * Finds all VLs connected to a single node
+ * @param node
+ * @returns {Array<Cy.CollectionFirstNode>}
+ */
+ public getConnectedVlToNode = (node: Cy.CollectionNodes): Cy.CollectionFirstNode[] => {
+ const connectedVls: Cy.CollectionFirstNode[] = new Array<Cy.CollectionFirstNode>();
+ _.forEach(node.connectedEdges().connectedNodes(), (connectedNode: Cy.CollectionFirstNode) => {
+ const connectedNodeIsVl = connectedNode.data().componentInstance.isVl();
+ if (connectedNodeIsVl) {
+ connectedVls.push(connectedNode);
+ }
+ });
+ return connectedVls;
+ }
+
+ /**
+ * Delete all VLs that have only two connected nodes (this function is called when deleting a node)
+ * @param connectedVls
+ */
+ public handleConnectedVlsToDelete = (connectedVls: Cy.CollectionFirstNode[]) => {
+ _.forEach(connectedVls, (vlToDelete: Cy.CollectionNodes) => {
+
+ if (vlToDelete.connectedEdges().length === 2) { // if vl connected only to 2 nodes need to delete the vl
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, vlToDelete.data().componentInstance.uniqueId);
+ }
+ });
+ }
+
+ /**
+ * This function will update nodes position.
+ * @param cy
+ * @param component
+ * @param nodesMoved - the node/multiple nodes now moved by the user
+ */
+ public onNodesPositionChanged = (cy: Cy.Instance, component: TopologyTemplate, nodesMoved: Cy.CollectionNodes): void => {
+
+ if (nodesMoved.length === 0) {
+ return;
+ }
+
+ const isValidMove: boolean = this.generalGraphUtils.isGroupValidDrop(cy, nodesMoved);
+ if (isValidMove) {
+
+ const instancesToUpdate: ComponentInstance[] = new Array<ComponentInstance>();
+
+ _.each(nodesMoved, (node: Cy.CollectionFirstNode) => { // update all nodes new position
+
+ // update position
+ const newPosition: Cy.Position = this.commonGraphUtils.getNodePosition(node);
+ node.data().componentInstance.updatePosition(newPosition.x, newPosition.y);
+ instancesToUpdate.push(node.data().componentInstance);
+
+ });
+
+ if (instancesToUpdate.length > 0) {
+ this.generalGraphUtils.pushMultipleUpdateComponentInstancesRequestToQueue(instancesToUpdate);
+ }
+ } else {
+ // reset nodes position
+ nodesMoved.positions((i, node) => {
+ return {
+ x: +node.data().componentInstance.posX,
+ y: +node.data().componentInstance.posY
+ };
+ });
+ }
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-palette-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-palette-utils.ts
new file mode 100644
index 0000000000..1776c2f9b9
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-palette-utils.ts
@@ -0,0 +1,233 @@
+/*-
+ * ============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 {Injectable} from "@angular/core";
+import {CompositionGraphGeneralUtils, RequirementAndCapabilities} from "./composition-graph-general-utils";
+import {CommonGraphUtils} from "../common/common-graph-utils";
+import {EventListenerService} from "../../../../../services/event-listener-service";
+import {ResourceNamePipe} from "app/ng2/pipes/resource-name.pipe";
+import {ComponentInstanceFactory} from "app/utils/component-instance-factory";
+import {GRAPH_EVENTS, GraphUIObjects} from "app/utils/constants";
+import {TopologyTemplateService} from "app/ng2/services/component-services/topology-template.service";
+import {DndDropEvent} from "ngx-drag-drop/ngx-drag-drop";
+import {SdcUiServices} from "onap-ui-angular"
+import { Component as TopologyTemplate, NodesFactory, CapabilitiesGroup, RequirementsGroup,
+ CompositionCiNodeBase, ComponentInstance, LeftPaletteComponent, Point } from "app/models";
+import {CompositionService} from "../../composition.service";
+import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service";
+import { QueueServiceUtils } from "app/ng2/utils/queue-service-utils";
+import {ComponentGenericResponse} from "../../../../services/responses/component-generic-response";
+import {MatchCapabilitiesRequirementsUtils} from "./match-capability-requierment-utils";
+import {CompositionGraphNodesUtils} from "./index";
+
+@Injectable()
+export class CompositionGraphPaletteUtils {
+
+ constructor(private generalGraphUtils:CompositionGraphGeneralUtils,
+ private nodesFactory:NodesFactory,
+ private commonGraphUtils:CommonGraphUtils,
+ private queueServiceUtils:QueueServiceUtils,
+ private eventListenerService:EventListenerService,
+ private topologyTemplateService: TopologyTemplateService,
+ private loaderService: SdcUiServices.LoaderService,
+ private compositionService: CompositionService,
+ private workspaceService: WorkspaceService,
+ private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils,
+ private nodesGraphUtils: CompositionGraphNodesUtils) {
+ }
+
+ /**
+ *
+ * @param Calculate matching nodes, highlight the matching nodes and fade the non matching nodes
+ * @param leftPaletteComponent
+ * @param _cy
+ * @returns void
+ * @private
+ */
+
+ public onComponentHoverIn = (leftPaletteComponent: LeftPaletteComponent, _cy: Cy.Instance) => {
+ const nodesData = this.nodesGraphUtils.getAllNodesData(_cy.nodes());
+ const nodesLinks = this.generalGraphUtils.getAllCompositionCiLinks(_cy);
+
+ if (this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.containsKey(leftPaletteComponent.uniqueId)) {
+ const reqAndCap: RequirementAndCapabilities = this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.getValue(leftPaletteComponent.uniqueId);
+ const filteredNodesData = this.matchCapabilitiesRequirementsUtils.findMatchingNodesToComponentInstance(
+ { uniqueId: leftPaletteComponent.uniqueId, requirements: reqAndCap.requirements, capabilities: reqAndCap.capabilities} as ComponentInstance, nodesData, nodesLinks);
+
+ this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, _cy);
+ this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, _cy);
+ } else {
+
+ this.topologyTemplateService.getCapabilitiesAndRequirements(leftPaletteComponent.componentType, leftPaletteComponent.uniqueId).subscribe((response: ComponentGenericResponse) => {
+ let reqAndCap: RequirementAndCapabilities = {
+ capabilities: response.capabilities,
+ requirements: response.requirements
+ }
+ this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.setValue(leftPaletteComponent.uniqueId, reqAndCap);
+ });
+ }
+ }
+
+ /**
+ * Calculate the dragged element (html element) position on canvas
+ * @param cy
+ * @param event
+ * @param position
+ * @returns {Cy.BoundingBox}
+ * @private
+ */
+ private _getNodeBBox(cy:Cy.Instance, event:DragEvent, position?:Cy.Position, eventPosition?: Point) {
+ let bbox = <Cy.BoundingBox>{};
+ if (!position) {
+ position = event ? this.commonGraphUtils.getCytoscapeNodePosition(cy, event) : eventPosition;
+ }
+ let cushionWidth:number = 40;
+ let cushionHeight:number = 40;
+
+ bbox.x1 = position.x - cushionWidth / 2;
+ bbox.y1 = position.y - cushionHeight / 2;
+ bbox.x2 = position.x + cushionWidth / 2;
+ bbox.y2 = position.y + cushionHeight / 2;
+ return bbox;
+ }
+
+ /**
+ * Create the component instance, update data from parent component in the left palette and notify on_insert_to_ucpe if component was dragg into ucpe
+ * @param cy
+ * @param fullComponent
+ * @param event
+ * @param component
+ */
+ private _createComponentInstanceOnGraphFromPaletteComponent(cy:Cy.Instance, fullComponent:LeftPaletteComponent, event:DragEvent) {
+
+ let componentInstanceToCreate:ComponentInstance = ComponentInstanceFactory.createComponentInstanceFromComponent(fullComponent);
+ let cytoscapePosition:Cy.Position = this.commonGraphUtils.getCytoscapeNodePosition(cy, event);
+ componentInstanceToCreate.posX = cytoscapePosition.x;
+ componentInstanceToCreate.posY = cytoscapePosition.y;
+
+ let onFailedCreatingInstance:(error:any) => void = (error:any) => {
+ this.loaderService.deactivate();
+ };
+
+ //on success - update node data
+ let onSuccessCreatingInstance = (createInstance:ComponentInstance):void => {
+
+ this.loaderService.deactivate();
+ this.compositionService.addComponentInstance(createInstance);
+ createInstance.name = ResourceNamePipe.getDisplayName(createInstance.name);
+ createInstance.requirements = new RequirementsGroup(createInstance.requirements);
+ createInstance.capabilities = new CapabilitiesGroup(createInstance.capabilities);
+ createInstance.componentVersion = fullComponent.version;
+ createInstance.icon = fullComponent.icon;
+ createInstance.setInstanceRC();
+
+ let newNode:CompositionCiNodeBase = this.nodesFactory.createNode(createInstance);
+ this.commonGraphUtils.addComponentInstanceNodeToGraph(cy, newNode);
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CREATE_COMPONENT_INSTANCE);
+ };
+
+ this.queueServiceUtils.addBlockingUIAction(() => {
+ let uniqueId = this.workspaceService.metadata.uniqueId;
+ let componentType = this.workspaceService.metadata.componentType;
+ this.topologyTemplateService.createComponentInstance(componentType, uniqueId, componentInstanceToCreate).subscribe(onSuccessCreatingInstance, onFailedCreatingInstance);
+
+ });
+ }
+ //
+ // /**
+ // * Thid function applay red/green background when component dragged from palette
+ // * @param cy
+ // * @param event
+ // * @param dragElement
+ // * @param dragComponent
+ // */
+ // public onComponentDrag(cy:Cy.Instance, event) {
+ // let draggedElement = document.getElementById("draggable_element");
+ // // event.dataTransfer.setDragImage(draggableElement, 0, 0);
+ // if (event.clientX < GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET || event.clientY < GraphUIObjects.DIAGRAM_HEADER_OFFSET) { //hovering over palette. Dont bother computing validity of drop
+ // draggedElement.className = 'invalid-drag';
+ // event.dataTransfer.setDragImage(draggedElement.cloneNode(true), 0, 0);
+ // return;
+ // }
+ //
+ // let offsetPosition = {
+ // x: event.clientX - GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET,
+ // y: event.clientY - GraphUIObjects.DIAGRAM_HEADER_OFFSET
+ // };
+ // let bbox = this._getNodeBBox(cy, event, offsetPosition);
+ //
+ // if (this.generalGraphUtils.isPaletteDropValid(cy, bbox)) {
+ // draggedElement.className = 'valid-drag';
+ // event.dataTransfer.setDragImage(draggedElement.cloneNode(true), 0, 0);
+ // // event.dataTransfer.setDragImage(draggedElement, 0, 0);
+ // // event.dataTransfer.setDragImage(draggedElement, 0, 0);
+ //
+ // } else {
+ // draggedElement.className = 'invalid-drag';
+ // event.dataTransfer.setDragImage(draggedElement.cloneNode(true), 0, 0);
+ // }
+ // }
+
+ public isDragValid(cy:Cy.Instance, position: Point):boolean {
+ if (position.x < GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET || position.y < GraphUIObjects.DIAGRAM_HEADER_OFFSET) { //hovering over palette. Dont bother computing validity of drop
+ return false;
+ }
+
+ let offsetPosition = {
+ x: position.x - GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET,
+ y: position.y - GraphUIObjects.DIAGRAM_HEADER_OFFSET
+ };
+ let bbox = this._getNodeBBox(cy, null, offsetPosition, position);
+
+ if (this.generalGraphUtils.isPaletteDropValid(cy, bbox)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ /**
+ * This function is called when after dropping node on canvas
+ * Check if the capability & requirements fulfilled and if not get from server
+ * @param cy
+ * @param dragEvent
+ * @param component
+ */
+ public addNodeFromPalette(cy:Cy.Instance, dragEvent:DndDropEvent) {
+ this.loaderService.activate();
+
+ let draggedComponent:LeftPaletteComponent = dragEvent.data;
+
+ if (this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.containsKey(draggedComponent.uniqueId)) {
+ let fullComponent = this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.getValue(draggedComponent.uniqueId);
+ draggedComponent.capabilities = fullComponent.capabilities;
+ draggedComponent.requirements = fullComponent.requirements;
+ this._createComponentInstanceOnGraphFromPaletteComponent(cy, draggedComponent, dragEvent.event);
+
+ } else {
+
+ this.topologyTemplateService.getFullComponent(draggedComponent.componentType, draggedComponent.uniqueId).subscribe((topologyTemplate:TopologyTemplate) => {
+ draggedComponent.capabilities = topologyTemplate.capabilities;
+ draggedComponent.requirements = topologyTemplate.requirements;
+ this._createComponentInstanceOnGraphFromPaletteComponent(cy, draggedComponent, dragEvent.event);
+ });
+ }
+ }
+}
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-service-path-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-service-path-utils.ts
new file mode 100644
index 0000000000..bc124fe9d1
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-service-path-utils.ts
@@ -0,0 +1,148 @@
+/*-
+ * ============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 * as _ from "lodash";
+import {CompositionGraphGeneralUtils} from "./composition-graph-general-utils";
+import {ServiceServiceNg2} from 'app/ng2/services/component-services/service.service';
+import {Service} from "app/models/components/service";
+import {ForwardingPath} from "app/models/forwarding-path";
+import {ForwardingPathLink} from "app/models/forwarding-path-link";
+import {ComponentRef, Injectable} from "@angular/core";
+import {CompositionCiServicePathLink} from "app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link";
+import {SdcUiServices} from "onap-ui-angular";
+import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils";
+import {ServicePathsListComponent} from "app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component";
+import {ButtonModel, ModalModel} from "app/models";
+import {ServicePathCreatorComponent} from "app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component";
+import {ModalService} from "app/ng2/services/modal.service";
+import {ModalComponent} from "app/ng2/components/ui/modal/modal.component";
+import {Select, Store} from "@ngxs/store";
+import {WorkspaceState} from "app/ng2/store/states/workspace.state";
+import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service";
+import {CompositionService} from "../../composition.service";
+import {CommonGraphUtils} from "../common/common-graph-utils";
+import {GRAPH_EVENTS} from "app/utils/constants";
+import {EventListenerService} from "app/services/event-listener-service";
+
+@Injectable()
+export class ServicePathGraphUtils {
+
+ constructor(
+ private generalGraphUtils: CompositionGraphGeneralUtils,
+ private serviceService: ServiceServiceNg2,
+ private commonGraphUtils: CommonGraphUtils,
+ private loaderService: SdcUiServices.LoaderService,
+ private queueServiceUtils: QueueServiceUtils,
+ private modalService: ModalService,
+ private workspaceService: WorkspaceService,
+ private compositionService: CompositionService,
+ private store:Store,
+ private eventListenerService: EventListenerService
+ ) {
+ }
+
+ private isViewOnly = (): boolean => {
+ return this.store.selectSnapshot(state => state.workspace.isViewOnly);
+ }
+ private modalInstance: ComponentRef<ModalComponent>;
+
+ public deletePathsFromGraph(cy: Cy.Instance) {
+ cy.remove(`[type="${CompositionCiServicePathLink.LINK_TYPE}"]`);
+ }
+
+ public drawPath(cy: Cy.Instance, forwardingPath: ForwardingPath) {
+ let pathElements = forwardingPath.pathElements.listToscaDataDefinition;
+
+ _.forEach(pathElements, (link: ForwardingPathLink) => {
+ let data: CompositionCiServicePathLink = new CompositionCiServicePathLink(link);
+ data.source = _.find(
+ this.compositionService.componentInstances,
+ instance => instance.name === data.forwardingPathLink.fromNode
+ ).uniqueId;
+ data.target = _.find(
+ this.compositionService.componentInstances,
+ instance => instance.name === data.forwardingPathLink.toNode
+ ).uniqueId;
+ data.pathId = forwardingPath.uniqueId;
+ data.pathName = forwardingPath.name;
+ this.commonGraphUtils.insertServicePathLinkToGraph(cy, data);
+ });
+ }
+
+ public createOrUpdateServicePath = (path: any): void => {
+ this.loaderService.activate();
+
+ let onSuccess: (response: ForwardingPath) => void = (response: ForwardingPath) => {
+ this.loaderService.deactivate();
+ this.compositionService.forwardingPaths[response.uniqueId] = response;
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_SERVICE_PATH_CREATED, response.uniqueId)
+ };
+
+ this.queueServiceUtils.addBlockingUIAction(
+ () => this.serviceService.createOrUpdateServicePath(this.workspaceService.metadata.uniqueId, path).subscribe(onSuccess
+ , (error) => {this.loaderService.deactivate()})
+ );
+ };
+
+ public onCreateServicePath = (): void => {
+ // this.showServicePathMenu = false;
+ let cancelButton: ButtonModel = new ButtonModel('Cancel', 'outline white', this.modalService.closeCurrentModal);
+ let saveButton: ButtonModel = new ButtonModel('Create', 'blue', this.createPath, this.getDisabled);
+ let modalModel: ModalModel = new ModalModel('l', 'Create Service Flow', '', [saveButton, cancelButton], 'standard', true);
+ this.modalInstance = this.modalService.createCustomModal(modalModel);
+ this.modalService.addDynamicContentToModal(this.modalInstance, ServicePathCreatorComponent, {serviceId: this.workspaceService.metadata.uniqueId});
+ this.modalInstance.instance.open();
+ };
+
+ public onListServicePath = (): void => {
+ // this.showServicePathMenu = false;
+ let cancelButton: ButtonModel = new ButtonModel('Close', 'outline white', this.modalService.closeCurrentModal);
+ let modalModel: ModalModel = new ModalModel('md', 'Service Flows List', '', [cancelButton], 'standard', true);
+ this.modalInstance = this.modalService.createCustomModal(modalModel);
+ this.modalService.addDynamicContentToModal(this.modalInstance, ServicePathsListComponent, {
+ serviceId: this.workspaceService.metadata.uniqueId,
+ onCreateServicePath: this.onCreateServicePath,
+ onEditServicePath: this.onEditServicePath,
+ isViewOnly: this.isViewOnly()
+ });
+ this.modalInstance.instance.open();
+ };
+
+ public onEditServicePath = (id: string): void => {
+ let cancelButton: ButtonModel = new ButtonModel('Cancel', 'outline white', this.modalService.closeCurrentModal);
+ let saveButton: ButtonModel = new ButtonModel('Save', 'blue', this.createPath, this.getDisabled);
+ let modalModel: ModalModel = new ModalModel('l', 'Edit Path', '', [saveButton, cancelButton], 'standard', true);
+ this.modalInstance = this.modalService.createCustomModal(modalModel);
+ this.modalService.addDynamicContentToModal(this.modalInstance, ServicePathCreatorComponent, {
+ serviceId: this.workspaceService.metadata.uniqueId,
+ pathId: id
+ });
+ this.modalInstance.instance.open();
+ };
+
+ public getDisabled = (): boolean => {
+ return this.isViewOnly() || !this.modalInstance.instance.dynamicContent.instance.checkFormValidForSubmit();
+ };
+
+ public createPath = (): void => {
+ this.createOrUpdateServicePath(this.modalInstance.instance.dynamicContent.instance.createServicePathData());
+ this.modalService.closeCurrentModal();
+ };
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-zone-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-zone-utils.ts
new file mode 100644
index 0000000000..9e97ec0f00
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-zone-utils.ts
@@ -0,0 +1,204 @@
+import {
+ Point,
+ PolicyInstance,
+ Zone,
+ LeftPaletteMetadataTypes,
+ ZoneInstance,
+ ZoneInstanceType,
+ ZoneInstanceAssignmentType
+} from "app/models";
+import {CanvasHandleTypes} from "app/utils";
+import {Observable} from "rxjs";
+import {GroupInstance} from "app/models/graph/zones/group-instance";
+import {Injectable} from "@angular/core";
+import {DynamicComponentService} from "app/ng2/services/dynamic-component.service";
+import {PoliciesService} from "app/ng2/services/policies.service";
+import {GroupsService} from "app/ng2/services/groups.service";
+import {Store} from "@ngxs/store";
+import {CompositionService} from "../../composition.service";
+import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service";
+import { PaletteAnimationComponent } from "app/ng2/pages/composition/palette/palette-animation/palette-animation.component";
+
+@Injectable()
+export class CompositionGraphZoneUtils {
+
+ constructor(private dynamicComponentService: DynamicComponentService,
+ private policiesService: PoliciesService,
+ private groupsService: GroupsService,
+ private workspaceService: WorkspaceService,
+ private compositionService: CompositionService) {
+ }
+
+
+ public createCompositionZones = (): Array<Zone> => {
+ let zones: Array<Zone> = [];
+
+ zones[ZoneInstanceType.POLICY] = new Zone('Policies', 'P', ZoneInstanceType.POLICY);
+ zones[ZoneInstanceType.GROUP] = new Zone('Groups', 'G', ZoneInstanceType.GROUP);
+
+ return zones;
+ }
+
+ public showZone = (zone: Zone): void => {
+ zone.visible = true;
+ zone.minimized = false;
+ }
+
+ public getZoneTypeForPaletteComponent = (componentCategory: LeftPaletteMetadataTypes) => {
+ if (componentCategory == LeftPaletteMetadataTypes.Group) {
+ return ZoneInstanceType.GROUP;
+ } else if (componentCategory == LeftPaletteMetadataTypes.Policy) {
+ return ZoneInstanceType.POLICY;
+ }
+ };
+
+ public initZoneInstances(zones: Array<Zone>) {
+
+ if (this.compositionService.groupInstances && this.compositionService.groupInstances.length) {
+ this.showZone(zones[ZoneInstanceType.GROUP]);
+ zones[ZoneInstanceType.GROUP].instances = [];
+ _.forEach(this.compositionService.groupInstances, (group: GroupInstance) => {
+ let newInstance = new ZoneInstance(group, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId);
+ this.addInstanceToZone(zones[ZoneInstanceType.GROUP], newInstance);
+ });
+ }
+
+ if (this.compositionService.policies && this.compositionService.policies.length) {
+ this.showZone(zones[ZoneInstanceType.POLICY]);
+ zones[ZoneInstanceType.POLICY].instances = [];
+ _.forEach(this.compositionService.policies, (policy: PolicyInstance) => {
+ let newInstance = new ZoneInstance(policy, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId);
+ this.addInstanceToZone(zones[ZoneInstanceType.POLICY], newInstance);
+
+ });
+ }
+ }
+
+ public findAndUpdateZoneInstanceData(zones: Array<Zone>, instanceData: PolicyInstance | GroupInstance) {
+ _.forEach(zones, (zone: Zone) => {
+ _.forEach(zone.instances, (zoneInstance: ZoneInstance) => {
+ if (zoneInstance.instanceData.uniqueId === instanceData.uniqueId) {
+ zoneInstance.updateInstanceData(instanceData);
+ }
+ });
+ });
+ }
+
+ public updateTargetsOrMembersOnCanvasDelete = (canvasNodeID: string, zones: Array<Zone>, type: ZoneInstanceAssignmentType): void => {
+ _.forEach(zones, (zone) => {
+ _.forEach(zone.instances, (zoneInstance: ZoneInstance) => {
+ if (zoneInstance.isAlreadyAssigned(canvasNodeID)) {
+ zoneInstance.addOrRemoveAssignment(canvasNodeID, type);
+ //remove it from our list of BE targets and members as well (so that it will not be sent in future calls to BE).
+ zoneInstance.instanceData.setSavedAssignments(zoneInstance.assignments);
+ }
+ });
+ });
+ };
+
+ public createZoneInstanceFromLeftPalette = (zoneType: ZoneInstanceType, paletteComponentType: string): Observable<ZoneInstance> => {
+
+ if (zoneType === ZoneInstanceType.POLICY) {
+ return this.policiesService.createPolicyInstance(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, paletteComponentType).map(response => {
+ let newInstance = new PolicyInstance(response);
+ this.compositionService.addPolicyInstance(newInstance);
+ return new ZoneInstance(newInstance, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId);
+ });
+ } else if (zoneType === ZoneInstanceType.GROUP) {
+ return this.groupsService.createGroupInstance(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, paletteComponentType).map(response => {
+ let newInstance = new GroupInstance(response);
+ this.compositionService.addGroupInstance(newInstance);
+ return new ZoneInstance(newInstance, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId);
+ });
+ }
+ }
+
+ public addInstanceToZone(zone: Zone, instance: ZoneInstance, hide?: boolean) {
+ if (hide) {
+ instance.hidden = true;
+ }
+ zone.instances.push(instance);
+
+ };
+
+ private findZoneCoordinates(zoneType): Point {
+ let point: Point = new Point(0, 0);
+ let zone = angular.element(document.querySelector('.' + zoneType + '-zone'));
+ let wrapperZone = zone.offsetParent();
+ point.x = zone.prop('offsetLeft') + wrapperZone.prop('offsetLeft');
+ point.y = zone.prop('offsetTop') + wrapperZone.prop('offsetTop');
+ return point;
+ }
+
+ public createPaletteToZoneAnimation = (startPoint: Point, zoneType: ZoneInstanceType, newInstance: ZoneInstance) => {
+ let zoneTypeName = ZoneInstanceType[zoneType].toLowerCase();
+ let paletteToZoneAnimation = this.dynamicComponentService.createDynamicComponent(PaletteAnimationComponent);
+ paletteToZoneAnimation.instance.from = startPoint;
+ paletteToZoneAnimation.instance.type = zoneType;
+ paletteToZoneAnimation.instance.to = this.findZoneCoordinates(zoneTypeName);
+ paletteToZoneAnimation.instance.zoneInstance = newInstance;
+ paletteToZoneAnimation.instance.iconName = zoneTypeName;
+ paletteToZoneAnimation.instance.runAnimation();
+ }
+
+ public startCyTagMode = (cy: Cy.Instance) => {
+ cy.autolock(true);
+ cy.nodes().unselectify();
+ cy.emit('tagstart'); //dont need to show handles because they're already visible bcz of hover event
+
+ };
+
+ public endCyTagMode = (cy: Cy.Instance) => {
+ cy.emit('tagend');
+ cy.nodes().selectify();
+ cy.autolock(false);
+ };
+
+ public handleTagClick = (cy: Cy.Instance, zoneInstance: ZoneInstance, nodeId: string) => {
+ zoneInstance.addOrRemoveAssignment(nodeId, ZoneInstanceAssignmentType.COMPONENT_INSTANCES);
+ this.showZoneTagIndicationForNode(nodeId, zoneInstance, cy);
+ };
+
+ public showGroupZoneIndications = (groupInstances: Array<ZoneInstance>, policyInstance: ZoneInstance) => {
+ groupInstances.forEach((groupInstance: ZoneInstance) => {
+ let handle: string = this.getCorrectHandleForNode(groupInstance.instanceData.uniqueId, policyInstance);
+ groupInstance.showHandle(handle);
+ })
+ };
+
+ public hideGroupZoneIndications = (instances: Array<ZoneInstance>) => {
+ instances.forEach((instance) => {
+ instance.hideHandle();
+ })
+ }
+
+ public showZoneTagIndications = (cy: Cy.Instance, zoneInstance: ZoneInstance) => {
+
+ cy.nodes().forEach(node => {
+ let handleType: string = this.getCorrectHandleForNode(node.id(), zoneInstance);
+ cy.emit('showhandle', [node, handleType]);
+ });
+ };
+
+ public showZoneTagIndicationForNode = (nodeId: string, zoneInstance: ZoneInstance, cy: Cy.Instance) => {
+ let node = cy.getElementById(nodeId);
+ let handleType: string = this.getCorrectHandleForNode(nodeId, zoneInstance);
+ cy.emit('showhandle', [node, handleType]);
+ }
+
+ public hideZoneTagIndications = (cy: Cy.Instance) => {
+ cy.emit('hidehandles');
+ };
+
+ public getCorrectHandleForNode = (nodeId: string, zoneInstance: ZoneInstance): string => {
+ if (zoneInstance.isAlreadyAssigned(nodeId)) {
+ if (zoneInstance.type == ZoneInstanceType.POLICY) {
+ return CanvasHandleTypes.TAGGED_POLICY;
+ } else {
+ return CanvasHandleTypes.TAGGED_GROUP;
+ }
+ } else {
+ return CanvasHandleTypes.TAG_AVAILABLE;
+ }
+ };
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/index.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/index.ts
new file mode 100644
index 0000000000..e7f11af248
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/index.ts
@@ -0,0 +1,29 @@
+/**
+ * Created by ob0695 on 6/3/2018.
+ */
+// export * from './composition-graph-general-utils';
+// export * from './composition-graph-links-utils';
+// export * from './composition-graph-nodes-utils';
+// export * from './composition-graph-palette-utils';
+// export * from './composition-graph-service-path-utils';
+// export * from './composition-graph-zone-utils';
+
+
+import {CompositionGraphGeneralUtils} from './composition-graph-general-utils';
+import {CompositionGraphNodesUtils} from './composition-graph-nodes-utils';
+import {MatchCapabilitiesRequirementsUtils} from './match-capability-requierment-utils'
+import {CompositionGraphPaletteUtils} from './composition-graph-palette-utils';
+import {CompositionGraphZoneUtils} from './composition-graph-zone-utils';
+import {ServicePathGraphUtils} from './composition-graph-service-path-utils';
+import {CompositionGraphLinkUtils} from "./composition-graph-links-utils";
+
+
+export {
+ CompositionGraphGeneralUtils,
+ CompositionGraphLinkUtils,
+ CompositionGraphNodesUtils,
+ MatchCapabilitiesRequirementsUtils,
+ CompositionGraphPaletteUtils,
+ CompositionGraphZoneUtils,
+ ServicePathGraphUtils
+}; \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.spec.ts
new file mode 100644
index 0000000000..dbfc3e7219
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.spec.ts
@@ -0,0 +1,342 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { Mock } from 'ts-mockery';
+import {
+ CapabilitiesGroup,
+ Capability, ComponentInstance, CompositionCiLinkBase, CompositionCiNodeBase, CompositionCiNodeCp,
+ CompositionCiNodeVf, CompositionCiNodeVl,
+ Requirement, RequirementsGroup
+} from '../../../../../models';
+import { MatchCapabilitiesRequirementsUtils } from './match-capability-requierment-utils';
+
+describe('match capability requirements utils service ', () => {
+
+ const bindableReq = Mock.of<Requirement>({
+ capability : 'tosca.capabilities.network.Bindable',
+ name: 'virtualBinding',
+ relationship: 'tosca.relationships.network.BindsTo',
+ uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.virtualBinding',
+ ownerId : 'extcp0',
+ ownerName : 's'
+ });
+
+ const virtualLinkReq = Mock.of<Requirement>({
+ capability: 'tosca.capabilities.network.Linkable',
+ name: 'virtualLink',
+ relationship: 'tosca.relationships.network.LinksTo',
+ uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.virtualLink',
+ ownerId : '',
+ ownerName : 's'
+ });
+
+ const storeAttachmentReq = Mock.of<Requirement>({
+ capability: 'tosca.capabilities.Attachment',
+ name: 'local_storage',
+ relationship: 'tosca.relationships.AttachesTo',
+ uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.local_storage',
+ node: 'tosca.nodes.BlockStorage',
+ ownerId : '',
+ ownerName : 's'
+ });
+
+ const vlAttachmentReq = Mock.of<Requirement>({
+ capability: 'tosca.capabilities.Attachment',
+ name: 'local_storage',
+ relationship: 'tosca.relationships.AttachesTo',
+ uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.local_storage',
+ node: 'tosca.nodes.BlockStorage',
+ ownerId : '',
+ ownerName : 's'
+ });
+
+ const extVirtualLinkReq = Mock.of<Requirement>({
+ capability: 'tosca.capabilities.network.Linkable',
+ name: 'external_virtualLink',
+ relationship: 'tosca.relationships.network.LinksTo',
+ uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.external_virtualLink'
+ });
+
+ const dependencyReq = Mock.of<Requirement>({
+ capability: 'tosca.capabilities.Node',
+ name: 'dependency',
+ relationship: 'tosca.relationships.DependsOn',
+ uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.dependency'
+ });
+
+ const featureCap = Mock.of<Capability>({
+ type: 'tosca.capabilities.Node',
+ name: 'feature',
+ uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.feature',
+ maxOccurrences: 'UNBOUNDED',
+ minOccurrences: '1'
+ });
+
+ const internalConnPointCap = Mock.of<Capability>({
+ type: 'tosca.capabilities.Node',
+ name: 'internal_connectionPoint',
+ capabilitySources : ['org.openecomp.resource.cp.extCP'],
+ uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.internal_connectionPoint',
+ maxOccurrences: 'UNBOUNDED',
+ minOccurrences: '1'
+ });
+
+ const blockStoreAttachmentCap = Mock.of<Capability>({
+ type: 'tosca.capabilities.Attachment',
+ name: 'attachment',
+ capabilitySources: ['tosca.nodes.BlockStorage'],
+ uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.attachment',
+ maxOccurrences: 'UNBOUNDED',
+ minOccurrences: '1'
+ });
+
+ const bindingCap = Mock.of<Capability>({
+ type: 'tosca.capabilities.network.Bindable',
+ name: 'binding',
+ capabilitySources: ['tosca.nodes.Compute'],
+ uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.binding',
+ maxOccurrences: 'UNBOUNDED',
+ minOccurrences: '1',
+ });
+
+ const linkableCap = Mock.of<Capability>({
+ type: 'tosca.capabilities.network.Linkable',
+ capabilitySources: ['org.openecomp.resource.vl.extVL'],
+ uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.virtual_linkable',
+ maxOccurrences: 'UNBOUNDED',
+ minOccurrences: '1'
+ });
+
+ const nodeCompute = Mock.of<CompositionCiNodeVf>({
+ name: 'Compute 0',
+ componentInstance: Mock.of<ComponentInstance>({
+ componentName: 'Compute',
+ uniqueId : 'compute0',
+ requirements: Mock.of<RequirementsGroup>({
+ 'tosca.capabilities.Node' : [ dependencyReq ],
+ 'tosca.capabilities.Attachment' : [ storeAttachmentReq ]
+ }),
+ capabilities: Mock.of<CapabilitiesGroup>({
+ 'tosca.capabilities.network.Bindable' : [ bindingCap ],
+ 'tosca.capabilities.Node' : [ featureCap ]
+ })
+ })
+ });
+
+ const nodeBlockStorage = Mock.of<CompositionCiNodeVf>({
+ name: 'BlockStorage 0',
+ componentInstance: Mock.of<ComponentInstance>({
+ componentName: 'BlockStorage',
+ uniqueId : 'blockstorage0',
+ requirements: Mock.of<RequirementsGroup>({
+ 'tosca.capabilities.Node' : [ dependencyReq ]
+ }),
+ capabilities: Mock.of<CapabilitiesGroup>({
+ 'tosca.capabilities.Attachment' : [ blockStoreAttachmentCap ],
+ 'tosca.capabilities.Node' : [ featureCap ]
+ })
+ })
+ });
+
+ const nodeVl = Mock.of<CompositionCiNodeVl>({
+ name: 'ExtVL 0',
+ componentInstance: Mock.of<ComponentInstance>({
+ componentName: 'BlockStorage',
+ uniqueId : 'extvl0',
+ requirements: Mock.of<RequirementsGroup>({
+ 'tosca.capabilities.Node' : [ dependencyReq ]
+ }),
+ capabilities: Mock.of<CapabilitiesGroup>({
+ 'tosca.capabilities.network.Linkable' : [ linkableCap ],
+ 'tosca.capabilities.Node' : [ featureCap ]
+ })
+ })
+ });
+
+ const nodeCp = Mock.of<CompositionCiNodeCp>({
+ name: 'ExtCP 0',
+ componentInstance: Mock.of<ComponentInstance>({
+ componentName: 'ExtCP',
+ uniqueId : 'extcp0',
+ requirements: Mock.of<RequirementsGroup>({
+ 'tosca.capabilities.network.Linkable' : [ virtualLinkReq ],
+ 'tosca.capabilities.network.Bindable' : [ bindableReq ]
+ }),
+ capabilities: Mock.of<CapabilitiesGroup>({
+ 'tosca.capabilities.Node' : [ featureCap ]
+ })
+ })
+ });
+
+ let service: MatchCapabilitiesRequirementsUtils;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [],
+ providers: [MatchCapabilitiesRequirementsUtils]
+ });
+
+ service = TestBed.get(MatchCapabilitiesRequirementsUtils);
+ });
+
+ it('match capability requirements utils should be defined', () => {
+ console.log(JSON.stringify(service));
+ expect(service).toBeDefined();
+ });
+
+ describe('isMatch function ', () => {
+
+ it('capability type not equal to requirement capability, match is false', () => {
+ const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable11'});
+ const capability = Mock.of<Capability>({type: 'tosca.capabilities.network.Linkable'});
+ expect(service.isMatch(requirement, capability)).toBeFalsy();
+ });
+
+ it('capability type equal to requirement capability and requirement node not exist, match is true', () => {
+ const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable'});
+ const capability = Mock.of<Capability>({type: 'tosca.capabilities.network.Linkable'});
+ expect(service.isMatch(requirement, capability)).toBeTruthy();
+ });
+
+ it('is match - capability type equal to requirement capability and requirement node exist and includes in capability sources, match is true', () => {
+ const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable', node: 'node1'});
+ const capability = Mock.of<Capability>({
+ type: 'tosca.capabilities.network.Linkable',
+ capabilitySources: ['node1', 'node2', 'node3']
+ });
+ expect(service.isMatch(requirement, capability)).toBeTruthy();
+ });
+
+ it('no match - capability type equal to requirement capability and requirement node but not includes in capability sources, match is false', () => {
+ const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable', node: 'node4'});
+ const capability = Mock.of<Capability>({
+ type: 'tosca.capabilities.network.Linkable',
+ capabilitySources: ['node1', 'node2', 'node3']
+ });
+ expect(service.isMatch(requirement, capability)).toBeFalsy();
+ });
+ });
+
+ describe('hasUnfulfilledRequirementContainingMatch function ', () => {
+
+ it('node have no componentInstance, return false', () => {
+ const node = Mock.of<CompositionCiNodeVf>({componentInstance: undefined});
+ expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeFalsy();
+ });
+
+ it('node have componentInstance data but no unfulfilled requirements, return false', () => {
+ const node = Mock.of<CompositionCiNodeVf>({componentInstance: Mock.of<ComponentInstance>()});
+ jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([]);
+ expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeFalsy();
+ });
+
+ it('node have componentInstance data and unfulfilled requirements but no match found, return false', () => {
+ const node = Mock.of<CompositionCiNodeVf>({componentInstance: Mock.of<ComponentInstance>()});
+ jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]);
+ jest.spyOn(service, 'containsMatch').mockReturnValue(false);
+ expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeFalsy();
+ });
+
+ it('node have componentInstance data with unfulfilled requirements and match found, return true', () => {
+ const node = Mock.of<CompositionCiNodeVf>({componentInstance: Mock.of<ComponentInstance>()});
+ jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]);
+ jest.spyOn(service, 'containsMatch').mockReturnValue(true);
+ expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeTruthy();
+ });
+ });
+
+ describe('getMatches function ', () => {
+ let fromId: string;
+ let toId: string;
+
+ beforeEach(() => {
+ fromId = 'from_id';
+ toId = 'to_id';
+ });
+
+ it('node have no unfulfilled requirements, return empty match array', () => {
+ jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([]);
+ expect(service.getMatches({}, {}, [], fromId, toId, true)).toHaveLength(0);
+ });
+
+ it('node have unfulfilled requirements but no capabilities, return empty match array', () => {
+ jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]);
+ expect(service.getMatches({}, {}, [], fromId, toId, true)).toHaveLength(0);
+ });
+
+ it('node have unfulfilled requirements and capabilities but no match found, return empty match array', () => {
+ jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]);
+ jest.spyOn(service, 'isMatch').mockReturnValue(false);
+ expect(service.getMatches({}, {}, [], fromId, toId, true)).toHaveLength(0);
+ });
+
+ it('node have 2 unfulfilled requirements and 2 capabilities and match found, return 4 matches', () => {
+ jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]);
+ const capabilities = {aaa: Mock.of<Capability>(), bbb: Mock.of<Capability>()};
+ jest.spyOn(service, 'isMatch').mockReturnValue(true);
+ expect(service.getMatches({}, capabilities, [], fromId, toId, true)).toHaveLength(4);
+ });
+ });
+
+ describe('Find matching nodes ===>', () => {
+
+ it('should find matching nodes with component instance', () => {
+ const nodes = [ nodeBlockStorage, nodeCompute, nodeVl ];
+ let matchingNodes: any;
+
+ // Compute can connect to Block Store
+ matchingNodes = service.findMatchingNodesToComponentInstance(nodeCompute.componentInstance, nodes, []);
+ expect(matchingNodes).toHaveLength(1);
+ expect(matchingNodes).toContain(nodeBlockStorage);
+
+ // Block Storage can connect to Compute
+ matchingNodes = service.findMatchingNodesToComponentInstance(nodeBlockStorage.componentInstance, nodes, []);
+ expect(matchingNodes).toHaveLength(1);
+ expect(matchingNodes).toContain(nodeCompute);
+
+ // Vl has no matches
+ matchingNodes = service.findMatchingNodesToComponentInstance(nodeVl.componentInstance, nodes, []);
+ expect(matchingNodes).toHaveLength(0);
+
+ // CP should be able to connect to VL and Compute
+ matchingNodes = service.findMatchingNodesToComponentInstance(nodeCp.componentInstance, nodes, []);
+ expect(matchingNodes).toHaveLength(2);
+ expect(matchingNodes).toContain(nodeCompute);
+ expect(matchingNodes).toContain(nodeVl);
+ });
+
+ it('try with empty list of nodes', () => {
+ const nodes = [ ];
+ let matchingNodes: any;
+
+ // Compute can connect to Block Store
+ matchingNodes = service.findMatchingNodesToComponentInstance(nodeCompute.componentInstance, nodes, []);
+ expect(matchingNodes).toHaveLength(0);
+ });
+
+ it('should detect fulfilled connection with compute node', () => {
+ const nodes = [ nodeBlockStorage, nodeCompute, nodeVl ];
+ let matchingNodes: any;
+ const link = {
+ relation: {
+ fromNode: 'extcp0',
+ toNode: 'compute0',
+ relationships: [{
+ relation: {
+ requirementOwnerId: 'extcp0',
+ requirement: 'virtualBinding',
+ relationship: {
+ type: 'tosca.relationships.network.BindsTo'
+ }
+
+ }
+ }]
+ }
+ };
+
+ const links = [link];
+ // CP should be able to connect to VL only since it already has a link with compute
+ matchingNodes = service.findMatchingNodesToComponentInstance(nodeCp.componentInstance, nodes, links as CompositionCiLinkBase[]);
+ expect(matchingNodes).toHaveLength(1);
+ expect(matchingNodes).toContain(nodeVl);
+ });
+ });
+});
diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.ts
new file mode 100644
index 0000000000..c3a1286a97
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.ts
@@ -0,0 +1,196 @@
+/*-
+ * ============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 { Injectable } from '@angular/core';
+import {
+ CapabilitiesGroup, Capability, ComponentInstance, CompositionCiLinkBase,
+ CompositionCiNodeBase, Match, Requirement, RequirementsGroup
+} from 'app/models';
+import * as _ from 'lodash';
+
+/**
+ * Created by obarda on 1/1/2017.
+ */
+@Injectable()
+export class MatchCapabilitiesRequirementsUtils {
+
+ /**
+ * Shows + icon in corner of each node passed in
+ * @param filteredNodesData
+ * @param cy
+ */
+ public highlightMatchingComponents(filteredNodesData, cy: Cy.Instance) {
+ _.each(filteredNodesData, (data: any) => {
+ const node = cy.getElementById(data.id);
+ cy.emit('showhandle', [node]);
+ });
+ }
+
+ /**
+ * Adds opacity to each node that cannot be linked to hovered node
+ * @param filteredNodesData
+ * @param nodesData
+ * @param cy
+ * @param hoveredNodeData
+ */
+ public fadeNonMachingComponents(filteredNodesData, nodesData, cy: Cy.Instance, hoveredNodeData?) {
+ const fadeNodes = _.xorWith(nodesData, filteredNodesData, (node1, node2) => {
+ return node1.id === node2.id;
+ });
+ if (hoveredNodeData) {
+ _.remove(fadeNodes, hoveredNodeData);
+ }
+ cy.batch(() => {
+ _.each(fadeNodes, (node) => {
+ cy.getElementById(node.id).style({'background-image-opacity': 0.4});
+ });
+ });
+ }
+
+ /**
+ * Resets all nodes to regular opacity
+ * @param cy
+ */
+ public resetFadedNodes(cy: Cy.Instance) {
+ cy.batch(() => {
+ cy.nodes().style({'background-image-opacity': 1});
+ });
+ }
+
+ public getMatchedRequirementsCapabilities(fromComponentInstance: ComponentInstance,
+ toComponentInstance: ComponentInstance,
+ links: CompositionCiLinkBase[]): Match[] {
+ const fromToMatches: Match[] = this.getMatches(fromComponentInstance.requirements,
+ toComponentInstance.capabilities,
+ links,
+ fromComponentInstance.uniqueId,
+ toComponentInstance.uniqueId, true);
+ const toFromMatches: Match[] = this.getMatches(toComponentInstance.requirements,
+ fromComponentInstance.capabilities,
+ links,
+ toComponentInstance.uniqueId,
+ fromComponentInstance.uniqueId, false);
+
+ return fromToMatches.concat(toFromMatches);
+ }
+
+ /***** REFACTORED FUNCTIONS START HERE *****/
+
+ public getMatches(requirements: RequirementsGroup, capabilities: CapabilitiesGroup, links: CompositionCiLinkBase[],
+ fromId: string, toId: string, isFromTo: boolean): Match[] {
+ const matches: Match[] = [];
+ const unfulfilledReqs = this.getUnfulfilledRequirements(fromId, requirements, links);
+ _.forEach(unfulfilledReqs, (req) => {
+ _.forEach(_.flatten(_.values(capabilities)), (capability: Capability) => {
+ if (this.isMatch(req, capability)) {
+ if (isFromTo) {
+ matches.push(new Match(req, capability, isFromTo, fromId, toId));
+ } else {
+ matches.push(new Match(req, capability, isFromTo, toId, fromId));
+ }
+ }
+ });
+ });
+ return matches;
+ }
+
+ public getUnfulfilledRequirements = (fromNodeId: string, requirements: RequirementsGroup, links: CompositionCiLinkBase[]): Requirement[] => {
+ const requirementArray: Requirement[] = [];
+ _.forEach(_.flatten(_.values(requirements)), (requirement: Requirement) => {
+ const reqFulfilled = this.isRequirementFulfilled(fromNodeId, requirement, links);
+ if (requirement.name !== 'dependency' && requirement.parentName !== 'dependency' && !reqFulfilled) {
+ requirementArray.push(requirement);
+ }
+ });
+ return requirementArray;
+ }
+
+ /**
+ * Returns true if there is a match between the capabilities and requirements that are passed in
+ * @param requirements
+ * @param capabilities
+ * @returns {boolean}
+ */
+ public containsMatch = (requirements: Requirement[], capabilities: CapabilitiesGroup): boolean => {
+ return _.some(requirements, (req: Requirement) => {
+ return _.some(_.flatten(_.values(capabilities)), (capability: Capability) => {
+ return this.isMatch(req, capability);
+ });
+ });
+ }
+
+ public hasUnfulfilledRequirementContainingMatch = (node: CompositionCiNodeBase, componentRequirements: Requirement[], capabilities: CapabilitiesGroup, links: CompositionCiLinkBase[]) => {
+ if (node && node.componentInstance) {
+ // Check if node has unfulfilled requirement that can be filled by component (#2)
+ const nodeRequirements: Requirement[] = this.getUnfulfilledRequirements(node.componentInstance.uniqueId, node.componentInstance.requirements, links);
+ if (!nodeRequirements.length) {
+ return false;
+ }
+ if (this.containsMatch(nodeRequirements, capabilities)) {
+ return true;
+ }
+ }
+ }
+
+ /**
+ * Returns array of nodes that can connect to the component.
+ * In order to connect, one of the following conditions must be met:
+ * 1. component has an unfulfilled requirement that matches a node's capabilities
+ * 2. node has an unfulfilled requirement that matches the component's capabilities
+ * 3. vl is passed in which has the capability to fulfill requirement from component and requirement on node.
+ */
+ public findMatchingNodesToComponentInstance(componentInstance: ComponentInstance, nodeDataArray: CompositionCiNodeBase[], links: CompositionCiLinkBase[]): any[] {
+ return _.filter(nodeDataArray, (node: CompositionCiNodeBase) => {
+ const matchedRequirementsCapabilities = this.getMatchedRequirementsCapabilities(node.componentInstance, componentInstance, links);
+ return matchedRequirementsCapabilities && matchedRequirementsCapabilities.length > 0;
+ });
+ }
+
+ public isMatch(requirement: Requirement, capability: Capability): boolean {
+ if (capability.type === requirement.capability) {
+ if (requirement.node) {
+ if (_.includes(capability.capabilitySources, requirement.node)) {
+ return true;
+ }
+ } else {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private isRequirementFulfilled(fromNodeId: string, requirement: any, links: CompositionCiLinkBase[]): boolean {
+ return _.some(links, {
+ relation: {
+ fromNode: fromNodeId,
+ relationships: [{
+ relation: {
+ requirementOwnerId: requirement.ownerId,
+ requirement: requirement.name,
+ relationship: {
+ type: requirement.relationship
+ }
+
+ }
+ }]
+ }
+ });
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/__snapshots__/palette.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/palette/__snapshots__/palette.component.spec.ts.snap
new file mode 100644
index 0000000000..74517e1eb0
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/__snapshots__/palette.component.spec.ts.snap
@@ -0,0 +1,51 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`palette component should match current snapshot of palette component 1`] = `
+<composition-palette
+ buildPaletteByCategories={[Function Function]}
+ compositionPaletteService={[Function Object]}
+ eventListenerService={[Function Object]}
+ numberOfElements="0"
+ onDragStart={[Function Function]}
+ onDraggableMoved={[Function Function]}
+ onDrop={[Function Function]}
+ onMouseOut={[Function Function]}
+ onMouseOver={[Function Function]}
+ onSearchChanged={[Function Function]}
+ position={[Function Point]}
+>
+ <div
+ class="composition-palette-component"
+ >
+ <div
+ class="palette-elements-count"
+ >
+ Elements
+ <span
+ class="palette-elements-count-value"
+ >
+
+ </span>
+ </div>
+ <sdc-filter-bar
+ placeholder="Search..."
+ testid="searchAsset"
+ />
+ <div
+ class="palette-elements-list"
+ >
+ <sdc-loader
+ name="palette-loader"
+ testid="palette-loader"
+ />
+
+
+ </div>
+ </div><div
+ dnddropzone=""
+ id="draggable_element"
+ >
+
+ </div>
+</composition-palette>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.component.html b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.html
index 8d1730f68c..efd619687c 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.component.html
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.html
@@ -14,15 +14,7 @@
~ limitations under the License.
-->
-<sdc-tabs>
- <sdc-tab titleIcon="info-circle">
- <policy-information-tab [policy]="policy" [isViewOnly]="isViewOnly" *ngIf="policy"></policy-information-tab>
- </sdc-tab>
- <sdc-tab titleIcon="inputs-o">
- <policy-targets-tab [policy]="policy" [topologyTemplate]="topologyTemplate" [isViewOnly]="isViewOnly" (isLoading)="setIsLoading($event)" *ngIf="policy"></policy-targets-tab>
- </sdc-tab>
- <sdc-tab titleIcon="settings-o">
- <policy-properties-tab [policy]="policy" [topologyTemplate]="topologyTemplate" [isViewOnly]="isViewOnly" *ngIf="policy"></policy-properties-tab>
- </sdc-tab>
-</sdc-tabs>
-
+<div class="palette-animation-wrapper" [style.top]="from.y + 50 + 'px'" [style.left]="from.x + 'px'" [style.transform]="transformStyle" [class.hidden]="!visible"
+ (transitionend)="animationComplete()">
+<div class="medium small sprite-resource-icons sprite-{{iconName}}-icons {{iconName}}" ></div>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.less b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.less
new file mode 100644
index 0000000000..54f04189c0
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.less
@@ -0,0 +1,5 @@
+.palette-animation-wrapper{
+ position: absolute;
+ z-index: 100;
+ transition: all 2s ease-in-out;
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.ts
new file mode 100644
index 0000000000..a445c87f42
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.ts
@@ -0,0 +1,71 @@
+/*-
+ * ============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, Input } from '@angular/core';
+import {BrowserModule} from '@angular/platform-browser';
+import { setTimeout } from 'core-js/library/web/timers';
+import { EventListenerService } from 'app/services';
+import { GRAPH_EVENTS } from 'app/utils';
+import { Point } from 'app/models';
+import { ZoneInstanceType, ZoneInstance } from 'app/models/graph/zones/zone-instance';
+
+
+
+@Component({
+ selector: 'palette-animation',
+ templateUrl: './palette-animation.component.html',
+ styleUrls:['./palette-animation.component.less'],
+})
+
+export class PaletteAnimationComponent {
+
+ @Input() from : Point;
+ @Input() to : Point;
+ @Input() type: ZoneInstanceType;
+ @Input() iconName : string;
+ @Input() zoneInstance : ZoneInstance;
+
+ public animation;
+ private visible:boolean = false;
+ private transformStyle:string = "";
+
+
+ constructor(private eventListenerService:EventListenerService) {}
+
+
+ ngOnDestroy(){
+ this.zoneInstance.hidden = false; //if animation component is destroyed before animation is complete
+ }
+
+ public runAnimation() {
+ this.visible = true;
+ let positionDiff:Point = new Point(this.to.x - this.from.x, this.to.y - this.from.y);
+ setTimeout(()=>{
+ this.transformStyle = 'translate('+ positionDiff.x + 'px,' + positionDiff.y +'px)';
+ }, 0);
+ };
+
+ public animationComplete = (e) => {
+ this.visible = false;
+ this.zoneInstance.hidden = false;
+ };
+
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.module.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.module.ts
new file mode 100644
index 0000000000..8674571138
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.module.ts
@@ -0,0 +1,16 @@
+import { NgModule } from "@angular/core";
+import { CommonModule } from "@angular/common";
+import { PaletteAnimationComponent } from "./palette-animation.component";
+
+
+@NgModule({
+ declarations: [
+ PaletteAnimationComponent
+ ],
+ imports: [ CommonModule ],
+ exports: [ PaletteAnimationComponent ]
+})
+
+export class PaletteAnimationModule {
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/__snapshots__/palette-element.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/__snapshots__/palette-element.component.spec.ts.snap
new file mode 100644
index 0000000000..40df575519
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/__snapshots__/palette-element.component.spec.ts.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`palette element component should match current snapshot of palette element component 1`] = `
+<palette-element>
+ <div
+ class="palette-element"
+ >
+ <sdc-element-icon
+ class="palette-element-icon"
+ />
+ <div
+ class="palette-element-text"
+ >
+ <div
+ class="palette-element-name"
+ sdc-tooltip=""
+ >
+
+ </div>
+ <span>
+ V.
+ </span>
+ <span>
+
+ </span>
+ </div>
+ </div>
+</palette-element>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.html b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.html
new file mode 100644
index 0000000000..3a6be5d082
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.html
@@ -0,0 +1,11 @@
+<div class="palette-element" >
+ <sdc-element-icon class="palette-element-icon" [iconName]="paletteElement.icon"
+ [elementType]="paletteElement.componentSubType"[uncertified]="this.paletteElement.certifiedIconClass"></sdc-element-icon>
+ <div class="palette-element-text">
+ <div class="palette-element-name" sdc-tooltip
+ tooltip-text='{{paletteElement.name | resourceName}}'>{{paletteElement.name | resourceName}}
+ </div>
+ <span> V.{{paletteElement.version}}</span>
+ <span>{{paletteElement.componentSubType}}</span>
+ </div>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.less b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.less
new file mode 100644
index 0000000000..e9c3253fbd
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.less
@@ -0,0 +1,32 @@
+@import "./../../../../../../assets/styles/override";
+.palette-element {
+ cursor: pointer;
+ display: flex;
+ flex-direction: row;
+ max-height: 65px;
+ border-bottom: 1px solid @sdcui_color_silver;
+ padding: 10px;
+ align-items: center;
+ .palette-element-icon {
+ min-width: 45px;
+ text-align: center;
+ }
+
+ .palette-element-text {
+ display: flex;
+ flex-direction: column;
+ font-size: 13px;
+ line-height: 15px;
+ padding-left: 10px;
+ font-family: OpenSans-Regular, sans-serif;
+ overflow: hidden;
+
+ .palette-element-name {
+ color: @sdcui_color_dark-gray;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.spec.ts
new file mode 100644
index 0000000000..64ed45ba9c
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.spec.ts
@@ -0,0 +1,30 @@
+import {async, ComponentFixture} from "@angular/core/testing";
+import {ConfigureFn, configureTests} from "../../../../../../jest/test-config.helper";
+import {NO_ERRORS_SCHEMA} from "@angular/core";
+import {PaletteElementComponent} from "./palette-element.component";
+import {ResourceNamePipe} from "../../../../pipes/resource-name.pipe";
+
+describe('palette element component', () => {
+
+ let fixture: ComponentFixture<PaletteElementComponent>;
+
+ beforeEach(
+ async(() => {
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [PaletteElementComponent, ResourceNamePipe],
+ imports: [],
+ schemas: [NO_ERRORS_SCHEMA]
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(PaletteElementComponent);
+ });
+ })
+ );
+
+ it('should match current snapshot of palette element component', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+}); \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-information-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.ts
index 26602224da..9e9e5a29da 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-information-tab.component.ts
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.ts
@@ -1,3 +1,6 @@
+/**
+ * Created by ob0695 on 6/28/2018.
+ */
/*-
* ============LICENSE_START=======================================================
* SDC
@@ -18,22 +21,15 @@
* ============LICENSE_END=========================================================
*/
-import * as _ from "lodash";
-import { Component, Inject, Input, Output, EventEmitter } from "@angular/core";
-import { GroupInstance } from 'app/models/graph/zones/group-instance';
+import {Component, Input} from "@angular/core";
+import {LeftPaletteComponent} from "app/models/components/displayComponent";
@Component({
- selector: 'group-information-tab',
- templateUrl: './group-information-tab.component.html',
- styleUrls: ['./../base/base-tab.component.less']
+ selector: 'palette-element',
+ templateUrl: './palette-element.component.html',
+ styleUrls: ['./palette-element.component.less']
})
-export class GroupInformationTabComponent {
-
- @Input() group: GroupInstance;
- @Input() isViewOnly: boolean;
-
- constructor() {
-
- }
+export class PaletteElementComponent {
+ @Input() paletteElement: LeftPaletteComponent;
}
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.html b/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.html
new file mode 100644
index 0000000000..86847eb28a
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.html
@@ -0,0 +1,30 @@
+<!--
+ ~ 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="popup-panel" [ngClass]="{'hide':!isShowPanel}" [style.left]="popupPanelPosition.x + 'px'" [style.top]="popupPanelPosition.y + 'px'"
+ (mousedown)="addZoneInstance()"
+ (mouseenter)="onMouseEnter()"
+ (mouseleave)="onMouseLeave()">
+ <div class="popup-panel-group">
+ <div class="popup-panel-plus">+</div>
+ <div class="popup-panel-title">{{panelTitle}}</div>
+ </div>
+</div>
+<!--<popup-menu-list [menuItemsData]="getMenuItems()">-->
+
+
+
+<!--</popup-menu-list>--> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.less b/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.less
new file mode 100644
index 0000000000..24f0485e76
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.less
@@ -0,0 +1,37 @@
+.popup-panel {
+ position: absolute;
+ display: inline-block;
+ background-color: white;
+ border: solid 1px #d2d2d2;
+ border-top: solid 3px #13a7df;
+ left: 208px; top: 0px;
+ width: 140px;
+ height: 40px;
+ z-index: 10000;
+
+ &:hover {
+ background-color: whitesmoke;
+ }
+
+ .popup-panel-group {
+ padding-left: 8px;
+ padding-top: 8px;
+ cursor: pointer;
+
+ .popup-panel-plus {
+ border-radius: 50%;
+ color: white;
+ background-color: #13a7df;
+ width: 20px;
+ text-align: center;
+ display: inline-block;
+ }
+
+ .popup-panel-title {
+ padding-left: 10px;
+ display: inline-block;
+ }
+
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.ts
new file mode 100644
index 0000000000..5d98fc7f78
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.ts
@@ -0,0 +1,98 @@
+import {Component, OnInit} from '@angular/core';
+import {GRAPH_EVENTS, SdcElementType} from "app/utils";
+import {LeftPaletteComponent, Point} from "app/models";
+import {EventListenerService} from "app/services";
+import {LeftPaletteMetadataTypes} from "app/models/components/displayComponent";
+
+@Component({
+ selector: 'app-palette-popup-panel',
+ templateUrl: './palette-popup-panel.component.html',
+ styleUrls: [ './palette-popup-panel.component.less' ],
+})
+export class PalettePopupPanelComponent implements OnInit {
+
+ public panelTitle: string;
+ public isShowPanel: boolean;
+ private component: Component;
+ private displayComponent: LeftPaletteComponent;
+ private popupPanelPosition:Point = new Point(0,0);
+
+ constructor(private eventListenerService: EventListenerService) {
+ this.isShowPanel = false;
+ }
+
+ ngOnInit() {
+ this.registerObserverCallbacks();
+ }
+
+ public onMouseEnter() {
+ this.isShowPanel = true;
+ }
+
+ public getMenuItems = () => {
+ return [{
+ text: 'Delete',
+ iconName: 'trash-o',
+ iconType: 'common',
+ iconMode: 'secondary',
+ iconSize: 'small',
+ type: '',
+ action: () => this.addZoneInstance()
+ }];
+ }
+
+ public onMouseLeave() {
+ this.isShowPanel = false;
+ }
+
+ public addZoneInstance(): void {
+ if(this.displayComponent) {
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_ADD_ZONE_INSTANCE_FROM_PALETTE, this.component, this.displayComponent, this.popupPanelPosition);
+ this.hidePopupPanel();
+ }
+ }
+
+ private registerObserverCallbacks() {
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_SHOW_POPUP_PANEL,
+ (displayComponent: LeftPaletteComponent, sectionElem: HTMLElement) => {
+ this.showPopupPanel(displayComponent, sectionElem);
+ });
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HIDE_POPUP_PANEL, () => this.hidePopupPanel());
+ }
+
+ private getPopupPanelPosition (sectionElem: HTMLElement):Point {
+ let pos: ClientRect = sectionElem.getBoundingClientRect();
+ let offsetX: number = -30;
+ const offsetY: number = pos.height / 2;
+ return new Point((pos.right + offsetX), (pos.top - offsetY + window.pageYOffset));
+ };
+
+ private setPopupPanelTitle(component: LeftPaletteComponent): void {
+ if (component.componentSubType === SdcElementType.GROUP) {
+ this.panelTitle = "Add Group";
+ return;
+ }
+
+ if (component.componentSubType === SdcElementType.POLICY) {
+ this.panelTitle = "Add Policy";
+ return;
+ }
+ }
+
+ private showPopupPanel(displayComponent:LeftPaletteComponent, sectionElem: HTMLElement) {
+ if(!this.isShowPanel){
+ this.displayComponent = displayComponent;
+ this.setPopupPanelTitle(displayComponent);
+ this.popupPanelPosition = this.getPopupPanelPosition(sectionElem);
+ this.isShowPanel = true;
+ }
+ };
+
+ private hidePopupPanel() {
+ if(this.isShowPanel){
+ this.isShowPanel = false;
+ }
+ };
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.html b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.html
new file mode 100644
index 0000000000..7963dd18b7
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.html
@@ -0,0 +1,41 @@
+<div class="composition-palette-component">
+ <div class="palette-elements-count">Elements
+ <span class="palette-elements-count-value">{{numberOfElements}}</span>
+ </div>
+
+ <sdc-filter-bar placeholder="Search..." (valueChange)="onSearchChanged($event)" testId="searchAsset"></sdc-filter-bar>
+
+ <div class="palette-elements-list">
+ <sdc-loader [global]="false" name="palette-loader" testId="palette-loader" [active]="this.isPaletteLoading" [class.inactive]="!this.isPaletteLoading"></sdc-loader>
+ <div *ngIf="numberOfElements === 0 && searchText" class="no-elements-found">No Elements Found</div>
+ <sdc-accordion *ngFor="let mapByCategory of paletteElements | keyValue; let first = first" [attr.data-tests-id]="'leftPalette.category.'+mapByCategory.key" [title]="mapByCategory.key" [css-class]="'palette-category'">
+ <div *ngFor="let mapBySubCategory of mapByCategory.value | keyValue">
+ <div class="palette-subcategory">{{mapBySubCategory.key}}</div>
+ <ng-container *ngIf="!(isViewOnly$ | async)">
+ <div *ngFor="let paletteElement of mapBySubCategory.value"
+ [dndDraggable]="paletteElement"
+ [dndDisableIf]="paletteElement.componentSubType == 'GROUP' && paletteElement.componentSubType == 'POLICY'"
+ (dndStart)="onDragStart($event, paletteElement)"
+ (drag)="onDraggableMoved($event)"
+ [dndEffectAllowed]="'copyMove'"
+ (mouseenter)="onMouseOver($event, paletteElement)"
+ (mouseleave)="onMouseOut(paletteElement)"
+ [attr.data-tests-id]="paletteElement.name">
+ <palette-element [paletteElement]="paletteElement"></palette-element>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="(isViewOnly$ | async)">
+ <div *ngFor="let paletteElement of mapBySubCategory.value"
+ [attr.data-tests-id]="paletteElement.name">
+ <palette-element [paletteElement]="paletteElement"></palette-element>
+ </div>
+ </ng-container>
+ </div>
+ </sdc-accordion>
+ </div>
+</div>
+
+<div id="draggable_element" dndDropzone (dndDrop)="onDrop($event)" [dndAllowExternal]="true">
+ <sdc-element-icon *ngIf="paletteDraggedElement" [iconName]="paletteDraggedElement.icon"
+ [elementType]="paletteDraggedElement.componentSubType" [uncertified]="paletteDraggedElement.certifiedIconClass"></sdc-element-icon>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.less b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.less
new file mode 100644
index 0000000000..37461ba1c5
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.less
@@ -0,0 +1,84 @@
+@import "./../../../../../assets/styles/override";
+
+:host(composition-palette) {
+ display:flex;
+ flex: 0 0 244px;
+}
+
+sdc-loader.inactive {
+ display:none;
+}
+
+:host ::ng-deep .sdc-filter-bar .sdc-input {
+ margin-bottom:0px;
+}
+:host ::ng-deep .sdc-loader-wrapper {
+ position:static;
+}
+
+.composition-palette-component {
+ background-color: @sdcui_color_white;
+ overflow-y: auto;
+ overflow-x: hidden;
+ display: flex;
+ flex-direction: column;
+ position:relative;
+ width: 244px;
+ box-shadow: 7px -3px 6px -8px @sdcui_color_gray;
+
+ .palette-elements-count {
+ background-color: @sdcui_color_gray;
+ line-height: 40px;
+ padding: 0 17px;
+ color: @sdcui_color_white;
+ .palette-elements-count-value {
+ float: right;
+ }
+ }
+
+ .palette-elements-list {
+
+ .no-elements-found {
+ padding-left: 40px;
+ }
+ /deep/ .palette-category {
+ display: flex;
+ margin: 0px;
+ .sdc-accordion-header {
+ background-color: @sdcui_color_silver;
+ margin: 0px;
+ line-height: 40px;
+ padding: 0px 10px;
+ }
+ .sdc-accordion-body {
+ padding: 0px;
+ }
+ }
+ .palette-subcategory {
+ padding: 0 10px;
+ background-color: @sdcui_color_lighter-silver;
+ line-height: 35px;
+ }
+ }
+}
+
+#draggable_element {
+ display: inline-block;
+ border-radius: 50%;
+ background: transparent;
+ position: absolute;
+ top: -9999px;
+ left: 0;
+ z-index: 100;
+}
+
+.invalid-drag {
+ border: 7px solid @red-shadow;
+}
+
+.valid-drag {
+ border: 7px solid @green-shadow;
+}
+
+@green-shadow: rgba(29, 154, 149, 0.3);
+@red-shadow: rgba(218, 31, 61, 0.3);
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.spec.ts
new file mode 100644
index 0000000000..efa9cd3370
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.spec.ts
@@ -0,0 +1,102 @@
+import {async, ComponentFixture, TestBed} from "@angular/core/testing";
+import {NO_ERRORS_SCHEMA} from "@angular/core";
+import {CompositionPaletteService} from "./services/palette.service";
+import {EventListenerService} from "../../../../services/event-listener-service";
+import {PaletteElementComponent} from "./palette-element/palette-element.component";
+import {PaletteComponent} from "./palette.component";
+import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper";
+import {GRAPH_EVENTS} from "../../../../utils/constants";
+import {KeyValuePipe} from "../../../pipes/key-value.pipe";
+import {ResourceNamePipe} from "../../../pipes/resource-name.pipe";
+import {LeftPaletteComponent} from "../../../../models/components/displayComponent";
+import {Observable} from "rxjs/Observable";
+import {leftPaletteElements} from "../../../../../jest/mocks/left-paeltte-elements.mock";
+import {NgxsModule, Select} from '@ngxs/store';
+import { WorkspaceState } from 'app/ng2/store/states/workspace.state';
+
+
+describe('palette component', () => {
+
+ const mockedEvent = <MouseEvent>{ target: {} }
+ let fixture: ComponentFixture<PaletteComponent>;
+ let eventServiceMock: Partial<EventListenerService>;
+ let compositionPaletteMockService: Partial<CompositionPaletteService>;
+
+ beforeEach(
+ async(() => {
+ eventServiceMock = {
+ notifyObservers: jest.fn()
+ }
+ compositionPaletteMockService = {
+ subscribeToLeftPaletteElements: jest.fn().mockImplementation(()=> Observable.of(leftPaletteElements)),
+ getLeftPaletteElements: jest.fn().mockImplementation(()=> leftPaletteElements)
+ }
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ declarations: [PaletteComponent, PaletteElementComponent, KeyValuePipe, ResourceNamePipe],
+ imports: [NgxsModule.forRoot([WorkspaceState])],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: CompositionPaletteService, useValue: compositionPaletteMockService},
+ {provide: EventListenerService, useValue: eventServiceMock}
+ ],
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(PaletteComponent);
+ });
+ })
+ );
+
+ it('should match current snapshot of palette component', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+ it('should call on palette component hover in event', () => {
+ let paletteObject = <LeftPaletteComponent>{categoryType: 'COMPONENT'};
+ fixture.componentInstance.onMouseOver(mockedEvent, paletteObject);
+ expect(eventServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, paletteObject);
+ });
+
+ it('should call on palette component hover out event', () => {
+ let paletteObject = <LeftPaletteComponent>{categoryType: 'COMPONENT'};
+ fixture.componentInstance.onMouseOut(paletteObject);
+ expect(eventServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_OUT);
+ });
+
+ it('should call show popup panel event', () => {
+ let paletteObject = <LeftPaletteComponent>{categoryType: 'GROUP'};
+ fixture.componentInstance.onMouseOver(mockedEvent, paletteObject);
+ expect(eventServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_PALETTE_COMPONENT_SHOW_POPUP_PANEL, paletteObject, mockedEvent.target);
+ });
+
+ it('should call hide popup panel event', () => {
+ let paletteObject = <LeftPaletteComponent>{categoryType: 'GROUP'};
+ fixture.componentInstance.onMouseOut(paletteObject);
+ expect(eventServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HIDE_POPUP_PANEL);
+ });
+
+ it('should build Palette By Categories without searchText', () => {
+ fixture.componentInstance.buildPaletteByCategories();
+ expect(fixture.componentInstance.paletteElements["Generic"]["Network"].length).toBe(5);
+ expect(fixture.componentInstance.paletteElements["Generic"]["Network"][0].searchFilterTerms).toBe("extvirtualmachineinterfacecp external port for virtual machine interface extvirtualmachineinterfacecp 3.0");
+ expect(fixture.componentInstance.paletteElements["Generic"]["Network"][1].searchFilterTerms).toBe("newservice2 asdfasdfa newservice2 0.3");
+
+ expect(fixture.componentInstance.paletteElements["Generic"]["Configuration"].length).toBe(1);
+ expect(fixture.componentInstance.paletteElements["Generic"]["Configuration"][0].systemName).toBe("Extvirtualmachineinterfacecp");
+ });
+
+ it('should build Palette By Categories with searchText', () => {
+ fixture.componentInstance.buildPaletteByCategories("testVal");
+ expect(fixture.componentInstance.paletteElements["Generic"]["Network"].length).toBe(1);
+ expect(fixture.componentInstance.paletteElements["Generic"]["Network"][0].searchFilterTerms).toBe("testVal and other values");
+ });
+
+ it('should change numbers of elements', () => {
+ fixture.componentInstance.buildPaletteByCategories();
+ expect(fixture.componentInstance.numberOfElements).toEqual(6);
+ fixture.componentInstance.buildPaletteByCategories("testVal");
+ expect(fixture.componentInstance.numberOfElements).toEqual(1);
+ });
+}); \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.ts
new file mode 100644
index 0000000000..02d270b39a
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.ts
@@ -0,0 +1,172 @@
+/**
+ * Created by ob0695 on 6/28/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 { Component, HostListener } from '@angular/core';
+import { Select } from '@ngxs/store';
+import { LeftPaletteComponent, LeftPaletteMetadataTypes } from 'app/models/components/displayComponent';
+import { Point } from 'app/models/graph/point';
+import { WorkspaceState } from 'app/ng2/store/states/workspace.state';
+import Dictionary = _.Dictionary;
+import { EventListenerService } from 'app/services/event-listener-service';
+import { GRAPH_EVENTS } from 'app/utils/constants';
+import { DndDropEvent } from 'ngx-drag-drop/ngx-drag-drop';
+import { CompositionPaletteService } from './services/palette.service';
+import {PolicyMetadata} from "../../../../models/policy-metadata";
+import {GenericBrowserDomAdapter} from "@angular/platform-browser/src/browser/generic_browser_adapter";
+
+@Component({
+ selector: 'composition-palette',
+ templateUrl: './palette.component.html',
+ styleUrls: ['./palette.component.less']
+})
+export class PaletteComponent {
+
+ constructor(private compositionPaletteService: CompositionPaletteService, private eventListenerService: EventListenerService) {}
+
+ @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean;
+ private paletteElements: Dictionary<Dictionary<LeftPaletteComponent[]>>;
+ public numberOfElements: number = 0;
+ public isPaletteLoading: boolean;
+ private paletteDraggedElement: LeftPaletteComponent;
+ public position: Point = new Point();
+
+ ngOnInit() {
+ this.isPaletteLoading = true;
+
+ this.compositionPaletteService.subscribeToLeftPaletteElements((leftPaletteElementsResponse) => {
+ this.paletteElements = leftPaletteElementsResponse;
+ this.numberOfElements = this.countLeftPalleteElements(this.paletteElements);
+ this.isPaletteLoading = false;
+ }, () => {
+ this.isPaletteLoading = false;
+ });
+
+ }
+
+ public buildPaletteByCategories = (searchText?: string) => { // create nested by category & subcategory, filtered by search parans
+ // Flat the object and run on its leaves
+ if (searchText) {
+ searchText = searchText.toLowerCase();
+ const paletteElementsAfterSearch = {};
+ this.paletteElements = this.compositionPaletteService.getLeftPaletteElements();
+ for (const category in this.paletteElements) {
+ for (const subCategory in this.paletteElements[category]) {
+ const subCategoryToCheck = this.paletteElements[category][subCategory];
+ const res = subCategoryToCheck.filter((item) => item.searchFilterTerms.toLowerCase().indexOf(searchText) >= 0)
+ if (res.length > 0) {
+ paletteElementsAfterSearch[category] = {};
+ paletteElementsAfterSearch[category][subCategory] = res;
+ }
+ }
+ }
+ this.paletteElements = paletteElementsAfterSearch;
+ } else {
+ this.paletteElements = this.compositionPaletteService.getLeftPaletteElements();
+ }
+ this.numberOfElements = this.countLeftPalleteElements(this.paletteElements);
+ }
+
+ public onSearchChanged = (searchText: string) => {
+
+ if (this.compositionPaletteService.getLeftPaletteElements()) {
+ this.buildPaletteByCategories(searchText);
+ }
+ }
+
+ private countLeftPalleteElements(leftPalleteElements: Dictionary<Dictionary<LeftPaletteComponent[]>>) {
+ // Use _ & flat map
+ let counter = 0;
+ for (const category in leftPalleteElements) {
+ for (const subCategory in leftPalleteElements[category]) {
+ counter += leftPalleteElements[category][subCategory].length;
+ }
+ }
+ return counter;
+ }
+
+ private isGroupOrPolicy(component: LeftPaletteComponent): boolean {
+ if (component &&
+ (component.categoryType === LeftPaletteMetadataTypes.Group ||
+ component.categoryType === LeftPaletteMetadataTypes.Policy)) {
+ return true;
+ }
+ return false;
+ }
+ @HostListener('document:dragover', ['$event'])
+ public onDrag(event) {
+ this.position.x = event.clientX;
+ this.position.y = event.clientY;
+ }
+
+ //---------------------------------------Palette Events-----------------------------------------//
+
+ public onDraggableMoved = (event:DragEvent) => {
+ let draggedElement = document.getElementById("draggable_element");
+ draggedElement.style.top = (this.position.y - 80) + "px";
+ draggedElement.style.left = (this.position.x - 30) + "px";
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_ACTION, this.position);
+ }
+
+ public onDragStart = (event, draggedElement:LeftPaletteComponent) => { // Applying the dragged svg component to the draggable element
+
+ this.paletteDraggedElement = draggedElement;
+ event.dataTransfer.dropEffect = "copy";
+ let hiddenImg = document.createElement("span");
+ event.dataTransfer.setDragImage(hiddenImg, 0, 0);
+ }
+
+
+ public onDrop = (event:DndDropEvent) => {
+ let draggedElement = document.getElementById("draggable_element");
+ draggedElement.style.top = "-9999px";
+ if(draggedElement.classList.contains('valid-drag')) {
+ if(!event.data){
+ event.data = this.paletteDraggedElement;
+ }
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DROP, event);
+ } else {
+ console.log("INVALID drop");
+ }
+ this.paletteDraggedElement = undefined;
+
+ }
+
+ public onMouseOver = (sectionElem:MouseEvent, displayComponent:LeftPaletteComponent) => {
+ console.debug("On palette element MOUSE HOVER: ", displayComponent);
+ if (this.isGroupOrPolicy(displayComponent)) {
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_SHOW_POPUP_PANEL, displayComponent, sectionElem.target);
+ } else {
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, displayComponent);
+ }
+ };
+
+ public onMouseOut = (displayComponent:LeftPaletteComponent) => {
+ console.debug("On palette element MOUSE OUT: ", displayComponent);
+ if (this.isGroupOrPolicy(displayComponent)) {
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HIDE_POPUP_PANEL);
+ } else {
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_OUT);
+ }
+ };
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette.module.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette.module.ts
new file mode 100644
index 0000000000..aeb4c4c60b
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette.module.ts
@@ -0,0 +1,25 @@
+import { NgModule } from "@angular/core";
+import { CompositionPaletteService } from "./services/palette.service";
+import { PaletteComponent } from "./palette.component";
+import { SdcUiComponentsModule } from "onap-ui-angular";
+import { GlobalPipesModule } from "app/ng2/pipes/global-pipes.module";
+import { CommonModule } from "@angular/common";
+import { DndModule } from "ngx-drag-drop";
+import {PaletteElementComponent} from "./palette-element/palette-element.component";
+import {EventListenerService} from "app/services/event-listener-service";
+import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module";
+
+@NgModule({
+ declarations: [PaletteComponent, PaletteElementComponent],
+ imports: [CommonModule, SdcUiComponentsModule, GlobalPipesModule, UiElementsModule, DndModule],
+ exports: [PaletteComponent],
+ entryComponents: [PaletteComponent],
+ providers: [CompositionPaletteService, EventListenerService]
+})
+export class PaletteModule {
+
+ constructor() {
+
+ }
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/services/palette.service.spec.ts b/catalog-ui/src/app/ng2/pages/composition/palette/services/palette.service.spec.ts
new file mode 100644
index 0000000000..3a660c1de7
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/services/palette.service.spec.ts
@@ -0,0 +1,41 @@
+import {TestBed} from "@angular/core/testing";
+import {CompositionPaletteService} from "./palette.service";
+import {ISdcConfig, SdcConfigToken} from "../../../../config/sdc-config.config";
+import {WorkspaceService} from "../../../../pages/workspace/workspace.service";
+import { HttpClient } from "@angular/common/http";
+describe('palette component', () => {
+
+ let service: CompositionPaletteService;
+
+ let httpServiceMock: Partial<HttpClient> = {
+ get: jest.fn()
+ }
+
+ let sdcConfigToken: Partial<ISdcConfig> = {
+ "api": {
+ "root": ''
+ }
+ }
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [],
+ providers: [CompositionPaletteService,
+ {provide: HttpClient, useValue: httpServiceMock},
+ {provide: SdcConfigToken, useValue: sdcConfigToken},
+ {provide: WorkspaceService, useValue{}}
+ ]
+ });
+
+ service = TestBed.get(CompositionPaletteService);
+ });
+
+ it('should create an instance', () => {
+ expect(service).toBeDefined();
+ });
+
+ // it('should create an instance2', async () => {
+ // expect(await service.subscribeToLeftPaletteElements("resources")).toEqual([]);
+ // });
+});
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/services/palette.service.ts b/catalog-ui/src/app/ng2/pages/composition/palette/services/palette.service.ts
new file mode 100644
index 0000000000..7587c5206f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/palette/services/palette.service.ts
@@ -0,0 +1,98 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Inject, Injectable } from '@angular/core';
+import { LeftPaletteComponent, LeftPaletteMetadataTypes } from 'app/models/components/displayComponent';
+import { GroupMetadata } from 'app/models/group-metadata';
+import { PolicyMetadata } from 'app/models/policy-metadata';
+import { SdcConfigToken } from 'app/ng2/config/sdc-config.config';
+import { ISdcConfig } from 'app/ng2/config/sdc-config.config.factory';
+import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service';
+import 'rxjs/add/observable/forkJoin';
+import { Observable } from 'rxjs/Rx';
+import Dictionary = _.Dictionary;
+
+
+
+@Injectable()
+export class CompositionPaletteService {
+
+ protected baseUrl = '';
+
+ private leftPaletteComponents: Dictionary<Dictionary<LeftPaletteComponent[]>>;
+ private facadeUrl: string;
+ constructor(protected http: HttpClient, @Inject(SdcConfigToken) sdcConfig: ISdcConfig, private workspaceService: WorkspaceService) {
+ this.baseUrl = sdcConfig.api.root + sdcConfig.api.component_api_root;
+ this.facadeUrl = sdcConfig.api.uicache_root + sdcConfig.api.GET_uicache_left_palette;
+
+ }
+
+ public subscribeToLeftPaletteElements(next, error) {
+
+ let params = new HttpParams();
+ params = params.append('internalComponentType', this.workspaceService.getMetadataType());
+
+ const loadInstances = this.http.get(this.facadeUrl, {params});
+ const loadGroups = this.http.get(this.baseUrl + 'groupTypes', {params});
+ const loadPolicies = this.http.get(this.baseUrl + 'policyTypes', {params});
+
+ Observable.forkJoin(
+ loadInstances, loadGroups, loadPolicies
+ ).subscribe( ([resInstances, resGrouops, resPolicies]) => {
+ const combinedDictionary = this.combineResoponses(resInstances, resGrouops, resPolicies);
+ this.leftPaletteComponents = combinedDictionary;
+ next(this.leftPaletteComponents);
+ });
+ }
+
+ public getLeftPaletteElements = (): Dictionary<Dictionary<LeftPaletteComponent[]>> => {
+ return this.leftPaletteComponents;
+ }
+
+
+ public convertPoliciesOrGroups = (paletteListResult, type: string ) => {
+ const components: LeftPaletteComponent[] = [];
+
+ if (type === 'Policies') {
+ _.forEach(paletteListResult, (policyMetadata: PolicyMetadata) => {
+ components.push(new LeftPaletteComponent(LeftPaletteMetadataTypes.Policy, policyMetadata));
+ });
+ return {
+ Policies: components
+ };
+ }
+
+ if (type === 'Groups') {
+ _.forEach(paletteListResult, (groupMetadata: GroupMetadata) => {
+ const item = new LeftPaletteComponent(LeftPaletteMetadataTypes.Group, groupMetadata);
+ components.push(item);
+ });
+ return {
+ Groups: components
+ };
+ }
+
+ return {};
+ }
+
+ private combineResoponses(resInstances: object, resGrouops: object, resPolicies: object) {
+ const retValObject = {};
+ // Generic will be the 1st category in the left Pallete
+ if (resInstances['Generic']) {
+ retValObject['Generic'] = resInstances['Generic'];
+ }
+ // Add all other categories
+ for (const category in resInstances) {
+ if (category === 'Generic') {
+ continue;
+ }
+ retValObject[category] = resInstances[category];
+ }
+
+ // Add Groups
+ retValObject["Groups"] = this.convertPoliciesOrGroups(resGrouops, 'Groups');
+
+ // Add policies
+ retValObject["Policies"] = this.convertPoliciesOrGroups(resPolicies, 'Policies');
+
+ return retValObject;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/__snapshots__/composition-panel.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/panel/__snapshots__/composition-panel.component.spec.ts.snap
new file mode 100644
index 0000000000..5f10806315
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/__snapshots__/composition-panel.component.spec.ts.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`composition-panel component should match current snapshot of composition-panel component. 1`] = `
+<ng2-composition-panel
+ activatePreviousActiveTab={[Function Function]}
+ classes={[Function String]}
+ initTabs={[Function Function]}
+ isComponentInstanceSelected={[Function Function]}
+ isConfiguration={[Function Function]}
+ isPNF={[Function Function]}
+ selectedComponentIsServiceProxyInstance={[Function Function]}
+ setActive={[Function Function]}
+ store={[Function Store]}
+ toggleSidebarDisplay={[Function Function]}
+>
+
+</ng2-composition-panel>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.html
new file mode 100644
index 0000000000..bd90b9a814
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.html
@@ -0,0 +1,21 @@
+<panel-wrapper-component *ngIf="compositionState$ | async as state"> <!-- HEADER -->
+
+ <ng2-composition-panel-header [isViewOnly]="state.isViewOnly"
+ [selectedComponent]="state.selectedComponent"></ng2-composition-panel-header>
+
+ <!-- TABS -->
+ <div class="component-details-panel-tabs">
+ <sdc-loader [global]="false" name="panel" testId="panel-loader" [active]="state.panelLoading"></sdc-loader>
+ <sdc-tabs (selectedTab)="setActive($event)" [iconsSize]="'large'">
+ <sdc-tab *ngFor="let tab of tabs" [titleIcon]="tab.titleIcon" [active]="tab.isActive"
+ [tooltipText]="tab.tooltipText">
+ <panel-tab [isActive]="tab.isActive" [component]="selectedComponent"
+ [componentType]="state.selectedComponentType" [isViewOnly]="isViewOnly$ | async"
+ [input]="tab.input" [panelTabType]="tab.component"></panel-tab>
+ </sdc-tab>
+ </sdc-tabs>
+ </div>
+
+</panel-wrapper-component>
+
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.less
new file mode 100644
index 0000000000..776ef68944
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.less
@@ -0,0 +1,27 @@
+@import '../../../../../assets/styles/variables';
+@import '../../../../../assets/styles/mixins_old';
+
+:host ::ng-deep .sdc-loader-wrapper {
+ position:static;
+}
+
+.component-details-panel-tabs {
+ flex: 1;
+ display:flex;
+ overflow:hidden;
+ }
+
+.component-details-panel-tabs /deep/ sdc-tabs {
+ display:flex;
+ flex-direction:column;
+
+ /deep/ sdc-tab {
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ }
+ .svg-icon-wrapper.label-placement-left .svg-icon-label {
+ margin-right: 0;
+ }
+}
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.spec.ts
new file mode 100644
index 0000000000..25a0c728a8
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.spec.ts
@@ -0,0 +1,228 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture } from '@angular/core/testing';
+import { NgxsModule, Store } from '@ngxs/store';
+import { Observable } from 'rxjs';
+import { Mock } from 'ts-mockery';
+import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper';
+import { Service } from '../../../../models/components/service';
+import { Resource } from '../../../../models/components/resource';
+import { GroupInstance } from '../../../../models/graph/zones/group-instance';
+import { PolicyInstance } from '../../../../models/graph/zones/policy-instance';
+import { ArtifactGroupType, ResourceType } from '../../../../utils/constants';
+import { WorkspaceState } from '../../../store/states/workspace.state';
+import { CompositionPanelComponent } from './composition-panel.component';
+import { ArtifactsTabComponent } from './panel-tabs/artifacts-tab/artifacts-tab.component';
+import { GroupMembersTabComponent } from './panel-tabs/group-members-tab/group-members-tab.component';
+import { GroupOrPolicyPropertiesTab } from './panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component';
+import { InfoTabComponent } from './panel-tabs/info-tab/info-tab.component';
+import { PolicyTargetsTabComponent } from './panel-tabs/policy-targets-tab/policy-targets-tab.component';
+import { PropertiesTabComponent } from './panel-tabs/properties-tab/properties-tab.component';
+import { ReqAndCapabilitiesTabComponent } from './panel-tabs/req-capabilities-tab/req-capabilities-tab.component';
+
+describe('composition-panel component', () => {
+
+ let fixture: ComponentFixture<CompositionPanelComponent>;
+ let store: Store;
+
+ const tabs = {
+ infoTab : {titleIcon: 'info-circle', component: InfoTabComponent, input: {}, isActive: true, tooltipText: 'Information'},
+ policyProperties: {
+ titleIcon: 'settings-o', component: GroupOrPolicyPropertiesTab, input: {type: 'policy'}, isActive: false, tooltipText: 'Properties'
+ },
+ policyTargets: {titleIcon: 'inputs-o', component: PolicyTargetsTabComponent, input: {}, isActive: false, tooltipText: 'Targets'},
+ groupMembers: {titleIcon: 'inputs-o', component: GroupMembersTabComponent, input: {}, isActive: false, tooltipText: 'Members'},
+ groupProperties: {
+ titleIcon: 'settings-o', component: GroupOrPolicyPropertiesTab, input: {type: 'group'}, isActive: false, tooltipText: 'Properties'
+ },
+ deploymentArtifacts: {
+ titleIcon: 'deployment-artifacts-o', component: ArtifactsTabComponent,
+ input: { type: ArtifactGroupType.DEPLOYMENT}, isActive: false, tooltipText: 'Deployment Artifacts'
+ },
+ apiArtifacts: {
+ titleIcon: 'api-o', component: ArtifactsTabComponent,
+ input: { type: ArtifactGroupType.SERVICE_API}, isActive: false, tooltipText: 'API Artifacts'
+ },
+ infoArtifacts: {
+ titleIcon: 'info-square-o', component: ArtifactsTabComponent,
+ input: { type: ArtifactGroupType.INFORMATION}, isActive: false, tooltipText: 'Information Artifacts'
+ },
+ properties: {
+ titleIcon: 'settings-o', component: PropertiesTabComponent,
+ input: {title: 'Properties and Attributes'}, isActive: false, tooltipText: 'Properties'
+ },
+ reqAndCapabilities : {
+ titleIcon: 'req-capabilities-o', component: ReqAndCapabilitiesTabComponent, input: {},
+ isActive: false, tooltipText: 'Requirements and Capabilities'
+ },
+ inputs: {titleIcon: 'inputs-o', component: PropertiesTabComponent, input: {title: 'Inputs'}, isActive: false, tooltipText: 'Inputs'},
+ settings: {titleIcon: 'settings-o', component: PropertiesTabComponent, input: {}, isActive: false, tooltipText: 'Settings'},
+ };
+
+ beforeEach(
+ async(() => {
+
+ const configure: ConfigureFn = (testBed) => {
+ testBed.configureTestingModule({
+ declarations: [CompositionPanelComponent],
+ imports: [NgxsModule.forRoot([WorkspaceState])],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [],
+ });
+ };
+
+ configureTests(configure).then((testBed) => {
+ fixture = testBed.createComponent(CompositionPanelComponent);
+ store = testBed.get(Store);
+ });
+ })
+ );
+
+ it('When PolicyInstance Selected => Expect (info, policyTargets and policyProperties) tabs appear', () => {
+
+ const testInstance = new PolicyInstance();
+
+ fixture.componentInstance.initTabs(testInstance);
+
+ expect (fixture.componentInstance.tabs.length).toBe(3);
+ expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab);
+ expect (fixture.componentInstance.tabs[1]).toEqual(tabs.policyTargets);
+ expect (fixture.componentInstance.tabs[2]).toEqual(tabs.policyProperties);
+ });
+
+ it('should match current snapshot of composition-panel component.', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+ it('When Topology Template is Service and no instance is selected Expect (info, deployment, inputs, info and api)', () => {
+
+ const selectedComponent: Service = new Service(null, null);
+ selectedComponent.isResource = jest.fn(() => false);
+ selectedComponent.isService = jest.fn(() => true );
+
+ fixture.componentInstance.store.select = jest.fn(() => Observable.of(selectedComponent));
+
+ // const pnfMock = Mock.of<Service>({ isResource : () => false });
+ fixture.componentInstance.topologyTemplate = selectedComponent;
+
+ // Call ngOnInit
+ fixture.componentInstance.ngOnInit();
+
+ // Expect that
+ expect (fixture.componentInstance.tabs.length).toBe(5);
+ expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab);
+ expect (fixture.componentInstance.tabs[1]).toEqual(tabs.deploymentArtifacts);
+ expect (fixture.componentInstance.tabs[2]).toEqual(tabs.inputs);
+ expect (fixture.componentInstance.tabs[3]).toEqual(tabs.infoArtifacts);
+ expect (fixture.componentInstance.tabs[4]).toEqual(tabs.apiArtifacts);
+
+ });
+
+ it('When Topology Template is Resource and no instance is selected Expect (info, deployment, inputs, info and api)', () => {
+
+ const selectedComponent: Service = new Service(null, null);
+ selectedComponent.isResource = jest.fn(() => true);
+ selectedComponent.isService = jest.fn(() => false );
+
+ fixture.componentInstance.store.select = jest.fn(() => Observable.of(selectedComponent));
+
+ fixture.componentInstance.topologyTemplate = selectedComponent;
+
+ // Call ngOnInit
+ fixture.componentInstance.ngOnInit();
+
+ // Expect that
+ expect (fixture.componentInstance.tabs.length).toBe(5);
+ expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab);
+ expect (fixture.componentInstance.tabs[1]).toEqual(tabs.deploymentArtifacts);
+ expect (fixture.componentInstance.tabs[2]).toEqual(tabs.properties);
+ expect (fixture.componentInstance.tabs[3]).toEqual(tabs.infoArtifacts);
+ expect (fixture.componentInstance.tabs[4]).toEqual(tabs.reqAndCapabilities);
+
+ });
+
+ it('When Topology Template is Service and proxyService instance is selected ' +
+ 'Expect (info, deployment, inputs, info and api)', () => {
+
+ const selectedComponent: Service = new Service(null, null);
+ selectedComponent.isResource = jest.fn(() => false);
+ selectedComponent.isService = jest.fn(() => true );
+
+ fixture.componentInstance.store.select = jest.fn(() => Observable.of(selectedComponent));
+ fixture.componentInstance.selectedComponentIsServiceProxyInstance = jest.fn(() => true);
+
+ // const pnfMock = Mock.of<Service>({ isResource : () => false });
+ fixture.componentInstance.topologyTemplate = selectedComponent;
+
+ // Call ngOnInit
+ fixture.componentInstance.ngOnInit();
+
+ // Expect that
+ expect (fixture.componentInstance.tabs.length).toBe(5);
+ expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab);
+ expect (fixture.componentInstance.tabs[1]).toEqual(tabs.properties);
+ expect (fixture.componentInstance.tabs[2]).toEqual(tabs.reqAndCapabilities);
+
+ });
+
+ it('When Topology Template is Resource and VL is selected ' +
+ 'Expect (info, deployment, inputs, info and api)', () => {
+
+ const topologyTemplate: Resource = new Resource(null, null);
+ topologyTemplate.isResource = jest.fn(() => true);
+ topologyTemplate.isService = jest.fn(() => false );
+
+ const vlMock = Mock.of<Resource>({ resourceType : 'VL', isResource : () => true, isService : () => false });
+ fixture.componentInstance.store.select = jest.fn(() => Observable.of(vlMock));
+
+ fixture.componentInstance.topologyTemplate = topologyTemplate;
+
+ // Call ngOnInit
+ fixture.componentInstance.ngOnInit();
+
+ // Expect that
+ expect (fixture.componentInstance.tabs.length).toBe(5);
+ expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab);
+ expect (fixture.componentInstance.tabs[1]).toEqual(tabs.deploymentArtifacts);
+ expect (fixture.componentInstance.tabs[2]).toEqual(tabs.properties);
+ expect (fixture.componentInstance.tabs[3]).toEqual(tabs.infoArtifacts);
+ expect (fixture.componentInstance.tabs[4]).toEqual(tabs.reqAndCapabilities);
+
+ });
+
+ it('When Topology Template is Service and VL is selected ' +
+ 'Expect (info, deployment, inputs, info and api)', () => {
+
+ const topologyTemplate: Service = new Service(null, null);
+ topologyTemplate.isResource = jest.fn(() => true);
+ topologyTemplate.isService = jest.fn(() => false );
+
+ const vlMock = Mock.of<Resource>({ resourceType : 'VL', isResource : () => true, isService : () => false });
+ fixture.componentInstance.store.select = jest.fn(() => Observable.of(vlMock));
+
+ fixture.componentInstance.topologyTemplate = topologyTemplate;
+
+ // Call ngOnInit
+ fixture.componentInstance.ngOnInit();
+
+ // Expect that
+ expect (fixture.componentInstance.tabs.length).toBe(5);
+ expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab);
+ expect (fixture.componentInstance.tabs[1]).toEqual(tabs.deploymentArtifacts);
+ expect (fixture.componentInstance.tabs[2]).toEqual(tabs.properties);
+ expect (fixture.componentInstance.tabs[3]).toEqual(tabs.infoArtifacts);
+ expect (fixture.componentInstance.tabs[4]).toEqual(tabs.reqAndCapabilities);
+
+ });
+
+ it('When GroupInstance Selected => Expect (info, groupMembers and groupProperties) tabs appear.', () => {
+
+ const testInstance = new GroupInstance();
+ fixture.componentInstance.initTabs(testInstance);
+
+ expect (fixture.componentInstance.tabs.length).toBe(3);
+ expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab);
+ expect (fixture.componentInstance.tabs[1]).toEqual(tabs.groupMembers);
+ expect (fixture.componentInstance.tabs[2]).toEqual(tabs.groupProperties);
+ });
+
+});
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.ts
new file mode 100644
index 0000000000..c5ea41bcd1
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.ts
@@ -0,0 +1,171 @@
+/*-
+ * ============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, HostBinding, Input } from '@angular/core';
+import { Select, Store } from '@ngxs/store';
+import { Component as TopologyTemplate, ComponentInstance, FullComponentInstance, GroupInstance, PolicyInstance, Resource, Service } from 'app/models';
+import { ArtifactsTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component';
+import { GroupMembersTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component';
+import { GroupOrPolicyPropertiesTab } from 'app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component';
+import { InfoTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component';
+import { PolicyTargetsTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component';
+import { PropertiesTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component';
+import { ReqAndCapabilitiesTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component';
+import { ComponentType, ResourceType } from 'app/utils';
+import * as _ from 'lodash';
+import { Subscription } from 'rxjs';
+import { Observable } from 'rxjs/Observable';
+import { ArtifactGroupType, COMPONENT_FIELDS } from '../../../../utils/constants';
+import { WorkspaceState } from '../../../store/states/workspace.state';
+import { OnSidebarOpenOrCloseAction } from '../common/store/graph.actions';
+import { CompositionStateModel, GraphState } from '../common/store/graph.state';
+import { ServiceConsumptionTabComponent } from './panel-tabs/service-consumption-tab/service-consumption-tab.component';
+import { ServiceDependenciesTabComponent } from './panel-tabs/service-dependencies-tab/service-dependencies-tab.component';
+
+const tabs = {
+ infoTab : {titleIcon: 'info-circle', component: InfoTabComponent, input: {}, isActive: true, tooltipText: 'Information'},
+ policyProperties: {titleIcon: 'settings-o', component: GroupOrPolicyPropertiesTab, input: {type: 'policy'}, isActive: false, tooltipText: 'Properties'},
+ policyTargets: {titleIcon: 'inputs-o', component: PolicyTargetsTabComponent, input: {}, isActive: false, tooltipText: 'Targets'},
+ groupMembers: {titleIcon: 'inputs-o', component: GroupMembersTabComponent, input: {}, isActive: false, tooltipText: 'Members'},
+ groupProperties: {titleIcon: 'settings-o', component: GroupOrPolicyPropertiesTab, input: {type: 'group'}, isActive: false, tooltipText: 'Properties'},
+ deploymentArtifacts: {titleIcon: 'deployment-artifacts-o', component: ArtifactsTabComponent, input: { type: ArtifactGroupType.DEPLOYMENT}, isActive: false, tooltipText: 'Deployment Artifacts'},
+ apiArtifacts: {titleIcon: 'api-o', component: ArtifactsTabComponent, input: { type: ArtifactGroupType.SERVICE_API}, isActive: false, tooltipText: 'API Artifacts'},
+ infoArtifacts: {titleIcon: 'info-square-o', component: ArtifactsTabComponent, input: { type: ArtifactGroupType.INFORMATION}, isActive: false, tooltipText: 'Information Artifacts'},
+ properties: {titleIcon: 'settings-o', component: PropertiesTabComponent, input: {title: 'Properties and Attributes'}, isActive: false, tooltipText: 'Properties'},
+ reqAndCapabilities : { titleIcon: 'req-capabilities-o', component: ReqAndCapabilitiesTabComponent, input: {}, isActive: false, tooltipText: 'Requirements and Capabilities'},
+ inputs: {titleIcon: 'inputs-o', component: PropertiesTabComponent, input: {title: 'Inputs'}, isActive: false, tooltipText: 'Inputs'},
+ settings: {titleIcon: 'settings-o', component: PropertiesTabComponent, input: {}, isActive: false, tooltipText: 'Settings'},
+ consumption: {titleIcon: 'api-o', component: ServiceConsumptionTabComponent, input: {title: 'OPERATION CONSUMPTION'}, isActive: false, tooltipText: 'Service Consumption'},
+ dependencies: {titleIcon: 'archive', component: ServiceDependenciesTabComponent, input: {title: 'SERVICE DEPENDENCIES'}, isActive: false, tooltipText: 'Service Dependencies'}
+};
+
+@Component({
+ selector: 'ng2-composition-panel',
+ templateUrl: './composition-panel.component.html',
+ styleUrls: ['./composition-panel.component.less', './panel-tabs/panel-tabs.less'],
+})
+export class CompositionPanelComponent {
+
+ @Input() topologyTemplate: TopologyTemplate;
+ @HostBinding('class') classes = 'component-details-panel';
+ @Select(GraphState) compositionState$: Observable<CompositionStateModel>;
+ @Select(GraphState.withSidebar) withSidebar$: boolean;
+ @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean;
+ tabs: any[];
+ subscription: Subscription;
+
+ private selectedComponent;
+
+ constructor(public store: Store) {
+ }
+
+ ngOnInit() {
+ this.subscription = this.store.select(GraphState.getSelectedComponent).subscribe((component) => {
+ this.selectedComponent = component;
+ this.initTabs(component);
+ this.activatePreviousActiveTab();
+ });
+ }
+
+ ngOnDestroy() {
+ if (this.subscription) {
+ this.subscription.unsubscribe();
+ }
+ }
+
+ public setActive = (tabToSelect) => {
+ this.tabs.map((tab) => tab.isActive = (tab.titleIcon === tabToSelect.titleIcon) ? true : false);
+ }
+
+ public activatePreviousActiveTab = () => { // sets the info tab to active if no other tab selected
+
+ this.setActive(this.tabs.find((tab) => tab.isActive) || tabs.infoTab);
+
+ }
+
+ private initTabs = (component) => {
+ this.tabs = [];
+
+ // Information
+ this.tabs.push(tabs.infoTab);
+
+ if (component instanceof PolicyInstance) {
+ this.tabs.push(tabs.policyTargets);
+ this.tabs.push(tabs.policyProperties);
+ return;
+ }
+
+ if (component instanceof GroupInstance) {
+ this.tabs.push(tabs.groupMembers);
+ this.tabs.push(tabs.groupProperties);
+ return;
+ }
+
+ // Deployment artifacts
+ if (!this.isPNF() && !this.isConfiguration() && !this.selectedComponentIsServiceProxyInstance()) {
+ this.tabs.push(tabs.deploymentArtifacts);
+ }
+
+ // Properties or Inputs
+ if (component.isResource() || this.selectedComponentIsServiceProxyInstance()) {
+ this.tabs.push(tabs.properties);
+ } else {
+ this.tabs.push(tabs.inputs);
+ }
+
+ if (!this.isConfiguration() && !this.selectedComponentIsServiceProxyInstance()) {
+ this.tabs.push(tabs.infoArtifacts);
+ }
+
+ if (!(component.isService()) || this.selectedComponentIsServiceProxyInstance()) {
+ this.tabs.push(tabs.reqAndCapabilities);
+ }
+
+ if (component.isService() && !this.selectedComponentIsServiceProxyInstance()) {
+ this.tabs.push(tabs.apiArtifacts);
+ }
+ if (component.isService() && this.selectedComponentIsServiceProxyInstance()) {
+ this.tabs.push(tabs.consumption);
+ this.tabs.push(tabs.dependencies);
+ }
+
+ }
+
+ private toggleSidebarDisplay = () => {
+ // this.withSidebar = !this.withSidebar;
+ this.store.dispatch(new OnSidebarOpenOrCloseAction());
+ }
+
+ private isPNF = (): boolean => {
+ return this.topologyTemplate.isResource() && (this.topologyTemplate as Resource).resourceType === ResourceType.PNF;
+ }
+
+ private isConfiguration = (): boolean => {
+ return this.topologyTemplate.isResource() && (this.topologyTemplate as Resource).resourceType === ResourceType.CONFIGURATION;
+ }
+
+ private isComponentInstanceSelected = (): boolean => {
+ return this.selectedComponent instanceof FullComponentInstance;
+ }
+
+ private selectedComponentIsServiceProxyInstance = (): boolean => {
+ return this.isComponentInstanceSelected() && this.selectedComponent.isServiceProxy();
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.module.ts b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.module.ts
new file mode 100644
index 0000000000..0fd1e51fa5
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.module.ts
@@ -0,0 +1,106 @@
+/*-
+ * ============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 { NgModule } from "@angular/core";
+import { FormsModule } from "@angular/forms";
+import { BrowserModule } from "@angular/platform-browser";
+import { CompositionPanelComponent } from "./composition-panel.component";
+import { CompositionPanelHeaderModule } from "app/ng2/pages/composition/panel/panel-header/panel-header.module";
+import { SdcUiComponentsModule, SdcUiServices } from "onap-ui-angular";
+// import { SdcUiServices } from "onap-ui-angular/";
+import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module';
+import { AddElementsModule } from "../../../components/ui/modal/add-elements/add-elements.module";
+import { TranslateModule } from "app/ng2/shared/translator/translate.module";
+import { InfoTabComponent } from './panel-tabs/info-tab/info-tab.component';
+import { PanelTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/panel-tab.component";
+import { ArtifactsTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component";
+import { PropertiesTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component";
+import { ReqAndCapabilitiesTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component";
+import { RequirementListComponent } from "app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component";
+import { PolicyTargetsTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component";
+import { GroupMembersTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component";
+import { GroupOrPolicyPropertiesTab } from "app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component";
+import { GlobalPipesModule } from "app/ng2/pipes/global-pipes.module";
+import {ModalModule} from "../../../components/ui/modal/modal.module";
+import {EnvParamsComponent} from "../../../components/forms/env-params/env-params.component";
+import {ModalsModule} from "../../../components/modals/modals.module";
+// import {EnvParamsModule} from "../../../components/forms/env-params/env-params.module";
+import { NgxDatatableModule } from "@swimlane/ngx-datatable";
+import {EnvParamsModule} from "../../../components/forms/env-params/env-params.module";
+import { ServiceConsumptionTabComponent } from "./panel-tabs/service-consumption-tab/service-consumption-tab.component";
+import { ServiceDependenciesTabComponent } from "./panel-tabs/service-dependencies-tab/service-dependencies-tab.component";
+import { ServiceDependenciesModule } from "../../../components/logic/service-dependencies/service-dependencies.module";
+import { ServiceConsumptionModule } from "../../../components/logic/service-consumption/service-consumption.module";
+
+
+
+@NgModule({
+ declarations: [
+ CompositionPanelComponent,
+ PolicyTargetsTabComponent,
+ GroupOrPolicyPropertiesTab,
+ GroupMembersTabComponent,
+ InfoTabComponent,
+ PanelTabComponent,
+ ArtifactsTabComponent,
+ PropertiesTabComponent,
+ ReqAndCapabilitiesTabComponent,
+ ServiceConsumptionTabComponent,
+ ServiceDependenciesTabComponent,
+ RequirementListComponent,
+ EnvParamsComponent
+ ],
+ imports: [
+ GlobalPipesModule,
+ BrowserModule,
+ FormsModule,
+ CompositionPanelHeaderModule,
+ SdcUiComponentsModule,
+ UiElementsModule,
+ AddElementsModule,
+ TranslateModule,
+ NgxDatatableModule,
+ ServiceDependenciesModule,
+ ServiceConsumptionModule
+ // EnvParamsModule
+ ],
+ entryComponents: [
+ CompositionPanelComponent,
+ PolicyTargetsTabComponent,
+ GroupOrPolicyPropertiesTab,
+ GroupMembersTabComponent,
+ InfoTabComponent,
+ ArtifactsTabComponent,
+ PropertiesTabComponent,
+ ReqAndCapabilitiesTabComponent,
+ ServiceConsumptionTabComponent,
+ ServiceDependenciesTabComponent,
+ RequirementListComponent,
+ PanelTabComponent,
+ EnvParamsComponent
+ ],
+ exports: [
+ CompositionPanelComponent
+ // EnvParamsModule
+ ],
+ providers: [SdcUiServices.ModalService]
+})
+export class CompositionPanelModule {
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.html
new file mode 100644
index 0000000000..75ee2d520f
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.html
@@ -0,0 +1,28 @@
+<!--
+ ~ 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="name-update-container">
+ <sdc-input #updateNameInput
+ label="Instance Name"
+ required="true"
+ [maxLength]="50"
+ [(value)]="name"
+ testId="instanceName"></sdc-input>
+ <sdc-validation [validateElement]="updateNameInput" (validityChanged)="validityChanged($event)">
+ <sdc-required-validator message="Name is required."></sdc-required-validator>
+ <sdc-regex-validator message="Special characters not allowed." [pattern]="pattern"></sdc-regex-validator>
+ </sdc-validation>
+</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.less
new file mode 100644
index 0000000000..b958ca17b7
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.less
@@ -0,0 +1,3 @@
+.name-update-container {
+ min-height: 90px;
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.ts
new file mode 100644
index 0000000000..9c4aab206e
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.ts
@@ -0,0 +1,25 @@
+import { Component, Input } from "@angular/core";
+
+@Component({
+ selector: 'edit-name-modal',
+ templateUrl: './edit-name-modal.component.html',
+ styleUrls: ['./edit-name-modal.component.less']
+})
+export class EditNameModalComponent {
+
+ @Input() name:String;
+ @Input() validityChangedCallback: Function;
+
+ private pattern:string = "^[\\s\\w\&_.:-]{1,1024}$"
+ constructor(){
+ }
+
+ private validityChanged = (value):void => {
+ if(this.validityChangedCallback) {
+ this.validityChangedCallback(value);
+ }
+ }
+
+
+
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.html
index 67c82389cc..d9c56198ea 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.html
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.html
@@ -1,30 +1,23 @@
-<!--
- ~ 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="component-details-panel-header" data-tests-id="w-sdc-designer-sidebar-head">
-
+<div *ngIf="selectedComponent" class="component-details-panel-header" data-tests-id="w-sdc-designer-sidebar-head">
<div class="icon">
- <div class="large {{iconClassName}}">
- <div [ngClass]="{'non-certified': nonCertified}" tooltip="Not certified"></div>
- </div>
+ <div *ngIf="iconClassName; else svgIcon" class="large {{iconClassName}}"></div>
+ <ng-template #svgIcon>
+ <sdc-element-icon
+ [elementType]="selectedComponent.componentType === 'RESOURCE' ? selectedComponent.resourceType: (selectedComponent.originType || selectedComponent.componentType)"
+ [iconName]="selectedComponent.icon"
+ [uncertified]="!isTopologyTemplateSelected && selectedComponent.lifecycleState && 'CERTIFIED' !== selectedComponent.lifecycleState"></sdc-element-icon>
+ </ng-template>
</div>
- <div class="title" data-tests-id="selectedCompTitle" tooltip="&#8203;{{name}}">{{name}}</div>
+ <div class="title" data-tests-id="selectedCompTitle" tooltip="&#8203;{{selectedComponent.name}}">
+ {{selectedComponent.name}}
+ </div>
+
+ <svg-icon-label *ngIf="!isViewOnly && !isTopologyTemplateSelected && !selectedComponent.archived" name="edit-file-o"
+ clickable="true" size="small" class="rename-instance" data-tests-id="renameInstance"
+ (click)="renameInstance()"></svg-icon-label>
+ <svg-icon-label *ngIf="!isViewOnly && !isTopologyTemplateSelected && !selectedComponent.archived" name="trash-o"
+ clickable="true" size="small" class="delete-instance" data-tests-id="deleteInstance"
+ (click)="deleteInstance()"></svg-icon-label>
- <svg-icon-label *ngIf="!isViewOnly" name="edit-file-o" clickable="true" size="small" class="rename-instance" data-tests-id="renameInstance" (click)="renameInstance()"></svg-icon-label>
- <svg-icon-label *ngIf="!isViewOnly" name="trash-o" clickable="true" size="small" class="delete-instance" data-tests-id="deleteInstance" (click)="deleteInstance()"></svg-icon-label>
-
</div> \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.less
index 9bbc765761..6685f74009 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.less
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.less
@@ -7,6 +7,7 @@
.icon {
margin: 0 20px;
+ display:flex;
}
.title {
@@ -31,4 +32,17 @@
cursor: pointer;
}
+
+ .non-certified {
+ position: absolute;
+ background-image: url('../../../../../../assets/styles/images/sprites/sprite-global-old.png');
+ background-position: -157px -3386px; width: 15px; height: 15px;
+
+ &.smaller-icon {
+ left: 35px;
+ bottom: -14px;
+ }
+ }
+
+
} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.spec.ts
new file mode 100644
index 0000000000..76e84a2323
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.spec.ts
@@ -0,0 +1,123 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed, async } from '@angular/core/testing';
+import { CompositionService } from 'app/ng2/pages/composition/composition.service';
+import { EventListenerService } from '../../../../../services/event-listener-service';
+import { ComponentInstanceServiceNg2 } from 'app/ng2/services/component-instance-services/component-instance.service';
+import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service';
+import { GroupsService } from 'app/services-ng2';
+import { PoliciesService } from 'app/services-ng2';
+import { CompositionPanelHeaderComponent } from './panel-header.component';
+import {SdcUiServices} from 'onap-ui-angular';
+import { Capability, Requirement, RequirementsGroup, CapabilitiesGroup, ComponentInstance, Component, FullComponentInstance, PolicyInstance, GroupInstance } from "app/models";
+import { of, Observable } from "rxjs";
+
+describe('CompositionPanelHeaderComponent', () => {
+ let component: CompositionPanelHeaderComponent;
+ let fixture: ComponentFixture<CompositionPanelHeaderComponent>;
+ const componentInstanceServiceNg2Stub = {
+ updateComponentInstance: jest.fn()
+ };
+ const valueEditModalInstance = {
+ innerModalContent : {
+ instance: { name : "VF Test" }
+ },
+ buttons: [{id: 'saveButton', text: 'OK', size: 'xsm', callback: jest.fn(), closeModal: false}],
+ closeModal : jest.fn()
+ };
+
+ beforeEach(
+ () => {
+ const compositionServiceStub = {};
+ const eventListenerServiceStub = {};
+
+ const workspaceServiceStub = {
+ metadata: {
+ componentType: "SERVICE",
+ uniqueId: "123"
+ }
+ };
+ const groupsServiceStub = {
+ updateName: jest.fn()
+ };
+ const policiesServiceStub = {
+ updateName: jest.fn()
+ };
+
+ TestBed.configureTestingModule({
+ schemas: [NO_ERRORS_SCHEMA],
+ declarations: [CompositionPanelHeaderComponent],
+ providers: [
+ { provide: CompositionService, useValue: compositionServiceStub },
+ { provide: EventListenerService, useValue: eventListenerServiceStub },
+ {
+ provide: ComponentInstanceServiceNg2,
+ useValue: componentInstanceServiceNg2Stub
+ },
+ { provide: WorkspaceService, useValue: workspaceServiceStub },
+ { provide: GroupsService, useValue: groupsServiceStub },
+ { provide: PoliciesService, useValue: policiesServiceStub },
+ { provide: SdcUiServices.ModalService, useValue: {}}
+ ]
+ });
+ fixture = TestBed.createComponent(CompositionPanelHeaderComponent);
+ component = fixture.componentInstance;
+ }
+ );
+
+ it('can load instance', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should close the modal without saving if the name has not changed', () => {
+ component.selectedComponent = <FullComponentInstance>{name: "VF Test"};
+ component.valueEditModalInstance = valueEditModalInstance;
+
+ component.saveInstanceName();
+ expect(component.componentInstanceService.updateComponentInstance).not.toHaveBeenCalled();
+ expect(component.valueEditModalInstance.closeModal).toHaveBeenCalled();
+ });
+
+ it('after editing instance name, capabilities/requirements should be updated with new name', () => {
+ const newName = "New VF NAME";
+ component.selectedComponent = new FullComponentInstance(<ComponentInstance>{
+ name: "VF Test",
+ requirements: <RequirementsGroup>{"key": [<Requirement>{ownerName: "VF Test"}, <Requirement>{ownerName: "VF Test"}]},
+ capabilities: new CapabilitiesGroup()
+ }, <Component>{});
+ component.selectedComponent.capabilities['key'] = [<Capability>{ownerName: "VF Test"}];
+ component.valueEditModalInstance = valueEditModalInstance;
+ component.valueEditModalInstance.innerModalContent.instance.name = newName;
+ jest.spyOn(component.componentInstanceService, 'updateComponentInstance').mockReturnValue(of(<ComponentInstance>{name: newName}));
+ component.saveInstanceName();
+
+ expect(component.selectedComponent.name).toBe(newName);
+ expect(component.selectedComponent.requirements['key'][0].ownerName).toEqual(newName);
+ expect(component.selectedComponent.requirements['key'][1].ownerName).toEqual(newName);
+ expect(component.selectedComponent.capabilities['key'][0].ownerName).toEqual(newName);
+ });
+
+ it('if update fails, name is reverted to old value', () => {
+ component.selectedComponent = new GroupInstance(<GroupInstance>{name: "GROUP NAME"});
+ component.valueEditModalInstance = valueEditModalInstance;
+ jest.spyOn(component.groupService, 'updateName').mockReturnValue(Observable.throw(new Error('Error')));
+ component.saveInstanceName();
+ expect(component.selectedComponent.name).toEqual("GROUP NAME");
+ });
+
+ it('policy instance uses policies service for update name', () => {
+ component.selectedComponent = new PolicyInstance(<PolicyInstance>{name: "Policy OLD NAME"});
+ component.valueEditModalInstance = valueEditModalInstance;
+ jest.spyOn(component.policiesService, 'updateName').mockReturnValue(of(true));
+ component.saveInstanceName();
+ expect(component.policiesService.updateName).toHaveBeenCalledTimes(1);
+ });
+
+ it('group instance uses groups service for update name', () => {
+ component.selectedComponent = new GroupInstance(<GroupInstance>{name: "GROUP NAME"});
+ component.valueEditModalInstance = valueEditModalInstance;
+ jest.spyOn(component.groupService, 'updateName').mockReturnValue(of(true));
+ component.saveInstanceName();
+ expect(component.groupService.updateName).toHaveBeenCalledTimes(1);
+ });
+
+});
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.ts
index ab659a3b8f..90a98147e9 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.ts
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.ts
@@ -18,64 +18,70 @@
* ============LICENSE_END=========================================================
*/
-import { Component, Input, AfterViewInit, SimpleChanges, OnInit, OnChanges } from "@angular/core";
-import { SdcUiComponents } from "sdc-ui/lib/angular";
-import { IModalConfig } from 'sdc-ui/lib/angular/modals/models/modal-config';
-import { ZoneInstanceType } from 'app/models/graph/zones/zone-instance';
-import { ValueEditComponent } from './../../../../components/ui/forms/value-edit/value-edit.component';
-import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models";
-import { PoliciesService } from '../../../../services/policies.service';
-import { GroupsService } from '../../../../services/groups.service';
-import {IZoneService} from "../../../../../models/graph/zones/zone";
-import { EventListenerService, LoaderService } from "../../../../../services";
-import { GRAPH_EVENTS, EVENTS } from "../../../../../utils";
+import { Component, Input, OnInit } from "@angular/core";
+import { SdcUiComponents, SdcUiCommon, SdcUiServices } from "onap-ui-angular";
+import { EditNameModalComponent } from "app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component";
+import {Component as TopologyTemplate, FullComponentInstance, GroupInstance, PolicyInstance, Requirement, Capability, ComponentInstance} from "app/models";
+import { Select } from "@ngxs/store";
+import { Observable } from "rxjs/Observable";
+import { Subscription } from "rxjs";
+import {GRAPH_EVENTS} from "../../../../../utils/constants";
+import { CompositionService } from "app/ng2/pages/composition/composition.service";
+import {EventListenerService} from "../../../../../services/event-listener-service";
+import { ComponentInstanceServiceNg2 } from "app/ng2/services/component-instance-services/component-instance.service";
+import { WorkspaceService } from "app/ng2/pages/workspace/workspace.service";
+import { GroupsService, PoliciesService } from "app/services-ng2";
import { UIZoneInstanceObject } from "../../../../../models/ui-models/ui-zone-instance-object";
-import { ModalButtonComponent } from "sdc-ui/lib/angular/components";
+import {SelectedComponentType} from "../../common/store/graph.actions";
+import * as _ from 'lodash';
+import {GraphState} from "../../common/store/graph.state";
+
@Component({
selector: 'ng2-composition-panel-header',
templateUrl: './panel-header.component.html',
styleUrls: ['./panel-header.component.less']
})
-export class CompositionPanelHeaderComponent implements OnInit, OnChanges {
-
- @Input() topologyTemplate: TopologyTemplate;
- @Input() selectedZoneInstanceType: ZoneInstanceType;
- @Input() selectedZoneInstanceId: string;
- @Input() name: string;
- @Input() nonCertified: boolean;
+export class CompositionPanelHeaderComponent implements OnInit {
@Input() isViewOnly: boolean;
- @Input() isLoading: boolean;
+ @Input() selectedComponent: FullComponentInstance | TopologyTemplate | GroupInstance | PolicyInstance;
+ @Select(GraphState.getSelectedComponentType) selectedComponentType$:Observable<SelectedComponentType>;
+
- constructor(private groupsService:GroupsService, private policiesService: PoliciesService,
- private modalService:SdcUiComponents.ModalService, private eventListenerService:EventListenerService) { }
+ constructor(private modalService: SdcUiServices.ModalService,
+ private groupService: GroupsService,
+ private policiesService: PoliciesService,
+ private eventListenerService: EventListenerService,
+ private compositionService: CompositionService,
+ private workspaceService: WorkspaceService,
+ private componentInstanceService: ComponentInstanceServiceNg2) { }
- private service:IZoneService;
private iconClassName: string;
+ private valueEditModalInstance: SdcUiComponents.ModalComponent;
+ private isTopologyTemplateSelected: boolean;
+ private componentTypeSubscription: Subscription;
ngOnInit(): void {
- this.init();
- }
+ this.componentTypeSubscription = this.selectedComponentType$.subscribe((newComponentType) => {
- ngOnChanges (changes:SimpleChanges):void {
- if(changes.selectedZoneInstanceId){
- this.init();
- }
+ this.initClasses(newComponentType);
+ this.isTopologyTemplateSelected = (newComponentType === SelectedComponentType.TOPOLOGY_TEMPLATE) ? true : false;
+ });
}
ngOnDestroy() {
-
-
+ if(this.componentTypeSubscription) {
+ this.componentTypeSubscription.unsubscribe();
+ }
}
- private init = (): void => {
- if (this.selectedZoneInstanceType === ZoneInstanceType.POLICY) {
+
+ private initClasses = (componentType:SelectedComponentType): void => {
+ if (componentType === SelectedComponentType.POLICY) {
this.iconClassName = "sprite-policy-icons policy";
- this.service = this.policiesService;
- } else if (this.selectedZoneInstanceType === ZoneInstanceType.GROUP) {
+ } else if (componentType === SelectedComponentType.GROUP) {
this.iconClassName = "sprite-group-icons group";
- this.service = this.groupsService;
} else {
- this.iconClassName = "sprite-resource-icons defaulticon";
+ this.iconClassName = undefined;
}
}
@@ -83,53 +89,95 @@ export class CompositionPanelHeaderComponent implements OnInit, OnChanges {
const modalConfig = {
title: "Edit Name",
size: "sm",
- type: "custom",
+ type: SdcUiCommon.ModalType.custom,
testId: "renameInstanceModal",
buttons: [
{id: 'saveButton', text: 'OK', size: 'xsm', callback: this.saveInstanceName, closeModal: false},
- {id: 'cancelButton', text: 'Cancel', size: 'sm', closeModal: true}
- ] as ModalButtonComponent[]
- } as IModalConfig;
- this.modalService.openCustomModal(modalConfig, ValueEditComponent, {name: this.name, validityChangedCallback: this.enableOrDisableSaveButton});
+ {id: 'cancelButton', text: 'Cancel', size: 'sm', closeModal: true}
+ ] as SdcUiCommon.IModalButtonComponent[]
+ } as SdcUiCommon.IModalConfig;
+ this.valueEditModalInstance = this.modalService.openCustomModal(modalConfig, EditNameModalComponent, {name: this.selectedComponent.name, validityChangedCallback: this.enableOrDisableSaveButton});
};
private enableOrDisableSaveButton = (shouldEnable: boolean): void => {
- let saveButton: ModalButtonComponent = this.modalService.getCurrentInstance().getButtonById('saveButton');
+ let saveButton: SdcUiComponents.ModalButtonComponent = this.valueEditModalInstance.getButtonById('saveButton');
saveButton.disabled = !shouldEnable;
}
private saveInstanceName = ():void => {
- let currentModal = this.modalService.getCurrentInstance();
- let nameFromModal:string = currentModal.innerModalContent.instance.name;
-
- if(nameFromModal != this.name){
- currentModal.buttons[0].disabled = true;
- this.service.updateName(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.selectedZoneInstanceId, nameFromModal).subscribe((success)=>{
- this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_ZONE_INSTANCE_NAME_CHANGED, nameFromModal);
- this.modalService.closeModal();
- }, (error)=> {
- currentModal.buttons[0].disabled = false;
- });
- } else {
- this.modalService.closeModal();
+ let nameFromModal:string = this.valueEditModalInstance.innerModalContent.instance.name;
+
+ if(nameFromModal != this.selectedComponent.name){
+ let oldName = this.selectedComponent.name;
+ this.selectedComponent.name = nameFromModal;
+ this.valueEditModalInstance.buttons[0].disabled = true;
+
+ let onFailed = (error) => {
+ this.selectedComponent.name = oldName;
+ this.valueEditModalInstance.buttons[0].disabled = false;
+ };
+
+ if(this.selectedComponent instanceof FullComponentInstance){
+ let onSuccess = (componentInstance:ComponentInstance) => {
+ //update requirements and capabilities owner name
+ _.forEach((<FullComponentInstance>this.selectedComponent).requirements, (requirementsArray:Array<Requirement>) => {
+ _.forEach(requirementsArray, (requirement:Requirement):void => {
+ requirement.ownerName = componentInstance.name;
+ });
+ });
+
+ _.forEach((<FullComponentInstance>this.selectedComponent).capabilities, (capabilitiesArray:Array<Capability>) => {
+ _.forEach(capabilitiesArray, (capability:Capability):void => {
+ capability.ownerName = componentInstance.name;
+ });
+ });
+ this.valueEditModalInstance.closeModal();
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_COMPONENT_INSTANCE_NAME_CHANGED, this.selectedComponent);
+ };
+
+ this.componentInstanceService.updateComponentInstance(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, new ComponentInstance(this.selectedComponent))
+ .subscribe(onSuccess, onFailed);
+ } else if (this.selectedComponent instanceof PolicyInstance) {
+ this.policiesService.updateName(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.selectedComponent.uniqueId, nameFromModal).subscribe((success)=>{
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, this.selectedComponent);
+ this.valueEditModalInstance.closeModal();
+ }, onFailed);
+ } else if (this.selectedComponent instanceof GroupInstance){
+ this.groupService.updateName(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.selectedComponent.uniqueId, nameFromModal).subscribe((success)=>{
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.selectedComponent);
+ this.valueEditModalInstance.closeModal();
+ }, onFailed);
+ }
+ } else {
+ this.valueEditModalInstance.closeModal();
}
};
-
+
private deleteInstance = (): void => {
let title:string = "Delete Confirmation";
- let message:string = "Are you sure you would like to delete "+ this.name + "?";
- this.modalService.openAlertModal(title, message, "OK", this.deleteInstanceConfirmed, "deleteInstanceModal");
+ let message:string = "Are you sure you would like to delete "+ this.selectedComponent.name + "?";
+ const okButton = {testId: "OK", text: "OK", type: SdcUiCommon.ButtonType.warning, callback: this.deleteInstanceConfirmed, closeModal: true} as SdcUiComponents.ModalButtonComponent;
+ this.modalService.openWarningModal(title, message, "delete-modal", [okButton]);
};
- private deleteInstanceConfirmed = () => {
- this.eventListenerService.notifyObservers(EVENTS.SHOW_LOADER_EVENT + 'composition-graph');
- this.service.deleteZoneInstance(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.selectedZoneInstanceId).finally(()=> {
- this.eventListenerService.notifyObservers(EVENTS.HIDE_LOADER_EVENT + 'composition-graph');
- }).subscribe(()=> {
- let deletedItem:UIZoneInstanceObject = new UIZoneInstanceObject(this.selectedZoneInstanceId, this.selectedZoneInstanceType, this.name);
- this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_ZONE_INSTANCE, deletedItem);
- });
- };
+ private deleteInstanceConfirmed: Function = () => {
+ if(this.selectedComponent instanceof FullComponentInstance){
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE , this.selectedComponent.uniqueId);
+ }
+ else if(this.selectedComponent instanceof PolicyInstance){
+ this.policiesService.deletePolicy(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.selectedComponent.uniqueId).subscribe((success)=>{
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_ZONE_INSTANCE ,
+ new UIZoneInstanceObject(this.selectedComponent.uniqueId, 1));
+ }, (err) => {});
+
+ }
+ else if(this.selectedComponent instanceof GroupInstance){
+ this.groupService.deleteGroup(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.selectedComponent.uniqueId).subscribe((success)=>{
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_ZONE_INSTANCE ,
+ new UIZoneInstanceObject(this.selectedComponent.uniqueId, 0));
+ }, (err) => {});
+ }
+ };
}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.module.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.module.ts
index bde0a14669..a11bc99fee 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.module.ts
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.module.ts
@@ -18,29 +18,26 @@
* ============LICENSE_END=========================================================
*/
import { NgModule } from "@angular/core";
-import { HttpModule } from "@angular/http";
import { FormsModule } from "@angular/forms";
import { BrowserModule } from "@angular/platform-browser";
import { CompositionPanelHeaderComponent } from "./panel-header.component";
import { UiElementsModule } from './../../../../components/ui/ui-elements.module';
-import { ValueEditComponent } from './../../../../components/ui/forms/value-edit/value-edit.component';
-import { SdcUiComponentsModule } from "sdc-ui/lib/angular";
-import { ModalFormsModule } from "app/ng2/components/ui/forms/modal-forms.module";
+import { SdcUiComponentsModule } from "onap-ui-angular";
+import { EditNameModalComponent } from "app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component";
@NgModule({
declarations: [
- CompositionPanelHeaderComponent
+ CompositionPanelHeaderComponent,
+ EditNameModalComponent
],
imports: [
BrowserModule,
FormsModule,
- HttpModule,
UiElementsModule,
- SdcUiComponentsModule,
- ModalFormsModule
+ SdcUiComponentsModule
],
entryComponents: [
- CompositionPanelHeaderComponent, ValueEditComponent
+ CompositionPanelHeaderComponent, EditNameModalComponent
],
exports: [
CompositionPanelHeaderComponent
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/__snapshots__/artifact-tab.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/__snapshots__/artifact-tab.component.spec.ts.snap
new file mode 100644
index 0000000000..c143e8106b
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/__snapshots__/artifact-tab.component.spec.ts.snap
@@ -0,0 +1,50 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`artifact-tab component should match current snapshot of artifact-tab component 1`] = `
+<artifacts-tab
+ addOrUpdate={[Function Function]}
+ allowDeleteAndUpdateArtifact={[Function Function]}
+ artifactService={[Function Object]}
+ componentInstanceService="undefined"
+ compositionService={[Function Object]}
+ delete={[Function Function]}
+ getEnvArtifact={[Function Function]}
+ getTitle={[Function Function]}
+ heatToEnv={[Function Map]}
+ isLicenseArtifact={[Function Function]}
+ loadArtifacts={[Function Function]}
+ store={[Function Store]}
+ topologyTemplateService="undefined"
+ updateEnvParams={[Function Function]}
+ viewEnvParams={[Function Function]}
+ workspaceService="undefined"
+>
+ <div
+ class="w-sdc-designer-sidebar-tab-content artifacts"
+ >
+ <div
+ class="w-sdc-designer-sidebar-section"
+ >
+ <ng2-expand-collapse
+ state="0"
+ >
+ <header
+ sdc-tooltip=""
+ >
+
+ </header>
+ <content
+ class="artifacts-container"
+ >
+ <div
+ class="w-sdc-designer-sidebar-section-content"
+ >
+
+ </div>
+
+ </content>
+ </ng2-expand-collapse>
+ </div>
+ </div>
+</artifacts-tab>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifact-tab.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifact-tab.component.spec.ts
new file mode 100644
index 0000000000..258f2295ab
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifact-tab.component.spec.ts
@@ -0,0 +1,303 @@
+import { async, ComponentFixture } from '@angular/core/testing';
+import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper';
+import { NgxsModule, Store } from '@ngxs/store';
+import { WorkspaceState } from '../../../../../store/states/workspace.state';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ArtifactsTabComponent } from './artifacts-tab.component';
+import { CompositionService } from '../../../composition.service';
+import { WorkspaceService } from '../../../../workspace/workspace.service';
+import { ComponentInstanceServiceNg2 } from '../../../../../services/component-instance-services/component-instance.service';
+import { TopologyTemplateService } from '../../../../../services/component-services/topology-template.service';
+import { ArtifactsService } from '../../../../../components/forms/artifacts-form/artifacts.service';
+import { ArtifactModel } from '../../../../../../models/artifacts';
+import { ArtifactType } from '../../../../../../utils/constants';
+import { FullComponentInstance } from '../../../../../../models/componentsInstances/fullComponentInstance';
+import { ComponentInstance } from '../../../../../../models/componentsInstances/componentInstance';
+import { Component } from '../../../../../../models/components/component';
+import { GetInstanceArtifactsByTypeAction } from '../../../../../store/actions/instance-artifacts.actions';
+import { Observable } from 'rxjs';
+
+
+describe('artifact-tab component', () => {
+
+ let fixture: ComponentFixture<ArtifactsTabComponent>;
+ let compositionMockService: Partial<CompositionService>;
+ const workspaceMockService: Partial<WorkspaceService>;
+ const componentInstanceMockService: Partial<ComponentInstanceServiceNg2>;
+ const topologyTemplateMockService: Partial<TopologyTemplateService>;
+ let artifactsServiceMockService: Partial<ArtifactsService>;
+ let store: Store;
+
+ beforeEach(
+ async(() => {
+ compositionMockService = {
+ updateInstance: jest.fn()
+ }
+
+ artifactsServiceMockService = {
+ deleteArtifact: jest.fn(),
+ openUpdateEnvParams: jest.fn(),
+ openArtifactModal: jest.fn()
+ }
+
+ const configure: ConfigureFn = (testBed) => {
+ testBed.configureTestingModule({
+ declarations: [ArtifactsTabComponent],
+ imports: [NgxsModule.forRoot([WorkspaceState])],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: CompositionService, useValue: compositionMockService},
+ {provide: WorkspaceService, useValue: workspaceMockService},
+ {provide: ComponentInstanceServiceNg2, useValue: componentInstanceMockService},
+ {provide: TopologyTemplateService, useValue: topologyTemplateMockService},
+ {provide: ArtifactsService, useValue: artifactsServiceMockService}
+ ],
+ });
+ };
+
+ configureTests(configure).then((testBed) => {
+ fixture = testBed.createComponent(ArtifactsTabComponent);
+ store = testBed.get(Store);
+ });
+ })
+ );
+
+ it ('on delete -> deleteArtifact is being called from artifactService', () => {
+ const artifact = new ArtifactModel();
+ const topologyTemplateType: string = undefined;
+ const topologyTemplateId: string = undefined;
+
+ fixture.componentInstance.delete(artifact);
+ expect(artifactsServiceMockService.deleteArtifact).toHaveBeenCalledWith(topologyTemplateType, topologyTemplateId, artifact);
+ });
+
+ it('should match current snapshot of artifact-tab component', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+
+ it ('should get API Artifacts as Title', () => {
+ const artifactType = ArtifactType.SERVICE_API;
+
+ const res = fixture.componentInstance.getTitle(artifactType);
+ expect(res).toBe('API Artifacts');
+ });
+
+
+ it ('should get Deployment Artifacts as Title', () => {
+ const artifactType = ArtifactType.DEPLOYMENT;
+
+ const res = fixture.componentInstance.getTitle(artifactType);
+ expect(res).toBe('Deployment Artifacts');
+ });
+
+ it ('should get Informational Artifacts as Title', () => {
+ const artifactType = ArtifactType.INFORMATION;
+
+ const res = fixture.componentInstance.getTitle(artifactType);
+ expect(res).toBe('Informational Artifacts');
+ });
+
+ it ('should get SomeString as Title - This is the default case (return the last val)', () => {
+ // So the last value will be "SomeString"
+ fixture.componentInstance.getTitle('SomeString');
+
+ const res = fixture.componentInstance.getTitle('SomeString');
+ expect(res).toBe('SomeString Artifacts');
+ });
+
+
+ it ('should return isLicenseArtifact false', () => {
+ const artifact = new ArtifactModel();
+ const componentInstance = new ComponentInstance();
+ const component = new Component();
+ fixture.componentInstance.component = new FullComponentInstance(componentInstance, component);
+
+ let res = fixture.componentInstance.isLicenseArtifact(artifact);
+ expect(res).toBe(false);
+ });
+
+ it ('should return isLicenseArtifact true', () => {
+ const artifact = new ArtifactModel();
+ const componentInstance = new ComponentInstance();
+ const component = new Component();
+ fixture.componentInstance.component = new FullComponentInstance(componentInstance, component);
+ fixture.componentInstance.component.isResource = jest.fn(() => true);
+ fixture.componentInstance.component.isCsarComponent = true;
+
+ artifact.artifactType = ArtifactType.VENDOR_LICENSE;
+ const res = fixture.componentInstance.isLicenseArtifact(artifact);
+ expect(res).toBe(true);
+ });
+
+ it ('should verify getEnvArtifact with match', () => {
+ const artifact = new ArtifactModel();
+ artifact.uniqueId = 'matchUniqueID';
+
+ const testItem1 = new ArtifactModel();
+ testItem1.generatedFromId = 'matchUniqueID';
+
+ const testItem2 = new ArtifactModel();
+ testItem2.generatedFromId = '123456';
+
+ const artifacts: ArtifactModel[] = [testItem1, testItem2];
+
+ const res = fixture.componentInstance.getEnvArtifact(artifact, artifacts);
+ expect(res.generatedFromId).toBe('matchUniqueID');
+ });
+
+ it ('should verify getEnvArtifact with no match', () => {
+ const artifact = new ArtifactModel();
+ artifact.uniqueId = 'matchUniqueID';
+
+ const testItem1 = new ArtifactModel();
+ testItem1.generatedFromId = '654321';
+
+ const testItem2 = new ArtifactModel();
+ testItem2.generatedFromId = '123456';
+
+ const artifacts: ArtifactModel[] = [testItem1, testItem2];
+
+ const res = fixture.componentInstance.getEnvArtifact(artifact, artifacts);
+ expect(res).toBe(undefined);
+ });
+
+ it ('on updateEnvParams -> openUpdateEnvParams is being called from artifactService when isComponentInstanceSelected = true', () => {
+ const artifact = new ArtifactModel();
+ artifact.envArtifact = new ArtifactModel();
+
+ const topologyTemplateType: string = undefined;
+ const topologyTemplateId: string = undefined;
+
+ const component = new Component();
+ component.uniqueId = 'id';
+
+ const isComponentInstanceSelected = true;
+
+ fixture.componentInstance.component = component;
+ fixture.componentInstance.isComponentInstanceSelected = isComponentInstanceSelected;
+ fixture.componentInstance.updateEnvParams(artifact);
+
+ expect(artifactsServiceMockService.openUpdateEnvParams).toHaveBeenCalledWith(topologyTemplateType, topologyTemplateId, undefined, component.uniqueId);
+ });
+
+ it ('on updateEnvParams -> openUpdateEnvParams is being called from artifactService when isComponentInstanceSelected = false', () => {
+ const artifact = new ArtifactModel();
+
+ const topologyTemplateType: string = undefined
+ const topologyTemplateId: string = undefined;
+
+ const component = new Component();
+
+ const isComponentInstanceSelected = false;
+
+ fixture.componentInstance.component = component;
+ fixture.componentInstance.isComponentInstanceSelected = isComponentInstanceSelected;
+ fixture.componentInstance.updateEnvParams(artifact);
+
+ expect(artifactsServiceMockService.openUpdateEnvParams).toHaveBeenCalledWith(topologyTemplateType, topologyTemplateId, artifact);
+ });
+
+ it ('on addOrUpdate -> openArtifactModal is being called from artifactService when isComponentInstanceSelected = true', () => {
+ const artifact = new ArtifactModel();
+
+ const topologyTemplateType: string = 'testType';
+ const topologyTemplateId: string = 'testID';
+ const type: string = 'testType';
+ const isViewOnly: boolean = false;
+
+ const component = new Component();
+ component.uniqueId = 'id';
+
+ const isComponentInstanceSelected = true;
+
+ fixture.componentInstance.component = component;
+ fixture.componentInstance.type = type;
+ fixture.componentInstance.topologyTemplateId = topologyTemplateId;
+ fixture.componentInstance.topologyTemplateType = topologyTemplateType;
+ fixture.componentInstance.isComponentInstanceSelected = isComponentInstanceSelected;
+ fixture.componentInstance.isViewOnly = isViewOnly;
+ fixture.componentInstance.addOrUpdate(artifact);
+
+
+ expect(artifactsServiceMockService.openArtifactModal).toHaveBeenCalledWith(topologyTemplateId, topologyTemplateType, artifact, type, isViewOnly, component.uniqueId);
+ });
+
+ it ('on addOrUpdate -> openArtifactModal is being called from artifactService when isComponentInstanceSelected = false', () => {
+ const artifact = new ArtifactModel();
+
+ const topologyTemplateType: string = 'testType';
+ const topologyTemplateId: string = 'testID';
+ const type: string = 'testType';
+ const isViewOnly: boolean = false;
+
+ const isComponentInstanceSelected = false;
+
+ fixture.componentInstance.type = type;
+ fixture.componentInstance.isComponentInstanceSelected = isComponentInstanceSelected;
+ fixture.componentInstance.topologyTemplateId = topologyTemplateId;
+ fixture.componentInstance.topologyTemplateType = topologyTemplateType;
+ fixture.componentInstance.isViewOnly = isViewOnly;
+ fixture.componentInstance.addOrUpdate(artifact);
+
+ expect(artifactsServiceMockService.openArtifactModal).toHaveBeenCalledWith(topologyTemplateId, topologyTemplateType, artifact, type, isViewOnly);
+ });
+
+
+ it ('verify allowDeleteAndUpdateArtifact return false since isViewOnly=true', () => {
+ const artifact = new ArtifactModel();
+ fixture.componentInstance.isViewOnly = true;
+
+ const res = fixture.componentInstance.allowDeleteAndUpdateArtifact(artifact);
+ expect(res).toBe(false)
+ });
+
+ it ('verify allowDeleteAndUpdateArtifact return artifact.isFromCsar since isViewOnly=false && artifactGroupType = DEPLOYMENT', () => {
+ const artifact = new ArtifactModel();
+ artifact.artifactGroupType = ArtifactType.DEPLOYMENT;
+ artifact.isFromCsar = false;
+
+ fixture.componentInstance.isViewOnly = false;
+
+ const res = fixture.componentInstance.allowDeleteAndUpdateArtifact(artifact);
+ expect(res).toBe(!artifact.isFromCsar);
+ });
+
+ it ('verify allowDeleteAndUpdateArtifact return !artifact.isHEAT() && !artifact.isThirdParty() &&' +
+ ' !this.isLicenseArtifact(artifact) since isViewOnly=false && artifactGroupType != DEPLOYMENT', () => {
+ const artifact = new ArtifactModel();
+ artifact.artifactGroupType = 'NOT_DEPLOYMENT';
+ artifact.isHEAT = () => false;
+ artifact.isThirdParty = () => false;
+
+ fixture.componentInstance.isLicenseArtifact = jest.fn(() => false);
+
+ fixture.componentInstance.isViewOnly = false;
+
+ const res = fixture.componentInstance.allowDeleteAndUpdateArtifact(artifact);
+ expect(res).toBe(true )
+ });
+
+ it('verify action on loadArtifacts in case isComponentInstanceSelected = true', () => {
+ fixture.componentInstance.isComponentInstanceSelected = true;
+ fixture.componentInstance.topologyTemplateType = 'topologyTemplateType';
+ fixture.componentInstance.topologyTemplateId = 'topologyTemplateId';
+ const component = new Component();
+ component.uniqueId = 'uniqueId';
+ fixture.componentInstance.component = component;
+ fixture.componentInstance.type = 'type';
+
+ const action = new GetInstanceArtifactsByTypeAction(({
+ componentType: 'topologyTemplateType',
+ componentId: 'topologyTemplateId',
+ instanceId: 'uniqueId',
+ artifactType: 'type'
+ }))
+
+ fixture.componentInstance.store.dispatch = jest.fn(() => Observable.of(true));
+ fixture.componentInstance.loadArtifacts();
+
+ expect(store.dispatch).toBeCalledWith(action);
+
+ });
+});
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.html
new file mode 100644
index 0000000000..264444b674
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.html
@@ -0,0 +1,119 @@
+<div class="w-sdc-designer-sidebar-tab-content artifacts">
+ <div class="w-sdc-designer-sidebar-section">
+ <ng2-expand-collapse state="0">
+ <header sdc-tooltip tooltip-text="{{title}}">{{title}}</header>
+ <content class="artifacts-container">
+ <div class="w-sdc-designer-sidebar-section-content">
+ <div class="i-sdc-designer-sidebar-section-content-item" *ngFor="let artifact of artifacts$ | async">
+ <div class="i-sdc-designer-sidebar-section-content-item-artifact"
+ *ngIf="(!isComponentInstanceSelected || artifact.esId) && 'HEAT_ENV' !== artifact.artifactType"
+ attr.data-tests-id="'artifact-item-' + artifact.artifactDisplayName">
+ <span *ngIf="artifact.heatParameters?.length"
+ class="i-sdc-designer-sidebar-section-content-item-file-link"></span>
+ <div class="i-sdc-designer-sidebar-section-content-item-artifact-details"
+ [class.heat]="artifact.isHEAT() && artifact.heatParameters?.length">
+ <div *ngIf="artifact.artifactName"
+ class="i-sdc-designer-sidebar-section-content-item-artifact-filename"
+ attr.data-tests-id="artifactName-{{artifact.artifactDisplayName}}"
+ sdc-tooltip tooltip-text="{{artifact.artifactName}}">{{artifact.artifactName}}
+ </div>
+ <div class="artifact-buttons-container upper-buttons">
+
+
+ <svg-icon
+ *ngIf="!isViewOnly && !artifact.isFromCsar && artifact.artifactName"
+ name="trash-o" clickable="true" size="medium" mode="info"
+ class="artifact-button" testId="delete_{{artifact.artifactDisplayName}}"
+ (click)="delete(artifact)"></svg-icon>
+
+ <!--Display env parameters edit button for Instance -->
+ <svg-icon
+ *ngIf="!isViewOnly && artifact.isHEAT() && isComponentInstanceSelected && artifact.heatParameters?.length"
+ name="indesign_status" clickable="true" size="medium" mode="info"
+ class="artifact-button"
+ testId="edit-parameters-of-{{artifact.artifactDisplayName}}"
+ (click)="updateEnvParams(artifact)"
+ tooltip="Edit ENV Params"
+ ></svg-icon>
+
+ <!--Display env parameters VIEW button for Instance -->
+ <svg-icon
+ *ngIf="isViewOnly && artifact.isHEAT() && isComponentInstanceSelected && artifact.heatParameters?.length"
+ name="inputs-o" clickable="true" size="medium" mode="info"
+ class="artifact-button"
+ testId="view-parameters-of-{{artifact.artifactDisplayName}}"
+ (click)="viewEnvParams(artifact)"
+ tooltip="View ENV Params"
+ ></svg-icon>
+
+ <!--Display env parameters edit button for VF -->
+ <svg-icon
+ *ngIf = "!isViewOnly && !isComponentInstanceSelected && artifact.heatParameters?.length"
+ name="indesign_status" clickable="true" size="medium" mode="info"
+ class="artifact-button"
+ testId="edit-parameters-of-{{artifact.artifactDisplayName}}"
+ (click)="updateEnvParams(artifact)"></svg-icon>
+
+
+ <download-artifact *ngIf="artifact.esId && 'deployment' != type"
+ class="artifact-button"
+ [artifact]="artifact" [componentType]="component.componentType"
+ [componentId]="component.uniqueId"
+ testId="download_{{artifact.artifactDisplayName}}"
+ [isInstance]="isComponentInstanceSelected"></download-artifact>
+ <download-artifact *ngIf="artifact.esId && 'deployment' == type"
+ class="artifact-button"
+ [artifact]="artifact" [componentType]="component.componentType"
+ [componentId]="component.uniqueId"
+ [isInstance]="isComponentInstanceSelected"
+ testId="download_{{artifact.artifactDisplayName}}"
+ [showLoader]="artifact.isHEAT()"></download-artifact>
+
+ <button *ngIf="!isViewOnly && !artifact.esId && type==='deployment' && !isComponentInstanceSelected && !artifact.isThirdParty()"
+ class="artifact-button attach sprite e-sdc-small-icon-upload"
+ (click)="addOrUpdate(artifact)" type="button"
+ attr.data-tests-id="add_Artifact"></button>
+ </div>
+ <div>
+ <span class="i-sdc-designer-sidebar-section-content-item-artifact-details-name"
+ attr.data-tests-id="artifact_Display_Name-{{artifact.artifactDisplayName}}"
+ [ngClass]="{'hand enabled': artifact.allowDeleteAndUpdate}"
+ (click)="artifact.allowDeleteAndUpdate && addOrUpdate(artifact)"
+ sdc-tooltip tooltip-text="{{artifact.artifactDisplayName}}">{{artifact.artifactDisplayName}}</span>
+ <div class="i-sdc-designer-sidebar-section-content-item-artifact-heat-env"
+ *ngIf="artifact.heatParameters?.length">
+ <span attr.data-tests-id="heat_env_{{artifact.artifactDisplayName}}">{{artifact.artifactDisplayName}} (ENV)</span>
+ <div class="artifact-buttons-container">
+ <svg-icon *ngIf="!isViewOnly && envArtifactOf(artifact)"
+ name="edit-o" clickable="true" size="medium"
+ mode="info" class="artifact-button edit-pencil"
+ testId="edit_{{artifact.artifactDisplayName}}"
+ (click)="addOrUpdate(envArtifactOf(artifact))"></svg-icon>
+
+ <download-artifact [artifact]="envArtifactOf(artifact)"
+ class="artifact-button"
+ [componentType]="component.componentType"
+ [componentId]="component.uniqueId"
+ [isInstance]="isComponentInstanceSelected"
+ testId="download_env_{{artifact.artifactDisplayName}}"></download-artifact>
+ </div>
+ </div>
+ </div>
+
+ <div class="i-sdc-designer-sidebar-section-content-item-artifact-details-desc">
+ <span class="i-sdc-designer-sidebar-section-content-item-artifact-details-desc-label"
+ *ngIf="artifact.description">Description:</span>{{artifact.description}}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="w-sdc-designer-sidebar-section-footer"
+ *ngIf="!isViewOnly && type!=='api' && (!isComponentInstanceSelected || isVfOrPnf() && (type !== 'deployment') || isComplex)">
+ <sdc-button testId="add_Artifact_Button" size="large" type="primary" text="Add Artifact"
+ (click)="addOrUpdate({})"></sdc-button>
+ </div>
+ </content>
+ </ng2-expand-collapse>
+ </div>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.less
new file mode 100644
index 0000000000..fef199dd97
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.less
@@ -0,0 +1,169 @@
+@import '../../../../../../../assets/styles/override';
+
+
+.artifacts /deep/ .expand-collapse-content {
+ padding: 10px 0px;
+
+ &.collapsed {
+ padding: 0 0;
+ }
+}
+
+.i-sdc-designer-sidebar-section-content-item-artifact {
+
+ &:not(:hover) .artifact-button {
+ display:none;
+ }
+ .artifact-buttons-container {
+ display: inline-flex;
+ flex-direction: row-reverse;
+ position: absolute;
+ right:0;
+
+ &.upper-buttons {
+ margin-top: 8px;
+ }
+
+ .artifact-button {
+ cursor:pointer;
+ padding-right:5px;
+
+ &.edit-pencil {
+ margin-top: 10px;
+ }
+ }
+ }
+}
+
+.w-sdc-designer-sidebar-section-footer {
+ padding: 20px;
+ display: flex;
+ justify-content: center;
+
+}
+
+
+.w-sdc-designer-sidebar-tab-content.artifacts {
+
+ .i-sdc-designer-sidebar-section-content-item-artifact.hand {
+ cursor: pointer;
+ }
+
+ .w-sdc-designer-sidebar-section-content {
+ padding: 0;
+ }
+ .w-sdc-designer-sidebar-section-title {
+ &.expanded {
+ margin-bottom: 0;
+ }
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-artifact-details {
+ display: inline-block;
+ margin-left: 5px;
+ vertical-align: middle;
+ width: 180px;
+ &.heat {
+ line-height: 18px;
+ width: 250px;
+ }
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-artifact-details-name {
+
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width:220px;
+ display: inline-block;
+ //text-transform: capitalize;
+ &.enabled {
+ &:hover {
+ color: @sdcui_color_dark-blue;
+ }
+ }
+
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-artifact-heat-env {
+ color: @sdcui_color_dark-gray;
+ margin-top: 6px;
+ line-height: 42px;
+ padding-top: 10px;
+ border-top:1px solid #c8cdd1;
+ .enabled {
+ &:hover {
+ cursor: pointer;
+ color: @sdcui_color_dark-blue;
+ }
+ }
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-artifact-filename {
+ color: @sdcui_color_dark-gray;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 225px;
+ display: inline-block;
+ font-weight: bold;
+ &.enabled {
+ &:hover {
+ color: @sdcui_color_dark-blue;
+ }
+ }
+ }
+
+
+ .i-sdc-designer-sidebar-section-content-item-file-link{
+ border-left: 1px #848586 solid;
+ height: 58px;
+ margin-left: -11px;
+ margin-top: 11px;
+ border-top: 1px #848586 solid;
+ border-bottom: 1px #848586 solid;
+ width: 12px;
+ float: left;
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-artifact-details-desc {
+ display: none;
+ line-height: 16px;
+ word-wrap: break-word;
+ white-space: normal;
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-artifact-details-desc-label {
+ color: @sdcui_color_dark-gray;
+ }
+
+
+ .i-sdc-designer-sidebar-section-content-item-artifact {
+ border-bottom: 1px solid #c8cdd1;
+ padding: 5px 10px 5px 18px;
+ position: relative;
+ // line-height: 36px;
+ min-height: 61px;
+ //cursor: default;
+ display: flex;
+ align-items: center;
+
+
+ .i-sdc-designer-sidebar-section-content-item-button {
+ top: 20px;
+ line-height: 10px;
+ }
+
+ &:hover {
+ //background-color: @color_c;
+ background-color: white;
+ transition: all .3s;
+
+ .i-sdc-designer-sidebar-section-content-item-button {
+ display: block;
+
+ }
+
+ }
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.ts
new file mode 100644
index 0000000000..53a6c267e2
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.ts
@@ -0,0 +1,204 @@
+import { Component, Input } from '@angular/core';
+import { Store } from '@ngxs/store';
+import { ArtifactModel, Component as TopologyTemplate, FullComponentInstance, Resource } from 'app/models';
+import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service';
+import { ResourceNamePipe } from 'app/ng2/pipes/resource-name.pipe';
+import { ComponentInstanceServiceNg2 } from 'app/ng2/services/component-instance-services/component-instance.service';
+import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service';
+import { ArtifactType } from 'app/utils';
+import * as _ from 'lodash';
+import { SdcUiServices } from 'onap-ui-angular';
+import { Observable } from 'rxjs/Observable';
+import { map } from 'rxjs/operators';
+import { ArtifactsService } from '../../../../../components/forms/artifacts-form/artifacts.service';
+import { GetArtifactsByTypeAction } from '../../../../../store/actions/artifacts.action';
+import { GetInstanceArtifactsByTypeAction } from '../../../../../store/actions/instance-artifacts.actions';
+import { ArtifactsState } from '../../../../../store/states/artifacts.state';
+import { InstanceArtifactsState } from '../../../../../store/states/instance-artifacts.state';
+import { SelectedComponentType, TogglePanelLoadingAction } from '../../../common/store/graph.actions';
+import { CompositionService } from '../../../composition.service';
+
+@Component({
+ selector: 'artifacts-tab',
+ styleUrls: ['./artifacts-tab.component.less'],
+ templateUrl: './artifacts-tab.component.html',
+ providers: [SdcUiServices.ModalService]
+})
+
+export class ArtifactsTabComponent {
+
+ @Input() component: FullComponentInstance | TopologyTemplate;
+ @Input() isViewOnly: boolean;
+ @Input() input: any;
+ @Input() componentType: SelectedComponentType;
+
+ public title: string;
+ public type: string;
+ public isComponentInstanceSelected: boolean;
+ public artifacts$: Observable<ArtifactModel[]>;
+ private topologyTemplateType: string;
+ private topologyTemplateId: string;
+ private heatToEnv: Map<string, ArtifactModel>;
+ private resourceType: string;
+ private isComplex: boolean;
+
+ constructor(private store: Store,
+ private compositionService: CompositionService,
+ private workspaceService: WorkspaceService,
+ private componentInstanceService: ComponentInstanceServiceNg2,
+ private topologyTemplateService: TopologyTemplateService,
+ private artifactService: ArtifactsService) {
+ this.heatToEnv = new Map();
+ }
+
+ ngOnInit() {
+ this.topologyTemplateType = this.workspaceService.metadata.componentType;
+ this.topologyTemplateId = this.workspaceService.metadata.uniqueId;
+ this.type = this.input.type;
+ this.title = this.getTitle(this.type);
+ this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE;
+ this.resourceType = this.component['resourceType'];
+ this.isComplex = this.component.isComplex();
+ this.loadArtifacts();
+ }
+
+ public addOrUpdate = (artifact: ArtifactModel): void => {
+ if (this.isComponentInstanceSelected) {
+ this.artifactService.openArtifactModal(this.topologyTemplateId, this.topologyTemplateType, artifact, this.type, this.isViewOnly, this.component.uniqueId);
+ } else {
+ this.artifactService.openArtifactModal(this.topologyTemplateId, this.topologyTemplateType, artifact, this.type, this.isViewOnly);
+ }
+ }
+
+ public updateEnvParams = (artifact: ArtifactModel) => {
+ if (this.isComponentInstanceSelected) {
+ this.artifactService.openUpdateEnvParams(this.topologyTemplateType, this.topologyTemplateId, this.heatToEnv.get(artifact.uniqueId), this.component.uniqueId);
+ } else {
+ this.artifactService.openUpdateEnvParams(this.topologyTemplateType, this.topologyTemplateId, artifact);
+ }
+ }
+
+ public viewEnvParams = (artifact: ArtifactModel) => {
+ if (this.isComponentInstanceSelected) {
+ this.artifactService.openViewEnvParams(this.topologyTemplateType, this.topologyTemplateId, this.heatToEnv.get(artifact.uniqueId), this.component.uniqueId);
+ } else {
+ this.artifactService.openViewEnvParams(this.topologyTemplateType, this.topologyTemplateId, artifact);
+ }
+ }
+
+ public getEnvArtifact = (heatArtifact: ArtifactModel, artifacts: ArtifactModel[]): ArtifactModel => {
+ const envArtifact = _.find(artifacts, (item: ArtifactModel) => {
+ return item.generatedFromId === heatArtifact.uniqueId;
+ });
+ if (envArtifact && heatArtifact) {
+ envArtifact.artifactDisplayName = heatArtifact.artifactDisplayName;
+ envArtifact.timeout = heatArtifact.timeout;
+ }
+ return envArtifact;
+ }
+
+ public delete = (artifact: ArtifactModel): void => {
+ if (this.isComponentInstanceSelected) {
+ this.artifactService.deleteArtifact(this.topologyTemplateType, this.topologyTemplateId, artifact, this.component.uniqueId);
+ } else {
+ this.artifactService.deleteArtifact(this.topologyTemplateType, this.topologyTemplateId, artifact);
+ }
+ }
+
+ public isVfOrPnf(): boolean {
+ if (this.component.isResource()){
+ if (this.resourceType) {
+ return this.resourceType === 'VF' || this.resourceType == 'PNF';
+ }
+ return false;
+ }
+
+ return false;
+ }
+
+ private envArtifactOf(artifact: ArtifactModel): ArtifactModel {
+ return this.heatToEnv.get(artifact.uniqueId);
+ }
+
+ private isLicenseArtifact = (artifact: ArtifactModel): boolean => {
+ let isLicense: boolean = false;
+ if (this.component.isResource && (this.component as Resource).isCsarComponent) {
+ if (ArtifactType.VENDOR_LICENSE === artifact.artifactType || ArtifactType.VF_LICENSE === artifact.artifactType) {
+ isLicense = true;
+ }
+ }
+
+ return isLicense;
+ }
+
+ private getTitle = (artifactType: string): string => {
+ switch (artifactType) {
+ case ArtifactType.SERVICE_API:
+ return 'API Artifacts';
+ case ArtifactType.DEPLOYMENT:
+ return 'Deployment Artifacts';
+ case ArtifactType.INFORMATION:
+ return 'Informational Artifacts';
+ default:
+ return ResourceNamePipe.getDisplayName(artifactType) + ' Artifacts';
+ }
+ }
+
+ private loadArtifacts = (forceLoad?: boolean): void => {
+
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: true}));
+
+ let action;
+ if (this.isComponentInstanceSelected) {
+ action = new GetInstanceArtifactsByTypeAction(({
+ componentType: this.topologyTemplateType,
+ componentId: this.topologyTemplateId,
+ instanceId: this.component.uniqueId,
+ artifactType: this.type
+ }));
+ } else {
+ action = new GetArtifactsByTypeAction({
+ componentType: this.topologyTemplateType,
+ componentId: this.topologyTemplateId,
+ artifactType: this.type
+ });
+ }
+ this.store.dispatch(action).subscribe(() => {
+ const stateSelector = this.isComponentInstanceSelected ? InstanceArtifactsState.getArtifactsByType : ArtifactsState.getArtifactsByType;
+ this.artifacts$ = this.store.select(stateSelector).pipe(map((filterFn) => filterFn(this.type))).pipe(map((artifacts) => {
+ _.forEach(artifacts, (artifact: ArtifactModel): void => {
+ const envArtifact = this.getEnvArtifact(artifact, artifacts); // Extract the env artifact (if exist) of the HEAT artifact
+ if (envArtifact) {
+ // Set a mapping between HEAT to HEAT_ENV
+ this.heatToEnv.set(artifact.uniqueId, envArtifact);
+ }
+ });
+ return _.orderBy(artifacts, ['mandatory', 'artifactDisplayName'], ['desc', 'asc']);
+ }));
+
+ this.artifacts$.subscribe((artifacts) => {
+ _.forEach(artifacts, (artifact: ArtifactModel) => {
+ artifact.allowDeleteAndUpdate = this.allowDeleteAndUpdateArtifact(artifact);
+ });
+ if (this.component instanceof FullComponentInstance) {
+ this.compositionService.updateInstance(this.component);
+ }
+ });
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: false}));
+ }, () => {
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: false}));
+ });
+ }
+
+ private allowDeleteAndUpdateArtifact = (artifact: ArtifactModel): boolean => {
+ if (!this.isViewOnly) {
+ if (artifact.artifactGroupType === ArtifactType.DEPLOYMENT) {
+ return !artifact.isFromCsar;
+ } else {
+
+ return (!artifact.isHEAT() && !artifact.isThirdParty() && !this.isLicenseArtifact(artifact));
+ }
+ }
+ return false;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.html
index 6585ad2da9..8c5c9c7663 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.html
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.html
@@ -14,7 +14,7 @@
~ limitations under the License.
-->
-<div class="w-sdc-designer-sidebar-section-title" tooltip="Members">Members
+<h1 class="w-sdc-designer-sidebar-section-title" tooltip="Members">Members
<svg-icon-label *ngIf="!isViewOnly"
class="add-members-btn"
name="plus-circle-o"
@@ -24,7 +24,7 @@
labelPlacement="right"
(click)="openAddMembersModal()">
</svg-icon-label>
-</div>
+</h1>
<div class="expand-collapse-content">
<ul>
<li *ngFor="let member of members; let i = index" class="component-details-panel-large-item"
@@ -40,7 +40,7 @@
</li>
</ul>
- <div *ngIf="members.length===0" class="component-details-panel-tab-no-data">
+ <div *ngIf="!members || members.length===0" class="component-details-panel-tab-no-data">
<div class="component-details-panel-tab-no-data-title">No data to display yet</div>
<div class="component-details-panel-tab-no-data-content">Add members to group to see members</div>
</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.spec.ts
new file mode 100644
index 0000000000..43f6aac2c7
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.spec.ts
@@ -0,0 +1,127 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture } from '@angular/core/testing';
+import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular';
+import { Observable } from 'rxjs/Rx';
+import { Mock } from 'ts-mockery';
+import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper';
+import { ComponentMetadata } from '../../../../../../models/component-metadata';
+import { GroupInstance } from '../../../../../../models/graph/zones/group-instance';
+import { EventListenerService } from '../../../../../../services/event-listener-service';
+import { GroupsService } from '../../../../../services/groups.service';
+import { TranslateService } from '../../../../../shared/translator/translate.service';
+import { WorkspaceService } from '../../../../workspace/workspace.service';
+import { CompositionService } from '../../../composition.service';
+import { GroupMembersTabComponent } from './group-members-tab.component';
+
+describe('group members tab component', () => {
+
+ let fixture: ComponentFixture<GroupMembersTabComponent>;
+
+ // Mocks
+ let workspaceServiceMock: Partial<WorkspaceService>;
+ let eventsListenerServiceMock: Partial<EventListenerService>;
+ let groupServiceMock: Partial<GroupsService>;
+ let loaderServiceMock: Partial<SdcUiServices.LoaderService>;
+ let compositionServiceMock: Partial<CompositionService>;
+ let modalServiceMock: Partial<SdcUiServices.ModalService>;
+
+ const membersToAdd = [
+ {uniqueId: '1', name: 'inst1'},
+ {uniqueId: '2', name: 'inst2'},
+ ];
+
+ beforeEach(
+ async(() => {
+
+ eventsListenerServiceMock = {};
+
+ groupServiceMock = Mock.of<GroupsService>(
+ {
+ updateMembers: jest.fn().mockImplementation((compType, uid, groupUniqueId, updatedMembers) => {
+ if (updatedMembers === undefined) {
+ return Observable.throwError('error');
+ } else {
+ return Observable.of(updatedMembers);
+ }
+ }
+ )});
+
+ compositionServiceMock = {
+ getComponentInstances: jest.fn().mockImplementation( () => {
+ return [{uniqueId: '1', name: 'inst1'},
+ {uniqueId: '2', name: 'inst2'},
+ {uniqueId: '3', name: 'inst3'},
+ {uniqueId: '4', name: 'inst4'},
+ {uniqueId: '5', name: 'inst5'}
+ ];
+ }
+ )
+ };
+
+ workspaceServiceMock = {
+ metadata: Mock.of<ComponentMetadata>()
+ };
+
+ const addMemberModalInstance = {
+ innerModalContent: { instance: { existingElements: membersToAdd }},
+ closeModal: jest.fn()
+ };
+
+ modalServiceMock = {
+ openInfoModal: jest.fn(),
+ openCustomModal: jest.fn().mockImplementation(() => addMemberModalInstance)
+ };
+
+ loaderServiceMock = {
+ activate: jest.fn(),
+ deactivate: jest.fn()
+ };
+
+ const groupInstanceMock = Mock.of<GroupInstance>();
+
+ const configure: ConfigureFn = (testBed) => {
+ testBed.configureTestingModule({
+ declarations: [GroupMembersTabComponent],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: TranslateService, useValue: { translate: jest.fn() }},
+ {provide: GroupsService, useValue: groupServiceMock},
+ {provide: SdcUiServices.ModalService, useValue: modalServiceMock },
+ {provide: EventListenerService, useValue: eventsListenerServiceMock },
+ {provide: CompositionService, useValue: compositionServiceMock },
+ {provide: WorkspaceService, useValue: workspaceServiceMock},
+ {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock }
+ ],
+ });
+ };
+
+ configureTests(configure).then((testBed) => {
+ fixture = testBed.createComponent(GroupMembersTabComponent);
+ fixture.componentInstance.group = groupInstanceMock;
+ });
+ })
+ );
+
+ it('test that initially all members are available for adding', () => {
+ const testedComponent = fixture.componentInstance;
+
+ // No members are currently in the group, all 5 members should be returned
+ const optionalMembersToAdd = testedComponent.getOptionalsMembersToAdd();
+ expect(optionalMembersToAdd).toHaveLength(5);
+ });
+
+ it('test list of available instances to add does not include existing members', () => {
+ const testedComponent = fixture.componentInstance;
+
+ // Mock the group instance to return the members that we are about to add
+ testedComponent.group.getMembersAsUiObject = jest.fn().mockImplementation( () => membersToAdd);
+
+ // The opened modal shall return 2 members to be added
+ testedComponent.openAddMembersModal();
+ testedComponent.addMembers(); // Shall add 2 members (1,2)
+
+ // Now the getOptionalsMembersToAdd shall return 3 which are the members that were no added yet
+ const optionalMembersToAdd = testedComponent.getOptionalsMembersToAdd();
+ expect(optionalMembersToAdd).toHaveLength(3);
+ });
+});
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.ts
new file mode 100644
index 0000000000..7f1222367d
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.ts
@@ -0,0 +1,158 @@
+/*-
+ * ============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, HostBinding, Input, OnDestroy, OnInit } from '@angular/core';
+import { Select } from '@ngxs/store';
+import { GroupInstance } from 'app/models/graph/zones/group-instance';
+import { CompositionService } from 'app/ng2/pages/composition/composition.service';
+import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service';
+import { EventListenerService } from 'app/services/event-listener-service';
+import { GRAPH_EVENTS } from 'app/utils';
+import * as _ from 'lodash';
+import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular';
+import { Observable, Subscription } from 'rxjs';
+import { tap } from 'rxjs/operators';
+import { ComponentInstance } from '../../../../../../models/componentsInstances/componentInstance';
+import { MemberUiObject } from '../../../../../../models/ui-models/ui-member-object';
+import { AddElementsComponent } from '../../../../../components/ui/modal/add-elements/add-elements.component';
+import {GraphState} from "../../../common/store/graph.state";
+import { GroupsService } from '../../../../../services/groups.service';
+import { TranslateService } from '../../../../../shared/translator/translate.service';
+
+@Component({
+ selector: 'group-members-tab',
+ templateUrl: './group-members-tab.component.html',
+ styleUrls: ['./../policy-targets-tab/policy-targets-tab.component.less']
+})
+
+export class GroupMembersTabComponent implements OnInit, OnDestroy {
+
+ @Input() group: GroupInstance;
+ @Input() isViewOnly: boolean;
+ @Select(GraphState.getSelectedComponent) group$: Observable<GroupInstance>;
+ @HostBinding('class') classes = 'component-details-panel-tab-group-members';
+
+ private members: MemberUiObject[];
+ private addMemberModalInstance: SdcUiComponents.ModalComponent;
+ private subscription: Subscription;
+
+ constructor(
+ private translateService: TranslateService,
+ private groupsService: GroupsService,
+ private modalService: SdcUiServices.ModalService,
+ private eventListenerService: EventListenerService,
+ private compositionService: CompositionService,
+ private workspaceService: WorkspaceService,
+ private loaderService: SdcUiServices.LoaderService
+ ) {
+ }
+
+ ngOnInit() {
+ this.subscription = this.group$.pipe(
+ tap((group) => {
+ this.group = group;
+ this.members = this.group.getMembersAsUiObject(this.compositionService.componentInstances);
+ })).subscribe();
+ }
+
+ ngOnDestroy() {
+ if (this.subscription) {
+ this.subscription.unsubscribe();
+ }
+ }
+
+ deleteMember = (member: MemberUiObject): void => {
+ this.loaderService.activate();
+ this.groupsService.deleteGroupMember(
+ this.workspaceService.metadata.componentType,
+ this.workspaceService.metadata.uniqueId,
+ this.group,
+ member.uniqueId).subscribe(
+ (updatedMembers: string[]) => {
+ this.group.members = updatedMembers;
+ this.initMembers();
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.group);
+ },
+ () => console.log('Error deleting member!'),
+ () => this.loaderService.deactivate()
+ );
+ }
+
+ addMembers = (): void => {
+ // TODO refactor sdc-ui modal in order to return the data
+ const membersToAdd: MemberUiObject[] = this.addMemberModalInstance.innerModalContent.instance.existingElements;
+ if (membersToAdd.length > 0) {
+ this.addMemberModalInstance.closeModal();
+ this.loaderService.activate();
+ const locallyUpdatedMembers: MemberUiObject[] = _.union(this.members, membersToAdd);
+ this.groupsService.updateMembers(
+ this.workspaceService.metadata.componentType,
+ this.workspaceService.metadata.uniqueId,
+ this.group.uniqueId,
+ locallyUpdatedMembers).subscribe(
+ (updatedMembers: string[]) => {
+ this.group.members = updatedMembers;
+ this.initMembers();
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.group);
+ },
+ () => {
+ console.log('Error updating members!');
+ }, () =>
+ this.loaderService.deactivate()
+ );
+ }
+ }
+
+ getOptionalsMembersToAdd(): MemberUiObject[] {
+ const optionalsMembersToAdd: MemberUiObject[] = [];
+ // adding all instances as optional members to add if not already exist
+ _.forEach(this.compositionService.getComponentInstances(), (instance: ComponentInstance) => {
+ if (!_.some(this.members, (member: MemberUiObject) => {
+ return member.uniqueId === instance.uniqueId;
+ })) {
+ optionalsMembersToAdd.push(new MemberUiObject(instance.uniqueId, instance.name));
+ }
+ });
+ return optionalsMembersToAdd;
+ }
+
+ openAddMembersModal(): void {
+ const addMembersModalConfig = {
+ title: this.group.name + ' ADD MEMBERS',
+ size: 'md',
+ type: SdcUiCommon.ModalType.custom,
+ testId: 'addMembersModal',
+ buttons: [
+ {text: 'ADD MEMBERS', size: 'medium', callback: this.addMembers, closeModal: false},
+ {text: 'CANCEL', size: 'sm', type: 'secondary', closeModal: true}
+ ]
+ } as SdcUiCommon.IModalConfig;
+ const optionalsMembersToAdd = this.getOptionalsMembersToAdd();
+ this.addMemberModalInstance = this.modalService.openCustomModal(addMembersModalConfig, AddElementsComponent, {
+ elementsToAdd: optionalsMembersToAdd,
+ elementName: 'member'
+ });
+ }
+
+ private initMembers = (groupInstance?: GroupInstance) => {
+ this.group = groupInstance ? groupInstance : this.group;
+ this.members = this.group.getMembersAsUiObject(this.compositionService.getComponentInstances());
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component.html
index fe1f6b4f0d..c57f99786c 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.html
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component.html
@@ -18,7 +18,7 @@
<header tooltip="Properties">Properties</header>
<content>
<ul>
- <li *ngFor="let property of properties; let i = index"
+ <li *ngFor="let property of component.properties; let i = index"
class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" data-tests-id="propertyRow">
<div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label hand"
[attr.data-tests-id]="'propertyName_'+property.name"
@@ -32,7 +32,7 @@
</li>
</ul>
- <div *ngIf="properties.length===0" class="component-details-panel-tab-no-data">
+ <div *ngIf="!component.properties || component.properties.length===0" class="component-details-panel-tab-no-data">
<div class="component-details-panel-tab-no-data-title">No properties to display</div>
</div>
</content>
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component.ts
index 5862135df2..24ae8b2833 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.ts
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component.ts
@@ -19,44 +19,32 @@
*/
import * as _ from "lodash";
-import { Component, Inject, Input, Output, EventEmitter, OnChanges, SimpleChanges } from "@angular/core";
+import { Component, Inject, Input} from "@angular/core";
import { TranslateService } from './../../../../../shared/translator/translate.service';
import { PolicyInstance } from 'app/models/graph/zones/policy-instance';
-import { PropertyBEModel } from 'app/models';
import { PropertyModel } from './../../../../../../models/properties';
import { ModalsHandler } from "app/utils";
-import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models";
+import { Component as TopologyTemplate, GroupInstance } from "app/models";
@Component({
- selector: 'policy-properties-tab',
- templateUrl: './policy-properties-tab.component.html',
- styleUrls: ['./../base/base-tab.component.less', 'policy-properties-tab.component.less'],
- host: {'class': 'component-details-panel-tab-policy-properties'}
+ selector: 'group-or-policy-properties-tab',
+ templateUrl: './group-or-policy-properties-tab.component.html',
+ styleUrls: ['./../properties-tab/properties-tab.component.less'],
})
-export class PolicyPropertiesTabComponent implements OnChanges {
+export class GroupOrPolicyPropertiesTab {
- @Input() policy:PolicyInstance;
+ @Input() component: GroupInstance | PolicyInstance;
@Input() topologyTemplate:TopologyTemplate;
@Input() isViewOnly: boolean;
+ @Input() input: {type: string};
- private properties:Array<PropertyModel>;
constructor(private translateService:TranslateService, private ModalsHandler:ModalsHandler) {
}
- ngOnChanges(changes: SimpleChanges): void {
- console.log("PolicyPropertiesTabComponent: ngAfterViewInit: ");
- console.log("policy: " + this.policy);
- this.properties = [];
- this.initProperties();
- }
-
- initProperties = ():void => {
- this.properties= this.policy.properties;
- }
editProperty = (property?:PropertyModel):void => {
- this.ModalsHandler.openEditPropertyModal((property ? property : new PropertyModel()), this.topologyTemplate, this.properties, false, 'policy', this.policy.uniqueId).then((updatedProperty:PropertyModel) => {
+ this.ModalsHandler.openEditPropertyModal((property ? property : new PropertyModel()), this.topologyTemplate, this.component.properties, false, this.input.type, this.component.uniqueId).then((updatedProperty:PropertyModel) => {
console.log("ok");
});
}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-information-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-information-tab.component.html
deleted file mode 100644
index 953b57bda1..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-information-tab.component.html
+++ /dev/null
@@ -1,47 +0,0 @@
-<!--
- ~ 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.
- -->
-
-
-<ng2-expand-collapse state="0">
-
- <header tooltip="General Information">General Info</header>
-
- <content>
- <!-- CATEGORY -->
- <div class="component-details-panel-item">
- <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span>
- <span class="value" data-tests-id="rightTab_category" tooltip="Group">Group</span>
- </div>
-
- <!-- SUB CATEGORY -->
- <div class="component-details-panel-item">
- <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span>
- <span class="value" data-tests-id="rightTab_subCategory" tooltip="Group">Group</span>
- </div>
-
- <!-- VERSION -->
- <div class="component-details-panel-item">
- <span class="name" [innerHTML]="'GENERAL_LABEL_VERSION' | translate"></span>
- <span class="value" data-tests-id="rightTab_version" tooltip="{{group.version}}">{{group.version}}</span>
- </div>
-
- <!-- DESCRIPTION -->
- <div class="component-details-panel-item description">
- <span class="name" [innerHTML]="'GENERAL_LABEL_DESCRIPTION' | translate"></span>
- <span class="value" ellipsis="group.description" max-chars="55" data-tests-id="rightTab_description">{{group.description}}</span>
- </div>
- </content>
-</ng2-expand-collapse>
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.less
deleted file mode 100644
index 1006e864fa..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.less
+++ /dev/null
@@ -1,13 +0,0 @@
-/deep/
-.component-details-panel-tab-group-members {
- .component-details-panel-large-item {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- }
-
- .w-sdc-designer-sidebar-section-title {
- display: flex;
- justify-content: space-between;
- }
-} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.ts
deleted file mode 100644
index 148f2133e8..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-/*-
- * ============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 * as _ from "lodash";
-import { Component, Input, Output, EventEmitter, OnChanges, HostBinding } from "@angular/core";
-import { TranslateService } from './../../../../../shared/translator/translate.service';
-import { Component as TopologyTemplate } from "app/models";
-import { GroupInstance } from "app/models/graph/zones/group-instance";
-import { GroupsService } from "../../../../../services/groups.service";
-import { SimpleChanges } from "@angular/core/src/metadata/lifecycle_hooks";
-import { MemberUiObject } from "../../../../../../models/ui-models/ui-member-object";
-import { IModalConfig } from "sdc-ui/lib/angular/modals/models/modal-config";
-import { AddElementsComponent } from "../../../../../components/ui/modal/add-elements/add-elements.component";
-import { GRAPH_EVENTS } from 'app/utils';
-import { EventListenerService } from 'app/services/event-listener-service';
-import { ComponentInstance } from "../../../../../../models/componentsInstances/componentInstance";
-import { SdcUiComponents } from "sdc-ui/lib/angular";
-
-@Component({
- selector: 'group-members-tab',
- templateUrl: './group-members-tab.component.html',
- styleUrls: ['./../base/base-tab.component.less', 'group-members-tab.component.less']
-})
-
-export class GroupMembersTabComponent implements OnChanges {
-
-
- private members: Array<MemberUiObject>;
-
- @Input() group: GroupInstance;
- @Input() topologyTemplate: TopologyTemplate;
- @Input() isViewOnly: boolean;
- @Output() isLoading: EventEmitter<boolean> = new EventEmitter<boolean>();
- @HostBinding('class') classes = 'component-details-panel-tab-group-members';
-
- constructor(private translateService: TranslateService,
- private groupsService: GroupsService,
- private modalService: SdcUiComponents.ModalService,
- private eventListenerService: EventListenerService
- ) {
- this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.initMembers)
- }
-
- ngOnChanges(changes:SimpleChanges):void {
- this.initMembers();
- }
-
- deleteMember = (member: MemberUiObject):void => {
- this.isLoading.emit(true);
- this.groupsService.deleteGroupMember(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.group, member.uniqueId).subscribe(
- (updatedMembers:Array<string>) => {
- this.group.members = updatedMembers;
- this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.group);
- },
- error => console.log("Error deleting member!"),
- () => this.isLoading.emit(false)
- );
- }
-
- private initMembers = (groupInstance?: GroupInstance) => {
- this.group = groupInstance ? groupInstance : this.group;
- this.members = this.group.getMembersAsUiObject(this.topologyTemplate.componentInstances);
- }
-
- addMembers = ():void => {
- var membersToAdd:Array<MemberUiObject> = this.modalService.getCurrentInstance().innerModalContent.instance.existingElements; //TODO refactor sdc-ui modal in order to return the data
- if(membersToAdd.length > 0) {
- this.modalService.closeModal();
- this.isLoading.emit(true);
- var updatedMembers: Array<MemberUiObject> = _.union(this.members, membersToAdd);
- this.groupsService.updateMembers(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.group.uniqueId, updatedMembers).subscribe(
- (updatedMembers:Array<string>) => {
- this.group.members = updatedMembers;
- this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.group);
- },
- error => {
- console.log("Error updating members!");
- }, () =>
- this.isLoading.emit(false)
- );
- }
- }
-
- getOptionalsMembersToAdd():Array<MemberUiObject> {
-
- let optionalsMembersToAdd:Array<MemberUiObject> = [];
-
- // adding all instances as optional members to add if not already exist
- _.forEach(this.topologyTemplate.componentInstances, (instance:ComponentInstance) => {
- if (!_.some(this.members, (member:MemberUiObject) => {
- return member.uniqueId === instance.uniqueId
- })) {
- optionalsMembersToAdd.push(new MemberUiObject(instance.uniqueId, instance.name));
- }
- });
- return optionalsMembersToAdd;
- }
-
- openAddMembersModal():void {
- let addMembersModalConfig:IModalConfig = {
- title: this.group.name + " ADD MEMBERS",
- size: "md",
- type: "custom",
- testId: "addMembersModal",
- buttons: [
- {text: 'ADD MEMBERS', size: 'xsm', callback: this.addMembers, closeModal: false},
- {text: 'CANCEL', size: 'sm', type: "secondary", closeModal: true}
- ]
- };
- var optionalsMembersToAdd = this.getOptionalsMembersToAdd();
- this.modalService.openCustomModal(addMembersModalConfig, AddElementsComponent, {
- elementsToAdd: optionalsMembersToAdd,
- elementName: "member"
- });
- }
-}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.html
deleted file mode 100644
index fe1f6b4f0d..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.html
+++ /dev/null
@@ -1,39 +0,0 @@
-<!--
- ~ 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.
- -->
-
-<ng2-expand-collapse state="0">
- <header tooltip="Properties">Properties</header>
- <content>
- <ul>
- <li *ngFor="let property of properties; let i = index"
- class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" data-tests-id="propertyRow">
- <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label hand"
- [attr.data-tests-id]="'propertyName_'+property.name"
- tooltip="{{property.name}}"
- (click)="!isViewOnly && editProperty(property)">{{property.name}}
- </div>
- <div class="i-sdc-designer-sidebar-section-content-item-property-value"
- [attr.data-tests-id]="'value_'+property.name"
- tooltip="{{property.value || property.defaultValue}}">{{property.value || property.defaultValue}}
- </div>
- </li>
- </ul>
-
- <div *ngIf="properties.length===0" class="component-details-panel-tab-no-data">
- <div class="component-details-panel-tab-no-data-title">No properties to display</div>
- </div>
- </content>
-</ng2-expand-collapse>
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.ts
deleted file mode 100644
index 69079347c4..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/*-
- * ============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 * as _ from "lodash";
-import { Component, Inject, Input, Output, EventEmitter, OnChanges, SimpleChanges } from "@angular/core";
-import { TranslateService } from './../../../../../shared/translator/translate.service';
-import { GroupInstance } from 'app/models/graph/zones/group-instance';
-import { PropertyBEModel } from 'app/models';
-import { PropertyModel } from './../../../../../../models/properties';
-import { ModalsHandler } from "app/utils";
-import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models";
-
-@Component({
- selector: 'group-properties-tab',
- templateUrl: './group-properties-tab.component.html',
- styleUrls: ['./../base/base-tab.component.less', 'group-properties-tab.component.less'],
- host: {'class': 'component-details-panel-tab-group-properties'}
-})
-export class GroupPropertiesTabComponent implements OnChanges {
-
- @Input() group:GroupInstance;
- @Input() topologyTemplate:TopologyTemplate;
- @Input() isViewOnly: boolean;
-
- private properties:Array<PropertyModel>;
-
- constructor(private translateService:TranslateService, private ModalsHandler:ModalsHandler) {
- }
-
- ngOnChanges(changes: SimpleChanges): void {
- console.log("GroupPropertiesTabComponent: ngAfterViewInit: ");
- console.log("group: " + JSON.stringify(this.group));
- this.properties = [];
- this.initProperties();
- }
-
- initProperties = ():void => {
- this.properties= this.group.properties;
- }
-
- editProperty = (property?:PropertyModel):void => {
- this.ModalsHandler.openEditPropertyModal((property ? property : new PropertyModel()), this.topologyTemplate, this.properties, false, 'group', this.group.uniqueId).then((updatedProperty:PropertyModel) => {
- console.log("ok");
- });
- }
-
-}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.component.ts
deleted file mode 100644
index 975d5c6153..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.component.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-/*-
- * ============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 * as _ from "lodash";
-import { Component, Inject, Input, Output, EventEmitter, SimpleChanges, OnChanges } from "@angular/core";
-import { TranslateService } from './../../../../../shared/translator/translate.service';
-import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models";
-import { GroupsService } from '../../../../../services/groups.service';
-import { GroupInstance } from "app/models/graph/zones/group-instance";
-
-@Component({
- selector: 'group-tabs',
- templateUrl: './group-tabs.component.html'
-})
-export class GroupTabsComponent implements OnChanges {
-
- @Input() topologyTemplate:TopologyTemplate;
- @Input() selectedZoneInstanceType:string;
- @Input() selectedZoneInstanceId:string;
- @Input() isViewOnly: boolean;
- @Output() isLoading: EventEmitter<boolean> = new EventEmitter<boolean>();
-
- private group:GroupInstance;
-
- constructor(private translateService:TranslateService,
- private groupsService:GroupsService
- ) {
- }
-
- ngOnChanges(changes: SimpleChanges): void {
- this.initGroup();
- }
-
- private initGroup = ():void => {
- this.isLoading.emit(true);
- this.groupsService.getSpecificGroup(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.selectedZoneInstanceId).subscribe(
- group => {
- this.group = group;
- console.log(JSON.stringify(group));
- },
- error => console.log("Error getting group!"),
- () => this.isLoading.emit(false)
- );
- }
-
- private setIsLoading = (value) :void => {
- this.isLoading.emit(value);
- }
-
-}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.module.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.module.ts
deleted file mode 100644
index 50797f862c..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.module.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/*-
- * ============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 { NgModule } from "@angular/core";
-import { HttpModule } from "@angular/http";
-import { FormsModule } from "@angular/forms";
-import { BrowserModule } from "@angular/platform-browser";
-import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module';
-import { ExpandCollapseComponent } from 'app/ng2/components/ui/expand-collapse/expand-collapse.component';
-import { PoliciesService } from "../../../../../services/policies.service";
-import { GroupInformationTabComponent } from './group-information-tab.component';
-import { TooltipModule } from './../../../../../components/ui/tooltip/tooltip.module';
-import { GroupTabsComponent } from "./group-tabs.component";
-import { SdcUiComponentsModule } from "sdc-ui/lib/angular";
-import { GroupMembersTabComponent } from './group-members-tab.component';
-import { TranslateModule } from './../../../../../shared/translator/translate.module';
-import { GroupPropertiesTabComponent } from "./group-properties-tab.component";
-
-@NgModule({
- declarations: [
- GroupInformationTabComponent,
- GroupMembersTabComponent,
- GroupTabsComponent,
- GroupPropertiesTabComponent
- ],
- imports: [
- BrowserModule,
- FormsModule,
- HttpModule,
- TooltipModule,
- UiElementsModule,
- SdcUiComponentsModule,
- TranslateModule
- ],
- entryComponents: [
- GroupInformationTabComponent,
- GroupMembersTabComponent,
- GroupTabsComponent,
- GroupPropertiesTabComponent,
- ExpandCollapseComponent
- ],
- exports: [
- TooltipModule,
- GroupInformationTabComponent,
- GroupMembersTabComponent,
- GroupTabsComponent,
- GroupPropertiesTabComponent
- ],
- providers: [
- PoliciesService
- ]
-})
-export class GroupTabsModule {
-
-}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/__snapshots__/info-tab.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/__snapshots__/info-tab.component.spec.ts.snap
new file mode 100644
index 0000000000..fdd0dcf75c
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/__snapshots__/info-tab.component.spec.ts.snap
@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`InfoTabComponent can load instance 1`] = `
+<panel-info-tab
+ componentInstanceService={[Function Object]}
+ compositionPaletteService={[Function Object]}
+ compositionService={[Function Object]}
+ eventListenerService={[Function Object]}
+ flatLeftPaletteElementsFromService={[Function Function]}
+ getPathNamesVersionChangeModal={[Function Function]}
+ initEditResourceVersion={[Function Function]}
+ modalService={[Function Object]}
+ onChangeVersion={[Function Function]}
+ sdcMenu={[Function Object]}
+ serviceService={[Function Object]}
+ store={[Function Object]}
+ versioning={[Function Function]}
+ workspaceService={[Function Object]}
+>
+ <ng2-expand-collapse
+ state="0"
+ >
+ <header
+ tooltip="General Information"
+ >
+ General Info
+ </header>
+ <content
+ class="general-info-container"
+ >
+
+
+ <div
+ class="component-details-panel-item"
+ >
+ <span
+ class="name"
+ />
+
+
+ </div>
+
+
+
+
+
+
+
+
+
+
+
+ <div
+ class="component-details-panel-item description"
+ >
+ <span
+ class="name"
+ />
+ <chars-ellipsis />
+ </div>
+
+
+ </content>
+ </ng2-expand-collapse>
+</panel-info-tab>
+`;
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.html
new file mode 100644
index 0000000000..71545f8143
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.html
@@ -0,0 +1,174 @@
+<!--
+ ~ 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.
+ -->
+
+<ng2-expand-collapse state="0">
+ <header tooltip="General Information">General Info</header>
+ <content class="general-info-container">
+ <!-- TYPE -->
+ <div class="component-details-panel-item" *ngIf="component.componentType">
+ <span class="name" [innerHTML]="'Type:'"></span>
+ <span class="value" data-tests-id="rightTab_componentType" tooltip="{{component.componentType}}">{{component.componentType}}</span>
+ </div>
+
+ <!-- RESOURCE TYPE-->
+ <div class="component-details-panel-item" *ngIf="component.resourceType">
+ <span class="name" [innerHTML]="'Resource Type:'"></span>
+ <span class="value" data-tests-id="rightTab_resourceType" tooltip="{{component.resourceType}}">{{component.resourceType}}</span>
+ </div>
+
+ <!-- VERSION -->
+ <div class="component-details-panel-item" >
+ <span class="name" [innerHTML]="'GENERAL_LABEL_VERSION' | translate"></span>
+ <span class="value" *ngIf="!isComponentSelectedFlag" data-tests-id="rightTab_version" tooltip="{{component.version}}">{{component.version}}</span>
+ <ng-container *ngIf="isComponentSelectedFlag">
+ <select #versionDropdown (change)="onChangeVersion(versionDropdown)" [ngModel]="component.getComponentUid()" data-tests-id="changeVersion">
+ <option *ngFor="let version of versions" value="{{version.value}}"
+ [disabled]="isDisabledFlag" [class.minor]="(component.componentVersion)%1"
+ >{{version.label}}</option>
+ </select>
+ </ng-container>
+ </div>
+
+ <!-- CATEGORY -->
+ <ng-container *ngIf="component.categories && component.categories[0]">
+ <div class="component-details-panel-item">
+ <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span>
+ <span class="value" data-tests-id="rightTab_category" tooltip="{{component.categories[0].name}}">{{component.categories[0].name}}</span>
+ </div>
+
+ <!-- SUB CATEGORY -->
+ <div class="component-details-panel-item" *ngIf="component.categories[0].subcategories && component.categories[0].subcategories[0]">
+ <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span>
+ <span class="value" data-tests-id="rightTab_subCategory" tooltip="{{component.categories[0].subcategories[0].name}}">{{component.categories[0].subcategories[0].name}}</span>
+ </div>
+ </ng-container>
+
+ <!-- CREATION DATE -->
+ <div class="component-details-panel-item" *ngIf="component.creationDate">
+ <span class="name" [innerHTML]="'Creation Date:'"></span>
+ <span class="value" data-tests-id="rightTab_version" tooltip="{{component.creationDate | date: 'MM/dd/yyyy'}}">{{component.creationDate | date: 'MM/dd/yyyy'}}</span>
+ </div>
+
+ <!-- AUTHOR -->
+ <div class="component-details-panel-item" *ngIf="component.creatorFullName">
+ <span class="name" [innerHTML]="'Author:'"></span>
+ <span class="value" data-tests-id="rightTab_author" tooltip="{{component.creatorFullName}}">{{component.creatorFullName}}</span>
+ </div>
+
+ <!-- Vendor Name data-ng-if="selectedComponent.isResource()"-->
+ <div class="component-details-panel-item" *ngIf="component.vendorName">
+ <span class="name" [innerHTML]="'Vendor Name:'"></span>
+ <span class="value" data-tests-id="rightTab_vendorName" tooltip="{{component.vendorName}}">{{component.vendorName}}</span>
+ </div>
+
+ <!-- Vendor Release data-ng-if="selectedComponent.isResource()"-->
+ <div class="component-details-panel-item" *ngIf="component.vendorRelease">
+ <span class="name" [innerHTML]="'Vendor Release:'"></span>
+ <span class="value" data-tests-id="rightTab_vendorRelease" tooltip="{{component.vendorRelease}}">{{component.vendorRelease}}</span>
+ </div>
+
+ <!-- Vendor Release data-ng-if="selectedComponent.isResource()"-->
+ <div class="component-details-panel-item" *ngIf="component.resourceVendorModelNumber">
+ <span class="name" [innerHTML]="'GENERAL_LABEL_RESOURCE_MODEL_NUMBER' | translate"></span>
+ <span class="value" data-tests-id="rightTab_resourceVendorModelNumber" tooltip="{{component.resourceVendorModelNumber}}">{{component.resourceVendorModelNumber}}</span>
+ </div>
+
+ <!-- Service Type data-ng-if="selectedComponent.isService()"-->
+ <div class="component-details-panel-item" *ngIf="component.serviceType">
+ <span class="name" [innerHTML]="'GENERAL_LABEL_SERVICE_TYPE' | translate"></span>
+ <span class="value" data-tests-id="rightTab_serviceType" tooltip="{{component.serviceType}}">{{component.serviceType}}</span>
+ </div>
+
+ <!-- Service Role data-ng-if="selectedComponent.isService()"-->
+ <div class="component-details-panel-item" *ngIf="component.serviceRole">
+ <span class="name" [innerHTML]="'GENERAL_LABEL_SERVICE_ROLE' | translate"></span>
+ <span class="value" data-tests-id="rightTab_serviceRole" tooltip="{{component.serviceRole}}">{{component.serviceRole}}</span>
+ </div>
+
+ <!-- Contact ID -->
+ <div class="component-details-panel-item" *ngIf="component.contactId">
+ <span class="name" [innerHTML]="'GENERAL_LABEL_CONTACT_ID' | translate"></span>
+ <span class="value" data-tests-id="rightTab_contactId" tooltip="{{component.contactId}}">{{component.contactId}}</span>
+ </div>
+
+ <!-- Service Name data-ng-if="isComponentInstanceSelected() && currentComponent.selectedInstance.isServiceProxy()"-->
+ <div class="component-details-panel-item" *ngIf="component.sourceModelName">
+ <span class="name" [innerHTML]="'GENERAL_LABEL_SOURCE_SERVICE_NAME' | translate"></span>
+ <span class="value" data-tests-id="rightTab_sourceModelName" tooltip="{{component.sourceModelName}}">{{component.sourceModelName}}</span>
+ </div>
+
+ <!-- Customization UUID data-ng-if="isViewMode() && currentComponent.isService() && selectedComponent.isResource()"-->
+ <div class="component-details-panel-item" *ngIf="component.customizationUUID">
+ <span class="name" [innerHTML]="'GENERAL_LABEL_RESOURCE_CUSTOMIZATION_UUID' | translate"></span>
+ <span class="value" data-tests-id="rightTab_customizationModuleUUID" tooltip="{{component.customizationUUID}}">{{component.customizationUUID}}</span>
+ </div>
+
+ <!-- DESCRIPTION -->
+ <div class="component-details-panel-item description">
+ <span class="name" [innerHTML]="'GENERAL_LABEL_DESCRIPTION' | translate"></span>
+ <chars-ellipsis [text]="component.description" [maxChars]="55" [testId]="'rightTab_description'"></chars-ellipsis>
+ </div>
+
+
+ <!--TODO: move to separate component!-->
+ <ng-container *ngIf="componentType == 'POLICY'">
+ <!-- TYPE -->
+ <div class="component-details-panel-item policy-item">
+ <span class="name" [innerHTML]="'GENERAL_LABEL_TYPE' | translate"></span>
+ <span class="value" data-tests-id="rightTab_componentType" tooltip="{{component.policyTypeUid}}">{{component.policyTypeUid}}</span>
+ </div>
+
+ <!-- CATEGORY -->
+ <div class="component-details-panel-item policy-item">
+ <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span>
+ <span class="value" data-tests-id="rightTab_category" tooltip="Policy">Policy</span>
+ </div>
+
+ <!-- SUB CATEGORY -->
+ <div class="component-details-panel-item policy-item">
+ <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span>
+ <span class="value" data-tests-id="rightTab_subCategory" tooltip="Policy">Policy</span>
+ </div>
+ </ng-container>
+
+ <!--TODO: move to separate component!-->
+ <ng-container *ngIf="componentType == 'GROUP'">
+ <!-- CATEGORY -->
+ <div class="component-details-panel-item group-item">
+ <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span>
+ <span class="value" data-tests-id="rightTab_category" tooltip="Group">Group</span>
+ </div>
+
+ <!-- SUB CATEGORY -->
+ <div class="component-details-panel-item group-item">
+ <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span>
+ <span class="value" data-tests-id="rightTab_subCategory" tooltip="Group">Group</span>
+ </div>
+
+ </ng-container>
+
+ </content>
+</ng2-expand-collapse>
+
+<ng2-expand-collapse *ngIf="component.tags || isComponentInstanceSelected()">
+ <header tooltip="Tags">Tags</header>
+ <content class="tags-container">
+ <span *ngIf="component.tags?.indexOf(component.name)===-1" class="i-sdc-designer-sidebar-section-content-item-tag"
+ data-tests-id="rightTab_tag" tooltip="{{component.name}}">{{component.name}}</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-tag" *ngFor="let tag of component.tags"
+ data-tests-id="rightTab_tag" tooltip="{{tag}}">{{tag}}</span>
+ </content>
+</ng2-expand-collapse>
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.less
new file mode 100644
index 0000000000..c8da4e3e68
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.less
@@ -0,0 +1,51 @@
+@import '../../../../../../../assets/styles/variables';
+
+.general-info-container {
+ display: flex;
+ flex-direction: column;
+ padding: 10px 20px;
+}
+
+.component-details-panel-item {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-bottom: 5px;
+ order:1;
+
+ .name { font-family: OpenSans-Semibold, sans-serif; }
+ .value { padding-left: 10px; }
+
+
+ &.description {
+ margin-top: 28px;
+ white-space: normal;
+ word-wrap: break-word;
+ overflow: ellipsis;
+
+ .value {
+ padding-left: 0;
+ max-width: none;
+ font-weight: normal;
+ font-family: @font-opensans-regular;
+ }
+ }
+
+ &.group-item, &.policy-item {
+ order:0;
+ }
+}
+
+.tags-container {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 10px 20px;
+
+ .i-sdc-designer-sidebar-section-content-item-tag {
+ padding: 5px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ user-select: all;
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.spec.ts
new file mode 100644
index 0000000000..6915d651f1
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.spec.ts
@@ -0,0 +1,98 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { Store } from '@ngxs/store';
+import { CompositionPaletteService } from '../../../../../pages/composition/palette/services/palette.service';
+import { IAppMenu, SdcMenuToken } from '../../../../../../../app/ng2/config/sdc-menu.config';
+import { CompositionService } from '../../../../../pages/composition/composition.service';
+import { ServiceServiceNg2 } from '../../../../../../../app/services-ng2';
+import { WorkspaceService } from '../../../../../../../app/ng2/pages/workspace/workspace.service';
+import { ComponentInstanceServiceNg2 } from '../../../../../../../app/ng2/services/component-instance-services/component-instance.service';
+import { EventListenerService } from '../../../../../../../app/services';
+import { InfoTabComponent } from './info-tab.component';
+import { ConfigureFn, configureTests } from "../../../../../../../jest/test-config.helper";
+import { Observable } from "rxjs";
+import { leftPaletteElements } from "../../../../../../../jest/mocks/left-paeltte-elements.mock";
+import { TranslatePipe } from "../../../../../shared/translator/translate.pipe";
+import { HttpClientModule } from "@angular/common/http";
+import { TranslateModule } from "../../../../../../../app/ng2/shared/translator/translate.module";
+import _ from "lodash";
+import { TranslateService } from "../../../../../shared/translator/translate.service";
+import { SdcUiServices } from "onap-ui-angular";
+import { Component as TopologyTemplate, FullComponentInstance, ComponentInstance } from '../../../../../../../app/models';
+
+
+describe('InfoTabComponent', () => {
+ // let comp: InfoTabComponent;
+ let fixture: ComponentFixture<InfoTabComponent>;
+
+ // let eventServiceMock: Partial<EventListenerService>;
+ let storeStub:Partial<Store>;
+ let compositionPaletteServiceStub:Partial<CompositionPaletteService>;
+ let iAppMenuStub:Partial<IAppMenu>;
+ let compositionServiceStub:Partial<CompositionService>;
+ let serviceServiceNg2Stub:Partial<ServiceServiceNg2>;
+ let workspaceServiceStub:Partial<WorkspaceService>;
+ let componentInstanceServiceNg2Stub:Partial<ComponentInstanceServiceNg2>;
+ let eventListenerServiceStub:Partial<EventListenerService>;
+
+ beforeEach(
+ async(() => {
+ storeStub = {};
+ iAppMenuStub = {};
+ eventListenerServiceStub = {
+ notifyObservers: jest.fn()
+ }
+ compositionPaletteServiceStub = {
+ getLeftPaletteElements: jest.fn().mockImplementation(()=> Observable.of(leftPaletteElements))
+ }
+ const configure: ConfigureFn = testBed => {
+ testBed.configureTestingModule({
+ imports: [ ],
+ declarations: [ InfoTabComponent, TranslatePipe ],
+ schemas: [ NO_ERRORS_SCHEMA ],
+ providers: [
+ { provide: Store, useValue: {} },
+ { provide: CompositionPaletteService, useValue: compositionPaletteServiceStub },
+ { provide: SdcMenuToken, useValue: {} },
+ { provide: CompositionService, useValue: {} },
+ { provide: SdcUiServices.ModalService, useValue: {}},
+ { provide: ServiceServiceNg2, useValue: {} },
+ { provide: WorkspaceService, useValue: {} },
+ { provide: ComponentInstanceServiceNg2, useValue: {} },
+ { provide: EventListenerService, useValue: eventListenerServiceStub },
+ { provide: TranslateService, useValue: {}}
+ ]
+ });
+ };
+
+ configureTests(configure).then(testBed => {
+ fixture = testBed.createComponent(InfoTabComponent);
+ let comp = fixture.componentInstance;
+
+ });
+ })
+ );
+
+
+ it('can load instance', () => {
+ expect(fixture).toMatchSnapshot();
+ });
+
+ describe('Version dropdown', () => {
+ it('is undefined for topologyTemplate', () => {
+ fixture.componentInstance.component = <TopologyTemplate>{};
+ fixture.componentInstance.initEditResourceVersion(fixture.componentInstance.component, fixture.componentInstance.flatLeftPaletteElementsFromService(leftPaletteElements));
+ expect(fixture.componentInstance.versions).toBe(undefined);
+ });
+ it('does not contain the highest minor version if it is checked out', () => {
+ fixture.componentInstance.component = new ComponentInstance();
+ fixture.componentInstance.component.allVersions =
+ {'1.0': "9c829122-af05-4bc9-b537-5d84f4c8ae25", '1.1': "930d56cb-868d-4e35-bd0f-e737d2fdb171"};
+ fixture.componentInstance.component.version = "1.0";
+ fixture.componentInstance.component.uuid = "a8cf015e-e4e5-4d4b-a01e-8624e8d36095";
+ fixture.componentInstance.initEditResourceVersion(fixture.componentInstance.component, fixture.componentInstance.flatLeftPaletteElementsFromService(leftPaletteElements));
+ expect(fixture.componentInstance.versions).toHaveLength(1);
+ });
+ });
+
+});
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.ts
new file mode 100644
index 0000000000..45f31e7b35
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.ts
@@ -0,0 +1,189 @@
+import { Component, OnInit, Input, Inject, OnDestroy } from '@angular/core';
+import {
+ PolicyInstance,
+ GroupInstance,
+ Component as TopologyTemplate,
+ ComponentInstance,
+ LeftPaletteComponent,
+ FullComponentInstance
+} from "app/models";
+import {Store} from "@ngxs/store";
+import { EVENTS, GRAPH_EVENTS } from 'app/utils';
+import {IDropDownOption} from "onap-ui-angular/dist/form-elements/dropdown/dropdown-models";
+import { CompositionPaletteService } from "app/ng2/pages/composition/palette/services/palette.service";
+import { SdcUiCommon, SdcUiComponents, SdcUiServices } from "onap-ui-angular";
+import { SdcMenuToken, IAppMenu } from "app/ng2/config/sdc-menu.config";
+import { CompositionService } from "app/ng2/pages/composition/composition.service";
+import { ServiceServiceNg2 } from "app/services-ng2";
+import { WorkspaceService } from "app/ng2/pages/workspace/workspace.service";
+import { ComponentInstanceServiceNg2 } from "app/ng2/services/component-instance-services/component-instance.service";
+import { EventListenerService } from "app/services";
+import * as _ from 'lodash';
+import {SelectedComponentType, TogglePanelLoadingAction} from "../../../common/store/graph.actions";
+import Dictionary = _.Dictionary;
+
+
+@Component({
+ selector: 'panel-info-tab',
+ templateUrl: './info-tab.component.html',
+ styleUrls: ['./info-tab.component.less'],
+ // providers: [SdcUiServices.ModalService]
+})
+export class InfoTabComponent implements OnInit, OnDestroy {
+
+ @Input() isViewOnly: boolean;
+ @Input() componentType: SelectedComponentType;
+ @Input() component: TopologyTemplate | PolicyInstance | GroupInstance | ComponentInstance;
+ public versions: IDropDownOption[];
+ private leftPalletElements: LeftPaletteComponent[];
+ private isDisabledFlag: boolean;
+ private isComponentSelectedFlag: boolean;
+
+ constructor(private store: Store,
+ private compositionPaletteService: CompositionPaletteService,
+ private compositionService: CompositionService,
+ private workspaceService: WorkspaceService,
+ private modalService: SdcUiServices.ModalService,
+ private componentInstanceService: ComponentInstanceServiceNg2,
+ private serviceService: ServiceServiceNg2,
+ private eventListenerService: EventListenerService,
+ @Inject(SdcMenuToken) public sdcMenu:IAppMenu) {
+ }
+
+ ngOnInit() {
+ this.leftPalletElements = this.flatLeftPaletteElementsFromService(this.compositionPaletteService.getLeftPaletteElements());
+ this.initEditResourceVersion(this.component, this.leftPalletElements);
+ this.eventListenerService.registerObserverCallback(EVENTS.ON_CHECKOUT, (comp) => {
+ this.component = comp;
+ });
+ this.isComponentSelectedFlag = this.isComponentInstanceSelected();
+ this.isDisabledFlag = this.isDisabled();
+
+ }
+
+ ngOnDestroy() {
+ this.eventListenerService.unRegisterObserver(EVENTS.ON_CHECKOUT);
+ }
+
+ flatLeftPaletteElementsFromService = (leftPalleteElementsFromService: Dictionary<Dictionary<LeftPaletteComponent[]>>): LeftPaletteComponent[] => {
+ let retValArr = [];
+ for (const category in leftPalleteElementsFromService) {
+ for (const subCategory in leftPalleteElementsFromService[category]) {
+ retValArr = retValArr.concat(leftPalleteElementsFromService[category][subCategory].slice(0));
+ }
+ }
+ return retValArr;
+ }
+
+ private isComponentInstanceSelected () {
+ return this.componentType === SelectedComponentType.COMPONENT_INSTANCE;
+ }
+
+ private versioning: Function = (versionNumber: string): string => {
+ let version: Array<string> = versionNumber && versionNumber.split('.');
+ return '00000000'.slice(version[0].length) + version[0] + '.' + '00000000'.slice(version[1].length) + version[1];
+ };
+
+
+ private onChangeVersion = (versionDropdown) => {
+ let newVersionValue = versionDropdown.value;
+ versionDropdown.value = (<FullComponentInstance>this.component).getComponentUid();
+
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: true}));
+
+ // let service = <Service>this.$scope.currentComponent;
+ if(this.component instanceof FullComponentInstance) {
+
+ let onCancel = (error:any) => {
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: false}));
+ if (error) {
+ console.log(error);
+ }
+ };
+
+ let onUpdate = () => {
+ //this function will update the instance version than the function call getComponent to update the current component and return the new instance version
+ this.componentInstanceService.changeResourceInstanceVersion(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.component.uniqueId, newVersionValue)
+ .subscribe((component) => {
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: false}));
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_VERSION_CHANGED, component);
+ }, onCancel);
+ };
+
+ if (this.component.isService() || this.component.isServiceProxy()) {
+ this.serviceService.checkComponentInstanceVersionChange(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId,
+ this.component.uniqueId, newVersionValue).subscribe((pathsToDelete:string[]) => {
+ if (pathsToDelete && pathsToDelete.length) {
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: false}));
+
+
+ const {title, message} = this.sdcMenu.alertMessages['upgradeInstance'];
+ let pathNames:string = this.getPathNamesVersionChangeModal(pathsToDelete);
+ let onOk: Function = () => {
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: true}));
+
+ onUpdate();
+ };
+ const okButton = {testId: "OK", text: "OK", type: SdcUiCommon.ButtonType.info, callback: onOk, closeModal: true} as SdcUiComponents.ModalButtonComponent;
+ const cancelButton = {testId: "Cancel", text: "Cancel", type: SdcUiCommon.ButtonType.secondary, callback: <Function>onCancel, closeModal: true} as SdcUiComponents.ModalButtonComponent;
+ const modal = this.modalService.openInfoModal(title, message.format([pathNames]), 'confirm-modal', [okButton, cancelButton]);
+ modal.getCloseButton().onClick(onCancel);
+ } else {
+ onUpdate();
+ }
+ }, onCancel);
+ } else {
+ onUpdate();
+ }
+ }
+ };
+
+
+ private getPathNamesVersionChangeModal = (pathsToDelete:string[]):string => {
+ const relatedPaths = _.filter(this.compositionService.forwardingPaths, path =>
+ _.find(pathsToDelete, id =>
+ path.uniqueId === id
+ )
+ ).map(path => path.name);
+ const pathNames = _.join(relatedPaths, ', ') || 'none';
+ return pathNames;
+ };
+
+
+ private initEditResourceVersion = (component, leftPaletteComponents): void => {
+ if(this.component instanceof ComponentInstance) {
+
+ this.versions = [];
+ let sorted:any = _.sortBy(_.toPairs(component.allVersions), (item) => {
+ return item[0] !== "undefined" && this.versioning(item[0]);
+ });
+ _.forEach(sorted, (item) => {
+ this.versions.push({label: item[0], value: item[1]});
+ });
+
+ let highestVersion = _.last(sorted)[0];
+
+ if (parseFloat(highestVersion) % 1) { //if highest is minor, make sure it is the latest checked in -
+ let latestVersionComponent: LeftPaletteComponent = _.maxBy(
+ _.filter(leftPaletteComponents, (leftPaletteComponent: LeftPaletteComponent) => { //latest checked in
+ return (leftPaletteComponent.systemName === component.systemName || leftPaletteComponent.uuid === component.uuid);
+ })
+ , (component) => {
+ return component.version
+ });
+
+ let latestVersion: string = latestVersionComponent ? latestVersionComponent.version : highestVersion;
+
+ if (latestVersion && highestVersion != latestVersion) { //highest is checked out - remove from options
+ this.versions = this.versions.filter(version => version.label != highestVersion);
+ }
+ }
+ }
+ }
+
+ private isDisabled() {
+ return this.isViewOnly || this.component['archived'] || this.component['resourceType'] === 'CVFC'
+ }
+
+};
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/panel-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/panel-tab.component.ts
new file mode 100644
index 0000000000..c148a4e579
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/panel-tab.component.ts
@@ -0,0 +1,55 @@
+import { NgModule, Component, Compiler, ViewContainerRef, ViewChild, Input, ComponentRef, ComponentFactoryResolver, ChangeDetectorRef } from '@angular/core';
+import {Component as TopologyTemplate} from "app/models";
+import { SdcUiServices } from "onap-ui-angular";
+
+// Helper component to add dynamic tabs
+@Component({
+ selector: 'panel-tab',
+ template: `<div #content></div>`
+})
+export class PanelTabComponent {
+ @ViewChild('content', { read: ViewContainerRef }) content;
+ @Input() isActive:boolean;
+ @Input() panelTabType;
+ @Input() input;
+ @Input() isViewOnly:boolean;
+ @Input() component:TopologyTemplate;
+ @Input() componentType;
+ cmpRef: ComponentRef<any>;
+ private isViewInitialized: boolean = false;
+
+ constructor(private componentFactoryResolver: ComponentFactoryResolver,
+ private cdRef: ChangeDetectorRef) { }
+
+ updateComponent() {
+ if (!this.isViewInitialized || !this.isActive) {
+ return;
+ }
+ if (this.cmpRef) {
+ this.cmpRef.destroy();
+ }
+
+ let factory = this.componentFactoryResolver.resolveComponentFactory(this.panelTabType);
+ this.cmpRef = this.content.createComponent(factory);
+ this.cmpRef.instance.input = this.input;
+ this.cmpRef.instance.isViewOnly = this.isViewOnly;
+ this.cmpRef.instance.component = this.component;
+ this.cmpRef.instance.componentType = this.componentType;
+ this.cdRef.detectChanges();
+ }
+
+ ngOnChanges() {
+ this.updateComponent();
+ }
+
+ ngAfterViewInit() {
+ this.isViewInitialized = true;
+ this.updateComponent();
+ }
+
+ ngOnDestroy() {
+ if (this.cmpRef) {
+ this.cmpRef.destroy();
+ }
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/panel-tabs.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/panel-tabs.less
new file mode 100644
index 0000000000..b3c03f85c5
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/panel-tabs.less
@@ -0,0 +1,65 @@
+@import '../../../../../../assets/styles/variables';
+@import '../../../../../../assets/styles/override';
+
+
+// ---------------------------------------------------------------------------------------------------
+///* override sdc-ui library tabs */
+// ---------------------------------------------------------------------------------------------------
+
+
+:host ::ng-deep .sdc-tabs {
+
+ .sdc-tabs-list {
+ display: flex;
+ border-bottom: 1px solid @sdcui_color_silver;
+ min-height: min-content;
+ }
+ .sdc-tab {
+ background-color: @sdcui_color_white;
+ border: 1px solid @sdcui_color_silver;
+ border-left: none;
+ border-bottom: none;
+ height: 36px;
+ width: 60px;
+ display: flex;
+ align-content: center;
+ justify-content: center;
+ cursor: pointer;
+ padding: 0;
+ margin: 0;
+
+
+ &.sdc-tab-active {
+ background-color: @sdcui_color_silver;
+ border-bottom: none;
+ }
+ &[disabled] {
+ opacity: 0.3;
+ cursor: default;
+ }
+ }
+ &.sdc-tabs-header {
+ .sdc-tab {
+ font-size: 24px;
+ }
+ }
+ &.sdc-tabs-menu {
+ .sdc-tab {
+ font-size: 14px;
+ padding: 0px 10px 4px 10px;
+ }
+ }
+ .sdc-tab-content {
+ margin-top: 0;
+ flex:1;
+ overflow-y:auto;
+ }
+}
+
+
+:host ::ng-deep .expand-collapse-title {
+ margin-top: 1px;
+ background-color: #eaeaea;
+ color: #5a5a5a;
+ font-family: OpenSans-Semibold, sans-serif;
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-information-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-information-tab.component.html
deleted file mode 100644
index 2a1c58c4cf..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-information-tab.component.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!--
- ~ 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.
- -->
-
-<ng2-expand-collapse state="0">
- <header tooltip="General Information">General Info</header>
- <content>
- <!-- TYPE -->
- <div class="component-details-panel-item">
- <span class="name" [innerHTML]="'GENERAL_LABEL_TYPE' | translate"></span>
- <span class="value" data-tests-id="rightTab_componentType" tooltip="{{policy.policyTypeUid}}">{{policy.policyTypeUid}}</span>
- </div>
-
- <!-- CATEGORY -->
- <div class="component-details-panel-item">
- <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span>
- <span class="value" data-tests-id="rightTab_category" tooltip="Policy">Policy</span>
- </div>
-
- <!-- SUB CATEGORY -->
- <div class="component-details-panel-item">
- <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span>
- <span class="value" data-tests-id="rightTab_subCategory" tooltip="Policy">Policy</span>
- </div>
-
- <!-- VERSION -->
- <div class="component-details-panel-item">
- <span class="name" [innerHTML]="'GENERAL_LABEL_VERSION' | translate"></span>
- <span class="value" data-tests-id="rightTab_version" tooltip="{{policy.version}}">{{policy.version}}</span>
- </div>
-
- <!-- DESCRIPTION -->
- <div class="component-details-panel-item description">
- <span class="name" [innerHTML]="'GENERAL_LABEL_DESCRIPTION' | translate"></span>
- <span class="value" ellipsis="policy.description" max-chars="55" data-tests-id="rightTab_description">{{policy.description}}</span>
- </div>
- </content>
-</ng2-expand-collapse>
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.less
deleted file mode 100644
index e69de29bb2..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.less
+++ /dev/null
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.component.ts
deleted file mode 100644
index 1e2739901d..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.component.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-/*-
- * ============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 * as _ from "lodash";
-import { Component, Inject, Input, Output, EventEmitter, AfterViewInit, OnChanges } from "@angular/core";
-import { TranslateService } from './../../../../../shared/translator/translate.service';
-import { PoliciesService } from "../../../../../services/policies.service";
-import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models";
-import { PolicyInstance } from 'app/models/graph/zones/policy-instance';
-import { GRAPH_EVENTS } from './../../../../../../utils/constants';
-import { EventListenerService } from 'app/services/event-listener-service';
-import { ZoneInstance } from 'app/models/graph/zones/zone-instance';
-import { SimpleChanges } from "@angular/core/src/metadata/lifecycle_hooks";
-
-@Component({
- selector: 'policy-tabs',
- templateUrl: './policy-tabs.component.html'
-})
-export class PolicyTabsComponent implements OnChanges {
-
- @Input() topologyTemplate:TopologyTemplate;
- @Input() selectedZoneInstanceType:string;
- @Input() selectedZoneInstanceId:string;
- @Input() isViewOnly: boolean;
- @Output() isLoading: EventEmitter<boolean> = new EventEmitter<boolean>();
-
- private policy:PolicyInstance;
-
- constructor(private translateService:TranslateService,
- private policiesService:PoliciesService
- ) {
-
- }
-
- ngOnChanges(changes: SimpleChanges): void {
- this.initPolicy();
- }
-
- private initPolicy = ():void => {
- this.isLoading.emit(true);
- this.policiesService.getSpecificPolicy(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.selectedZoneInstanceId).subscribe(
- policy => {
- this.policy = policy;
- console.log(JSON.stringify(policy));
- },
- error => console.log("Error getting policy!"),
- () => this.isLoading.emit(false)
- );
- }
-
- private setIsLoading = (value) :void => {
- this.isLoading.emit(value);
- }
-
-}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.module.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.module.ts
deleted file mode 100644
index 38dc19e1af..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.module.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/*-
- * ============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 { NgModule } from "@angular/core";
-import { HttpModule } from "@angular/http";
-import { FormsModule } from "@angular/forms";
-import { BrowserModule } from "@angular/platform-browser";
-import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module';
-import { ExpandCollapseComponent } from 'app/ng2/components/ui/expand-collapse/expand-collapse.component';
-import { PoliciesService } from "../../../../../services/policies.service";
-import { PolicyInformationTabComponent } from "./policy-information-tab.component";
-import { PolicyTargetsTabComponent } from "./policy-targets-tab.component";
-import { PolicyTabsComponent } from "./policy-tabs.component";
-import { PolicyPropertiesTabComponent } from "./policy-properties-tab.component";
-import { SdcUiComponentsModule } from "sdc-ui/lib/angular";
-import { TranslateModule } from './../../../../../shared/translator/translate.module';
-
-@NgModule({
- declarations: [
- PolicyInformationTabComponent,
- PolicyTargetsTabComponent,
- PolicyPropertiesTabComponent,
- PolicyTabsComponent
- ],
- imports: [
- BrowserModule,
- FormsModule,
- HttpModule,
- SdcUiComponentsModule,
- TranslateModule,
- UiElementsModule
- ],
- entryComponents: [
- PolicyInformationTabComponent,
- PolicyTargetsTabComponent,
- PolicyPropertiesTabComponent,
- PolicyTabsComponent,
- ExpandCollapseComponent
- ],
- exports: [
- PolicyInformationTabComponent,
- PolicyTargetsTabComponent,
- PolicyPropertiesTabComponent,
- PolicyTabsComponent
- ],
- providers: [
- PoliciesService
- ]
-})
-export class PolicyTabsModule {
-
-}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.less
deleted file mode 100644
index cd7ace2b6f..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.less
+++ /dev/null
@@ -1,12 +0,0 @@
-/deep/
-.component-details-panel-tab-policy-targets {
- .component-details-panel-large-item {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- }
- .w-sdc-designer-sidebar-section-title {
- display: flex;
- justify-content: space-between;
- }
-} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.html
index e263836fb1..838fd8bb51 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.html
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.html
@@ -14,7 +14,7 @@
~ limitations under the License.
-->
-<div class="w-sdc-designer-sidebar-section-title" titleTooltip="Targets">Targets
+<h1 class="w-sdc-designer-sidebar-section-title" titleTooltip="Targets">Targets
<svg-icon-label *ngIf="!isViewOnly"
class="add-policy-button"
name="plus-circle-o"
@@ -24,7 +24,7 @@
labelPlacement="right"
(click)="openAddTargetModal()">
</svg-icon-label>
-</div>
+</h1>
<div class="expand-collapse-content">
<ul>
<li *ngFor="let target of targets; let i = index" class="component-details-panel-large-item"
@@ -40,7 +40,7 @@
</li>
</ul>
- <div *ngIf="targets.length===0" class="component-details-panel-tab-no-data">
+ <div *ngIf="!targets || targets.length===0" class="component-details-panel-tab-no-data">
<div class="component-details-panel-tab-no-data-title">No data to display yet</div>
<div class="component-details-panel-tab-no-data-content">Add targets to policy to see targets</div>
</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/base/base-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.less
index aa8e75115f..d16a1595df 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/base/base-tab.component.less
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.less
@@ -1,8 +1,6 @@
@import './../../../../../../../assets/styles/mixins';
-@import "./../../../../../../../assets/styles/variables-old";
-@import './../../../../../../../assets/styles/mixins_old';
-/deep/
+
.expand-collapse-content {
padding: 20px;
}
@@ -25,7 +23,9 @@
white-space: nowrap;
height: 32px;
line-height: 32px;
- vertical-align: middle;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
&:hover {
background-color: #f8f8f8;
@@ -37,30 +37,25 @@
}
}
-.component-details-panel-item {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- height: 22px;
- line-height: 22px;
- vertical-align: middle;
-
- &.description {
- margin-top: 28px;
- white-space: normal;
- word-wrap: break-word;
- .value {
- max-width: none;
- font-weight: normal;
- font-family: @font-opensans-regular;
- }
- }
-
- .name { font-family: OpenSans-Semibold, sans-serif; }
- .value { }
-}
-
.component-details-panel-item-delete {
cursor: pointer;
visibility: hidden;
}
+
+/deep/ .w-sdc-designer-sidebar-section-title {
+ color: #5a5a5a;
+ font-family: OpenSans-Semibold, sans-serif;
+ font-size: 14px;
+ background-color: #eaeaea;
+ cursor: pointer;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ text-transform: uppercase;
+ line-height: 32px;
+ padding: 0 10px 0 20px;
+ margin-top: 1px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.spec.ts
new file mode 100644
index 0000000000..7774138cab
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.spec.ts
@@ -0,0 +1,113 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular';
+import { Observable } from 'rxjs/Rx';
+import { Mock } from 'ts-mockery';
+import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper';
+import { ComponentMetadata } from '../../../../../../models/component-metadata';
+import { EventListenerService } from '../../../../../../services/event-listener-service';
+import { TranslateService } from '../../../../../shared/translator/translate.service';
+import { WorkspaceService } from '../../../../workspace/workspace.service';
+import { CompositionService } from '../../../composition.service';
+import { PolicyTargetsTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component";
+import { PoliciesService } from "app/services-ng2";
+import { PolicyInstance, GroupInstance } from "app/models";
+import { NgxsModule } from "@ngxs/store";
+import { GraphState } from "app/ng2/pages/composition/common/store/graph.state";
+import { WorkspaceState } from "app/ng2/store/states/workspace.state";
+import { TargetUiObject } from "app/models/ui-models/ui-target-object";
+import { TargetOrMemberType } from "app/utils";
+
+
+
+
+describe('policy targets tab component', () => {
+
+ let fixture: ComponentFixture<PolicyTargetsTabComponent>;
+ let component: PolicyTargetsTabComponent;
+
+ let policiesServiceMock = Mock.of<PoliciesService>(
+ {
+ updateTargets: jest.fn().mockImplementation((compType, uid, policyUniqueId, updatedTargets) => {
+ if (updatedTargets === undefined) {
+ return Observable.throwError('error');
+ } else {
+ return Observable.of(updatedTargets);
+ }
+ }
+ )});
+
+ let compositionServiceMock = {
+ componentInstances: [{uniqueId: '1', name: 'inst1'},
+ {uniqueId: '2', name: 'inst2'},
+ {uniqueId: '3', name: 'inst3'},
+ {uniqueId: '4', name: 'inst4'},
+ {uniqueId: '5', name: 'inst5'}
+ ],
+ groupInstances : [
+ Mock.of<GroupInstance>({uniqueId: "group1", name: "group1"}),
+ Mock.of<GroupInstance>({uniqueId: "group2", name: "group2"}),
+ Mock.of<GroupInstance>({uniqueId: "group3", name: "group3"})
+ ]
+ };
+
+ let workspaceServiceMock = {
+ metadata: Mock.of<ComponentMetadata>()
+ };
+
+ let modalServiceMock = {
+ openInfoModal: jest.fn(),
+ openCustomModal: jest.fn().mockImplementation(() => { return {
+ innerModalContent: { instance: { existingElements: targetsToAdd }},
+ closeModal: jest.fn()
+ }})
+ };
+
+ let loaderServiceMock = {
+ activate: jest.fn(),
+ deactivate: jest.fn()
+ };
+
+ const targetsToAdd = [
+ <TargetUiObject>{uniqueId: '1', name: 'inst1', type: TargetOrMemberType.COMPONENT_INSTANCES},
+ <TargetUiObject>{uniqueId: "group1", name: "group1", type: TargetOrMemberType.GROUPS}
+ ];
+
+ const policyInstanceMock = Mock.of<PolicyInstance>(
+ { getTargetsAsUiObject: jest.fn().mockImplementation( () => targetsToAdd)
+ });
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [PolicyTargetsTabComponent],
+ imports: [NgxsModule.forRoot([WorkspaceState])],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {provide: TranslateService, useValue: { translate: jest.fn() }},
+ {provide: PoliciesService, useValue: policiesServiceMock},
+ {provide: SdcUiServices.ModalService, useValue: modalServiceMock },
+ {provide: EventListenerService, useValue: {} },
+ {provide: CompositionService, useValue: compositionServiceMock },
+ {provide: WorkspaceService, useValue: workspaceServiceMock},
+ {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock }
+ ],
+ });
+
+ fixture = TestBed.createComponent(PolicyTargetsTabComponent);
+ component = fixture.componentInstance;
+ component.policy = policyInstanceMock;
+ });
+
+
+ it('if there are no existing targets, all component instances AND all groups are available for adding', () => {
+ component.targets = [];
+ const optionalTargetsToAdd = component.getOptionalsTargetsToAdd();
+ expect(optionalTargetsToAdd).toHaveLength(8);
+ });
+
+ it('list of available instances to add does not include existing targets', () => {
+ component.targets = targetsToAdd;
+ const optionalMembersToAdd = component.getOptionalsTargetsToAdd();
+ expect(optionalMembersToAdd).toHaveLength(6);
+ });
+});
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.ts
index b79f4d9e07..f117290397 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.ts
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.ts
@@ -19,84 +19,106 @@
*/
import * as _ from "lodash";
-import { Component, Input, Output, EventEmitter, OnChanges, HostBinding, OnDestroy } from "@angular/core";
+import { Component, Input, Output, EventEmitter, OnChanges, HostBinding, OnDestroy, OnInit } from "@angular/core";
import { TranslateService } from './../../../../../shared/translator/translate.service';
-import { Component as TopologyTemplate } from "app/models";
import { PoliciesService } from "../../../../../services/policies.service";
-import { PolicyInstance, PolicyTargetsMap } from './../../../../../../models/graph/zones/policy-instance';
-import { SimpleChanges } from "@angular/core/src/metadata/lifecycle_hooks";
-import { SdcUiComponents } from "sdc-ui/lib/angular";
-import { IModalConfig } from "sdc-ui/lib/angular/modals/models/modal-config";
+import { PolicyInstance } from './../../../../../../models/graph/zones/policy-instance';
+import { SdcUiComponents, SdcUiCommon, SdcUiServices } from "onap-ui-angular";
import { AddElementsComponent } from "../../../../../components/ui/modal/add-elements/add-elements.component";
import { TargetUiObject } from "../../../../../../models/ui-models/ui-target-object";
import { ComponentInstance } from "../../../../../../models/componentsInstances/componentInstance";
import { TargetOrMemberType } from "../../../../../../utils/constants";
import { GRAPH_EVENTS } from 'app/utils';
import { EventListenerService } from 'app/services/event-listener-service';
+import { CompositionService } from "app/ng2/pages/composition/composition.service";
+import { WorkspaceService } from "app/ng2/pages/workspace/workspace.service";
+import { Store } from "@ngxs/store";
+import { Select } from "@ngxs/store";
+import { Observable } from "rxjs";
+import { tap } from "rxjs/operators";
+import {GraphState} from "../../../common/store/graph.state";
@Component({
selector: 'policy-targets-tab',
templateUrl: './policy-targets-tab.component.html',
- styleUrls: ['./../base/base-tab.component.less', 'policy-targets-tab.component.less']
+ styleUrls: ['policy-targets-tab.component.less']
})
+
+export class PolicyTargetsTabComponent implements OnInit {
-export class PolicyTargetsTabComponent implements OnChanges, OnDestroy {
+ @Input() input:any;
- private targets: Array<TargetUiObject>; // UI object to hold all targets with names.
- @Input() policy: PolicyInstance;
- @Input() topologyTemplate: TopologyTemplate;
@Input() isViewOnly: boolean;
- @Output() isLoading: EventEmitter<boolean> = new EventEmitter<boolean>();
@HostBinding('class') classes = 'component-details-panel-tab-policy-targets';
+ @Select(GraphState.getSelectedComponent) policy$: Observable<PolicyInstance>;
+ public policy: PolicyInstance;
+ private subscription;
+
+ private addModalInstance: SdcUiComponents.ModalComponent;
+ public targets: Array<TargetUiObject>; // UI object to hold all targets with names.
+
constructor(private translateService: TranslateService,
private policiesService: PoliciesService,
- private modalService: SdcUiComponents.ModalService,
- private eventListenerService: EventListenerService
- ) {
- this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, this.initTargets)
- }
+ private modalService: SdcUiServices.ModalService,
+ private eventListenerService: EventListenerService,
+ private compositionService: CompositionService,
+ private workspaceService: WorkspaceService,
+ private loaderService: SdcUiServices.LoaderService,
+ private store: Store
+ ) { }
- ngOnChanges(changes:SimpleChanges):void {
- this.initTargets();
+ ngOnInit() {
+ this.subscription = this.policy$.pipe(
+ tap((policy) => {
+ if(policy instanceof PolicyInstance){
+ this.policy = policy;
+ this.targets = this.policy.getTargetsAsUiObject(<ComponentInstance[]>this.compositionService.componentInstances, this.compositionService.groupInstances);
+ }
+ })).subscribe();
}
- ngOnDestroy() {
- this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE);
+ ngOnDestroy () {
+ if(this.subscription)
+ this.subscription.unsubscribe();
}
deleteTarget(target: TargetUiObject): void {
- this.isLoading.emit(true);
- this.policiesService.deletePolicyTarget(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.policy, target.uniqueId, target.type).subscribe(
+ this.loaderService.activate();
+ this.policiesService.deletePolicyTarget(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.policy, target.uniqueId, target.type).subscribe(
(policyInstance:PolicyInstance) => {
+ this.targets = this.targets.filter(item => item.uniqueId !== target.uniqueId);
this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, policyInstance);
+ // this.store.dispatch(new UpdateSelectedComponentAction({uniqueId: policyInstance.uniqueId, type:ComponentType.}));
+ },
+ error => {
+ console.log("Error deleting target!");
+ this.loaderService.deactivate();
},
- error => console.log("Error deleting target!"),
- () => this.isLoading.emit(false)
+ () => this.loaderService.deactivate()
);
}
- private initTargets = (policyInstance?: PolicyInstance) => {
- this.policy = policyInstance ? policyInstance : this.policy;
- this.targets = this.policy.getTargetsAsUiObject(this.topologyTemplate.componentInstances, this.topologyTemplate.groupInstances);
- }
addTargets = ():void => {
- var targetsToAdd:Array<TargetUiObject> = this.modalService.getCurrentInstance().innerModalContent.instance.existingElements; //TODO refactor sdc-ui modal in order to return the data
+ var targetsToAdd:Array<TargetUiObject> = this.addModalInstance.innerModalContent.instance.existingElements; //TODO refactor sdc-ui modal in order to return the data
if(targetsToAdd.length > 0) {
- this.modalService.closeModal();
- this.isLoading.emit(true);
- var updatedTarget: Array<TargetUiObject> = _.union(this.targets, targetsToAdd);
- this.policiesService.updateTargets(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.policy.uniqueId, updatedTarget).subscribe(
+ this.addModalInstance.closeModal();
+ this.loaderService.activate();
+ var updatedTargets: Array<TargetUiObject> = _.union(this.targets, targetsToAdd);
+ this.policiesService.updateTargets(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.policy.uniqueId, updatedTargets).subscribe(
(updatedPolicyInstance:PolicyInstance) => {
+ this.targets = updatedTargets;
this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, updatedPolicyInstance);
+ // this.store.dispatch(new UpdateSelectedComponentAction({component: updatedPolicyInstance}));
},
error => {
console.log("Error updating targets!");
+ this.loaderService.deactivate();
},
- () => this.isLoading.emit(false)
+ () => this.loaderService.deactivate()
);
}
}
@@ -104,7 +126,7 @@ export class PolicyTargetsTabComponent implements OnChanges, OnDestroy {
getOptionalsTargetsToAdd():Array<TargetUiObject> {
let optionalsTargetsToAdd:Array<TargetUiObject> = [];
// adding all instances as optional targets to add if not already exist
- _.forEach(this.topologyTemplate.componentInstances, (instance:ComponentInstance) => {
+ _.forEach(this.compositionService.componentInstances, (instance:ComponentInstance) => {
if (!_.some(this.targets, (target:TargetUiObject) => {
return target.uniqueId === instance.uniqueId
})) {
@@ -113,7 +135,7 @@ export class PolicyTargetsTabComponent implements OnChanges, OnDestroy {
});
// adding all groups as optional targets to add if not already exist
- _.forEach(this.topologyTemplate.groupInstances, (groupInstance:ComponentInstance) => { // adding all instances as optional targets to add if not already exist
+ _.forEach(this.compositionService.groupInstances, (groupInstance:ComponentInstance) => { // adding all instances as optional targets to add if not already exist
if (!_.some(this.targets, (target:TargetUiObject) => {
return target.uniqueId === groupInstance.uniqueId
})) {
@@ -125,21 +147,20 @@ export class PolicyTargetsTabComponent implements OnChanges, OnDestroy {
}
openAddTargetModal(): void {
- let addTargetModalConfig: IModalConfig = {
+ let addTargetModalConfig = {
title: this.policy.name + " ADD TARGETS",
size: "md",
- type: "custom",
+ type: SdcUiCommon.ModalType.custom,
testId: "addTargetsModal",
buttons: [
{text: "ADD TARGETS", size: 'xsm', callback: this.addTargets, closeModal: false},
{text: 'CANCEL', size: 'sm', type: "secondary", closeModal: true}
]
- };
+ } as SdcUiCommon.IModalConfig;
var optionalTargetsToAdd = this.getOptionalsTargetsToAdd();
- this.modalService.openCustomModal(addTargetModalConfig, AddElementsComponent, {
+ this.addModalInstance = this.modalService.openCustomModal(addTargetModalConfig, AddElementsComponent, {
elementsToAdd: optionalTargetsToAdd,
elementName: "target"
});
-
}
}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.html
new file mode 100644
index 0000000000..86c6fea1ef
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.html
@@ -0,0 +1,97 @@
+<ng2-expand-collapse state="0">
+ <header sdc-tooltip tooltip-text="{{input.title}}">{{input.title}}</header>
+ <content>
+ <div class="w-sdc-designer-sidebar-section">
+ <div *ngIf="properties">
+ <ng-container *ngFor="let key of objectKeys(properties); let idx = index">
+ <sdc-accordion [title]="groupNameByKey(key) + ' Properties'" [css-class]="'properties-accordion'" [arrow-direction]="'right'" [testId]="groupNameByKey(key) + 'properties'" [open]="true">
+
+ <!--ng-show="isShowDetailsSection" -->
+ <div class="i-sdc-designer-sidebar-section-content-item" *ngIf="!groupPropertiesByInstance">
+ <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" attr.data-tests-id="propertyRow"
+ *ngFor="let property of properties[key]">
+
+ <div class="property-details">
+ <span class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label"
+ [ngClass]="{'hand enabled': !isViewOnly}"
+ sdc-tooltip tooltip-text="{{property.name}}"
+ (click)="!isViewOnly && updateProperty(property)"
+ attr.data-tests-id="{{property.name}}">{{property.name}}</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-value" *ngIf="isPropertyOwner()"
+ sdc-tooltip tooltip-text="{{property.defaultValue}}">{{property.defaultValue}}</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-value" *ngIf="!isPropertyOwner()"
+ sdc-tooltip tooltip-text="{{property.value}}"
+ attr.data-tests-id="value_{{property.name}}">{{property.value}}</span>
+ </div>
+ <div class="property-buttons">
+ <svg-icon *ngIf="!isViewOnly && (isPropertyOwner() && !property.readonly)" name="trash-o" clickable="true" size="medium" mode="info" testId="delete_{{property.name}}" (click)="deleteProperty(property)"></svg-icon>
+ </div>
+ </div>
+ </div>
+ <div class="i-sdc-designer-sidebar-section-content-item" *ngIf="groupPropertiesByInstance">
+ <ng-container *ngFor="let InstanceProperties of properties[key]; let propIndex = index">
+ <div class="vfci-properties-group">
+ <div class="second-level">
+ <div class="expand-collapse-title-icon"></div>
+ <span class="w-sdc-designer-sidebar-section-title-text" sdc-tooltip tooltip-text="{{getComponentInstanceNameFromInstanceByKey(InstanceProperties.key)}}&nbsp;Properties"
+ attr.data-tests-id="vfci-properties">{{getComponentInstanceNameFromInstanceByKey(InstanceProperties.key) + ' Properties'}}</span>
+ </div>
+ </div>
+ <div class="w-sdc-designer-sidebar-section-content instance-properties {{propIndex}}">
+ <div class="i-sdc-designer-sidebar-section-content-item">
+ <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" attr.data-tests-id="propertyRow"
+ *ngFor="let instanceProperty of InstanceProperties.value">
+ <div>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label"
+ [ngClass]="{'hand enabled': !isViewOnly}"
+ sdc-tooltip tooltip-text="{{instanceProperty.name}}"
+ attr.data-tests-id="vfci-property">{{instanceProperty.name}}</span>
+ </div>
+ <div>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-value"
+ sdc-tooltip tooltip-text="{{instanceProperty.value === undefined ? instanceProperty.defaultValue : instanceProperty.value}}">
+ {{instanceProperty.value === undefined ? instanceProperty.defaultValue : instanceProperty.value}}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </ng-container>
+ </div>
+ <!--<div class="w-sdc-designer-sidebar-section-footer" *ngIf="(!isViewOnly && isPropertyOwner()) || showAddPropertyButton">-->
+ <!--<button class="w-sdc-designer-sidebar-section-footer-action tlv-btn blue" attr.data-tests-id="addGrey" (click)="addProperty()" type="button">-->
+ <!--Add Property-->
+ <!--</button>-->
+ <!--</div>-->
+ </sdc-accordion>
+ </ng-container>
+ </div>
+
+ <!--attributes-->
+ <div *ngIf="attributes">
+ <ng-container *ngFor="let key of objectKeys(attributes); let attrIndex = index">
+ <sdc-accordion [title]="groupNameByKey(key) + ' Attributes'" [arrow-direction]="'right'" [testId]="groupNameByKey(key) + 'attributes'" [css-class]="'attributes-accordion'">
+ <!--ng-show="isShowDetailsSection" -->
+ <div class="i-sdc-designer-sidebar-section-content-item">
+ <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute"
+ *ngFor="let attribute of attributes[key]">
+ <div>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label"
+ [ngClass]="{'hand enabled': !isViewOnly}"
+ sdc-tooltip tooltip-text="{{attribute.name}}"
+ (click)="!isViewOnly && viewAttribute(attribute)"
+ attr.data-tests-id="{{attribute.name}}-attr">{{attribute.name}}</span>
+ </div>
+ <div>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-value" *ngIf="isPropertyOwner()"
+ sdc-tooltip tooltip-text="{{attribute.defaultValue}}">{{attribute.defaultValue}}</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-value" *ngIf="!isPropertyOwner()"
+ sdc-tooltip tooltip-text="{{attribute.value}}" attr.data-tests-id="value-of-{{attribute.name}}">{{attribute.value}}</span>
+ </div>
+ </div>
+ </div>
+ </sdc-accordion>
+ </ng-container>
+ </div>
+ </div>
+ </content>
+</ng2-expand-collapse>
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.less
new file mode 100644
index 0000000000..5cb0697da1
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.less
@@ -0,0 +1,66 @@
+.scroll-container {
+ display: flex;
+ overflow-y: auto;
+}
+
+.i-sdc-designer-sidebar-section-content-item-property-and-attribute {
+ color: #666666;
+ font-family: OpenSans-Semibold, sans-serif;
+ font-size: 14px;
+ border-bottom: 1px solid #cdcdcd;
+ min-height: 72px;
+ padding: 15px 10px 10px 18px;
+ // position: relative;
+ display:flex;
+
+ .property-details {
+ flex:1;
+ }
+
+ .property-buttons {
+ flex: 0 0 auto;
+ align-self: center;
+ }
+}
+
+.i-sdc-designer-sidebar-section-content-item-property-and-attribute-label {
+ display: block;
+ font-weight: bold;
+ &:hover {
+ color: #3b7b9b;
+ }
+}
+
+.i-sdc-designer-sidebar-section-content-item-property-and-attribute-label, .i-sdc-designer-sidebar-section-content-item-property-value {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 245px;
+ white-space: nowrap;
+ display: block;
+}
+
+
+
+/deep/ .expand-collapse-content {
+ max-height: max-content;
+ padding: 10px 0;
+
+ .sdc-accordion .sdc-accordion-header {
+
+ background-color: #e6f6fb;
+ border-left: solid #009fdb 4px;
+ box-shadow: 0 0px 3px -1px rgba(0, 0, 0, 0.3);
+ margin-bottom: 2px;
+ width: auto;
+ height: auto;
+ padding: 10px;
+ color: #666666;
+ font-family: OpenSans-Semibold, sans-serif;
+ font-size: 14px;
+
+ }
+
+ /deep/.sdc-accordion .sdc-accordion-body {
+ padding-left: 0;
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.ts
new file mode 100644
index 0000000000..b4b8248ed0
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.ts
@@ -0,0 +1,212 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { Store } from '@ngxs/store';
+import {
+ AttributeModel,
+ AttributesGroup,
+ Component as TopologyTemplate,
+ ComponentMetadata,
+ FullComponentInstance,
+ PropertiesGroup,
+ PropertyModel
+} from 'app/models';
+import { CompositionService } from 'app/ng2/pages/composition/composition.service';
+import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service';
+import { GroupByPipe } from 'app/ng2/pipes/groupBy.pipe';
+import { ResourceNamePipe } from 'app/ng2/pipes/resource-name.pipe';
+import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service';
+import { ComponentGenericResponse } from 'app/ng2/services/responses/component-generic-response';
+import { TranslateService } from 'app/ng2/shared/translator/translate.service';
+import { ModalsHandler } from 'app/utils';
+import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular';
+import {SelectedComponentType, TogglePanelLoadingAction} from "../../../common/store/graph.actions";
+
+@Component({
+ selector: 'properties-tab',
+ templateUrl: './properties-tab.component.html',
+ styleUrls: ['./properties-tab.component.less']
+})
+export class PropertiesTabComponent implements OnInit {
+ attributes: AttributesGroup;
+ isComponentInstanceSelected: boolean;
+ properties: PropertiesGroup;
+ groupPropertiesByInstance: boolean;
+ propertiesMessage: string;
+ metadata: ComponentMetadata;
+ objectKeys = Object.keys;
+
+ @Input() isViewOnly: boolean;
+ @Input() componentType: SelectedComponentType;
+ @Input() component: FullComponentInstance | TopologyTemplate;
+ @Input() input: {title: string};
+
+ constructor(private store: Store,
+ private workspaceService: WorkspaceService,
+ private compositionService: CompositionService,
+ private modalsHandler: ModalsHandler,
+ private topologyTemplateService: TopologyTemplateService,
+ private modalService: SdcUiServices.ModalService,
+ private translateService: TranslateService,
+ private groupByPipe: GroupByPipe) {
+ }
+
+ ngOnInit() {
+ this.metadata = this.workspaceService.metadata;
+ this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE;
+ this.getComponentInstancesPropertiesAndAttributes();
+ }
+
+ public isPropertyOwner = (): boolean => {
+ return this.component instanceof TopologyTemplate && this.component.isResource();
+ }
+
+ public updateProperty = (property: PropertyModel): void => {
+ this.openEditPropertyModal(property);
+ }
+
+ public deleteProperty = (property: PropertyModel): void => {
+
+ const onOk: Function = (): void => {
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: true}));
+ this.topologyTemplateService.deleteProperty(this.component.componentType, this.component.uniqueId, property.uniqueId)
+ .subscribe((response) => {
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: false}));
+ this.component.properties = this.component.properties.filter((prop) => prop.uniqueId !== property.uniqueId);
+ this.initComponentProperties();
+ }, () => {
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: false}));
+ });
+ };
+
+ const title: string = this.translateService.translate('PROPERTY_VIEW_DELETE_MODAL_TITLE');
+ const message: string = this.translateService.translate('PROPERTY_VIEW_DELETE_MODAL_TEXT', {name: property.name});
+ const okButton = {
+ testId: 'OK',
+ text: 'OK',
+ type: SdcUiCommon.ButtonType.info,
+ callback: onOk,
+ closeModal: true} as SdcUiComponents.ModalButtonComponent;
+ this.modalService.openInfoModal(title, message, 'delete-modal', [okButton]);
+ }
+
+ public groupNameByKey = (key: string): string => {
+ switch (key) {
+ case 'derived':
+ return 'Derived';
+
+ case this.metadata.uniqueId:
+ return ResourceNamePipe.getDisplayName(this.metadata.name);
+
+ default:
+ return this.getComponentInstanceNameFromInstanceByKey(key);
+ }
+ }
+
+ public getComponentInstanceNameFromInstanceByKey = (key: string): string => {
+ let instanceName: string = '';
+ const componentInstance = this.compositionService.getComponentInstances().find((item) => item.uniqueId === key);
+ if (key !== undefined && componentInstance) {
+
+ instanceName = ResourceNamePipe.getDisplayName(componentInstance.name);
+ }
+ return instanceName;
+ }
+
+ private getComponentInstancesPropertiesAndAttributes = () => {
+ this.topologyTemplateService.getComponentInstanceAttributesAndProperties(
+ this.workspaceService.metadata.uniqueId,
+ this.workspaceService.metadata.componentType)
+ .subscribe((genericResponse: ComponentGenericResponse) => {
+ this.compositionService.componentInstancesAttributes = genericResponse.componentInstancesAttributes || new AttributesGroup();
+ this.compositionService.componentInstancesProperties = genericResponse.componentInstancesProperties;
+ this.initPropertiesAndAttributes();
+ });
+ }
+
+ private initComponentProperties = (): void => {
+ let result: PropertiesGroup = {};
+
+ this.propertiesMessage = undefined;
+ this.groupPropertiesByInstance = false;
+ if (this.component instanceof FullComponentInstance) {
+ result[this.component.uniqueId] = _.orderBy(this.compositionService.componentInstancesProperties[this.component.uniqueId], ['name']);
+ if (this.component.originType === 'VF') {
+ this.groupPropertiesByInstance = true;
+ result[this.component.uniqueId] = Array.from(this.groupByPipe.transform(result[this.component.uniqueId], 'path'));
+ }
+ } else if (this.metadata.isService()) {
+ // Temporally fix to hide properties for service (UI stack when there are many properties)
+ result = this.compositionService.componentInstancesProperties;
+ this.propertiesMessage = 'Note: properties for service are disabled';
+ } else {
+ const componentUid = this.component.uniqueId;
+ result[componentUid] = Array<PropertyModel>();
+ const derived = Array<PropertyModel>();
+ _.forEach(this.component.properties, (property: PropertyModel) => {
+ if (componentUid === property.parentUniqueId) {
+ result[componentUid].push(property);
+ } else {
+ property.readonly = true;
+ derived.push(property);
+ }
+ });
+ if (derived.length) {
+ result['derived'] = derived;
+ }
+ this.objectKeys(result).forEach((key) => { result[key] = _.orderBy(result[key], ['name']); });
+ }
+ this.properties = result;
+ }
+
+ private initComponentAttributes = (): void => {
+ let result: AttributesGroup = {};
+
+ if (this.component) {
+ if (this.component instanceof FullComponentInstance) {
+ result[this.component.uniqueId] = this.compositionService.componentInstancesAttributes[this.component.uniqueId] || [];
+ } else if (this.metadata.isService()) {
+ result = this.compositionService.componentInstancesAttributes;
+ } else {
+ result[this.component.uniqueId] = (this.component as TopologyTemplate).attributes;
+ }
+ this.attributes = result;
+ this.objectKeys(this.attributes).forEach((key) => {
+ this.attributes[key] = _.orderBy(this.attributes[key], ['name']);
+ });
+
+ }
+ }
+
+ /**
+ * This function is checking if the component is the value owner of the current property
+ * in order to notify the edit property modal which fields to disable
+ */
+ private isPropertyValueOwner = (): boolean => {
+ return this.metadata.isService() || !!this.component;
+ }
+
+ /**
+ * The function opens the edit property modal.
+ * It checks if the property is from the VF or from one of it's resource instances and sends the needed property list.
+ * For create property reasons an empty array is transferd
+ *
+ * @param property the wanted property to edit/create
+ */
+ private openEditPropertyModal = (property: PropertyModel): void => {
+ this.modalsHandler.newOpenEditPropertyModal(property,
+ (this.isPropertyOwner() ?
+ this.properties[property.parentUniqueId] :
+ this.properties[property.resourceInstanceUniqueId]) || [],
+ this.isPropertyValueOwner(), 'component', property.resourceInstanceUniqueId).then((updatedProperty: PropertyModel) => {
+ if (updatedProperty) {
+ const oldProp = _.find(this.properties[updatedProperty.resourceInstanceUniqueId],
+ (prop: PropertyModel) => prop.uniqueId === updatedProperty.uniqueId);
+ oldProp.value = updatedProperty.value;
+ }
+ });
+ }
+
+ private initPropertiesAndAttributes = (): void => {
+ this.initComponentProperties();
+ this.initComponentAttributes();
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.html
new file mode 100644
index 0000000000..27e05ec1f0
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.html
@@ -0,0 +1,36 @@
+<div class="w-sdc-designer-sidebar-tab-content sdc-general-tab relations">
+ <div *ngIf="!isCurrentDisplayComponentIsComplex(); else complexComponentTemplate">
+ <div class="w-sdc-designer-sidebar-section w-sdc-designer-sidebar-section-relations">
+ <sdc-accordion [title]="'Capabilities'" [arrow-direction]="'right'" [testId]="'Capabilities-accordion'">
+ <div *ngFor="let capability of capabilities" class="relations-details-container">
+ <div class="relations-name">{{capability.name}}&nbsp;</div>
+ <div class="relations-desc"> {{capability.type}} </div>
+ </div>
+ </sdc-accordion>
+ </div>
+ <div class="w-sdc-designer-sidebar-section w-sdc-designer-sidebar-section-relations">
+ <sdc-accordion [title]="'Requirements'" [arrow-direction]="'right'" [testId]="'Requirements-accordion'">
+ <requirement-list [component]='component' [requirements]="requirements" [isInstanceSelected]="isComponentInstanceSelected"></requirement-list>
+ </sdc-accordion>
+
+ </div>
+ </div>
+
+ <ng-template #complexComponentTemplate>
+ <sdc-accordion *ngIf="capabilitiesInstancesMap" [title]="'Capabilities'" [arrow-direction]="'right'" [testId]="'Capabilities-accordion'">
+ <sdc-accordion *ngFor="let key of objectKeys(capabilitiesInstancesMap); let i = index" [title]="key">
+ <div *ngFor="let capability of capabilitiesInstancesMap[key]" class="relations-details-container">
+ <div class="relations-name">{{capability.name}}&nbsp;</div>
+ <div class="relations-desc"> {{capability.type}} </div>
+ </div>
+ </sdc-accordion>
+ </sdc-accordion>
+
+ <sdc-accordion *ngIf="requirementsInstancesMap" [title]="'Requirements'" [arrow-direction]="'right'" [testId]="'Requirements-accordion'">
+ <sdc-accordion *ngFor="let key of objectKeys(requirementsInstancesMap); let i = index" [title]="key">
+ <requirement-list [component]='component' [requirements]="requirementsInstancesMap[key]" [isInstanceSelected]="isComponentInstanceSelected"></requirement-list>
+ </sdc-accordion>
+ </sdc-accordion>
+
+ </ng-template>
+</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.less
new file mode 100644
index 0000000000..fe4573aadc
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.less
@@ -0,0 +1,57 @@
+
+/deep/.sdc-accordion {
+ margin-bottom: 0;
+ display: grid;
+
+ .sdc-accordion-header {
+ background-color: #e6f6fb;
+ border-left: solid #009fdb 4px;
+ box-shadow: 0 0px 3px -1px rgba(0, 0, 0, 0.3);
+ margin-bottom: 2px;
+ width: auto;
+ height: auto;
+ padding: 10px;
+ color: #666666;
+ font-family: OpenSans-Semibold, sans-serif;
+ font-size: 14px;
+ }
+
+ .sdc-accordion-body.open {
+ padding-left: 0;
+ padding-top: 0;
+ .sdc-accordion-header { /*Second level - nested accordion */
+ background-color: #f8f8f8;
+ padding: 4px 20px 4px 37px;
+ border-bottom: 1px solid #d2d2d2;
+ border-left:none;
+ height: 30px;
+ }
+ }
+}
+
+
+.relations-details-container {
+ border-bottom: 1px solid #cdcdcd;
+ padding: 10px 10px 10px 18px;
+
+ font-size: 14px;
+ font-family: OpenSans-Regular, sans-serif;
+
+ .relations-name {
+ color: #666666;
+ font-weight: bold;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ text-transform: capitalize;
+ max-width: 240px;
+ display: inline-block;
+ }
+
+ .relations-desc {
+ color: #8c8c8c;
+ word-wrap: break-word;
+ white-space: normal;
+ max-width: 265px;
+ }
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.ts
new file mode 100644
index 0000000000..03697b38f2
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.ts
@@ -0,0 +1,165 @@
+import { Component, OnInit, Input, OnDestroy } from '@angular/core';
+import { Component as TopologyTemplate, Capability, Requirement, CapabilitiesGroup, RequirementsGroup, ComponentInstance, FullComponentInstance } from "app/models";
+import { Store } from "@ngxs/store";
+import { GRAPH_EVENTS } from "app/utils";
+import { ComponentGenericResponse } from "app/ng2/services/responses/component-generic-response";
+import { TopologyTemplateService } from "app/ng2/services/component-services/topology-template.service";
+import { EventListenerService } from "app/services";
+import { WorkspaceService } from "app/ng2/pages/workspace/workspace.service";
+import { CompositionService } from "app/ng2/pages/composition/composition.service";
+import {SelectedComponentType, TogglePanelLoadingAction} from "../../../common/store/graph.actions";
+
+
+export class InstanceCapabilitiesMap {
+ [key:string]:Array<Capability>;
+}
+
+export class InstanceRequirementsMap {
+ [key:string]:Array<Requirement>;
+}
+
+@Component({
+ selector: 'req-capabilities-tab',
+ templateUrl: './req-capabilities-tab.component.html',
+ styleUrls: ['./req-capabilities-tab.component.less']
+})
+export class ReqAndCapabilitiesTabComponent implements OnInit, OnDestroy {
+
+ isComponentInstanceSelected: boolean;
+ capabilities:Array<Capability>;
+ requirements:Array<Requirement>;
+ capabilitiesInstancesMap:InstanceCapabilitiesMap;
+ requirementsInstancesMap:InstanceRequirementsMap;
+ objectKeys = Object.keys;
+
+ @Input() isViewOnly: boolean;
+ @Input() componentType: SelectedComponentType;
+ @Input() component: TopologyTemplate | FullComponentInstance;
+ @Input() input: any;
+
+
+ constructor(private store: Store,
+ private topologyTemplateService:TopologyTemplateService,
+ private workspaceService: WorkspaceService,
+ private compositionService: CompositionService,
+ private eventListenerService:EventListenerService) { }
+
+ ngOnInit(): void {
+
+ this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE;
+
+ this.requirements = [];
+ this.capabilities = [];
+ this.initEvents();
+ this.initRequirementsAndCapabilities();
+
+ }
+
+ private initEvents = ():void => {
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_NODE_SELECTED, this.initRequirementsAndCapabilities);
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED, this.updateRequirementCapabilities);
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_CREATE_COMPONENT_INSTANCE, this.updateRequirementCapabilities);
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, this.updateRequirementCapabilities);
+ }
+
+ ngOnDestroy(): void {
+ this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_NODE_SELECTED, this.initRequirementsAndCapabilities);
+ this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED, this.updateRequirementCapabilities);
+ this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_CREATE_COMPONENT_INSTANCE, this.updateRequirementCapabilities);
+ this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, this.updateRequirementCapabilities);
+ }
+
+ public isCurrentDisplayComponentIsComplex = ():boolean => {
+
+ if (this.component instanceof FullComponentInstance) {
+ if (this.component.originType === 'VF') {
+ return true;
+ }
+ return false;
+ } else {
+ return this.component.isComplex();
+ }
+ }
+
+ private loadComplexComponentData = () => {
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: true}));
+
+ this.topologyTemplateService.getCapabilitiesAndRequirements(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId).subscribe((response:ComponentGenericResponse) => {
+ this.workspaceService.metadata.capabilities = response.capabilities;
+ this.workspaceService.metadata.requirements = response.requirements;
+ this.setScopeCapabilitiesRequirements(response.capabilities, response.requirements);
+ this.initInstancesMap();
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: false}));
+ }, (error) => { this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); });
+ }
+
+
+ private extractValuesFromMap = (map:CapabilitiesGroup | RequirementsGroup):Array<any> => {
+ let values = [];
+ _.forEach(map, (capabilitiesOrRequirements:Array<Capability> | Array<Requirement>, key) => {
+ values = values.concat(capabilitiesOrRequirements)
+ }
+ );
+ return values;
+ }
+
+ private setScopeCapabilitiesRequirements = (capabilities:CapabilitiesGroup, requirements:RequirementsGroup) => {
+ this.capabilities = this.extractValuesFromMap(capabilities);
+ this.requirements = this.extractValuesFromMap(requirements);
+ }
+
+
+ private initInstancesMap = ():void => {
+
+ this.capabilitiesInstancesMap = new InstanceCapabilitiesMap();
+ _.forEach(this.capabilities, (capability:Capability) => {
+ if (this.capabilitiesInstancesMap[capability.ownerName]) {
+ this.capabilitiesInstancesMap[capability.ownerName] = this.capabilitiesInstancesMap[capability.ownerName].concat(capability);
+ } else {
+ this.capabilitiesInstancesMap[capability.ownerName] = new Array<Capability>(capability);
+ }
+ });
+
+ this.requirementsInstancesMap = new InstanceRequirementsMap();
+ _.forEach(this.requirements, (requirement:Requirement) => {
+ if (this.requirementsInstancesMap[requirement.ownerName]) {
+ this.requirementsInstancesMap[requirement.ownerName] = this.requirementsInstancesMap[requirement.ownerName].concat(requirement);
+ } else {
+ this.requirementsInstancesMap[requirement.ownerName] = new Array<Requirement>(requirement);
+ }
+ });
+ }
+
+ private initRequirementsAndCapabilities = (needUpdate?: boolean) => {
+
+ // if instance selected, we take the requirement and capabilities of the instance - always exist because we load them with the graph
+ if (this.component instanceof FullComponentInstance) {
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: false}));
+ this.setScopeCapabilitiesRequirements(this.component.capabilities, this.component.requirements);
+ if (this.component.originType === 'VF') {
+ this.initInstancesMap();
+ }
+ } else {
+ // if instance not selected, we take the requirement and capabilities of the VF/SERVICE, if not exist we call api
+ if (needUpdate || !this.component.capabilities || !this.component.requirements) {
+ this.loadComplexComponentData();
+
+ } else {
+ this.store.dispatch(new TogglePanelLoadingAction({isLoading: false}));
+ this.setScopeCapabilitiesRequirements(this.component.capabilities, this.component.requirements);
+ this.initInstancesMap();
+ }
+ }
+ }
+
+ private updateRequirementCapabilities = () => {
+ if (!this.isComponentInstanceSelected) {
+ this.loadComplexComponentData();
+ }
+ }
+
+
+
+
+}
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component.html
new file mode 100644
index 0000000000..8292729cf8
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component.html
@@ -0,0 +1,20 @@
+<div class="i-sdc-designer-sidebar-capabilities-requirements">
+ <div class="i-sdc-designer-sidebar-section-content-item-relations-group">
+ <div class="i-sdc-designer-sidebar-section-content-item-relations"
+ *ngFor="let requirement of requirements">
+ <div class="i-sdc-designer-sidebar-section-content-item-relations-details">
+ <div class="i-sdc-designer-sidebar-section-content-item-relations-details-name">{{requirement.name}}&nbsp;</div>
+ <div class="i-sdc-designer-sidebar-section-content-item-relations-details-desc">{{requirement.node}}
+ <div *ngIf="getRelation(requirement) != null">
+ <div class="i-sdc-designer-sidebar-section-content-item-relations-details-indent-box"></div>
+ <div class="i-sdc-designer-sidebar-section-content-item-relations-details-child">
+ <span class="i-sdc-designer-sidebar-section-content-item-relations-details-desc">{{getRelation(requirement).type}} <br/></span>
+ <span class="i-sdc-designer-sidebar-section-content-item-relations-details-name">{{getRelation(requirement).requirementName}}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component.ts
new file mode 100644
index 0000000000..e167c47dcc
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component.ts
@@ -0,0 +1,40 @@
+import { Component, Input } from '@angular/core';
+import { Component as TopologyTemplate, RelationshipModel, Relationship, Requirement } from "app/models";
+import { CompositionService } from "app/ng2/pages/composition/composition.service";
+import { ResourceNamePipe } from "app/ng2/pipes/resource-name.pipe";
+
+@Component({
+ selector: 'requirement-list',
+ templateUrl: './requirement-list.component.html'
+})
+export class RequirementListComponent {
+ @Input() component: TopologyTemplate;
+ @Input() requirements: Array<Requirement>;
+ @Input() isInstanceSelected:boolean;
+
+
+ constructor(private compositionService: CompositionService) { }
+
+
+ public getRelation = (requirement:any):any => {
+ if (this.isInstanceSelected && this.component.componentInstancesRelations) {
+ let relationItem:Array<RelationshipModel> = _.filter(this.component.componentInstancesRelations, (relation:RelationshipModel) => {
+ return relation.fromNode === this.component.uniqueId &&
+ _.filter(relation.relationships, (relationship:Relationship) => {
+ return relationship.relation.requirement == requirement.name && relationship.relation.requirementOwnerId == requirement.ownerId;
+ }).length;
+ });
+
+ if (relationItem && relationItem.length) {
+ return {
+ type: requirement.relationship.split('.').pop(),
+ requirementName: ResourceNamePipe.getDisplayName(this.compositionService.componentInstances[_.map
+ (this.compositionService.componentInstances, "uniqueId").indexOf(relationItem[0].toNode)].name)
+ };
+ }
+ }
+ return null;
+ };
+
+};
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.html
new file mode 100644
index 0000000000..a52c841156
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.html
@@ -0,0 +1,15 @@
+<ng2-expand-collapse state="0">
+ <header sdc-tooltip tooltip-text="{{input.title}}">{{input.title}}</header>
+ <content>
+ <service-consumption
+ [parentService]="metadata"
+ [selectedService]="component"
+ [selectedServiceInstanceId]="component.uniqueId"
+ [instancesMappedList]="instancesMappedList"
+ [parentServiceInputs]="componentInputs"
+ [instancesCapabilitiesMap]="instancesCapabilitiesMap"
+ [readonly]="isViewOnly">
+ </service-consumption>
+ </content>
+</ng2-expand-collapse>
+
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.less
index e69de29bb2..e69de29bb2 100644
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.less
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.less
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.ts
new file mode 100644
index 0000000000..8715afd047
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.ts
@@ -0,0 +1,89 @@
+
+import { Component, Input } from '@angular/core';
+import { Store } from '@ngxs/store';
+import {
+ CapabilitiesGroup,
+ Capability,
+ Component as TopologyTemplate,
+ ComponentInstance,
+ FullComponentInstance,
+ InputBEModel,
+ InputsGroup,
+ InterfaceModel,
+ PropertiesGroup
+} from 'app/models';
+import { ComponentMetadata } from '../../../../../../models/component-metadata';
+import { ServiceInstanceObject } from '../../../../../../models/service-instance-properties-and-interfaces';
+import { EventListenerService } from '../../../../../../services/event-listener-service';
+import { TopologyTemplateService } from '../../../../../services/component-services/topology-template.service';
+import { ComponentGenericResponse } from '../../../../../services/responses/component-generic-response';
+import { WorkspaceService } from '../../../../workspace/workspace.service';
+import { SelectedComponentType } from '../../../common/store/graph.actions';
+import { CompositionService } from '../../../composition.service';
+
+@Component({
+ selector: 'service-consumption-tab',
+ templateUrl: './service-consumption-tab.component.html',
+ styleUrls: ['./service-consumption-tab.component.less'],
+})
+export class ServiceConsumptionTabComponent {
+ isComponentInstanceSelected: boolean;
+
+ instancesMappedList: ServiceInstanceObject[];
+ componentInstancesProperties: PropertiesGroup;
+ componentInstancesInputs: InputsGroup;
+ componentInstancesInterfaces: Map<string, InterfaceModel[]>;
+ componentInputs: InputBEModel[];
+ componentCapabilities: Capability[];
+ instancesCapabilitiesMap: Map<string, Capability[]>;
+ metadata: ComponentMetadata;
+
+ @Input() isViewOnly: boolean;
+ @Input() componentType: SelectedComponentType;
+ @Input() component: TopologyTemplate | FullComponentInstance;
+ @Input() input: any;
+
+ constructor(private store: Store,
+ private topologyTemplateService: TopologyTemplateService,
+ private workspaceService: WorkspaceService,
+ private compositionService: CompositionService,
+ private eventListenerService: EventListenerService ) {}
+ ngOnInit() {
+ this.metadata = this.workspaceService.metadata;
+ this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE;
+ this.initInstances();
+ }
+
+ private initInstances = (): void => {
+ this.topologyTemplateService.getServiceConsumptionData(this.metadata.componentType, this.metadata.uniqueId).subscribe((genericResponse: ComponentGenericResponse) => {
+ this.componentInstancesProperties = genericResponse.componentInstancesProperties;
+ this.componentInstancesInputs = genericResponse.componentInstancesInputs;
+ this.componentInstancesInterfaces = genericResponse.componentInstancesInterfaces;
+ this.componentInputs = genericResponse.inputs;
+ this.buildInstancesCapabilitiesMap(genericResponse.componentInstances);
+ this.updateInstanceAttributes();
+ });
+ }
+
+ private buildInstancesCapabilitiesMap = (componentInstances: Array<ComponentInstance>): void => {
+ this.instancesCapabilitiesMap = new Map();
+ let flattenCapabilities = [];
+ _.forEach(componentInstances, (componentInstance) => {
+ flattenCapabilities = CapabilitiesGroup.getFlattenedCapabilities(componentInstance.capabilities);
+ this.instancesCapabilitiesMap[componentInstance.uniqueId] = _.filter(flattenCapabilities, cap => cap.properties && cap.ownerId === componentInstance.uniqueId);
+ });
+ }
+
+ private updateInstanceAttributes = (): void => {
+ if (this.isComponentInstanceSelected && this.componentInstancesProperties) {
+ this.instancesMappedList = this.compositionService.componentInstances.map((coInstance) => new ServiceInstanceObject({
+ id: coInstance.uniqueId,
+ name: coInstance.name,
+ properties: this.componentInstancesProperties[coInstance.uniqueId] || [],
+ inputs: this.componentInstancesInputs[coInstance.uniqueId] || [],
+ interfaces: this.componentInstancesInterfaces[coInstance.uniqueId] || []
+ }));
+ }
+ }
+
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.html
new file mode 100644
index 0000000000..47351a46a1
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.html
@@ -0,0 +1,18 @@
+<ng2-expand-collapse state="0">
+ <header sdc-tooltip tooltip-text="{{input.title}}">{{input.title}}</header>
+ <content>
+ <div *ngIf="isComponentInstanceSelected">
+ <service-dependencies
+ [compositeService]="metaData"
+ [currentServiceInstance]="component"
+ [selectedInstanceProperties]="selectedInstanceProperties"
+ [selectedInstanceSiblings]="selectedInstanceSiblings"
+ [selectedInstanceConstraints]="selectedInstanceConstraints"
+ [readonly]="isViewOnly"
+ (dependencyStatus)="notifyDependencyEventsObserver($event)"
+ (updateRulesListEvent)="updateSelectedInstanceConstraints($event)"
+ (loadRulesListEvent)="loadConstraints()">
+ </service-dependencies>
+ </div>
+ </content>
+</ng2-expand-collapse>
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.less
new file mode 100644
index 0000000000..47e26e2d64
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.less
@@ -0,0 +1,3 @@
+:host /deep/ .expand-collapse-content {
+ padding: 0 0 10px;
+} \ No newline at end of file
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.ts
new file mode 100644
index 0000000000..5171e3b607
--- /dev/null
+++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.ts
@@ -0,0 +1,95 @@
+
+import { Component, Input } from '@angular/core';
+import { Store } from '@ngxs/store';
+import {
+ CapabilitiesGroup,
+ Capability,
+ Component as TopologyTemplate,
+ ComponentInstance,
+ FullComponentInstance,
+ InputBEModel,
+ InputsGroup,
+ InterfaceModel,
+ PropertiesGroup,
+ PropertyBEModel,
+} from 'app/models';
+import { DEPENDENCY_EVENTS } from 'app/utils/constants';
+import { ComponentMetadata } from '../../../../../../models/component-metadata';
+import { ServiceInstanceObject } from '../../../../../../models/service-instance-properties-and-interfaces';
+import { EventListenerService } from '../../../../../../services/event-listener-service';
+import { ConstraintObject } from '../../../../../components/logic/service-dependencies/service-dependencies.component';
+import { TopologyTemplateService } from '../../../../../services/component-services/topology-template.service';
+import { ComponentGenericResponse } from '../../../../../services/responses/component-generic-response';
+import { WorkspaceService } from '../../../../workspace/workspace.service';
+import { SelectedComponentType } from '../../../common/store/graph.actions';
+import { CompositionService } from '../../../composition.service';
+
+@Component({
+ selector: 'service-dependencies-tab',
+ templateUrl: 'service-dependencies-tab.component.html',
+ styleUrls: ['service-dependencies-tab.component.less']
+})
+export class ServiceDependenciesTabComponent {
+ isComponentInstanceSelected: boolean;
+
+ selectedInstanceSiblings: ServiceInstanceObject[];
+ componentInstancesConstraints: any[];
+ selectedInstanceConstraints: ConstraintObject[];
+ selectedInstanceProperties: PropertyBEModel[];
+ componentInstanceProperties: PropertiesGroup;
+ metaData: ComponentMetadata;
+
+ @Input() isViewOnly: boolean;
+ @Input() componentType: SelectedComponentType;
+ @Input() component: FullComponentInstance | TopologyTemplate;
+ @Input() input: any;
+
+ constructor(private store: Store,
+ private topologyTemplateService: TopologyTemplateService,
+ private workspaceService: WorkspaceService,
+ private compositionService: CompositionService,
+ private eventListenerService: EventListenerService) {
+ }
+
+ ngOnInit() {
+ this.metaData = this.workspaceService.metadata;
+ this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE;
+ this.initInstancesWithProperties();
+ this.loadConstraints();
+ this.initInstancesWithProperties();
+ }
+
+ public loadConstraints = (): void => {
+ this.topologyTemplateService.getServiceFilterConstraints(this.metaData.componentType, this.metaData.uniqueId).subscribe((response) => {
+ this.componentInstancesConstraints = response.nodeFilterData;
+ });
+ }
+
+ public notifyDependencyEventsObserver = (isChecked: boolean): void => {
+ this.eventListenerService.notifyObservers(DEPENDENCY_EVENTS.ON_DEPENDENCY_CHANGE, isChecked);
+ }
+
+ public updateSelectedInstanceConstraints = (constraintsList:Array<ConstraintObject>):void => {
+ this.componentInstancesConstraints[this.component.uniqueId].properties = constraintsList;
+ this.selectedInstanceConstraints = this.componentInstancesConstraints[this.component.uniqueId].properties;
+ }
+
+ private initInstancesWithProperties = (): void => {
+ this.topologyTemplateService.getComponentInstanceProperties(this.metaData.componentType, this.metaData.uniqueId).subscribe((genericResponse: ComponentGenericResponse) => {
+ this.componentInstanceProperties = genericResponse.componentInstancesProperties;
+ this.updateInstanceAttributes();
+ });
+ }
+
+ private updateInstanceAttributes = (): void => {
+ if (this.isComponentInstanceSelected && this.componentInstanceProperties) {
+ const instancesMappedList = this.compositionService.componentInstances.map((coInstance) => new ServiceInstanceObject({
+ id: coInstance.uniqueId,
+ name: coInstance.name,
+ properties: this.componentInstanceProperties[coInstance.uniqueId] || []
+ }));
+ this.selectedInstanceProperties = this.componentInstanceProperties[this.component.uniqueId];
+ this.selectedInstanceSiblings = instancesMappedList.filter((coInstance) => coInstance.id !== this.component.uniqueId);
+ }
+ }
+}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.html
deleted file mode 100644
index 9bb809249a..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!--
- ~ 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.
- -->
-
-<ng2-composition-panel-header
- [name]="selectedZoneInstanceName"
- [topologyTemplate]="topologyTemplate"
- [selectedZoneInstanceType]="selectedZoneInstanceType"
- [selectedZoneInstanceId]="selectedZoneInstanceId"
- [nonCertified]="nonCertified"
- [isViewOnly]="isViewOnly"
- [isLoading]="isLoading"
-></ng2-composition-panel-header>
-
-<div class="component-details-panel-tabs">
- <loader [display]="isLoading" [size]="'large'" [relative]="true" [loaderDelay]="500"></loader>
-
- <div *ngIf="selectedZoneInstanceType === zoneInstanceType.POLICY">
- <policy-tabs
- [topologyTemplate]="topologyTemplate"
- [selectedZoneInstanceType]="selectedZoneInstanceType"
- [selectedZoneInstanceId]="selectedZoneInstanceId"
- [isViewOnly]="isViewOnly"
- (isLoading)="setIsLoading($event)"
- ></policy-tabs>
- </div>
-
- <div *ngIf="selectedZoneInstanceType === zoneInstanceType.GROUP">
- <group-tabs
- [topologyTemplate]="topologyTemplate"
- [selectedZoneInstanceType]="selectedZoneInstanceType"
- [selectedZoneInstanceId]="selectedZoneInstanceId"
- [isViewOnly]="isViewOnly"
- (isLoading)="setIsLoading($event)"
- ></group-tabs>
- </div>
-
-</div>
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.less
deleted file mode 100644
index 1777d54486..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.less
+++ /dev/null
@@ -1,11 +0,0 @@
-/deep/
-.component-details-panel {
-
- color: #666666;
- font-family: OpenSans-Regular, sans-serif;
- font-size: 14px;
-
- .component-details-panel-tabs {
-
- }
-}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.ts
deleted file mode 100644
index 53599d6366..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*-
- * ============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 * as _ from "lodash";
-import { Component, Inject, Input, Output, EventEmitter, AfterViewInit, SimpleChanges, HostBinding } from "@angular/core";
-import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models";
-import { PolicyInstance } from 'app/models/graph/zones/policy-instance';
-import { TranslateService } from 'app/ng2/shared/translator/translate.service';
-import { ZoneInstanceType } from "app/models/graph/zones/zone-instance";
-import { GroupsService } from "../../../services/groups.service";
-import { PoliciesService } from "../../../services/policies.service";
-import { SdcUiComponents } from "sdc-ui/lib/angular";
-import { IZoneService } from "../../../../models/graph/zones/zone";
-
-@Component({
- selector: 'ng2-composition-panel',
- templateUrl: './panel.component.html',
- styleUrls: ['./panel.component.less'],
- providers: [TranslateService]
-})
-export class CompositionPanelComponent {
-
- @Input() topologyTemplate: TopologyTemplate;
- @Input() selectedZoneInstanceType: ZoneInstanceType;
- @Input() selectedZoneInstanceId: string;
- @Input() selectedZoneInstanceName: string;
- @Input() nonCertified: boolean;
- @Input() isViewOnly: boolean;
- @Input() isLoading: boolean;
-
-
- @HostBinding('class') classes = 'component-details-panel';
-
- private zoneInstanceType = ZoneInstanceType; // Expose ZoneInstanceType to use in template.
-
- constructor(){
- }
-
- private setIsLoading = (value):void => {
- this.isLoading = value;
- }
-
-}
diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel.module.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel.module.ts
deleted file mode 100644
index 57f6be8b8e..0000000000
--- a/catalog-ui/src/app/ng2/pages/composition/panel/panel.module.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-/*-
- * ============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 {NgModule} from "@angular/core";
-import {HttpModule} from "@angular/http";
-import {FormsModule} from "@angular/forms";
-import {BrowserModule} from "@angular/platform-browser";
-import {CompositionPanelComponent} from "./panel.component";
-import {CompositionPanelHeaderModule} from "app/ng2/pages/composition/panel/panel-header/panel-header.module";
-import {GroupTabsModule} from "./panel-tabs/groups/group-tabs.module";
-import {PolicyTabsModule} from "./panel-tabs/policies/policy-tabs.module";
-import {SdcUiComponents} from "sdc-ui/lib/angular";
-import {UiElementsModule} from 'app/ng2/components/ui/ui-elements.module';
-import {AddElementsModule} from "../../../components/ui/modal/add-elements/add-elements.module";
-
-@NgModule({
- declarations: [
- CompositionPanelComponent
- ],
- imports: [
- BrowserModule,
- FormsModule,
- HttpModule,
- CompositionPanelHeaderModule,
- PolicyTabsModule,
- GroupTabsModule,
- UiElementsModule,
- AddElementsModule
- ],
- entryComponents: [
- CompositionPanelComponent
- ],
- exports: [],
- providers: [SdcUiComponents.ModalService]
-})
-export class CompositionPanelModule {
-
-}