Просмотр исходного кода

Add notifications view error details button

API errors can now be viewed in more detail after clicking the 'View
more details' button in notifications.

Such errors are no longer auto dismissed, the user must interact with
(click anywhere inside) the API error notification item in order for
them to be dismissed.
Sergiu Miclea 6 лет назад
Родитель
Сommit
5aa489c520

+ 1 - 0
src/components/atoms/CopyMultilineValue/CopyMultilineValue.jsx

@@ -28,6 +28,7 @@ const CopyButtonStyled = styled(CopyButton)`
 `
 const Wrapper = styled.div`
   cursor: pointer;
+  word-break: break-word;
 
   &:hover ${CopyButtonStyled} {
     opacity: 1;

+ 110 - 5
src/components/organisms/Notifications/Notifications.jsx

@@ -19,22 +19,69 @@ import { observer } from 'mobx-react'
 import styled, { injectGlobal } from 'styled-components'
 import NotificationSystem from 'react-notification-system'
 import { observe } from 'mobx'
+import type { AxiosXHRConfig } from 'axios'
 
 import notificationStore from '../../../stores/NotificationStore'
-import type { AlertInfo } from '../../../types/NotificationItem'
 
+import CopyMultilineValue from '../../atoms/CopyMultilineValue'
+import Button from '../../atoms/Button'
+import Modal from '../../molecules/Modal'
+
+import StyleProps from '../../styleUtils/StyleProps'
+import Palette from '../../styleUtils/Palette'
 import NotificationsStyle from './style.js'
+import DomUtils from '../../../utils/DomUtils'
+
+import type { AlertInfo } from '../../../types/NotificationItem'
 
 injectGlobal`
   ${NotificationsStyle}
 `
 
 const Wrapper = styled.div``
+const ErrorInfoWrapper = styled.div`
+  margin: 32px;
+  overflow: auto;
+`
+const ErrorInfoRequest = styled.div``
+const ErrorInfoRequestItem = styled.div`
+  margin-bottom: 16px;
+`
+const ErrorInfoRequestLabel = styled.div`
+  font-size: 10px;
+  font-weight: ${StyleProps.fontWeights.medium};
+  color: ${Palette.grayscale[3]};
+  text-transform: uppercase;
+  margin-bottom: 4px;
+`
+const ErrorInfoRequestData = styled.pre`
+  word-break: break-word;
+  white-space: pre-wrap;
+  margin: 0;
+  .key { color: #053997; }
+  .number { color: #107947; }
+  .string { color: #92000C; }
+  .boolean { color: #0000FF; }
+  .null { color: #000A5D; }
+`
+const ButtonWrapper = styled.div`
+  display: flex;
+  justify-content: center;
+  margin-bottom: 32px;
+`
+
+type State = {
+  errorInfo: ?{ error: { message: ?string, status: ?string }, request: AxiosXHRConfig<any> },
+}
 
 const MAX_NOTIFICATIONS = 3
 
 @observer
-class Notifications extends React.Component<{}> {
+class Notifications extends React.Component<{}, State> {
+  state = {
+    errorInfo: null,
+  }
+
   notificationSystem: NotificationSystem
   notificationsCount = 0
   activeNotifications: any[] = []
@@ -49,15 +96,27 @@ class Notifications extends React.Component<{}> {
     if (!alerts.length || alerts.length <= this.notificationsCount) {
       return
     }
-
     let lastNotification = alerts[alerts.length - 1]
+    let action = lastNotification.options ? lastNotification.options.action : null
+    let autoDismiss = lastNotification.message.length < 150 ? 10 : 30
+    if (action && lastNotification.level === 'error') {
+      let errorInfo = action.callback()
+      action = {
+        ...action,
+        callback: () => {
+          this.setState({ errorInfo })
+        },
+      }
+      autoDismiss = 0
+    }
+
     this.notificationSystem.addNotification({
       title: lastNotification.title || lastNotification.message,
       message: lastNotification.title ? lastNotification.message : null,
       level: lastNotification.level || 'info',
       position: 'br',
-      autoDismiss: lastNotification.message.length < 150 ? 10 : 30,
-      action: lastNotification.options ? lastNotification.options.action : null,
+      autoDismiss,
+      action,
       onAdd: notification => {
         this.activeNotifications.push(notification)
         for (let i = 0; i < this.activeNotifications.length - MAX_NOTIFICATIONS; i += 1) {
@@ -73,9 +132,55 @@ class Notifications extends React.Component<{}> {
   }
 
   render() {
+    let error = this.state.errorInfo
+    let jsonData = error && error.request.data
+    try {
+      jsonData = JSON.stringify(jsonData, null, 2)
+      jsonData = DomUtils.jsonSyntaxHighlight(jsonData)
+      // eslint-disable-next-line no-empty
+    } catch (err) { }
     return (
       <Wrapper>
         <NotificationSystem ref={(n) => { this.notificationSystem = n }} />
+        {error ? (
+          <Modal
+            title="Error Details"
+            isOpen
+            onRequestClose={() => { this.setState({ errorInfo: null }) }}
+          >
+            <ErrorInfoWrapper>
+              <ErrorInfoRequest>
+                <ErrorInfoRequestItem>
+                  <ErrorInfoRequestLabel>Request URL</ErrorInfoRequestLabel>
+                  <CopyMultilineValue value={error.request.url} />
+                </ErrorInfoRequestItem>
+                <ErrorInfoRequestItem>
+                  <ErrorInfoRequestLabel>Request Method</ErrorInfoRequestLabel>
+                  <CopyMultilineValue value={error.request.method || 'GET'} />
+                </ErrorInfoRequestItem>
+                {error.request.data ? (
+                  <ErrorInfoRequestItem>
+                    <ErrorInfoRequestLabel>Request Data</ErrorInfoRequestLabel>
+                    <ErrorInfoRequestData dangerouslySetInnerHTML={{ __html: jsonData }} />
+                  </ErrorInfoRequestItem>
+                ) : null}
+                <ErrorInfoRequestItem>
+                  <ErrorInfoRequestLabel>Response Status</ErrorInfoRequestLabel>
+                  <CopyMultilineValue value={error.error.status || '-'} />
+                </ErrorInfoRequestItem>
+                {error.error.message ? (
+                  <ErrorInfoRequestItem>
+                    <ErrorInfoRequestLabel>Response Message</ErrorInfoRequestLabel>
+                    <CopyMultilineValue value={error.error.message} />
+                  </ErrorInfoRequestItem>
+                ) : null}
+              </ErrorInfoRequest>
+            </ErrorInfoWrapper>
+            <ButtonWrapper>
+              <Button secondary onClick={() => { this.setState({ errorInfo: null }) }}>Dismiss</Button>
+            </ButtonWrapper>
+          </Modal>
+        ) : null}
       </Wrapper>
     )
   }

+ 3 - 0
src/components/organisms/Notifications/style.js

@@ -36,6 +36,9 @@ const NotificationsStyle = css`
     cursor: pointer;
     margin-left: 25px !important;
   }
+  .notification-error .notification-action-button {
+    background: ${Palette.secondaryLight} !important;
+  }
   .notification {
     border-radius: ${StyleProps.borderRadius} !important;
     background-color: ${Palette.grayscale[1]} !important;

+ 11 - 7
src/types/NotificationItem.js

@@ -14,17 +14,21 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 // @flow
 
+export type AlertInfoLevel = 'success' | 'error' | 'info'
+
+export type AlertInfoOptions = {
+  action?: {
+    label: string,
+    callback: () => any,
+  }
+}
+
 export type AlertInfo = {
-  options?: {
-    action?: {
-      label: string,
-      callback: () => void,
-    }
-  },
+  options?: AlertInfoOptions,
   message: string,
   title?: string,
   id?: string,
-  level?: 'success' | 'error' | 'info',
+  level?: AlertInfoLevel,
 }
 
 export type NotificationItemData = {

+ 36 - 8
src/utils/ApiCaller.js

@@ -152,10 +152,16 @@ class ApiCaller {
             !options.quietError) {
             let data = error.response.data
             let message = (data && data.error && data.error.message) || (data && data.description)
-            message = message || `${error.response.statusText || error.response.status} ${truncateUrl(options.url)}`
-            if (message) {
-              notificationStore.alert(message, 'error')
-            }
+            let alertMessage = message || `${error.response.statusText || error.response.status} ${truncateUrl(options.url)}`
+            let status = error.response.status && error.response.statusText
+              ? `${error.response.status} - ${error.response.statusText}`
+              : error.response.statusText || error.response.status
+            notificationStore.alert(alertMessage, 'error', {
+              action: {
+                label: 'View details',
+                callback: () => ({ request: axiosOptions, error: { status, message } }),
+              },
+            })
           }
 
           if (error.request.responseURL.indexOf('/proxy/') === -1) {
@@ -174,8 +180,19 @@ class ApiCaller {
           // The request was made but no response was received
           // `error.request` is an instance of XMLHttpRequest
           if (!isOnLoginPage() && !options.quietError) {
-            notificationStore.alert(`Request failed, there might be a problem with the connection to the server.
-              ${truncateUrl(options.url)}`, 'error')
+            notificationStore.alert(
+              `Request failed, there might be a problem with the connection to the server. ${truncateUrl(options.url)}`,
+              'error',
+              {
+                action: {
+                  label: 'View details',
+                  callback: () => ({
+                    request: axiosOptions,
+                    error: { message: 'Request was made but no response was received' },
+                  }),
+                },
+              }
+            )
           }
           logger.log({
             url: axiosOptions.url,
@@ -207,8 +224,19 @@ class ApiCaller {
             description: 'Something happened in setting up the request',
             requestStatus: 500,
           })
-          notificationStore.alert(`Request failed, there might be a problem with the connection to the server.
-            ${options.url}`, 'error')
+          notificationStore.alert(
+            `Request failed, there might be a problem with the connection to the server. ${truncateUrl(options.url)}`,
+            'error',
+            {
+              action: {
+                label: 'View details',
+                callback: () => ({
+                  request: axiosOptions,
+                  error: { message: 'Something happened in setting up the request' },
+                }),
+              },
+            }
+          )
         }
       })
     })

+ 18 - 0
src/utils/DomUtils.js

@@ -121,6 +121,24 @@ class DomUtils {
     downloadAnchorNode.click()
     downloadAnchorNode.remove()
   }
+  static jsonSyntaxHighlight(json: any) {
+    json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
+    return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, (match) => {
+      let cls = 'number'
+      if (/^"/.test(match)) {
+        if (/:$/.test(match)) {
+          cls = 'key'
+        } else {
+          cls = 'string'
+        }
+      } else if (/true|false/.test(match)) {
+        cls = 'boolean'
+      } else if (/null/.test(match)) {
+        cls = 'null'
+      }
+      return `<span class="${cls}">${match}</span>`
+    })
+  }
 }
 
 export default DomUtils