@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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type VariantProps } from 'class-variance-authority';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
declare const iconButtonVariants: (props?: ({
|
|
4
|
+
variant?: "muted" | "tertiary" | null | undefined;
|
|
5
|
+
size?: "small" | "tiny" | null | undefined;
|
|
6
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
|
+
export type IconButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'type'> & VariantProps<typeof iconButtonVariants> & {
|
|
8
|
+
'aria-label': string;
|
|
9
|
+
type?: 'button' | 'submit' | 'reset';
|
|
10
|
+
};
|
|
11
|
+
export declare function IconButton({ className, variant, size, type, ...props }: IconButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export { iconButtonVariants };
|
|
13
|
+
//# sourceMappingURL=icon-button.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icon-button.d.ts","sourceRoot":"","sources":["../../../src/components/ui/icon-button.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAGpC,QAAA,MAAM,kBAAkB;;;8EAyBvB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAC7C,MAAM,CACP,GACC,YAAY,CAAC,OAAO,kBAAkB,CAAC,GAAG;IACxC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;CACtC,CAAC;AAEJ,wBAAgB,UAAU,CAAC,EACzB,SAAS,EACT,OAAO,EACP,IAAI,EACJ,IAAe,EACf,GAAG,KAAK,EACT,EAAE,eAAe,2CAQjB;AAED,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cva } from 'class-variance-authority';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
const iconButtonVariants = cva([
|
|
5
|
+
'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]',
|
|
6
|
+
'transition-colors duration-150 ease-in-out',
|
|
7
|
+
'enabled:cursor-pointer disabled:cursor-not-allowed disabled:opacity-40',
|
|
8
|
+
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--ds-focus-color)] focus-visible:outline-offset-2',
|
|
9
|
+
], {
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
tertiary: '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',
|
|
13
|
+
muted: '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',
|
|
14
|
+
},
|
|
15
|
+
size: {
|
|
16
|
+
tiny: 'h-6 w-6 rounded-[4px] [&_svg]:h-4 [&_svg]:w-4',
|
|
17
|
+
small: 'h-8 w-8 rounded-md [&_svg]:h-4 [&_svg]:w-4',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
variant: 'tertiary',
|
|
22
|
+
size: 'tiny',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
export function IconButton({ className, variant, size, type = 'button', ...props }) {
|
|
26
|
+
return (_jsx("button", { type: type, className: cn(iconButtonVariants({ variant, size, className })), ...props }));
|
|
27
|
+
}
|
|
28
|
+
export { iconButtonVariants };
|
|
29
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaWNvbi1idXR0b24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29tcG9uZW50cy91aS9pY29uLWJ1dHRvbi50c3giXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBLE9BQU8sRUFBRSxHQUFHLEVBQXFCLE1BQU0sMEJBQTBCLENBQUM7QUFFbEUsT0FBTyxFQUFFLEVBQUUsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBRXJDLE1BQU0sa0JBQWtCLEdBQUcsR0FBRyxDQUM1QjtJQUNFLGlPQUFpTztJQUNqTyw0Q0FBNEM7SUFDNUMsd0VBQXdFO0lBQ3hFLDRIQUE0SDtDQUM3SCxFQUNEO0lBQ0UsUUFBUSxFQUFFO1FBQ1IsT0FBTyxFQUFFO1lBQ1AsUUFBUSxFQUNOLDBJQUEwSTtZQUM1SSxLQUFLLEVBQ0gsMElBQTBJO1NBQzdJO1FBQ0QsSUFBSSxFQUFFO1lBQ0osSUFBSSxFQUFFLCtDQUErQztZQUNyRCxLQUFLLEVBQUUsNENBQTRDO1NBQ3BEO0tBQ0Y7SUFDRCxlQUFlLEVBQUU7UUFDZixPQUFPLEVBQUUsVUFBVTtRQUNuQixJQUFJLEVBQUUsTUFBTTtLQUNiO0NBQ0YsQ0FDRixDQUFDO0FBV0YsTUFBTSxVQUFVLFVBQVUsQ0FBQyxFQUN6QixTQUFTLEVBQ1QsT0FBTyxFQUNQLElBQUksRUFDSixJQUFJLEdBQUcsUUFBUSxFQUNmLEdBQUcsS0FBSyxFQUNRO0lBQ2hCLE9BQU8sQ0FDTCxpQkFDRSxJQUFJLEVBQUUsSUFBSSxFQUNWLFNBQVMsRUFBRSxFQUFFLENBQUMsa0JBQWtCLENBQUMsRUFBRSxPQUFPLEVBQUUsSUFBSSxFQUFFLFNBQVMsRUFBRSxDQUFDLENBQUMsS0FDM0QsS0FBSyxHQUNULENBQ0gsQ0FBQztBQUNKLENBQUM7QUFFRCxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGN2YSwgdHlwZSBWYXJpYW50UHJvcHMgfSBmcm9tICdjbGFzcy12YXJpYW5jZS1hdXRob3JpdHknO1xuaW1wb3J0IHR5cGUgKiBhcyBSZWFjdCBmcm9tICdyZWFjdCc7XG5pbXBvcnQgeyBjbiB9IGZyb20gJy4uLy4uL2xpYi91dGlscyc7XG5cbmNvbnN0IGljb25CdXR0b25WYXJpYW50cyA9IGN2YShcbiAgW1xuICAgICdtLTAgaW5saW5lLWZsZXggc2hyaW5rLTAgYXBwZWFyYW5jZS1ub25lIGl0ZW1zLWNlbnRlciBqdXN0aWZ5LWNlbnRlciBib3JkZXItMCBiZy10cmFuc3BhcmVudCBwLTAgYWxpZ24tYmFzZWxpbmUgZm9udC1pbmhlcml0IG5vLXVuZGVybGluZSBbYmFja2dyb3VuZDpub25lXSBbLXdlYmtpdC1hcHBlYXJhbmNlOm5vbmVdIFstd2Via2l0LXRhcC1oaWdobGlnaHQtY29sb3I6dHJhbnNwYXJlbnRdJyxcbiAgICAndHJhbnNpdGlvbi1jb2xvcnMgZHVyYXRpb24tMTUwIGVhc2UtaW4tb3V0JyxcbiAgICAnZW5hYmxlZDpjdXJzb3ItcG9pbnRlciBkaXNhYmxlZDpjdXJzb3Itbm90LWFsbG93ZWQgZGlzYWJsZWQ6b3BhY2l0eS00MCcsXG4gICAgJ2ZvY3VzLXZpc2libGU6b3V0bGluZSBmb2N1cy12aXNpYmxlOm91dGxpbmUtMiBmb2N1cy12aXNpYmxlOm91dGxpbmUtW3ZhcigtLWRzLWZvY3VzLWNvbG9yKV0gZm9jdXMtdmlzaWJsZTpvdXRsaW5lLW9mZnNldC0yJyxcbiAgXSxcbiAge1xuICAgIHZhcmlhbnRzOiB7XG4gICAgICB2YXJpYW50OiB7XG4gICAgICAgIHRlcnRpYXJ5OlxuICAgICAgICAgICd0ZXh0LWdyYXktOTAwIGVuYWJsZWQ6aG92ZXI6YmctZ3JheS1hbHBoYS0yMDAgZW5hYmxlZDpob3Zlcjp0ZXh0LWdyYXktMTAwMCBlbmFibGVkOmFjdGl2ZTpiZy1ncmF5LWFscGhhLTIwMCBmb2N1cy12aXNpYmxlOnRleHQtZ3JheS0xMDAwJyxcbiAgICAgICAgbXV0ZWQ6XG4gICAgICAgICAgJ3RleHQtZ3JheS05MDAgZW5hYmxlZDpob3ZlcjpiZy1ncmF5LWFscGhhLTIwMCBlbmFibGVkOmhvdmVyOnRleHQtZ3JheS0xMDAwIGVuYWJsZWQ6YWN0aXZlOmJnLWdyYXktYWxwaGEtMjAwIGZvY3VzLXZpc2libGU6dGV4dC1ncmF5LTEwMDAnLFxuICAgICAgfSxcbiAgICAgIHNpemU6IHtcbiAgICAgICAgdGlueTogJ2gtNiB3LTYgcm91bmRlZC1bNHB4XSBbJl9zdmddOmgtNCBbJl9zdmddOnctNCcsXG4gICAgICAgIHNtYWxsOiAnaC04IHctOCByb3VuZGVkLW1kIFsmX3N2Z106aC00IFsmX3N2Z106dy00JyxcbiAgICAgIH0sXG4gICAgfSxcbiAgICBkZWZhdWx0VmFyaWFudHM6IHtcbiAgICAgIHZhcmlhbnQ6ICd0ZXJ0aWFyeScsXG4gICAgICBzaXplOiAndGlueScsXG4gICAgfSxcbiAgfVxuKTtcblxuZXhwb3J0IHR5cGUgSWNvbkJ1dHRvblByb3BzID0gT21pdDxcbiAgUmVhY3QuQnV0dG9uSFRNTEF0dHJpYnV0ZXM8SFRNTEJ1dHRvbkVsZW1lbnQ+LFxuICAndHlwZSdcbj4gJlxuICBWYXJpYW50UHJvcHM8dHlwZW9mIGljb25CdXR0b25WYXJpYW50cz4gJiB7XG4gICAgJ2FyaWEtbGFiZWwnOiBzdHJpbmc7XG4gICAgdHlwZT86ICdidXR0b24nIHwgJ3N1Ym1pdCcgfCAncmVzZXQnO1xuICB9O1xuXG5leHBvcnQgZnVuY3Rpb24gSWNvbkJ1dHRvbih7XG4gIGNsYXNzTmFtZSxcbiAgdmFyaWFudCxcbiAgc2l6ZSxcbiAgdHlwZSA9ICdidXR0b24nLFxuICAuLi5wcm9wc1xufTogSWNvbkJ1dHRvblByb3BzKSB7XG4gIHJldHVybiAoXG4gICAgPGJ1dHRvblxuICAgICAgdHlwZT17dHlwZX1cbiAgICAgIGNsYXNzTmFtZT17Y24oaWNvbkJ1dHRvblZhcmlhbnRzKHsgdmFyaWFudCwgc2l6ZSwgY2xhc3NOYW1lIH0pKX1cbiAgICAgIHsuLi5wcm9wc31cbiAgICAvPlxuICApO1xufVxuXG5leHBvcnQgeyBpY29uQnV0dG9uVmFyaWFudHMgfTtcbiJdfQ==
|
package/dist/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%);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workflow/web-shared",
|
|
3
3
|
"description": "Shared components for Workflow Observability UI",
|
|
4
|
-
"version": "5.0.0-beta.
|
|
4
|
+
"version": "5.0.0-beta.9",
|
|
5
5
|
"private": false,
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"streamdown": "2.3.0",
|
|
54
54
|
"tailwind-merge": "3.5.0",
|
|
55
55
|
"tailwindcss": "4",
|
|
56
|
-
"@workflow/core": "5.0.0-beta.
|
|
56
|
+
"@workflow/core": "5.0.0-beta.9",
|
|
57
57
|
"@workflow/utils": "5.0.0-beta.3",
|
|
58
58
|
"@workflow/world": "5.0.0-beta.5"
|
|
59
59
|
},
|
package/src/components/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ export {
|
|
|
32
32
|
type DecryptClickContextValue,
|
|
33
33
|
} from './ui/data-inspector';
|
|
34
34
|
export { DecryptButton } from './ui/decrypt-button';
|
|
35
|
+
export { IconButton } from './ui/icon-button';
|
|
35
36
|
export { LoadMoreButton } from './ui/load-more-button';
|
|
36
37
|
export { MenuDropdown, type MenuDropdownOption } from './ui/menu-dropdown';
|
|
37
38
|
export { Spinner } from './ui/spinner';
|
|
@@ -3,11 +3,12 @@ import { cn } from '../../../lib/utils';
|
|
|
3
3
|
import type { Span } from '../../trace-viewer/types';
|
|
4
4
|
import { formatDuration } from '../../trace-viewer/util/timing';
|
|
5
5
|
import {
|
|
6
|
-
WorkflowIcon,
|
|
7
|
-
WebhookIcon,
|
|
8
6
|
SleepIcon,
|
|
9
7
|
StepForwardIcon,
|
|
8
|
+
WebhookIcon,
|
|
9
|
+
WorkflowIcon,
|
|
10
10
|
} from '../icons';
|
|
11
|
+
import { isSpanDimmedBySearch, type SpanSearchResult } from '../search';
|
|
11
12
|
import { getSpanDurationMs } from '../utils';
|
|
12
13
|
import { MiddleTruncate } from './middle-truncate/middle-truncate';
|
|
13
14
|
|
|
@@ -41,10 +42,12 @@ function getEventStyle(resource: string, isErrored: boolean): EventStyle {
|
|
|
41
42
|
const EventRow = ({
|
|
42
43
|
span,
|
|
43
44
|
isSelected,
|
|
45
|
+
isDimmed,
|
|
44
46
|
onSelectSpan,
|
|
45
47
|
}: {
|
|
46
48
|
span: Span;
|
|
47
49
|
isSelected: boolean;
|
|
50
|
+
isDimmed?: boolean;
|
|
48
51
|
onSelectSpan: (spanId: string) => void;
|
|
49
52
|
}) => {
|
|
50
53
|
const durationMs = getSpanDurationMs(span);
|
|
@@ -58,8 +61,9 @@ const EventRow = ({
|
|
|
58
61
|
return (
|
|
59
62
|
<li
|
|
60
63
|
className={cn(
|
|
61
|
-
'relative overflow-clip group
|
|
62
|
-
ROW_HEIGHT_CLASS
|
|
64
|
+
'relative overflow-clip group transition-opacity',
|
|
65
|
+
ROW_HEIGHT_CLASS,
|
|
66
|
+
isDimmed && 'opacity-35'
|
|
63
67
|
)}
|
|
64
68
|
role="treeitem"
|
|
65
69
|
aria-selected={isSelected}
|
|
@@ -91,20 +95,27 @@ const EventRow = ({
|
|
|
91
95
|
const EventList = ({
|
|
92
96
|
spans,
|
|
93
97
|
activeSpanId,
|
|
98
|
+
searchResult,
|
|
94
99
|
onSelectSpan,
|
|
95
100
|
}: {
|
|
96
101
|
spans: Span[];
|
|
97
102
|
activeSpanId: string | null;
|
|
103
|
+
searchResult: SpanSearchResult;
|
|
98
104
|
onSelectSpan: (spanId: string) => void;
|
|
99
105
|
}) => {
|
|
100
106
|
return (
|
|
101
|
-
<ul
|
|
107
|
+
<ul
|
|
108
|
+
id="event-list"
|
|
109
|
+
role="tree"
|
|
110
|
+
className="block min-h-0 overflow-visible divide-y divide-gray-alpha-400 border-b border-gray-alpha-400"
|
|
111
|
+
>
|
|
102
112
|
{spans.map((span) => {
|
|
103
113
|
return (
|
|
104
114
|
<EventRow
|
|
105
115
|
key={span.spanId}
|
|
106
116
|
span={span}
|
|
107
117
|
isSelected={span.spanId === activeSpanId}
|
|
118
|
+
isDimmed={isSpanDimmedBySearch(span.spanId, searchResult)}
|
|
108
119
|
onSelectSpan={onSelectSpan}
|
|
109
120
|
/>
|
|
110
121
|
);
|
|
@@ -7,6 +7,7 @@ import { cn } from '../../../lib/utils';
|
|
|
7
7
|
import type { Span } from '../../trace-viewer/types';
|
|
8
8
|
import { formatDuration, getHighResInMs } from '../../trace-viewer/util/timing';
|
|
9
9
|
import type { Segment, SegmentStatus, TimeMarker } from '../utils';
|
|
10
|
+
import { isSpanDimmedBySearch, type SpanSearchResult } from '../search';
|
|
10
11
|
import {
|
|
11
12
|
computeSpanGaps,
|
|
12
13
|
computeSpanSegments,
|
|
@@ -264,6 +265,7 @@ const TimelineBar = memo(function TimelineBar({
|
|
|
264
265
|
viewDuration,
|
|
265
266
|
containerWidth,
|
|
266
267
|
isSelected,
|
|
268
|
+
isDimmed,
|
|
267
269
|
onSelect,
|
|
268
270
|
}: {
|
|
269
271
|
span: Span;
|
|
@@ -271,6 +273,7 @@ const TimelineBar = memo(function TimelineBar({
|
|
|
271
273
|
viewDuration: number;
|
|
272
274
|
containerWidth: number;
|
|
273
275
|
isSelected: boolean;
|
|
276
|
+
isDimmed?: boolean;
|
|
274
277
|
onSelect: (spanId: string) => void;
|
|
275
278
|
}): ReactNode {
|
|
276
279
|
const startMs = getHighResInMs(span.startTime);
|
|
@@ -323,12 +326,15 @@ const TimelineBar = memo(function TimelineBar({
|
|
|
323
326
|
aria-selected={isSelected}
|
|
324
327
|
aria-expanded={isSelected}
|
|
325
328
|
aria-level={1}
|
|
326
|
-
className=
|
|
329
|
+
className={cn(
|
|
330
|
+
'group/timeline-row h-10 relative flex items-center hover:bg-gray-100 aria-selected:bg-gray-100 aria-selected:hover:bg-gray-200 transition-opacity',
|
|
331
|
+
isDimmed && 'opacity-35'
|
|
332
|
+
)}
|
|
327
333
|
onClick={handleClick}
|
|
328
334
|
>
|
|
329
335
|
<div className="absolute inset-y-0" style={TIMELINE_INSET_STYLE}>
|
|
330
336
|
<div
|
|
331
|
-
className="absolute top-1/2 h-6 -translate-y-1/2"
|
|
337
|
+
className="absolute top-1/2 h-6 -translate-y-1/2 rounded-[0.25rem]"
|
|
332
338
|
style={getBarPositionStyle(geometry)}
|
|
333
339
|
>
|
|
334
340
|
{geometry.mode.kind === 'arrow' ? (
|
|
@@ -448,6 +454,7 @@ export function Timeline({
|
|
|
448
454
|
viewEnd,
|
|
449
455
|
markers,
|
|
450
456
|
selectedId,
|
|
457
|
+
searchResult,
|
|
451
458
|
onSelect,
|
|
452
459
|
hoverFraction,
|
|
453
460
|
altHeld = false,
|
|
@@ -457,6 +464,7 @@ export function Timeline({
|
|
|
457
464
|
viewEnd: number;
|
|
458
465
|
markers: TimeMarker[];
|
|
459
466
|
selectedId: string | null;
|
|
467
|
+
searchResult: SpanSearchResult;
|
|
460
468
|
onSelect: (spanId: string) => void;
|
|
461
469
|
hoverFraction?: number | null;
|
|
462
470
|
altHeld?: boolean;
|
|
@@ -518,6 +526,7 @@ export function Timeline({
|
|
|
518
526
|
viewDuration={viewDuration}
|
|
519
527
|
containerWidth={timelineWidth}
|
|
520
528
|
isSelected={selectedId === span.spanId}
|
|
529
|
+
isDimmed={isSpanDimmedBySearch(span.spanId, searchResult)}
|
|
521
530
|
onSelect={onSelect}
|
|
522
531
|
/>
|
|
523
532
|
))}
|
|
@@ -4,6 +4,7 @@ import { X } from 'lucide-react';
|
|
|
4
4
|
import type { ReactNode } from 'react';
|
|
5
5
|
import type { Span } from '../trace-viewer/types';
|
|
6
6
|
import { formatDuration, getHighResInMs } from '../trace-viewer/util/timing';
|
|
7
|
+
import { IconButton } from '../ui/icon-button';
|
|
7
8
|
import { getSpanDurationMs } from './utils';
|
|
8
9
|
|
|
9
10
|
interface DetailPanelProps {
|
|
@@ -27,13 +28,9 @@ export function DetailPanel({
|
|
|
27
28
|
<span className="text-sm font-medium text-gray-1000 truncate">
|
|
28
29
|
{span.name}
|
|
29
30
|
</span>
|
|
30
|
-
<
|
|
31
|
-
type="button"
|
|
32
|
-
className="p-1 rounded-md text-gray-1000 hover:bg-gray-alpha-100 transition-colors"
|
|
33
|
-
onClick={onClose}
|
|
34
|
-
>
|
|
31
|
+
<IconButton aria-label="Close span details" onClick={onClose}>
|
|
35
32
|
<X className="w-4 h-4" />
|
|
36
|
-
</
|
|
33
|
+
</IconButton>
|
|
37
34
|
</div>
|
|
38
35
|
<div className="overflow-y-auto p-3 space-y-3">
|
|
39
36
|
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-sm">
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { Span } from '../trace-viewer/types';
|
|
2
|
+
|
|
3
|
+
const ATTR_FILTER_REGEX = /(?:^|\s)(?<pair>(?<key>[\w.]+):(?<value>\S*))/g;
|
|
4
|
+
|
|
5
|
+
export interface SpanAttributeFilter {
|
|
6
|
+
key: string;
|
|
7
|
+
value: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ParsedSpanSearchQuery {
|
|
11
|
+
text: string;
|
|
12
|
+
attributes: SpanAttributeFilter[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SpanSearchResult {
|
|
16
|
+
isActive: boolean;
|
|
17
|
+
matchedSpanIds: Set<string>;
|
|
18
|
+
matchingSpans: Span[];
|
|
19
|
+
query: ParsedSpanSearchQuery;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseSpanSearchQuery(rawQuery: string): ParsedSpanSearchQuery {
|
|
23
|
+
const attributes: SpanAttributeFilter[] = [];
|
|
24
|
+
for (const match of rawQuery.matchAll(ATTR_FILTER_REGEX)) {
|
|
25
|
+
const key = match.groups?.key?.trim();
|
|
26
|
+
if (!key) continue;
|
|
27
|
+
attributes.push({
|
|
28
|
+
key,
|
|
29
|
+
value: (match.groups?.value ?? '').toLocaleLowerCase(),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const text = rawQuery
|
|
34
|
+
.replace(ATTR_FILTER_REGEX, ' ')
|
|
35
|
+
.replace(/\s{2,}/g, ' ')
|
|
36
|
+
.trim()
|
|
37
|
+
.toLocaleLowerCase();
|
|
38
|
+
|
|
39
|
+
return { text, attributes };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isSpanDimmedBySearch(
|
|
43
|
+
spanId: string,
|
|
44
|
+
result: SpanSearchResult
|
|
45
|
+
): boolean {
|
|
46
|
+
return result.isActive && !result.matchedSpanIds.has(spanId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function searchSpans(spans: Span[], rawQuery: string): SpanSearchResult {
|
|
50
|
+
const query = parseSpanSearchQuery(rawQuery);
|
|
51
|
+
const isActive = Boolean(query.text || query.attributes.length);
|
|
52
|
+
const matchingSpans = isActive
|
|
53
|
+
? spans.filter((span) => spanMatchesQuery(span, query))
|
|
54
|
+
: spans;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
isActive,
|
|
58
|
+
matchedSpanIds: new Set(matchingSpans.map((span) => span.spanId)),
|
|
59
|
+
matchingSpans,
|
|
60
|
+
query,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function spanMatchesQuery(span: Span, query: ParsedSpanSearchQuery): boolean {
|
|
65
|
+
if (query.text && !spanMatchesText(span, query.text)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return query.attributes.every(({ key, value }) =>
|
|
70
|
+
spanMatchesAttribute(span, key, value)
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function spanMatchesText(span: Span, text: string): boolean {
|
|
75
|
+
return [
|
|
76
|
+
span.name,
|
|
77
|
+
span.spanId,
|
|
78
|
+
span.resource,
|
|
79
|
+
span.library.name,
|
|
80
|
+
span.library.version,
|
|
81
|
+
]
|
|
82
|
+
.filter((value): value is string => typeof value === 'string')
|
|
83
|
+
.some((value) => value.toLocaleLowerCase().includes(text));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function spanMatchesAttribute(
|
|
87
|
+
span: Span,
|
|
88
|
+
key: string,
|
|
89
|
+
expectedValue: string
|
|
90
|
+
): boolean {
|
|
91
|
+
const candidates = getSearchValues(span, key);
|
|
92
|
+
if (!candidates.length) return false;
|
|
93
|
+
|
|
94
|
+
if (!expectedValue) {
|
|
95
|
+
return candidates.some((value) => value != null);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return candidates.some((value) =>
|
|
99
|
+
valueToSearchString(value).toLocaleLowerCase().includes(expectedValue)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getSearchValues(span: Span, key: string): unknown[] {
|
|
104
|
+
const candidates: unknown[] = [];
|
|
105
|
+
const normalizedKey = key.toLocaleLowerCase();
|
|
106
|
+
|
|
107
|
+
switch (normalizedKey) {
|
|
108
|
+
case 'id':
|
|
109
|
+
case 'spanid':
|
|
110
|
+
case 'span.id':
|
|
111
|
+
candidates.push(span.spanId);
|
|
112
|
+
break;
|
|
113
|
+
case 'name':
|
|
114
|
+
candidates.push(span.name);
|
|
115
|
+
break;
|
|
116
|
+
case 'resource':
|
|
117
|
+
candidates.push(span.resource);
|
|
118
|
+
break;
|
|
119
|
+
case 'library':
|
|
120
|
+
case 'library.name':
|
|
121
|
+
candidates.push(span.library.name);
|
|
122
|
+
break;
|
|
123
|
+
case 'library.version':
|
|
124
|
+
candidates.push(span.library.version);
|
|
125
|
+
break;
|
|
126
|
+
case 'status':
|
|
127
|
+
case 'status.code':
|
|
128
|
+
candidates.push(span.status.code);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
candidates.push(getOwnProperty(span.attributes, key));
|
|
133
|
+
candidates.push(getPathValue(span.attributes, key));
|
|
134
|
+
|
|
135
|
+
if (!normalizedKey.startsWith('data.')) {
|
|
136
|
+
candidates.push(getPathValue(span.attributes.data, key));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return candidates.filter((value) => value !== undefined);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getOwnProperty(source: unknown, key: string): unknown {
|
|
143
|
+
if (!source || typeof source !== 'object') return undefined;
|
|
144
|
+
if (!Object.hasOwn(source, key)) return undefined;
|
|
145
|
+
return (source as Record<string, unknown>)[key];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getPathValue(source: unknown, path: string): unknown {
|
|
149
|
+
if (!source || typeof source !== 'object') return undefined;
|
|
150
|
+
|
|
151
|
+
let current: unknown = source;
|
|
152
|
+
for (const segment of path.split('.')) {
|
|
153
|
+
if (!segment) return undefined;
|
|
154
|
+
current = getOwnProperty(current, segment);
|
|
155
|
+
if (current === undefined) return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return current;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function valueToSearchString(value: unknown): string {
|
|
162
|
+
if (value instanceof Date) return value.toISOString();
|
|
163
|
+
if (typeof value === 'string') return value;
|
|
164
|
+
if (
|
|
165
|
+
value == null ||
|
|
166
|
+
typeof value === 'number' ||
|
|
167
|
+
typeof value === 'boolean' ||
|
|
168
|
+
typeof value === 'bigint'
|
|
169
|
+
) {
|
|
170
|
+
return String(value);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
return JSON.stringify(value);
|
|
175
|
+
} catch {
|
|
176
|
+
return String(value);
|
|
177
|
+
}
|
|
178
|
+
}
|