|
| 1 | +import { defineMessages } from 'react-intl'; |
| 2 | +import type { MessageDescriptor } from 'react-intl'; |
| 3 | + |
| 4 | +import { AxiosError } from 'axios'; |
| 5 | +import type { AxiosResponse } from 'axios'; |
| 6 | + |
| 7 | +interface Alert { |
| 8 | + title: string | MessageDescriptor; |
| 9 | + message: string | MessageDescriptor; |
| 10 | + values?: Record<string, string | number | Date>; |
| 11 | +} |
| 12 | + |
| 13 | +interface ApiErrorResponse { |
| 14 | + error?: string; |
| 15 | +} |
| 16 | + |
| 17 | +const messages = defineMessages({ |
| 18 | + unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, |
| 19 | + unexpectedMessage: { |
| 20 | + id: 'alert.unexpected.message', |
| 21 | + defaultMessage: 'An unexpected error occurred.', |
| 22 | + }, |
| 23 | + rateLimitedTitle: { |
| 24 | + id: 'alert.rate_limited.title', |
| 25 | + defaultMessage: 'Rate limited', |
| 26 | + }, |
| 27 | + rateLimitedMessage: { |
| 28 | + id: 'alert.rate_limited.message', |
| 29 | + defaultMessage: 'Please retry after {retry_time, time, medium}.', |
| 30 | + }, |
| 31 | +}); |
| 32 | + |
| 33 | +export const ALERT_SHOW = 'ALERT_SHOW'; |
| 34 | +export const ALERT_DISMISS = 'ALERT_DISMISS'; |
| 35 | +export const ALERT_CLEAR = 'ALERT_CLEAR'; |
| 36 | +export const ALERT_NOOP = 'ALERT_NOOP'; |
| 37 | + |
| 38 | +export const dismissAlert = (alert: Alert) => ({ |
| 39 | + type: ALERT_DISMISS, |
| 40 | + alert, |
| 41 | +}); |
| 42 | + |
| 43 | +export const clearAlert = () => ({ |
| 44 | + type: ALERT_CLEAR, |
| 45 | +}); |
| 46 | + |
| 47 | +export const showAlert = (alert: Alert) => ({ |
| 48 | + type: ALERT_SHOW, |
| 49 | + alert, |
| 50 | +}); |
| 51 | + |
| 52 | +export const showAlertForError = (error: unknown, skipNotFound = false) => { |
| 53 | + if (error instanceof AxiosError && error.response) { |
| 54 | + const { status, statusText, headers } = error.response; |
| 55 | + const { data } = error.response as AxiosResponse<ApiErrorResponse>; |
| 56 | + |
| 57 | + // Skip these errors as they are reflected in the UI |
| 58 | + if (skipNotFound && (status === 404 || status === 410)) { |
| 59 | + return { type: ALERT_NOOP }; |
| 60 | + } |
| 61 | + |
| 62 | + // Rate limit errors |
| 63 | + if (status === 429 && headers['x-ratelimit-reset']) { |
| 64 | + return showAlert({ |
| 65 | + title: messages.rateLimitedTitle, |
| 66 | + message: messages.rateLimitedMessage, |
| 67 | + values: { |
| 68 | + retry_time: new Date(headers['x-ratelimit-reset'] as string), |
| 69 | + }, |
| 70 | + }); |
| 71 | + } |
| 72 | + |
| 73 | + return showAlert({ |
| 74 | + title: `${status}`, |
| 75 | + message: data.error ?? statusText, |
| 76 | + }); |
| 77 | + } |
| 78 | + |
| 79 | + // An aborted request, e.g. due to reloading the browser window, it not really error |
| 80 | + if (error instanceof AxiosError && error.code === AxiosError.ECONNABORTED) { |
| 81 | + return { type: ALERT_NOOP }; |
| 82 | + } |
| 83 | + |
| 84 | + console.error(error); |
| 85 | + |
| 86 | + return showAlert({ |
| 87 | + title: messages.unexpectedTitle, |
| 88 | + message: messages.unexpectedMessage, |
| 89 | + }); |
| 90 | +}; |
0 commit comments