Skip to content

Commit 9304c44

Browse files
authored
Test, output and canceling improvements (#952)
* The vertical lines in the run tree view are now brighter * Blank state when you click a task with no runs * Canceling from the runs table * Fix for styles coming through incorrectly to properties * Fix for the OTEL output column having a $output prefix if it’s an object
1 parent ebec140 commit 9304c44

File tree

11 files changed

+179
-58
lines changed

11 files changed

+179
-58
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ export const TableCellMenu = forwardRef<
253253
type TableBlankRowProps = {
254254
className?: string;
255255
colSpan: number;
256-
children: ReactNode;
256+
children?: ReactNode;
257257
};
258258

259259
export const TableBlankRow = forwardRef<HTMLTableRowElement, TableBlankRowProps>(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { StopCircleIcon } from "@heroicons/react/20/solid";
2+
import { useFetcher } from "@remix-run/react";
3+
import { Button } from "~/components/primitives/Buttons";
4+
import {
5+
DialogContent,
6+
DialogDescription,
7+
DialogFooter,
8+
DialogHeader,
9+
} from "~/components/primitives/Dialog";
10+
11+
type CancelRunDialogProps = {
12+
runFriendlyId: string;
13+
redirectPath: string;
14+
};
15+
16+
export function CancelRunDialog({ runFriendlyId, redirectPath }: CancelRunDialogProps) {
17+
const cancelFetcher = useFetcher();
18+
19+
return (
20+
<DialogContent>
21+
<DialogHeader>Cancel this run?</DialogHeader>
22+
<DialogDescription>
23+
Canceling a run will stop execution. If you want to run this later you will have to replay
24+
the entire run with the original payload.
25+
</DialogDescription>
26+
<DialogFooter>
27+
<cancelFetcher.Form action={`/resources/taskruns/${runFriendlyId}/cancel`} method="post">
28+
<Button
29+
type="submit"
30+
name="redirectUrl"
31+
value={redirectPath}
32+
variant="danger/small"
33+
LeadingIcon={cancelFetcher.state === "idle" ? StopCircleIcon : "spinner-white"}
34+
disabled={cancelFetcher.state !== "idle"}
35+
shortcut={{ modifiers: ["meta"], key: "enter" }}
36+
>
37+
{cancelFetcher.state === "idle" ? "Cancel run" : "Canceling..."}
38+
</Button>
39+
</cancelFetcher.Form>
40+
</DialogFooter>
41+
</DialogContent>
42+
);
43+
}

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

+90-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { StopIcon } from "@heroicons/react/24/outline";
2-
import { CheckIcon } from "@heroicons/react/24/solid";
2+
import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
33
import { User } from "@trigger.dev/database";
44
import { useOrganization } from "~/hooks/useOrganizations";
55
import { useProject } from "~/hooks/useProject";
6-
import { RunListItem } from "~/presenters/v3/RunListPresenter.server";
7-
import { v3RunPath } from "~/utils/pathBuilder";
6+
import { RunListAppliedFilters, RunListItem } from "~/presenters/v3/RunListPresenter.server";
7+
import { docsPath, v3RunPath, v3TestPath } from "~/utils/pathBuilder";
88
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
99
import { DateTime } from "../../primitives/DateTime";
1010
import { Paragraph } from "../../primitives/Paragraph";
@@ -15,16 +15,24 @@ import {
1515
TableBody,
1616
TableCell,
1717
TableCellChevron,
18+
TableCellMenu,
1819
TableHeader,
1920
TableHeaderCell,
2021
TableRow,
2122
} from "../../primitives/Table";
2223
import { formatDuration } from "@trigger.dev/core/v3";
2324
import { TaskRunStatusCombo } from "./TaskRunStatus";
25+
import { useEnvironments } from "~/hooks/useEnvironments";
26+
import { Button, LinkButton } from "~/components/primitives/Buttons";
27+
import { StopCircleIcon } from "@heroicons/react/20/solid";
28+
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
29+
import { CancelRunDialog } from "./CancelRunDialog";
30+
import { useLocation } from "@remix-run/react";
2431

2532
type RunsTableProps = {
2633
total: number;
2734
hasFilters: boolean;
35+
filters: RunListAppliedFilters;
2836
showJob?: boolean;
2937
runs: RunListItem[];
3038
isLoading?: boolean;
@@ -34,12 +42,14 @@ type RunsTableProps = {
3442
export function TaskRunsTable({
3543
total,
3644
hasFilters,
45+
filters,
3746
runs,
3847
isLoading = false,
3948
currentUser,
4049
}: RunsTableProps) {
4150
const organization = useOrganization();
4251
const project = useProject();
52+
const location = useLocation();
4353

4454
return (
4555
<Table>
@@ -65,9 +75,7 @@ export function TaskRunsTable({
6575
{!isLoading && <NoRuns title="No runs found" />}
6676
</TableBlankRow>
6777
) : runs.length === 0 ? (
68-
<TableBlankRow colSpan={9}>
69-
{!isLoading && <NoRuns title="No runs match your filters" />}
70-
</TableBlankRow>
78+
<BlankState isLoading={isLoading} filters={filters} />
7179
) : (
7280
runs.map((run) => {
7381
const path = v3RunPath(organization, project, run);
@@ -102,7 +110,23 @@ export function TaskRunsTable({
102110
<TableCell to={path}>
103111
{run.createdAt ? <DateTime date={run.createdAt} /> : "–"}
104112
</TableCell>
105-
<TableCellChevron to={path} isSticky />
113+
{run.isCancellable ? (
114+
<TableCellMenu isSticky>
115+
<Dialog>
116+
<DialogTrigger asChild>
117+
<Button variant="small-menu-item" LeadingIcon={StopCircleIcon}>
118+
Cancel run
119+
</Button>
120+
</DialogTrigger>
121+
<CancelRunDialog
122+
runFriendlyId={run.friendlyId}
123+
redirectPath={`${location.pathname}${location.search}`}
124+
/>
125+
</Dialog>
126+
</TableCellMenu>
127+
) : (
128+
<TableCell to={path}>{""}</TableCell>
129+
)}
106130
</TableRow>
107131
);
108132
})
@@ -127,3 +151,62 @@ function NoRuns({ title }: { title: string }) {
127151
</div>
128152
);
129153
}
154+
155+
function BlankState({ isLoading, filters }: Pick<RunsTableProps, "isLoading" | "filters">) {
156+
const organization = useOrganization();
157+
const project = useProject();
158+
const envs = useEnvironments();
159+
if (isLoading) return <TableBlankRow colSpan={9}></TableBlankRow>;
160+
161+
const { environments, tasks, from, to, ...otherFilters } = filters;
162+
163+
if (
164+
filters.environments.length === 1 &&
165+
filters.tasks.length === 1 &&
166+
filters.from === undefined &&
167+
filters.to === undefined &&
168+
Object.values(otherFilters).every((filterArray) => filterArray.length === 0)
169+
) {
170+
const environment = envs?.find((env) => env.id === filters.environments[0]);
171+
return (
172+
<TableBlankRow colSpan={9}>
173+
<div className="py-14">
174+
<Paragraph className="w-auto" variant="base/bright" spacing>
175+
There are no runs for {filters.tasks[0]}
176+
{environment ? (
177+
<>
178+
{" "}
179+
in <EnvironmentLabel environment={environment} size="large" />
180+
</>
181+
) : null}
182+
</Paragraph>
183+
<div className="flex items-center justify-center gap-2">
184+
<LinkButton
185+
to={v3TestPath(organization, project)}
186+
variant="primary/small"
187+
LeadingIcon={BeakerIcon}
188+
className="inline-flex"
189+
>
190+
Create a test run
191+
</LinkButton>
192+
<Paragraph variant="small">or</Paragraph>
193+
<LinkButton
194+
to={docsPath("v3/triggering")}
195+
variant="primary/small"
196+
LeadingIcon={BookOpenIcon}
197+
className="inline-flex"
198+
>
199+
Triggering a task docs
200+
</LinkButton>
201+
</div>
202+
</div>
203+
</TableBlankRow>
204+
);
205+
}
206+
207+
return (
208+
<TableBlankRow colSpan={9}>
209+
<NoRuns title="No runs match your filters" />
210+
</TableBlankRow>
211+
);
212+
}

apps/webapp/app/presenters/v3/RunListPresenter.server.ts

+11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Prisma, TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/databa
22
import { Direction } from "~/components/runs/RunStatuses";
33
import { PrismaClient, prisma } from "~/db.server";
44
import { getUsername } from "~/utils/username";
5+
import { CANCELLABLE_STATUSES } from "~/v3/services/cancelTaskRun.server";
56

67
type RunListOptions = {
78
userId: string;
@@ -23,6 +24,7 @@ const DEFAULT_PAGE_SIZE = 20;
2324

2425
export type RunList = Awaited<ReturnType<RunListPresenter["call"]>>;
2526
export type RunListItem = RunList["runs"][0];
27+
export type RunListAppliedFilters = RunList["filters"];
2628

2729
export class RunListPresenter {
2830
#prismaClient: PrismaClient;
@@ -220,6 +222,7 @@ export class RunListPresenter {
220222
version: run.version,
221223
taskIdentifier: run.taskIdentifier,
222224
attempts: Number(run.attempts),
225+
isCancellable: CANCELLABLE_STATUSES.includes(run.status),
223226
environment: {
224227
type: environment.type,
225228
slug: environment.slug,
@@ -233,6 +236,14 @@ export class RunListPresenter {
233236
previous,
234237
},
235238
possibleTasks: possibleTasks.map((task) => task.slug),
239+
filters: {
240+
tasks: tasks || [],
241+
versions: versions || [],
242+
statuses: statuses || [],
243+
environments: environments || [],
244+
from,
245+
to,
246+
},
236247
hasFilters,
237248
};
238249
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx

+10-33
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import { Header2 } from "~/components/primitives/Headers";
2020
import { Paragraph } from "~/components/primitives/Paragraph";
2121
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
22+
import { CancelRunDialog } from "~/components/runs/v3/CancelRunDialog";
2223
import { LiveTimer } from "~/components/runs/v3/LiveTimer";
2324
import { RunIcon } from "~/components/runs/v3/RunIcon";
2425
import { SpanEvents } from "~/components/runs/v3/SpanEvents";
@@ -54,7 +55,6 @@ export default function Page() {
5455
const organization = useOrganization();
5556
const project = useProject();
5657
const { runParam } = useParams();
57-
const cancelFetcher = useFetcher();
5858

5959
return (
6060
<div
@@ -176,38 +176,15 @@ export default function Page() {
176176
Cancel run
177177
</Button>
178178
</DialogTrigger>
179-
<DialogContent>
180-
<DialogHeader>Cancel this run?</DialogHeader>
181-
<DialogDescription>
182-
Canceling a run will stop execution. If you want to run this later you will have
183-
to replay the entire run with the original payload.
184-
</DialogDescription>
185-
<DialogFooter>
186-
<cancelFetcher.Form
187-
action={`/resources/taskruns/${event.runId}/cancel`}
188-
method="post"
189-
>
190-
<Button
191-
type="submit"
192-
name="redirectUrl"
193-
value={v3RunSpanPath(
194-
organization,
195-
project,
196-
{ friendlyId: runParam },
197-
{ spanId: event.spanId }
198-
)}
199-
variant="danger/small"
200-
LeadingIcon={
201-
cancelFetcher.state === "idle" ? StopCircleIcon : "spinner-white"
202-
}
203-
disabled={cancelFetcher.state !== "idle"}
204-
shortcut={{ modifiers: ["meta"], key: "enter" }}
205-
>
206-
{cancelFetcher.state === "idle" ? "Cancel run" : "Canceling..."}
207-
</Button>
208-
</cancelFetcher.Form>
209-
</DialogFooter>
210-
</DialogContent>
179+
<CancelRunDialog
180+
runFriendlyId={event.runId}
181+
redirectPath={v3RunSpanPath(
182+
organization,
183+
project,
184+
{ friendlyId: runParam },
185+
{ spanId: event.spanId }
186+
)}
187+
/>
211188
</Dialog>
212189
)}
213190
</div>

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

+1-5
Original file line numberDiff line numberDiff line change
@@ -581,11 +581,7 @@ function NodeStatusIcon({ node }: { node: RunEvent }) {
581581
}
582582

583583
function TaskLine({ isError, isSelected }: { isError: boolean; isSelected: boolean }) {
584-
return (
585-
<div
586-
className={cn("h-8 w-2 border-r", isError ? "border-rose-500/10" : "border-charcoal-800")}
587-
/>
588-
);
584+
return <div className={cn("h-8 w-2 border-r border-grid-bright")} />;
589585
}
590586

591587
function ShowParentLink({ runFriendlyId }: { runFriendlyId: string }) {

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

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export default function Page() {
8484
<TaskRunsTable
8585
total={list.runs.length}
8686
hasFilters={list.hasFilters}
87+
filters={list.filters}
8788
runs={list.runs}
8889
isLoading={isLoading}
8990
currentUser={user}

apps/webapp/app/v3/eventRepository.server.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import {
99
SpanEvents,
1010
TaskEventStyle,
1111
correctErrorStackTrace,
12-
flattenAndNormalizeAttributes,
1312
flattenAttributes,
1413
isExceptionSpanEvent,
1514
omit,
15+
primitiveValueOrflattenedAttributes,
1616
unflattenAttributes,
1717
} from "@trigger.dev/core/v3";
1818
import { Prisma, TaskEvent, TaskEventStatus, type TaskEventKind } from "@trigger.dev/database";
@@ -178,10 +178,7 @@ export class EventRepository {
178178
metadata: event.metadata as Attributes,
179179
style: event.style as Attributes,
180180
output: options?.attributes.output
181-
? flattenAndNormalizeAttributes(
182-
options.attributes.output,
183-
SemanticInternalAttributes.OUTPUT
184-
)
181+
? primitiveValueOrflattenedAttributes(options.attributes.output, undefined)
185182
: undefined,
186183
});
187184
}
@@ -564,7 +561,6 @@ export class EventRepository {
564561
queueName: options.attributes.queueName,
565562
batchId: options.attributes.batchId ?? undefined,
566563
properties: {
567-
...style,
568564
...(flattenAttributes(metadata, SemanticInternalAttributes.METADATA) as Record<
569565
string,
570566
string

apps/webapp/app/v3/services/cancelTaskRun.server.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { assertUnreachable } from "../utils/asserts.server";
88
import { CancelAttemptService } from "./cancelAttempt.server";
99
import { logger } from "~/services/logger.server";
1010

11-
const CANCELLABLE_STATUSES: Array<TaskRunStatus> = [
11+
export const CANCELLABLE_STATUSES: Array<TaskRunStatus> = [
1212
"PENDING",
1313
"EXECUTING",
1414
"PAUSED",

packages/core/src/v3/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export { ConsoleInterceptor } from "./consoleInterceptor";
4242
export {
4343
flattenAttributes,
4444
unflattenAttributes,
45-
flattenAndNormalizeAttributes,
45+
primitiveValueOrflattenedAttributes,
4646
} from "./utils/flattenAttributes";
4747
export { defaultRetryOptions, calculateNextRetryDelay, calculateResetAt } from "./utils/retries";
4848
export { accessoryAttributes } from "./utils/styleAttributes";

0 commit comments

Comments
 (0)