Merge lp:~bcsaller/juju-gui/exportXY into lp:juju-gui/experimental

Proposed by Benjamin Saller
Status: Merged
Merged at revision: 1136
Proposed branch: lp:~bcsaller/juju-gui/exportXY
Merge into: lp:juju-gui/experimental
Diff against target: 797 lines (+184/-216)
15 files modified
app/models/models.js (+9/-22)
app/store/env/fakebackend.js (+3/-3)
app/views/ghost-inspector.js (+21/-42)
app/views/topology/relation.js (+5/-6)
app/views/topology/service.js (+77/-130)
app/views/topology/topology.js (+46/-0)
app/views/utils.js (+1/-1)
test/test_conflict_ux.js (+2/-1)
test/test_environment_view.js (+8/-7)
test/test_fakebackend.js (+5/-1)
test/test_inspector_constraints.js (+1/-0)
test/test_inspector_overview.js (+1/-0)
test/test_inspector_settings.js (+1/-0)
test/test_model.js (+3/-2)
test/test_service_module.js (+1/-1)
To merge this branch: bzr merge lp:~bcsaller/juju-gui/exportXY
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+191119@code.launchpad.net

Description of the change

Redo Service Placement

This changes a number of aspects of how service placement is handled. There
may still be some edge cases and QA will need to be extensive as this
has moved through phases where various parts worked and didn't. I believe
this version to be good however.

Position annotations remain on service models now rather than being deleted
when applied. We favor this to having vars such as hasBeenPositioned,
positionedFromGhost and service model x/y (which mixed into BoundingBoxes).
These various complications are mostly gone and the handling of updating
position annotations falls to a single method on the topology. (Though to be
fair that is still called from more than one place).

Further simplification might be possible by unifying the node creation and
node update paths wrt annotations. This didn't however fit in the time
provided.

This also includes a basic fix for always pulling positions from the client
during an export.

https://codereview.appspot.com/14695043/

To post a comment you must log in.
Revision history for this message
Benjamin Saller (bcsaller) wrote :

Reviewers: mp+191119_code.launchpad.net,

Message:
Please take a look.

Description:
Redo Service Placement

This changes a number of aspects of how service placement is handled.
There
may still be some edge cases and QA will need to be extensive as this
has moved through phases where various parts worked and didn't. I
believe
this version to be good however.

Position annotations remain on service models now rather than being
deleted
when applied. We favor this to having vars such as hasBeenPositioned,
positionedFromGhost and service model x/y (which mixed into
BoundingBoxes).
These various complications are mostly gone and the handling of updating

position annotations falls to a single method on the topology. (Though
to be
fair that is still called from more than one place).

Further simplification might be possible by unifying the node creation
and
node update paths wrt annotations. This didn't however fit in the time
provided.

This also includes a basic fix for always pulling positions from the
client
during an export.

https://code.launchpad.net/~bcsaller/juju-gui/exportXY/+merge/191119

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/14695043/

Affected files (+169, -185 lines):
   A [revision details]
   M app/app.js
   M app/models/models.js
   M app/views/ghost-inspector.js
   M app/views/topology/relation.js
   M app/views/topology/service.js
   M app/views/topology/topology.js
   M test/test_environment_view.js
   M test/test_fakebackend.js
   M test/test_model.js
   M test/test_service_module.js

Revision history for this message
Gary Poster (gary) wrote :

On 2013/10/15 08:26:10, bcsaller wrote:
> Please take a look.

Doing qa first. A lot looks good. There's one class of serious
problems I still see (and I am pretty sure I did not see these during qa
last night). Here are symptom descriptions. They seem very related.

- In a sandbox, go to
http://localhost:8888/sidebar/search/:flags:/charmworldv3/?text=benji
and drag the bundle out to deploy it. It will appear and then seem to
disappear. It has moved to the bottom right. You can drag the bundle
up to the center, but within a few seconds it will return to the bottom
right.

- In a real environment, when you first go to the GUI and see only the
GUI service, it shows up in the middle of the canvas and then moves down
to the bottom right. Again, you can drag it to the right place, but it
will return within a minute or so to the bottom right.

The thing that unifies these scenarios is that the services do not begin
with x/y annotations.

I'll look again when you've had a chance to address, or if I you need to
hand it off to us.

https://codereview.appspot.com/14695043/

Revision history for this message
Benjamin Saller (bcsaller) wrote :

I believe you when you say that it didn't happen in QA at the midway
point. My feeling was that it was working better as well before but
trying to get it passing the existing tests forced some changes and
while I thought I got it back into shape it seems this must be resolved.

One difference I know is that I don't publish annotations for packed
elements as this was interfering with imports. They'd get build and the
delta comes back, they get placed, and the annotation set and then the
imports set annotations call fires and another delta comes in, racing
with the cli placement. That change and the case where I decide when to
pan (which should only happen after packing more than one new service)
both need looking at. I'll try to resolve these. If that doesn't work we
can back up the patch a few revisions and try to merge in test changes.

I'll look into this now.

https://codereview.appspot.com/14695043/

Revision history for this message
Gary Poster (gary) wrote :
Download full text (5.6 KiB)

Hey Ben. I just saw your reply to my qa, which touches on some of my
review comments below. I'm not sure I said thank you the first time:
thank you! this will be a great change, and a nice last (major?) commit
(for now?) :-)

Gary

https://codereview.appspot.com/14695043/diff/1/app/app.js
File app/app.js (right):

https://codereview.appspot.com/14695043/diff/1/app/app.js#newcode626
app/app.js:626: var result = this.db.exportDeployer(topology);
why is this necessary if we don't remove x/y annotations anymore?

https://codereview.appspot.com/14695043/diff/1/app/models/models.js
File app/models/models.js (right):

