FEATURE: Nice error handling page

This commit is contained in:
riking 2014-06-16 11:25:33 -07:00
parent 0612018569
commit 0d4163e0a2
16 changed files with 265 additions and 7 deletions

View File

@ -0,0 +1,107 @@
var ButtonBackBright = {
classes: "btn-primary",
action: "back",
key: "errors.buttons.back"
},
ButtonBackDim = {
classes: "",
action: "back",
key: "errors.buttons.back"
},
ButtonTryAgain = {
classes: "btn-primary",
action: "tryLoading",
key: "errors.buttons.again"
},
ButtonLoadPage = {
classes: "btn-primary",
action: "tryLoading",
key: "errors.buttons.fixed"
};
/**
The controller for the nice error page
@class ExceptionController
@extends Discourse.ObjectController
@namespace Discourse
@module Discourse
**/
export default Discourse.ObjectController.extend({
thrown: null,
lastTransition: null,
isNetwork: function() {
// never made it on the wire
if (this.get('thrown.readyState') === 0) return true;
// timed out
if (this.get('thrown.jqTextStatus') === "timeout") return true;
return false;
}.property(),
isServer: Em.computed.gte('thrown.status', 500),
isUnknown: Em.computed.none('isNetwork', 'isServer'),
// TODO
// make ajax requests to /srv/status with exponential backoff
// if one succeeds, set networkFixed to true, which puts a "Fixed!" message on the page
networkFixed: false,
loading: false,
_init: function() {
this.set('loading', false);
}.on('init'),
reason: function() {
if (this.get('isNetwork')) {
return I18n.t('errors.reasons.network');
} else if (this.get('isServer')) {
return I18n.t('errors.reasons.server');
} else {
// TODO
return I18n.t('errors.reasons.unknown');
}
}.property('isNetwork', 'isServer', 'isUnknown'),
requestUrl: Em.computed.alias('thrown.requestedUrl'),
desc: function() {
if (this.get('networkFixed')) {
return I18n.t('errors.desc.network_fixed');
} else if (this.get('isNetwork')) {
return I18n.t('errors.desc.network');
} else if (this.get('isServer')) {
return I18n.t('errors.desc.server', this.get('thrown.statusText'));
} else {
// TODO
return I18n.t('errors.desc.unknown');
}
}.property('networkFixed', 'isNetwork', 'isServer', 'isUnknown'),
enabledButtons: function() {
if (this.get('networkFixed')) {
return [ButtonLoadPage];
} else if (this.get('isNetwork')) {
return [ButtonBackDim, ButtonTryAgain];
} else if (this.get('isServer')) {
return [ButtonBackBright];
} else {
return [ButtonBackBright, ButtonTryAgain];
}
}.property('networkFixed', 'isNetwork', 'isServer', 'isUnknown'),
actions: {
back: function() {
window.history.back();
},
tryLoading: function() {
this.set('loading', true);
var self = this;
Em.run.schedule('afterRender', function() {
self.get('lastTransition').retry();
self.set('loading', false);
});
}
}
});

View File

@ -385,6 +385,16 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
});
},
retryLoading: function() {
var self = this;
self.set('retrying', true);
this.get('postStream').refresh().then(function() {
self.set('retrying', false);
}, function() {
self.set('retrying', false);
});
},
toggleWiki: function(post) {
post.toggleProperty('wiki');
}

View File

