Merge ~ya-bo-ng/maas:kvm-storage-pods-landing-view into maas:master

Proposed by Anthony Dillon
Status: Merged
Merge reported by: Blake Rouse
Merged at revision: 82e7e306cd6861e6c99e3d0839257244f4d56143
Proposed branch: ~ya-bo-ng/maas:kvm-storage-pods-landing-view
Merge into: maas:master
Diff against target: 794 lines (+509/-46)
14 files modified
src/maasserver/static/js/angular/3rdparty/ng-tags-input.js (+1/-1)
src/maasserver/static/js/angular/controllers/pod_details.js (+46/-11)
src/maasserver/static/js/angular/controllers/tests/test_pod_details.js (+4/-0)
src/maasserver/static/js/angular/filters/format_bytes.js (+75/-0)
src/maasserver/static/js/angular/filters/format_storage_type.js (+20/-0)
src/maasserver/static/js/angular/filters/tests/test_format_bytes.js (+39/-0)
src/maasserver/static/js/angular/filters/tests/test_storage_type.js (+29/-0)
src/maasserver/static/partials/pod-details.html (+85/-27)
src/maasserver/static/scss/_patterns_charts.scss (+40/-0)
src/maasserver/static/scss/_patterns_option-selector.scss (+130/-0)
src/maasserver/static/scss/_patterns_table-expanding.scss (+6/-7)
src/maasserver/static/scss/_tables.scss (+26/-0)
src/maasserver/static/scss/_utils.scss (+4/-0)
src/maasserver/static/scss/build.scss (+4/-0)
Reviewer Review Type Date Requested Status
Caleb Ellis (community) Needs Fixing
MAAS Lander Needs Fixing
MAAS Maintainers Pending
Review via email: mp+354690@code.launchpad.net

Commit message

KVM storage pod configuration UI

Description of the change

Added a storage drop down widget to the compose pod table.

To post a comment you must log in.
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b kvm-storage-pods-landing-view lp:~ya-bo-ng/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/3892/console
COMMIT: 671f56d044dcda5a288081eb8d1fdc8b23ed80c6

review: Needs Fixing
5c2ed41... by Anthony Dillon

Fix tests

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

UNIT TESTS
-b kvm-storage-pods-landing-view lp:~ya-bo-ng/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/3893/console
COMMIT: ba0d5fbae52d1fab586e84b5ee85f5001ea101c8

review: Needs Fixing
4f9ca99... by Anthony Dillon

Fix lint issues

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

UNIT TESTS
-b kvm-storage-pods-landing-view lp:~ya-bo-ng/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/3894/console
COMMIT: 1cdc58449c2eb58989a187085be7a530e2c164bc

review: Needs Fixing
d3d86f4... by Anthony Dillon

White space

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

UNIT TESTS
-b kvm-storage-pods-landing-view lp:~ya-bo-ng/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/3896/console
COMMIT: a253d14a19de98c8d4e8929825894b628a2bda2a

review: Needs Fixing
f52cea9... by Anthony Dillon

Make the dropdown keyboard accessible

e905752... by Anthony Dillon

Make the storage location unique to the storage object

f212454... by Anthony Dillon

Set default storage pool

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

UNIT TESTS
-b kvm-storage-pods-landing-view lp:~ya-bo-ng/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/3939/console
COMMIT: 5af783308fd8ba63cef2a4c4f248e3347c98dd68

review: Needs Fixing
Revision history for this message
Martin Storey (cassiocassio) wrote :

Following a call... notes:

8 GB mininimum check?
compose machine button grey and warning around capacity field?

default should be selected
“name (default)” shuold be be shown

tick align with name of pool?
spacing of green tick - not wide enough
tick should be black chosen tick not green success tick

love the “impossible to remove the boot partition” function… check with team nobody has a good reason to make unbootable machines, you never know

it’s big pulldown, but still a pulldown -
- single click to open - stays open
- click and drag - stays open, unike a system pulldown
- click to select an item, selects that pool, AND closes
- moving focus, e.g. tab to tags field, also closes…
- HOWEVER clicking on capacity

Capacity field is a number so should be input type="number”>

escape key to close dropdown
mouseover on cross rectangular - square would make more visual sense

focus to tags should close

consider: alighing used key to left and putting close X top right?

what happens when there are 10 pools to pick from, or 20?
max depth and clipping and autoscroll

clicking + to add another row needs to close any open dropdown?

handle multiple requests to the same pool in the same compose dialog with “other” colour and o+o in the key…
https://zpl.io/2j4v7GQ

a647124... by Anthony Dillon

Fix tests

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

UNIT TESTS
-b kvm-storage-pods-landing-view lp:~ya-bo-ng/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/3943/console
COMMIT: 0065a188774bed75a55f5d4eaacc55116b251bd6

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

