fix(ui): error handling

This commit is contained in:
Shekar Siri 2025-02-04 12:54:57 +01:00
parent ff6342298e
commit 7485016f92
9 changed files with 150 additions and 177 deletions

View file

@ -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<string, any>): Promise<Response> {
async fetch<T>(
path: string,
params?: any,
method: string = 'GET',
options: { clean?: boolean } = { clean: true },
headers?: Record<string, any>
): Promise<Response> {
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<string> {

View file

@ -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 (
<div>
<Checkbox
name="isPublic"
type="checkbox"
checked={ optOut }
onClick={ onChange }
checked={optOut}
onClick={onChange}
className="font-medium mr-8"
label="Anonymize"
/>
</div>
)
);
}
export default observer(OptOut);

View file

@ -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 (

View file

@ -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"
>
<Input
placeholder="Ex. OpenReplay"
@ -112,10 +105,10 @@ function ProjectForm(props: Props) {
maxLength={40}
value={project.name}
onChange={handleEdit}
className='font-normal rounded-lg'
className="font-normal rounded-lg"
/>
</Form.Item>
<Form.Item label="Project Type" className='font-medium'>
<Form.Item label="Project Type" className="font-medium">
<div>
<Segmented
options={[
@ -137,34 +130,34 @@ function ProjectForm(props: Props) {
</div>
</Form.Item>
<div className="mt-6 flex justify-between">
<div className='flex gap-0 items-center'>
<Button
htmlType="submit"
type="primary"
className="float-left mr-2 btn-add-edit-project"
loading={loading}
// disabled={!project.validate}
>
{project.exists() ? 'Save' : 'Add'}
</Button>
<Button
type="text"
onClick={handleCancel}
className="btn-cancel-project"
>
Cancel
</Button>
</div>
{project.exists() && (
<Tooltip title='Delete project' placement='top' >
<div className="flex gap-0 items-center">
<Button
htmlType="submit"
type="primary"
className="float-left mr-2 btn-add-edit-project"
loading={loading}
// disabled={!project.validate}
>
{project.exists() ? 'Save' : 'Add'}
</Button>
<Button
type="text"
onClick={handleRemove}
disabled={!canDelete}
className='btn-delete-project'
onClick={handleCancel}
className="btn-cancel-project"
>
<Icon name="trash" size="16" />
Cancel
</Button>
</div>
{project.exists() && (
<Tooltip title="Delete project" placement="top">
<Button
type="text"
onClick={handleRemove}
disabled={!canDelete}
className="btn-delete-project"
>
<Icon name="trash" size="16" />
</Button>
</Tooltip>
)}
</div>

View file

@ -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) {
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
<h3 className="p-5 text-2xl">{webhook.exists() ? 'Update' : 'Add'} Webhook</h3>
<Form className={styles.wrapper}>
<Form.Field>
<Form.Item>
<label>{'Name'}</label>
<Input
name="name"
@ -40,14 +34,14 @@ function WebhookForm(props) {
placeholder="Name"
maxLength={50}
/>
</Form.Field>
</Form.Item>
<Form.Field>
<Form.Item>
<label>{'Endpoint'}</label>
<Input name="endpoint" value={webhook.endpoint} onChange={write} placeholder="Endpoint" />
</Form.Field>
</Form.Item>
<Form.Field>
<Form.Item>
<label>{'Auth Header (optional)'}</label>
<Input
name="authHeader"
@ -55,15 +49,15 @@ function WebhookForm(props) {
onChange={write}
placeholder="Auth Header"
/>
</Form.Field>
</Form.Item>
<div className="flex justify-between">
<div className="flex items-center">
<Button
onClick={save}
disabled={!webhook.validate()}
loading={loading}
variant="primary"
loading={saving}
type="primary"
className="float-left mr-2"
>
{webhook.exists() ? 'Update' : 'Add'}
@ -73,7 +67,7 @@ function WebhookForm(props) {
{webhook.exists() && (
<Button
icon="trash"
variant="text"
type="text"
onClick={() => props.onDelete(webhook.webhookId)}
></Button>
)}

View file

@ -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);
}

View file

@ -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<any> => {
fetchCaptureConditions = async (projectId: number): Promise<any> => {
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<IWebhook> | 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<IWebhook>) => {

View file

@ -492,7 +492,7 @@ class UserStore {
});
});
} catch (error) {
// TODO error handling
throw error;
} finally {
runInAction(() => {
this.loading = false;

View file

@ -20,17 +20,8 @@ export default class ProjectsService extends BaseService {
};
saveProject = async (projectData: any): Promise<any> => {
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();
};
}