Factor out the drain popover and implement its behaviors

This commit is contained in:
Michael Lange
2019-11-11 17:05:47 -08:00
parent d401a11d0c
commit e47d255b07
4 changed files with 239 additions and 34 deletions

View File

@@ -0,0 +1,161 @@
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';
const durationDecode = str => {
const durationUnits = ['d', 'h', 'm', 's'];
const unitToMs = {
s: 1000,
m: 1000 * 60,
h: 1000 * 60 * 60,
d: 1000 * 60 * 60 * 24,
};
if (typeof str === 'number') return str;
// Split the string into characters to make iteration easier
const chars = str.split('');
// Collect tokens
const tokens = [];
// A token can be multi-character, so collect characters
let token = [];
// If a non-numeric character follows a non-numeric character, that's a
// parse error, so this marker bool is needed
let disallowChar = false;
// Take the first character off the chars array until there are no more
while (chars.length) {
let next = chars.shift();
// Check to see if the char is numeric
if (next >= 0 && next < 10) {
// Collect numeric characters until a non-numeric shows up
token.push(next);
// Release the double non-numeric mark
disallowChar = false;
} else {
if (disallowChar) {
throw new Error(
`ParseError: [${str}] Cannot follow a non-numeric token with a non-numeric token`
);
}
if (!durationUnits.includes(next)) {
throw new Error(`ParseError: [${str}] Unallowed duration unit "${next}"`);
}
// The token array now becomes a single int token
tokens.push(parseInt(token.join('')));
// This non-numeric char is its own token
tokens.push(next);
// Reset token array
token = [];
// Set the double non-numeric mark
disallowChar = true;
}
}
// If there are numeric characters still in the token array, then there must have
// not been a followup non-numeric character which would have flushed the numeric tokens.
if (token.length) {
throw new Error(`ParseError: [${str}] Unmatched quantities and units`);
}
// Loop over the tokens array, two at a time, converting unit and quanties into milliseconds
let duration = 0;
while (tokens.length) {
const quantity = tokens.shift();
const unit = tokens.shift();
duration += quantity * unitToMs[unit];
}
// Convert from Milliseconds to Nanoseconds
duration *= 1000000;
console.log('DURATION', duration);
return duration;
};
export default Component.extend({
tagName: '',
client: null,
onError() {},
onDrain() {},
parseError: '',
deadlineEnabled: false,
forceDrain: false,
drainSystemJobs: true,
selectedDurationQuickOption: overridable(function() {
return this.durationQuickOptions.findBy('value', '4h');
}),
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;
let deadline;
try {
deadline = durationDecode(this.deadline);
} catch (err) {
this.set('parseError', err.message);
return;
}
const spec = {
Deadline: deadline,
IgnoreSystemJobs: !this.drainSystemJobs,
};
console.log('Draining:', spec);
close();
try {
if (this.forceDrain) {
yield this.client.forceDrain(spec);
} else {
yield this.client.drain(spec);
}
this.onDrain();
} catch (err) {
this.onError(err);
}
}),
preventDefault(e) {
e.preventDefault();
},
});

View File

@@ -41,6 +41,10 @@
display: inline-block;
}
&.is-sub-field {
margin-left: 2em;
}
&:not(:last-child) {
margin-bottom: 1rem;
}

View File

@@ -17,7 +17,7 @@
{{or model.name model.shortId}}
</h1>
<p>
<label>
<label class="is-interactive">
<input
type="checkbox"
checked={{model.isEligible}}
@@ -35,39 +35,7 @@
</p>
</div>
<div class="toolbar-item is-right-aligned is-top-aligned">
{{#popover-menu label="Drain"}}
<form class="form is-small">
<h4 class="group-heading">Drain Options</h4>
<div class="field">
<label class="label">
<input type="checkbox"> Deadline
<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>
<div class="field">
<label class="label">
<input type="checkbox"> Force Drain
<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">
<input type="checkbox"> Drain System Jobs
<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 class="popover-action is-primary">Drain</button>
<button class="popover-action">Cancel</button>
</div>
</form>
{{/popover-menu}}
{{drain-popover client=model}}
</div>
</div>

View File

@@ -0,0 +1,72 @@
{{#popover-menu label="Drain" triggerClass=(if drain.isRunning "is-loading") as |m|}}
<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">
<input
type="checkbox"
checked={{deadlineEnabled}}
onchange={{action (mut deadlineEnabled) value="target.checked"}}> Deadline
<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
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
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">
<input
type="checkbox"
checked={{forceDrain}}
onchange={{action (mut forceDrain) value="target.checked"}}> Force Drain
<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">
<input
type="checkbox"
checked={{drainSystemJobs}}
onchange={{action (mut drainSystemJobs) value="target.checked"}}> Drain System Jobs
<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
type="button"
class="popover-action is-primary"
onclick={{perform drain m.actions.close}}>
Drain
</button>
<button type="button" class="popover-action" onclick={{action m.actions.close}}>Cancel</button>
</div>
</form>
{{/popover-menu}}