Skip to content

Commit 3fdffc0

Browse files
committed
[#noissue] Enhanced UI Error pop-up according to ProblemDetail
1 parent 7a0e5fe commit 3fdffc0

File tree

8 files changed

+203
-132
lines changed

8 files changed

+203
-132
lines changed

web-frontend/src/main/v3/packages/ui/src/components/Error/ErrorBoundary.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
} from 'react-error-boundary';
77
import { ErrorDetailDialog } from './ErrorDetailDialog';
88
import { useReactToastifyToast } from '../Toast';
9-
import { ErrorResponse } from '@pinpoint-fe/ui/src/constants';
9+
import { ErrorLike } from '@pinpoint-fe/ui/src/constants';
1010
import { ErrorToast } from './ErrorToast';
1111
import { useSearchParameters } from '@pinpoint-fe/ui/src/hooks';
1212

@@ -28,7 +28,7 @@ export const ErrorBoundary = ({
2828
<ErrorBoundaryComponent
2929
resetKeys={[search, ...(resetKeys || [])]}
3030
onError={(props) => {
31-
const error = props as unknown as ErrorResponse;
31+
const error = props as unknown as ErrorLike;
3232
toast.error(<ErrorToast error={error} />, {
3333
bodyClassName: '!items-start',
3434
autoClose: false,
@@ -39,17 +39,22 @@ export const ErrorBoundary = ({
3939
return fallbackRender({ error, resetErrorBoundary });
4040
}
4141
return (
42-
<div className="flex flex-col items-center justify-center w-full h-full gap-5 p-3">
42+
<div className="flex flex-col gap-5 justify-center items-center p-3 w-full h-full">
4343
<div className="w-full text-center max-w-[28rem]">
4444
{errorMessage ? (
4545
typeof errorMessage === 'function' ? (
46-
errorMessage(error?.message)
46+
errorMessage(
47+
(error as ErrorLike)?.detail ?? error?.message ?? (error as ErrorLike)?.title,
48+
)
4749
) : (
4850
errorMessage
4951
)
5052
) : (
5153
<p className="mb-2 text-sm truncate">
52-
{error?.message || t('COMMON.SOMETHING_WENT_WRONG')}
54+
{(error as ErrorLike)?.detail ??
55+
error?.message ??
56+
(error as ErrorLike)?.title ??
57+
t('COMMON.SOMETHING_WENT_WRONG')}
5358
</p>
5459
)}
5560
<ErrorDetailDialog error={error} />

web-frontend/src/main/v3/packages/ui/src/components/Error/ErrorDetailDialog.tsx

Lines changed: 117 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,49 @@ import {
1010
Separator,
1111
} from '../ui';
1212
import * as PopoverPrimitive from '@radix-ui/react-popover';
13-
import { ErrorDetailResponse } from '@pinpoint-fe/ui/src/constants';
13+
import { ErrorLike } from '@pinpoint-fe/ui/src/constants';
1414
import { cn } from '../../lib';
1515
import { HighLightCode } from '../HighLightCode';
1616
import { RxChevronDown, RxChevronUp } from 'react-icons/rx';
1717

1818
export interface ErrorDetailDialogProps {
19-
error: ErrorDetailResponse | Error;
19+
error: Error | ErrorLike;
2020
contentOption?: PopoverPrimitive.PopoverContentProps;
2121
contentClassName?: string;
2222
}
2323

24+
function getDisplayMessage(error: Error | ErrorLike): string {
25+
const msg = (error as Error).message;
26+
const detail = (error as ErrorLike).detail;
27+
return detail ?? msg ?? '';
28+
}
29+
30+
function getClientStack(error: Error | ErrorLike): string | null {
31+
const stack = (error as Error).stack;
32+
return stack ?? null;
33+
}
34+
35+
function getServerTrace(error: Error | ErrorLike): string | null {
36+
const trace = (error as ErrorLike).trace;
37+
if (Array.isArray(trace) && trace.length > 0) return trace.join('\n');
38+
if (typeof trace === 'string') return trace;
39+
return null;
40+
}
41+
2442
export const ErrorDetailDialog = ({
2543
error,
2644
contentOption,
2745
contentClassName,
2846
}: ErrorDetailDialogProps) => {
29-
const [headerOpen, setHeaderOpen] = React.useState(false);
30-
const [paremetersOpen, setParametersOpen] = React.useState(false);
31-
const [stackTraceOpen, setStackTraceOpen] = React.useState(true);
47+
const [serverTraceOpen, setServerTraceOpen] = React.useState(false);
48+
const serverError = error as ErrorLike;
49+
const hasMethod = serverError?.method != null;
50+
const hasStatus = serverError?.status != null;
51+
const params = serverError?.parameters ?? {};
52+
const hasParams = Object.keys(params).length > 0;
53+
const clientStack = getClientStack(error);
54+
const serverTrace = getServerTrace(error);
55+
3256
return (
3357
<Dialog>
3458
<DialogTrigger asChild>
@@ -42,113 +66,113 @@ export const ErrorDetailDialog = ({
4266
onMouseDown={(e) => e.stopPropagation()}
4367
{...contentOption}
4468
>
45-
<div className="flex flex-col gap-4 overflow-hidden">
69+
<div className="flex overflow-hidden flex-col gap-4">
4670
<div className="space-y-2">
47-
<h4 className="flex items-center gap-1 font-medium">
71+
<h4 className="flex gap-1 items-center font-medium">
4872
<div className="w-1 h-4 rounded-sm bg-status-fail" />
4973
Error Details
5074
</h4>
51-
<div className="flex items-center gap-1">
52-
<a
53-
className="text-sm font-semibold text-primary hover:underline"
54-
href={(error as ErrorDetailResponse)?.url}
55-
target="_blank"
56-
>
57-
{(error as ErrorDetailResponse)?.instance}
58-
</a>
59-
</div>
60-
<p className="text-sm break-all text-muted-foreground">{error?.message}</p>
61-
</div>
62-
<Separator />
63-
{(error as Error)?.stack && (
64-
<div className="overflow-auto">
65-
<pre>{(error as Error)?.stack}</pre>
66-
</div>
67-
)}
68-
{(error as ErrorDetailResponse)?.data && (
69-
<div className="grid gap-2 text-sm scrollbar-hide">
70-
<div className="grid grid-cols-[7rem_auto] gap-2">
75+
76+
{serverError?.instance && (
77+
<div className="flex gap-1 items-center">
78+
<a
79+
className="text-sm font-semibold text-primary hover:underline"
80+
href={serverError?.url}
81+
target="_blank"
82+
rel="noreferrer"
83+
>
84+
{serverError.instance}
85+
</a>
86+
</div>
87+
)}
88+
{hasMethod && (
89+
<div className="grid grid-cols-[5rem_auto] gap-x-2 gap-y-1 text-sm items-baseline">
7190
<div className="text-muted-foreground">Method</div>
72-
<div>{(error as ErrorDetailResponse)?.data?.requestInfo?.method}</div>
91+
<div>{serverError.method}</div>
7392
</div>
74-
<Collapsible className="space-y-2" open={headerOpen} onOpenChange={setHeaderOpen}>
75-
<CollapsibleTrigger
76-
className="flex items-center gap-1 text-muted-foreground hover:text-foreground"
77-
onClick={(e) => e.stopPropagation()}
78-
>
79-
Header {headerOpen ? <RxChevronUp /> : <RxChevronDown />}
80-
</CollapsibleTrigger>
81-
<CollapsibleContent>
82-
<div className="grid grid-cols-[7rem_auto] gap-2 text-xs p-2">
83-
{(error as ErrorDetailResponse)?.data?.requestInfo?.headers &&
84-
Object.keys((error as ErrorDetailResponse)?.data?.requestInfo?.headers)
85-
.sort()
86-
.map((key) => {
87-
return (
88-
<React.Fragment key={key}>
89-
<div className="text-muted-foreground">{key}</div>
90-
<div className="break-all">
91-
{(error as ErrorDetailResponse)?.data?.requestInfo?.headers?.[key]}
92-
</div>
93-
</React.Fragment>
94-
);
95-
})}
96-
</div>
97-
</CollapsibleContent>
98-
</Collapsible>
99-
<Collapsible
100-
className="space-y-2"
101-
open={paremetersOpen}
102-
onOpenChange={setParametersOpen}
103-
>
104-
<CollapsibleTrigger
105-
className="flex items-center gap-1 text-muted-foreground hover:text-foreground"
106-
onClick={(e) => e.stopPropagation()}
107-
>
108-
Parameters {paremetersOpen ? <RxChevronUp /> : <RxChevronDown />}
109-
</CollapsibleTrigger>
110-
<CollapsibleContent>
111-
<div className="grid grid-cols-[7rem_auto] gap-2 text-xs p-2">
112-
{(error as ErrorDetailResponse)?.data?.requestInfo?.parameters &&
113-
Object.keys((error as ErrorDetailResponse)?.data?.requestInfo?.parameters)
114-
.sort()
115-
.map((key) => {
116-
return (
117-
<React.Fragment key={key}>
118-
<div className="text-muted-foreground">{key}</div>
119-
<div className="break-all">
120-
{
121-
(error as ErrorDetailResponse)?.data?.requestInfo?.parameters?.[
122-
key
123-
]
124-
}
125-
</div>
126-
</React.Fragment>
127-
);
128-
})}
129-
</div>
130-
</CollapsibleContent>
131-
</Collapsible>
93+
)}
94+
{hasStatus && (
95+
<div className="grid grid-cols-[5rem_auto] gap-x-2 gap-y-1 text-sm items-baseline">
96+
<div className="text-muted-foreground">Status</div>
97+
<div>{serverError.status}</div>
98+
</div>
99+
)}
100+
{serverError.title != null && serverError.title !== '' && (
101+
<div className="grid grid-cols-[5rem_auto] gap-x-2 gap-y-1 text-sm items-baseline">
102+
<div className="text-muted-foreground">Title</div>
103+
<div>{serverError.title}</div>
104+
</div>
105+
)}
106+
{serverError.detail != null && serverError.detail !== '' && (
107+
<div className="grid grid-cols-[5rem_auto] gap-x-2 gap-y-1 text-sm items-baseline">
108+
<div className="text-muted-foreground">Detail</div>
109+
<p className="text-sm break-all">{getDisplayMessage(error) || '—'}</p>
110+
</div>
111+
)}
112+
{!hasStatus && (
113+
<p className="text-sm break-all text-muted-foreground">
114+
{getDisplayMessage(error) || '—'}
115+
</p>
116+
)}
117+
{hasParams && (
118+
<>
119+
<div className="grid gap-2 text-sm scrollbar-hide">
120+
{hasParams && (
121+
<>
122+
<div className="text-muted-foreground">Parameters</div>
123+
<div className="grid grid-cols-[10rem_auto] gap-2 text-xs">
124+
{Object.keys(params)
125+
.sort()
126+
.map((key) => {
127+
const value = params[key];
128+
return (
129+
<React.Fragment key={key}>
130+
<div className="pl-3 text-muted-foreground">{key}</div>
131+
<div className="pl-3 break-all">
132+
{Array.isArray(value) ? value.join(', ') : String(value)}
133+
</div>
134+
</React.Fragment>
135+
);
136+
})}
137+
</div>
138+
</>
139+
)}
140+
</div>
141+
</>
142+
)}
143+
</div>
144+
{clientStack != null && (
145+
<>
146+
<Separator />
147+
<div className="overflow-auto">
148+
<pre className="p-2 text-sm">{clientStack}</pre>
149+
</div>
150+
</>
151+
)}
152+
{serverTrace != null && (
153+
<>
154+
<Separator />
132155
<Collapsible
133156
className="space-y-2"
134-
open={stackTraceOpen}
135-
onOpenChange={setStackTraceOpen}
157+
open={serverTraceOpen}
158+
onOpenChange={setServerTraceOpen}
159+
defaultOpen={false}
136160
>
137161
<CollapsibleTrigger
138-
className="flex items-center gap-1 text-muted-foreground hover:text-foreground"
162+
className="flex gap-1 items-center text-sm text-muted-foreground hover:text-foreground"
139163
onClick={(e) => e.stopPropagation()}
140164
>
141-
StackTrace {stackTraceOpen ? <RxChevronUp /> : <RxChevronDown />}
165+
Server stack trace {serverTraceOpen ? <RxChevronUp /> : <RxChevronDown />}
142166
</CollapsibleTrigger>
143167
<CollapsibleContent>
144168
<HighLightCode
145169
language="java"
146-
code={(error as ErrorDetailResponse)?.trace}
147-
className="p-2 text-xs"
170+
code={serverTrace}
171+
className="overflow-auto p-2 text-xs"
148172
/>
149173
</CollapsibleContent>
150174
</Collapsible>
151-
</div>
175+
</>
152176
)}
153177
</div>
154178
</DialogContent>

web-frontend/src/main/v3/packages/ui/src/components/Error/ErrorToast.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
import { useTranslation } from 'react-i18next';
2-
import { ErrorResponse } from '@pinpoint-fe/ui/src/constants';
2+
import { ErrorLike } from '@pinpoint-fe/ui/src/constants';
33
import { Separator } from '../../components/ui/separator';
44
import { ErrorDetailDialog } from './ErrorDetailDialog';
55
import { cn } from '../../lib/utils';
66

77
export interface ErrorToastProps {
8-
error: ErrorResponse;
9-
title?: string;
8+
error: Error | ErrorLike;
109
className?: string;
1110
}
1211

13-
export const ErrorToast = ({ error, title, className }: ErrorToastProps) => {
12+
export const ErrorToast = ({ error, className }: ErrorToastProps) => {
1413
const { t } = useTranslation();
14+
const err = error as ErrorLike;
1515
return (
1616
<div className={cn('space-y-2', className)}>
1717
<div>
18-
{title ? title : t('COMMON.SOMETHING_WENT_WRONG')}:
18+
{err?.title ? err.title : t('COMMON.SOMETHING_WENT_WRONG')}:
1919
<br />
20-
{error?.instance && (
20+
{err?.instance && (
2121
<>
22-
<span className="font-semibold items">{error.instance}</span>
22+
<span className="font-semibold items">{err.instance}</span>
2323
</>
2424
)}
2525
</div>
2626
<Separator />
27-
<p className="text-xs">{error?.message}</p>
27+
<p className="text-xs">{err?.detail ?? error?.message}</p>
2828
<ErrorDetailDialog error={error} />
2929
</div>
3030
);
Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1-
export const ThrowError = ({ error }: { error: Error }) => {
2-
throw new Error(error.message || 'An error occurred');
1+
import { ErrorLike } from '@pinpoint-fe/ui/src/constants';
2+
3+
export const ThrowError = ({ error }: { error: Error | ErrorLike }) => {
4+
const err = new Error(
5+
(error as ErrorLike).detail || (error as ErrorLike).title || 'An error occurred.',
6+
) as Error;
7+
Object.assign(err, error);
8+
err.message = (error as ErrorLike).detail || (error as ErrorLike).title || err.message;
9+
throw err;
310
};

web-frontend/src/main/v3/packages/ui/src/components/FilterMap/FilteredMapFetcher.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const FilteredMapFetcher = ({
3434
const setServerMapCurrentTarget = useSetAtom(serverMapCurrentTargetAtom);
3535
const { dateRange, application } = useFilteredMapParameters();
3636
const from = dateRange.from.getTime();
37-
const { data, isLoading, setQueryParams } = useGetFilteredServerMapData(isPaused);
37+
const { data, error, isLoading, setQueryParams } = useGetFilteredServerMapData(isPaused);
3838
const { t } = useTranslation();
3939

4040
React.useEffect(() => {
@@ -178,6 +178,7 @@ export const FilteredMapFetcher = ({
178178
return (
179179
<ServerMapCore
180180
data={serverMapData}
181+
error={error}
181182
onClickNode={handleClickNode}
182183
onClickEdge={handleClickEdge}
183184
onClickMenuItem={handleClickServerMapMenuItem}

0 commit comments

Comments
 (0)