Skip to content

Trace view fixes and improvements #1046

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
56775fd
Query param for span using history.replaceState is working
matt-aitken Apr 19, 2024
9142a31
Merge remote-tracking branch 'origin/main' into fixes/trace-view
matt-aitken Apr 19, 2024
beab7cc
When clicking again on a node, don’t collapse it
matt-aitken Apr 19, 2024
2626dfe
Close the span view using the same replacing of the search param
matt-aitken Apr 19, 2024
0ae1790
Conditional rendering of the resize panels was causing the tree view …
matt-aitken Apr 19, 2024
8d8606d
Live reloading moved to where the parent label is
matt-aitken Apr 19, 2024
a185329
Span action bar is now deeper
matt-aitken Apr 19, 2024
15c0211
WIP on trace view navigation changes with shortcuts
matt-aitken Apr 19, 2024
0d780e5
Shortcuts for expanding and collapsing en masse
matt-aitken Apr 19, 2024
9eebc1c
Number keys expand/collapse levels
matt-aitken Apr 19, 2024
d0cb1b3
Changed duration toggle to a shortcut key
matt-aitken Apr 19, 2024
d06108b
Option + click expands/collapse at that level
matt-aitken Apr 19, 2024
a61adcd
Option/alt left/right expands/collapse at that level
matt-aitken Apr 19, 2024
8be5b11
Merge branch 'main' into fixes/trace-view
matt-aitken Apr 19, 2024
bb91bd2
Removed unused imports
matt-aitken Apr 22, 2024
e8673a8
Link from the runs table to the specific span
matt-aitken Apr 22, 2024
1de3d3b
Latest lockfile
matt-aitken Apr 22, 2024
bf3b9fa
Sorted imports
matt-aitken Apr 22, 2024
fcd1d59
When doing a test link directly to a span
matt-aitken Apr 22, 2024
8c9eab8
Replay links to the span
matt-aitken Apr 22, 2024
07b1399
Merge remote-tracking branch 'origin/main' into fixes/trace-view
matt-aitken Apr 22, 2024
5e0b8cb
CLI log links go directly to a span
matt-aitken Apr 22, 2024
2c157d2
Keyboard shortcuts are in a popover if the width is narrow
matt-aitken Apr 22, 2024
aea39d4
If holding alt only collapse level
matt-aitken Apr 22, 2024
3a550b5
Don’t expand the individual node if you’re holding alt
matt-aitken Apr 22, 2024
961fe70
Merge branch 'main' into fixes/trace-view
matt-aitken Apr 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions apps/webapp/app/components/primitives/ShortcutKey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ import { Fragment } from "react";
import { Modifier, ShortcutDefinition } from "~/hooks/useShortcutKeys";
import { cn } from "~/utils/cn";
import { useOperatingSystem } from "./OperatingSystemProvider";
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
ChevronUpIcon,
} from "@heroicons/react/20/solid";

