Run page turned into components so it can be shared

This commit is contained in:
Matt Aitken
2023-07-10 15:47:13 +01:00
parent eae0b8a95e
commit a9e7cda37d
9 changed files with 330 additions and 312 deletions
@@ -0,0 +1,73 @@
import { CodeBlock } from "~/components/code/CodeBlock";
import { DateTime } from "~/components/primitives/DateTime";
import { Paragraph } from "~/components/primitives/Paragraph";
import { RunStatusIcon, RunStatusLabel } from "~/components/runs/RunStatuses";
import { useRun } from "~/hooks/useRun";
import { formatDuration } from "~/utils";
import {
RunPanel,
RunPanelBody,
RunPanelDivider,
RunPanelError,
RunPanelHeader,
RunPanelIconProperty,
RunPanelIconSection,
} from "./RunCard";
export function RunCompletedDetail() {
const run = useRun();
return (
<RunPanel>
<RunPanelHeader
icon={<RunStatusIcon status={run.status} className={"h-5 w-5"} />}
title={
<Paragraph variant="small/bright">
<RunStatusLabel status={run.status} />
</Paragraph>
}
/>
<RunPanelBody>
<RunPanelIconSection>
{run.startedAt && (
<RunPanelIconProperty
icon="calendar"
label="Started at"
value={<DateTime date={run.startedAt} />}
/>
)}
{run.completedAt && (
<RunPanelIconProperty
icon="flag"
label="Finished at"
value={<DateTime date={run.completedAt} />}
/>
)}
{run.startedAt && run.completedAt && (
<RunPanelIconProperty
icon="clock"
label="Total duration"
value={formatDuration(run.startedAt, run.completedAt, {
style: "long",
})}
/>
)}
</RunPanelIconSection>
<RunPanelDivider />
{run.error && (
<RunPanelError
text={run.error.message}
stackTrace={run.error.stack}
/>
)}
{run.output ? (
<CodeBlock language="json" code={run.output} />
) : (
run.output === null && (
<Paragraph variant="small">This run returned nothing</Paragraph>
)
)}
</RunPanelBody>
</RunPanel>
);
}
@@ -169,15 +169,7 @@ export function RunOverview({
selected={selectedId === "trigger"}
onClick={() => navigate(runTriggerPath(paths.run))}
>
<RunPanelHeader
icon={<BoltIcon className="h-5 w-5 text-orange-500" />}
title={
<RunPanelIconTitle
icon={trigger.icon}
title={trigger.title}
/>
}
/>
<RunPanelHeader icon={trigger.icon} title={trigger.title} />
<RunPanelBody>
<RunPanelProperties
properties={[
@@ -0,0 +1,175 @@
import { DetailedTask } from "~/presenters/TaskDetailsPresenter.server";
import {
RunPanel,
RunPanelBody,
RunPanelDescription,
RunPanelDivider,
RunPanelHeader,
RunPanelIconProperty,
RunPanelIconSection,
RunPanelIconTitle,
RunPanelProperties,
UpdatingDelay,
UpdatingDuration,
} from "./RunCard";
import { sensitiveDataReplacer } from "~/services/sensitiveDataReplacer";
import { formatDuration } from "~/utils";
import { cn } from "~/utils/cn";
import { CodeBlock } from "../code/CodeBlock";
import { DateTime } from "../primitives/DateTime";
import { Header3 } from "../primitives/Headers";
import { Paragraph } from "../primitives/Paragraph";
import {
TableHeader,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
Table,
} from "../primitives/Table";
import { TaskAttemptStatusLabel } from "./TaskAttemptStatus";
import { TaskStatusIcon } from "./TaskStatus";
export function TaskDetail({ task }: { task: DetailedTask }) {
const {
name,
description,
icon,
startedAt,
completedAt,
status,
delayUntil,
params,
properties,
output,
style,
attempts,
} = task;
return (
<RunPanel selected={false}>
<RunPanelHeader
icon={
<TaskStatusIcon
status={status}
minimal={true}
className={cn("h-5 w-5")}
/>
}
title={<RunPanelIconTitle icon={icon} title={name} />}
accessory={
<Paragraph variant="extra-small">
<UpdatingDuration
start={startedAt ?? undefined}
end={completedAt ?? undefined}
/>
</Paragraph>
}
/>
<RunPanelBody>
<RunPanelIconSection>
{startedAt && (
<RunPanelIconProperty
icon="calendar"
label="Started at"
value={<DateTime date={startedAt} />}
/>
)}
{completedAt && (
<RunPanelIconProperty
icon="flag"
label="Finished at"
value={<DateTime date={completedAt} />}
/>
)}
{delayUntil && !completedAt && (
<>
<RunPanelIconProperty
icon="flag"
label="Continues at"
value={<DateTime date={delayUntil} />}
/>
<UpdatingDelay delayUntil={delayUntil} />
</>
)}
{delayUntil && completedAt && (
<RunPanelIconProperty
icon="clock"
label="Delay duration"
value={formatDuration(startedAt, delayUntil, {
style: "long",
maxDecimalPoints: 0,
})}
/>
)}
</RunPanelIconSection>
<RunPanelDivider />
{description && (
<RunPanelDescription text={description} variant={style?.variant} />
)}
{properties.length > 0 && (
<div className="mt-4 flex flex-col gap-2">
<Header3>Properties</Header3>
<RunPanelProperties properties={properties} layout="horizontal" />
</div>
)}
{attempts.length > 1 && (
<div className="mt-4 flex flex-col gap-2">
<Header3>Retries</Header3>
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>Attempt</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Date</TableHeaderCell>
<TableHeaderCell>Error</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{attempts.map((attempt) => (
<TableRow key={attempt.number}>
<TableCell>{attempt.number}</TableCell>
<TableCell>
<TaskAttemptStatusLabel status={attempt.status} />
</TableCell>
<TableCell>
<DateTime
date={
attempt.status === "PENDING" && attempt.runAt
? attempt.runAt
: attempt.updatedAt
}
/>
</TableCell>
<TableCell>{attempt.error}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
<div className="mt-4 flex flex-col gap-2">
<Header3>Input</Header3>
{params ? (
<CodeBlock
code={JSON.stringify(params, sensitiveDataReplacer, 2)}
maxLines={35}
/>
) : (
<Paragraph variant="small">No input</Paragraph>
)}
</div>
<div className="mt-4 flex flex-col gap-2">
<Header3>Output</Header3>
{output ? (
<CodeBlock code={JSON.stringify(output, null, 2)} />
) : (
<Paragraph variant="small">No output</Paragraph>
)}
</div>
</RunPanelBody>
</RunPanel>
);
}
@@ -0,0 +1,63 @@
import { DetailedEvent } from "~/presenters/TriggerDetailsPresenter.server";
import { CodeBlock } from "../code/CodeBlock";
import { DateTime } from "../primitives/DateTime";
import { Header3 } from "../primitives/Headers";
import {
RunPanel,
RunPanelBody,
RunPanelDivider,
RunPanelHeader,
RunPanelIconProperty,
RunPanelIconSection,
RunPanelProperties,
} from "./RunCard";
import { DisplayProperty } from "@trigger.dev/internal";
export function TriggerDetail({
trigger,
event,
properties,
}: {
trigger: DetailedEvent;
event: {
title: string;
icon: string;
};
properties: DisplayProperty[];
}) {
const { id, name, payload, timestamp, deliveredAt } = trigger;
return (
<RunPanel selected={false}>
<RunPanelHeader icon={event.icon} title={event.title} />
<RunPanelBody>
<RunPanelIconSection>
<RunPanelIconProperty
icon="calendar"
label="Created"
value={<DateTime date={timestamp} />}
/>
{deliveredAt && (
<RunPanelIconProperty
icon="flag"
label="Finished at"
value={<DateTime date={deliveredAt} />}
/>
)}
<RunPanelIconProperty icon="id" label="Event name" value={name} />
</RunPanelIconSection>
<RunPanelDivider />
<div className="mt-4 flex flex-col gap-2">
{properties.length > 0 && (
<div className="mb-2 flex flex-col gap-4">
<Header3>Properties</Header3>
<RunPanelProperties properties={properties} layout="horizontal" />
</div>
)}
<Header3>Payload</Header3>
<CodeBlock code={JSON.stringify(payload, null, 2)} />
</div>
</RunPanelBody>
</RunPanel>
);
}
@@ -1,15 +1,10 @@
import { PrismaClient, prisma } from "~/db.server";
type DetailsProps = {
id: string;
userId: string;
};
export type DetailedEvent = NonNullable<
Awaited<ReturnType<EventDetailsPresenter["call"]>>
Awaited<ReturnType<TriggerDetailsPresenter["call"]>>
>;
export class EventDetailsPresenter {
export class TriggerDetailsPresenter {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
@@ -1,73 +1,5 @@
import { CodeBlock } from "~/components/code/CodeBlock";
import { DateTime } from "~/components/primitives/DateTime";
import { Paragraph } from "~/components/primitives/Paragraph";
import { RunStatusIcon, RunStatusLabel } from "~/components/runs/RunStatuses";
import { useRun } from "~/hooks/useRun";
import { formatDuration } from "~/utils";
import {
RunPanel,
RunPanelBody,
RunPanelDivider,
RunPanelError,
RunPanelHeader,
RunPanelIconProperty,
RunPanelIconSection,
} from "../../components/run/RunCard";
import { RunCompletedDetail } from "~/components/run/RunCompletedDetail";
export default function RunCompletedPage() {
const run = useRun();
return (
<RunPanel>
<RunPanelHeader
icon={<RunStatusIcon status={run.status} className={"h-5 w-5"} />}
title={
<Paragraph variant="small/bright">
<RunStatusLabel status={run.status} />
</Paragraph>
}
/>
<RunPanelBody>
<RunPanelIconSection>
{run.startedAt && (
<RunPanelIconProperty
icon="calendar"
label="Started at"
value={<DateTime date={run.startedAt} />}
/>
)}
{run.completedAt && (
<RunPanelIconProperty
icon="flag"
label="Finished at"
value={<DateTime date={run.completedAt} />}
/>
)}
{run.startedAt && run.completedAt && (
<RunPanelIconProperty
icon="clock"
label="Total duration"
value={formatDuration(run.startedAt, run.completedAt, {
style: "long",
})}
/>
)}
</RunPanelIconSection>
<RunPanelDivider />
{run.error && (
<RunPanelError
text={run.error.message}
stackTrace={run.error.stack}
/>
)}
{run.output ? (
<CodeBlock language="json" code={run.output} />
) : (
run.output === null && (
<Paragraph variant="small">This run returned nothing</Paragraph>
)
)}
</RunPanelBody>
</RunPanel>
);
return <RunCompletedDetail />;
}
@@ -1,38 +1,9 @@
import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { CodeBlock } from "~/components/code/CodeBlock";
import { DateTime } from "~/components/primitives/DateTime";
import { Header3 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import {
Table,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow,
} from "~/components/primitives/Table";
import { TaskDetail } from "~/components/run/TaskDetail";
import { TaskDetailsPresenter } from "~/presenters/TaskDetailsPresenter.server";
import { sensitiveDataReplacer } from "~/services/sensitiveDataReplacer";
import { requireUserId } from "~/services/session.server";
import { formatDuration } from "~/utils";
import { cn } from "~/utils/cn";
import { TaskParamsSchema } from "~/utils/pathBuilder";
import {
RunPanel,
RunPanelBody,
RunPanelDescription,
RunPanelDivider,
RunPanelHeader,
RunPanelIconProperty,
RunPanelIconSection,
RunPanelIconTitle,
RunPanelProperties,
UpdatingDelay,
UpdatingDuration,
} from "../../components/run/RunCard";
import { TaskAttemptStatusLabel } from "./TaskAttemptStatus";
import { TaskStatusIcon } from "~/components/run/TaskStatus";
export const loader = async ({ request, params }: LoaderArgs) => {
const userId = await requireUserId(request);
@@ -57,146 +28,5 @@ export const loader = async ({ request, params }: LoaderArgs) => {
export default function Page() {
const { task } = useTypedLoaderData<typeof loader>();
const {
name,
description,
icon,
startedAt,
completedAt,
status,
delayUntil,
params,
properties,
output,
style,
attempts,
} = task;
return (
<RunPanel selected={false}>
<RunPanelHeader
icon={
<TaskStatusIcon
status={status}
minimal={true}
className={cn("h-5 w-5")}
/>
}
title={<RunPanelIconTitle icon={icon} title={name} />}
accessory={
<Paragraph variant="extra-small">
<UpdatingDuration
start={startedAt ?? undefined}
end={completedAt ?? undefined}
/>
</Paragraph>
}
/>
<RunPanelBody>
<RunPanelIconSection>
{startedAt && (
<RunPanelIconProperty
icon="calendar"
label="Started at"
value={<DateTime date={startedAt} />}
/>
)}
{completedAt && (
<RunPanelIconProperty
icon="flag"
label="Finished at"
value={<DateTime date={completedAt} />}
/>
)}
{delayUntil && !completedAt && (
<>
<RunPanelIconProperty
icon="flag"
label="Continues at"
value={<DateTime date={delayUntil} />}
/>
<UpdatingDelay delayUntil={delayUntil} />
</>
)}
{delayUntil && completedAt && (
<RunPanelIconProperty
icon="clock"
label="Delay duration"
value={formatDuration(startedAt, delayUntil, {
style: "long",
maxDecimalPoints: 0,
})}
/>
)}
</RunPanelIconSection>
<RunPanelDivider />
{description && (
<RunPanelDescription text={description} variant={style?.variant} />
)}
{properties.length > 0 && (
<div className="mt-4 flex flex-col gap-2">
<Header3>Properties</Header3>
<RunPanelProperties properties={properties} layout="horizontal" />
</div>
)}
{attempts.length > 1 && (
<div className="mt-4 flex flex-col gap-2">
<Header3>Retries</Header3>
<Table>
<TableHeader>
<TableRow>
<TableHeaderCell>Attempt</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Date</TableHeaderCell>
<TableHeaderCell>Error</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{attempts.map((attempt) => (
<TableRow key={attempt.number}>
<TableCell>{attempt.number}</TableCell>
<TableCell>
<TaskAttemptStatusLabel status={attempt.status} />
</TableCell>
<TableCell>
<DateTime
date={
attempt.status === "PENDING" && attempt.runAt
? attempt.runAt
: attempt.updatedAt
}
/>
</TableCell>
<TableCell>{attempt.error}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
<div className="mt-4 flex flex-col gap-2">
<Header3>Input</Header3>
{params ? (
<CodeBlock
code={JSON.stringify(params, sensitiveDataReplacer, 2)}
maxLines={35}
/>
) : (
<Paragraph variant="small">No input</Paragraph>
)}
</div>
<div className="mt-4 flex flex-col gap-2">
<Header3>Output</Header3>
{output ? (
<CodeBlock code={JSON.stringify(output, null, 2)} />
) : (
<Paragraph variant="small">No output</Paragraph>
)}
</div>
</RunPanelBody>
</RunPanel>
);
return <TaskDetail task={task} />;
}
@@ -1,80 +1,38 @@
import { LoaderArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { CodeBlock } from "~/components/code/CodeBlock";
import { DateTime } from "~/components/primitives/DateTime";
import { Header3 } from "~/components/primitives/Headers";
import { TriggerDetail } from "~/components/run/TriggerDetail";
import { useJob } from "~/hooks/useJob";
import { useRun } from "~/hooks/useRun";
import { EventDetailsPresenter } from "~/presenters/EventDetailsPresenter.server";
import { TriggerDetailsPresenter } from "~/presenters/TriggerDetailsPresenter.server";
import { RunParamsSchema } from "~/utils/pathBuilder";
import {
RunPanel,
RunPanelBody,
RunPanelDivider,
RunPanelHeader,
RunPanelIconProperty,
RunPanelIconSection,
RunPanelProperties,
} from "../../components/run/RunCard";
export const loader = async ({ request, params }: LoaderArgs) => {
const { runParam } = RunParamsSchema.parse(params);
const presenter = new EventDetailsPresenter();
const event = await presenter.call(runParam);
const presenter = new TriggerDetailsPresenter();
const trigger = await presenter.call(runParam);
if (!event) {
if (!trigger) {
throw new Response(null, {
status: 404,
});
}
return typedjson({
event,
trigger,
});
};
export default function Page() {
const { event } = useTypedLoaderData<typeof loader>();
const { trigger } = useTypedLoaderData<typeof loader>();
const job = useJob();
const run = useRun();
const { id, name, payload, timestamp, deliveredAt } = event;
return (
<RunPanel selected={false}>
<RunPanelHeader icon={job.event.icon} title={job.event.title} />
<RunPanelBody>
<RunPanelIconSection>
<RunPanelIconProperty
icon="calendar"
label="Created"
value={<DateTime date={timestamp} />}
/>
{deliveredAt && (
<RunPanelIconProperty
icon="flag"
label="Finished at"
value={<DateTime date={deliveredAt} />}
/>
)}
<RunPanelIconProperty icon="id" label="Event name" value={name} />
</RunPanelIconSection>
<RunPanelDivider />
<div className="mt-4 flex flex-col gap-2">
{run.properties.length > 0 && (
<div className="mb-2 flex flex-col gap-4">
<Header3>Properties</Header3>
<RunPanelProperties
properties={run.properties}
layout="horizontal"
/>
</div>
)}
<Header3>Payload</Header3>
<CodeBlock code={JSON.stringify(payload, null, 2)} />
</div>
</RunPanelBody>
</RunPanel>
<TriggerDetail
trigger={trigger}
event={job.event}
properties={run.properties}
/>
);
}