Merge ~ya-bo-ng/maas:esxi-rebuild-2 into maas:master

Proposed by Anthony Dillon
Status: Merged
Approved by: Anthony Dillon
Approved revision: 86b71e599f8ab1df011479426171e636d58ab6c9
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~ya-bo-ng/maas:esxi-rebuild-2
Merge into: maas:master
Diff against target: 2258 lines (+1397/-303)
14 files modified
src/maasserver/static/js/angular/controllers/node_details_storage.js (+257/-8)
src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js (+310/-8)
src/maasserver/static/js/angular/directives/nodedetails/storage_datastores.js (+9/-0)
src/maasserver/static/js/angular/entry.js (+5/-1)
src/maasserver/static/js/angular/factories/machines.js (+15/-0)
src/maasserver/static/js/angular/factories/tests/test_machines.js (+52/-0)
src/maasserver/static/partials/node-details.html (+256/-157)
src/maasserver/static/partials/nodedetails/storage/datastores.html (+112/-0)
src/maasserver/static/partials/nodedetails/storage/disks-partitions.html (+319/-121)
src/maasserver/static/partials/nodedetails/storage/filesystems.html (+1/-1)
src/maasserver/static/scss/_base_tables.scss (+0/-1)
src/maasserver/static/scss/_patterns_notification.scss (+12/-0)
src/maasserver/static/scss/_tables.scss (+45/-6)
src/maasserver/static/scss/_utils.scss (+4/-0)
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Lilyana Videnova (community) Approve
Steve Rydz (community) Approve
Review via email: mp+367016@code.launchpad.net

Commit message

Update machine storage to add datastore support for ESXi

Description of the change

## Done
Updated the machine storage UI to add the ability to create a ESXi datastore from available disk.

## QA
- Open a machine is Ready state
- Open the storage tab
- Change the layout to ESXi and confirm
- See that there is a datastore table.
- Under available disks table, select a disk and create a datastore
- See that is works
- Change the layout and confirm
- Check it does not show the datastores table

## Sreenshots
Storage UI: https://screenshots.firefox.com/0XmfpOlZmMoEgQ7h/10.54.72.65
Layout change confirmation: https://screenshots.firefox.com/0XmfpOlZmMoEgQ7h/10.54.72.65
ESXi layout: https://screenshots.firefox.com/0XmfpOlZmMoEgQ7h/10.54.72.65
Creating a datastore: https://screenshots.firefox.com/DGU5MpBaPAdJwJqq/10.54.72.65

To post a comment you must log in.
Revision history for this message
Steve Rydz (steverydz) wrote :

LGTM +1

review: Approve
Revision history for this message
Lilyana Videnova (lilyanavidenova) wrote :

LGTM :)

Revision history for this message
Lilyana Videnova (lilyanavidenova) :
review: Approve
Revision history for this message
Blake Rouse (blake-rouse) wrote :

JS needs fixing.

review: Needs Fixing
Revision history for this message
Anthony Dillon (ya-bo-ng) wrote :

All done Blake, thanks

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

Approved.

