Skip to content

Commit 9fcbf1d

Browse files
authored
Merge pull request #84 from UiPath/feat/waterfall-timeline-view
feat: add waterfall timeline view and tabbed trace panels
2 parents b0819bc + f09ffeb commit 9fcbf1d

File tree

13 files changed

+366
-161
lines changed

13 files changed

+366
-161
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-dev"
3-
version = "0.0.57"
3+
version = "0.0.58"
44
description = "UiPath Developer Console"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/dev/server/frontend/src/components/runs/RunDetailsPanel.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,12 +460,14 @@ function DataSection({
460460
{copyText && (
461461
<button
462462
onClick={copy}
463-
className="ml-auto text-[10px] cursor-pointer px-1.5 py-0.5 rounded"
463+
className="ml-auto text-[10px] cursor-pointer px-1.5 py-0.5 rounded transition-colors"
464464
style={{
465465
color: copied ? "var(--success)" : "var(--text-muted)",
466466
background: "var(--bg-secondary)",
467467
border: "1px solid var(--border)",
468468
}}
469+
onMouseEnter={(e) => { if (!copied) e.currentTarget.style.color = "var(--text-primary)"; }}
470+
onMouseLeave={(e) => { e.currentTarget.style.color = copied ? "var(--success)" : "var(--text-muted)"; }}
469471
>
470472
{copied ? "Copied" : "Copy"}
471473
</button>

src/uipath/dev/server/frontend/src/components/traces/SpanDetails.tsx

Lines changed: 122 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,6 @@ function tryParseJson(value: unknown): string | null {
2828
return null;
2929
}
3030

31-
function formatDuration(ms: number): string {
32-
if (ms < 1) return `${(ms * 1000).toFixed(0)}us`;
33-
if (ms < 1000) return `${ms.toFixed(2)}ms`;
34-
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
35-
const mins = Math.floor(ms / 60000);
36-
const secs = ((ms % 60000) / 1000).toFixed(1);
37-
return `${mins}m ${secs}s`;
38-
}
39-
4031
const TRUNCATE_LIMIT = 200;
4132

4233
function stringifyValue(value: unknown): string {
@@ -112,12 +103,16 @@ function AttributeValue({ value }: { value: unknown }) {
112103
export default function SpanDetails({ span }: Props) {
113104
const [attrsOpen, setAttrsOpen] = useState(true);
114105
const [idsOpen, setIdsOpen] = useState(false);
106+
const [detailMode, setDetailMode] = useState<"table" | "json">("table");
107+
const [copied, setCopied] = useState(false);
115108
const status = STATUS_CONFIG[span.status.toLowerCase()] ?? { ...DEFAULT_STATUS, label: span.status };
116-
117-
const time = new Date(span.timestamp).toLocaleTimeString(undefined, {
118-
hour12: false,
119-
fractionalSecondDigits: 3,
120-
} as Intl.DateTimeFormatOptions);
109+
const spanJson = useMemo(() => JSON.stringify(span, null, 2), [span]);
110+
const copySpanJson = useCallback(() => {
111+
navigator.clipboard.writeText(spanJson).then(() => {
112+
setCopied(true);
113+
setTimeout(() => setCopied(false), 1500);
114+
});
115+
}, [spanJson]);
121116

122117
const attrEntries = Object.entries(span.attributes);
123118
const ids: { label: string; value: string }[] = [
@@ -128,17 +123,38 @@ export default function SpanDetails({ span }: Props) {
128123
];
129124

130125
return (
131-
<div className="overflow-y-auto h-full text-xs leading-normal">
132-
{/* Header: name + pills */}
126+
<div className="flex flex-col h-full text-xs leading-normal">
127+
{/* Header: tabs + status pill — fixed */}
133128
<div
134-
className="px-2 py-1.5 border-b flex items-center gap-2 flex-wrap"
135-
style={{ borderColor: "var(--border)", background: "var(--bg-secondary)" }}
129+
className="px-2 border-b flex items-center gap-1 shrink-0"
130+
style={{ borderColor: "var(--border)", background: "var(--bg-secondary)", height: "28px" }}
136131
>
137-
<span className="text-xs font-semibold mr-auto" style={{ color: "var(--text-primary)" }}>
138-
{span.span_name}
139-
</span>
132+
<button
133+
onClick={() => setDetailMode("table")}
134+
className="px-2 h-[18px] text-[10px] uppercase tracking-wider font-semibold rounded transition-colors cursor-pointer inline-flex items-center"
135+
style={{
136+
color: detailMode === "table" ? "var(--accent)" : "var(--text-muted)",
137+
background: detailMode === "table" ? "color-mix(in srgb, var(--accent) 10%, transparent)" : "transparent",
138+
}}
139+
onMouseEnter={(e) => { if (detailMode !== "table") e.currentTarget.style.color = "var(--text-primary)"; }}
140+
onMouseLeave={(e) => { if (detailMode !== "table") e.currentTarget.style.color = "var(--text-muted)"; }}
141+
>
142+
Table
143+
</button>
144+
<button
145+
onClick={() => setDetailMode("json")}
146+
className="px-2 h-[18px] text-[10px] uppercase tracking-wider font-semibold rounded transition-colors cursor-pointer inline-flex items-center"
147+
style={{
148+
color: detailMode === "json" ? "var(--accent)" : "var(--text-muted)",
149+
background: detailMode === "json" ? "color-mix(in srgb, var(--accent) 10%, transparent)" : "transparent",
150+
}}
151+
onMouseEnter={(e) => { if (detailMode !== "json") e.currentTarget.style.color = "var(--text-primary)"; }}
152+
onMouseLeave={(e) => { if (detailMode !== "json") e.currentTarget.style.color = "var(--text-muted)"; }}
153+
>
154+
JSON
155+
</button>
140156
<span
141-
className="shrink-0 inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider"
157+
className="ml-auto shrink-0 inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider"
142158
style={{
143159
background: `color-mix(in srgb, ${status.color} 15%, var(--bg-secondary))`,
144160
color: status.color,
@@ -147,87 +163,103 @@ export default function SpanDetails({ span }: Props) {
147163
<span className="inline-block w-1.5 h-1.5 rounded-full" style={{ background: status.color }} />
148164
{status.label}
149165
</span>
150-
{span.duration_ms != null && (
151-
<span className="shrink-0 font-mono text-[11px] font-semibold" style={{ color: "var(--warning)" }}>
152-
{formatDuration(span.duration_ms)}
153-
</span>
154-
)}
155-
<span className="shrink-0 font-mono text-[11px]" style={{ color: "var(--text-muted)" }}>
156-
{time}
157-
</span>
158166
</div>
159167

160-
{/* Attributes — collapsible */}
161-
{attrEntries.length > 0 && (
168+
{/* Scrollable content */}
169+
<div className="overflow-y-auto flex-1 p-0.5 pr-0 pt-0 mr-0.5 mt-0.5">
170+
{detailMode === "table" ? (
162171
<>
163-
<div
164-
className="px-2 py-1 text-[10px] uppercase font-bold tracking-wider border-b cursor-pointer flex items-center"
165-
style={{ color: "var(--accent)", borderColor: "var(--border)", background: "var(--bg-secondary)" }}
166-
onClick={() => setAttrsOpen((o) => !o)}
167-
>
168-
<span className="flex-1">Attributes ({attrEntries.length})</span>
169-
<span style={{ color: "var(--text-muted)", transform: attrsOpen ? "rotate(0deg)" : "rotate(-90deg)" }}>
170-
&#x25BE;
171-
</span>
172-
</div>
173-
{attrsOpen && attrEntries.map(([key, value], idx) => (
172+
{/* Attributes — collapsible */}
173+
{attrEntries.length > 0 && (
174+
<>
174175
<div
175-
key={key}
176-
className="flex gap-2 px-2 py-1 items-start border-b"
177-
style={{
178-
borderColor: "var(--border)",
179-
background: idx % 2 === 0 ? "var(--bg-primary)" : "var(--bg-secondary)",
180-
}}
176+
className="px-2 py-1 text-[10px] uppercase font-bold tracking-wider border-b cursor-pointer flex items-center"
177+
style={{ color: "var(--accent)", borderColor: "var(--border)", background: "var(--bg-secondary)" }}
178+
onClick={() => setAttrsOpen((o) => !o)}
181179
>
182-
<span
183-
className="font-mono font-semibold shrink-0 pt-px truncate text-[11px]"
184-
style={{ color: "var(--info)", width: "35%" }}
185-
title={key}
186-
>
187-
{key}
188-
</span>
189-
<span className="flex-1 min-w-0">
190-
<AttributeValue value={value} />
180+
<span className="flex-1">Attributes ({attrEntries.length})</span>
181+
<span style={{ color: "var(--text-muted)", transform: attrsOpen ? "rotate(0deg)" : "rotate(-90deg)" }}>
182+
&#x25BE;
191183
</span>
192184
</div>
193-
))}
194-
</>
195-
)}
185+
{attrsOpen && attrEntries.map(([key, value], idx) => (
186+
<div
187+
key={key}
188+
className="flex gap-2 px-2 py-1 items-start border-b"
189+
style={{
190+
borderColor: "var(--border)",
191+
background: idx % 2 === 0 ? "var(--bg-primary)" : "var(--bg-secondary)",
192+
}}
193+
>
194+
<span
195+
className="font-mono font-semibold shrink-0 pt-px truncate text-[11px]"
196+
style={{ color: "var(--info)", width: "35%" }}
197+
title={key}
198+
>
199+
{key}
200+
</span>
201+
<span className="flex-1 min-w-0">
202+
<AttributeValue value={value} />
203+
</span>
204+
</div>
205+
))}
206+
</>
207+
)}
196208

197-
{/* Identifiers — collapsible */}
198-
<div
199-
className="px-2 py-1 text-[10px] uppercase font-bold tracking-wider border-b cursor-pointer flex items-center"
200-
style={{ color: "var(--accent)", borderColor: "var(--border)", background: "var(--bg-secondary)" }}
201-
onClick={() => setIdsOpen((o) => !o)}
202-
>
203-
<span className="flex-1">Identifiers ({ids.length})</span>
204-
<span style={{ color: "var(--text-muted)", transform: idsOpen ? "rotate(0deg)" : "rotate(-90deg)" }}>
205-
&#x25BE;
206-
</span>
207-
</div>
208-
{idsOpen && ids.map((id, idx) => (
209+
{/* Identifiers — collapsible */}
209210
<div
210-
key={id.label}
211-
className="flex gap-2 px-2 py-1 items-start border-b"
212-
style={{
213-
borderColor: "var(--border)",
214-
background: idx % 2 === 0 ? "var(--bg-primary)" : "var(--bg-secondary)",
215-
}}
211+
className="px-2 py-1 text-[10px] uppercase font-bold tracking-wider border-b cursor-pointer flex items-center"
212+
style={{ color: "var(--accent)", borderColor: "var(--border)", background: "var(--bg-secondary)" }}
213+
onClick={() => setIdsOpen((o) => !o)}
216214
>
217-
<span
218-
className="font-mono font-semibold shrink-0 pt-px truncate text-[11px]"
219-
style={{ color: "var(--info)", width: "35%" }}
220-
title={id.label}
221-
>
222-
{id.label}
215+
<span className="flex-1">Identifiers ({ids.length})</span>
216+
<span style={{ color: "var(--text-muted)", transform: idsOpen ? "rotate(0deg)" : "rotate(-90deg)" }}>
217+
&#x25BE;
223218
</span>
224-
<span className="flex-1 min-w-0">
225-
<span className="font-mono text-[11px] break-all" style={{ color: "var(--text-primary)" }}>
226-
{id.value}
219+
</div>
220+
{idsOpen && ids.map((id, idx) => (
221+
<div
222+
key={id.label}
223+
className="flex gap-2 px-2 py-1 items-start border-b"
224+
style={{
225+
borderColor: "var(--border)",
226+
background: idx % 2 === 0 ? "var(--bg-primary)" : "var(--bg-secondary)",
227+
}}
228+
>
229+
<span
230+
className="font-mono font-semibold shrink-0 pt-px truncate text-[11px]"
231+
style={{ color: "var(--info)", width: "35%" }}
232+
title={id.label}
233+
>
234+
{id.label}
227235
</span>
228-
</span>
236+
<span className="flex-1 min-w-0">
237+
<span className="font-mono text-[11px] break-all" style={{ color: "var(--text-primary)" }}>
238+
{id.value}
239+
</span>
240+
</span>
241+
</div>
242+
))}
243+
</>
244+
) : (
245+
<div className="relative">
246+
<button
247+
onClick={copySpanJson}
248+
className="absolute top-1 right-1 z-10 text-[10px] cursor-pointer px-1.5 py-0.5 rounded transition-colors"
249+
style={{
250+
color: copied ? "var(--success)" : "var(--text-muted)",
251+
background: "var(--bg-secondary)",
252+
border: "1px solid var(--border)",
253+
}}
254+
onMouseEnter={(e) => { if (!copied) e.currentTarget.style.color = "var(--text-primary)"; }}
255+
onMouseLeave={(e) => { e.currentTarget.style.color = copied ? "var(--success)" : "var(--text-muted)"; }}
256+
>
257+
{copied ? "Copied!" : "Copy"}
258+
</button>
259+
<JsonHighlight json={spanJson} className="font-mono text-[11px] whitespace-pre-wrap p-2" style={{}} />
229260
</div>
230-
))}
261+
)}
262+
</div>
231263
</div>
232264
);
233265
}

0 commit comments

Comments
 (0)