Merge pull request #6819 from hashicorp/f-ui-node-drain
UI: Invoke Node Drains
This commit is contained in:
commit
0c18d92395
|
@ -1,3 +1,59 @@
|
||||||
import Watchable from './watchable';
|
import Watchable from './watchable';
|
||||||
|
import addToPath from 'nomad-ui/utils/add-to-path';
|
||||||
|
|
||||||
export default Watchable.extend();
|
export default Watchable.extend({
|
||||||
|
setEligible(node) {
|
||||||
|
return this.setEligibility(node, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
setIneligible(node) {
|
||||||
|
return this.setEligibility(node, false);
|
||||||
|
},
|
||||||
|
|
||||||
|
setEligibility(node, isEligible) {
|
||||||
|
const url = addToPath(this.urlForFindRecord(node.id, 'node'), '/eligibility');
|
||||||
|
return this.ajax(url, 'POST', {
|
||||||
|
data: {
|
||||||
|
NodeID: node.id,
|
||||||
|
Eligibility: isEligible ? 'eligible' : 'ineligible',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Force: -1s deadline
|
||||||
|
// No Deadline: 0 deadline
|
||||||
|
drain(node, drainSpec) {
|
||||||
|
const url = addToPath(this.urlForFindRecord(node.id, 'node'), '/drain');
|
||||||
|
return this.ajax(url, 'POST', {
|
||||||
|
data: {
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: Object.assign(
|
||||||
|
{
|
||||||
|
Deadline: 0,
|
||||||
|
IgnoreSystemJobs: true,
|
||||||
|
},
|
||||||
|
drainSpec
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
forceDrain(node, drainSpec) {
|
||||||
|
return this.drain(
|
||||||
|
node,
|
||||||
|
Object.assign({}, drainSpec, {
|
||||||
|
Deadline: -1,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelDrain(node) {
|
||||||
|
const url = addToPath(this.urlForFindRecord(node.id, 'node'), '/drain');
|
||||||
|
return this.ajax(url, 'POST', {
|
||||||
|
data: {
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
import { computed } from '@ember/object';
|
||||||
|
import { equal } from '@ember/object/computed';
|
||||||
|
import { computed as overridable } from 'ember-overridable-computed';
|
||||||
|
import { task } from 'ember-concurrency';
|
||||||
|
import Duration from 'duration-js';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
tagName: '',
|
||||||
|
|
||||||
|
client: null,
|
||||||
|
|
||||||
|
onError() {},
|
||||||
|
onDrain() {},
|
||||||
|
|
||||||
|
parseError: '',
|
||||||
|
|
||||||
|
deadlineEnabled: false,
|
||||||
|
forceDrain: false,
|
||||||
|
drainSystemJobs: true,
|
||||||
|
|
||||||
|
selectedDurationQuickOption: overridable(function() {
|
||||||
|
return this.durationQuickOptions[0];
|
||||||
|
}),
|
||||||
|
|
||||||
|
durationIsCustom: equal('selectedDurationQuickOption.value', 'custom'),
|
||||||
|
customDuration: '',
|
||||||
|
|
||||||
|
durationQuickOptions: computed(() => [
|
||||||
|
{ label: '1 Hour', value: '1h' },
|
||||||
|
{ label: '4 Hours', value: '4h' },
|
||||||
|
{ label: '8 Hours', value: '8h' },
|
||||||
|
{ label: '12 Hours', value: '12h' },
|
||||||
|
{ label: '1 Day', value: '1d' },
|
||||||
|
{ label: 'Custom', value: 'custom' },
|
||||||
|
]),
|
||||||
|
|
||||||
|
deadline: computed(
|
||||||
|
'deadlineEnabled',
|
||||||
|
'durationIsCustom',
|
||||||
|
'customDuration',
|
||||||
|
'selectedDurationQuickOption.value',
|
||||||
|
function() {
|
||||||
|
if (!this.deadlineEnabled) return 0;
|
||||||
|
if (this.durationIsCustom) return this.customDuration;
|
||||||
|
return this.selectedDurationQuickOption.value;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
drain: task(function*(close) {
|
||||||
|
if (!this.client) return;
|
||||||
|
const isUpdating = this.client.isDraining;
|
||||||
|
|
||||||
|
let deadline;
|
||||||
|
try {
|
||||||
|
deadline = new Duration(this.deadline).nanoseconds();
|
||||||
|
} catch (err) {
|
||||||
|
this.set('parseError', err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spec = {
|
||||||
|
Deadline: deadline,
|
||||||
|
IgnoreSystemJobs: !this.drainSystemJobs,
|
||||||
|
};
|
||||||
|
|
||||||
|
close();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.forceDrain) {
|
||||||
|
yield this.client.forceDrain(spec);
|
||||||
|
} else {
|
||||||
|
yield this.client.drain(spec);
|
||||||
|
}
|
||||||
|
this.onDrain(isUpdating);
|
||||||
|
} catch (err) {
|
||||||
|
this.onError(err);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
preventDefault(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,56 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
import { run } from '@ember/runloop';
|
||||||
|
|
||||||
|
const TAB = 9;
|
||||||
|
const ARROW_DOWN = 40;
|
||||||
|
const FOCUSABLE = [
|
||||||
|
'a:not([disabled])',
|
||||||
|
'button:not([disabled])',
|
||||||
|
'input:not([disabled]):not([type="hidden"])',
|
||||||
|
'textarea:not([disabled])',
|
||||||
|
'[tabindex]:not([disabled]):not([tabindex="-1"])',
|
||||||
|
].join(', ');
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
classnames: ['popover'],
|
||||||
|
|
||||||
|
triggerClass: '',
|
||||||
|
isOpen: false,
|
||||||
|
label: '',
|
||||||
|
|
||||||
|
dropdown: null,
|
||||||
|
|
||||||
|
capture(dropdown) {
|
||||||
|
// It's not a good idea to grab a dropdown reference like this, but it's necessary
|
||||||
|
// in order to invoke dropdown.actions.close in traverseList as well as
|
||||||
|
// dropdown.actions.reposition when the label or selection length changes.
|
||||||
|
this.set('dropdown', dropdown);
|
||||||
|
},
|
||||||
|
|
||||||
|
didReceiveAttrs() {
|
||||||
|
const dropdown = this.dropdown;
|
||||||
|
if (this.isOpen && dropdown) {
|
||||||
|
run.scheduleOnce('afterRender', () => {
|
||||||
|
dropdown.actions.reposition();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
openOnArrowDown(dropdown, e) {
|
||||||
|
if (!this.isOpen && e.keyCode === ARROW_DOWN) {
|
||||||
|
dropdown.actions.open(e);
|
||||||
|
e.preventDefault();
|
||||||
|
} else if (this.isOpen && (e.keyCode === TAB || e.keyCode === ARROW_DOWN)) {
|
||||||
|
const optionsId = this.element.querySelector('.popover-trigger').getAttribute('aria-owns');
|
||||||
|
const popoverContentEl = document.querySelector(`#${optionsId}`);
|
||||||
|
const firstFocusableElement = popoverContentEl.querySelector(FOCUSABLE);
|
||||||
|
|
||||||
|
if (firstFocusableElement) {
|
||||||
|
firstFocusableElement.focus();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,13 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
tagName: 'label',
|
||||||
|
classNames: ['toggle'],
|
||||||
|
classNameBindings: ['isDisabled:is-disabled', 'isActive:is-active'],
|
||||||
|
|
||||||
|
'data-test-label': true,
|
||||||
|
|
||||||
|
isActive: false,
|
||||||
|
isDisabled: false,
|
||||||
|
onToggle() {},
|
||||||
|
});
|
|
@ -13,6 +13,8 @@ export default Component.extend({
|
||||||
confirmationMessage: '',
|
confirmationMessage: '',
|
||||||
awaitingConfirmation: false,
|
awaitingConfirmation: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
alignRight: false,
|
||||||
|
isInfoAction: false,
|
||||||
onConfirm() {},
|
onConfirm() {},
|
||||||
onCancel() {},
|
onCancel() {},
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { alias } from '@ember/object/computed';
|
import { alias } from '@ember/object/computed';
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { computed } from '@ember/object';
|
import { computed, observer } from '@ember/object';
|
||||||
|
import { task } from 'ember-concurrency';
|
||||||
import Sortable from 'nomad-ui/mixins/sortable';
|
import Sortable from 'nomad-ui/mixins/sortable';
|
||||||
import Searchable from 'nomad-ui/mixins/searchable';
|
import Searchable from 'nomad-ui/mixins/searchable';
|
||||||
|
import messageFromAdapterError from 'nomad-ui/utils/message-from-adapter-error';
|
||||||
|
|
||||||
export default Controller.extend(Sortable, Searchable, {
|
export default Controller.extend(Sortable, Searchable, {
|
||||||
queryParams: {
|
queryParams: {
|
||||||
|
@ -13,6 +15,9 @@ export default Controller.extend(Sortable, Searchable, {
|
||||||
onlyPreemptions: 'preemptions',
|
onlyPreemptions: 'preemptions',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Set in the route
|
||||||
|
flagAsDraining: false,
|
||||||
|
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 8,
|
pageSize: 8,
|
||||||
|
|
||||||
|
@ -36,6 +41,13 @@ export default Controller.extend(Sortable, Searchable, {
|
||||||
listToSearch: alias('listSorted'),
|
listToSearch: alias('listSorted'),
|
||||||
sortedAllocations: alias('listSearched'),
|
sortedAllocations: alias('listSearched'),
|
||||||
|
|
||||||
|
eligibilityError: null,
|
||||||
|
stopDrainError: null,
|
||||||
|
drainError: null,
|
||||||
|
showDrainNotification: false,
|
||||||
|
showDrainUpdateNotification: false,
|
||||||
|
showDrainStoppedNotification: false,
|
||||||
|
|
||||||
preemptions: computed('model.allocations.@each.wasPreempted', function() {
|
preemptions: computed('model.allocations.@each.wasPreempted', function() {
|
||||||
return this.model.allocations.filterBy('wasPreempted');
|
return this.model.allocations.filterBy('wasPreempted');
|
||||||
}),
|
}),
|
||||||
|
@ -50,6 +62,46 @@ export default Controller.extend(Sortable, Searchable, {
|
||||||
return this.get('model.drivers').sortBy('name');
|
return this.get('model.drivers').sortBy('name');
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
setEligibility: task(function*(value) {
|
||||||
|
try {
|
||||||
|
yield value ? this.model.setEligible() : this.model.setIneligible();
|
||||||
|
} catch (err) {
|
||||||
|
const error = messageFromAdapterError(err) || 'Could not set eligibility';
|
||||||
|
this.set('eligibilityError', error);
|
||||||
|
}
|
||||||
|
}).drop(),
|
||||||
|
|
||||||
|
stopDrain: task(function*() {
|
||||||
|
try {
|
||||||
|
this.set('flagAsDraining', false);
|
||||||
|
yield this.model.cancelDrain();
|
||||||
|
this.set('showDrainStoppedNotification', true);
|
||||||
|
} catch (err) {
|
||||||
|
this.set('flagAsDraining', true);
|
||||||
|
const error = messageFromAdapterError(err) || 'Could not stop drain';
|
||||||
|
this.set('stopDrainError', error);
|
||||||
|
}
|
||||||
|
}).drop(),
|
||||||
|
|
||||||
|
forceDrain: task(function*() {
|
||||||
|
try {
|
||||||
|
yield this.model.forceDrain({
|
||||||
|
IgnoreSystemJobs: this.model.drainStrategy.ignoreSystemJobs,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const error = messageFromAdapterError(err) || 'Could not force drain';
|
||||||
|
this.set('drainError', error);
|
||||||
|
}
|
||||||
|
}).drop(),
|
||||||
|
|
||||||
|
triggerDrainNotification: observer('model.isDraining', function() {
|
||||||
|
if (!this.model.isDraining && this.flagAsDraining) {
|
||||||
|
this.set('showDrainNotification', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set('flagAsDraining', this.model.isDraining);
|
||||||
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
gotoAllocation(allocation) {
|
gotoAllocation(allocation) {
|
||||||
this.transitionToRoute('allocations.allocation', allocation);
|
this.transitionToRoute('allocations.allocation', allocation);
|
||||||
|
@ -58,5 +110,14 @@ export default Controller.extend(Sortable, Searchable, {
|
||||||
setPreemptionFilter(value) {
|
setPreemptionFilter(value) {
|
||||||
this.set('onlyPreemptions', value);
|
this.set('onlyPreemptions', value);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
drainNotify(isUpdating) {
|
||||||
|
this.set('showDrainUpdateNotification', isUpdating);
|
||||||
|
},
|
||||||
|
|
||||||
|
drainError(err) {
|
||||||
|
const error = messageFromAdapterError(err) || 'Could not run drain';
|
||||||
|
this.set('drainError', error);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -41,6 +41,7 @@ export default Model.extend({
|
||||||
}),
|
}),
|
||||||
|
|
||||||
isRunning: equal('clientStatus', 'running'),
|
isRunning: equal('clientStatus', 'running'),
|
||||||
|
isMigrating: attr('boolean'),
|
||||||
|
|
||||||
// When allocations are server-side rescheduled, a paper trail
|
// When allocations are server-side rescheduled, a paper trail
|
||||||
// is left linking all reschedule attempts.
|
// is left linking all reschedule attempts.
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Model from 'ember-data/model';
|
||||||
import attr from 'ember-data/attr';
|
import attr from 'ember-data/attr';
|
||||||
import { hasMany } from 'ember-data/relationships';
|
import { hasMany } from 'ember-data/relationships';
|
||||||
import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes';
|
import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes';
|
||||||
|
import RSVP from 'rsvp';
|
||||||
import shortUUIDProperty from '../utils/properties/short-uuid';
|
import shortUUIDProperty from '../utils/properties/short-uuid';
|
||||||
import ipParts from '../utils/ip-parts';
|
import ipParts from '../utils/ip-parts';
|
||||||
|
|
||||||
|
@ -43,6 +44,25 @@ export default Model.extend({
|
||||||
}),
|
}),
|
||||||
|
|
||||||
allocations: hasMany('allocations', { inverse: 'node' }),
|
allocations: hasMany('allocations', { inverse: 'node' }),
|
||||||
|
completeAllocations: computed('allocations.@each.clientStatus', function() {
|
||||||
|
return this.allocations.filterBy('clientStatus', 'complete');
|
||||||
|
}),
|
||||||
|
runningAllocations: computed('allocations.@each.isRunning', function() {
|
||||||
|
return this.allocations.filterBy('isRunning');
|
||||||
|
}),
|
||||||
|
migratingAllocations: computed('allocations.@each.{isMigrating,isRunning}', function() {
|
||||||
|
return this.allocations.filter(alloc => alloc.isRunning && alloc.isMigrating);
|
||||||
|
}),
|
||||||
|
lastMigrateTime: computed('allocations.@each.{isMigrating,isRunning,modifyTime}', function() {
|
||||||
|
const allocation = this.allocations
|
||||||
|
.filterBy('isRunning', false)
|
||||||
|
.filterBy('isMigrating')
|
||||||
|
.sortBy('modifyTime')
|
||||||
|
.reverse()[0];
|
||||||
|
if (allocation) {
|
||||||
|
return allocation.modifyTime;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
drivers: fragmentArray('node-driver'),
|
drivers: fragmentArray('node-driver'),
|
||||||
events: fragmentArray('node-event'),
|
events: fragmentArray('node-event'),
|
||||||
|
@ -70,4 +90,30 @@ export default Model.extend({
|
||||||
return this.status;
|
return this.status;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
setEligible() {
|
||||||
|
if (this.isEligible) return RSVP.resolve();
|
||||||
|
// Optimistically update schedulingEligibility for immediate feedback
|
||||||
|
this.set('schedulingEligibility', 'eligible');
|
||||||
|
return this.store.adapterFor('node').setEligible(this);
|
||||||
|
},
|
||||||
|
|
||||||
|
setIneligible() {
|
||||||
|
if (!this.isEligible) return RSVP.resolve();
|
||||||
|
// Optimistically update schedulingEligibility for immediate feedback
|
||||||
|
this.set('schedulingEligibility', 'ineligible');
|
||||||
|
return this.store.adapterFor('node').setIneligible(this);
|
||||||
|
},
|
||||||
|
|
||||||
|
drain(drainSpec) {
|
||||||
|
return this.store.adapterFor('node').drain(this, drainSpec);
|
||||||
|
},
|
||||||
|
|
||||||
|
forceDrain(drainSpec) {
|
||||||
|
return this.store.adapterFor('node').forceDrain(this, drainSpec);
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelDrain() {
|
||||||
|
return this.store.adapterFor('node').cancelDrain(this);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -29,6 +29,24 @@ export default Route.extend(WithWatchers, {
|
||||||
return model && model.get('allocations');
|
return model && model.get('allocations');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setupController(controller, model) {
|
||||||
|
controller.set('flagAsDraining', model && model.isDraining);
|
||||||
|
|
||||||
|
return this._super(...arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
resetController(controller) {
|
||||||
|
controller.setProperties({
|
||||||
|
eligibilityError: null,
|
||||||
|
stopDrainError: null,
|
||||||
|
drainError: null,
|
||||||
|
flagAsDraining: false,
|
||||||
|
showDrainNotification: false,
|
||||||
|
showDrainUpdateNotification: false,
|
||||||
|
showDrainStoppedNotification: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
startWatchers(controller, model) {
|
startWatchers(controller, model) {
|
||||||
if (model) {
|
if (model) {
|
||||||
controller.set('watchModel', this.watch.perform(model));
|
controller.set('watchModel', this.watch.perform(model));
|
||||||
|
|
|
@ -40,6 +40,8 @@ export default ApplicationSerializer.extend({
|
||||||
|
|
||||||
hash.RescheduleEvents = (hash.RescheduleTracker || {}).Events;
|
hash.RescheduleEvents = (hash.RescheduleTracker || {}).Events;
|
||||||
|
|
||||||
|
hash.IsMigrating = (hash.DesiredTransition || {}).Migrate;
|
||||||
|
|
||||||
// API returns empty strings instead of null
|
// API returns empty strings instead of null
|
||||||
hash.PreviousAllocationID = hash.PreviousAllocation ? hash.PreviousAllocation : null;
|
hash.PreviousAllocationID = hash.PreviousAllocation ? hash.PreviousAllocation : null;
|
||||||
hash.NextAllocationID = hash.NextAllocation ? hash.NextAllocation : null;
|
hash.NextAllocationID = hash.NextAllocation ? hash.NextAllocation : null;
|
||||||
|
|
|
@ -19,11 +19,13 @@
|
||||||
@import './components/node-status-light';
|
@import './components/node-status-light';
|
||||||
@import './components/nomad-logo';
|
@import './components/nomad-logo';
|
||||||
@import './components/page-layout';
|
@import './components/page-layout';
|
||||||
|
@import './components/popover-menu';
|
||||||
@import './components/primary-metric';
|
@import './components/primary-metric';
|
||||||
@import './components/search-box';
|
@import './components/search-box';
|
||||||
@import './components/simple-list';
|
@import './components/simple-list';
|
||||||
@import './components/status-text';
|
@import './components/status-text';
|
||||||
@import './components/timeline';
|
@import './components/timeline';
|
||||||
|
@import './components/toggle';
|
||||||
@import './components/toolbar';
|
@import './components/toolbar';
|
||||||
@import './components/tooltip';
|
@import './components/tooltip';
|
||||||
@import './components/two-step-button';
|
@import './components/two-step-button';
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
.ember-power-select-trigger,
|
.ember-power-select-trigger,
|
||||||
.dropdown-trigger {
|
.dropdown-trigger {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0.3em 16px 0.3em 0.3em;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.375em 16px 0.375em 0.3em;
|
||||||
|
line-height: 1;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
box-shadow: $button-box-shadow-standard;
|
box-shadow: $button-box-shadow-standard;
|
||||||
background: $white-bis;
|
background: $white-bis;
|
||||||
border: 1px solid $grey-light;
|
border: 1px solid $grey-light;
|
||||||
|
height: 2.25em;
|
||||||
|
font-size: 1rem;
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
|
|
@ -25,4 +25,8 @@
|
||||||
font-weight: $weight-semibold;
|
font-weight: $weight-semibold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-small {
|
||||||
|
font-size: $size-7;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
$size: 0.75em;
|
$size: 1.3rem;
|
||||||
|
|
||||||
.node-status-light {
|
.node-status-light {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
.popover-content {
|
||||||
|
border: 1px solid $grey-blue;
|
||||||
|
box-shadow: 0 6px 8px -2px rgba($black, 0.05), 0 8px 4px -4px rgba($black, 0.1);
|
||||||
|
margin-right: -$radius;
|
||||||
|
margin-top: -1px;
|
||||||
|
border-radius: $radius;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
width: 200px;
|
||||||
|
z-index: $z-popover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-actions {
|
||||||
|
margin: 1rem -1rem -0.5rem -1rem;
|
||||||
|
border-top: 1px solid $grey-lighter;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.popover-action {
|
||||||
|
border: none;
|
||||||
|
height: 2.75em;
|
||||||
|
width: 100%;
|
||||||
|
margin: 4px;
|
||||||
|
border-radius: $radius;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: $weight-bold;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $white-ter;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: $white-ter;
|
||||||
|
box-shadow: inset 0 0 0 2px rgba($blue, 0.5);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@each $name, $pair in $colors {
|
||||||
|
$color: nth($pair, 1);
|
||||||
|
|
||||||
|
&.is-#{$name} {
|
||||||
|
color: $color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override is-primary, which is normally green, to be blue
|
||||||
|
&.is-primary {
|
||||||
|
color: $blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
$size: 12px;
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: $weight-semibold;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
color: $grey-blue;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggler {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: $size * 2;
|
||||||
|
height: $size;
|
||||||
|
border-radius: $size;
|
||||||
|
background: $grey-blue;
|
||||||
|
transition: background 0.3s ease-in-out;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: calc(#{$size} - 4px);
|
||||||
|
height: calc(#{$size} - 4px);
|
||||||
|
border-radius: 100%;
|
||||||
|
background: $white;
|
||||||
|
left: 2px;
|
||||||
|
top: 2px;
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always style the toggler based off the input state to
|
||||||
|
// ensure that the native input is driving behavior.
|
||||||
|
.input:focus + .toggler {
|
||||||
|
box-shadow: 0 0 0 1px $grey-light;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:checked + .toggler {
|
||||||
|
background: $blue;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
transform: translateX($size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:checked:focus + .toggler {
|
||||||
|
box-shadow: 0 0 0 1px rgba($blue, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:disabled + .toggler {
|
||||||
|
background: rgba($grey-blue, 0.6);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:checked:disabled + .toggler {
|
||||||
|
background: rgba($blue, 0.5);
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,11 +15,19 @@ $spacing: 1.5em;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
&.is-minimum {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&.is-right-aligned {
|
&.is-right-aligned {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-top-aligned {
|
||||||
|
align-self: auto;
|
||||||
|
}
|
||||||
|
|
||||||
&.is-mobile-full-width {
|
&.is-mobile-full-width {
|
||||||
@include mobile {
|
@include mobile {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
|
@ -61,3 +61,8 @@
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
top: -20%;
|
top: -20%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip.multiline::after {
|
||||||
|
width: 200px;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
|
@ -14,5 +14,14 @@
|
||||||
font-weight: $weight-normal;
|
font-weight: $weight-normal;
|
||||||
color: darken($grey-blue, 20%);
|
color: darken($grey-blue, 20%);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.is-right-aligned {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inherit-color {
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ $button-box-shadow-standard: 0 2px 0 0 rgba($grey, 0.2);
|
||||||
box-shadow: $button-box-shadow-standard;
|
box-shadow: $button-box-shadow-standard;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&.is-hovered {
|
&.is-hovered {
|
||||||
|
@ -70,6 +71,7 @@ $button-box-shadow-standard: 0 2px 0 0 rgba($grey, 0.2);
|
||||||
|
|
||||||
&.is-outlined {
|
&.is-outlined {
|
||||||
border-color: $grey-lighter;
|
border-color: $grey-lighter;
|
||||||
|
background-color: $white;
|
||||||
|
|
||||||
&.is-important {
|
&.is-important {
|
||||||
border-color: $color;
|
border-color: $color;
|
||||||
|
@ -79,14 +81,14 @@ $button-box-shadow-standard: 0 2px 0 0 rgba($grey, 0.2);
|
||||||
&.is-hovered,
|
&.is-hovered,
|
||||||
&:focus,
|
&:focus,
|
||||||
&.is-focused {
|
&.is-focused {
|
||||||
background-color: transparent;
|
background-color: $white;
|
||||||
border-color: darken($color, 10%);
|
border-color: darken($color, 10%);
|
||||||
color: $color;
|
color: $color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&.is-active {
|
&.is-active {
|
||||||
background-color: transparent;
|
background-color: $white;
|
||||||
border-color: darken($color, 10%);
|
border-color: darken($color, 10%);
|
||||||
color: darken($color, 10%);
|
color: darken($color, 10%);
|
||||||
}
|
}
|
||||||
|
@ -95,6 +97,7 @@ $button-box-shadow-standard: 0 2px 0 0 rgba($grey, 0.2);
|
||||||
&.is-inverted.is-outlined {
|
&.is-inverted.is-outlined {
|
||||||
border-color: rgba($color-invert, 0.5);
|
border-color: rgba($color-invert, 0.5);
|
||||||
color: rgba($color-invert, 0.9);
|
color: rgba($color-invert, 0.9);
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&.is-hovered,
|
&.is-hovered,
|
||||||
|
|
|
@ -40,4 +40,34 @@
|
||||||
&.is-inline {
|
&.is-inline {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-sub-field {
|
||||||
|
margin-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
&.is-small {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: $weight-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-heading {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05ch;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
color: $grey;
|
||||||
|
font-weight: $weight-medium;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,12 @@ $icon-dimensions-large: 2rem;
|
||||||
width: $icon-dimensions;
|
width: $icon-dimensions;
|
||||||
fill: $text;
|
fill: $text;
|
||||||
|
|
||||||
&.is-small,
|
|
||||||
&.is-text {
|
&.is-text {
|
||||||
|
width: 1.2em;
|
||||||
|
height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-small {
|
||||||
height: $icon-dimensions-small;
|
height: $icon-dimensions-small;
|
||||||
width: $icon-dimensions-small;
|
width: $icon-dimensions-small;
|
||||||
}
|
}
|
||||||
|
@ -30,6 +34,7 @@ $icon-dimensions-large: 2rem;
|
||||||
|
|
||||||
&.is-faded {
|
&.is-faded {
|
||||||
fill: $grey-light;
|
fill: $grey-light;
|
||||||
|
color: $grey-light;
|
||||||
}
|
}
|
||||||
|
|
||||||
@each $name, $pair in $colors {
|
@each $name, $pair in $colors {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
.section {
|
.section {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
|
||||||
|
&.with-headspace {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-width-section {
|
.full-width-section {
|
||||||
|
|
|
@ -8,4 +8,8 @@
|
||||||
&.with-headroom {
|
&.with-headroom {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.with-subheading {
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
$z-tooltip: 250;
|
$z-tooltip: 250;
|
||||||
$z-header: 210;
|
|
||||||
$z-gutter: 220;
|
$z-gutter: 220;
|
||||||
$z-gutter-backdrop: 219;
|
$z-gutter-backdrop: 219;
|
||||||
|
$z-header: 210;
|
||||||
$z-subnav: 200;
|
$z-subnav: 200;
|
||||||
|
$z-popover: 150;
|
||||||
$z-base: 100;
|
$z-base: 100;
|
||||||
$z-icon-decorators: 50;
|
$z-icon-decorators: 50;
|
||||||
|
|
|
@ -1,13 +1,131 @@
|
||||||
{{title "Client " (or model.name model.shortId)}}
|
{{title "Client " (or model.name model.shortId)}}
|
||||||
<section class="section">
|
<section class="section with-headspace">
|
||||||
<h1 data-test-title class="title">
|
{{#if eligibilityError}}
|
||||||
|
<div data-test-eligibility-error class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<div class="notification is-danger">
|
||||||
|
<h3 data-test-title class="title is-4">Eligibility Error</h3>
|
||||||
|
<p data-test-message>{{eligibilityError}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-centered is-minimum">
|
||||||
|
<button data-test-dismiss class="button is-danger" onclick={{action (mut eligibilityError) ""}}>Okay</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if stopDrainError}}
|
||||||
|
<div data-test-stop-drain-error class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<div class="notification is-danger">
|
||||||
|
<h3 data-test-title class="title is-4">Stop Drain Error</h3>
|
||||||
|
<p data-test-message>{{stopDrainError}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-centered is-minimum">
|
||||||
|
<button data-test-dismiss class="button is-danger" onclick={{action (mut stopDrainError) ""}}>Okay</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if drainError}}
|
||||||
|
<div data-test-drain-error class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<div class="notification is-danger">
|
||||||
|
<h3 data-test-title class="title is-4">Drain Error</h3>
|
||||||
|
<p data-test-message>{{drainError}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-centered is-minimum">
|
||||||
|
<button data-test-dismiss class="button is-danger" onclick={{action (mut drainError) ""}}>Okay</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if showDrainStoppedNotification}}
|
||||||
|
<div class="notification is-info">
|
||||||
|
<div data-test-drain-stopped-notification class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<h3 data-test-title class="title is-4">Drain Stopped</h3>
|
||||||
|
<p data-test-message>The drain has been stopped and the node has been set to ineligible.</p>
|
||||||
|
</div>
|
||||||
|
<div class="column is-centered is-minimum">
|
||||||
|
<button data-test-dismiss class="button is-info" onclick={{action (mut showDrainStoppedNotification) false}}>Okay</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if showDrainUpdateNotification}}
|
||||||
|
<div class="notification is-info">
|
||||||
|
<div data-test-drain-updated-notification class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<h3 data-test-title class="title is-4">Drain Updated</h3>
|
||||||
|
<p data-test-message>The new drain specification has been applied.</p>
|
||||||
|
</div>
|
||||||
|
<div class="column is-centered is-minimum">
|
||||||
|
<button data-test-dismiss class="button is-info" onclick={{action (mut showDrainUpdateNotification) false}}>Okay</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if showDrainNotification}}
|
||||||
|
<div class="notification is-info">
|
||||||
|
<div data-test-drain-complete-notification class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<h3 data-test-title class="title is-4">Drain Complete</h3>
|
||||||
|
<p data-test-message>Allocations have been drained and the node has been set to ineligible.</p>
|
||||||
|
</div>
|
||||||
|
<div class="column is-centered is-minimum">
|
||||||
|
<button data-test-dimiss class="button is-info" onclick={{action (mut showDrainNotification) false}}>Okay</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="toolbar-item is-top-aligned is-minimum">
|
||||||
|
<span class="title">
|
||||||
<span data-test-node-status="{{model.compositeStatus}}" class="node-status-light {{model.compositeStatus}}"></span>
|
<span data-test-node-status="{{model.compositeStatus}}" class="node-status-light {{model.compositeStatus}}"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-item">
|
||||||
|
<h1 data-test-title class="title with-subheading">
|
||||||
{{or model.name model.shortId}}
|
{{or model.name model.shortId}}
|
||||||
<span class="tag is-hollow is-small no-text-transform">
|
</h1>
|
||||||
|
<p>
|
||||||
|
<label class="is-interactive">
|
||||||
|
{{#toggle
|
||||||
|
data-test-eligibility-toggle
|
||||||
|
isActive=model.isEligible
|
||||||
|
isDisabled=(or setEligibility.isRunning model.isDraining)
|
||||||
|
onToggle=(perform setEligibility (not model.isEligible))}}
|
||||||
|
Eligible
|
||||||
|
{{/toggle}}
|
||||||
|
<span class="tooltip" aria-label="Only eligible clients can receive allocations">
|
||||||
|
{{x-icon "info-circle-outline" class="is-faded"}}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<span data-test-node-id class="tag is-hollow is-small no-text-transform">
|
||||||
{{model.id}}
|
{{model.id}}
|
||||||
{{copy-button clipboardText=model.id}}
|
{{copy-button clipboardText=model.id}}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-item is-right-aligned is-top-aligned">
|
||||||
|
{{#if model.isDraining}}
|
||||||
|
{{two-step-button
|
||||||
|
data-test-drain-stop
|
||||||
|
idleText="Stop Drain"
|
||||||
|
cancelText="Cancel"
|
||||||
|
confirmText="Yes, Stop"
|
||||||
|
confirmationMessage="Are you sure you want to stop this drain?"
|
||||||
|
awaitingConfirmation=stopDrain.isRunning
|
||||||
|
onConfirm=(perform stopDrain)}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-item is-right-aligned is-top-aligned">
|
||||||
|
{{drain-popover
|
||||||
|
client=model
|
||||||
|
onDrain=(action "drainNotify")
|
||||||
|
onError=(action "drainError")}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="boxed-section is-small">
|
<div class="boxed-section is-small">
|
||||||
<div class="boxed-section-body inline-definitions">
|
<div class="boxed-section-body inline-definitions">
|
||||||
|
@ -20,22 +138,6 @@
|
||||||
<span class="term">Address</span>
|
<span class="term">Address</span>
|
||||||
{{model.httpAddr}}
|
{{model.httpAddr}}
|
||||||
</span>
|
</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="pair" data-test-datacenter-definition>
|
||||||
<span class="term">Datacenter</span>
|
<span class="term">Datacenter</span>
|
||||||
{{model.datacenter}}
|
{{model.datacenter}}
|
||||||
|
@ -53,30 +155,95 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if model.drainStrategy}}
|
{{#if model.drainStrategy}}
|
||||||
<div class="boxed-section is-small is-info">
|
<div data-test-drain-details class="boxed-section is-info">
|
||||||
<div class="boxed-section-body inline-definitions">
|
<div class="boxed-section-head">
|
||||||
<span class="label">Drain Strategy</span>
|
<div class="boxed-section-row">Drain Strategy</div>
|
||||||
<span class="pair" data-test-drain-deadline>
|
<div class="boxed-section-row">
|
||||||
<span class="term">Deadline</span>
|
<div class="inline-definitions is-small">
|
||||||
|
{{#if (not model.drainStrategy.hasNoDeadline)}}
|
||||||
|
<span class="pair">
|
||||||
|
<span class="term">Duration</span>
|
||||||
{{#if model.drainStrategy.isForced}}
|
{{#if model.drainStrategy.isForced}}
|
||||||
<span class="badge is-danger">Forced Drain</span>
|
<span data-test-duration>--</span>
|
||||||
{{else if model.drainStrategy.hasNoDeadline}}
|
|
||||||
No deadline
|
|
||||||
{{else}}
|
{{else}}
|
||||||
|
<span data-test-duration class="tooltip" aria-label={{format-duration model.drainStrategy.deadline}}>
|
||||||
{{format-duration model.drainStrategy.deadline}}
|
{{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>
|
|
||||||
{{format-ts model.drainStrategy.forceDeadline}}
|
|
||||||
({{moment-from-now model.drainStrategy.forceDeadline interval=1000}})
|
|
||||||
</span>
|
</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<span class="pair" data-test-drain-ignore-system-jobs>
|
|
||||||
<span class="term">Ignore System Jobs?</span>
|
|
||||||
{{if model.drainStrategy.ignoreSystemJobs "Yes" "No"}}
|
|
||||||
</span>
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
<span class="pair">
|
||||||
|
<span class="term">{{if model.drainStrategy.hasNoDeadline "Deadline" "Remaining"}}</span>
|
||||||
|
{{#if model.drainStrategy.hasNoDeadline}}
|
||||||
|
<span data-test-deadline>No deadline</span>
|
||||||
|
{{else if model.drainStrategy.isForced}}
|
||||||
|
<span data-test-deadline>--</span>
|
||||||
|
{{else}}
|
||||||
|
<span data-test-deadline class="tooltip" aria-label={{format-ts model.drainStrategy.forceDeadline}}>
|
||||||
|
{{moment-from-now model.drainStrategy.forceDeadline interval=1000 hideAffix=true}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
</span>
|
||||||
|
<span data-test-force-drain-text class="pair">
|
||||||
|
<span class="term">Force Drain</span>
|
||||||
|
{{#if model.drainStrategy.isForced}}
|
||||||
|
{{x-icon "warning" class="is-text is-warning"}} Yes
|
||||||
|
{{else}}
|
||||||
|
No
|
||||||
|
{{/if}}
|
||||||
|
</span>
|
||||||
|
<span data-test-drain-system-jobs-text class="pair">
|
||||||
|
<span class="term">Drain System Jobs</span>
|
||||||
|
{{if model.drainStrategy.ignoreSystemJobs "No" "Yes"}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{#if (not model.drainStrategy.isForced)}}
|
||||||
|
<div class="pull-right">
|
||||||
|
{{two-step-button
|
||||||
|
data-test-force
|
||||||
|
alignRight=true
|
||||||
|
isInfoAction=true
|
||||||
|
idleText="Force Drain"
|
||||||
|
cancelText="Cancel"
|
||||||
|
confirmText="Yes, Force Drain"
|
||||||
|
confirmationMessage="Are you sure you want to force drain?"
|
||||||
|
awaitingConfirmation=forceDrain.isRunning
|
||||||
|
onConfirm=(perform forceDrain)}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="boxed-section-body">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column nowrap is-minimum">
|
||||||
|
<div class="metric-group">
|
||||||
|
<div class="metric is-primary">
|
||||||
|
<h3 class="label">Complete</h3>
|
||||||
|
<p data-test-complete-count class="value">{{model.completeAllocations.length}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-group">
|
||||||
|
<div class="metric">
|
||||||
|
<h3 class="label">Migrating</h3>
|
||||||
|
<p data-test-migrating-count class="value">{{model.migratingAllocations.length}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="metric-group">
|
||||||
|
<div class="metric">
|
||||||
|
<h3 class="label">Remaining</h3>
|
||||||
|
<p data-test-remaining-count class="value">{{model.runningAllocations.length}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column">
|
||||||
|
<h3 class="title is-4">Status</h3>
|
||||||
|
{{#if model.lastMigrateTime}}
|
||||||
|
<p data-test-status>{{moment-to-now model.lastMigrateTime interval=1000 hideAffix=true}} since an allocation was successfully migrated.</p>
|
||||||
|
{{else}}
|
||||||
|
<p data-test-status>No allocations migrated.</p>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
{{#popover-menu
|
||||||
|
data-test-drain-popover
|
||||||
|
label=(if client.isDraining "Update Drain" "Drain")
|
||||||
|
triggerClass=(concat "is-small " (if drain.isRunning "is-loading")) as |m|}}
|
||||||
|
<form data-test-drain-popover-form onsubmit={{action (queue (action preventDefault) (perform drain m.actions.close))}} class="form is-small">
|
||||||
|
<h4 class="group-heading">Drain Options</h4>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label is-interactive">
|
||||||
|
{{#toggle
|
||||||
|
data-test-drain-deadline-toggle
|
||||||
|
isActive=deadlineEnabled
|
||||||
|
onToggle=(action (mut deadlineEnabled) value="target.checked")}}
|
||||||
|
Deadline
|
||||||
|
{{/toggle}}
|
||||||
|
<span class="tooltip multiline" aria-label="The amount of time a drain must complete within.">
|
||||||
|
{{x-icon "info-circle-outline" class="is-faded"}}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{{#if deadlineEnabled}}
|
||||||
|
<div class="field is-sub-field">
|
||||||
|
{{#power-select
|
||||||
|
data-test-drain-deadline-option-select
|
||||||
|
tagName="div"
|
||||||
|
options=durationQuickOptions
|
||||||
|
selected=selectedDurationQuickOption
|
||||||
|
onChange=(action (mut selectedDurationQuickOption)) as |opt|}}
|
||||||
|
{{opt.label}}
|
||||||
|
{{/power-select}}
|
||||||
|
</div>
|
||||||
|
{{#if durationIsCustom}}
|
||||||
|
<div class="field is-sub-field">
|
||||||
|
<label class="label">Deadline</label>
|
||||||
|
<input
|
||||||
|
data-test-drain-custom-deadline
|
||||||
|
type="text"
|
||||||
|
class="input {{if parseError "is-danger"}}"
|
||||||
|
placeholder="1h30m"
|
||||||
|
oninput={{action (queue
|
||||||
|
(action (mut parseError) '')
|
||||||
|
(action (mut customDuration) value="target.value"))}} />
|
||||||
|
{{#if parseError}}
|
||||||
|
<em class="help is-danger">{{parseError}}</em>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
<div class="field">
|
||||||
|
<label class="label is-interactive">
|
||||||
|
{{#toggle
|
||||||
|
data-test-force-drain-toggle
|
||||||
|
isActive=forceDrain
|
||||||
|
onToggle=(action (mut forceDrain) value="target.checked")}}
|
||||||
|
Force Drain
|
||||||
|
{{/toggle}}
|
||||||
|
<span class="tooltip multiline" aria-label="Immediately remove allocations from the client.">
|
||||||
|
{{x-icon "info-circle-outline" class="is-faded"}}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label is-interactive">
|
||||||
|
{{#toggle
|
||||||
|
data-test-system-jobs-toggle
|
||||||
|
isActive=drainSystemJobs
|
||||||
|
onToggle=(action (mut drainSystemJobs) value="target.checked")}}
|
||||||
|
Drain System Jobs
|
||||||
|
{{/toggle}}
|
||||||
|
<span class="tooltip multiline" aria-label="Stop allocations for system jobs.">
|
||||||
|
{{x-icon "info-circle-outline" class="is-faded"}}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="popover-actions">
|
||||||
|
<button
|
||||||
|
data-test-drain-submit
|
||||||
|
type="button"
|
||||||
|
class="popover-action is-primary"
|
||||||
|
onclick={{perform drain m.actions.close}}>
|
||||||
|
Drain
|
||||||
|
</button>
|
||||||
|
<button data-test-drain-cancel type="button" class="popover-action" onclick={{action m.actions.close}}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{/popover-menu}}
|
|
@ -0,0 +1,12 @@
|
||||||
|
<BasicDropdown
|
||||||
|
@horizontalPosition="right"
|
||||||
|
@onOpen={{action (queue (action (mut isOpen) true) (action capture))}}
|
||||||
|
@onClose={{action (mut isOpen) false}} as |dd|
|
||||||
|
>
|
||||||
|
<dd.Trigger data-test-popover-trigger class={{concat "popover-trigger button is-primary " triggerClass}} {{on "keydown" (action "openOnArrowDown" dd)}}>
|
||||||
|
{{label}} {{x-icon "chevron-down" class="is-text"}}
|
||||||
|
</dd.Trigger>
|
||||||
|
<dd.Content data-test-popover-menu class="popover-content">
|
||||||
|
{{yield dd}}
|
||||||
|
</dd.Content>
|
||||||
|
</BasicDropdown>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<input
|
||||||
|
data-test-input
|
||||||
|
type="checkbox"
|
||||||
|
checked={{isActive}}
|
||||||
|
disabled={{isDisabled}}
|
||||||
|
class="input"
|
||||||
|
onchange={{action onToggle}} />
|
||||||
|
<span data-test-toggler class="toggler" />
|
||||||
|
{{yield}}
|
|
@ -2,17 +2,21 @@
|
||||||
<button
|
<button
|
||||||
data-test-idle-button
|
data-test-idle-button
|
||||||
type="button"
|
type="button"
|
||||||
class="button is-danger is-outlined is-important is-small"
|
class="button {{if isInfoAction "is-warning" "is-danger is-outlined"}} is-important is-small"
|
||||||
disabled={{disabled}}
|
disabled={{disabled}}
|
||||||
onclick={{action "promptForConfirmation"}}>
|
onclick={{action "promptForConfirmation"}}>
|
||||||
{{idleText}}
|
{{idleText}}
|
||||||
</button>
|
</button>
|
||||||
{{else if isPendingConfirmation}}
|
{{else if isPendingConfirmation}}
|
||||||
<span data-test-confirmation-message class="confirmation-text">{{confirmationMessage}}</span>
|
<span
|
||||||
|
data-test-confirmation-message
|
||||||
|
class="confirmation-text {{if isInfoAction "inherit-color"}} {{if alignRight "is-right-aligned"}}">
|
||||||
|
{{confirmationMessage}}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
data-test-cancel-button
|
data-test-cancel-button
|
||||||
type="button"
|
type="button"
|
||||||
class="button is-dark is-outlined is-small"
|
class="button {{if isInfoAction "is-danger is-important" "is-dark"}} is-outlined is-small"
|
||||||
disabled={{awaitingConfirmation}}
|
disabled={{awaitingConfirmation}}
|
||||||
onclick={{action (queue
|
onclick={{action (queue
|
||||||
(action "setToIdle")
|
(action "setToIdle")
|
||||||
|
@ -22,7 +26,7 @@
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
data-test-confirm-button
|
data-test-confirm-button
|
||||||
class="button is-danger is-small {{if awaitingConfirmation "is-loading"}}"
|
class="button {{if isInfoAction "is-warning" "is-danger"}} is-small {{if awaitingConfirmation "is-loading"}}"
|
||||||
disabled={{awaitingConfirmation}}
|
disabled={{awaitingConfirmation}}
|
||||||
onclick={{action "confirm"}}>
|
onclick={{action "confirm"}}>
|
||||||
{{confirmText}}
|
{{confirmText}}
|
||||||
|
|
|
@ -207,6 +207,18 @@ export default function() {
|
||||||
return this.serialize(allocations.where({ nodeId: params.id }));
|
return this.serialize(allocations.where({ nodeId: params.id }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.post('/node/:id/eligibility', function({ nodes }, { params, requestBody }) {
|
||||||
|
const body = JSON.parse(requestBody);
|
||||||
|
const node = nodes.find(params.id);
|
||||||
|
|
||||||
|
node.update({ schedulingEligibility: body.Elibility === 'eligible' });
|
||||||
|
return this.serialize(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.post('/node/:id/drain', function({ nodes }, { params }) {
|
||||||
|
return this.serialize(nodes.find(params.id));
|
||||||
|
});
|
||||||
|
|
||||||
this.get('/allocations');
|
this.get('/allocations');
|
||||||
|
|
||||||
this.get('/allocation/:id');
|
this.get('/allocation/:id');
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
"d3-shape": "^1.2.0",
|
"d3-shape": "^1.2.0",
|
||||||
"d3-time-format": "^2.1.0",
|
"d3-time-format": "^2.1.0",
|
||||||
"d3-transition": "^1.1.0",
|
"d3-transition": "^1.1.0",
|
||||||
|
"duration-js": "^4.0.0",
|
||||||
"ember-ajax": "^5.0.0",
|
"ember-ajax": "^5.0.0",
|
||||||
"ember-auto-import": "^1.2.21",
|
"ember-auto-import": "^1.2.21",
|
||||||
"ember-can": "^2.0.0",
|
"ember-can": "^2.0.0",
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { currentURL } from '@ember/test-helpers';
|
import { currentURL, waitUntil } from '@ember/test-helpers';
|
||||||
import { assign } from '@ember/polyfills';
|
import { assign } from '@ember/polyfills';
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupApplicationTest } from 'ember-qunit';
|
import { setupApplicationTest } from 'ember-qunit';
|
||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
import { formatBytes } from 'nomad-ui/helpers/format-bytes';
|
import { formatBytes } from 'nomad-ui/helpers/format-bytes';
|
||||||
import formatDuration from 'nomad-ui/utils/format-duration';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import ClientDetail from 'nomad-ui/tests/pages/clients/detail';
|
import ClientDetail from 'nomad-ui/tests/pages/clients/detail';
|
||||||
import Clients from 'nomad-ui/tests/pages/clients/list';
|
import Clients from 'nomad-ui/tests/pages/clients/list';
|
||||||
|
@ -55,10 +54,17 @@ module('Acceptance | client detail', function(hooks) {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('/clients/:id should list immediate details for the node in the title', async function(assert) {
|
test('/clients/:id should list immediate details for the node in the title', async function(assert) {
|
||||||
|
node = server.create('node', 'forceIPv4', { schedulingEligibility: 'eligible', drain: false });
|
||||||
|
|
||||||
await ClientDetail.visit({ id: node.id });
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
|
||||||
assert.ok(ClientDetail.title.includes(node.name), 'Title includes name');
|
assert.ok(ClientDetail.title.includes(node.name), 'Title includes name');
|
||||||
assert.ok(ClientDetail.title.includes(node.id), 'Title includes id');
|
assert.ok(ClientDetail.clientId.includes(node.id), 'Title includes id');
|
||||||
|
assert.equal(
|
||||||
|
ClientDetail.statusLight.objectAt(0).id,
|
||||||
|
node.status,
|
||||||
|
'Title includes status light'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('/clients/:id should list additional detail for the node below the title', async function(assert) {
|
test('/clients/:id should list additional detail for the node below the title', async function(assert) {
|
||||||
|
@ -76,14 +82,6 @@ module('Acceptance | client detail', function(hooks) {
|
||||||
ClientDetail.addressDefinition.includes(node.httpAddr),
|
ClientDetail.addressDefinition.includes(node.httpAddr),
|
||||||
'Address is in additional details'
|
'Address is in additional details'
|
||||||
);
|
);
|
||||||
assert.ok(
|
|
||||||
ClientDetail.drainingDefinition.includes(node.drain + ''),
|
|
||||||
'Drain status is in additional details'
|
|
||||||
);
|
|
||||||
assert.ok(
|
|
||||||
ClientDetail.eligibilityDefinition.includes(node.schedulingEligibility),
|
|
||||||
'Scheduling eligibility is in additional details'
|
|
||||||
);
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
ClientDetail.datacenterDefinition.includes(node.datacenter),
|
ClientDetail.datacenterDefinition.includes(node.datacenter),
|
||||||
'Datacenter is in additional details'
|
'Datacenter is in additional details'
|
||||||
|
@ -472,23 +470,19 @@ module('Acceptance | client detail', function(hooks) {
|
||||||
await ClientDetail.visit({ id: node.id });
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
ClientDetail.drain.deadline.includes(formatDuration(deadline)),
|
ClientDetail.drainDetails.deadline.includes(forceDeadline.fromNow(true)),
|
||||||
'Deadline is shown in a human formatted way'
|
'Deadline is shown in a human formatted way'
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.equal(
|
||||||
ClientDetail.drain.forcedDeadline.includes(forceDeadline.format("MMM DD, 'YY HH:mm:ss ZZ")),
|
ClientDetail.drainDetails.deadlineTooltip,
|
||||||
'Force deadline is shown as an absolute date'
|
forceDeadline.format("MMM DD, 'YY HH:mm:ss ZZ"),
|
||||||
|
'The tooltip for deadline shows the force deadline as an absolute date'
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
ClientDetail.drain.forcedDeadline.includes(forceDeadline.fromNow()),
|
ClientDetail.drainDetails.drainSystemJobsText.endsWith('Yes'),
|
||||||
'Force deadline is shown as a relative date'
|
'Drain System Jobs state is shown'
|
||||||
);
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
ClientDetail.drain.ignoreSystemJobs.endsWith('No'),
|
|
||||||
'Ignore System Jobs state is shown'
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -507,19 +501,16 @@ module('Acceptance | client detail', function(hooks) {
|
||||||
|
|
||||||
await ClientDetail.visit({ id: node.id });
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
|
||||||
|
assert.notOk(ClientDetail.drainDetails.durationIsShown, 'Duration is omitted');
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
ClientDetail.drain.deadline.includes('No deadline'),
|
ClientDetail.drainDetails.deadline.includes('No deadline'),
|
||||||
'The value for Deadline is "no deadline"'
|
'The value for Deadline is "no deadline"'
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.notOk(
|
|
||||||
ClientDetail.drain.hasForcedDeadline,
|
|
||||||
'Forced deadline is not shown since there is no forced deadline'
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
ClientDetail.drain.ignoreSystemJobs.endsWith('Yes'),
|
ClientDetail.drainDetails.drainSystemJobsText.endsWith('No'),
|
||||||
'Ignore System Jobs state is shown'
|
'Drain System Jobs state is shown'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -538,19 +529,362 @@ module('Acceptance | client detail', function(hooks) {
|
||||||
|
|
||||||
await ClientDetail.visit({ id: node.id });
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
|
||||||
assert.equal(ClientDetail.drain.badgeLabel, 'Forced Drain', 'Forced Drain badge is described');
|
assert.ok(
|
||||||
assert.ok(ClientDetail.drain.badgeIsDangerous, 'Forced Drain is shown in a red badge');
|
ClientDetail.drainDetails.forceDrainText.endsWith('Yes'),
|
||||||
|
'Forced Drain is described'
|
||||||
assert.notOk(
|
|
||||||
ClientDetail.drain.hasForcedDeadline,
|
|
||||||
'Forced deadline is not shown since there is no forced deadline'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert.ok(ClientDetail.drainDetails.duration.includes('--'), 'Duration is shown but unset');
|
||||||
|
|
||||||
|
assert.ok(ClientDetail.drainDetails.deadline.includes('--'), 'Deadline is shown but unset');
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
ClientDetail.drain.ignoreSystemJobs.endsWith('No'),
|
ClientDetail.drainDetails.drainSystemJobsText.endsWith('Yes'),
|
||||||
'Ignore System Jobs state is shown'
|
'Drain System Jobs state is shown'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('toggling node eligibility disables the toggle and sends the correct POST request', async function(assert) {
|
||||||
|
node = server.create('node', {
|
||||||
|
drain: false,
|
||||||
|
schedulingEligibility: 'eligible',
|
||||||
|
});
|
||||||
|
|
||||||
|
server.pretender.post('/v1/node/:id/eligibility', () => [200, {}, ''], true);
|
||||||
|
|
||||||
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
assert.ok(ClientDetail.eligibilityToggle.isActive);
|
||||||
|
|
||||||
|
ClientDetail.eligibilityToggle.toggle();
|
||||||
|
await waitUntil(() => server.pretender.handledRequests.findBy('method', 'POST'));
|
||||||
|
|
||||||
|
assert.ok(ClientDetail.eligibilityToggle.isDisabled);
|
||||||
|
server.pretender.resolve(server.pretender.requestReferences[0].request);
|
||||||
|
|
||||||
|
assert.notOk(ClientDetail.eligibilityToggle.isActive);
|
||||||
|
assert.notOk(ClientDetail.eligibilityToggle.isDisabled);
|
||||||
|
|
||||||
|
const request = server.pretender.handledRequests.findBy('method', 'POST');
|
||||||
|
assert.equal(request.url, `/v1/node/${node.id}/eligibility`);
|
||||||
|
assert.deepEqual(JSON.parse(request.requestBody), {
|
||||||
|
NodeID: node.id,
|
||||||
|
Eligibility: 'ineligible',
|
||||||
|
});
|
||||||
|
|
||||||
|
ClientDetail.eligibilityToggle.toggle();
|
||||||
|
await waitUntil(() => server.pretender.handledRequests.filterBy('method', 'POST').length === 2);
|
||||||
|
server.pretender.resolve(server.pretender.requestReferences[0].request);
|
||||||
|
|
||||||
|
assert.ok(ClientDetail.eligibilityToggle.isActive);
|
||||||
|
const request2 = server.pretender.handledRequests.filterBy('method', 'POST')[1];
|
||||||
|
|
||||||
|
assert.equal(request2.url, `/v1/node/${node.id}/eligibility`);
|
||||||
|
assert.deepEqual(JSON.parse(request2.requestBody), {
|
||||||
|
NodeID: node.id,
|
||||||
|
Eligibility: 'eligible',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('starting a drain sends the correct POST request', async function(assert) {
|
||||||
|
let request;
|
||||||
|
|
||||||
|
node = server.create('node', {
|
||||||
|
drain: false,
|
||||||
|
schedulingEligibility: 'eligible',
|
||||||
|
});
|
||||||
|
|
||||||
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
await ClientDetail.drainPopover.toggle();
|
||||||
|
await ClientDetail.drainPopover.submit();
|
||||||
|
|
||||||
|
request = server.pretender.handledRequests.filterBy('method', 'POST').pop();
|
||||||
|
|
||||||
|
assert.equal(request.url, `/v1/node/${node.id}/drain`);
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(request.requestBody),
|
||||||
|
{
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: {
|
||||||
|
Deadline: 0,
|
||||||
|
IgnoreSystemJobs: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Drain with default settings'
|
||||||
|
);
|
||||||
|
|
||||||
|
await ClientDetail.drainPopover.toggle();
|
||||||
|
await ClientDetail.drainPopover.deadlineToggle.toggle();
|
||||||
|
await ClientDetail.drainPopover.submit();
|
||||||
|
|
||||||
|
request = server.pretender.handledRequests.filterBy('method', 'POST').pop();
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(request.requestBody),
|
||||||
|
{
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: {
|
||||||
|
Deadline: 60 * 60 * 1000 * 1000000,
|
||||||
|
IgnoreSystemJobs: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Drain with deadline toggled'
|
||||||
|
);
|
||||||
|
|
||||||
|
await ClientDetail.drainPopover.toggle();
|
||||||
|
await ClientDetail.drainPopover.deadlineOptions.open();
|
||||||
|
await ClientDetail.drainPopover.deadlineOptions.options[1].choose();
|
||||||
|
await ClientDetail.drainPopover.submit();
|
||||||
|
|
||||||
|
request = server.pretender.handledRequests.filterBy('method', 'POST').pop();
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(request.requestBody),
|
||||||
|
{
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: {
|
||||||
|
Deadline: 4 * 60 * 60 * 1000 * 1000000,
|
||||||
|
IgnoreSystemJobs: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Drain with non-default preset deadline set'
|
||||||
|
);
|
||||||
|
|
||||||
|
await ClientDetail.drainPopover.toggle();
|
||||||
|
await ClientDetail.drainPopover.deadlineOptions.open();
|
||||||
|
const optionsCount = ClientDetail.drainPopover.deadlineOptions.options.length;
|
||||||
|
await ClientDetail.drainPopover.deadlineOptions.options.objectAt(optionsCount - 1).choose();
|
||||||
|
await ClientDetail.drainPopover.setCustomDeadline('1h40m20s');
|
||||||
|
await ClientDetail.drainPopover.submit();
|
||||||
|
|
||||||
|
request = server.pretender.handledRequests.filterBy('method', 'POST').pop();
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(request.requestBody),
|
||||||
|
{
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: {
|
||||||
|
Deadline: ((1 * 60 + 40) * 60 + 20) * 1000 * 1000000,
|
||||||
|
IgnoreSystemJobs: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Drain with custom deadline set'
|
||||||
|
);
|
||||||
|
|
||||||
|
await ClientDetail.drainPopover.toggle();
|
||||||
|
await ClientDetail.drainPopover.deadlineToggle.toggle();
|
||||||
|
await ClientDetail.drainPopover.forceDrainToggle.toggle();
|
||||||
|
await ClientDetail.drainPopover.submit();
|
||||||
|
|
||||||
|
request = server.pretender.handledRequests.filterBy('method', 'POST').pop();
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(request.requestBody),
|
||||||
|
{
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: {
|
||||||
|
Deadline: -1,
|
||||||
|
IgnoreSystemJobs: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Drain with force set'
|
||||||
|
);
|
||||||
|
|
||||||
|
await ClientDetail.drainPopover.toggle();
|
||||||
|
await ClientDetail.drainPopover.systemJobsToggle.toggle();
|
||||||
|
await ClientDetail.drainPopover.submit();
|
||||||
|
|
||||||
|
request = server.pretender.handledRequests.filterBy('method', 'POST').pop();
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(request.requestBody),
|
||||||
|
{
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: {
|
||||||
|
Deadline: -1,
|
||||||
|
IgnoreSystemJobs: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Drain system jobs unset'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the drain popover cancel button closes the popover', async function(assert) {
|
||||||
|
node = server.create('node', {
|
||||||
|
drain: false,
|
||||||
|
schedulingEligibility: 'eligible',
|
||||||
|
});
|
||||||
|
|
||||||
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
assert.notOk(ClientDetail.drainPopover.isOpen);
|
||||||
|
|
||||||
|
await ClientDetail.drainPopover.toggle();
|
||||||
|
assert.ok(ClientDetail.drainPopover.isOpen);
|
||||||
|
|
||||||
|
await ClientDetail.drainPopover.cancel();
|
||||||
|
assert.notOk(ClientDetail.drainPopover.isOpen);
|
||||||
|
assert.equal(server.pretender.handledRequests.filterBy('method', 'POST'), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toggling eligibility is disabled while a drain is active', async function(assert) {
|
||||||
|
node = server.create('node', {
|
||||||
|
drain: true,
|
||||||
|
schedulingEligibility: 'ineligible',
|
||||||
|
});
|
||||||
|
|
||||||
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
assert.ok(ClientDetail.eligibilityToggle.isDisabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stopping a drain sends the correct POST request', async function(assert) {
|
||||||
|
node = server.create('node', {
|
||||||
|
drain: true,
|
||||||
|
schedulingEligibility: 'ineligible',
|
||||||
|
});
|
||||||
|
|
||||||
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
assert.ok(ClientDetail.stopDrainIsPresent);
|
||||||
|
|
||||||
|
await ClientDetail.stopDrain.idle();
|
||||||
|
await ClientDetail.stopDrain.confirm();
|
||||||
|
|
||||||
|
const request = server.pretender.handledRequests.findBy('method', 'POST');
|
||||||
|
assert.equal(request.url, `/v1/node/${node.id}/drain`);
|
||||||
|
assert.deepEqual(JSON.parse(request.requestBody), {
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when a drain is active, the "drain" popover is labeled as the "update" popover', async function(assert) {
|
||||||
|
node = server.create('node', {
|
||||||
|
drain: true,
|
||||||
|
schedulingEligibility: 'ineligible',
|
||||||
|
});
|
||||||
|
|
||||||
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
assert.equal(ClientDetail.drainPopover.label, 'Update Drain');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('forcing a drain sends the correct POST request', async function(assert) {
|
||||||
|
node = server.create('node', {
|
||||||
|
drain: true,
|
||||||
|
schedulingEligibility: 'ineligible',
|
||||||
|
drainStrategy: {
|
||||||
|
Deadline: 0,
|
||||||
|
IgnoreSystemJobs: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
await ClientDetail.drainDetails.force.idle();
|
||||||
|
await ClientDetail.drainDetails.force.confirm();
|
||||||
|
|
||||||
|
const request = server.pretender.handledRequests.findBy('method', 'POST');
|
||||||
|
assert.equal(request.url, `/v1/node/${node.id}/drain`);
|
||||||
|
assert.deepEqual(JSON.parse(request.requestBody), {
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: {
|
||||||
|
Deadline: -1,
|
||||||
|
IgnoreSystemJobs: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when stopping a drain fails, an error is shown', async function(assert) {
|
||||||
|
node = server.create('node', {
|
||||||
|
drain: true,
|
||||||
|
schedulingEligibility: 'ineligible',
|
||||||
|
});
|
||||||
|
|
||||||
|
server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']);
|
||||||
|
|
||||||
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
await ClientDetail.stopDrain.idle();
|
||||||
|
await ClientDetail.stopDrain.confirm();
|
||||||
|
|
||||||
|
assert.ok(ClientDetail.stopDrainError.isPresent);
|
||||||
|
assert.ok(ClientDetail.stopDrainError.title.includes('Stop Drain Error'));
|
||||||
|
|
||||||
|
await ClientDetail.stopDrainError.dismiss();
|
||||||
|
assert.notOk(ClientDetail.stopDrainError.isPresent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when starting a drain fails, an error message is shown', async function(assert) {
|
||||||
|
node = server.create('node', {
|
||||||
|
drain: false,
|
||||||
|
schedulingEligibility: 'eligible',
|
||||||
|
});
|
||||||
|
|
||||||
|
server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']);
|
||||||
|
|
||||||
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
await ClientDetail.drainPopover.toggle();
|
||||||
|
await ClientDetail.drainPopover.submit();
|
||||||
|
|
||||||
|
assert.ok(ClientDetail.drainError.isPresent);
|
||||||
|
assert.ok(ClientDetail.drainError.title.includes('Drain Error'));
|
||||||
|
|
||||||
|
await ClientDetail.drainError.dismiss();
|
||||||
|
assert.notOk(ClientDetail.drainError.isPresent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when updating a drain fails, an error message is shown', async function(assert) {
|
||||||
|
node = server.create('node', {
|
||||||
|
drain: true,
|
||||||
|
schedulingEligibility: 'ineligible',
|
||||||
|
});
|
||||||
|
|
||||||
|
server.pretender.post('/v1/node/:id/drain', () => [500, {}, '']);
|
||||||
|
|
||||||
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
await ClientDetail.drainPopover.toggle();
|
||||||
|
await ClientDetail.drainPopover.submit();
|
||||||
|
|
||||||
|
assert.ok(ClientDetail.drainError.isPresent);
|
||||||
|
assert.ok(ClientDetail.drainError.title.includes('Drain Error'));
|
||||||
|
|
||||||
|
await ClientDetail.drainError.dismiss();
|
||||||
|
assert.notOk(ClientDetail.drainError.isPresent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when toggling eligibility fails, an error message is shown', async function(assert) {
|
||||||
|
node = server.create('node', {
|
||||||
|
drain: false,
|
||||||
|
schedulingEligibility: 'eligible',
|
||||||
|
});
|
||||||
|
|
||||||
|
server.pretender.post('/v1/node/:id/eligibility', () => [500, {}, '']);
|
||||||
|
|
||||||
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
await ClientDetail.eligibilityToggle.toggle();
|
||||||
|
|
||||||
|
assert.ok(ClientDetail.eligibilityError.isPresent);
|
||||||
|
assert.ok(ClientDetail.eligibilityError.title.includes('Eligibility Error'));
|
||||||
|
|
||||||
|
await ClientDetail.eligibilityError.dismiss();
|
||||||
|
assert.notOk(ClientDetail.eligibilityError.isPresent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('when navigating away from a client that has an error message to another client, the error is not shown', async function(assert) {
|
||||||
|
node = server.create('node', {
|
||||||
|
drain: false,
|
||||||
|
schedulingEligibility: 'eligible',
|
||||||
|
});
|
||||||
|
|
||||||
|
const node2 = server.create('node');
|
||||||
|
|
||||||
|
server.pretender.post('/v1/node/:id/eligibility', () => [500, {}, '']);
|
||||||
|
|
||||||
|
await ClientDetail.visit({ id: node.id });
|
||||||
|
await ClientDetail.eligibilityToggle.toggle();
|
||||||
|
|
||||||
|
assert.ok(ClientDetail.eligibilityError.isPresent);
|
||||||
|
assert.ok(ClientDetail.eligibilityError.title.includes('Eligibility Error'));
|
||||||
|
|
||||||
|
await ClientDetail.visit({ id: node2.id });
|
||||||
|
|
||||||
|
assert.notOk(ClientDetail.eligibilityError.isPresent);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module('Acceptance | client detail (multi-namespace)', function(hooks) {
|
module('Acceptance | client detail (multi-namespace)', function(hooks) {
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { click } from '@ember/test-helpers';
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupRenderingTest } from 'ember-qunit';
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
import { create } from 'ember-cli-page-object';
|
||||||
|
import popoverMenuPageObject from 'nomad-ui/tests/pages/components/popover-menu';
|
||||||
|
|
||||||
|
const PopoverMenu = create(popoverMenuPageObject());
|
||||||
|
|
||||||
|
module('Integration | Component | popover-menu', function(hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
const commonProperties = overrides =>
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
triggerClass: '',
|
||||||
|
label: 'Trigger Label',
|
||||||
|
},
|
||||||
|
overrides
|
||||||
|
);
|
||||||
|
|
||||||
|
const commonTemplate = hbs`
|
||||||
|
{{#popover-menu
|
||||||
|
isOpen=isOpen
|
||||||
|
label=label
|
||||||
|
triggerClass=triggerClass as |m|}}
|
||||||
|
<h1>This is a heading</h1>
|
||||||
|
<label>This is an input: <input id="mock-input-for-test" type="text" /></label>
|
||||||
|
<button id="mock-button-for-test" type="button" onclick={{action m.actions.close}}>Close Button</button>
|
||||||
|
{{/popover-menu}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test('presents as a button with a chevron-down icon', async function(assert) {
|
||||||
|
const props = commonProperties();
|
||||||
|
this.setProperties(props);
|
||||||
|
await this.render(commonTemplate);
|
||||||
|
|
||||||
|
assert.ok(PopoverMenu.isPresent);
|
||||||
|
assert.ok(PopoverMenu.labelHasIcon);
|
||||||
|
assert.notOk(PopoverMenu.menu.isOpen);
|
||||||
|
assert.equal(PopoverMenu.label, props.label);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking the trigger button toggles the popover menu', async function(assert) {
|
||||||
|
const props = commonProperties();
|
||||||
|
this.setProperties(props);
|
||||||
|
await this.render(commonTemplate);
|
||||||
|
assert.notOk(PopoverMenu.menu.isOpen);
|
||||||
|
|
||||||
|
await PopoverMenu.toggle();
|
||||||
|
|
||||||
|
assert.ok(PopoverMenu.menu.isOpen);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the trigger gets the triggerClass prop assigned as a class', async function(assert) {
|
||||||
|
const specialClass = 'is-special';
|
||||||
|
const props = commonProperties({ triggerClass: specialClass });
|
||||||
|
this.setProperties(props);
|
||||||
|
await this.render(commonTemplate);
|
||||||
|
|
||||||
|
assert.dom('[data-test-popover-trigger]').hasClass('is-special');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pressing DOWN ARROW when the trigger is focused opens the popover menu', async function(assert) {
|
||||||
|
const props = commonProperties();
|
||||||
|
this.setProperties(props);
|
||||||
|
await this.render(commonTemplate);
|
||||||
|
assert.notOk(PopoverMenu.menu.isOpen);
|
||||||
|
|
||||||
|
await PopoverMenu.focus();
|
||||||
|
await PopoverMenu.downArrow();
|
||||||
|
|
||||||
|
assert.ok(PopoverMenu.menu.isOpen);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pressing TAB when the trigger button is focused and the menu is open focuses the first focusable element in the popover menu', async function(assert) {
|
||||||
|
const props = commonProperties();
|
||||||
|
this.setProperties(props);
|
||||||
|
await this.render(commonTemplate);
|
||||||
|
|
||||||
|
await PopoverMenu.focus();
|
||||||
|
await PopoverMenu.downArrow();
|
||||||
|
|
||||||
|
assert.dom('[data-test-popover-trigger]').isFocused();
|
||||||
|
|
||||||
|
await PopoverMenu.focusNext();
|
||||||
|
|
||||||
|
assert.dom('#mock-input-for-test').isFocused();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pressing ESC when the popover menu is open closes the menu and returns focus to the trigger button', async function(assert) {
|
||||||
|
const props = commonProperties();
|
||||||
|
this.setProperties(props);
|
||||||
|
await this.render(commonTemplate);
|
||||||
|
|
||||||
|
await PopoverMenu.toggle();
|
||||||
|
assert.ok(PopoverMenu.menu.isOpen);
|
||||||
|
|
||||||
|
await PopoverMenu.esc();
|
||||||
|
|
||||||
|
assert.notOk(PopoverMenu.menu.isOpen);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the ember-basic-dropdown object is yielded as context, including the close action', async function(assert) {
|
||||||
|
const props = commonProperties();
|
||||||
|
this.setProperties(props);
|
||||||
|
await this.render(commonTemplate);
|
||||||
|
|
||||||
|
await PopoverMenu.toggle();
|
||||||
|
assert.ok(PopoverMenu.menu.isOpen);
|
||||||
|
|
||||||
|
await click('#mock-button-for-test');
|
||||||
|
assert.notOk(PopoverMenu.menu.isOpen);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { find, settled } from '@ember/test-helpers';
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupRenderingTest } from 'ember-qunit';
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import { create } from 'ember-cli-page-object';
|
||||||
|
import togglePageObject from 'nomad-ui/tests/pages/components/toggle';
|
||||||
|
|
||||||
|
const Toggle = create(togglePageObject());
|
||||||
|
|
||||||
|
module('Integration | Component | toggle', function(hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
const commonProperties = () => ({
|
||||||
|
isActive: false,
|
||||||
|
isDisabled: false,
|
||||||
|
label: 'Label',
|
||||||
|
onToggle: sinon.spy(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const commonTemplate = hbs`
|
||||||
|
{{#toggle
|
||||||
|
isActive=isActive
|
||||||
|
isDisabled=isDisabled
|
||||||
|
onToggle=onToggle}}
|
||||||
|
{{label}}
|
||||||
|
{{/toggle}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test('presents as a label with an inner checkbox and display span, and text', async function(assert) {
|
||||||
|
const props = commonProperties();
|
||||||
|
this.setProperties(props);
|
||||||
|
await this.render(commonTemplate);
|
||||||
|
|
||||||
|
assert.equal(Toggle.label, props.label, `Label should be ${props.label}`);
|
||||||
|
assert.ok(Toggle.isPresent);
|
||||||
|
assert.notOk(Toggle.isActive);
|
||||||
|
assert.ok(find('[data-test-toggler]'));
|
||||||
|
assert.equal(
|
||||||
|
find('[data-test-input]').tagName.toLowerCase(),
|
||||||
|
'input',
|
||||||
|
'The input is a real HTML input'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
find('[data-test-input]').getAttribute('type'),
|
||||||
|
'checkbox',
|
||||||
|
'The input type is checkbox'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the isActive property dictates the active state and class', async function(assert) {
|
||||||
|
const props = commonProperties();
|
||||||
|
this.setProperties(props);
|
||||||
|
await this.render(commonTemplate);
|
||||||
|
|
||||||
|
assert.notOk(Toggle.isActive);
|
||||||
|
assert.notOk(Toggle.hasActiveClass);
|
||||||
|
|
||||||
|
this.set('isActive', true);
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
assert.ok(Toggle.isActive);
|
||||||
|
assert.ok(Toggle.hasActiveClass);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the isDisabled property dictates the disabled state and class', async function(assert) {
|
||||||
|
const props = commonProperties();
|
||||||
|
this.setProperties(props);
|
||||||
|
await this.render(commonTemplate);
|
||||||
|
|
||||||
|
assert.notOk(Toggle.isDisabled);
|
||||||
|
assert.notOk(Toggle.hasDisabledClass);
|
||||||
|
|
||||||
|
this.set('isDisabled', true);
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
assert.ok(Toggle.isDisabled);
|
||||||
|
assert.ok(Toggle.hasDisabledClass);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toggling the input calls the onToggle action', async function(assert) {
|
||||||
|
const props = commonProperties();
|
||||||
|
this.setProperties(props);
|
||||||
|
await this.render(commonTemplate);
|
||||||
|
|
||||||
|
await Toggle.toggle();
|
||||||
|
assert.equal(props.onToggle.callCount, 1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -3,13 +3,16 @@ import {
|
||||||
create,
|
create,
|
||||||
collection,
|
collection,
|
||||||
clickable,
|
clickable,
|
||||||
hasClass,
|
fillable,
|
||||||
text,
|
text,
|
||||||
isPresent,
|
isPresent,
|
||||||
visitable,
|
visitable,
|
||||||
} from 'ember-cli-page-object';
|
} from 'ember-cli-page-object';
|
||||||
|
|
||||||
import allocations from 'nomad-ui/tests/pages/components/allocations';
|
import allocations from 'nomad-ui/tests/pages/components/allocations';
|
||||||
|
import twoStepButton from 'nomad-ui/tests/pages/components/two-step-button';
|
||||||
|
import notification from 'nomad-ui/tests/pages/components/notification';
|
||||||
|
import toggle from 'nomad-ui/tests/pages/components/toggle';
|
||||||
|
|
||||||
export default create({
|
export default create({
|
||||||
visit: visitable('/clients/:id'),
|
visit: visitable('/clients/:id'),
|
||||||
|
@ -25,6 +28,7 @@ export default create({
|
||||||
},
|
},
|
||||||
|
|
||||||
title: text('[data-test-title]'),
|
title: text('[data-test-title]'),
|
||||||
|
clientId: text('[data-test-node-id]'),
|
||||||
|
|
||||||
statusLight: collection('[data-test-node-status]', {
|
statusLight: collection('[data-test-node-status]', {
|
||||||
id: attribute('data-test-node-status'),
|
id: attribute('data-test-node-status'),
|
||||||
|
@ -34,8 +38,6 @@ export default create({
|
||||||
statusDefinition: text('[data-test-status-definition]'),
|
statusDefinition: text('[data-test-status-definition]'),
|
||||||
statusDecorationClass: attribute('class', '[data-test-status-definition] .status-text'),
|
statusDecorationClass: attribute('class', '[data-test-status-definition] .status-text'),
|
||||||
addressDefinition: text('[data-test-address-definition]'),
|
addressDefinition: text('[data-test-address-definition]'),
|
||||||
drainingDefinition: text('[data-test-draining]'),
|
|
||||||
eligibilityDefinition: text('[data-test-eligibility]'),
|
|
||||||
datacenterDefinition: text('[data-test-datacenter-definition]'),
|
datacenterDefinition: text('[data-test-datacenter-definition]'),
|
||||||
|
|
||||||
resourceCharts: collection('[data-test-primary-metric]', {
|
resourceCharts: collection('[data-test-primary-metric]', {
|
||||||
|
@ -92,12 +94,65 @@ export default create({
|
||||||
attributesAreShown: isPresent('[data-test-driver-attributes]'),
|
attributesAreShown: isPresent('[data-test-driver-attributes]'),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
drain: {
|
drainDetails: {
|
||||||
deadline: text('[data-test-drain-deadline]'),
|
scope: '[data-test-drain-details]',
|
||||||
forcedDeadline: text('[data-test-drain-forced-deadline]'),
|
durationIsPresent: isPresent('[data-test-duration]'),
|
||||||
hasForcedDeadline: isPresent('[data-test-drain-forced-deadline]'),
|
duration: text('[data-test-duration]'),
|
||||||
ignoreSystemJobs: text('[data-test-drain-ignore-system-jobs]'),
|
durationTooltip: attribute('aria-label', '[data-test-duration]'),
|
||||||
badgeIsDangerous: hasClass('is-danger', '[data-test-drain-deadline] .badge'),
|
durationIsShown: isPresent('[data-test-duration]'),
|
||||||
badgeLabel: text('[data-test-drain-deadline] .badge'),
|
deadline: text('[data-test-deadline]'),
|
||||||
|
deadlineTooltip: attribute('aria-label', '[data-test-deadline]'),
|
||||||
|
deadlineIsShown: isPresent('[data-test-deadline]'),
|
||||||
|
forceDrainText: text('[data-test-force-drain-text]'),
|
||||||
|
drainSystemJobsText: text('[data-test-drain-system-jobs-text]'),
|
||||||
|
|
||||||
|
completeCount: text('[data-test-complete-count]'),
|
||||||
|
migratingCount: text('[data-test-migrating-count]'),
|
||||||
|
remainingCount: text('[data-test-remaining-count]'),
|
||||||
|
status: text('[data-test-status]'),
|
||||||
|
force: twoStepButton('[data-test-force]'),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
drainPopover: {
|
||||||
|
label: text('[data-test-drain-popover] [data-test-popover-trigger]'),
|
||||||
|
isOpen: isPresent('[data-test-drain-popover-form]'),
|
||||||
|
toggle: clickable('[data-test-drain-popover] [data-test-popover-trigger]'),
|
||||||
|
|
||||||
|
deadlineToggle: toggle('[data-test-drain-deadline-toggle]'),
|
||||||
|
deadlineOptions: {
|
||||||
|
open: clickable('[data-test-drain-deadline-option-select] .ember-power-select-trigger'),
|
||||||
|
options: collection('.ember-power-select-option', {
|
||||||
|
label: text(),
|
||||||
|
choose: clickable(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
setCustomDeadline: fillable('[data-test-drain-custom-deadline]'),
|
||||||
|
customDeadline: attribute('value', '[data-test-drain-custom-deadline]'),
|
||||||
|
forceDrainToggle: toggle('[data-test-force-drain-toggle]'),
|
||||||
|
systemJobsToggle: toggle('[data-test-system-jobs-toggle]'),
|
||||||
|
|
||||||
|
submit: clickable('[data-test-drain-submit]'),
|
||||||
|
cancel: clickable('[data-test-drain-cancel]'),
|
||||||
|
|
||||||
|
setDeadline(label) {
|
||||||
|
this.deadlineOptions.open();
|
||||||
|
this.deadlineOptions.options
|
||||||
|
.toArray()
|
||||||
|
.findBy('label', label)
|
||||||
|
.choose();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
stopDrain: twoStepButton('[data-test-drain-stop]'),
|
||||||
|
stopDrainIsPresent: isPresent('[data-test-drain-stop]'),
|
||||||
|
|
||||||
|
eligibilityToggle: toggle('[data-test-eligibility-toggle]'),
|
||||||
|
|
||||||
|
eligibilityError: notification('[data-test-eligibility-error]'),
|
||||||
|
stopDrainError: notification('[data-test-stop-drain-error]'),
|
||||||
|
drainError: notification('[data-test-drain-error]'),
|
||||||
|
drainStoppedNotification: notification('[data-test-drain-stopped-notification]'),
|
||||||
|
drainUpdatedNotification: notification('[data-test-drain-updated-notification]'),
|
||||||
|
drainCompleteNotification: notification('[data-test-drain-complete-notification]'),
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { isPresent, clickable, text } from 'ember-cli-page-object';
|
||||||
|
|
||||||
|
export default scope => ({
|
||||||
|
scope,
|
||||||
|
|
||||||
|
isPresent: isPresent(),
|
||||||
|
dismiss: clickable('[data-test-dismiss]'),
|
||||||
|
title: text('[data-test-title]'),
|
||||||
|
message: text('[data-test-message]'),
|
||||||
|
});
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { clickable, focusable, isPresent, text, triggerable } from 'ember-cli-page-object';
|
||||||
|
|
||||||
|
const ARROW_DOWN = 40;
|
||||||
|
const ESC = 27;
|
||||||
|
const TAB = 9;
|
||||||
|
|
||||||
|
export default scope => ({
|
||||||
|
scope,
|
||||||
|
|
||||||
|
isPresent: isPresent(),
|
||||||
|
label: text('[data-test-popover-trigger]'),
|
||||||
|
labelHasIcon: isPresent('[data-test-popover-trigger] svg.icon'),
|
||||||
|
|
||||||
|
toggle: clickable('[data-test-popover-trigger]'),
|
||||||
|
focus: focusable('[data-test-popover-trigger]'),
|
||||||
|
downArrow: triggerable('keydown', '[data-test-popover-trigger]', {
|
||||||
|
eventProperties: { keyCode: ARROW_DOWN },
|
||||||
|
}),
|
||||||
|
focusNext: triggerable('keydown', '[data-test-popover-trigger]', {
|
||||||
|
eventProperties: { keyCode: TAB },
|
||||||
|
}),
|
||||||
|
esc: triggerable('keydown', '[data-test-popover-trigger]', {
|
||||||
|
eventProperties: { keyCode: ESC },
|
||||||
|
}),
|
||||||
|
|
||||||
|
menu: {
|
||||||
|
scope: '[data-test-popover-menu]',
|
||||||
|
testContainer: '#ember-testing',
|
||||||
|
resetScope: true,
|
||||||
|
isOpen: isPresent(),
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { attribute, property, clickable, hasClass, isPresent, text } from 'ember-cli-page-object';
|
||||||
|
|
||||||
|
export default scope => ({
|
||||||
|
scope,
|
||||||
|
|
||||||
|
isPresent: isPresent(),
|
||||||
|
isDisabled: attribute('disabled', '[data-test-input]'),
|
||||||
|
isActive: property('checked', '[data-test-input]'),
|
||||||
|
|
||||||
|
hasDisabledClass: hasClass('is-disabled', '[data-test-label]'),
|
||||||
|
hasActiveClass: hasClass('is-active', '[data-test-label]'),
|
||||||
|
|
||||||
|
label: text('[data-test-label]'),
|
||||||
|
|
||||||
|
toggle: clickable('[data-test-input]'),
|
||||||
|
});
|
|
@ -85,6 +85,159 @@ module('Unit | Adapter | Node', function(hooks) {
|
||||||
'The deleted allocation is removed from the store and the allocations associated with the other node are untouched'
|
'The deleted allocation is removed from the store and the allocations associated with the other node are untouched'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('setEligible makes the correct POST request to /:node_id/eligibility', async function(assert) {
|
||||||
|
const { pretender } = this.server;
|
||||||
|
const node = await run(() => this.store.findRecord('node', 'node-1'));
|
||||||
|
|
||||||
|
await this.subject().setEligible(node);
|
||||||
|
|
||||||
|
const request = pretender.handledRequests.lastObject;
|
||||||
|
assert.equal(
|
||||||
|
request.url,
|
||||||
|
`/v1/node/${node.id}/eligibility`,
|
||||||
|
'Request was made to /:node_id/eligibility'
|
||||||
|
);
|
||||||
|
assert.equal(request.method, 'POST', 'Request was made with the POST method');
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(request.requestBody),
|
||||||
|
{
|
||||||
|
NodeID: node.id,
|
||||||
|
Eligibility: 'eligible',
|
||||||
|
},
|
||||||
|
'POST request is made with the correct body arguments'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setIneligible makes the correct POST request to /:node_id/eligibility', async function(assert) {
|
||||||
|
const { pretender } = this.server;
|
||||||
|
const node = await run(() => this.store.findRecord('node', 'node-1'));
|
||||||
|
|
||||||
|
await this.subject().setIneligible(node);
|
||||||
|
|
||||||
|
const request = pretender.handledRequests.lastObject;
|
||||||
|
assert.equal(
|
||||||
|
request.url,
|
||||||
|
`/v1/node/${node.id}/eligibility`,
|
||||||
|
'Request was made to /:node_id/eligibility'
|
||||||
|
);
|
||||||
|
assert.equal(request.method, 'POST', 'Request was made with the POST method');
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(request.requestBody),
|
||||||
|
{
|
||||||
|
NodeID: node.id,
|
||||||
|
Eligibility: 'ineligible',
|
||||||
|
},
|
||||||
|
'POST request is made with the correct body arguments'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('drain makes the correct POST request to /:node_id/drain with appropriate defaults', async function(assert) {
|
||||||
|
const { pretender } = this.server;
|
||||||
|
const node = await run(() => this.store.findRecord('node', 'node-1'));
|
||||||
|
|
||||||
|
await this.subject().drain(node);
|
||||||
|
|
||||||
|
const request = pretender.handledRequests.lastObject;
|
||||||
|
assert.equal(request.url, `/v1/node/${node.id}/drain`, 'Request was made to /:node_id/drain');
|
||||||
|
assert.equal(request.method, 'POST', 'Request was made with the POST method');
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(request.requestBody),
|
||||||
|
{
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: {
|
||||||
|
Deadline: 0,
|
||||||
|
IgnoreSystemJobs: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'POST request is made with the default body arguments'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('drain makes the correct POST request to /:node_id/drain with the provided drain spec', async function(assert) {
|
||||||
|
const { pretender } = this.server;
|
||||||
|
const node = await run(() => this.store.findRecord('node', 'node-1'));
|
||||||
|
|
||||||
|
const spec = { Deadline: 123456789, IgnoreSystemJobs: false };
|
||||||
|
await this.subject().drain(node, spec);
|
||||||
|
|
||||||
|
const request = pretender.handledRequests.lastObject;
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(request.requestBody),
|
||||||
|
{
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: {
|
||||||
|
Deadline: spec.Deadline,
|
||||||
|
IgnoreSystemJobs: spec.IgnoreSystemJobs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'POST request is made with the drain spec as body arguments'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('forceDrain makes the correct POST request to /:node_id/drain with appropriate defaults', async function(assert) {
|
||||||
|
const { pretender } = this.server;
|
||||||
|
const node = await run(() => this.store.findRecord('node', 'node-1'));
|
||||||
|
|
||||||
|
await this.subject().forceDrain(node);
|
||||||
|
|
||||||
|
const request = pretender.handledRequests.lastObject;
|
||||||
|
assert.equal(request.url, `/v1/node/${node.id}/drain`, 'Request was made to /:node_id/drain');
|
||||||
|
assert.equal(request.method, 'POST', 'Request was made with the POST method');
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(request.requestBody),
|
||||||
|
{
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: {
|
||||||
|
Deadline: -1,
|
||||||
|
IgnoreSystemJobs: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'POST request is made with the default body arguments'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('forceDrain makes the correct POST request to /:node_id/drain with the provided drain spec', async function(assert) {
|
||||||
|
const { pretender } = this.server;
|
||||||
|
const node = await run(() => this.store.findRecord('node', 'node-1'));
|
||||||
|
|
||||||
|
const spec = { Deadline: 123456789, IgnoreSystemJobs: false };
|
||||||
|
await this.subject().forceDrain(node, spec);
|
||||||
|
|
||||||
|
const request = pretender.handledRequests.lastObject;
|
||||||
|
assert.equal(request.url, `/v1/node/${node.id}/drain`, 'Request was made to /:node_id/drain');
|
||||||
|
assert.equal(request.method, 'POST', 'Request was made with the POST method');
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(request.requestBody),
|
||||||
|
{
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: {
|
||||||
|
Deadline: -1,
|
||||||
|
IgnoreSystemJobs: spec.IgnoreSystemJobs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'POST request is made with the drain spec, except deadline is not overridden'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cancelDrain makes the correct POST request to /:node_id/drain', async function(assert) {
|
||||||
|
const { pretender } = this.server;
|
||||||
|
const node = await run(() => this.store.findRecord('node', 'node-1'));
|
||||||
|
|
||||||
|
await this.subject().cancelDrain(node);
|
||||||
|
|
||||||
|
const request = pretender.handledRequests.lastObject;
|
||||||
|
assert.equal(request.url, `/v1/node/${node.id}/drain`, 'Request was made to /:node_id/drain');
|
||||||
|
assert.equal(request.method, 'POST', 'Request was made with the POST method');
|
||||||
|
assert.deepEqual(
|
||||||
|
JSON.parse(request.requestBody),
|
||||||
|
{
|
||||||
|
NodeID: node.id,
|
||||||
|
DrainSpec: null,
|
||||||
|
},
|
||||||
|
'POST request is made with a null drain spec'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Using fetchLink on a model's hasMany relationship exercises the adapter's
|
// Using fetchLink on a model's hasMany relationship exercises the adapter's
|
||||||
|
|
|
@ -6375,6 +6375,11 @@ duplexify@^3.4.2, duplexify@^3.6.0:
|
||||||
readable-stream "^2.0.0"
|
readable-stream "^2.0.0"
|
||||||
stream-shift "^1.0.0"
|
stream-shift "^1.0.0"
|
||||||
|
|
||||||
|
duration-js@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/duration-js/-/duration-js-4.0.0.tgz#ab91575a4f1a6b096034685cfc6ea9aca99cd63f"
|
||||||
|
integrity sha1-q5FXWk8aawlgNGhc/G6prKmc1j8=
|
||||||
|
|
||||||
ecc-jsbn@~0.1.1:
|
ecc-jsbn@~0.1.1:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
|
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
|
||||||
|
|
Loading…
Reference in New Issue