From 7485016f922ea4b15ddccb5f669aaa8da5bc0e95 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 4 Feb 2025 12:54:57 +0100 Subject: [PATCH] fix(ui): error handling --- frontend/app/api_client.ts | 70 ++++++--------- .../Client/ProfileSettings/OptOut.js | 27 +++--- .../Client/ProfileSettings/Settings.js | 9 +- .../Client/Projects/ProjectForm.tsx | 81 ++++++++--------- .../components/Client/Webhooks/WebhookForm.js | 32 +++---- frontend/app/mstore/projectsStore.ts | 6 +- frontend/app/mstore/settingsStore.ts | 86 +++++++++---------- frontend/app/mstore/userStore.ts | 2 +- frontend/app/services/ProjectsService.ts | 14 +-- 9 files changed, 150 insertions(+), 177 deletions(-) diff --git a/frontend/app/api_client.ts b/frontend/app/api_client.ts index a8ac9b3b2..dc1abcf5f 100644 --- a/frontend/app/api_client.ts +++ b/frontend/app/api_client.ts @@ -143,9 +143,13 @@ export default class APIClient { return this.refreshingTokenPromise; } - async fetch(path: string, params?: any, method: string = 'GET', options: { - clean?: boolean - } = { clean: true }, headers?: Record): Promise { + async fetch( + path: string, + params?: any, + method: string = 'GET', + options: { clean?: boolean } = { clean: true }, + headers?: Record + ): Promise { let _path = path; let jwt = this.getJwt(); if (!path.includes('/refresh') && jwt && this.isTokenExpired(jwt)) { @@ -157,58 +161,40 @@ export default class APIClient { if (params !== undefined) { const cleanedParams = options.clean ? clean(params) : params; - this.init.body = JSON.stringify(cleanedParams); + init.body = JSON.stringify(cleanedParams); + } + if (init.method === 'GET') { + delete init.body; } - if (this.init.method === 'GET') { - delete this.init.body; - } - - let fetch = window.fetch; let edp = window.env.API_EDP || window.location.origin + '/api'; - const noChalice = path.includes('v1/integrations') || path.includes('/spot') && !path.includes('/login') - if (noChalice && !edp.includes('api.openreplay.com')) { - edp = edp.replace('/api', '') - } if ( path !== '/targets_temp' && !path.includes('/metadata/session_search') && !path.includes('/assist/credentials') && - siteIdRequiredPaths.some(sidPath => path.startsWith(sidPath)) + siteIdRequiredPaths.some((sidPath) => path.startsWith(sidPath)) ) { - if (!this.siteId) console.trace('no site id', path, this.siteId) - edp = `${edp}/${this.siteId}`; + edp = `${edp}/${this.siteId ?? ''}`; } - - if ( - ( - path.includes('login') - || path.includes('refresh') - || path.includes('logout') - || path.includes('reset') - ) && window.env.NODE_ENV !== 'development' - ) { - init.credentials = 'include'; - } else { - delete init.credentials; - } - if (path.includes('PROJECT_ID')) { _path = _path.replace('PROJECT_ID', this.siteId + ''); } - return fetch(edp + _path, init).then((response) => { - if (response.status === 403) { - console.warn('API returned 403. Clearing JWT token.'); - this.onUpdateJwt({ jwt: undefined }); // Clear the token - } - - if (response.ok) { - return response; - } else { - return Promise.reject({ message: `! ${this.init.method} error on ${path}; ${response.status}`, response }); - } - }) + const fullUrl = edp + _path; + const response = await window.fetch(fullUrl, init); + if (response.status === 403) { + console.warn('API returned 403. Clearing JWT token.'); + this.onUpdateJwt({ jwt: undefined }); + } + if (response.ok) { + return response; + } + let errorMsg = `Something went wrong. Status: ${response.status}`; + try { + const errorData = await response.json(); + errorMsg = errorData.errors?.[0] || errorMsg; + } catch {} + throw new Error(errorMsg); } async refreshToken(): Promise { diff --git a/frontend/app/components/Client/ProfileSettings/OptOut.js b/frontend/app/components/Client/ProfileSettings/OptOut.js index 7cc271394..5fe44c0d1 100644 --- a/frontend/app/components/Client/ProfileSettings/OptOut.js +++ b/frontend/app/components/Client/ProfileSettings/OptOut.js @@ -1,29 +1,36 @@ -import React from 'react' -import { Checkbox } from 'UI' -import { observer } from 'mobx-react-lite' -import { useStore } from "App/mstore"; +import React from 'react'; +import { Checkbox } from 'UI'; +import { observer } from 'mobx-react-lite'; +import { useStore } from 'App/mstore'; +import { toast } from 'react-toastify'; function OptOut() { const { userStore } = useStore(); - const optOut = userStore.account.optOut; const updateClient = userStore.updateClient; + const [optOut, setOptOut] = React.useState(userStore.account.optOut); const onChange = () => { - void updateClient({ optOut: !optOut }); - } + setOptOut(!optOut); + void updateClient({ optOut: !optOut }).then(() => { + toast('Account settings updated successfully', { type: 'success' }); + }).catch((e) => { + toast(e.message || 'Failed to update account settings', { type: 'error' }); + setOptOut(optOut); + }); + }; return (
- ) + ); } export default observer(OptOut); diff --git a/frontend/app/components/Client/ProfileSettings/Settings.js b/frontend/app/components/Client/ProfileSettings/Settings.js index e6c5a6b96..2852fd796 100644 --- a/frontend/app/components/Client/ProfileSettings/Settings.js +++ b/frontend/app/components/Client/ProfileSettings/Settings.js @@ -3,6 +3,7 @@ import { Button, Input, Form } from 'UI'; import styles from './profileSettings.module.css'; import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; +import { toast } from 'react-toastify'; function Settings() { const { userStore } = useStore(); @@ -26,8 +27,12 @@ function Settings() { const handleSubmit = async (e) => { e.preventDefault(); - await updateClient({ name: accountName, tenantName: organizationName }); - setChanged(false); + await updateClient({ name: accountName, tenantName: organizationName }).then(() => { + setChanged(false); + toast('Profile settings updated successfully', { type: 'success' }); + }).catch((e) => { + toast(e.message || 'Failed to update account settings', { type: 'error' }); + }); } return ( diff --git a/frontend/app/components/Client/Projects/ProjectForm.tsx b/frontend/app/components/Client/Projects/ProjectForm.tsx index 9eb652685..0c2dd3cee 100644 --- a/frontend/app/components/Client/Projects/ProjectForm.tsx +++ b/frontend/app/components/Client/Projects/ProjectForm.tsx @@ -31,20 +31,11 @@ function ProjectForm(props: Props) { projectsStore .updateProject(project.id, project) .then((response: any) => { - if (!response || !response.errors || response.errors.size === 0) { - if (onClose) { - onClose(null); - } - // if (!pathname.includes('onboarding')) { - // void projectsStore.fetchList(); - // } - toast.success('Project updated successfully'); - if (onClose) { - onClose(null); - } - } else { - toast.error(response.errors[0]); - } + toast.success('Project updated successfully'); + onClose?.(null); + }) + .catch((error: Error) => { + toast.error(error.message || 'An error occurred while updating the project'); }); } else { projectsStore @@ -59,8 +50,8 @@ function ProjectForm(props: Props) { projectsStore.setConfigProject(parseInt(resp.id!)); }) - .catch((error: string) => { - toast.error(error || 'An error occurred while creating the project'); + .catch((error: Error) => { + toast.error(error.message || 'An error occurred while creating the project'); }); } }; @@ -78,6 +69,8 @@ function ProjectForm(props: Props) { if (project.id === projectsStore.active?.id) { projectsStore.setSiteId(projectStore.list[0].id!); } + }).catch((error: Error) => { + toast.error(error.message || 'An error occurred while deleting the project'); }); } }); @@ -86,7 +79,7 @@ function ProjectForm(props: Props) { const handleCancel = () => { form.resetFields(); if (onClose) { - onClose(null); + onClose(null); } }; @@ -104,7 +97,7 @@ function ProjectForm(props: Props) { label="Name" name="name" rules={[{ required: true, message: 'Please enter a name' }]} - className='font-medium' + className="font-medium" > - +
-
- - -
- {project.exists() && ( - +
+ +
+ {project.exists() && ( + + )}
diff --git a/frontend/app/components/Client/Webhooks/WebhookForm.js b/frontend/app/components/Client/Webhooks/WebhookForm.js index 08799456f..9cdc50708 100644 --- a/frontend/app/components/Client/Webhooks/WebhookForm.js +++ b/frontend/app/components/Client/Webhooks/WebhookForm.js @@ -1,5 +1,6 @@ import React from 'react'; -import { Form, Button, Input } from 'UI'; +import { Input } from 'UI'; +import { Button, Form } from 'antd'; import styles from './webhookForm.module.css'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; @@ -7,7 +8,7 @@ import { toast } from 'react-toastify'; function WebhookForm(props) { const { settingsStore } = useStore(); - const { webhookInst: webhook, hooksLoading: loading, saveWebhook, editWebhook } = settingsStore; + const { webhookInst: webhook, saveWebhook, editWebhook, saving } = settingsStore; const write = ({ target: { value, name } }) => editWebhook({ [name]: value }); const save = () => { @@ -16,14 +17,7 @@ function WebhookForm(props) { props.onClose(); }) .catch((e) => { - const baseStr = 'Error saving webhook'; - if (e.response) { - e.response.json().then(({ errors }) => { - toast.error(baseStr + ': ' + errors.join(',')); - }); - } else { - toast.error(baseStr); - } + toast.error(e.message || 'Failed to save webhook'); }); }; @@ -31,7 +25,7 @@ function WebhookForm(props) {

{webhook.exists() ? 'Update' : 'Add'} Webhook

- + - + - + - + - + - +
)} diff --git a/frontend/app/mstore/projectsStore.ts b/frontend/app/mstore/projectsStore.ts index 483e2d271..af47a1cf1 100644 --- a/frontend/app/mstore/projectsStore.ts +++ b/frontend/app/mstore/projectsStore.ts @@ -195,8 +195,8 @@ export default class ProjectsStore { this.setSiteId(this.list[0].id!); } }); - } catch (e) { - console.error('Failed to remove project:', e); + } catch (error) { + throw error || new Error('An error occurred while deleting the project.'); } finally { this.setLoading(false); } @@ -217,7 +217,7 @@ export default class ProjectsStore { } }); } catch (error) { - console.error('Failed to update site:', error); + throw error || new Error('An error occurred while updating the project.'); } finally { this.setLoading(false); } diff --git a/frontend/app/mstore/settingsStore.ts b/frontend/app/mstore/settingsStore.ts index 2c84ac202..a0f4f547c 100644 --- a/frontend/app/mstore/settingsStore.ts +++ b/frontend/app/mstore/settingsStore.ts @@ -22,12 +22,13 @@ export default class SettingsStore { webhooks: Webhook[] = []; webhookInst = new Webhook(); hooksLoading = false; + saving: boolean = false; gettingStarted: GettingStarted = new GettingStarted(); menuCollapsed: boolean = localStorage.getItem(MENU_COLLAPSED) === 'true'; constructor() { makeAutoObservable(this, { - sessionSettings: observable, + sessionSettings: observable }); } @@ -43,7 +44,7 @@ export default class SettingsStore { .then(({ data }) => { this.sessionSettings.merge({ captureRate: data.rate, - conditionalCapture: data.conditionalCapture, + conditionalCapture: data.conditionalCapture }); toast.success('Settings updated successfully'); }) @@ -59,7 +60,7 @@ export default class SettingsStore { .then((data) => { this.sessionSettings.merge({ captureRate: data.rate, - conditionalCapture: data.conditionalCapture, + conditionalCapture: data.conditionalCapture }); this.captureRateFetched = true; }) @@ -68,20 +69,19 @@ export default class SettingsStore { }); }; - fetchCaptureConditions = (projectId: number): Promise => { + fetchCaptureConditions = async (projectId: number): Promise => { this.loadingCaptureRate = true; - return sessionService - .fetchCaptureConditions(projectId) - .then((data) => { - this.sessionSettings.merge({ - captureRate: data.rate, - conditionalCapture: data.conditionalCapture, - captureConditions: data.conditions, - }); - }) - .finally(() => { - this.loadingCaptureRate = false; + try { + const data = await sessionService + .fetchCaptureConditions(projectId); + this.sessionSettings.merge({ + captureRate: data.rate, + conditionalCapture: data.conditionalCapture, + captureConditions: data.conditions }); + } finally { + this.loadingCaptureRate = false; + } }; updateCaptureConditions = (projectId: number, data: CaptureConditions) => { @@ -101,14 +101,14 @@ export default class SettingsStore { this.sessionSettings.merge({ captureRate: data.rate, conditionalCapture: data.conditionalCapture, - captureConditions: data.conditions, + captureConditions: data.conditions }); try { projectStore.syncProjectInList({ id: projectId + '', - sampleRate: data.rate, - }) + sampleRate: data.rate + }); } catch (e) { console.error('Failed to update project in list:', e); } @@ -122,46 +122,44 @@ export default class SettingsStore { }); }; - fetchWebhooks = () => { + fetchWebhooks = async () => { this.hooksLoading = true; - return webhookService.fetchList().then((data) => { - this.webhooks = data.map((hook) => new Webhook(hook)); - this.hooksLoading = false; - }); + const data = await webhookService.fetchList(); + this.webhooks = data.map((hook) => new Webhook(hook)); + this.hooksLoading = false; }; initWebhook = (inst?: Partial | Webhook) => { this.webhookInst = inst instanceof Webhook ? inst : new Webhook(inst); }; - saveWebhook = (inst: Webhook) => { - this.hooksLoading = true; - return webhookService - .saveWebhook(inst) - .then((data) => { - this.webhookInst = new Webhook(data); - if (inst.webhookId === undefined) this.setWebhooks([...this.webhooks, this.webhookInst]); - else - this.setWebhooks([ - ...this.webhooks.filter((hook) => hook.webhookId !== data.webhookId), - this.webhookInst, - ]); - }) - .finally(() => { - this.hooksLoading = false; - }); + saveWebhook = async (inst: Webhook) => { + this.saving = true; + try { + const data = await webhookService.saveWebhook(inst); + this.webhookInst = new Webhook(data); + if (inst.webhookId === undefined) { + this.setWebhooks([...this.webhooks, this.webhookInst]); + } else { + this.setWebhooks([ + ...this.webhooks.filter((hook) => hook.webhookId !== data.webhookId), + this.webhookInst + ]); + } + } finally { + this.saving = false; + } }; setWebhooks = (webhooks: Webhook[]) => { this.webhooks = webhooks; }; - removeWebhook = (hookId: string) => { + removeWebhook = async (hookId: string) => { this.hooksLoading = true; - return webhookService.removeWebhook(hookId).then(() => { - this.webhooks = this.webhooks.filter((hook) => hook.webhookId !== hookId); - this.hooksLoading = false; - }); + await webhookService.removeWebhook(hookId); + this.webhooks = this.webhooks.filter((hook) => hook.webhookId !== hookId); + this.hooksLoading = false; }; editWebhook = (diff: Partial) => { diff --git a/frontend/app/mstore/userStore.ts b/frontend/app/mstore/userStore.ts index b8bee0386..617323bbb 100644 --- a/frontend/app/mstore/userStore.ts +++ b/frontend/app/mstore/userStore.ts @@ -492,7 +492,7 @@ class UserStore { }); }); } catch (error) { - // TODO error handling + throw error; } finally { runInAction(() => { this.loading = false; diff --git a/frontend/app/services/ProjectsService.ts b/frontend/app/services/ProjectsService.ts index 393bcb835..b661ba141 100644 --- a/frontend/app/services/ProjectsService.ts +++ b/frontend/app/services/ProjectsService.ts @@ -20,17 +20,8 @@ export default class ProjectsService extends BaseService { }; saveProject = async (projectData: any): Promise => { - try { - const response = await this.client.post('/projects', projectData); - return response.json(); - } catch (error: any) { - if (error.response) { - const errorData = await error.response.json(); - throw errorData.errors?.[0] || 'An error occurred while saving the project.'; - } - - throw 'An unexpected error occurred.'; - } + const response = await this.client.post('/projects', projectData); + return response.json(); }; removeProject = async (projectId: string) => { @@ -41,7 +32,6 @@ export default class ProjectsService extends BaseService { updateProject = async (projectId: string, projectData: any) => { const r = await this.client.put(`/projects/${projectId}`, projectData); - return await r.json(); }; }