@ -65,7 +65,9 @@ Discourse.Ajax = Em.Mixin.create({
// If it's a parsererror, don't reject
if (xhr.status === 200) return args.success(xhr);
// Fill in some extra info
xhr.jqTextStatus = textStatus;
xhr.requestedUrl = url;
Ember.run(promise, promise.reject, xhr);
if (oldError) oldError(xhr);

View File

@ -249,6 +249,7 @@ Discourse.PostStream = Em.Object.extend({
self.setProperties({ loadingFilter: false, loaded: true });
}).catch(function(result) {
self.errorLoading(result);
throw result;
});
},
hasLoadedData: Em.computed.and('hasPosts', 'hasStream'),

View File

@ -10,6 +10,19 @@ Discourse.ApplicationRoute = Em.Route.extend({
actions: {
error: function(err, transition) {
if (err.status === 404) {
// 404
this.intermediateTransitionTo('unknown');
return;
}
var exceptionController = this.controllerFor('exception');
exceptionController.setProperties({ lastTransition: transition, thrown: err });
this.intermediateTransitionTo('exception');
},
showLogin: function() {
if (Discourse.get("isReadOnly")) {
bootbox.alert(I18n.t("read_only_mode.login_disabled"));

View File

@ -13,6 +13,9 @@ Discourse.Route.buildRoutes(function() {
router.route(page, { path: '/' + page });
});
// Error page
this.route('exception', { path: '/exception' });
// Topic routes
this.resource('topic', { path: '/t/:slug/:id' }, function() {
this.route('fromParams', { path: '/' });

View File

@ -0,0 +1,13 @@
/**
Client-side pseudo-route for showing an error page.
@class ExceptionRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.ExceptionRoute = Discourse.Route.extend({
serialize: function() {
return "";
}
});

View File

@ -89,7 +89,7 @@ Discourse.TopicRoute = Discourse.Route.extend({
}
}, 150),
willTransition: function() { this.set("isTransitioning", true); }
willTransition: function() { this.set("isTransitioning", true); return true; }
},

View File

@ -0,0 +1,24 @@
<div class="container">
<div class="error-page">
<div class="face">:(</div>
<div class="reason">{{reason}}</div>
<div class="url">
{{i18n errors.prev_page}} <a {{bind-attr href=requestUrl}} data-auto-route="true">{{requestUrl}}</a>
</div>
<div class="desc">
{{#if networkFixed}}
<i class="fa fa-check-circle"></i>
{{/if}}
{{desc}}
</div>
<div class="buttons">
{{#each buttonData in enabledButtons}}
<button class="btn {{unbound buttonData.classes}}" {{action buttonData.action}}>{{boundI18n buttonData.key}}</button>
{{/each}}
{{#if loading}}
<i class="fa fa-spin fa-spinner"></i>
{{/if}}
</div>
</div>
</div>

View File

@ -117,17 +117,19 @@
{{else}}
{{#if hasError}}
<div class='container'>
{{#if errorBodyHtml}}
<div class="topic-error">
{{{errorBodyHtml}}}
{{/if}}
{{#if message}}
<div class="message">
<h2>{{message}}</h2>
{{#if message}}
{{message}}
{{#unless currentUser}}
<button {{action showLogin}} class='btn btn-primary btn-small'>{{i18n log_in}}</button>
{{/unless}}
</div>
{{/if}}
<button class="btn btn-primary topic-retry" {{action retryLoading}}>{{i18n errors.buttons.again}}</button>
</div>
{{#if retrying}}
<div class='spinner'>{{i18n loading}}</div>
{{/if}}
</div>
{{else}}

View File

@ -0,0 +1,30 @@
.error-page {
text-align: center;
padding-top: 2em;
.face {
font-size: 60px;
height: 60px;
}
.reason {
font-size: 24px;
height: 24px;
}
.url {
font-style: italic;
font-size: 11px;
}
.desc {
margin-top: 16px;
.fa-check-circle {
color: $success;
}
}
.buttons {
margin-top: 15px;
button {
margin: 0 20px;
}
}
}

View File

@ -84,6 +84,23 @@ a:hover.reply-new {
}
}
.topic-error {
padding: 18px;
width: 60%;
margin-left: auto;
margin-right: auto;
font-size: 24px;
text-align: center;
line-height: 1.1em;
.topic-retry {
display: block;
margin-top: 28px;
margin-left: auto;
margin-right: auto;
}
}
#topic-closing-info {
border-top: 1px solid scale-color-diff();
padding-top: 10px;

View File

@ -146,6 +146,22 @@
}
}
.topic-error {
padding: 18px;
width: 90%;
margin-left: auto;
margin-right: auto;
font-size: 24px;
line-height: 1.1em;
.topic-retry {
display: block;
margin-top: 20px;
margin-left: auto;
margin-right: auto;
}
}
#topic-progress-wrapper.docked {
position: absolute;
}

View File

@ -16,4 +16,8 @@ class ForumsController < ApplicationController
raise "WAT - #{Time.now.to_s}"
end
def home_redirect
redirect_to '/'
end
end

View File

@ -468,6 +468,21 @@ en:
the_topic: "the topic"
loading: "Loading..."
errors:
prev_page: "while trying to load"
reasons:
network: "Network Error"
server: "Server Error: {{code}}"
unknown: "Error"
desc:
network: "Please check your connection."
network_fixed: "Looks like it's back."
server: "Something went wrong."
unknown: "Something went wrong."
buttons:
back: "Go Back"
again: "Try Again"
fixed: "Load Page"
close: "Close"
assets_changed_confirm: "This site was just updated. Refresh now for the latest version?"
read_only_mode:

View File

@ -377,6 +377,7 @@ Discourse::Application.routes.draw do
get "onebox" => "onebox#show"
get "error" => "forums#error"
get "exception" => "list#latest"
get "message-bus/poll" => "message_bus#poll"