From 6b63576c15d63f868a3060f825f338c227eb4663 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 29 May 2018 10:05:16 -0700 Subject: [PATCH 01/10] Data modeling for node drain and scheduling eligibility --- ui/app/models/node.js | 4 ++++ ui/app/serializers/node.js | 1 + 2 files changed, 5 insertions(+) diff --git a/ui/app/models/node.js b/ui/app/models/node.js index 4dbb4fec6f9..d90057dfce0 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -1,4 +1,5 @@ import { computed } from '@ember/object'; +import { equal } from '@ember/object/computed'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { hasMany } from 'ember-data/relationships'; @@ -11,6 +12,7 @@ export default Model.extend({ name: attr('string'), datacenter: attr('string'), isDraining: attr('boolean'), + schedulingEligibility: attr('string'), status: attr('string'), statusDescription: attr('string'), shortId: shortUUIDProperty('id'), @@ -24,6 +26,8 @@ export default Model.extend({ resources: fragment('resources'), reserved: fragment('resources'), + isEligible: equal('schedulingEligibility', 'eligible'), + address: computed('httpAddr', function() { return ipParts(this.get('httpAddr')).address; }), diff --git a/ui/app/serializers/node.js b/ui/app/serializers/node.js index a8d3410e2d9..e6a1f2c22cb 100644 --- a/ui/app/serializers/node.js +++ b/ui/app/serializers/node.js @@ -7,6 +7,7 @@ export default ApplicationSerializer.extend({ config: service(), attrs: { + isDraining: 'Drain', httpAddr: 'HTTPAddr', }, From c193a1dc4a9e5ad15441348d13c3799a6aeab4ac Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 29 May 2018 10:06:48 -0700 Subject: [PATCH 02/10] Update the clients list table to include drain and eligibility This makes it match the CLI node status output --- ui/app/templates/clients/index.hbs | 3 ++- ui/app/templates/components/client-node-row.hbs | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/app/templates/clients/index.hbs b/ui/app/templates/clients/index.hbs index 888c0563420..83ccc379619 100644 --- a/ui/app/templates/clients/index.hbs +++ b/ui/app/templates/clients/index.hbs @@ -27,8 +27,9 @@ {{#t.sort-by prop="id"}}ID{{/t.sort-by}} {{#t.sort-by class="is-200px is-truncatable" prop="name"}}Name{{/t.sort-by}} {{#t.sort-by prop="status"}}Status{{/t.sort-by}} + {{#t.sort-by prop="isDraining"}}Drain{{/t.sort-by}} + {{#t.sort-by prop="schedulingEligibility"}}Eligibility{{/t.sort-by}} Address - Port {{#t.sort-by prop="datacenter"}}Datacenter{{/t.sort-by}} # Allocs {{/t.head}} diff --git a/ui/app/templates/components/client-node-row.hbs b/ui/app/templates/components/client-node-row.hbs index 9a946006dfe..9a28d6bc921 100644 --- a/ui/app/templates/components/client-node-row.hbs +++ b/ui/app/templates/components/client-node-row.hbs @@ -8,8 +8,9 @@ {{#link-to "clients.client" node.id class="is-primary"}}{{node.shortId}}{{/link-to}} {{node.name}} {{node.status}} -{{node.address}} -{{node.port}} +{{if node.isDraining "true" "false"}} +{{node.schedulingEligibility}} +{{node.httpAddr}} {{node.datacenter}} {{#if node.allocations.isPending}} From bb855b5f2113968ce7477ef8e828890d389d0176 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 29 May 2018 10:27:24 -0700 Subject: [PATCH 03/10] Add ineligibility to the status light --- ui/app/models/node.js | 6 ++++++ ui/app/styles/components/node-status-light.scss | 4 ++++ ui/app/templates/clients/client.hbs | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ui/app/models/node.js b/ui/app/models/node.js index d90057dfce0..ead56d7cd47 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -56,4 +56,10 @@ export default Model.extend({ unhealthyDriverNames: computed('unhealthyDrivers.@each.name', function() { return this.get('unhealthyDrivers').mapBy('name'); }), + + // A status attribute that includes states not included in node status. + // Useful for coloring and sorting nodes + compositeStatus: computed('status', 'isEligible', function() { + return this.get('isEligible') ? this.get('status') : 'ineligible'; + }), }); diff --git a/ui/app/styles/components/node-status-light.scss b/ui/app/styles/components/node-status-light.scss index 8acb0cfce79..9077e333a8f 100644 --- a/ui/app/styles/components/node-status-light.scss +++ b/ui/app/styles/components/node-status-light.scss @@ -26,4 +26,8 @@ $size: 0.75em; darken($grey-lighter, 25%) 6px ); } + + &.ineligible { + background: $warning; + } } diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index f5ad36c9f49..c68c67ac00c 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -9,7 +9,7 @@ {{#gutter-menu class="page-body"}}

- + {{or model.name model.shortId}} {{model.id}}

From df81f316af3509916a0801a72a823a2a77557c57 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 29 May 2018 10:32:41 -0700 Subject: [PATCH 04/10] Add drain and eligibility to the client details strip --- ui/app/styles/components/status-text.scss | 11 +++++++++++ ui/app/templates/clients/client.hbs | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/ui/app/styles/components/status-text.scss b/ui/app/styles/components/status-text.scss index 7e49ab4e103..b8a49ce1cfc 100644 --- a/ui/app/styles/components/status-text.scss +++ b/ui/app/styles/components/status-text.scss @@ -1,4 +1,6 @@ .status-text { + font-weight: $weight-semibold; + &.node-ready { color: $nomad-green-dark; } @@ -10,4 +12,13 @@ &.node-initializing { color: $grey; } + + @each $name, $pair in $colors { + $color: nth($pair, 1); + $color-invert: nth($pair, 2); + + &.is-#{$name} { + color: $color; + } + } } diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index c68c67ac00c..7f0ab1d1cfd 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -25,6 +25,22 @@ Address {{model.httpAddr}} + + Draining + {{#if model.isDraining}} + true + {{else}} + false + {{/if}} + + + Eligibility + {{#if model.isEligible}} + {{model.schedulingEligibility}} + {{else}} + {{model.schedulingEligibility}} + {{/if}} + Datacenter {{model.datacenter}} From c949b4acd3994a5541cd53e41316e72b3573ed45 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 29 May 2018 10:36:09 -0700 Subject: [PATCH 05/10] Color-code node drain and eligibility in the full client list This makes it easier to scan for interesting nodes --- ui/app/templates/components/client-node-row.hbs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ui/app/templates/components/client-node-row.hbs b/ui/app/templates/components/client-node-row.hbs index 9a28d6bc921..269e522a39c 100644 --- a/ui/app/templates/components/client-node-row.hbs +++ b/ui/app/templates/components/client-node-row.hbs @@ -8,8 +8,20 @@ {{#link-to "clients.client" node.id class="is-primary"}}{{node.shortId}}{{/link-to}} {{node.name}} {{node.status}} -{{if node.isDraining "true" "false"}} -{{node.schedulingEligibility}} + + {{#if node.isDraining}} + true + {{else}} + false + {{/if}} + + + {{#if node.isEligible}} + {{node.schedulingEligibility}} + {{else}} + {{node.schedulingEligibility}} + {{/if}} + {{node.httpAddr}} {{node.datacenter}} From 4d00f2f46d374ba1104d3de3296308d7527dfa60 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 30 May 2018 01:03:32 -0700 Subject: [PATCH 06/10] Duration formatting utility The cloest Moment.js has is "homanize" which isn't precise enough. --- ui/app/helpers/format-duration.js | 8 ++++ ui/app/utils/format-duration.js | 47 +++++++++++++++++++++ ui/tests/unit/utils/format-duration-test.js | 23 ++++++++++ 3 files changed, 78 insertions(+) create mode 100644 ui/app/helpers/format-duration.js create mode 100644 ui/app/utils/format-duration.js create mode 100644 ui/tests/unit/utils/format-duration-test.js diff --git a/ui/app/helpers/format-duration.js b/ui/app/helpers/format-duration.js new file mode 100644 index 00000000000..c85a14b41e9 --- /dev/null +++ b/ui/app/helpers/format-duration.js @@ -0,0 +1,8 @@ +import Helper from '@ember/component/helper'; +import formatDuration from '../utils/format-duration'; + +function formatDurationHelper([duration], { units }) { + return formatDuration(duration, units); +} + +export default Helper.helper(formatDurationHelper); diff --git a/ui/app/utils/format-duration.js b/ui/app/utils/format-duration.js new file mode 100644 index 00000000000..2ed5f238da0 --- /dev/null +++ b/ui/app/utils/format-duration.js @@ -0,0 +1,47 @@ +import moment from 'moment'; + +const allUnits = [ + { name: 'years', suffix: 'year', inMoment: true, pluralizable: true }, + { name: 'months', suffix: 'month', inMoment: true, pluralizable: true }, + { name: 'days', suffix: 'day', inMoment: true, pluralizable: true }, + { name: 'hours', suffix: 'h', inMoment: true, pluralizable: false }, + { name: 'minutes', suffix: 'm', inMoment: true, pluralizable: false }, + { name: 'seconds', suffix: 's', inMoment: true, pluralizable: false }, + { name: 'milliseconds', suffix: 'ms', inMoment: true, pluralizable: false }, + { name: 'microseconds', suffix: 'µs', inMoment: false, pluralizable: false }, + { name: 'nanoseconds', suffix: 'ns', inMoment: false, pluralizable: false }, +]; + +export default function formatDuration(duration = 0, units = 'ns') { + const durationParts = {}; + + if (units === 'ns') { + durationParts.nanoseconds = duration % 1000; + durationParts.microseconds = Math.floor((duration % 1000000) / 1000); + duration = Math.floor(duration / 1000000); + } else if (units === 'mms') { + durationParts.microseconds = duration % 1000; + duration = Math.floor(duration / 1000); + } + + const momentDuration = moment.duration(duration, ['ns', 'mms'].includes(units) ? 'ms' : units); + + allUnits + .filterBy('inMoment') + .mapBy('name') + .forEach(unit => { + durationParts[unit] = momentDuration[unit](); + }); + + const displayParts = allUnits.reduce((parts, unitType) => { + if (durationParts[unitType.name]) { + const count = durationParts[unitType.name]; + const suffix = + count === 1 || !unitType.pluralizable ? unitType.suffix : unitType.suffix.pluralize(); + parts.push(`${count}${unitType.pluralizable ? ' ' : ''}${suffix}`); + } + return parts; + }, []); + + return displayParts.join(' '); +} diff --git a/ui/tests/unit/utils/format-duration-test.js b/ui/tests/unit/utils/format-duration-test.js new file mode 100644 index 00000000000..1e38c222a34 --- /dev/null +++ b/ui/tests/unit/utils/format-duration-test.js @@ -0,0 +1,23 @@ +import { module, test } from 'ember-qunit'; +import formatDuration from 'nomad-ui/utils/format-duration'; + +module('Unit | Util | formatDuration'); + +test('When all units have values, all units are displayed', function(assert) { + const expectation = '39 years 1 month 13 days 23h 31m 30s 987ms 654µs 400ns'; + assert.equal(formatDuration(1234567890987654321), expectation, expectation); +}); + +test('Any unit without values gets dropped from the display', function(assert) { + const expectation = '14 days 6h 56m 890ms 980µs'; + assert.equal(formatDuration(1234560890980000), expectation, expectation); +}); + +test('The units option allows for units coarser than nanoseconds', function(assert) { + const expectation1 = '1s 200ms'; + const expectation2 = '20m'; + const expectation3 = '1 month 1 day'; + assert.equal(formatDuration(1200, 'ms'), expectation1, expectation1); + assert.equal(formatDuration(1200, 's'), expectation2, expectation2); + assert.equal(formatDuration(32, 'd'), expectation3, expectation3); +}); From d649f6245ff9989593a2cc872bb73788c1714235 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 30 May 2018 01:04:51 -0700 Subject: [PATCH 07/10] Display node drain strategy information on the client detail page --- ui/app/models/drain-strategy.js | 12 ++++++++++++ ui/app/models/node.js | 1 + ui/app/serializers/drain-strategy.js | 13 +++++++++++++ ui/app/templates/clients/client.hbs | 29 ++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+) create mode 100644 ui/app/models/drain-strategy.js create mode 100644 ui/app/serializers/drain-strategy.js diff --git a/ui/app/models/drain-strategy.js b/ui/app/models/drain-strategy.js new file mode 100644 index 00000000000..4d273896187 --- /dev/null +++ b/ui/app/models/drain-strategy.js @@ -0,0 +1,12 @@ +import { lt, equal } from '@ember/object/computed'; +import attr from 'ember-data/attr'; +import Fragment from 'ember-data-model-fragments/fragment'; + +export default Fragment.extend({ + deadline: attr('number'), + forceDeadline: attr('date'), + ignoreSystemJobs: attr('boolean'), + + isForced: lt('deadline', 0), + hasNoDeadline: equal('deadline', 0), +}); diff --git a/ui/app/models/node.js b/ui/app/models/node.js index ead56d7cd47..93ac0038f52 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -25,6 +25,7 @@ export default Model.extend({ meta: fragment('node-attributes'), resources: fragment('resources'), reserved: fragment('resources'), + drainStrategy: fragment('drain-strategy'), isEligible: equal('schedulingEligibility', 'eligible'), diff --git a/ui/app/serializers/drain-strategy.js b/ui/app/serializers/drain-strategy.js new file mode 100644 index 00000000000..033cc45a658 --- /dev/null +++ b/ui/app/serializers/drain-strategy.js @@ -0,0 +1,13 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + normalize(typeHash, hash) { + // TODO API: finishedAt is always marshaled as a date even when unset. + // To simplify things, unset it here when it's the empty date value. + if (hash.ForceDeadline === '0001-01-01T00:00:00Z') { + hash.ForceDeadline = null; + } + + return this._super(typeHash, hash); + }, +}); diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 7f0ab1d1cfd..b2d7d3717bb 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -57,6 +57,35 @@ + {{#if model.drainStrategy}} +
+
+ Drain Strategy + + Deadline + {{#if model.drainStrategy.isForced}} + Forced Drain + {{else if model.drainStrategy.hasNoDeadline}} + No deadline + {{else}} + {{format-duration model.drainStrategy.deadline}} + {{/if}} + + {{#if model.drainStrategy.forceDeadline}} + + Forced Deadline + {{moment-format model.drainStrategy.forceDeadline "MM/DD/YY HH:mm:ss"}} + ({{moment-from-now model.drainStrategy.forceDeadline interval=1000}}) + + {{/if}} + + Ignore System Jobs? + {{if model.drainStrategy.ignoreSystemJobs "Yes" "No"}} + +
+
+ {{/if}} +
Allocations {{model.allocations.length}}
From 03aee6f270de1d0aa6cd3a07aa8efa615804e33b Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 30 May 2018 11:20:59 -0700 Subject: [PATCH 08/10] Acceptance testing for node drain information --- ui/app/templates/clients/client.hbs | 6 +- ui/mirage/factories/node.js | 16 ++- ui/tests/acceptance/client-detail-test.js | 151 +++++++++++++++++++++- ui/tests/acceptance/nodes-list-test.js | 28 ++-- 4 files changed, 182 insertions(+), 19 deletions(-) diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index b2d7d3717bb..af3d918fa7a 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -61,7 +61,7 @@
Drain Strategy - + Deadline {{#if model.drainStrategy.isForced}} Forced Drain @@ -72,13 +72,13 @@ {{/if}} {{#if model.drainStrategy.forceDeadline}} - + Forced Deadline {{moment-format model.drainStrategy.forceDeadline "MM/DD/YY HH:mm:ss"}} ({{moment-from-now model.drainStrategy.forceDeadline interval=1000}}) {{/if}} - + Ignore System Jobs? {{if model.drainStrategy.ignoreSystemJobs "Yes" "No"}} diff --git a/ui/mirage/factories/node.js b/ui/mirage/factories/node.js index 3950f36928f..5df22c58ed4 100644 --- a/ui/mirage/factories/node.js +++ b/ui/mirage/factories/node.js @@ -1,6 +1,7 @@ import { Factory, faker, trait } from 'ember-cli-mirage'; import { provide } from '../utils'; import { DATACENTERS, HOSTS } from '../common'; +import moment from 'moment'; const UUIDS = provide(100, faker.random.uuid.bind(faker.random)); const NODE_STATUSES = ['initializing', 'ready', 'down']; @@ -11,9 +12,10 @@ export default Factory.extend({ name: i => `nomad@${HOSTS[i % HOSTS.length]}`, datacenter: faker.list.random(...DATACENTERS), - isDraining: faker.random.boolean, + drain: faker.random.boolean, status: faker.list.random(...NODE_STATUSES), tls_enabled: faker.random.boolean, + schedulingEligibility: () => (faker.random.boolean() ? 'eligible' : 'ineligible'), createIndex: i => i, modifyIndex: () => faker.random.number({ min: 10, max: 2000 }), @@ -29,6 +31,18 @@ export default Factory.extend({ }, }), + draining: trait({ + drain: true, + schedulingEligibility: 'ineligible', + drainStrategy: { + Deadline: faker.random.number({ min: 30 * 1000, max: 5 * 60 * 60 * 1000 }) * 1000000, + ForceDeadline: moment(REF_DATE).add(faker.random.number({ min: 1, max: 5 }), 'd'), + IgnoreSystemJobs: faker.random.boolean(), + }, + }), + + drainStrategy: null, + drivers: makeDrivers, attributes() { diff --git a/ui/tests/acceptance/client-detail-test.js b/ui/tests/acceptance/client-detail-test.js index 357779a56ab..f1860c61033 100644 --- a/ui/tests/acceptance/client-detail-test.js +++ b/ui/tests/acceptance/client-detail-test.js @@ -4,13 +4,14 @@ import { click, find, findAll, currentURL, visit } from 'ember-native-dom-helper import { test } from 'qunit'; import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; import { formatBytes } from 'nomad-ui/helpers/format-bytes'; +import formatDuration from 'nomad-ui/utils/format-duration'; import moment from 'moment'; let node; moduleForAcceptance('Acceptance | client detail', { beforeEach() { - server.create('node', 'forceIPv4'); + server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible' }); node = server.db.nodes[0]; // Related models @@ -75,6 +76,18 @@ test('/clients/:id should list additional detail for the node below the title', .includes(node.httpAddr), 'Address is in additional details' ); + assert.ok( + find('[data-test-draining]') + .textContent.trim() + .includes(node.drain + ''), + 'Drain status is in additional details' + ); + assert.ok( + find('[data-test-eligibility]') + .textContent.trim() + .includes(node.schedulingEligibility), + 'Scheduling eligibility is in additional details' + ); assert.ok( find('[data-test-datacenter-definition]') .textContent.trim() @@ -505,3 +518,139 @@ test('each driver can be opened to see a message and attributes', function(asser ); }); }); + +test('the status light indicates when the node is ineligible for scheduling', function(assert) { + node = server.create('node', { + schedulingEligibility: 'ineligible', + }); + + visit(`/clients/${node.id}`); + + andThen(() => { + assert.ok( + find('[data-test-node-status="ineligible"]'), + 'Title status light is in the ineligible state' + ); + }); +}); + +test('when the node has a drain strategy with a positive deadline, the drain stategy section prints the duration', function(assert) { + const deadline = 5400000000000; // 1.5 hours in nanoseconds + const forceDeadline = moment().add(1, 'd'); + + node = server.create('node', { + drain: true, + schedulingEligibility: 'ineligible', + drainStrategy: { + Deadline: deadline, + ForceDeadline: forceDeadline.toISOString(), + IgnoreSystemJobs: false, + }, + }); + + visit(`/clients/${node.id}`); + + andThen(() => { + assert.ok( + find('[data-test-drain-deadline]') + .textContent.trim() + .includes(formatDuration(deadline)), + 'Deadline is shown in a human formatted way' + ); + + assert.ok( + find('[data-test-drain-forced-deadline]') + .textContent.trim() + .includes(forceDeadline.format('MM/DD/YY HH:mm:ss')), + 'Force deadline is shown as an absolute date' + ); + + assert.ok( + find('[data-test-drain-forced-deadline]') + .textContent.trim() + .includes(forceDeadline.fromNow()), + 'Force deadline is shown as a relative date' + ); + + assert.ok( + find('[data-test-drain-ignore-system-jobs]') + .textContent.trim() + .endsWith('No'), + 'Ignore System Jobs state is shown' + ); + }); +}); + +test('when the node has a drain stategy with no deadline, the drain stategy section mentions that and omits the force deadline', function(assert) { + const deadline = 0; + + node = server.create('node', { + drain: true, + schedulingEligibility: 'ineligible', + drainStrategy: { + Deadline: deadline, + ForceDeadline: '0001-01-01T00:00:00Z', // null as a date + IgnoreSystemJobs: true, + }, + }); + + visit(`/clients/${node.id}`); + + andThen(() => { + assert.ok( + find('[data-test-drain-deadline]') + .textContent.trim() + .includes('No deadline'), + 'The value for Deadline is "no deadline"' + ); + + assert.notOk( + find('[data-test-drain-forced-deadline]'), + 'Forced deadline is not shown since there is no forced deadline' + ); + + assert.ok( + find('[data-test-drain-ignore-system-jobs]') + .textContent.trim() + .endsWith('Yes'), + 'Ignore System Jobs state is shown' + ); + }); +}); + +test('when the node has a drain stategy with a negative deadline, the drain strategy section shows the force badge', function(assert) { + const deadline = -1; + + node = server.create('node', { + drain: true, + schedulingEligibility: 'ineligible', + drainStrategy: { + Deadline: deadline, + ForceDeadline: '0001-01-01T00:00:00Z', // null as a date + IgnoreSystemJobs: false, + }, + }); + + visit(`/clients/${node.id}`); + + andThen(() => { + assert.ok( + find('[data-test-drain-deadline] .badge.is-danger') + .textContent.trim() + .includes('Forced Drain'), + 'Forced Drain is shown in a red badge' + ); + + assert.notOk( + find('[data-test-drain-forced-deadline]'), + 'Forced deadline is not shown since there is no forced deadline' + ); + + assert.ok( + find('[data-test-drain-ignore-system-jobs]') + .textContent.trim() + .endsWith('No'), + 'Ignore System Jobs state is shown' + ); + }); +}); diff --git a/ui/tests/acceptance/nodes-list-test.js b/ui/tests/acceptance/nodes-list-test.js index dde20b744ae..837e8cbe6ce 100644 --- a/ui/tests/acceptance/nodes-list-test.js +++ b/ui/tests/acceptance/nodes-list-test.js @@ -2,7 +2,6 @@ import { click, find, findAll, currentURL, visit } from 'ember-native-dom-helper import { test } from 'qunit'; import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; import { findLeader } from '../../mirage/config'; -import ipParts from 'nomad-ui/utils/ip-parts'; function minimumSetup() { server.createList('node', 1); @@ -47,7 +46,6 @@ test('each client record should show high-level info of the client', function(as andThen(() => { const nodeRow = find('[data-test-client-node-row]'); const allocations = server.db.allocations.where({ nodeId: node.id }); - const { address, port } = ipParts(node.httpAddr); assert.equal( nodeRow.querySelector('[data-test-client-id]').textContent.trim(), @@ -64,12 +62,20 @@ test('each client record should show high-level info of the client', function(as node.status, 'Status' ); + assert.equal( + nodeRow.querySelector('[data-test-client-drain]').textContent.trim(), + node.drain + '', + 'Draining' + ); + assert.equal( + nodeRow.querySelector('[data-test-client-eligibility]').textContent.trim(), + node.schedulingEligibility, + 'Eligibility' + ); assert.equal( nodeRow.querySelector('[data-test-client-address]').textContent.trim(), - address, - 'Address' + node.httpAddr ); - assert.equal(nodeRow.querySelector('[data-test-client-port]').textContent.trim(), port, 'Port'); assert.equal( nodeRow.querySelector('[data-test-client-datacenter]').textContent.trim(), node.datacenter, @@ -108,9 +114,7 @@ test('when there are no clients, there is an empty message', function(assert) { }); }); -test('when there are clients, but no matches for a search term, there is an empty message', function( - assert -) { +test('when there are clients, but no matches for a search term, there is an empty message', function(assert) { server.createList('agent', 1); server.create('node', { name: 'node' }); @@ -126,9 +130,7 @@ test('when there are clients, but no matches for a search term, there is an empt }); }); -test('when accessing clients is forbidden, show a message with a link to the tokens page', function( - assert -) { +test('when accessing clients is forbidden, show a message with a link to the tokens page', function(assert) { server.create('agent'); server.create('node', { name: 'node' }); server.pretender.get('/v1/nodes', () => [403, {}, null]); @@ -236,9 +238,7 @@ test('each server should link to the server detail page', function(assert) { }); }); -test('when accessing servers is forbidden, show a message with a link to the tokens page', function( - assert -) { +test('when accessing servers is forbidden, show a message with a link to the tokens page', function(assert) { server.create('agent'); server.pretender.get('/v1/agent/members', () => [403, {}, null]); From 9aabe13b468c83e352ac0407bf710472b513a684 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 30 May 2018 11:26:48 -0700 Subject: [PATCH 09/10] New traits for node states --- ui/mirage/factories/node.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ui/mirage/factories/node.js b/ui/mirage/factories/node.js index 5df22c58ed4..13c42736cf7 100644 --- a/ui/mirage/factories/node.js +++ b/ui/mirage/factories/node.js @@ -41,6 +41,26 @@ export default Factory.extend({ }, }), + forcedDraining: trait({ + drain: true, + schedulingEligibility: 'ineligible', + drainStrategy: { + Deadline: -1, + ForceDeadline: '0001-01-01T00:00:00Z', + IgnoreSystemJobs: faker.random.boolean(), + }, + }), + + noDeadlineDraining: trait({ + drain: true, + schedulingEligibility: 'ineligible', + drainStrategy: { + Deadline: 0, + ForceDeadline: '0001-01-01T00:00:00Z', + IgnoreSystemJobs: faker.random.boolean(), + }, + }), + drainStrategy: null, drivers: makeDrivers, From 83e0b10451f86cc443ea4fee0492b2c6f76dbd4a Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 30 May 2018 14:30:45 -0700 Subject: [PATCH 10/10] Account for the 0 case in format-duration --- ui/app/utils/format-duration.js | 21 +++++++++++++++++++-- ui/tests/unit/utils/format-duration-test.js | 5 +++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/ui/app/utils/format-duration.js b/ui/app/utils/format-duration.js index 2ed5f238da0..2f8ea1b72b2 100644 --- a/ui/app/utils/format-duration.js +++ b/ui/app/utils/format-duration.js @@ -15,6 +15,9 @@ const allUnits = [ export default function formatDuration(duration = 0, units = 'ns') { const durationParts = {}; + // Moment only handles up to millisecond precision. + // Microseconds and nanoseconds need to be handled first, + // then Moment can take over for all larger units. if (units === 'ns') { durationParts.nanoseconds = duration % 1000; durationParts.microseconds = Math.floor((duration % 1000000) / 1000); @@ -24,8 +27,13 @@ export default function formatDuration(duration = 0, units = 'ns') { duration = Math.floor(duration / 1000); } - const momentDuration = moment.duration(duration, ['ns', 'mms'].includes(units) ? 'ms' : units); + let momentUnits = units; + if (units === 'ns' || units === 'mms') { + momentUnits = 'ms'; + } + const momentDuration = moment.duration(duration, momentUnits); + // Get the count of each time unit that Moment handles allUnits .filterBy('inMoment') .mapBy('name') @@ -33,6 +41,8 @@ export default function formatDuration(duration = 0, units = 'ns') { durationParts[unit] = momentDuration[unit](); }); + // Format each time time bucket as a string + // e.g., { years: 5, seconds: 30 } -> [ '5 years', '30s' ] const displayParts = allUnits.reduce((parts, unitType) => { if (durationParts[unitType.name]) { const count = durationParts[unitType.name]; @@ -43,5 +53,12 @@ export default function formatDuration(duration = 0, units = 'ns') { return parts; }, []); - return displayParts.join(' '); + if (displayParts.length) { + return displayParts.join(' '); + } + + // When the duration is 0, show 0 in terms of `units` + const unitTypeForUnits = allUnits.findBy('suffix', units); + const suffix = unitTypeForUnits.pluralizable ? units.pluralize() : units; + return `0${unitTypeForUnits.pluralizable ? ' ' : ''}${suffix}`; } diff --git a/ui/tests/unit/utils/format-duration-test.js b/ui/tests/unit/utils/format-duration-test.js index 1e38c222a34..c4867590f32 100644 --- a/ui/tests/unit/utils/format-duration-test.js +++ b/ui/tests/unit/utils/format-duration-test.js @@ -21,3 +21,8 @@ test('The units option allows for units coarser than nanoseconds', function(asse assert.equal(formatDuration(1200, 's'), expectation2, expectation2); assert.equal(formatDuration(32, 'd'), expectation3, expectation3); }); + +test('When duration is 0, 0 is shown in terms of the units provided to the function', function(assert) { + assert.equal(formatDuration(0), '0ns', 'formatDuration(0) -> 0ns'); + assert.equal(formatDuration(0, 'year'), '0 years', 'formatDuration(0, "year") -> 0 years'); +});