From 74d7dd6603002b7f3b2411db5c4cf7518bad864a Mon Sep 17 00:00:00 2001
From: Wang Sijie <wangsijie@silverhand.io>
Date: Mon, 14 Mar 2022 11:17:04 +0800
Subject: [PATCH] feat(console): wrap ky to handle response error (#370)

* feat(console): wrap ky to handle response error

* fix: handle error without body
---
 .../src/pages/ApiResourceDetails/index.tsx    |  4 +--
 .../components/CreateForm/index.tsx           |  4 +--
 .../components/CreateForm/index.tsx           |  4 +--
 .../src/pages/ConnectorDetails/index.tsx      | 15 ++++------
 packages/console/src/utilities/api.ts         | 29 +++++++++++++++++++
 packages/phrases/src/locales/en.ts            |  3 ++
 packages/phrases/src/locales/zh-cn.ts         |  3 ++
 7 files changed, 47 insertions(+), 15 deletions(-)
 create mode 100644 packages/console/src/utilities/api.ts

diff --git a/packages/console/src/pages/ApiResourceDetails/index.tsx b/packages/console/src/pages/ApiResourceDetails/index.tsx
index ddc035b21..4378f1f81 100644
--- a/packages/console/src/pages/ApiResourceDetails/index.tsx
+++ b/packages/console/src/pages/ApiResourceDetails/index.tsx
@@ -1,5 +1,4 @@
 import { Resource } from '@logto/schemas';
-import ky from 'ky';
 import React, { useEffect, useState } from 'react';
 import { useForm } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
@@ -15,6 +14,7 @@ import ImagePlaceholder from '@/components/ImagePlaceholder';
 import TabNav, { TabNavLink } from '@/components/TabNav';
 import TextInput from '@/components/TextInput';
 import { RequestError } from '@/swr';
+import api from '@/utilities/api';
 
 import * as styles from './index.module.scss';
 
@@ -50,7 +50,7 @@ const ApiResourceDetails = () => {
     setSubmitting(true);
 
     try {
-      const updatedApiResource = await ky
+      const updatedApiResource = await api
         .patch(`/api/resources/${data.id}`, { json: formData })
         .json<Resource>();
       void mutate(updatedApiResource);
diff --git a/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx b/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx
index fbda8ecfb..991246a13 100644
--- a/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx
+++ b/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx
@@ -1,5 +1,4 @@
 import { Resource } from '@logto/schemas';
-import ky from 'ky';
 import React from 'react';
 import { useForm } from 'react-hook-form';
 
@@ -9,6 +8,7 @@ import CardTitle from '@/components/CardTitle';
 import FormField from '@/components/FormField';
 import TextInput from '@/components/TextInput';
 import Close from '@/icons/Close';
+import api from '@/utilities/api';
 
 import * as styles from './index.module.scss';
 
@@ -26,7 +26,7 @@ const CreateForm = ({ onClose }: Props) => {
 
   const onSubmit = handleSubmit(async (data) => {
     try {
-      const createdApiResource = await ky.post('/api/resources', { json: data }).json<Resource>();
+      const createdApiResource = await api.post('/api/resources', { json: data }).json<Resource>();
       onClose?.(createdApiResource);
     } catch (error: unknown) {
       console.error(error);
diff --git a/packages/console/src/pages/Applications/components/CreateForm/index.tsx b/packages/console/src/pages/Applications/components/CreateForm/index.tsx
index 073593a8b..515921fb9 100644
--- a/packages/console/src/pages/Applications/components/CreateForm/index.tsx
+++ b/packages/console/src/pages/Applications/components/CreateForm/index.tsx
@@ -1,5 +1,4 @@
 import { Application, ApplicationType } from '@logto/schemas';
-import ky from 'ky';
 import React from 'react';
 import { useController, useForm } from 'react-hook-form';
 import { useTranslation } from 'react-i18next';
@@ -12,6 +11,7 @@ import RadioGroup, { Radio } from '@/components/RadioGroup';
 import TextInput from '@/components/TextInput';
 import Close from '@/icons/Close';
 import { applicationTypeI18nKey } from '@/types/applications';
+import api from '@/utilities/api';
 
 import TypeDescription from '../TypeDescription';
 import * as styles from './index.module.scss';
@@ -40,7 +40,7 @@ const CreateForm = ({ onClose }: Props) => {
 
   const onSubmit = handleSubmit(async (data) => {
     try {
-      const createdApp = await ky.post('/api/applications', { json: data }).json<Application>();
+      const createdApp = await api.post('/api/applications', { json: data }).json<Application>();
 
       onClose?.(createdApp);
     } catch (error: unknown) {
diff --git a/packages/console/src/pages/ConnectorDetails/index.tsx b/packages/console/src/pages/ConnectorDetails/index.tsx
index c69a6c95c..a1739ead0 100644
--- a/packages/console/src/pages/ConnectorDetails/index.tsx
+++ b/packages/console/src/pages/ConnectorDetails/index.tsx
@@ -1,5 +1,4 @@
-import { ConnectorDTO, ConnectorType, RequestErrorBody } from '@logto/schemas';
-import ky, { HTTPError } from 'ky';
+import { ConnectorDTO, ConnectorType } from '@logto/schemas';
 import React, { useEffect, useState } from 'react';
 import { toast } from 'react-hot-toast';
 import { useTranslation } from 'react-i18next';
@@ -17,6 +16,7 @@ import TabNav, { TabNavLink } from '@/components/TabNav';
 import Close from '@/icons/Close';
 import * as drawerStyles from '@/scss/drawer.module.scss';
 import { RequestError } from '@/swr';
+import api from '@/utilities/api';
 
 import SenderTester from './components/SenderTester';
 import * as styles from './index.module.scss';
@@ -56,18 +56,15 @@ const ConnectorDetails = () => {
     try {
       const configJson = JSON.parse(config) as JSON;
       setIsSubmitLoading(true);
-      await ky
-        .patch(`/api/connectors/${connectorId}`, { json: { config: configJson } })
+      await api
+        .patch(`/api/connectors/${connectorId}`, {
+          json: { config: configJson },
+        })
         .json<ConnectorDTO>();
       toast.success(t('connector_details.save_success'));
     } catch (error: unknown) {
       if (error instanceof SyntaxError) {
         setSaveError(t('connector_details.save_error_json_parse_error'));
-      } else if (error instanceof HTTPError) {
-        const { message } = (await error.response.json()) as RequestErrorBody;
-        setSaveError(message);
-      } else {
-        console.error(error);
       }
     }
 
diff --git a/packages/console/src/utilities/api.ts b/packages/console/src/utilities/api.ts
new file mode 100644
index 000000000..a9605060b
--- /dev/null
+++ b/packages/console/src/utilities/api.ts
@@ -0,0 +1,29 @@
+import { RequestErrorBody } from '@logto/schemas';
+import { t } from 'i18next';
+import ky from 'ky';
+import { toast } from 'react-hot-toast';
+
+const toastError = async (response: Response) => {
+  try {
+    const data = (await response.json()) as RequestErrorBody;
+    toast.error(data.message || t('admin_console.errors.unknown_server_error'));
+  } catch {
+    toast.error(t('admin_console.errors.unknown_server_error'));
+  }
+};
+
+const api = ky.create({
+  hooks: {
+    beforeError: [
+      (error) => {
+        const { response } = error;
+
+        void toastError(response);
+
+        return error;
+      },
+    ],
+  },
+});
+
+export default api;
diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts
index 83f839c64..6302d6c1b 100644
--- a/packages/phrases/src/locales/en.ts
+++ b/packages/phrases/src/locales/en.ts
@@ -25,6 +25,9 @@ const translation = {
     form: {
       required: 'Required',
     },
+    errors: {
+      unknown_server_error: 'Unknown server error occurred.',
+    },
     tab_sections: {
       overview: 'Overview',
       resource_management: 'Resource Management',
diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts
index 846bde195..2dae60e97 100644
--- a/packages/phrases/src/locales/zh-cn.ts
+++ b/packages/phrases/src/locales/zh-cn.ts
@@ -27,6 +27,9 @@ const translation = {
     form: {
       required: '必填',
     },
+    errors: {
+      unknown_server_error: '服务器发生未知错误。',
+    },
     tab_sections: {
       overview: '概览',
       resource_management: '资源管理',