From eae0b8a95e3795f2ffe5ac037b244c8fecb76d00 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 10 Jul 2023 15:20:01 +0100 Subject: [PATCH] The Run page now uses a component so it can be shared --- .../webapp/app/components/run/RunOverview.tsx | 402 ++++++++++++++++++ .../app/presenters/RunPresenter.server.ts | 1 + .../route.tsx | 399 +---------------- apps/webapp/app/utils/pathBuilder.ts | 32 +- 4 files changed, 425 insertions(+), 409 deletions(-) create mode 100644 apps/webapp/app/components/run/RunOverview.tsx diff --git a/apps/webapp/app/components/run/RunOverview.tsx b/apps/webapp/app/components/run/RunOverview.tsx new file mode 100644 index 000000000..bb31546a4 --- /dev/null +++ b/apps/webapp/app/components/run/RunOverview.tsx @@ -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 ( + + + + + + {run.isTest && ( + + + Test run + + )} + {showRerun && ( + + )} + + + + + } + label={"Status"} + value={runStatusTitle(run.status)} + /> + + ) : ( + "Not started yet" + ) + } + /> + + } + /> + + + + + RUN ID: {run.id} + + + + + +
+
+
+ Trigger + navigate(runTriggerPath(paths.run))} + > + } + title={ + + } + /> + + + + +
+
+ Tasks + + {run.tasks.length > 0 ? ( + run.tasks.map((task, index) => { + const isLast = index === run.tasks.length - 1; + + return ( + { + navigate(runTaskPath(paths.run, taskId)); + }} + isLast={isLast} + depth={0} + {...task} + /> + ); + }) + ) : ( + + )} +
+ {(basicStatus === "COMPLETED" || basicStatus === "FAILED") && ( +
+ Run Summary + navigate(runCompletedPath(paths.run))} + > + + } + title={ + + + + } + /> + + + {run.startedAt && ( + } + /> + )} + {run.completedAt && ( + } + /> + )} + {run.startedAt && run.completedAt && ( + + )} + + + {run.error && ( + + )} + {run.output ? ( + + ) : ( + run.output === null && ( + + This Run returned nothing. + + ) + )} + + +
+ )} +
+ + {/* Detail view */} +
+ Detail + {selectedId ? ( + + ) : ( + Select a task or trigger + )} +
+
+
+
+ ); +} + +function BlankTasks({ + status, + basicStatus, +}: { + status: JobRunStatus; + basicStatus: RunBasicStatus; +}) { + switch (basicStatus) { + case "COMPLETED": + return ( + There were no tasks for this run. + ); + case "FAILED": + return No tasks were run.; + case "WAITING": + case "PENDING": + case "RUNNING": + return ( +
+ + Waiting for tasks… + + +
+ ); + default: + return ( + There were no tasks for this run. + ); + } +} + +function RerunPopover({ + environmentType, + status, +}: { + environmentType: RuntimeEnvironmentType; + status: RunBasicStatus; +}) { + return ( + + + + + +
+ {environmentType === "PRODUCTION" && ( + + This will rerun this Job in your Production environment. + + )} + +
+
+ + + + Start a brand new job run with the same Trigger data as this + one. This will re-do every task. + +
+ {status === "FAILED" && ( +
+ + + + Continue running this job run from where it left off. This + will skip any task that has already been completed. + +
+ )} +
+
+
+
+ ); +} diff --git a/apps/webapp/app/presenters/RunPresenter.server.ts b/apps/webapp/app/presenters/RunPresenter.server.ts index fb8ef050e..1dce328ec 100644 --- a/apps/webapp/app/presenters/RunPresenter.server.ts +++ b/apps/webapp/app/presenters/RunPresenter.server.ts @@ -12,6 +12,7 @@ type RunOptions = { userId: string; }; +export type Run = NonNullable>>; export type Task = NonNullable< Awaited> >["tasks"][number]; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route.tsx index b01bf1adf..f0baa8c8d 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.jobs.$jobParam.runs.$runParam/route.tsx @@ -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(); 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 ( - - - - - - {run.isTest && ( - - - Test run - - )} - - - - - - } - label={"Status"} - value={runStatusTitle(run.status)} - /> - - ) : ( - "Not started yet" - ) - } - /> - - } - /> - - - - - RUN ID: {run.id} - - - - - -
-
-
- Trigger - - navigate(runTriggerPath(organization, project, job, run)) - } - > - } - title={ - - } - /> - - - - -
-
- Tasks - - {run.tasks.length > 0 ? ( - run.tasks.map((task, index) => { - const isLast = index === run.tasks.length - 1; - - return ( - { - navigate( - runTaskPath(organization, project, job, run, taskId) - ); - }} - isLast={isLast} - depth={0} - {...task} - /> - ); - }) - ) : ( - - )} -
- {(basicStatus === "COMPLETED" || basicStatus === "FAILED") && ( -
- Run Summary - - navigate(runCompletedPath(organization, project, job, run)) - } - > - - } - title={ - - - - } - /> - - - {run.startedAt && ( - } - /> - )} - {run.completedAt && ( - } - /> - )} - {run.startedAt && run.completedAt && ( - - )} - - - {run.error && ( - - )} - {run.output ? ( - - ) : ( - run.output === null && ( - - This Run returned nothing. - - ) - )} - - -
- )} -
- - {/* Detail view */} -
- Detail - {selectedId ? ( - - ) : ( - Select a task or trigger - )} -
-
-
-
- ); -} - -function BlankTasks({ - status, - basicStatus, -}: { - status: JobRunStatus; - basicStatus: RunBasicStatus; -}) { - switch (basicStatus) { - case "COMPLETED": - return ( - There were no tasks for this run. - ); - case "FAILED": - return No tasks were run.; - case "WAITING": - case "PENDING": - case "RUNNING": - return ( -
- - Waiting for tasks… - - -
- ); - default: - return ( - There were no tasks for this run. - ); - } -} - -function RerunPopover({ - environmentType, - status, -}: { - environmentType: RuntimeEnvironmentType; - status: RunBasicStatus; -}) { - return ( - - - - - -
- {environmentType === "PRODUCTION" && ( - - This will rerun this Job in your Production environment. - - )} - -
-
- - - - Start a brand new job run with the same Trigger data as this - one. This will re-do every task. - -
- {status === "FAILED" && ( -
- - - - Continue running this job run from where it left off. This - will skip any task that has already been completed. - -
- )} -
-
-
-
+ ); } diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index 96604ef19..fac248a05 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -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