Merge pull request #3300 from hashicorp/f-ui-404-pages

404 pages for the UI
This commit is contained in:
Michael Lange 2017-10-05 15:20:18 -07:00 committed by GitHub
commit ab72eb164f
18 changed files with 229 additions and 5 deletions

View file

@ -0,0 +1,35 @@
import Ember from 'ember';
const { Controller, computed } = Ember;
export default Controller.extend({
error: null,
errorStr: computed('error', function() {
return this.get('error').toString();
}),
errorCodes: computed('error', function() {
const error = this.get('error');
const codes = [error.code];
if (error.errors) {
error.errors.forEach(err => {
codes.push(err.status);
});
}
return codes
.compact()
.uniq()
.map(code => '' + code);
}),
is404: computed('errorCodes.[]', function() {
return this.get('errorCodes').includes('404');
}),
is500: computed('errorCodes.[]', function() {
return this.get('errorCodes').includes('500');
}),
});

View file

@ -0,0 +1,10 @@
import Ember from 'ember';
import notifyError from 'nomad-ui/utils/notify-error';
const { Mixin } = Ember;
export default Mixin.create({
model() {
return this._super(...arguments).catch(notifyError(this));
},
});

View file

@ -35,6 +35,8 @@ Router.map(function() {
if (config.environment === 'development') {
this.route('freestyle');
}
this.route('not-found', { path: '/*' });
});
export default Router;

View file

@ -0,0 +1,6 @@
import Ember from 'ember';
import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling';
const { Route } = Ember;
export default Route.extend(WithModelErrorHandling);

View file

@ -3,9 +3,19 @@ import Ember from 'ember';
const { Route } = Ember;
export default Route.extend({
resetController(controller, isExiting) {
if (isExiting) {
controller.set('error', null);
}
},
actions: {
didTransition() {
window.scrollTo(0, 0);
},
error(error) {
this.controllerFor('application').set('error', error);
},
},
});

View file

@ -1,4 +1,5 @@
import Ember from 'ember';
import notifyError from 'nomad-ui/utils/notify-error';
const { Route, inject } = Ember;
@ -10,6 +11,7 @@ export default Route.extend({
.find('job', job_id)
.then(job => {
return job.get('allocations').then(() => job);
});
})
.catch(notifyError(this));
},
});

View file