By the way.... smaller branches next time!!!!!

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Revision history for this message
MAAS Lander (maas-lander) wrote :
Revision history for this message
MAAS Lander (maas-lander) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/static/js/angular/controllers/node_details_storage.js b/src/maasserver/static/js/angular/controllers/node_details_storage.js
2index 2ddf2e0..87edee0 100644
3--- a/src/maasserver/static/js/angular/controllers/node_details_storage.js
4+++ b/src/maasserver/static/js/angular/controllers/node_details_storage.js
5@@ -45,12 +45,23 @@ export function removeAvailableByNew() {
6 };
7 }
8
9+export function datastoresOnly() {
10+ return function(filesystems) {
11+ return filesystems.filter(filesystem => {
12+ return filesystem.used_for === "VMFS Datastore";
13+ });
14+ };
15+}
16+
17 /* @ngInject */
18 export function NodeStorageController(
19 $scope,
20 MachinesManager,
21 ConverterService,
22- UsersManager
23+ UsersManager,
24+ $log,
25+ $timeout,
26+ $filter
27 ) {
28 // From models/partitiontable.py - must be kept in sync.
29 var INITIAL_PARTITION_OFFSET = 4 * 1024 * 1024;
30@@ -129,6 +140,8 @@ export function NodeStorageController(
31 }
32 ];
33
34+ var datastoreOnly = $filter("datastoresOnly");
35+
36 $scope.tableInfo = { column: "name" };
37 $scope.has_disks = false;
38 $scope.filesystems = [];
39@@ -148,6 +161,235 @@ export function NodeStorageController(
40 $scope.nodeManager = MachinesManager;
41 $scope.used = [];
42 $scope.showMembers = [];
43+ $scope.createNewDatastore = false;
44+ $scope.addToExistingDatastore = false;
45+ $scope.datastores = {
46+ new: {},
47+ old: {}
48+ };
49+ $scope.selectedAvailableDatastores = [];
50+ $scope.creatingDatastore = false;
51+ $scope.updatingDatastore = false;
52+ $scope.updatingOSFamily = false;
53+ $scope.updatingStorageLayout = false;
54+ $scope.confirmStorageLayout = false;
55+ $scope.newLayout = "";
56+ $scope.addToDatastoreValid = false;
57+
58+ // XXX: Steve Rydz 09/08/2019
59+ // Hardcoded for now in current cycle as no mapping exists
60+ $scope.osFamilies = [
61+ {
62+ id: "linux",
63+ name: "Linux",
64+ layouts: [
65+ {
66+ id: "flat",
67+ name: "Flat"
68+ },
69+ {
70+ id: "lvm",
71+ name: "LVM"
72+ },
73+ {
74+ id: "bcache",
75+ name: "bcache"
76+ },
77+ {
78+ id: "vmfs6",
79+ name: "VMFS6 (VMware ESXI)"
80+ },
81+ {
82+ id: "blank",
83+ name: "No storage (blank) layout"
84+ }
85+ ]
86+ }
87+ ];
88+
89+ $scope.osFamily = $scope.osFamilies[0];
90+ $scope.storageLayout = $scope.osFamily.layouts.find(layout => {
91+ return layout.id === $scope.node.detected_storage_layout;
92+ });
93+
94+ $scope.openStorageLayoutConfirm = function(selectedLayout) {
95+ $scope.osFamily.layouts.forEach(layout => {
96+ if (layout.id === selectedLayout) {
97+ $scope.newLayout = layout;
98+ }
99+ });
100+ $scope.confirmStorageLayout = true;
101+ };
102+
103+ $scope.closeStorageLayoutConfirm = function() {
104+ $scope.confirmStorageLayout = false;
105+ };
106+
107+ $scope.updateStorageLayout = function(storageLayout) {
108+ storageLayout = $scope.storageLayout = $scope.newLayout;
109+
110+ var params = {
111+ system_id: $scope.node.system_id,
112+ storage_layout: storageLayout.id
113+ };
114+
115+ $scope.updatingStorageLayout = true;
116+
117+ MachinesManager.applyStorageLayout(params)
118+ .then(function() {
119+ $timeout(function() {
120+ $scope.updatingStorageLayout = false;
121+ }, 0);
122+ })
123+ .catch(function(error) {
124+ $log.error(error);
125+ $timeout(function() {
126+ $scope.updatingStorageLayout = false;
127+ }, 0);
128+ });
129+
130+ $scope.closeStorageLayoutConfirm();
131+ };
132+
133+ $scope.openNewDatastorePanel = function() {
134+ $scope.createNewDatastore = true;
135+ var selectedDisks = $scope.getSelectedAvailable();
136+ $scope.datastores.new = {
137+ id: selectedDisks[0].id,
138+ name: "",
139+ mountpoint: selectedDisks[0].mount_point,
140+ filesystem: "VMFS6",
141+ size: selectedDisks[0].size_human
142+ };
143+ };
144+
145+ $scope.closeNewDatastorePanel = function() {
146+ $scope.createNewDatastore = false;
147+ $scope.datastores.new = {};
148+ };
149+
150+ $scope.openAddToExistingDatastorePanel = function() {
151+ $scope.addToExistingDatastore = true;
152+ $scope.selectedAvailableDatastores = $scope.getSelectedAvailable();
153+ $scope.datastores.old = datastoreOnly($scope.node.disks)[0];
154+ };
155+
156+ $scope.closeAddToExistingDatastorePanel = function() {
157+ $scope.addToExistingDatastore = false;
158+ $scope.datastores.new = {};
159+ };
160+
161+ $scope.canPerformActionOnDatastoreSet = function() {
162+ var editing = $scope.addToExistingDatastore || $scope.createNewDatastore;
163+ var selected = $scope.selectedAvailableDatastores.length > 0;
164+ var vmfs6 = $scope.storageLayout.id === "vmfs6";
165+ return !editing && selected && vmfs6;
166+ };
167+
168+ $scope.createDatastore = function() {
169+ $scope.createNewDatastore = true;
170+
171+ var selectedAvailable = $scope.getSelectedAvailable();
172+ var blockDeviceIDs = [];
173+ var partitionIDs = [];
174+
175+ selectedAvailable.forEach(function(item) {
176+ if (item.type === "partition") {
177+ partitionIDs.push(item.partition_id);
178+ } else {
179+ blockDeviceIDs.push(item.block_id);
180+ }
181+ });
182+
183+ var params = {
184+ system_id: $scope.node.system_id,
185+ block_devices: blockDeviceIDs,
186+ partitions: partitionIDs,
187+ name: $scope.datastores.new.name
188+ };
189+
190+ $scope.creatingDatastore = true;
191+
192+ MachinesManager.createDatastore(params)
193+ .then(function() {
194+ $timeout(function() {
195+ $scope.creatingDatastore = false;
196+ }, 0);
197+ $scope.closeNewDatastorePanel();
198+ $scope.selectedAvailableDatastores = [];
199+ })
200+ .catch(function(error) {
201+ $log.error(error);
202+ $timeout(function() {
203+ $scope.creatingDatastore = false;
204+ }, 0);
205+ });
206+ };
207+
208+ $scope.checkAddToDatastoreValid = function() {
209+ var selectedAvailable = $scope.getSelectedAvailable();
210+ var valid = true;
211+ if (selectedAvailable.length < 1) {
212+ valid = false;
213+ }
214+ selectedAvailable.forEach(function(item) {
215+ if (item.has_partitions) {
216+ valid = false;
217+ }
218+ });
219+ $scope.addToDatastoreValid = valid;
220+ };
221+
222+ $scope.addToDatastore = function() {
223+ var selectedAvailable = $scope.getSelectedAvailable();
224+ var blockDeviceIDs = [];
225+ var partitionIDs = [];
226+
227+ selectedAvailable.forEach(function(item) {
228+ if (item.type === "partition") {
229+ partitionIDs.push(item.partition_id);
230+ } else {
231+ blockDeviceIDs.push(item.block_id);
232+ }
233+ });
234+
235+ var params = {
236+ system_id: $scope.node.system_id,
237+ add_block_devices: blockDeviceIDs,
238+ add_partitions: partitionIDs,
239+ name: $scope.datastores.old.name,
240+ vmfs_datastore_id: $scope.datastores.old.id
241+ };
242+
243+ $scope.updatingDatastore = true;
244+
245+ MachinesManager.updateDatastore(params)
246+ .then(function() {
247+ $timeout(function() {
248+ $scope.updatingDatastore = false;
249+ }, 0);
250+ $scope.closeAddToExistingDatastorePanel();
251+ $scope.selectedAvailableDatastores = [];
252+ })
253+ .catch(function(error) {
254+ $log.error(error);
255+ $timeout(function() {
256+ $scope.updatingDatastore = false;
257+ }, 0);
258+ });
259+ };
260+
261+ $scope.storageLayoutIsReadOnly = function(layouts) {
262+ return layouts.length <= 1;
263+ };
264+
265+ $scope.storageLayoutIsDisabled = function(layouts) {
266+ return !layouts.length;
267+ };
268+
269+ $scope.hasStorageLayout = function(storageLayout) {
270+ return storageLayout ? true : false;
271+ };
272
273 // Return True if the filesystem is mounted.
274 function isMountedFilesystem(filesystem) {
275@@ -461,6 +703,7 @@ export function NodeStorageController(
276 type: disk.type,
277 model: disk.model,
278 serial: disk.serial,
279+ size_human: disk.size_human,
280 tags: getTags(disk),
281 used_for: disk.used_for,
282 is_boot: disk.is_boot,
283@@ -480,6 +723,7 @@ export function NodeStorageController(
284 type: "partition",
285 model: "",
286 serial: "",
287+ size_human: partition.size_human,
288 tags: [],
289 used_for: partition.used_for,
290 is_boot: false
291@@ -776,6 +1020,9 @@ export function NodeStorageController(
292 } else if (filesystem.original_type === "partition") {
293 // Delete the partition.
294 MachinesManager.deletePartition($scope.node, filesystem.original.id);
295+ } else if (filesystem.parent_type === "vmfs6") {
296+ // Delete the datastore.
297+ MachinesManager.deleteDisk($scope.node, filesystem.id);
298 } else {
299 // Delete the disk.
300 MachinesManager.deleteFilesystem(
301@@ -878,6 +1125,8 @@ export function NodeStorageController(
302 $scope.toggleAvailableSelect = function(disk) {
303 disk.$selected = !disk.$selected;
304 $scope.updateAvailableSelection(true);
305+ $scope.selectedAvailableDatastores = $scope.getSelectedAvailable();
306+ $scope.checkAddToDatastoreValid();
307 };
308
309 // Toggle the selection of all available disks.
310@@ -1592,24 +1841,24 @@ export function NodeStorageController(
311 };
312
313 // Return true when the name of the new disk is invalid.
314- $scope.isNewDiskNameInvalid = function() {
315+ $scope.isNewDiskNameInvalid = function(newDiskName) {
316 if (!angular.isObject($scope.node) || !angular.isArray($scope.node.disks)) {
317 return true;
318 }
319
320- if ($scope.availableNew.name === "") {
321+ if (newDiskName === "") {
322 return true;
323 } else {
324 var i, j;
325 for (i = 0; i < $scope.node.disks.length; i++) {
326 var disk = $scope.node.disks[i];
327- if ($scope.availableNew.name === disk.name) {
328+ if (newDiskName === disk.name) {
329 return true;
330 }
331 if (angular.isArray(disk.partitions)) {
332 for (j = 0; j < disk.partitions.length; j++) {
333 var partition = disk.partitions[j];
334- if ($scope.availableNew.name === partition.name) {
335+ if (newDiskName === partition.name) {
336 return true;
337 }
338 }
339@@ -1622,7 +1871,7 @@ export function NodeStorageController(
340 // Return true if bcache can be saved.
341 $scope.createBcacheCanSave = function() {
342 return (
343- !$scope.isNewDiskNameInvalid() &&
344+ !$scope.isNewDiskNameInvalid($scope.availableNew.name) &&
345 !$scope.isMountPointInvalid($scope.availableNew.mountPoint)
346 );
347 };
348@@ -1831,7 +2080,7 @@ export function NodeStorageController(
349 // Return true if RAID can be saved.
350 $scope.createRAIDCanSave = function() {
351 return (
352- !$scope.isNewDiskNameInvalid() &&
353+ !$scope.isNewDiskNameInvalid($scope.availableNew.name) &&
354 !$scope.isMountPointInvalid($scope.availableNew.mountPoint)
355 );
356 };
357@@ -1944,7 +2193,7 @@ export function NodeStorageController(
358
359 // Return true if volume group can be saved.
360 $scope.createVolumeGroupCanSave = function() {
361- return !$scope.isNewDiskNameInvalid();
362+ return !$scope.isNewDiskNameInvalid($scope.availableNew.name);
363 };
364
365 // Confirm and create the volume group device.
366diff --git a/src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js b/src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js
367index f267fa8..023c970 100644
368--- a/src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js
369+++ b/src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js
370@@ -431,6 +431,7 @@ describe("NodeStorageController", function() {
371 type: disks[2].type,
372 model: disks[2].model,
373 serial: disks[2].serial,
374+ size_human: disks[2].size_human,
375 tags: disks[2].tags,
376 used_for: disks[2].used_for,
377 has_partitions: false,
378@@ -443,6 +444,7 @@ describe("NodeStorageController", function() {
379 type: disks[3].type,
380 model: disks[3].model,
381 serial: disks[3].serial,
382+ size_human: disks[3].size_human,
383 tags: disks[3].tags,
384 used_for: disks[3].used_for,
385 has_partitions: true,
386@@ -455,6 +457,7 @@ describe("NodeStorageController", function() {
387 type: "partition",
388 model: "",
389 serial: "",
390+ size_human: disks[3].partitions[1].size_human,
391 tags: [],
392 used_for: disks[3].partitions[1].used_for
393 }
394@@ -3597,9 +3600,8 @@ describe("NodeStorageController", function() {
395 it("returns true if blank name", function() {
396 makeController();
397 $scope.node.disks = [];
398- $scope.availableNew.name = "";
399
400- expect($scope.isNewDiskNameInvalid()).toBe(true);
401+ expect($scope.isNewDiskNameInvalid("")).toBe(true);
402 });
403
404 it("returns true if name used by disk", function() {
405@@ -3610,9 +3612,8 @@ describe("NodeStorageController", function() {
406 name: name
407 }
408 ];
409- $scope.availableNew.name = name;
410
411- expect($scope.isNewDiskNameInvalid()).toBe(true);
412+ expect($scope.isNewDiskNameInvalid(name)).toBe(true);
413 });
414
415 it("returns true if name used by partition", function() {
416@@ -3628,9 +3629,8 @@ describe("NodeStorageController", function() {
417 ]
418 }
419 ];
420- $scope.availableNew.name = name;
421
422- expect($scope.isNewDiskNameInvalid()).toBe(true);
423+ expect($scope.isNewDiskNameInvalid(name)).toBe(true);
424 });
425
426 it("returns false if the name is not already used", function() {
427@@ -3646,9 +3646,8 @@ describe("NodeStorageController", function() {
428 ]
429 }
430 ];
431- $scope.availableNew.name = name;
432
433- expect($scope.isNewDiskNameInvalid()).toBe(false);
434+ expect($scope.isNewDiskNameInvalid(name)).toBe(false);
435 });
436 });
437
438@@ -5073,4 +5072,307 @@ describe("NodeStorageController", function() {
439 expect($scope.hasStorageLayoutIssues()).toBe(false);
440 });
441 });
442+
443+ describe("openStorageLayoutConfirm", function() {
444+ it("sets 'confirmStorageLayout' to true", function() {
445+ makeController();
446+ $scope.confirmStorageLayout = false;
447+ $scope.osFamilies = [
448+ {
449+ id: "linux",
450+ name: "Linux",
451+ layouts: [
452+ {
453+ id: "flat",
454+ name: "Flat"
455+ },
456+ {
457+ id: "lvm",
458+ name: "LVM"
459+ },
460+ {
461+ id: "bcache",
462+ name: "bcache"
463+ },
464+ {
465+ id: "vmfs6",
466+ name: "VMFS6 (VMware ESXI)"
467+ },
468+ {
469+ id: "blank",
470+ name: "No storage (blank) layout"
471+ }
472+ ]
473+ }
474+ ];
475+ $scope.openStorageLayoutConfirm("flat");
476+ expect($scope.confirmStorageLayout).toBe(true);
477+ });
478+
479+ it("sets 'newLayout' to layout argument", function() {
480+ makeController();
481+ $scope.osFamilies = [
482+ {
483+ id: "linux",
484+ name: "Linux",
485+ layouts: [
486+ {
487+ id: "flat",
488+ name: "Flat"
489+ },
490+ {
491+ id: "lvm",
492+ name: "LVM"
493+ },
494+ {
495+ id: "bcache",
496+ name: "bcache"
497+ },
498+ {
499+ id: "vmfs6",
500+ name: "VMFS6 (VMware ESXI)"
501+ },
502+ {
503+ id: "blank",
504+ name: "No storage (blank) layout"
505+ }
506+ ]
507+ }
508+ ];
509+ $scope.openStorageLayoutConfirm("flat");
510+ expect($scope.newLayout).toEqual($scope.osFamilies[0].layouts[0]);
511+ });
512+ });
513+
514+ describe("closeStorageLayoutConfirm", function() {
515+ it("sets 'confirmStorageLayout' to false", function() {
516+ makeController();
517+ $scope.confirmStorageLayout = true;
518+ $scope.closeStorageLayoutConfirm();
519+ expect($scope.confirmStorageLayout).toBe(false);
520+ });
521+ });
522+
523+ describe("updateStorageLayout", function() {
524+ it("calls 'applyStorageLayout'", function() {
525+ makeController();
526+ spyOn(MachinesManager, "applyStorageLayout").and.callFake(function() {
527+ var deferred = $q.defer();
528+ return deferred.promise;
529+ });
530+ $scope.newLayout = {
531+ id: "flat",
532+ name: "Flat"
533+ };
534+ $scope.updateStorageLayout($scope.newLayout);
535+ expect(MachinesManager.applyStorageLayout).toHaveBeenCalled();
536+ });
537+
538+ it("calls 'closeStorageLayoutConfirm'", function() {
539+ makeController();
540+ spyOn(MachinesManager, "applyStorageLayout").and.callFake(function() {
541+ var deferred = $q.defer();
542+ return deferred.promise;
543+ });
544+ spyOn($scope, "closeStorageLayoutConfirm");
545+ $scope.updateStorageLayout({
546+ id: "flat",
547+ name: "Flat"
548+ });
549+ expect($scope.closeStorageLayoutConfirm).toHaveBeenCalled();
550+ });
551+ });
552+
553+ describe("openNewDatastorePanel", function() {
554+ it("sets 'createNewDatastore' to true", function() {
555+ makeController();
556+ $scope.createNewDatastore = false;
557+ $scope.available = [
558+ {
559+ $selected: true,
560+ id: 1
561+ }
562+ ];
563+ $scope.openNewDatastorePanel();
564+ expect($scope.createNewDatastore).toBe(true);
565+ });
566+
567+ it("sets newDatastore", function() {
568+ makeController();
569+ $scope.available = [
570+ {
571+ $selected: true,
572+ id: 1,
573+ mount_point: "dev/null",
574+ size_human: "35 GB"
575+ }
576+ ];
577+ $scope.openNewDatastorePanel();
578+ expect($scope.datastores.new).toEqual({
579+ id: $scope.available[0].id,
580+ name: "",
581+ mountpoint: $scope.available[0].mount_point,
582+ filesystem: "VMFS6",
583+ size: $scope.available[0].size_human
584+ });
585+ });
586+ });
587+
588+ describe("closeNewDatastorePanel", function() {
589+ it("sets 'createNewDatastore' to false", function() {
590+ makeController();
591+ $scope.createNewDatastore = true;
592+ $scope.closeNewDatastorePanel();
593+ expect($scope.createNewDatastore).toBe(false);
594+ });
595+
596+ it("sets 'newDatastore' to '{}'", function() {
597+ makeController();
598+ $scope.datastores.new = { id: 1, name: "" };
599+ $scope.closeNewDatastorePanel();
600+ expect($scope.datastores.new).toEqual({});
601+ });
602+ });
603+
604+ describe("canPerformActionOnDatastoreSet", function() {
605+ it("return false if not on vmsf6 storage layout", function() {
606+ makeController();
607+ $scope.addToExistingDatastore = false;
608+ $scope.createNewDatastore = false;
609+ $scope.selectedAvailableDatastores = [1];
610+ $scope.storageLayout = { id: "flat" };
611+ expect($scope.canPerformActionOnDatastoreSet()).toBe(false);
612+ });
613+
614+ it("return false if already editing datastores", function() {
615+ makeController();
616+ $scope.addToExistingDatastore = false;
617+ $scope.createNewDatastore = true;
618+ $scope.selectedAvailableDatastores = [1];
619+ $scope.storageLayout = { id: "vmfs6" };
620+ expect($scope.canPerformActionOnDatastoreSet()).toBe(false);
621+ });
622+
623+ it("return false if no device is selected", function() {
624+ makeController();
625+ $scope.addToExistingDatastore = false;
626+ $scope.createNewDatastore = false;
627+ $scope.selectedAvailableDatastores = [];
628+ $scope.storageLayout = { id: "vmfs6" };
629+ expect($scope.canPerformActionOnDatastoreSet()).toBe(false);
630+ });
631+
632+ it("return true when conditions are matched", function() {
633+ makeController();
634+ $scope.addToExistingDatastore = false;
635+ $scope.createNewDatastore = false;
636+ $scope.selectedAvailableDatastores = [1];
637+ $scope.storageLayout = { id: "vmfs6" };
638+ expect($scope.canPerformActionOnDatastoreSet()).toBe(true);
639+ });
640+ });
641+
642+ describe("checkAddToDatastoreValid", function() {
643+ it("selected disks are valid when that condition is true", function() {
644+ makeController();
645+ var selected = {
646+ has_partitions: false
647+ };
648+ spyOn($scope, "getSelectedAvailable").and.returnValue([selected]);
649+ expect($scope.addToDatastoreValid).toBe(false);
650+ $scope.checkAddToDatastoreValid();
651+ expect($scope.addToDatastoreValid).toBe(true);
652+ });
653+
654+ it("selected disks are not valid disk has a partition", function() {
655+ makeController();
656+ var selected = {
657+ has_partitions: true
658+ };
659+ spyOn($scope, "getSelectedAvailable").and.returnValue([selected]);
660+ expect($scope.addToDatastoreValid).toBe(false);
661+ $scope.checkAddToDatastoreValid();
662+ expect($scope.addToDatastoreValid).toBe(false);
663+ });
664+
665+ it("selected disks are not valid when no selected disks", function() {
666+ makeController();
667+ spyOn($scope, "getSelectedAvailable").and.returnValue([]);
668+ expect($scope.addToDatastoreValid).toBe(false);
669+ $scope.checkAddToDatastoreValid();
670+ expect($scope.addToDatastoreValid).toBe(false);
671+ });
672+ });
673+
674+ describe("openAddToExistingDatastorePanel", function() {
675+ it("sets 'addToExistingDatastore' to true", function() {
676+ makeController();
677+ $scope.addToExistingDatastore = false;
678+ $scope.available = [
679+ {
680+ $selected: true,
681+ id: 1
682+ }
683+ ];
684+ $scope.openAddToExistingDatastorePanel();
685+ expect($scope.addToExistingDatastore).toBe(true);
686+ });
687+
688+ it("sets 'selectedAvailableDatastores' to selected", function() {
689+ makeController();
690+ $scope.datastores.old = [
691+ {
692+ $selected: true,
693+ id: 1
694+ }
695+ ];
696+ $scope.openAddToExistingDatastorePanel();
697+ expect($scope.selectedAvailableDatastores).toEqual($scope.available);
698+ });
699+
700+ it("sets 'datastores.old' to first disk", function() {
701+ makeController();
702+ $scope.openAddToExistingDatastorePanel();
703+ expect($scope.datastores.old).toBe($scope.node.disks[0]);
704+ });
705+ });
706+
707+ describe("closeAddToExistingDatastorePanel", function() {
708+ it("sets 'addToExistingDatastore' to false", function() {
709+ makeController();
710+ $scope.addToExistingDatastore = true;
711+ $scope.closeAddToExistingDatastorePanel();
712+ expect($scope.addToExistingDatastore).toBe(false);
713+ });
714+
715+ it("sets, 'newDatasore' to '{}'", function() {
716+ makeController();
717+ $scope.datastores.new = { id: 1, name: "" };
718+ $scope.closeAddToExistingDatastorePanel();
719+ expect($scope.datastores.new).toEqual({});
720+ });
721+ });
722+
723+ describe("createDatastore", function() {
724+ it("sets 'createNewDatastore' to true", function() {
725+ makeController();
726+ spyOn(MachinesManager, "createDatastore").and.callFake(function() {
727+ var deferred = $q.defer();
728+ return deferred.promise;
729+ });
730+ $scope.createNewDatastore = false;
731+ $scope.createDatastore();
732+ expect($scope.createNewDatastore).toBe(true);
733+ });
734+
735+ it("calls 'MachinesManager.createDatastore'", function() {
736+ makeController();
737+ spyOn(MachinesManager, "createDatastore").and.callFake(function() {
738+ var deferred = $q.defer();
739+ return deferred.promise;
740+ });
741+ $scope.createDatastore();
742+ expect(MachinesManager.createDatastore).toHaveBeenCalled();
743+ });
744+ });
745 });
746diff --git a/src/maasserver/static/js/angular/directives/nodedetails/storage_datastores.js b/src/maasserver/static/js/angular/directives/nodedetails/storage_datastores.js
747new file mode 100644
748index 0000000..d040063
749--- /dev/null
750+++ b/src/maasserver/static/js/angular/directives/nodedetails/storage_datastores.js
751@@ -0,0 +1,9 @@
752+function storageDatastores() {
753+ const path = "static/partials/nodedetails/storage/datastores.html";
754+ return {
755+ restrict: "E",
756+ templateUrl: `${path}?v=${MAAS_config.files_version}`
757+ };
758+}
759+
760+export default storageDatastores;
761diff --git a/src/maasserver/static/js/angular/entry.js b/src/maasserver/static/js/angular/entry.js
762index 0cf83dc..cea8d08 100644
763--- a/src/maasserver/static/js/angular/entry.js
764+++ b/src/maasserver/static/js/angular/entry.js
765@@ -36,7 +36,8 @@ import {
766 } from "./controllers/node_details_networking"; // TODO: fix export/namespace
767 // prettier-ignore
768 import {
769- removeAvailableByNew
770+ removeAvailableByNew,
771+ datastoresOnly
772 } from "./controllers/node_details_storage"; // TODO: fix export/namespace
773 // prettier-ignore
774 import {
775@@ -152,6 +153,7 @@ import ZonesListController from "./controllers/zones_list";
776 import storageDisksPartitions
777 from "./directives/nodedetails/storage_disks_partitions";
778 import storageFilesystems from "./directives/nodedetails/storage_filesystems";
779+import storageDatastores from "./directives/nodedetails/storage_datastores";
780 import maasMachinesTable from "./directives/machines_table";
781 import addMachine from "./directives/nodelist/add_machine";
782 import maasAccordion from "./directives/accordion";
783@@ -493,6 +495,7 @@ angular
784 .filter("removeDefaultVLANIfVLAN", removeDefaultVLANIfVLAN)
785 .filter("filterLinkModes", filterLinkModes)
786 .filter("removeAvailableByNew", removeAvailableByNew)
787+ .filter("datastoresOnly", datastoresOnly)
788 .filter("filterSource", filterSource)
789 .filter("ignoreSelf", ignoreSelf)
790 .filter("removeNoDHCP", removeNoDHCP)
791@@ -594,6 +597,7 @@ angular
792 // directives
793 .directive("storageDisksPartitions", storageDisksPartitions)
794 .directive("storageFilesystems", storageFilesystems)
795+ .directive("storageDatastores", storageDatastores)
796 .directive("addMachine", addMachine)
797 .directive("maasAccordion", maasAccordion)
798 .directive("maasActionButton", maasActionButton)
799diff --git a/src/maasserver/static/js/angular/factories/machines.js b/src/maasserver/static/js/angular/factories/machines.js
800index c3c3a5b..e70b096 100644
801--- a/src/maasserver/static/js/angular/factories/machines.js
802+++ b/src/maasserver/static/js/angular/factories/machines.js
803@@ -77,6 +77,21 @@ function MachinesManager(RegionConnection, NodesManager) {
804 return RegionConnection.callMethod(method, params);
805 };
806
807+ MachinesManager.prototype.applyStorageLayout = function(params) {
808+ var method = this._handler + ".apply_storage_layout";
809+ return RegionConnection.callMethod(method, params);
810+ };
811+
812+ MachinesManager.prototype.createDatastore = function(params) {
813+ var method = this._handler + ".create_vmfs_datastore";
814+ return RegionConnection.callMethod(method, params);
815+ };
816+
817+ MachinesManager.prototype.updateDatastore = function(params) {
818+ var method = this._handler + ".update_vmfs_datastore";
819+ return RegionConnection.callMethod(method, params);
820+ };
821+
822 return new MachinesManager();
823 }
824
825diff --git a/src/maasserver/static/js/angular/factories/tests/test_machines.js b/src/maasserver/static/js/angular/factories/tests/test_machines.js
826index 26eff00..9d4f7b6 100644
827--- a/src/maasserver/static/js/angular/factories/tests/test_machines.js
828+++ b/src/maasserver/static/js/angular/factories/tests/test_machines.js
829@@ -87,4 +87,56 @@ describe("MachinesManager", function() {
830 );
831 });
832 });
833+
834+ describe("applyStorageLayout", function() {
835+ it("calls apply_storage_layout", function() {
836+ spyOn(RegionConnection, "callMethod");
837+ var params = {
838+ system_id: makeName("system-id"),
839+ mount_point: makeName("/dir")
840+ };
841+ MachinesManager.applyStorageLayout(params);
842+ expect(RegionConnection.callMethod).toHaveBeenCalledWith(
843+ "machine.apply_storage_layout",
844+ params
845+ );
846+ });
847+ });
848+
849+ describe("createDatastore", function() {
850+ it("calls create_vmfs_datastore", function() {
851+ spyOn(RegionConnection, "callMethod");
852+ var params = {
853+ system_id: makeName("system-id"),
854+ block_devices: [1, 2, 3, 5],
855+ partitions: [5, 6, 7, 8],
856+ name: "New datastore"
857+ };
858+ MachinesManager.createDatastore(params);
859+ expect(RegionConnection.callMethod).toHaveBeenCalledWith(
860+ "machine.create_vmfs_datastore",
861+ params
862+ );
863+ });
864+ });
865+
866+ describe("updateDatastore", function() {
867+ it("calls update_vmfs_datastore", function() {
868+ spyOn(RegionConnection, "callMethod");
869+ var params = {
870+ system_id: makeName("system-id"),
871+ add_block_devices: [1, 2, 3, 4],
872+ add_partitions: [5, 6, 7, 8],
873+ remove_partitions: [],
874+ remove_block_devices: [],
875+ name: "New datastore",
876+ vmfs_datastore_id: 1
877+ };
878+ MachinesManager.updateDatastore(params);
879+ expect(RegionConnection.callMethod).toHaveBeenCalledWith(
880+ "machine.update_vmfs_datastore",
881+ params
882+ );
883+ });
884+ });
885 });
886diff --git a/src/maasserver/static/partials/node-details.html b/src/maasserver/static/partials/node-details.html
887index d722404..5612ef2 100755
888--- a/src/maasserver/static/partials/node-details.html
889+++ b/src/maasserver/static/partials/node-details.html
890@@ -69,7 +69,7 @@
891 <p class="page-header__message"><i class="p-icon--warning">Warning:</i> MAAS is not providing DHCP.</p>
892 </div>
893 </div>
894- <div class="row ng-hide u-no-margin--top" data-ng-hide="isActionError() || isDeployError() || hasActionPowerError(action.option.name)">
895+ <div class="row ng-hide u-no-margin--top" data-ng-hide="isActionError() || isDeployError() || isSSHKeyError() || hasActionPowerError(action.option.name)">
896 <!-- XXX blake_r 2015-02-19 - Need to add e2e test. -->
897 <div class="page-header__section">
898 <form class="p-form">
899@@ -110,7 +110,7 @@
900 </label>
901 <span data-ng-if="!nodesManager.isModernUbuntu(osSelection)">
902 <i class="p-icon--warning"></i>
903- <strong>Warning:</strong> Ubuntu 18.04 is the minimum required to create a KVM host. <a target="_blank"
904+ <strong>Warning:</strong> Ubuntu 18.04 is the minimum required. <a target="_blank"
905 class="p-link--external"
906 href="https://docs.maas.io/2.5/en/manage-pods-webui#add-a-kvm-host">Learn more</a>
907 </span>
908@@ -122,11 +122,6 @@
909 </div>
910 </div>
911 </div>
912- <div data-ng-if="isSSHKeyWarning()" class="p-strip is-shallow u-no-padding--top">
913- <p class="u-remove-max-width">
914- <i class="p-icon--warning">Warning:</i> Login will not be possible because no SSH keys have been added to your account. To add an SSH key, visit <a href="account/prefs/">your account page</a>.
915- </p>
916- </div>
917 </div>
918 </div>
919 </div>
920@@ -172,7 +167,7 @@
921 </form>
922 </div>
923 </div>
924- <div class="row ng-hide" data-ng-hide="isActionError() || isDeployError() || hasActionPowerError(action.option.name) || action.option.name !== 'commission'" data-ng-if="hasCustomCommissioningScripts()">
925+ <div class="row ng-hide" data-ng-hide="isActionError() || isDeployError() || isSSHKeyError() || hasActionPowerError(action.option.name) || action.option.name !== 'commission'" data-ng-if="hasCustomCommissioningScripts()">
926 <div class="page-header__section">
927 <form class="p-form">
928 <div class="col-8">
929@@ -184,7 +179,7 @@
930 </form>
931 </div>
932 </div>
933- <div class="row u-no-margin--top ng-hide" data-ng-hide="isActionError() || isDeployError() || hasActionPowerError(action.option.name) || (action.option.name !== 'commission' && action.option.name !== 'test')">
934+ <div class="row u-no-margin--top ng-hide" data-ng-hide="isActionError() || isDeployError() || isSSHKeyError() || hasActionPowerError(action.option.name) || (action.option.name !== 'commission' && action.option.name !== 'test')">
935 <hr />
936 <div class="page-header__section">
937 <form class="p-form">
938@@ -197,7 +192,7 @@
939 </form>
940 </div>
941 </div>
942- <div class="u-no-margin--top row ng-hide" data-ng-hide="isActionError() || isDeployError() || hasActionPowerError(action.option.name)" data-ng-if="action.option.name === 'commission' || action.option.name === 'test' && !action.showing_confirmation">
943+ <div class="u-no-margin--top row ng-hide" data-ng-hide="isActionError() || isDeployError() || isSSHKeyError() || hasActionPowerError(action.option.name)" data-ng-if="action.option.name === 'commission' || action.option.name === 'test' && !action.showing_confirmation">
944 <hr />
945 <div class="page-header__section col-12">
946 <form class="p-form">
947@@ -219,18 +214,18 @@
948 <div data-ng-repeat="confirmation_detail in action.confirmation_details">
949 <span>{$ confirmation_detail $}</span>
950 </div>
951- <div class="u-equal-height">
952- <div class="col-9 u-vertically-center">
953- <p class="u-remove-max-width">
954- Are you sure you want to {$ action.actionOption.name $} this {$ type_name $}?
955- </p>
956- </div>
957- <div class="col-3">
958- <div class="u-align--right">
959- <button class="p-button--base" data-ng-click="actionCancel()">No</button>
960- <button class="p-button--negative" data-ng-click="actionGo()">Yes</button>
961- </div>
962- </div>
963+ <div class="u-equal-height">
964+ <div class="col-9 u-vertically-center">
965+ <p class="u-remove-max-width">
966+ Are you sure you want to {$ action.actionOption.name $} this {$ type_name $}?
967+ </p>
968+ </div>
969+ <div class="col-3">
970+ <div class="u-align--right">
971+ <button class="p-button--base" data-ng-click="actionCancel()">No</button>
972+ <button class="p-button--negative" data-ng-click="actionGo()">Yes</button>
973+ </div>
974+ </div>
975 </div>
976 </div>
977 </div>
978@@ -273,6 +268,18 @@
979 </div>
980 </div>
981 </div>
982+ <div class="row u-equal-height ng-hide" data-ng-show="!isDeployError() && isSSHKeyError()">
983+ <div class="col-9 u-vertically-center">
984+ <p class="u-remove-max-width">
985+ <i class="p-icon--error">Error:</i> Node cannot be {$ action.option.sentence $}, because an SSH key has not been added to your account. To add an SSH key, visit <a href="account/prefs/">your account page</a>.
986+ </p>
987+ </div>
988+ <div class="col-3">
989+ <div class="u-align--right">
990+ <button class="p-button--base" data-ng-click="actionCancel()">Cancel</button>
991+ </div>
992+ </div>
993+ </div>
994 </div>
995
996 <nav class="p-tabs u-hr--fixed-width">
997@@ -394,97 +401,97 @@
998 </div>
999 <div class="p-strip is-shallow">
1000 <div class="row u-equal-height" data-ng-if="isDevice">
1001- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1002+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1003 <div maas-card-loader="device"></div>
1004- </div>
1005- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1006+ </div>
1007+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1008 <div maas-card-loader="tags"></div>
1009- </div>
1010+ </div>
1011 </div>
1012 <div class="row u-equal-height" data-ng-if="node.node_type == 3">
1013- <div class="col-4 p-card--highlighted action-card u-border--solid">
1014+ <div class="col-4 p-card--highlighted action-card u-border--solid">
1015 <div maas-card-loader="services"></div>
1016- </div>
1017- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1018+ </div>
1019+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1020 <div maas-card-loader="controller"></div>
1021- </div>
1022- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1023+ </div>
1024+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1025 <div maas-card-loader="hardware_info"></div>
1026- </div>
1027+ </div>
1028 </div>
1029 <div class="row u-equal-height" data-ng-if="node.node_type == 3">
1030- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-class="{ 'is-error': node.cpu_test_status === 3 }">
1031+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-class="{ 'is-error': node.cpu_test_status === 3 }">
1032 <div maas-card-loader="cpu"></div>
1033- </div>
1034- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-class="{ 'is-error': node.memory_test_status === 3 }">
1035+ </div>
1036+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-class="{ 'is-error': node.memory_test_status === 3 }">
1037 <div maas-card-loader="memory"></div>
1038- </div>
1039- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-class="{ 'is-error': node.storage_test_status === 3 }">
1040+ </div>
1041+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-class="{ 'is-error': node.storage_test_status === 3 }">
1042 <div maas-card-loader="storage"></div>
1043- </div>
1044+ </div>
1045 </div>
1046 <div class="row u-equal-height" data-ng-if="node.node_type == 3">
1047- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1048+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1049 <div maas-card-loader="tags"></div>
1050- </div>
1051+ </div>
1052 </div>
1053 <div class="row u-equal-height" data-ng-if="node.node_type == 2 || node.node_type == 4">
1054- <div class="col-4 p-card--highlighted action-card u-border--solid">
1055+ <div class="col-4 p-card--highlighted action-card u-border--solid">
1056 <div maas-card-loader="services"></div>
1057- </div>
1058- <div class="col-8">
1059+ </div>
1060+ <div class="col-8">
1061 <div class="row u-equal-height">
1062- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1063+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1064 <div maas-card-loader="images"></div>
1065- </div>
1066- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1067+ </div>
1068+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1069 <div maas-card-loader="controller"></div>
1070- </div>
1071+ </div>
1072 </div>
1073 <div class="row u-equal-height">
1074- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1075+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1076 <div maas-card-loader="hardware_info"></div>
1077- </div>
1078- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1079+ </div>
1080+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
1081 <div maas-card-loader="tags"></div>
1082- </div>
1083+ </div>
1084+ </div>
1085 </div>
1086- </div>
1087 </div>
1088 <div class="row u-equal-height" data-ng-if="node.node_type == 2 || node.node_type == 4">
1089- <div class="col-4 p-card--highlighted action-card u-border--solid" data-ng-class="{ 'is-error': node.cpu_test_status === 3 }">
1090+ <div class="col-4 p-card--highlighted action-card u-border--solid" data-ng-class="{ 'is-error': node.cpu_test_status === 3 }">
1091 <div maas-card-loader="cpu"></div>
1092- </div>
1093- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-class="{ 'is-error': node.memory_test_status === 3 }">
1094+ </div>
1095+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-class="{ 'is-error': node.memory_test_status === 3 }">
1096 <div maas-card-loader="memory"></div>
1097- </div>
1098- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-class="{ 'is-error': node.storage_test_status === 3 }">
1099+ </div>
1100+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-class="{ 'is-error': node.storage_test_status === 3 }">
1101 <div maas-card-loader="storage"></div>
1102- </div>
1103+ </div>
1104 </div>
1105 <div class="row u-equal-height" data-ng-if="!isDevice && !isController">
1106- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController" data-ng-class="{ 'is-error': node.cpu_test_status === 3 }">
1107+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController" data-ng-class="{ 'is-error': node.cpu_test_status === 3 }">
1108 <div maas-card-loader="cpu"></div>
1109- </div>
1110- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController" data-ng-class="{ 'is-error': node.memory_test_status === 3 }">
1111+ </div>
1112+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController" data-ng-class="{ 'is-error': node.memory_test_status === 3 }">
1113 <div maas-card-loader="memory"></div>
1114- </div>
1115- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="node.node_type != 3" data-ng-class="{ 'is-error': node.storage_test_status === 3 }">
1116+ </div>
1117+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="node.node_type != 3" data-ng-class="{ 'is-error': node.storage_test_status === 3 }">
1118 <div maas-card-loader="storage"></div>
1119- </div>
1120+ </div>
1121 </div>
1122 <div class="row u-equal-height" data-ng-if="!isDevice && !isController">
1123- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController">
1124+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController">
1125 <div maas-card-loader="machine"></div>
1126- </div>
1127- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController">
1128+ </div>
1129+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController">
1130 <div maas-card-loader="hardware_info"></div>
1131- </div>
1132- <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController">
1133+ </div>
1134+ <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController">
1135 <div maas-card-loader="tags"></div>
1136- </div>
1137+ </div>
1138+ </div>
1139 </div>
1140- </div>
1141 </section>
1142 <section class="p-strip" data-ng-if="section.area === 'containers'">
1143 <div class="row">
1144@@ -839,7 +846,7 @@
1145 </td>
1146 <td>
1147 <div class="u-no-margin--top" data-ng-repeat="subnet in vlanRow['subnets']">
1148- <a href="#/subnet/{$ subnet.id $}" title="{$ getSubnetText(subnet) $}">{$ getSubnetText(subnet) $}</a>
1149+ <a href="#/subnet/{$ subnet.id $}" title="{$ getSubnetText(subnet) $}">{$ getSubnetText(subnet) $}</a>
1150 </div>
1151 </td>
1152 <td>
1153@@ -2890,105 +2897,197 @@
1154 </form>
1155 </div>
1156 </section>
1157- <section class="p-strip" data-ng-if="section.area === 'storage'">
1158- <form data-ng-controller="NodeStorageController">
1159- <div class="row">
1160- <div class="col-12">
1161- <div class="p-notification--negative ng-hide" data-ng-hide="has_disks">
1162- <p class="p-notification__response">
1163- <span class="p-notification__status">Error:</span> No storage information. Commissioning this node will gather the storage information.
1164- </p>
1165- </div>
1166- <div class="p-notification ng-hide" data-ng-show="isAllStorageDisabled() && canEdit()">
1167- <p class="p-notification__response">Storage configuration cannot be modified unless the machine is Ready or Allocated.</p>
1168- </div>
1169- <div class="p-notification ng-hide" data-ng-show="!isUbuntuOS() && !isCentOS()">
1170- <p class="p-notification__response">Custom storage configuration is only supported on Ubuntu, CentOS, and RHEL.</p>
1171- </div>
1172- <div class="p-notification ng-hide" data-ng-show="!isUbuntuOS()">
1173- <p class="p-notification__response">Bcache and ZFS are only supported on Ubuntu.</p>
1174+
1175+ <section class="p-strip is-shallow" data-ng-if="section.area === 'storage'" data-ng-controller="NodeStorageController">
1176+ <div class="row">
1177+ <div class="col-12">
1178+ <div class="p-notification--negative ng-hide" data-ng-hide="has_disks">
1179+ <p class="p-notification__response">
1180+ <span class="p-notification__status">Error:</span> No storage information. Commissioning this node will gather the storage information.
1181+ </p>
1182+ </div>
1183+ <div class="p-notification ng-hide" data-ng-show="isAllStorageDisabled() && canEdit()">
1184+ <p class="p-notification__response">Storage configuration cannot be modified unless the machine is Ready or Allocated.</p>
1185+ </div>
1186+ <div class="p-notification ng-hide" data-ng-show="!isUbuntuOS() && !isCentOS()">
1187+ <p class="p-notification__response">Custom storage configuration is only supported on Ubuntu, CentOS, and RHEL.</p>
1188+ </div>
1189+ <div class="p-notification ng-hide" data-ng-show="!isUbuntuOS()">
1190+ <p class="p-notification__response">Bcache and ZFS are only supported on Ubuntu.</p>
1191+ </div>
1192+ <div data-ng-repeat="issue in node.storage_layout_issues" class="p-notification--negative ng-hide" data-ng-show="hasStorageLayoutIssues()">
1193+ <p class="p-notification__response">
1194+ <span class="p-notification__status">Error:</span> {$ issue $}
1195+ </p>
1196+ </div>
1197+ </div>
1198+ </div>
1199+ </section>
1200+
1201+ <section class="p-strip u-no-padding--bottom" data-ng-if="section.area === 'storage'" data-ng-controller="NodeStorageController">
1202+ <form>
1203+ <div data-ng-if="node.status_code === 4 || node.status_code === 10">
1204+ <div class="row" data-ng-if="!confirmStorageLayout">
1205+ <div class="col-12 prefix-9 u-sv3">
1206+ <div class="p-form__group u-align--right">
1207+ <div data-ng-if="storageLayoutIsDisabled(osFamily.layouts) && storageLayoutIsDisabled(osFamily.layouts)">
1208+ <button class="p-button--neutral" disabled>Change storage layout</button>
1209+ </div>
1210+ <div data-ng-if="!(storageLayoutIsDisabled(osFamily.layouts) && storageLayoutIsDisabled(osFamily.layouts))">
1211+ <div class="p-contextual-menu" toggle-ctrl>
1212+ <button class="p-button--neutral p-contextual-menu__toggle u-no-margin--bottom" data-ng-click="toggleMenu()">
1213+ <span data-ng-if="!updatingStorageLayout">
1214+ Change storage layout&nbsp;
1215+ <i class="p-icon--chevron" data-ng-class="{'u-rotate':isToggled}"></i>
1216+ </span>
1217+
1218+ <span data-ng-if="updatingStorageLayout">
1219+ <i class="p-icon--spinner u-animation--spin"></i>
1220+ &nbsp;
1221+ Updating storage layout&hellip;
1222+ </span>
1223+ </button>
1224+ <div class="p-contextual-menu__dropdown" role="menu" data-ng-show="isToggled">
1225+ <button class="p-contextual-menu__link" data-ng-click="toggleMenu(); openStorageLayoutConfirm('flat')">Flat</button>
1226+ <button class="p-contextual-menu__link" data-ng-click="toggleMenu(); openStorageLayoutConfirm('lvm')">LVM</button>
1227+ <button class="p-contextual-menu__link" data-ng-click="toggleMenu(); openStorageLayoutConfirm('bcache')">bcache</button>
1228+ <hr class="u-no-margin--bottom"/>
1229+ <button class="p-contextual-menu__link" data-ng-click="toggleMenu(); openStorageLayoutConfirm('vmfs6')">VMFS6 (VMware ESXi)</button>
1230+ <hr class="u-no-margin--bottom"/>
1231+ <button class="p-contextual-menu__link" data-ng-click="toggleMenu(); openStorageLayoutConfirm('blank')">No storage (blank) layout</button>
1232+ </div>
1233+ </div>
1234+ </div>
1235+ </div>
1236 </div>
1237- <div data-ng-repeat="issue in node.storage_layout_issues" class="p-notification--negative ng-hide" data-ng-show="hasStorageLayoutIssues()">
1238- <p class="p-notification__response">
1239- <span class="p-notification__status">Error:</span> {$ issue $}
1240- </p>
1241+ </div>
1242+ <div class="row" data-ng-if="confirmStorageLayout">
1243+ <div class="p-card--highlighted">
1244+ <div class="col-6 u-no-margin--left">
1245+ <div class="p-notification--caution is-subtle" data-ng-if="newLayout.id === 'vmfs6'">
1246+ <p class="p-notification__response">
1247+ <strong>Are you sure you want to change the storage layout to VMFS6?</strong><br />
1248+ Any changes done already will be discarded.<br />
1249+ This layout allows only for the deployment of <strong>VMware ESXi</strong> images.<br />
1250+ The storage layout will be applied to a node when it is deployed.
1251+ </p>
1252+ </div>
1253+ <div class="p-notification--caution is-subtle" data-ng-if="newLayout.id === 'lvm'">
1254+ <p class="p-notification__response">
1255+ <strong>Are you sure you want to change the storage layout to LVM?</strong><br />
1256+ Any changes done already will be lost.<br />
1257+ The storage layout will be applied to a node when it is deployed.
1258+ </p>
1259+ </div>
1260+ <div class="p-notification--caution is-subtle" data-ng-if="newLayout.id === 'blank'">
1261+ <p class="p-notification__response">
1262+ <strong>Are you sure you want to change this storage layout to blank?</strong><br />
1263+ Used disks will be returned to available, and any volume groups, raid sets,
1264caches, and filesystems removed.<br />
1265+ The storage layout will be applied to a node when it is deployed.
1266+ </p>
1267+ </div>
1268+ <div class="p-notification--caution is-subtle" data-ng-if="newLayout.id !== 'lvm' && newLayout.id !== 'vmfs6' && newLayout.id !== 'blank'">
1269+ <p class="p-notification__response">
1270+ <strong>Are you sure you want to change the storage layout to {$ newLayout.id $}?</strong><br />
1271+ Any changes done already will be lost.<br />
1272+ The storage layout will be applied to a node when it is deployed.
1273+ </p>
1274+ </div>
1275+ </div>
1276+ <div class="col-6 u-align--right">
1277+ <button class="p-button--base"
1278+ data-ng-click="closeStorageLayoutConfirm()">Cancel</button>
1279+ <button class="p-button--negative"
1280+ data-ng-click="updateStorageLayout(storageLayout)">
1281+ Change storage layout
1282+ </button>
1283+ </div>
1284 </div>
1285 </div>
1286 </div>
1287- <div class="row">
1288- <div data-ng-controller="NodeFilesystemsController">
1289- <storage-filesystems></storage-filesystems>
1290+
1291+ <div class="p-strip">
1292+ <div class="row" data-ng-if="storageLayout.id === 'vmfs6'">
1293+ <div data-ng-controller="NodeFilesystemsController">
1294+ <storage-datastores></storage-datastores>
1295+ </div>
1296 </div>
1297- <div data-ng-show="cachesets.length">
1298- <div class="row">
1299- <h3 class="p-heading--four">Available cache sets</h3>
1300+
1301+ <div class="row">
1302+ <div data-ng-controller="NodeFilesystemsController" data-ng-if="storageLayout.id !== 'vmfs6'">
1303+ <storage-filesystems></storage-filesystems>
1304 </div>
1305- <div class="row">
1306- <table class="p-table-expanding">
1307- <thead>
1308- <tr>
1309- <th class="col-2">Name</th>
1310- <th class="col-3">Size</th>
1311- <th class="col-4">Used by</th>
1312- <th class="col-2">
1313- <div class="u-align--right">Actions</div>
1314- </th>
1315- </tr>
1316- </thead>
1317- <tbody>
1318- <tr data-ng-repeat="cacheset in cachesets" data-ng-class="{ 'is-active': cacheset.$selected }">
1319- <td class="col-2" aria-label="Name" title="{$ cacheset.name $}">{$ cacheset.name $}</td>
1320- <td class="col-3" aria-label="Size" title="{$ cacheset.size_human $}">{$ cacheset.size_human $}</td>
1321- <td class="col-4" aria-label="Used by" title="{$ cacheset.used_by $}">{$ cacheset.used_by $}</td>
1322- <td class="col-2 p-table--action-cell">
1323- <div class="u-align--right">
1324- <div class="p-contextual-menu" toggle-ctrl data-ng-if="canDeleteCacheSet(cacheset) && !cacheset.$selected">
1325- <button class="p-button--base is-small p-contextual-menu__toggle" data-ng-click="toggleMenu()">
1326- <i class="p-icon--contextual-menu u-no-margin--right">Actions</i>
1327- </button>
1328- <div class="p-contextual-menu__dropdown" role="menu" data-ng-show="isToggled">
1329- <button class="p-contextual-menu__link"
1330- aria-label="Remove"
1331- data-ng-show="canDeleteCacheSet(cacheset)"
1332- data-ng-click="toggleMenu(); quickCacheSetDelete(cacheset)">Remove&hellip;</button>
1333- </div>
1334- </div>
1335- </div>
1336- </td>
1337- <td class="p-table-expanding__panel col-12" data-ng-if="cacheset.$selected && cachesetsMode === 'delete'">
1338- <div class="row" data-ng-if="windowWidth <= 768">
1339- <div class="col-8">
1340- <h2 data-ng-click="filesystemCancel()" class="p-heading--four">
1341- <span data-ng-if="cachesetsMode === 'delete'">Removing {$ cacheset.name $}</span>
1342- </h2>
1343+ <div data-ng-show="cachesets.length">
1344+ <div class="row">
1345+ <h3 class="p-heading--four">Available cache sets</h3>
1346+ </div>
1347+ <div class="row">
1348+ <table class="p-table-expanding">
1349+ <thead>
1350+ <tr>
1351+ <th class="col-2">Name</th>
1352+ <th class="col-3">Size</th>
1353+ <th class="col-4">Used by</th>
1354+ <th class="col-2">
1355+ <div class="u-align--right">Actions</div>
1356+ </th>
1357+ </tr>
1358+ </thead>
1359+ <tbody>
1360+ <tr data-ng-repeat="cacheset in cachesets" data-ng-class="{ 'is-active': cacheset.$selected }">
1361+ <td class="col-2" aria-label="Name" title="{$ cacheset.name $}">{$ cacheset.name $}</td>
1362+ <td class="col-3" aria-label="Size" title="{$ cacheset.size_human $}">{$ cacheset.size_human $}</td>
1363+ <td class="col-4" aria-label="Used by" title="{$ cacheset.used_by $}">{$ cacheset.used_by $}</td>
1364+ <td class="col-2 p-table--action-cell">
1365+ <div class="u-align--right">
1366+ <div class="p-contextual-menu" toggle-ctrl data-ng-if="canDeleteCacheSet(cacheset) && !cacheset.$selected">
1367+ <button class="p-button--base is-small p-contextual-menu__toggle" data-ng-click="toggleMenu()">
1368+ <i class="p-icon--contextual-menu u-no-margin--right">Actions</i>
1369+ </button>
1370+ <div class="p-contextual-menu__dropdown" role="menu" data-ng-show="isToggled">
1371+ <button class="p-contextual-menu__link"
1372+ aria-label="Remove"
1373+ data-ng-show="canDeleteCacheSet(cacheset)"
1374+ data-ng-click="toggleMenu(); quickCacheSetDelete(cacheset)">Remove&hellip;</button>
1375+ </div>
1376 </div>
1377- </div>
1378- <div class="row" data-ng-class="{ 'is-active': cachesetsMode !== null && cachesetsMode !== 'multi' }">
1379- <div data-ng-show="cachesetsMode === 'single' && canDeleteCacheSet(cacheset)">
1380- <button class="p-button--base"
1381- data-ng-show="canDeleteCacheSet(cacheset)"
1382- data-ng-click="cacheSetDelete()">Remove</button>
1383 </div>
1384- <div class="row ng-hide" data-ng-show="cachesetsMode === 'delete'">
1385+ </td>
1386+ <td class="p-table-expanding__panel col-12" data-ng-if="cacheset.$selected && cachesetsMode === 'delete'">
1387+ <div class="row" data-ng-if="windowWidth <= 768">
1388 <div class="col-8">
1389- <p><span class="p-icon--warning">Warning:</span> Are you sure you want to delete this cache set?</p>
1390+ <h2 data-ng-click="filesystemCancel()" class="p-heading--four">
1391+ <span data-ng-if="cachesetsMode === 'delete'">Removing {$ cacheset.name $}</span>
1392+ </h2>
1393+ </div>
1394+ </div>
1395+ <div class="row" data-ng-class="{ 'is-active': cachesetsMode !== null && cachesetsMode !== 'multi' }">
1396+ <div data-ng-show="cachesetsMode === 'single' && canDeleteCacheSet(cacheset)">
1397+ <button class="p-button--base"
1398+ data-ng-show="canDeleteCacheSet(cacheset)"
1399+ data-ng-click="cacheSetDelete()">Remove</button>
1400 </div>
1401- <div class="col-4">
1402- <div class="u-align--right">
1403- <button class="p-button--base" type="button" data-ng-click="cacheSetCancel()">Cancel</button>
1404- <button class="p-button--negative u-no-margin--top" data-ng-click="cacheSetConfirmDelete(cacheset)">Remove cache set</button>
1405+ <div class="row ng-hide" data-ng-show="cachesetsMode === 'delete'">
1406+ <div class="col-8">
1407+ <p><span class="p-icon--warning">Warning:</span> Are you sure you want to delete this cache set?</p>
1408+ </div>
1409+ <div class="col-4">
1410+ <div class="u-align--right">
1411+ <button class="p-button--base" type="button" data-ng-click="cacheSetCancel()">Cancel</button>
1412+ <button class="p-button--negative u-no-margin--top" data-ng-click="cacheSetConfirmDelete(cacheset)">Remove cache set</button>
1413+ </div>
1414 </div>
1415 </div>
1416 </div>
1417- </div>
1418- </td>
1419- </tr>
1420- </tbody>
1421- </table>
1422+ </td>
1423+ </tr>
1424+ </tbody>
1425+ </table>
1426+ </div>
1427+ </div>
1428+ <div>
1429+ <storage-disks-partitions></storage-disks-partitions>
1430 </div>
1431- </div>
1432- <div>
1433- <storage-disks-partitions></storage-disks-partitions>
1434 </div>
1435 </div>
1436 </form>
1437diff --git a/src/maasserver/static/partials/nodedetails/storage/datastores.html b/src/maasserver/static/partials/nodedetails/storage/datastores.html
1438new file mode 100644
1439index 0000000..28984c8
1440--- /dev/null
1441+++ b/src/maasserver/static/partials/nodedetails/storage/datastores.html
1442@@ -0,0 +1,112 @@
1443+<div class="p-strip is-shallow">
1444+ <h3 class="p-heading--four">Datastores</h3>
1445+
1446+ <table class="p-table-expanding p-table--datastores col-12" role="grid">
1447+ <thead>
1448+ <tr class="p-table__row">
1449+ <th scope="col" aria-sort="none" class="p-table__cell">Name</th>
1450+ <th scope="col" aria-sort="none" class="p-table__cell">Filesystem</th>
1451+ <th scope="col" aria-sort="none" class="p-table__cell">Size</th>
1452+ <th scope="col" aria-sort="none" class="p-table__cell">Mount point</th>
1453+ <th scope="col" aria-sort="none" class="p-table__cell u-align--right">Actions</th>
1454+ <th class="u-hide">
1455+ <!-- empty cell for validation -->
1456+ </th>
1457+ </tr>
1458+ </thead>
1459+ <tbody>
1460+ <tr data-ng-hide="node.disks.length" class="col-12">
1461+ <td>
1462+ No datastores defined.
1463+ </td>
1464+ </tr>
1465+ <tr class="p-table__row" data-ng-repeat="filesystem in node.disks" data-ng-class="{ 'is-active': filesystem.$selected }" data-ng-if="filesystem.used_for == 'VMFS Datastore'">
1466+ <td role="gridcell" class="p-table__cell" aria-label="Name" title="{$ filesystem.name $}">{$ filesystem.name $}</td>
1467+ <td role="gridcell" class="p-table__cell" aria-label="Filesystem" title="VMFS6">VMFS6</td>
1468+ <td role="gridcell" class="p-table__cell" aria-label="Size" title="{$ filesystem.size_human $}">{$ filesystem.size_human $}</td>
1469+ <td role="gridcell" class="p-table__cell" aria-label="Mount point" title="{$ filesystem.path $}">{$ filesystem.path $}</td>
1470+ <td role="gridcell" class="p-table__cell p-table--action-cell u-align--right">
1471+ <div class="p-contextual-menu" toggle-ctrl data-ng-if="!isAllStorageDisabled()">
1472+ <button class="p-button--base p-contextual-menu__toggle" aria-controls="#{$ item.name $}-menu"
1473+ data-ng-click="toggleMenu()" aria-haspopup="true">
1474+ <i class="p-icon--contextual-menu">Actions</i>
1475+ </button>
1476+ <div class="p-contextual-menu__dropdown" role="menu" data-ng-show="isToggled" id="{$ item.name $}-menu">
1477+ <button class="p-contextual-menu__link" aria-label="Remove"
1478+ data-ng-click="toggleMenu(); quickFilesystemDelete(filesystem)"
1479+ data-ng-show="!isAllStorageDisabled() && filesystemMode !== 'delete'">Remove&hellip;</button>
1480+ </div>
1481+ </div>
1482+ </td>
1483+ <td class="p-table-expanding__panel--bordered"
1484+ data-ng-if="filesystem.$selected && filesystemMode === 'delete'"
1485+ aria-hidden="!filesystem.$selected && filesystemMode !== 'delete'">
1486+ <div class="row u-flex--no-wrap" data-ng-if="windowWidth <= 768">
1487+ <h2 data-ng-click="filesystemCancel()" class="p-heading--four">
1488+ <span data-ng-if="filesystemMode === 'delete'">Removing {$ filesystem.name $}</span>
1489+ </h2>
1490+ <button class="p-button--close" data-ng-click="filesystemCancel()"><span
1491+ class="p-icon--close">Cancel</span></button>
1492+ </div>
1493+ <div data-ng-if="filesystemMode !== null && filesystemMode !== 'multi'"
1494+ data-ng-class="{ 'is-active': filesystemMode !== null && filesystemMode !== 'multi' }">
1495+ <div data-ng-if="filesystemMode === 'delete'" class="p-space-between">
1496+ <p><span class="p-icon--warning">Warning:</span> Are you sure you want to remove this {$
1497+ getRemoveTypeText(filesystem) $}?</p>
1498+ <div class="p-space-between__align-right">
1499+ <button class="p-button--base u-width--auto" type="button"
1500+ data-ng-click="filesystemCancel(filesystem)">Cancel</button>
1501+ <button class="p-button--negative u-width--auto"
1502+ data-ng-click="filesystemConfirmDelete(filesystem)">Remove</button>
1503+ </div>
1504+ </div>
1505+ </div>
1506+ </td>
1507+ </tr>
1508+
1509+ <tr class="is-active p-table__row" data-ng-if="dropdown" data-ng-switch="dropdown">
1510+ <!-- Adding a new TMPFS or RAMFPS filesystem -->
1511+ <td class="p-table-expanding__panel" data-ng-controller="NodeAddSpecialFilesystemController"
1512+ data-ng-switch-when="special">
1513+ <maas-obj-form obj="newFilesystem" manager="machineManager" manager-method="mountSpecialFilesystem"
1514+ inline="false" save-on-blur="false" after-save="cancel">
1515+ <div class="row" data-ng-if="windowWidth <= 768">
1516+ <div class="u-flex--no-wrap">
1517+ <h2 data-ng-click="cancel()" class="u-align--left p-heading--four">Adding filesystem</h2>
1518+ <button class="p-button--close" data-ng-click="cancel()" type="button">
1519+ <i class="p-icon--close">Cancel</i></button>
1520+ </div>
1521+ </div>
1522+ <div class="row p-form p-form--stacked">
1523+ <div class="col-6">
1524+ <div class="p-form__group">
1525+ <label class="p-form__label mobile-col-2">Description</label>
1526+ <div class="p-form__control p-form__control--placeholder mobile-col-2">
1527+ <span data-ng-bind="description"></span>
1528+ </div>
1529+ </div>
1530+ <maas-obj-field type="options" key="fstype" label="Filesystem" subtle="false"
1531+ options="type for type in specialFilesystemTypes"></maas-obj-field>
1532+ </div>
1533+ <div class="col-6">
1534+ <maas-obj-field type="text" key="mount_point" label="Mount point" subtle="false"
1535+ placeholder="Absolute path"></maas-obj-field>
1536+ <maas-obj-field type="text" key="mount_options" label="Mount options" subtle="false"
1537+ placeholder="Separated by commas, no spaces"></maas-obj-field>
1538+ </div>
1539+ </div>
1540+ <hr>
1541+ <div class="p-space-between">
1542+ <maas-obj-errors></maas-obj-errors>
1543+ <div class="p-space-between__align-right">
1544+ <button class="p-button--base u-width--auto" type="button" data-ng-click="cancel()">Cancel</button>
1545+ <button class="p-button--neutral u-width--auto ng-binding" data-ng-disabled="!canMount()"
1546+ maas-obj-save>Mount</button>
1547+ </div>
1548+ </div>
1549+ </maas-obj-form>
1550+ </td>
1551+ </tr>
1552+ </tbody>
1553+ </table>
1554+</div>
1555\ No newline at end of file
1556diff --git a/src/maasserver/static/partials/nodedetails/storage/disks-partitions.html b/src/maasserver/static/partials/nodedetails/storage/disks-partitions.html
1557index 8f85fc7..041f4ae 100644
1558--- a/src/maasserver/static/partials/nodedetails/storage/disks-partitions.html
1559+++ b/src/maasserver/static/partials/nodedetails/storage/disks-partitions.html
1560@@ -4,27 +4,25 @@
1561 <table class="p-table-expanding p-table--disks-partitions col-12">
1562 <thead>
1563 <tr class="p-table__row">
1564- <th class="col-3">
1565- <div class="u-float--left">
1566- <input type="checkbox" class="checkbox u-float--left" id="available-check-all" data-ng-hide="isAvailableDisabled()" data-ng-checked="availableAllSelected"
1567- data-ng-click="toggleAvailableAllSelect()" data-ng-disabled="isAvailableDisabled()" />
1568- <label for="available-check-all"></label>
1569- </div>
1570- <a data-ng-click="tableInfo.column = 'name'" data-ng-class="{'p-link--soft': tableInfo.column === 'name'}">Name</a>
1571- <span class="divide"> | </span>
1572- <a data-ng-click="tableInfo.column = 'model'" data-ng-class="{'p-link--soft': tableInfo.column === 'model'}">Model</a>
1573- <span class="divide"> | </span>
1574- <a data-ng-click="tableInfo.column = 'serial'" data-ng-class="{'p-link--soft': tableInfo.column === 'serial'}">Serial</a>
1575- <span class="divide"> | </span>
1576- <a data-ng-click="tableInfo.column = 'firmware_version'" data-ng-class="{'p-link--soft': tableInfo.column === 'firmware_version'}">Firmware</a>
1577+ <th class="p-double-row p-table__cell">
1578+ <div class="p-double-row__checkbox">&nbsp;</div>
1579+ <div class="p-double-row__rows-container--checkbox">
1580+ <div>Name</div>
1581+ <div>Serial</div>
1582+ </div>
1583+ </th>
1584+ <th class="p-double-row p-table__cell">
1585+ <div>Model</div>
1586+ <div>Firmware</div>
1587 </th>
1588- <th class="col-1"><div class="u-align--center">Boot</div></th>
1589- <th class="col-1">Size</th>
1590- <th class="col-1">Type</th>
1591- <th class="col-2">Filesystem</th>
1592- <th class="col-2">Tags</th>
1593- <th class="col-1">Health</th>
1594- <th class="col-1"><div class="u-align--right">Actions</div></th>
1595+ <th class="p-table__cell"><div class="u-align--center">Boot</div></th>
1596+ <th class="p-table__cell">Size</th>
1597+ <th class="col-3 p-double-row p-table__cell">
1598+ <div>Type</div>
1599+ <div>Tags</div>
1600+ </th>
1601+ <th class="p-table__cell">Health</th>
1602+ <th class="p-table__cell"><div class="u-align--right">Actions</div></th>
1603 </tr>
1604 </thead>
1605 <tbody>
1606@@ -34,32 +32,34 @@
1607 </td>
1608 </tr>
1609 <tr class="p-table__row" data-ng-repeat="item in available | removeAvailableByNew:availableNew"
1610- data-ng-class="{ 'is-active': item.$selected }">
1611- <td class="col-3 p-form-validation" aria-label="Name"
1612+ data-ng-class="{ 'is-active': item.$selected }" data-ng-if="item.parent_type !== 'vmfs6'">
1613+ <td class="p-form-validation p-double-row p-table__cell" aria-label="Name"
1614 data-ng-class="{ 'is-error': isNameInvalid(item) }">
1615- <div class="u-float--left">
1616- <input type="checkbox" class="checkbox u-float--left" id="{$ item.name $}"
1617- data-ng-hide="isAvailableDisabled()"
1618- data-ng-checked="item.$selected"
1619- data-ng-click="toggleAvailableSelect(item)"
1620- data-ng-disabled="isAvailableDisabled()" />
1621- <label for="{$ item.name $}">
1622- <span data-ng-show="tableInfo.column === 'name'"
1623- data-ng-hide="availableMode === 'edit' && item.$selected"
1624- title="{$ item.name $}">{$ item.name $}</span>
1625- </label>
1626- </div>
1627- <span data-ng-show="tableInfo.column === 'model'" title="{$ item.model $}">{$ item.model $}</span>
1628- <span data-ng-show="tableInfo.column === 'serial'" title="{$ item.serial $}">{$ item.serial $}</span>
1629- <span data-ng-show="tableInfo.column === 'firmware_version'" title="{$ item.firmware_version $}">{$ item.firmware_version $}</span>
1630- <input type="text" class="p-form-validation__input"
1631- data-ng-model="item.name"
1632- data-ng-show="availableMode === 'edit' && item.$selected"
1633- data-ng-disabled="item.type === 'partition' || isAllStorageDisabled() || !canEdit()"
1634- data-ng-change="nameHasChanged(item)">
1635+ <div class="p-double-row__checkbox">
1636+ <input type="checkbox" class="checkbox u-float--left" id="{$ item.name $}" data-ng-hide="isAvailableDisabled()" data-ng-checked="item.$selected" data-ng-click="toggleAvailableSelect(item)" data-ng-disabled="isAvailableDisabled()" />
1637+ <label for="{$ item.name $}"></label>
1638+ </div>
1639+ <div class="p-double-row__rows-container--checkbox">
1640+ <div class="p-double-row__main-row">
1641+ {$ item.name $}
1642+ </div>
1643+ <div class="p-double-row__muted-row">
1644+ {$ item.serial $}
1645+ </div>
1646+ </div>
1647 </td>
1648- <td class="col-1" aria-label="Boot">
1649- <div class="u-align--center">
1650+ <td class="p-double-row p-table__cell">
1651+ <div class="p-double-row__container">
1652+ <div class="p-double-row__main-row">
1653+ {$ item.model $}
1654+ </div>
1655+ <div class="p-double-row__muted-row">
1656+ {$ item.firmware_version $}
1657+ </div>
1658+ </div>
1659+ </td>
1660+ <td class="p-table__cell" aria-label="Boot">
1661+ <div class="u-align--center" data-ng-if="storageLayout.id !== 'vmfs6'">
1662 <input type="radio" name="boot-disk" id="{$ item.name $}-boot" class="u-no-margin--right"
1663 data-ng-click="setAsBootDisk(item)"
1664 data-ng-checked="item.is_boot"
1665@@ -69,30 +69,31 @@
1666 <label for="{$ item.name $}-boot" data-ng-hide="availableMode === 'edit' && item.$selected"></label>
1667 </div>
1668 </td>
1669- <td class="col-1" aria-label="Size">
1670+ <td class="p-table__cell" aria-label="Size">
1671 <span data-ng-hide="availableMode === 'edit' && item.$selected" title="{$ item.size_human $}">
1672- {$ item.size_human $} <span class="table__label ng-hide" data-ng-show="showFreeSpace(item)">Free: {$ item.available_size_human $}</span>
1673+ {$ item.size_human $} <span class="table__labeldatastore-name ng-hide" data-ng-show="showFreeSpace(item)">Free: {$ item.available_size_human $}</span>
1674 </span>
1675 </td>
1676- <td class="col-1" aria-label="type">
1677- <span data-ng-hide="availableMode === 'edit' && item.$selected" title="{$ getDeviceType(item) $}">{$ getDeviceType(item) $}</span>
1678- </td>
1679- <td class="col-2" aria-label="Filesystem">
1680- <span data-ng-hide="availableMode === 'edit' && item.$selected" title="{$ item.fstype $}">{$ item.fstype $}</span>
1681- </td>
1682- <td class="col-2" aria-label="Tags">
1683- <span class="table__tag" data-ng-repeat="tag in item.tags" data-ng-hide="item.$options.editingTags">
1684- <a href="#/machines/?query=storage_tags:({$ tag.text $})" title="{$ tag.text $}">{$ tag.text $}</a>
1685- </span>
1686+ <td class="p-double-row p-table__cell" aria-label="type">
1687+ <div class="p-double-rows__container">
1688+ <div class="p-double-row__main-row">
1689+ {$ getDeviceType(item) $}
1690+ </div>
1691+ <div class="p-double-row__muted-row">
1692+ <span class="table__tag" data-ng-repeat="tag in item.tags" data-ng-hide="item.$options.editingTags">
1693+ <a href="#/machines/?query=storage_tags:({$ tag.text $})" title="{$ tag.text $}">{$ tag.text $}</a>
1694+ </span>
1695+ </div>
1696+ </div>
1697 </td>
1698- <td class="col-1" aria-label="Health">
1699+ <td class="p-table__cell" aria-label="Health">
1700 <span data-maas-script-status="script-status" data-script-status="item.test_status" data-ng-if="item.type === 'physical'"></span>
1701 <span data-ng-if="item.test_status === 0 || item.test_status === 1 || item.test_status === 2 || item.test_status === 5 || item.test_status === 7" title="Ok">Ok</span>
1702 <span data-ng-if="item.test_status === 3 || item.test_status === 4 || item.test_status === 8" title="Error">Error</span>
1703 <span data-ng-if="item.test_status === 6" title="Degraded">Degraded</span>
1704 <span data-ng-if="item.test_status === -1" title="Unknown">Unknown</span>
1705 </td>
1706- <td class="col-1 p-table--action-cell">
1707+ <td class="p-table--action-cell p-table__cell">
1708 <div class="u-align--right">
1709 <div class="p-contextual-menu" toggle-ctrl
1710 data-ng-if="canAddLogicalVolume(item) || canAddPartition(item) || canEdit(item) || canDelete(item)">
1711@@ -463,7 +464,7 @@
1712 <label class="checkbox-label" for="bcache-create"></label>
1713 </div>
1714 <div class="u-float--left p-form-validation"
1715- data-ng-class="{ 'is-error': isNewDiskNameInvalid() }">
1716+ data-ng-class="{ 'is-error': isNewDiskNameInvalid(availableNew.name) }">
1717 <input type="text" class="p-form-validation__input"
1718 data-ng-model="availableNew.name">
1719 </div>
1720@@ -481,7 +482,7 @@
1721 <div class="row">
1722 <div class="col-6">
1723 <div class="p-form__group u-hide--large p-form-validation"
1724- data-ng-class="{ 'is-error': isNewDiskNameInvalid() }">
1725+ data-ng-class="{ 'is-error': isNewDiskNameInvalid(availableNew.name) }">
1726 <label for="bcache-name" class="p-form__label">Name</label>
1727 <div class="p-form__control">
1728 <input type="text" id="bcache-name" data-ng-model="availableNew.name"
1729@@ -600,7 +601,7 @@
1730 <label for="raid-create"></label>
1731 </div>
1732 <div class="u-float--left p-form-validation"
1733- data-ng-class="{ 'is-error': isNewDiskNameInvalid() }">
1734+ data-ng-class="{ 'is-error': isNewDiskNameInvalid(availableNew.name) }">
1735 <input type="text" class="p-form-validation__input"
1736 data-ng-model="availableNew.name">
1737 </div>
1738@@ -619,7 +620,7 @@
1739 <div class="col-6">
1740 <div class="p-form__group u-hide--medium u-hide--large">
1741 <label for="new-raid-name" class="p-form__label">Name</label>
1742- <div class="p-form__control" data-ng-class="{ 'is-error': isNewDiskNameInvalid() }">
1743+ <div class="p-form__control" data-ng-class="{ 'is-error': isNewDiskNameInvalid(availableNew.name) }">
1744 <input type="text" id="new-raid-name" data-ng-model="availableNew.name">
1745 </div>
1746 </div>
1747@@ -734,7 +735,7 @@
1748 <label for="vg-create"></label>
1749 </div>
1750 <div class="u-float--left p-form-validation"
1751- data-ng-class="{ 'is-error': isNewDiskNameInvalid() }">
1752+ data-ng-class="{ 'is-error': isNewDiskNameInvalid(availableNew.name) }">
1753 <input type="text" class="p-form-validation__input"
1754 data-ng-model="availableNew.name">
1755 </div>
1756@@ -753,7 +754,7 @@
1757 <div class="col-6">
1758 <div class="p-form__group p-form-validation">
1759 <label for="new-vg-name" class="p-form__label">Name</label>
1760- <div class="p-form__control" data-ng-class="{ 'is-error': isNewDiskNameInvalid() }">
1761+ <div class="p-form__control" data-ng-class="{ 'is-error': isNewDiskNameInvalid(availableNew.name) }">
1762 <input type="text" class="p-form-validation__input" id="new-vg-name" data-ng-model="availableNew.name">
1763 </div>
1764 </div>
1765@@ -806,57 +807,212 @@
1766 </tr>
1767 </tbody>
1768 </table>
1769- <button class="p-button--neutral p-tooltip p-tooltip--top-center"
1770- data-ng-disabled="!canCreateRAID()"
1771- data-ng-hide="isAllStorageDisabled() || !canEdit()"
1772- data-ng-click="createRAID()">
1773- Create RAID
1774- <span class="p-tooltip__message" role="tooltip">Select two or more physical devices to create a RAID</span>
1775- </button>
1776- <button class="p-button--neutral p-tooltip p-tooltip--top-center"
1777- data-ng-disabled="!canCreateVolumeGroup()"
1778- data-ng-hide="isAllStorageDisabled() || !canEdit()"
1779- data-ng-click="createVolumeGroup()">
1780- Create volume group
1781- <span class="p-tooltip__message" role="tooltip">Select one or more devices to create a volume group</span>
1782- </button>
1783- <button class="p-button--neutral p-tooltip p-tooltip--top-center"
1784- data-ng-disabled="!canCreateCacheSet()"
1785- data-ng-hide="isAllStorageDisabled() || !canEdit()"
1786- data-ng-click="createCacheSet()">
1787- Create cache Set
1788- <span class="p-tooltip__message" role="tooltip">Select one device to create a cache set</span>
1789- </button>
1790- <button class="p-button--neutral p-tooltip--top-center"
1791- data-ng-class="{ 'p-tooltip': !canCreateBcache() }"
1792- data-ng-disabled="!canCreateBcache()"
1793- data-ng-hide="isAllStorageDisabled() || !canEdit()"
1794- data-ng-click="createBcache()">
1795- Create bcache
1796- <span class="p-tooltip__message" role="tooltip">{$ getCannotCreateBcacheMsg() $}</span>
1797- </button>
1798+
1799+ <div class="p-card" data-ng-if="createNewDatastore">
1800+ <div class="row">
1801+ <div class="row p-form--stacked">
1802+ <div class="col-6">
1803+ <div class="p-form__group p-form-validation"
1804+ data-ng-class="{ 'is-error': isNewDiskNameInvalid(datastores.new.name) }">
1805+ <label for="datastore-name" class="p-form__label u-sv3">Name</label>
1806+ <div class="p-form__control">
1807+ <input type="text"
1808+ name="datastore-name"
1809+ id="datastore-name"
1810+ class="p-form-validation__input"
1811+ data-ng-model="datastores.new.name"
1812+ data-ng-keydown="$event.keyCode == 13 && !isNewDiskNameInvalid(datastores.new.name) && $event.preventDefault()"
1813+ data-ng-keyup="$event.keyCode == 13 && !isNewDiskNameInvalid(datastores.new.name) && createDatastore()">
1814+
1815+ <p class="p-form-validation__message"
1816+ data-ng-if="isNewDiskNameInvalid(datastores.new.name) && datastores.new.name != ''">
1817+ <strong>Error:</strong> Disk name is already in use
1818+ </p>
1819+ <p class="p-form-validation__message"
1820+ data-ng-if="isNewDiskNameInvalid(datastores.new.name) && datastores.new.name == ''">
1821+ <strong>Error:</strong> Please enter a name for your new datastore
1822+ </p>
1823+ </div>
1824+ </div>
1825+ </div>
1826+ <div class="col-6">
1827+ <div class="p-form__group">
1828+ <label for="datastore-filesystem" class="p-form__label u-sv3">Filesystem</label>
1829+ <div class="p-form__control">
1830+ <p>{$ datastores.new.filesystem $}</p>
1831+ </div>
1832+ </div>
1833+ <div class="p-form__group" data-ng-if="datastores.new.path">
1834+ <label for="datastore-mountpoint" class="p-form__label">Mount point</label>
1835+ <div class="p-form__control">
1836+ <p>{$ datastores.new.path $}</p>
1837+ </div>
1838+ </div>
1839+ </div>
1840+ </div>
1841+ <div class="u-sv2">
1842+ <hr />
1843+ </div>
1844+ <div class="u-align--right">
1845+ <button class="p-button--neutral u-no-margin--bottom" data-ng-click="closeNewDatastorePanel()" data-ng-disabled="creatingDatastore">Cancel</button>
1846+ <button class="p-button--positive u-no-margin--bottom" data-ng-click="createDatastore()" data-ng-if="!creatingDatastore" data-ng-disabled="isNewDiskNameInvalid(datastores.new.name)">
1847+ Create datastore
1848+ </button>
1849+ <button class="p-button--positive u-no-margin--bottom" data-ng-if="creatingDatastore">
1850+ <i class="p-icon--spinner is-light u-animation--spin"></i>
1851+ &nbsp;
1852+ Creating datastore
1853+ </button>
1854+ </div>
1855+ </div>
1856+ </div>
1857+ <div class="p-card" data-ng-if="addToExistingDatastore">
1858+ <div class="row p-form--stacked">
1859+ <div class="col-6">
1860+ <div class="p-form__group">
1861+ <label for="datastore-name" class="p-form__label u-sv3">Datastore</label>
1862+ <div class="p-form__control">
1863+ <select name="datastore-name" id="datastore-name"
1864+ data-ng-model="datastores.old"
1865+ data-ng-options="disk as disk.name for disk in node.disks | datastoresOnly">
1866+ </select>
1867+ </div>
1868+ </div>
1869+ </div>
1870+ <div class="col-6">
1871+ <div class="p-form__group">
1872+ <label for="datastore-mountpoint" class="p-form__label">Mount point</label>
1873+ <div class="p-form__control">
1874+ <p>{$ datastores.old.path $}</p>
1875+ </div>
1876+ </div>
1877+ </div>
1878+ </div>
1879+ <div class="u-sv2">
1880+ <hr />
1881+ </div>
1882+ <div class="u-align--right">
1883+ <button class="p-button--neutral u-no-margin--bottom" data-ng-click="closeAddToExistingDatastorePanel()" data-ng-disabled="updatingDatastore">Cancel</button>
1884+ <span class="p-tooltip p-tooltip--top-right">
1885+ <button class="p-button--positive u-no-margin--bottom"
1886+ data-ng-click="addToDatastore()"
1887+ data-ng-if="!updatingDatastore"
1888+ data-ng-disabled="!addToDatastoreValid">
1889+ Add to datastore
1890+ </button>
1891+ <span class="p-tooltip__message" role="tooltip" data-ng-if="!addToDatastoreValid">Disks with partitions cannot be added to a datastore</span>
1892+ </span>
1893+ <button class="p-button--positive u-no-margin--bottom" data-ng-if="updatingDatastore">
1894+ <i class="p-icon--spinner is-light u-animation--spin"></i>
1895+ &nbsp;
1896+ Adding to datastore
1897+ </button>
1898+ </div>
1899+ </div>
1900+
1901+ <div>
1902+ <span class="p-tooltip p-tooltip--top-left">
1903+ <button class="p-button--neutral"
1904+ data-ng-disabled="!canCreateRAID() || storageLayout.id === 'vmfs6'"
1905+ data-ng-hide="isAllStorageDisabled() || !canEdit()"
1906+ data-ng-click="createRAID()">
1907+ Create RAID
1908+ </button>
1909+ <span data-ng-if="!canCreateRAID()">
1910+ <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id !== 'vmfs6'">Select two or more physical devices to create a RAID</span>
1911+ <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id === 'vmfs6'">Not supported in the VMFS6 layout</span>
1912+ </span>
1913+ </span>
1914+ <span class="p-tooltip p-tooltip--top-center">
1915+ <button class="p-button--neutral"
1916+ data-ng-disabled="!canCreateVolumeGroup() || storageLayout.id === 'vmfs6'"
1917+ data-ng-hide="isAllStorageDisabled() || !canEdit()"
1918+ data-ng-click="createVolumeGroup()">
1919+ Create volume group
1920+ </button>
1921+ <span data-ng-if="!canCreateVolumeGroup()">
1922+ <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id !== 'vmfs6'">Select one or more devices to create a volume group</span>
1923+ <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id === 'vmfs6'">Not supported in the VMFS6 layout</span>
1924+ </span>
1925+ </span>
1926+ <span class="p-tooltip p-tooltip--top-center">
1927+ <button class="p-button--neutral"
1928+ data-ng-disabled="!canCreateCacheSet() || storageLayout.id === 'vmfs6'"
1929+ data-ng-hide="isAllStorageDisabled() || !canEdit()"
1930+ data-ng-click="createCacheSet()">
1931+ Create cache Set
1932+ </button>
1933+ <span data-ng-if="!canCreateCacheSet()">
1934+ <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id !== 'vmfs6'">Select one device to create a cache set</span>
1935+ <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id === 'vmfs6'">Not supported in the VMFS6 layout</span>
1936+ </span>
1937+ </span>
1938+ <span class="p-tooltip p-tooltip--top-center">
1939+ <button class="p-button--neutral"
1940+ data-ng-class="{ 'p-tooltip': !canCreateBcache() }"
1941+ data-ng-disabled="!canCreateBcache() || storageLayout.id === 'vmfs6'"
1942+ data-ng-hide="isAllStorageDisabled() || !canEdit()"
1943+ data-ng-click="createBcache()">
1944+ Create bcache
1945+ </button>
1946+ <span data-ng-if="!canCreateBcache()">
1947+ <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id !== 'vmfs6'">{$ getCannotCreateBcacheMsg() $}</span>
1948+ <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id === 'vmfs6'">Not supported in the VMFS6 layout</span>
1949+ </span>
1950+ </span>
1951+ <span class="p-tooltip p-tooltip--top-center">
1952+ <button class="p-button--neutral"
1953+ data-ng-click="openNewDatastorePanel()"
1954+ data-ng-hide="isAllStorageDisabled() || !canEdit()"
1955+ data-ng-disabled="!canPerformActionOnDatastoreSet()">
1956+ Create new datastore
1957+ </button>
1958+ <span data-ng-if="!canPerformActionOnDatastoreSet()">
1959+ <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id === 'vmfs6'">Select one or more devices to create a datastore</span>
1960+ <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id !== 'vmfs6'">Only available in the VMFS6 layout</span>
1961+ </span>
1962+ </span>
1963+ <span class="p-tooltip p-tooltip--top-center" data-ng-if="storageLayout.id === 'vmfs6'">
1964+ <button class="p-button--neutral"
1965+ data-ng-click="openAddToExistingDatastorePanel()"
1966+ data-ng-hide="isAllStorageDisabled() || !canEdit()"
1967+ data-ng-disabled="!canPerformActionOnDatastoreSet()">
1968+ Add to existing datastore
1969+ </button>
1970+ <span data-ng-if="!canPerformActionOnDatastoreSet()">
1971+ <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id === 'vmfs6'">Select one or more devices to add an existing datastore</span>
1972+ <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id !== 'vmfs6'">Only available in the VMFS6 layout</span>
1973+ </span>
1974+ </span>
1975+ </div>
1976 </div>
1977 </div>
1978+
1979 <div class="p-strip is-shallow">
1980 <div class="row">
1981- <hr>
1982 <h3 class="p-heading--four">Used disks and partitions</h3>
1983- <table class="p-table-expanding">
1984+ <table class="p-table-expanding p-table--used-disks">
1985 <thead>
1986- <tr>
1987- <th class="col-3">
1988- <a data-ng-click="tableInfo.column = 'name'" data-ng-class="{'p-link--soft': tableInfo.column === 'name'}">Name</a>
1989- <span class="divide"> | </span>
1990- <a data-ng-click="tableInfo.column = 'model'" data-ng-class="{'p-link--soft': tableInfo.column === 'model'}">Model</a>
1991- <span class="divide"> | </span>
1992- <a data-ng-click="tableInfo.column = 'serial'" data-ng-class="{'p-link--soft': tableInfo.column === 'serial'}">Serial</a>
1993- <span class="divide"> | </span>
1994- <a data-ng-click="tableInfo.column = 'firmware_version'" data-ng-class="{'p-link--soft': tableInfo.column === 'firmware_version'}">Firmware</a>
1995+ <tr class="p-table__row">
1996+ <th class="p-double-row p-table__cell">
1997+ <div>Name</div>
1998+ <div>Serial</div>
1999+ </th>
2000+ <th class="p-double-row p-table__cell">
2001+ <div>Model</div>
2002+ <div>Firmware</div>
2003+ </th>
2004+ <th class="p-table__cell"><div class="u-align--center">Boot</div></th>
2005+ <th class="p-table__cell">Size</th>
2006+ <th class="p-double-row p-table__cell">
2007+ <div>Type</div>
2008+ <div>Tags</div>
2009+ </th>
2010+ <th class="p-table__cell">Health</th>
2011+ <th class="p-double-row p-table__cell">
2012+ <div>Used for</div>
2013+ <div>Mount point</div>
2014 </th>
2015- <th class="col-1"><div class="u-align--center">Boot</div></th>
2016- <th class="col-2">Device type</th>
2017- <th class="col-3">Used for</th>
2018- <th class="col-2">Health</th>
2019 </tr>
2020 </thead>
2021 <tbody>
2022@@ -865,16 +1021,31 @@
2023 No disk or partition has been fully utilized.
2024 </td>
2025 </tr>
2026- <tr data-ng-repeat="item in used" class="table__row details__used">
2027- <td class="col-3" aria-label="Name">
2028- <span data-ng-show="tableInfo.column === 'name'" title="{$ item.name $}">{$ item.name $}</span>
2029- <span data-ng-show="tableInfo.column === 'model'" title="{$ item.model $}">{$ item.model $}</span>
2030- <span data-ng-show="tableInfo.column === 'serial'" title="{$ item.serial $}">{$ item.serial $}</span>
2031- <span data-ng-show="tableInfo.column === 'firmware_version'" title="{$ item.firmware_version $}">{$ item.firmware_version $}</span>
2032+ <tr data-ng-repeat="item in used" class="table__row details__used p-table__row">
2033+ <td class="p-double-row p-table__cell" aria-label="Name">
2034+ <div class="p-double-rows__container">
2035+ <div class="p-double-row__main-row">
2036+ {$ item.name $}
2037+ </div>
2038+ <div class="p-double-row__muted-row">
2039+ {$ item.serial $}
2040+ </div>
2041+ </div>
2042+ </td>
2043+ <td class="p-double-row p-table__cell">
2044+ <div class="p-double-rows__container">
2045+ <div class="p-double-row__main-row">
2046+ {$ item.model $}
2047+ </div>
2048+ <div class="p-double-row__muted-row">
2049+ {$ item.firmware_version $}
2050+ </div>
2051+ </div>
2052 </td>
2053- <td class="col-1" aria-label="Boot disk">
2054- <div class="u-align--center">
2055+ <td aria-label="Boot disk" class="p-table__cell">
2056+ <div class="u-align--center" data-ng-hide="item.parent_type !== 'vmfs6' && !item.is_boot">
2057 <input type="radio" id="{$ item.name $}-boot" name="boot-disk"
2058+ class="u-no-margin--right"
2059 data-ng-click="setAsBootDisk(item)"
2060 data-ng-checked="item.is_boot"
2061 data-ng-if="item.type === 'physical'"
2062@@ -882,19 +1053,46 @@
2063 <label for="{$ item.name $}-boot"></label>
2064 </div>
2065 </td>
2066- <td class="col-2" aria-label="Device type" title="{$ getDeviceType(item) $}">{$ getDeviceType(item) $}</td>
2067- <td class="col-3" aria-label="Used for" title="{$ item.used_for $}">{$ item.used_for $}</td>
2068- <td class="col-2" aria-label="Health">
2069+ <td class="p-table__cell">{$ item.size_human $}</td>
2070+ <td class="p-double-row p-table__cell">
2071+ <div class="p-double-rows__container">
2072+ <div class="p-double-row__main-row">
2073+ {$ getDeviceType(item) $}
2074+ </div>
2075+ <div class="p-double-row__muted-row">
2076+ <span class="table__tag" data-ng-repeat="tag in item.tags" data-ng-hide="item.$options.editingTags">
2077+ <a href="#/machines/?query=storage_tags:({$ tag.text $})" title="{$ tag.text $}">{$ tag.text $}</a>
2078+ </span>
2079+ </div>
2080+ </div>
2081+ </td>
2082+ <td aria-label="Health" class="p-table__cell">
2083 <span data-ng-if="item.type === 'physical'">
2084 <span data-maas-script-status="script-status" data-script-status="item.test_status"></span>
2085- <span data-ng-if="item.test_status === 0 || item.test_status === 1 || item.test_status === 2 || item.test_status === 5 || item.test_status === 7" title="Ok">Ok</span>
2086- <span data-ng-if="item.test_status === 3 || item.test_status === 4 || item.test_status === 8" title="Error">Error</span>
2087+ <span
2088+ data-ng-if="item.test_status === 0 || item.test_status === 1 || item.test_status === 2 || item.test_status === 5 || item.test_status === 7"
2089+ title="Ok">Ok</span>
2090+ <span data-ng-if="item.test_status === 3 || item.test_status === 4 || item.test_status === 8"
2091+ title="Error">Error</span>
2092 <span data-ng-if="item.test_status === 6" title="Degraded">Degraded</span>
2093 <span data-ng-if="item.test_status === -1" title="Unknown">Unknown</span>
2094 </span>
2095 </td>
2096+ <td class="p-double-row p-table__cell" aria-label="Used for" title="{$ item.used_for $}">
2097+ <div class="p-double-rows__container">
2098+ <div class="p-double-row__main-row">
2099+ {$ item.used_for $}
2100+ </div>
2101+ <div class="p-double-row__muted-row">
2102+ {$ item.mount_point $}
2103+ </div>
2104+ </div>
2105+ </td>
2106 </tr>
2107 </tbody>
2108 </table>
2109 </div>
2110 </div>
2111+
2112+<p>Learn more about deploying <a href="https://docs.maas.io/en/installconfig-images" class="p-link--external" target="_blank">Windows</a></p>
2113+<p>Change the default layout in <a href="/MAAS/settings/storage/">Settings &rsaquo; Storage</a></p>
2114diff --git a/src/maasserver/static/partials/nodedetails/storage/filesystems.html b/src/maasserver/static/partials/nodedetails/storage/filesystems.html
2115index 340739e..908e7aa 100644
2116--- a/src/maasserver/static/partials/nodedetails/storage/filesystems.html
2117+++ b/src/maasserver/static/partials/nodedetails/storage/filesystems.html
2118@@ -110,7 +110,7 @@
2119 </tr>
2120 </tbody>
2121 </table>
2122- <button class="p-button--neutral p-tooltip--top-center" data-ng-disabled="dropdown !== null" data-ng-class="{ 'p-tooltip': dropdown === null}"
2123+ <button class="p-button--neutral p-tooltip--top-left" data-ng-disabled="dropdown !== null" data-ng-class="{ 'p-tooltip': dropdown === null}"
2124 data-ng-if="!isAllStorageDisabled()" data-ng-click="addSpecialFilesystem()">
2125 Add special filesystem
2126 <span class="p-tooltip__message" role="tooltip">Create a tmpfs or ramfs filesystem</span>
2127diff --git a/src/maasserver/static/scss/_base_tables.scss b/src/maasserver/static/scss/_base_tables.scss
2128index 710e416..bfdd68e 100644
2129--- a/src/maasserver/static/scss/_base_tables.scss
2130+++ b/src/maasserver/static/scss/_base_tables.scss
2131@@ -40,7 +40,6 @@
2132 flex-basis: auto !important;
2133 flex-grow: 0;
2134 vertical-align: top;
2135- padding-bottom: 0.05rem;
2136
2137 &:first-of-type {
2138 padding-left: $sph-intra--condensed;
2139diff --git a/src/maasserver/static/scss/_patterns_notification.scss b/src/maasserver/static/scss/_patterns_notification.scss
2140index ab3dab2..65620d4 100644
2141--- a/src/maasserver/static/scss/_patterns_notification.scss
2142+++ b/src/maasserver/static/scss/_patterns_notification.scss
2143@@ -1,6 +1,7 @@
2144 @mixin maas-p-notifications {
2145 @include maas-notification;
2146 @include maas-notification-group;
2147+ @include maas-notification-subtle;
2148 }
2149
2150 @mixin maas-notification-group {
2151@@ -23,3 +24,14 @@
2152 }
2153 }
2154 }
2155+
2156+@mixin maas-notification-subtle {
2157+ [class*="p-notification"].is-subtle {
2158+ background: transparent;
2159+ box-shadow: none;
2160+
2161+ &::before {
2162+ height: 0;
2163+ }
2164+ }
2165+}
2166diff --git a/src/maasserver/static/scss/_tables.scss b/src/maasserver/static/scss/_tables.scss
2167index 47738fd..6194abb 100644
2168--- a/src/maasserver/static/scss/_tables.scss
2169+++ b/src/maasserver/static/scss/_tables.scss
2170@@ -326,9 +326,12 @@
2171 }
2172 }
2173
2174- .p-table--disks-partitions {
2175+ .p-table--disks-partitions,
2176+ .p-table--used-disks {
2177 .p-table__row {
2178 .p-table__cell {
2179+ flex: 0 0 auto !important;
2180+
2181 &:nth-child(1) {
2182 width: 15%;
2183 }
2184@@ -346,22 +349,58 @@
2185 }
2186
2187 &:nth-child(5) {
2188- width: 12%;
2189+ width: 22%;
2190 }
2191
2192 &:nth-child(6) {
2193+ width: 22%;
2194+ }
2195+
2196+ &:nth-child(7) {
2197 width: 10%;
2198 }
2199+ }
2200+ }
2201+ }
2202+
2203+ .p-table--used-disks {
2204+ .p-table__row {
2205+ .p-table__cell {
2206+ &:nth-child(6) {
2207+ width: 7%;
2208+ }
2209
2210 &:nth-child(7) {
2211- width: 12%;
2212+ width: 25%;
2213 }
2214+ }
2215+ }
2216+ }
2217
2218- &:nth-child(8) {
2219- width: 10%;
2220+ .p-table--datastores {
2221+ flex: 0 0 auto !important;
2222+
2223+ .p-table__row {
2224+ .p-table__cell {
2225+ flex: 0 0 auto !important;
2226+
2227+ &:nth-child(1) {
2228+ width: 15%;
2229+ }
2230+
2231+ &:nth-child(2) {
2232+ width: 22%;
2233 }
2234
2235- &:nth-child(9) {
2236+ &:nth-child(3) {
2237+ width: 9%;
2238+ }
2239+
2240+ &:nth-child(4) {
2241+ width: 44%;
2242+ }
2243+
2244+ &:nth-child(5) {
2245 width: 10%;
2246 }
2247 }
2248diff --git a/src/maasserver/static/scss/_utils.scss b/src/maasserver/static/scss/_utils.scss
2249index f6528d2..1724cf8 100644
2250--- a/src/maasserver/static/scss/_utils.scss
2251+++ b/src/maasserver/static/scss/_utils.scss
2252@@ -67,3 +67,7 @@ $table-h-indent: $sph-intra--condensed;
2253 .u-mirror--y {
2254 transform: rotate(180deg);
2255 }
2256+
2257+.u-rotate {
2258+ transform: rotate(180deg);
2259+}

Subscribers

People subscribed via source and target branches