@workflow/web-shared 4.1.0-beta.55 → 4.1.0-beta.57
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/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +34 -2
- package/dist/components/event-list-view.js.map +1 -1
- package/dist/components/run-trace-view.d.ts +4 -1
- package/dist/components/run-trace-view.d.ts.map +1 -1
- package/dist/components/run-trace-view.js +2 -2
- package/dist/components/run-trace-view.js.map +1 -1
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +122 -45
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/sidebar/copyable-data-block.d.ts +4 -0
- package/dist/components/sidebar/copyable-data-block.d.ts.map +1 -0
- package/dist/components/sidebar/copyable-data-block.js +33 -0
- package/dist/components/sidebar/copyable-data-block.js.map +1 -0
- package/dist/components/sidebar/detail-card.d.ts +7 -1
- package/dist/components/sidebar/detail-card.d.ts.map +1 -1
- package/dist/components/sidebar/detail-card.js +12 -3
- package/dist/components/sidebar/detail-card.js.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +30 -16
- package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +37 -7
- package/dist/components/sidebar/events-list.js.map +1 -1
- package/dist/components/ui/error-stack-block.d.ts +19 -0
- package/dist/components/ui/error-stack-block.d.ts.map +1 -0
- package/dist/components/ui/error-stack-block.js +39 -0
- package/dist/components/ui/error-stack-block.js.map +1 -0
- package/dist/components/workflow-trace-view.d.ts +7 -1
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +137 -24
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/package.json +10 -10
- package/src/components/event-list-view.tsx +53 -2
- package/src/components/run-trace-view.tsx +9 -0
- package/src/components/sidebar/attribute-panel.tsx +285 -127
- package/src/components/sidebar/copyable-data-block.tsx +51 -0
- package/src/components/sidebar/detail-card.tsx +28 -2
- package/src/components/sidebar/entity-detail-panel.tsx +138 -81
- package/src/components/sidebar/events-list.tsx +72 -21
- package/src/components/ui/error-stack-block.tsx +80 -0
- package/src/components/workflow-trace-view.tsx +208 -28
|
@@ -156,20 +156,14 @@ export function EntityDetailPanel({
|
|
|
156
156
|
if (resource !== 'sleep' || !rawEvents) return false;
|
|
157
157
|
const terminalStates = ['completed', 'failed', 'cancelled'];
|
|
158
158
|
if (terminalStates.includes(run.status)) return false;
|
|
159
|
+
const hasWaitCreated = rawEvents.some(
|
|
160
|
+
(e) => e.eventType === 'wait_created'
|
|
161
|
+
);
|
|
162
|
+
if (!hasWaitCreated) return false;
|
|
159
163
|
const hasWaitCompleted = rawEvents.some(
|
|
160
164
|
(e) => e.eventType === 'wait_completed'
|
|
161
165
|
);
|
|
162
|
-
|
|
163
|
-
const waitCreatedEvent = rawEvents.find(
|
|
164
|
-
(e) => e.eventType === 'wait_created'
|
|
165
|
-
);
|
|
166
|
-
const eventData = (waitCreatedEvent as any)?.eventData as
|
|
167
|
-
| { resumeAt?: string | Date }
|
|
168
|
-
| undefined;
|
|
169
|
-
const resumeAt = eventData?.resumeAt;
|
|
170
|
-
if (!resumeAt) return false;
|
|
171
|
-
const resumeAtDate = new Date(resumeAt);
|
|
172
|
-
return resumeAtDate.getTime() > Date.now();
|
|
166
|
+
return !hasWaitCompleted;
|
|
173
167
|
}, [resource, rawEvents, rawEventsLength, run.status]);
|
|
174
168
|
|
|
175
169
|
// Check if this hook can be resolved
|
|
@@ -328,70 +322,146 @@ export function EntityDetailPanel({
|
|
|
328
322
|
return undefined;
|
|
329
323
|
}, [displayData, run.workflowName]);
|
|
330
324
|
|
|
325
|
+
const resourceLabel = resource.charAt(0).toUpperCase() + resource.slice(1);
|
|
326
|
+
const hasPendingActions =
|
|
327
|
+
(resource === 'sleep' && canWakeUp) ||
|
|
328
|
+
(resource === 'hook' && canResolveHook);
|
|
329
|
+
const runStateLabel = run.completedAt ? 'Completed' : 'Live';
|
|
330
|
+
|
|
331
331
|
return (
|
|
332
|
-
<div
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
332
|
+
<div className="flex h-full flex-col">
|
|
333
|
+
<div
|
|
334
|
+
className="border-b px-3 py-3"
|
|
335
|
+
style={{ borderColor: 'var(--ds-gray-200)' }}
|
|
336
|
+
>
|
|
337
|
+
<div className="flex items-start justify-between gap-2">
|
|
338
|
+
<div className="min-w-0">
|
|
339
|
+
<div className="flex items-center gap-2">
|
|
340
|
+
<span
|
|
341
|
+
className="inline-flex items-center rounded-full border px-2 py-0.5 text-[13px] font-medium"
|
|
342
|
+
style={{
|
|
343
|
+
borderColor: 'var(--ds-gray-300)',
|
|
344
|
+
color: 'var(--ds-gray-900)',
|
|
345
|
+
backgroundColor: 'var(--ds-background-100)',
|
|
346
|
+
}}
|
|
347
|
+
>
|
|
348
|
+
{resourceLabel}
|
|
349
|
+
</span>
|
|
350
|
+
<span
|
|
351
|
+
className="text-[13px]"
|
|
352
|
+
style={{
|
|
353
|
+
color: run.completedAt
|
|
354
|
+
? 'var(--ds-gray-700)'
|
|
355
|
+
: 'var(--ds-green-800)',
|
|
356
|
+
}}
|
|
357
|
+
>
|
|
358
|
+
{runStateLabel}
|
|
359
|
+
</span>
|
|
360
|
+
</div>
|
|
361
|
+
<p
|
|
362
|
+
className="mt-1 truncate font-mono text-[13px]"
|
|
363
|
+
style={{ color: 'var(--ds-gray-700)' }}
|
|
364
|
+
title={resourceId}
|
|
365
|
+
>
|
|
366
|
+
{resourceId}
|
|
367
|
+
</p>
|
|
368
|
+
</div>
|
|
363
369
|
</div>
|
|
364
|
-
|
|
370
|
+
</div>
|
|
365
371
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
style={{ borderBottom: '1px solid var(--ds-gray-alpha-400)' }}
|
|
371
|
-
>
|
|
372
|
-
<button
|
|
373
|
-
type="button"
|
|
374
|
-
onClick={() => setShowResolveHookModal(true)}
|
|
375
|
-
disabled={resolvingHook}
|
|
376
|
-
className={clsx(
|
|
377
|
-
'flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md w-full',
|
|
378
|
-
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
379
|
-
'transition-colors',
|
|
380
|
-
resolvingHook ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
|
|
381
|
-
)}
|
|
372
|
+
<div className="flex-1 overflow-y-auto px-3 pt-3 pb-8">
|
|
373
|
+
{hasPendingActions && (
|
|
374
|
+
<div
|
|
375
|
+
className="mb-4 rounded-lg border p-2"
|
|
382
376
|
style={{
|
|
383
|
-
|
|
384
|
-
|
|
377
|
+
borderColor: 'var(--ds-gray-300)',
|
|
378
|
+
backgroundColor: 'var(--ds-gray-100)',
|
|
385
379
|
}}
|
|
386
380
|
>
|
|
387
|
-
<
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
381
|
+
<p
|
|
382
|
+
className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide"
|
|
383
|
+
style={{ color: 'var(--ds-gray-700)' }}
|
|
384
|
+
>
|
|
385
|
+
Actions
|
|
386
|
+
</p>
|
|
387
|
+
<div className="flex flex-col gap-2">
|
|
388
|
+
{/* Wake up button for pending sleep calls */}
|
|
389
|
+
{resource === 'sleep' && canWakeUp && (
|
|
390
|
+
<button
|
|
391
|
+
type="button"
|
|
392
|
+
onClick={handleWakeUp}
|
|
393
|
+
disabled={stoppingSleep}
|
|
394
|
+
className={clsx(
|
|
395
|
+
'flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium',
|
|
396
|
+
'disabled:opacity-50 disabled:cursor-not-allowed transition-colors',
|
|
397
|
+
stoppingSleep
|
|
398
|
+
? 'opacity-50 cursor-not-allowed'
|
|
399
|
+
: 'cursor-pointer'
|
|
400
|
+
)}
|
|
401
|
+
style={{
|
|
402
|
+
background: 'var(--ds-amber-200)',
|
|
403
|
+
color: 'var(--ds-amber-900)',
|
|
404
|
+
}}
|
|
405
|
+
>
|
|
406
|
+
<Zap className="h-4 w-4" />
|
|
407
|
+
{stoppingSleep ? 'Waking up...' : 'Wake Up Sleep'}
|
|
408
|
+
</button>
|
|
409
|
+
)}
|
|
410
|
+
|
|
411
|
+
{/* Resolve hook button for pending hooks */}
|
|
412
|
+
{resource === 'hook' && canResolveHook && (
|
|
413
|
+
<button
|
|
414
|
+
type="button"
|
|
415
|
+
onClick={() => setShowResolveHookModal(true)}
|
|
416
|
+
disabled={resolvingHook}
|
|
417
|
+
className={clsx(
|
|
418
|
+
'flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm font-medium',
|
|
419
|
+
'disabled:opacity-50 disabled:cursor-not-allowed transition-colors',
|
|
420
|
+
resolvingHook
|
|
421
|
+
? 'opacity-50 cursor-not-allowed'
|
|
422
|
+
: 'cursor-pointer'
|
|
423
|
+
)}
|
|
424
|
+
style={{
|
|
425
|
+
background: 'var(--ds-gray-1000)',
|
|
426
|
+
color: 'var(--ds-background-100)',
|
|
427
|
+
}}
|
|
428
|
+
>
|
|
429
|
+
<Send className="h-4 w-4" />
|
|
430
|
+
Resolve Hook
|
|
431
|
+
</button>
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
)}
|
|
436
|
+
|
|
437
|
+
<div className="space-y-4">
|
|
438
|
+
<section>
|
|
439
|
+
<h3
|
|
440
|
+
className="mb-2 text-[13px] font-medium uppercase tracking-wide"
|
|
441
|
+
style={{ color: 'var(--ds-gray-700)' }}
|
|
442
|
+
>
|
|
443
|
+
Details
|
|
444
|
+
</h3>
|
|
445
|
+
<AttributePanel
|
|
446
|
+
data={displayData}
|
|
447
|
+
moduleSpecifier={moduleSpecifier}
|
|
448
|
+
expiredAt={run.expiredAt}
|
|
449
|
+
isLoading={loading}
|
|
450
|
+
error={error ?? undefined}
|
|
451
|
+
onStreamClick={onStreamClick}
|
|
452
|
+
/>
|
|
453
|
+
</section>
|
|
454
|
+
|
|
455
|
+
{resource !== 'run' && rawEvents && (
|
|
456
|
+
<section>
|
|
457
|
+
<EventsList
|
|
458
|
+
events={rawEvents}
|
|
459
|
+
onLoadEventData={onLoadEventData}
|
|
460
|
+
/>
|
|
461
|
+
</section>
|
|
462
|
+
)}
|
|
393
463
|
</div>
|
|
394
|
-
|
|
464
|
+
</div>
|
|
395
465
|
|
|
396
466
|
{/* Resolve Hook Modal */}
|
|
397
467
|
<ResolveHookModal
|
|
@@ -400,19 +470,6 @@ export function EntityDetailPanel({
|
|
|
400
470
|
onSubmit={handleResolveHook}
|
|
401
471
|
isSubmitting={resolvingHook}
|
|
402
472
|
/>
|
|
403
|
-
|
|
404
|
-
{/* Content display */}
|
|
405
|
-
<AttributePanel
|
|
406
|
-
data={displayData}
|
|
407
|
-
moduleSpecifier={moduleSpecifier}
|
|
408
|
-
expiredAt={run.expiredAt}
|
|
409
|
-
isLoading={loading}
|
|
410
|
-
error={error ?? undefined}
|
|
411
|
-
onStreamClick={onStreamClick}
|
|
412
|
-
/>
|
|
413
|
-
{resource !== 'run' && rawEvents && (
|
|
414
|
-
<EventsList events={rawEvents} onLoadEventData={onLoadEventData} />
|
|
415
|
-
)}
|
|
416
473
|
</div>
|
|
417
474
|
);
|
|
418
475
|
}
|
|
@@ -2,10 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import type { Event } from '@workflow/world';
|
|
4
4
|
import { useCallback, useMemo, useState } from 'react';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ErrorStackBlock,
|
|
7
|
+
isStructuredErrorWithStack,
|
|
8
|
+
} from '../ui/error-stack-block';
|
|
9
|
+
import { Skeleton } from '../ui/skeleton';
|
|
6
10
|
import { localMillisecondTime } from './attribute-panel';
|
|
11
|
+
import { CopyableDataBlock } from './copyable-data-block';
|
|
7
12
|
import { DetailCard } from './detail-card';
|
|
8
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Event types whose eventData contains an error field with a StructuredError.
|
|
16
|
+
*/
|
|
17
|
+
const ERROR_EVENT_TYPES = new Set(['step_failed', 'step_retrying']);
|
|
18
|
+
|
|
9
19
|
/**
|
|
10
20
|
* Event types that carry user-serialized data in their eventData field.
|
|
11
21
|
*/
|
|
@@ -69,6 +79,7 @@ function EventItem({
|
|
|
69
79
|
|
|
70
80
|
return (
|
|
71
81
|
<DetailCard
|
|
82
|
+
summaryClassName="text-base py-2"
|
|
72
83
|
summary={
|
|
73
84
|
<>
|
|
74
85
|
<span
|
|
@@ -100,36 +111,35 @@ function EventItem({
|
|
|
100
111
|
}}
|
|
101
112
|
>
|
|
102
113
|
<div
|
|
103
|
-
className="flex items-center justify-between px-2.5 py-1.5"
|
|
114
|
+
className="flex min-h-[32px] items-center justify-between gap-4 px-2.5 py-1.5"
|
|
104
115
|
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
105
116
|
>
|
|
106
|
-
<span
|
|
107
|
-
className="text-[11px] font-medium"
|
|
108
|
-
style={{ color: 'var(--ds-gray-700)' }}
|
|
109
|
-
>
|
|
117
|
+
<span className="text-[14px]" style={{ color: 'var(--ds-gray-700)' }}>
|
|
110
118
|
eventId
|
|
111
119
|
</span>
|
|
112
120
|
<span
|
|
113
|
-
className="text-[
|
|
121
|
+
className="max-w-[70%] truncate text-right text-[13px] font-mono"
|
|
114
122
|
style={{ color: 'var(--ds-gray-1000)' }}
|
|
123
|
+
title={event.eventId}
|
|
115
124
|
>
|
|
116
125
|
{event.eventId}
|
|
117
126
|
</span>
|
|
118
127
|
</div>
|
|
119
128
|
{event.correlationId && (
|
|
120
129
|
<div
|
|
121
|
-
className="flex items-center justify-between px-2.5 py-1.5"
|
|
130
|
+
className="flex min-h-[32px] items-center justify-between gap-4 px-2.5 py-1.5"
|
|
122
131
|
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
123
132
|
>
|
|
124
133
|
<span
|
|
125
|
-
className="text-[
|
|
134
|
+
className="text-[14px]"
|
|
126
135
|
style={{ color: 'var(--ds-gray-700)' }}
|
|
127
136
|
>
|
|
128
137
|
correlationId
|
|
129
138
|
</span>
|
|
130
139
|
<span
|
|
131
|
-
className="text-[
|
|
140
|
+
className="max-w-[70%] truncate text-right text-[13px] font-mono"
|
|
132
141
|
style={{ color: 'var(--ds-gray-1000)' }}
|
|
142
|
+
title={event.correlationId}
|
|
133
143
|
>
|
|
134
144
|
{event.correlationId}
|
|
135
145
|
</span>
|
|
@@ -140,20 +150,21 @@ function EventItem({
|
|
|
140
150
|
{/* Loading state */}
|
|
141
151
|
{isLoading && (
|
|
142
152
|
<div
|
|
143
|
-
className="mt-2
|
|
153
|
+
className="mt-2 rounded-md border p-3"
|
|
144
154
|
style={{
|
|
145
155
|
borderColor: 'var(--ds-gray-300)',
|
|
146
|
-
color: 'var(--ds-gray-600)',
|
|
147
156
|
}}
|
|
148
157
|
>
|
|
149
|
-
|
|
158
|
+
<Skeleton className="h-4 w-[35%]" />
|
|
159
|
+
<Skeleton className="mt-2 h-4 w-[90%]" />
|
|
160
|
+
<Skeleton className="mt-2 h-4 w-[75%]" />
|
|
150
161
|
</div>
|
|
151
162
|
)}
|
|
152
163
|
|
|
153
164
|
{/* Error state */}
|
|
154
165
|
{loadError && (
|
|
155
166
|
<div
|
|
156
|
-
className="mt-2
|
|
167
|
+
className="mt-2 rounded-md border p-2 text-sm"
|
|
157
168
|
style={{
|
|
158
169
|
borderColor: 'var(--ds-red-300)',
|
|
159
170
|
color: 'var(--ds-red-700)',
|
|
@@ -165,17 +176,51 @@ function EventItem({
|
|
|
165
176
|
|
|
166
177
|
{/* Event data */}
|
|
167
178
|
{displayData != null && (
|
|
168
|
-
<div
|
|
169
|
-
|
|
170
|
-
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
171
|
-
>
|
|
172
|
-
<DataInspector data={displayData} />
|
|
179
|
+
<div className="mt-2">
|
|
180
|
+
<EventDataBlock eventType={event.eventType} data={displayData} />
|
|
173
181
|
</div>
|
|
174
182
|
)}
|
|
175
183
|
</DetailCard>
|
|
176
184
|
);
|
|
177
185
|
}
|
|
178
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Renders event data, using ErrorStackBlock for error events that contain
|
|
189
|
+
* a structured error with a stack trace, and CopyableDataBlock otherwise.
|
|
190
|
+
*/
|
|
191
|
+
function EventDataBlock({
|
|
192
|
+
eventType,
|
|
193
|
+
data,
|
|
194
|
+
}: {
|
|
195
|
+
eventType: string;
|
|
196
|
+
data: unknown;
|
|
197
|
+
}) {
|
|
198
|
+
// For error events (step_failed, step_retrying), the eventData has the shape
|
|
199
|
+
// { error: StructuredError, stack?: string, ... }. Check both the top-level
|
|
200
|
+
// value and the nested `error` field for a stack trace.
|
|
201
|
+
if (
|
|
202
|
+
ERROR_EVENT_TYPES.has(eventType) &&
|
|
203
|
+
data != null &&
|
|
204
|
+
typeof data === 'object'
|
|
205
|
+
) {
|
|
206
|
+
const record = data as Record<string, unknown>;
|
|
207
|
+
|
|
208
|
+
// Check the nested `error` field first (the StructuredError)
|
|
209
|
+
if (isStructuredErrorWithStack(record.error)) {
|
|
210
|
+
return <ErrorStackBlock value={record.error} />;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Some error formats put the stack at the top level of eventData
|
|
214
|
+
if (isStructuredErrorWithStack(record)) {
|
|
215
|
+
return <ErrorStackBlock value={record} />;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// For non-error events or errors without a stack, fall back to the
|
|
220
|
+
// generic JSON viewer.
|
|
221
|
+
return <CopyableDataBlock data={data} />;
|
|
222
|
+
}
|
|
223
|
+
|
|
179
224
|
export function EventsList({
|
|
180
225
|
events,
|
|
181
226
|
isLoading = false,
|
|
@@ -208,12 +253,18 @@ export function EventsList({
|
|
|
208
253
|
>
|
|
209
254
|
Events {!isLoading && `(${sortedEvents.length})`}
|
|
210
255
|
</h3>
|
|
211
|
-
{isLoading ?
|
|
256
|
+
{isLoading ? (
|
|
257
|
+
<div className="flex flex-col gap-3">
|
|
258
|
+
<Skeleton className="h-[48px] w-full rounded-lg border" />
|
|
259
|
+
<Skeleton className="h-[48px] w-full rounded-lg border" />
|
|
260
|
+
<Skeleton className="h-[48px] w-full rounded-lg border" />
|
|
261
|
+
</div>
|
|
262
|
+
) : null}
|
|
212
263
|
{!isLoading && !error && sortedEvents.length === 0 && (
|
|
213
264
|
<div className="text-sm">No events found</div>
|
|
214
265
|
)}
|
|
215
266
|
{sortedEvents.length > 0 && !error ? (
|
|
216
|
-
<div className="flex flex-col gap-
|
|
267
|
+
<div className="flex flex-col gap-4">
|
|
217
268
|
{sortedEvents.map((event) => (
|
|
218
269
|
<EventItem
|
|
219
270
|
key={event.eventId}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Copy } from 'lucide-react';
|
|
4
|
+
import { toast } from 'sonner';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check whether `value` looks like a structured error object with a `stack`
|
|
8
|
+
* field that we can render as pre-formatted text.
|
|
9
|
+
*/
|
|
10
|
+
export function isStructuredErrorWithStack(
|
|
11
|
+
value: unknown
|
|
12
|
+
): value is Record<string, unknown> & { stack: string } {
|
|
13
|
+
return (
|
|
14
|
+
value != null &&
|
|
15
|
+
typeof value === 'object' &&
|
|
16
|
+
'stack' in value &&
|
|
17
|
+
typeof (value as Record<string, unknown>).stack === 'string'
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Renders an error with a `stack` field as readable pre-formatted text,
|
|
23
|
+
* styled to match the CopyableDataBlock component. The error message is
|
|
24
|
+
* displayed at the top with a visual separator from the stack trace.
|
|
25
|
+
* The entire block is copyable via a copy button.
|
|
26
|
+
*/
|
|
27
|
+
export function ErrorStackBlock({
|
|
28
|
+
value,
|
|
29
|
+
}: {
|
|
30
|
+
value: Record<string, unknown> & { stack: string };
|
|
31
|
+
}) {
|
|
32
|
+
const stack = value.stack;
|
|
33
|
+
const message = typeof value.message === 'string' ? value.message : undefined;
|
|
34
|
+
const copyText = message ? `${message}\n\n${stack}` : stack;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className="relative overflow-x-auto rounded-md border p-3 pt-9"
|
|
39
|
+
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
40
|
+
>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
aria-label="Copy error"
|
|
44
|
+
title="Copy"
|
|
45
|
+
className="!absolute !right-2 !top-2 !flex !h-6 !w-6 !items-center !justify-center !rounded-md !border !bg-[var(--ds-background-100)] !text-[var(--ds-gray-800)] transition-transform transition-colors duration-100 hover:!bg-[var(--ds-gray-alpha-200)] active:!scale-95 active:!bg-[var(--ds-gray-alpha-300)]"
|
|
46
|
+
style={{ borderColor: 'var(--ds-gray-300)' }}
|
|
47
|
+
onClick={() => {
|
|
48
|
+
navigator.clipboard
|
|
49
|
+
.writeText(copyText)
|
|
50
|
+
.then(() => {
|
|
51
|
+
toast.success('Copied to clipboard');
|
|
52
|
+
})
|
|
53
|
+
.catch(() => {
|
|
54
|
+
toast.error('Failed to copy');
|
|
55
|
+
});
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
<Copy size={12} />
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
{message && (
|
|
62
|
+
<p
|
|
63
|
+
className="pb-2 mb-2 text-xs font-semibold font-mono"
|
|
64
|
+
style={{
|
|
65
|
+
color: 'var(--ds-red-900)',
|
|
66
|
+
borderBottom: '1px solid var(--ds-gray-300)',
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{message}
|
|
70
|
+
</p>
|
|
71
|
+
)}
|
|
72
|
+
<pre
|
|
73
|
+
className="text-xs font-mono whitespace-pre-wrap break-words overflow-auto m-0"
|
|
74
|
+
style={{ color: 'var(--ds-gray-1000)' }}
|
|
75
|
+
>
|
|
76
|
+
{stack}
|
|
77
|
+
</pre>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|