@workflow/web-shared 4.1.0-beta.62 → 4.1.0-beta.64
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/README.md +4 -0
- package/dist/components/event-list-view.d.ts +9 -3
- package/dist/components/event-list-view.d.ts.map +1 -1
- package/dist/components/event-list-view.js +222 -98
- package/dist/components/event-list-view.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/run-trace-view.d.ts +1 -3
- 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 +11 -1
- package/dist/components/sidebar/attribute-panel.js.map +1 -1
- package/dist/components/sidebar/detail-card.d.ts.map +1 -1
- package/dist/components/sidebar/detail-card.js +4 -2
- package/dist/components/sidebar/detail-card.js.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.d.ts +3 -3
- package/dist/components/sidebar/entity-detail-panel.d.ts.map +1 -1
- package/dist/components/sidebar/entity-detail-panel.js +43 -26
- package/dist/components/sidebar/entity-detail-panel.js.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.d.ts +7 -1
- package/dist/components/trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/trace-viewer/trace-viewer.js +36 -11
- package/dist/components/trace-viewer/trace-viewer.js.map +1 -1
- package/dist/components/ui/error-stack-block.d.ts +3 -4
- package/dist/components/ui/error-stack-block.d.ts.map +1 -1
- package/dist/components/ui/error-stack-block.js +18 -9
- package/dist/components/ui/error-stack-block.js.map +1 -1
- package/dist/components/ui/menu-dropdown.d.ts +16 -0
- package/dist/components/ui/menu-dropdown.d.ts.map +1 -0
- package/dist/components/ui/menu-dropdown.js +50 -0
- package/dist/components/ui/menu-dropdown.js.map +1 -0
- package/dist/components/workflow-trace-view.d.ts +3 -3
- package/dist/components/workflow-trace-view.d.ts.map +1 -1
- package/dist/components/workflow-trace-view.js +31 -129
- package/dist/components/workflow-trace-view.js.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.d.ts +18 -5
- package/dist/components/workflow-traces/trace-span-construction.d.ts.map +1 -1
- package/dist/components/workflow-traces/trace-span-construction.js +65 -18
- package/dist/components/workflow-traces/trace-span-construction.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/event-materialization.d.ts +72 -0
- package/dist/lib/event-materialization.d.ts.map +1 -0
- package/dist/lib/event-materialization.js +171 -0
- package/dist/lib/event-materialization.js.map +1 -0
- package/dist/lib/trace-builder.d.ts +32 -0
- package/dist/lib/trace-builder.d.ts.map +1 -0
- package/dist/lib/trace-builder.js +129 -0
- package/dist/lib/trace-builder.js.map +1 -0
- package/package.json +3 -3
- package/src/components/event-list-view.tsx +324 -103
- package/src/components/index.ts +1 -0
- package/src/components/run-trace-view.tsx +0 -6
- package/src/components/sidebar/attribute-panel.tsx +17 -2
- package/src/components/sidebar/detail-card.tsx +10 -2
- package/src/components/sidebar/entity-detail-panel.tsx +59 -21
- package/src/components/trace-viewer/trace-viewer.tsx +47 -2
- package/src/components/ui/error-stack-block.tsx +26 -16
- package/src/components/ui/menu-dropdown.tsx +114 -0
- package/src/components/workflow-trace-view.tsx +95 -195
- package/src/components/workflow-traces/trace-span-construction.ts +85 -32
- package/src/index.ts +13 -0
- package/src/lib/event-materialization.ts +243 -0
- package/src/lib/trace-builder.ts +201 -0
package/src/components/index.ts
CHANGED
|
@@ -22,4 +22,5 @@ export type {
|
|
|
22
22
|
export { type StreamChunk, StreamViewer } from './stream-viewer';
|
|
23
23
|
export type { Span, SpanEvent } from './trace-viewer/types';
|
|
24
24
|
export { DataInspector, type DataInspectorProps } from './ui/data-inspector';
|
|
25
|
+
export { MenuDropdown, type MenuDropdownOption } from './ui/menu-dropdown';
|
|
25
26
|
export { WorkflowTraceViewer } from './workflow-trace-view';
|
|
@@ -7,8 +7,6 @@ import { WorkflowTraceViewer } from './workflow-trace-view';
|
|
|
7
7
|
|
|
8
8
|
interface RunTraceViewProps {
|
|
9
9
|
run: WorkflowRun;
|
|
10
|
-
steps: Step[];
|
|
11
|
-
hooks: Hook[];
|
|
12
10
|
events: Event[];
|
|
13
11
|
isLoading?: boolean;
|
|
14
12
|
error?: Error | null;
|
|
@@ -34,8 +32,6 @@ interface RunTraceViewProps {
|
|
|
34
32
|
|
|
35
33
|
export function RunTraceView({
|
|
36
34
|
run,
|
|
37
|
-
steps,
|
|
38
|
-
hooks,
|
|
39
35
|
events,
|
|
40
36
|
isLoading,
|
|
41
37
|
error,
|
|
@@ -65,9 +61,7 @@ export function RunTraceView({
|
|
|
65
61
|
<div className="w-full h-full relative">
|
|
66
62
|
<WorkflowTraceViewer
|
|
67
63
|
error={error}
|
|
68
|
-
steps={steps}
|
|
69
64
|
events={events}
|
|
70
|
-
hooks={hooks}
|
|
71
65
|
run={run}
|
|
72
66
|
isLoading={isLoading}
|
|
73
67
|
spanDetailData={spanDetailData}
|
|
@@ -215,7 +215,10 @@ type AttributeKey =
|
|
|
215
215
|
| 'eventData'
|
|
216
216
|
| 'resumeAt'
|
|
217
217
|
| 'expiredAt'
|
|
218
|
-
| 'workflowCoreVersion'
|
|
218
|
+
| 'workflowCoreVersion'
|
|
219
|
+
| 'receivedCount'
|
|
220
|
+
| 'lastReceivedAt'
|
|
221
|
+
| 'disposedAt';
|
|
219
222
|
|
|
220
223
|
const attributeOrder: AttributeKey[] = [
|
|
221
224
|
'workflowName',
|
|
@@ -228,6 +231,9 @@ const attributeOrder: AttributeKey[] = [
|
|
|
228
231
|
'runId',
|
|
229
232
|
'attempt',
|
|
230
233
|
'token',
|
|
234
|
+
'receivedCount',
|
|
235
|
+
'lastReceivedAt',
|
|
236
|
+
'disposedAt',
|
|
231
237
|
'correlationId',
|
|
232
238
|
'eventType',
|
|
233
239
|
'deploymentId',
|
|
@@ -262,6 +268,7 @@ const sortByAttributeOrder = (a: string, b: string): number => {
|
|
|
262
268
|
*/
|
|
263
269
|
const attributeDisplayNames: Partial<Record<AttributeKey, string>> = {
|
|
264
270
|
workflowCoreVersion: '@workflow/core version',
|
|
271
|
+
receivedCount: 'times resolved',
|
|
265
272
|
};
|
|
266
273
|
|
|
267
274
|
/**
|
|
@@ -353,6 +360,9 @@ const attributeToDisplayFn: Record<
|
|
|
353
360
|
// Hook details
|
|
354
361
|
token: (value: unknown) => String(value),
|
|
355
362
|
isWebhook: (value: unknown) => String(value),
|
|
363
|
+
receivedCount: (value: unknown) => String(value),
|
|
364
|
+
lastReceivedAt: localMillisecondTimeOrNull,
|
|
365
|
+
disposedAt: localMillisecondTimeOrNull,
|
|
356
366
|
// Event details
|
|
357
367
|
eventType: (value: unknown) => String(value),
|
|
358
368
|
correlationId: (value: unknown) => String(value),
|
|
@@ -773,13 +783,18 @@ export const AttributePanel = ({
|
|
|
773
783
|
typeof displayValue === 'string'
|
|
774
784
|
? displayValue
|
|
775
785
|
: String(displayValue ?? displayData.moduleSpecifier ?? '');
|
|
786
|
+
const shouldCapitalizeLabel = attribute !== 'workflowCoreVersion';
|
|
776
787
|
const showDivider = index < orderedBasicAttributes.length - 1;
|
|
777
788
|
|
|
778
789
|
return (
|
|
779
790
|
<div key={attribute} className="py-1">
|
|
780
791
|
<div className="flex min-h-[32px] items-center justify-between gap-4 rounded-sm px-2.5 py-1">
|
|
781
792
|
<span
|
|
782
|
-
className=
|
|
793
|
+
className={
|
|
794
|
+
shouldCapitalizeLabel
|
|
795
|
+
? 'text-[14px] first-letter:uppercase'
|
|
796
|
+
: 'text-[14px]'
|
|
797
|
+
}
|
|
783
798
|
style={{ color: 'var(--ds-gray-700)' }}
|
|
784
799
|
>
|
|
785
800
|
{getAttributeDisplayName(attribute)}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ChevronRight } from 'lucide-react';
|
|
1
2
|
import type { ReactNode } from 'react';
|
|
2
3
|
|
|
3
4
|
export function DetailCard({
|
|
@@ -42,14 +43,21 @@ export function DetailCard({
|
|
|
42
43
|
onToggle={(e) => onToggle?.((e.target as HTMLDetailsElement).open)}
|
|
43
44
|
>
|
|
44
45
|
<summary
|
|
45
|
-
className={`cursor-pointer rounded-md border px-2.5 py-1.5 text-xs hover:brightness-95 ${summaryClassName ?? ''}`}
|
|
46
|
+
className={`cursor-pointer rounded-md border px-2.5 py-1.5 text-xs hover:brightness-95 [&::-webkit-details-marker]:hidden ${summaryClassName ?? ''}`}
|
|
46
47
|
style={{
|
|
47
48
|
borderColor: 'var(--ds-gray-300)',
|
|
48
49
|
backgroundColor: 'var(--ds-gray-100)',
|
|
49
50
|
color: 'var(--ds-gray-900)',
|
|
51
|
+
listStyle: 'none',
|
|
50
52
|
}}
|
|
51
53
|
>
|
|
52
|
-
|
|
54
|
+
<span className="flex items-center gap-1.5">
|
|
55
|
+
<ChevronRight
|
|
56
|
+
size={14}
|
|
57
|
+
className="shrink-0 transition-transform group-open:rotate-90"
|
|
58
|
+
/>
|
|
59
|
+
{summary}
|
|
60
|
+
</span>
|
|
53
61
|
</summary>
|
|
54
62
|
{/* Expanded content with connecting line */}
|
|
55
63
|
<div className={`relative pl-6 mt-3 ${contentClassName ?? ''}`}>
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import type { Event, Hook, Step, WorkflowRun } from '@workflow/world';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
|
-
import { Send, Zap } from 'lucide-react';
|
|
5
|
+
import { Lock, Send, Unlock, Zap } from 'lucide-react';
|
|
6
6
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
7
|
import { toast } from 'sonner';
|
|
8
|
+
import { isEncryptedMarker } from '../../lib/hydration';
|
|
8
9
|
import { AttributePanel } from './attribute-panel';
|
|
9
10
|
import { EventsList } from './events-list';
|
|
10
11
|
import { ResolveHookModal } from './resolve-hook-modal';
|
|
@@ -53,7 +54,6 @@ export interface SelectedSpanInfo {
|
|
|
53
54
|
*/
|
|
54
55
|
export function EntityDetailPanel({
|
|
55
56
|
run,
|
|
56
|
-
hooks,
|
|
57
57
|
onStreamClick,
|
|
58
58
|
spanDetailData,
|
|
59
59
|
spanDetailError,
|
|
@@ -63,11 +63,10 @@ export function EntityDetailPanel({
|
|
|
63
63
|
onLoadEventData,
|
|
64
64
|
onResolveHook,
|
|
65
65
|
encryptionKey,
|
|
66
|
+
onDecrypt,
|
|
66
67
|
selectedSpan,
|
|
67
68
|
}: {
|
|
68
69
|
run: WorkflowRun;
|
|
69
|
-
/** All hooks for the current run (used as fallback for token lookup). */
|
|
70
|
-
hooks?: Hook[];
|
|
71
70
|
/** Callback when a stream reference is clicked */
|
|
72
71
|
onStreamClick?: (streamId: string) => void;
|
|
73
72
|
/** Pre-fetched span detail data for the selected span. */
|
|
@@ -96,6 +95,8 @@ export function EntityDetailPanel({
|
|
|
96
95
|
) => Promise<void>;
|
|
97
96
|
/** Encryption key (available after Decrypt is clicked), used to re-load event data */
|
|
98
97
|
encryptionKey?: Uint8Array;
|
|
98
|
+
/** Callback to initiate decryption of encrypted run data */
|
|
99
|
+
onDecrypt?: () => void;
|
|
99
100
|
/** Info about the currently selected span from the trace viewer */
|
|
100
101
|
selectedSpan: SelectedSpanInfo | null;
|
|
101
102
|
}): React.JSX.Element | null {
|
|
@@ -129,10 +130,11 @@ export function EntityDetailPanel({
|
|
|
129
130
|
return { resource: 'hook', resourceId: data.hookId, runId: undefined };
|
|
130
131
|
}
|
|
131
132
|
if (res === 'sleep') {
|
|
133
|
+
const waitData = data as { runId?: string } | undefined;
|
|
132
134
|
return {
|
|
133
135
|
resource: 'sleep',
|
|
134
136
|
resourceId: selectedSpan.spanId,
|
|
135
|
-
runId:
|
|
137
|
+
runId: waitData?.runId,
|
|
136
138
|
};
|
|
137
139
|
}
|
|
138
140
|
return { resource: undefined, resourceId: undefined, runId: undefined };
|
|
@@ -196,6 +198,17 @@ export function EntityDetailPanel({
|
|
|
196
198
|
const error = spanDetailError ?? undefined;
|
|
197
199
|
const loading = spanDetailLoading ?? false;
|
|
198
200
|
|
|
201
|
+
const hasEncryptedFields = useMemo(() => {
|
|
202
|
+
if (!spanDetailData) return false;
|
|
203
|
+
const d = spanDetailData as Record<string, unknown>;
|
|
204
|
+
return (
|
|
205
|
+
isEncryptedMarker(d.input) ||
|
|
206
|
+
isEncryptedMarker(d.output) ||
|
|
207
|
+
isEncryptedMarker(d.error) ||
|
|
208
|
+
isEncryptedMarker(d.metadata)
|
|
209
|
+
);
|
|
210
|
+
}, [spanDetailData]);
|
|
211
|
+
|
|
199
212
|
// Get the hook token for resolving (prefer fetched data, then hooks array fallback)
|
|
200
213
|
const hookToken = useMemo(() => {
|
|
201
214
|
if (resource !== 'hook' || !resourceId) return undefined;
|
|
@@ -203,17 +216,12 @@ export function EntityDetailPanel({
|
|
|
203
216
|
if (isHook(spanDetailData) && spanDetailData.token) {
|
|
204
217
|
return spanDetailData.token;
|
|
205
218
|
}
|
|
206
|
-
// 2. Try the
|
|
207
|
-
const hookFromArray = hooks?.find((h) => h.hookId === resourceId);
|
|
208
|
-
if (hookFromArray?.token) {
|
|
209
|
-
return hookFromArray.token;
|
|
210
|
-
}
|
|
211
|
-
// 3. Try the span's inline data (partial hook from events - may lack token)
|
|
219
|
+
// 2. Try the span's inline data (reconstructed from hook_created event)
|
|
212
220
|
if (isHook(data) && (data as Hook).token) {
|
|
213
221
|
return (data as Hook).token;
|
|
214
222
|
}
|
|
215
223
|
return undefined;
|
|
216
|
-
}, [resource, resourceId, spanDetailData, data
|
|
224
|
+
}, [resource, resourceId, spanDetailData, data]);
|
|
217
225
|
|
|
218
226
|
useEffect(() => {
|
|
219
227
|
if (error && selectedSpan && resource) {
|
|
@@ -299,16 +307,15 @@ export function EntityDetailPanel({
|
|
|
299
307
|
[onResolveHook, hookToken, resolvingHook, spanDetailData, data]
|
|
300
308
|
);
|
|
301
309
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
310
|
+
// Prefer externally-fetched details when available. For sleep spans, the
|
|
311
|
+
// host fetches full correlated events (withData=true) and materializes a wait
|
|
312
|
+
// entity, so this includes resumeAt/completedAt without bloating trace payloads.
|
|
313
|
+
const displayData = (spanDetailData ?? data) as
|
|
314
|
+
| WorkflowRun
|
|
315
|
+
| Step
|
|
316
|
+
| Hook
|
|
317
|
+
| Event;
|
|
305
318
|
|
|
306
|
-
// For sleep spans, spanDetailData from the host is typically an events array
|
|
307
|
-
// (not a single entity), so always prefer the inline wait entity from span
|
|
308
|
-
// attributes which contains waitId, runId, createdAt, resumeAt, completedAt.
|
|
309
|
-
const displayData = (
|
|
310
|
-
resource === 'sleep' ? data : (spanDetailData ?? data)
|
|
311
|
-
) as WorkflowRun | Step | Hook | Event;
|
|
312
319
|
const moduleSpecifier = useMemo(() => {
|
|
313
320
|
const displayRecord = displayData as Record<string, unknown>;
|
|
314
321
|
const displayStepName = displayRecord.stepName;
|
|
@@ -325,6 +332,10 @@ export function EntityDetailPanel({
|
|
|
325
332
|
return undefined;
|
|
326
333
|
}, [displayData, run.workflowName]);
|
|
327
334
|
|
|
335
|
+
if (!selectedSpan || !resource || !resourceId) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
328
339
|
const resourceLabel = resource.charAt(0).toUpperCase() + resource.slice(1);
|
|
329
340
|
const hasPendingActions =
|
|
330
341
|
(resource === 'sleep' && canWakeUp) ||
|
|
@@ -369,6 +380,33 @@ export function EntityDetailPanel({
|
|
|
369
380
|
{resourceId}
|
|
370
381
|
</p>
|
|
371
382
|
</div>
|
|
383
|
+
{(hasEncryptedFields || encryptionKey) && onDecrypt && (
|
|
384
|
+
<button
|
|
385
|
+
type="button"
|
|
386
|
+
onClick={onDecrypt}
|
|
387
|
+
disabled={!!encryptionKey}
|
|
388
|
+
className="inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs font-medium transition-colors flex-shrink-0"
|
|
389
|
+
style={{
|
|
390
|
+
borderColor: encryptionKey
|
|
391
|
+
? 'var(--ds-green-400)'
|
|
392
|
+
: 'var(--ds-gray-300)',
|
|
393
|
+
color: encryptionKey
|
|
394
|
+
? 'var(--ds-green-900)'
|
|
395
|
+
: 'var(--ds-gray-900)',
|
|
396
|
+
backgroundColor: encryptionKey
|
|
397
|
+
? 'var(--ds-green-100)'
|
|
398
|
+
: 'var(--ds-background-100)',
|
|
399
|
+
cursor: encryptionKey ? 'default' : 'pointer',
|
|
400
|
+
}}
|
|
401
|
+
>
|
|
402
|
+
{encryptionKey ? (
|
|
403
|
+
<Unlock className="h-3 w-3" />
|
|
404
|
+
) : (
|
|
405
|
+
<Lock className="h-3 w-3" />
|
|
406
|
+
)}
|
|
407
|
+
{encryptionKey ? 'Decrypted' : 'Decrypt'}
|
|
408
|
+
</button>
|
|
409
|
+
)}
|
|
372
410
|
</div>
|
|
373
411
|
</div>
|
|
374
412
|
|
|
@@ -103,7 +103,16 @@ export function TraceViewerTimeline({
|
|
|
103
103
|
highlightedSpans,
|
|
104
104
|
eagerRender = false,
|
|
105
105
|
isLive = false,
|
|
106
|
-
|
|
106
|
+
footer,
|
|
107
|
+
knownDurationMs,
|
|
108
|
+
hasMoreData = false,
|
|
109
|
+
}: Omit<TraceViewerProps, 'getQuickLinks'> & {
|
|
110
|
+
footer?: ReactNode;
|
|
111
|
+
/** Duration in ms from trace start to the latest known event. Used to render the unknown-time overlay. */
|
|
112
|
+
knownDurationMs?: number;
|
|
113
|
+
/** Whether more data pages are expected. Controls the unknown-data overlay visibility. */
|
|
114
|
+
hasMoreData?: boolean;
|
|
115
|
+
}): ReactNode {
|
|
107
116
|
const isSkeleton = trace === skeletonTrace;
|
|
108
117
|
const { state, dispatch } = useTraceViewer();
|
|
109
118
|
const { timelineRef, scrollSnapshotRef } = state;
|
|
@@ -450,7 +459,7 @@ export function TraceViewerTimeline({
|
|
|
450
459
|
style={{
|
|
451
460
|
position: 'relative',
|
|
452
461
|
width: state.timelineWidth,
|
|
453
|
-
|
|
462
|
+
minHeight: state.timelineHeight - TIMELINE_PADDING * 2,
|
|
454
463
|
padding: TIMELINE_PADDING,
|
|
455
464
|
paddingBottom: 0,
|
|
456
465
|
}}
|
|
@@ -487,8 +496,44 @@ export function TraceViewerTimeline({
|
|
|
487
496
|
scrollSnapshotRef={scrollSnapshotRef}
|
|
488
497
|
spans={spans}
|
|
489
498
|
/>
|
|
499
|
+
{/* Horizontal "unknown time" overlay — covers the region to the
|
|
500
|
+
right of the latest known event, indicating data beyond this
|
|
501
|
+
point hasn't been loaded yet. */}
|
|
502
|
+
{knownDurationMs != null &&
|
|
503
|
+
knownDurationMs > 0 &&
|
|
504
|
+
(hasMoreData || isLive) &&
|
|
505
|
+
state.root.duration > 0 &&
|
|
506
|
+
(() => {
|
|
507
|
+
const knownPx = knownDurationMs * scale;
|
|
508
|
+
const totalPx = state.root.duration * scale;
|
|
509
|
+
const unknownWidth = totalPx - knownPx;
|
|
510
|
+
// Only show if the unknown region is meaningfully wide
|
|
511
|
+
if (unknownWidth < 4) return null;
|
|
512
|
+
// Offset ~5% into the unknown region so it doesn't touch spans
|
|
513
|
+
const insetPx = Math.min(unknownWidth * 0.05, 20);
|
|
514
|
+
return (
|
|
515
|
+
<div
|
|
516
|
+
style={{
|
|
517
|
+
position: 'absolute',
|
|
518
|
+
top: 0,
|
|
519
|
+
left: knownPx + insetPx,
|
|
520
|
+
width: unknownWidth - insetPx,
|
|
521
|
+
height: '100%',
|
|
522
|
+
pointerEvents: 'none',
|
|
523
|
+
zIndex: 1,
|
|
524
|
+
maskImage:
|
|
525
|
+
'linear-gradient(to right, transparent 1%, black 3%)',
|
|
526
|
+
WebkitMaskImage:
|
|
527
|
+
'linear-gradient(to right, transparent 1%, black 3%)',
|
|
528
|
+
background:
|
|
529
|
+
'repeating-linear-gradient(-45deg, var(--ds-background-200) 0, var(--ds-background-200) 11px, var(--ds-gray-200) 11px, var(--ds-gray-200) 12px)',
|
|
530
|
+
}}
|
|
531
|
+
/>
|
|
532
|
+
);
|
|
533
|
+
})()}
|
|
490
534
|
</div>
|
|
491
535
|
</div>
|
|
536
|
+
{footer}
|
|
492
537
|
</div>
|
|
493
538
|
<div className={styles.zoomButtonTraceViewer}>
|
|
494
539
|
<ZoomButton />
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Copy } from 'lucide-react';
|
|
3
|
+
import { AlertCircle, Copy } from 'lucide-react';
|
|
4
4
|
import { toast } from 'sonner';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -19,10 +19,9 @@ export function isStructuredErrorWithStack(
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Renders an error with a `stack` field as
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* The entire block is copyable via a copy button.
|
|
22
|
+
* Renders an error with a `stack` field as a visually distinct error block.
|
|
23
|
+
* Shows the error message with an alert icon at the top, separated from
|
|
24
|
+
* the stack trace below.
|
|
26
25
|
*/
|
|
27
26
|
export function ErrorStackBlock({
|
|
28
27
|
value,
|
|
@@ -35,15 +34,22 @@ export function ErrorStackBlock({
|
|
|
35
34
|
|
|
36
35
|
return (
|
|
37
36
|
<div
|
|
38
|
-
className="relative overflow-
|
|
39
|
-
style={{
|
|
37
|
+
className="relative overflow-hidden rounded-md border"
|
|
38
|
+
style={{
|
|
39
|
+
borderColor: 'var(--ds-red-400)',
|
|
40
|
+
background: 'var(--ds-red-100)',
|
|
41
|
+
}}
|
|
40
42
|
>
|
|
41
43
|
<button
|
|
42
44
|
type="button"
|
|
43
45
|
aria-label="Copy error"
|
|
44
46
|
title="Copy"
|
|
45
|
-
className="!absolute !right-2 !top-2 !flex !h-6 !w-6 !items-center !justify-center !rounded-md !border
|
|
46
|
-
style={{
|
|
47
|
+
className="!absolute !right-2 !top-2 !flex !h-6 !w-6 !items-center !justify-center !rounded-md !border transition-transform transition-colors duration-100 hover:!bg-[var(--ds-red-200)] active:!scale-95"
|
|
48
|
+
style={{
|
|
49
|
+
borderColor: 'var(--ds-red-400)',
|
|
50
|
+
background: 'var(--ds-red-100)',
|
|
51
|
+
color: 'var(--ds-red-900)',
|
|
52
|
+
}}
|
|
47
53
|
onClick={() => {
|
|
48
54
|
navigator.clipboard
|
|
49
55
|
.writeText(copyText)
|
|
@@ -59,19 +65,23 @@ export function ErrorStackBlock({
|
|
|
59
65
|
</button>
|
|
60
66
|
|
|
61
67
|
{message && (
|
|
62
|
-
<
|
|
63
|
-
className="
|
|
68
|
+
<div
|
|
69
|
+
className="flex items-start gap-2 px-3 py-2.5 pr-10"
|
|
64
70
|
style={{
|
|
65
71
|
color: 'var(--ds-red-900)',
|
|
66
|
-
borderBottom: '1px solid var(--ds-
|
|
72
|
+
borderBottom: '1px solid var(--ds-red-400)',
|
|
67
73
|
}}
|
|
68
74
|
>
|
|
69
|
-
{
|
|
70
|
-
|
|
75
|
+
<AlertCircle className="h-4 w-4 shrink-0" style={{ marginTop: 1 }} />
|
|
76
|
+
<p className="text-xs font-semibold m-0 break-words">{message}</p>
|
|
77
|
+
</div>
|
|
71
78
|
)}
|
|
72
79
|
<pre
|
|
73
|
-
className="text-xs font-mono whitespace-pre-wrap break-words overflow-auto m-0"
|
|
74
|
-
style={{
|
|
80
|
+
className="px-3 py-2.5 text-xs font-mono whitespace-pre-wrap break-words overflow-auto m-0"
|
|
81
|
+
style={{
|
|
82
|
+
color: 'var(--ds-red-900)',
|
|
83
|
+
background: 'var(--ds-red-200)',
|
|
84
|
+
}}
|
|
75
85
|
>
|
|
76
86
|
{stack}
|
|
77
87
|
</pre>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface MenuDropdownOption<T extends string = string> {
|
|
6
|
+
value: T;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface MenuDropdownProps<T extends string = string> {
|
|
11
|
+
options: MenuDropdownOption<T>[];
|
|
12
|
+
value: T;
|
|
13
|
+
onChange: (value: T) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A dropdown menu that matches Geist's MenuButton (secondary) + Menu styling.
|
|
18
|
+
* Uses CSS classes with proper :hover specificity (no inline background).
|
|
19
|
+
*/
|
|
20
|
+
export function MenuDropdown<T extends string = string>({
|
|
21
|
+
options,
|
|
22
|
+
value,
|
|
23
|
+
onChange,
|
|
24
|
+
}: MenuDropdownProps<T>) {
|
|
25
|
+
const [open, setOpen] = useState(false);
|
|
26
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
27
|
+
const label =
|
|
28
|
+
options.find((o) => o.value === value)?.label ?? options[0]?.label ?? '';
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!open) return;
|
|
32
|
+
function handleClickOutside(e: MouseEvent) {
|
|
33
|
+
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
34
|
+
setOpen(false);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
38
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
39
|
+
}, [open]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div ref={ref} style={{ position: 'relative', flexShrink: 0 }}>
|
|
43
|
+
<style>{`
|
|
44
|
+
.wf-menu-btn{appearance:none;-webkit-appearance:none;border:none;display:inline-flex;align-items:center;justify-content:center;height:40px;padding:0 12px;border-radius:6px;font-size:14px;font-weight:500;line-height:20px;color:var(--ds-gray-1000);background:var(--ds-background-100);box-shadow:0 0 0 1px var(--ds-gray-400);cursor:pointer;white-space:nowrap;transition:background 150ms}
|
|
45
|
+
.wf-menu-btn:hover{background:var(--ds-gray-alpha-200)}
|
|
46
|
+
.wf-menu-item{appearance:none;-webkit-appearance:none;border:none;display:flex;align-items:center;width:100%;height:40px;padding:0 8px;border-radius:6px;font-size:14px;color:var(--ds-gray-1000);background:transparent;cursor:pointer;transition:background 150ms}
|
|
47
|
+
.wf-menu-item:hover{background:var(--ds-gray-alpha-100)}
|
|
48
|
+
`}</style>
|
|
49
|
+
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
className="wf-menu-btn"
|
|
53
|
+
onClick={() => setOpen(!open)}
|
|
54
|
+
>
|
|
55
|
+
<span>{label}</span>
|
|
56
|
+
<svg
|
|
57
|
+
width={16}
|
|
58
|
+
height={16}
|
|
59
|
+
viewBox="0 0 16 16"
|
|
60
|
+
fill="none"
|
|
61
|
+
style={{
|
|
62
|
+
marginLeft: 16,
|
|
63
|
+
marginRight: -4,
|
|
64
|
+
color: 'var(--ds-gray-900)',
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<path
|
|
68
|
+
d="M4.5 6L8 9.5L11.5 6"
|
|
69
|
+
stroke="currentColor"
|
|
70
|
+
strokeWidth="1.5"
|
|
71
|
+
strokeLinecap="round"
|
|
72
|
+
strokeLinejoin="round"
|
|
73
|
+
/>
|
|
74
|
+
</svg>
|
|
75
|
+
</button>
|
|
76
|
+
|
|
77
|
+
{open && (
|
|
78
|
+
<div
|
|
79
|
+
style={{
|
|
80
|
+
position: 'absolute',
|
|
81
|
+
right: 0,
|
|
82
|
+
top: '100%',
|
|
83
|
+
marginTop: 4,
|
|
84
|
+
minWidth: 140,
|
|
85
|
+
padding: 4,
|
|
86
|
+
borderRadius: 12,
|
|
87
|
+
background: 'var(--ds-background-100)',
|
|
88
|
+
boxShadow: 'var(--ds-shadow-menu, var(--ds-shadow-medium))',
|
|
89
|
+
zIndex: 2001,
|
|
90
|
+
}}
|
|
91
|
+
role="menu"
|
|
92
|
+
>
|
|
93
|
+
{options.map((option) => (
|
|
94
|
+
<button
|
|
95
|
+
key={option.value}
|
|
96
|
+
type="button"
|
|
97
|
+
role="menuitem"
|
|
98
|
+
className="wf-menu-item"
|
|
99
|
+
style={{
|
|
100
|
+
fontWeight: option.value === value ? 500 : 400,
|
|
101
|
+
}}
|
|
102
|
+
onClick={() => {
|
|
103
|
+
onChange(option.value);
|
|
104
|
+
setOpen(false);
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{option.label}
|
|
108
|
+
</button>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|