Skip to content

Commit

Permalink
Add job version revert buttons (#10336)
Browse files Browse the repository at this point in the history
This adds a Revert two-step button to the JobVersions component for
not-current versions, which redirects to the overview on success. It
checks the job version before and after reversion to mitigate the edge
case where reverting to an otherwise-identical version has no effect, as
discussed in #10337.

It uses existing facilities for handling other errors and disabling the
button when permissions are lacking.
  • Loading branch information
backspace authored Apr 20, 2021
1 parent 0f4027d commit d0f4750
Show file tree
Hide file tree
Showing 16 changed files with 303 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ IMPROVEMENTS:
* networking: Added support for user-defined iptables rules on the NOMAD-ADMIN chain. [[GH-10181](https://github.com/hashicorp/nomad/issues/10181)]
* networking: Added support for interpolating host network names with node attributes. [[GH-10196](https://github.com/hashicorp/nomad/issues/10196)]
* nomad/structs: Removed deprecated Node.Drain field, added API extensions to restore it [[GH-10202](https://github.com/hashicorp/nomad/issues/10202)]
* ui: Added a job reversion button [[GH-10336](https://github.com/hashicorp/nomad/pull/10336)]

BUG FIXES:
* core (Enterprise): Update licensing library to v0.0.11 to include race condition fix. [[GH-10253](https://github.com/hashicorp/nomad/issues/10253)]
Expand Down
18 changes: 18 additions & 0 deletions ui/app/adapters/job-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import ApplicationAdapter from './application';
import addToPath from 'nomad-ui/utils/add-to-path';

export default class JobVersionAdapter extends ApplicationAdapter {
revertTo(jobVersion) {
const jobAdapter = this.store.adapterFor('job');

const url = addToPath(jobAdapter.urlForFindRecord(jobVersion.get('job.id'), 'job'), '/revert');
const [jobName] = JSON.parse(jobVersion.get('job.id'));

return this.ajax(url, 'POST', {
data: {
JobID: jobName,
JobVersion: jobVersion.number,
},
});
}
}
42 changes: 42 additions & 0 deletions ui/app/components/job-version.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Component from '@ember/component';
import { action, computed } from '@ember/object';
import { classNames } from '@ember-decorators/component';
import { inject as service } from '@ember/service';
import { task } from 'ember-concurrency';
import messageForError from 'nomad-ui/utils/message-from-adapter-error';
import classic from 'ember-classic-decorator';

const changeTypes = ['Added', 'Deleted', 'Edited'];
Expand All @@ -14,6 +17,8 @@ export default class JobVersion extends Component {
// Passes through to the job-diff component
verbose = true;

@service router;

@computed('version.diff')
get changeCount() {
const diff = this.get('version.diff');
Expand All @@ -30,10 +35,47 @@ export default class JobVersion extends Component {
);
}

@computed('version.{number,job.version}')
get isCurrent() {
return this.get('version.number') === this.get('version.job.version');
}

@action
toggleDiff() {
this.toggleProperty('isOpen');
}

@task(function*() {
try {
const versionBeforeReversion = this.get('version.job.version');

yield this.version.revertTo();
yield this.version.job.reload();

const versionAfterReversion = this.get('version.job.version');

if (versionBeforeReversion === versionAfterReversion) {
this.handleError({
level: 'warn',
title: 'Reversion Had No Effect',
description: 'Reverting to an identical older version doesn’t produce a new version',
});
} else {
const job = this.get('version.job');

this.router.transitionTo('jobs.job', job.get('plainId'), {
queryParams: { namespace: job.get('namespace.name') },
});
}
} catch (e) {
this.handleError({
level: 'danger',
title: 'Could Not Revert',
description: messageForError(e, 'revert'),
});
}
})
revertTo;
}

const flatten = (accumulator, array) => accumulator.concat(array);
Expand Down
2 changes: 1 addition & 1 deletion ui/app/components/two-step-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import classic from 'ember-classic-decorator';

@classic
@classNames('two-step-button')
@classNameBindings('inlineText:has-inline-text')
@classNameBindings('inlineText:has-inline-text', 'fadingBackground:has-fading-background')
export default class TwoStepButton extends Component {
idleText = '';
cancelText = '';
Expand Down
24 changes: 24 additions & 0 deletions ui/app/controllers/jobs/job/versions.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
import Controller from '@ember/controller';
import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
import { alias } from '@ember/object/computed';
import { action, computed } from '@ember/object';
import classic from 'ember-classic-decorator';

const alertClassFallback = 'is-info';

const errorLevelToAlertClass = {
danger: 'is-danger',
warn: 'is-warning',
};

@classic
export default class VersionsController extends Controller.extend(WithNamespaceResetting) {
error = null;

@alias('model') job;

@computed('error.level')
get errorLevelClass() {
return errorLevelToAlertClass[this.get('error.level')] || alertClassFallback;
}

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

@action
handleError(errorObject) {
this.set('error', errorObject);
}
}
4 changes: 4 additions & 0 deletions ui/app/models/job-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ export default class JobVersion extends Model {
@attr('date') submitTime;
@attr('number') number;
@attr() diff;

revertTo() {
return this.store.adapterFor('job-version').revertTo(this);
}
}
9 changes: 6 additions & 3 deletions ui/app/routes/jobs/job/versions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Route from '@ember/routing/route';
import { collect } from '@ember/object/computed';
import { watchRelationship } from 'nomad-ui/utils/properties/watch';
import { watchRecord, watchRelationship } from 'nomad-ui/utils/properties/watch';
import WithWatchers from 'nomad-ui/mixins/with-watchers';

export default class VersionsRoute extends Route.extend(WithWatchers) {
Expand All @@ -11,10 +11,13 @@ export default class VersionsRoute extends Route.extend(WithWatchers) {

startWatchers(controller, model) {
if (model) {
controller.set('watcher', this.watchVersions.perform(model));
controller.set('watcher', this.watch.perform(model));
controller.set('watchVersions', this.watchVersions.perform(model));
}
}

@watchRecord('job') watch;
@watchRelationship('versions') watchVersions;
@collect('watchVersions') watchers;

@collect('watch', 'watchVersions') watchers;
}
5 changes: 5 additions & 0 deletions ui/app/styles/components/boxed-section.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
margin-left: auto;
}

.is-fixed-width {
display: inline-block;
width: 8em;
}

&.is-compact {
padding: 0.75em;
}
Expand Down
12 changes: 10 additions & 2 deletions ui/app/styles/components/two-step-button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@
}

.confirmation-text {
position: static;
margin-right: 0.5ch;
position: absolute;
left: auto;
right: 0;
top: auto;
margin-right: 100%;
}
}

&.has-fading-background .confirmation-text {
padding: 0.5rem 0 0.5rem 4rem;
background: linear-gradient(to left, white, white 90%, transparent 100%);
}

.confirmation-text {
position: absolute;
left: 0;
Expand Down
41 changes: 36 additions & 5 deletions ui/app/templates/components/job-version.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,42 @@
<span class="term">Submitted</span>
<span data-test-version-submit-time class="submit-date">{{format-ts this.version.submitTime}}</span>
</span>
{{#if this.version.diff}}
<button class="button is-light is-compact pull-right" {{action "toggleDiff"}} type="button">{{this.changeCount}} {{pluralize "Change" this.changeCount}}</button>
{{else}}
<span class="pull-right">No Changes</span>
{{/if}}
<div class="pull-right">
{{#unless this.isCurrent}}
{{#if (can "run job")}}
<TwoStepButton
data-test-revert-to
@classes={{hash
idleButton="is-warning is-outlined"
confirmButton="is-warning"}}
@alignRight={{true}}
@idleText="Revert"
@cancelText="Cancel"
@confirmText="Yes, Revert"
@confirmationMessage="Are you sure you want to revert to this version?"
@inlineText={{true}}
@fadingBackground={{true}}
@awaitingConfirmation={{this.revertTo.isRunning}}
@onConfirm={{perform this.revertTo}} />
{{else}}
<button
data-test-revert-to
type="button"
class="button is-warning is-outlined is-small tooltip"
disabled
aria-label="You don’t have permission to revert"
>
Revert
</button>
{{/if}}
{{/unless}}

{{#if this.version.diff}}
<button class="button is-light is-small is-fixed-width" {{action "toggleDiff"}} type="button">{{this.changeCount}} {{pluralize "Change" this.changeCount}}</button>
{{else}}
<div class="is-fixed-width is-size-7 has-text-centered">No Changes</div>
{{/if}}
</div>
</div>
{{#if this.isOpen}}
<div class="boxed-section-body is-dark">
Expand Down
2 changes: 1 addition & 1 deletion ui/app/templates/components/job-versions-stream.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
</li>
{{/if}}
<li data-test-version class="timeline-object">
<JobVersion @version={{record.version}} @verbose={{this.verbose}} />
<JobVersion @version={{record.version}} @verbose={{this.verbose}} @handleError={{@handleError}} />
</li>
{{/each}}
16 changes: 15 additions & 1 deletion ui/app/templates/jobs/job/versions.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
{{page-title "Job " this.job.name " versions"}}
<JobSubnav @job={{this.job}} />
<section class="section">
<JobVersionsStream @versions={{this.model.versions}} @verbose={{true}} />
{{#if this.error}}
<div data-test-inline-error class="notification {{this.errorLevelClass}}">
<div class="columns">
<div class="column">
<h3 data-test-inline-error-title class="title is-4">{{this.error.title}}</h3>
<p data-test-inline-error-body>{{this.error.description}}</p>
</div>
<div class="column is-centered is-minimum">
<button data-test-inline-error-close class="button {{this.errorLevelClass}}" onclick={{action this.onDismiss}} type="button">Okay</button>
</div>
</div>
</div>
{{/if}}

<JobVersionsStream @versions={{this.model.versions}} @verbose={{true}} @handleError={{action this.handleError}} />
</section>
9 changes: 9 additions & 0 deletions ui/mirage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ export default function() {
return okEmpty();
});

this.post('/job/:id/revert', function({ jobs }, { requestBody }) {
const { JobID, JobVersion } = JSON.parse(requestBody);
const job = jobs.find(JobID);
job.version = JobVersion;
job.save();

return okEmpty();
});

this.post('/job/:id/scale', function({ jobs }, { params }) {
return this.serialize(jobs.find(params.id));
});
Expand Down
Loading

0 comments on commit d0f4750

Please sign in to comment.