3
3
YUI.add('juju-view-environment', function(Y) {
5
var views = Y.namespace('juju.views'),
6
Templates = views.Templates;
8
var EnvironmentView = Y.Base.create('EnvironmentView', Y.View, [views.JujuBaseView], {
10
'#add-relation-btn': {click: 'add_relation'},
11
'#zoom-out-btn': {click: 'zoom_out'},
12
'#zoom-in-btn': {click: 'zoom_in'}
15
initializer: function () {
16
console.log('View: Initialized: Env');
17
this.publish('showService', {preventable: false});
21
console.log('View: Render: Env');
22
var container = this.get('container');
23
EnvironmentView.superclass.render.apply(this, arguments);
24
container.setHTML(Templates.overview());
29
render_canvas: function(){
31
container = this.get('container'),
32
m = this.get('domain_models'),
36
var services = m.services.toArray().map(function(s) {
5
var views = Y.namespace('juju.views'),
6
Templates = views.Templates;
8
var EnvironmentView = Y.Base.create('EnvironmentView',
9
Y.View, [views.JujuBaseView], {
11
'#add-relation-btn': {click: 'add_relation'},
12
'#zoom-out-btn': {click: 'zoom_out'},
13
'#zoom-in-btn': {click: 'zoom_in'}
16
initializer: function() {
17
console.log('View: Initialized: Env');
18
this.publish('showService', {preventable: false});
22
console.log('View: Render: Env');
23
var container = this.get('container');
24
EnvironmentView.superclass.render.apply(this, arguments);
25
container.setHTML(Templates.overview());
30
render_canvas: function() {
32
function processRelation(r) {
33
var endpoints = r.get('endpoints'),
35
Y.each(endpoints, function(ep) {
36
rel_services.push(services.filter(function(d) {
37
return d.get('id') === ep[0];
43
function processRelations(rels) {
45
Y.each(rels, function(rel) {
46
var pair = processRelation(rel);
48
if (pair.length === 2) {
49
pairs.push({source: pair[0],
58
container = this.get('container'),
59
m = this.get('domain_models'),
63
var services = m.services.toArray().map(function(s) {
37
64
s.value = s.get('unit_count');
40
var relations = m.relations.toArray();
41
var fill = d3.scale.category20();
67
var relations = m.relations.toArray();
68
var fill = d3.scale.category20();
43
var xscale = d3.scale.linear()
70
var xscale = d3.scale.linear()
44
71
.domain([-width / 2, width / 2])
45
72
.range([2, width]);
47
var yscale = d3.scale.linear()
74
var yscale = d3.scale.linear()
48
75
.domain([-height / 2, height / 2])
49
76
.range([height, 0]);
51
// Create a pan/zoom behavior manager.
52
var zoom = d3.behavior.zoom()
78
// Create a pan/zoom behavior manager.
79
var zoom = d3.behavior.zoom()
55
82
.scaleExtent([0.25, 1.75])
56
83
.on('zoom', function() {
57
84
self.rescale(vis, d3.event);
59
self.set('zoom', zoom);
61
// Scales for unit sizes.
62
// XXX magic numbers will have to change; likely during
64
var service_scale_width = d3.scale.log().range([164, 200]);
65
var service_scale_height = d3.scale.log().range([64, 100]);
67
// Set up the visualization with a pack layout.
68
var vis = d3.select(container.getDOMNode())
86
self.set('zoom', zoom);
88
// Scales for unit sizes.
89
// XXX magic numbers will have to change; likely during
91
var service_scale_width = d3.scale.log().range([164, 200]);
92
var service_scale_height = d3.scale.log().range([64, 100]);
94
// Set up the visualization with a pack layout.
95
var vis = d3.select(container.getDOMNode())
69
96
.selectAll('#canvas')
71
98
.attr('pointer-events', 'all')
75
vis.append('svg:rect')
102
vis.append('svg:rect')
76
103
.attr('fill', 'white');
78
// Bind visualization resizing on window resize
79
Y.on('windowresize', function() {
80
self.setSizesFromViewport(vis, container, xscale, yscale);
83
// If the view is bound to the dom, set sizes from viewport
85
self.setSizesFromViewport(vis, container, xscale, yscale);
88
var tree = d3.layout.pack()
105
// Bind visualization resizing on window resize
106
Y.on('windowresize', function() {
107
self.setSizesFromViewport(vis, container, xscale, yscale);
110
// If the view is bound to the dom, set sizes from viewport
112
self.setSizesFromViewport(vis, container, xscale, yscale);
115
var tree = d3.layout.pack()
89
116
.size([width, height])
92
var rel_data = processRelations(relations);
119
var rel_data = processRelations(relations);
94
function update_links() {
121
function update_links() {
95
122
var link = vis.selectAll('polyline.relation')
97
124
link = vis.selectAll('polyline.relation')
132
159
// with the service, the SVG node, and the view
134
161
(self.service_click_actions[curr_click_action])(m, this, self);
139
166
.attr('class', 'service-border')
140
167
.attr('width', function(d) {
141
168
var w = service_scale_width(d.get('unit_count'));
142
169
d.set('width', w);
145
172
.attr('height', function(d) {
146
173
var h = service_scale_height(d.get('unit_count'));
147
174
d.set('height', h);
150
var service_labels = node.append('text').append('tspan')
177
var service_labels = node.append('text').append('tspan')
151
178
.attr('class', 'name')
153
180
.attr('y', '1em')
154
181
.text(function(d) {return d.get('id'); });
156
var charm_labels = node.append('text').append('tspan')
183
var charm_labels = node.append('text').append('tspan')
158
185
.attr('y', '2.5em')
159
186
.attr('dy', '3em')
160
187
.attr('class', 'charm-label')
161
188
.text(function(d) { return d.get('charm'); });
163
// Show whether or not the service is exposed using an
164
// indicator (currently a simple circle).
165
// TODO this will likely change to an image with UI uodates.
166
var exposed_indicator = node.filter(function(d) {
167
return d.get('exposed');
190
// Show whether or not the service is exposed using an
191
// indicator (currently a simple circle).
192
// TODO this will likely change to an image with UI uodates.
193
var exposed_indicator = node.filter(function(d) {
194
return d.get('exposed');
169
196
.append('circle')
173
200
.attr('class', 'exposed-indicator on');
174
exposed_indicator.append('title')
201
exposed_indicator.append('title')
175
202
.text(function(d) {
176
203
return d.get('exposed') ? 'Exposed' : '';
179
// Add the relative health of a service in the form of a pie chart
180
// comprised of units styled appropriately.
181
// TODO aggregate statuses into good/bad/pending
182
var status_chart_arc = d3.svg.arc()
206
// Add the relative health of a service in the form of a pie chart
207
// comprised of units styled appropriately.
208
// TODO aggregate statuses into good/bad/pending
209
var status_chart_arc = d3.svg.arc()
184
211
.outerRadius(25);
185
var status_chart_layout = d3.layout.pie()
212
var status_chart_layout = d3.layout.pie()
186
213
.value(function(d) { return (d.value ? d.value : 1); });
188
var status_chart = node.append('g')
215
var status_chart = node.append('g')
189
216
.attr('class', 'service-status')
190
217
.attr('transform', 'translate(30,32)');
191
var status_arcs = status_chart.selectAll('path')
218
var status_arcs = status_chart.selectAll('path')
192
219
.data(function(d) {
193
220
var aggregate_map = d.get('aggregated_status'),
194
221
aggregate_list = [];
196
for (var status_name in aggregate_map) {
197
aggregate_list.push({
199
value: aggregate_map[status_name]
222
Y.Object.each(aggregate_map, function(value, name) {
223
aggregate_list.push({name: name, value: value});
203
226
return status_chart_layout(aggregate_list);
205
228
.enter().append('path')
206
229
.attr('d', status_chart_arc)
207
230
.attr('class', function(d) { return 'status-' + d.data.name; })
208
231
.attr('fill-rule', 'evenodd')
209
232
.append('title').text(function(d) {
210
233
return d.data.name;
213
// Add the unit counts, visible only on hover.
214
var unit_count = status_chart.append('text')
236
// Add the unit counts, visible only on hover.
237
var unit_count = status_chart.append('text')
215
238
.attr('class', 'unit-count hide-count')
216
239
.on('mouseover', function() {
217
240
d3.select(this).attr('class', 'unit-count show-count');
219
242
.on('mouseout', function() {
220
243
d3.select(this).attr('class', 'unit-count hide-count');
222
245
.text(function(d) {
223
246
return self.humanizeNumber(d.get('unit_count'));
226
function processRelation(r) {
227
var endpoints = r.get('endpoints'),
229
Y.each(endpoints, function(ep) {
230
rel_services.push(services.filter(function(d) {
231
return d.get('id') == ep[0];
237
function processRelations(rels) {
239
Y.each(rels, function(rel) {
240
var pair = processRelation(rel);
241
// Skip peer for now.
242
if (pair.length == 2) {
243
pairs.push({source: pair[0],
251
self.set('tree', tree);
252
self.set('vis', vis);
249
self.set('tree', tree);
250
self.set('vis', vis);
257
255
* Check to make sure that every service has saved coordinates.
259
_saved_coords: function(services) {
260
var saved_coords = true;
261
services.forEach(function(service) {
257
_saved_coords: function(services) {
258
var saved_coords = true;
259
services.forEach(function(service) {
262
260
if (!service.x || !service.y) {
263
saved_coords = false;
261
saved_coords = false;
270
268
* Generates coordinates for those services that are missing them.
272
_generate_coords: function(services, tree) {
273
services.forEach(function(service) {
270
_generate_coords: function(services, tree) {
271
services.forEach(function(service) {
274
272
if (service.x && service.y) {
275
service.set('x', service.x);
276
service.set('y', service.y);
273
service.set('x', service.x);
274
service.set('y', service.y);
279
var services_with_coords = tree.nodes({children: services})
277
var services_with_coords = tree.nodes({children: services})
280
278
.filter(function(d) { return !d.children; });
281
services_with_coords.forEach(function(service) {
279
services_with_coords.forEach(function(service) {
282
280
if (service.get('x') && service.get('y')) {
283
service.x = service.get('x');
284
service.y = service.get('y');
281
service.x = service.get('x');
282
service.y = service.get('y');
287
return services_with_coords;
285
return services_with_coords;
291
289
* Draw a relation between services. Polylines take a list of points
292
290
* in the form 'x y,( x y,)* x y'.
294
292
* TODO For now, just draw a straight line;
295
293
* will eventually use A* to route around other services.
297
draw_relation: function(relation) {
298
return (relation.source.x + (
299
relation.source.get('width') / 2)) + ' ' +
300
relation.source.y + ', ' +
301
(relation.target.x + (relation.target.get('width') / 2)) + ' ' +
295
draw_relation: function(relation) {
296
return (relation.source.x + (
297
relation.source.get('width') / 2)) + ' ' +
298
relation.source.y + ', ' +
299
(relation.target.x + (relation.target.get('width') / 2)) + ' ' +
306
304
* Event handler for the add relation button.
308
add_relation: function(evt) {
309
var curr_action = this.get('current_service_click_action'),
310
container = this.get('container');
311
if (curr_action == 'show_service') {
306
add_relation: function(evt) {
307
var curr_action = this.get('current_service_click_action'),
308
container = this.get('container');
309
if (curr_action === 'show_service') {
312
310
this.set('current_service_click_action', 'add_relation_start');
314
312
// Add .selectable-service to all .service-border.
315
313
this.addSVGClass('.service-border', 'selectable-service');
316
314
container.one('#add-relation-btn').addClass('active');
317
} else if (curr_action == 'add_relation_start' ||
318
curr_action == 'add_relation_end') {
315
} else if (curr_action === 'add_relation_start' ||
316
curr_action === 'add_relation_end') {
319
317
this.set('current_service_click_action', 'show_service');
321
319
// Remove selectable border from all nodes.
322
320
this.removeSVGClass('.service-border', 'selectable-service');
323
321
container.one('#add-relation-btn').removeClass('active');
324
} // Otherwise do nothing.
322
} // Otherwise do nothing.
328
326
* Zoom in event handler.
330
zoom_out: function(evt) {
331
this._fire_zoom(-0.2);
328
zoom_out: function(evt) {
329
this._fire_zoom(-0.2);
335
333
* Zoom out event handler.
337
zoom_in: function(evt) {
338
this._fire_zoom(0.2);
335
zoom_in: function(evt) {
336
this._fire_zoom(0.2);
342
340
* Wraper around the actual rescale method for zoom buttons.
344
_fire_zoom: function(delta) {
345
var vis = this.get('vis'),
346
zoom = this.get('zoom'),
349
// Build a temporary event that rescale can use of a similar
350
// construction to d3.event.
351
evt.translate = zoom.translate();
352
evt.scale = zoom.scale() + delta;
354
// Update the scale in our zoom behavior manager to maintain state.
355
this.get('zoom').scale(evt.scale);
357
this.rescale(vis, evt);
342
_fire_zoom: function(delta) {
343
var vis = this.get('vis'),
344
zoom = this.get('zoom'),
347
// Build a temporary event that rescale can use of a similar
348
// construction to d3.event.
349
evt.translate = zoom.translate();
350
evt.scale = zoom.scale() + delta;
352
// Update the scale in our zoom behavior manager to maintain state.
353
this.get('zoom').scale(evt.scale);
355
this.rescale(vis, evt);
361
359
* Rescale the visualization on a zoom/pan event.
363
rescale: function(vis, evt) {
364
this.set('scale', evt.scale);
365
vis.attr('transform', 'translate(' + evt.translate + ')' +
366
' scale(' + evt.scale + ')');
361
rescale: function(vis, evt) {
362
this.set('scale', evt.scale);
363
vis.attr('transform', 'translate(' + evt.translate + ')' +
364
' scale(' + evt.scale + ')');
370
368
* Set the visualization size based on the viewport
372
setSizesFromViewport: function(vis, container, xscale, yscale) {
373
// start with some reasonable defaults
374
var viewport_height = '100%',
375
viewport_width = parseInt(
376
container.getComputedStyle('width'), 10),
377
svg = container.one('svg'),
380
if (container.get('winHeight') &&
381
Y.one('#overview-tasks') &&
370
setSizesFromViewport: function(vis, container, xscale, yscale) {
371
// start with some reasonable defaults
372
var viewport_height = '100%',
373
viewport_width = parseInt(
374
container.getComputedStyle('width'), 10),
375
svg = container.one('svg'),
378
if (container.get('winHeight') &&
379
Y.one('#overview-tasks') &&
383
381
// Attempt to get the viewport height minus the navbar at top and
384
382
// control bar at the bottom. Use Y.one() to ensure that the
385
383
// container is attached first (provides some sensible defaults)
394
392
// Make sure we don't get sized any smaller than 800x600
395
393
viewport_height = Math.max(viewport_height, height);
396
394
if (container.get('winWidth') < width) {
397
viewport_width = width;
395
viewport_width = width;
401
svg.setAttribute('width', viewport_width)
399
svg.setAttribute('width', viewport_width)
402
400
.setAttribute('height', viewport_height);
404
// Get the resulting computed sizes (in the case of 100%)
405
width = parseInt(svg.getComputedStyle('width'), 10);
406
height = parseInt(svg.getComputedStyle('height'), 10);
402
// Get the resulting computed sizes (in the case of 100%)
403
width = parseInt(svg.getComputedStyle('width'), 10);
404
height = parseInt(svg.getComputedStyle('height'), 10);
408
// Set the internal rect's size
409
svg.one('rect').setAttribute('width', width)
406
// Set the internal rect's size
407
svg.one('rect').setAttribute('width', width)
410
408
.setAttribute('height', height);
412
// Reset the scale parameters
413
xscale.domain([-width / 2, width / 2])
410
// Reset the scale parameters
411
xscale.domain([-width / 2, width / 2])
414
412
.range([0, width]);
415
yscale.domain([-height / 2, height / 2])
413
yscale.domain([-height / 2, height / 2])
416
414
.range([height, 0]);
421
419
* Actions to be called on clicking a service.
423
service_click_actions: {
421
service_click_actions: {
425
423
* Default action: view a service
427
show_service: function(m, context, view) {
425
show_service: function(m, context, view) {
428
426
view.fire('showService', {service: m});
432
430
* Fired when clicking the first service in the add relation
435
add_relation_start: function(m, context, view) {
433
add_relation_start: function(m, context, view) {
436
434
// Remove selectable border from current node.
437
435
var node = Y.one(context).one('.service-border');
438
436
view.removeSVGClass(node, 'selectable-service');