const variants = {
export const variants = {
small:
"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",
medium:
Expand All @@ -23,7 +29,7 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps)
const isMac = platform === "mac";
let relevantShortcut = "mac" in shortcut ? (isMac ? shortcut.mac : shortcut.windows) : shortcut;
const modifiers = relevantShortcut.modifiers ?? [];
const character = keyString(relevantShortcut.key, isMac);
const character = keyString(relevantShortcut.key, isMac, variant);

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

function keyString(key: String, isMac: boolean) {
function keyString(key: String, isMac: boolean, size: "small" | "medium") {
key = key.toLowerCase();

const className = size === "small" ? "w-2.5 h-4" : "w-3 h-5";

switch (key) {
case "enter":
return isMac ? "↵" : key;
case "arrowdown":
return <ChevronDownIcon className={className} />;
case "arrowup":
return <ChevronUpIcon className={className} />;
case "arrowleft":
return <ChevronLeftIcon className={className} />;
case "arrowright":
return <ChevronRightIcon className={className} />;
default:
return key;
}
Expand Down
76 changes: 72 additions & 4 deletions apps/webapp/app/components/primitives/TreeView/TreeView.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { VirtualItem, Virtualizer, useVirtualizer } from "@tanstack/react-virtual";
import { motion } from "framer-motion";
import { MutableRefObject, RefObject, useCallback, useEffect, useReducer, useRef } from "react";
import { UnmountClosed } from "react-collapse";
import { cn } from "~/utils/cn";
import { NodeState, NodesState, reducer } from "./reducer";
import { applyFilterToState, concreteStateFromInput, selectedIdFromState } from "./utils";
import { motion } from "framer-motion";

export type TreeViewProps<TData> = {
tree: FlatTree<TData>;
Expand Down Expand Up @@ -165,6 +165,11 @@ export type UseTreeStateOutput = {
expandNode: (id: string, scrollToNode?: boolean) => void;
collapseNode: (id: string) => void;
toggleExpandNode: (id: string, scrollToNode?: boolean) => void;
expandAllBelowDepth: (depth: number) => void;
collapseAllBelowDepth: (depth: number) => void;
expandLevel: (level: number) => void;
collapseLevel: (level: number) => void;
toggleExpandLevel: (level: number) => void;
selectFirstVisibleNode: (scrollToNode?: boolean) => void;
selectLastVisibleNode: (scrollToNode?: boolean) => void;
selectNextVisibleNode: (scrollToNode?: boolean) => void;
Expand Down Expand Up @@ -333,6 +338,41 @@ export function useTree<TData>({
[state]
);

const expandAllBelowDepth = useCallback(
(depth: number) => {
dispatch({ type: "EXPAND_ALL_BELOW_DEPTH", payload: { tree, depth } });
},
[state]
);

const collapseAllBelowDepth = useCallback(
(depth: number) => {
dispatch({ type: "COLLAPSE_ALL_BELOW_DEPTH", payload: { tree, depth } });
},
[state]
);

const expandLevel = useCallback(
(level: number) => {
dispatch({ type: "EXPAND_LEVEL", payload: { tree, level } });
},
[state]
);

const collapseLevel = useCallback(
(level: number) => {
dispatch({ type: "COLLAPSE_LEVEL", payload: { tree, level } });
},
[state]
);

const toggleExpandLevel = useCallback(
(level: number) => {
dispatch({ type: "TOGGLE_EXPAND_LEVEL", payload: { tree, level } });
},
[state]
);

const getTreeProps = useCallback(() => {
return {
role: "tree",
Expand Down Expand Up @@ -368,25 +408,48 @@ export function useTree<TData>({
}
case "Left":
case "ArrowLeft": {
e.preventDefault();

const selected = selectedIdFromState(state.nodes);
if (selected) {
const treeNode = tree.find((node) => node.id === selected);
if (treeNode && treeNode.hasChildren && state.nodes[selected].expanded) {

if (e.altKey) {
if (treeNode && treeNode.hasChildren) {
collapseLevel(treeNode.level);
}
break;
}

const shouldCollapse =
treeNode && treeNode.hasChildren && state.nodes[selected].expanded;
if (shouldCollapse) {
collapseNode(selected);
} else {
selectParentNode(true);
}
}
e.preventDefault();

break;
}
case "Right":
case "ArrowRight": {
e.preventDefault();

const selected = selectedIdFromState(state.nodes);

if (selected) {
const treeNode = tree.find((node) => node.id === selected);

if (e.altKey) {
if (treeNode && treeNode.hasChildren) {
expandLevel(treeNode.level);
}
break;
}

expandNode(selected, true);
}
e.preventDefault();
break;
}
case "Escape": {
Expand Down Expand Up @@ -427,6 +490,11 @@ export function useTree<TData>({
expandNode,
collapseNode,
toggleExpandNode,
expandAllBelowDepth,
collapseAllBelowDepth,
expandLevel,
collapseLevel,
toggleExpandLevel,
selectFirstVisibleNode,
selectLastVisibleNode,
selectNextVisibleNode,
Expand Down
148 changes: 148 additions & 0 deletions apps/webapp/app/components/primitives/TreeView/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,46 @@ type ToggleExpandNodeAction = {
} & WithScrollToNode;
};

type ExpandAllBelowDepthAction = {
type: "EXPAND_ALL_BELOW_DEPTH";
payload: {
depth: number;
tree: FlatTree<any>;
};
};

type CollapseAllBelowDepthAction = {
type: "COLLAPSE_ALL_BELOW_DEPTH";
payload: {
depth: number;
tree: FlatTree<any>;
};
};

type ExpandLevelAction = {
type: "EXPAND_LEVEL";
payload: {
level: number;
tree: FlatTree<any>;
};
};

type CollapseLevelAction = {
type: "COLLAPSE_LEVEL";
payload: {
level: number;
tree: FlatTree<any>;
};
};

type ToggleExpandLevelAction = {
type: "TOGGLE_EXPAND_LEVEL";
payload: {
level: number;
tree: FlatTree<any>;
};
};

type SelectFirstVisibleNodeAction = {
type: "SELECT_FIRST_VISIBLE_NODE";
payload: {
Expand Down Expand Up @@ -135,6 +175,11 @@ export type Action =
| ExpandNodeAction
| CollapseNodeAction
| ToggleExpandNodeAction
| ExpandAllBelowDepthAction
| CollapseAllBelowDepthAction
| ExpandLevelAction
| CollapseLevelAction
| ToggleExpandLevelAction
| SelectFirstVisibleNodeAction
| SelectLastVisibleNodeAction
| SelectNextVisibleNodeAction
Expand Down Expand Up @@ -229,6 +274,109 @@ export function reducer(state: TreeState, action: Action): TreeState {
});
}
}
case "EXPAND_ALL_BELOW_DEPTH": {
const nodesToExpand = action.payload.tree.filter(
(n) => n.level >= action.payload.depth && n.hasChildren
);

const newNodes = Object.fromEntries(
Object.entries(state.nodes).map(([key, value]) => [
key,
{
...value,
expanded: nodesToExpand.find((n) => n.id === key) ? true : value.expanded,
},
])
);

const visibleNodes = applyVisibility(action.payload.tree, newNodes);
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
}
case "COLLAPSE_ALL_BELOW_DEPTH": {
const nodesToCollapse = action.payload.tree.filter(
(n) => n.level >= action.payload.depth && n.hasChildren
);

const newNodes = Object.fromEntries(
Object.entries(state.nodes).map(([key, value]) => [
key,
{
...value,
expanded: nodesToCollapse.find((n) => n.id === key) ? false : value.expanded,
},
])
);

const visibleNodes = applyVisibility(action.payload.tree, newNodes);
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
}
case "EXPAND_LEVEL": {
const nodesToExpand = action.payload.tree.filter(
(n) => n.level <= action.payload.level && n.hasChildren
);

const newNodes = Object.fromEntries(
Object.entries(state.nodes).map(([key, value]) => [
key,
{
...value,
expanded: nodesToExpand.find((n) => n.id === key) ? true : value.expanded,
},
])
);

const visibleNodes = applyVisibility(action.payload.tree, newNodes);
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
}
case "COLLAPSE_LEVEL": {
const nodesToCollapse = action.payload.tree.filter(
(n) => n.level === action.payload.level && n.hasChildren
);

const newNodes = Object.fromEntries(
Object.entries(state.nodes).map(([key, value]) => [
key,
{
...value,
expanded: nodesToCollapse.find((n) => n.id === key) ? false : value.expanded,
},
])
);

const visibleNodes = applyVisibility(action.payload.tree, newNodes);
return { nodes: visibleNodes, changes: generateChanges(state.nodes, visibleNodes) };
}
case "TOGGLE_EXPAND_LEVEL": {
//first get the first item at that level in the tree. If it is expanded, collapse all nodes at that level
//if it is collapsed, expand all nodes at that level
const nodesAtLevel = action.payload.tree.filter(
(n) => n.level === action.payload.level && n.hasChildren
);
const firstNode = nodesAtLevel[0];
if (!firstNode) {
return state;
}

const currentlyExpanded = state.nodes[firstNode.id]?.expanded ?? true;
const currentVisible = state.nodes[firstNode.id]?.visible ?? true;
if (currentlyExpanded && currentVisible) {
return reducer(state, {
type: "COLLAPSE_LEVEL",
payload: {
level: action.payload.level,
tree: action.payload.tree,
},
});
} else {
return reducer(state, {
type: "EXPAND_LEVEL",
payload: {
level: action.payload.level,
tree: action.payload.tree,
},
});
}
}
case "SELECT_FIRST_VISIBLE_NODE": {
const node = firstVisibleNode(action.payload.tree, state.nodes);
if (node) {
Expand Down
19 changes: 9 additions & 10 deletions apps/webapp/app/components/runs/v3/TaskRunsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { ArrowPathIcon, StopCircleIcon } from "@heroicons/react/20/solid";
import { StopIcon } from "@heroicons/react/24/outline";
import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
import { useLocation } from "@remix-run/react";
import { formatDuration } from "@trigger.dev/core/v3";
import { User } from "@trigger.dev/database";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
import { useEnvironments } from "~/hooks/useEnvironments";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { RunListAppliedFilters, RunListItem } from "~/presenters/v3/RunListPresenter.server";
import { docsPath, v3RunPath, v3TestPath } from "~/utils/pathBuilder";
import { docsPath, v3RunSpanPath, v3TestPath } from "~/utils/pathBuilder";
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
import { DateTime } from "../../primitives/DateTime";
import { Paragraph } from "../../primitives/Paragraph";
Expand All @@ -14,21 +20,14 @@ import {
TableBlankRow,
TableBody,
TableCell,
TableCellChevron,
TableCellMenu,
TableHeader,
TableHeaderCell,
TableRow,
} from "../../primitives/Table";
import { formatDuration } from "@trigger.dev/core/v3";
import { TaskRunStatusCombo } from "./TaskRunStatus";
import { useEnvironments } from "~/hooks/useEnvironments";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { ArrowPathIcon, StopCircleIcon } from "@heroicons/react/20/solid";
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
import { CancelRunDialog } from "./CancelRunDialog";
import { useLocation } from "@remix-run/react";
import { ReplayRunDialog } from "./ReplayRunDialog";
import { TaskRunStatusCombo } from "./TaskRunStatus";

type RunsTableProps = {
total: number;
Expand Down Expand Up @@ -78,7 +77,7 @@ export function TaskRunsTable({
<BlankState isLoading={isLoading} filters={filters} />
) : (
runs.map((run) => {
const path = v3RunPath(organization, project, run);
const path = v3RunSpanPath(organization, project, run, { spanId: run.spanId });
const usernameForEnv =
currentUser.id !== run.environment.userId ? run.environment.userName : undefined;
return (
Expand Down
Loading