@workflow/web-shared 5.0.0-beta.8 → 5.0.0-beta.9
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/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -1
- package/dist/components/new-trace-viewer/components/event-list.d.ts +3 -1
- package/dist/components/new-trace-viewer/components/event-list.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/event-list.js +8 -7
- package/dist/components/new-trace-viewer/components/timeline.d.ts +4 -1
- package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/timeline.js +6 -5
- package/dist/components/new-trace-viewer/detail-panel.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/detail-panel.js +3 -2
- package/dist/components/new-trace-viewer/search.d.ts +19 -0
- package/dist/components/new-trace-viewer/search.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/search.js +134 -0
- package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/trace-viewer.js +23 -18
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/button.js +10 -3
- package/dist/components/ui/icon-button.d.ts +13 -0
- package/dist/components/ui/icon-button.d.ts.map +1 -0
- package/dist/components/ui/icon-button.js +29 -0
- package/dist/styles.css +5 -0
- package/package.json +2 -2
- package/src/components/index.ts +1 -0
- package/src/components/new-trace-viewer/components/event-list.tsx +16 -5
- package/src/components/new-trace-viewer/components/timeline.tsx +11 -2
- package/src/components/new-trace-viewer/detail-panel.tsx +3 -6
- package/src/components/new-trace-viewer/search.ts +178 -0
- package/src/components/new-trace-viewer/trace-viewer.tsx +53 -46
- package/src/components/ui/button.tsx +9 -2
- package/src/components/ui/icon-button.tsx +57 -0
- package/src/styles.css +5 -0
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
type ReactNode,
|
|
15
15
|
useCallback,
|
|
16
|
+
useDeferredValue,
|
|
16
17
|
useEffect,
|
|
17
18
|
useMemo,
|
|
18
19
|
useRef,
|
|
@@ -27,15 +28,17 @@ import {
|
|
|
27
28
|
import { useSidebarDataOptional } from '../sidebar/sidebar-data-context';
|
|
28
29
|
import type { Trace } from '../trace-viewer/types';
|
|
29
30
|
import { formatDuration, getHighResInMs } from '../trace-viewer/util/timing';
|
|
31
|
+
import { IconButton } from '../ui/icon-button';
|
|
30
32
|
import EventList from './components/event-list';
|
|
31
33
|
import { SplitPane } from './components/split-pane';
|
|
32
34
|
import {
|
|
35
|
+
TIMELINE_PADDING_PX,
|
|
33
36
|
Timeline,
|
|
34
37
|
TimelineHeader,
|
|
35
|
-
TIMELINE_PADDING_PX,
|
|
36
38
|
} from './components/timeline';
|
|
37
39
|
import { ActiveSpanProvider, useActiveSpan } from './context';
|
|
38
40
|
import { DetailPanel } from './detail-panel';
|
|
41
|
+
import { searchSpans } from './search';
|
|
39
42
|
import { computeRootBounds, computeTimeMarkers } from './utils';
|
|
40
43
|
|
|
41
44
|
interface NewTraceViewerProps {
|
|
@@ -142,7 +145,7 @@ function useSelectedSpanInfo(): SelectedSpanInfo | null {
|
|
|
142
145
|
spanId: activeSpan.spanId,
|
|
143
146
|
rawEvents,
|
|
144
147
|
};
|
|
145
|
-
}, [activeSpan, sidebar
|
|
148
|
+
}, [activeSpan, sidebar]);
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
// ---------------------------------------------------------------------------
|
|
@@ -165,15 +168,12 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
165
168
|
const selectedSpan = useSelectedSpanInfo();
|
|
166
169
|
|
|
167
170
|
const [searchQuery, setSearchQuery] = useState('');
|
|
171
|
+
const deferredSearchQuery = useDeferredValue(searchQuery);
|
|
168
172
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
(s) =>
|
|
174
|
-
s.name.toLowerCase().includes(q) || s.resource.toLowerCase().includes(q)
|
|
175
|
-
);
|
|
176
|
-
}, [trace.spans, searchQuery]);
|
|
173
|
+
const searchResult = useMemo(
|
|
174
|
+
() => searchSpans(trace.spans, deferredSearchQuery),
|
|
175
|
+
[trace.spans, deferredSearchQuery]
|
|
176
|
+
);
|
|
177
177
|
|
|
178
178
|
const root = useMemo(() => computeRootBounds(trace.spans), [trace.spans]);
|
|
179
179
|
|
|
@@ -204,7 +204,7 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
204
204
|
});
|
|
205
205
|
|
|
206
206
|
prevRootRef.current = { start: newStart, end: newEnd };
|
|
207
|
-
}, [root.startTime, root.duration]);
|
|
207
|
+
}, [root.startTime, root.duration, setViewport]);
|
|
208
208
|
|
|
209
209
|
const viewDuration = viewport.end - viewport.start;
|
|
210
210
|
|
|
@@ -420,7 +420,8 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
420
420
|
(e.clientX - rect.left - TIMELINE_PADDING_PX) / contentWidth
|
|
421
421
|
)
|
|
422
422
|
);
|
|
423
|
-
const
|
|
423
|
+
const isMouseWheel = e.deltaMode === 1 || Math.abs(e.deltaY) >= 50;
|
|
424
|
+
const scaleFactor = Math.pow(2, dy / (isMouseWheel ? 200 : 60));
|
|
424
425
|
|
|
425
426
|
setViewport((prev) => {
|
|
426
427
|
const prevDuration = prev.end - prev.start;
|
|
@@ -471,7 +472,7 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
471
472
|
|
|
472
473
|
el.addEventListener('wheel', onWheel, { passive: false });
|
|
473
474
|
return () => el.removeEventListener('wheel', onWheel);
|
|
474
|
-
}, [root.startTime, root.duration]);
|
|
475
|
+
}, [root.startTime, root.duration, setViewport]);
|
|
475
476
|
|
|
476
477
|
// Derive the selected span name and metadata for the panel header
|
|
477
478
|
const selectedSpanName = useMemo(() => {
|
|
@@ -495,13 +496,13 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
495
496
|
|
|
496
497
|
const { prevSpanId, nextSpanId } = useMemo(() => {
|
|
497
498
|
if (!activeSpanId) return { prevSpanId: null, nextSpanId: null };
|
|
498
|
-
const i =
|
|
499
|
+
const i = trace.spans.findIndex((s) => s.spanId === activeSpanId);
|
|
499
500
|
if (i === -1) return { prevSpanId: null, nextSpanId: null };
|
|
500
501
|
return {
|
|
501
|
-
prevSpanId:
|
|
502
|
-
nextSpanId:
|
|
502
|
+
prevSpanId: trace.spans[i - 1]?.spanId ?? null,
|
|
503
|
+
nextSpanId: trace.spans[i + 1]?.spanId ?? null,
|
|
503
504
|
};
|
|
504
|
-
}, [activeSpanId,
|
|
505
|
+
}, [activeSpanId, trace.spans]);
|
|
505
506
|
|
|
506
507
|
const handleSelectPrevSpan = useCallback(() => {
|
|
507
508
|
if (prevSpanId) handleSelectSpan(prevSpanId);
|
|
@@ -538,6 +539,13 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
538
539
|
type="text"
|
|
539
540
|
value={searchQuery}
|
|
540
541
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
542
|
+
onKeyDown={(e) => {
|
|
543
|
+
if (e.key === 'Escape' && searchQuery) {
|
|
544
|
+
e.preventDefault();
|
|
545
|
+
e.stopPropagation();
|
|
546
|
+
setSearchQuery('');
|
|
547
|
+
}
|
|
548
|
+
}}
|
|
541
549
|
placeholder="Search spans..."
|
|
542
550
|
aria-label="Search spans"
|
|
543
551
|
className="flex-1 min-w-0 bg-transparent text-sm text-gray-1000 placeholder:text-gray-800 outline-none"
|
|
@@ -547,9 +555,11 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
547
555
|
type="button"
|
|
548
556
|
aria-label="Clear search"
|
|
549
557
|
onClick={() => setSearchQuery('')}
|
|
550
|
-
className="shrink-0
|
|
558
|
+
className="-mr-2 hidden h-full max-w-full shrink-0 cursor-pointer items-center rounded-r-md border-0 bg-transparent px-2.5 font-inherit text-base text-gray-900 no-underline transition-colors duration-150 ease-in hover:text-gray-1000 focus-visible:-outline-offset-1 focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--ds-focus-color)] min-[961px]:flex"
|
|
551
559
|
>
|
|
552
|
-
<
|
|
560
|
+
<kbd className="inline-flex h-5 min-h-5 min-w-5 items-center justify-center rounded border border-gray-alpha-400 bg-background-100 px-1 font-sans text-[13px] font-medium leading-[1.7em] text-gray-900">
|
|
561
|
+
Esc
|
|
562
|
+
</kbd>
|
|
553
563
|
</button>
|
|
554
564
|
)}
|
|
555
565
|
</div>
|
|
@@ -560,11 +570,13 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
560
570
|
>
|
|
561
571
|
<div className="block overflow-visible">
|
|
562
572
|
<EventList
|
|
563
|
-
spans={
|
|
573
|
+
spans={trace.spans}
|
|
564
574
|
activeSpanId={activeSpanId}
|
|
575
|
+
searchResult={searchResult}
|
|
565
576
|
onSelectSpan={handleSelectSpan}
|
|
566
577
|
/>
|
|
567
578
|
</div>
|
|
579
|
+
{/* biome-ignore lint/a11y/noStaticElementInteractions: timeline hover and wheel gestures are pointer-only annotations */}
|
|
568
580
|
<div
|
|
569
581
|
ref={timelineRef}
|
|
570
582
|
className="block min-h-0 overflow-visible relative"
|
|
@@ -573,42 +585,43 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
573
585
|
onMouseLeave={handleTimelineMouseLeave}
|
|
574
586
|
>
|
|
575
587
|
<Timeline
|
|
576
|
-
spans={
|
|
588
|
+
spans={trace.spans}
|
|
577
589
|
viewStart={viewport.start}
|
|
578
590
|
viewEnd={viewport.end}
|
|
579
591
|
markers={timeMarkers}
|
|
580
592
|
selectedId={activeSpanId}
|
|
593
|
+
searchResult={searchResult}
|
|
581
594
|
onSelect={handleSelectSpan}
|
|
582
595
|
hoverFraction={hoverFraction}
|
|
583
596
|
altHeld={altHeld}
|
|
584
597
|
/>
|
|
585
598
|
</div>
|
|
586
599
|
</SplitPane>
|
|
587
|
-
<div className="absolute right-3 bottom-3 z-[5] flex items-center border border-gray-alpha-400 rounded-
|
|
588
|
-
<
|
|
589
|
-
|
|
590
|
-
|
|
600
|
+
<div className="absolute right-3 bottom-3 z-[5] flex items-center border border-gray-alpha-400 rounded-md bg-background-100 shadow-sm overflow-hidden divide-x divide-gray-alpha-400">
|
|
601
|
+
<IconButton
|
|
602
|
+
variant="muted"
|
|
603
|
+
size="small"
|
|
591
604
|
onClick={zoomOut}
|
|
592
605
|
aria-label="Zoom out"
|
|
593
606
|
>
|
|
594
607
|
<ZoomOut className="w-4 h-4" />
|
|
595
|
-
</
|
|
596
|
-
<
|
|
597
|
-
|
|
598
|
-
|
|
608
|
+
</IconButton>
|
|
609
|
+
<IconButton
|
|
610
|
+
variant="muted"
|
|
611
|
+
size="small"
|
|
599
612
|
onClick={resetZoom}
|
|
600
613
|
aria-label="Reset zoom"
|
|
601
614
|
>
|
|
602
615
|
<RotateCcw className="w-3.5 h-3.5" />
|
|
603
|
-
</
|
|
604
|
-
<
|
|
605
|
-
|
|
606
|
-
|
|
616
|
+
</IconButton>
|
|
617
|
+
<IconButton
|
|
618
|
+
variant="muted"
|
|
619
|
+
size="small"
|
|
607
620
|
onClick={zoomIn}
|
|
608
621
|
aria-label="Zoom in"
|
|
609
622
|
>
|
|
610
623
|
<ZoomIn className="w-4 h-4" />
|
|
611
|
-
</
|
|
624
|
+
</IconButton>
|
|
612
625
|
</div>
|
|
613
626
|
</div>
|
|
614
627
|
|
|
@@ -621,36 +634,30 @@ function NewTraceViewerContent({ trace }: NewTraceViewerProps): ReactNode {
|
|
|
621
634
|
{selectedSpanName}
|
|
622
635
|
</span>
|
|
623
636
|
<div className="flex items-center gap-0.5 shrink-0">
|
|
624
|
-
<
|
|
625
|
-
type="button"
|
|
637
|
+
<IconButton
|
|
626
638
|
aria-label="Navigate to previous span"
|
|
627
639
|
aria-keyshortcuts="K"
|
|
628
640
|
onClick={handleSelectPrevSpan}
|
|
629
641
|
disabled={!prevSpanId}
|
|
630
|
-
className="p-1 rounded text-gray-1000 transition-colors enabled:hover:bg-gray-alpha-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
631
642
|
>
|
|
632
643
|
<ChevronUp className="w-4 h-4" />
|
|
633
|
-
</
|
|
634
|
-
<
|
|
635
|
-
type="button"
|
|
644
|
+
</IconButton>
|
|
645
|
+
<IconButton
|
|
636
646
|
aria-label="Navigate to next span"
|
|
637
647
|
aria-keyshortcuts="J"
|
|
638
648
|
onClick={handleSelectNextSpan}
|
|
639
649
|
disabled={!nextSpanId}
|
|
640
|
-
className="p-1 rounded text-gray-1000 transition-colors enabled:hover:bg-gray-alpha-100 disabled:opacity-40 disabled:cursor-not-allowed"
|
|
641
650
|
>
|
|
642
651
|
<ChevronDown className="w-4 h-4" />
|
|
643
|
-
</
|
|
652
|
+
</IconButton>
|
|
644
653
|
<div aria-hidden className="w-px h-4 bg-gray-alpha-400 mx-1" />
|
|
645
|
-
<
|
|
646
|
-
type="button"
|
|
654
|
+
<IconButton
|
|
647
655
|
aria-label="Close span details"
|
|
648
656
|
aria-keyshortcuts="Escape"
|
|
649
|
-
className="p-1 rounded text-gray-1000 hover:bg-gray-alpha-100 transition-colors"
|
|
650
657
|
onClick={clearActiveSpan}
|
|
651
658
|
>
|
|
652
659
|
<X className="w-4 h-4" />
|
|
653
|
-
</
|
|
660
|
+
</IconButton>
|
|
654
661
|
</div>
|
|
655
662
|
</div>
|
|
656
663
|
{/* Panel body */}
|
|
@@ -10,7 +10,14 @@ const buttonVariants = cva(
|
|
|
10
10
|
'!text-gray-100',
|
|
11
11
|
'bg-gray-1000',
|
|
12
12
|
'transition-[border-color,background,color,transform,box-shadow] duration-150 ease-in-out',
|
|
13
|
-
|
|
13
|
+
// Geist invert hover: literal fallbacks (no token dep, so it resolves in any
|
|
14
|
+
// consuming app), darkened via ancestor theme class without a registered variant.
|
|
15
|
+
'hover:bg-[var(--themed-hover-bg,_hsl(0,_0%,_22%))]',
|
|
16
|
+
'[.dark-theme_&]:hover:bg-[var(--themed-hover-bg,_hsl(0,_0%,_80%))]',
|
|
17
|
+
'[[data-theme=dark]_&]:hover:bg-[var(--themed-hover-bg,_hsl(0,_0%,_80%))]',
|
|
18
|
+
// Geist focus ring as arbitrary properties (no bare `outline`/ambiguous
|
|
19
|
+
// arbitrary-color utilities, which differ between Tailwind v3 and v4).
|
|
20
|
+
'focus-visible:[outline:2px_solid_var(--ds-focus-color)] focus-visible:[outline-offset:2px]',
|
|
14
21
|
// disabled styles
|
|
15
22
|
'disabled:cursor-not-allowed aria-disabled:cursor-not-allowed',
|
|
16
23
|
'disabled:bg-gray-100 disabled:!text-gray-700 disabled:hover:bg-gray-100',
|
|
@@ -28,7 +35,7 @@ const buttonVariants = cva(
|
|
|
28
35
|
size: {
|
|
29
36
|
default: 'h-10 px-4 text-[14px]',
|
|
30
37
|
sm: 'h-8 px-3 text-[14px]',
|
|
31
|
-
xs: 'h-6 px-1.5 py-0.5 text-button-12',
|
|
38
|
+
xs: 'h-6 px-1.5 py-0.5 text-button-12 rounded-[4px]',
|
|
32
39
|
icon: 'h-8 w-8',
|
|
33
40
|
},
|
|
34
41
|
},
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
|
|
5
|
+
const iconButtonVariants = cva(
|
|
6
|
+
[
|
|
7
|
+
'm-0 inline-flex shrink-0 appearance-none items-center justify-center border-0 bg-transparent p-0 align-baseline font-inherit no-underline [background:none] [-webkit-appearance:none] [-webkit-tap-highlight-color:transparent]',
|
|
8
|
+
'transition-colors duration-150 ease-in-out',
|
|
9
|
+
'enabled:cursor-pointer disabled:cursor-not-allowed disabled:opacity-40',
|
|
10
|
+
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--ds-focus-color)] focus-visible:outline-offset-2',
|
|
11
|
+
],
|
|
12
|
+
{
|
|
13
|
+
variants: {
|
|
14
|
+
variant: {
|
|
15
|
+
tertiary:
|
|
16
|
+
'text-gray-900 enabled:hover:bg-gray-alpha-200 enabled:hover:text-gray-1000 enabled:active:bg-gray-alpha-200 focus-visible:text-gray-1000',
|
|
17
|
+
muted:
|
|
18
|
+
'text-gray-900 enabled:hover:bg-gray-alpha-200 enabled:hover:text-gray-1000 enabled:active:bg-gray-alpha-200 focus-visible:text-gray-1000',
|
|
19
|
+
},
|
|
20
|
+
size: {
|
|
21
|
+
tiny: 'h-6 w-6 rounded-[4px] [&_svg]:h-4 [&_svg]:w-4',
|
|
22
|
+
small: 'h-8 w-8 rounded-md [&_svg]:h-4 [&_svg]:w-4',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
variant: 'tertiary',
|
|
27
|
+
size: 'tiny',
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export type IconButtonProps = Omit<
|
|
33
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
34
|
+
'type'
|
|
35
|
+
> &
|
|
36
|
+
VariantProps<typeof iconButtonVariants> & {
|
|
37
|
+
'aria-label': string;
|
|
38
|
+
type?: 'button' | 'submit' | 'reset';
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function IconButton({
|
|
42
|
+
className,
|
|
43
|
+
variant,
|
|
44
|
+
size,
|
|
45
|
+
type = 'button',
|
|
46
|
+
...props
|
|
47
|
+
}: IconButtonProps) {
|
|
48
|
+
return (
|
|
49
|
+
<button
|
|
50
|
+
type={type}
|
|
51
|
+
className={cn(iconButtonVariants({ variant, size, className }))}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { iconButtonVariants };
|
package/src/styles.css
CHANGED
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
--ds-gray-alpha-700: hsla(0, 0%, 0%, 0.44);
|
|
37
37
|
--ds-gray-alpha-1000: hsla(0, 0%, 0%, 0.91);
|
|
38
38
|
|
|
39
|
+
/* Focus */
|
|
40
|
+
--ds-focus-color: var(--ds-blue-700);
|
|
41
|
+
|
|
39
42
|
/* Blue */
|
|
40
43
|
--ds-blue-100: hsl(212, 100%, 97%);
|
|
41
44
|
--ds-blue-200: hsl(210, 100%, 96%);
|
|
@@ -126,6 +129,8 @@
|
|
|
126
129
|
--ds-gray-alpha-700: rgba(255, 255, 255, 0.45);
|
|
127
130
|
--ds-gray-alpha-1000: rgba(255, 255, 255, 0.92);
|
|
128
131
|
|
|
132
|
+
--ds-focus-color: var(--ds-blue-900);
|
|
133
|
+
|
|
129
134
|
--ds-blue-100: hsl(216, 50%, 12%);
|
|
130
135
|
--ds-blue-200: hsl(214, 59%, 15%);
|
|
131
136
|
--ds-blue-300: hsl(213, 71%, 20%);
|