From 3e24e08ffd35b55784a97d7fa75096f6b0efb07f Mon Sep 17 00:00:00 2001
From: Mathis Neumann <mathis@simpletechs.net>
Date: Mon, 11 Jul 2016 18:07:07 +0200
Subject: [PATCH] extend sidebar if clicked on graph entity

---
 app/components/architecture-viewer.js         | 10 +++-
 .../component.js                              | 12 +++-
 app/controllers/deployments/single.js         |  1 +
 app/routes/deployments/single.js              |  1 +
 app/routes/deployments/single/details.js      | 31 +++++++++++
 app/services/session.js                       | 11 +++-
 app/services/visualisation-events.js          | 55 +++++++++++++++++++
 app/styles/components/_architecture.scss      | 53 ++++++------------
 app/styles/components/_cytoscape.scss         | 21 ++++---
 .../components/architecture-viewer.hbs        |  4 +-
 10 files changed, 149 insertions(+), 50 deletions(-)
 create mode 100644 app/services/visualisation-events.js

diff --git a/app/components/architecture-viewer.js b/app/components/architecture-viewer.js
index 8648937..ad39f58 100644
--- a/app/components/architecture-viewer.js
+++ b/app/components/architecture-viewer.js
@@ -2,15 +2,16 @@ import Ember from 'ember';
 import Themes from './architecture-visualisation-cytoscape/themes';
 
 export default Ember.Component.extend({
+    visualisationEvents: Ember.inject.service(),
     graph: null,
     entityDetails: null,
     layoutAlgorithm: 'cose-bilkent',
-    theme: Themes[Object.keys(Themes)[0]], // first theme
+    theme: Themes[Object.keys(Themes)[0]], // first theme // TODO use a "default: true" flag, order is not fixed for browsers
     themes: Object.keys(Themes),
+    classNameBindings: ['resizing'],
     layoutAlgorithms: [
         'cose-bilkent',
         'cose',
-        // 'cose-bilkent', // broken - see https://github.com/cytoscape/cytoscape.js-cose-bilkent/issues/18
         'cola',
         'grid',
         'concentric',
@@ -19,6 +20,11 @@ export default Ember.Component.extend({
     init() {
         this._super();
         this.debug('init!', this.get('graph'), this.get('layoutAlgorithm'));
+
+        // add class while the sidebar grows
+        const visualisationEvents = this.get('visualisationEvents');
+        visualisationEvents.on('resize:start', () => this.set('resizing', true));
+        visualisationEvents.on('resize:end', () => this.set('resizing', false));
     },
     actions: {
         selectLayoutAlgorithm(value) {
diff --git a/app/components/architecture-visualisation-cytoscape/component.js b/app/components/architecture-visualisation-cytoscape/component.js
index e72a6b9..8d1fb25 100644
--- a/app/components/architecture-visualisation-cytoscape/component.js
+++ b/app/components/architecture-visualisation-cytoscape/component.js
@@ -9,6 +9,7 @@ import coseBilkent from 'npm:cytoscape-cose-bilkent';
 coseBilkent(cytoscape); // register
 
 export default Ember.Component.extend({
+    visualisationEvents: Ember.inject.service(),
     theme: null, // set by architecture-viewer
     layoutAlgorithm: null, // set by architecture-viewer
     classNames: ['cytoscapeRenderingSpace'],
@@ -16,6 +17,9 @@ export default Ember.Component.extend({
         cycola( cytoscape, window.cola );
         this._super();
         this.debug('loaded', this.get('graph'));
+
+        // we only need to listen to :end since opacity changes are done via css, since architecture-viewer adds a resizing class
+        this.get('visualisationEvents').on('resize:end', this.resize.bind(this));
     },
     willDestroyElement() {
         clearInterval(this.interval);
@@ -65,5 +69,11 @@ export default Ember.Component.extend({
         // just for development purposes - TODO: remove
         window.cy = cytoscape;
         window.cytoscape = this.rendering;
-    }.on('didInsertElement').observes('layoutAlgorithm', 'graph', 'theme')
+    }.on('didInsertElement').observes('layoutAlgorithm', 'graph', 'theme'),
+    resize() {
+        if(this.rendering) {
+            this.rendering.resize();
+            this.rendering.center(); // TODO: keep focus or even focus clicked element
+        }
+    }
 });
diff --git a/app/controllers/deployments/single.js b/app/controllers/deployments/single.js
index 7ebb0b8..eec7138 100644
--- a/app/controllers/deployments/single.js
+++ b/app/controllers/deployments/single.js
@@ -7,6 +7,7 @@ const observables = [
     'communicationInstances'
 ];
 
+// we require the use of controllers solely because Ember does not allow routes to pass more than model() to templates
 export default Ember.Controller.extend({
     graphingService: Ember.inject.service(),
     changelogQueue: Ember.inject.service(),
diff --git a/app/routes/deployments/single.js b/app/routes/deployments/single.js
index c8668ae..1a2b8a4 100644
--- a/app/routes/deployments/single.js
+++ b/app/routes/deployments/single.js
@@ -42,6 +42,7 @@ export default Ember.Route.extend({
 
         /* I would love to not generate the url first, but there seem to be unknown (to me) assumptions about
         * passing object parameters to transitionTo which break with the current path variables.
+        * Otherwise this would use transitionTo('deployments.single.details', {...})
         */
         const url = this.router.generate('deployments.single.details', {
             systemId: this.get('session.systemId'),
diff --git a/app/routes/deployments/single/details.js b/app/routes/deployments/single/details.js
index 8ef7367..9b4b777 100644
--- a/app/routes/deployments/single/details.js
+++ b/app/routes/deployments/single/details.js
@@ -1,7 +1,38 @@
 import Ember from 'ember';
 
 export default Ember.Route.extend({
+    visualisationEvents: Ember.inject.service(),
+    /**
+     * the duration of the extending sidebar animation, which is configured as transition in _architecture.scss.
+     * Since we apparently cannot listen to css transitionEnd events, we have to manually wait the time.
+     * Since this is fragile (since animations might lag on slower machines), we add a bit of buffer time.
+     *
+     * @type {Number} in milliseconds
+     * @property animationDuration
+     */
+    animationDuration: 600, // ms
     model(params) {
         return this.store.findRecord(params.entityType.toLowerCase(), params.entityId); // TODO: match entity.type with model names
+    },
+    activate() {
+        this.debug('route activated');
+        Ember.$('body').addClass('extendedSidebar');
+        this.notifyResize();
+    },
+    deactivate() {
+        this.debug('route deactivated');
+        Ember.$('body').removeClass('extendedSidebar');
+        this.notifyResize();
+    },
+    notifyResize() {
+        if(this.animationTimeout) { // in case a user navigated too fast
+            this.endAnimation();
+        }
+        this.get('visualisationEvents').trigger('resize:start');
+        this.animationTimeout = setTimeout(this.endAnimation.bind(this), this.animationDuration);
+    },
+    endAnimation() {
+        this.get('visualisationEvents').trigger('resize:end');
+        this.animationTimeout = null;
     }
 });
diff --git a/app/services/session.js b/app/services/session.js
index 8445138..03aee72 100644
--- a/app/services/session.js
+++ b/app/services/session.js
@@ -2,9 +2,16 @@ import Ember from 'ember';
 
 /**
  * this service stores the global state of the system.
- * @property {String} systemId store the id of the system which is currently used for
- *      loading metamodel components
+ *
+ * @class  SessionService
+ * @extends {Ember.Service}
+ * @public
  */
 export default Ember.Service.extend({
+    /** systemId store the id of the system which is currently used for
+     * loading metamodel components
+     * @type {String}
+     * @property systemId
+     */
     systemId: null
 });
diff --git a/app/services/visualisation-events.js b/app/services/visualisation-events.js
new file mode 100644
index 0000000..0f6ba0b
--- /dev/null
+++ b/app/services/visualisation-events.js
@@ -0,0 +1,55 @@
+import Ember from 'ember';
+
+/**
+ * This service allows to bind global events which influence a visualisation.
+ * Since cytoscape visualisations are not behaving like regular DOM elements,
+ * the service provides means let the visualisation know about environment changes.
+ * As of time of writing it only provides events for resizing
+ *
+ * @class VisualisationEventsService
+ * @extends {Ember.Service}
+ * @public
+ */
+export default Ember.Service.extend(Ember.Evented, {
+    /**
+     * forces listing of allowed events to improve documentations.
+     * Currently only resizing, used by deployments/single route (triggers events),
+     * cytoscape and architecture viewer component.
+     *
+     * @type {Array}
+     * @property allowedEvents
+     * @default resize:start, resize:end
+     */
+    allowedEvents: [
+        'resize:start',
+        'resize:end'
+    ],
+    /**
+     * triggers an events, see Ember docs.
+     *
+     * @method trigger
+     * @parameter {String} eventName
+     * @throws {Error} exception if an unknown event name was used.
+     */
+    trigger(eventName) {
+        if(this.get('allowedEvents').indexOf(eventName) < 0) {
+            throw new Error(`unknown event "${eventName}"`);
+        } else {
+            this._super(...arguments);
+        }
+    },
+    /**
+     * Subscribes to an event, see Ember docs.
+     *
+     * @method on
+     * @parameter {String} eventName
+     * @throws {Error} exception if an unknown event name was used.
+     */
+    on(eventName) {
+        if(this.get('allowedEvents').indexOf(eventName) < 0) {
+            throw new Error(`unknown event "${eventName}"`);
+        } else {
+            this._super(...arguments);
+        }
+    },
+});
\ No newline at end of file
diff --git a/app/styles/components/_architecture.scss b/app/styles/components/_architecture.scss
index 4a98a5e..dec485d 100644
--- a/app/styles/components/_architecture.scss
+++ b/app/styles/components/_architecture.scss
@@ -1,43 +1,24 @@
-svg.architectureVisualisation {
-    .root {
-        g.compound {
-            &:first-child rect { // hide root compound
-                opacity: 0;
-            }
+$resizingAnimationDuration: .5s; // see deployments/detail route for property
 
-            & > rect {
-                opacity: 0.1;
+.visualisationContainer {
+    @extend .col-md-10;
+    transition: width $resizingAnimationDuration;
 
-                &:hover {
-                    stroke: white;
-                    stroke-opacity: 1;
-                    fill: red;
-                }
-            }
-        }
+    canvas {
+        max-width: 100%;
     }
-    .link {
-        stroke: #999;
-        stroke-opacity: .6;
-        fill: none;
-        stroke-width: 2px;
+}
 
-        &:hover {
-          stroke: #A000;
-          stroke-opacity: 1;
-        }
-    }
-    g.leaf > rect {
-      stroke: #fff;
-      stroke-width: 1px;
-      opacity: .5;
-    }
+.visualisationSidebar {
+    @extend .col-md-2;
+    transition: width $resizingAnimationDuration;
+}
 
-    // .node {}
-
-    .port {
-      stroke: #000;
-      width: 1px;
-      opacity: .6;
+.extendedSidebar {
+    .visualisationContainer {
+        @extend .col-md-6
+    }
+    .visualisationSidebar {
+        @extend .col-md-6
     }
 }
\ No newline at end of file
diff --git a/app/styles/components/_cytoscape.scss b/app/styles/components/_cytoscape.scss
index 61c56f4..f4413cd 100644
--- a/app/styles/components/_cytoscape.scss
+++ b/app/styles/components/_cytoscape.scss
@@ -1,8 +1,15 @@
 .cytoscapeRenderingSpace {
-  height: 720px;
-  max-height: 100%;
-  width: 100%;
-  left: 0;
-  top: 0;
-  border: solid 1px #ccc;
-}
\ No newline at end of file
+    height: 720px;
+    max-height: 100%;
+    width: 100%;
+    left: 0;
+    top: 0;
+    border: solid 1px #ccc;
+    transition: opacity 100ms ease-out; // for resizing
+}
+
+.resizing {
+    .cytoscapeRenderingSpace {
+        opacity: 0;
+    }
+}
diff --git a/app/templates/components/architecture-viewer.hbs b/app/templates/components/architecture-viewer.hbs
index 915841f..45c695e 100644
--- a/app/templates/components/architecture-viewer.hbs
+++ b/app/templates/components/architecture-viewer.hbs
@@ -1,8 +1,8 @@
 <div class="row">
-    <div class="col-md-10 visualisationContainer">
+    <div class="visualisationContainer">
         {{architecture-visualisation-cytoscape graph=graph layoutAlgorithm=layoutAlgorithm theme=theme select=(route-action 'loadDetails')}}
     </div>
-    <div class="col-md-2 visualisationSidebar">
+    <div class="visualisationSidebar">
         <div class="form-group">
           <label for="layoutAlgorithm">Layout:</label>
           <select class="form-control" id="layoutAlgorithm" onchange={{action "selectLayoutAlgorithm" value="target.value"}}>
-- 
GitLab