UNIT TESTS
-b kvm-storage-pods-landing-view lp:~ya-bo-ng/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/3944/console
COMMIT: 91bbb1a41015ae7193dcbee00d533004c3ea1925

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

UNIT TESTS
-b kvm-storage-pods-landing-view lp:~ya-bo-ng/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci-jenkins.internal:8080/job/maas/job/branch-tester/3945/console
COMMIT: 72ed76c2098d62e7aa2b324f2b3be739d3075e70

review: Needs Fixing
9d0503e... by Anthony Dillon

Set the default pod storage

8499adc... by Anthony Dillon

Lint the pod details

Revision history for this message
Caleb Ellis (caleb-ellis) wrote :

- I get an angular error: TypeError: Cannot read property 'storage_pools' of null at Scope.$scope.getDefaultStoragePool. If I go back a couple of commits it works.

- How many different locations or storage configurations can we expect? It's already pretty close to the bottom of my viewport, and with it attached to a sticky header it's possible for content to become unreachable. Maybe we can remove sticky when the header is expanded?

- Other comments inline

review: Needs Fixing
82e7e30... by Anthony Dillon

Fix the defaultPod function call order

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/static/js/angular/3rdparty/ng-tags-input.js b/src/maasserver/static/js/angular/3rdparty/ng-tags-input.js
2index 86eb45c..481d915 100644
3--- a/src/maasserver/static/js/angular/3rdparty/ng-tags-input.js
4+++ b/src/maasserver/static/js/angular/3rdparty/ng-tags-input.js
5@@ -1119,7 +1119,7 @@ tagsInput.factory('tiUtil', ["$timeout", function($timeout) {
6 /* HTML templates */
7 tagsInput.run(["$templateCache", function($templateCache) {
8 $templateCache.put('ngTagsInput/tags-input.html',
9- "<div class=\"host\" tabindex=\"-1\" data-ng-click=\"eventHandlers.host.click()\" ti-transclude-append=\"\"><div class=\"tags\" data-ng-class=\"{focused: hasFocus}\"><ul class=\"tag-list\"><li class=\"tag-item\" data-ng-repeat=\"tag in tagList.items track by track(tag)\" data-ng-class=\"{ selected: tag == tagList.selected }\"><ti-tag-item data=\"tag\"></ti-tag-item></li></ul><input class=\"input u-no-margin--top\" autocomplete=\"off\" data-ng-model=\"newTag.text\" data-ng-change=\"eventHandlers.input.change(newTag.text)\" data-ng-keydown=\"eventHandlers.input.keydown($event)\" data-ng-focus=\"eventHandlers.input.focus($event)\" data-ng-blur=\"eventHandlers.input.blur($event)\" data-ng-paste=\"eventHandlers.input.paste($event)\" data-ng-trim=\"false\" data-ng-class=\"{'invalid-tag': newTag.invalid}\" data-ng-disabled=\"disabled\" ti-bind-attrs=\"{type: options.type, placeholder: options.placeholder, tabindex: options.tabindex, spellcheck: options.spellcheck}\" ti-autosize=\"\"></div></div>"
10+ "<div class=\"host\" tabindex=\"-1\" data-ng-click=\"eventHandlers.host.click()\" ti-transclude-append=\"\"><div class=\"tags\" data-ng-class=\"{focused: hasFocus}\"><ul class=\"tag-list\"><li class=\"tag-item\" data-ng-repeat=\"tag in tagList.items track by track(tag)\" data-ng-class=\"{ selected: tag == tagList.selected }\"><ti-tag-item data=\"tag\"></ti-tag-item></li></ul><input class=\"input u-no-margin--top u-no-margin--bottom\" autocomplete=\"off\" data-ng-model=\"newTag.text\" data-ng-change=\"eventHandlers.input.change(newTag.text)\" data-ng-keydown=\"eventHandlers.input.keydown($event)\" data-ng-focus=\"eventHandlers.input.focus($event)\" data-ng-blur=\"eventHandlers.input.blur($event)\" data-ng-paste=\"eventHandlers.input.paste($event)\" data-ng-trim=\"false\" data-ng-class=\"{'invalid-tag': newTag.invalid}\" data-ng-disabled=\"disabled\" ti-bind-attrs=\"{type: options.type, placeholder: options.placeholder, tabindex: options.tabindex, spellcheck: options.spellcheck}\" ti-autosize=\"\"></div></div>"
11 );
12
13 $templateCache.put('ngTagsInput/tag-item.html',
14diff --git a/src/maasserver/static/js/angular/controllers/pod_details.js b/src/maasserver/static/js/angular/controllers/pod_details.js
15index 8f015cc..192274a 100644
16--- a/src/maasserver/static/js/angular/controllers/pod_details.js
17+++ b/src/maasserver/static/js/angular/controllers/pod_details.js
18@@ -53,6 +53,7 @@ angular.module('MAAS').controller('PodDetailsController', [
19 type: 'local',
20 size: 8,
21 tags: [],
22+ pool: {},
23 boot: true
24 }]
25 }
26@@ -211,6 +212,21 @@ angular.module('MAAS').controller('PodDetailsController', [
27 });
28 };
29
30+ $scope.openOptions = function(storage) {
31+ angular.forEach($scope.compose.obj.storage, function(disk) {
32+ disk.showOptions = false;
33+ });
34+ storage.showOptions = true;
35+ }
36+
37+ $scope.closeOptions = function(storage) {
38+ storage.showOptions = false;
39+ }
40+
41+ $scope.selectStoragePool = function(storagePool, storage) {
42+ storage.pool = storagePool;
43+ }
44+
45 // Return the title of the pod type.
46 $scope.getPodTypeTitle = function() {
47 var i;
48@@ -274,6 +290,7 @@ angular.module('MAAS').controller('PodDetailsController', [
49 type: 'local',
50 size: 8,
51 tags: [],
52+ pool: {},
53 boot: true
54 }]
55 };
56@@ -282,16 +299,18 @@ angular.module('MAAS').controller('PodDetailsController', [
57
58 // Add another storage device.
59 $scope.composeAddStorage = function() {
60- var storage = {
61- type: 'local',
62- size: 8,
63- tags: [],
64- boot: false
65- };
66- if($scope.pod.capabilities.indexOf('iscsi_storage') >= 0) {
67- storage.type = 'iscsi';
68- }
69- $scope.compose.obj.storage.push(storage);
70+ var storage = {
71+ type: 'local',
72+ size: 8,
73+ tags: [],
74+ pool: $scope.getDefaultStoragePool(),
75+ boot: false
76+ };
77+
78+ if($scope.pod.capabilities.indexOf('iscsi_storage') >= 0) {
79+ storage.type = 'iscsi';
80+ }
81+ $scope.compose.obj.storage.push(storage);
82 };
83
84 // Change which disk is the boot disk.
85@@ -302,6 +321,18 @@ angular.module('MAAS').controller('PodDetailsController', [
86 storage.boot = true;
87 };
88
89+ // Get the default pod pool
90+ $scope.getDefaultStoragePool = function () {
91+ var defaultPool = {};
92+ console.log($scope);
93+ if($scope.pod.storage_pools && $scope.pod.default_storage_pool) {
94+ defaultPool = $scope.pod.storage_pools.filter(
95+ pool => pool.id == $scope.pod.default_storage_pool
96+ )[0];
97+ }
98+ return defaultPool;
99+ };
100+
101 // Remove a disk from storage config.
102 $scope.composeRemoveDisk = function(storage) {
103 var idx = $scope.compose.obj.storage.indexOf(storage);
104@@ -357,8 +388,10 @@ angular.module('MAAS').controller('PodDetailsController', [
105 // the activeItem.
106 var activePod = PodsManager.getActiveItem();
107 if(angular.isObject(activePod) &&
108- activePod.id === parseInt($routeParams.id, 10)) {
109+ activePod.id === parseInt($routeParams.id, 10)) {
110 $scope.pod = activePod;
111+ $scope.compose.obj.storage[0].pool = (
112+ $scope.getDefaultStoragePool());
113 $scope.loaded = true;
114 $scope.machinesSearch = 'pod-id:=' + $scope.pod.id;
115 $scope.startWatching();
116@@ -366,6 +399,8 @@ angular.module('MAAS').controller('PodDetailsController', [
117 PodsManager.setActiveItem(
118 parseInt($routeParams.id, 10)).then(function(pod) {
119 $scope.pod = pod;
120+ $scope.compose.obj.storage[0].pool = (
121+ $scope.getDefaultStoragePool());
122 $scope.loaded = true;
123 $scope.machinesSearch = 'pod-id:=' + $scope.pod.id;
124 $scope.startWatching();
125diff --git a/src/maasserver/static/js/angular/controllers/tests/test_pod_details.js b/src/maasserver/static/js/angular/controllers/tests/test_pod_details.js
126index f1f9322..998d263 100644
127--- a/src/maasserver/static/js/angular/controllers/tests/test_pod_details.js
128+++ b/src/maasserver/static/js/angular/controllers/tests/test_pod_details.js
129@@ -157,6 +157,7 @@ describe("PodDetailsController", function() {
130 type: 'local',
131 size: 8,
132 tags: [],
133+ pool: {},
134 boot: true
135 }]
136 }
137@@ -660,6 +661,7 @@ describe("PodDetailsController", function() {
138 type: 'local',
139 size: 8,
140 tags: [],
141+ pool: {},
142 boot: true
143 }]
144 });
145@@ -678,6 +680,7 @@ describe("PodDetailsController", function() {
146 type: 'local',
147 size: 8,
148 tags: [],
149+ pool: {},
150 boot: false
151 });
152 });
153@@ -692,6 +695,7 @@ describe("PodDetailsController", function() {
154 type: 'iscsi',
155 size: 8,
156 tags: [],
157+ pool: {},
158 boot: false
159 });
160 });
161diff --git a/src/maasserver/static/js/angular/filters/format_bytes.js b/src/maasserver/static/js/angular/filters/format_bytes.js
162new file mode 100644
163index 0000000..1ea393d
164--- /dev/null
165+++ b/src/maasserver/static/js/angular/filters/format_bytes.js
166@@ -0,0 +1,75 @@
167+/* Copyright 2016-2018 Canonical Ltd. This software is licensed under the
168+ * GNU Affero General Public License version 3 (see the file LICENSE).
169+ *
170+ * Converts bytes into human readable string, e.g. 10 GB
171+ */
172+
173+angular.module('MAAS').filter('formatBytes', function() {
174+ return function(bytes) {
175+ var bytesInKilobyte = 1000;
176+ var kilobytesInMegabyte = 1000;
177+ var megabytesInGigabyte = 1000;
178+ var gigabytesInTerabyte = 1000;
179+
180+ var bytesInMegabyte = (
181+ bytesInKilobyte * kilobytesInMegabyte
182+ );
183+ var bytesInGigabyte = (
184+ bytesInKilobyte * kilobytesInMegabyte * megabytesInGigabyte
185+ );
186+ var bytesInTerabyte = (
187+ bytesInKilobyte *
188+ kilobytesInMegabyte *
189+ megabytesInGigabyte * gigabytesInTerabyte
190+ );
191+
192+ if (bytes >= bytesInTerabyte) {
193+ return Math.round(
194+ bytes /
195+ bytesInKilobyte /
196+ kilobytesInMegabyte /
197+ megabytesInGigabyte /
198+ gigabytesInTerabyte
199+ ) + ' TB';
200+ } else if (bytes >= bytesInGigabyte) {
201+ return Math.round(
202+ bytes /
203+ bytesInKilobyte /
204+ kilobytesInMegabyte /
205+ megabytesInGigabyte
206+ ) + ' GB';
207+ } else if (bytes >= bytesInMegabyte) {
208+ return Math.round(
209+ bytes /
210+ bytesInKilobyte /
211+ kilobytesInMegabyte
212+ ) + ' MB';
213+ } else if (bytes >= bytesInKilobyte) {
214+ return Math.round(bytes / bytesInKilobyte) + ' KB';
215+ } else if (bytes > 0) {
216+ return bytes + ' B';
217+ } else {
218+ return 0;
219+ }
220+ }
221+});
222+
223+angular.module('MAAS').filter('convertGigabyteToBytes', function() {
224+ return function(gigabytes) {
225+ var bytesInKilobyte = 1000;
226+ var kilobytesInMegabyte = 1000;
227+ var megabytesInGigabyte = 1000;
228+
229+ if (gigabytes) {
230+ return Math.round(
231+ gigabytes *
232+ bytesInKilobyte *
233+ kilobytesInMegabyte *
234+ megabytesInGigabyte
235+ );
236+ } else {
237+ return 0;
238+ }
239+ }
240+});
241+
242diff --git a/src/maasserver/static/js/angular/filters/format_storage_type.js b/src/maasserver/static/js/angular/filters/format_storage_type.js
243new file mode 100644
244index 0000000..2d05113
245--- /dev/null
246+++ b/src/maasserver/static/js/angular/filters/format_storage_type.js
247@@ -0,0 +1,20 @@
248+/* Copyright 2016-2018 Canonical Ltd. This software is licensed under the
249+ * GNU Affero General Public License version 3 (see the file LICENSE).
250+ *
251+ * Converts storage type into human readable string, e.g. LVM
252+ */
253+
254+angular.module('MAAS').filter('formatStorageType', function() {
255+ return function(storageType) {
256+ if (!storageType) {
257+ return '';
258+ }
259+
260+ switch(storageType) {
261+ case 'lvm':
262+ return 'LVM';
263+ default:
264+ return storageType;
265+ }
266+ }
267+});
268diff --git a/src/maasserver/static/js/angular/filters/tests/test_format_bytes.js b/src/maasserver/static/js/angular/filters/tests/test_format_bytes.js
269new file mode 100644
270index 0000000..4086438
271--- /dev/null
272+++ b/src/maasserver/static/js/angular/filters/tests/test_format_bytes.js
273@@ -0,0 +1,39 @@
274+/* Copyright 2015-2018 Canonical Ltd. This software is licensed under the
275+ * GNU Affero General Public License version 3 (see the file LICENSE).
276+ *
277+ * Unit tests for formatBytes.
278+ */
279+
280+describe("formatBytes", function() {
281+
282+ // Load the MAAS module.
283+ beforeEach(module("MAAS"));
284+
285+ // Load the formatBytes.
286+ var formatBytes;
287+ beforeEach(inject(function($filter) {
288+ formatBytes = $filter("formatBytes");
289+ }));
290+
291+ it("returns zero if undefined bytes", function() {
292+ expect(formatBytes()).toEqual(0);
293+ });
294+
295+ it("returns value in bytes if less than a kilobyte", function() {
296+ expect(formatBytes(456)).toEqual('456 B');
297+ });
298+
299+ it("returns value in kilobytes if less than a megabyte", function() {
300+ expect(formatBytes(568000000)).toEqual('568 MB');
301+ });
302+
303+ it("returns value in gigabytes if less than a terabyte", function() {
304+ expect(formatBytes(382000000000)).toEqual('382 GB');
305+ });
306+
307+ it("returns value in terabytes if great than or equal to 1 terabyte",
308+ function() {
309+ expect(formatBytes(2000000000000)).toEqual('2 TB');
310+ }
311+ );
312+});
313diff --git a/src/maasserver/static/js/angular/filters/tests/test_storage_type.js b/src/maasserver/static/js/angular/filters/tests/test_storage_type.js
314new file mode 100644
315index 0000000..5de13ca
316--- /dev/null
317+++ b/src/maasserver/static/js/angular/filters/tests/test_storage_type.js
318@@ -0,0 +1,29 @@
319+/* Copyright 2015-2018 Canonical Ltd. This software is licensed under the
320+ * GNU Affero General Public License version 3 (see the file LICENSE).
321+ *
322+ * Unit tests for formatStorageType.
323+ */
324+
325+describe("formatStorageType", function() {
326+
327+ // Load the MAAS module.
328+ beforeEach(module("MAAS"));
329+
330+ // Load the storageType.
331+ var storageType;
332+ beforeEach(inject(function($filter) {
333+ storageType = $filter("formatStorageType");
334+ }));
335+
336+ it("returns empty string if undefined storage type", function() {
337+ expect(storageType()).toEqual('');
338+ });
339+
340+ it("returns original value if not recognised", function() {
341+ expect(storageType('foo')).toEqual('foo');
342+ });
343+
344+ it("returns formatted when recognised", function() {
345+ expect(storageType('lvm')).toEqual('LVM');
346+ });
347+});
348diff --git a/src/maasserver/static/partials/pod-details.html b/src/maasserver/static/partials/pod-details.html
349index 1b41b4d..a30f03c 100644
350--- a/src/maasserver/static/partials/pod-details.html
351+++ b/src/maasserver/static/partials/pod-details.html
352@@ -16,7 +16,7 @@
353 data-ng-model="name.value"
354 data-ng-disabled="!canEdit()"
355 data-ng-click="editName()"></span>
356- </h1>
357+ </h1>
358 <button class="p-button--base u-no-margin--top ng-hide"
359 data-ng-show="name.editing"
360 data-ng-click="cancelEditName()">Cancel</button>
361@@ -64,41 +64,97 @@
362 <div class="row">
363 <div class="col-12">
364 <h3 class="p-heading--four">Storage configuration</h3>
365- <table class="p-table-expanding">
366+ <table class="p-table-expanding p-table--pod-storage-config">
367 <thead>
368- <tr>
369- <th class="col-2">Location</th>
370- <th class="col-3">Capacity (GB)</th>
371- <th class="col-4">Tags</th>
372- <th class="col-1">
373+ <tr class="p-table__row">
374+ <th></th>
375+ <th>Capacity (GB)</th>
376+ <th>Location</th>
377+ <th>Tags</th>
378+ <th>
379 <div class="u-align--center">Boot</div>
380 </th>
381- <th class="col-2"></th>
382 </tr>
383 </thead>
384 <tbody>
385- <tr data-ng-repeat="storage in compose.obj.storage">
386- <td class="col-2">
387+ <tr class="p-table__row" data-ng-repeat="storage in compose.obj.storage">
388+ <td>
389+ <div data-ng-if="!storage.boot">
390+ <button class="p-button--base" data-ng-click="composeRemoveDisk(storage)">
391+ <i class="p-icon--close"></i>
392+ <span class="u-off-screen">Remove</span>
393+ </button>
394+ </div>
395+ </td>
396+ <td>
397 <div class="form__group-input">
398- <select data-ng-model="storage.type">
399- <option value="local">Local</option>
400- <option value="iscsi"
401- data-ng-if="pod.capabilities.indexOf('iscsi_storage') >= 0"
402- data-ng-disabled="storage.boot">iSCSI</option>
403- </select>
404+ <input type="text" class="u-no-margin--bottom" placeholder="Enter capacity" data-ng-model="storage.size">
405 </div>
406 </td>
407- <td class="col-3">
408+ <td style="overflow: visible">
409 <div class="form__group-input">
410- <input type="text" placeholder="Enter capacity" data-ng-model="storage.size">
411+ <div class="p-option-selector" tabindex="0">
412+ <input class="p-option-selector__input u-no-margin--bottom" type="text" data-ng-model="storage.pool.name" ng-focus="openOptions(storage)">
413+ <div class="p-option-selector__options" ng-if="storage.showOptions" ng-keyup="$event.keyCode == 27 ? closeOptions(storage) : null">
414+ <button class="p-button--base u-float--left" ng-click="closeOptions(storage)">
415+ <i class="p-icon--close">Close</i>
416+ </button>
417+ <ul class="p-inline-list u-align--right p-option-selector__options-key">
418+ <li class="p-inline-list__item">
419+ <span class="p-key-icon--used"></span> Used
420+ </li>
421+ <li class="p-inline-list__item">
422+ <span class="p-key-icon--requests"></span> Requests
423+ </li>
424+ <li class="p-inline-list__item">
425+ <span class="p-key-icon--free"></span> Free
426+ </li>
427+ </ul>
428+ <hr class="u-no-margin--bottom">
429+ <div class="p-option-selector__option"
430+ tabindex="0"
431+ ng-repeat="storage_pool in pod.storage_pools" ng-click="selectStoragePool(storage_pool, storage)" ng-keyup="$event.keyCode == 13 ? selectStoragePool(storage_pool, storage) : null" ng-class="{'is-selected': storage.pool === storage_pool}">
432+ <div class="p-option-selector__option-cell">
433+ <div><strong>{$ storage_pool.name $}</strong></div>
434+ <div class="u-text--light">{$ storage_pool.path $}</div>
435+ </div>
436+ <div class="p-option-selector__option-cell u-align--right">
437+ <div>{$ storage_pool.type | formatStorageType $}</div>
438+ <div>{$ storage_pool.total | formatBytes $}</div>
439+ </div>
440+ <div class="p-option-selector__option-cell">
441+ <div class="p-chart">
442+ <div class="p-chart__bar--requests" style="width: {$ (storage_pool.used / storage_pool.total * 100) + ((storage.size | convertGigabyteToBytes) / storage_pool.total * 100) $}%"
443+ ng-class="{'is-over': (storage_pool.used / storage_pool.total * 100) + ((storage.size | convertGigabyteToBytes) / storage_pool.total * 100) >= 100}"></div>
444+ <div class="p-chart__bar--used" style="width: {$ storage_pool.used / storage_pool.total * 100 $}%"></div>
445+ </div>
446+
447+ <ul class="p-inline-list u-no-margin--bottom p-space-between">
448+ <li class="p-inline-list__item">
449+ <span class="p-key-icon--used"></span>
450+ {$ storage_pool.used | formatBytes $}
451+ </li>
452+ <li class="p-inline-list__item">
453+ <span class="p-key-icon--requests"></span>
454+ {$ storage.size $} GB
455+ </li>
456+ <li class="p-inline-list__item">
457+ <span class="p-key-icon--free"></span>
458+ {$ storage_pool.available | formatBytes $}
459+ </li>
460+ </ul>
461+ </div>
462+ </div>
463+ </div>
464+ </div>
465 </div>
466 </td>
467- <td class="col-4">
468+ <td>
469 <div class="form__group-input">
470- <tags-input data-ng-model="storage.tags" allow-tags-pattern="[\\w-]+"></tags-input>
471+ <tags-input data-ng-model="storage.tags" class="u-no-margin--bottom" allow-tags-pattern="[\\w-]+"></tags-input>
472 </div>
473 </td>
474- <td class="col-1">
475+ <td>
476 <div class="u-align--center">
477 <input type="radio" id="{$ $index $}-boot" class="u-no-margin--right"
478 data-ng-click="composeSetBootDisk(storage)"
479@@ -107,19 +163,21 @@
480 <label for="{$ $index $}-boot"></label>
481 </div>
482 </td>
483- <td class="col-2">
484- <div class="u-align--right" data-ng-if="!storage.boot">
485- <button data-ng-click="composeRemoveDisk(storage)">Remove</button>
486- </div>
487- </td>
488 </tr>
489+ <tr class="p-table__row">
490+ <td>
491+ <button class="p-button--base" data-ng-click="composeAddStorage()">
492+ <i class="p-icon--plus"></i>
493+ <span class="u-off-screen">Add another device</span>
494+ </button>
495+ </td>
496 </tbody>
497 </table>
498- <button class="p-button--neutral" data-ng-click="composeAddStorage()">Add another device</button>
499 </div>
500 </div>
501 <div class="row">
502 <div class="col-12">
503+ <hr>
504 <p maas-obj-hide-saving><maas-obj-errors></maas-obj-errors></p>
505 <p maas-obj-show-saving><maas-obj-saving>Composing machine</maas-obj-saving></p>
506 <div class="u-align--right u-no-margin--top" maas-obj-hide-saving>
507diff --git a/src/maasserver/static/scss/_patterns_charts.scss b/src/maasserver/static/scss/_patterns_charts.scss
508new file mode 100644
509index 0000000..2f391ac
510--- /dev/null
511+++ b/src/maasserver/static/scss/_patterns_charts.scss
512@@ -0,0 +1,40 @@
513+@mixin patterns_charts {
514+ .p-chart {
515+ position: relative;
516+ overflow: hidden;
517+ width: 100%;
518+ height: 1rem;
519+ border-radius: 1rem;
520+ background-color: $color-x-light;
521+ border: 1px solid $color-mid-light;
522+ margin-top: .25rem;
523+ margin-bottom: .25rem;
524+ }
525+
526+ .p-chart__bar {
527+ position: absolute;
528+ top: 0;
529+ bottom: 0;
530+ left: 0;
531+ }
532+
533+ .p-chart__bar--used {
534+ @extend .p-chart__bar;
535+ background-color: #c6b3d4;
536+ border-right: 1px solid $color-x-light;
537+ }
538+
539+ .p-chart__bar--other {
540+ @extend .p-chart__bar;
541+ background-color: #adcde1;
542+ }
543+
544+ .p-chart__bar--requests {
545+ @extend .p-chart__bar;
546+ background-color: #3a77af;
547+
548+ &.is-over {
549+ background-color: #ff8936;
550+ }
551+ }
552+}
553diff --git a/src/maasserver/static/scss/_patterns_option-selector.scss b/src/maasserver/static/scss/_patterns_option-selector.scss
554new file mode 100644
555index 0000000..badafbd
556--- /dev/null
557+++ b/src/maasserver/static/scss/_patterns_option-selector.scss
558@@ -0,0 +1,130 @@
559+@mixin patterns_option-selector {
560+ .p-option-selector {
561+ position: relative;
562+ }
563+
564+ .p-option-selector__input {
565+ @extend select;
566+ }
567+
568+ .p-option-selector__options {
569+ position: absolute;
570+ top: 2.25rem;
571+ padding: $sph-intra 0 0;
572+ border: 1px solid $color-mid-light;
573+ border-radius: $border-radius;
574+ box-shadow: $box-shadow;
575+ background-color: $color-x-light;
576+ z-index: 10;
577+ width: 100%;
578+
579+ @media (min-width: $breakpoint-medium) {
580+ width: 70vw;
581+ }
582+
583+ @media (min-width: $breakpoint-large) {
584+ width: 750px;
585+ }
586+ }
587+
588+ .p-option-selector__options-key {
589+ padding: 0 $sph-intra;
590+ }
591+
592+ .p-option-selector__option {
593+ @include vf-focus;
594+ display: flex;
595+ align-items: center;
596+ flex-wrap: wrap;
597+ padding: $sph-intra $sph-intra;
598+ border-bottom: 1px solid $color-mid-light;
599+ cursor: pointer;
600+
601+ &:last-child {
602+ border-bottom: 0;
603+ }
604+
605+ &:hover {
606+ background-color: $color-light;
607+ }
608+
609+ &.is-selected {
610+ background-color: $color-light;
611+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='17' height='17' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='translate(1 1)' fill='none' fill-rule='evenodd'%3E%3Ccircle stroke='%230e8420' stroke-width='1.5' fill='%230e8420' cx='7.25' cy='7.25' r='7.25'/%3E%3Cpath fill='%23fff' d='M11.05 4.173l-.066.058L6.25 8.378l-2.776-2.38-.839.948L6.25 10.75l5.5-5.787-.7-.79z'/%3E%3C/g%3E%3C/svg%3E");
612+ background-repeat: no-repeat;
613+ background-position: 10px center;
614+ }
615+ }
616+
617+ .p-option-selector__option-cell {
618+ padding: $sp-small $sp-medium;
619+
620+ &:first-child {
621+ width: 70%;
622+
623+ @media (min-width: $breakpoint-medium) {
624+ border-right: 1px solid $color-mid-light;
625+ width: 35%;
626+ }
627+
628+ @media (min-width: $breakpoint-large) {
629+ width: 40%;
630+ }
631+ }
632+
633+ > * {
634+ overflow: hidden;
635+ text-overflow: ellipsis;
636+ white-space: nowrap;
637+ }
638+
639+ &:nth-child(2) {
640+ width: 30%;
641+
642+ @media (min-width: $breakpoint-medium) {
643+ width: 20%;
644+ border-right: 1px solid $color-mid-light;
645+ }
646+
647+ @media (min-width: $breakpoint-large) {
648+ width: 15%;
649+ }
650+ }
651+
652+ &:nth-child(3) {
653+ width: 100%;
654+
655+ @media (min-width: $breakpoint-medium) {
656+ width: 45%;
657+ }
658+ }
659+ }
660+}
661+
662+// Key inside option selector
663+.p-key-icon {
664+ display: inline-block;
665+ width: .75rem;
666+ height: .75rem;
667+ border-width: 1px;
668+ border-style: solid;
669+ border-radius: 50%;
670+}
671+
672+.p-key-icon--free {
673+ @extend .p-key-icon;
674+ background-color: $color-x-light;
675+ border-color: $color-mid-light;
676+}
677+
678+.p-key-icon--used {
679+ @extend .p-key-icon;
680+ background-color: #c6b3d4;
681+ border-color: #c6b3d4;
682+}
683+
684+.p-key-icon--requests {
685+ @extend .p-key-icon;
686+ background-color: #3a77af;
687+ border-color: #3a77af;
688+}
689diff --git a/src/maasserver/static/scss/_patterns_table-expanding.scss b/src/maasserver/static/scss/_patterns_table-expanding.scss
690index d210c3f..0298c81 100644
691--- a/src/maasserver/static/scss/_patterns_table-expanding.scss
692+++ b/src/maasserver/static/scss/_patterns_table-expanding.scss
693@@ -1,10 +1,4 @@
694 @mixin maas-table-expanding {
695- .p-table-expanding__panel {
696- // .p-form-validation .p-form-validation__input {
697- // padding: .5rem .75rem;
698- // }
699- }
700-
701 .p-table-expanding {
702 .p-table-expanding__panel {
703 @extend %vf-card;
704@@ -30,7 +24,6 @@
705 }
706 }
707
708-
709 tr {
710 display: table-row; //XXX (2) ove flex to only direct descendants as it is breaking nexsted tables
711 .is-active {
712@@ -38,8 +31,14 @@
713 }
714 }
715 }
716+
717 .p-table-expanding > thead > tr,
718 .p-table-expanding > tbody > tr {
719 display: flex; //XXX (1): move flex to only direct descendants as it is breaking nexsted tables
720 }
721+
722+ // Remove margins from tag inputs within tables
723+ .p-table-expanding .tags-input .tags .input {
724+ margin-bottom: 0;
725+ }
726 }
727diff --git a/src/maasserver/static/scss/_tables.scss b/src/maasserver/static/scss/_tables.scss
728index 509e233..0f21734 100644
729--- a/src/maasserver/static/scss/_tables.scss
730+++ b/src/maasserver/static/scss/_tables.scss
731@@ -452,6 +452,32 @@
732 }
733 }
734
735+ .p-table--pod-storage-config {
736+ @media (min-width: $breakpoint-small) {
737+ margin-bottom: 0;
738+
739+ .p-table__row {
740+ th, td {
741+ &:nth-child(1) {
742+ width: 5%;
743+ }
744+ &:nth-child(2) {
745+ width: 20%;
746+ }
747+ &:nth-child(3) {
748+ width: 30%;
749+ }
750+ &:nth-child(4) {
751+ width: 35%;
752+ }
753+ &:nth-child(5) {
754+ width: 10%;
755+ }
756+ }
757+ }
758+ }
759+ }
760+
761 .p-double-row {
762 $checkbox-space: 1rem + $sph-intra;
763 $icon-space: map-get($icon-sizes, default) + $sph-intra--condensed;
764diff --git a/src/maasserver/static/scss/_utils.scss b/src/maasserver/static/scss/_utils.scss
765index 2587691..8e95c14 100644
766--- a/src/maasserver/static/scss/_utils.scss
767+++ b/src/maasserver/static/scss/_utils.scss
768@@ -19,3 +19,7 @@
769 .u-flex--no-wrap {
770 display: flex;
771 }
772+
773+.u-text--light {
774+ color: $color-mid-dark;
775+}
776diff --git a/src/maasserver/static/scss/build.scss b/src/maasserver/static/scss/build.scss
777index 0846e1a..7a54ba4 100644
778--- a/src/maasserver/static/scss/build.scss
779+++ b/src/maasserver/static/scss/build.scss
780@@ -33,6 +33,8 @@
781 @import 'tables';
782 @import 'patterns_space-between';
783 @import 'patterns_filter';
784+@import 'patterns_charts';
785+@import 'patterns_option-selector';
786
787 // Include local patterns
788 @include maas;
789@@ -61,3 +63,5 @@
790 @include maas-table-sortable;
791 @include maas-table-widths;
792 @include patterns_space-between;
793+@include patterns_charts;
794+@include patterns_option-selector;

Subscribers

People subscribed via source and target branches