Show network and server errors in a toast 🍞 notification
gr-error-manager is introduced to deal with error events
fired from gr-rest-api-interface. Currently it will display
the first error it sees for 5 seconds, ignoring others during
that time. If a response comes back as a 403, it prompts the
user to refresh the page and the alert remains on the screen
indefinitely.
Bug: Issue 3992
Bug: Issue 3953
Change-Id: I4a54eb1e865b88f9a5531e864e0b1d58d638a4cd
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 47324ba..be536da 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -141,16 +141,13 @@
this.disabled = true;
this._saveReview(obj).then(function(response) {
this.disabled = false;
- if (!response.ok) {
- alert('Oops. Something went wrong. Check the console and bug the ' +
- 'PolyGerrit team for assistance.');
- return response.text().then(function(text) {
- console.error(text);
- });
- }
+ if (!response.ok) { return response; }
this.draft = '';
this.fire('send', null, {bubbles: false});
+ }.bind(this)).catch(function(err) {
+ this.disabled = false;
+ throw err;
}.bind(this));
},
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index 09ce7d7..de99039 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -144,11 +144,8 @@
this._xhrPromise =
this._removeReviewer(accountID).then(function(response) {
this.disabled = false;
- if (!response.ok) {
- return response.text().then(function(text) {
- alert(text);
- });
- }
+ if (!response.ok) { return response; }
+
var reviewers = this.change.reviewers;
['REVIEWER', 'CC'].forEach(function(type) {
reviewers[type] = reviewers[type] || [];
@@ -159,6 +156,9 @@
}
}
}, this);
+ }.bind(this)).catch(function(err) {
+ this.disabled = false;
+ throw err;
}.bind(this));
},
@@ -304,11 +304,8 @@
this._xhrPromise = this._addReviewer(reviewerID).then(function(response) {
this.change.reviewers['CC'] = this.change.reviewers['CC'] || [];
this.disabled = false;
- if (!response.ok) {
- return response.text().then(function(text) {
- alert(text);
- });
- }
+ if (!response.ok) { return response; }
+
return this.$.restAPI.getResponseObject(response).then(function(obj) {
obj.reviewers.forEach(function(r) {
this.push('change.removable_reviewers', r);
@@ -317,6 +314,9 @@
this._inputVal = '';
this.$.input.focus();
}.bind(this));
+ }.bind(this)).catch(function(err) {
+ this.disabled = false;
+ throw err;
}.bind(this));
},
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
new file mode 100644
index 0000000..80f293d
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
@@ -0,0 +1,27 @@
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-alert/gr-alert.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
+<dom-module id="gr-error-manager">
+ <template>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ </template>
+ <script src="gr-error-manager.js"></script>
+</dom-module>
+
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
new file mode 100644
index 0000000..757d79f
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -0,0 +1,109 @@
+// Copyright (C) 2016 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-error-manager',
+
+ properties: {
+ _alertElement: Element,
+ _hideAlertHandle: Number,
+ _hideAlertTimeout: {
+ type: Number,
+ value: 5000,
+ },
+ },
+
+ attached: function() {
+ this.listen(document, 'server-error', '_handleServerError');
+ this.listen(document, 'network-error', '_handleNetworkError');
+ },
+
+ detached: function() {
+ this._clearHideAlertHandle();
+ this.unlisten(document, 'server-error', '_handleServerError');
+ this.unlisten(document, 'network-error', '_handleNetworkError');
+ },
+
+ _handleServerError: function(e) {
+ if (e.detail.response.status === 403) {
+ this._getLoggedIn().then(function(loggedIn) {
+ if (loggedIn) {
+ // The app was logged at one point and is now getting auth errors.
+ // This indicates the auth token is no longer valid.
+ this._showAuthErrorAlert();
+ }
+ }.bind(this));
+ } else {
+ e.detail.response.text().then(function(text) {
+ this._showAlert('Server error: ' + text);
+ }.bind(this));
+ }
+ },
+
+ _handleNetworkError: function(e) {
+ this._showAlert('Server unavailable');
+ console.error(e.detail.error.message);
+ },
+
+ _getLoggedIn: function() {
+ return this.$.restAPI.getLoggedIn();
+ },
+
+ _showAlert: function(text) {
+ if (this._alertElement) { return; }
+
+ this._clearHideAlertHandle();
+ this._hideAlertHandle =
+ this.async(this._hideAlert.bind(this), this._hideAlertTimeout);
+ var el = this._createToastAlert();
+ el.show(text);
+ this._alertElement = el;
+ },
+
+ _hideAlert: function() {
+ if (!this._alertElement) { return; }
+
+ this._alertElement.hide();
+ this._alertElement = null;
+ },
+
+ _clearHideAlertHandle: function() {
+ if (this._hideAlertHandle != null) {
+ this.cancelAsync(this._hideAlertHandle);
+ this._hideAlertHandle = null;
+ }
+ },
+
+ _showAuthErrorAlert: function() {
+ if (this._alertElement) { return; }
+
+ var el = this._createToastAlert();
+ el.addEventListener('action', this._refreshPage.bind(this));
+ el.show('Auth error', 'Refresh page');
+ this._alertElement = el;
+ },
+
+ _createToastAlert: function() {
+ var el = document.createElement('gr-alert');
+ el.toast = true;
+ return el;
+ },
+
+ _refreshPage: function() {
+ window.location.reload();
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
new file mode 100644
index 0000000..98f79b0
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-error-manager</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-error-manager.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-error-manager></gr-error-manager>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-error-manager tests', function() {
+ var element;
+
+ setup(function() {
+ stub('gr-rest-api-interface', {
+ getLoggedIn: function() { return Promise.resolve(true); },
+ });
+ element = fixture('basic');
+ });
+
+ test('show auth error', function(done) {
+ var showAuthErrorStub = sinon.stub(element, '_showAuthErrorAlert');
+ element.fire('server-error', {response: {status: 403}});
+ flush(function() {
+ assert.isTrue(showAuthErrorStub.calledOnce);
+ showAuthErrorStub.restore();
+ done();
+ });
+ });
+
+ test('show normal server error', function(done) {
+ var showAlertStub = sinon.stub(element, '_showAlert');
+ element.fire('server-error', {response: {
+ status: 500,
+ text: function() { return Promise.resolve('ZOMG'); },
+ }});
+ flush(function() {
+ assert.isTrue(showAlertStub.calledOnce);
+ assert.isTrue(showAlertStub.lastCall.calledWithExactly(
+ 'Server error: ZOMG'));
+ showAlertStub.restore();
+ done();
+ });
+ });
+
+ test('show network error', function(done) {
+ var consoleErrorStub = sinon.stub(console, 'error');
+ var showAlertStub = sinon.stub(element, '_showAlert');
+ element.fire('network-error', {error: new Error('ZOMG')});
+ flush(function() {
+ assert.isTrue(showAlertStub.calledOnce);
+ assert.isTrue(showAlertStub.lastCall.calledWithExactly(
+ 'Server unavailable'));
+ assert.isTrue(consoleErrorStub.calledOnce);
+ assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG'));
+ showAlertStub.restore();
+ consoleErrorStub.restore();
+ done();
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index b39295a..ded5108 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -74,13 +74,8 @@
this.disabled = true;
this._xhrPromise = this._saveDraft(this.comment).then(function(response) {
this.disabled = false;
- if (!response.ok) {
- alert('Your draft couldn’t be saved. Check the console and contact ' +
- 'the PolyGerrit team for assistance.');
- return response.text().then(function(text) {
- console.error(text);
- });
- }
+ if (!response.ok) { return response; }
+
return this.$.restAPI.getResponseObject(response).then(function(obj) {
var comment = obj;
comment.__draft = true;
@@ -95,10 +90,8 @@
return obj;
}.bind(this));
}.bind(this)).catch(function(err) {
- alert('Your draft couldn’t be saved. Check the console and contact ' +
- 'the PolyGerrit team for assistance.');
this.disabled = false;
- console.error(err.message);
+ throw err;
}.bind(this));
},
@@ -198,18 +191,12 @@
this._xhrPromise =
this._deleteDraft(this.comment).then(function(response) {
this.disabled = false;
- if (!response.ok) {
- alert('Your draft couldn’t be deleted. Check the console and ' +
- 'contact the PolyGerrit team for assistance.');
- return response.text().then(function(text) {
- console.error(text);
- });
- }
+ if (!response.ok) { return response; }
+
this.fire('comment-discard');
}.bind(this)).catch(function(err) {
- alert('Your draft couldn’t be deleted. Check the console and contact ' +
- 'the PolyGerrit team for assistance.');
this.disabled = false;
+ throw err;
}.bind(this));;
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 85371f4..b2adcf4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -344,14 +344,11 @@
el.disabled = true;
this._saveDiffPreferences().then(function(response) {
el.disabled = false;
- if (!response.ok) {
- alert('Oops. Something went wrong. Check the console and bug the ' +
- 'PolyGerrit team for assistance.');
- return response.text().then(function(text) {
- console.error(text);
- });
- }
+ if (!response.ok) { return response; }
+
this.$.prefsOverlay.close();
+ }.bind(this)).catch(function(err) {
+ el.disabled = false;
}.bind(this));
},
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 95b8ae6..c7ff59f 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -18,6 +18,7 @@
<link rel="import" href="../behaviors/keyboard-shortcut-behavior.html">
<link rel="import" href="../styles/app-theme.html">
+<link rel="import" href="./core/gr-error-manager/gr-error-manager.html">
<link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html">
<link rel="import" href="./core/gr-main-header/gr-main-header.html">
<link rel="import" href="./core/gr-router/gr-router.html">
@@ -139,6 +140,7 @@
<template is="dom-repeat" items="[[_serverConfig.plugin.js_resource_paths]]" as="path">
<script src$="/[[path]]" defer></script>
</template>
+ <gr-error-manager></gr-error-manager>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-app.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
index 28e45a4..140fbaa 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
@@ -39,6 +39,14 @@
:host([shown]) {
transform: translateY(0);
}
+ .text {
+ display: inline-block;
+ max-width: 25em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: bottom;
+ white-space: nowrap;
+ }
.action {
color: #a1c2fa;
font-weight: bold;
@@ -46,11 +54,11 @@
text-decoration: none;
}
</style>
- [[text]]
+ <span class="text">[[text]]</span>
<gr-button
link
class="action"
- hidden$="[[!actionText]]"
+ hidden$="[[_hideActionButton]]"
on-tap="_handleActionTap">[[actionText]]</gr-button>
</template>
<script src="gr-alert.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
index ba481ec..a3e933f 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -38,6 +38,7 @@
reflectToAttribute: true,
},
+ _hideActionButton: Boolean,
_boundTransitionEndHandler: {
type: Function,
value: function() { return this._handleTransitionEnd.bind(this); },
@@ -56,6 +57,7 @@
show: function(text, opt_actionText) {
this.text = text;
this.actionText = opt_actionText;
+ this._hideActionButton = !opt_actionText
document.body.appendChild(this);
this._setShown(true);
},
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
index e64e9dc..23c56b4 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -38,18 +38,7 @@
var newVal = !this.change.starred;
this.set('change.starred', newVal);
this._xhrPromise = this.$.restAPI.saveChangeStarred(this.change._number,
- newVal).then(function(response) {
- if (!response.ok) {
- return response.text().then(function(text) {
- throw Error(text);
- });
- }
- }).catch(function(err) {
- this.set('change.starred', !newVal);
- alert('Change couldn’t be starred. Check the console and contact ' +
- 'the PolyGerrit team for assistance.');
- throw err;
- }.bind(this));
+ newVal);
},
});
})();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 82e045e..1ada7e9 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -68,6 +68,18 @@
Polymer({
is: 'gr-rest-api-interface',
+ /**
+ * Fired when an server error occurs.
+ *
+ * @event server-error
+ */
+
+ /**
+ * Fired when a network error occurs.
+ *
+ * @event network-error
+ */
+
properties: {
_cache: {
type: Object,
@@ -95,14 +107,24 @@
return;
}
- if (!response.ok && opt_errFn) {
- opt_errFn.call(null, response);
- return undefined;
+ if (!response.ok) {
+ if (opt_errFn) {
+ opt_errFn.call(null, response);
+ return undefined;
+ }
+ this.fire('server-error', {response: response});
}
+
return this.getResponseObject(response);
}.bind(this)).catch(function(err) {
+ if (opt_errFn) {
+ opt_errFn.call(null, null, err);
+ } else {
+ this.fire('network-error', {error: err});
+ throw err;
+ }
throw err;
- });
+ }.bind(this));
},
_urlWithParams: function(url, opt_params) {
@@ -503,13 +525,24 @@
}
options.body = opt_body;
}
- return fetch(url, options).catch(function(err) {
+ return fetch(url, options).then(function(response) {
+ if (!response.ok) {
+ if (opt_errFn) {
+ opt_errFn.call(null, response);
+ return undefined;
+ }
+ this.fire('server-error', {response: response});
+ }
+
+ return response;
+ }.bind(this)).catch(function(err) {
+ this.fire('network-error', {error: err});
if (opt_errFn) {
- opt_errFn.call(opt_ctx || this);
+ opt_errFn.call(opt_ctx, null, err);
} else {
throw err;
}
- });
+ }.bind(this));
},
getDiff: function(changeNum, basePatchNum, patchNum, path,
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index dc12bf9..9d16b84 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -38,6 +38,7 @@
'../elements/change-list/gr-change-list/gr-change-list_test.html',
'../elements/change-list/gr-change-list-item/gr-change-list-item_test.html',
'../elements/core/gr-account-dropdown/gr-account-dropdown_test.html',
+ '../elements/core/gr-error-manager/gr-error-manager_test.html',
'../elements/core/gr-main-header/gr-main-header_test.html',
'../elements/core/gr-search-bar/gr-search-bar_test.html',
'../elements/diff/gr-diff/gr-diff-builder_test.html',
@@ -48,9 +49,9 @@
'../elements/diff/gr-diff-preferences/gr-diff-preferences_test.html',
'../elements/diff/gr-diff-view/gr-diff-view_test.html',
'../elements/diff/gr-patch-range-select/gr-patch-range-select_test.html',
- '../elements/shared/gr-alert/gr-alert_test.html',
'../elements/shared/gr-account-label/gr-account-label_test.html',
'../elements/shared/gr-account-link/gr-account-link_test.html',
+ '../elements/shared/gr-alert/gr-alert_test.html',
'../elements/shared/gr-avatar/gr-avatar_test.html',
'../elements/shared/gr-change-star/gr-change-star_test.html',
'../elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html',