diff --git a/app/components/architecture-viewer.js b/app/components/architecture-viewer.js index 8648937394d6a12d0898898f116735392525cb6e..ad39f5841d87dec46b428b808c4553f7f65e2612 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 e72a6b97ec5e755d99bb75d28550d2c5f01cbcba..8d1fb250d521f08ba306594327e8749a360fc3d7 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 7ebb0b8533139267848b9f67045d9691c9398fde..eec7138d2f3f3fabb335366c46a521a171d9a69c 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 c8668aef55665e65fe8dde1471fea79b351d0249..1a2b8a4747256b4a64332db3f5f4fe5fe0805783 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 8ef736718125dcc37e3e1a18bfb5b43e375e4939..9b4b777a76d6811896b2fdcb9c5c2d5345232fae 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 844513856fc903b5747cd409a9bd537ed942e3fa..03aee72c2091526ae661f76e0ae486b17f53ff8d 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 0000000000000000000000000000000000000000..0f6ba0ba5fad962d721ca5f1f5bdb0ef9eb80b89 --- /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 4a98a5e8b06fca9a4ef8e545e91fa969ff68ed62..dec485d32ec2658fdb6ef20cd524f2c9141a7368 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 61c56f454ac9c7408683b5d27bc3acae2d087076..f4413cdf636e6f49bad276fa0d4345178830c940 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 915841f3b7b997febf8a6488472d5eaa2af5bf2f..45c695e1ac0969538fd65468ba32980e00337de6 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"}}>