https://codereview.appspot.com/14695043/diff/1/app/models/models.js#newcode318
app/models/models.js:318: life: {
indentation needs an extra space.

https://codereview.appspot.com/14695043/diff/1/app/models/models.js#newcode1143
app/models/models.js:1143: exportDeployer: function(topology) {
Same comment as before: if we are handling x/y annotations properly now,
why is the topology necessary?

https://codereview.appspot.com/14695043/diff/1/app/views/ghost-inspector.js
File app/views/ghost-inspector.js (right):

https://codereview.appspot.com/14695043/diff/1/app/views/ghost-inspector.js#newcode64
app/views/ghost-inspector.js:64: annotations['gui-y'] =
ghostAttributes.coordinates[1];
Note to self: double check that these annotations are actually sent to
server after creation.

https://codereview.appspot.com/14695043/diff/1/app/views/ghost-inspector.js#newcode65
app/views/ghost-inspector.js:65: ghostService.set('annotations',
annotations);
Why is this necessary? Do we need to trigger an event? If so, please
add a comment to that effect.

https://codereview.appspot.com/14695043/diff/1/app/views/ghost-inspector.js#newcode301
app/views/ghost-inspector.js:301: topo.annotateBoxPosition(boxModel);
Is this the rough equivalent of "options.env.update_annotations(
serviceName, 'service', ghostService.get('annotations')"?

https://codereview.appspot.com/14695043/diff/1/app/views/topology/relation.js
File app/views/topology/relation.js (left):

https://codereview.appspot.com/14695043/diff/1/app/views/topology/relation.js#oldcode187
app/views/topology/relation.js:187: self.updateLinkEndpoints({ service:
svc });
Why is this no longer necessary?

https://codereview.appspot.com/14695043/diff/1/app/views/topology/service.js
File app/views/topology/service.js (right):

https://codereview.appspot.com/14695043/diff/1/app/views/topology/service.js#newcode106
app/views/topology/service.js:106: // current drag supercedes any
previous annotations).
Until we have timestamps associated with our x/y annotations, we will
perpetually have race conditions ISTM. :-(

That's not for this branch, just thinking.

https://codereview.appspot.com/14695043/diff/1/app/views/topology/service.js#newcode115
app/views/topology/service.js:115: topo.fire('panToCenter');
Ah, I bet this is what I'm encountering in qa.

https://codereview.appspot.com/14695043/diff/1/app/views/topology/service.js#newcode927
app/views/topology/service.js:927: } else {
delete that extra space to get the linting right, yeah?

https://codereview.appspot.com/14695043/diff/1/app/views/topology/service...

Read more...

Revision history for this message
Benjamin Saller (bcsaller) wrote :
Download full text (5.7 KiB)

Thanks, new version pushing now

https://codereview.appspot.com/14695043/diff/1/app/views/ghost-inspector.js
File app/views/ghost-inspector.js (right):

https://codereview.appspot.com/14695043/diff/1/app/views/ghost-inspector.js#newcode64
app/views/ghost-inspector.js:64: annotations['gui-y'] =
ghostAttributes.coordinates[1];
On 2013/10/15 15:34:16, gary.poster wrote:
> Note to self: double check that these annotations are actually sent to
server
> after creation.

Line ~301 in the callback handler. We know its on the backend at that
point, there is an explicit call.

https://codereview.appspot.com/14695043/diff/1/app/views/ghost-inspector.js#newcode65
app/views/ghost-inspector.js:65: ghostService.set('annotations',
annotations);
On 2013/10/15 15:34:16, gary.poster wrote:
> Why is this necessary? Do we need to trigger an event? If so, please
add a
> comment to that effect.

I was thinking of handling position changes on annotation change events,
but no, this was a guard rail vs some strange behavior I was seeing, it
shouldn't technically be needed.

https://codereview.appspot.com/14695043/diff/1/app/views/ghost-inspector.js#newcode301
app/views/ghost-inspector.js:301: topo.annotateBoxPosition(boxModel);
On 2013/10/15 15:34:16, gary.poster wrote:
> Is this the rough equivalent of "options.env.update_annotations(
> serviceName, 'service', ghostService.get('annotations')"?

It is, it adds support for putting the box in the DRAG_ENDING state with
a timed window in which it shouldn't resend position annotations.

https://codereview.appspot.com/14695043/diff/1/app/views/topology/relation.js
File app/views/topology/relation.js (left):

https://codereview.appspot.com/14695043/diff/1/app/views/topology/relation.js#oldcode187
app/views/topology/relation.js:187: self.updateLinkEndpoints({ service:
svc });
On 2013/10/15 15:34:16, gary.poster wrote:
> Why is this no longer necessary?

It shouldn't have been needed for some time. The only ways to position
objects all explicitly request updates to endpoint/relations connected
to the moved object.

https://codereview.appspot.com/14695043/diff/1/app/views/topology/service.js
File app/views/topology/service.js (right):

https://codereview.appspot.com/14695043/diff/1/app/views/topology/service.js#newcode106
app/views/topology/service.js:106: // current drag supercedes any
previous annotations).
On 2013/10/15 15:34:16, gary.poster wrote:
> Until we have timestamps associated with our x/y annotations, we will
> perpetually have race conditions ISTM. :-(

> That's not for this branch, just thinking.

This is also the last write wins case where two clients are moving the
same service at the same time, the first dragend will send a delta
event, but the other client still in the drag will ignore it rather than
have it jump mid drag to some new position.

https://codereview.appspot.com/14695043/diff/1/app/views/topology/service.js#newcode106
app/views/topology/service.js:106: // current drag supercedes any
previous annotations).
On 2013/10/15 15:34:16, gary.poster wrote:
> Until we have timestamps associated with our x/y annotations, we will
> perpetually have race conditions ISTM. :-(

> That's not for this branch...

Read more...

lp:~bcsaller/juju-gui/exportXY updated
1135. By Benjamin Saller

review feedback

1136. By Benjamin Saller

lint

1137. By Benjamin Saller

work around a test failure, also update to apiv3

1138. By Benjamin Saller

changes to tests were needed

1139. By Benjamin Saller

lint

Revision history for this message
Benjamin Saller (bcsaller) wrote :
Revision history for this message
Gary Poster (gary) wrote :

Awesome! LGTM and qa good, with trivial. Thank you!

https://codereview.appspot.com/14695043/diff/9001/app/models/models.js
File app/models/models.js (right):

https://codereview.appspot.com/14695043/diff/9001/app/models/models.js#newcode1139
app/models/models.js:1139: * @param {Object} [topology] used to get
position when available.
Remove this line

https://codereview.appspot.com/14695043/diff/9001/app/models/models.js#newcode1142
app/models/models.js:1142: exportDeployer: function(topology) {
Remove topology from arguments

https://codereview.appspot.com/14695043/diff/9001/app/views/topology/service.js
File app/views/topology/service.js (right):

https://codereview.appspot.com/14695043/diff/9001/app/views/topology/service.js#newcode1123
app/views/topology/service.js:1123: @method findCentroid
panToCentroid seems more apt. <shrug>

https://codereview.appspot.com/14695043/diff/9001/app/views/topology/topology.js
File app/views/topology/topology.js (right):

https://codereview.appspot.com/14695043/diff/9001/app/views/topology/topology.js#newcode212
app/views/topology/topology.js:212: @param {ms} window.
timeout? wait? window is overloaded. <shrug>

https://codereview.appspot.com/14695043/diff/9001/test/test_environment_view.js
File test/test_environment_view.js (right):

https://codereview.appspot.com/14695043/diff/9001/test/test_environment_view.js#newcode625
test/test_environment_view.js:625: /*view.topo.servicePointOutside =
function() {
so...should we uncomment this or delete this? I didn't quite follow
from your reply.

https://codereview.appspot.com/14695043/diff/9001/test/test_environment_view.js#newcode644
test/test_environment_view.js:644: *view.topo.servicePointOutside =
function() {
Should we uncomment this or delete this? I didn't quite follow from
your reply.

https://codereview.appspot.com/14695043/

Revision history for this message
Madison Scott-Clary (makyo) wrote :

LGTMly code, QAing quickly.

https://codereview.appspot.com/14695043/diff/9001/app/views/topology/topology.js
File app/views/topology/topology.js (right):

https://codereview.appspot.com/14695043/diff/9001/app/views/topology/topology.js#newcode212
app/views/topology/topology.js:212: @param {ms} window.
On 2013/10/15 22:16:56, gary.poster wrote:
> timeout? wait? window is overloaded. <shrug>

+1 on timeout

https://codereview.appspot.com/14695043/diff/9001/test/test_environment_view.js
File test/test_environment_view.js (right):

https://codereview.appspot.com/14695043/diff/9001/test/test_environment_view.js#newcode625
test/test_environment_view.js:625: /*view.topo.servicePointOutside =
function() {
On 2013/10/15 22:16:56, gary.poster wrote:
> so...should we uncomment this or delete this? I didn't quite follow
from your
> reply.

 From my branch, these tests will pass/still test what they if you reset
the db; I think it's worth testing, but that means that tests aren't
written properly. Rewrite, or maybe move to their own test with
it.skip?

https://codereview.appspot.com/14695043/diff/9001/test/test_environment_view.js#newcode644
test/test_environment_view.js:644: *view.topo.servicePointOutside =
function() {
On 2013/10/15 22:16:56, gary.poster wrote:
> Should we uncomment this or delete this? I didn't quite follow from
your reply.

as above

https://codereview.appspot.com/14695043/

Revision history for this message
Benjamin Saller (bcsaller) wrote :

Thanks again, sorry for so many rounds.

https://codereview.appspot.com/14695043/diff/9001/app/models/models.js
File app/models/models.js (right):

https://codereview.appspot.com/14695043/diff/9001/app/models/models.js#newcode1139
app/models/models.js:1139: * @param {Object} [topology] used to get
position when available.
On 2013/10/15 22:16:56, gary.poster wrote:
> Remove this line

Doh! Thanks

https://codereview.appspot.com/14695043/diff/9001/app/views/topology/topology.js
File app/views/topology/topology.js (right):

https://codereview.appspot.com/14695043/diff/9001/app/views/topology/topology.js#newcode212
app/views/topology/topology.js:212: @param {ms} window.
On 2013/10/15 22:16:56, gary.poster wrote:
> timeout? wait? window is overloaded. <shrug>

Done.

https://codereview.appspot.com/14695043/diff/9001/test/test_environment_view.js
File test/test_environment_view.js (right):

https://codereview.appspot.com/14695043/diff/9001/test/test_environment_view.js#newcode644
test/test_environment_view.js:644: *view.topo.servicePointOutside =
function() {
On 2013/10/15 22:16:56, gary.poster wrote:
> Should we uncomment this or delete this? I didn't quite follow from
your reply.

Done.

https://codereview.appspot.com/14695043/

Revision history for this message
Benjamin Saller (bcsaller) wrote :

Thanks to you both.

https://codereview.appspot.com/14695043/diff/9001/test/test_environment_view.js
File test/test_environment_view.js (right):

https://codereview.appspot.com/14695043/diff/9001/test/test_environment_view.js#newcode625
test/test_environment_view.js:625: /*view.topo.servicePointOutside =
function() {
On 2013/10/15 22:29:57, matthew.scott wrote:
> On 2013/10/15 22:16:56, gary.poster wrote:
> > so...should we uncomment this or delete this? I didn't quite follow
from your
> > reply.

> From my branch, these tests will pass/still test what they if you
reset the db;
> I think it's worth testing, but that means that tests aren't written
properly.
> Rewrite, or maybe move to their own test with it.skip?

I deleted these for now, but if someone wants to check the QA issue gary
found with reloading positioned objects from a real env, adding in
improvements here might fit.

https://codereview.appspot.com/14695043/

Revision history for this message
Benjamin Saller (bcsaller) wrote :

*** Submitted:

Redo Service Placement

This changes a number of aspects of how service placement is handled.
There
may still be some edge cases and QA will need to be extensive as this
has moved through phases where various parts worked and didn't. I
believe
this version to be good however.

Position annotations remain on service models now rather than being
deleted
when applied. We favor this to having vars such as hasBeenPositioned,
positionedFromGhost and service model x/y (which mixed into
BoundingBoxes).
These various complications are mostly gone and the handling of updating

position annotations falls to a single method on the topology. (Though
to be
fair that is still called from more than one place).

Further simplification might be possible by unifying the node creation
and
node update paths wrt annotations. This didn't however fit in the time
provided.

This also includes a basic fix for always pulling positions from the
client
during an export.

R=gary.poster, benjamin.saller, matthew.scott
CC=
https://codereview.appspot.com/14695043

https://codereview.appspot.com/14695043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'app/models/models.js'
2--- app/models/models.js 2013-10-10 17:06:41 +0000
3+++ app/models/models.js 2013-10-15 20:38:14 +0000
4@@ -314,19 +314,6 @@
5 pending: {
6 value: false
7 },
8-
9- /**
10- Flag from ghost inspector to service topology. Helps topology
11- keep from unnecessarily jumping the service around. Essentially
12- an internal value that should be ignored except by this machinery.
13-
14- @attribute placeFromGhostPosition
15- @default false
16- @type {Boolean}
17- */
18- placeFromGhostPosition: {
19- value: false
20- },
21 life: {
22 value: ALIVE
23 },
24@@ -1149,9 +1136,10 @@
25 * that.
26 *
27 * @method exportDeployer
28+ * @param {Object} [topology] used to get position when available.
29 * @return {Object} export object suitable for serialization.
30 */
31- exportDeployer: function() {
32+ exportDeployer: function(topology) {
33 var self = this,
34 serviceList = this.services,
35 relationList = this.relations,
36@@ -1203,15 +1191,14 @@
37 serviceData.constraints = constraints;
38 }
39
40- var annotations = service.get('annotations');
41- if (annotations && annotations['gui-x']) {
42- // XXX: Only expose position. Currently these are position absolute
43- // rather than relative.
44- serviceData.annotations = {
45- 'gui-x': annotations['gui-x'],
46- 'gui-y': annotations['gui-y']
47- };
48+ // XXX: Only expose position. Currently these are position absolute
49+ // rather than relative.
50+ var anno = service.get('annotations');
51+ if (anno && anno['gui-x'] && anno['gui-y']) {
52+ serviceData.annotations = {'gui-x': anno['gui-x'],
53+ 'gui-y': anno['gui-y']};
54 }
55+
56 result.envExport.services[service.get('id')] = serviceData;
57 });
58
59
60=== modified file 'app/store/env/fakebackend.js'
61--- app/store/env/fakebackend.js 2013-10-10 17:06:41 +0000
62+++ app/store/env/fakebackend.js 2013-10-15 20:38:14 +0000
63@@ -1524,9 +1524,9 @@
64 var serviceData = ingestedData.services[serviceId];
65
66 // Force the annotation update (deploy doesn't handle this).
67- var annotiations = serviceData.annotations;
68- if (annotiations) {
69- service.set('annotations', annotiations);
70+ var anno = serviceData.annotations;
71+ if (anno) {
72+ self.updateAnnotations(service.get('id'), anno);
73 }
74
75 // Expose
76
77=== modified file 'app/views/ghost-inspector.js'
78--- app/views/ghost-inspector.js 2013-09-30 11:08:14 +0000
79+++ app/views/ghost-inspector.js 2013-10-15 20:38:14 +0000
80@@ -59,16 +59,14 @@
81 var ghostService = this.db.services.ghostService(charm);
82 if (ghostAttributes !== undefined) {
83 if (ghostAttributes.coordinates !== undefined) {
84- ghostService.set('x', ghostAttributes.coordinates[0]);
85- ghostService.set('y', ghostAttributes.coordinates[1]);
86+ var annotations = ghostService.get('annotations');
87+ annotations['gui-x'] = ghostAttributes.coordinates[0];
88+ annotations['gui-y'] = ghostAttributes.coordinates[1];
89 }
90 ghostService.set('icon', ghostAttributes.icon);
91- // Set the dragged attribute to true so that the x/y coords are
92- // stored in annotations as well as on the service box.
93- ghostService.set('hasBeenPositioned', true);
94 }
95- var environment = this.views.environment.instance,
96- ghostInspector = environment.createServiceInspector(ghostService);
97+ var environment = this.views.environment.instance;
98+ environment.createServiceInspector(ghostService);
99 }
100 };
101
102@@ -251,7 +249,9 @@
103 _deployCallbackHandler: function(serviceName, config, constraints, e) {
104 var options = this.options,
105 db = options.db,
106- ghostService = this.model;
107+ ghostService = this.model,
108+ environmentView = this.options.environment,
109+ topo = environmentView.topo;
110
111 if (e.err) {
112 db.notifications.add(
113@@ -270,41 +270,13 @@
114 level: 'info'
115 }));
116
117- // Update the annotations with the box's x/y coordinates if
118- // they have been set by dragging the ghost.
119- if (ghostService.get('hasBeenPositioned')) {
120- options.env.update_annotations(
121- serviceName, 'service',
122- { 'gui-x': ghostService.get('x'),
123- 'gui-y': ghostService.get('y') },
124- function() {
125- // Make sure that annotations are set on the ghost
126- // service before they come back from the delta to
127- // prevent the service from jumping to the middle of
128- // the canvas and back.
129- var annotations = ghostService.get('annotations');
130- if (!annotations) {
131- annotations = {};
132- }
133- Y.mix(annotations, {
134- 'gui-x': ghostService.get('x'),
135- 'gui-y': ghostService.get('y')
136- });
137- ghostService.set('annotations', annotations);
138- // The x/y attributes need to be removed to prevent
139- // lingering position problems after the service is
140- // positioned by the update code.
141- ghostService.removeAttr('x');
142- ghostService.removeAttr('y');
143- });
144- }
145-
146 // Now that we are using the same model for the ghost and service views
147 // we need to close the inspector to deactivate the databinding
148 // before setting else we end up with a race condition on nodes which
149 // no longer exist.
150 this.closeInspector();
151
152+ var ghostId = ghostService.get('id');
153 ghostService.setAttrs({
154 id: serviceName,
155 displayName: undefined,
156@@ -314,11 +286,18 @@
157 constraints: constraints
158 });
159
160- // This flag is used twice in the service topology module as a marker
161- // to know that it should not move the service or the canvas around
162- // (as opposed to services received from the environment).
163- ghostService.set('placeFromGhostPosition', true);
164- this.options.environment.createServiceInspector(ghostService);
165+ // Transition the ghost viewModel to the new
166+ // service. It's alive!
167+ var boxModel = topo.service_boxes[ghostId];
168+ boxModel.id = serviceName;
169+ boxModel.pending = false;
170+ delete topo.service_boxes[ghostId];
171+ topo.service_boxes[serviceName] = boxModel;
172+
173+ // Set to initial UI state.
174+ environmentView.createServiceInspector(ghostService);
175+ topo.showMenu(serviceName);
176+ topo.annotateBoxPosition(boxModel);
177 }
178
179 };
180
181=== modified file 'app/views/topology/relation.js'
182--- app/views/topology/relation.js 2013-09-19 20:11:53 +0000
183+++ app/views/topology/relation.js 2013-10-15 20:38:14 +0000
184@@ -176,17 +176,11 @@
185
186 var topo = this.get('component');
187 var db = topo.get('db');
188- var self = this;
189 var relations = db.relations.toArray();
190 this.relations = this.decorateRelations(relations);
191 this.updateLinks();
192 this.updateSubordinateRelationsCount();
193
194- // Ensure that link endpoints are up-to-date.
195- Y.each(topo.service_boxes, function(svc, key) {
196- self.updateLinkEndpoints({ service: svc });
197- });
198-
199 return this;
200 },
201
202@@ -258,6 +252,11 @@
203 updateLinkEndpoints: function(evt) {
204 var self = this;
205 var service = evt.service;
206+
207+ if (!service.relations || service.relations.size() === 0) {
208+ return;
209+ }
210+
211 Y.each(Y.Array.filter(self.relations, function(relation) {
212 return relation.source.id === service.id ||
213 relation.target.id === service.id;
214
215=== modified file 'app/views/topology/service.js'
216--- app/views/topology/service.js 2013-10-11 20:13:50 +0000
217+++ app/views/topology/service.js 2013-10-15 20:38:14 +0000
218@@ -80,13 +80,14 @@
219 // This is done after the services_boxes
220 // binding as the event handler will
221 // use that index.
222+ var movedNodes = 0;
223 node.each(function(d) {
224 var service = d.model,
225 annotations = service.get('annotations'),
226 x, y;
227
228 // If there are no annotations or the service is being dragged
229- if (!annotations || service.inDrag === views.DRAG_ACTIVE) {
230+ if (!annotations || d.inDrag) {
231 return;
232 }
233
234@@ -95,23 +96,24 @@
235 // node, as the annotations may have been set in another session.
236 x = annotations['gui-x'];
237 y = annotations['gui-y'];
238- if (!d ||
239- (x !== undefined && x !== d.x) ||
240- (y !== undefined && y !== d.y)) {
241- // Delete gui-x and gui-y from annotations as we use the values.
242- // This is to prevent deltas coming in on a service while it is
243- // being dragged from resetting its position during the drag.
244-
245- delete annotations['gui-x'];
246- delete annotations['gui-y'];
247+ if (x === undefined || y === undefined) {
248+ return;
249+ }
250+ x = parseFloat(x);
251+ y = parseFloat(y);
252+ if ((x !== d.x) || (y !== d.y)) {
253 // Only update position if we're not already in a drag state (the
254 // current drag supercedes any previous annotations).
255- var fromGhost = d.model.get('placeFromGhostPosition');
256 if (!d.inDrag) {
257- var useTransitions = self.get('useTransitions') && !fromGhost;
258+ var useTransitions = self.get('useTransitions');
259 self.drag.call(this, d, self, {x: x, y: y}, useTransitions);
260+ movedNodes += 1;
261+ topo.annotateBoxPosition(d);
262 }
263 }});
264+ if (movedNodes > 1) {
265+ this.findCentroid();
266+ }
267
268 // Mark subordinates as such. This is needed for when a new service
269 // is created.
270@@ -167,18 +169,6 @@
271 'x': 64,
272 'y': 47 * 0.8});
273
274- // Handle the last step of models that were made locally from ghosts.
275- node.filter(function(d) {
276- return d.model.get('placeFromGhostPosition');
277- }).each(function(d) {
278- // Show the service menu from the start.
279- self.showServiceMenu(d);
280- // This flag has served its purpose, at initialization time on the
281- // canvas. Remove it, so future changes will have the usual
282- // behavior.
283- d.model.set('placeFromGhostPosition', false);
284- });
285-
286 // Landscape badge
287 if (landscape) {
288 node.each(function(d) {
289@@ -828,7 +818,7 @@
290 var topo = context.get('component');
291 context.longClickTimer = Y.later(750, this, function(d, e) {
292 // Provide some leeway for accidental dragging.
293- if ((Math.abs(box.x - box.oldX) + Math.abs(box.y - box.oldY)) /
294+ if ((Math.abs(box.x - box.px) + Math.abs(box.y - box.py)) /
295 2 > 5) {
296 return;
297 }
298@@ -868,8 +858,6 @@
299 * @method dragstart
300 */
301 dragstart: function(box, self) {
302- box.oldX = box.x;
303- box.oldY = box.y;
304 box.inDrag = views.DRAG_START;
305 },
306
307@@ -884,36 +872,18 @@
308 if (topo.buildingRelation) {
309 topo.ignoreServiceClick = true;
310 topo.fire('addRelationDragEnd');
311- }
312- else {
313-
314+ } else {
315 // If the service hasn't been dragged (in the case of long-click to
316 // add relation, or a double-fired event) or the old and new
317 // coordinates are the same, exit.
318- if (!box.inDrag ||
319- (box.oldX === box.x &&
320- box.oldY === box.y)) {
321- return;
322- }
323-
324- // If the service is still pending, persist x/y coordinates in
325- // order to set them as annotations when the service is created.
326- if (box.pending) {
327- box.model.set('hasBeenPositioned', true);
328- box.model.set('x', box.x);
329- box.model.set('y', box.y);
330+ if (box.inDrag !== views.DRAG_ACTIVE) {
331 return;
332 }
333
334 // If the service has been dragged, ignore the subsequent service
335 // click event.
336 topo.ignoreServiceClick = true;
337-
338- topo.get('env').update_annotations(
339- box.id, 'service', {'gui-x': box.x, 'gui-y': box.y},
340- function() {
341- box.inDrag = false;
342- });
343+ topo.annotateBoxPosition(box);
344 }
345 },
346
347@@ -954,8 +924,6 @@
348 if (pos) {
349 box.x = pos.x;
350 box.y = pos.y;
351- // Explicitly reassign data.
352- selection = selection.data([box]);
353 } else {
354 box.x += d3.event.dx;
355 box.y += d3.event.dy;
356@@ -1011,15 +979,6 @@
357 this.service_scale_height = d3.scale.log().range([64, 100]);
358 }
359
360- if (!this.tree) {
361- this.tree = d3.layout.unscaledPack()
362- .size([width, height])
363- .value(function(d) {
364- return Math.max(d.unit_count, 1);
365- })
366- .padding(300);
367- }
368-
369 if (!this.dragBehavior) {
370 this.dragBehavior = d3.behavior.drag()
371 .on('dragstart', function(d) { self.dragstart.call(this, d, self);})
372@@ -1039,43 +998,52 @@
373 // entire graph. As a short term work around we layout only new
374 // nodes. New nodes are those that haven't been positioned by drag
375 // and drop, or those who don't have position attributes/annotations.
376- var vertices;
377- var fromGhost = false;
378- var new_services = Y.Object.values(topo.service_boxes)
379+ var vertices = [];
380+ Y.each(topo.service_boxes, function(boundingBox) {
381+ var annotations = boundingBox.annotations;
382+ if (annotations['gui-x'] && boundingBox.x === undefined) {
383+ boundingBox.x = annotations['gui-x'];
384+ }
385+ if (annotations['gui-y'] && boundingBox.y === undefined) {
386+ boundingBox.y = annotations['gui-y'];
387+ }
388+ });
389+
390+ // new_service_boxes are those w/o current x/y pos and no
391+ // annotations.
392+ var new_service_boxes = Y.Object.values(topo.service_boxes)
393 .filter(function(boundingBox) {
394 var annotations = boundingBox.model.get('annotations');
395- return !boundingBox.hasBeenPositioned ||
396- (!Y.Lang.isNumber(boundingBox.x) &&
397- !(annotations && annotations['gui-x']));
398+ return ((!Y.Lang.isNumber(boundingBox.x) &&
399+ !(annotations && annotations['gui-x'])));
400 });
401- if (new_services.length > 0) {
402+
403+ if (new_service_boxes.length > 0) {
404 // If the there is only one new service and it's pending (as in, it was
405 // added via the charm panel as a ghost), position it intelligently and
406 // set its position coordinates such that they'll be saved when the
407 // service is actually created. Otherwise, rely on our pack layout (as
408 // in the case of opening an unannotated environment for the first
409 // time).
410- var pendingServicePlaced = false;
411- if (new_services.length === 1 &&
412- new_services[0].model.get('pending')) {
413- pendingServicePlaced = true;
414+ if (new_service_boxes.length === 1 &&
415+ new_service_boxes[0].model.get('pending') &&
416+ (new_service_boxes[0].x === undefined ||
417+ new_service_boxes[0].y === undefined)) {
418 // Get a coordinate outside the cluster of existing services.
419 var coords = topo.servicePointOutside();
420 // Set the coordinates on both the box model and the service
421 // model.
422- new_services[0].x = coords[0];
423- new_services[0].y = coords[1];
424- new_services[0].model.set('x', coords[0]);
425- new_services[0].model.set('y', coords[1]);
426- // This ensures that the x/y coordinates will be saved as
427- // annotations.
428- new_services[0].model.set('hasBeenPositioned', true);
429+ new_service_boxes[0].x = coords[0];
430+ new_service_boxes[0].y = coords[1];
431 // Set the centroid to the new service's position
432- topo.centroid = coords;
433- topo.fire('panToPoint', {point: topo.centroid});
434+ topo.fire('panToPoint', {point: coords});
435 } else {
436- this.tree.nodes({children: new_services});
437- if (new_services.length < Y.Object.size(topo.service_boxes)) {
438+ d3.layout.unscaledPack()
439+ .size([width, height])
440+ .value(function(d) { return Math.max(d.unit_count, 1); })
441+ .padding(300)
442+ .nodes({children: new_service_boxes});
443+ if (new_service_boxes.length < Y.Object.size(topo.service_boxes)) {
444 // If we have new services that do not have x/y coords and are
445 // not pending, then they've likely been created from the CLI.
446 // In this case, to avoid placing them overlaying any existing
447@@ -1084,49 +1052,34 @@
448 // will result in annotations being set in the environment
449 // below.
450 var pointOutside = topo.servicePointOutside();
451- Y.each(new_services, function(service) {
452- service.x += pointOutside[0] - service.x;
453- service.y += pointOutside[1] - service.y;
454- service.model.set('x', service.x);
455- service.model.set('y', service.y);
456+ Y.each(new_service_boxes, function(boxModel) {
457+ boxModel.x += pointOutside[0] - boxModel.x;
458+ boxModel.y += pointOutside[1] - boxModel.y;
459 });
460 }
461- }
462- // Update annotations settings position on backend (but only do
463- // this if there is no existing annotations).
464- if (!pendingServicePlaced) {
465- vertices = [];
466- }
467- Y.each(new_services, function(box) {
468- if (box.model.get('placeFromGhostPosition')) {
469- fromGhost = true;
470- }
471- var existing = box.model.get('annotations') || {};
472- if (!existing && !existing['gui-x']) {
473- topo.get('env').update_annotations(
474- box.id, 'service', {'gui-x': box.x, 'gui-y': box.y},
475- function() {
476- box.inDrag = false;
477- });
478- vertices.push([box.x || 0, box.y || 0]);
479- } else {
480- if (vertices) {
481- vertices.push([
482- existing['gui-x'] || (box.x || 0),
483- existing['gui-y'] || (box.y || 0)
484- ]);
485+
486+ Y.each(new_service_boxes, function(box) {
487+ var existing = box.model.get('annotations') || {};
488+ if (!existing || !existing['gui-x']) {
489+ vertices.push([box.x || 0, box.y || 0]);
490+ topo.annotateBoxPosition(box, false);
491+ } else {
492+ if (vertices.length > 0) {
493+ vertices.push([
494+ existing['gui-x'] || (box.x || 0),
495+ existing['gui-y'] || (box.y || 0)
496+ ]);
497+ }
498 }
499- }
500- });
501- }
502- if (!topo.centroid || vertices) {
503+ });
504+ }
505 // Find the centroid of our hull of services and inform the
506 // topology.
507- if (!vertices) {
508- vertices = topoUtils.serviceBoxesToVertices(topo.service_boxes);
509+ if (vertices.length) {
510+ this.findCentroid(vertices);
511 }
512- this.findAndSetCentroid(vertices, fromGhost);
513 }
514+
515 // enter
516 node
517 .enter().append('g')
518@@ -1135,10 +1088,10 @@
519 'class': function(d) {
520 return (d.subordinate ? 'subordinate ' : '') +
521 (d.pending ? 'pending ' : '') + 'service';
522- },
523- 'transform': function(d) {return d.translateStr;}})
524+ }})
525 .call(this.dragBehavior)
526- .call(self.createServiceNode, self);
527+ .call(self.createServiceNode, self)
528+ .attr('transform', function(d) { return d.translateStr; });
529
530 // Update all nodes.
531 self.updateServiceNodes(node);
532@@ -1161,26 +1114,20 @@
533 panToCenter: function(evt) {
534 var topo = this.get('component');
535 var vertices = topoUtils.serviceBoxesToVertices(topo.service_boxes);
536- this.findAndSetCentroid(vertices);
537+ this.findCentroid(vertices);
538 },
539
540 /**
541 Given a set of vertices, find the centroid and pan to that location.
542
543- @method findAndSetCentroid
544+ @method findCentroid
545 @param {array} vertices A list of vertices in the form [x, y].
546 @return {undefined} Side effects only.
547 */
548- findAndSetCentroid: function(vertices, preventPan) {
549+ findCentroid: function(vertices) {
550 var topo = this.get('component'),
551 centroid = topoUtils.centroid(vertices);
552- // The centroid is set on the topology object due to the fact that it is
553- // used as a sigil to tell whether or not to pan to the point after the
554- // first delta.
555- topo.centroid = centroid;
556- if (!preventPan) {
557- topo.fire('panToPoint', {point: topo.centroid});
558- }
559+ topo.fire('panToPoint', {point: centroid});
560 },
561
562 /**
563
564=== modified file 'app/views/topology/topology.js'
565--- app/views/topology/topology.js 2013-10-04 18:37:47 +0000
566+++ app/views/topology/topology.js 2013-10-15 20:38:14 +0000
567@@ -186,7 +186,52 @@
568 return utils.pointOutside(
569 utils.serviceBoxesToVertices(existingBoxes),
570 this.get('servicePadding'));
571+ },
572+
573+ /**
574+ Show the menu for a given service
575+
576+ @method showMenu
577+ @param {String} serviceId
578+ */
579+ showMenu: function(serviceId) {
580+ var serviceModule = this.modules.ServiceModule;
581+ if (!serviceModule) { return;}
582+ var boxModel = this.service_boxes[serviceId];
583+ serviceModule.showServiceMenu(boxModel);
584+ },
585+
586+ /**
587+ Record a new box position on the backend. This maintains the proper drag
588+ state. This method also transitions the viewModel to a DRAG_ENDING state
589+ with a timeout. During this window the box will behave as if its in a drag
590+ state refusing annotation updates. This masks certain classes of races.
591+
592+ @method annotateBoxPosition
593+ @param {Object} box.
594+ @param {ms} window.
595+ */
596+ annotateBoxPosition: function(box, window) {
597+ if (box.pending) { return; }
598+ window = window || 1000;
599+
600+ // This can happen in some tests.
601+ this.get('env').update_annotations(
602+ box.id, 'service', {'gui-x': box.x, 'gui-y': box.y},
603+ function() {
604+ if (window) {
605+ box.inDrag = views.DRAG_ENDING;
606+ Y.later(window, box, function() {
607+ // Provide (t) ms of protection from sending additional
608+ // annotations or applying them locally.
609+ box.inDrag = false;
610+ });
611+ } else {
612+ box.inDrag = false;
613+ }
614+ });
615 }
616+
617 }, {
618 ATTRS: {
619 /**
620@@ -238,6 +283,7 @@
621 */
622 views.DRAG_START = 1;
623 views.DRAG_ACTIVE = 2;
624+ views.DRAG_ENDING = 3;
625 }, '0.1.0', {
626 requires: [
627 'd3',
628
629=== modified file 'app/views/utils.js'
630--- app/views/utils.js 2013-10-11 20:13:50 +0000
631+++ app/views/utils.js 2013-10-15 20:38:14 +0000
632@@ -1738,7 +1738,7 @@
633
634 utils.deployBundleCallback = function(notifications, result) {
635 if (result.err) {
636- console.log('import failed', result);
637+ console.log('import failed', result.err);
638 notifications.add({
639 title: 'Deploy Bundle',
640 message: 'Environment deploy of the bundle failed.<br/>',
641
642=== modified file 'test/test_conflict_ux.js'
643--- test/test_conflict_ux.js 2013-09-18 13:04:27 +0000
644+++ test/test_conflict_ux.js 2013-10-15 20:38:14 +0000
645@@ -53,6 +53,7 @@
646 db = new models.Database();
647 conn = new utils.SocketStub();
648 env = juju.newEnvironment({conn: conn});
649+ env.update_annotations = function() {};
650 inspector = setUpInspector();
651 });
652
653@@ -80,7 +81,7 @@
654 ['unit', 'add', {id: 'mediawiki/0', agent_state: 'pending'}]
655 ]}});
656
657- var fakeStore = new Y.juju.charmworld.APIv2({});
658+ var fakeStore = new Y.juju.charmworld.APIv3({});
659 fakeStore.iconpath = function() {
660 return 'charm icon url';
661 };
662
663=== modified file 'test/test_environment_view.js'
664--- test/test_environment_view.js 2013-10-07 19:38:23 +0000
665+++ test/test_environment_view.js 2013-10-15 20:38:14 +0000
666@@ -622,9 +622,9 @@
667 match[2].should.eql('211.2');
668
669 // A positioned service will never be auto-positioned.
670- view.topo.servicePointOutside = function() {
671+ /*view.topo.servicePointOutside = function() {
672 assert(false, 'We should never get here because annotations are set');
673- };
674+ };*/
675 tmp_data = {
676 op: 'delta',
677 result: [
678@@ -640,10 +640,12 @@
679 db.onDelta({ data: tmp_data });
680 view.update();
681
682- view.topo.servicePointOutside = function() {
683- assert(false,
684- 'We should never get here because service was positioned');
685- };
686+ /*
687+ *view.topo.servicePointOutside = function() {
688+ * assert(false,
689+ * 'We should never get here because service was positioned');
690+ *};
691+ */
692 tmp_data = {
693 op: 'delta',
694 result: [
695@@ -656,7 +658,6 @@
696 ]]
697 };
698 db.onDelta({ data: tmp_data });
699- db.services.getById('wordpressb').set('hasBeenPositioned', true);
700 view.update();
701 });
702
703
704=== modified file 'test/test_fakebackend.js'
705--- test/test_fakebackend.js 2013-10-10 05:52:08 +0000
706+++ test/test_fakebackend.js 2013-10-15 20:38:14 +0000
707@@ -162,7 +162,11 @@
708 packageName: 'wordpress'
709 };
710
711- assert.deepEqual(attrs, expectedAttrs);
712+ // Assert some key properties
713+ assert.equal(attrs.id, expectedAttrs.id);
714+ assert.equal(attrs.charm, expectedAttrs.charm);
715+ assert.deepEqual(attrs.config, expectedAttrs.config);
716+ assert.deepEqual(attrs.annotations, expectedAttrs.annotations);
717 var units = service.get('units').toArray();
718 assert.lengthOf(units, 1);
719 assert.lengthOf(result.units, 1);
720
721=== modified file 'test/test_inspector_constraints.js'
722--- test/test_inspector_constraints.js 2013-09-18 13:04:27 +0000
723+++ test/test_inspector_constraints.js 2013-10-15 20:38:14 +0000
724@@ -91,6 +91,7 @@
725 return 'charm icon url';
726 };
727 env = juju.newEnvironment({conn: conn});
728+ env.update_annotations = function() {};
729 env.connect();
730 view = new views.environment({
731 container: container,
732
733=== modified file 'test/test_inspector_overview.js'
734--- test/test_inspector_overview.js 2013-10-11 19:33:07 +0000
735+++ test/test_inspector_overview.js 2013-10-15 20:38:14 +0000
736@@ -47,6 +47,7 @@
737 conn = new utils.SocketStub();
738 db = new models.Database();
739 env = juju.newEnvironment({conn: conn});
740+ env.update_annotations = function() {};
741 env.expose = function(s) {
742 exposeCalled = true;
743 service.set('exposed', true);
744
745=== modified file 'test/test_inspector_settings.js'
746--- test/test_inspector_settings.js 2013-09-27 23:41:44 +0000
747+++ test/test_inspector_settings.js 2013-10-15 20:38:14 +0000
748@@ -43,6 +43,7 @@
749 conn = new utils.SocketStub();
750 db = new models.Database();
751 env = juju.newEnvironment({conn: conn});
752+ env.update_annotations = function() {};
753 });
754
755 afterEach(function(done) {
756
757=== modified file 'test/test_model.js'
758--- test/test_model.js 2013-10-10 17:06:41 +0000
759+++ test/test_model.js 2013-10-15 20:38:14 +0000
760@@ -18,7 +18,7 @@
761
762 'use strict';
763
764-describe('test_models.js', function() {
765+describe('test_model.js', function() {
766 describe('Charm initialization', function() {
767 var models;
768
769@@ -797,13 +797,14 @@
770 });
771
772 it('can export in deployer format', function() {
773+ // Mock a topology that can return positions.
774 db.services.add({id: 'mysql', charm: 'precise/mysql-1'});
775 db.services.add({
776 id: 'wordpress',
777 charm: 'precise/wordpress-1',
778 config: {debug: 'no', username: 'admin'},
779 constraints: 'cpu-power=2,cpu-cores=4',
780- annotations: {'gui-x': 100, 'gui-y': 200, 'ignored': true}
781+ annotations: {'gui-x': 100, 'gui-y': 200}
782 });
783 db.relations.add({
784 id: 'relation-0',
785
786=== modified file 'test/test_service_module.js'
787--- test/test_service_module.js 2013-10-01 18:33:29 +0000
788+++ test/test_service_module.js 2013-10-15 20:38:14 +0000
789@@ -73,7 +73,7 @@
790 function() {
791 var d =
792 { id: 'wordpress',
793- inDrag: true,
794+ inDrag: views.DRAG_ACTIVE,
795 x: 100.1,
796 y: 200.2};
797 serviceModule.dragend(d, serviceModule);

Subscribers

People subscribed via source and target branches