@workflow/web-shared 5.0.0-beta.26 → 5.0.0-beta.28
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/new-trace-viewer/components/detail-panel-width.d.ts +34 -0
- package/dist/components/new-trace-viewer/components/detail-panel-width.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/detail-panel-width.js +67 -0
- package/dist/components/new-trace-viewer/components/detail-panel.d.ts +14 -0
- package/dist/components/new-trace-viewer/components/detail-panel.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/detail-panel.js +122 -0
- package/dist/components/new-trace-viewer/components/draggable-border.d.ts +31 -0
- package/dist/components/new-trace-viewer/components/draggable-border.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/draggable-border.js +140 -0
- package/dist/components/new-trace-viewer/components/event-list.d.ts +1 -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 +3 -5
- package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts +1 -1
- package/dist/components/new-trace-viewer/components/middle-truncate/truncate.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/middle-truncate/truncate.js +2 -2
- package/dist/components/new-trace-viewer/components/minimap.d.ts +14 -0
- package/dist/components/new-trace-viewer/components/minimap.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/minimap.js +337 -0
- package/dist/components/new-trace-viewer/components/pane-constants.d.ts +14 -0
- package/dist/components/new-trace-viewer/components/pane-constants.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/pane-constants.js +16 -0
- package/dist/components/new-trace-viewer/components/split-pane.d.ts +1 -4
- package/dist/components/new-trace-viewer/components/split-pane.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/split-pane.js +18 -16
- package/dist/components/new-trace-viewer/components/timeline.d.ts +2 -2
- package/dist/components/new-trace-viewer/components/timeline.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/timeline.js +5 -7
- package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/components/trace-viewer-skeleton.js +10 -3
- package/dist/components/new-trace-viewer/components/use-element-width.d.ts +8 -0
- package/dist/components/new-trace-viewer/components/use-element-width.d.ts.map +1 -0
- package/dist/components/new-trace-viewer/components/use-element-width.js +25 -0
- package/dist/components/new-trace-viewer/context.d.ts +2 -0
- package/dist/components/new-trace-viewer/context.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/context.js +3 -2
- package/dist/components/new-trace-viewer/trace-viewer.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/trace-viewer.js +53 -181
- package/dist/components/new-trace-viewer/utils.d.ts +21 -0
- package/dist/components/new-trace-viewer/utils.d.ts.map +1 -1
- package/dist/components/new-trace-viewer/utils.js +44 -3
- package/dist/components/sidebar/attribute-panel.d.ts.map +1 -1
- package/dist/components/sidebar/attribute-panel.js +38 -35
- package/dist/components/sidebar/attributes-block.d.ts +18 -7
- package/dist/components/sidebar/attributes-block.d.ts.map +1 -1
- package/dist/components/sidebar/attributes-block.js +76 -18
- package/dist/components/sidebar/events-list.d.ts +1 -1
- package/dist/components/sidebar/events-list.d.ts.map +1 -1
- package/dist/components/sidebar/events-list.js +9 -10
- package/dist/components/trace-viewer/components/map.d.ts +2 -2
- package/dist/components/trace-viewer/components/map.d.ts.map +1 -1
- package/dist/components/trace-viewer/components/node.d.ts +7 -7
- package/dist/components/trace-viewer/components/node.d.ts.map +1 -1
- package/dist/components/ui/collapsible.d.ts +46 -0
- package/dist/components/ui/collapsible.d.ts.map +1 -0
- package/dist/components/ui/collapsible.js +66 -0
- package/dist/components/ui/data-inspector.js +2 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/lib/hydration.d.ts +12 -2
- package/dist/lib/hydration.d.ts.map +1 -1
- package/dist/lib/hydration.js +41 -18
- package/dist/lib/utils.d.ts +8 -5
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +13 -16
- package/package.json +6 -6
- package/src/components/new-trace-viewer/components/detail-panel-width.test.ts +123 -0
- package/src/components/new-trace-viewer/components/detail-panel-width.ts +76 -0
- package/src/components/new-trace-viewer/components/detail-panel.tsx +272 -0
- package/src/components/new-trace-viewer/components/draggable-border.tsx +216 -0
- package/src/components/new-trace-viewer/components/event-list.tsx +3 -5
- package/src/components/new-trace-viewer/components/middle-truncate/truncate.ts +1 -1
- package/src/components/new-trace-viewer/components/minimap.tsx +559 -0
- package/src/components/new-trace-viewer/components/pane-constants.ts +19 -0
- package/src/components/new-trace-viewer/components/split-pane.tsx +44 -55
- package/src/components/new-trace-viewer/components/timeline.tsx +3 -5
- package/src/components/new-trace-viewer/components/trace-viewer-skeleton.tsx +21 -1
- package/src/components/new-trace-viewer/components/use-element-width.ts +29 -0
- package/src/components/new-trace-viewer/context.tsx +4 -1
- package/src/components/new-trace-viewer/trace-viewer.tsx +93 -312
- package/src/components/new-trace-viewer/utils.test.ts +65 -0
- package/src/components/new-trace-viewer/utils.ts +76 -2
- package/src/components/sidebar/attribute-panel.tsx +106 -105
- package/src/components/sidebar/attributes-block.tsx +170 -50
- package/src/components/sidebar/events-list.tsx +107 -92
- package/src/components/ui/collapsible.tsx +219 -0
- package/src/index.ts +8 -7
- package/src/lib/hydration.ts +48 -25
- package/src/lib/utils.test.ts +33 -0
- package/src/lib/utils.ts +12 -16
- package/dist/components/sidebar/detail-card.d.ts +0 -13
- package/dist/components/sidebar/detail-card.d.ts.map +0 -1
- package/dist/components/sidebar/detail-card.js +0 -36
- package/src/components/sidebar/detail-card.tsx +0 -143
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
type SyntheticEvent,
|
|
8
|
+
useContext,
|
|
9
|
+
useState,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { cn } from '../../lib/cn';
|
|
12
|
+
|
|
13
|
+
type CollapsibleVariant = 'section' | 'card';
|
|
14
|
+
|
|
15
|
+
type CollapsibleContextValue = {
|
|
16
|
+
variant: CollapsibleVariant;
|
|
17
|
+
disabled: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const CollapsibleContext = createContext<CollapsibleContextValue | null>(null);
|
|
21
|
+
|
|
22
|
+
function useCollapsibleContext(part: string): CollapsibleContextValue {
|
|
23
|
+
const context = useContext(CollapsibleContext);
|
|
24
|
+
if (!context) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`<Collapsible${part}> must be rendered inside <CollapsibleRoot>`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return context;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type CollapsibleRootProps = {
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
variant?: CollapsibleVariant;
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
defaultOpen?: boolean;
|
|
37
|
+
onOpenChange?: (open: boolean) => void;
|
|
38
|
+
className?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Low-level collapsible container. Use this together with `CollapsibleTrigger`
|
|
43
|
+
* and `CollapsibleContent` when you need to override the styling of individual
|
|
44
|
+
* parts. For the common case, prefer the all-in-one `Collapsible`.
|
|
45
|
+
*/
|
|
46
|
+
export function CollapsibleRoot({
|
|
47
|
+
children,
|
|
48
|
+
variant = 'section',
|
|
49
|
+
disabled = false,
|
|
50
|
+
defaultOpen = false,
|
|
51
|
+
onOpenChange,
|
|
52
|
+
className,
|
|
53
|
+
}: CollapsibleRootProps) {
|
|
54
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
55
|
+
|
|
56
|
+
const handleToggle = (event: SyntheticEvent<HTMLDetailsElement>) => {
|
|
57
|
+
// React bubbles the `toggle` event even though the native one doesn't, so
|
|
58
|
+
// a nested <details> (e.g. an event card inside the Events section)
|
|
59
|
+
// collapsing would otherwise flip this card. Only react to direct toggles.
|
|
60
|
+
if (event.target !== event.currentTarget) return;
|
|
61
|
+
const next = event.currentTarget.open;
|
|
62
|
+
setOpen(next);
|
|
63
|
+
onOpenChange?.(next);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const details = (
|
|
67
|
+
<details
|
|
68
|
+
data-slot="collapsible"
|
|
69
|
+
data-state={open ? 'open' : 'closed'}
|
|
70
|
+
open={open}
|
|
71
|
+
onToggle={disabled ? undefined : handleToggle}
|
|
72
|
+
className={cn(
|
|
73
|
+
variant === 'card'
|
|
74
|
+
? 'group/card border-gray-alpha-400 last:border-b'
|
|
75
|
+
: 'group',
|
|
76
|
+
className
|
|
77
|
+
)}
|
|
78
|
+
>
|
|
79
|
+
<CollapsibleContext.Provider value={{ variant, disabled }}>
|
|
80
|
+
{children}
|
|
81
|
+
</CollapsibleContext.Provider>
|
|
82
|
+
</details>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (variant === 'card') {
|
|
86
|
+
return details;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<section className="-mx-4 border-t border-gray-alpha-400 px-4 py-2">
|
|
91
|
+
{details}
|
|
92
|
+
</section>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type CollapsibleTriggerProps = {
|
|
97
|
+
children: ReactNode;
|
|
98
|
+
className?: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** The clickable header of a `CollapsibleRoot`. */
|
|
102
|
+
export function CollapsibleTrigger({
|
|
103
|
+
children,
|
|
104
|
+
className,
|
|
105
|
+
}: CollapsibleTriggerProps) {
|
|
106
|
+
const { variant, disabled } = useCollapsibleContext('Trigger');
|
|
107
|
+
|
|
108
|
+
if (variant === 'card') {
|
|
109
|
+
return (
|
|
110
|
+
<summary
|
|
111
|
+
data-slot="collapsible-trigger"
|
|
112
|
+
className={cn(
|
|
113
|
+
'flex cursor-pointer list-none items-center gap-1.5 border-t border-gray-alpha-400 bg-background-200 px-3 py-4 hover:bg-gray-100 [&::-webkit-details-marker]:hidden',
|
|
114
|
+
className
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
<ChevronRight
|
|
118
|
+
size={14}
|
|
119
|
+
className="shrink-0 text-gray-700 group-hover/card:text-gray-1000 group-open/card:rotate-90"
|
|
120
|
+
/>
|
|
121
|
+
{children}
|
|
122
|
+
</summary>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Shared row metrics keep every header the same height regardless of variant.
|
|
127
|
+
const row =
|
|
128
|
+
'flex h-9 items-center gap-2 -mx-2 px-2 text-heading-14 font-medium list-none [&::-webkit-details-marker]:hidden';
|
|
129
|
+
|
|
130
|
+
if (disabled) {
|
|
131
|
+
return (
|
|
132
|
+
<summary
|
|
133
|
+
data-slot="collapsible-trigger"
|
|
134
|
+
className={cn(row, 'pointer-events-none text-gray-700', className)}
|
|
135
|
+
>
|
|
136
|
+
<span className="min-w-0 flex-1">{children}</span>
|
|
137
|
+
</summary>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<summary
|
|
143
|
+
data-slot="collapsible-trigger"
|
|
144
|
+
className={cn(
|
|
145
|
+
row,
|
|
146
|
+
'group/trigger cursor-pointer rounded hover:bg-gray-alpha-100',
|
|
147
|
+
className
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
150
|
+
<span className="relative isolate h-3.5 w-3.5 shrink-0 text-gray-700 group-hover/trigger:text-gray-1000">
|
|
151
|
+
<ChevronRight
|
|
152
|
+
size={14}
|
|
153
|
+
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-100 group-open:opacity-0"
|
|
154
|
+
/>
|
|
155
|
+
<ChevronDown
|
|
156
|
+
size={14}
|
|
157
|
+
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 group-open:opacity-100"
|
|
158
|
+
/>
|
|
159
|
+
</span>
|
|
160
|
+
<span className="min-w-0 flex-1">{children}</span>
|
|
161
|
+
</summary>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
type CollapsibleContentProps = {
|
|
166
|
+
children: ReactNode;
|
|
167
|
+
className?: string;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** The collapsible body of a `CollapsibleRoot`. */
|
|
171
|
+
export function CollapsibleContent({
|
|
172
|
+
children,
|
|
173
|
+
className,
|
|
174
|
+
}: CollapsibleContentProps) {
|
|
175
|
+
const { variant } = useCollapsibleContext('Content');
|
|
176
|
+
return (
|
|
177
|
+
<div
|
|
178
|
+
data-slot="collapsible-content"
|
|
179
|
+
className={cn(variant === 'section' && 'mt-2 mb-3', className)}
|
|
180
|
+
>
|
|
181
|
+
{children}
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
type CollapsibleProps = {
|
|
187
|
+
/** Header content shown in the trigger row. */
|
|
188
|
+
label: ReactNode;
|
|
189
|
+
/** Body content. Omit for a header-only (e.g. disabled or skeleton) row. */
|
|
190
|
+
children?: ReactNode;
|
|
191
|
+
disabled?: boolean;
|
|
192
|
+
defaultOpen?: boolean;
|
|
193
|
+
onOpenChange?: (open: boolean) => void;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* All-in-one collapsible section: a labeled header plus body content. This is
|
|
198
|
+
* the component to reach for in most cases. When you need to override the
|
|
199
|
+
* styling of a specific part, compose `CollapsibleRoot`, `CollapsibleTrigger`,
|
|
200
|
+
* and `CollapsibleContent` directly instead.
|
|
201
|
+
*/
|
|
202
|
+
export function Collapsible({
|
|
203
|
+
label,
|
|
204
|
+
children,
|
|
205
|
+
disabled,
|
|
206
|
+
defaultOpen,
|
|
207
|
+
onOpenChange,
|
|
208
|
+
}: CollapsibleProps) {
|
|
209
|
+
return (
|
|
210
|
+
<CollapsibleRoot
|
|
211
|
+
disabled={disabled}
|
|
212
|
+
defaultOpen={defaultOpen}
|
|
213
|
+
onOpenChange={onOpenChange}
|
|
214
|
+
>
|
|
215
|
+
<CollapsibleTrigger>{label}</CollapsibleTrigger>
|
|
216
|
+
{children != null && <CollapsibleContent>{children}</CollapsibleContent>}
|
|
217
|
+
</CollapsibleRoot>
|
|
218
|
+
);
|
|
219
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,13 +14,6 @@ export {
|
|
|
14
14
|
waitEventsToWaitEntity,
|
|
15
15
|
} from './components/workflow-traces/trace-span-construction';
|
|
16
16
|
export type { EventAnalysis } from './lib/event-analysis';
|
|
17
|
-
export {
|
|
18
|
-
parseExactWorkflowSearchId,
|
|
19
|
-
looksLikeWorkflowIdSearchInput,
|
|
20
|
-
type ExactWorkflowSearchId,
|
|
21
|
-
type ExactWorkflowSearchIdKind,
|
|
22
|
-
type ExactIdSearchResult,
|
|
23
|
-
} from './lib/exact-event-search-id';
|
|
24
17
|
export {
|
|
25
18
|
analyzeEvents,
|
|
26
19
|
hasPendingHooksFromEvents,
|
|
@@ -40,6 +33,13 @@ export {
|
|
|
40
33
|
materializeSteps,
|
|
41
34
|
materializeWaits,
|
|
42
35
|
} from './lib/event-materialization';
|
|
36
|
+
export {
|
|
37
|
+
type ExactIdSearchResult,
|
|
38
|
+
type ExactWorkflowSearchId,
|
|
39
|
+
type ExactWorkflowSearchIdKind,
|
|
40
|
+
looksLikeWorkflowIdSearchInput,
|
|
41
|
+
parseExactWorkflowSearchId,
|
|
42
|
+
} from './lib/exact-event-search-id';
|
|
43
43
|
export type { Revivers, StreamRef } from './lib/hydration';
|
|
44
44
|
export {
|
|
45
45
|
CLASS_INSTANCE_REF_TYPE,
|
|
@@ -49,6 +49,7 @@ export {
|
|
|
49
49
|
getWebRevivers,
|
|
50
50
|
hasEncryptedFields,
|
|
51
51
|
hydrateResourceIO,
|
|
52
|
+
hydrateResourceIOAsync,
|
|
52
53
|
hydrateResourceIOWithKey,
|
|
53
54
|
isClassInstanceRef,
|
|
54
55
|
isEncryptedMarker,
|
package/src/lib/hydration.ts
CHANGED
|
@@ -18,6 +18,20 @@ import {
|
|
|
18
18
|
} from '@workflow/core/serialization-format';
|
|
19
19
|
import { EVENT_DATA_REF_FIELDS } from '@workflow/world';
|
|
20
20
|
|
|
21
|
+
const V4_EXTRA_EVENT_DATA_REF_FIELDS: Record<string, string[]> = {
|
|
22
|
+
run_started: ['input'],
|
|
23
|
+
step_started: ['input'],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function getEventDataRefFields(eventType: string): string[] {
|
|
27
|
+
return [
|
|
28
|
+
...new Set([
|
|
29
|
+
...(EVENT_DATA_REF_FIELDS[eventType] ?? []),
|
|
30
|
+
...(V4_EXTRA_EVENT_DATA_REF_FIELDS[eventType] ?? []),
|
|
31
|
+
]),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
// Re-export types and utilities that consumers need
|
|
22
36
|
export {
|
|
23
37
|
CLASS_INSTANCE_REF_TYPE,
|
|
@@ -446,7 +460,7 @@ function replaceEncryptedAndExpiredWithMarkers<T>(resource: T): T {
|
|
|
446
460
|
if (result.eventData && typeof result.eventData === 'object') {
|
|
447
461
|
const eventType =
|
|
448
462
|
typeof result.eventType === 'string' ? result.eventType : '';
|
|
449
|
-
const refKeys =
|
|
463
|
+
const refKeys = getEventDataRefFields(eventType);
|
|
450
464
|
const ed = { ...(result.eventData as Record<string, unknown>) };
|
|
451
465
|
for (const key of refKeys) {
|
|
452
466
|
if (key in ed) {
|
|
@@ -465,17 +479,31 @@ function replaceEncryptedAndExpiredWithMarkers<T>(resource: T): T {
|
|
|
465
479
|
* When a key is provided, encrypted fields are decrypted before hydration.
|
|
466
480
|
* This is the async version used when the user clicks "Decrypt" in the web UI.
|
|
467
481
|
*
|
|
468
|
-
* Handles both top-level fields (input, output, metadata) and nested
|
|
469
|
-
*
|
|
482
|
+
* Handles both top-level fields (input, output, metadata) and nested eventData
|
|
483
|
+
* payload fields for that event type.
|
|
470
484
|
*/
|
|
471
485
|
export async function hydrateResourceIOWithKey<T>(
|
|
472
486
|
resource: T,
|
|
473
487
|
key: Uint8Array
|
|
488
|
+
): Promise<T> {
|
|
489
|
+
return hydrateResourceIOAsync(resource, key);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Async hydration for web display.
|
|
494
|
+
*
|
|
495
|
+
* This follows the same resource-field mapping as {@link hydrateResourceIO},
|
|
496
|
+
* but can also inflate compressed browser payloads through the registered
|
|
497
|
+
* zstd WASM decoder. When a key is provided, encrypted fields are decrypted
|
|
498
|
+
* first and then inflated/hydrated.
|
|
499
|
+
*/
|
|
500
|
+
export async function hydrateResourceIOAsync<T>(
|
|
501
|
+
resource: T,
|
|
502
|
+
key?: Uint8Array
|
|
474
503
|
): Promise<T> {
|
|
475
504
|
const { hydrateDataWithKey } = await import(
|
|
476
505
|
'@workflow/core/serialization-format'
|
|
477
506
|
);
|
|
478
|
-
const { importKey } = await import('@workflow/core/encryption');
|
|
479
507
|
// Payloads may be zstd-compressed (the Web DecompressionStream has no zstd);
|
|
480
508
|
// register the WASM-backed browser decoder before hydrating. Idempotent and
|
|
481
509
|
// lazy — the WASM is only compiled when a zstd payload is actually decoded.
|
|
@@ -483,25 +511,24 @@ export async function hydrateResourceIOWithKey<T>(
|
|
|
483
511
|
'./zstd-browser-decoder.js'
|
|
484
512
|
);
|
|
485
513
|
ensureZstdDecoderRegistered();
|
|
486
|
-
const cryptoKey =
|
|
514
|
+
const cryptoKey = key
|
|
515
|
+
? await import('@workflow/core/encryption').then(({ importKey }) =>
|
|
516
|
+
importKey(key)
|
|
517
|
+
)
|
|
518
|
+
: undefined;
|
|
487
519
|
const revivers = getRevivers();
|
|
488
520
|
|
|
489
|
-
|
|
490
|
-
async function decryptField(
|
|
491
|
-
value: unknown,
|
|
492
|
-
rev: Revivers,
|
|
493
|
-
k: Awaited<ReturnType<typeof importKey>>
|
|
494
|
-
): Promise<unknown> {
|
|
521
|
+
async function hydrateField(value: unknown): Promise<unknown> {
|
|
495
522
|
// Already-hydrated: encrypted marker with stored bytes
|
|
496
523
|
if (isEncryptedMarker(value)) {
|
|
497
524
|
const raw = (value as any).__encryptedData as Uint8Array;
|
|
498
|
-
return hydrateDataWithKey(raw,
|
|
525
|
+
return cryptoKey ? hydrateDataWithKey(raw, revivers, cryptoKey) : value;
|
|
499
526
|
}
|
|
500
|
-
// Raw encrypted
|
|
527
|
+
// Raw Uint8Array: may be encrypted, compressed, or plain devalue.
|
|
501
528
|
if (value instanceof Uint8Array) {
|
|
502
|
-
return hydrateDataWithKey(value,
|
|
529
|
+
return hydrateDataWithKey(value, revivers, cryptoKey);
|
|
503
530
|
}
|
|
504
|
-
// Not
|
|
531
|
+
// Not serialized — return as-is.
|
|
505
532
|
return value;
|
|
506
533
|
}
|
|
507
534
|
|
|
@@ -511,29 +538,25 @@ export async function hydrateResourceIOWithKey<T>(
|
|
|
511
538
|
// Decrypt + hydrate top-level serialized fields (runs, steps, hooks)
|
|
512
539
|
for (const field of ['input', 'output', 'metadata', 'error']) {
|
|
513
540
|
if (field in result) {
|
|
514
|
-
result[field] = await
|
|
541
|
+
result[field] = await hydrateField(result[field]);
|
|
515
542
|
}
|
|
516
543
|
}
|
|
517
544
|
|
|
518
|
-
//
|
|
545
|
+
// Hydrate eventData subfields (events)
|
|
519
546
|
if (result.eventData && typeof result.eventData === 'object') {
|
|
520
547
|
const eventType =
|
|
521
548
|
typeof result.eventType === 'string' ? result.eventType : '';
|
|
522
|
-
const refKeys =
|
|
549
|
+
const refKeys = getEventDataRefFields(eventType);
|
|
523
550
|
const eventData = { ...(result.eventData as Record<string, unknown>) };
|
|
524
551
|
for (const field of refKeys) {
|
|
525
552
|
if (field in eventData) {
|
|
526
|
-
eventData[field] = await
|
|
527
|
-
eventData[field],
|
|
528
|
-
revivers,
|
|
529
|
-
cryptoKey
|
|
530
|
-
);
|
|
553
|
+
eventData[field] = await hydrateField(eventData[field]);
|
|
531
554
|
}
|
|
532
555
|
}
|
|
533
556
|
result.eventData = eventData;
|
|
534
557
|
}
|
|
535
558
|
|
|
536
|
-
return result as T;
|
|
559
|
+
return replaceEncryptedAndExpiredWithMarkers(result as T);
|
|
537
560
|
}
|
|
538
561
|
|
|
539
562
|
/**
|
|
@@ -552,7 +575,7 @@ export function hasEncryptedFields(resource: unknown): boolean {
|
|
|
552
575
|
|
|
553
576
|
if (r.eventData && typeof r.eventData === 'object') {
|
|
554
577
|
const eventType = typeof r.eventType === 'string' ? r.eventType : '';
|
|
555
|
-
const refKeys =
|
|
578
|
+
const refKeys = getEventDataRefFields(eventType);
|
|
556
579
|
const ed = r.eventData as Record<string, unknown>;
|
|
557
580
|
for (const key of refKeys) {
|
|
558
581
|
if (key in ed && isEncryptedMarker(ed[key])) return true;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { formatDurationPrecise } from './utils';
|
|
3
|
+
|
|
4
|
+
describe('formatDurationPrecise', () => {
|
|
5
|
+
it('shows whole milliseconds below 1s', () => {
|
|
6
|
+
expect(formatDurationPrecise(0)).toBe('0s');
|
|
7
|
+
expect(formatDurationPrecise(626)).toBe('626ms');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('keeps sub-second precision instead of rounding up to a whole second', () => {
|
|
11
|
+
// The bug this guards: 1626ms must not read as "2s".
|
|
12
|
+
expect(formatDurationPrecise(1626)).toBe('1.63s');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('trims trailing zeros so whole/half seconds read cleanly', () => {
|
|
16
|
+
expect(formatDurationPrecise(2000)).toBe('2s');
|
|
17
|
+
expect(formatDurationPrecise(1500)).toBe('1.5s');
|
|
18
|
+
expect(formatDurationPrecise(1600)).toBe('1.6s');
|
|
19
|
+
expect(formatDurationPrecise(45_000)).toBe('45s');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('honors a custom fraction-digit count (used by the timeline ruler)', () => {
|
|
23
|
+
// Fewer digits also coarsen the rounding: 1 digit rounds to 100ms.
|
|
24
|
+
expect(formatDurationPrecise(1500, 1)).toBe('1.5s');
|
|
25
|
+
expect(formatDurationPrecise(1620, 1)).toBe('1.6s');
|
|
26
|
+
expect(formatDurationPrecise(2000, 1)).toBe('2s');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('decomposes durations of a minute or more', () => {
|
|
30
|
+
expect(formatDurationPrecise(65_000)).toBe('1m 5s');
|
|
31
|
+
expect(formatDurationPrecise(63_450)).toBe('1m 3.45s');
|
|
32
|
+
});
|
|
33
|
+
});
|
package/src/lib/utils.ts
CHANGED
|
@@ -74,25 +74,20 @@ export function formatDuration(ms: number, compact = false): string {
|
|
|
74
74
|
return parts.join(' ');
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// Locale-aware formatter that always renders exactly two fraction digits.
|
|
78
|
-
// Used for the seconds component of precise durations so values are never
|
|
79
|
-
// snapped to a whole second (and thousands separators stay correct).
|
|
80
|
-
const preciseSecondsFormatter = new Intl.NumberFormat(undefined, {
|
|
81
|
-
minimumFractionDigits: 2,
|
|
82
|
-
maximumFractionDigits: 2,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
77
|
/**
|
|
86
78
|
* Formats a duration in milliseconds without snapping to whole seconds.
|
|
87
79
|
*
|
|
88
|
-
* Unlike {@link formatDuration}, this
|
|
89
|
-
*
|
|
80
|
+
* Unlike {@link formatDuration}, this keeps sub-second detail, so 1626ms
|
|
81
|
+
* renders as "1.63s" rather than "2s". `fractionDigits` sets both the number of
|
|
82
|
+
* decimals on the seconds component (trailing zeros trimmed, so 2000ms is "2s"
|
|
83
|
+
* and 1500ms is "1.5s") and the rounding granularity — the default of 2 rounds
|
|
84
|
+
* to 10ms; the timeline ruler passes fewer digits to match its tick step.
|
|
90
85
|
*
|
|
91
86
|
* - < 1s: shows whole milliseconds (e.g. "626ms")
|
|
92
|
-
* - 1s – 1m: shows seconds with
|
|
93
|
-
* - >= 1m: decomposes into d/h/m with
|
|
87
|
+
* - 1s – 1m: shows seconds with trimmed decimals (e.g. "1.63s", "1.5s", "45s")
|
|
88
|
+
* - >= 1m: decomposes into d/h/m with trimmed-decimal seconds (e.g. "1m 13.45s")
|
|
94
89
|
*/
|
|
95
|
-
export function formatDurationPrecise(ms: number): string {
|
|
90
|
+
export function formatDurationPrecise(ms: number, fractionDigits = 2): string {
|
|
96
91
|
if (ms === 0) {
|
|
97
92
|
return '0s';
|
|
98
93
|
}
|
|
@@ -104,10 +99,11 @@ export function formatDurationPrecise(ms: number): string {
|
|
|
104
99
|
}
|
|
105
100
|
}
|
|
106
101
|
|
|
107
|
-
const
|
|
102
|
+
const granularityMs = MS_IN_SECOND / 10 ** fractionDigits;
|
|
103
|
+
const normalizedMs = Math.round(ms / granularityMs) * granularityMs;
|
|
108
104
|
|
|
109
105
|
if (normalizedMs < MS_IN_MINUTE) {
|
|
110
|
-
return `${
|
|
106
|
+
return `${Number((normalizedMs / MS_IN_SECOND).toFixed(fractionDigits))}s`;
|
|
111
107
|
}
|
|
112
108
|
|
|
113
109
|
const days = Math.floor(normalizedMs / MS_IN_DAY);
|
|
@@ -126,7 +122,7 @@ export function formatDurationPrecise(ms: number): string {
|
|
|
126
122
|
if (minutes > 0) {
|
|
127
123
|
parts.push(`${minutes}m`);
|
|
128
124
|
}
|
|
129
|
-
parts.push(`${
|
|
125
|
+
parts.push(`${Number(seconds.toFixed(fractionDigits))}s`);
|
|
130
126
|
|
|
131
127
|
return parts.join(' ');
|
|
132
128
|
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { type ReactNode } from 'react';
|
|
2
|
-
export declare function DetailCard({ summary, children, onToggle, disabled, defaultOpen, variant, trailing, summaryClassName, contentClassName, }: {
|
|
3
|
-
summary: ReactNode;
|
|
4
|
-
children?: ReactNode;
|
|
5
|
-
onToggle?: (open: boolean) => void;
|
|
6
|
-
disabled?: boolean;
|
|
7
|
-
defaultOpen?: boolean;
|
|
8
|
-
variant?: 'section' | 'card';
|
|
9
|
-
trailing?: ReactNode;
|
|
10
|
-
summaryClassName?: string;
|
|
11
|
-
contentClassName?: string;
|
|
12
|
-
}): import("react/jsx-runtime").JSX.Element;
|
|
13
|
-
//# sourceMappingURL=detail-card.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"detail-card.d.ts","sourceRoot":"","sources":["../../../src/components/sidebar/detail-card.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAC;AAGjD,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAgB,EAChB,WAAmB,EACnB,OAAmB,EACnB,QAAQ,EACR,gBAAgB,EAChB,gBAAgB,GACjB,EAAE;IACD,OAAO,EAAE,SAAS,CAAC;IACnB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,2CAsHA"}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { cn } from '../../lib/cn';
|
|
5
|
-
export function DetailCard({ summary, children, onToggle, disabled = false, defaultOpen = false, variant = 'section', trailing, summaryClassName, contentClassName, }) {
|
|
6
|
-
const [open, setOpen] = useState(defaultOpen);
|
|
7
|
-
const handleToggle = (e) => {
|
|
8
|
-
// React surfaces `toggle` as a bubbling synthetic event even though the
|
|
9
|
-
// native event doesn't bubble. Without this guard, a nested <details>
|
|
10
|
-
// (e.g. an event card inside the Events section) collapsing would flip
|
|
11
|
-
// the outer DetailCard's state. Only react to direct toggles.
|
|
12
|
-
if (e.target !== e.currentTarget)
|
|
13
|
-
return;
|
|
14
|
-
const next = e.currentTarget.open;
|
|
15
|
-
setOpen(next);
|
|
16
|
-
onToggle?.(next);
|
|
17
|
-
};
|
|
18
|
-
if (variant === 'card') {
|
|
19
|
-
if (disabled) {
|
|
20
|
-
return (_jsx("div", { className: cn('list-none px-3 py-4 bg-background-200 [&::-webkit-details-marker]:hidden', summaryClassName), style: { cursor: 'not-allowed', opacity: 0.8 }, children: summary }));
|
|
21
|
-
}
|
|
22
|
-
return (_jsxs("details", { className: "group/card last:border-b border-gray-alpha-400", open: open, onToggle: handleToggle, children: [_jsx("summary", { className: cn('list-none cursor-pointer px-3 py-4 border-t border-gray-alpha-400 bg-background-200 hover:bg-gray-100 [&::-webkit-details-marker]:hidden', summaryClassName), children: _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(ChevronRight, { size: 14, className: cn('shrink-0 text-gray-700 group-hover/card:text-gray-1000', open && 'rotate-90') }), summary] }) }), _jsx("div", { className: contentClassName, children: children })] }));
|
|
23
|
-
}
|
|
24
|
-
// Shared height with the expandable summary row. Keeps the trailing /
|
|
25
|
-
// disabled / chevron variants visually identical in height regardless of
|
|
26
|
-
// what's in the trailing slot.
|
|
27
|
-
const rowClasses = 'flex h-9 items-center gap-2 px-2 -mx-2 text-heading-14 font-medium my-2';
|
|
28
|
-
if (trailing) {
|
|
29
|
-
return (_jsx("section", { className: "-mx-4 border-t px-4 border-gray-alpha-400", children: _jsxs("div", { className: cn(rowClasses, summaryClassName), children: [_jsx("div", { className: "isolate relative shrink-0 w-3.5 h-3.5 text-gray-700", children: _jsx(ChevronRight, { size: 14, className: "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" }) }), _jsx("span", { className: "min-w-0 flex-1", children: summary }), _jsx("div", { className: "shrink-0 pr-1", children: trailing })] }) }));
|
|
30
|
-
}
|
|
31
|
-
if (disabled) {
|
|
32
|
-
return (_jsx("section", { className: "-mx-4 border-t px-4 border-gray-alpha-400", children: _jsx("div", { className: cn(rowClasses, summaryClassName), style: { color: 'var(--ds-gray-700)', cursor: 'not-allowed' }, children: _jsx("span", { className: "min-w-0 flex-1", children: summary }) }) }));
|
|
33
|
-
}
|
|
34
|
-
return (_jsx("section", { className: "-mx-4 border-t px-4 border-gray-alpha-400", children: _jsxs("details", { className: "group", open: open, onToggle: handleToggle, children: [_jsxs("summary", { className: cn('group/trigger list-none cursor-pointer rounded hover:bg-gray-alpha-100 [&::-webkit-details-marker]:hidden', rowClasses, summaryClassName), children: [_jsxs("div", { className: "isolate relative shrink-0 text-gray-700 group-hover/trigger:text-gray-1000 w-3.5 h-3.5", children: [_jsx(ChevronRight, { size: 14, className: "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-100 group-open:opacity-0" }), _jsx(ChevronDown, { size: 14, className: "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 group-open:opacity-100" })] }), _jsx("span", { className: "min-w-0 flex-1", children: summary })] }), _jsx("div", { className: cn('mb-3', contentClassName), children: children })] }) }));
|
|
35
|
-
}
|
|
36
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGV0YWlsLWNhcmQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29tcG9uZW50cy9zaWRlYmFyL2RldGFpbC1jYXJkLnRzeCJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUEsT0FBTyxFQUFFLFdBQVcsRUFBRSxZQUFZLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDekQsT0FBTyxFQUFrQixRQUFRLEVBQUUsTUFBTSxPQUFPLENBQUM7QUFDakQsT0FBTyxFQUFFLEVBQUUsRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUVsQyxNQUFNLFVBQVUsVUFBVSxDQUFDLEVBQ3pCLE9BQU8sRUFDUCxRQUFRLEVBQ1IsUUFBUSxFQUNSLFFBQVEsR0FBRyxLQUFLLEVBQ2hCLFdBQVcsR0FBRyxLQUFLLEVBQ25CLE9BQU8sR0FBRyxTQUFTLEVBQ25CLFFBQVEsRUFDUixnQkFBZ0IsRUFDaEIsZ0JBQWdCLEdBV2pCO0lBQ0MsTUFBTSxDQUFDLElBQUksRUFBRSxPQUFPLENBQUMsR0FBRyxRQUFRLENBQUMsV0FBVyxDQUFDLENBQUM7SUFFOUMsTUFBTSxZQUFZLEdBQUcsQ0FBQyxDQUEyQyxFQUFFLEVBQUU7UUFDbkUsd0VBQXdFO1FBQ3hFLHNFQUFzRTtRQUN0RSx1RUFBdUU7UUFDdkUsOERBQThEO1FBQzlELElBQUksQ0FBQyxDQUFDLE1BQU0sS0FBSyxDQUFDLENBQUMsYUFBYTtZQUFFLE9BQU87UUFDekMsTUFBTSxJQUFJLEdBQUcsQ0FBQyxDQUFDLGFBQWEsQ0FBQyxJQUFJLENBQUM7UUFDbEMsT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ2QsUUFBUSxFQUFFLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDbkIsQ0FBQyxDQUFDO0lBRUYsSUFBSSxPQUFPLEtBQUssTUFBTSxFQUFFLENBQUM7UUFDdkIsSUFBSSxRQUFRLEVBQUUsQ0FBQztZQUNiLE9BQU8sQ0FDTCxjQUNFLFNBQVMsRUFBRSxFQUFFLENBQ1gsMEVBQTBFLEVBQzFFLGdCQUFnQixDQUNqQixFQUNELEtBQUssRUFBRSxFQUFFLE1BQU0sRUFBRSxhQUFhLEVBQUUsT0FBTyxFQUFFLEdBQUcsRUFBRSxZQUU3QyxPQUFPLEdBQ0osQ0FDUCxDQUFDO1FBQ0osQ0FBQztRQUNELE9BQU8sQ0FDTCxtQkFDRSxTQUFTLEVBQUMsZ0RBQWdELEVBQzFELElBQUksRUFBRSxJQUFJLEVBQ1YsUUFBUSxFQUFFLFlBQVksYUFFdEIsa0JBQ0UsU0FBUyxFQUFFLEVBQUUsQ0FDWCwwSUFBMEksRUFDMUksZ0JBQWdCLENBQ2pCLFlBRUQsZ0JBQU0sU0FBUyxFQUFDLDJCQUEyQixhQUN6QyxLQUFDLFlBQVksSUFDWCxJQUFJLEVBQUUsRUFBRSxFQUNSLFNBQVMsRUFBRSxFQUFFLENBQ1gsd0RBQXdELEVBQ3hELElBQUksSUFBSSxXQUFXLENBQ3BCLEdBQ0QsRUFDRCxPQUFPLElBQ0gsR0FDQyxFQUNWLGNBQUssU0FBUyxFQUFFLGdCQUFnQixZQUFHLFFBQVEsR0FBTyxJQUMxQyxDQUNYLENBQUM7SUFDSixDQUFDO0lBRUQsc0VBQXNFO0lBQ3RFLHlFQUF5RTtJQUN6RSwrQkFBK0I7SUFDL0IsTUFBTSxVQUFVLEdBQ2QseUVBQXlFLENBQUM7SUFFNUUsSUFBSSxRQUFRLEVBQUUsQ0FBQztRQUNiLE9BQU8sQ0FDTCxrQkFBUyxTQUFTLEVBQUMsMkNBQTJDLFlBQzVELGVBQUssU0FBUyxFQUFFLEVBQUUsQ0FBQyxVQUFVLEVBQUUsZ0JBQWdCLENBQUMsYUFDOUMsY0FBSyxTQUFTLEVBQUMscURBQXFELFlBQ2xFLEtBQUMsWUFBWSxJQUNYLElBQUksRUFBRSxFQUFFLEVBQ1IsU0FBUyxFQUFDLDZEQUE2RCxHQUN2RSxHQUNFLEVBQ04sZUFBTSxTQUFTLEVBQUMsZ0JBQWdCLFlBQUUsT0FBTyxHQUFRLEVBQ2pELGNBQUssU0FBUyxFQUFDLGVBQWUsWUFBRSxRQUFRLEdBQU8sSUFDM0MsR0FDRSxDQUNYLENBQUM7SUFDSixDQUFDO0lBRUQsSUFBSSxRQUFRLEVBQUUsQ0FBQztRQUNiLE9BQU8sQ0FDTCxrQkFBUyxTQUFTLEVBQUMsMkNBQTJDLFlBQzVELGNBQ0UsU0FBUyxFQUFFLEVBQUUsQ0FBQyxVQUFVLEVBQUUsZ0JBQWdCLENBQUMsRUFDM0MsS0FBSyxFQUFFLEVBQUUsS0FBSyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sRUFBRSxhQUFhLEVBQUUsWUFFN0QsZUFBTSxTQUFTLEVBQUMsZ0JBQWdCLFlBQUUsT0FBTyxHQUFRLEdBQzdDLEdBQ0UsQ0FDWCxDQUFDO0lBQ0osQ0FBQztJQUVELE9BQU8sQ0FDTCxrQkFBUyxTQUFTLEVBQUMsMkNBQTJDLFlBQzVELG1CQUFTLFNBQVMsRUFBQyxPQUFPLEVBQUMsSUFBSSxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUUsWUFBWSxhQUMzRCxtQkFDRSxTQUFTLEVBQUUsRUFBRSxDQUNYLDJHQUEyRyxFQUMzRyxVQUFVLEVBQ1YsZ0JBQWdCLENBQ2pCLGFBRUQsZUFBSyxTQUFTLEVBQUMsd0ZBQXdGLGFBQ3JHLEtBQUMsWUFBWSxJQUNYLElBQUksRUFBRSxFQUFFLEVBQ1IsU0FBUyxFQUFDLDhGQUE4RixHQUN4RyxFQUNGLEtBQUMsV0FBVyxJQUNWLElBQUksRUFBRSxFQUFFLEVBQ1IsU0FBUyxFQUFDLDhGQUE4RixHQUN4RyxJQUNFLEVBQ04sZUFBTSxTQUFTLEVBQUMsZ0JBQWdCLFlBQUUsT0FBTyxHQUFRLElBQ3pDLEVBQ1YsY0FBSyxTQUFTLEVBQUUsRUFBRSxDQUFDLE1BQU0sRUFBRSxnQkFBZ0IsQ0FBQyxZQUFHLFFBQVEsR0FBTyxJQUN0RCxHQUNGLENBQ1gsQ0FBQztBQUNKLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBDaGV2cm9uRG93biwgQ2hldnJvblJpZ2h0IH0gZnJvbSAnbHVjaWRlLXJlYWN0JztcbmltcG9ydCB7IHR5cGUgUmVhY3ROb2RlLCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0JztcbmltcG9ydCB7IGNuIH0gZnJvbSAnLi4vLi4vbGliL2NuJztcblxuZXhwb3J0IGZ1bmN0aW9uIERldGFpbENhcmQoe1xuICBzdW1tYXJ5LFxuICBjaGlsZHJlbixcbiAgb25Ub2dnbGUsXG4gIGRpc2FibGVkID0gZmFsc2UsXG4gIGRlZmF1bHRPcGVuID0gZmFsc2UsXG4gIHZhcmlhbnQgPSAnc2VjdGlvbicsXG4gIHRyYWlsaW5nLFxuICBzdW1tYXJ5Q2xhc3NOYW1lLFxuICBjb250ZW50Q2xhc3NOYW1lLFxufToge1xuICBzdW1tYXJ5OiBSZWFjdE5vZGU7XG4gIGNoaWxkcmVuPzogUmVhY3ROb2RlO1xuICBvblRvZ2dsZT86IChvcGVuOiBib29sZWFuKSA9PiB2b2lkO1xuICBkaXNhYmxlZD86IGJvb2xlYW47XG4gIGRlZmF1bHRPcGVuPzogYm9vbGVhbjtcbiAgdmFyaWFudD86ICdzZWN0aW9uJyB8ICdjYXJkJztcbiAgdHJhaWxpbmc/OiBSZWFjdE5vZGU7XG4gIHN1bW1hcnlDbGFzc05hbWU/OiBzdHJpbmc7XG4gIGNvbnRlbnRDbGFzc05hbWU/OiBzdHJpbmc7XG59KSB7XG4gIGNvbnN0IFtvcGVuLCBzZXRPcGVuXSA9IHVzZVN0YXRlKGRlZmF1bHRPcGVuKTtcblxuICBjb25zdCBoYW5kbGVUb2dnbGUgPSAoZTogUmVhY3QuU3ludGhldGljRXZlbnQ8SFRNTERldGFpbHNFbGVtZW50PikgPT4ge1xuICAgIC8vIFJlYWN0IHN1cmZhY2VzIGB0b2dnbGVgIGFzIGEgYnViYmxpbmcgc3ludGhldGljIGV2ZW50IGV2ZW4gdGhvdWdoIHRoZVxuICAgIC8vIG5hdGl2ZSBldmVudCBkb2Vzbid0IGJ1YmJsZS4gV2l0aG91dCB0aGlzIGd1YXJkLCBhIG5lc3RlZCA8ZGV0YWlscz5cbiAgICAvLyAoZS5nLiBhbiBldmVudCBjYXJkIGluc2lkZSB0aGUgRXZlbnRzIHNlY3Rpb24pIGNvbGxhcHNpbmcgd291bGQgZmxpcFxuICAgIC8vIHRoZSBvdXRlciBEZXRhaWxDYXJkJ3Mgc3RhdGUuIE9ubHkgcmVhY3QgdG8gZGlyZWN0IHRvZ2dsZXMuXG4gICAgaWYgKGUudGFyZ2V0ICE9PSBlLmN1cnJlbnRUYXJnZXQpIHJldHVybjtcbiAgICBjb25zdCBuZXh0ID0gZS5jdXJyZW50VGFyZ2V0Lm9wZW47XG4gICAgc2V0T3BlbihuZXh0KTtcbiAgICBvblRvZ2dsZT8uKG5leHQpO1xuICB9O1xuXG4gIGlmICh2YXJpYW50ID09PSAnY2FyZCcpIHtcbiAgICBpZiAoZGlzYWJsZWQpIHtcbiAgICAgIHJldHVybiAoXG4gICAgICAgIDxkaXZcbiAgICAgICAgICBjbGFzc05hbWU9e2NuKFxuICAgICAgICAgICAgJ2xpc3Qtbm9uZSBweC0zIHB5LTQgYmctYmFja2dyb3VuZC0yMDAgWyY6Oi13ZWJraXQtZGV0YWlscy1tYXJrZXJdOmhpZGRlbicsXG4gICAgICAgICAgICBzdW1tYXJ5Q2xhc3NOYW1lXG4gICAgICAgICAgKX1cbiAgICAgICAgICBzdHlsZT17eyBjdXJzb3I6ICdub3QtYWxsb3dlZCcsIG9wYWNpdHk6IDAuOCB9fVxuICAgICAgICA+XG4gICAgICAgICAge3N1bW1hcnl9XG4gICAgICAgIDwvZGl2PlxuICAgICAgKTtcbiAgICB9XG4gICAgcmV0dXJuIChcbiAgICAgIDxkZXRhaWxzXG4gICAgICAgIGNsYXNzTmFtZT1cImdyb3VwL2NhcmQgbGFzdDpib3JkZXItYiBib3JkZXItZ3JheS1hbHBoYS00MDBcIlxuICAgICAgICBvcGVuPXtvcGVufVxuICAgICAgICBvblRvZ2dsZT17aGFuZGxlVG9nZ2xlfVxuICAgICAgPlxuICAgICAgICA8c3VtbWFyeVxuICAgICAgICAgIGNsYXNzTmFtZT17Y24oXG4gICAgICAgICAgICAnbGlzdC1ub25lIGN1cnNvci1wb2ludGVyIHB4LTMgcHktNCBib3JkZXItdCBib3JkZXItZ3JheS1hbHBoYS00MDAgYmctYmFja2dyb3VuZC0yMDAgaG92ZXI6YmctZ3JheS0xMDAgWyY6Oi13ZWJraXQtZGV0YWlscy1tYXJrZXJdOmhpZGRlbicsXG4gICAgICAgICAgICBzdW1tYXJ5Q2xhc3NOYW1lXG4gICAgICAgICAgKX1cbiAgICAgICAgPlxuICAgICAgICAgIDxzcGFuIGNsYXNzTmFtZT1cImZsZXggaXRlbXMtY2VudGVyIGdhcC0xLjVcIj5cbiAgICAgICAgICAgIDxDaGV2cm9uUmlnaHRcbiAgICAgICAgICAgICAgc2l6ZT17MTR9XG4gICAgICAgICAgICAgIGNsYXNzTmFtZT17Y24oXG4gICAgICAgICAgICAgICAgJ3Nocmluay0wIHRleHQtZ3JheS03MDAgZ3JvdXAtaG92ZXIvY2FyZDp0ZXh0LWdyYXktMTAwMCcsXG4gICAgICAgICAgICAgICAgb3BlbiAmJiAncm90YXRlLTkwJ1xuICAgICAgICAgICAgICApfVxuICAgICAgICAgICAgLz5cbiAgICAgICAgICAgIHtzdW1tYXJ5fVxuICAgICAgICAgIDwvc3Bhbj5cbiAgICAgICAgPC9zdW1tYXJ5PlxuICAgICAgICA8ZGl2IGNsYXNzTmFtZT17Y29udGVudENsYXNzTmFtZX0+e2NoaWxkcmVufTwvZGl2PlxuICAgICAgPC9kZXRhaWxzPlxuICAgICk7XG4gIH1cblxuICAvLyBTaGFyZWQgaGVpZ2h0IHdpdGggdGhlIGV4cGFuZGFibGUgc3VtbWFyeSByb3cuIEtlZXBzIHRoZSB0cmFpbGluZyAvXG4gIC8vIGRpc2FibGVkIC8gY2hldnJvbiB2YXJpYW50cyB2aXN1YWxseSBpZGVudGljYWwgaW4gaGVpZ2h0IHJlZ2FyZGxlc3Mgb2ZcbiAgLy8gd2hhdCdzIGluIHRoZSB0cmFpbGluZyBzbG90LlxuICBjb25zdCByb3dDbGFzc2VzID1cbiAgICAnZmxleCBoLTkgaXRlbXMtY2VudGVyIGdhcC0yIHB4LTIgLW14LTIgdGV4dC1oZWFkaW5nLTE0IGZvbnQtbWVkaXVtIG15LTInO1xuXG4gIGlmICh0cmFpbGluZykge1xuICAgIHJldHVybiAoXG4gICAgICA8c2VjdGlvbiBjbGFzc05hbWU9XCItbXgtNCBib3JkZXItdCBweC00IGJvcmRlci1ncmF5LWFscGhhLTQwMFwiPlxuICAgICAgICA8ZGl2IGNsYXNzTmFtZT17Y24ocm93Q2xhc3Nlcywgc3VtbWFyeUNsYXNzTmFtZSl9PlxuICAgICAgICAgIDxkaXYgY2xhc3NOYW1lPVwiaXNvbGF0ZSByZWxhdGl2ZSBzaHJpbmstMCB3LTMuNSBoLTMuNSB0ZXh0LWdyYXktNzAwXCI+XG4gICAgICAgICAgICA8Q2hldnJvblJpZ2h0XG4gICAgICAgICAgICAgIHNpemU9ezE0fVxuICAgICAgICAgICAgICBjbGFzc05hbWU9XCJhYnNvbHV0ZSB0b3AtMS8yIGxlZnQtMS8yIC10cmFuc2xhdGUteC0xLzIgLXRyYW5zbGF0ZS15LTEvMlwiXG4gICAgICAgICAgICAvPlxuICAgICAgICAgIDwvZGl2PlxuICAgICAgICAgIDxzcGFuIGNsYXNzTmFtZT1cIm1pbi13LTAgZmxleC0xXCI+e3N1bW1hcnl9PC9zcGFuPlxuICAgICAgICAgIDxkaXYgY2xhc3NOYW1lPVwic2hyaW5rLTAgcHItMVwiPnt0cmFpbGluZ308L2Rpdj5cbiAgICAgICAgPC9kaXY+XG4gICAgICA8L3NlY3Rpb24+XG4gICAgKTtcbiAgfVxuXG4gIGlmIChkaXNhYmxlZCkge1xuICAgIHJldHVybiAoXG4gICAgICA8c2VjdGlvbiBjbGFzc05hbWU9XCItbXgtNCBib3JkZXItdCBweC00IGJvcmRlci1ncmF5LWFscGhhLTQwMFwiPlxuICAgICAgICA8ZGl2XG4gICAgICAgICAgY2xhc3NOYW1lPXtjbihyb3dDbGFzc2VzLCBzdW1tYXJ5Q2xhc3NOYW1lKX1cbiAgICAgICAgICBzdHlsZT17eyBjb2xvcjogJ3ZhcigtLWRzLWdyYXktNzAwKScsIGN1cnNvcjogJ25vdC1hbGxvd2VkJyB9fVxuICAgICAgICA+XG4gICAgICAgICAgPHNwYW4gY2xhc3NOYW1lPVwibWluLXctMCBmbGV4LTFcIj57c3VtbWFyeX08L3NwYW4+XG4gICAgICAgIDwvZGl2PlxuICAgICAgPC9zZWN0aW9uPlxuICAgICk7XG4gIH1cblxuICByZXR1cm4gKFxuICAgIDxzZWN0aW9uIGNsYXNzTmFtZT1cIi1teC00IGJvcmRlci10IHB4LTQgYm9yZGVyLWdyYXktYWxwaGEtNDAwXCI+XG4gICAgICA8ZGV0YWlscyBjbGFzc05hbWU9XCJncm91cFwiIG9wZW49e29wZW59IG9uVG9nZ2xlPXtoYW5kbGVUb2dnbGV9PlxuICAgICAgICA8c3VtbWFyeVxuICAgICAgICAgIGNsYXNzTmFtZT17Y24oXG4gICAgICAgICAgICAnZ3JvdXAvdHJpZ2dlciBsaXN0LW5vbmUgY3Vyc29yLXBvaW50ZXIgcm91bmRlZCBob3ZlcjpiZy1ncmF5LWFscGhhLTEwMCBbJjo6LXdlYmtpdC1kZXRhaWxzLW1hcmtlcl06aGlkZGVuJyxcbiAgICAgICAgICAgIHJvd0NsYXNzZXMsXG4gICAgICAgICAgICBzdW1tYXJ5Q2xhc3NOYW1lXG4gICAgICAgICAgKX1cbiAgICAgICAgPlxuICAgICAgICAgIDxkaXYgY2xhc3NOYW1lPVwiaXNvbGF0ZSByZWxhdGl2ZSBzaHJpbmstMCB0ZXh0LWdyYXktNzAwIGdyb3VwLWhvdmVyL3RyaWdnZXI6dGV4dC1ncmF5LTEwMDAgdy0zLjUgaC0zLjVcIj5cbiAgICAgICAgICAgIDxDaGV2cm9uUmlnaHRcbiAgICAgICAgICAgICAgc2l6ZT17MTR9XG4gICAgICAgICAgICAgIGNsYXNzTmFtZT1cImFic29sdXRlIHRvcC0xLzIgbGVmdC0xLzIgLXRyYW5zbGF0ZS14LTEvMiAtdHJhbnNsYXRlLXktMS8yIG9wYWNpdHktMTAwIGdyb3VwLW9wZW46b3BhY2l0eS0wXCJcbiAgICAgICAgICAgIC8+XG4gICAgICAgICAgICA8Q2hldnJvbkRvd25cbiAgICAgICAgICAgICAgc2l6ZT17MTR9XG4gICAgICAgICAgICAgIGNsYXNzTmFtZT1cImFic29sdXRlIHRvcC0xLzIgbGVmdC0xLzIgLXRyYW5zbGF0ZS14LTEvMiAtdHJhbnNsYXRlLXktMS8yIG9wYWNpdHktMCBncm91cC1vcGVuOm9wYWNpdHktMTAwXCJcbiAgICAgICAgICAgIC8+XG4gICAgICAgICAgPC9kaXY+XG4gICAgICAgICAgPHNwYW4gY2xhc3NOYW1lPVwibWluLXctMCBmbGV4LTFcIj57c3VtbWFyeX08L3NwYW4+XG4gICAgICAgIDwvc3VtbWFyeT5cbiAgICAgICAgPGRpdiBjbGFzc05hbWU9e2NuKCdtYi0zJywgY29udGVudENsYXNzTmFtZSl9PntjaGlsZHJlbn08L2Rpdj5cbiAgICAgIDwvZGV0YWlscz5cbiAgICA8L3NlY3Rpb24+XG4gICk7XG59XG4iXX0=
|