Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Node drain and eligibility #4353

Merged
merged 10 commits into from
May 30, 2018
8 changes: 8 additions & 0 deletions ui/app/helpers/format-duration.js
Original file line number Diff line number Diff line change
@@ -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);
12 changes: 12 additions & 0 deletions ui/app/models/drain-strategy.js
Original file line number Diff line number Diff line change
@@ -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),
});
11 changes: 11 additions & 0 deletions ui/app/models/node.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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'),
Expand All @@ -23,6 +25,9 @@ export default Model.extend({
meta: fragment('node-attributes'),
resources: fragment('resources'),
reserved: fragment('resources'),
drainStrategy: fragment('drain-strategy'),

isEligible: equal('schedulingEligibility', 'eligible'),

address: computed('httpAddr', function() {
return ipParts(this.get('httpAddr')).address;
Expand Down Expand Up @@ -52,4 +57,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';
}),
});
13 changes: 13 additions & 0 deletions ui/app/serializers/drain-strategy.js
Original file line number Diff line number Diff line change
@@ -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);
},
});
1 change: 1 addition & 0 deletions ui/app/serializers/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default ApplicationSerializer.extend({
config: service(),

attrs: {
isDraining: 'Drain',
httpAddr: 'HTTPAddr',
},

Expand Down
4 changes: 4 additions & 0 deletions ui/app/styles/components/node-status-light.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ $size: 0.75em;
darken($grey-lighter, 25%) 6px
);
}

&.ineligible {
background: $warning;
}
}
11 changes: 11 additions & 0 deletions ui/app/styles/components/status-text.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.status-text {
font-weight: $weight-semibold;

&.node-ready {
color: $nomad-green-dark;
}
Expand All @@ -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;
}
}
}
47 changes: 46 additions & 1 deletion ui/app/templates/clients/client.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{{#gutter-menu class="page-body"}}
<section class="section">
<h1 data-test-title class="title">
<span data-test-node-status="{{model.status}}" class="node-status-light {{model.status}}"></span>
<span data-test-node-status="{{model.compositeStatus}}" class="node-status-light {{model.compositeStatus}}"></span>
{{or model.name model.shortId}}
<span class="tag is-hollow is-small no-text-transform">{{model.id}}</span>
</h1>
Expand All @@ -25,6 +25,22 @@
<span class="term">Address</span>
{{model.httpAddr}}
</span>
<span class="pair" data-test-draining>
<span class="term">Draining</span>
{{#if model.isDraining}}
<span class="status-text is-info">true</span>
{{else}}
false
{{/if}}
</span>
<span class="pair" data-test-eligibility>
<span class="term">Eligibility</span>
{{#if model.isEligible}}
{{model.schedulingEligibility}}
{{else}}
<span class="status-text is-warning">{{model.schedulingEligibility}}</span>
{{/if}}
</span>
<span class="pair" data-test-datacenter-definition>
<span class="term">Datacenter</span>
{{model.datacenter}}
Expand All @@ -41,6 +57,35 @@
</div>
</div>

{{#if model.drainStrategy}}
<div class="boxed-section is-small is-info">
<div class="boxed-section-body inline-definitions">
<span class="label">Drain Strategy</span>
<span class="pair" data-test-drain-deadline>
<span class="term">Deadline</span>
{{#if model.drainStrategy.isForced}}
<span class="badge is-danger">Forced Drain</span>
{{else if model.drainStrategy.hasNoDeadline}}
No deadline
{{else}}
{{format-duration model.drainStrategy.deadline}}
{{/if}}
</span>
{{#if model.drainStrategy.forceDeadline}}
<span class="pair" data-test-drain-forced-deadline>
<span class="term">Forced Deadline</span>
{{moment-format model.drainStrategy.forceDeadline "MM/DD/YY HH:mm:ss"}}
({{moment-from-now model.drainStrategy.forceDeadline interval=1000}})
</span>
{{/if}}
<span class="pair" data-test-drain-ignore-system-jobs>
<span class="term">Ignore System Jobs?</span>
{{if model.drainStrategy.ignoreSystemJobs "Yes" "No"}}
</span>
</div>
</div>
{{/if}}

<div class="boxed-section">
<div class="boxed-section-head">
<div>Allocations <span class="badge is-white">{{model.allocations.length}}</span></div>
Expand Down
3 changes: 2 additions & 1 deletion ui/app/templates/clients/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
<th>Address</th>
<th>Port</th>
{{#t.sort-by prop="datacenter"}}Datacenter{{/t.sort-by}}
<th># Allocs</th>
{{/t.head}}
Expand Down
17 changes: 15 additions & 2 deletions ui/app/templates/components/client-node-row.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,21 @@
<td data-test-client-id>{{#link-to "clients.client" node.id class="is-primary"}}{{node.shortId}}{{/link-to}}</td>
<td data-test-client-name class="is-200px is-truncatable" title="{{node.name}}">{{node.name}}</td>
<td data-test-client-status>{{node.status}}</td>
<td data-test-client-address>{{node.address}}</td>
<td data-test-client-port>{{node.port}}</td>
<td data-test-client-drain>
{{#if node.isDraining}}
<span class="status-text is-info">true</span>
{{else}}
false
{{/if}}
</td>
<td data-test-client-eligibility>
{{#if node.isEligible}}
{{node.schedulingEligibility}}
{{else}}
<span class="status-text is-warning">{{node.schedulingEligibility}}</span>
{{/if}}
</td>
<td data-test-client-address>{{node.httpAddr}}</td>
<td data-test-client-datacenter>{{node.datacenter}}</td>
<td data-test-client-allocations>
{{#if node.allocations.isPending}}
Expand Down
64 changes: 64 additions & 0 deletions ui/app/utils/format-duration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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 = {};

// 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);
duration = Math.floor(duration / 1000000);
} else if (units === 'mms') {
durationParts.microseconds = duration % 1000;
duration = Math.floor(duration / 1000);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this part a little tricky to follow. My reading is that the unit of duration can be one of nanoseconds, microseconds, or milliseconds, and for the former two cases we're normalizing it back to milliseconds here.

If that's correct, it might also be clearer to mutate the units argument in these blocks also, rather than in the dense ternary on the following line. And perhaps some commentary explaining this might also help?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Units can be any unit moment supports (ms, s, m, h, days, months, years) as well as microseconds and nanoseconds, which moment doesn't support.

So this ternary:

  const momentDuration = moment.duration(duration, ['ns', 'mms'].includes(units) ? 'ms' : units);

Uses units as is unless units is either microseconds or nanoseconds, in which case nanos and micros need to be calculated upfront and duration needs to be normalized as milliseconds.

But your suggestion to reassign units to ms is still valid. Also point taken about needing comments.


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')
.forEach(unit => {
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];
const suffix =
count === 1 || !unitType.pluralizable ? unitType.suffix : unitType.suffix.pluralize();
parts.push(`${count}${unitType.pluralizable ? ' ' : ''}${suffix}`);
}
return parts;
}, []);

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}`;
}
36 changes: 35 additions & 1 deletion ui/mirage/factories/node.js
Original file line number Diff line number Diff line change
@@ -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'];
Expand All @@ -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 }),
Expand All @@ -29,6 +31,38 @@ 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(),
},
}),

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,

attributes() {
Expand Down
Loading