Skip to content

Commit 3913e57

Browse files
authored
Trace view fixes and improvements (#1046)
* Query param for span using history.replaceState is working * When clicking again on a node, don’t collapse it * Close the span view using the same replacing of the search param * Conditional rendering of the resize panels was causing the tree view re-rendering and collapsing… * Live reloading moved to where the parent label is * Span action bar is now deeper * WIP on trace view navigation changes with shortcuts * Shortcuts for expanding and collapsing en masse * Number keys expand/collapse levels * Changed duration toggle to a shortcut key * Option + click expands/collapse at that level * Option/alt left/right expands/collapse at that level * Removed unused imports * Link from the runs table to the specific span * Latest lockfile * Sorted imports * When doing a test link directly to a span * Replay links to the span * CLI log links go directly to a span * Keyboard shortcuts are in a popover if the width is narrow * If holding alt only collapse level * Don’t expand the individual node if you’re holding alt
1 parent 26f3103 commit 3913e57

File tree

12 files changed

+574
-130
lines changed

12 files changed

+574
-130
lines changed

apps/webapp/app/components/primitives/ShortcutKey.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import { Fragment } from "react";
22
import { Modifier, ShortcutDefinition } from "~/hooks/useShortcutKeys";
33
import { cn } from "~/utils/cn";
44
import { useOperatingSystem } from "./OperatingSystemProvider";
5+
import {
6+
ChevronDownIcon,
7+
ChevronLeftIcon,
8+
ChevronRightIcon,
9+
ChevronUpIcon,
10+
} from "@heroicons/react/20/solid";
511

6-
const variants = {
12+
export const variants = {
713
small:
814
"text-[0.6rem] font-medium min-w-[17px] rounded-[2px] px-1 ml-1 -mr-0.5 grid place-content-center border border-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-dimmed/60 transition uppercase",
915
medium:
@@ -23,7 +29,7 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
2329
const isMac = platform === "mac";
2430
let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
2531
const modifiers = relevantShortcut.modifiers ?? [];
26-
const character = keyString(relevantShortcut.key, isMac);
32+
const character = keyString(relevantShortcut.key, isMac, variant);
2733

2834
return (
2935
<span className={cn(variants[variant], className)}>
@@ -35,10 +41,22 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
3541
);
3642
}
3743

38-
function keyString(key: String, isMac: boolean) {
44+
function keyString(key: String, isMac: boolean, size: "small" | "medium") {
45+
key = key.toLowerCase();
46+
47+
const className = size === "small" ? "w-2.5 h-4" : "w-3 h-5";
48+
3949
switch (key) {
4050
case "enter":
4151
return isMac ? "↵" : key;
52+
case "arrowdown":
53+
return <ChevronDownIcon className={className} />;
54+
case "arrowup":
55+
return <ChevronUpIcon className={className} />;
56+
case "arrowleft":
57+
return <ChevronLeftIcon className={className} />;
58+
case "arrowright":
59+
return <ChevronRightIcon className={className} />;
4260
default:
4361
return key;
4462
}

apps/webapp/app/components/primitives/TreeView/TreeView.tsx

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { VirtualItem, Virtualizer, useVirtualizer } from "@tanstack/react-virtual";
2+
import { motion } from "framer-motion";
23
import { MutableRefObject, RefObject, useCallback, useEffect, useReducer, useRef } from "react";
34
import { UnmountClosed } from "react-collapse";
45
import { cn } from "~/utils/cn";
56
import { NodeState, NodesState, reducer } from "./reducer";
67
import { applyFilterToState, concreteStateFromInput, selectedIdFromState } from "./utils";
7-
import { motion } from "framer-motion";
88

99
export type TreeViewProps<TData> = {
1010
tree: FlatTree<TData>;
@@ -165,6 +165,11 @@ export type UseTreeStateOutput = {
165165
expandNode: (id: string, scrollToNode?: boolean) => void;
166166
collapseNode: (id: string) => void;
167167
toggleExpandNode: (id: string, scrollToNode?: boolean) => void;
168+
expandAllBelowDepth: (depth: number) => void;
169+
collapseAllBelowDepth: (depth: number) => void;
170+
expandLevel: (level: number) => void;
171+
collapseLevel: (level: number) => void;
172+
toggleExpandLevel: (level: number) => void;
168173
selectFirstVisibleNode: (scrollToNode?: boolean) => void;
169174
selectLastVisibleNode: (scrollToNode?: boolean) => void;
170175
selectNextVisibleNode: (scrollToNode?: boolean) => void;
@@ -333,6 +338,41 @@ export function useTree<TData>({
333338
[state]
334339
);
335340

341+
const expandAllBelowDepth = useCallback(
342+
(depth: number) => {
343+
dispatch({ type: "EXPAND_ALL_BELOW_DEPTH", payload: { tree, depth } });
344+
},
345+
[state]
346+
);
347+
348+
const collapseAllBelowDepth = useCallback(
349+
(depth: number) => {
350+
dispatch({ type: "COLLAPSE_ALL_BELOW_DEPTH", payload: { tree, depth } });
351+
},
352+
[state]
353+
);
354+
355+
const expandLevel = useCallback(
356+
(level: number) => {
357+
dispatch({ type: "EXPAND_LEVEL", payload: { tree, level } });
358+
},
359+
[state]
360+
);
361+
362+
const collapseLevel = useCallback(
363+
(level: number) => {
364+
dispatch({ type: "COLLAPSE_LEVEL", payload: { tree, level } });
365+
},
366+
[state]
367+
);
368+
369+
const toggleExpandLevel = useCallback(
370+
(level: number) => {
371+
dispatch({ type: "TOGGLE_EXPAND_LEVEL", payload: { tree, level } });
372+
},
373+
[state]
374+
);
375+
336376
const getTreeProps = useCallback(() => {
337377
return {
338378
role: "tree",
@@ -368,25 +408,48 @@ export function useTree<TData>({
368408
}
369409
case "Left":
370410
case "ArrowLeft": {
411+
e.preventDefault();
412+
371413
const selected = selectedIdFromState(state.nodes);
372414
if (selected) {
373415
const treeNode = tree.find((node) => node.id === selected);
374-
if (treeNode && treeNode.hasChildren && state.nodes[selected].expanded) {
416+
417+
if (e.altKey) {
418+
if (treeNode && treeNode.hasChildren) {
419+
collapseLevel(treeNode.level);
420+
}
421+
break;
422+
}
423+
424+
const shouldCollapse =
425+
treeNode && treeNode.hasChildren && state.nodes[selected].expanded;
426+
if (shouldCollapse) {
375427
collapseNode(selected);
376428
} else {
377429
selectParentNode(true);
378430
}
379431
}
380-
e.preventDefault();
432+
381433
break;
382434
}
383435
case "Right":
384436
case "ArrowRight": {
437+
e.preventDefault();
438+
385439
const selected = selectedIdFromState(state.nodes);
440+
386441
if (selected) {
442+
const treeNode = tree.find((node) => node.id === selected);
443+
444+
if (e.altKey) {
445+
if (treeNode && treeNode.hasChildren) {
446+
expandLevel(treeNode.level);
447+
}
448+
break;
449+
}
450+
387451
expandNode(selected, true);
388452
}
389-
e.preventDefault();
390453
break;
391454
}
392455
case "Escape": {
@@ -427,6 +490,11 @@ export function useTree<TData>({
427490
expandNode,
428491
collapseNode,
429492
toggleExpandNode,
493+
expandAllBelowDepth,
494+
collapseAllBelowDepth,
495+
expandLevel,
496+
collapseLevel,
497+
toggleExpandLevel,
430498
selectFirstVisibleNode,
431499
selectLastVisibleNode,
432500
selectNextVisibleNode,

apps/webapp/app/components/primitives/TreeView/reducer.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,46 @@ type ToggleExpandNodeAction = {
9191
} & WithScrollToNode;
9292
};
9393

94+
type ExpandAllBelowDepthAction = {
95+
type: "EXPAND_ALL_BELOW_DEPTH";
96+
payload: {
97+
depth: number;
98+
tree: FlatTree<any>;
99+
};
100+
};
101+
102+
type CollapseAllBelowDepthAction = {
103+
type: "COLLAPSE_ALL_BELOW_DEPTH";
104+
payload: {
105+
depth: number;
106+
tree: FlatTree<any>;
107+
};
108+
};
109+
110+
type ExpandLevelAction = {
111+
type: "EXPAND_LEVEL";
112+
payload: {
113+
level: number;
114+
tree: FlatTree<any>;
115+
};
116+
};
117+
118+
type CollapseLevelAction = {
119+
type: "COLLAPSE_LEVEL";
120+
payload: {
121+
level: number;
122+
tree: FlatTree<any>;
123+
};
124+
};
125+
126+
type ToggleExpandLevelAction = {
127+
type: "TOGGLE_EXPAND_LEVEL";
128+
payload: {
129+
level: number;
130+
tree: FlatTree<any>;
131+
};
132+
};
133+
94134
type SelectFirstVisibleNodeAction = {
95135
type: "SELECT_FIRST_VISIBLE_NODE";
96136
payload: {
@@ -135,6 +175,11 @@ export type Action =
135175
| ExpandNodeAction
136176
| CollapseNodeAction
137177
| ToggleExpandNodeAction
178+
| ExpandAllBelowDepthAction
179+
| CollapseAllBelowDepthAction
180+
| ExpandLevelAction
181+
| CollapseLevelAction
182+
| ToggleExpandLevelAction
138183
| SelectFirstVisibleNodeAction
139184
| SelectLastVisibleNodeAction
140185
| SelectNextVisibleNodeAction
@@ -229,6 +274,109 @@ export function reducer(state: TreeState, action: Action): TreeState {
229274
});
230275
}
231276
}
277+
case "EXPAND_ALL_BELOW_DEPTH": {
278+
const nodesToExpand = action.payload.tree.filter(
279+
(n) => n.level >= action.payload.depth && n.hasChildren
280+
);
281+
282+
const newNodes = Object.fromEntries(
283+
Object.entries(state.nodes).map(([key, value]) => [
284+
key,
285+
{
286+
...value,
287+
expanded: nodesToExpand.find((n) => n.id === key) ? true : value.expanded,
288+
},
289+
])
290+
);
291+
292+
const visibleNodes = applyVisibility(action.payload.tree, newNodes);
293+
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
294+
}
295+
case "COLLAPSE_ALL_BELOW_DEPTH": {
296+
const nodesToCollapse = action.payload.tree.filter(
297+
(n) => n.level >= action.payload.depth && n.hasChildren
298+
);
299+
300+
const newNodes = Object.fromEntries(
301+
Object.entries(state.nodes).map(([key, value]) => [
302+
key,
303+
{
304+
...value,
305+
expanded: nodesToCollapse.find((n) => n.id === key) ? false : value.expanded,
306+
},
307+
])
308+
);
309+
310+
const visibleNodes = applyVisibility(action.payload.tree, newNodes);
311+
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
312+
}
313+
case "EXPAND_LEVEL": {
314+
const nodesToExpand = action.payload.tree.filter(
315+
(n) => n.level <= action.payload.level && n.hasChildren
316+
);
317+
318+
const newNodes = Object.fromEntries(
319+
Object.entries(state.nodes).map(([key, value]) => [
320+
key,
321+
{
322+
...value,
323+
expanded: nodesToExpand.find((n) => n.id === key) ? true : value.expanded,
324+
},
325+
])
326+
);
327+
328+
const visibleNodes = applyVisibility(action.payload.tree, newNodes);
329+
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
330+
}
331+
case "COLLAPSE_LEVEL": {
332+
const nodesToCollapse = action.payload.tree.filter(
333+
(n) => n.level === action.payload.level && n.hasChildren
334+
);
335+
336+
const newNodes = Object.fromEntries(
337+
Object.entries(state.nodes).map(([key, value]) => [
338+
key,
339+
{
340+
...value,
341+
expanded: nodesToCollapse.find((n) => n.id === key) ? false : value.expanded,
342+
},
343+
])
344+
);
345+
346+
const visibleNodes = applyVisibility(action.payload.tree, newNodes);
347+
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
348+
}
349+
case "TOGGLE_EXPAND_LEVEL": {
350+
//first get the first item at that level in the tree. If it is expanded, collapse all nodes at that level
351+
//if it is collapsed, expand all nodes at that level
352+
const nodesAtLevel = action.payload.tree.filter(
353+
(n) => n.level === action.payload.level && n.hasChildren
354+
);
355+
const firstNode = nodesAtLevel[0];
356+
if (!firstNode) {
357+
return state;
358+
}
359+
360+
const currentlyExpanded = state.nodes[firstNode.id]?.expanded ?? true;
361+
const currentVisible = state.nodes[firstNode.id]?.visible ?? true;
362+
if (currentlyExpanded && currentVisible) {
363+
return reducer(state, {
364+
type: "COLLAPSE_LEVEL",
365+
payload: {
366+
level: action.payload.level,
367+
tree: action.payload.tree,
368+
},
369+
});
370+
} else {
371+
return reducer(state, {
372+
type: "EXPAND_LEVEL",
373+
payload: {
374+
level: action.payload.level,
375+
tree: action.payload.tree,
376+
},
377+
});
378+
}
379+
}
232380
case "SELECT_FIRST_VISIBLE_NODE": {
233381
const node = firstVisibleNode(action.payload.tree, state.nodes);
234382
if (node) {

apps/webapp/app/components/runs/v3/TaskRunsTable.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
import { ArrowPathIcon, StopCircleIcon } from "@heroicons/react/20/solid";
12
import { StopIcon } from "@heroicons/react/24/outline";
23
import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
4+
import { useLocation } from "@remix-run/react";
5+
import { formatDuration } from "@trigger.dev/core/v3";
36
import { User } from "@trigger.dev/database";
7+
import { Button, LinkButton } from "~/components/primitives/Buttons";
8+
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
9+
import { useEnvironments } from "~/hooks/useEnvironments";
410
import { useOrganization } from "~/hooks/useOrganizations";
511
import { useProject } from "~/hooks/useProject";
612
import { RunListAppliedFilters, RunListItem } from "~/presenters/v3/RunListPresenter.server";
7-
import { docsPath, v3RunPath, v3TestPath } from "~/utils/pathBuilder";
13+
import { docsPath, v3RunSpanPath, v3TestPath } from "~/utils/pathBuilder";
814
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
915
import { DateTime } from "../../primitives/DateTime";
1016
import { Paragraph } from "../../primitives/Paragraph";
@@ -14,21 +20,14 @@ import {
1420
TableBlankRow,
1521
TableBody,
1622
TableCell,
17-
TableCellChevron,
1823
TableCellMenu,
1924
TableHeader,
2025
TableHeaderCell,
2126
TableRow,
2227
} from "../../primitives/Table";
23-
import { formatDuration } from "@trigger.dev/core/v3";
24-
import { TaskRunStatusCombo } from "./TaskRunStatus";
25-
import { useEnvironments } from "~/hooks/useEnvironments";
26-
import { Button, LinkButton } from "~/components/primitives/Buttons";
27-
import { ArrowPathIcon, StopCircleIcon } from "@heroicons/react/20/solid";
28-
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
2928
import { CancelRunDialog } from "./CancelRunDialog";
30-
import { useLocation } from "@remix-run/react";
3129
import { ReplayRunDialog } from "./ReplayRunDialog";
30+
import { TaskRunStatusCombo } from "./TaskRunStatus";
3231

3332
type RunsTableProps = {
3433
total: number;
@@ -78,7 +77,7 @@ export function TaskRunsTable({
7877
<BlankState isLoading={isLoading} filters={filters} />
7978
) : (
8079
runs.map((run) => {
81-
const path = v3RunPath(organization, project, run);
80+
const path = v3RunSpanPath(organization, project, run, { spanId: run.spanId });
8281
const usernameForEnv =
8382
currentUser.id !== run.environment.userId ? run.environment.userName : undefined;
8483
return (

0 commit comments

Comments
 (0)