tycho-components 0.38.7 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common/AppToast/AppToast.d.ts +1 -0
- package/dist/common/AppToast/AppToast.js +53 -20
- package/dist/common/AppToast/ToastMessage.d.ts +14 -4
- package/dist/common/AppToast/style.scss +3 -0
- package/dist/configs/CommonDispatchBridge.d.ts +2 -0
- package/dist/configs/CommonDispatchBridge.js +13 -0
- package/dist/configs/api/apiHandledError.d.ts +16 -0
- package/dist/configs/api/apiHandledError.js +22 -0
- package/dist/configs/api/applyAuthErrorHandling.d.ts +15 -0
- package/dist/configs/api/applyAuthErrorHandling.js +72 -0
- package/dist/configs/api/createApiClient.d.ts +16 -0
- package/dist/configs/api/createApiClient.js +59 -0
- package/dist/configs/api/dispatchApiError.d.ts +16 -0
- package/dist/configs/api/dispatchApiError.js +71 -0
- package/dist/configs/api/index.d.ts +18 -0
- package/dist/configs/api/index.js +18 -0
- package/dist/configs/api/resolveAccessDeniedMessage.d.ts +2 -0
- package/dist/configs/api/resolveAccessDeniedMessage.js +19 -0
- package/dist/configs/index.d.ts +5 -1
- package/dist/configs/index.js +3 -0
- package/dist/configs/localization/CommonTexts.d.ts +24 -0
- package/dist/configs/localization/CommonTexts.js +24 -0
- package/dist/configs/services/ErrorReportService.d.ts +34 -0
- package/dist/configs/services/ErrorReportService.js +63 -0
- package/dist/configs/store/commonDispatchBridge.d.ts +6 -0
- package/dist/configs/store/commonDispatchBridge.js +7 -0
- package/dist/configs/tour/index.d.ts +1 -0
- package/dist/configs/useMessageUtils.js +2 -2
- package/dist/features/ServerErrorReport/ServerErrorReport.d.ts +11 -0
- package/dist/features/ServerErrorReport/ServerErrorReport.js +49 -0
- package/dist/features/ServerErrorReport/index.d.ts +2 -0
- package/dist/features/ServerErrorReport/index.js +2 -0
- package/dist/features/ServerErrorReport/styles.scss +20 -0
- package/dist/shell/Base/ErrorBoundary.js +17 -13
- package/package.json +3 -2
- package/dist/configs/api.d.ts +0 -10
- package/dist/configs/api.js +0 -52
|
@@ -1,34 +1,65 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useContext, useEffect } from 'react';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useContext, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import ReactLoading from 'react-loading';
|
|
5
5
|
import { ToastContainer, toast } from 'react-toastify';
|
|
6
6
|
import 'react-toastify/dist/ReactToastify.css';
|
|
7
7
|
import CommonContext from '../../configs/CommonContext';
|
|
8
8
|
import { message } from '../../configs/store/actions';
|
|
9
|
+
import ServerErrorReport from '../../features/ServerErrorReport';
|
|
9
10
|
import { EMPTY_TOAST } from './ToastMessage';
|
|
11
|
+
import './style.scss';
|
|
10
12
|
export default function AppToast() {
|
|
11
13
|
const { t } = useTranslation('common');
|
|
12
14
|
const { dispatch, state } = useContext(CommonContext);
|
|
15
|
+
const [reportModalOpen, setReportModalOpen] = useState(false);
|
|
16
|
+
const [reportErrorContext, setReportErrorContext] = useState();
|
|
17
|
+
const openingReportModalRef = useRef(false);
|
|
13
18
|
const handleClose = () => {
|
|
14
19
|
toast.dismiss();
|
|
15
20
|
dispatch(message(EMPTY_TOAST));
|
|
21
|
+
if (openingReportModalRef.current) {
|
|
22
|
+
openingReportModalRef.current = false;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
setReportModalOpen(false);
|
|
26
|
+
setReportErrorContext(undefined);
|
|
16
27
|
};
|
|
17
28
|
const handleClipboard = () => {
|
|
18
29
|
navigator.clipboard.writeText(state.message.value);
|
|
19
30
|
};
|
|
31
|
+
const handleToastAction = () => {
|
|
32
|
+
if (state.message.action === 'report') {
|
|
33
|
+
setReportErrorContext(state.message.errorContext);
|
|
34
|
+
openingReportModalRef.current = true;
|
|
35
|
+
setReportModalOpen(true);
|
|
36
|
+
toast.dismiss();
|
|
37
|
+
dispatch(message(EMPTY_TOAST));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
handleClipboard();
|
|
41
|
+
};
|
|
42
|
+
const handleReportModalClose = () => {
|
|
43
|
+
setReportModalOpen(false);
|
|
44
|
+
setReportErrorContext(undefined);
|
|
45
|
+
};
|
|
46
|
+
const handleReportSent = () => {
|
|
47
|
+
setReportModalOpen(false);
|
|
48
|
+
setReportErrorContext(undefined);
|
|
49
|
+
dispatch(message({ value: t('server.error.report.sent'), type: 'success' }));
|
|
50
|
+
};
|
|
20
51
|
const getLoading = () => (_jsxs("div", { className: "d-flex", children: [_jsx(ReactLoading, { type: "spinningBubbles", color: "blue", height: 24, width: 24 }), _jsx("span", { className: "ms-3", children: state.toastLoadingText ?? t('generic.loading') })] }));
|
|
21
52
|
const attachCloseToEscape = () => {
|
|
22
53
|
const closeOnEscape = (e) => {
|
|
23
|
-
if (e.
|
|
54
|
+
if (e.key === 'Escape' && !reportModalOpen)
|
|
24
55
|
handleClose();
|
|
25
56
|
};
|
|
26
57
|
window.addEventListener('keydown', closeOnEscape);
|
|
27
58
|
return () => window.removeEventListener('keydown', closeOnEscape);
|
|
28
59
|
};
|
|
29
60
|
useEffect(() => {
|
|
30
|
-
attachCloseToEscape();
|
|
31
|
-
}, []);
|
|
61
|
+
return attachCloseToEscape();
|
|
62
|
+
}, [reportModalOpen]);
|
|
32
63
|
useEffect(() => {
|
|
33
64
|
if (state.toastLoading) {
|
|
34
65
|
toast(getLoading(), {
|
|
@@ -44,32 +75,34 @@ export default function AppToast() {
|
|
|
44
75
|
});
|
|
45
76
|
}
|
|
46
77
|
else {
|
|
47
|
-
toast.dismiss();
|
|
78
|
+
toast.dismiss('loading');
|
|
48
79
|
}
|
|
49
80
|
}, [state.toastLoading, state.toastLoadingText]);
|
|
50
81
|
useEffect(() => {
|
|
51
82
|
if (state.message && state.message.value !== '') {
|
|
83
|
+
const persistent = state.message.autoClose === false;
|
|
84
|
+
const toastOptions = {
|
|
85
|
+
onClose: () => handleClose(),
|
|
86
|
+
onClick: () => handleToastAction(),
|
|
87
|
+
...(persistent
|
|
88
|
+
? { autoClose: false, closeOnClick: false }
|
|
89
|
+
: {}),
|
|
90
|
+
...(state.message.action === 'report'
|
|
91
|
+
? { bodyClassName: 'toast-report-action' }
|
|
92
|
+
: {}),
|
|
93
|
+
};
|
|
52
94
|
switch (state.message.type) {
|
|
53
95
|
case 'error':
|
|
54
|
-
toast.error(state.message.value,
|
|
55
|
-
onClose: () => handleClose(),
|
|
56
|
-
onClick: () => handleClipboard(),
|
|
57
|
-
});
|
|
96
|
+
toast.error(state.message.value, toastOptions);
|
|
58
97
|
break;
|
|
59
98
|
case 'warning':
|
|
60
|
-
toast.warning(state.message.value,
|
|
61
|
-
onClose: () => handleClose(),
|
|
62
|
-
});
|
|
99
|
+
toast.warning(state.message.value, toastOptions);
|
|
63
100
|
break;
|
|
64
101
|
case 'success':
|
|
65
|
-
toast.success(state.message.value,
|
|
66
|
-
onClose: () => handleClose(),
|
|
67
|
-
});
|
|
102
|
+
toast.success(state.message.value, toastOptions);
|
|
68
103
|
break;
|
|
69
104
|
default:
|
|
70
|
-
toast(state.message.value,
|
|
71
|
-
onClose: () => handleClose(),
|
|
72
|
-
});
|
|
105
|
+
toast(state.message.value, toastOptions);
|
|
73
106
|
break;
|
|
74
107
|
}
|
|
75
108
|
}
|
|
@@ -77,5 +110,5 @@ export default function AppToast() {
|
|
|
77
110
|
dispatch(message(EMPTY_TOAST));
|
|
78
111
|
}
|
|
79
112
|
}, [state.message]);
|
|
80
|
-
return _jsx(ToastContainer, { closeOnClick:
|
|
113
|
+
return (_jsxs(_Fragment, { children: [_jsx(ToastContainer, { closeOnClick: state.message.action !== 'report' }), reportModalOpen && (_jsx(ServerErrorReport, { errorContext: reportErrorContext, corpus: state.corpus, onClose: handleReportModalClose, onSent: handleReportSent }))] }));
|
|
81
114
|
}
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
export type ToastAction = 'report' | 'copy';
|
|
2
|
+
export type ServerErrorContext = {
|
|
3
|
+
status?: number;
|
|
4
|
+
url?: string;
|
|
5
|
+
method?: string;
|
|
6
|
+
responseMessage?: string;
|
|
7
|
+
responseDescription?: string;
|
|
8
|
+
requestId?: string;
|
|
9
|
+
occurredAt?: string;
|
|
10
|
+
};
|
|
1
11
|
export default interface ToastMessage {
|
|
2
12
|
value: string;
|
|
3
13
|
type: string;
|
|
14
|
+
autoClose?: boolean;
|
|
15
|
+
action?: ToastAction;
|
|
16
|
+
errorContext?: ServerErrorContext;
|
|
4
17
|
}
|
|
5
|
-
export declare const EMPTY_TOAST:
|
|
6
|
-
value: string;
|
|
7
|
-
type: string;
|
|
8
|
-
};
|
|
18
|
+
export declare const EMPTY_TOAST: ToastMessage;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useContext, useEffect } from 'react';
|
|
2
|
+
import CommonContext from './CommonContext';
|
|
3
|
+
import { registerCommonDispatch } from './store/commonDispatchBridge';
|
|
4
|
+
/** Registers CommonContext.dispatch for non-React callers (e.g. axios interceptors). */
|
|
5
|
+
export default function CommonDispatchBridge() {
|
|
6
|
+
const { dispatch } = useContext(CommonContext);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
registerCommonDispatch(dispatch);
|
|
9
|
+
const noopDispatch = () => undefined;
|
|
10
|
+
return () => registerCommonDispatch(noopDispatch);
|
|
11
|
+
}, [dispatch]);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AxiosError } from 'axios';
|
|
2
|
+
export type ApiErrorHandled = AxiosError & {
|
|
3
|
+
apiErrorHandled?: boolean;
|
|
4
|
+
/** @deprecated use apiErrorHandled */
|
|
5
|
+
authErrorHandled?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function isServerError(status: number | undefined): boolean;
|
|
8
|
+
export declare function markApiErrorHandled(error: AxiosError): void;
|
|
9
|
+
/** True when the API client already surfaced a 5xx error via dispatchApiError. */
|
|
10
|
+
export declare function isHandledApiError(reason: unknown): boolean;
|
|
11
|
+
/** @deprecated use markApiErrorHandled */
|
|
12
|
+
export declare const markAuthErrorHandled: typeof markApiErrorHandled;
|
|
13
|
+
/** @deprecated use isHandledApiError */
|
|
14
|
+
export declare const isHandledAuthError: typeof isHandledApiError;
|
|
15
|
+
/** @deprecated use ApiErrorHandled */
|
|
16
|
+
export type AuthErrorHandled = ApiErrorHandled;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function isServerError(status) {
|
|
2
|
+
return status !== undefined && status >= 500 && status < 600;
|
|
3
|
+
}
|
|
4
|
+
export function markApiErrorHandled(error) {
|
|
5
|
+
error.apiErrorHandled = true;
|
|
6
|
+
}
|
|
7
|
+
/** True when the API client already surfaced a 5xx error via dispatchApiError. */
|
|
8
|
+
export function isHandledApiError(reason) {
|
|
9
|
+
if (!reason || typeof reason !== 'object')
|
|
10
|
+
return false;
|
|
11
|
+
const err = reason;
|
|
12
|
+
if (err.apiErrorHandled || err.authErrorHandled)
|
|
13
|
+
return true;
|
|
14
|
+
const status = err.response?.status;
|
|
15
|
+
if (!err.config || status === undefined)
|
|
16
|
+
return false;
|
|
17
|
+
return isServerError(status);
|
|
18
|
+
}
|
|
19
|
+
/** @deprecated use markApiErrorHandled */
|
|
20
|
+
export const markAuthErrorHandled = markApiErrorHandled;
|
|
21
|
+
/** @deprecated use isHandledApiError */
|
|
22
|
+
export const isHandledAuthError = isHandledApiError;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AxiosInstance } from 'axios';
|
|
2
|
+
export type AuthErrorHandling = 'toast' | 'redirect' | 'none';
|
|
3
|
+
export type ApplyAuthErrorHandlingOptions = {
|
|
4
|
+
authErrorHandling?: AuthErrorHandling;
|
|
5
|
+
/** HTTP statuses that trigger auth handling. Defaults to [401, 403]. */
|
|
6
|
+
authStatuses?: Array<401 | 403>;
|
|
7
|
+
redirect?: {
|
|
8
|
+
on401?: string;
|
|
9
|
+
on403?: string;
|
|
10
|
+
};
|
|
11
|
+
onBeforeRedirect?: (status: 401 | 403) => void;
|
|
12
|
+
skipAuthHandlingInStorybook?: boolean;
|
|
13
|
+
};
|
|
14
|
+
/** Opt-in 401/403 handling per app. Not part of the shared 5xx dispatchError path. */
|
|
15
|
+
export declare function applyAuthErrorHandling(api: AxiosInstance, options: ApplyAuthErrorHandlingOptions): AxiosInstance;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { markApiErrorHandled } from './apiHandledError';
|
|
2
|
+
import CookieStorage from '../CookieStorage';
|
|
3
|
+
import { resolveAccessDeniedMessage } from './resolveAccessDeniedMessage';
|
|
4
|
+
import { logged, message } from '../store/actions';
|
|
5
|
+
import { getCommonDispatch } from '../store/commonDispatchBridge';
|
|
6
|
+
const isStorybook = () => typeof window !== 'undefined' &&
|
|
7
|
+
(window.location.port === '6006' ||
|
|
8
|
+
window.__STORYBOOK__);
|
|
9
|
+
function defaultRedirectUrls() {
|
|
10
|
+
const publicUrl = import.meta.env.VITE_APP_PUBLIC_URL;
|
|
11
|
+
return {
|
|
12
|
+
on401: import.meta.env.VITE_APP_AUTH_URL,
|
|
13
|
+
on403: import.meta.env.VITE_APP_UNAUTHORIZED_URL ??
|
|
14
|
+
(publicUrl ? `${publicUrl}/unauthorized` : undefined),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function clearAuthStorage() {
|
|
18
|
+
CookieStorage.removeJwtToken();
|
|
19
|
+
sessionStorage.clear();
|
|
20
|
+
localStorage.clear();
|
|
21
|
+
}
|
|
22
|
+
function handleToastAuthError(status) {
|
|
23
|
+
if (status === 401) {
|
|
24
|
+
clearAuthStorage();
|
|
25
|
+
getCommonDispatch()?.(logged(undefined));
|
|
26
|
+
}
|
|
27
|
+
getCommonDispatch()?.(message({
|
|
28
|
+
value: resolveAccessDeniedMessage(),
|
|
29
|
+
type: 'error',
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
function handleRedirectAuthError(status, redirect, onBeforeRedirect, skipAuthHandlingInStorybook = true) {
|
|
33
|
+
if (skipAuthHandlingInStorybook && isStorybook()) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
onBeforeRedirect?.(status);
|
|
37
|
+
if (status === 401) {
|
|
38
|
+
clearAuthStorage();
|
|
39
|
+
if (redirect.on401) {
|
|
40
|
+
window.location.replace(redirect.on401);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (redirect.on403) {
|
|
45
|
+
window.location.href = redirect.on403;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Opt-in 401/403 handling per app. Not part of the shared 5xx dispatchError path. */
|
|
49
|
+
export function applyAuthErrorHandling(api, options) {
|
|
50
|
+
const { authErrorHandling = 'none', authStatuses = [401, 403], redirect: redirectOverrides, onBeforeRedirect, skipAuthHandlingInStorybook = true, } = options;
|
|
51
|
+
if (authErrorHandling === 'none') {
|
|
52
|
+
return api;
|
|
53
|
+
}
|
|
54
|
+
const redirect = { ...defaultRedirectUrls(), ...redirectOverrides };
|
|
55
|
+
api.interceptors.response.use((response) => response, async (error) => {
|
|
56
|
+
const status = error.response?.status;
|
|
57
|
+
if (status === 401 || status === 403) {
|
|
58
|
+
if (!authStatuses.includes(status)) {
|
|
59
|
+
return Promise.reject(error);
|
|
60
|
+
}
|
|
61
|
+
if (authErrorHandling === 'toast') {
|
|
62
|
+
handleToastAuthError(status);
|
|
63
|
+
}
|
|
64
|
+
else if (authErrorHandling === 'redirect') {
|
|
65
|
+
handleRedirectAuthError(status, redirect, onBeforeRedirect, skipAuthHandlingInStorybook);
|
|
66
|
+
}
|
|
67
|
+
markApiErrorHandled(error);
|
|
68
|
+
}
|
|
69
|
+
return Promise.reject(error);
|
|
70
|
+
});
|
|
71
|
+
return api;
|
|
72
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AxiosInstance, InternalAxiosRequestConfig } from 'axios';
|
|
2
|
+
export type ServerErrorHandling = 'toast' | 'none';
|
|
3
|
+
export type CreateApiClientOptions = {
|
|
4
|
+
baseURL: string;
|
|
5
|
+
attachJwt?: boolean;
|
|
6
|
+
/** How to surface 5xx responses. Defaults to 'toast'. */
|
|
7
|
+
serverErrorHandling?: ServerErrorHandling;
|
|
8
|
+
augmentRequest?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Shared API client: JWT attachment + global 5xx dispatchError toast.
|
|
12
|
+
* Handled 5xx responses resolve (they do not reach .catch()).
|
|
13
|
+
* 4xx are left to front-end callers unless the app opts in via
|
|
14
|
+
* applyAuthErrorHandling() in its local api.ts wrapper.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createApiClient(options: CreateApiClientOptions): AxiosInstance;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { isServerError, markApiErrorHandled } from './apiHandledError';
|
|
3
|
+
import CookieStorage from '../CookieStorage';
|
|
4
|
+
import { dispatchServerApiError } from './dispatchApiError';
|
|
5
|
+
const DEFAULT_HEADERS = {
|
|
6
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
7
|
+
Pragma: 'no-cache',
|
|
8
|
+
'Content-Type': 'application/json',
|
|
9
|
+
Accept: 'application/json',
|
|
10
|
+
};
|
|
11
|
+
function toHandledServerErrorResponse(error) {
|
|
12
|
+
const status = error.response?.status ?? 500;
|
|
13
|
+
return {
|
|
14
|
+
data: null,
|
|
15
|
+
status,
|
|
16
|
+
statusText: error.response?.statusText ?? 'Internal Server Error',
|
|
17
|
+
headers: error.response?.headers ?? {},
|
|
18
|
+
config: error.config,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Shared API client: JWT attachment + global 5xx dispatchError toast.
|
|
23
|
+
* Handled 5xx responses resolve (they do not reach .catch()).
|
|
24
|
+
* 4xx are left to front-end callers unless the app opts in via
|
|
25
|
+
* applyAuthErrorHandling() in its local api.ts wrapper.
|
|
26
|
+
*/
|
|
27
|
+
export function createApiClient(options) {
|
|
28
|
+
const { baseURL, attachJwt = true, serverErrorHandling = 'toast', augmentRequest, } = options;
|
|
29
|
+
const api = axios.create({
|
|
30
|
+
headers: DEFAULT_HEADERS,
|
|
31
|
+
baseURL,
|
|
32
|
+
});
|
|
33
|
+
api.interceptors.response.use((response) => response, async (error) => {
|
|
34
|
+
const status = error.response?.status;
|
|
35
|
+
if (isServerError(status) && serverErrorHandling === 'toast') {
|
|
36
|
+
dispatchServerApiError({ err: error });
|
|
37
|
+
markApiErrorHandled(error);
|
|
38
|
+
return toHandledServerErrorResponse(error);
|
|
39
|
+
}
|
|
40
|
+
if (error.status === null) {
|
|
41
|
+
console.log(JSON.stringify(error));
|
|
42
|
+
}
|
|
43
|
+
return Promise.reject(error);
|
|
44
|
+
});
|
|
45
|
+
api.interceptors.request.use((config) => {
|
|
46
|
+
if (!config?.headers) {
|
|
47
|
+
throw new Error('no header available');
|
|
48
|
+
}
|
|
49
|
+
if (attachJwt) {
|
|
50
|
+
const token = CookieStorage.getJwtToken();
|
|
51
|
+
if (token) {
|
|
52
|
+
// eslint-disable-next-line no-param-reassign
|
|
53
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return augmentRequest ? augmentRequest(config) : config;
|
|
57
|
+
});
|
|
58
|
+
return api;
|
|
59
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AxiosResponseHeaders, RawAxiosResponseHeaders } from 'axios';
|
|
2
|
+
import type { TFunction } from 'i18next';
|
|
3
|
+
type DispatchApiErrorOptions = {
|
|
4
|
+
err: unknown;
|
|
5
|
+
t?: TFunction;
|
|
6
|
+
key?: string;
|
|
7
|
+
autoClose?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function extractRequestId(headers?: RawAxiosResponseHeaders | AxiosResponseHeaders): string | undefined;
|
|
10
|
+
export declare function dispatchApiError({ err, t, key, autoClose, }: DispatchApiErrorOptions): void;
|
|
11
|
+
/** Persistent 5xx toast that opens the server-error report modal on click. */
|
|
12
|
+
export declare function dispatchServerApiError({ err, t, }: {
|
|
13
|
+
err: unknown;
|
|
14
|
+
t?: TFunction;
|
|
15
|
+
}): void;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import i18n from 'i18next';
|
|
2
|
+
import { getErrorMessageValue } from '../useMessageUtils';
|
|
3
|
+
import { message } from '../store/actions';
|
|
4
|
+
import { getCommonDispatch } from '../store/commonDispatchBridge';
|
|
5
|
+
const REQUEST_ID_HEADERS = [
|
|
6
|
+
'x-request-id',
|
|
7
|
+
'x-correlation-id',
|
|
8
|
+
'traceparent',
|
|
9
|
+
'x-amzn-trace-id',
|
|
10
|
+
];
|
|
11
|
+
function normalizeHeaderValue(value) {
|
|
12
|
+
if (typeof value === 'string' && value.trim()) {
|
|
13
|
+
return value.trim();
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(value) && typeof value[0] === 'string' && value[0].trim()) {
|
|
16
|
+
return value[0].trim();
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
export function extractRequestId(headers) {
|
|
21
|
+
if (!headers || typeof headers !== 'object') {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const normalized = Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
|
|
25
|
+
for (const header of REQUEST_ID_HEADERS) {
|
|
26
|
+
const value = normalizeHeaderValue(normalized[header]);
|
|
27
|
+
if (value) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
function buildServerErrorContext(err) {
|
|
34
|
+
if (!err || typeof err !== 'object' || !('response' in err)) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
const axiosErr = err;
|
|
38
|
+
const data = axiosErr.response?.data;
|
|
39
|
+
return {
|
|
40
|
+
status: axiosErr.response?.status,
|
|
41
|
+
url: axiosErr.config?.url,
|
|
42
|
+
method: axiosErr.config?.method?.toUpperCase(),
|
|
43
|
+
responseMessage: typeof data?.message === 'string' ? data.message : undefined,
|
|
44
|
+
responseDescription: typeof data?.description === 'string' ? data.description : undefined,
|
|
45
|
+
requestId: extractRequestId(axiosErr.response?.headers),
|
|
46
|
+
occurredAt: new Date().toISOString(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function dispatchApiError({ err, t, key, autoClose, }) {
|
|
50
|
+
const translate = t ?? i18n.t.bind(i18n);
|
|
51
|
+
const value = getErrorMessageValue({ err, t: translate, key });
|
|
52
|
+
getCommonDispatch()?.(message({
|
|
53
|
+
value,
|
|
54
|
+
type: 'error',
|
|
55
|
+
autoClose,
|
|
56
|
+
action: 'copy',
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
/** Persistent 5xx toast that opens the server-error report modal on click. */
|
|
60
|
+
export function dispatchServerApiError({ err, t, }) {
|
|
61
|
+
const translate = t ?? i18n.t.bind(i18n);
|
|
62
|
+
const value = translate('server.error.report.prompt', { ns: 'common' });
|
|
63
|
+
const errorContext = buildServerErrorContext(err);
|
|
64
|
+
getCommonDispatch()?.(message({
|
|
65
|
+
value,
|
|
66
|
+
type: 'error',
|
|
67
|
+
autoClose: false,
|
|
68
|
+
action: 'report',
|
|
69
|
+
errorContext,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Platform API roots (formerly separate env vars per service). */
|
|
2
|
+
export declare const platformApi: {
|
|
3
|
+
base: string;
|
|
4
|
+
revision: string;
|
|
5
|
+
catalog: string;
|
|
6
|
+
editor: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const parserApiBase: string;
|
|
9
|
+
declare const api: import("axios").AxiosInstance;
|
|
10
|
+
export { createApiClient } from './createApiClient';
|
|
11
|
+
export type { CreateApiClientOptions, ServerErrorHandling } from './createApiClient';
|
|
12
|
+
export { applyAuthErrorHandling } from './applyAuthErrorHandling';
|
|
13
|
+
export type { ApplyAuthErrorHandlingOptions, AuthErrorHandling, } from './applyAuthErrorHandling';
|
|
14
|
+
export { isHandledApiError, isHandledAuthError, markApiErrorHandled, markAuthErrorHandled, } from './apiHandledError';
|
|
15
|
+
export type { ApiErrorHandled, AuthErrorHandled } from './apiHandledError';
|
|
16
|
+
export { dispatchApiError, dispatchServerApiError } from './dispatchApiError';
|
|
17
|
+
export { resolveAccessDeniedMessage } from './resolveAccessDeniedMessage';
|
|
18
|
+
export default api;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { applyAuthErrorHandling } from './applyAuthErrorHandling';
|
|
2
|
+
import { createApiClient } from './createApiClient';
|
|
3
|
+
const platformApiBase = import.meta.env.VITE_APP_PLATFORM_API;
|
|
4
|
+
/** Platform API roots (formerly separate env vars per service). */
|
|
5
|
+
export const platformApi = {
|
|
6
|
+
base: platformApiBase,
|
|
7
|
+
revision: `${platformApiBase}/revision`,
|
|
8
|
+
catalog: `${platformApiBase}/catalog`,
|
|
9
|
+
editor: `${platformApiBase}/editor`,
|
|
10
|
+
};
|
|
11
|
+
export const parserApiBase = import.meta.env.VITE_APP_PARSER_API;
|
|
12
|
+
const api = applyAuthErrorHandling(createApiClient({ baseURL: platformApiBase }), { authErrorHandling: 'redirect' });
|
|
13
|
+
export { createApiClient } from './createApiClient';
|
|
14
|
+
export { applyAuthErrorHandling } from './applyAuthErrorHandling';
|
|
15
|
+
export { isHandledApiError, isHandledAuthError, markApiErrorHandled, markAuthErrorHandled, } from './apiHandledError';
|
|
16
|
+
export { dispatchApiError, dispatchServerApiError } from './dispatchApiError';
|
|
17
|
+
export { resolveAccessDeniedMessage } from './resolveAccessDeniedMessage';
|
|
18
|
+
export default api;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import i18n from 'i18next';
|
|
2
|
+
import { BaseTexts } from '../localization/BaseTexts';
|
|
3
|
+
const ACCESS_DENIED_KEY = 'error.access.authorization';
|
|
4
|
+
function resolveLocale() {
|
|
5
|
+
const lng = i18n.resolvedLanguage || i18n.language || 'en';
|
|
6
|
+
if (lng.startsWith('pt'))
|
|
7
|
+
return 'pt-BR';
|
|
8
|
+
if (lng.startsWith('it'))
|
|
9
|
+
return 'it';
|
|
10
|
+
return 'en';
|
|
11
|
+
}
|
|
12
|
+
/** Resolves the access-denied toast message, with BaseTexts fallback when i18n is not ready. */
|
|
13
|
+
export function resolveAccessDeniedMessage() {
|
|
14
|
+
const translated = i18n.t(ACCESS_DENIED_KEY, { ns: 'message' });
|
|
15
|
+
if (translated && translated !== ACCESS_DENIED_KEY) {
|
|
16
|
+
return translated;
|
|
17
|
+
}
|
|
18
|
+
return BaseTexts[resolveLocale()][ACCESS_DENIED_KEY];
|
|
19
|
+
}
|
package/dist/configs/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export { CommonProvider } from "./CommonContext";
|
|
2
2
|
export { default as CommonContext } from "./CommonContext";
|
|
3
|
+
export { default as CommonDispatchBridge } from "./CommonDispatchBridge";
|
|
3
4
|
export { default as CookieStorage } from "./CookieStorage";
|
|
5
|
+
export { createApiClient, applyAuthErrorHandling, dispatchApiError, dispatchServerApiError, resolveAccessDeniedMessage, isHandledApiError, isHandledAuthError, markApiErrorHandled, markAuthErrorHandled, platformApi, parserApiBase, } from "./api";
|
|
6
|
+
export type { ApplyAuthErrorHandlingOptions, AuthErrorHandling, CreateApiClientOptions, ServerErrorHandling, ApiErrorHandled, AuthErrorHandled, } from "./api";
|
|
7
|
+
export { getCommonDispatch, registerCommonDispatch, } from "./store/commonDispatchBridge";
|
|
4
8
|
export { commonResources, featureResources, mergeResources, commonLocalization, } from "./Localization";
|
|
5
9
|
export { default as ProfileService } from "./services/ProfileService";
|
|
6
10
|
export type { Corpus, CorpusRequest, Github } from "./types/Corpus";
|
|
@@ -30,4 +34,4 @@ export { useLoggedUtils } from "./useLoggedUtils";
|
|
|
30
34
|
export { useMessageUtils } from "./useMessageUtils";
|
|
31
35
|
export { useTourUtils } from "./useTourUtils";
|
|
32
36
|
export { useDriverTour, loadKnowledgeBase, defaultMatchPathnames, buildAnchorMap, flattenWorkflowSteps, getWorkflowsForPath, buildDriveSteps, createTourDriver, hasMatchingWorkflow, resolveAnchor, resolveAnchorById, waitForSelector, localizeKnowledgeBase, pickLocalizedString, resolveTourLocale, TOUR_FALLBACK_LOCALE, TOUR_LOCALES, } from "./tour";
|
|
33
|
-
export type { AnchorSelector, KnowledgeBase, KnowledgeBaseRaw, LocalizedString, PopoverSide, ResolveKnowledgeBaseUrl, TourAnchor, TourButtonLabels, TourStep, TourStepCopy, TourStepStructural, TourWorkflow, TourWorkflowRaw, UseDriverTourOptions, } from "./tour";
|
|
37
|
+
export type { AnchorSelector, Driver, KnowledgeBase, KnowledgeBaseRaw, LocalizedString, PopoverSide, ResolveKnowledgeBaseUrl, TourAnchor, TourButtonLabels, TourStep, TourStepCopy, TourStepStructural, TourWorkflow, TourWorkflowRaw, UseDriverTourOptions, } from "./tour";
|
package/dist/configs/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { CommonProvider } from "./CommonContext";
|
|
2
2
|
export { default as CommonContext } from "./CommonContext";
|
|
3
|
+
export { default as CommonDispatchBridge } from "./CommonDispatchBridge";
|
|
3
4
|
export { default as CookieStorage } from "./CookieStorage";
|
|
5
|
+
export { createApiClient, applyAuthErrorHandling, dispatchApiError, dispatchServerApiError, resolveAccessDeniedMessage, isHandledApiError, isHandledAuthError, markApiErrorHandled, markAuthErrorHandled, platformApi, parserApiBase, } from "./api";
|
|
6
|
+
export { getCommonDispatch, registerCommonDispatch, } from "./store/commonDispatchBridge";
|
|
4
7
|
export { commonResources, featureResources, mergeResources, commonLocalization, } from "./Localization";
|
|
5
8
|
export { default as ProfileService } from "./services/ProfileService";
|
|
6
9
|
export { CorpusStatusNames } from "./types/CorpusStatus";
|
|
@@ -42,6 +42,14 @@ export declare const CommonTexts: {
|
|
|
42
42
|
'user.status.visitor': string;
|
|
43
43
|
'update.success': string;
|
|
44
44
|
'internal.server.error': string;
|
|
45
|
+
'server.error.report.prompt': string;
|
|
46
|
+
'server.error.report.modal.title': string;
|
|
47
|
+
'server.error.report.modal.subtitle': string;
|
|
48
|
+
'server.error.report.modal.placeholder': string;
|
|
49
|
+
'server.error.report.modal.send': string;
|
|
50
|
+
'server.error.report.validation': string;
|
|
51
|
+
'server.error.report.send.failed': string;
|
|
52
|
+
'server.error.report.sent': string;
|
|
45
53
|
'table.label.rows-page': string;
|
|
46
54
|
'table.label.items': string;
|
|
47
55
|
'table.pagination.scroll-strip-left': string;
|
|
@@ -121,6 +129,14 @@ export declare const CommonTexts: {
|
|
|
121
129
|
'user.status.visitor': string;
|
|
122
130
|
'update.success': string;
|
|
123
131
|
'internal.server.error': string;
|
|
132
|
+
'server.error.report.prompt': string;
|
|
133
|
+
'server.error.report.modal.title': string;
|
|
134
|
+
'server.error.report.modal.subtitle': string;
|
|
135
|
+
'server.error.report.modal.placeholder': string;
|
|
136
|
+
'server.error.report.modal.send': string;
|
|
137
|
+
'server.error.report.validation': string;
|
|
138
|
+
'server.error.report.send.failed': string;
|
|
139
|
+
'server.error.report.sent': string;
|
|
124
140
|
'table.label.rows-page': string;
|
|
125
141
|
'table.label.items': string;
|
|
126
142
|
'table.pagination.scroll-strip-left': string;
|
|
@@ -200,6 +216,14 @@ export declare const CommonTexts: {
|
|
|
200
216
|
'user.status.visitor': string;
|
|
201
217
|
'update.success': string;
|
|
202
218
|
'internal.server.error': string;
|
|
219
|
+
'server.error.report.prompt': string;
|
|
220
|
+
'server.error.report.modal.title': string;
|
|
221
|
+
'server.error.report.modal.subtitle': string;
|
|
222
|
+
'server.error.report.modal.placeholder': string;
|
|
223
|
+
'server.error.report.modal.send': string;
|
|
224
|
+
'server.error.report.validation': string;
|
|
225
|
+
'server.error.report.send.failed': string;
|
|
226
|
+
'server.error.report.sent': string;
|
|
203
227
|
'table.label.rows-page': string;
|
|
204
228
|
'table.label.items': string;
|
|
205
229
|
'table.pagination.scroll-strip-left': string;
|
|
@@ -42,6 +42,14 @@ export const CommonTexts = {
|
|
|
42
42
|
'user.status.visitor': 'Visitor',
|
|
43
43
|
'update.success': 'saved!',
|
|
44
44
|
'internal.server.error': 'An unexpected error occurred. Contact the administrator.',
|
|
45
|
+
'server.error.report.prompt': 'Something went wrong on our end. Click here to tell us what happened.',
|
|
46
|
+
'server.error.report.modal.title': 'Report a problem',
|
|
47
|
+
'server.error.report.modal.subtitle': 'Describe what you were doing when the error occurred. We will use your message to investigate.',
|
|
48
|
+
'server.error.report.modal.placeholder': 'What were you trying to do? Any extra details help us reproduce the issue.',
|
|
49
|
+
'server.error.report.modal.send': 'Send report',
|
|
50
|
+
'server.error.report.validation': 'Please describe what happened before sending.',
|
|
51
|
+
'server.error.report.send.failed': 'We could not send your report. Please try again in a moment.',
|
|
52
|
+
'server.error.report.sent': 'Thank you — your report was sent.',
|
|
45
53
|
'table.label.rows-page': 'Items per page',
|
|
46
54
|
'table.label.items': '{{first}} - {{last}} of {{total}}',
|
|
47
55
|
'table.pagination.scroll-strip-left': 'Scroll page numbers left',
|
|
@@ -121,6 +129,14 @@ export const CommonTexts = {
|
|
|
121
129
|
'user.status.visitor': 'Visitante',
|
|
122
130
|
'update.success': 'salvo!',
|
|
123
131
|
'internal.server.error': 'Ocorreu um erro inesperado. Entre em contato com o administrador.',
|
|
132
|
+
'server.error.report.prompt': 'Algo deu errado do nosso lado. Clique aqui para nos contar o que aconteceu.',
|
|
133
|
+
'server.error.report.modal.title': 'Reportar um problema',
|
|
134
|
+
'server.error.report.modal.subtitle': 'Descreva o que você estava fazendo quando o erro ocorreu. Usaremos sua mensagem para investigar.',
|
|
135
|
+
'server.error.report.modal.placeholder': 'O que você estava tentando fazer? Detalhes extras nos ajudam a reproduzir o problema.',
|
|
136
|
+
'server.error.report.modal.send': 'Enviar relatório',
|
|
137
|
+
'server.error.report.validation': 'Descreva o que aconteceu antes de enviar.',
|
|
138
|
+
'server.error.report.send.failed': 'Não foi possível enviar seu relatório. Tente novamente em instantes.',
|
|
139
|
+
'server.error.report.sent': 'Obrigado — seu relatório foi enviado.',
|
|
124
140
|
'table.label.rows-page': 'Itens por página',
|
|
125
141
|
'table.label.items': '{{first}} - {{last}} de {{total}}',
|
|
126
142
|
'table.pagination.scroll-strip-left': 'Rolar números das páginas à esquerda',
|
|
@@ -200,6 +216,14 @@ export const CommonTexts = {
|
|
|
200
216
|
'user.status.visitor': 'Visitatore',
|
|
201
217
|
'update.success': 'salvato!',
|
|
202
218
|
'internal.server.error': "Si è verificato un errore inaspettato. Contatta l'amministratore.",
|
|
219
|
+
'server.error.report.prompt': 'Qualcosa è andato storto da parte nostra. Clicca qui per raccontarci cosa è successo.',
|
|
220
|
+
'server.error.report.modal.title': 'Segnala un problema',
|
|
221
|
+
'server.error.report.modal.subtitle': 'Descrivi cosa stavi facendo quando si è verificato l\'errore. Useremo il tuo messaggio per indagare.',
|
|
222
|
+
'server.error.report.modal.placeholder': 'Cosa stavi cercando di fare? Dettagli aggiuntivi ci aiutano a riprodurre il problema.',
|
|
223
|
+
'server.error.report.modal.send': 'Invia segnalazione',
|
|
224
|
+
'server.error.report.validation': 'Descrivi cosa è successo prima di inviare.',
|
|
225
|
+
'server.error.report.send.failed': 'Impossibile inviare la segnalazione. Riprova tra poco.',
|
|
226
|
+
'server.error.report.sent': 'Grazie — la tua segnalazione è stata inviata.',
|
|
203
227
|
'table.label.rows-page': 'Elementi per pagina',
|
|
204
228
|
'table.label.items': '{{first}} - {{last}} di {{total}}',
|
|
205
229
|
'table.pagination.scroll-strip-left': 'Scorri i numeri di pagina a sinistra',
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ServerErrorContext } from '../../common/AppToast/ToastMessage';
|
|
2
|
+
export type ReportSendContext = {
|
|
3
|
+
locale?: string;
|
|
4
|
+
corpusUid?: string;
|
|
5
|
+
corpusName?: string;
|
|
6
|
+
};
|
|
7
|
+
type ClientContext = {
|
|
8
|
+
timestamp?: string;
|
|
9
|
+
timezone?: string;
|
|
10
|
+
appUrl?: string;
|
|
11
|
+
environment?: string;
|
|
12
|
+
locale?: string;
|
|
13
|
+
corpusUid?: string;
|
|
14
|
+
corpusName?: string;
|
|
15
|
+
userAgent?: string;
|
|
16
|
+
viewport?: string;
|
|
17
|
+
screen?: string;
|
|
18
|
+
referrer?: string;
|
|
19
|
+
pageUrl?: string;
|
|
20
|
+
};
|
|
21
|
+
type ErrorReportPayload = {
|
|
22
|
+
userMessage: string;
|
|
23
|
+
errorContext?: ServerErrorContext;
|
|
24
|
+
sendContext?: ReportSendContext;
|
|
25
|
+
};
|
|
26
|
+
declare function send({ userMessage, errorContext, sendContext, }: ErrorReportPayload): Promise<import("axios").AxiosResponse<any, {
|
|
27
|
+
userMessage: string;
|
|
28
|
+
errorContext: ServerErrorContext | undefined;
|
|
29
|
+
clientContext: ClientContext;
|
|
30
|
+
}, {}, any>>;
|
|
31
|
+
declare const ErrorReportService: {
|
|
32
|
+
send: typeof send;
|
|
33
|
+
};
|
|
34
|
+
export default ErrorReportService;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createApiClient } from '../api/createApiClient';
|
|
2
|
+
function getFunctionsApiBase() {
|
|
3
|
+
const explicit = import.meta.env.VITE_APP_FUNCTIONS_API;
|
|
4
|
+
if (explicit?.trim()) {
|
|
5
|
+
return explicit.trim().replace(/\/+$/, '');
|
|
6
|
+
}
|
|
7
|
+
const origin = import.meta.env.VITE_APP_ORIGIN;
|
|
8
|
+
if (origin?.trim()) {
|
|
9
|
+
return `${origin.trim().replace(/\/+$/, '')}/api/functions`;
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
function buildClientContext(sendContext) {
|
|
14
|
+
const now = new Date();
|
|
15
|
+
const timezone = typeof Intl !== 'undefined'
|
|
16
|
+
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
17
|
+
: undefined;
|
|
18
|
+
const clientContext = {
|
|
19
|
+
timestamp: now.toISOString(),
|
|
20
|
+
timezone,
|
|
21
|
+
environment: import.meta.env.MODE,
|
|
22
|
+
locale: sendContext?.locale,
|
|
23
|
+
corpusUid: sendContext?.corpusUid,
|
|
24
|
+
corpusName: sendContext?.corpusName,
|
|
25
|
+
};
|
|
26
|
+
const appUrl = import.meta.env.VITE_APP_PUBLIC_URL;
|
|
27
|
+
if (appUrl) {
|
|
28
|
+
clientContext.appUrl = appUrl;
|
|
29
|
+
}
|
|
30
|
+
if (typeof navigator !== 'undefined' && navigator.userAgent) {
|
|
31
|
+
clientContext.userAgent = navigator.userAgent;
|
|
32
|
+
}
|
|
33
|
+
if (typeof window !== 'undefined') {
|
|
34
|
+
clientContext.pageUrl = window.location.href;
|
|
35
|
+
clientContext.viewport = `${window.innerWidth}×${window.innerHeight}`;
|
|
36
|
+
const dpr = window.devicePixelRatio ?? 1;
|
|
37
|
+
clientContext.screen =
|
|
38
|
+
typeof screen !== 'undefined'
|
|
39
|
+
? `${screen.width}×${screen.height} (DPR: ${dpr})`
|
|
40
|
+
: 'unknown';
|
|
41
|
+
}
|
|
42
|
+
if (typeof document !== 'undefined' && document.referrer) {
|
|
43
|
+
clientContext.referrer = document.referrer;
|
|
44
|
+
}
|
|
45
|
+
return clientContext;
|
|
46
|
+
}
|
|
47
|
+
function send({ userMessage, errorContext, sendContext, }) {
|
|
48
|
+
const functionsApiBase = getFunctionsApiBase();
|
|
49
|
+
if (!functionsApiBase) {
|
|
50
|
+
return Promise.reject(new Error('functions.api.unavailable'));
|
|
51
|
+
}
|
|
52
|
+
return createApiClient({
|
|
53
|
+
baseURL: functionsApiBase,
|
|
54
|
+
attachJwt: true,
|
|
55
|
+
serverErrorHandling: 'none',
|
|
56
|
+
}).post('/error-report', {
|
|
57
|
+
userMessage,
|
|
58
|
+
errorContext,
|
|
59
|
+
clientContext: buildClientContext(sendContext),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const ErrorReportService = { send };
|
|
63
|
+
export default ErrorReportService;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Dispatch } from 'react';
|
|
2
|
+
import type { StoreAction } from './types';
|
|
3
|
+
type CommonDispatch = Dispatch<StoreAction>;
|
|
4
|
+
export declare function registerCommonDispatch(dispatch: CommonDispatch): void;
|
|
5
|
+
export declare function getCommonDispatch(): CommonDispatch | null;
|
|
6
|
+
export {};
|
|
@@ -4,3 +4,4 @@ export { buildDriveSteps, createTourDriver, hasMatchingWorkflow, } from './drive
|
|
|
4
4
|
export { localizeKnowledgeBase, pickLocalizedString, resolveTourLocale, TOUR_FALLBACK_LOCALE, TOUR_LOCALES, } from './localizeKnowledgeBase';
|
|
5
5
|
export { resolveAnchor, resolveAnchorById, waitForSelector, } from './anchorResolver';
|
|
6
6
|
export type { AnchorSelector, KnowledgeBase, KnowledgeBaseRaw, LocalizedString, PopoverSide, ResolveKnowledgeBaseUrl, TourAnchor, TourButtonLabels, TourStep, TourStepCopy, TourStepStructural, TourWorkflow, TourWorkflowRaw, UseDriverTourOptions, } from './types';
|
|
7
|
+
export type { Driver } from 'driver.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useContext } from "react";
|
|
2
|
+
import { dispatchApiError } from "./api/dispatchApiError";
|
|
2
3
|
import { message, silentLoading, toastLoading } from "../configs/store/actions";
|
|
3
4
|
import CommonContext from "./CommonContext";
|
|
4
5
|
const translateErrorValue = (t, namespace, rawValue) => {
|
|
@@ -49,8 +50,7 @@ export const useMessageUtils = () => {
|
|
|
49
50
|
: toastLoading(val));
|
|
50
51
|
};
|
|
51
52
|
const dispatchError = ({ err, t, key }) => {
|
|
52
|
-
|
|
53
|
-
dispatch(message({ value, type: "error" }));
|
|
53
|
+
dispatchApiError({ err, t, key });
|
|
54
54
|
};
|
|
55
55
|
const dispatchMessage = ({ key, ns, t }) => {
|
|
56
56
|
const entry = ns ? `${ns}:${key}` : `message:${key}`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ServerErrorContext } from '../../common/AppToast/ToastMessage';
|
|
2
|
+
import type { Corpus } from '../../configs/types/Corpus';
|
|
3
|
+
import './styles.scss';
|
|
4
|
+
type Props = {
|
|
5
|
+
errorContext?: ServerErrorContext;
|
|
6
|
+
corpus?: Corpus;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
onSent: () => void;
|
|
9
|
+
};
|
|
10
|
+
export default function ServerErrorReport({ errorContext, corpus, onClose, onSent, }: Props): import("react").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import AppModal from '../../common/AppModal/AppModal';
|
|
6
|
+
import AppTextArea from '../../common/AppTextArea/AppTextArea';
|
|
7
|
+
import ErrorReportService from '../../configs/services/ErrorReportService';
|
|
8
|
+
import './styles.scss';
|
|
9
|
+
export default function ServerErrorReport({ errorContext, corpus, onClose, onSent, }) {
|
|
10
|
+
const { t, i18n } = useTranslation('common');
|
|
11
|
+
const [userMessage, setUserMessage] = useState('');
|
|
12
|
+
const [sending, setSending] = useState(false);
|
|
13
|
+
const [sendError, setSendError] = useState(null);
|
|
14
|
+
const handleSend = () => {
|
|
15
|
+
if (!userMessage.trim()) {
|
|
16
|
+
setSendError(t('server.error.report.validation'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
setSending(true);
|
|
20
|
+
setSendError(null);
|
|
21
|
+
ErrorReportService.send({
|
|
22
|
+
userMessage,
|
|
23
|
+
errorContext,
|
|
24
|
+
sendContext: {
|
|
25
|
+
locale: i18n.resolvedLanguage || i18n.language,
|
|
26
|
+
corpusUid: corpus?.uid,
|
|
27
|
+
corpusName: corpus?.name,
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
.then(() => {
|
|
31
|
+
onSent();
|
|
32
|
+
})
|
|
33
|
+
.catch((error) => {
|
|
34
|
+
console.error('Failed to send server error report:', axios.isAxiosError(error)
|
|
35
|
+
? {
|
|
36
|
+
message: error.message,
|
|
37
|
+
status: error.response?.status,
|
|
38
|
+
data: error.response?.data,
|
|
39
|
+
url: error.config?.url,
|
|
40
|
+
}
|
|
41
|
+
: error);
|
|
42
|
+
setSendError(t('server.error.report.send.failed'));
|
|
43
|
+
})
|
|
44
|
+
.finally(() => {
|
|
45
|
+
setSending(false);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
return (_jsx(AppModal, { title: t('server.error.report.modal.title'), subtitle: t('server.error.report.modal.subtitle'), close: onClose, confirm: handleSend, disableConfirm: sending || !userMessage.trim(), confirmLabel: t('server.error.report.modal.send'), className: "server-error-report-modal", children: _jsxs("div", { className: "server-error-report-modal__body", children: [_jsx(AppTextArea, { value: userMessage, onChange: (e) => setUserMessage(e.target.value), placeholder: t('server.error.report.modal.placeholder'), rows: 6, disabled: sending, "aria-label": t('server.error.report.modal.placeholder') }), sendError && (_jsx("p", { className: "server-error-report-modal__error", role: "alert", children: sendError }))] }) }));
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.server-error-report-modal {
|
|
2
|
+
&__body {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: var(--spacing-200);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
&__error {
|
|
9
|
+
margin: 0;
|
|
10
|
+
color: var(--text-danger);
|
|
11
|
+
@include body-small-1;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.app-text-area {
|
|
15
|
+
min-height: 160px;
|
|
16
|
+
border: var(--border-default);
|
|
17
|
+
border-radius: var(--radius-100);
|
|
18
|
+
background-color: var(--background-default);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { isHandledApiError } from '../../configs/api';
|
|
4
5
|
import logo from './logo.png';
|
|
5
6
|
import './style.scss';
|
|
6
7
|
export default function ErrorBoundary({ children }) {
|
|
@@ -9,27 +10,30 @@ export default function ErrorBoundary({ children }) {
|
|
|
9
10
|
hasError: false,
|
|
10
11
|
error: null,
|
|
11
12
|
});
|
|
12
|
-
// Hook to simulate the behavior of componentDidCatch
|
|
13
13
|
useEffect(() => {
|
|
14
14
|
const handleError = (error) => {
|
|
15
15
|
console.error('Caught by ErrorBoundary:', error);
|
|
16
16
|
setState({ hasError: true, error });
|
|
17
17
|
};
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const onUnhandledRejection = (event) => {
|
|
19
|
+
if (isHandledApiError(event.reason)) {
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
20
23
|
handleError(event.reason);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
};
|
|
25
|
+
const onError = (event) => {
|
|
26
|
+
if (isHandledApiError(event.error)) {
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
24
30
|
handleError(event.error);
|
|
25
|
-
}
|
|
31
|
+
};
|
|
32
|
+
window.addEventListener('unhandledrejection', onUnhandledRejection);
|
|
33
|
+
window.addEventListener('error', onError);
|
|
26
34
|
return () => {
|
|
27
|
-
window.removeEventListener('unhandledrejection',
|
|
28
|
-
|
|
29
|
-
});
|
|
30
|
-
window.removeEventListener('error', (event) => {
|
|
31
|
-
handleError(event.error);
|
|
32
|
-
});
|
|
35
|
+
window.removeEventListener('unhandledrejection', onUnhandledRejection);
|
|
36
|
+
window.removeEventListener('error', onError);
|
|
33
37
|
};
|
|
34
38
|
}, []);
|
|
35
39
|
if (state.hasError && state.error) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tycho-components",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.39.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@hookform/resolvers": "^5.2.2",
|
|
51
51
|
"@tanstack/react-table": "^8.20.6",
|
|
52
|
-
"axios": "^1.13.6",
|
|
53
52
|
"classnames": "^2.5.1",
|
|
54
53
|
"cytoscape": "^3.28.1",
|
|
55
54
|
"cytoscape-dagre": "^2.5.0",
|
|
@@ -74,6 +73,7 @@
|
|
|
74
73
|
"@emotion/styled": "^11.13.0",
|
|
75
74
|
"@mui/icons-material": "7.3.0",
|
|
76
75
|
"@mui/material": "7.3.0",
|
|
76
|
+
"axios": "^1.13.0",
|
|
77
77
|
"i18next": "^23.3.0",
|
|
78
78
|
"i18next-browser-languagedetector": "^7.1.0",
|
|
79
79
|
"react": ">=17 <19",
|
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
|
102
102
|
"@typescript-eslint/parser": "^5.62.0",
|
|
103
103
|
"@vitejs/plugin-react": "^5.0.0",
|
|
104
|
+
"axios": "^1.13.6",
|
|
104
105
|
"eslint": "^8.57.1",
|
|
105
106
|
"eslint-plugin-react-hooks": "^4.6.2",
|
|
106
107
|
"eslint-plugin-react-refresh": "^0.4.1",
|
package/dist/configs/api.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/** Platform API roots (formerly separate env vars per service). */
|
|
2
|
-
export declare const platformApi: {
|
|
3
|
-
base: string;
|
|
4
|
-
revision: string;
|
|
5
|
-
catalog: string;
|
|
6
|
-
editor: string;
|
|
7
|
-
};
|
|
8
|
-
export declare const parserApiBase: string;
|
|
9
|
-
declare const api: import("axios").AxiosInstance;
|
|
10
|
-
export default api;
|
package/dist/configs/api.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import axios from 'axios';
|
|
2
|
-
import CookieStorage from './CookieStorage';
|
|
3
|
-
const platformApiBase = import.meta.env.VITE_APP_PLATFORM_API;
|
|
4
|
-
/** Platform API roots (formerly separate env vars per service). */
|
|
5
|
-
export const platformApi = {
|
|
6
|
-
base: platformApiBase,
|
|
7
|
-
revision: `${platformApiBase}/revision`,
|
|
8
|
-
catalog: `${platformApiBase}/catalog`,
|
|
9
|
-
editor: `${platformApiBase}/editor`,
|
|
10
|
-
};
|
|
11
|
-
export const parserApiBase = import.meta.env.VITE_APP_PARSER_API;
|
|
12
|
-
const api = axios.create({
|
|
13
|
-
headers: {
|
|
14
|
-
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
15
|
-
Pragma: 'no-cache',
|
|
16
|
-
'Content-Type': 'application/json',
|
|
17
|
-
Accept: 'application/json',
|
|
18
|
-
},
|
|
19
|
-
baseURL: platformApiBase,
|
|
20
|
-
});
|
|
21
|
-
const isStorybook = () => typeof window !== 'undefined' &&
|
|
22
|
-
(window.location.port === '6006' ||
|
|
23
|
-
window.__STORYBOOK__);
|
|
24
|
-
api.interceptors.response.use((response) => response, async (error) => {
|
|
25
|
-
if (error.response?.status === 401) {
|
|
26
|
-
CookieStorage.removeJwtToken();
|
|
27
|
-
sessionStorage.clear();
|
|
28
|
-
localStorage.clear();
|
|
29
|
-
if (!isStorybook()) {
|
|
30
|
-
window.location.replace(import.meta.env.VITE_APP_AUTH_URL);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
else if (error.response?.status === 403 && !isStorybook()) {
|
|
34
|
-
window.location.href = import.meta.env.VITE_APP_UNAUTHORIZED_URL;
|
|
35
|
-
}
|
|
36
|
-
else if (error.status === null) {
|
|
37
|
-
console.log(JSON.stringify(error));
|
|
38
|
-
}
|
|
39
|
-
return Promise.reject(error);
|
|
40
|
-
});
|
|
41
|
-
api.interceptors.request.use((config) => {
|
|
42
|
-
if (!config?.headers) {
|
|
43
|
-
throw new Error('no header available');
|
|
44
|
-
}
|
|
45
|
-
const token = CookieStorage.getJwtToken();
|
|
46
|
-
if (!token)
|
|
47
|
-
return config;
|
|
48
|
-
// eslint-disable-next-line no-param-reassign
|
|
49
|
-
config.headers.Authorization = `Bearer ${token}`;
|
|
50
|
-
return config;
|
|
51
|
-
});
|
|
52
|
-
export default api;
|