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