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
diff --git a/src/maasserver/static/js/angular/controllers/node_details_storage.js b/src/maasserver/static/js/angular/controllers/node_details_storage.js
index 2ddf2e0..87edee0 100644
--- a/src/maasserver/static/js/angular/controllers/node_details_storage.js
+++ b/src/maasserver/static/js/angular/controllers/node_details_storage.js
@@ -45,12 +45,23 @@ export function removeAvailableByNew() {
45 };45 };
46}46}
4747
48export function datastoresOnly() {
49 return function(filesystems) {
50 return filesystems.filter(filesystem => {
51 return filesystem.used_for === "VMFS Datastore";
52 });
53 };
54}
55
48/* @ngInject */56/* @ngInject */
49export function NodeStorageController(57export function NodeStorageController(
50 $scope,58 $scope,
51 MachinesManager,59 MachinesManager,
52 ConverterService,60 ConverterService,
53 UsersManager61 UsersManager,
62 $log,
63 $timeout,
64 $filter
54) {65) {
55 // From models/partitiontable.py - must be kept in sync.66 // From models/partitiontable.py - must be kept in sync.
56 var INITIAL_PARTITION_OFFSET = 4 * 1024 * 1024;67 var INITIAL_PARTITION_OFFSET = 4 * 1024 * 1024;
@@ -129,6 +140,8 @@ export function NodeStorageController(
129 }140 }
130 ];141 ];
131142
143 var datastoreOnly = $filter("datastoresOnly");
144
132 $scope.tableInfo = { column: "name" };145 $scope.tableInfo = { column: "name" };
133 $scope.has_disks = false;146 $scope.has_disks = false;
134 $scope.filesystems = [];147 $scope.filesystems = [];
@@ -148,6 +161,235 @@ export function NodeStorageController(
148 $scope.nodeManager = MachinesManager;161 $scope.nodeManager = MachinesManager;
149 $scope.used = [];162 $scope.used = [];
150 $scope.showMembers = [];163 $scope.showMembers = [];
164 $scope.createNewDatastore = false;
165 $scope.addToExistingDatastore = false;
166 $scope.datastores = {
167 new: {},
168 old: {}
169 };
170 $scope.selectedAvailableDatastores = [];
171 $scope.creatingDatastore = false;
172 $scope.updatingDatastore = false;
173 $scope.updatingOSFamily = false;
174 $scope.updatingStorageLayout = false;
175 $scope.confirmStorageLayout = false;
176 $scope.newLayout = "";
177 $scope.addToDatastoreValid = false;
178
179 // XXX: Steve Rydz 09/08/2019
180 // Hardcoded for now in current cycle as no mapping exists
181 $scope.osFamilies = [
182 {
183 id: "linux",
184 name: "Linux",
185 layouts: [
186 {
187 id: "flat",
188 name: "Flat"
189 },
190 {
191 id: "lvm",
192 name: "LVM"
193 },
194 {
195 id: "bcache",
196 name: "bcache"
197 },
198 {
199 id: "vmfs6",
200 name: "VMFS6 (VMware ESXI)"
201 },
202 {
203 id: "blank",
204 name: "No storage (blank) layout"
205 }
206 ]
207 }
208 ];
209
210 $scope.osFamily = $scope.osFamilies[0];
211 $scope.storageLayout = $scope.osFamily.layouts.find(layout => {
212 return layout.id === $scope.node.detected_storage_layout;
213 });
214
215 $scope.openStorageLayoutConfirm = function(selectedLayout) {
216 $scope.osFamily.layouts.forEach(layout => {
217 if (layout.id === selectedLayout) {
218 $scope.newLayout = layout;
219 }
220 });
221 $scope.confirmStorageLayout = true;
222 };
223
224 $scope.closeStorageLayoutConfirm = function() {
225 $scope.confirmStorageLayout = false;
226 };
227
228 $scope.updateStorageLayout = function(storageLayout) {
229 storageLayout = $scope.storageLayout = $scope.newLayout;
230
231 var params = {
232 system_id: $scope.node.system_id,
233 storage_layout: storageLayout.id
234 };
235
236 $scope.updatingStorageLayout = true;
237
238 MachinesManager.applyStorageLayout(params)
239 .then(function() {
240 $timeout(function() {
241 $scope.updatingStorageLayout = false;
242 }, 0);
243 })
244 .catch(function(error) {
245 $log.error(error);
246 $timeout(function() {
247 $scope.updatingStorageLayout = false;
248 }, 0);
249 });
250
251 $scope.closeStorageLayoutConfirm();
252 };
253
254 $scope.openNewDatastorePanel = function() {
255 $scope.createNewDatastore = true;
256 var selectedDisks = $scope.getSelectedAvailable();
257 $scope.datastores.new = {
258 id: selectedDisks[0].id,
259 name: "",
260 mountpoint: selectedDisks[0].mount_point,
261 filesystem: "VMFS6",
262 size: selectedDisks[0].size_human
263 };
264 };
265
266 $scope.closeNewDatastorePanel = function() {
267 $scope.createNewDatastore = false;
268 $scope.datastores.new = {};
269 };
270
271 $scope.openAddToExistingDatastorePanel = function() {
272 $scope.addToExistingDatastore = true;
273 $scope.selectedAvailableDatastores = $scope.getSelectedAvailable();
274 $scope.datastores.old = datastoreOnly($scope.node.disks)[0];
275 };
276
277 $scope.closeAddToExistingDatastorePanel = function() {
278 $scope.addToExistingDatastore = false;
279 $scope.datastores.new = {};
280 };
281
282 $scope.canPerformActionOnDatastoreSet = function() {
283 var editing = $scope.addToExistingDatastore || $scope.createNewDatastore;
284 var selected = $scope.selectedAvailableDatastores.length > 0;
285 var vmfs6 = $scope.storageLayout.id === "vmfs6";
286 return !editing && selected && vmfs6;
287 };
288
289 $scope.createDatastore = function() {
290 $scope.createNewDatastore = true;
291
292 var selectedAvailable = $scope.getSelectedAvailable();
293 var blockDeviceIDs = [];
294 var partitionIDs = [];
295
296 selectedAvailable.forEach(function(item) {
297 if (item.type === "partition") {
298 partitionIDs.push(item.partition_id);
299 } else {
300 blockDeviceIDs.push(item.block_id);
301 }
302 });
303
304 var params = {
305 system_id: $scope.node.system_id,
306 block_devices: blockDeviceIDs,
307 partitions: partitionIDs,
308 name: $scope.datastores.new.name
309 };
310
311 $scope.creatingDatastore = true;
312
313 MachinesManager.createDatastore(params)
314 .then(function() {
315 $timeout(function() {
316 $scope.creatingDatastore = false;
317 }, 0);
318 $scope.closeNewDatastorePanel();
319 $scope.selectedAvailableDatastores = [];
320 })
321 .catch(function(error) {
322 $log.error(error);
323 $timeout(function() {
324 $scope.creatingDatastore = false;
325 }, 0);
326 });
327 };
328
329 $scope.checkAddToDatastoreValid = function() {
330 var selectedAvailable = $scope.getSelectedAvailable();
331 var valid = true;
332 if (selectedAvailable.length < 1) {
333 valid = false;
334 }
335 selectedAvailable.forEach(function(item) {
336 if (item.has_partitions) {
337 valid = false;
338 }
339 });
340 $scope.addToDatastoreValid = valid;
341 };
342
343 $scope.addToDatastore = function() {
344 var selectedAvailable = $scope.getSelectedAvailable();
345 var blockDeviceIDs = [];
346 var partitionIDs = [];
347
348 selectedAvailable.forEach(function(item) {
349 if (item.type === "partition") {
350 partitionIDs.push(item.partition_id);
351 } else {
352 blockDeviceIDs.push(item.block_id);
353 }
354 });
355
356 var params = {
357 system_id: $scope.node.system_id,
358 add_block_devices: blockDeviceIDs,
359 add_partitions: partitionIDs,
360 name: $scope.datastores.old.name,
361 vmfs_datastore_id: $scope.datastores.old.id
362 };
363
364 $scope.updatingDatastore = true;
365
366 MachinesManager.updateDatastore(params)
367 .then(function() {
368 $timeout(function() {
369 $scope.updatingDatastore = false;
370 }, 0);
371 $scope.closeAddToExistingDatastorePanel();
372 $scope.selectedAvailableDatastores = [];
373 })
374 .catch(function(error) {
375 $log.error(error);
376 $timeout(function() {
377 $scope.updatingDatastore = false;
378 }, 0);
379 });
380 };
381
382 $scope.storageLayoutIsReadOnly = function(layouts) {
383 return layouts.length <= 1;
384 };
385
386 $scope.storageLayoutIsDisabled = function(layouts) {
387 return !layouts.length;
388 };
389
390 $scope.hasStorageLayout = function(storageLayout) {
391 return storageLayout ? true : false;
392 };
151393
152 // Return True if the filesystem is mounted.394 // Return True if the filesystem is mounted.
153 function isMountedFilesystem(filesystem) {395 function isMountedFilesystem(filesystem) {
@@ -461,6 +703,7 @@ export function NodeStorageController(
461 type: disk.type,703 type: disk.type,
462 model: disk.model,704 model: disk.model,
463 serial: disk.serial,705 serial: disk.serial,
706 size_human: disk.size_human,
464 tags: getTags(disk),707 tags: getTags(disk),
465 used_for: disk.used_for,708 used_for: disk.used_for,
466 is_boot: disk.is_boot,709 is_boot: disk.is_boot,
@@ -480,6 +723,7 @@ export function NodeStorageController(
480 type: "partition",723 type: "partition",
481 model: "",724 model: "",
482 serial: "",725 serial: "",
726 size_human: partition.size_human,
483 tags: [],727 tags: [],
484 used_for: partition.used_for,728 used_for: partition.used_for,
485 is_boot: false729 is_boot: false
@@ -776,6 +1020,9 @@ export function NodeStorageController(
776 } else if (filesystem.original_type === "partition") {1020 } else if (filesystem.original_type === "partition") {
777 // Delete the partition.1021 // Delete the partition.
778 MachinesManager.deletePartition($scope.node, filesystem.original.id);1022 MachinesManager.deletePartition($scope.node, filesystem.original.id);
1023 } else if (filesystem.parent_type === "vmfs6") {
1024 // Delete the datastore.
1025 MachinesManager.deleteDisk($scope.node, filesystem.id);
779 } else {1026 } else {
780 // Delete the disk.1027 // Delete the disk.
781 MachinesManager.deleteFilesystem(1028 MachinesManager.deleteFilesystem(
@@ -878,6 +1125,8 @@ export function NodeStorageController(
878 $scope.toggleAvailableSelect = function(disk) {1125 $scope.toggleAvailableSelect = function(disk) {
879 disk.$selected = !disk.$selected;1126 disk.$selected = !disk.$selected;
880 $scope.updateAvailableSelection(true);1127 $scope.updateAvailableSelection(true);
1128 $scope.selectedAvailableDatastores = $scope.getSelectedAvailable();
1129 $scope.checkAddToDatastoreValid();
881 };1130 };
8821131
883 // Toggle the selection of all available disks.1132 // Toggle the selection of all available disks.
@@ -1592,24 +1841,24 @@ export function NodeStorageController(
1592 };1841 };
15931842
1594 // Return true when the name of the new disk is invalid.1843 // Return true when the name of the new disk is invalid.
1595 $scope.isNewDiskNameInvalid = function() {1844 $scope.isNewDiskNameInvalid = function(newDiskName) {
1596 if (!angular.isObject($scope.node) || !angular.isArray($scope.node.disks)) {1845 if (!angular.isObject($scope.node) || !angular.isArray($scope.node.disks)) {
1597 return true;1846 return true;
1598 }1847 }
15991848
1600 if ($scope.availableNew.name === "") {1849 if (newDiskName === "") {
1601 return true;1850 return true;
1602 } else {1851 } else {
1603 var i, j;1852 var i, j;
1604 for (i = 0; i < $scope.node.disks.length; i++) {1853 for (i = 0; i < $scope.node.disks.length; i++) {
1605 var disk = $scope.node.disks[i];1854 var disk = $scope.node.disks[i];
1606 if ($scope.availableNew.name === disk.name) {1855 if (newDiskName === disk.name) {
1607 return true;1856 return true;
1608 }1857 }
1609 if (angular.isArray(disk.partitions)) {1858 if (angular.isArray(disk.partitions)) {
1610 for (j = 0; j < disk.partitions.length; j++) {1859 for (j = 0; j < disk.partitions.length; j++) {
1611 var partition = disk.partitions[j];1860 var partition = disk.partitions[j];
1612 if ($scope.availableNew.name === partition.name) {1861 if (newDiskName === partition.name) {
1613 return true;1862 return true;
1614 }1863 }
1615 }1864 }
@@ -1622,7 +1871,7 @@ export function NodeStorageController(
1622 // Return true if bcache can be saved.1871 // Return true if bcache can be saved.
1623 $scope.createBcacheCanSave = function() {1872 $scope.createBcacheCanSave = function() {
1624 return (1873 return (
1625 !$scope.isNewDiskNameInvalid() &&1874 !$scope.isNewDiskNameInvalid($scope.availableNew.name) &&
1626 !$scope.isMountPointInvalid($scope.availableNew.mountPoint)1875 !$scope.isMountPointInvalid($scope.availableNew.mountPoint)
1627 );1876 );
1628 };1877 };
@@ -1831,7 +2080,7 @@ export function NodeStorageController(
1831 // Return true if RAID can be saved.2080 // Return true if RAID can be saved.
1832 $scope.createRAIDCanSave = function() {2081 $scope.createRAIDCanSave = function() {
1833 return (2082 return (
1834 !$scope.isNewDiskNameInvalid() &&2083 !$scope.isNewDiskNameInvalid($scope.availableNew.name) &&
1835 !$scope.isMountPointInvalid($scope.availableNew.mountPoint)2084 !$scope.isMountPointInvalid($scope.availableNew.mountPoint)
1836 );2085 );
1837 };2086 };
@@ -1944,7 +2193,7 @@ export function NodeStorageController(
19442193
1945 // Return true if volume group can be saved.2194 // Return true if volume group can be saved.
1946 $scope.createVolumeGroupCanSave = function() {2195 $scope.createVolumeGroupCanSave = function() {
1947 return !$scope.isNewDiskNameInvalid();2196 return !$scope.isNewDiskNameInvalid($scope.availableNew.name);
1948 };2197 };
19492198
1950 // Confirm and create the volume group device.2199 // Confirm and create the volume group device.
diff --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
index f267fa8..023c970 100644
--- 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
@@ -431,6 +431,7 @@ describe("NodeStorageController", function() {
431 type: disks[2].type,431 type: disks[2].type,
432 model: disks[2].model,432 model: disks[2].model,
433 serial: disks[2].serial,433 serial: disks[2].serial,
434 size_human: disks[2].size_human,
434 tags: disks[2].tags,435 tags: disks[2].tags,
435 used_for: disks[2].used_for,436 used_for: disks[2].used_for,
436 has_partitions: false,437 has_partitions: false,
@@ -443,6 +444,7 @@ describe("NodeStorageController", function() {
443 type: disks[3].type,444 type: disks[3].type,
444 model: disks[3].model,445 model: disks[3].model,
445 serial: disks[3].serial,446 serial: disks[3].serial,
447 size_human: disks[3].size_human,
446 tags: disks[3].tags,448 tags: disks[3].tags,
447 used_for: disks[3].used_for,449 used_for: disks[3].used_for,
448 has_partitions: true,450 has_partitions: true,
@@ -455,6 +457,7 @@ describe("NodeStorageController", function() {
455 type: "partition",457 type: "partition",
456 model: "",458 model: "",
457 serial: "",459 serial: "",
460 size_human: disks[3].partitions[1].size_human,
458 tags: [],461 tags: [],
459 used_for: disks[3].partitions[1].used_for462 used_for: disks[3].partitions[1].used_for
460 }463 }
@@ -3597,9 +3600,8 @@ describe("NodeStorageController", function() {
3597 it("returns true if blank name", function() {3600 it("returns true if blank name", function() {
3598 makeController();3601 makeController();
3599 $scope.node.disks = [];3602 $scope.node.disks = [];
3600 $scope.availableNew.name = "";
36013603
3602 expect($scope.isNewDiskNameInvalid()).toBe(true);3604 expect($scope.isNewDiskNameInvalid("")).toBe(true);
3603 });3605 });
36043606
3605 it("returns true if name used by disk", function() {3607 it("returns true if name used by disk", function() {
@@ -3610,9 +3612,8 @@ describe("NodeStorageController", function() {
3610 name: name3612 name: name
3611 }3613 }
3612 ];3614 ];
3613 $scope.availableNew.name = name;
36143615
3615 expect($scope.isNewDiskNameInvalid()).toBe(true);3616 expect($scope.isNewDiskNameInvalid(name)).toBe(true);
3616 });3617 });
36173618
3618 it("returns true if name used by partition", function() {3619 it("returns true if name used by partition", function() {
@@ -3628,9 +3629,8 @@ describe("NodeStorageController", function() {
3628 ]3629 ]
3629 }3630 }
3630 ];3631 ];
3631 $scope.availableNew.name = name;
36323632
3633 expect($scope.isNewDiskNameInvalid()).toBe(true);3633 expect($scope.isNewDiskNameInvalid(name)).toBe(true);
3634 });3634 });
36353635
3636 it("returns false if the name is not already used", function() {3636 it("returns false if the name is not already used", function() {
@@ -3646,9 +3646,8 @@ describe("NodeStorageController", function() {
3646 ]3646 ]
3647 }3647 }
3648 ];3648 ];
3649 $scope.availableNew.name = name;
36503649
3651 expect($scope.isNewDiskNameInvalid()).toBe(false);3650 expect($scope.isNewDiskNameInvalid(name)).toBe(false);
3652 });3651 });
3653 });3652 });
36543653
@@ -5073,4 +5072,307 @@ describe("NodeStorageController", function() {
5073 expect($scope.hasStorageLayoutIssues()).toBe(false);5072 expect($scope.hasStorageLayoutIssues()).toBe(false);
5074 });5073 });
5075 });5074 });
5075
5076 describe("openStorageLayoutConfirm", function() {
5077 it("sets 'confirmStorageLayout' to true", function() {
5078 makeController();
5079 $scope.confirmStorageLayout = false;
5080 $scope.osFamilies = [
5081 {
5082 id: "linux",
5083 name: "Linux",
5084 layouts: [
5085 {
5086 id: "flat",
5087 name: "Flat"
5088 },
5089 {
5090 id: "lvm",
5091 name: "LVM"
5092 },
5093 {
5094 id: "bcache",
5095 name: "bcache"
5096 },
5097 {
5098 id: "vmfs6",
5099 name: "VMFS6 (VMware ESXI)"
5100 },
5101 {
5102 id: "blank",
5103 name: "No storage (blank) layout"
5104 }
5105 ]
5106 }
5107 ];
5108 $scope.openStorageLayoutConfirm("flat");
5109 expect($scope.confirmStorageLayout).toBe(true);
5110 });
5111
5112 it("sets 'newLayout' to layout argument", function() {
5113 makeController();
5114 $scope.osFamilies = [
5115 {
5116 id: "linux",
5117 name: "Linux",
5118 layouts: [
5119 {
5120 id: "flat",
5121 name: "Flat"
5122 },
5123 {
5124 id: "lvm",
5125 name: "LVM"
5126 },
5127 {
5128 id: "bcache",
5129 name: "bcache"
5130 },
5131 {
5132 id: "vmfs6",
5133 name: "VMFS6 (VMware ESXI)"
5134 },
5135 {
5136 id: "blank",
5137 name: "No storage (blank) layout"
5138 }
5139 ]
5140 }
5141 ];
5142 $scope.openStorageLayoutConfirm("flat");
5143 expect($scope.newLayout).toEqual($scope.osFamilies[0].layouts[0]);
5144 });
5145 });
5146
5147 describe("closeStorageLayoutConfirm", function() {
5148 it("sets 'confirmStorageLayout' to false", function() {
5149 makeController();
5150 $scope.confirmStorageLayout = true;
5151 $scope.closeStorageLayoutConfirm();
5152 expect($scope.confirmStorageLayout).toBe(false);
5153 });
5154 });
5155
5156 describe("updateStorageLayout", function() {
5157 it("calls 'applyStorageLayout'", function() {
5158 makeController();
5159 spyOn(MachinesManager, "applyStorageLayout").and.callFake(function() {
5160 var deferred = $q.defer();
5161 return deferred.promise;
5162 });
5163 $scope.newLayout = {
5164 id: "flat",
5165 name: "Flat"
5166 };
5167 $scope.updateStorageLayout($scope.newLayout);
5168 expect(MachinesManager.applyStorageLayout).toHaveBeenCalled();
5169 });
5170
5171 it("calls 'closeStorageLayoutConfirm'", function() {
5172 makeController();
5173 spyOn(MachinesManager, "applyStorageLayout").and.callFake(function() {
5174 var deferred = $q.defer();
5175 return deferred.promise;
5176 });
5177 spyOn($scope, "closeStorageLayoutConfirm");
5178 $scope.updateStorageLayout({
5179 id: "flat",
5180 name: "Flat"
5181 });
5182 expect($scope.closeStorageLayoutConfirm).toHaveBeenCalled();
5183 });
5184 });
5185
5186 describe("openNewDatastorePanel", function() {
5187 it("sets 'createNewDatastore' to true", function() {
5188 makeController();
5189 $scope.createNewDatastore = false;
5190 $scope.available = [
5191 {
5192 $selected: true,
5193 id: 1
5194 }
5195 ];
5196 $scope.openNewDatastorePanel();
5197 expect($scope.createNewDatastore).toBe(true);
5198 });
5199
5200 it("sets newDatastore", function() {
5201 makeController();
5202 $scope.available = [
5203 {
5204 $selected: true,
5205 id: 1,
5206 mount_point: "dev/null",
5207 size_human: "35 GB"
5208 }
5209 ];
5210 $scope.openNewDatastorePanel();
5211 expect($scope.datastores.new).toEqual({
5212 id: $scope.available[0].id,
5213 name: "",
5214 mountpoint: $scope.available[0].mount_point,
5215 filesystem: "VMFS6",
5216 size: $scope.available[0].size_human
5217 });
5218 });
5219 });
5220
5221 describe("closeNewDatastorePanel", function() {
5222 it("sets 'createNewDatastore' to false", function() {
5223 makeController();
5224 $scope.createNewDatastore = true;
5225 $scope.closeNewDatastorePanel();
5226 expect($scope.createNewDatastore).toBe(false);
5227 });
5228
5229 it("sets 'newDatastore' to '{}'", function() {
5230 makeController();
5231 $scope.datastores.new = { id: 1, name: "" };
5232 $scope.closeNewDatastorePanel();
5233 expect($scope.datastores.new).toEqual({});
5234 });
5235 });
5236
5237 describe("canPerformActionOnDatastoreSet", function() {
5238 it("return false if not on vmsf6 storage layout", function() {
5239 makeController();
5240 $scope.addToExistingDatastore = false;
5241 $scope.createNewDatastore = false;
5242 $scope.selectedAvailableDatastores = [1];
5243 $scope.storageLayout = { id: "flat" };
5244 expect($scope.canPerformActionOnDatastoreSet()).toBe(false);
5245 });
5246
5247 it("return false if already editing datastores", function() {
5248 makeController();
5249 $scope.addToExistingDatastore = false;
5250 $scope.createNewDatastore = true;
5251 $scope.selectedAvailableDatastores = [1];
5252 $scope.storageLayout = { id: "vmfs6" };
5253 expect($scope.canPerformActionOnDatastoreSet()).toBe(false);
5254 });
5255
5256 it("return false if no device is selected", function() {
5257 makeController();
5258 $scope.addToExistingDatastore = false;
5259 $scope.createNewDatastore = false;
5260 $scope.selectedAvailableDatastores = [];
5261 $scope.storageLayout = { id: "vmfs6" };
5262 expect($scope.canPerformActionOnDatastoreSet()).toBe(false);
5263 });
5264
5265 it("return true when conditions are matched", function() {
5266 makeController();
5267 $scope.addToExistingDatastore = false;
5268 $scope.createNewDatastore = false;
5269 $scope.selectedAvailableDatastores = [1];
5270 $scope.storageLayout = { id: "vmfs6" };
5271 expect($scope.canPerformActionOnDatastoreSet()).toBe(true);
5272 });
5273 });
5274
5275 describe("checkAddToDatastoreValid", function() {
5276 it("selected disks are valid when that condition is true", function() {
5277 makeController();
5278 var selected = {
5279 has_partitions: false
5280 };
5281 spyOn($scope, "getSelectedAvailable").and.returnValue([selected]);
5282 expect($scope.addToDatastoreValid).toBe(false);
5283 $scope.checkAddToDatastoreValid();
5284 expect($scope.addToDatastoreValid).toBe(true);
5285 });
5286
5287 it("selected disks are not valid disk has a partition", function() {
5288 makeController();
5289 var selected = {
5290 has_partitions: true
5291 };
5292 spyOn($scope, "getSelectedAvailable").and.returnValue([selected]);
5293 expect($scope.addToDatastoreValid).toBe(false);
5294 $scope.checkAddToDatastoreValid();
5295 expect($scope.addToDatastoreValid).toBe(false);
5296 });
5297
5298 it("selected disks are not valid when no selected disks", function() {
5299 makeController();
5300 spyOn($scope, "getSelectedAvailable").and.returnValue([]);
5301 expect($scope.addToDatastoreValid).toBe(false);
5302 $scope.checkAddToDatastoreValid();
5303 expect($scope.addToDatastoreValid).toBe(false);
5304 });
5305 });
5306
5307 describe("openAddToExistingDatastorePanel", function() {
5308 it("sets 'addToExistingDatastore' to true", function() {
5309 makeController();
5310 $scope.addToExistingDatastore = false;
5311 $scope.available = [
5312 {
5313 $selected: true,
5314 id: 1
5315 }
5316 ];
5317 $scope.openAddToExistingDatastorePanel();
5318 expect($scope.addToExistingDatastore).toBe(true);
5319 });
5320
5321 it("sets 'selectedAvailableDatastores' to selected", function() {
5322 makeController();
5323 $scope.datastores.old = [
5324 {
5325 $selected: true,
5326 id: 1
5327 }
5328 ];
5329 $scope.openAddToExistingDatastorePanel();
5330 expect($scope.selectedAvailableDatastores).toEqual($scope.available);
5331 });
5332
5333 it("sets 'datastores.old' to first disk", function() {
5334 makeController();
5335 $scope.openAddToExistingDatastorePanel();
5336 expect($scope.datastores.old).toBe($scope.node.disks[0]);
5337 });
5338 });
5339
5340 describe("closeAddToExistingDatastorePanel", function() {
5341 it("sets 'addToExistingDatastore' to false", function() {
5342 makeController();
5343 $scope.addToExistingDatastore = true;
5344 $scope.closeAddToExistingDatastorePanel();
5345 expect($scope.addToExistingDatastore).toBe(false);
5346 });
5347
5348 it("sets, 'newDatasore' to '{}'", function() {
5349 makeController();
5350 $scope.datastores.new = { id: 1, name: "" };
5351 $scope.closeAddToExistingDatastorePanel();
5352 expect($scope.datastores.new).toEqual({});
5353 });
5354 });
5355
5356 describe("createDatastore", function() {
5357 it("sets 'createNewDatastore' to true", function() {
5358 makeController();
5359 spyOn(MachinesManager, "createDatastore").and.callFake(function() {
5360 var deferred = $q.defer();
5361 return deferred.promise;
5362 });
5363 $scope.createNewDatastore = false;
5364 $scope.createDatastore();
5365 expect($scope.createNewDatastore).toBe(true);
5366 });
5367
5368 it("calls 'MachinesManager.createDatastore'", function() {
5369 makeController();
5370 spyOn(MachinesManager, "createDatastore").and.callFake(function() {
5371 var deferred = $q.defer();
5372 return deferred.promise;
5373 });
5374 $scope.createDatastore();
5375 expect(MachinesManager.createDatastore).toHaveBeenCalled();
5376 });
5377 });
5076});5378});
diff --git a/src/maasserver/static/js/angular/directives/nodedetails/storage_datastores.js b/src/maasserver/static/js/angular/directives/nodedetails/storage_datastores.js
5077new file mode 1006445379new file mode 100644
index 0000000..d040063
--- /dev/null
+++ b/src/maasserver/static/js/angular/directives/nodedetails/storage_datastores.js
@@ -0,0 +1,9 @@
1function storageDatastores() {
2 const path = "static/partials/nodedetails/storage/datastores.html";
3 return {
4 restrict: "E",
5 templateUrl: `${path}?v=${MAAS_config.files_version}`
6 };
7}
8
9export default storageDatastores;
diff --git a/src/maasserver/static/js/angular/entry.js b/src/maasserver/static/js/angular/entry.js
index 0cf83dc..cea8d08 100644
--- a/src/maasserver/static/js/angular/entry.js
+++ b/src/maasserver/static/js/angular/entry.js
@@ -36,7 +36,8 @@ import {
36} from "./controllers/node_details_networking"; // TODO: fix export/namespace36} from "./controllers/node_details_networking"; // TODO: fix export/namespace
37// prettier-ignore37// prettier-ignore
38import {38import {
39 removeAvailableByNew39 removeAvailableByNew,
40 datastoresOnly
40} from "./controllers/node_details_storage"; // TODO: fix export/namespace41} from "./controllers/node_details_storage"; // TODO: fix export/namespace
41// prettier-ignore42// prettier-ignore
42import {43import {
@@ -152,6 +153,7 @@ import ZonesListController from "./controllers/zones_list";
152import storageDisksPartitions153import storageDisksPartitions
153 from "./directives/nodedetails/storage_disks_partitions";154 from "./directives/nodedetails/storage_disks_partitions";
154import storageFilesystems from "./directives/nodedetails/storage_filesystems";155import storageFilesystems from "./directives/nodedetails/storage_filesystems";
156import storageDatastores from "./directives/nodedetails/storage_datastores";
155import maasMachinesTable from "./directives/machines_table";157import maasMachinesTable from "./directives/machines_table";
156import addMachine from "./directives/nodelist/add_machine";158import addMachine from "./directives/nodelist/add_machine";
157import maasAccordion from "./directives/accordion";159import maasAccordion from "./directives/accordion";
@@ -493,6 +495,7 @@ angular
493 .filter("removeDefaultVLANIfVLAN", removeDefaultVLANIfVLAN)495 .filter("removeDefaultVLANIfVLAN", removeDefaultVLANIfVLAN)
494 .filter("filterLinkModes", filterLinkModes)496 .filter("filterLinkModes", filterLinkModes)
495 .filter("removeAvailableByNew", removeAvailableByNew)497 .filter("removeAvailableByNew", removeAvailableByNew)
498 .filter("datastoresOnly", datastoresOnly)
496 .filter("filterSource", filterSource)499 .filter("filterSource", filterSource)
497 .filter("ignoreSelf", ignoreSelf)500 .filter("ignoreSelf", ignoreSelf)
498 .filter("removeNoDHCP", removeNoDHCP)501 .filter("removeNoDHCP", removeNoDHCP)
@@ -594,6 +597,7 @@ angular
594 // directives597 // directives
595 .directive("storageDisksPartitions", storageDisksPartitions)598 .directive("storageDisksPartitions", storageDisksPartitions)
596 .directive("storageFilesystems", storageFilesystems)599 .directive("storageFilesystems", storageFilesystems)
600 .directive("storageDatastores", storageDatastores)
597 .directive("addMachine", addMachine)601 .directive("addMachine", addMachine)
598 .directive("maasAccordion", maasAccordion)602 .directive("maasAccordion", maasAccordion)
599 .directive("maasActionButton", maasActionButton)603 .directive("maasActionButton", maasActionButton)
diff --git a/src/maasserver/static/js/angular/factories/machines.js b/src/maasserver/static/js/angular/factories/machines.js
index c3c3a5b..e70b096 100644
--- a/src/maasserver/static/js/angular/factories/machines.js
+++ b/src/maasserver/static/js/angular/factories/machines.js
@@ -77,6 +77,21 @@ function MachinesManager(RegionConnection, NodesManager) {
77 return RegionConnection.callMethod(method, params);77 return RegionConnection.callMethod(method, params);
78 };78 };
7979
80 MachinesManager.prototype.applyStorageLayout = function(params) {
81 var method = this._handler + ".apply_storage_layout";
82 return RegionConnection.callMethod(method, params);
83 };
84
85 MachinesManager.prototype.createDatastore = function(params) {
86 var method = this._handler + ".create_vmfs_datastore";
87 return RegionConnection.callMethod(method, params);
88 };
89
90 MachinesManager.prototype.updateDatastore = function(params) {
91 var method = this._handler + ".update_vmfs_datastore";
92 return RegionConnection.callMethod(method, params);
93 };
94
80 return new MachinesManager();95 return new MachinesManager();
81}96}
8297
diff --git a/src/maasserver/static/js/angular/factories/tests/test_machines.js b/src/maasserver/static/js/angular/factories/tests/test_machines.js
index 26eff00..9d4f7b6 100644
--- a/src/maasserver/static/js/angular/factories/tests/test_machines.js
+++ b/src/maasserver/static/js/angular/factories/tests/test_machines.js
@@ -87,4 +87,56 @@ describe("MachinesManager", function() {
87 );87 );
88 });88 });
89 });89 });
90
91 describe("applyStorageLayout", function() {
92 it("calls apply_storage_layout", function() {
93 spyOn(RegionConnection, "callMethod");
94 var params = {
95 system_id: makeName("system-id"),
96 mount_point: makeName("/dir")
97 };
98 MachinesManager.applyStorageLayout(params);
99 expect(RegionConnection.callMethod).toHaveBeenCalledWith(
100 "machine.apply_storage_layout",
101 params
102 );
103 });
104 });
105
106 describe("createDatastore", function() {
107 it("calls create_vmfs_datastore", function() {
108 spyOn(RegionConnection, "callMethod");
109 var params = {
110 system_id: makeName("system-id"),
111 block_devices: [1, 2, 3, 5],
112 partitions: [5, 6, 7, 8],
113 name: "New datastore"
114 };
115 MachinesManager.createDatastore(params);
116 expect(RegionConnection.callMethod).toHaveBeenCalledWith(
117 "machine.create_vmfs_datastore",
118 params
119 );
120 });
121 });
122
123 describe("updateDatastore", function() {
124 it("calls update_vmfs_datastore", function() {
125 spyOn(RegionConnection, "callMethod");
126 var params = {
127 system_id: makeName("system-id"),
128 add_block_devices: [1, 2, 3, 4],
129 add_partitions: [5, 6, 7, 8],
130 remove_partitions: [],
131 remove_block_devices: [],
132 name: "New datastore",
133 vmfs_datastore_id: 1
134 };
135 MachinesManager.updateDatastore(params);
136 expect(RegionConnection.callMethod).toHaveBeenCalledWith(
137 "machine.update_vmfs_datastore",
138 params
139 );
140 });
141 });
90});142});
diff --git a/src/maasserver/static/partials/node-details.html b/src/maasserver/static/partials/node-details.html
index d722404..5612ef2 100755
--- a/src/maasserver/static/partials/node-details.html
+++ b/src/maasserver/static/partials/node-details.html
@@ -69,7 +69,7 @@
69 <p class="page-header__message"><i class="p-icon--warning">Warning:</i> MAAS is not providing DHCP.</p>69 <p class="page-header__message"><i class="p-icon--warning">Warning:</i> MAAS is not providing DHCP.</p>
70 </div>70 </div>
71 </div>71 </div>
72 <div class="row ng-hide u-no-margin--top" data-ng-hide="isActionError() || isDeployError() || hasActionPowerError(action.option.name)">72 <div class="row ng-hide u-no-margin--top" data-ng-hide="isActionError() || isDeployError() || isSSHKeyError() || hasActionPowerError(action.option.name)">
73 <!-- XXX blake_r 2015-02-19 - Need to add e2e test. -->73 <!-- XXX blake_r 2015-02-19 - Need to add e2e test. -->
74 <div class="page-header__section">74 <div class="page-header__section">
75 <form class="p-form">75 <form class="p-form">
@@ -110,7 +110,7 @@
110 </label>110 </label>
111 <span data-ng-if="!nodesManager.isModernUbuntu(osSelection)">111 <span data-ng-if="!nodesManager.isModernUbuntu(osSelection)">
112 <i class="p-icon--warning"></i>112 <i class="p-icon--warning"></i>
113 <strong>Warning:</strong> Ubuntu 18.04 is the minimum required to create a KVM host. <a target="_blank"113 <strong>Warning:</strong> Ubuntu 18.04 is the minimum required. <a target="_blank"
114 class="p-link--external"114 class="p-link--external"
115 href="https://docs.maas.io/2.5/en/manage-pods-webui#add-a-kvm-host">Learn more</a>115 href="https://docs.maas.io/2.5/en/manage-pods-webui#add-a-kvm-host">Learn more</a>
116 </span>116 </span>
@@ -122,11 +122,6 @@
122 </div>122 </div>
123 </div>123 </div>
124 </div>124 </div>
125 <div data-ng-if="isSSHKeyWarning()" class="p-strip is-shallow u-no-padding--top">
126 <p class="u-remove-max-width">
127 <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>.
128 </p>
129 </div>
130 </div>125 </div>
131 </div>126 </div>
132 </div>127 </div>
@@ -172,7 +167,7 @@
172 </form>167 </form>
173 </div>168 </div>
174 </div>169 </div>
175 <div class="row ng-hide" data-ng-hide="isActionError() || isDeployError() || hasActionPowerError(action.option.name) || action.option.name !== 'commission'" data-ng-if="hasCustomCommissioningScripts()">170 <div class="row ng-hide" data-ng-hide="isActionError() || isDeployError() || isSSHKeyError() || hasActionPowerError(action.option.name) || action.option.name !== 'commission'" data-ng-if="hasCustomCommissioningScripts()">
176 <div class="page-header__section">171 <div class="page-header__section">
177 <form class="p-form">172 <form class="p-form">
178 <div class="col-8">173 <div class="col-8">
@@ -184,7 +179,7 @@
184 </form>179 </form>
185 </div>180 </div>
186 </div>181 </div>
187 <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')">182 <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')">
188 <hr />183 <hr />
189 <div class="page-header__section">184 <div class="page-header__section">
190 <form class="p-form">185 <form class="p-form">
@@ -197,7 +192,7 @@
197 </form>192 </form>
198 </div>193 </div>
199 </div>194 </div>
200 <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">195 <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">
201 <hr />196 <hr />
202 <div class="page-header__section col-12">197 <div class="page-header__section col-12">
203 <form class="p-form">198 <form class="p-form">
@@ -219,18 +214,18 @@
219 <div data-ng-repeat="confirmation_detail in action.confirmation_details">214 <div data-ng-repeat="confirmation_detail in action.confirmation_details">
220 <span>{$ confirmation_detail $}</span>215 <span>{$ confirmation_detail $}</span>
221 </div>216 </div>
222 <div class="u-equal-height">217 <div class="u-equal-height">
223 <div class="col-9 u-vertically-center">218 <div class="col-9 u-vertically-center">
224 <p class="u-remove-max-width">219 <p class="u-remove-max-width">
225 Are you sure you want to {$ action.actionOption.name $} this {$ type_name $}?220 Are you sure you want to {$ action.actionOption.name $} this {$ type_name $}?
226 </p>221 </p>
227 </div>222 </div>
228 <div class="col-3">223 <div class="col-3">
229 <div class="u-align--right">224 <div class="u-align--right">
230 <button class="p-button--base" data-ng-click="actionCancel()">No</button>225 <button class="p-button--base" data-ng-click="actionCancel()">No</button>
231 <button class="p-button--negative" data-ng-click="actionGo()">Yes</button>226 <button class="p-button--negative" data-ng-click="actionGo()">Yes</button>
232 </div>227 </div>
233 </div>228 </div>
234 </div>229 </div>
235 </div>230 </div>
236 </div>231 </div>
@@ -273,6 +268,18 @@
273 </div>268 </div>
274 </div>269 </div>
275 </div>270 </div>
271 <div class="row u-equal-height ng-hide" data-ng-show="!isDeployError() && isSSHKeyError()">
272 <div class="col-9 u-vertically-center">
273 <p class="u-remove-max-width">
274 <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>.
275 </p>
276 </div>
277 <div class="col-3">
278 <div class="u-align--right">
279 <button class="p-button--base" data-ng-click="actionCancel()">Cancel</button>
280 </div>
281 </div>
282 </div>
276 </div>283 </div>
277284
278 <nav class="p-tabs u-hr--fixed-width">285 <nav class="p-tabs u-hr--fixed-width">
@@ -394,97 +401,97 @@
394 </div>401 </div>
395 <div class="p-strip is-shallow">402 <div class="p-strip is-shallow">
396 <div class="row u-equal-height" data-ng-if="isDevice">403 <div class="row u-equal-height" data-ng-if="isDevice">
397 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">404 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
398 <div maas-card-loader="device"></div>405 <div maas-card-loader="device"></div>
399 </div>406 </div>
400 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">407 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
401 <div maas-card-loader="tags"></div>408 <div maas-card-loader="tags"></div>
402 </div>409 </div>
403 </div>410 </div>
404 <div class="row u-equal-height" data-ng-if="node.node_type == 3">411 <div class="row u-equal-height" data-ng-if="node.node_type == 3">
405 <div class="col-4 p-card--highlighted action-card u-border--solid">412 <div class="col-4 p-card--highlighted action-card u-border--solid">
406 <div maas-card-loader="services"></div>413 <div maas-card-loader="services"></div>
407 </div>414 </div>
408 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">415 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
409 <div maas-card-loader="controller"></div>416 <div maas-card-loader="controller"></div>
410 </div>417 </div>
411 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">418 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
412 <div maas-card-loader="hardware_info"></div>419 <div maas-card-loader="hardware_info"></div>
413 </div>420 </div>
414 </div>421 </div>
415 <div class="row u-equal-height" data-ng-if="node.node_type == 3">422 <div class="row u-equal-height" data-ng-if="node.node_type == 3">
416 <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 }">423 <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 }">
417 <div maas-card-loader="cpu"></div>424 <div maas-card-loader="cpu"></div>
418 </div>425 </div>
419 <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 }">426 <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 }">
420 <div maas-card-loader="memory"></div>427 <div maas-card-loader="memory"></div>
421 </div>428 </div>
422 <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 }">429 <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 }">
423 <div maas-card-loader="storage"></div>430 <div maas-card-loader="storage"></div>
424 </div>431 </div>
425 </div>432 </div>
426 <div class="row u-equal-height" data-ng-if="node.node_type == 3">433 <div class="row u-equal-height" data-ng-if="node.node_type == 3">
427 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">434 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
428 <div maas-card-loader="tags"></div>435 <div maas-card-loader="tags"></div>
429 </div>436 </div>
430 </div>437 </div>
431 <div class="row u-equal-height" data-ng-if="node.node_type == 2 || node.node_type == 4">438 <div class="row u-equal-height" data-ng-if="node.node_type == 2 || node.node_type == 4">
432 <div class="col-4 p-card--highlighted action-card u-border--solid">439 <div class="col-4 p-card--highlighted action-card u-border--solid">
433 <div maas-card-loader="services"></div>440 <div maas-card-loader="services"></div>
434 </div>441 </div>
435 <div class="col-8">442 <div class="col-8">
436 <div class="row u-equal-height">443 <div class="row u-equal-height">
437 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">444 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
438 <div maas-card-loader="images"></div>445 <div maas-card-loader="images"></div>
439 </div>446 </div>
440 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">447 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
441 <div maas-card-loader="controller"></div>448 <div maas-card-loader="controller"></div>
442 </div>449 </div>
443 </div>450 </div>
444 <div class="row u-equal-height">451 <div class="row u-equal-height">
445 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">452 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
446 <div maas-card-loader="hardware_info"></div>453 <div maas-card-loader="hardware_info"></div>
447 </div>454 </div>
448 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">455 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid">
449 <div maas-card-loader="tags"></div>456 <div maas-card-loader="tags"></div>
450 </div>457 </div>
458 </div>
451 </div>459 </div>
452 </div>
453 </div>460 </div>
454 <div class="row u-equal-height" data-ng-if="node.node_type == 2 || node.node_type == 4">461 <div class="row u-equal-height" data-ng-if="node.node_type == 2 || node.node_type == 4">
455 <div class="col-4 p-card--highlighted action-card u-border--solid" data-ng-class="{ 'is-error': node.cpu_test_status === 3 }">462 <div class="col-4 p-card--highlighted action-card u-border--solid" data-ng-class="{ 'is-error': node.cpu_test_status === 3 }">
456 <div maas-card-loader="cpu"></div>463 <div maas-card-loader="cpu"></div>
457 </div>464 </div>
458 <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 }">465 <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 }">
459 <div maas-card-loader="memory"></div>466 <div maas-card-loader="memory"></div>
460 </div>467 </div>
461 <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 }">468 <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 }">
462 <div maas-card-loader="storage"></div>469 <div maas-card-loader="storage"></div>
463 </div>470 </div>
464 </div>471 </div>
465 <div class="row u-equal-height" data-ng-if="!isDevice && !isController">472 <div class="row u-equal-height" data-ng-if="!isDevice && !isController">
466 <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 }">473 <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 }">
467 <div maas-card-loader="cpu"></div>474 <div maas-card-loader="cpu"></div>
468 </div>475 </div>
469 <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 }">476 <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 }">
470 <div maas-card-loader="memory"></div>477 <div maas-card-loader="memory"></div>
471 </div>478 </div>
472 <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 }">479 <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 }">
473 <div maas-card-loader="storage"></div>480 <div maas-card-loader="storage"></div>
474 </div>481 </div>
475 </div>482 </div>
476 <div class="row u-equal-height" data-ng-if="!isDevice && !isController">483 <div class="row u-equal-height" data-ng-if="!isDevice && !isController">
477 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController">484 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController">
478 <div maas-card-loader="machine"></div>485 <div maas-card-loader="machine"></div>
479 </div>486 </div>
480 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController">487 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController">
481 <div maas-card-loader="hardware_info"></div>488 <div maas-card-loader="hardware_info"></div>
482 </div>489 </div>
483 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController">490 <div class="col-4 p-card--highlighted action-card action-card--positionable u-border--solid" data-ng-if="!isController">
484 <div maas-card-loader="tags"></div>491 <div maas-card-loader="tags"></div>
485 </div>492 </div>
493 </div>
486 </div>494 </div>
487 </div>
488 </section>495 </section>
489 <section class="p-strip" data-ng-if="section.area === 'containers'">496 <section class="p-strip" data-ng-if="section.area === 'containers'">
490 <div class="row">497 <div class="row">
@@ -839,7 +846,7 @@
839 </td>846 </td>
840 <td>847 <td>
841 <div class="u-no-margin--top" data-ng-repeat="subnet in vlanRow['subnets']">848 <div class="u-no-margin--top" data-ng-repeat="subnet in vlanRow['subnets']">
842 <a href="#/subnet/{$ subnet.id $}" title="{$ getSubnetText(subnet) $}">{$ getSubnetText(subnet) $}</a>849 <a href="#/subnet/{$ subnet.id $}" title="{$ getSubnetText(subnet) $}">{$ getSubnetText(subnet) $}</a>
843 </div>850 </div>
844 </td>851 </td>
845 <td>852 <td>
@@ -2890,105 +2897,197 @@
2890 </form>2897 </form>
2891 </div>2898 </div>
2892 </section>2899 </section>
2893 <section class="p-strip" data-ng-if="section.area === 'storage'">2900
2894 <form data-ng-controller="NodeStorageController">2901 <section class="p-strip is-shallow" data-ng-if="section.area === 'storage'" data-ng-controller="NodeStorageController">
2895 <div class="row">2902 <div class="row">
2896 <div class="col-12">2903 <div class="col-12">
2897 <div class="p-notification--negative ng-hide" data-ng-hide="has_disks">2904 <div class="p-notification--negative ng-hide" data-ng-hide="has_disks">
2898 <p class="p-notification__response">2905 <p class="p-notification__response">
2899 <span class="p-notification__status">Error:</span> No storage information. Commissioning this node will gather the storage information.2906 <span class="p-notification__status">Error:</span> No storage information. Commissioning this node will gather the storage information.
2900 </p>2907 </p>
2901 </div>2908 </div>
2902 <div class="p-notification ng-hide" data-ng-show="isAllStorageDisabled() && canEdit()">2909 <div class="p-notification ng-hide" data-ng-show="isAllStorageDisabled() && canEdit()">
2903 <p class="p-notification__response">Storage configuration cannot be modified unless the machine is Ready or Allocated.</p>2910 <p class="p-notification__response">Storage configuration cannot be modified unless the machine is Ready or Allocated.</p>
2904 </div>2911 </div>
2905 <div class="p-notification ng-hide" data-ng-show="!isUbuntuOS() && !isCentOS()">2912 <div class="p-notification ng-hide" data-ng-show="!isUbuntuOS() && !isCentOS()">
2906 <p class="p-notification__response">Custom storage configuration is only supported on Ubuntu, CentOS, and RHEL.</p>2913 <p class="p-notification__response">Custom storage configuration is only supported on Ubuntu, CentOS, and RHEL.</p>
2907 </div>2914 </div>
2908 <div class="p-notification ng-hide" data-ng-show="!isUbuntuOS()">2915 <div class="p-notification ng-hide" data-ng-show="!isUbuntuOS()">
2909 <p class="p-notification__response">Bcache and ZFS are only supported on Ubuntu.</p>2916 <p class="p-notification__response">Bcache and ZFS are only supported on Ubuntu.</p>
2917 </div>
2918 <div data-ng-repeat="issue in node.storage_layout_issues" class="p-notification--negative ng-hide" data-ng-show="hasStorageLayoutIssues()">
2919 <p class="p-notification__response">
2920 <span class="p-notification__status">Error:</span> {$ issue $}
2921 </p>
2922 </div>
2923 </div>
2924 </div>
2925 </section>
2926
2927 <section class="p-strip u-no-padding--bottom" data-ng-if="section.area === 'storage'" data-ng-controller="NodeStorageController">
2928 <form>
2929 <div data-ng-if="node.status_code === 4 || node.status_code === 10">
2930 <div class="row" data-ng-if="!confirmStorageLayout">
2931 <div class="col-12 prefix-9 u-sv3">
2932 <div class="p-form__group u-align--right">
2933 <div data-ng-if="storageLayoutIsDisabled(osFamily.layouts) && storageLayoutIsDisabled(osFamily.layouts)">
2934 <button class="p-button--neutral" disabled>Change storage layout</button>
2935 </div>
2936 <div data-ng-if="!(storageLayoutIsDisabled(osFamily.layouts) && storageLayoutIsDisabled(osFamily.layouts))">
2937 <div class="p-contextual-menu" toggle-ctrl>
2938 <button class="p-button--neutral p-contextual-menu__toggle u-no-margin--bottom" data-ng-click="toggleMenu()">
2939 <span data-ng-if="!updatingStorageLayout">
2940 Change storage layout&nbsp;
2941 <i class="p-icon--chevron" data-ng-class="{'u-rotate':isToggled}"></i>
2942 </span>
2943
2944 <span data-ng-if="updatingStorageLayout">
2945 <i class="p-icon--spinner u-animation--spin"></i>
2946 &nbsp;
2947 Updating storage layout&hellip;
2948 </span>
2949 </button>
2950 <div class="p-contextual-menu__dropdown" role="menu" data-ng-show="isToggled">
2951 <button class="p-contextual-menu__link" data-ng-click="toggleMenu(); openStorageLayoutConfirm('flat')">Flat</button>
2952 <button class="p-contextual-menu__link" data-ng-click="toggleMenu(); openStorageLayoutConfirm('lvm')">LVM</button>
2953 <button class="p-contextual-menu__link" data-ng-click="toggleMenu(); openStorageLayoutConfirm('bcache')">bcache</button>
2954 <hr class="u-no-margin--bottom"/>
2955 <button class="p-contextual-menu__link" data-ng-click="toggleMenu(); openStorageLayoutConfirm('vmfs6')">VMFS6 (VMware ESXi)</button>
2956 <hr class="u-no-margin--bottom"/>
2957 <button class="p-contextual-menu__link" data-ng-click="toggleMenu(); openStorageLayoutConfirm('blank')">No storage (blank) layout</button>
2958 </div>
2959 </div>
2960 </div>
2961 </div>
2910 </div>2962 </div>
2911 <div data-ng-repeat="issue in node.storage_layout_issues" class="p-notification--negative ng-hide" data-ng-show="hasStorageLayoutIssues()">2963 </div>
2912 <p class="p-notification__response">2964 <div class="row" data-ng-if="confirmStorageLayout">
2913 <span class="p-notification__status">Error:</span> {$ issue $}2965 <div class="p-card--highlighted">
2914 </p>2966 <div class="col-6 u-no-margin--left">
2967 <div class="p-notification--caution is-subtle" data-ng-if="newLayout.id === 'vmfs6'">
2968 <p class="p-notification__response">
2969 <strong>Are you sure you want to change the storage layout to VMFS6?</strong><br />
2970 Any changes done already will be discarded.<br />
2971 This layout allows only for the deployment of <strong>VMware ESXi</strong> images.<br />
2972 The storage layout will be applied to a node when it is deployed.
2973 </p>
2974 </div>
2975 <div class="p-notification--caution is-subtle" data-ng-if="newLayout.id === 'lvm'">
2976 <p class="p-notification__response">
2977 <strong>Are you sure you want to change the storage layout to LVM?</strong><br />
2978 Any changes done already will be lost.<br />
2979 The storage layout will be applied to a node when it is deployed.
2980 </p>
2981 </div>
2982 <div class="p-notification--caution is-subtle" data-ng-if="newLayout.id === 'blank'">
2983 <p class="p-notification__response">
2984 <strong>Are you sure you want to change this storage layout to blank?</strong><br />
2985 Used disks will be returned to available, and any volume groups, raid sets,
2915caches, and filesystems removed.<br />2986caches, and filesystems removed.<br />
2987 The storage layout will be applied to a node when it is deployed.
2988 </p>
2989 </div>
2990 <div class="p-notification--caution is-subtle" data-ng-if="newLayout.id !== 'lvm' && newLayout.id !== 'vmfs6' && newLayout.id !== 'blank'">
2991 <p class="p-notification__response">
2992 <strong>Are you sure you want to change the storage layout to {$ newLayout.id $}?</strong><br />
2993 Any changes done already will be lost.<br />
2994 The storage layout will be applied to a node when it is deployed.
2995 </p>
2996 </div>
2997 </div>
2998 <div class="col-6 u-align--right">
2999 <button class="p-button--base"
3000 data-ng-click="closeStorageLayoutConfirm()">Cancel</button>
3001 <button class="p-button--negative"
3002 data-ng-click="updateStorageLayout(storageLayout)">
3003 Change storage layout
3004 </button>
3005 </div>
2916 </div>3006 </div>
2917 </div>3007 </div>
2918 </div>3008 </div>
2919 <div class="row">3009
2920 <div data-ng-controller="NodeFilesystemsController">3010 <div class="p-strip">
2921 <storage-filesystems></storage-filesystems>3011 <div class="row" data-ng-if="storageLayout.id === 'vmfs6'">
3012 <div data-ng-controller="NodeFilesystemsController">
3013 <storage-datastores></storage-datastores>
3014 </div>
2922 </div>3015 </div>
2923 <div data-ng-show="cachesets.length">3016
2924 <div class="row">3017 <div class="row">
2925 <h3 class="p-heading--four">Available cache sets</h3>3018 <div data-ng-controller="NodeFilesystemsController" data-ng-if="storageLayout.id !== 'vmfs6'">
3019 <storage-filesystems></storage-filesystems>
2926 </div>3020 </div>
2927 <div class="row">3021 <div data-ng-show="cachesets.length">
2928 <table class="p-table-expanding">3022 <div class="row">
2929 <thead>3023 <h3 class="p-heading--four">Available cache sets</h3>
2930 <tr>3024 </div>
2931 <th class="col-2">Name</th>3025 <div class="row">
2932 <th class="col-3">Size</th>3026 <table class="p-table-expanding">
2933 <th class="col-4">Used by</th>3027 <thead>
2934 <th class="col-2">3028 <tr>
2935 <div class="u-align--right">Actions</div>3029 <th class="col-2">Name</th>
2936 </th>3030 <th class="col-3">Size</th>
2937 </tr>3031 <th class="col-4">Used by</th>
2938 </thead>3032 <th class="col-2">
2939 <tbody>3033 <div class="u-align--right">Actions</div>
2940 <tr data-ng-repeat="cacheset in cachesets" data-ng-class="{ 'is-active': cacheset.$selected }">3034 </th>
2941 <td class="col-2" aria-label="Name" title="{$ cacheset.name $}">{$ cacheset.name $}</td>3035 </tr>
2942 <td class="col-3" aria-label="Size" title="{$ cacheset.size_human $}">{$ cacheset.size_human $}</td>3036 </thead>
2943 <td class="col-4" aria-label="Used by" title="{$ cacheset.used_by $}">{$ cacheset.used_by $}</td>3037 <tbody>
2944 <td class="col-2 p-table--action-cell">3038 <tr data-ng-repeat="cacheset in cachesets" data-ng-class="{ 'is-active': cacheset.$selected }">
2945 <div class="u-align--right">3039 <td class="col-2" aria-label="Name" title="{$ cacheset.name $}">{$ cacheset.name $}</td>
2946 <div class="p-contextual-menu" toggle-ctrl data-ng-if="canDeleteCacheSet(cacheset) && !cacheset.$selected">3040 <td class="col-3" aria-label="Size" title="{$ cacheset.size_human $}">{$ cacheset.size_human $}</td>
2947 <button class="p-button--base is-small p-contextual-menu__toggle" data-ng-click="toggleMenu()">3041 <td class="col-4" aria-label="Used by" title="{$ cacheset.used_by $}">{$ cacheset.used_by $}</td>
2948 <i class="p-icon--contextual-menu u-no-margin--right">Actions</i>3042 <td class="col-2 p-table--action-cell">
2949 </button>3043 <div class="u-align--right">
2950 <div class="p-contextual-menu__dropdown" role="menu" data-ng-show="isToggled">3044 <div class="p-contextual-menu" toggle-ctrl data-ng-if="canDeleteCacheSet(cacheset) && !cacheset.$selected">
2951 <button class="p-contextual-menu__link"3045 <button class="p-button--base is-small p-contextual-menu__toggle" data-ng-click="toggleMenu()">
2952 aria-label="Remove"3046 <i class="p-icon--contextual-menu u-no-margin--right">Actions</i>
2953 data-ng-show="canDeleteCacheSet(cacheset)"3047 </button>
2954 data-ng-click="toggleMenu(); quickCacheSetDelete(cacheset)">Remove&hellip;</button>3048 <div class="p-contextual-menu__dropdown" role="menu" data-ng-show="isToggled">
2955 </div>3049 <button class="p-contextual-menu__link"
2956 </div>3050 aria-label="Remove"
2957 </div>3051 data-ng-show="canDeleteCacheSet(cacheset)"
2958 </td>3052 data-ng-click="toggleMenu(); quickCacheSetDelete(cacheset)">Remove&hellip;</button>
2959 <td class="p-table-expanding__panel col-12" data-ng-if="cacheset.$selected && cachesetsMode === 'delete'">3053 </div>
2960 <div class="row" data-ng-if="windowWidth <= 768">
2961 <div class="col-8">
2962 <h2 data-ng-click="filesystemCancel()" class="p-heading--four">
2963 <span data-ng-if="cachesetsMode === 'delete'">Removing {$ cacheset.name $}</span>
2964 </h2>
2965 </div>3054 </div>
2966 </div>
2967 <div class="row" data-ng-class="{ 'is-active': cachesetsMode !== null && cachesetsMode !== 'multi' }">
2968 <div data-ng-show="cachesetsMode === 'single' && canDeleteCacheSet(cacheset)">
2969 <button class="p-button--base"
2970 data-ng-show="canDeleteCacheSet(cacheset)"
2971 data-ng-click="cacheSetDelete()">Remove</button>
2972 </div>3055 </div>
2973 <div class="row ng-hide" data-ng-show="cachesetsMode === 'delete'">3056 </td>
3057 <td class="p-table-expanding__panel col-12" data-ng-if="cacheset.$selected && cachesetsMode === 'delete'">
3058 <div class="row" data-ng-if="windowWidth <= 768">
2974 <div class="col-8">3059 <div class="col-8">
2975 <p><span class="p-icon--warning">Warning:</span> Are you sure you want to delete this cache set?</p>3060 <h2 data-ng-click="filesystemCancel()" class="p-heading--four">
3061 <span data-ng-if="cachesetsMode === 'delete'">Removing {$ cacheset.name $}</span>
3062 </h2>
3063 </div>
3064 </div>
3065 <div class="row" data-ng-class="{ 'is-active': cachesetsMode !== null && cachesetsMode !== 'multi' }">
3066 <div data-ng-show="cachesetsMode === 'single' && canDeleteCacheSet(cacheset)">
3067 <button class="p-button--base"
3068 data-ng-show="canDeleteCacheSet(cacheset)"
3069 data-ng-click="cacheSetDelete()">Remove</button>
2976 </div>3070 </div>
2977 <div class="col-4">3071 <div class="row ng-hide" data-ng-show="cachesetsMode === 'delete'">
2978 <div class="u-align--right">3072 <div class="col-8">
2979 <button class="p-button--base" type="button" data-ng-click="cacheSetCancel()">Cancel</button>3073 <p><span class="p-icon--warning">Warning:</span> Are you sure you want to delete this cache set?</p>
2980 <button class="p-button--negative u-no-margin--top" data-ng-click="cacheSetConfirmDelete(cacheset)">Remove cache set</button>3074 </div>
3075 <div class="col-4">
3076 <div class="u-align--right">
3077 <button class="p-button--base" type="button" data-ng-click="cacheSetCancel()">Cancel</button>
3078 <button class="p-button--negative u-no-margin--top" data-ng-click="cacheSetConfirmDelete(cacheset)">Remove cache set</button>
3079 </div>
2981 </div>3080 </div>
2982 </div>3081 </div>
2983 </div>3082 </div>
2984 </div>3083 </td>
2985 </td>3084 </tr>
2986 </tr>3085 </tbody>
2987 </tbody>3086 </table>
2988 </table>3087 </div>
3088 </div>
3089 <div>
3090 <storage-disks-partitions></storage-disks-partitions>
2989 </div>3091 </div>
2990 </div>
2991 <div>
2992 <storage-disks-partitions></storage-disks-partitions>
2993 </div>3092 </div>
2994 </div>3093 </div>
2995 </form>3094 </form>
diff --git a/src/maasserver/static/partials/nodedetails/storage/datastores.html b/src/maasserver/static/partials/nodedetails/storage/datastores.html
2996new file mode 1006443095new file mode 100644
index 0000000..28984c8
--- /dev/null
+++ b/src/maasserver/static/partials/nodedetails/storage/datastores.html
@@ -0,0 +1,112 @@
1<div class="p-strip is-shallow">
2 <h3 class="p-heading--four">Datastores</h3>
3
4 <table class="p-table-expanding p-table--datastores col-12" role="grid">
5 <thead>
6 <tr class="p-table__row">
7 <th scope="col" aria-sort="none" class="p-table__cell">Name</th>
8 <th scope="col" aria-sort="none" class="p-table__cell">Filesystem</th>
9 <th scope="col" aria-sort="none" class="p-table__cell">Size</th>
10 <th scope="col" aria-sort="none" class="p-table__cell">Mount point</th>
11 <th scope="col" aria-sort="none" class="p-table__cell u-align--right">Actions</th>
12 <th class="u-hide">
13 <!-- empty cell for validation -->
14 </th>
15 </tr>
16 </thead>
17 <tbody>
18 <tr data-ng-hide="node.disks.length" class="col-12">
19 <td>
20 No datastores defined.
21 </td>
22 </tr>
23 <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'">
24 <td role="gridcell" class="p-table__cell" aria-label="Name" title="{$ filesystem.name $}">{$ filesystem.name $}</td>
25 <td role="gridcell" class="p-table__cell" aria-label="Filesystem" title="VMFS6">VMFS6</td>
26 <td role="gridcell" class="p-table__cell" aria-label="Size" title="{$ filesystem.size_human $}">{$ filesystem.size_human $}</td>
27 <td role="gridcell" class="p-table__cell" aria-label="Mount point" title="{$ filesystem.path $}">{$ filesystem.path $}</td>
28 <td role="gridcell" class="p-table__cell p-table--action-cell u-align--right">
29 <div class="p-contextual-menu" toggle-ctrl data-ng-if="!isAllStorageDisabled()">
30 <button class="p-button--base p-contextual-menu__toggle" aria-controls="#{$ item.name $}-menu"
31 data-ng-click="toggleMenu()" aria-haspopup="true">
32 <i class="p-icon--contextual-menu">Actions</i>
33 </button>
34 <div class="p-contextual-menu__dropdown" role="menu" data-ng-show="isToggled" id="{$ item.name $}-menu">
35 <button class="p-contextual-menu__link" aria-label="Remove"
36 data-ng-click="toggleMenu(); quickFilesystemDelete(filesystem)"
37 data-ng-show="!isAllStorageDisabled() && filesystemMode !== 'delete'">Remove&hellip;</button>
38 </div>
39 </div>
40 </td>
41 <td class="p-table-expanding__panel--bordered"
42 data-ng-if="filesystem.$selected && filesystemMode === 'delete'"
43 aria-hidden="!filesystem.$selected && filesystemMode !== 'delete'">
44 <div class="row u-flex--no-wrap" data-ng-if="windowWidth <= 768">
45 <h2 data-ng-click="filesystemCancel()" class="p-heading--four">
46 <span data-ng-if="filesystemMode === 'delete'">Removing {$ filesystem.name $}</span>
47 </h2>
48 <button class="p-button--close" data-ng-click="filesystemCancel()"><span
49 class="p-icon--close">Cancel</span></button>
50 </div>
51 <div data-ng-if="filesystemMode !== null && filesystemMode !== 'multi'"
52 data-ng-class="{ 'is-active': filesystemMode !== null && filesystemMode !== 'multi' }">
53 <div data-ng-if="filesystemMode === 'delete'" class="p-space-between">
54 <p><span class="p-icon--warning">Warning:</span> Are you sure you want to remove this {$
55 getRemoveTypeText(filesystem) $}?</p>
56 <div class="p-space-between__align-right">
57 <button class="p-button--base u-width--auto" type="button"
58 data-ng-click="filesystemCancel(filesystem)">Cancel</button>
59 <button class="p-button--negative u-width--auto"
60 data-ng-click="filesystemConfirmDelete(filesystem)">Remove</button>
61 </div>
62 </div>
63 </div>
64 </td>
65 </tr>
66
67 <tr class="is-active p-table__row" data-ng-if="dropdown" data-ng-switch="dropdown">
68 <!-- Adding a new TMPFS or RAMFPS filesystem -->
69 <td class="p-table-expanding__panel" data-ng-controller="NodeAddSpecialFilesystemController"
70 data-ng-switch-when="special">
71 <maas-obj-form obj="newFilesystem" manager="machineManager" manager-method="mountSpecialFilesystem"
72 inline="false" save-on-blur="false" after-save="cancel">
73 <div class="row" data-ng-if="windowWidth <= 768">
74 <div class="u-flex--no-wrap">
75 <h2 data-ng-click="cancel()" class="u-align--left p-heading--four">Adding filesystem</h2>
76 <button class="p-button--close" data-ng-click="cancel()" type="button">
77 <i class="p-icon--close">Cancel</i></button>
78 </div>
79 </div>
80 <div class="row p-form p-form--stacked">
81 <div class="col-6">
82 <div class="p-form__group">
83 <label class="p-form__label mobile-col-2">Description</label>
84 <div class="p-form__control p-form__control--placeholder mobile-col-2">
85 <span data-ng-bind="description"></span>
86 </div>
87 </div>
88 <maas-obj-field type="options" key="fstype" label="Filesystem" subtle="false"
89 options="type for type in specialFilesystemTypes"></maas-obj-field>
90 </div>
91 <div class="col-6">
92 <maas-obj-field type="text" key="mount_point" label="Mount point" subtle="false"
93 placeholder="Absolute path"></maas-obj-field>
94 <maas-obj-field type="text" key="mount_options" label="Mount options" subtle="false"
95 placeholder="Separated by commas, no spaces"></maas-obj-field>
96 </div>
97 </div>
98 <hr>
99 <div class="p-space-between">
100 <maas-obj-errors></maas-obj-errors>
101 <div class="p-space-between__align-right">
102 <button class="p-button--base u-width--auto" type="button" data-ng-click="cancel()">Cancel</button>
103 <button class="p-button--neutral u-width--auto ng-binding" data-ng-disabled="!canMount()"
104 maas-obj-save>Mount</button>
105 </div>
106 </div>
107 </maas-obj-form>
108 </td>
109 </tr>
110 </tbody>
111 </table>
112</div>
0\ No newline at end of file113\ No newline at end of file
diff --git a/src/maasserver/static/partials/nodedetails/storage/disks-partitions.html b/src/maasserver/static/partials/nodedetails/storage/disks-partitions.html
index 8f85fc7..041f4ae 100644
--- a/src/maasserver/static/partials/nodedetails/storage/disks-partitions.html
+++ b/src/maasserver/static/partials/nodedetails/storage/disks-partitions.html
@@ -4,27 +4,25 @@
4 <table class="p-table-expanding p-table--disks-partitions col-12">4 <table class="p-table-expanding p-table--disks-partitions col-12">
5 <thead>5 <thead>
6 <tr class="p-table__row">6 <tr class="p-table__row">
7 <th class="col-3">7 <th class="p-double-row p-table__cell">
8 <div class="u-float--left">8 <div class="p-double-row__checkbox">&nbsp;</div>
9 <input type="checkbox" class="checkbox u-float--left" id="available-check-all" data-ng-hide="isAvailableDisabled()" data-ng-checked="availableAllSelected"9 <div class="p-double-row__rows-container--checkbox">
10 data-ng-click="toggleAvailableAllSelect()" data-ng-disabled="isAvailableDisabled()" />10 <div>Name</div>
11 <label for="available-check-all"></label>11 <div>Serial</div>
12 </div>12 </div>
13 <a data-ng-click="tableInfo.column = 'name'" data-ng-class="{'p-link--soft': tableInfo.column === 'name'}">Name</a>13 </th>
14 <span class="divide"> | </span>14 <th class="p-double-row p-table__cell">
15 <a data-ng-click="tableInfo.column = 'model'" data-ng-class="{'p-link--soft': tableInfo.column === 'model'}">Model</a>15 <div>Model</div>
16 <span class="divide"> | </span>16 <div>Firmware</div>
17 <a data-ng-click="tableInfo.column = 'serial'" data-ng-class="{'p-link--soft': tableInfo.column === 'serial'}">Serial</a>
18 <span class="divide"> | </span>
19 <a data-ng-click="tableInfo.column = 'firmware_version'" data-ng-class="{'p-link--soft': tableInfo.column === 'firmware_version'}">Firmware</a>
20 </th>17 </th>
21 <th class="col-1"><div class="u-align--center">Boot</div></th>18 <th class="p-table__cell"><div class="u-align--center">Boot</div></th>
22 <th class="col-1">Size</th>19 <th class="p-table__cell">Size</th>
23 <th class="col-1">Type</th>20 <th class="col-3 p-double-row p-table__cell">
24 <th class="col-2">Filesystem</th>21 <div>Type</div>
25 <th class="col-2">Tags</th>22 <div>Tags</div>
26 <th class="col-1">Health</th>23 </th>
27 <th class="col-1"><div class="u-align--right">Actions</div></th>24 <th class="p-table__cell">Health</th>
25 <th class="p-table__cell"><div class="u-align--right">Actions</div></th>
28 </tr>26 </tr>
29 </thead>27 </thead>
30 <tbody>28 <tbody>
@@ -34,32 +32,34 @@
34 </td>32 </td>
35 </tr>33 </tr>
36 <tr class="p-table__row" data-ng-repeat="item in available | removeAvailableByNew:availableNew"34 <tr class="p-table__row" data-ng-repeat="item in available | removeAvailableByNew:availableNew"
37 data-ng-class="{ 'is-active': item.$selected }">35 data-ng-class="{ 'is-active': item.$selected }" data-ng-if="item.parent_type !== 'vmfs6'">
38 <td class="col-3 p-form-validation" aria-label="Name"36 <td class="p-form-validation p-double-row p-table__cell" aria-label="Name"
39 data-ng-class="{ 'is-error': isNameInvalid(item) }">37 data-ng-class="{ 'is-error': isNameInvalid(item) }">
40 <div class="u-float--left">38 <div class="p-double-row__checkbox">
41 <input type="checkbox" class="checkbox u-float--left" id="{$ item.name $}"39 <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()" />
42 data-ng-hide="isAvailableDisabled()"40 <label for="{$ item.name $}"></label>
43 data-ng-checked="item.$selected"41 </div>
44 data-ng-click="toggleAvailableSelect(item)"42 <div class="p-double-row__rows-container--checkbox">
45 data-ng-disabled="isAvailableDisabled()" />43 <div class="p-double-row__main-row">
46 <label for="{$ item.name $}">44 {$ item.name $}
47 <span data-ng-show="tableInfo.column === 'name'"45 </div>
48 data-ng-hide="availableMode === 'edit' && item.$selected"46 <div class="p-double-row__muted-row">
49 title="{$ item.name $}">{$ item.name $}</span>47 {$ item.serial $}
50 </label>48 </div>
51 </div>49 </div>
52 <span data-ng-show="tableInfo.column === 'model'" title="{$ item.model $}">{$ item.model $}</span>
53 <span data-ng-show="tableInfo.column === 'serial'" title="{$ item.serial $}">{$ item.serial $}</span>
54 <span data-ng-show="tableInfo.column === 'firmware_version'" title="{$ item.firmware_version $}">{$ item.firmware_version $}</span>
55 <input type="text" class="p-form-validation__input"
56 data-ng-model="item.name"
57 data-ng-show="availableMode === 'edit' && item.$selected"
58 data-ng-disabled="item.type === 'partition' || isAllStorageDisabled() || !canEdit()"
59 data-ng-change="nameHasChanged(item)">
60 </td>50 </td>
61 <td class="col-1" aria-label="Boot">51 <td class="p-double-row p-table__cell">
62 <div class="u-align--center">52 <div class="p-double-row__container">
53 <div class="p-double-row__main-row">
54 {$ item.model $}
55 </div>
56 <div class="p-double-row__muted-row">
57 {$ item.firmware_version $}
58 </div>
59 </div>
60 </td>
61 <td class="p-table__cell" aria-label="Boot">
62 <div class="u-align--center" data-ng-if="storageLayout.id !== 'vmfs6'">
63 <input type="radio" name="boot-disk" id="{$ item.name $}-boot" class="u-no-margin--right"63 <input type="radio" name="boot-disk" id="{$ item.name $}-boot" class="u-no-margin--right"
64 data-ng-click="setAsBootDisk(item)"64 data-ng-click="setAsBootDisk(item)"
65 data-ng-checked="item.is_boot"65 data-ng-checked="item.is_boot"
@@ -69,30 +69,31 @@
69 <label for="{$ item.name $}-boot" data-ng-hide="availableMode === 'edit' && item.$selected"></label>69 <label for="{$ item.name $}-boot" data-ng-hide="availableMode === 'edit' && item.$selected"></label>
70 </div>70 </div>
71 </td>71 </td>
72 <td class="col-1" aria-label="Size">72 <td class="p-table__cell" aria-label="Size">
73 <span data-ng-hide="availableMode === 'edit' && item.$selected" title="{$ item.size_human $}">73 <span data-ng-hide="availableMode === 'edit' && item.$selected" title="{$ item.size_human $}">
74 {$ item.size_human $} <span class="table__label ng-hide" data-ng-show="showFreeSpace(item)">Free: {$ item.available_size_human $}</span>74 {$ item.size_human $} <span class="table__labeldatastore-name ng-hide" data-ng-show="showFreeSpace(item)">Free: {$ item.available_size_human $}</span>
75 </span>75 </span>
76 </td>76 </td>
77 <td class="col-1" aria-label="type">77 <td class="p-double-row p-table__cell" aria-label="type">
78 <span data-ng-hide="availableMode === 'edit' && item.$selected" title="{$ getDeviceType(item) $}">{$ getDeviceType(item) $}</span>78 <div class="p-double-rows__container">
79 </td>79 <div class="p-double-row__main-row">
80 <td class="col-2" aria-label="Filesystem">80 {$ getDeviceType(item) $}
81 <span data-ng-hide="availableMode === 'edit' && item.$selected" title="{$ item.fstype $}">{$ item.fstype $}</span>81 </div>
82 </td>82 <div class="p-double-row__muted-row">
83 <td class="col-2" aria-label="Tags">83 <span class="table__tag" data-ng-repeat="tag in item.tags" data-ng-hide="item.$options.editingTags">
84 <span class="table__tag" data-ng-repeat="tag in item.tags" data-ng-hide="item.$options.editingTags">84 <a href="#/machines/?query=storage_tags:({$ tag.text $})" title="{$ tag.text $}">{$ tag.text $}</a>
85 <a href="#/machines/?query=storage_tags:({$ tag.text $})" title="{$ tag.text $}">{$ tag.text $}</a>85 </span>
86 </span>86 </div>
87 </div>
87 </td>88 </td>
88 <td class="col-1" aria-label="Health">89 <td class="p-table__cell" aria-label="Health">
89 <span data-maas-script-status="script-status" data-script-status="item.test_status" data-ng-if="item.type === 'physical'"></span>90 <span data-maas-script-status="script-status" data-script-status="item.test_status" data-ng-if="item.type === 'physical'"></span>
90 <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>91 <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>
91 <span data-ng-if="item.test_status === 3 || item.test_status === 4 || item.test_status === 8" title="Error">Error</span>92 <span data-ng-if="item.test_status === 3 || item.test_status === 4 || item.test_status === 8" title="Error">Error</span>
92 <span data-ng-if="item.test_status === 6" title="Degraded">Degraded</span>93 <span data-ng-if="item.test_status === 6" title="Degraded">Degraded</span>
93 <span data-ng-if="item.test_status === -1" title="Unknown">Unknown</span>94 <span data-ng-if="item.test_status === -1" title="Unknown">Unknown</span>
94 </td>95 </td>
95 <td class="col-1 p-table--action-cell">96 <td class="p-table--action-cell p-table__cell">
96 <div class="u-align--right">97 <div class="u-align--right">
97 <div class="p-contextual-menu" toggle-ctrl98 <div class="p-contextual-menu" toggle-ctrl
98 data-ng-if="canAddLogicalVolume(item) || canAddPartition(item) || canEdit(item) || canDelete(item)">99 data-ng-if="canAddLogicalVolume(item) || canAddPartition(item) || canEdit(item) || canDelete(item)">
@@ -463,7 +464,7 @@
463 <label class="checkbox-label" for="bcache-create"></label>464 <label class="checkbox-label" for="bcache-create"></label>
464 </div>465 </div>
465 <div class="u-float--left p-form-validation"466 <div class="u-float--left p-form-validation"
466 data-ng-class="{ 'is-error': isNewDiskNameInvalid() }">467 data-ng-class="{ 'is-error': isNewDiskNameInvalid(availableNew.name) }">
467 <input type="text" class="p-form-validation__input"468 <input type="text" class="p-form-validation__input"
468 data-ng-model="availableNew.name">469 data-ng-model="availableNew.name">
469 </div>470 </div>
@@ -481,7 +482,7 @@
481 <div class="row">482 <div class="row">
482 <div class="col-6">483 <div class="col-6">
483 <div class="p-form__group u-hide--large p-form-validation"484 <div class="p-form__group u-hide--large p-form-validation"
484 data-ng-class="{ 'is-error': isNewDiskNameInvalid() }">485 data-ng-class="{ 'is-error': isNewDiskNameInvalid(availableNew.name) }">
485 <label for="bcache-name" class="p-form__label">Name</label>486 <label for="bcache-name" class="p-form__label">Name</label>
486 <div class="p-form__control">487 <div class="p-form__control">
487 <input type="text" id="bcache-name" data-ng-model="availableNew.name"488 <input type="text" id="bcache-name" data-ng-model="availableNew.name"
@@ -600,7 +601,7 @@
600 <label for="raid-create"></label>601 <label for="raid-create"></label>
601 </div>602 </div>
602 <div class="u-float--left p-form-validation"603 <div class="u-float--left p-form-validation"
603 data-ng-class="{ 'is-error': isNewDiskNameInvalid() }">604 data-ng-class="{ 'is-error': isNewDiskNameInvalid(availableNew.name) }">
604 <input type="text" class="p-form-validation__input"605 <input type="text" class="p-form-validation__input"
605 data-ng-model="availableNew.name">606 data-ng-model="availableNew.name">
606 </div>607 </div>
@@ -619,7 +620,7 @@
619 <div class="col-6">620 <div class="col-6">
620 <div class="p-form__group u-hide--medium u-hide--large">621 <div class="p-form__group u-hide--medium u-hide--large">
621 <label for="new-raid-name" class="p-form__label">Name</label>622 <label for="new-raid-name" class="p-form__label">Name</label>
622 <div class="p-form__control" data-ng-class="{ 'is-error': isNewDiskNameInvalid() }">623 <div class="p-form__control" data-ng-class="{ 'is-error': isNewDiskNameInvalid(availableNew.name) }">
623 <input type="text" id="new-raid-name" data-ng-model="availableNew.name">624 <input type="text" id="new-raid-name" data-ng-model="availableNew.name">
624 </div>625 </div>
625 </div>626 </div>
@@ -734,7 +735,7 @@
734 <label for="vg-create"></label>735 <label for="vg-create"></label>
735 </div>736 </div>
736 <div class="u-float--left p-form-validation"737 <div class="u-float--left p-form-validation"
737 data-ng-class="{ 'is-error': isNewDiskNameInvalid() }">738 data-ng-class="{ 'is-error': isNewDiskNameInvalid(availableNew.name) }">
738 <input type="text" class="p-form-validation__input"739 <input type="text" class="p-form-validation__input"
739 data-ng-model="availableNew.name">740 data-ng-model="availableNew.name">
740 </div>741 </div>
@@ -753,7 +754,7 @@
753 <div class="col-6">754 <div class="col-6">
754 <div class="p-form__group p-form-validation">755 <div class="p-form__group p-form-validation">
755 <label for="new-vg-name" class="p-form__label">Name</label>756 <label for="new-vg-name" class="p-form__label">Name</label>
756 <div class="p-form__control" data-ng-class="{ 'is-error': isNewDiskNameInvalid() }">757 <div class="p-form__control" data-ng-class="{ 'is-error': isNewDiskNameInvalid(availableNew.name) }">
757 <input type="text" class="p-form-validation__input" id="new-vg-name" data-ng-model="availableNew.name">758 <input type="text" class="p-form-validation__input" id="new-vg-name" data-ng-model="availableNew.name">
758 </div>759 </div>
759 </div>760 </div>
@@ -806,57 +807,212 @@
806 </tr>807 </tr>
807 </tbody>808 </tbody>
808 </table>809 </table>
809 <button class="p-button--neutral p-tooltip p-tooltip--top-center"810
810 data-ng-disabled="!canCreateRAID()"811 <div class="p-card" data-ng-if="createNewDatastore">
811 data-ng-hide="isAllStorageDisabled() || !canEdit()"812 <div class="row">
812 data-ng-click="createRAID()">813 <div class="row p-form--stacked">
813 Create RAID814 <div class="col-6">
814 <span class="p-tooltip__message" role="tooltip">Select two or more physical devices to create a RAID</span>815 <div class="p-form__group p-form-validation"
815 </button>816 data-ng-class="{ 'is-error': isNewDiskNameInvalid(datastores.new.name) }">
816 <button class="p-button--neutral p-tooltip p-tooltip--top-center"817 <label for="datastore-name" class="p-form__label u-sv3">Name</label>
817 data-ng-disabled="!canCreateVolumeGroup()"818 <div class="p-form__control">
818 data-ng-hide="isAllStorageDisabled() || !canEdit()"819 <input type="text"
819 data-ng-click="createVolumeGroup()">820 name="datastore-name"
820 Create volume group821 id="datastore-name"
821 <span class="p-tooltip__message" role="tooltip">Select one or more devices to create a volume group</span>822 class="p-form-validation__input"
822 </button>823 data-ng-model="datastores.new.name"
823 <button class="p-button--neutral p-tooltip p-tooltip--top-center"824 data-ng-keydown="$event.keyCode == 13 && !isNewDiskNameInvalid(datastores.new.name) && $event.preventDefault()"
824 data-ng-disabled="!canCreateCacheSet()"825 data-ng-keyup="$event.keyCode == 13 && !isNewDiskNameInvalid(datastores.new.name) && createDatastore()">
825 data-ng-hide="isAllStorageDisabled() || !canEdit()"826
826 data-ng-click="createCacheSet()">827 <p class="p-form-validation__message"
827 Create cache Set828 data-ng-if="isNewDiskNameInvalid(datastores.new.name) && datastores.new.name != ''">
828 <span class="p-tooltip__message" role="tooltip">Select one device to create a cache set</span>829 <strong>Error:</strong> Disk name is already in use
829 </button>830 </p>
830 <button class="p-button--neutral p-tooltip--top-center"831 <p class="p-form-validation__message"
831 data-ng-class="{ 'p-tooltip': !canCreateBcache() }"832 data-ng-if="isNewDiskNameInvalid(datastores.new.name) && datastores.new.name == ''">
832 data-ng-disabled="!canCreateBcache()"833 <strong>Error:</strong> Please enter a name for your new datastore
833 data-ng-hide="isAllStorageDisabled() || !canEdit()"834 </p>
834 data-ng-click="createBcache()">835 </div>
835 Create bcache836 </div>
836 <span class="p-tooltip__message" role="tooltip">{$ getCannotCreateBcacheMsg() $}</span>837 </div>
837 </button>838 <div class="col-6">
839 <div class="p-form__group">
840 <label for="datastore-filesystem" class="p-form__label u-sv3">Filesystem</label>
841 <div class="p-form__control">
842 <p>{$ datastores.new.filesystem $}</p>
843 </div>
844 </div>
845 <div class="p-form__group" data-ng-if="datastores.new.path">
846 <label for="datastore-mountpoint" class="p-form__label">Mount point</label>
847 <div class="p-form__control">
848 <p>{$ datastores.new.path $}</p>
849 </div>
850 </div>
851 </div>
852 </div>
853 <div class="u-sv2">
854 <hr />
855 </div>
856 <div class="u-align--right">
857 <button class="p-button--neutral u-no-margin--bottom" data-ng-click="closeNewDatastorePanel()" data-ng-disabled="creatingDatastore">Cancel</button>
858 <button class="p-button--positive u-no-margin--bottom" data-ng-click="createDatastore()" data-ng-if="!creatingDatastore" data-ng-disabled="isNewDiskNameInvalid(datastores.new.name)">
859 Create datastore
860 </button>
861 <button class="p-button--positive u-no-margin--bottom" data-ng-if="creatingDatastore">
862 <i class="p-icon--spinner is-light u-animation--spin"></i>
863 &nbsp;
864 Creating datastore
865 </button>
866 </div>
867 </div>
868 </div>
869 <div class="p-card" data-ng-if="addToExistingDatastore">
870 <div class="row p-form--stacked">
871 <div class="col-6">
872 <div class="p-form__group">
873 <label for="datastore-name" class="p-form__label u-sv3">Datastore</label>
874 <div class="p-form__control">
875 <select name="datastore-name" id="datastore-name"
876 data-ng-model="datastores.old"
877 data-ng-options="disk as disk.name for disk in node.disks | datastoresOnly">
878 </select>
879 </div>
880 </div>
881 </div>
882 <div class="col-6">
883 <div class="p-form__group">
884 <label for="datastore-mountpoint" class="p-form__label">Mount point</label>
885 <div class="p-form__control">
886 <p>{$ datastores.old.path $}</p>
887 </div>
888 </div>
889 </div>
890 </div>
891 <div class="u-sv2">
892 <hr />
893 </div>
894 <div class="u-align--right">
895 <button class="p-button--neutral u-no-margin--bottom" data-ng-click="closeAddToExistingDatastorePanel()" data-ng-disabled="updatingDatastore">Cancel</button>
896 <span class="p-tooltip p-tooltip--top-right">
897 <button class="p-button--positive u-no-margin--bottom"
898 data-ng-click="addToDatastore()"
899 data-ng-if="!updatingDatastore"
900 data-ng-disabled="!addToDatastoreValid">
901 Add to datastore
902 </button>
903 <span class="p-tooltip__message" role="tooltip" data-ng-if="!addToDatastoreValid">Disks with partitions cannot be added to a datastore</span>
904 </span>
905 <button class="p-button--positive u-no-margin--bottom" data-ng-if="updatingDatastore">
906 <i class="p-icon--spinner is-light u-animation--spin"></i>
907 &nbsp;
908 Adding to datastore
909 </button>
910 </div>
911 </div>
912
913 <div>
914 <span class="p-tooltip p-tooltip--top-left">
915 <button class="p-button--neutral"
916 data-ng-disabled="!canCreateRAID() || storageLayout.id === 'vmfs6'"
917 data-ng-hide="isAllStorageDisabled() || !canEdit()"
918 data-ng-click="createRAID()">
919 Create RAID
920 </button>
921 <span data-ng-if="!canCreateRAID()">
922 <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id !== 'vmfs6'">Select two or more physical devices to create a RAID</span>
923 <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id === 'vmfs6'">Not supported in the VMFS6 layout</span>
924 </span>
925 </span>
926 <span class="p-tooltip p-tooltip--top-center">
927 <button class="p-button--neutral"
928 data-ng-disabled="!canCreateVolumeGroup() || storageLayout.id === 'vmfs6'"
929 data-ng-hide="isAllStorageDisabled() || !canEdit()"
930 data-ng-click="createVolumeGroup()">
931 Create volume group
932 </button>
933 <span data-ng-if="!canCreateVolumeGroup()">
934 <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id !== 'vmfs6'">Select one or more devices to create a volume group</span>
935 <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id === 'vmfs6'">Not supported in the VMFS6 layout</span>
936 </span>
937 </span>
938 <span class="p-tooltip p-tooltip--top-center">
939 <button class="p-button--neutral"
940 data-ng-disabled="!canCreateCacheSet() || storageLayout.id === 'vmfs6'"
941 data-ng-hide="isAllStorageDisabled() || !canEdit()"
942 data-ng-click="createCacheSet()">
943 Create cache Set
944 </button>
945 <span data-ng-if="!canCreateCacheSet()">
946 <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id !== 'vmfs6'">Select one device to create a cache set</span>
947 <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id === 'vmfs6'">Not supported in the VMFS6 layout</span>
948 </span>
949 </span>
950 <span class="p-tooltip p-tooltip--top-center">
951 <button class="p-button--neutral"
952 data-ng-class="{ 'p-tooltip': !canCreateBcache() }"
953 data-ng-disabled="!canCreateBcache() || storageLayout.id === 'vmfs6'"
954 data-ng-hide="isAllStorageDisabled() || !canEdit()"
955 data-ng-click="createBcache()">
956 Create bcache
957 </button>
958 <span data-ng-if="!canCreateBcache()">
959 <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id !== 'vmfs6'">{$ getCannotCreateBcacheMsg() $}</span>
960 <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id === 'vmfs6'">Not supported in the VMFS6 layout</span>
961 </span>
962 </span>
963 <span class="p-tooltip p-tooltip--top-center">
964 <button class="p-button--neutral"
965 data-ng-click="openNewDatastorePanel()"
966 data-ng-hide="isAllStorageDisabled() || !canEdit()"
967 data-ng-disabled="!canPerformActionOnDatastoreSet()">
968 Create new datastore
969 </button>
970 <span data-ng-if="!canPerformActionOnDatastoreSet()">
971 <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id === 'vmfs6'">Select one or more devices to create a datastore</span>
972 <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id !== 'vmfs6'">Only available in the VMFS6 layout</span>
973 </span>
974 </span>
975 <span class="p-tooltip p-tooltip--top-center" data-ng-if="storageLayout.id === 'vmfs6'">
976 <button class="p-button--neutral"
977 data-ng-click="openAddToExistingDatastorePanel()"
978 data-ng-hide="isAllStorageDisabled() || !canEdit()"
979 data-ng-disabled="!canPerformActionOnDatastoreSet()">
980 Add to existing datastore
981 </button>
982 <span data-ng-if="!canPerformActionOnDatastoreSet()">
983 <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id === 'vmfs6'">Select one or more devices to add an existing datastore</span>
984 <span class="p-tooltip__message" role="tooltip" data-ng-if="storageLayout.id !== 'vmfs6'">Only available in the VMFS6 layout</span>
985 </span>
986 </span>
987 </div>
838 </div>988 </div>
839</div>989</div>
990
840<div class="p-strip is-shallow">991<div class="p-strip is-shallow">
841 <div class="row">992 <div class="row">
842 <hr>
843 <h3 class="p-heading--four">Used disks and partitions</h3>993 <h3 class="p-heading--four">Used disks and partitions</h3>
844 <table class="p-table-expanding">994 <table class="p-table-expanding p-table--used-disks">
845 <thead>995 <thead>
846 <tr>996 <tr class="p-table__row">
847 <th class="col-3">997 <th class="p-double-row p-table__cell">
848 <a data-ng-click="tableInfo.column = 'name'" data-ng-class="{'p-link--soft': tableInfo.column === 'name'}">Name</a>998 <div>Name</div>
849 <span class="divide"> | </span>999 <div>Serial</div>
850 <a data-ng-click="tableInfo.column = 'model'" data-ng-class="{'p-link--soft': tableInfo.column === 'model'}">Model</a>1000 </th>
851 <span class="divide"> | </span>1001 <th class="p-double-row p-table__cell">
852 <a data-ng-click="tableInfo.column = 'serial'" data-ng-class="{'p-link--soft': tableInfo.column === 'serial'}">Serial</a>1002 <div>Model</div>
853 <span class="divide"> | </span>1003 <div>Firmware</div>
854 <a data-ng-click="tableInfo.column = 'firmware_version'" data-ng-class="{'p-link--soft': tableInfo.column === 'firmware_version'}">Firmware</a>1004 </th>
1005 <th class="p-table__cell"><div class="u-align--center">Boot</div></th>
1006 <th class="p-table__cell">Size</th>
1007 <th class="p-double-row p-table__cell">
1008 <div>Type</div>
1009 <div>Tags</div>
1010 </th>
1011 <th class="p-table__cell">Health</th>
1012 <th class="p-double-row p-table__cell">
1013 <div>Used for</div>
1014 <div>Mount point</div>
855 </th>1015 </th>
856 <th class="col-1"><div class="u-align--center">Boot</div></th>
857 <th class="col-2">Device type</th>
858 <th class="col-3">Used for</th>
859 <th class="col-2">Health</th>
860 </tr>1016 </tr>
861 </thead>1017 </thead>
862 <tbody>1018 <tbody>
@@ -865,16 +1021,31 @@
865 No disk or partition has been fully utilized.1021 No disk or partition has been fully utilized.
866 </td>1022 </td>
867 </tr>1023 </tr>
868 <tr data-ng-repeat="item in used" class="table__row details__used">1024 <tr data-ng-repeat="item in used" class="table__row details__used p-table__row">
869 <td class="col-3" aria-label="Name">1025 <td class="p-double-row p-table__cell" aria-label="Name">
870 <span data-ng-show="tableInfo.column === 'name'" title="{$ item.name $}">{$ item.name $}</span>1026 <div class="p-double-rows__container">
871 <span data-ng-show="tableInfo.column === 'model'" title="{$ item.model $}">{$ item.model $}</span>1027 <div class="p-double-row__main-row">
872 <span data-ng-show="tableInfo.column === 'serial'" title="{$ item.serial $}">{$ item.serial $}</span>1028 {$ item.name $}
873 <span data-ng-show="tableInfo.column === 'firmware_version'" title="{$ item.firmware_version $}">{$ item.firmware_version $}</span>1029 </div>
1030 <div class="p-double-row__muted-row">
1031 {$ item.serial $}
1032 </div>
1033 </div>
1034 </td>
1035 <td class="p-double-row p-table__cell">
1036 <div class="p-double-rows__container">
1037 <div class="p-double-row__main-row">
1038 {$ item.model $}
1039 </div>
1040 <div class="p-double-row__muted-row">
1041 {$ item.firmware_version $}
1042 </div>
1043 </div>
874 </td>1044 </td>
875 <td class="col-1" aria-label="Boot disk">1045 <td aria-label="Boot disk" class="p-table__cell">
876 <div class="u-align--center">1046 <div class="u-align--center" data-ng-hide="item.parent_type !== 'vmfs6' && !item.is_boot">
877 <input type="radio" id="{$ item.name $}-boot" name="boot-disk"1047 <input type="radio" id="{$ item.name $}-boot" name="boot-disk"
1048 class="u-no-margin--right"
878 data-ng-click="setAsBootDisk(item)"1049 data-ng-click="setAsBootDisk(item)"
879 data-ng-checked="item.is_boot"1050 data-ng-checked="item.is_boot"
880 data-ng-if="item.type === 'physical'"1051 data-ng-if="item.type === 'physical'"
@@ -882,19 +1053,46 @@
882 <label for="{$ item.name $}-boot"></label>1053 <label for="{$ item.name $}-boot"></label>
883 </div>1054 </div>
884 </td>1055 </td>
885 <td class="col-2" aria-label="Device type" title="{$ getDeviceType(item) $}">{$ getDeviceType(item) $}</td>1056 <td class="p-table__cell">{$ item.size_human $}</td>
886 <td class="col-3" aria-label="Used for" title="{$ item.used_for $}">{$ item.used_for $}</td>1057 <td class="p-double-row p-table__cell">
887 <td class="col-2" aria-label="Health">1058 <div class="p-double-rows__container">
1059 <div class="p-double-row__main-row">
1060 {$ getDeviceType(item) $}
1061 </div>
1062 <div class="p-double-row__muted-row">
1063 <span class="table__tag" data-ng-repeat="tag in item.tags" data-ng-hide="item.$options.editingTags">
1064 <a href="#/machines/?query=storage_tags:({$ tag.text $})" title="{$ tag.text $}">{$ tag.text $}</a>
1065 </span>
1066 </div>
1067 </div>
1068 </td>
1069 <td aria-label="Health" class="p-table__cell">
888 <span data-ng-if="item.type === 'physical'">1070 <span data-ng-if="item.type === 'physical'">
889 <span data-maas-script-status="script-status" data-script-status="item.test_status"></span>1071 <span data-maas-script-status="script-status" data-script-status="item.test_status"></span>
890 <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>1072 <span
891 <span data-ng-if="item.test_status === 3 || item.test_status === 4 || item.test_status === 8" title="Error">Error</span>1073 data-ng-if="item.test_status === 0 || item.test_status === 1 || item.test_status === 2 || item.test_status === 5 || item.test_status === 7"
1074 title="Ok">Ok</span>
1075 <span data-ng-if="item.test_status === 3 || item.test_status === 4 || item.test_status === 8"
1076 title="Error">Error</span>
892 <span data-ng-if="item.test_status === 6" title="Degraded">Degraded</span>1077 <span data-ng-if="item.test_status === 6" title="Degraded">Degraded</span>
893 <span data-ng-if="item.test_status === -1" title="Unknown">Unknown</span>1078 <span data-ng-if="item.test_status === -1" title="Unknown">Unknown</span>
894 </span>1079 </span>
895 </td>1080 </td>
1081 <td class="p-double-row p-table__cell" aria-label="Used for" title="{$ item.used_for $}">
1082 <div class="p-double-rows__container">
1083 <div class="p-double-row__main-row">
1084 {$ item.used_for $}
1085 </div>
1086 <div class="p-double-row__muted-row">
1087 {$ item.mount_point $}
1088 </div>
1089 </div>
1090 </td>
896 </tr>1091 </tr>
897 </tbody>1092 </tbody>
898 </table>1093 </table>
899 </div>1094 </div>
900</div>1095</div>
1096
1097<p>Learn more about deploying <a href="https://docs.maas.io/en/installconfig-images" class="p-link--external" target="_blank">Windows</a></p>
1098<p>Change the default layout in <a href="/MAAS/settings/storage/">Settings &rsaquo; Storage</a></p>
diff --git a/src/maasserver/static/partials/nodedetails/storage/filesystems.html b/src/maasserver/static/partials/nodedetails/storage/filesystems.html
index 340739e..908e7aa 100644
--- a/src/maasserver/static/partials/nodedetails/storage/filesystems.html
+++ b/src/maasserver/static/partials/nodedetails/storage/filesystems.html
@@ -110,7 +110,7 @@
110 </tr>110 </tr>
111 </tbody>111 </tbody>
112 </table>112 </table>
113 <button class="p-button--neutral p-tooltip--top-center" data-ng-disabled="dropdown !== null" data-ng-class="{ 'p-tooltip': dropdown === null}"113 <button class="p-button--neutral p-tooltip--top-left" data-ng-disabled="dropdown !== null" data-ng-class="{ 'p-tooltip': dropdown === null}"
114 data-ng-if="!isAllStorageDisabled()" data-ng-click="addSpecialFilesystem()">114 data-ng-if="!isAllStorageDisabled()" data-ng-click="addSpecialFilesystem()">
115 Add special filesystem115 Add special filesystem
116 <span class="p-tooltip__message" role="tooltip">Create a tmpfs or ramfs filesystem</span>116 <span class="p-tooltip__message" role="tooltip">Create a tmpfs or ramfs filesystem</span>
diff --git a/src/maasserver/static/scss/_base_tables.scss b/src/maasserver/static/scss/_base_tables.scss
index 710e416..bfdd68e 100644
--- a/src/maasserver/static/scss/_base_tables.scss
+++ b/src/maasserver/static/scss/_base_tables.scss
@@ -40,7 +40,6 @@
40 flex-basis: auto !important;40 flex-basis: auto !important;
41 flex-grow: 0;41 flex-grow: 0;
42 vertical-align: top;42 vertical-align: top;
43 padding-bottom: 0.05rem;
4443
45 &:first-of-type {44 &:first-of-type {
46 padding-left: $sph-intra--condensed;45 padding-left: $sph-intra--condensed;
diff --git a/src/maasserver/static/scss/_patterns_notification.scss b/src/maasserver/static/scss/_patterns_notification.scss
index ab3dab2..65620d4 100644
--- a/src/maasserver/static/scss/_patterns_notification.scss
+++ b/src/maasserver/static/scss/_patterns_notification.scss
@@ -1,6 +1,7 @@
1@mixin maas-p-notifications {1@mixin maas-p-notifications {
2 @include maas-notification;2 @include maas-notification;
3 @include maas-notification-group;3 @include maas-notification-group;
4 @include maas-notification-subtle;
4}5}
56
6@mixin maas-notification-group {7@mixin maas-notification-group {
@@ -23,3 +24,14 @@
23 }24 }
24 }25 }
25}26}
27
28@mixin maas-notification-subtle {
29 [class*="p-notification"].is-subtle {
30 background: transparent;
31 box-shadow: none;
32
33 &::before {
34 height: 0;
35 }
36 }
37}
diff --git a/src/maasserver/static/scss/_tables.scss b/src/maasserver/static/scss/_tables.scss
index 47738fd..6194abb 100644
--- a/src/maasserver/static/scss/_tables.scss
+++ b/src/maasserver/static/scss/_tables.scss
@@ -326,9 +326,12 @@
326 }326 }
327 }327 }
328328
329 .p-table--disks-partitions {329 .p-table--disks-partitions,
330 .p-table--used-disks {
330 .p-table__row {331 .p-table__row {
331 .p-table__cell {332 .p-table__cell {
333 flex: 0 0 auto !important;
334
332 &:nth-child(1) {335 &:nth-child(1) {
333 width: 15%;336 width: 15%;
334 }337 }
@@ -346,22 +349,58 @@
346 }349 }
347350
348 &:nth-child(5) {351 &:nth-child(5) {
349 width: 12%;352 width: 22%;
350 }353 }
351354
352 &:nth-child(6) {355 &:nth-child(6) {
356 width: 22%;
357 }
358
359 &:nth-child(7) {
353 width: 10%;360 width: 10%;
354 }361 }
362 }
363 }
364 }
365
366 .p-table--used-disks {
367 .p-table__row {
368 .p-table__cell {
369 &:nth-child(6) {
370 width: 7%;
371 }
355372
356 &:nth-child(7) {373 &:nth-child(7) {
357 width: 12%;374 width: 25%;
358 }375 }
376 }
377 }
378 }
359379
360 &:nth-child(8) {380 .p-table--datastores {
361 width: 10%;381 flex: 0 0 auto !important;
382
383 .p-table__row {
384 .p-table__cell {
385 flex: 0 0 auto !important;
386
387 &:nth-child(1) {
388 width: 15%;
389 }
390
391 &:nth-child(2) {
392 width: 22%;
362 }393 }
363394
364 &:nth-child(9) {395 &:nth-child(3) {
396 width: 9%;
397 }
398
399 &:nth-child(4) {
400 width: 44%;
401 }
402
403 &:nth-child(5) {
365 width: 10%;404 width: 10%;
366 }405 }
367 }406 }
diff --git a/src/maasserver/static/scss/_utils.scss b/src/maasserver/static/scss/_utils.scss
index f6528d2..1724cf8 100644
--- a/src/maasserver/static/scss/_utils.scss
+++ b/src/maasserver/static/scss/_utils.scss
@@ -67,3 +67,7 @@ $table-h-indent: $sph-intra--condensed;
67.u-mirror--y {67.u-mirror--y {
68 transform: rotate(180deg);68 transform: rotate(180deg);
69}69}
70
71.u-rotate {
72 transform: rotate(180deg);
73}

Subscribers

People subscribed via source and target branches