@ -1,14 +1,19 @@
import Ember from 'ember';
import notifyError from 'nomad-ui/utils/notify-error';
const { Route, inject } = Ember;
export default Route.extend({
store: inject.service(),
model() {
return this._super(...arguments).catch(notifyError(this));
},
afterModel(model) {
if (model.get('isPartial')) {
if (model && model.get('isPartial')) {
return model.reload().then(node => node.get('allocations'));
}
return model.get('allocations');
return model && model.get('allocations');
},
});

View file

@ -0,0 +1,11 @@
import Ember from 'ember';
const { Route, Error: EmberError } = Ember;
export default Route.extend({
model() {
const err = new EmberError('Page not found');
err.code = '404';
this.controllerFor('application').set('error', err);
},
});

View file

@ -0,0 +1,6 @@
import Ember from 'ember';
import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling';
const { Route } = Ember;
export default Route.extend(WithModelErrorHandling);

View file

@ -1,4 +1,5 @@
import ApplicationSerializer from './application';
import { AdapterError } from 'ember-data/adapters/errors';
export default ApplicationSerializer.extend({
attrs: {
@ -8,6 +9,16 @@ export default ApplicationSerializer.extend({
},
normalize(typeHash, hash) {
if (!hash) {
// It's unusual to throw an adapter error from a serializer,
// but there is no single server end point so the serializer
// acts like the API in this case.
const error = new AdapterError([{ status: '404' }]);
error.message = 'Requested Agent was not found in set of available Agents';
throw error;
}
hash.ID = hash.Name;
hash.Datacenter = hash.Tags && hash.Tags.dc;
hash.Region = hash.Tags && hash.Tags.region;

View file

@ -2,6 +2,7 @@
@import "./components/boxed-section";
@import "./components/breadcrumbs";
@import "./components/empty-message";
@import "./components/error-container";
@import "./components/gutter";
@import "./components/inline-definitions";
@import "./components/job-diff";

View file

@ -0,0 +1,22 @@
.error-container {
width: 100%;
height: 100%;
padding-top: 25vh;
display: flex;
justify-content: center;
background: $grey-lighter;
.error-message {
max-width: 600px;
.title,
.subtitle {
text-align: center;
}
}
.error-stack-trace {
border: 1px solid $grey-light;
border-radius: $radius;
}
}

View file

@ -1,2 +1,19 @@
{{partial "svg-patterns"}}
{{outlet}}
{{#unless error}}
{{outlet}}
{{else}}
<div class="error-container">
<div class="error-message">
{{#if is500}}
<h1 class="title is-spaced">Server Error</h1>
<p class="subtitle">A server error prevented data from being sent to the client.</p>
{{else if is404}}
<h1 class="title is-spaced">Not Found</h1>
<p class="subtitle">What you're looking for couldn't be found. It either doesn't exist or you are not authorized to see it.</p>
{{/if}}
{{#if (eq config.environment "development")}}
<pre class="error-stack-trace"><code>{{errorStr}}</code></pre>
{{/if}}
</div>
</div>
{{/unless}}

View file

@ -0,0 +1,7 @@
// An error handler to provide to a promise catch to set an error
// on the application controller.
export default function notifyError(route) {
return error => {
route.controllerFor('application').set('error', error);
};
}

View file

@ -183,3 +183,24 @@ test('each recent event should list the time, type, and description of the event
'Event message'
);
});
test('when the allocation is not found, an error message is shown, but the URL persists', function(
assert
) {
visit('/allocations/not-a-real-allocation');
andThen(() => {
assert.equal(
server.pretender.handledRequests.findBy('status', 404).url,
'/v1/allocation/not-a-real-allocation',
'A request to the non-existent allocation is made'
);
assert.equal(currentURL(), '/allocations/not-a-real-allocation', 'The URL persists');
assert.ok(find('.error-message'), 'Error message is shown');
assert.equal(
find('.error-message .title').textContent,
'Not Found',
'Error message is for 404'
);
});
});

View file

@ -181,3 +181,24 @@ test('each allocation should link to the job the allocation belongs to', functio
test('/nodes/:id should list all attributes for the node', function(assert) {
assert.ok(find('.attributes-table'), 'Attributes table is on the page');
});
test('when the node is not found, an error message is shown, but the URL persists', function(
assert
) {
visit('/nodes/not-a-real-node');
andThen(() => {
assert.equal(
server.pretender.handledRequests.findBy('status', 404).url,
'/v1/node/not-a-real-node',
'A request to the non-existent node is made'
);
assert.equal(currentURL(), '/nodes/not-a-real-node', 'The URL persists');
assert.ok(find('.error-message'), 'Error message is shown');
assert.equal(
find('.error-message .title').textContent,
'Not Found',
'Error message is for 404'
);
});
});

View file

@ -260,3 +260,24 @@ test('the active deployment section can be expanded to show task groups and allo
);
});
});
test('when the job is not found, an error message is shown, but the URL persists', function(
assert
) {
visit('/jobs/not-a-real-job');
andThen(() => {
assert.equal(
server.pretender.handledRequests.findBy('status', 404).url,
'/v1/job/not-a-real-job',
'A request to the non-existent job is made'
);
assert.equal(currentURL(), '/jobs/not-a-real-job', 'The URL persists');
assert.ok(find('.error-message'), 'Error message is shown');
assert.equal(
find('.error-message .title').textContent,
'Not Found',
'Error message is for 404'
);
});
});

View file

@ -1,5 +1,5 @@
import Ember from 'ember';
import { findAll, currentURL, visit } from 'ember-native-dom-helpers';
import { find, findAll, currentURL, visit } from 'ember-native-dom-helpers';
import { test } from 'qunit';
import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance';
@ -42,3 +42,19 @@ test('the active server should be denoted in the table', function(assert) {
'Active server matches current route'
);
});
test('when the server is not found, an error message is shown, but the URL persists', function(
assert
) {
visit('/servers/not-a-real-server');
andThen(() => {
assert.equal(currentURL(), '/servers/not-a-real-server', 'The URL persists');
assert.ok(find('.error-message'), 'Error message is shown');
assert.equal(
find('.error-message .title').textContent,
'Not Found',
'Error message is for 404'
);
});
});