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
This commit is contained in:
@@ -253,7 +253,7 @@ export const TableCellMenu = forwardRef<
|
||||
type TableBlankRowProps = {
|
||||
className?: string;
|
||||
colSpan: number;
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const TableBlankRow = forwardRef<HTMLTableRowElement, TableBlankRowProps>(
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { StopCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import {
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
} from "~/components/primitives/Dialog";
|
||||
|
||||
type CancelRunDialogProps = {
|
||||
runFriendlyId: string;
|
||||
redirectPath: string;
|
||||
};
|
||||
|
||||
export function CancelRunDialog({ runFriendlyId, redirectPath }: CancelRunDialogProps) {
|
||||
const cancelFetcher = useFetcher();
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogHeader>Cancel this run?</DialogHeader>
|
||||
<DialogDescription>
|
||||
Canceling a run will stop execution. If you want to run this later you will have to replay
|
||||
the entire run with the original payload.
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<cancelFetcher.Form action={`/resources/taskruns/${runFriendlyId}/cancel`} method="post">
|
||||
<Button
|
||||
type="submit"
|
||||
name="redirectUrl"
|
||||
value={redirectPath}
|
||||
variant="danger/small"
|
||||
LeadingIcon={cancelFetcher.state === "idle" ? StopCircleIcon : "spinner-white"}
|
||||
disabled={cancelFetcher.state !== "idle"}
|
||||
shortcut={{ modifiers: ["meta"], key: "enter" }}
|
||||
>
|
||||
{cancelFetcher.state === "idle" ? "Cancel run" : "Canceling..."}
|
||||
</Button>
|
||||
</cancelFetcher.Form>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { StopIcon } from "@heroicons/react/24/outline";
|
||||
import { CheckIcon } from "@heroicons/react/24/solid";
|
||||
import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid";
|
||||
import { User } from "@trigger.dev/database";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { RunListItem } from "~/presenters/v3/RunListPresenter.server";
|
||||
import { v3RunPath } from "~/utils/pathBuilder";
|
||||
import { RunListAppliedFilters, RunListItem } from "~/presenters/v3/RunListPresenter.server";
|
||||
import { docsPath, v3RunPath, v3TestPath } from "~/utils/pathBuilder";
|
||||
import { EnvironmentLabel } from "../../environments/EnvironmentLabel";
|
||||
import { DateTime } from "../../primitives/DateTime";
|
||||
import { Paragraph } from "../../primitives/Paragraph";
|
||||
@@ -15,16 +15,24 @@ import {
|
||||
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 { StopCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
|
||||
import { CancelRunDialog } from "./CancelRunDialog";
|
||||
import { useLocation } from "@remix-run/react";
|
||||
|
||||
type RunsTableProps = {
|
||||
total: number;
|
||||
hasFilters: boolean;
|
||||
filters: RunListAppliedFilters;
|
||||
showJob?: boolean;
|
||||
runs: RunListItem[];
|
||||
isLoading?: boolean;
|
||||
@@ -34,12 +42,14 @@ type RunsTableProps = {
|
||||
export function TaskRunsTable({
|
||||
total,
|
||||
hasFilters,
|
||||
filters,
|
||||
runs,
|
||||
isLoading = false,
|
||||
currentUser,
|
||||
}: RunsTableProps) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Table>
|
||||
@@ -65,9 +75,7 @@ export function TaskRunsTable({
|
||||
{!isLoading && <NoRuns title="No runs found" />}
|
||||
</TableBlankRow>
|
||||
) : runs.length === 0 ? (
|
||||
<TableBlankRow colSpan={9}>
|
||||
{!isLoading && <NoRuns title="No runs match your filters" />}
|
||||
</TableBlankRow>
|
||||
<BlankState isLoading={isLoading} filters={filters} />
|
||||
) : (
|
||||
runs.map((run) => {
|
||||
const path = v3RunPath(organization, project, run);
|
||||
@@ -102,7 +110,23 @@ export function TaskRunsTable({
|
||||
<TableCell to={path}>
|
||||
{run.createdAt ? <DateTime date={run.createdAt} /> : "–"}
|
||||
</TableCell>
|
||||
<TableCellChevron to={path} isSticky />
|
||||
{run.isCancellable ? (
|
||||
<TableCellMenu isSticky>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="small-menu-item" LeadingIcon={StopCircleIcon}>
|
||||
Cancel run
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<CancelRunDialog
|
||||
runFriendlyId={run.friendlyId}
|
||||
redirectPath={`${location.pathname}${location.search}`}
|
||||
/>
|
||||
</Dialog>
|
||||
</TableCellMenu>
|
||||
) : (
|
||||
<TableCell to={path}>{""}</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
@@ -127,3 +151,62 @@ function NoRuns({ title }: { title: string }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BlankState({ isLoading, filters }: Pick<RunsTableProps, "isLoading" | "filters">) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const envs = useEnvironments();
|
||||
if (isLoading) return <TableBlankRow colSpan={9}></TableBlankRow>;
|
||||
|
||||
const { environments, tasks, from, to, ...otherFilters } = filters;
|
||||
|
||||
if (
|
||||
filters.environments.length === 1 &&
|
||||
filters.tasks.length === 1 &&
|
||||
filters.from === undefined &&
|
||||
filters.to === undefined &&
|
||||
Object.values(otherFilters).every((filterArray) => filterArray.length === 0)
|
||||
) {
|
||||
const environment = envs?.find((env) => env.id === filters.environments[0]);
|
||||
return (
|
||||
<TableBlankRow colSpan={9}>
|
||||
<div className="py-14">
|
||||
<Paragraph className="w-auto" variant="base/bright" spacing>
|
||||
There are no runs for {filters.tasks[0]}
|
||||
{environment ? (
|
||||
<>
|
||||
{" "}
|
||||
in <EnvironmentLabel environment={environment} size="large" />
|
||||
</>
|
||||
) : null}
|
||||
</Paragraph>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<LinkButton
|
||||
to={v3TestPath(organization, project)}
|
||||
variant="primary/small"
|
||||
LeadingIcon={BeakerIcon}
|
||||
className="inline-flex"
|
||||
>
|
||||
Create a test run
|
||||
</LinkButton>
|
||||
<Paragraph variant="small">or</Paragraph>
|
||||
<LinkButton
|
||||
to={docsPath("v3/triggering")}
|
||||
variant="primary/small"
|
||||
LeadingIcon={BookOpenIcon}
|
||||
className="inline-flex"
|
||||
>
|
||||
Triggering a task docs
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
</TableBlankRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableBlankRow colSpan={9}>
|
||||
<NoRuns title="No runs match your filters" />
|
||||
</TableBlankRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Prisma, TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/databa
|
||||
import { Direction } from "~/components/runs/RunStatuses";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { getUsername } from "~/utils/username";
|
||||
import { CANCELLABLE_STATUSES } from "~/v3/services/cancelTaskRun.server";
|
||||
|
||||
type RunListOptions = {
|
||||
userId: string;
|
||||
@@ -23,6 +24,7 @@ const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
export type RunList = Awaited<ReturnType<RunListPresenter["call"]>>;
|
||||
export type RunListItem = RunList["runs"][0];
|
||||
export type RunListAppliedFilters = RunList["filters"];
|
||||
|
||||
export class RunListPresenter {
|
||||
#prismaClient: PrismaClient;
|
||||
@@ -220,6 +222,7 @@ export class RunListPresenter {
|
||||
version: run.version,
|
||||
taskIdentifier: run.taskIdentifier,
|
||||
attempts: Number(run.attempts),
|
||||
isCancellable: CANCELLABLE_STATUSES.includes(run.status),
|
||||
environment: {
|
||||
type: environment.type,
|
||||
slug: environment.slug,
|
||||
@@ -233,6 +236,14 @@ export class RunListPresenter {
|
||||
previous,
|
||||
},
|
||||
possibleTasks: possibleTasks.map((task) => task.slug),
|
||||
filters: {
|
||||
tasks: tasks || [],
|
||||
versions: versions || [],
|
||||
statuses: statuses || [],
|
||||
environments: environments || [],
|
||||
from,
|
||||
to,
|
||||
},
|
||||
hasFilters,
|
||||
};
|
||||
}
|
||||
|
||||
+10
-33
@@ -19,6 +19,7 @@ import {
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import { CancelRunDialog } from "~/components/runs/v3/CancelRunDialog";
|
||||
import { LiveTimer } from "~/components/runs/v3/LiveTimer";
|
||||
import { RunIcon } from "~/components/runs/v3/RunIcon";
|
||||
import { SpanEvents } from "~/components/runs/v3/SpanEvents";
|
||||
@@ -54,7 +55,6 @@ export default function Page() {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const { runParam } = useParams();
|
||||
const cancelFetcher = useFetcher();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -176,38 +176,15 @@ export default function Page() {
|
||||
Cancel run
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>Cancel this run?</DialogHeader>
|
||||
<DialogDescription>
|
||||
Canceling a run will stop execution. If you want to run this later you will have
|
||||
to replay the entire run with the original payload.
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<cancelFetcher.Form
|
||||
action={`/resources/taskruns/${event.runId}/cancel`}
|
||||
method="post"
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
name="redirectUrl"
|
||||
value={v3RunSpanPath(
|
||||
organization,
|
||||
project,
|
||||
{ friendlyId: runParam },
|
||||
{ spanId: event.spanId }
|
||||
)}
|
||||
variant="danger/small"
|
||||
LeadingIcon={
|
||||
cancelFetcher.state === "idle" ? StopCircleIcon : "spinner-white"
|
||||
}
|
||||
disabled={cancelFetcher.state !== "idle"}
|
||||
shortcut={{ modifiers: ["meta"], key: "enter" }}
|
||||
>
|
||||
{cancelFetcher.state === "idle" ? "Cancel run" : "Canceling..."}
|
||||
</Button>
|
||||
</cancelFetcher.Form>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
<CancelRunDialog
|
||||
runFriendlyId={event.runId}
|
||||
redirectPath={v3RunSpanPath(
|
||||
organization,
|
||||
project,
|
||||
{ friendlyId: runParam },
|
||||
{ spanId: event.spanId }
|
||||
)}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+1
-5
@@ -581,11 +581,7 @@ function NodeStatusIcon({ node }: { node: RunEvent }) {
|
||||
}
|
||||
|
||||
function TaskLine({ isError, isSelected }: { isError: boolean; isSelected: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={cn("h-8 w-2 border-r", isError ? "border-rose-500/10" : "border-charcoal-800")}
|
||||
/>
|
||||
);
|
||||
return <div className={cn("h-8 w-2 border-r border-grid-bright")} />;
|
||||
}
|
||||
|
||||
function ShowParentLink({ runFriendlyId }: { runFriendlyId: string }) {
|
||||
|
||||
+1
@@ -84,6 +84,7 @@ export default function Page() {
|
||||
<TaskRunsTable
|
||||
total={list.runs.length}
|
||||
hasFilters={list.hasFilters}
|
||||
filters={list.filters}
|
||||
runs={list.runs}
|
||||
isLoading={isLoading}
|
||||
currentUser={user}
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
SpanEvents,
|
||||
TaskEventStyle,
|
||||
correctErrorStackTrace,
|
||||
flattenAndNormalizeAttributes,
|
||||
flattenAttributes,
|
||||
isExceptionSpanEvent,
|
||||
omit,
|
||||
primitiveValueOrflattenedAttributes,
|
||||
unflattenAttributes,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { Prisma, TaskEvent, TaskEventStatus, type TaskEventKind } from "@trigger.dev/database";
|
||||
@@ -178,10 +178,7 @@ export class EventRepository {
|
||||
metadata: event.metadata as Attributes,
|
||||
style: event.style as Attributes,
|
||||
output: options?.attributes.output
|
||||
? flattenAndNormalizeAttributes(
|
||||
options.attributes.output,
|
||||
SemanticInternalAttributes.OUTPUT
|
||||
)
|
||||
? primitiveValueOrflattenedAttributes(options.attributes.output, undefined)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
@@ -564,7 +561,6 @@ export class EventRepository {
|
||||
queueName: options.attributes.queueName,
|
||||
batchId: options.attributes.batchId ?? undefined,
|
||||
properties: {
|
||||
...style,
|
||||
...(flattenAttributes(metadata, SemanticInternalAttributes.METADATA) as Record<
|
||||
string,
|
||||
string
|
||||
|
||||
@@ -8,7 +8,7 @@ import { assertUnreachable } from "../utils/asserts.server";
|
||||
import { CancelAttemptService } from "./cancelAttempt.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
|
||||
const CANCELLABLE_STATUSES: Array<TaskRunStatus> = [
|
||||
export const CANCELLABLE_STATUSES: Array<TaskRunStatus> = [
|
||||
"PENDING",
|
||||
"EXECUTING",
|
||||
"PAUSED",
|
||||
|
||||
@@ -42,7 +42,7 @@ export { ConsoleInterceptor } from "./consoleInterceptor";
|
||||
export {
|
||||
flattenAttributes,
|
||||
unflattenAttributes,
|
||||
flattenAndNormalizeAttributes,
|
||||
primitiveValueOrflattenedAttributes,
|
||||
} from "./utils/flattenAttributes";
|
||||
export { defaultRetryOptions, calculateNextRetryDelay, calculateResetAt } from "./utils/retries";
|
||||
export { accessoryAttributes } from "./utils/styleAttributes";
|
||||
|
||||
@@ -95,13 +95,27 @@ export function unflattenAttributes(obj: Attributes): Record<string, unknown> {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function flattenAndNormalizeAttributes(
|
||||
export function primitiveValueOrflattenedAttributes(
|
||||
obj: Record<string, unknown> | Array<unknown> | string | boolean | number | undefined,
|
||||
prefix: string
|
||||
): Attributes {
|
||||
prefix: string | undefined
|
||||
): Attributes | string | number | boolean | undefined {
|
||||
if (
|
||||
typeof obj === "string" ||
|
||||
typeof obj === "number" ||
|
||||
typeof obj === "boolean" ||
|
||||
obj === null ||
|
||||
obj === undefined
|
||||
) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const attributes = flattenAttributes(obj, prefix);
|
||||
|
||||
if (typeof attributes[prefix] !== "undefined" && attributes[prefix] !== null) {
|
||||
if (
|
||||
prefix !== undefined &&
|
||||
typeof attributes[prefix] !== "undefined" &&
|
||||
attributes[prefix] !== null
|
||||
) {
|
||||
return attributes[prefix] as unknown as Attributes;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user