Show errors on views when main requests fail

Bug: Issue 3953
Change-Id: Ic20ac5cfc8cbf25c0744e0208b60f447ba9da718
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 0709f62..27e2cb8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -23,6 +23,12 @@
      * @event title-change
      */
 
+    /**
+     * Fired if an error occurs when fetching the change data.
+     *
+     * @event page-error
+     */
+
     properties: {
       /**
        * URL params passed from the router.
@@ -318,6 +324,10 @@
       page.show(this.changePath(this._changeNum));
     },
 
+    _handleGetChangeDetailError: function(response) {
+      this.fire('page-error', {response: response});
+    },
+
     _getDiffDrafts: function() {
       return this.$.restAPI.getDiffDrafts(this._changeNum).then(
           function(drafts) {
@@ -337,10 +347,11 @@
     },
 
     _getChangeDetail: function() {
-      return this.$.restAPI.getChangeDetail(this._changeNum).then(
-          function(change) {
-            this._change = change;
-          }.bind(this));
+      return this.$.restAPI.getChangeDetail(this._changeNum,
+          this._handleGetChangeDetailError.bind(this)).then(
+              function(change) {
+                this._change = change;
+              }.bind(this));
     },
 
     _getComments: function() {
@@ -382,6 +393,8 @@
       this._getComments();
 
       var reloadPatchNumDependentResources = function() {
+        if (!this._change) { return Promise.resolve(); }
+
         return Promise.all([
           this._getCommitInfo(),
           this.$.actions.reload(),
@@ -389,6 +402,8 @@
         ]);
       }.bind(this);
       var reloadDetailDependentResources = function() {
+        if (!this._change) { return Promise.resolve(); }
+
         return Promise.all([
           this.$.relatedChanges.reload(),
           this._getProjectConfig(),
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index c32ab43a..7e89f83 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -411,12 +411,17 @@
       this.$.diffTable.innerHTML = null;
     },
 
+    _handleGetDiffError: function(response) {
+      this.fire('page-error', {response: response});
+    },
+
     _getDiff: function() {
       return this.$.restAPI.getDiff(
           this.changeNum,
           this.patchRange.basePatchNum,
           this.patchRange.patchNum,
-          this.path);
+          this.path,
+          this._handleGetDiffError.bind(this));
     },
 
     _getDiffComments: function() {
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 67cfad5..723766d 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -49,6 +49,31 @@
       }
       main {
         flex: 1;
+        position: relative;
+      }
+      .errorView {
+        align-items: center;
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        position: absolute;
+        top: 0;
+        right: 0;
+        bottom: 0;
+        left: 0;
+      }
+      .errorEmoji {
+        font-size: 2.6em;
+      }
+      .errorText,
+      .errorMoreInfo {
+        margin-top: .75em;
+      }
+      .errorText {
+        font-size: 1.2em;
+      }
+      .errorMoreInfo {
+        color: #999;
       }
       .feedback {
         color: #b71c1c;
@@ -80,6 +105,11 @@
             params="[[params]]"
             change-view-state="{{_viewState.changeView}}"></gr-diff-view>
       </template>
+      <div id="errorView" class="errorView" hidden>
+        <div class="errorEmoji">[[_lastError.emoji]]</div>
+        <div class="errorText">[[_lastError.text]]</div>
+        <div class="errorMoreInfo">[[_lastError.moreInfo]]</div>
+      </div>
     </main>
     <footer role="contentinfo">
       Powered by <a href="https://www.gerritcodereview.com/" target="_blank">Gerrit Code Review</a>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 74f7afd..3495218 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -36,9 +36,11 @@
       _showChangeView: Boolean,
       _showDiffView: Boolean,
       _viewState: Object,
+      _lastError: Object,
     },
 
     listeners: {
+      'page-error': '_handlePageError',
       'title-change': '_handleTitleChange',
     },
 
@@ -100,6 +102,7 @@
     },
 
     _viewChanged: function(view) {
+      this.$.errorView.hidden = true;
       this.set('_showChangeListView', view === 'gr-change-list-view');
       this.set('_showDashboardView', view === 'gr-dashboard-view');
       this.set('_showChangeView', view === 'gr-change-view');
@@ -112,10 +115,36 @@
           window.location.pathname + window.location.hash));
     },
 
-    _computeLoggedIn: function(account) { // argument used for binding update only
+    // Argument used for binding update only.
+    _computeLoggedIn: function(account) {
       return this.loggedIn;
     },
 
+    _handlePageError: function(e) {
+      [
+        '_showChangeListView',
+        '_showDashboardView',
+        '_showChangeView',
+        '_showDiffView',
+      ].forEach(function(showProp) {
+        this.set(showProp, false);
+      }.bind(this));
+
+      this.$.errorView.hidden = false;
+      var response = e.detail.response;
+      var err = {text: [response.status, response.statusText].join(' ')};
+      if (response.status === 404) {
+        err.emoji = '¯\\_(ツ)_/¯';
+        this._lastError = err;
+      } else {
+        err.emoji = 'o_O';
+        response.text().then(function(text) {
+          err.moreInfo = text;
+          this._lastError = err;
+        }.bind(this));
+      }
+    },
+
     _handleTitleChange: function(e) {
       if (e.detail.title) {
         document.title = e.detail.title + ' · Gerrit Code Review';
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 935e18a..ef74b3d 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
@@ -79,7 +79,8 @@
       },
     },
 
-    fetchJSON: function(url, opt_cancelCondition, opt_params, opt_opts) {
+    fetchJSON: function(url, opt_errFn, opt_cancelCondition, opt_params,
+        opt_opts) {
       opt_opts = opt_opts || {};
 
       var fetchOptions = {
@@ -110,13 +111,17 @@
           return;
         }
 
+        if (!response.ok && opt_errFn) {
+          opt_errFn.call(null, response);
+          return undefined;
+        }
         return this.getResponseObject(response);
       }.bind(this)).catch(function(err) {
         if (opt_opts.noCredentials) {
           throw err;
         } else {
           // This could be because of a 302 auth redirect. Retry the request.
-          return this.fetchJSON(url, opt_cancelCondition, opt_params,
+          return this.fetchJSON(url, opt_errFn, opt_cancelCondition, opt_params,
               Object.assign(opt_opts, {noCredentials: true}));
         }
       }.bind(this));
@@ -196,7 +201,7 @@
       return this._changeBaseURL(changeNum, opt_patchNum) + endpoint;
     },
 
-    getChangeDetail: function(changeNum, opt_cancelCondition) {
+    getChangeDetail: function(changeNum, opt_errFn, opt_cancelCondition) {
       var options = this._listChangesOptionsToHex(
           ListChangesOption.ALL_REVISIONS,
           ListChangesOption.CHANGE_ACTIONS,
@@ -204,6 +209,7 @@
       );
       return this.fetchJSON(
           this.getChangeActionURL(changeNum, null, '/detail'),
+          opt_errFn,
           opt_cancelCondition,
           {O: options});
     },
@@ -263,7 +269,7 @@
     },
 
     getDiff: function(changeNum, basePatchNum, patchNum, path,
-        opt_cancelCondition) {
+        opt_errFn, opt_cancelCondition) {
       var url = this._getDiffFetchURL(changeNum, patchNum, path);
       var params =  {
         context: 'ALL',
@@ -274,7 +280,7 @@
         params.base = basePatchNum;
       }
 
-      return this.fetchJSON(url, opt_cancelCondition, params);
+      return this.fetchJSON(url, opt_errFn, opt_cancelCondition, params);
     },
 
     _getDiffFetchURL: function(changeNum, patchNum, path) {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 44e49cb..4e2c5ad 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -93,7 +93,7 @@
         gr: 'guten tag',
         noval: null,
       };
-      element.fetchJSON('/path/', null, params);
+      element.fetchJSON('/path/', null, null, params);
       assert.equal(fetchStub.args[0][0], '/path/?gr=guten%20tag&noval&sp=hola');
       fetchStub.restore();
     });
@@ -105,7 +105,7 @@
           cancel: function() { cancelCalled = true; }
         }});
       });
-      element.fetchJSON('/dummy/url', function() { return true; }).then(
+      element.fetchJSON('/dummy/url', null, function() { return true; }).then(
         function(obj) {
           assert.isUndefined(obj);
           assert.isTrue(cancelCalled);