The Run page now uses a component so it can be shared
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
import { conform } from "@conform-to/react";
|
||||
import { BoltIcon, ForwardIcon } from "@heroicons/react/24/solid";
|
||||
import { Form, Outlet, useNavigate } from "@remix-run/react";
|
||||
import { JobRunStatus, RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { useMemo } from "react";
|
||||
import { usePathName } from "~/hooks/usePathName";
|
||||
import { Run } from "~/presenters/RunPresenter.server";
|
||||
import { formatDuration } from "~/utils";
|
||||
import { cn } from "~/utils/cn";
|
||||
import {
|
||||
runCompletedPath,
|
||||
runTaskPath,
|
||||
runTriggerPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { CodeBlock } from "../code/CodeBlock";
|
||||
import { EnvironmentLabel } from "../environments/EnvironmentLabel";
|
||||
import { PageBody, PageContainer } from "../layout/AppLayout";
|
||||
import { Button } from "../primitives/Buttons";
|
||||
import { Callout } from "../primitives/Callout";
|
||||
import { DateTime } from "../primitives/DateTime";
|
||||
import { Header2 } from "../primitives/Headers";
|
||||
import { NamedIcon } from "../primitives/NamedIcon";
|
||||
import {
|
||||
PageButtons,
|
||||
PageHeader,
|
||||
PageInfoGroup,
|
||||
PageInfoProperty,
|
||||
PageInfoRow,
|
||||
PageTitle,
|
||||
PageTitleRow,
|
||||
} from "../primitives/PageHeader";
|
||||
import { Paragraph } from "../primitives/Paragraph";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../primitives/Popover";
|
||||
import {
|
||||
RunBasicStatus,
|
||||
RunStatusIcon,
|
||||
RunStatusLabel,
|
||||
runBasicStatus,
|
||||
runStatusTitle,
|
||||
} from "../runs/RunStatuses";
|
||||
import {
|
||||
RunPanel,
|
||||
RunPanelBody,
|
||||
RunPanelDivider,
|
||||
RunPanelError,
|
||||
RunPanelHeader,
|
||||
RunPanelIconProperty,
|
||||
RunPanelIconSection,
|
||||
RunPanelIconTitle,
|
||||
RunPanelProperties,
|
||||
} from "./RunCard";
|
||||
import { TaskCard } from "./TaskCard";
|
||||
import { TaskCardSkeleton } from "./TaskCardSkeleton";
|
||||
|
||||
type RunOverviewProps = {
|
||||
run: Run;
|
||||
trigger: {
|
||||
icon: string;
|
||||
title: string;
|
||||
};
|
||||
showRerun: boolean;
|
||||
paths: {
|
||||
back: string;
|
||||
run: string;
|
||||
};
|
||||
};
|
||||
|
||||
const taskPattern = /\/tasks\/(.*)/;
|
||||
|
||||
export function RunOverview({
|
||||
run,
|
||||
trigger,
|
||||
showRerun,
|
||||
paths,
|
||||
}: RunOverviewProps) {
|
||||
const navigate = useNavigate();
|
||||
const pathName = usePathName();
|
||||
|
||||
const selectedId = useMemo(() => {
|
||||
if (pathName.endsWith("/completed")) {
|
||||
return "completed";
|
||||
}
|
||||
|
||||
if (pathName.endsWith("/trigger")) {
|
||||
return "trigger";
|
||||
}
|
||||
|
||||
const taskMatch = pathName.match(taskPattern);
|
||||
const taskId = taskMatch ? taskMatch[1] : undefined;
|
||||
if (taskId) {
|
||||
return taskId;
|
||||
}
|
||||
}, [pathName]);
|
||||
|
||||
const basicStatus = runBasicStatus(run.status);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<PageTitleRow>
|
||||
<PageTitle
|
||||
backButton={{
|
||||
to: paths.back,
|
||||
text: "Runs",
|
||||
}}
|
||||
title={`Run #${run.number}`}
|
||||
/>
|
||||
<PageButtons>
|
||||
{run.isTest && (
|
||||
<span className="flex items-center gap-1 text-xs uppercase text-slate-600">
|
||||
<NamedIcon name="beaker" className="h-4 w-4 text-slate-600" />
|
||||
Test run
|
||||
</span>
|
||||
)}
|
||||
{showRerun && (
|
||||
<RerunPopover
|
||||
environmentType={run.environment.type}
|
||||
status={basicStatus}
|
||||
/>
|
||||
)}
|
||||
</PageButtons>
|
||||
</PageTitleRow>
|
||||
<PageInfoRow>
|
||||
<PageInfoGroup>
|
||||
<PageInfoProperty
|
||||
icon={<RunStatusIcon status={run.status} className="h-4 w-4" />}
|
||||
label={"Status"}
|
||||
value={runStatusTitle(run.status)}
|
||||
/>
|
||||
<PageInfoProperty
|
||||
icon={"calendar"}
|
||||
label={"Started"}
|
||||
value={
|
||||
run.startedAt ? (
|
||||
<DateTime date={run.startedAt} />
|
||||
) : (
|
||||
"Not started yet"
|
||||
)
|
||||
}
|
||||
/>
|
||||
<PageInfoProperty
|
||||
icon={"property"}
|
||||
label={"Version"}
|
||||
value={`v${run.version}`}
|
||||
/>
|
||||
<PageInfoProperty
|
||||
label={"Env"}
|
||||
value={<EnvironmentLabel environment={run.environment} />}
|
||||
/>
|
||||
<PageInfoProperty
|
||||
icon={"clock"}
|
||||
label={"Duration"}
|
||||
value={formatDuration(run.startedAt, run.completedAt)}
|
||||
/>
|
||||
</PageInfoGroup>
|
||||
<PageInfoGroup alignment="right">
|
||||
<Paragraph variant="extra-small" className="text-slate-600">
|
||||
RUN ID: {run.id}
|
||||
</Paragraph>
|
||||
</PageInfoGroup>
|
||||
</PageInfoRow>
|
||||
</PageHeader>
|
||||
<PageBody scrollable={false}>
|
||||
<div className="grid h-full grid-cols-2 gap-2">
|
||||
<div className="flex flex-col gap-6 overflow-y-auto py-4 pl-4 pr-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<div>
|
||||
<Header2 className="mb-2">Trigger</Header2>
|
||||
<RunPanel
|
||||
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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<RunPanelBody>
|
||||
<RunPanelProperties
|
||||
properties={[
|
||||
{ label: "Event name", text: run.event.name },
|
||||
...run.properties,
|
||||
]}
|
||||
/>
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
</div>
|
||||
<div>
|
||||
<Header2 className="mb-2">Tasks</Header2>
|
||||
|
||||
{run.tasks.length > 0 ? (
|
||||
run.tasks.map((task, index) => {
|
||||
const isLast = index === run.tasks.length - 1;
|
||||
|
||||
return (
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
selectedId={selectedId}
|
||||
selectedTask={(taskId) => {
|
||||
navigate(runTaskPath(paths.run, taskId));
|
||||
}}
|
||||
isLast={isLast}
|
||||
depth={0}
|
||||
{...task}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<BlankTasks status={run.status} basicStatus={basicStatus} />
|
||||
)}
|
||||
</div>
|
||||
{(basicStatus === "COMPLETED" || basicStatus === "FAILED") && (
|
||||
<div>
|
||||
<Header2 className={cn("mb-2")}>Run Summary</Header2>
|
||||
<RunPanel
|
||||
selected={selectedId === "completed"}
|
||||
onClick={() => navigate(runCompletedPath(paths.run))}
|
||||
>
|
||||
<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}
|
||||
maxLines={10}
|
||||
/>
|
||||
) : (
|
||||
run.output === null && (
|
||||
<Paragraph variant="small">
|
||||
This Run returned nothing.
|
||||
</Paragraph>
|
||||
)
|
||||
)}
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Detail view */}
|
||||
<div className="overflow-y-auto py-4 pr-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<Header2 className="mb-2">Detail</Header2>
|
||||
{selectedId ? (
|
||||
<Outlet />
|
||||
) : (
|
||||
<Callout variant="info">Select a task or trigger</Callout>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function BlankTasks({
|
||||
status,
|
||||
basicStatus,
|
||||
}: {
|
||||
status: JobRunStatus;
|
||||
basicStatus: RunBasicStatus;
|
||||
}) {
|
||||
switch (basicStatus) {
|
||||
case "COMPLETED":
|
||||
return (
|
||||
<Paragraph variant="small">There were no tasks for this run.</Paragraph>
|
||||
);
|
||||
case "FAILED":
|
||||
return <Paragraph variant="small">No tasks were run.</Paragraph>;
|
||||
case "WAITING":
|
||||
case "PENDING":
|
||||
case "RUNNING":
|
||||
return (
|
||||
<div>
|
||||
<Paragraph variant="small" className="mb-4">
|
||||
Waiting for tasks…
|
||||
</Paragraph>
|
||||
<TaskCardSkeleton />
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Paragraph variant="small">There were no tasks for this run.</Paragraph>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function RerunPopover({
|
||||
environmentType,
|
||||
status,
|
||||
}: {
|
||||
environmentType: RuntimeEnvironmentType;
|
||||
status: RunBasicStatus;
|
||||
}) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild={true}>
|
||||
<Button variant="primary/small" shortcut={{ key: "R" }}>
|
||||
Rerun Job
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="flex w-80 flex-col gap-2 p-4" align="end">
|
||||
<Form method="post">
|
||||
{environmentType === "PRODUCTION" && (
|
||||
<Callout variant="warning">
|
||||
This will rerun this Job in your Production environment.
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-start gap-4 divide-y divide-slate-600">
|
||||
<div>
|
||||
<Button
|
||||
variant="primary/small"
|
||||
type="submit"
|
||||
name={conform.INTENT}
|
||||
value="start"
|
||||
fullWidth
|
||||
LeadingIcon={BoltIcon}
|
||||
>
|
||||
Run again
|
||||
</Button>
|
||||
|
||||
<Paragraph variant="extra-small" className="mt-2">
|
||||
Start a brand new job run with the same Trigger data as this
|
||||
one. This will re-do every task.
|
||||
</Paragraph>
|
||||
</div>
|
||||
{status === "FAILED" && (
|
||||
<div className="pt-4">
|
||||
<Button
|
||||
variant="primary/small"
|
||||
type="submit"
|
||||
name={conform.INTENT}
|
||||
value="continue"
|
||||
fullWidth
|
||||
LeadingIcon={ForwardIcon}
|
||||
>
|
||||
Retry job run
|
||||
</Button>
|
||||
|
||||
<Paragraph variant="extra-small" className="mt-2">
|
||||
Continue running this job run from where it left off. This
|
||||
will skip any task that has already been completed.
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ type RunOptions = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export type Run = NonNullable<Awaited<ReturnType<RunPresenter["call"]>>>;
|
||||
export type Task = NonNullable<
|
||||
Awaited<ReturnType<RunPresenter["call"]>>
|
||||
>["tasks"][number];
|
||||
|
||||
+14
-385
@@ -1,48 +1,15 @@
|
||||
import { conform } from "@conform-to/react";
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { BoltIcon, ForwardIcon } from "@heroicons/react/24/solid";
|
||||
import { Form, Outlet, useNavigate, useRevalidator } from "@remix-run/react";
|
||||
import { useRevalidator } from "@remix-run/react";
|
||||
import { ActionFunction, LoaderArgs, json } from "@remix-run/server-runtime";
|
||||
import type { RuntimeEnvironmentType } from "@trigger.dev/internal";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { useEventSource } from "remix-utils";
|
||||
import { z } from "zod";
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { Button } from "~/components/primitives/Buttons";
|
||||
import { Callout } from "~/components/primitives/Callout";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { NamedIcon } from "~/components/primitives/NamedIcon";
|
||||
import {
|
||||
PageButtons,
|
||||
PageHeader,
|
||||
PageInfoGroup,
|
||||
PageInfoProperty,
|
||||
PageInfoRow,
|
||||
PageTitle,
|
||||
PageTitleRow,
|
||||
} from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "~/components/primitives/Popover";
|
||||
import {
|
||||
RunBasicStatus,
|
||||
RunStatusIcon,
|
||||
RunStatusLabel,
|
||||
runBasicStatus,
|
||||
runStatusTitle,
|
||||
} from "~/components/runs/RunStatuses";
|
||||
import { RunOverview } from "~/components/run/RunOverview";
|
||||
import { runBasicStatus } from "~/components/runs/RunStatuses";
|
||||
import { useJob } from "~/hooks/useJob";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { usePathName } from "~/hooks/usePathName";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
import { JobRunStatus } from "~/models/job.server";
|
||||
import {
|
||||
redirectBackWithErrorMessage,
|
||||
redirectWithSuccessMessage,
|
||||
@@ -51,31 +18,14 @@ import { RunPresenter } from "~/presenters/RunPresenter.server";
|
||||
import { ContinueRunService } from "~/services/runs/continueRun.server";
|
||||
import { ReRunService } from "~/services/runs/reRun.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { formatDuration } from "~/utils";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import {
|
||||
RunParamsSchema,
|
||||
jobPath,
|
||||
runCompletedPath,
|
||||
jobRunDashboardPath,
|
||||
runPath,
|
||||
runStreamingPath,
|
||||
runTaskPath,
|
||||
runTriggerPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import {
|
||||
RunPanel,
|
||||
RunPanelBody,
|
||||
RunPanelDivider,
|
||||
RunPanelError,
|
||||
RunPanelHeader,
|
||||
RunPanelIconProperty,
|
||||
RunPanelIconSection,
|
||||
RunPanelIconTitle,
|
||||
RunPanelProperties,
|
||||
} from "../../components/run/RunCard";
|
||||
import { TaskCard } from "~/components/run/TaskCard";
|
||||
import { TaskCardSkeleton } from "~/components/run/TaskCardSkeleton";
|
||||
|
||||
export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -156,34 +106,11 @@ export const handle: Handle = {
|
||||
},
|
||||
};
|
||||
|
||||
const taskPattern = /\/tasks\/(.*)/;
|
||||
|
||||
export default function Page() {
|
||||
const { run } = useTypedLoaderData<typeof loader>();
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const job = useJob();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const pathName = usePathName();
|
||||
|
||||
const selectedId = useMemo(() => {
|
||||
if (pathName.endsWith("/completed")) {
|
||||
return "completed";
|
||||
}
|
||||
|
||||
if (pathName.endsWith("/trigger")) {
|
||||
return "trigger";
|
||||
}
|
||||
|
||||
const taskMatch = pathName.match(taskPattern);
|
||||
const taskId = taskMatch ? taskMatch[1] : undefined;
|
||||
if (taskId) {
|
||||
return taskId;
|
||||
}
|
||||
}, [pathName]);
|
||||
|
||||
const basicStatus = runBasicStatus(run.status);
|
||||
|
||||
const revalidator = useRevalidator();
|
||||
const events = useEventSource(
|
||||
@@ -200,312 +127,14 @@ export default function Page() {
|
||||
}, [events]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader>
|
||||
<PageTitleRow>
|
||||
<PageTitle
|
||||
backButton={{
|
||||
to: jobPath(organization, project, job),
|
||||
text: "Runs",
|
||||
}}
|
||||
title={`Run #${run.number}`}
|
||||
/>
|
||||
<PageButtons>
|
||||
{run.isTest && (
|
||||
<span className="flex items-center gap-1 text-xs uppercase text-slate-600">
|
||||
<NamedIcon name="beaker" className="h-4 w-4 text-slate-600" />
|
||||
Test run
|
||||
</span>
|
||||
)}
|
||||
<RerunPopover
|
||||
environmentType={run.environment.type}
|
||||
status={basicStatus}
|
||||
/>
|
||||
</PageButtons>
|
||||
</PageTitleRow>
|
||||
<PageInfoRow>
|
||||
<PageInfoGroup>
|
||||
<PageInfoProperty
|
||||
icon={<RunStatusIcon status={run.status} className="h-4 w-4" />}
|
||||
label={"Status"}
|
||||
value={runStatusTitle(run.status)}
|
||||
/>
|
||||
<PageInfoProperty
|
||||
icon={"calendar"}
|
||||
label={"Started"}
|
||||
value={
|
||||
run.startedAt ? (
|
||||
<DateTime date={run.startedAt} />
|
||||
) : (
|
||||
"Not started yet"
|
||||
)
|
||||
}
|
||||
/>
|
||||
<PageInfoProperty
|
||||
icon={"property"}
|
||||
label={"Version"}
|
||||
value={`v${run.version}`}
|
||||
/>
|
||||
<PageInfoProperty
|
||||
label={"Env"}
|
||||
value={<EnvironmentLabel environment={run.environment} />}
|
||||
/>
|
||||
<PageInfoProperty
|
||||
icon={"clock"}
|
||||
label={"Duration"}
|
||||
value={formatDuration(run.startedAt, run.completedAt)}
|
||||
/>
|
||||
</PageInfoGroup>
|
||||
<PageInfoGroup alignment="right">
|
||||
<Paragraph variant="extra-small" className="text-slate-600">
|
||||
RUN ID: {run.id}
|
||||
</Paragraph>
|
||||
</PageInfoGroup>
|
||||
</PageInfoRow>
|
||||
</PageHeader>
|
||||
<PageBody scrollable={false}>
|
||||
<div className="grid h-full grid-cols-2 gap-2">
|
||||
<div className="flex flex-col gap-6 overflow-y-auto py-4 pl-4 pr-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<div>
|
||||
<Header2 className="mb-2">Trigger</Header2>
|
||||
<RunPanel
|
||||
selected={selectedId === "trigger"}
|
||||
onClick={() =>
|
||||
navigate(runTriggerPath(organization, project, job, run))
|
||||
}
|
||||
>
|
||||
<RunPanelHeader
|
||||
icon={<BoltIcon className="h-5 w-5 text-orange-500" />}
|
||||
title={
|
||||
<RunPanelIconTitle
|
||||
icon={job.event.icon}
|
||||
title={job.event.title}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<RunPanelBody>
|
||||
<RunPanelProperties
|
||||
properties={[
|
||||
{ label: "Event name", text: run.event.name },
|
||||
...run.properties,
|
||||
]}
|
||||
/>
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
</div>
|
||||
<div>
|
||||
<Header2 className="mb-2">Tasks</Header2>
|
||||
|
||||
{run.tasks.length > 0 ? (
|
||||
run.tasks.map((task, index) => {
|
||||
const isLast = index === run.tasks.length - 1;
|
||||
|
||||
return (
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
selectedId={selectedId}
|
||||
selectedTask={(taskId) => {
|
||||
navigate(
|
||||
runTaskPath(organization, project, job, run, taskId)
|
||||
);
|
||||
}}
|
||||
isLast={isLast}
|
||||
depth={0}
|
||||
{...task}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<BlankTasks status={run.status} basicStatus={basicStatus} />
|
||||
)}
|
||||
</div>
|
||||
{(basicStatus === "COMPLETED" || basicStatus === "FAILED") && (
|
||||
<div>
|
||||
<Header2 className={cn("mb-2")}>Run Summary</Header2>
|
||||
<RunPanel
|
||||
selected={selectedId === "completed"}
|
||||
onClick={() =>
|
||||
navigate(runCompletedPath(organization, project, job, run))
|
||||
}
|
||||
>
|
||||
<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}
|
||||
maxLines={10}
|
||||
/>
|
||||
) : (
|
||||
run.output === null && (
|
||||
<Paragraph variant="small">
|
||||
This Run returned nothing.
|
||||
</Paragraph>
|
||||
)
|
||||
)}
|
||||
</RunPanelBody>
|
||||
</RunPanel>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Detail view */}
|
||||
<div className="overflow-y-auto py-4 pr-4 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-slate-700">
|
||||
<Header2 className="mb-2">Detail</Header2>
|
||||
{selectedId ? (
|
||||
<Outlet />
|
||||
) : (
|
||||
<Callout variant="info">Select a task or trigger</Callout>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PageBody>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function BlankTasks({
|
||||
status,
|
||||
basicStatus,
|
||||
}: {
|
||||
status: JobRunStatus;
|
||||
basicStatus: RunBasicStatus;
|
||||
}) {
|
||||
switch (basicStatus) {
|
||||
case "COMPLETED":
|
||||
return (
|
||||
<Paragraph variant="small">There were no tasks for this run.</Paragraph>
|
||||
);
|
||||
case "FAILED":
|
||||
return <Paragraph variant="small">No tasks were run.</Paragraph>;
|
||||
case "WAITING":
|
||||
case "PENDING":
|
||||
case "RUNNING":
|
||||
return (
|
||||
<div>
|
||||
<Paragraph variant="small" className="mb-4">
|
||||
Waiting for tasks…
|
||||
</Paragraph>
|
||||
<TaskCardSkeleton />
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Paragraph variant="small">There were no tasks for this run.</Paragraph>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function RerunPopover({
|
||||
environmentType,
|
||||
status,
|
||||
}: {
|
||||
environmentType: RuntimeEnvironmentType;
|
||||
status: RunBasicStatus;
|
||||
}) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild={true}>
|
||||
<Button variant="primary/small" shortcut={{ key: "R" }}>
|
||||
Rerun Job
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="flex w-80 flex-col gap-2 p-4" align="end">
|
||||
<Form method="post">
|
||||
{environmentType === "PRODUCTION" && (
|
||||
<Callout variant="warning">
|
||||
This will rerun this Job in your Production environment.
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-start gap-4 divide-y divide-slate-600">
|
||||
<div>
|
||||
<Button
|
||||
variant="primary/small"
|
||||
type="submit"
|
||||
name={conform.INTENT}
|
||||
value="start"
|
||||
fullWidth
|
||||
LeadingIcon={BoltIcon}
|
||||
>
|
||||
Run again
|
||||
</Button>
|
||||
|
||||
<Paragraph variant="extra-small" className="mt-2">
|
||||
Start a brand new job run with the same Trigger data as this
|
||||
one. This will re-do every task.
|
||||
</Paragraph>
|
||||
</div>
|
||||
{status === "FAILED" && (
|
||||
<div className="pt-4">
|
||||
<Button
|
||||
variant="primary/small"
|
||||
type="submit"
|
||||
name={conform.INTENT}
|
||||
value="continue"
|
||||
fullWidth
|
||||
LeadingIcon={ForwardIcon}
|
||||
>
|
||||
Retry job run
|
||||
</Button>
|
||||
|
||||
<Paragraph variant="extra-small" className="mt-2">
|
||||
Continue running this job run from where it left off. This
|
||||
will skip any task that has already been completed.
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<RunOverview
|
||||
run={run}
|
||||
trigger={job.event}
|
||||
showRerun={true}
|
||||
paths={{
|
||||
back: jobPath(organization, project, job),
|
||||
run: runPath(organization, project, job, run),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ export function jobRunsParentPath(
|
||||
return `${jobPath(organization, project, job)}/runs`;
|
||||
}
|
||||
|
||||
function runPath(
|
||||
export function runPath(
|
||||
organization: OrgForPath,
|
||||
project: ProjectForPath,
|
||||
job: JobForPath,
|
||||
@@ -259,7 +259,7 @@ export function jobRunDashboardPath(
|
||||
job: JobForPath,
|
||||
run: RunForPath
|
||||
) {
|
||||
return runTriggerPath(organization, project, job, run);
|
||||
return runTriggerPath(runPath(organization, project, job, run));
|
||||
}
|
||||
|
||||
export function runStreamingPath(
|
||||
@@ -276,34 +276,18 @@ export function runParam(run: RunForPath) {
|
||||
}
|
||||
|
||||
// Task
|
||||
export function runTaskPath(
|
||||
organization: OrgForPath,
|
||||
project: ProjectForPath,
|
||||
job: JobForPath,
|
||||
run: RunForPath,
|
||||
taskId: string
|
||||
) {
|
||||
return `${runPath(organization, project, job, run)}/tasks/${taskId}`;
|
||||
export function runTaskPath(runPath: string, taskId: string) {
|
||||
return `${runPath}/tasks/${taskId}`;
|
||||
}
|
||||
|
||||
// Event
|
||||
export function runTriggerPath(
|
||||
organization: OrgForPath,
|
||||
project: ProjectForPath,
|
||||
job: JobForPath,
|
||||
run: RunForPath
|
||||
) {
|
||||
return `${runPath(organization, project, job, run)}/trigger`;
|
||||
export function runTriggerPath(runPath: string) {
|
||||
return `${runPath}/trigger`;
|
||||
}
|
||||
|
||||
// Event
|
||||
export function runCompletedPath(
|
||||
organization: OrgForPath,
|
||||
project: ProjectForPath,
|
||||
job: JobForPath,
|
||||
run: RunForPath
|
||||
) {
|
||||
return `${runPath(organization, project, job, run)}/completed`;
|
||||
export function runCompletedPath(runPath: string) {
|
||||
return `${runPath}/completed`;
|
||||
}
|
||||
|
||||
// Docs
|
||||
|
||||
Reference in New Issue
Block a user