Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

UI: Allocation lifecycle #5734

Merged
merged 11 commits into from
May 21, 2019
Merged
20 changes: 19 additions & 1 deletion ui/app/adapters/allocation.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
import Watchable from './watchable';
import addToPath from 'nomad-ui/utils/add-to-path';

export default Watchable.extend();
export default Watchable.extend({
stop: adapterAction('/stop'),

restart(allocation, taskName) {
const prefix = `${this.host || '/'}${this.urlPrefix()}`;
const url = `${prefix}/client/allocation/${allocation.id}/restart`;
return this.ajax(url, 'PUT', {
data: taskName && { TaskName: taskName },
});
},
});

function adapterAction(path, verb = 'POST') {
return function(allocation) {
const url = addToPath(this.urlForFindRecord(allocation.id, 'allocation'), path);
return this.ajax(url, verb);
};
}
12 changes: 1 addition & 11 deletions ui/app/adapters/job.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { inject as service } from '@ember/service';
import Watchable from './watchable';
import addToPath from 'nomad-ui/utils/add-to-path';

export default Watchable.extend({
system: service(),
Expand Down Expand Up @@ -118,14 +119,3 @@ function associateNamespace(url, namespace) {
}
return url;
}

function addToPath(url, extension = '') {
const [path, params] = url.split('?');
let newUrl = `${path}${extension}`;

if (params) {
newUrl += `?${params}`;
}

return newUrl;
}
16 changes: 16 additions & 0 deletions ui/app/components/two-step-button.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import Component from '@ember/component';
import { next } from '@ember/runloop';
import { equal } from '@ember/object/computed';
import { task, waitForEvent } from 'ember-concurrency';
import RSVP from 'rsvp';

export default Component.extend({
Expand All @@ -10,19 +12,33 @@ export default Component.extend({
confirmText: '',
confirmationMessage: '',
awaitingConfirmation: false,
disabled: false,
onConfirm() {},
onCancel() {},

state: 'idle',
isIdle: equal('state', 'idle'),
isPendingConfirmation: equal('state', 'prompt'),

cancelOnClickOutside: task(function*() {
while (true) {
let ev = yield waitForEvent(document.body, 'click');
if (!this.element.contains(ev.target) && !this.awaitingConfirmation) {
this.send('setToIdle');
}
}
}),

actions: {
setToIdle() {
this.set('state', 'idle');
this.cancelOnClickOutside.cancelAll();
},
promptForConfirmation() {
this.set('state', 'prompt');
next(() => {
this.cancelOnClickOutside.perform();
});
},
confirm() {
RSVP.resolve(this.onConfirm()).then(() => {
Expand Down
47 changes: 47 additions & 0 deletions ui/app/controllers/allocations/allocation/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { computed, observer } from '@ember/object';
import { alias } from '@ember/object/computed';
import { task } from 'ember-concurrency';
import Sortable from 'nomad-ui/mixins/sortable';
import { lazyClick } from 'nomad-ui/helpers/lazy-click';
import { watchRecord } from 'nomad-ui/utils/properties/watch';

export default Controller.extend(Sortable, {
token: service(),
Expand All @@ -21,6 +24,50 @@ export default Controller.extend(Sortable, {
// Set in the route
preempter: null,

error: computed(() => {
// { title, description }
return null;
}),

onDismiss() {
this.set('error', null);
},

watchNext: watchRecord('allocation'),

observeWatchNext: observer('model.nextAllocation.clientStatus', function() {
const nextAllocation = this.model.nextAllocation;
if (nextAllocation && nextAllocation.content) {
this.watchNext.perform(nextAllocation);
} else {
this.watchNext.cancelAll();
}
}),

stopAllocation: task(function*() {
try {
yield this.model.stop();
// Eagerly update the allocation clientStatus to avoid flickering
this.model.set('clientStatus', 'complete');
} catch (err) {
this.set('error', {
title: 'Could Not Stop Allocation',
description: 'Your ACL token does not grant allocation lifecycle permissions.',
});
}
}),

restartAllocation: task(function*() {
try {
yield this.model.restart();
} catch (err) {
this.set('error', {
title: 'Could Not Restart Allocation',
description: 'Your ACL token does not grant allocation lifecycle permissions.',
});
}
}),

actions: {
gotoTask(allocation, task) {
this.transitionToRoute('allocations.allocation.task', task);
Expand Down
21 changes: 21 additions & 0 deletions ui/app/controllers/allocations/allocation/task/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { task } from 'ember-concurrency';

export default Controller.extend({
network: alias('model.resources.networks.firstObject'),
Expand All @@ -20,4 +21,24 @@ export default Controller.extend({
)
.sortBy('name');
}),

error: computed(() => {
// { title, description }
return null;
}),

onDismiss() {
this.set('error', null);
},

restartTask: task(function*() {
try {
yield this.model.restart();
} catch (err) {
this.set('error', {
title: 'Could Not Restart Task',
description: 'Your ACL token does not grant allocation lifecycle permissions.',
});
}
}),
});
7 changes: 5 additions & 2 deletions ui/app/mixins/with-watchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ export default Mixin.create(WithVisibilityDetection, {
},

actions: {
willTransition() {
this.cancelAllWatchers();
willTransition(transition) {
// Don't cancel watchers if transitioning into a sub-route
if (!transition.intent.name || !transition.intent.name.startsWith(this.routeName)) {
this.cancelAllWatchers();
}

// Bubble the action up to the application route
return true;
Expand Down
8 changes: 8 additions & 0 deletions ui/app/models/allocation.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,12 @@ export default Model.extend({
);
}
),

stop() {
return this.store.adapterFor('allocation').stop(this);
},

restart(taskName) {
return this.store.adapterFor('allocation').restart(this, taskName);
},
});
4 changes: 4 additions & 0 deletions ui/app/models/task-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ export default Fragment.extend({

return classMap[this.state] || 'is-dark';
}),

restart() {
return this.allocation.restart(this.name);
},
});
6 changes: 6 additions & 0 deletions ui/app/routes/allocations/allocation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ export default Route.extend({

return this._super(...arguments);
},

resetController(controller, isExiting) {
if (isExiting) {
controller.watchNext.cancelAll();
}
},
});
4 changes: 4 additions & 0 deletions ui/app/styles/core/title.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@
&.is-6 {
margin-bottom: 0.5rem;
}

&.with-headroom {
margin-top: 1rem;
}
}
36 changes: 35 additions & 1 deletion ui/app/templates/allocations/allocation/index.hbs
Original file line number Diff line number Diff line change
@@ -1,8 +1,42 @@
<section class="section">
<h1 data-test-title class="title">
{{#if error}}
<div data-test-inline-error class="notification is-danger">
<div class="columns">
<div class="column">
<h3 data-test-inline-error-title class="title is-4">{{error.title}}</h3>
<p data-test-inline-error-body>{{error.description}}</p>
</div>
<div class="column is-centered is-minimum">
<button data-test-inline-error-close class="button is-danger" onclick={{action onDismiss}}>Okay</button>
</div>
</div>
</div>
{{/if}}

<h1 data-test-title class="title with-headroom">
Allocation {{model.name}}
<span class="bumper-left tag {{model.statusClass}}">{{model.clientStatus}}</span>
<span class="tag is-hollow is-small no-text-transform">{{model.id}}</span>
{{#if model.isRunning}}
{{two-step-button
data-test-stop
idleText="Stop"
cancelText="Cancel"
confirmText="Yes, Stop"
confirmationMessage="Are you sure? This will reschedule the allocation on a different client."
awaitingConfirmation=stopAllocation.isRunning
disabled=(or stopAllocation.isRunning restartAllocation.isRunning)
onConfirm=(perform stopAllocation)}}
{{two-step-button
data-test-restart
idleText="Restart"
cancelText="Cancel"
confirmText="Yes, Restart"
confirmationMessage="Are you sure? This will restart the allocation in-place."
awaitingConfirmation=restartAllocation.isRunning
disabled=(or stopAllocation.isRunning restartAllocation.isRunning)
onConfirm=(perform restartAllocation)}}
{{/if}}
</h1>

<div class="boxed-section is-small">
Expand Down
25 changes: 25 additions & 0 deletions ui/app/templates/allocations/allocation/task/index.hbs
Original file line number Diff line number Diff line change
@@ -1,8 +1,33 @@
{{partial "allocations/allocation/task/subnav"}}
<section class="section">
{{#if error}}
<div data-test-inline-error class="notification is-danger">
<div class="columns">
<div class="column">
<h3 data-test-inline-error-title class="title is-4">{{error.title}}</h3>
<p data-test-inline-error-body>{{error.description}}</p>
</div>
<div class="column is-centered is-minimum">
<button data-test-inline-error-close class="button is-danger" onclick={{action onDismiss}}>Okay</button>
</div>
</div>
</div>
{{/if}}

<h1 class="title" data-test-title>
{{model.name}}
<span class="bumper-left tag {{model.stateClass}}" data-test-state>{{model.state}}</span>
{{#if model.isRunning}}
{{two-step-button
data-test-restart
idleText="Restart"
cancelText="Cancel"
confirmText="Yes, Restart"
confirmationMessage="Are you sure? This will restart the task in-place."
awaitingConfirmation=restartTask.isRunning
disabled=restartTask.isRunning
onConfirm=(perform restartTask)}}
{{/if}}
</h1>

<div class="boxed-section is-small">
Expand Down
7 changes: 6 additions & 1 deletion ui/app/templates/components/two-step-button.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
{{#if isIdle}}
<button data-test-idle-button type="button" class="button is-danger is-outlined is-important is-small" onclick={{action "promptForConfirmation"}}>
<button
data-test-idle-button
type="button"
class="button is-danger is-outlined is-important is-small"
disabled={{disabled}}
onclick={{action "promptForConfirmation"}}>
{{idleText}}
</button>
{{else if isPendingConfirmation}}
Expand Down
11 changes: 11 additions & 0 deletions ui/app/utils/add-to-path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Adds a string to the end of a URL path while being mindful of query params
export default function addToPath(url, extension = '') {
const [path, params] = url.split('?');
let newUrl = `${path}${extension}`;

if (params) {
newUrl += `?${params}`;
}

return newUrl;
}
8 changes: 8 additions & 0 deletions ui/mirage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ export default function() {

this.get('/allocation/:id');

this.post('/allocation/:id/stop', function() {
return new Response(204, {}, '');
});

this.get('/namespaces', function({ namespaces }) {
const records = namespaces.all();

Expand Down Expand Up @@ -301,6 +305,10 @@ export default function() {
};

// Client requests are available on the server and the client
this.put('/client/allocation/:id/restart', function() {
return new Response(204, {}, '');
});

this.get('/client/allocation/:id/stats', clientAllocationStatsHandler);
this.get('/client/fs/logs/:allocation_id', clientAllocationLog);

Expand Down
Loading