fix(ui): error handling
This commit is contained in:
parent
ff6342298e
commit
7485016f92
9 changed files with 150 additions and 177 deletions
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>) => {
|
||||
|
|
|
|||
|
|
@ -492,7 +492,7 @@ class UserStore {
|
|||
});
|
||||
});
|
||||
} catch (error) {
|
||||
// TODO error handling
|
||||
throw error;
|
||||
} finally {
|
||||
runInAction(() => {
|
||||
this.loading = false;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue