Procházet zdrojové kódy

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 před 6 roky
rodič
revize
5aa489c520

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

@@ -28,6 +28,7 @@ const CopyButtonStyled = styled(CopyButton)`
 `
 `
 const Wrapper = styled.div`
 const Wrapper = styled.div`
   cursor: pointer;
   cursor: pointer;
+  word-break: break-word;
 
 
   &:hover ${CopyButtonStyled} {
   &:hover ${CopyButtonStyled} {
     opacity: 1;
     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 styled, { injectGlobal } from 'styled-components'
 import NotificationSystem from 'react-notification-system'
 import NotificationSystem from 'react-notification-system'
 import { observe } from 'mobx'
 import { observe } from 'mobx'
+import type { AxiosXHRConfig } from 'axios'
 
 
 import notificationStore from '../../../stores/NotificationStore'
 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 NotificationsStyle from './style.js'
+import DomUtils from '../../../utils/DomUtils'
+
+import type { AlertInfo } from '../../../types/NotificationItem'
 
 
 injectGlobal`
 injectGlobal`
   ${NotificationsStyle}
   ${NotificationsStyle}
 `
 `
 
 
 const Wrapper = styled.div``
 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
 const MAX_NOTIFICATIONS = 3
 
 
 @observer
 @observer
-class Notifications extends React.Component<{}> {
+class Notifications extends React.Component<{}, State> {
+  state = {
+    errorInfo: null,
+  }
+
   notificationSystem: NotificationSystem
   notificationSystem: NotificationSystem
   notificationsCount = 0
   notificationsCount = 0
   activeNotifications: any[] = []
   activeNotifications: any[] = []
@@ -49,15 +96,27 @@ class Notifications extends React.Component<{}> {
     if (!alerts.length || alerts.length <= this.notificationsCount) {
     if (!alerts.length || alerts.length <= this.notificationsCount) {
       return
       return
     }
     }
-
     let lastNotification = alerts[alerts.length - 1]
     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({
     this.notificationSystem.addNotification({
       title: lastNotification.title || lastNotification.message,
       title: lastNotification.title || lastNotification.message,
       message: lastNotification.title ? lastNotification.message : null,
       message: lastNotification.title ? lastNotification.message : null,
       level: lastNotification.level || 'info',
       level: lastNotification.level || 'info',
       position: 'br',
       position: 'br',
-      autoDismiss: lastNotification.message.length < 150 ? 10 : 30,
-      action: lastNotification.options ? lastNotification.options.action : null,
+      autoDismiss,
+      action,
       onAdd: notification => {
       onAdd: notification => {
         this.activeNotifications.push(notification)
         this.activeNotifications.push(notification)
         for (let i = 0; i < this.activeNotifications.length - MAX_NOTIFICATIONS; i += 1) {
         for (let i = 0; i < this.activeNotifications.length - MAX_NOTIFICATIONS; i += 1) {
@@ -73,9 +132,55 @@ class Notifications extends React.Component<{}> {
   }
   }
 
 
   render() {
   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 (
     return (
       <Wrapper>
       <Wrapper>
         <NotificationSystem ref={(n) => { this.notificationSystem = n }} />
         <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>
       </Wrapper>
     )
     )
   }
   }

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

@@ -36,6 +36,9 @@ const NotificationsStyle = css`
     cursor: pointer;
     cursor: pointer;
     margin-left: 25px !important;
     margin-left: 25px !important;
   }
   }
+  .notification-error .notification-action-button {
+    background: ${Palette.secondaryLight} !important;
+  }
   .notification {
   .notification {
     border-radius: ${StyleProps.borderRadius} !important;
     border-radius: ${StyleProps.borderRadius} !important;
     background-color: ${Palette.grayscale[1]} !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
 // @flow
 
 
+export type AlertInfoLevel = 'success' | 'error' | 'info'
+
+export type AlertInfoOptions = {
+  action?: {
+    label: string,
+    callback: () => any,
+  }
+}
+
 export type AlertInfo = {
 export type AlertInfo = {
-  options?: {
-    action?: {
-      label: string,
-      callback: () => void,
-    }
-  },
+  options?: AlertInfoOptions,
   message: string,
   message: string,
   title?: string,
   title?: string,
   id?: string,
   id?: string,
-  level?: 'success' | 'error' | 'info',
+  level?: AlertInfoLevel,
 }
 }
 
 
 export type NotificationItemData = {
 export type NotificationItemData = {

+ 36 - 8
src/utils/ApiCaller.js

@@ -152,10 +152,16 @@ class ApiCaller {
             !options.quietError) {
             !options.quietError) {
             let data = error.response.data
             let data = error.response.data
             let message = (data && data.error && data.error.message) || (data && data.description)
             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) {
           if (error.request.responseURL.indexOf('/proxy/') === -1) {
@@ -174,8 +180,19 @@ class ApiCaller {
           // The request was made but no response was received
           // The request was made but no response was received
           // `error.request` is an instance of XMLHttpRequest
           // `error.request` is an instance of XMLHttpRequest
           if (!isOnLoginPage() && !options.quietError) {
           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({
           logger.log({
             url: axiosOptions.url,
             url: axiosOptions.url,
@@ -207,8 +224,19 @@ class ApiCaller {
             description: 'Something happened in setting up the request',
             description: 'Something happened in setting up the request',
             requestStatus: 500,
             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.click()
     downloadAnchorNode.remove()
     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
 export default DomUtils