[CORD-2324] Position the nodes not defined in the constraints

Change-Id: I712a90828e0b6a12b31f62f7391feee64c123f2c
(cherry picked from commit 35fdf249b31b003a163ee0582a77c91f15782587)
diff --git a/src/app/core/confirm/confirm.html b/src/app/core/confirm/confirm.html
index c9b4c81..aacaa8b 100644
--- a/src/app/core/confirm/confirm.html
+++ b/src/app/core/confirm/confirm.html
@@ -19,9 +19,9 @@
     <h4>{{vm.config.header}}</h4>
 </div>
 <div class="modal-body" ng-show="vm.config.text">
-    {{vm.config.text}}
+    <div ng-bind-html="vm.config.text"></div>
 </div>
 <div class="modal-footer">
-    <a class="btn btn-default" ng-click="vm.dismiss()">Cancel</a>
+    <a class="btn btn-default" ng-click="vm.dismiss()">Close</a>
     <a class="btn" ng-class="action.class" ng-repeat="action in vm.config.actions" ng-click="vm.close(action.cb())">{{action.label}}</a>
 </div>
\ No newline at end of file
diff --git a/src/app/core/index.ts b/src/app/core/index.ts
index bdd0157..3f9d503 100644
--- a/src/app/core/index.ts
+++ b/src/app/core/index.ts
@@ -31,6 +31,7 @@
 import {xosForm} from './form/form';
 import {xosField} from './field/field';
 import 'angular-toastr';
+import 'angular-sanitize';
 import {xosAlert} from './alert/alert';
 import {xosValidation} from './validation/validation';
 import {xosSidePanel} from './side-panel/side-panel';
@@ -53,10 +54,11 @@
 
 angular
   .module('xosCore', [
+    'ngSanitize',
     'ui.router',
     'toastr',
     'ui.bootstrap.typeahead',
-    'ui.bootstrap.tabs'
+    'ui.bootstrap.tabs',
   ])
   .config(routesConfig)
   .provider('XosRuntimeStates', XosRuntimeStates)
diff --git a/src/app/service-graph/components/graph/graph.component.ts b/src/app/service-graph/components/graph/graph.component.ts
index 5769b6f..5f84384 100644
--- a/src/app/service-graph/components/graph/graph.component.ts
+++ b/src/app/service-graph/components/graph/graph.component.ts
@@ -87,6 +87,10 @@
       this.$log.info(`[XosServiceGraph] Received event: xos.sg.update`);
       this.renderGraph(this.graph);
     });
+
+    $(window).resize(() => {
+      this.renderGraph(this.graph);
+    });
   }
 
   $onDestroy() {
diff --git a/src/app/service-graph/interfaces.ts b/src/app/service-graph/interfaces.ts
index 861ba75..4f2e470 100644
--- a/src/app/service-graph/interfaces.ts
+++ b/src/app/service-graph/interfaces.ts
@@ -22,7 +22,7 @@
 
 export interface IXosSgNode extends Id3Element {
   id: string;
-  data: any; // this can be a Service, ServiceInstance or Instance
+  data: IXosBaseModel; // this can be a Service, ServiceInstance or Instance
 
   // do we need those?
   type: string;
diff --git a/src/app/service-graph/services/node-positioner.service.spec.ts b/src/app/service-graph/services/node-positioner.service.spec.ts
index 14fb9c4..c6308e9 100644
--- a/src/app/service-graph/services/node-positioner.service.spec.ts
+++ b/src/app/service-graph/services/node-positioner.service.spec.ts
@@ -43,7 +43,8 @@
   beforeEach(() => {
     angular.module('XosNodePositioner', [])
       .service('XosNodePositioner', XosNodePositioner)
-      .value('ModelRest', mockModelRest);
+      .value('ModelRest', mockModelRest)
+      .value('XosConfirm', {});
 
     angular.mock.module('XosNodePositioner');
   });
@@ -125,4 +126,30 @@
 
     scope.$apply();
   });
+
+  it('should set unpositioned nodes at the top', (done) => {
+    const svg = {width: 300, height: 200};
+    const nodes = [
+      {data: {name: 'a'}},
+      {data: {name: 'b'}},
+      {data: {name: 'c'}, type: 'service'},
+      {data: {name: 'd'}, type: 'service'}
+    ];
+    constraints = '["a", "b"]';
+
+    service.positionNodes(svg, nodes)
+      .then((positioned) => {
+        expect(positioned[0].x).toBe(100);
+        expect(positioned[0].y).toBe(100);
+        expect(positioned[1].x).toBe(200);
+        expect(positioned[1].y).toBe(100);
+        expect(positioned[2].x).toBe(100);
+        expect(positioned[2].y).toBe(150);
+        expect(positioned[3].x).toBe(200);
+        expect(positioned[3].y).toBe(150);
+        done();
+      });
+
+    scope.$apply();
+  });
 });
diff --git a/src/app/service-graph/services/node-positioner.service.ts b/src/app/service-graph/services/node-positioner.service.ts
index 499afd0..17823b7 100644
--- a/src/app/service-graph/services/node-positioner.service.ts
+++ b/src/app/service-graph/services/node-positioner.service.ts
@@ -18,6 +18,7 @@
 import * as _ from 'lodash';
 import {IXosResourceService} from '../../datasources/rest/model.rest';
 import {IXosSgNode} from '../interfaces';
