From 061201856984cd685211a4fa4662d541f69f6034 Mon Sep 17 00:00:00 2001 From: riking Date: Sun, 15 Jun 2014 22:23:54 -0700 Subject: [PATCH 1/2] Include the 'textStatus' field in DC.ajax rejection --- app/assets/javascripts/discourse/mixins/ajax.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/mixins/ajax.js b/app/assets/javascripts/discourse/mixins/ajax.js index 4b27624dc..f6cdbdbf7 100644 --- a/app/assets/javascripts/discourse/mixins/ajax.js +++ b/app/assets/javascripts/discourse/mixins/ajax.js @@ -54,7 +54,7 @@ Discourse.Ajax = Em.Mixin.create({ }; var oldError = args.error; - args.error = function(xhr) { + args.error = function(xhr, textStatus) { // note: for bad CSRF we don't loop an extra request right away. // this allows us to eliminate the possibility of having a loop. @@ -62,9 +62,11 @@ Discourse.Ajax = Em.Mixin.create({ Discourse.Session.current().set('csrfToken', null); } - // If it's a parseerror, don't reject + // If it's a parsererror, don't reject if (xhr.status === 200) return args.success(xhr); + xhr.jqTextStatus = textStatus; + Ember.run(promise, promise.reject, xhr); if (oldError) oldError(xhr); }; From 0d4163e0a2cfec1ab2dea9dcba6eaa33f5a25ae5 Mon Sep 17 00:00:00 2001 From: riking Date: Mon, 16 Jun 2014 11:25:33 -0700 Subject: [PATCH 2/2] FEATURE: Nice error handling page --- .../discourse/controllers/exception.js.es6 | 107 ++++++++++++++++++ .../discourse/controllers/topic_controller.js | 10 ++ .../javascripts/discourse/mixins/ajax.js | 2 + .../discourse/models/post_stream.js | 1 + .../discourse/routes/application_route.js | 13 +++ .../discourse/routes/application_routes.js | 3 + .../discourse/routes/exception_route.js | 13 +++ .../discourse/routes/topic_route.js | 2 +- .../templates/exception.js.handlebars | 24 ++++ .../discourse/templates/topic.js.handlebars | 14 ++- .../stylesheets/common/base/exception.scss | 30 +++++ app/assets/stylesheets/desktop/topic.scss | 17 +++ app/assets/stylesheets/mobile/topic.scss | 16 +++ app/controllers/forums_controller.rb | 4 + config/locales/client.en.yml | 15 +++ config/routes.rb | 1 + 16 files changed, 265 insertions(+), 7 deletions(-) create mode 100644 app/assets/javascripts/discourse/controllers/exception.js.es6 create mode 100644 app/assets/javascripts/discourse/routes/exception_route.js create mode 100644 app/assets/javascripts/discourse/templates/exception.js.handlebars create mode 100644 app/assets/stylesheets/common/base/exception.scss diff --git a/app/assets/javascripts/discourse/controllers/exception.js.es6 b/app/assets/javascripts/discourse/controllers/exception.js.es6 new file mode 100644 index 000000000..e6b8ae881 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/exception.js.es6 @@ -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); + }); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js b/app/assets/javascripts/discourse/controllers/topic_controller.js index 2834cfb49..15f9c1b59 100644 --- a/app/assets/javascripts/discourse/controllers/topic_controller.js +++ b/app/assets/javascripts/discourse/controllers/topic_controller.js @@ -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'); } diff --git a/app/assets/javascripts/discourse/mixins/ajax.js b/app/assets/javascripts/discourse/mixins/ajax.js index f6cdbdbf7..a3ed93dc6 100644 --- a/app/assets/javascripts/discourse/mixins/ajax.js +++ b/app/assets/javascripts/discourse/mixins/ajax.js @@ -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); diff --git a/app/assets/javascripts/discourse/models/post_stream.js b/app/assets/javascripts/discourse/models/post_stream.js index 7ec488c73..a8ff38bf3 100644 --- a/app/assets/javascripts/discourse/models/post_stream.js +++ b/app/assets/javascripts/discourse/models/post_stream.js @@ -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'), diff --git a/app/assets/javascripts/discourse/routes/application_route.js b/app/assets/javascripts/discourse/routes/application_route.js index 9d599853a..5d0a1ef6e 100644 --- a/app/assets/javascripts/discourse/routes/application_route.js +++ b/app/assets/javascripts/discourse/routes/application_route.js @@ -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")); diff --git a/app/assets/javascripts/discourse/routes/application_routes.js b/app/assets/javascripts/discourse/routes/application_routes.js index 813528d26..05eb76927 100644 --- a/app/assets/javascripts/discourse/routes/application_routes.js +++ b/app/assets/javascripts/discourse/routes/application_routes.js @@ -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: '/' }); diff --git a/app/assets/javascripts/discourse/routes/exception_route.js b/app/assets/javascripts/discourse/routes/exception_route.js new file mode 100644 index 000000000..8640600e0 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/exception_route.js @@ -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 ""; + } +}); diff --git a/app/assets/javascripts/discourse/routes/topic_route.js b/app/assets/javascripts/discourse/routes/topic_route.js index 0d0a097e7..5722b19ff 100644 --- a/app/assets/javascripts/discourse/routes/topic_route.js +++ b/app/assets/javascripts/discourse/routes/topic_route.js @@ -89,7 +89,7 @@ Discourse.TopicRoute = Discourse.Route.extend({ } }, 150), - willTransition: function() { this.set("isTransitioning", true); } + willTransition: function() { this.set("isTransitioning", true); return true; } }, diff --git a/app/assets/javascripts/discourse/templates/exception.js.handlebars b/app/assets/javascripts/discourse/templates/exception.js.handlebars new file mode 100644 index 000000000..63ff0b64b --- /dev/null +++ b/app/assets/javascripts/discourse/templates/exception.js.handlebars @@ -0,0 +1,24 @@ +
+
+
:(
+
{{reason}}
+
+ {{i18n errors.prev_page}} {{requestUrl}} +
+
+ {{#if networkFixed}} + + {{/if}} + + {{desc}} +
+
+ {{#each buttonData in enabledButtons}} + + {{/each}} + {{#if loading}} + + {{/if}} +
+
+
diff --git a/app/assets/javascripts/discourse/templates/topic.js.handlebars b/app/assets/javascripts/discourse/templates/topic.js.handlebars index 0ecc43dd3..ca9b2de6f 100644 --- a/app/assets/javascripts/discourse/templates/topic.js.handlebars +++ b/app/assets/javascripts/discourse/templates/topic.js.handlebars @@ -117,17 +117,19 @@ {{else}} {{#if hasError}}
- {{#if errorBodyHtml}} +
{{{errorBodyHtml}}} - {{/if}} - {{#if message}} -
-

{{message}}

+ {{#if message}} + {{message}} {{#unless currentUser}} {{/unless}} -
+ {{/if}} + +
+ {{#if retrying}} +
{{i18n loading}}
{{/if}}
{{else}} diff --git a/app/assets/stylesheets/common/base/exception.scss b/app/assets/stylesheets/common/base/exception.scss new file mode 100644 index 000000000..a1d411981 --- /dev/null +++ b/app/assets/stylesheets/common/base/exception.scss @@ -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; + } + } +} diff --git a/app/assets/stylesheets/desktop/topic.scss b/app/assets/stylesheets/desktop/topic.scss index 7e6542acf..858ac9cb9 100644 --- a/app/assets/stylesheets/desktop/topic.scss +++ b/app/assets/stylesheets/desktop/topic.scss @@ -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; diff --git a/app/assets/stylesheets/mobile/topic.scss b/app/assets/stylesheets/mobile/topic.scss index 0fbf32666..57d79cfa9 100644 --- a/app/assets/stylesheets/mobile/topic.scss +++ b/app/assets/stylesheets/mobile/topic.scss @@ -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; } diff --git a/app/controllers/forums_controller.rb b/app/controllers/forums_controller.rb index a15b6090e..32f7f9b2c 100644 --- a/app/controllers/forums_controller.rb +++ b/app/controllers/forums_controller.rb @@ -16,4 +16,8 @@ class ForumsController < ApplicationController raise "WAT - #{Time.now.to_s}" end + def home_redirect + redirect_to '/' + end + end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index c2098561b..8cf29ff5f 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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: diff --git a/config/routes.rb b/config/routes.rb index ef282eabb..d43e86350 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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"