Skip to content

Commit

Permalink
Merge pull request #5734 from hashicorp/f-ui/allocation-lifecycle
Browse files Browse the repository at this point in the history
UI: Allocation lifecycle
  • Loading branch information
DingoEatingFuzz committed May 21, 2019
2 parents bedf483 + 2002836 commit 121927f
Show file tree
Hide file tree
Showing 23 changed files with 543 additions and 50 deletions.
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

0 comments on commit 121927f

Please sign in to comment.