Skip to content

Commit

Permalink
Merge pull request #4353 from hashicorp/f-ui-node-drain
Browse files Browse the repository at this point in the history
UI: Node drain and eligibility
  • Loading branch information
DingoEatingFuzz committed May 30, 2018
2 parents 265dee7 + 83e0b10 commit 400ca29
Show file tree
Hide file tree
Showing 15 changed files with 414 additions and 20 deletions.
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);
}

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

0 comments on commit 400ca29

Please sign in to comment.