+import {IXosConfirm} from '../../core/confirm/confirm.service';
 
 export interface IXosNodePositioner {
   positionNodes(svg: {width: number, height: number}, nodes: any[]): ng.IPromise<IXosSgNode[]>;
@@ -27,51 +28,70 @@
   static $inject = [
     '$log',
     '$q',
-    'ModelRest'
+    'ModelRest',
+    'XosConfirm'
   ];
 
   constructor (
     private $log: ng.ILogService,
     private $q: ng.IQService,
     private ModelRest: IXosResourceService,
+    private XosConfirm: IXosConfirm
   ) {
     this.$log.info('[XosNodePositioner] Setup');
   }
 
-  public positionNodes(svg: {width: number, height: number}, nodes: any[]): ng.IPromise<IXosSgNode[]> {
+  public positionNodes(svg: {width: number, height: number}, nodes: IXosSgNode[]): ng.IPromise<IXosSgNode[]> {
 
-    // TODO refactor naming in this loop to make it clearer
     const d =  this.$q.defer();
 
     this.getConstraints()
       .then(constraints => {
         const hStep = this.getHorizontalStep(svg.width, constraints);
-        const positionConstraints = _.reduce(constraints, (all: any, c: string | string[], i: number) => {
-          let pos: {x: number, y: number, fixed: boolean} = {
-            x: svg.width / 2,
-            y: svg.height / 2,
-            fixed: true
-          };
+        const positionConstraints = _.reduce(constraints, (all: any, horizontalConstraint: string | string[], i: number) => {
           // NOTE it's a single element, leave it in the middle
-          if (angular.isString(c)) {
-            pos.x = (i + 1) * hStep;
-            all[c] = pos;
+          if (angular.isString(horizontalConstraint)) {
+            all[horizontalConstraint] = {
+              x: (i + 1) * hStep,
+              y: svg.height / 2,
+              fixed: true
+            };
           }
           else {
-            const verticalConstraints = c;
+            const verticalConstraints = horizontalConstraint;
             const vStep = this.getVerticalStep(svg.height, verticalConstraints);
-            _.forEach(verticalConstraints, (c: string, v: number) => {
-              if (angular.isString(c)) {
-                let p = angular.copy(pos);
-                p.x = (i + 1) * hStep;
-                p.y = (v + 1) * vStep;
-                all[c] = p;
+            _.forEach(verticalConstraints, (verticalConstraint: string, v: number) => {
+              if (angular.isString(verticalConstraint)) {
+                all[verticalConstraint] = {
+                  x: (i + 1) * hStep,
+                  y: (v + 1) * vStep,
+                  fixed: true
+                };
               }
             });
           }
           return all;
         }, {});
 
+        // find the nodes that don't have a position defined and put them at the top
+        const allNodes = _.reduce(nodes, (all: string[], n: IXosSgNode) => {
+          if (n.type === 'service') {
+            all.push(n.data.name);
+          }
+          return all;
+        }, []);
+        const positionedNodes = Object.keys(positionConstraints);
+        const unpositionedNodes = _.difference(allNodes, positionedNodes);
+
+        _.forEach(unpositionedNodes, (node: string, i: number) => {
+          const hStep = this.getHorizontalStep(svg.width, unpositionedNodes);
+          positionConstraints[node] = {
+            x: (i + 1) * hStep,
+            y: svg.height - 50,
+            fixed: true
+          };
+        });
+
         d.resolve(_.map(nodes, n => {
           return angular.merge(n, positionConstraints[n.data.name]);
         }));
@@ -90,6 +110,17 @@
         d.resolve(JSON.parse(res[0].constraints));
       })
       .catch(e => {
+        this.XosConfirm.open({
+          header: 'Error in graph constraints config',
+          text: `
+            There was an error in the settings you provided as graph constraints. 
+            Please check the declaration of the <code>"Graph Constraints"</code> model. <br/> 
+            The error was: <br/><br/>
+            <code>${e}</code>
+            <br/><br/>
+            Please fix it to see the graph.`,
+          actions: []
+        });
         d.reject(e);
       });
     return d.promise;
diff --git a/src/app/service-graph/services/renderer/node.renderer.ts b/src/app/service-graph/services/renderer/node.renderer.ts
index 17bcf70..fb29046 100644
--- a/src/app/service-graph/services/renderer/node.renderer.ts
+++ b/src/app/service-graph/services/renderer/node.renderer.ts
@@ -50,15 +50,13 @@
       .selectAll('g.node')
       .data(nodes, n => n.id);
 
-    node
-      .call(this.drag);
-
     const entering = node.enter()
       .append('g')
       .attr({
         id: n => n.id,
         class: n => `node ${n.type} ${this.XosGraphHelpers.parseElemClasses(n.d3Class)}`,
-      });
+      })
+      .call(this.drag);
 
     this.renderServiceNodes(entering.filter('.service'));
     this.renderServiceInstanceNodes(entering.filter('.serviceinstance'));