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

Add flash messages when viewed model is deleted #5759

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3cb5138
Add ember-cli-flash with hardcoded template use
backspace May 24, 2019
59eed85
Add ability for watchRecord to report 404s
backspace May 24, 2019
cdfaded
Add close button to flash message
backspace May 24, 2019
e09966e
Add notification for allocation-stopping
backspace May 24, 2019
88a9619
Add notifications for client
backspace May 24, 2019
f34ee8e
Add test for job-deletion warning
backspace May 27, 2019
acc76e0
Restore mistakenly-deleted file
backspace May 27, 2019
b0b7b1d
Fix test
backspace May 27, 2019
0c41e2d
Change timeout
backspace May 27, 2019
4e654c2
Change flash title to be dynamic
backspace May 28, 2019
d428450
Convert page object to use test selectors
backspace May 28, 2019
0a98005
Add test for closing flash message
backspace May 28, 2019
490f793
Add assertion for icon class
backspace May 28, 2019
70e3073
Add test for allocation 404ing on server
backspace May 28, 2019
ffe892a
Add service to mixin to avoid awkward lookup
backspace Jun 3, 2019
26be441
Change to always report 404s by default
backspace Jun 3, 2019
becb8db
Convert watch perform calls to have options object
backspace Jun 3, 2019
598a7ca
Add flag to force watching in tests
backspace Jun 4, 2019
a65b1b0
Convert flash tests to use settled
backspace Jun 4, 2019
a2117d5
Remove superfluous module
backspace Jun 4, 2019
c7f2b0f
Add auto-clear for messages when leaving route
backspace Jun 4, 2019
b02b159
Merge branch 'master' into f-ui/blocking-query-404s-flash
backspace Jun 19, 2019
290482a
Replace error-determination with existing util
backspace Jun 19, 2019
ec437ec
Remove traces of watchInTesting
backspace Jun 20, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ui/app/components/client-node-row.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default Component.extend(WithVisibilityDetection, {
const node = this.node;
if (node) {
node.reload().then(() => {
this.watch.perform(node, 100);
this.watch.perform(node, { throttle: 100 });
});
}
},
Expand All @@ -34,7 +34,7 @@ export default Component.extend(WithVisibilityDetection, {
} else {
const node = this.node;
if (node) {
this.watch.perform(node, 100);
this.watch.perform(node, { throttle: 100 });
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion ui/app/controllers/allocations/allocation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default Controller.extend(Sortable, {
this.set('error', null);
},

watchNext: watchRecord('allocation'),
watchNext: watchRecord('allocation', { ignore404: true }),

observeWatchNext: observer('model.nextAllocation.clientStatus', function() {
const nextAllocation = this.model.nextAllocation;
Expand Down
3 changes: 3 additions & 0 deletions ui/app/mixins/with-watchers.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import Mixin from '@ember/object/mixin';
import { computed } from '@ember/object';
import { assert } from '@ember/debug';
import { inject as service } from '@ember/service';
import WithVisibilityDetection from './with-route-visibility-detection';

export default Mixin.create(WithVisibilityDetection, {
flashMessages: service(),

watchers: computed(() => []),

cancelAllWatchers() {
Expand Down
2 changes: 1 addition & 1 deletion ui/app/routes/jobs/job/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default Route.extend(WithWatchers, {

watch: watchRecord('job'),
watchAll: watchAll('job'),
watchSummary: watchRecord('job-summary'),
watchSummary: watchRecord('job-summary', { ignore404: true }),
watchAllocations: watchRelationship('allocations'),
watchEvaluations: watchRelationship('evaluations'),
watchLatestDeployment: watchRelationship('latestDeployment'),
Expand Down
4 changes: 2 additions & 2 deletions ui/app/routes/jobs/job/task-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ export default Route.extend(WithWatchers, {
}
},

watchJob: watchRecord('job'),
watchSummary: watchRecord('job-summary'),
watchJob: watchRecord('job', { ignore404: true }),
watchSummary: watchRecord('job-summary', { ignore404: true }),
watchAllocations: watchRelationship('allocations'),

watchers: collect('watchJob', 'watchSummary', 'watchAllocations'),
Expand Down
2 changes: 2 additions & 0 deletions ui/app/services/watch-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ export default Service.extend({
setIndexFor(url, value) {
list[url] = +value;
},

watchInTesting: false,
});
1 change: 1 addition & 0 deletions ui/app/styles/components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
@import './components/ember-power-select';
@import './components/empty-message';
@import './components/error-container';
@import './components/global-flash';
@import './components/gutter';
@import './components/gutter-toggle';
@import './components/inline-definitions';
Expand Down
76 changes: 76 additions & 0 deletions ui/app/styles/components/global-flash.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
.global-flash {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are existing notification styles that at the very least the flash notifications should look like. Ideally they would use the same styles via some sass pattern or composition.

bottom: 0;
left: 12px;
margin: 10px;
max-width: 300px;
position: fixed;
width: 95%;
z-index: 300;

.message {
box-shadow: 0 12px 5px -7px rgba($black, 0.08), 0 11px 10px -3px rgba($black, 0.1);
}

.message {
border: 1px solid;
margin-bottom: 12px;
padding: 12px 16px 16px 12px;
position: relative;

.close-button {
background: transparent;
border: 0;
color: $grey;
cursor: pointer;
position: absolute;
right: 12px;
top: 16px;

.icon {
width: 9px;
height: 9px;
}
}

.message-title {
font-size: 16px;
font-weight: bold;
line-height: 1.25;
}

.close-button + .message-title {
padding-right: 16px;
}

.message-body {
border: 0;
margin-top: 4px;
}

p {
font-size: $size-7;
border: 0;
padding: 0;
}

.message-actions {
margin-top: 8px;

a,
a:not(.button):not(.file-delete-button):not(.tag) {
color: $blue;
font-weight: 600;
text-decoration: none;
}

> * + * {
margin-left: 8px;
}
}

&.is-highlight {
background: #fffdf6;
border: 1px solid #fdeeba;
}
}
}
26 changes: 26 additions & 0 deletions ui/app/templates/application.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,30 @@
{{partial "svg-patterns"}}
<div class='global-flash'>
{{#each flashMessages.queue as |flash|}}
{{#flash-message flash=flash data-test-flash-message=true}}
<div class="message is-highlight">
<div class="columns is-mobile is-variable is-1">
<div class="column is-narrow message-icon">
{{x-icon "warning" class="is-warning"}}
</div>
<div class="column">
<button type="button" class="close-button" data-test-flash-message-close>
{{x-icon "cancel"}}
</button>
<div class="message-title" data-test-flash-message-title>
{{titleize flash.type}}
</div>
{{#if flash.message}}
<p class="message-body" data-test-flash-message-body>
{{flash.message}}
</p>
{{/if}}
</div>
</div>
</div>
{{/flash-message}}
{{/each}}
</div>
{{#unless error}}
{{outlet}}
{{else}}
Expand Down
19 changes: 15 additions & 4 deletions ui/app/utils/properties/watch.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Ember from 'ember';
import { get } from '@ember/object';
import { get, getWithDefault } from '@ember/object';
import RSVP from 'rsvp';
import { task } from 'ember-concurrency';
import wait from 'nomad-ui/utils/wait';
Expand All @@ -8,13 +8,14 @@ import config from 'nomad-ui/config/environment';

const isEnabled = config.APP.blockingQueries !== false;

export function watchRecord(modelName) {
return task(function*(id, throttle = 2000) {
export function watchRecord(modelName, { ignore404 } = {}) {
return task(function*(id, { throttle = 2000, runInTests = false } = {}) {
const token = new XHRToken();
if (typeof id === 'object') {
id = get(id, 'id');
}
while (isEnabled && !Ember.testing) {

while (isEnabled && (!Ember.testing || runInTests)) {
try {
yield RSVP.all([
this.store.findRecord(modelName, id, {
Expand All @@ -24,6 +25,16 @@ export function watchRecord(modelName) {
wait(throttle),
]);
} catch (e) {
if (!ignore404) {
const statusIs404 = getWithDefault(e, 'errors', [])
.mapBy('status')
.includes('404');
const errorIsEmptyResponse = e.message.includes('response did not have any data');
if (statusIs404 || errorIsEmptyResponse) {
this.flashMessages.warning(`This ${modelName} no longer exists`, { sticky: true });
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recall talking about pushing this logic into the with-watchers by making these tasks evented.

Did that not pan out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that out in this branch, what do you think? There’s some awkwardness involving bundling the modelName along with the error because otherwise the error handler doesn’t know what to put in the flash message. Another approach would be to use the args on the task, as the model is passed in to perform, but given that the signature is id vs model, it seems questionable.

I also tried making it an encapsulated task so it could contain modelName for future reference, but that seemed to require passing in the Ember Data store because this would therefore be the task instance and not the containing model 😞

One significant problem is that the tests no longer pass:

image

That could be partially addressed by ignoring the uncaught exception, hopefully in a more targeted way to avoid how that burned me in the past.

BUT it’s not just that; the assertion that a flash message is displayed is also failing, despite me being able to see that it’s there 🤔 (that’s the inscrutable failed error 8 which I opened this PR to improve so we don’t need to write custom assertion failure messages)

I can hopefully address these problems but I wanted to check in on what you thought of the modelName weirdness and the general approach before sinking more time into it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could perhaps be revived as it’s not a huge change and would still be nice to have, maybe the above weirdness is a non-problem now 🤞


yield e;
break;
} finally {
Expand Down
3 changes: 3 additions & 0 deletions ui/config/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = function(environment) {
mirageWithNamespaces: false,
mirageWithTokens: true,
mirageWithRegions: true,
watchThrottle: 2000,
},
};

Expand Down Expand Up @@ -53,6 +54,8 @@ module.exports = function(environment) {
ENV.APP.rootElement = '#ember-testing';
ENV.APP.autoboot = false;

ENV.APP.watchThrottle = 0;

ENV.browserify = {
tests: true,
};
Expand Down
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"ember-cli-dependency-checker": "^3.0.0",
"ember-cli-deprecation-workflow": "^1.0.1",
"ember-cli-eslint": "^5.1.0",
"ember-cli-flash": "^1.7.2",
"ember-cli-funnel": "^0.6.1",
"ember-cli-htmlbars": "^3.0.0",
"ember-cli-htmlbars-inline-precompile": "^1.0.3",
Expand Down
39 changes: 38 additions & 1 deletion ui/tests/acceptance/allocation-detail-test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { run } from '@ember/runloop';
import { currentURL } from '@ember/test-helpers';
import { currentURL, settled } from '@ember/test-helpers';
import { assign } from '@ember/polyfills';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import Allocation from 'nomad-ui/tests/pages/allocations/detail';
import PageLayout from 'nomad-ui/tests/pages/layout';
import moment from 'moment';

let job;
Expand Down Expand Up @@ -215,6 +216,42 @@ module('Acceptance | allocation detail', function(hooks) {
});
});

// This is a separate module only because of the watchInTesting adjustment, hopefully that can change
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is watchInTesting used for? It doesn't seem wired up to anything.

module('Acceptance | allocation detail after stopping', function(hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);

hooks.beforeEach(async function() {
server.create('agent');

node = server.create('node');
job = server.create('job', { groupsCount: 1, createAllocations: false });
allocation = server.create('allocation', 'withTaskWithPorts', { clientStatus: 'running' });

this.owner.lookup('service:watch-list').set('watchInTesting', true);
await Allocation.visit({ id: allocation.id });
});

test('when the allocation endpoint 404s, a flash message appears', async function(assert) {
assert.equal(PageLayout.flashMessages.length, 0);

const controller = this.owner.lookup('controller:allocations/allocation');
const route = this.owner.lookup('route:allocations/allocation');
controller.watcher = route.watch.perform(controller.model, { throttle: 0, runInTests: true });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you know, these are distinctly not acceptance tests since they invoke methods on objects rather than treating the app like a black box.

I'm fine with running with this as-is and adding an overarching acceptance testing + watching task to the backlog.

To ramble for a minute...

I think the best solution will make use of either ember-concurrency-test-waiter or some abstraction over run.cancelTimers like I experimented with over here: d5e4117

The dream is to have a test that is easy to write, reads well, and works via a series of button clicks and waiting. Which is a tall order given the inner workings of Ember concurrency.


await this.server.db.allocations.remove();
await settled();

assert.equal(PageLayout.flashMessages.length, 1);

PageLayout.flashMessages[0].as(message => {
assert.ok(message.icon.isWarning);
assert.equal(message.title, 'Warning');
assert.equal(message.body, 'This allocation no longer exists');
});
});
});

module('Acceptance | allocation detail (rescheduled)', function(hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
Expand Down
45 changes: 44 additions & 1 deletion ui/tests/acceptance/job-detail-test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { currentURL } from '@ember/test-helpers';
import { currentURL, settled } from '@ember/test-helpers';
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { selectChoose } from 'ember-power-select/test-support';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import moduleForJob from 'nomad-ui/tests/helpers/module-for-job';
import JobDetail from 'nomad-ui/tests/pages/jobs/detail';
import JobsList from 'nomad-ui/tests/pages/jobs/list';
import PageLayout from 'nomad-ui/tests/pages/layout';

moduleForJob('Acceptance | job detail (batch)', 'allocations', () =>
server.create('job', { type: 'batch', shallow: true })
Expand Down Expand Up @@ -100,3 +101,45 @@ module('Acceptance | job detail (with namespaces)', function(hooks) {
});
});
});

module('Acceptance | job deletion warning', function(hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);

let job;

hooks.beforeEach(function() {
server.create('node');
job = server.create('job');

this.owner.lookup('service:watch-list').set('watchInTesting', true);
});

test('when the job endpoint 404s, a flash message appears', async function(assert) {
await JobDetail.visit({ id: job.id });

assert.equal(PageLayout.flashMessages.length, 0);

const controller = this.owner.lookup('controller:jobs/job/index');
const route = this.owner.lookup('route:jobs/job/index');
controller.watchers.model = route.watch.perform(controller.model, {
throttle: 0,
runInTests: true,
});

await this.server.db.jobs.remove();
await this.server.db.jobSummaries.remove();
await settled();

assert.equal(PageLayout.flashMessages.length, 1);

PageLayout.flashMessages[0].as(message => {
assert.ok(message.icon.isWarning);
assert.equal(message.title, 'Warning');
assert.equal(message.body, 'This job no longer exists');
});

await PageLayout.flashMessages[0].close.click();
assert.equal(PageLayout.flashMessages.length, 0);
});
});
15 changes: 14 additions & 1 deletion ui/tests/pages/layout.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { create, clickable, collection, isPresent, text } from 'ember-cli-page-object';
import { create, clickable, collection, isPresent, hasClass, text } from 'ember-cli-page-object';

export default create({
navbar: {
Expand Down Expand Up @@ -28,4 +28,17 @@ export default create({
visitClients: clickable('[data-test-gutter-link="clients"]'),
visitServers: clickable('[data-test-gutter-link="servers"]'),
},

flashMessages: collection('[data-test-flash-message]', {
title: text('[data-test-flash-message-title]'),
body: text('[data-test-flash-message-body]'),
icon: {
scope: '.message-icon .icon',
isWarning: hasClass('is-warning'),
},

close: {
scope: '[data-test-flash-message-close]',
},
}),
});
Loading