Merge lp:~lamont/maas/bug-1660188b into lp:~maas-committers/maas/trunk

Proposed by LaMont Jones
Status: Merged
Approved by: LaMont Jones
Approved revision: no longer in the source branch.
Merged at revision: 5770
Proposed branch: lp:~lamont/maas/bug-1660188b
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~lamont/maas/bug-1660188a
Diff against target: 968 lines (+347/-160)
7 files modified
src/maasserver/static/js/angular/controllers/node_details.js (+35/-7)
src/maasserver/static/js/angular/controllers/node_details_networking.js (+135/-47)
src/maasserver/static/js/angular/controllers/tests/test_node_details.js (+5/-3)
src/maasserver/static/js/angular/factories/devices.js (+5/-4)
src/maasserver/static/js/angular/factories/tests/test_devices.js (+1/-1)
src/maasserver/static/partials/node-details.html (+163/-97)
src/maasserver/static/partials/nodes-list.html (+3/-1)
To merge this branch: bzr merge lp:~lamont/maas/bug-1660188b
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+318463@code.launchpad.net

Commit message

Device-details page, with editing.

Description of the change

Device-details page, with editing.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Just some comments on the code. I am pulling it now to test it locally.

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

Couple things I noticed using the branch.

1. The checkbox should be removed on the interfaces table. At the moment you cannot perform and action between two interfaces, so it should be removed because at the moment it does not server a purpose.

2. The add interface MAC address input is miss aligned and does not match how it is show to edit an interface. This should match. Looks like a mark up problem.

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

I also tried to modify an existing interface. It was set to Dynamic, I changed it to External and it failed to save:

