فهرست منبع

Merge pull request #475 from smiclea/view-error-details

Add notifications view error details button
Nashwan Azhari 6 سال پیش
والد
کامیت
077fe3f521

+ 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