Merge lp:~blake-rouse/maas/fix-cache-clusters-images into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 5045
Proposed branch: lp:~blake-rouse/maas/fix-cache-clusters-images
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 786 lines (+469/-83)
14 files modified
src/maasserver/static/js/angular/controllers/node_details.js (+6/-27)
src/maasserver/static/js/angular/controllers/node_details_storage.js (+2/-1)
src/maasserver/static/js/angular/controllers/nodes_list.js (+1/-23)
src/maasserver/static/js/angular/directives/controller_image_status.js (+156/-0)
src/maasserver/static/js/angular/directives/tests/test_controller_image_status.js (+240/-0)
src/maasserver/static/js/angular/directives/tests/test_version_reloader.js (+5/-0)
src/maasserver/static/js/angular/directives/version_reloader.js (+3/-0)
src/maasserver/static/js/angular/maas.js (+32/-13)
src/maasserver/static/partials/node-details.html (+2/-2)
src/maasserver/static/partials/nodes-list.html (+3/-1)
src/maasserver/templates/maasserver/js-conf.html (+15/-13)
src/maasserver/views/combo.py (+1/-0)
src/maasserver/websockets/handlers/controller.py (+1/-1)
src/maasserver/websockets/handlers/tests/test_controller.py (+2/-2)
To merge this branch: bzr merge lp:~blake-rouse/maas/fix-cache-clusters-images
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Jeffrey C Jones (community) Approve
Review via email: mp+295585@code.launchpad.net

Commit message

Convert the controllers image status to a directive to allow it to be used across MAAS. Add ?v={version} to templateUrl in routes to prevent caching on newer versions.

To post a comment you must log in.
Revision history for this message
Jeffrey C Jones (trapnine) wrote :

Nice, now the status can be plopped anywhere. A few minor comments below.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

So much code for such a little thing! Still, at least it's bottled now.

There are a few hundred lines of diff for something that, at first glance, appears unrelated, so Needs Information for that alone.

review: Needs Information
Revision history for this message
Blake Rouse (blake-rouse) wrote :

> So much code for such a little thing! Still, at least it's bottled now.

Thats javascript! ;-)

> There are a few hundred lines of diff for something that, at first glance, appears unrelated, so
> Needs Information for that alone.

That addresses the bug linked to this branch. That bug shows that he was viewing a 1.9 UI with 2.0 javascript. The issue is that the partial views are being cached and the cache is not being broken with the ?v={version} on the file path.

In the process of fixing that bug I ran into an issue where the controllers listing was causing the regiond to crash. I fixed that, but by the time I was done trapnine landed image statuses on the details page as well. So I had to fix both, what was a very simple fix became large to do it correctly.

Revision history for this message
Gavin Panella (allenap) wrote :

Cool, thanks for the reply. Looks like Jeff went through it fairly closely, so +1.

review: Approve
Revision history for this message
Blake Rouse (blake-rouse) :
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (31.3 KiB)

