The run timeline updates on the client if there's no new data (#955)
* Use “@v3” instead of “@latest” for the npx commands for v3 * Split the Timeline into a component so it can live refresh without re-rendering everything * Timeline now live refreshes on the client every 500ms when run is executing * The timeline bars now animate their position/width when it changes * Export some more types from TreeView
This commit is contained in:
@@ -131,6 +131,7 @@ export function TriggerDevStep({ extra }: { extra?: string }) {
|
||||
}
|
||||
|
||||
// Trigger.dev version 3 setup commands
|
||||
const v3PackageTag = "v3";
|
||||
|
||||
export function InitCommandV3() {
|
||||
const project = useProject();
|
||||
@@ -147,7 +148,7 @@ export function InitCommandV3() {
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`npx trigger.dev@latest init -p ${projectRef}`}
|
||||
value={`npx trigger.dev@${v3PackageTag} init -p ${projectRef}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
@@ -155,7 +156,7 @@ export function InitCommandV3() {
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`pnpm dlx trigger.dev@latest init -p ${projectRef}`}
|
||||
value={`pnpm dlx trigger.dev@${v3PackageTag} init -p ${projectRef}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
@@ -163,7 +164,7 @@ export function InitCommandV3() {
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`yarn dlx trigger.dev@latest init -p ${projectRef}`}
|
||||
value={`yarn dlx trigger.dev@${v3PackageTag} init -p ${projectRef}`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
@@ -183,7 +184,7 @@ export function TriggerDevStepV3() {
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`npx trigger.dev@latest dev`}
|
||||
value={`npx trigger.dev@${v3PackageTag} dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"pnpm"}>
|
||||
@@ -191,7 +192,7 @@ export function TriggerDevStepV3() {
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`pnpm dlx trigger.dev@latest dev`}
|
||||
value={`pnpm dlx trigger.dev@${v3PackageTag} dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
<ClientTabsContent value={"yarn"}>
|
||||
@@ -199,7 +200,7 @@ export function TriggerDevStepV3() {
|
||||
variant="primary/medium"
|
||||
iconButton
|
||||
className="mb-4"
|
||||
value={`yarn dlx trigger.dev@latest dev`}
|
||||
value={`yarn dlx trigger.dev@${v3PackageTag} dev`}
|
||||
/>
|
||||
</ClientTabsContent>
|
||||
</ClientTabs>
|
||||
|
||||
@@ -23,6 +23,9 @@ export type TreeViewProps<TData> = {
|
||||
onScroll?: (scrollTop: number) => void;
|
||||
} & Pick<UseTreeStateOutput, "getTreeProps" | "getNodeProps">;
|
||||
|
||||
export type GetTreePropsFn = UseTreeStateOutput["getTreeProps"];
|
||||
export type GetNodePropsFn = UseTreeStateOutput["getNodeProps"];
|
||||
|
||||
export function TreeView<TData>({
|
||||
tree,
|
||||
renderNode,
|
||||
@@ -144,7 +147,7 @@ type HTMLAttributes = Omit<
|
||||
"onAnimationStart" | "onDragStart" | "onDragEnd" | "onDrag"
|
||||
>;
|
||||
|
||||
type UseTreeStateOutput = {
|
||||
export type UseTreeStateOutput = {
|
||||
selected: string | undefined;
|
||||
nodes: NodesState;
|
||||
virtualizer: Virtualizer<HTMLElement, Element>;
|
||||
|
||||
@@ -79,25 +79,22 @@ export class RunPresenter {
|
||||
n.data.startTime.getTime() - treeRootStartTimeMs
|
||||
);
|
||||
totalDuration = Math.max(totalDuration, offset + n.data.duration);
|
||||
return { ...n, data: { ...n.data, offset, isRoot: n.id === traceSummary.rootSpan.id } };
|
||||
return {
|
||||
...n,
|
||||
data: {
|
||||
...n.data,
|
||||
//set partial nodes to null duration
|
||||
duration: n.data.isPartial ? null : n.data.duration,
|
||||
offset,
|
||||
isRoot: n.id === traceSummary.rootSpan.id,
|
||||
},
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
//if any elements are partial we want the total duration to represent all of the time until now
|
||||
totalDuration = events.some((e) => e.data.isPartial)
|
||||
? millisecondsToNanoseconds(Date.now() - treeRootStartTimeMs)
|
||||
: totalDuration;
|
||||
|
||||
//total duration should be a minimum of 1ms
|
||||
totalDuration = Math.max(totalDuration, millisecondsToNanoseconds(1));
|
||||
|
||||
//we need to adjust any partial nodes so they run the full duration
|
||||
for (const event of events) {
|
||||
if (event.data.isPartial) {
|
||||
event.data.duration = totalDuration - event.data.offset;
|
||||
}
|
||||
}
|
||||
|
||||
let rootSpanStatus: "executing" | "completed" | "failed" = "executing";
|
||||
if (events[0]) {
|
||||
if (events[0].data.isError) {
|
||||
@@ -123,6 +120,7 @@ export class RunPresenter {
|
||||
parentRunFriendlyId:
|
||||
tree?.id === traceSummary.rootSpan.id ? undefined : traceSummary.rootSpan.runId,
|
||||
duration: totalDuration,
|
||||
rootStartedAt: tree?.data.startTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+279
-184
@@ -4,9 +4,16 @@ import {
|
||||
MagnifyingGlassMinusIcon,
|
||||
MagnifyingGlassPlusIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { Time } from "@internationalized/date";
|
||||
import { Link, Outlet, useNavigate, useParams, useRevalidator } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { formatDurationMilliseconds, nanosecondsToMilliseconds } from "@trigger.dev/core/v3";
|
||||
import { Virtualizer } from "@tanstack/react-virtual";
|
||||
import {
|
||||
formatDurationMilliseconds,
|
||||
millisecondsToNanoseconds,
|
||||
nanosecondsToMilliseconds,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { ShowParentIcon, ShowParentIconSelected } from "~/assets/icons/ShowParentIcon";
|
||||
@@ -26,7 +33,15 @@ import {
|
||||
import { Slider } from "~/components/primitives/Slider";
|
||||
import { Switch } from "~/components/primitives/Switch";
|
||||
import * as Timeline from "~/components/primitives/Timeline";
|
||||
import { TreeView, useTree } from "~/components/primitives/TreeView/TreeView";
|
||||
import {
|
||||
GetNodePropsFn,
|
||||
GetTreePropsFn,
|
||||
TreeView,
|
||||
TreeViewProps,
|
||||
UseTreeStateOutput,
|
||||
useTree,
|
||||
} from "~/components/primitives/TreeView/TreeView";
|
||||
import { NodesState } from "~/components/primitives/TreeView/reducer";
|
||||
import { RunIcon } from "~/components/runs/v3/RunIcon";
|
||||
import { SpanTitle, eventBackgroundClassName } from "~/components/runs/v3/SpanTitle";
|
||||
import { TaskRunStatusIcon, runStatusClassNameColor } from "~/components/runs/v3/TaskRunStatus";
|
||||
@@ -55,7 +70,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const { projectParam, organizationSlug, runParam } = v3RunParamsSchema.parse(params);
|
||||
|
||||
const presenter = new RunPresenter();
|
||||
const { run, events, parentRunFriendlyId, duration, rootSpanStatus } = await presenter.call({
|
||||
const result = await presenter.call({
|
||||
userId,
|
||||
organizationSlug,
|
||||
projectSlug: projectParam,
|
||||
@@ -66,12 +81,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const resizeSettings = await getResizableRunSettings(request);
|
||||
|
||||
return typedjson({
|
||||
run,
|
||||
events,
|
||||
parentRunFriendlyId,
|
||||
...result,
|
||||
resizeSettings,
|
||||
duration,
|
||||
rootSpanStatus,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -82,8 +93,15 @@ function getSpanId(path: string): string | undefined {
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const { run, events, parentRunFriendlyId, resizeSettings, duration, rootSpanStatus } =
|
||||
useTypedLoaderData<typeof loader>();
|
||||
const {
|
||||
run,
|
||||
events,
|
||||
parentRunFriendlyId,
|
||||
resizeSettings,
|
||||
duration,
|
||||
rootSpanStatus,
|
||||
rootStartedAt,
|
||||
} = useTypedLoaderData<typeof loader>();
|
||||
const navigate = useNavigate();
|
||||
const organization = useOrganization();
|
||||
const pathName = usePathName();
|
||||
@@ -142,6 +160,7 @@ export default function Page() {
|
||||
}}
|
||||
totalDuration={duration}
|
||||
rootSpanStatus={rootSpanStatus}
|
||||
rootStartedAt={rootStartedAt}
|
||||
/>
|
||||
) : (
|
||||
<ResizablePanelGroup
|
||||
@@ -169,6 +188,7 @@ export default function Page() {
|
||||
}}
|
||||
totalDuration={duration}
|
||||
rootSpanStatus={rootSpanStatus}
|
||||
rootStartedAt={rootStartedAt}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
@@ -183,7 +203,15 @@ export default function Page() {
|
||||
);
|
||||
}
|
||||
|
||||
const tickCount = 5;
|
||||
type TasksTreeViewProps = {
|
||||
events: RunEvent[];
|
||||
selectedId?: string;
|
||||
parentRunFriendlyId?: string;
|
||||
onSelectedIdChanged: (selectedId: string | undefined) => void;
|
||||
totalDuration: number;
|
||||
rootSpanStatus: "executing" | "completed" | "failed";
|
||||
rootStartedAt: Date | undefined;
|
||||
};
|
||||
|
||||
function TasksTreeView({
|
||||
events,
|
||||
@@ -192,14 +220,8 @@ function TasksTreeView({
|
||||
onSelectedIdChanged,
|
||||
totalDuration,
|
||||
rootSpanStatus,
|
||||
}: {
|
||||
events: RunEvent[];
|
||||
selectedId?: string;
|
||||
parentRunFriendlyId?: string;
|
||||
onSelectedIdChanged: (selectedId: string | undefined) => void;
|
||||
totalDuration: number;
|
||||
rootSpanStatus: "executing" | "completed" | "failed";
|
||||
}) {
|
||||
rootStartedAt,
|
||||
}: TasksTreeViewProps) {
|
||||
const [filterText, setFilterText] = useState("");
|
||||
const [errorsOnly, setErrorsOnly] = useState(false);
|
||||
const [showDurations, setShowDurations] = useState(false);
|
||||
@@ -207,8 +229,6 @@ function TasksTreeView({
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const treeScrollRef = useRef<HTMLDivElement>(null);
|
||||
const timelineScrollRef = useRef<HTMLDivElement>(null);
|
||||
const timelineContainerRef = useRef<HTMLDivElement>(null);
|
||||
const initialTimelineDimensions = useInitialDimensions(timelineContainerRef);
|
||||
|
||||
const {
|
||||
nodes,
|
||||
@@ -238,9 +258,6 @@ function TasksTreeView({
|
||||
},
|
||||
});
|
||||
|
||||
const minTimelineWidth = initialTimelineDimensions?.width ?? 300;
|
||||
const maxTimelineWidth = minTimelineWidth * 10;
|
||||
|
||||
return (
|
||||
<div className="grid h-full grid-rows-[2.5rem_1fr] overflow-hidden">
|
||||
<div className="mx-3 flex items-center justify-between gap-2 border-b border-grid-dimmed">
|
||||
@@ -378,170 +395,247 @@ function TasksTreeView({
|
||||
<ResizableHandle withHandle />
|
||||
{/* Timeline */}
|
||||
<ResizablePanel order={2} minSize={20} defaultSize={50}>
|
||||
<div
|
||||
className="h-full overflow-x-auto overflow-y-hidden scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
ref={timelineContainerRef}
|
||||
>
|
||||
<Timeline.Root
|
||||
durationMs={nanosecondsToMilliseconds(totalDuration * 1.05)}
|
||||
scale={scale}
|
||||
className="h-full overflow-hidden"
|
||||
minWidth={minTimelineWidth}
|
||||
maxWidth={maxTimelineWidth}
|
||||
>
|
||||
{/* Follows the cursor */}
|
||||
<CurrentTimeIndicator totalDuration={totalDuration} />
|
||||
<TimelineView
|
||||
totalDuration={totalDuration}
|
||||
scale={scale}
|
||||
events={events}
|
||||
rootSpanStatus={rootSpanStatus}
|
||||
rootStartedAt={rootStartedAt}
|
||||
parentRef={parentRef}
|
||||
timelineScrollRef={timelineScrollRef}
|
||||
nodes={nodes}
|
||||
getNodeProps={getNodeProps}
|
||||
getTreeProps={getTreeProps}
|
||||
showDurations={showDurations}
|
||||
treeScrollRef={treeScrollRef}
|
||||
virtualizer={virtualizer}
|
||||
toggleNodeSelection={toggleNodeSelection}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<Timeline.Row className="grid h-full grid-rows-[2rem_1fr]">
|
||||
{/* The duration labels */}
|
||||
<Timeline.Row>
|
||||
<Timeline.Row className="h-6">
|
||||
<Timeline.EquallyDistribute count={tickCount}>
|
||||
{(ms: number, index: number) => {
|
||||
if (index === tickCount - 1) return null;
|
||||
return (
|
||||
<Timeline.Point
|
||||
ms={ms}
|
||||
className={"relative bottom-[2px] text-xxs text-text-dimmed"}
|
||||
>
|
||||
{(ms) => (
|
||||
<div
|
||||
className={cn(
|
||||
"whitespace-nowrap",
|
||||
index === 0
|
||||
? "ml-1"
|
||||
: index === tickCount - 1
|
||||
? "-ml-1 -translate-x-full"
|
||||
: "-translate-x-1/2"
|
||||
)}
|
||||
>
|
||||
{formatDurationMilliseconds(ms, {
|
||||
style: "short",
|
||||
maxDecimalPoints: ms < 1000 ? 0 : 1,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Timeline.Point>
|
||||
);
|
||||
}}
|
||||
</Timeline.EquallyDistribute>
|
||||
{rootSpanStatus !== "executing" && (
|
||||
<Timeline.Point
|
||||
ms={nanosecondsToMilliseconds(totalDuration)}
|
||||
className={cn(
|
||||
"relative bottom-[2px] text-xxs",
|
||||
rootSpanStatus === "completed" ? "text-success" : "text-error"
|
||||
)}
|
||||
>
|
||||
type TimelineViewProps = Pick<
|
||||
TasksTreeViewProps,
|
||||
"totalDuration" | "rootSpanStatus" | "events" | "rootStartedAt"
|
||||
> & {
|
||||
scale: number;
|
||||
parentRef: React.RefObject<HTMLDivElement>;
|
||||
timelineScrollRef: React.RefObject<HTMLDivElement>;
|
||||
virtualizer: Virtualizer<HTMLElement, Element>;
|
||||
nodes: NodesState;
|
||||
getNodeProps: UseTreeStateOutput["getNodeProps"];
|
||||
getTreeProps: UseTreeStateOutput["getTreeProps"];
|
||||
toggleNodeSelection: UseTreeStateOutput["toggleNodeSelection"];
|
||||
showDurations: boolean;
|
||||
treeScrollRef: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
const tickCount = 5;
|
||||
|
||||
function TimelineView({
|
||||
totalDuration,
|
||||
scale,
|
||||
rootSpanStatus,
|
||||
rootStartedAt,
|
||||
parentRef,
|
||||
timelineScrollRef,
|
||||
virtualizer,
|
||||
events,
|
||||
nodes,
|
||||
getNodeProps,
|
||||
getTreeProps,
|
||||
toggleNodeSelection,
|
||||
showDurations,
|
||||
treeScrollRef,
|
||||
}: TimelineViewProps) {
|
||||
const timelineContainerRef = useRef<HTMLDivElement>(null);
|
||||
const initialTimelineDimensions = useInitialDimensions(timelineContainerRef);
|
||||
const minTimelineWidth = initialTimelineDimensions?.width ?? 300;
|
||||
const maxTimelineWidth = minTimelineWidth * 10;
|
||||
|
||||
//we want to live-update the duration if the root span is still executing
|
||||
const [duration, setDuration] = useState(totalDuration);
|
||||
useEffect(() => {
|
||||
if (rootSpanStatus !== "executing" || !rootStartedAt) {
|
||||
setDuration(totalDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setDuration(millisecondsToNanoseconds(Date.now() - rootStartedAt.getTime()));
|
||||
}, 500);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [totalDuration, rootSpanStatus]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full overflow-x-auto overflow-y-hidden scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
ref={timelineContainerRef}
|
||||
>
|
||||
<Timeline.Root
|
||||
durationMs={nanosecondsToMilliseconds(duration * 1.05)}
|
||||
scale={scale}
|
||||
className="h-full overflow-hidden"
|
||||
minWidth={minTimelineWidth}
|
||||
maxWidth={maxTimelineWidth}
|
||||
>
|
||||
{/* Follows the cursor */}
|
||||
<CurrentTimeIndicator totalDuration={duration} />
|
||||
|
||||
<Timeline.Row className="grid h-full grid-rows-[2rem_1fr]">
|
||||
{/* The duration labels */}
|
||||
<Timeline.Row>
|
||||
<Timeline.Row className="h-6">
|
||||
<Timeline.EquallyDistribute count={tickCount}>
|
||||
{(ms: number, index: number) => {
|
||||
if (index === tickCount - 1) return null;
|
||||
return (
|
||||
<Timeline.Point
|
||||
ms={ms}
|
||||
className={"relative bottom-[2px] text-xxs text-text-dimmed"}
|
||||
>
|
||||
{(ms) => (
|
||||
<div
|
||||
className={cn(
|
||||
"whitespace-nowrap",
|
||||
index === 0
|
||||
? "ml-1"
|
||||
: index === tickCount - 1
|
||||
? "-ml-1 -translate-x-full"
|
||||
: "-translate-x-1/2"
|
||||
)}
|
||||
>
|
||||
{formatDurationMilliseconds(ms, {
|
||||
style: "short",
|
||||
maxDecimalPoints: ms < 1000 ? 0 : 1,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Timeline.Point>
|
||||
);
|
||||
}}
|
||||
</Timeline.EquallyDistribute>
|
||||
{rootSpanStatus !== "executing" && (
|
||||
<Timeline.Point
|
||||
ms={nanosecondsToMilliseconds(duration)}
|
||||
className={cn(
|
||||
"relative bottom-[2px] text-xxs",
|
||||
rootSpanStatus === "completed" ? "text-success" : "text-error"
|
||||
)}
|
||||
>
|
||||
{(ms) => (
|
||||
<div className={cn("-translate-x-1/2 whitespace-nowrap")}>
|
||||
{formatDurationMilliseconds(ms, {
|
||||
style: "short",
|
||||
maxDecimalPoints: ms < 1000 ? 0 : 1,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Timeline.Point>
|
||||
)}
|
||||
</Timeline.Row>
|
||||
<Timeline.Row className="h-2">
|
||||
<Timeline.EquallyDistribute count={tickCount}>
|
||||
{(ms: number, index: number) => {
|
||||
if (index === 0 || index === tickCount - 1) return null;
|
||||
return (
|
||||
<Timeline.Point ms={ms} className={"h-full border-r border-grid-dimmed"} />
|
||||
);
|
||||
}}
|
||||
</Timeline.EquallyDistribute>
|
||||
<Timeline.Point
|
||||
ms={nanosecondsToMilliseconds(duration)}
|
||||
className={cn(
|
||||
"h-full border-r",
|
||||
rootSpanStatus === "completed" ? "border-success/30" : "border-error/30"
|
||||
)}
|
||||
/>
|
||||
</Timeline.Row>
|
||||
</Timeline.Row>
|
||||
{/* Main timeline body */}
|
||||
<Timeline.Row className="overflow-hidden">
|
||||
{/* The vertical tick lines */}
|
||||
<Timeline.EquallyDistribute count={tickCount}>
|
||||
{(ms: number, index: number) => {
|
||||
if (index === 0) return null;
|
||||
return <Timeline.Point ms={ms} className={"h-full border-r border-grid-dimmed"} />;
|
||||
}}
|
||||
</Timeline.EquallyDistribute>
|
||||
{/* The completed line */}
|
||||
{rootSpanStatus !== "executing" && (
|
||||
<Timeline.Point
|
||||
ms={nanosecondsToMilliseconds(duration)}
|
||||
className={cn(
|
||||
"h-full border-r",
|
||||
rootSpanStatus === "completed" ? "border-success/30" : "border-error/30"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<TreeView
|
||||
parentRef={parentRef}
|
||||
scrollRef={timelineScrollRef}
|
||||
virtualizer={virtualizer}
|
||||
tree={events}
|
||||
nodes={nodes}
|
||||
getNodeProps={getNodeProps}
|
||||
getTreeProps={getTreeProps}
|
||||
parentClassName="h-full scrollbar-hide"
|
||||
renderNode={({ node, state, index, virtualizer, virtualItem }) => {
|
||||
return (
|
||||
<Timeline.Row
|
||||
key={index}
|
||||
className={cn(
|
||||
"group flex h-8 items-center",
|
||||
state.selected
|
||||
? "bg-grid-dimmed hover:bg-grid-bright"
|
||||
: "bg-transparent hover:bg-grid-dimmed"
|
||||
)}
|
||||
// onMouseOver={() => console.log(`hover ${index}`)}
|
||||
onClick={(e) => {
|
||||
toggleNodeSelection(node.id);
|
||||
}}
|
||||
>
|
||||
{node.data.level === "TRACE" ? (
|
||||
<SpanWithDuration
|
||||
showDuration={state.selected ? true : showDurations}
|
||||
startMs={nanosecondsToMilliseconds(node.data.offset)}
|
||||
durationMs={
|
||||
node.data.duration
|
||||
? nanosecondsToMilliseconds(node.data.duration)
|
||||
: nanosecondsToMilliseconds(duration - node.data.offset)
|
||||
}
|
||||
node={node}
|
||||
/>
|
||||
) : (
|
||||
<Timeline.Point ms={nanosecondsToMilliseconds(node.data.offset)}>
|
||||
{(ms) => (
|
||||
<div className={cn("-translate-x-1/2 whitespace-nowrap")}>
|
||||
{formatDurationMilliseconds(ms, {
|
||||
style: "short",
|
||||
maxDecimalPoints: ms < 1000 ? 0 : 1,
|
||||
})}
|
||||
</div>
|
||||
<motion.div
|
||||
className={cn(
|
||||
"-ml-1 h-3 w-3 rounded-full border-2 border-background-bright",
|
||||
eventBackgroundClassName(node.data)
|
||||
)}
|
||||
layoutId={node.id}
|
||||
/>
|
||||
)}
|
||||
</Timeline.Point>
|
||||
)}
|
||||
</Timeline.Row>
|
||||
<Timeline.Row className="h-2">
|
||||
<Timeline.EquallyDistribute count={tickCount}>
|
||||
{(ms: number, index: number) => {
|
||||
if (index === 0 || index === tickCount - 1) return null;
|
||||
return (
|
||||
<Timeline.Point
|
||||
ms={ms}
|
||||
className={"h-full border-r border-grid-dimmed"}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Timeline.EquallyDistribute>
|
||||
<Timeline.Point
|
||||
ms={nanosecondsToMilliseconds(totalDuration)}
|
||||
className={cn(
|
||||
"h-full border-r",
|
||||
rootSpanStatus === "completed" ? "border-success/30" : "border-error/30"
|
||||
)}
|
||||
/>
|
||||
</Timeline.Row>
|
||||
</Timeline.Row>
|
||||
{/* Main timeline body */}
|
||||
<Timeline.Row className="overflow-hidden">
|
||||
{/* The vertical tick lines */}
|
||||
<Timeline.EquallyDistribute count={tickCount}>
|
||||
{(ms: number, index: number) => {
|
||||
if (index === 0) return null;
|
||||
return (
|
||||
<Timeline.Point ms={ms} className={"h-full border-r border-grid-dimmed"} />
|
||||
);
|
||||
}}
|
||||
</Timeline.EquallyDistribute>
|
||||
{/* The completed line */}
|
||||
{rootSpanStatus !== "executing" && (
|
||||
<Timeline.Point
|
||||
ms={nanosecondsToMilliseconds(totalDuration)}
|
||||
className={cn(
|
||||
"h-full border-r",
|
||||
rootSpanStatus === "completed" ? "border-success/30" : "border-error/30"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<TreeView
|
||||
parentRef={parentRef}
|
||||
scrollRef={timelineScrollRef}
|
||||
virtualizer={virtualizer}
|
||||
tree={events}
|
||||
nodes={nodes}
|
||||
getNodeProps={getNodeProps}
|
||||
getTreeProps={getTreeProps}
|
||||
parentClassName="h-full scrollbar-hide"
|
||||
renderNode={({ node, state, index, virtualizer, virtualItem }) => {
|
||||
return (
|
||||
<Timeline.Row
|
||||
key={index}
|
||||
className={cn(
|
||||
"group flex h-8 items-center",
|
||||
state.selected
|
||||
? "bg-grid-dimmed hover:bg-grid-bright"
|
||||
: "bg-transparent hover:bg-grid-dimmed"
|
||||
)}
|
||||
// onMouseOver={() => console.log(`hover ${index}`)}
|
||||
onClick={(e) => {
|
||||
toggleNodeSelection(node.id);
|
||||
}}
|
||||
>
|
||||
{node.data.level === "TRACE" ? (
|
||||
<SpanWithDuration
|
||||
showDuration={state.selected ? true : showDurations}
|
||||
startMs={nanosecondsToMilliseconds(node.data.offset)}
|
||||
durationMs={nanosecondsToMilliseconds(node.data.duration)}
|
||||
node={node}
|
||||
/>
|
||||
) : (
|
||||
<Timeline.Point
|
||||
ms={nanosecondsToMilliseconds(node.data.offset)}
|
||||
className={cn(
|
||||
"-ml-1 h-3 w-3 rounded-full border-2 border-background-bright",
|
||||
eventBackgroundClassName(node.data)
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Timeline.Row>
|
||||
);
|
||||
}}
|
||||
onScroll={(scrollTop) => {
|
||||
//sync the scroll to the tree
|
||||
if (treeScrollRef.current && treeScrollRef.current.scrollTop !== scrollTop) {
|
||||
treeScrollRef.current.scrollTop = scrollTop;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Timeline.Row>
|
||||
</Timeline.Row>
|
||||
</Timeline.Root>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
);
|
||||
}}
|
||||
onScroll={(scrollTop) => {
|
||||
//sync the scroll to the tree
|
||||
if (treeScrollRef.current && treeScrollRef.current.scrollTop !== scrollTop) {
|
||||
treeScrollRef.current.scrollTop = scrollTop;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Timeline.Row>
|
||||
</Timeline.Row>
|
||||
</Timeline.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -659,11 +753,12 @@ function SpanWithDuration({
|
||||
}: Timeline.SpanProps & { node: RunEvent; showDuration: boolean }) {
|
||||
return (
|
||||
<Timeline.Span {...props}>
|
||||
<div
|
||||
<motion.div
|
||||
className={cn(
|
||||
"relative flex h-4 w-full min-w-[2px] items-center rounded-sm",
|
||||
eventBackgroundClassName(node.data)
|
||||
)}
|
||||
layoutId={node.id}
|
||||
>
|
||||
{node.data.isPartial && (
|
||||
<div
|
||||
@@ -684,7 +779,7 @@ function SpanWithDuration({
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Timeline.Span>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user