TypeError: Cannot read property 'id' of null
    at Scope.$scope.saveInterface (http://localhost:5240/MAAS/combo/maas-angular.js?v=fromsource(+bzr5763):12602:41)
    at $$childScopeClass.$scope.editSave (http://localhost:5240/MAAS/combo/maas-angular.js?v=fromsource(+bzr5763):12675:20)
    at http://localhost:5240/MAAS/combo/angular.js?v=fromsource(+bzr5763):2:121986
    at callback (http://localhost:5240/MAAS/combo/angular.js?v=fromsource(+bzr5763):2:184779)
    at $$childScopeClass.$eval (http://localhost:5240/MAAS/combo/angular.js?v=fromsource(+bzr5763):2:141788)
    at $$childScopeClass.$apply (http://localhost:5240/MAAS/combo/angular.js?v=fromsource(+bzr5763):2:142144)
    at HTMLButtonElement.<anonymous> (http://localhost:5240/MAAS/combo/angular.js?v=fromsource(+bzr5763):2:184895)
    at HTMLButtonElement.dispatch (http://localhost:5240/MAAS/combo/jquery.js?v=fromsource(+bzr5763):3:31988)
    at HTMLButtonElement.elemData.handle (http://localhost:5240/MAAS/combo/jquery.js?v=fromsource(+bzr5763):3:26531)

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

I then changed an interface from Dynamic to Static, selected a Subnet, did not select an IP address at the interface now shows twice, with two different IP addresses.

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

I am no longer getting two static IP addresses, but some of the other issues still remain.

http://imgur.com/a/cSF0J

1. When selecting a Static IP "Unconfigued" subnet is not supported for a device.
2. When adding a new interface the MAC address field is miss aligned.

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

The create interface padding is incorrect:

http://imgur.com/a/0lWlh

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

It also should pick a default subnet in the dropdown, it should not be blank. Same goes for editing an interface as well. When I click "static" it should pick the first subnet in the dropdown.

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

Looks good. The only other thing I see is that IP address is not optional when providing an external IP address. In that case the IP address is always required. The form is validated correctly, its just that the placeholder text for that field needs to be updated.

review: Approve

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 2017-02-28 20:57:13 +0000
3+++ src/maasserver/static/js/angular/controllers/node_details.js 2017-03-02 00:31:00 +0000
4@@ -6,14 +6,22 @@
5
6 angular.module('MAAS').controller('NodeDetailsController', [
7 '$scope', '$rootScope', '$routeParams', '$location', '$interval',
8+ 'DevicesManager',
9 'MachinesManager', 'ControllersManager', 'ZonesManager', 'GeneralManager',
10 'UsersManager', 'TagsManager', 'DomainsManager', 'ManagerHelperService',
11 'ServicesManager', 'ErrorService', 'ValidationService', function(
12- $scope, $rootScope, $routeParams, $location, $interval,
13+ $scope, $rootScope, $routeParams, $location, $interval, DevicesManager,
14 MachinesManager, ControllersManager, ZonesManager, GeneralManager,
15 UsersManager, TagsManager, DomainsManager, ManagerHelperService,
16 ServicesManager, ErrorService, ValidationService) {
17
18+ // Mapping of device.ip_assignment to viewable text.
19+ var DEVICE_IP_ASSIGNMENT = {
20+ external: "External",
21+ dynamic: "Dynamic",
22+ "static": "Static"
23+ };
24+
25 // Set title and page.
26 $rootScope.title = "Loading...";
27 $rootScope.page = "nodes";
28@@ -84,6 +92,11 @@
29 parameters: {}
30 };
31
32+ // Get the display text for device ip assignment type.
33+ $scope.getDeviceIPAssignment = function(ipAssignment) {
34+ return DEVICE_IP_ASSIGNMENT[ipAssignment];
35+ };
36+
37 // Events section.
38 $scope.events = {
39 limit: 10
40@@ -674,9 +687,10 @@
41 $scope.hasInvalidArchitecture = function() {
42 if(angular.isObject($scope.node)) {
43 return (
44- $scope.node.architecture === "" ||
45- $scope.summary.architecture.options.indexOf(
46- $scope.node.architecture) === -1);
47+ !$scope.isDevice && (
48+ $scope.node.architecture === "" ||
49+ $scope.summary.architecture.options.indexOf(
50+ $scope.node.architecture) === -1));
51 } else {
52 return false;
53 }
54@@ -685,9 +699,10 @@
55 // Return true if the current architecture selection is invalid.
56 $scope.invalidArchitecture = function() {
57 return (
58- $scope.summary.architecture.selected === "" ||
59- $scope.summary.architecture.options.indexOf(
60- $scope.summary.architecture.selected) === -1);
61+ !$scope.isDevice && (
62+ $scope.summary.architecture.selected === "" ||
63+ $scope.summary.architecture.options.indexOf(
64+ $scope.summary.architecture.selected) === -1));
65 };
66
67 // Return true if at least a rack controller is connected to the
68@@ -1029,6 +1044,7 @@
69 // Load all the required managers.
70 ManagerHelperService.loadManagers($scope, [
71 MachinesManager,
72+ DevicesManager,
73 ControllersManager,
74 ZonesManager,
75 GeneralManager,
76@@ -1040,11 +1056,19 @@
77 if('controller' === $routeParams.type) {
78 $scope.nodesManager = ControllersManager;
79 $scope.isController = true;
80+ $scope.isDevice = false;
81 $scope.type_name = 'controller';
82 $scope.type_name_title = 'Controller';
83+ }else if('device' === $routeParams.type) {
84+ $scope.nodesManager = DevicesManager;
85+ $scope.isController = false;
86+ $scope.isDevice = true;
87+ $scope.type_name = 'device';
88+ $scope.type_name_title = 'Device';
89 }else{
90 $scope.nodesManager = MachinesManager;
91 $scope.isController = false;
92+ $scope.isDevice = false;
93 $scope.type_name = 'machine';
94 $scope.type_name_title = 'Machine';
95 }
96@@ -1062,6 +1086,10 @@
97 }, function(error) {
98 ErrorService.raiseError(error);
99 });
100+ activeNode = $scope.nodesManager.getActiveItem();
101+ }
102+ if($scope.isDevice) {
103+ $scope.ip_assignment = activeNode.ip_assignment;
104 }
105
106 // Poll for architectures, hwe_kernels, and osinfo the whole
107
108=== modified file 'src/maasserver/static/js/angular/controllers/node_details_networking.js'
109--- src/maasserver/static/js/angular/controllers/node_details_networking.js 2016-10-26 19:05:43 +0000
110+++ src/maasserver/static/js/angular/controllers/node_details_networking.js 2017-03-02 00:31:00 +0000
111@@ -171,6 +171,28 @@
112 EDIT: "edit"
113 };
114
115+ var IP_ASSIGNMENT = {
116+ DYNAMIC: "dynamic",
117+ EXTERNAL: "external",
118+ STATIC: "static"
119+ };
120+
121+ // Device ip assignment options.
122+ $scope.ipAssignments = [
123+ {
124+ name: IP_ASSIGNMENT.EXTERNAL,
125+ text: "External"
126+ },
127+ {
128+ name: IP_ASSIGNMENT.DYNAMIC,
129+ text: "Dynamic"
130+ },
131+ {
132+ name: IP_ASSIGNMENT.STATIC,
133+ text: "Static"
134+ }
135+ ];
136+
137 // Set the initial values for this scope.
138 $scope.loaded = false;
139 $scope.nodeHasLoaded = false;
140@@ -542,6 +564,10 @@
141 // If the user is not the superuser, pretend it's not Ready.
142 return false;
143 }
144+ if ($scope.$parent.isDevice) {
145+ // Devices are never Ready, for our purposes, for now.
146+ return true;
147+ }
148 if ($scope.$parent.isController) {
149 // Controllers are always Ready, for our purposes.
150 return true;
151@@ -563,8 +589,8 @@
152 // If the user is not the superuser, pretend it's not Ready.
153 return false;
154 }
155- if ($scope.$parent.isController) {
156- // Controllers never in limited mode.
157+ if ($scope.$parent.isController || $scope.$parent.isDevice) {
158+ // Controllers and Devices are never in limited mode.
159 return false;
160 }
161 return (
162@@ -581,9 +607,9 @@
163 // If the user is not a superuser, disable the networking panel.
164 return true;
165 }
166- if ($scope.$parent.isController) {
167+ if ($scope.$parent.isController || $scope.$parent.isDevice) {
168 // Never disable the full networking panel when its a
169- // controller.
170+ // Controller or Device.
171 return false;
172 }
173 if (angular.isObject($scope.node) &&
174@@ -772,18 +798,29 @@
175 $scope.edit = function(nic) {
176 $scope.selectedInterfaces = [$scope.getUniqueKey(nic)];
177 $scope.selectedMode = SELECTION_MODE.EDIT;
178- $scope.editInterface = {
179- id: nic.id,
180- name: nic.name,
181- mac_address: nic.mac_address,
182- tags: angular.copy(nic.tags),
183- fabric: nic.fabric,
184- vlan: nic.vlan,
185- subnet: nic.subnet,
186- mode: nic.mode,
187- ip_address: nic.ip_address,
188- link_id: nic.link_id
189- };
190+ if($scope.$parent.isDevice) {
191+ $scope.editInterface = {
192+ id: nic.id,
193+ name: nic.name,
194+ mac_address: nic.mac_address,
195+ subnet: nic.subnet,
196+ ip_address: nic.ip_address,
197+ ip_assignment: nic.ip_assignment
198+ };
199+ } else {
200+ $scope.editInterface = {
201+ id: nic.id,
202+ name: nic.name,
203+ mac_address: nic.mac_address,
204+ tags: angular.copy(nic.tags),
205+ fabric: nic.fabric,
206+ vlan: nic.vlan,
207+ subnet: nic.subnet,
208+ mode: nic.mode,
209+ ip_address: nic.ip_address,
210+ link_id: nic.link_id
211+ };
212+ }
213 };
214
215 // Called when the fabric is changed.
216@@ -837,13 +874,24 @@
217
218 // Save the following interface on the node.
219 $scope.saveInterface = function(nic) {
220- var params = {
221- "name": nic.name,
222- "mac_address": nic.mac_address,
223- "tags": nic.tags.map(
224- function(tag) { return tag.text; })
225- };
226- if(nic.vlan !== null) {
227+ var params;
228+ if($scope.$parent.isDevice) {
229+ params = {
230+ "name": nic.name,
231+ "mac_address": nic.mac_address,
232+ "ip_assignment": nic.ip_assignment,
233+ "ip_address": nic.ip_address,
234+ "subnet": nic.subnet.id
235+ };
236+ } else {
237+ params = {
238+ "name": nic.name,
239+ "mac_address": nic.mac_address,
240+ "tags": nic.tags.map(
241+ function(tag) { return tag.text; })
242+ };
243+ }
244+ if(nic.vlan !== undefined && nic.vlan !== null) {
245 params.vlan = nic.vlan.id;
246 } else {
247 params.vlan = null;
248@@ -866,6 +914,9 @@
249 var params = {
250 "mode": nic.mode
251 };
252+ if($scope.$parent.isDevice) {
253+ params.ip_assignment = nic.ip_assignment;
254+ }
255 if(angular.isObject(nic.subnet)) {
256 params.subnet = nic.subnet.id;
257 }
258@@ -903,11 +954,17 @@
259
260 // Make a copy so a change after clicking save is not saved.
261 var nic = angular.copy($scope.editInterface);
262- $scope.saveInterface(nic).then(function() {
263- $scope.saveInterfaceLink(nic).then(function() {
264+ if($scope.$parent.isDevice) {
265+ $scope.saveInterface(nic).then(function() {
266 $scope.editCancel();
267 });
268- });
269+ } else {
270+ $scope.saveInterface(nic).then(function() {
271+ $scope.saveInterfaceLink(nic).then(function() {
272+ $scope.editCancel();
273+ });
274+ });
275+ }
276 };
277
278 // Return true if the interface delete confirm is being shown.
279@@ -1110,9 +1167,18 @@
280
281 // Perform the add action over the websocket.
282 $scope.addInterface = function(type) {
283- if($scope.newInterface.type === INTERFACE_TYPE.ALIAS) {
284+ var nic;
285+ if($scope.$parent.isDevice) {
286+ nic = {
287+ id: $scope.newInterface.parent.id,
288+ ip_assignment: $scope.newInterface.ip_assignment,
289+ subnet: $scope.newInterface.subnet,
290+ ip_address: $scope.newInterface.ip_address
291+ };
292+ $scope.saveInterfaceLink(nic);
293+ } else if($scope.newInterface.type === INTERFACE_TYPE.ALIAS) {
294 // Add a link to the current interface.
295- var nic = {
296+ nic = {
297 id: $scope.newInterface.parent.id,
298 mode: $scope.newInterface.mode,
299 subnet: $scope.newInterface.subnet,
300@@ -1416,17 +1482,29 @@
301 $scope.showCreatePhysical = function() {
302 if($scope.selectedMode === SELECTION_MODE.NONE) {
303 $scope.selectedMode = SELECTION_MODE.CREATE_PHYSICAL;
304- $scope.newInterface = {
305- name: getNextName("eth"),
306- macAddress: "",
307- macError: false,
308- tags: [],
309- errorMsg: null,
310- fabric: $scope.fabrics[0],
311- vlan: getDefaultVLAN($scope.fabrics[0]),
312- subnet: null,
313- mode: LINK_MODE.LINK_UP
314- };
315+ if($scope.$parent.isDevice) {
316+ $scope.newInterface = {
317+ name: getNextName("eth"),
318+ macAddress: "",
319+ macError: false,
320+ tags: [],
321+ errorMsg: null,
322+ subnet: null,
323+ ip_assignment: IP_ASSIGNMENT.DYNAMIC
324+ };
325+ } else {
326+ $scope.newInterface = {
327+ name: getNextName("eth"),
328+ macAddress: "",
329+ macError: false,
330+ tags: [],
331+ errorMsg: null,
332+ fabric: $scope.fabrics[0],
333+ vlan: getDefaultVLAN($scope.fabrics[0]),
334+ subnet: null,
335+ mode: LINK_MODE.LINK_UP
336+ };
337+ }
338 }
339 };
340
341@@ -1444,14 +1522,24 @@
342 return;
343 }
344
345- var params = {
346- name: $scope.newInterface.name,
347- tags: $scope.newInterface.tags.map(
348- function(tag) { return tag.text; }),
349- mac_address: $scope.newInterface.macAddress,
350- vlan: $scope.newInterface.vlan.id,
351- mode: $scope.newInterface.mode
352- };
353+ var params;
354+ if($scope.$parent.isDevice) {
355+ params = {
356+ name: $scope.newInterface.name,
357+ mac_address: $scope.newInterface.macAddress,
358+ ip_assignment: $scope.newInterface.ip_assignment,
359+ ip_address: $scope.newInterface.ip_address
360+ };
361+ } else {
362+ params = {
363+ name: $scope.newInterface.name,
364+ tags: $scope.newInterface.tags.map(
365+ function(tag) { return tag.text; }),
366+ mac_address: $scope.newInterface.macAddress,
367+ vlan: $scope.newInterface.vlan.id,
368+ mode: $scope.newInterface.mode
369+ };
370+ }
371 if(angular.isObject($scope.newInterface.subnet)) {
372 params.subnet = $scope.newInterface.subnet.id;
373 }
374
375=== modified file 'src/maasserver/static/js/angular/controllers/tests/test_node_details.js'
376--- src/maasserver/static/js/angular/controllers/tests/test_node_details.js 2017-02-28 20:57:13 +0000
377+++ src/maasserver/static/js/angular/controllers/tests/test_node_details.js 2017-03-02 00:31:00 +0000
378@@ -42,6 +42,7 @@
379 var webSocket;
380 beforeEach(inject(function($injector) {
381 MachinesManager = $injector.get("MachinesManager");
382+ DevicesManager = $injector.get("DevicesManager");
383 ControllersManager = $injector.get("ControllersManager");
384 ZonesManager = $injector.get("ZonesManager");
385 GeneralManager = $injector.get("GeneralManager");
386@@ -137,6 +138,7 @@
387 $routeParams: $routeParams,
388 $location: $location,
389 MachinesManager: MachinesManager,
390+ DevicesManager: DevicesManager,
391 ControllersManager: ControllersManager,
392 ZonesManager: ZonesManager,
393 GeneralManager: GeneralManager,
394@@ -261,9 +263,9 @@
395 var controller = makeController();
396 expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith(
397 $scope, [
398- MachinesManager, ControllersManager, ZonesManager,
399- GeneralManager, UsersManager, TagsManager, DomainsManager,
400- ServicesManager]);
401+ MachinesManager, DevicesManager, ControllersManager,
402+ ZonesManager, GeneralManager, UsersManager, TagsManager,
403+ DomainsManager, ServicesManager]);
404 });
405
406 it("doesnt call setActiveItem if node is loaded", function() {
407
408=== modified file 'src/maasserver/static/js/angular/factories/devices.js'
409--- src/maasserver/static/js/angular/factories/devices.js 2016-09-27 00:47:54 +0000
410+++ src/maasserver/static/js/angular/factories/devices.js 2017-03-02 00:31:00 +0000
411@@ -11,16 +11,17 @@
412
413 angular.module('MAAS').factory(
414 'DevicesManager',
415- ['$q', '$rootScope', 'RegionConnection', 'Manager', function(
416- $q, $rootScope, RegionConnection, Manager) {
417+ ['$q', '$rootScope', 'RegionConnection', 'NodesManager', function(
418+ $q, $rootScope, RegionConnection, NodesManager) {
419
420 function DevicesManager() {
421- Manager.call(this);
422+ NodesManager.call(this);
423
424 this._pk = "system_id";
425 this._handler = "device";
426 this._metadataAttributes = {
427 "owner": null,
428+ "subnets": null,
429 "tags": null,
430 "zone": function(device) {
431 return device.zone.name;
432@@ -34,7 +35,7 @@
433 });
434 }
435
436- DevicesManager.prototype = new Manager();
437+ DevicesManager.prototype = new NodesManager();
438
439 // Create a device.
440 DevicesManager.prototype.create = function(node) {
441
442=== modified file 'src/maasserver/static/js/angular/factories/tests/test_devices.js'
443--- src/maasserver/static/js/angular/factories/tests/test_devices.js 2016-09-27 14:24:19 +0000
444+++ src/maasserver/static/js/angular/factories/tests/test_devices.js 2017-03-02 00:31:00 +0000
445@@ -46,7 +46,7 @@
446 expect(DevicesManager._pk).toBe("system_id");
447 expect(DevicesManager._handler).toBe("device");
448 expect(Object.keys(DevicesManager._metadataAttributes)).toEqual(
449- ["owner", "tags", "zone"]);
450+ ["owner", "subnets", "tags", "zone"]);
451 });
452
453 describe("createInferface", function() {
454
455=== modified file 'src/maasserver/static/partials/node-details.html'
456--- src/maasserver/static/partials/node-details.html 2017-03-01 07:12:26 +0000
457+++ src/maasserver/static/partials/node-details.html 2017-03-02 00:31:00 +0000
458@@ -30,7 +30,7 @@
459 data-ng-click="saveEditHeader()">Save</a>
460
461 <!-- XXX ricgard 2016-06-16 - Need to add e2e test. -->
462- <p class="page-header__status" data-ng-hide="isController || header.editing">
463+ <p class="page-header__status" data-ng-if="!isController && !isDevice && !header.editing">
464 {$ node.status $}
465 <span class="u-text--{$ getPowerStateClass() $} u-margin--left-small"><i class="icon icon--power-{$ getPowerStateClass() $} u-margin--right-tiny"></i>{$ getPowerStateText() $}</span>
466 <a href="" class="page-header__status-check" data-ng-show="canCheckPowerState()" data-ng-click="checkPowerState()">check now</a>
467@@ -47,7 +47,7 @@
468 <!-- XXX blake_r 2015-02-19 - Need to add e2e test. -->
469 <div class="page-header__dropdown" data-ng-class="{ 'is-open': actionOption }">
470
471- <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-show="!node.dhcp_on">
472+ <div class="page-header__section twelve-col u-margin--bottom-none ng-hide" data-ng-if="!isDevice && !node.dhcp_on">
473 <p class="page-header__message page-header__message--warning">
474 MAAS is not providing DHCP.
475 </p>
476@@ -89,58 +89,20 @@
477 <div class="page-header__controls">
478 <a href="" class="button--base button--inline" data-ng-click="actionCancel()">Cancel</a>
479 <button class="button--inline" data-ng-class="actionOption.name === 'delete' ? 'button--destructive' : 'button--positive'" data-ng-click="actionGo('nodes')" data-ng-hide="hasActionsFailed('nodes')">
480- <span data-ng-if="actionOption.name === 'commission'">Commission
481- <span data-ng-if="!isController">machine</span>
482- <span data-ng-if="isController">controller</span>
483- </span>
484- <span data-ng-if="actionOption.name === 'acquire'">Acquire
485- <span data-ng-if="!isController">machine</span>
486- <span data-ng-if="isController">controller</span>
487- </span>
488- <span data-ng-if="actionOption.name === 'deploy'">Deploy
489- <span data-ng-if="!isController">machine</span>
490- <span data-ng-if="isController">controller</span>
491- </span>
492- <span data-ng-if="actionOption.name === 'release'">Release
493- <span data-ng-if="!isController">machine</span>
494- <span data-ng-if="isController">controller</span>
495- </span>
496- <span data-ng-if="actionOption.name === 'set-zone'">Set zone for
497- <span data-ng-if="!isController">machine</span>
498- <span data-ng-if="isController">controller</span>
499- </span>
500- <span data-ng-if="actionOption.name === 'on'">Power on
501- <span data-ng-if="!isController">machine</span>
502- <span data-ng-if="isController">controller</span>
503- </span>
504- <span data-ng-if="actionOption.name === 'off'">Power off
505- <span data-ng-if="!isController">machine</span>
506- <span data-ng-if="isController">controller</span>
507- </span>
508- <span data-ng-if="actionOption.name === 'abort'">Abort action on
509- <span data-ng-if="!isController">machine</span>
510- <span data-ng-if="isController">controller</span>
511- </span>
512- <span data-ng-if="actionOption.name === 'rescue-mode'">Rescue
513- <span data-ng-if="!isController">machine</span>
514- <span data-ng-if="isController">controller</span>
515- </span>
516- <span data-ng-if="actionOption.name === 'exit-rescue-mode'">Exit rescue mode
517- </span>
518- <span data-ng-if="actionOption.name === 'mark-broken'">Mark
519- <span data-ng-if="!isController">machine</span>
520- <span data-ng-if="isController">controller</span> as broken
521- </span>
522- <span data-ng-if="actionOption.name === 'mark-fixed'">Mark
523- <span data-ng-if="!isController">machine</span>
524- <span data-ng-if="isController">controller</span> as fixed
525- </span>
526- <span data-ng-if="actionOption.name === 'delete'">Delete
527- <span data-ng-if="!isController">machine</span>
528- <span data-ng-if="isController">controller</span>
529- </span>
530- <span data-ng-if="actionOption.name === 'import-images'">Import images
531- </span>
532+ <span data-ng-if="actionOption.name === 'commission'">Commission {$ type_name $}</span>
533+ <span data-ng-if="actionOption.name === 'acquire'">Acquire {$ type_name $}</span>
534+ <span data-ng-if="actionOption.name === 'deploy'">Deploy {$ type_name $}</span>
535+ <span data-ng-if="actionOption.name === 'release'">Release {$ type_name $}</span>
536+ <span data-ng-if="actionOption.name === 'set-zone'">Set zone for {$ type_name $}</span>
537+ <span data-ng-if="actionOption.name === 'on'">Power on {$ type_name $}</span>
538+ <span data-ng-if="actionOption.name === 'off'">Power off {$ type_name $}</span>
539+ <span data-ng-if="actionOption.name === 'abort'">Abort action on {$ type_name $}</span>
540+ <span data-ng-if="actionOption.name === 'rescue-mode'">Rescue {$ type_name $}</span>
541+ <span data-ng-if="actionOption.name === 'exit-rescue-mode'">Exit rescue mode</span>
542+ <span data-ng-if="actionOption.name === 'mark-broken'">Mark {$ type_name $}</span>
543+ <span data-ng-if="actionOption.name === 'mark-fixed'">Mark {$ type_name $}</span>
544+ <span data-ng-if="actionOption.name === 'delete'">Delete {$ type_name $}</span>
545+ <span data-ng-if="actionOption.name === 'import-images'">Import images</span>
546 </button>
547 </div>
548 </form>
549@@ -201,12 +163,13 @@
550 </nav> -->
551 <div class="row" id="summary">
552 <div class="wrapper--inner">
553- <h2 data-ng-hide="isController" class="eight-col">Machine summary</h2>
554- <h2 data-ng-show="isController" class="eight-col">Controller summary</h2>
555- <div data-ng-hide="isController">
556+ <h2 data-ng-if="!isController && !isDevice" class="eight-col">Machine summary</h2>
557+ <h2 data-ng-if="isController" class="eight-col">Controller summary</h2>
558+ <h2 data-ng-if="isDevice" class="eight-col">Device summary</h2>
559+ <div data-ng-if="!isController">
560 <div class="four-col last-col u-align--right" data-ng-hide="summary.editing">
561 <a href="" class="button--secondary button--inline"
562- data-ng-show="canEdit()"
563+ data-ng-if="canEdit()"
564 data-ng-click="editSummary()">Edit</a>
565 </div>
566 <div class="four-col last-col u-align--right ng-hide" data-ng-show="summary.editing">
567@@ -219,17 +182,17 @@
568 </div>
569 <div class="twelve-col" data-ng-if="isSuperUser()">
570 <div class="p-notification--error"
571- data-ng-if="!isController && !isRackControllerConnected()">
572+ data-ng-if="!isController && !isDevice && !isRackControllerConnected()">
573 <p class="p-notification__response">
574 <span class="p-notification__status">Error:</span>Editing is currently disabled because no rack controller is currently connected to the region.</p>
575 </div>
576 <div class="p-notification--error"
577- data-ng-if="!isController && hasInvalidArchitecture() && isRackControllerConnected() && hasUsableArchitectures()">
578+ data-ng-if="!isController && !isDevice && hasInvalidArchitecture() && isRackControllerConnected() && hasUsableArchitectures()">
579 <p class="p-notification__response">
580 <span class="p-notification__status">Error:</span>This machine currently has an invalid architecture. Update the architecture of this machine to make it deployable.</p>
581 </div>
582 <div class="p-notification--error"
583- data-ng-if="!isController && hasInvalidArchitecture() && isRackControllerConnected() && !hasUsableArchitectures()">
584+ data-ng-if="!isController && !isDevice && hasInvalidArchitecture() && isRackControllerConnected() && !hasUsableArchitectures()">
585 <p class="p-notification__response">
586 <span class="p-notification__status">Error:</span>No boot images have been imported for a valid architecture to be selected. Visit the <a href="images">images page</a> to start the import process.</p>
587 </div>
588@@ -247,7 +210,7 @@
589 </select>
590 </div>
591 </div>
592- <div class="form__group">
593+ <div class="form__group" data-ng-if="!isDevice">
594 <label for="architecture" class="form__group-label two-col">Architecture</label>
595 <div class="form__group-input three-col">
596 <select name="architecture" id="architecture"
597@@ -259,7 +222,7 @@
598 </select>
599 </div>
600 </div>
601- <div class="form__group">
602+ <div class="form__group" data-ng-if="!isDevice">
603 <label for="min_hwe_kernel" class="form__group-label two-col">Minimum Kernel</label>
604 <div class="form__group-input three-col">
605 <select name="min_hwe_kernel" id="min_hwe_kernel" placeholder="No minimum kernel"
606@@ -293,7 +256,7 @@
607 <dd class="four-col last-col">
608 {$ node.zone.name $}
609 </dd>
610- <dt class="two-col">Architecture</dt>
611+ <dt class="two-col" data-ng-if="!isDevice">Architecture</dt>
612 <dd class="four-col last-col">
613 {$ node.architecture || "Missing" $}
614 </dd>
615@@ -336,7 +299,7 @@
616 </dd>
617 </dl>
618 </div>
619- <div class="six-col last-col">
620+ <div class="six-col last-col" data-ng-if="!isDevice">
621 <dl>
622 <dt class="two-col">CPU</dt>
623 <dd class="four-col last-col">
624@@ -388,7 +351,7 @@
625 </div>
626 </div>
627 </div>
628- <div class="row" id="power" data-ng-show="!isController && isSuperUser()">
629+ <div class="row" id="power" data-ng-if="!isController && !isDevice && isSuperUser()">
630 <div class="wrapper--inner">
631 <h2 class="eight-col">Power</h2>
632 <div class="four-col last-col u-align--right" data-ng-hide="power.editing">
633@@ -613,8 +576,8 @@
634 <h2 class="title">Interfaces</h2>
635 </div>
636 <div class="twelve-col" data-ng-if="
637- (!isController && isAllNetworkingDisabled() && isSuperUser()) ||
638- !node.on_network || (!isController && !isUbuntuOS())">
639+ !isDevice && ((!isController && isAllNetworkingDisabled() && isSuperUser()) ||
640+ !node.on_network || (!isController && !isUbuntuOS()))">
641 <div class="p-notification--error" data-ng-if="!node.on_network">
642 <p class="p-notification__response">
643 <span class="p-notification__status">Error:</span>Node must be connected to a network.</p>
644@@ -633,8 +596,9 @@
645 <div class="table table--hover u-margin--bottom">
646 <header class="table__head">
647 <div class="table__row">
648- <div class="table__header table-col--3"></div>
649+ <div class="table__header table-col--3" data-ng-if="!isDevice"></div>
650 <div class="table__header table-col--12">
651+ <span data-ng-if="!isDevice">
652 <a class="table__header-link is-active"
653 data-ng-class="{ 'is-active': column == 'name' }"
654 data-ng-click="column = 'name'">
655@@ -645,11 +609,16 @@
656 data-ng-click="column = 'mac'">
657 MAC
658 </a>
659- </div>
660- <div class="table__header table-col--3"><span data-ng-if="!isController">PXE</span></div>
661- <div class="table__header table-col--9">Type</div>
662- <div class="table__header table-col--14">Fabric</div>
663- <div class="table__header table-col--14">VLAN</div>
664+ </span>
665+ <span data-ng-if="isDevice">MAC</span>
666+ </div>
667+ <div class="table__header table-col--3"><span data-ng-if="!isController && !isDevice">PXE</span></div>
668+ <div class="table__header table-col--9"><span data-ng-if="!isDevice">Type</span></div>
669+ <div class="table__header table-col--14">
670+ <span data-ng-if="!isDevice">Fabric</span>
671+ <span data-ng-if="isDevice">IP Assignment</span>
672+ </div>
673+ <div class="table__header table-col--14"><span data-ng-if="!isDevice">VLAN</span></div>
674 <div class="table__header table-col--18">Subnet</div>
675 <div class="table__header table-col--27">IP Address</div>
676 </div>
677@@ -658,7 +627,7 @@
678 <div class="table__row"
679 data-ng-class="{ disabled: isDisabled(), 'is-active': isInterfaceSelected(interface) && (isNodeEditingAllowed() || isLimitedEditingAllowed(interface)), noEdit: cannotEditInterface(interface) }"
680 data-ng-repeat="interface in interfaces | removeInterfaceParents:newBondInterface:!isNodeEditingAllowed() | removeInterfaceParents:newBridgeInterface:!isNodeEditingAllowed()">
681- <div class="table__data table-col--3" aria-label="Select">
682+ <div class="table__data table-col--3" data-ng-if="!isDevice" aria-label="Select">
683 <input type="checkbox" class="checkbox" id="{$ getUniqueKey(interface) $}"
684 data-ng-hide="isAllNetworkingDisabled()"
685 data-ng-checked="isInterfaceSelected(interface)"
686@@ -667,43 +636,48 @@
687 data-ng-if="!isController && isNodeEditingAllowed()">
688 <label for="{$ getUniqueKey(interface) $}"></label>
689 </div>
690- <div class="table__data table-col--12" aria-label="Name" data-ng-show="column == 'name'">
691+ <div class="table__data table-col--12" aria-label="Name" data-ng-if="!isDevice" data-ng-show="column == 'name'">
692 <span data-ng-if="!isEditing(interface)">{$ interface.name $}</span>
693 <input type="text" class="table__input"
694 data-ng-if="isEditing(interface) && interface.type != 'vlan'"
695 data-ng-model="editInterface.name"
696 data-ng-class="{ 'has-error': isInterfaceNameInvalid(editInterface) }">
697 </div>
698- <div class="table__data table-col--12 ng-hide" data-ng-show="column == 'mac'">
699+ <div class="table__data table-col--12 ng-hide" data-ng-if="!isDevice" data-ng-show="column == 'mac'">
700 {$ interface.mac_address $}
701 </div>
702+ <div class="table__data table-col--12" data-ng-if="isDevice">{$ interface.mac_address $}</div>
703 <div class="table__data table-col--3 u-align--center" aria-label="Boot interface">
704 <input type="radio" name="bootInterface" id="{$ interface.name $}" checked
705- data-ng-if="!isController && isBootInterface(interface)" class="u-display--desktop">
706+ data-ng-if="!isController && !isDevice && isBootInterface(interface)" class="u-display--desktop">
707 <label for="{$ interface.name $}" class="u-display--desktop"></label>
708- <span class="u-display--mobile" data-ng-if="!isController && isBootInterface(interface)">Yes</span>
709- <span class="u-display--mobile" data-ng-if="!isController && !isBootInterface(interface)">No</span>
710+ <span class="u-display--mobile" data-ng-if="!isController && !isDevice && isBootInterface(interface)">Yes</span>
711+ <span class="u-display--mobile" data-ng-if="!isController && !isDevice && !isBootInterface(interface)">No</span>
712 </div>
713 <div class="table__data table-col--9" aria-label="Type">
714- <span data-ng-if="!isEditing(interface)">{$ getInterfaceTypeText(interface) $}</span>
715+ <span data-ng-if="!isDevice && !isEditing(interface)">{$ getInterfaceTypeText(interface) $}</span>
716 </div>
717 <div class="table__data table-col--14" aria-label="Fabric">
718- <span data-ng-if="!isEditing(interface)">{$ interface.fabric.name || "Disconnected" $}</span>
719+ <span data-ng-if="!isDevice && !isEditing(interface)">{$ interface.fabric.name || "Disconnected" $}</span>
720+ <span data-ng-if="isDevice && !isEditing(interface)">{$ getDeviceIPAssignment(interface.ip_assignment) $}</span>
721 </div>
722 <div class="table__data table-col--14" aria-label="VLAN">
723- <span data-ng-if="!isEditing(interface)">{$ getVLANText(interface.vlan) $}</span>
724+ <span data-ng-if="!isDevice && !isEditing(interface)">{$ getVLANText(interface.vlan) $}</span>
725 </div>
726 <div class="table__data table-col--18" aria-label="Subnet">
727- <span data-ng-if="!isEditing(interface) && interface.fabric">{$ getSubnetText(interface.subnet) $}</span>
728+ <span data-ng-if="!isEditing(interface) && ((isDevice && interface.subnet) || interface.fabric)">{$ getSubnetText(interface.subnet) $}</span>
729 <span data-ng-if="isAllNetworkingDisabled() && interface.discovered[0].subnet_id">
730 {$ getSubnetText(getSubnet(interface.discovered[0].subnet_id)) $}
731 </span>
732 </div>
733 <div class="table__data table-col--19" aria-label="IP Address">
734- <span data-ng-if="!isEditing(interface) && !interface.discovered[0].ip_address && interface.fabric">
735+ <span data-ng-if="isDevice">
736+ {$ interface.ip_address $}
737+ </span>
738+ <span data-ng-if="!isDevice && !isEditing(interface) && !interface.discovered[0].ip_address && interface.fabric" >
739 {$ interface.ip_address $} ({$ getLinkModeText(interface) $})
740 </span>
741- <span data-ng-if="!isEditing(interface) && interface.discovered[0].ip_address && interface.fabric">
742+ <span data-ng-if="!isDevice && !isEditing(interface) && interface.discovered[0].ip_address && interface.fabric">
743 {$ interface.discovered[0].ip_address $} (DHCP)
744 </span>
745 </div>
746@@ -711,7 +685,7 @@
747 <div class="table__controls u-align--right" data-ng-if="isNodeEditingAllowed() || isLimitedEditingAllowed(interface)">
748 <a class="u-display--desktop icon icon--add tooltip"
749 aria-label="Add Alias or VLAN"
750- data-ng-if="canAddAliasOrVLAN(interface)"
751+ data-ng-if="!isDevice && canAddAliasOrVLAN(interface)"
752 data-ng-click="quickAdd(interface)"></a>
753 <a class="u-display--desktop icon icon--edit tooltip"
754 data-ng-if="!cannotEditInterface(interface)"
755@@ -723,7 +697,7 @@
756 data-ng-click="quickRemove(interface)"></a>
757 <a class="button--secondary u-display--mobile"
758 aria-label="Add Alias or VLAN"
759- data-ng-if="canAddAliasOrVLAN(interface)"
760+ data-ng-if="!isDevice && canAddAliasOrVLAN(interface)"
761 data-ng-click="quickAdd(interface)">Add alias or VLAN</a>
762 <a class="button--secondary u-display--mobile"
763 data-ng-if="!cannotEditInterface(interface)"
764@@ -837,8 +811,10 @@
765 <div class="table__data table-col--100" data-ng-if="isEditing(interface)">
766 <fieldset class="form__fieldset six-col">
767 <dl>
768+ <span data-ng-if="!isDevice">
769 <dt class="two-col">Type</dt>
770 <dd class="four-col last-col">{$ getInterfaceTypeText(interface) $}</dd>
771+ </span>
772 </dl>
773 <div class="form__group" data-ng-if="interface.type != 'alias' && interface.type != 'vlan'">
774 <label for="mac" class="two-col">MAC address</label>
775@@ -847,14 +823,14 @@
776 data-ng-model="editInterface.mac_address"
777 data-ng-class="{ 'has-error': isMACAddressInvalid(editInterface.mac_address, true) }">
778 </div>
779- <div class="form__group" data-ng-if="!isLimitedEditingAllowed(interface)">
780+ <div class="form__group" data-ng-if="!isDevice && !isLimitedEditingAllowed(interface)">
781 <label class="two-col" data-ng-if="interface.type != 'alias'">Tags</label>
782 <div class="form__group-input three-col last-col" data-ng-if="interface.type != 'alias'">
783 <tags-input ng-model="editInterface.tags" allow-tags-pattern="[\w-]+"></tags-input>
784 </div>
785 </div>
786 </fieldset>
787- <fieldset class="form__fieldset six-col last-col" data-ng-if="!isLimitedEditingAllowed(interface)">
788+ <fieldset class="form__fieldset six-col last-col" data-ng-if="!isDevice && !isLimitedEditingAllowed(interface)">
789 <div class="form__group">
790 <label for="fabric" class="two-col">Fabric</label>
791 <select name="fabric" class="three-col"
792@@ -904,6 +880,42 @@
793 </div>
794 </div>
795 </fieldset>
796+ <fieldset class="form__fieldset six-col last-col" data-ng-if="isDevice && !isLimitedEditingAllowed(interface)">
797+ <div class="form__group">
798+ <label for="ip-assignment" class="two-col">IP Assignment</label>
799+ <select name="ip-assignment" class="three-col"
800+ ng-model="editInterface.ip_assignment"
801+ data-ng-options="assignment.name as assignment.text for assignment in ipAssignments">
802+ </select>
803+ </div>
804+ <div class="form__group" data-ng-if="editInterface.ip_assignment === 'static'">
805+ <label for="subnet" class="two-col">Subnet</label>
806+ <select name="subnet" class="three-col"
807+ ng-init="editInterface.subnet = subnets[0]"
808+ ng-model="editInterface.subnet"
809+ data-ng-change="subnetChanged(newInterface)"
810+ data-ng-options="subnet as getSubnetText(subnet) for subnet in subnets">
811+ </select>
812+ </div>
813+ <div class="form__group" data-ng-if="editInterface.ip_assignment !== 'dynamic'">
814+ <label for="ip-address" class="two-col">IP address</label>
815+ <div class="form__group-input three-col last-col">
816+ <input name="ip-address" type="text"
817+ data-ng-if="editInterface.ip_assignment === 'static'"
818+ placeholder="IP address (optional)"
819+ ng-model="editInterface.ip_address"
820+ data-ng-class="{ 'has-error': isIPAddressInvalid(editInterface) }">
821+ <input name="ip-address" type="text"
822+ data-ng-if="editInterface.ip_assignment !== 'static'"
823+ placeholder="IP address"
824+ ng-model="editInterface.ip_address"
825+ data-ng-class="{ 'has-error': isIPAddressInvalid(editInterface) }">
826+ <ul class="errors u-margin--bottom-none form__group--errors" data-ng-if="getInterfaceError(editInterface)">
827+ <li>{$ getInterfaceError(editInterface) $}</li>
828+ </ul>
829+ </div>
830+ </div>
831+ </fieldset>
832 </div>
833 </div>
834 <div class="table__row is-active" data-ng-if="isShowingDeleteConfirm()">
835@@ -1120,11 +1132,11 @@
836 </div>
837 </div>
838 <div class="table__row is-active" data-ng-show="isShowingCreatePhysical()">
839- <div class="table__data table-col--3">
840+ <div class="table__data table-col--3" data-ng-if="!isDevice">
841 <input type="checkbox" class="checkbox" id="interface-create" disabled="disabled" checked />
842 <label for="interface-create"></label>
843 </div>
844- <div class="table__data table-col--12">
845+ <div class="table__data table-col--12" data-ng-if="!isDevice">
846 <input type="text" class="table__input"
847 data-ng-class="{ 'has-error': isInterfaceNameInvalid(newInterface) }"
848 data-ng-model="newInterface.name">
849@@ -1136,9 +1148,9 @@
850 </h2>
851 <i data-ng-click="cancel()" class="icon icon--remove u-float--right u-margin--top-small u-margin--right-small"></i>
852 </div>
853- <div class="table__row form form--stack is-active">
854+ <div class="table__row form form--stack is-active" data-ng-if="!isDevice">
855 <div class="table__data table-col--100">
856- <div class="form__fieldset six-col">
857+ <div class="form__fieldset six-col" data-ng-if="!isDevice">
858 <div class="form__group u-display--mobile">
859 <label for="new-interface-name" class="two-col">Name</label>
860 <input type="text" class="three-col" id="new-interface-name"
861@@ -1206,6 +1218,55 @@
862 </div>
863 </div>
864 </div>
865+ <div class="table__row form form--stack is-active">
866+ <div class="table__data table-col--100" data-ng-if="isDevice">
867+ <fieldset class="form__fieldset six-col">
868+ <div class="form__group">
869+ <label for="mac" class="two-col">MAC address</label>
870+ <input name="mac" type="text" class="three-col"
871+ placeholder="00:00:00:00:00:00"
872+ data-ng-model="newInterface.mac_address"
873+ data-ng-class="{ 'has-error': isMACAddressInvalid(newInterface.mac_address, true) }">
874+ </div>
875+ </fieldset>
876+ <fieldset class="form__fieldset six-col last-col" data-ng-if="!isLimitedEditingAllowed(interface)">
877+ <div class="form__group">
878+ <label for="ip-assignment" class="two-col">IP Assignment</label>
879+ <select name="ip-assignment" class="three-col"
880+ ng-model="newInterface.ip_assignment"
881+ data-ng-options="assignment.name as assignment.text for assignment in ipAssignments">
882+ </select>
883+ </div>
884+ <div class="form__group" data-ng-if="newInterface.ip_assignment === 'static'">
885+ <label for="subnet" class="two-col">Subnet</label>
886+ <select name="subnet" class="three-col"
887+ ng-init="newInterface.subnet = subnets[0]"
888+ ng-model="newInterface.subnet"
889+ data-ng-change="subnetChanged(newInterface)"
890+ data-ng-options="subnet as getSubnetText(subnet) for subnet in subnets">
891+ </select>
892+ </div>
893+ <div class="form__group" data-ng-if="newInterface.ip_assignment !== 'dynamic'">
894+ <label for="ip-address" class="two-col">IP address</label>
895+ <div class="form__group-input three-col last-col">
896+ <input name="ip-address" type="text"
897+ data-ng-if="editInterface.ip_assignment === 'static'"
898+ placeholder="IP address (optional)"
899+ ng-model="newInterface.ip_address"
900+ data-ng-class="{ 'has-error': isIPAddressInvalid(newInterface) }">
901+ <input name="ip-address" type="text"
902+ data-ng-if="editInterface.ip_assignment !== 'static'"
903+ placeholder="IP address"
904+ ng-model="newInterface.ip_address"
905+ data-ng-class="{ 'has-error': isIPAddressInvalid(newInterface) }">
906+ <ul class="errors u-margin--bottom-none form__group--errors" data-ng-if="getInterfaceError(newInterface)">
907+ <li>{$ getInterfaceError(newInterface) $}</li>
908+ </ul>
909+ </div>
910+ </div>
911+ </fieldset>
912+ </div>
913+ </div>
914 <div class="table__row is-active">
915 <div class="table__data u-float--left" data-ng-show="newInterface.errorMsg">
916 <span class="icon icon--error u-margin--right-small"></span>{$ newInterface.errorMsg $}
917@@ -1221,7 +1282,7 @@
918 </div>
919 </main>
920 </div>
921- <div data-ng-if="!isController" data-ng-hide="isAllNetworkingDisabled() || isShowingCreateBond() || isShowingCreatePhysical()">
922+ <div data-ng-if="!isController && !isDevice" data-ng-hide="isAllNetworkingDisabled() || isShowingCreateBond() || isShowingCreatePhysical()">
923 <a class="button--secondary button--inline"
924 data-ng-disabled="selectedMode !== null"
925 data-ng-click="showCreatePhysical()">Add interface</a>
926@@ -1232,12 +1293,17 @@
927 data-ng-disabled="!canCreateBridge()"
928 data-ng-click="showCreateBridge()">Create bridge</a>
929 </div>
930+ <div data-ng-if="isDevice" data-ng-hide="isAllNetworkingDisabled() || isShowingCreateBond() || isShowingCreatePhysical()">
931+ <a class="button--secondary button--inline"
932+ data-ng-disabled="selectedMode !== null"
933+ data-ng-click="showCreatePhysical()">Add interface</a>
934+ </div>
935 </div>
936 </form>
937 </div>
938 </div>
939 </div>
940- <div class="row" id="storage" data-ng-hide="isController">
941+ <div class="row" id="storage" data-ng-if="!isController && !isDevice">
942 <div class="wrapper--inner" ng-controller="NodeStorageController">
943 <form>
944 <div class="twelve-col">
945@@ -2223,7 +2289,7 @@
946 </form>
947 </div>
948 </div>
949- <div class="row" id="events">
950+ <div class="row" id="events" data-ng-if="!isDevice">
951 <div class="wrapper--inner">
952 <div class="eight-col">
953 <h2 class="title">Latest {$ type_name $} events</h2>
954
955=== modified file 'src/maasserver/static/partials/nodes-list.html'
956--- src/maasserver/static/partials/nodes-list.html 2017-02-17 14:23:04 +0000
957+++ src/maasserver/static/partials/nodes-list.html 2017-03-02 00:31:00 +0000
958@@ -902,7 +902,9 @@
959 <input type="checkbox" class="checkbox" data-ng-click="toggleChecked(device, 'devices')" data-ng-checked="device.$selected" id="{$ device.fqdn $}" data-ng-disabled="hasActionsInProgress('devices')"/>
960 <label for="{$ device.fqdn $}" class="checkbox-label"></label>
961 </td>
962- <td class="table-col--24" aria-label="FQDN">{$ device.fqdn $}</td>
963+ <td class="table-col--24" aria-label="FQDN">
964+ <a href="#/node/device/{$ device.system_id $}">{$ device.fqdn $}</a>
965+ </td>
966 <td class="table-col--25" aria-label="MAC">
967 {$ device.primary_mac $}
968 <span class="extra-macs" data-ng-show="device.extra_macs.length">(+{$ device.extra_macs.length $})</span>