The attempt to merge lp:~blake-rouse/maas/fix-cache-clusters-images into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease [94.5 kB]
Get:3 http://security.ubuntu.com/ubuntu xenial-security InRelease [94.5 kB]
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Fetched 189 kB in 0s (425 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind bash bind9 bind9utils build-essential bzr bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm postgresql pxelinux python3-all python3-apt python3-bson python3-convoy python3-coverage python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-dnspython python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-netaddr python3-netifaces python3-novaclient python3-oauth python3-oauthlib python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-requests python3-seamicroclient python3-setuptools python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-simplejson python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-2ubuntu3).
archdetect-deb is already the newest version (1.117ubuntu2).
authbind is already the newest version (2.1.1+nmu1).
bash is already the newest version (4.3-14ubuntu1).
build-essential is already the newest version (12.1ubuntu2).
bzr is already the newest version (2.7.0-2ubuntu1).
curl is already the newest version (7.47.0-1ubuntu2).
debhelper is already the newest version (9.20160115ubuntu3).
distro-info is already the newest version (0.14build1).
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.4-0ubuntu1).
isc-dhcp-common is already the newest version (4.3.3-5ubuntu12).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
pxelinux is already the...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/static/js/angular/controllers/node_details.js'
2--- src/maasserver/static/js/angular/controllers/node_details.js 2016-05-21 03:24:08 +0000
3+++ src/maasserver/static/js/angular/controllers/node_details.js 2016-05-25 20:28:15 +0000
4@@ -185,8 +185,10 @@
5 return;
6 }
7
8- $scope.summary.zone.selected = ZonesManager.getItemFromList(
9- $scope.node.zone.id);
10+ if(angular.isObject($scope.node.zone)) {
11+ $scope.summary.zone.selected = ZonesManager.getItemFromList(
12+ $scope.node.zone.id);
13+ }
14 $scope.summary.architecture.selected = $scope.node.architecture;
15 $scope.summary.min_hwe_kernel.selected = $scope.node.min_hwe_kernel;
16 $scope.summary.tags = angular.copy($scope.node.tags);
17@@ -335,23 +337,6 @@
18 });
19 }
20
21- function getControllersImageSyncStatus() {
22- if(angular.isObject($scope.node)) {
23- $scope.nodesManager.checkImageStates(
24- [{system_id: $scope.node.system_id}]).then(
25- function(results) {
26- // Results is a map of system_id to displayable status.
27- if(results[$scope.node.system_id]) {
28- $scope.node.image_sync_status =
29- results[$scope.node.system_id];
30- } else {
31- $scope.node.image_sync_status = "Unknown";
32- }
33- }
34- );
35- }
36- }
37-
38 // Starts the watchers on the scope.
39 function startWatching() {
40 // Update the title and name when the node fqdn changes.
41@@ -398,13 +383,6 @@
42 $scope.$watch("node.summary_yaml", updateMachineOutput);
43 $scope.$watch("node.commissioning_results", updateMachineOutput);
44 $scope.$watch("node.installation_results", updateMachineOutput);
45-
46- if($scope.isController) {
47- getControllersImageSyncStatus();
48- $scope.imageStatusPoll = $interval(function() {
49- getControllersImageSyncStatus();
50- }, 10000);
51- }
52 }
53
54 // Called when the node has been loaded.
55@@ -867,7 +845,8 @@
56 };
57
58 $scope.getPowerEventError = function() {
59- if(!angular.isObject($scope.node)) {
60+ if(!angular.isObject($scope.node) ||
61+ !angular.isArray($scope.node.events)) {
62 return;
63 }
64
65
66=== modified file 'src/maasserver/static/js/angular/controllers/node_details_storage.js'
67--- src/maasserver/static/js/angular/controllers/node_details_storage.js 2016-03-28 13:54:47 +0000
68+++ src/maasserver/static/js/angular/controllers/node_details_storage.js 2016-05-25 20:28:15 +0000
69@@ -2000,7 +2000,8 @@
70
71 // Returns true if there are storage layout errors
72 $scope.hasStorageLayoutIssues = function() {
73- if(angular.isObject($scope.node)) {
74+ if(angular.isObject($scope.node) &&
75+ angular.isArray($scope.node.storage_layout_issues)) {
76 return $scope.node.storage_layout_issues.length > 0;
77 }
78 return false;
79
80=== modified file 'src/maasserver/static/js/angular/controllers/nodes_list.js'
81--- src/maasserver/static/js/angular/controllers/nodes_list.js 2016-05-21 03:24:08 +0000
82+++ src/maasserver/static/js/angular/controllers/nodes_list.js 2016-05-25 20:28:15 +0000
83@@ -124,6 +124,7 @@
84 errors: {}
85 };
86 $scope.tabs.controllers.zoneSelection = null;
87+ $scope.tabs.controllers.syncStatuses = {};
88
89 // Options for add hardware dropdown.
90 $scope.addHardwareOption = null;
91@@ -570,21 +571,6 @@
92 return DEVICE_IP_ASSIGNMENT[ipAssignment];
93 };
94
95- $scope.getControllersImageSyncStatus = function() {
96- ControllersManager.checkImageStates($scope.controllers).then(
97- function(results) {
98- angular.forEach($scope.controllers, function(controller) {
99- // Results is a map of system_id to displayable status.
100- if(results[controller.system_id]) {
101- controller.image_sync_status =
102- results[controller.system_id];
103- } else {
104- controller.image_sync_status = "Unknown";
105- }
106- });
107- });
108- };
109-
110 // Return true if the authenticated user is super user.
111 $scope.isSuperUser = function() {
112 return UsersManager.isSuperUser();
113@@ -597,14 +583,6 @@
114 [MachinesManager, DevicesManager, ControllersManager,
115 GeneralManager, ZonesManager, UsersManager, ServicesManager]).then(
116 function() {
117-
118- if($routeParams.tab === "controllers") {
119- $scope.getControllersImageSyncStatus();
120- $scope.statusPoll = $interval(function() {
121- $scope.getControllersImageSyncStatus();
122- }, 10000);
123- }
124-
125 $scope.loading = false;
126 });
127
128
129=== added file 'src/maasserver/static/js/angular/directives/controller_image_status.js'
130--- src/maasserver/static/js/angular/directives/controller_image_status.js 1970-01-01 00:00:00 +0000
131+++ src/maasserver/static/js/angular/directives/controller_image_status.js 2016-05-25 20:28:15 +0000
132@@ -0,0 +1,156 @@
133+/* Copyright 2016 Canonical Ltd. This software is licensed under the
134+ * GNU Affero General Public License version 3 (see the file LICENSE).
135+ *
136+ * Controller image status directive.
137+ *
138+ * Shows the image status for a controller.
139+ */
140+
141+ angular.module('MAAS').service('ControllerImageStatusService',
142+ ['$timeout', '$interval', 'ControllersManager', function(
143+ $timeout, $interval, ControllersManager) {
144+ var self = this;
145+
146+ // List of controllers that need to have the image status updated.
147+ this.controllers = [];
148+
149+ // List of current controller statues.
150+ this.statuses = {};
151+
152+ // Interval function that is called to update the statuses.
153+ this.updateStatuses = function() {
154+ var controllerIds = [];
155+ angular.forEach(self.controllers, function(system_id) {
156+ controllerIds.push({system_id: system_id});
157+ });
158+
159+ // Check the image states.
160+ ControllersManager.checkImageStates(controllerIds).then(
161+ function(results) {
162+ angular.forEach(controllerIds, function(controller) {
163+ var status = results[controller.system_id];
164+ if(status) {
165+ self.statuses[controller.system_id] = status;
166+ } else {
167+ self.statuses[controller.system_id] = "Unknown";
168+ }
169+ });
170+ });
171+ };
172+
173+ // Register this controller system_id.
174+ this.register = function(system_id) {
175+ var known = self.controllers.indexOf(system_id) >= 0;
176+ if(!known) {
177+ self.controllers.push(system_id);
178+ }
179+
180+ // When the interval is already running and its a new controller then
181+ // the interval needs to be reset. When it already exists it doesn't
182+ // need to be reset.
183+ if(angular.isDefined(self.runningInterval)) {
184+ if(known) {
185+ return;
186+ } else {
187+ $interval.cancel(self.runningInterval);
188+ self.runningInterval = undefined;
189+ }
190+ }
191+
192+ // If its not running and the timeout has been started we re-create
193+ // the timeout. This delays the start of the interval until the
194+ // all directives on the page have been fully loaded.
195+ if(angular.isDefined(self.startTimeout)) {
196+ $timeout.cancel(self.startTimeout);
197+ }
198+ self.startTimeout = $timeout(function() {
199+ self.startTimeout = undefined;
200+ self.runningInterval = $interval(function() {
201+ self.updateStatuses();
202+ }, 10 * 1000);
203+ self.updateStatuses();
204+ }, 100);
205+ };
206+
207+ // Unregister the controller.
208+ this.unregister = function(system_id) {
209+ var idx = self.controllers.indexOf(system_id);
210+ if(idx > -1) {
211+ self.controllers.splice(idx, 1);
212+ }
213+
214+ // If no controllers are left stop all intervals and timeouts.
215+ if(self.controllers.length === 0) {
216+ if(angular.isDefined(self.startTimeout)) {
217+ $timeout.cancel(self.startTimeout);
218+ self.startTimeout = undefined;
219+ }
220+ if(angular.isDefined(self.runningInterval)) {
221+ $interval.cancel(self.runningInterval);
222+ self.runningInterval = undefined;
223+ }
224+ }
225+ };
226+
227+ // Return true if the spinner should be shown.
228+ this.showSpinner = function(system_id) {
229+ var status = self.statuses[system_id];
230+ if(angular.isString(status) && status !== "Syncing") {
231+ return false;
232+ } else {
233+ return true;
234+ }
235+ };
236+
237+ // Get the image status.
238+ this.getImageStatus = function(system_id) {
239+ var status = self.statuses[system_id];
240+ if(angular.isString(status)) {
241+ return status;
242+ } else {
243+ return "Asking for status...";
244+ }
245+ };
246+}]);
247+
248+angular.module('MAAS').directive('maasControllerImageStatus',
249+ ['ControllerImageStatusService', function(
250+ ControllerImageStatusService) {
251+
252+ return {
253+ restrict: "E",
254+ scope: {
255+ systemId: "="
256+ },
257+ template: [
258+ '<i class="icon loading" data-ng-if="showSpinner()"></i> ',
259+ '{$ getImageStatus() $}'].join(''),
260+ link: function(scope, element, attrs) {
261+ // Don't register until the systemId is set.
262+ var unwatch, registered = false;
263+ unwatch = scope.$watch("systemId", function() {
264+ if(angular.isDefined(scope.systemId) && !registered) {
265+ ControllerImageStatusService.register(scope.systemId);
266+ registered = true;
267+ unwatch();
268+ }
269+ });
270+
271+ scope.showSpinner = function() {
272+ return ControllerImageStatusService.showSpinner(
273+ scope.systemId);
274+ };
275+ scope.getImageStatus = function() {
276+ return ControllerImageStatusService.getImageStatus(
277+ scope.systemId);
278+ };
279+
280+ // Unregister when destroyed.
281+ scope.$on("$destroy", function() {
282+ if(registered) {
283+ ControllerImageStatusService.unregister(scope.systemId);
284+ }
285+ });
286+ }
287+ };
288+}]);
289
290=== added file 'src/maasserver/static/js/angular/directives/tests/test_controller_image_status.js'
291--- src/maasserver/static/js/angular/directives/tests/test_controller_image_status.js 1970-01-01 00:00:00 +0000
292+++ src/maasserver/static/js/angular/directives/tests/test_controller_image_status.js 2016-05-25 20:28:15 +0000
293@@ -0,0 +1,240 @@
294+/* Copyright 2016 Canonical Ltd. This software is licensed under the
295+ * GNU Affero General Public License version 3 (see the file LICENSE).
296+ *
297+ * Unit tests for controller image status directive.
298+ */
299+
300+describe("maasControllerImageStatus", function() {
301+
302+ // Load the MAAS module.
303+ beforeEach(module("MAAS"));
304+
305+ // Get the manager and the directive service.
306+ var $timeout, $interval, $q;
307+ var ControllersManager, ControllerImageStatusService;
308+ beforeEach(inject(function($injector) {
309+ $timeout = $injector.get("$timeout");
310+ $interval = $injector.get("$interval");
311+ $q = $injector.get("$q");
312+ ControllersManager = $injector.get("ControllersManager");
313+ ControllerImageStatusService = $injector.get(
314+ "ControllerImageStatusService");
315+ }));
316+
317+ // Create a new scope before each test.
318+ var $scope;
319+ beforeEach(inject(function($rootScope) {
320+ $scope = $rootScope.$new();
321+ }));
322+
323+ // Return the compiled directive with the osinfo from the scope.
324+ function compileDirective() {
325+ var directive;
326+ var html = [
327+ '<div>',
328+ '<maas-controller-image-status system-id="system_id">',
329+ '</maas-controller-image-status>',
330+ '</div>'].join('');
331+
332+ // Compile the directive.
333+ inject(function($compile) {
334+ directive = $compile(html)($scope);
335+ });
336+
337+ // Perform the digest cycle to finish the compile.
338+ $scope.$digest();
339+ return directive.find("maas-controller-image-status");
340+ }
341+
342+ describe("service", function() {
343+
344+ var service;
345+ beforeEach(function() {
346+ service = ControllerImageStatusService;
347+ });
348+
349+ describe("updateStatuses", function() {
350+ it("calls checkImageStates with controllers", function() {
351+ service.controllers = [
352+ makeName("systemId"), makeName("systemId")];
353+ spyOn(ControllersManager, "checkImageStates").and.returnValue(
354+ $q.defer().promise);
355+ service.updateStatuses();
356+
357+ expect(
358+ ControllersManager.checkImageStates).toHaveBeenCalledWith([
359+ { system_id: service.controllers[0] },
360+ { system_id: service.controllers[1] }
361+ ]);
362+ });
363+
364+ it("sets statuses with result", function() {
365+ var defer = $q.defer();
366+ service.controllers = [
367+ makeName("systemId"),
368+ makeName("systemId"),
369+ makeName("systemId")];
370+ spyOn(ControllersManager, "checkImageStates").and.returnValue(
371+ defer.promise);
372+ service.updateStatuses();
373+
374+ var results = {};
375+ results[service.controllers[0]] = makeName("status");
376+ results[service.controllers[1]] = makeName("status");
377+ defer.resolve(results);
378+ $scope.$digest();
379+
380+ var statues = {};
381+ statues[service.controllers[0]] = (
382+ results[service.controllers[0]]);
383+ statues[service.controllers[1]] = (
384+ results[service.controllers[1]]);
385+ statues[service.controllers[2]] = "Unknown";
386+ expect(service.statuses).toEqual(statues);
387+ });
388+ });
389+
390+ describe("register", function() {
391+ it("added to controllers and starts timer", function() {
392+ var systemId = makeName("systemId");
393+ service.register(systemId);
394+
395+ expect(service.controllers).toEqual([systemId]);
396+ expect(service.startTimeout).toBeDefined();
397+ $timeout.cancel(service.startTimeout);
398+ });
399+
400+ it("added multiple and restarts timer", function() {
401+ var systemId1 = makeName("systemId");
402+ var systemId2 = makeName("systemId");
403+
404+ service.register(systemId1);
405+ var firstTimeout = service.startTimeout;
406+
407+ service.register(systemId2);
408+ var secondTimeout = service.startTimeout;
409+
410+ expect(firstTimeout).not.toBe(secondTimeout);
411+ expect(secondTimeout).toBeDefined();
412+ $timeout.cancel(secondTimeout);
413+ });
414+
415+ it("starts interval after 100ms", function() {
416+ var systemId = makeName("systemId");
417+ spyOn(service, "updateStatuses");
418+
419+ service.register(systemId);
420+ $timeout.flush(100);
421+
422+ expect(service.runningInterval).toBeDefined();
423+ });
424+
425+ it("cancels running interval on new controller", function() {
426+ var systemId = makeName("systemId");
427+
428+ var interval = service.runningInterval = {};
429+ spyOn($interval, "cancel");
430+
431+ service.register(systemId);
432+ expect($interval.cancel).toHaveBeenCalledWith(interval);
433+ expect(service.runningInterval).not.toBeDefined();
434+ expect(service.controllers).toEqual([systemId]);
435+ expect(service.startTimeout).toBeDefined();
436+ $timeout.cancel(service.startTimeout);
437+ });
438+ });
439+
440+ describe("unregister", function() {
441+ it("removes controller", function() {
442+ var systemId = makeName("systemId");
443+ service.controllers.push(systemId);
444+ service.unregister(systemId);
445+
446+ expect(service.controllers.length).toBe(0);
447+ });
448+
449+ it("stops timeout and interval", function() {
450+ var systemId = makeName("systemId");
451+ service.controllers.push(systemId);
452+
453+ var startTimeout = service.startTimeout = {};
454+ var runningInterval = service.runningInterval = {};
455+ spyOn($timeout, "cancel");
456+ spyOn($interval, "cancel");
457+
458+ service.unregister(systemId);
459+ expect($timeout.cancel).toHaveBeenCalledWith(startTimeout);
460+ expect(service.startTimeout).not.toBeDefined();
461+ expect($interval.cancel).toHaveBeenCalledWith(runningInterval);
462+ expect(service.runningInterval).not.toBeDefined();
463+ });
464+ });
465+
466+ describe("showSpinner", function() {
467+ it("returns false if set and not syncing", function() {
468+ var systemId = makeName("systemId");
469+ var status = "out-of-sync";
470+ service.statuses[systemId] = status;
471+ expect(service.showSpinner(systemId)).toBe(false);
472+ });
473+
474+ it("returns true if not set", function() {
475+ var systemId = makeName("systemId");
476+ expect(service.showSpinner(systemId)).toBe(true);
477+ });
478+
479+ it("returns true if set and syncing", function() {
480+ var systemId = makeName("systemId");
481+ var status = "Syncing";
482+ service.statuses[systemId] = status;
483+ expect(service.showSpinner(systemId)).toBe(true);
484+ });
485+ });
486+
487+ describe("getImageStatus", function() {
488+ it("returns status if set", function() {
489+ var systemId = makeName("systemId");
490+ var status = "out-of-sync";
491+ service.statuses[systemId] = status;
492+ expect(service.getImageStatus(systemId)).toBe(status);
493+ });
494+
495+ it("returns asking", function() {
496+ var systemId = makeName("systemId");
497+ expect(service.getImageStatus(systemId)).toBe(
498+ "Asking for status...");
499+ });
500+ });
501+ });
502+
503+ describe("directive", function() {
504+ it("registers when systemId is set", function() {
505+ spyOn(ControllerImageStatusService, "register");
506+ var directive = compileDirective();
507+
508+ // Should only be called once system_id is set.
509+ expect(
510+ ControllerImageStatusService.register).not.toHaveBeenCalled();
511+ $scope.system_id = makeName("systemId");
512+ $scope.$digest();
513+ expect(ControllerImageStatusService.register).toHaveBeenCalledWith(
514+ $scope.system_id);
515+ });
516+
517+ it("unregisters when destroyed", function() {
518+ spyOn(ControllerImageStatusService, "register");
519+ spyOn(ControllerImageStatusService, "unregister");
520+ var directive = compileDirective();
521+
522+ $scope.system_id = makeName("systemId");
523+ $scope.$digest();
524+ expect(ControllerImageStatusService.register).toHaveBeenCalledWith(
525+ $scope.system_id);
526+
527+ directive.scope().$destroy();
528+ expect(
529+ ControllerImageStatusService.unregister).toHaveBeenCalledWith(
530+ $scope.system_id);
531+ });
532+ });
533+});
534
535=== modified file 'src/maasserver/static/js/angular/directives/tests/test_version_reloader.js'
536--- src/maasserver/static/js/angular/directives/tests/test_version_reloader.js 2015-10-21 19:48:04 +0000
537+++ src/maasserver/static/js/angular/directives/tests/test_version_reloader.js 2016-05-25 20:28:15 +0000
538@@ -35,6 +35,11 @@
539 $scope = $rootScope.$new();
540 }));
541
542+ // Prevent console.log messages in tests.
543+ beforeEach(function() {
544+ spyOn(console, "log");
545+ });
546+
547 // Return the compiled directive with the items from the scope.
548 function compileDirective() {
549 var directive;
550
551=== modified file 'src/maasserver/static/js/angular/directives/version_reloader.js'
552--- src/maasserver/static/js/angular/directives/version_reloader.js 2015-10-21 19:48:04 +0000
553+++ src/maasserver/static/js/angular/directives/version_reloader.js 2016-05-25 20:28:15 +0000
554@@ -27,6 +27,9 @@
555 GeneralManager.enableAutoReload(true);
556 $scope.$watch("version.text",
557 function(newValue, oldValue) {
558+ console.log(
559+ "Detected new MAAS version; " +
560+ "forcing reload.");
561 if(newValue !== oldValue) {
562 $scope.reloadPage();
563 }
564
565=== modified file 'src/maasserver/static/js/angular/maas.js'
566--- src/maasserver/static/js/angular/maas.js 2016-04-11 16:23:26 +0000
567+++ src/maasserver/static/js/angular/maas.js 2016-05-25 20:28:15 +0000
568@@ -13,6 +13,12 @@
569 $interpolateProvider.startSymbol('{$');
570 $interpolateProvider.endSymbol('$}');
571
572+ // Helper that wrappers the templateUrl to append the files version
573+ // to the path. Used to override client cache.
574+ function versionedPath(path) {
575+ return path + "?v=" + MAAS_config.files_version;
576+ }
577+
578 // Setup routes only for the index page, all remaining pages should
579 // not use routes. Once all pages are converted to using Angular this
580 // will go away. Causing the page to never have to reload.
581@@ -24,60 +30,73 @@
582 if(path === href) {
583 $routeProvider.
584 when('/nodes', {
585- templateUrl: 'static/partials/nodes-list.html',
586+ templateUrl: versionedPath(
587+ 'static/partials/nodes-list.html'),
588 controller: 'NodesListController'
589 }).
590 when('/node/:system_id/result/:filename', {
591- templateUrl: 'static/partials/node-result.html',
592+ templateUrl: versionedPath(
593+ 'static/partials/node-result.html'),
594 controller: 'NodeResultController'
595 }).
596 when('/node/:system_id/events', {
597- templateUrl: 'static/partials/node-events.html',
598+ templateUrl: versionedPath(
599+ 'static/partials/node-events.html'),
600 controller: 'NodeEventsController'
601 }).
602 when('/node/:type/:system_id', {
603- templateUrl: 'static/partials/node-details.html',
604+ templateUrl: versionedPath(
605+ 'static/partials/node-details.html'),
606 controller: 'NodeDetailsController'
607 }).
608 when('/node/:system_id', {
609- templateUrl: 'static/partials/node-details.html',
610+ templateUrl: versionedPath(
611+ 'static/partials/node-details.html'),
612 controller: 'NodeDetailsController'
613 }).
614 when('/domains', {
615- templateUrl: 'static/partials/domains-list.html',
616+ templateUrl: versionedPath(
617+ 'static/partials/domains-list.html'),
618 controller: 'DomainsListController'
619 }).
620 when('/domain/:domain_id', {
621- templateUrl: 'static/partials/domain-details.html',
622+ templateUrl: versionedPath(
623+ 'static/partials/domain-details.html'),
624 controller: 'DomainDetailsController'
625 }).
626 when('/space/:space_id', {
627- templateUrl: 'static/partials/space-details.html',
628+ templateUrl: versionedPath(
629+ 'static/partials/space-details.html'),
630 controller: 'SpaceDetailsController'
631 }).
632 when('/fabric/:fabric_id', {
633- templateUrl: 'static/partials/fabric-details.html',
634+ templateUrl: versionedPath(
635+ 'static/partials/fabric-details.html'),
636 controller: 'FabricDetailsController'
637 }).
638 when('/subnets', {
639 redirectTo: '/networks?by=fabric'
640 }).
641 when('/networks', {
642- templateUrl: 'static/partials/networks-list.html',
643+ templateUrl: versionedPath(
644+ 'static/partials/networks-list.html'),
645 controller: 'NetworksListController',
646 reloadOnSearch: false
647 }).
648 when('/subnet/:subnet_id', {
649- templateUrl: 'static/partials/subnet-details.html',
650+ templateUrl: versionedPath(
651+ 'static/partials/subnet-details.html'),
652 controller: 'SubnetDetailsController'
653 }).
654 when('/vlan/:vlan_id', {
655- templateUrl: 'static/partials/vlan-details.html',
656+ templateUrl: versionedPath(
657+ 'static/partials/vlan-details.html'),
658 controller: 'VLANDetailsController',
659 controllerAs: 'vlanDetails'
660 }).
661 when('/settings/:section', {
662- templateUrl: 'static/partials/settings.html',
663+ templateUrl: versionedPath(
664+ 'static/partials/settings.html'),
665 controller: 'SettingsController'
666 }).
667 otherwise({
668
669=== modified file 'src/maasserver/static/partials/node-details.html'
670--- src/maasserver/static/partials/node-details.html 2016-05-23 14:38:36 +0000
671+++ src/maasserver/static/partials/node-details.html 2016-05-25 20:28:15 +0000
672@@ -224,7 +224,7 @@
673 </dd>
674 </dl>
675 </div>
676- <div data-ng-show="isController">
677+ <div data-ng-if="isController">
678 <dl>
679 <dt class="two-col">Type</dt>
680 <dd class="four-col last-col">
681@@ -236,7 +236,7 @@
682 </dd>
683 <dt class="two-col">Images status</dt>
684 <dd class="four-col last-col">
685- {$ node.image_sync_status || 'Checking...' $}
686+ <maas-controller-image-status system-id="node.system_id"></maas-controller-image-status>
687 </dd>
688 <dt class="two-col">Zone</dt>
689 <dd class="four-col last-col">
690
691=== modified file 'src/maasserver/static/partials/nodes-list.html'
692--- src/maasserver/static/partials/nodes-list.html 2016-05-20 18:49:43 +0000
693+++ src/maasserver/static/partials/nodes-list.html 2016-05-25 20:28:15 +0000
694@@ -810,7 +810,9 @@
695 <td class="table-col--4 table-listing__cell align-center" data-maas-controller-status="controller" data-maas-services="services"></td>
696 <td class="table-col--25 table-listing__cell">{$ controller.node_type_display $}</td>
697 <td class="table-col--15 table-listing__cell">{$ controller.last_image_sync || 'Never' $}</td>
698- <td class="table-col--15 table-listing__cell">{$ controller.image_sync_status || 'Checking...' $}</td>
699+ <td class="table-col--15 table-listing__cell">
700+ <maas-controller-image-status system-id="controller.system_id"></maas-controller-image-status>
701+ </td>
702 </tr>
703 </tbody>
704 </table>
705
706=== modified file 'src/maasserver/templates/maasserver/js-conf.html'
707--- src/maasserver/templates/maasserver/js-conf.html 2015-11-12 16:13:05 +0000
708+++ src/maasserver/templates/maasserver/js-conf.html 2016-05-25 20:28:15 +0000
709@@ -1,16 +1,3 @@
710-<script type="text/javascript"
711- src="{% url "merge" filename="jquery.js" %}?v={{files_version}}">
712-</script>
713-<script type="text/javascript"
714- src="{% url "merge" filename="angular.js" %}?v={{files_version}}">
715-</script>
716-<script type="text/javascript"
717- src="{% url "merge" filename="ng-tags-input.js" %}?v={{files_version}}">
718-</script>
719-<script type="text/javascript"
720- src="{% url "merge" filename="maas-angular.js" %}?v={{files_version}}">
721-</script>
722-
723 <script type="text/javascript">
724 <!--
725 var YUI_config = {
726@@ -30,10 +17,25 @@
727 account_handler: '{% url "account_handler" %}',
728 images_handler: '{% url "images" %}'
729 },
730+ files_version: '{{files_version}}',
731 debug: {% if YUI_DEBUG %}true{% else %}false{% endif %}
732 };
733 // -->
734 </script>
735+
736+<script type="text/javascript"
737+ src="{% url "merge" filename="jquery.js" %}?v={{files_version}}">
738+</script>
739+<script type="text/javascript"
740+ src="{% url "merge" filename="angular.js" %}?v={{files_version}}">
741+</script>
742+<script type="text/javascript"
743+ src="{% url "merge" filename="ng-tags-input.js" %}?v={{files_version}}">
744+</script>
745+<script type="text/javascript"
746+ src="{% url "merge" filename="maas-angular.js" %}?v={{files_version}}">
747+</script>
748+
749 <script type="text/javascript"
750 src="{% url "merge" filename="yui.js" %}?v={{files_version}}">
751 </script>
752
753=== modified file 'src/maasserver/views/combo.py'
754--- src/maasserver/views/combo.py 2016-05-11 19:01:48 +0000
755+++ src/maasserver/views/combo.py 2016-05-25 20:28:15 +0000
756@@ -80,6 +80,7 @@
757 "js/angular/directives/accordion.js",
758 "js/angular/directives/call_to_action.js",
759 "js/angular/directives/code_lines.js",
760+ "js/angular/directives/controller_image_status.js",
761 "js/angular/directives/controller_status.js",
762 "js/angular/directives/dbl_click_overlay.js",
763 "js/angular/directives/enter_blur.js",
764
765=== modified file 'src/maasserver/websockets/handlers/controller.py'
766--- src/maasserver/websockets/handlers/controller.py 2016-04-22 21:25:06 +0000
767+++ src/maasserver/websockets/handlers/controller.py 2016-05-25 20:28:15 +0000
768@@ -98,5 +98,5 @@
769 # We use a RackController method; without the cast, it's a Node.
770 node = typecast_to_node_type(node)
771 if isinstance(node, RackController):
772- result[node.system_id] = node.get_image_sync_status()
773+ result[node.system_id] = node.get_image_sync_status().title()
774 return result
775
776=== modified file 'src/maasserver/websockets/handlers/tests/test_controller.py'
777--- src/maasserver/websockets/handlers/tests/test_controller.py 2016-05-16 08:36:33 +0000
778+++ src/maasserver/websockets/handlers/tests/test_controller.py 2016-05-25 20:28:15 +0000
779@@ -82,5 +82,5 @@
780 {"system_id": node1.system_id},
781 {"system_id": node2.system_id}])
782 self.assertEqual({
783- node1.system_id: "unknown",
784- node2.system_id: "unknown"}, data)
785+ node1.system_id: "Unknown",
786+ node2.system_id: "Unknown"}, data)