Skip to content

Commit 7f7f993

Browse files
authored
feat(webapp): improve adjacent runs navigation and timeline performance (#2776)
- Add replace prop to LinkButton to use history replacement for adjacent run navigation - Preserve span and tab params when navigating between adjacent runs - Disable animations for completed spans in timeline to improve performance - Include spanId in runs list navigation for better context preservation - Direct link to task test page when filtering by single task with no runs - Fix minor styling issue with run friendlyId display padding
1 parent d287078 commit 7f7f993

File tree

4 files changed

+105
-65
lines changed
  • .github/workflows
  • apps/webapp/app
    • components/primitives
    • routes
      • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam
      • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index

4 files changed

+105
-65
lines changed

.github/workflows/typecheck.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ jobs:
3535

3636
- name: 🔎 Type check
3737
run: pnpm run typecheck
38+
env:
39+
NODE_OPTIONS: --max-old-space-size=8192
3840

3941
- name: 🔎 Check exports
4042
run: pnpm run check-exports

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
331331
type LinkPropsType = Pick<
332332
LinkProps,
333333
"to" | "target" | "onClick" | "onMouseDown" | "onMouseEnter" | "onMouseLeave" | "download"
334-
> & { disabled?: boolean } & React.ComponentProps<typeof ButtonContent>;
334+
> & { disabled?: boolean; replace?: boolean } & React.ComponentProps<typeof ButtonContent>;
335335
export const LinkButton = ({
336336
to,
337337
onClick,
@@ -340,6 +340,7 @@ export const LinkButton = ({
340340
onMouseLeave,
341341
download,
342342
disabled = false,
343+
replace,
343344
...props
344345
}: LinkPropsType) => {
345346
const innerRef = useRef<HTMLAnchorElement>(null);
@@ -387,6 +388,7 @@ export const LinkButton = ({
387388
<Link
388389
to={to}
389390
ref={innerRef}
391+
replace={replace}
390392
className={cn("group/button block focus-custom", props.fullWidth ? "w-full" : "")}
391393
onClick={onClick}
392394
onMouseDown={onMouseDown}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx

Lines changed: 86 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from "@trigger.dev/core/v3";
2424
import type { RuntimeEnvironmentType } from "@trigger.dev/database";
2525
import { motion } from "framer-motion";
26-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
26+
import { useCallback, useEffect, useRef, useState } from "react";
2727
import { useHotkeys } from "react-hotkeys-hook";
2828
import { redirect } from "remix-typedjson";
2929
import { MoveToTopIcon } from "~/assets/icons/MoveToTopIcon";
@@ -140,10 +140,10 @@ const resizableSettings = {
140140
type TraceEvent = NonNullable<SerializeFrom<typeof loader>["trace"]>["events"][0];
141141

142142
type RunsListNavigation = {
143-
runs: Array<{ friendlyId: string }>;
143+
runs: Array<{ friendlyId: string; spanId: string }>;
144144
pagination: { next?: string; previous?: string };
145-
prevPageLastRun?: { friendlyId: string; cursor: string };
146-
nextPageFirstRun?: { friendlyId: string; cursor: string };
145+
prevPageLastRun?: { friendlyId: string; spanId: string; cursor: string };
146+
nextPageFirstRun?: { friendlyId: string; spanId: string; cursor: string };
147147
};
148148

149149
async function getRunsListFromTableState({
@@ -204,6 +204,7 @@ async function getRunsListFromTableState({
204204
if (prevPageResult.runs.length > 0) {
205205
runsList.prevPageLastRun = {
206206
friendlyId: prevPageResult.runs[0].friendlyId,
207+
spanId: prevPageResult.runs[0].spanId,
207208
cursor: currentPageResult.pagination.previous,
208209
};
209210
}
@@ -222,6 +223,7 @@ async function getRunsListFromTableState({
222223
if (nextPageResult.runs.length > 0) {
223224
runsList.nextPageFirstRun = {
224225
friendlyId: nextPageResult.runs[0].friendlyId,
226+
spanId: nextPageResult.runs[0].spanId,
225227
cursor: currentPageResult.pagination.next,
226228
};
227229
}
@@ -296,7 +298,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
296298
type LoaderData = SerializeFrom<typeof loader>;
297299

298300
export default function Page() {
299-
const { run, trace, resizable, maximumLiveReloadingSetting, runsList } = useLoaderData<typeof loader>();
301+
const { run, trace, maximumLiveReloadingSetting, runsList } = useLoaderData<typeof loader>();
300302
const organization = useOrganization();
301303
const project = useProject();
302304
const environment = useEnvironment();
@@ -308,8 +310,10 @@ export default function Page() {
308310
const tableState = decodeURIComponent(value("tableState") ?? "");
309311
const tableStateSearchParams = new URLSearchParams(tableState);
310312
const filters = getRunFiltersFromSearchParams(tableStateSearchParams);
313+
const tabParam = value("tab") ?? undefined;
314+
const spanParam = value("span") ?? undefined;
311315

312-
const [previousRunPath, nextRunPath] = useAdjacentRunPaths({organization, project, environment, tableState, run, runsList});
316+
const [previousRunPath, nextRunPath] = useAdjacentRunPaths({organization, project, environment, tableState, run, runsList, tabParam, useSpan: !!spanParam});
313317

314318
return (
315319
<>
@@ -320,7 +324,7 @@ export default function Page() {
320324
text: "Runs",
321325
}}
322326
title={<>
323-
<CopyableText value={run.friendlyId} variant="text-below" className="font-mono"/>
327+
<CopyableText value={run.friendlyId} variant="text-below" className="font-mono px-0 py-0 pb-[2px]"/>
324328
{tableState && (<div className="flex">
325329
<PreviousRunButton to={previousRunPath} />
326330
<NextRunButton to={nextRunPath} />
@@ -940,7 +944,6 @@ function TimelineView({
940944
scale,
941945
rootSpanStatus,
942946
rootStartedAt,
943-
parentRef,
944947
timelineScrollRef,
945948
virtualizer,
946949
events,
@@ -1127,7 +1130,8 @@ function TimelineView({
11271130
"-ml-[0.5px] h-[0.5625rem] w-px rounded-none",
11281131
eventBackgroundClassName(node.data)
11291132
)}
1130-
layoutId={`${node.id}-${event.name}`}
1133+
layoutId={node.data.isPartial ? `${node.id}-${event.name}` : undefined}
1134+
animate={!node.data.isPartial ? false : undefined}
11311135
/>
11321136
)}
11331137
</Timeline.Point>
@@ -1145,7 +1149,8 @@ function TimelineView({
11451149
"-ml-[0.1562rem] size-[0.3125rem] rounded-full border bg-background-bright",
11461150
eventBorderClassName(node.data)
11471151
)}
1148-
layoutId={`${node.id}-${event.name}`}
1152+
layoutId={node.data.isPartial ? `${node.id}-${event.name}` : undefined}
1153+
animate={!node.data.isPartial ? false : undefined}
11491154
/>
11501155
)}
11511156
</Timeline.Point>
@@ -1164,7 +1169,8 @@ function TimelineView({
11641169
>
11651170
<motion.div
11661171
className={cn("h-px w-full", eventBackgroundClassName(node.data))}
1167-
layoutId={`mark-${node.id}`}
1172+
layoutId={node.data.isPartial ? `mark-${node.id}` : undefined}
1173+
animate={!node.data.isPartial ? false : undefined}
11681174
/>
11691175
</Timeline.Span>
11701176
) : null}
@@ -1201,7 +1207,8 @@ function TimelineView({
12011207
"-ml-0.5 size-3 rounded-full border-2 border-background-bright",
12021208
eventBackgroundClassName(node.data)
12031209
)}
1204-
layoutId={node.id}
1210+
layoutId={node.data.isPartial ? node.id : undefined}
1211+
animate={!node.data.isPartial ? false : undefined}
12051212
/>
12061213
)}
12071214
</Timeline.Point>
@@ -1444,7 +1451,8 @@ function SpanWithDuration({
14441451
fadeLeft ? "rounded-r-sm bg-gradient-to-r from-black/50 to-transparent" : "rounded-sm"
14451452
)}
14461453
style={{ backgroundSize: "20px 100%", backgroundRepeat: "no-repeat" }}
1447-
layoutId={node.id}
1454+
layoutId={node.data.isPartial ? node.id : undefined}
1455+
animate={!node.data.isPartial ? false : undefined}
14481456
>
14491457
{node.data.isPartial && (
14501458
<div
@@ -1457,10 +1465,12 @@ function SpanWithDuration({
14571465
"sticky left-0 z-10 transition-opacity group-hover:opacity-100",
14581466
!showDuration && "opacity-0"
14591467
)}
1468+
animate={!node.data.isPartial ? false : undefined}
14601469
>
14611470
<motion.div
14621471
className="whitespace-nowrap rounded-sm px-1 py-0.5 text-xxs text-text-bright text-shadow-custom"
1463-
layout="position"
1472+
layout={node.data.isPartial ? "position" : undefined}
1473+
animate={!node.data.isPartial ? false : undefined}
14641474
>
14651475
{formatDurationMilliseconds(props.durationMs, {
14661476
style: "short",
@@ -1543,12 +1553,11 @@ function KeyboardShortcuts({
15431553
expandAllBelowDepth,
15441554
collapseAllBelowDepth,
15451555
toggleExpandLevel,
1546-
setShowDurations,
15471556
}: {
15481557
expandAllBelowDepth: (depth: number) => void;
15491558
collapseAllBelowDepth: (depth: number) => void;
15501559
toggleExpandLevel: (depth: number) => void;
1551-
setShowDurations: (show: (show: boolean) => boolean) => void;
1560+
setShowDurations?: (show: (show: boolean) => boolean) => void;
15521561
}) {
15531562
return (
15541563
<>
@@ -1666,63 +1675,77 @@ function useAdjacentRunPaths({
16661675
tableState,
16671676
run,
16681677
runsList,
1678+
tabParam,
1679+
useSpan
16691680
}: {
16701681
organization: { slug: string };
16711682
project: { slug: string };
16721683
environment: { slug: string };
16731684
tableState: string;
1674-
run: { friendlyId: string };
1685+
run: { friendlyId: string, spanId: string };
16751686
runsList: RunsListNavigation | null;
1687+
tabParam?: string;
1688+
useSpan?: boolean;
16761689
}): [string | null, string | null] {
1677-
return useMemo(() => {
1678-
if (!runsList || runsList.runs.length === 0) {
1679-
return [null, null];
1680-
}
1681-
1682-
const currentIndex = runsList.runs.findIndex((r) => r.friendlyId === run.friendlyId);
1683-
1684-
if (currentIndex === -1) {
1685-
return [null, null];
1686-
}
1690+
if (!runsList || runsList.runs.length === 0) {
1691+
return [null, null];
1692+
}
16871693

1688-
// Determine previous run: use prevPageLastRun if at first position, otherwise use previous run in list
1689-
let previousRun: { friendlyId: string } | null = null;
1690-
const previousRunTableState = new URLSearchParams(tableState);
1691-
if (currentIndex > 0) {
1692-
previousRun = runsList.runs[currentIndex - 1];
1693-
} else if (runsList.prevPageLastRun) {
1694-
previousRun = runsList.prevPageLastRun;
1695-
// Update tableState with the new cursor for the previous page
1696-
previousRunTableState.set("cursor", runsList.prevPageLastRun.cursor);
1697-
previousRunTableState.set("direction", "backward");
1698-
}
1694+
const currentIndex = runsList.runs.findIndex((r) => r.friendlyId === run.friendlyId);
1695+
1696+
if (currentIndex === -1) {
1697+
return [null, null];
1698+
}
16991699

1700-
// Determine next run: use nextPageFirstRun if at last position, otherwise use next run in list
1701-
let nextRun: { friendlyId: string } | null = null;
1702-
const nextRunTableState = new URLSearchParams(tableState);
1703-
if (currentIndex < runsList.runs.length - 1) {
1704-
nextRun = runsList.runs[currentIndex + 1];
1705-
} else if (runsList.nextPageFirstRun) {
1706-
nextRun = runsList.nextPageFirstRun;
1707-
// Update tableState with the new cursor for the next page
1708-
nextRunTableState.set("cursor", runsList.nextPageFirstRun.cursor);
1709-
nextRunTableState.set("direction", "forward");
1710-
}
1700+
// Determine previous run: use prevPageLastRun if at first position, otherwise use previous run in list
1701+
let previousRun: { friendlyId: string; spanId: string } | null = null;
1702+
const previousRunTableState = new URLSearchParams(tableState);
1703+
if (currentIndex > 0) {
1704+
previousRun = runsList.runs[currentIndex - 1];
1705+
} else if (runsList.prevPageLastRun) {
1706+
previousRun = runsList.prevPageLastRun;
1707+
// Update tableState with the new cursor for the previous page
1708+
previousRunTableState.set("cursor", runsList.prevPageLastRun.cursor);
1709+
previousRunTableState.set("direction", "backward");
1710+
}
17111711

1712-
const previousURLSearchParams = new URLSearchParams();
1713-
previousURLSearchParams.set("tableState", previousRunTableState.toString());
1714-
const previousRunPath = previousRun
1715-
? v3RunPath(organization, project, environment, previousRun, previousURLSearchParams)
1716-
: null;
1712+
// Determine next run: use nextPageFirstRun if at last position, otherwise use next run in list
1713+
let nextRun: { friendlyId: string; spanId: string } | null = null;
1714+
const nextRunTableState = new URLSearchParams(tableState);
1715+
if (currentIndex < runsList.runs.length - 1) {
1716+
nextRun = runsList.runs[currentIndex + 1];
1717+
} else if (runsList.nextPageFirstRun) {
1718+
nextRun = runsList.nextPageFirstRun;
1719+
// Update tableState with the new cursor for the next page
1720+
nextRunTableState.set("cursor", runsList.nextPageFirstRun.cursor);
1721+
nextRunTableState.set("direction", "forward");
1722+
}
17171723

1718-
const nextURLSearchParams = new URLSearchParams();
1719-
nextURLSearchParams.set("tableState", nextRunTableState.toString());
1720-
const nextRunPath = nextRun
1721-
? v3RunPath(organization, project, environment, nextRun, nextURLSearchParams)
1722-
: null;
1724+
const previousURLSearchParams = new URLSearchParams();
1725+
previousURLSearchParams.set("tableState", previousRunTableState.toString());
1726+
if (previousRun && useSpan) {
1727+
previousURLSearchParams.set("span", previousRun.spanId);
1728+
}
1729+
if (tabParam && useSpan) {
1730+
previousURLSearchParams.set("tab", tabParam);
1731+
}
1732+
const previousRunPath = previousRun
1733+
? v3RunPath(organization, project, environment, previousRun, previousURLSearchParams)
1734+
: null;
1735+
1736+
const nextURLSearchParams = new URLSearchParams();
1737+
nextURLSearchParams.set("tableState", nextRunTableState.toString());
1738+
if (nextRun && useSpan) {
1739+
nextURLSearchParams.set("span", nextRun.spanId);
1740+
}
1741+
if (tabParam && useSpan) {
1742+
nextURLSearchParams.set("tab", tabParam);
1743+
}
1744+
const nextRunPath = nextRun
1745+
? v3RunPath(organization, project, environment, nextRun, nextURLSearchParams)
1746+
: null;
17231747

1724-
return [previousRunPath, nextRunPath];
1725-
}, [organization, project, environment, tableState, run.friendlyId, runsList]);
1748+
return [previousRunPath, nextRunPath];
17261749
}
17271750

17281751

@@ -1741,6 +1764,7 @@ function PreviousRunButton({ to }: { to: string | null }) {
17411764
shortcut={{ key: "[" }}
17421765
tooltip="Previous Run"
17431766
disabled={!to}
1767+
replace
17441768
/>
17451769
</div>
17461770
);
@@ -1761,6 +1785,7 @@ function NextRunButton({ to }: { to: string | null }) {
17611785
shortcut={{ key: "]" }}
17621786
tooltip="Next Run"
17631787
disabled={!to}
1788+
replace
17641789
/>
17651790
</div>
17661791
);

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
v3CreateBulkActionPath,
5656
v3ProjectPath,
5757
v3TestPath,
58+
v3TestTaskPath,
5859
} from "~/utils/pathBuilder";
5960
import { ListPagination } from "../../components/ListPagination";
6061
import { CreateBulkActionInspector } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction";
@@ -235,7 +236,13 @@ function RunsList({
235236
list.possibleTasks.length === 0 ? (
236237
<CreateFirstTaskInstructions />
237238
) : (
238-
<RunTaskInstructions />
239+
<RunTaskInstructions
240+
task={
241+
list.filters.tasks.length === 1
242+
? list.possibleTasks.find((t) => t.slug === list.filters.tasks[0])
243+
: undefined
244+
}
245+
/>
239246
)
240247
) : (
241248
<div className={cn("grid h-full max-h-full grid-rows-[auto_1fr] overflow-hidden")}>
@@ -339,7 +346,7 @@ function CreateFirstTaskInstructions() {
339346
);
340347
}
341348

342-
function RunTaskInstructions() {
349+
function RunTaskInstructions({ task }: { task?: { slug: string } }) {
343350
const organization = useOrganization();
344351
const project = useProject();
345352
const environment = useEnvironment();
@@ -352,7 +359,11 @@ function RunTaskInstructions() {
352359
Perform a test run with a payload directly from the dashboard.
353360
</Paragraph>
354361
<LinkButton
355-
to={v3TestPath(organization, project, environment)}
362+
to={
363+
task
364+
? v3TestTaskPath(organization, project, environment, { taskIdentifier: task.slug })
365+
: v3TestPath(organization, project, environment)
366+
}
356367
variant="secondary/medium"
357368
LeadingIcon={BeakerIcon}
358369
leadingIconClassName="text-lime-500"

0 commit comments

Comments
 (0)