From 3d8ea667c8296b73b1fc6d1d263a787d7b9a9237 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 30 Aug 2024 17:35:33 +0100 Subject: [PATCH] =?UTF-8?q?More=20major=20overhauls=20to=20get=20the=20syn?= =?UTF-8?q?ced=20version=20of=20the=20run=20page=20working=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/runs/v3/RunInspector.tsx | 715 ++++++++++++++++ .../app/components/runs/v3/SpanInspector.tsx | 6 +- apps/webapp/app/hooks/useSyncRun.ts | 17 - apps/webapp/app/hooks/useSyncRunPage.ts | 12 +- apps/webapp/app/hooks/useSyncTrace.ts | 7 +- apps/webapp/app/hooks/useSyncTraceRuns.ts | 19 + apps/webapp/app/hooks/useSyncedShape.ts | 69 ++ .../app/presenters/v3/RunPresenter.server.ts | 1 + .../route.tsx | 100 ++- .../route.tsx | 780 ------------------ .../webapp/app/routes/sync.traces.$traceId.ts | 2 +- ...$runId.ts => sync.traces.runs.$traceId.ts} | 12 +- 12 files changed, 898 insertions(+), 842 deletions(-) delete mode 100644 apps/webapp/app/hooks/useSyncRun.ts create mode 100644 apps/webapp/app/hooks/useSyncTraceRuns.ts create mode 100644 apps/webapp/app/hooks/useSyncedShape.ts delete mode 100644 apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx rename apps/webapp/app/routes/{sync.runs.$runId.ts => sync.traces.runs.$traceId.ts} (83%) diff --git a/apps/webapp/app/components/runs/v3/RunInspector.tsx b/apps/webapp/app/components/runs/v3/RunInspector.tsx index e69de29bb..0f563f638 100644 --- a/apps/webapp/app/components/runs/v3/RunInspector.tsx +++ b/apps/webapp/app/components/runs/v3/RunInspector.tsx @@ -0,0 +1,715 @@ +import { CheckIcon, ClockIcon, CloudArrowDownIcon, QueueListIcon } from "@heroicons/react/20/solid"; +import { + formatDuration, + formatDurationMilliseconds, + nanosecondsToMilliseconds, + TaskRunError, +} from "@trigger.dev/core/v3"; +import { TaskRun } from "@trigger.dev/database"; +import { ReactNode } from "react"; +import { ExitIcon } from "~/assets/icons/ExitIcon"; +import { CodeBlock } from "~/components/code/CodeBlock"; +import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; +import { Button, LinkButton } from "~/components/primitives/Buttons"; +import { Callout } from "~/components/primitives/Callout"; +import { DateTime, DateTimeAccurate } from "~/components/primitives/DateTime"; +import { Header2, Header3 } from "~/components/primitives/Headers"; +import { Paragraph } from "~/components/primitives/Paragraph"; +import * as Property from "~/components/primitives/PropertyTable"; +import { Spinner } from "~/components/primitives/Spinner"; +import { TabButton, TabContainer } from "~/components/primitives/Tabs"; +import { TextLink } from "~/components/primitives/TextLink"; +import { SimpleTooltip } from "~/components/primitives/Tooltip"; +import { LiveTimer } from "~/components/runs/v3/LiveTimer"; +import { RunIcon } from "~/components/runs/v3/RunIcon"; +import { useOrganization } from "~/hooks/useOrganizations"; +import { useProject } from "~/hooks/useProject"; +import { useSearchParams } from "~/hooks/useSearchParam"; +import { RawRun } from "~/hooks/useSyncTraceRuns"; +import { cn } from "~/utils/cn"; +import { formatCurrencyAccurate } from "~/utils/numberFormatter"; +import { + v3RunDownloadLogsPath, + v3RunPath, + v3RunSpanPath, + v3RunsPath, + v3TraceSpanPath, +} from "~/utils/pathBuilder"; +import { SpanLink } from "~/v3/eventRepository.server"; +import { isFinalRunStatus } from "~/v3/taskStatus"; +import { TaskRunStatusCombo } from "./TaskRunStatus"; + +/** + * The RunInspector displays live information about a run. + * Most of that data comes in as params but for some we need to fetch it. + */ +export function RunInspector({ + run, + runParam, + closePanel, +}: { + run?: RawRun; + runParam: string; + closePanel?: () => void; +}) { + const organization = useOrganization(); + const project = useProject(); + const { value, replace } = useSearchParams(); + const tab = value("tab"); + + if (!run) { + return ( +
+
+
+ + + + +
+ {closePanel && ( +
+
+
+ ); + } + + const environment = project.environments.find((e) => e.id === run.runtimeEnvironmentId); + + return ( +
+
+
+ + + {run.taskIdentifier} + +
+ {closePanel && ( +
+
+ + { + replace({ tab: "overview" }); + }} + shortcut={{ key: "o" }} + > + Overview + + { + replace({ tab: "detail" }); + }} + shortcut={{ key: "d" }} + > + Detail + + { + replace({ tab: "context" }); + }} + shortcut={{ key: "c" }} + > + Context + + +
+
+
+ {tab === "detail" ? ( +
+ + + Status + + {run ? : } + + + + Task + + + {run.taskIdentifier} + + } + content={`Filter runs by ${run.taskIdentifier}`} + /> + + + + Version + {/* + {run.version ? ( + run.version + ) : ( + + Never started + + + )} + */} + + + SDK version + {/* + {run.sdkVersion ? ( + run.sdkVersion + ) : ( + + Never started + + + )} + */} + + + Test run + + {run.isTest ? : "–"} + + + {environment && ( + + Environment + + + + + )} + {/* {run.schedule && ( + + Schedule + +
+
+ {run.schedule.generatorExpression} + ({run.schedule.timezone}) +
+ + {run.schedule.description} + + } + content={`Go to schedule ${run.schedule.friendlyId}`} + /> +
+
+
+ )} */} + + Queue + {/* +
Name: {run.queue.name}
+
+ Concurrency key: {run.queue.concurrencyKey ? run.queue.concurrencyKey : "–"} +
+
*/} +
+ + Time to live (TTL) + {run.ttl ?? "–"} + + + Tags + {/* + {run.tags.length === 0 ? ( + "–" + ) : ( +
+ {run.tags.map((tag) => ( + + + + } + content={`Filter runs by ${tag}`} + /> + ))} +
+ )} +
*/} +
+ {/* {run.links && run.links.length > 0 && ( + + Links + +
+ {run.links.map((link, index) => ( + + ))} +
+
+
+ )} */} + + Run invocation cost + + {run.baseCostInCents > 0 + ? formatCurrencyAccurate(run.baseCostInCents / 100) + : "–"} + + + + Compute cost + + {run.costInCents > 0 ? formatCurrencyAccurate(run.costInCents / 100) : "–"} + + + + Total cost + + {run.costInCents > 0 + ? formatCurrencyAccurate((run.baseCostInCents + run.costInCents) / 100) + : "–"} + + + + Usage duration + + {run.usageDurationMs > 0 + ? formatDurationMilliseconds(run.usageDurationMs, { style: "short" }) + : "–"} + + +
+
+ ) : tab === "context" ? ( +
+ {/* */} +
+ ) : ( +
+
+ +
+ + {run.payload !== undefined && ( + + )} + {/* {run.error !== undefined ? ( + + ) : run.output !== undefined ? ( + + ) : null} */} +
+ )} +
+
+
+
+ {run.friendlyId !== runParam && ( + + Focus on run + + )} +
+
+ {run.logsDeletedAt === null ? ( + + Download logs + + ) : null} +
+
+
+ ); +} + +function PropertyLoading() { + return ; +} + +function RunTimeline({ run }: { run: RawRun }) { + const createdAt = new Date(run.createdAt); + const startedAt = run.startedAt ? new Date(run.startedAt) : null; + const delayUntil = run.delayUntil ? new Date(run.delayUntil) : null; + const expiredAt = run.expiredAt ? new Date(run.expiredAt) : null; + const updatedAt = new Date(run.updatedAt); + + const isFinished = isFinalRunStatus(run.status); + + return ( +
+ } + state="complete" + /> + {delayUntil && !expiredAt ? ( + {formatDuration(createdAt, delayUntil)} delay + ) : ( + + + + Delayed until {run.ttl && <>(TTL {run.ttl})} + + + ) + } + state={run.startedAt ? "complete" : "delayed"} + /> + ) : startedAt ? ( + + ) : ( + + {" "} + {run.ttl && <>(TTL {run.ttl})} + + } + state={run.startedAt || run.expiredAt ? "complete" : "inprogress"} + /> + )} + {expiredAt ? ( + } + state="error" + /> + ) : startedAt ? ( + <> + } + state="complete" + /> + {isFinished ? ( + <> + + } + state="complete" + /> + + ) : ( + + + + + + + } + state={"inprogress"} + /> + )} + + ) : null} +
+ ); +} + +type RunTimelineItemProps = { + title: ReactNode; + subtitle?: ReactNode; + state: "complete" | "error"; +}; + +function RunTimelineEvent({ title, subtitle, state }: RunTimelineItemProps) { + return ( +
+
+
+
+
+ {title} + {subtitle ? {subtitle} : null} +
+
+ ); +} + +type RunTimelineLineProps = { + title: ReactNode; + state: "complete" | "delayed" | "inprogress"; +}; + +function RunTimelineLine({ title, state }: RunTimelineLineProps) { + return ( +
+
+
+
+
+ {title} +
+
+ ); +} + +function RunError({ error }: { error: TaskRunError }) { + switch (error.type) { + case "STRING_ERROR": + case "CUSTOM_ERROR": { + return ( +
+ +
+ ); + } + case "BUILT_IN_ERROR": + case "INTERNAL_ERROR": { + const name = "name" in error ? error.name : error.code; + return ( +
+ {name} + {error.message && {error.message}} + {error.stackTrace && ( + + )} +
+ ); + } + } +} + +function PacketDisplay({ + data, + dataType, + title, +}: { + data: string; + dataType: string; + title: string; +}) { + switch (dataType) { + case "application/store": { + return ( +
+ + {title} + + + Download + +
+ ); + } + case "text/plain": { + return ( + + ); + } + default: { + return ( + + ); + } + } +} + +type TimelineProps = { + startTime: Date; + duration: number; + inProgress: boolean; + isError: boolean; +}; + +type TimelineState = "error" | "pending" | "complete"; + +function SpanTimeline({ startTime, duration, inProgress, isError }: TimelineProps) { + const state = isError ? "error" : inProgress ? "pending" : "complete"; + return ( + <> +
+ } + state="complete" + /> + {state === "pending" ? ( + + + + + + + } + state={"inprogress"} + /> + ) : ( + <> + + + } + state={isError ? "error" : "complete"} + /> + + )} +
+ + ); +} + +function VerticalBar({ state }: { state: TimelineState }) { + return
; +} + +function DottedLine() { + return ( +
+
+
+
+
+
+ ); +} + +function classNameForState(state: TimelineState) { + switch (state) { + case "pending": { + return "bg-pending"; + } + case "complete": { + return "bg-success"; + } + case "error": { + return "bg-error"; + } + } +} + +function SpanLinkElement({ link }: { link: SpanLink }) { + const organization = useOrganization(); + const project = useProject(); + + switch (link.type) { + case "run": { + return ( + + {link.title} + + ); + } + case "span": { + return ( + + {link.title} + + ); + } + } + + return null; +} diff --git a/apps/webapp/app/components/runs/v3/SpanInspector.tsx b/apps/webapp/app/components/runs/v3/SpanInspector.tsx index 32b8e62e8..eb0cd79ab 100644 --- a/apps/webapp/app/components/runs/v3/SpanInspector.tsx +++ b/apps/webapp/app/components/runs/v3/SpanInspector.tsx @@ -28,7 +28,7 @@ export function SpanInspector({ runParam, closePanel, }: { - span: TraceSpan; + span?: TraceSpan; runParam?: string; closePanel?: () => void; }) { @@ -41,6 +41,10 @@ export function SpanInspector({ tab = "overview"; } + if (span === undefined) { + return null; + } + return (
diff --git a/apps/webapp/app/hooks/useSyncRun.ts b/apps/webapp/app/hooks/useSyncRun.ts deleted file mode 100644 index 6ec5795a4..000000000 --- a/apps/webapp/app/hooks/useSyncRun.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useShape } from "@electric-sql/react"; -import { TaskRun } from "@trigger.dev/database"; - -type Params = { - origin: string; - runId: string; -}; - -export function useSyncRun({ origin, runId }: Params) { - const { isUpToDate, data } = useShape({ - url: `${origin}/sync/runs/${runId}`, - }); - - const run = (data as unknown as TaskRun[])?.at(0); - - return { isUpToDate, run }; -} diff --git a/apps/webapp/app/hooks/useSyncRunPage.ts b/apps/webapp/app/hooks/useSyncRunPage.ts index 33a6b5daf..a07c41bf5 100644 --- a/apps/webapp/app/hooks/useSyncRunPage.ts +++ b/apps/webapp/app/hooks/useSyncRunPage.ts @@ -1,18 +1,16 @@ -import { useSyncRun } from "./useSyncRun"; import { useSyncTrace } from "./useSyncTrace"; +import { useSyncTraceRuns } from "./useSyncTraceRuns"; type Params = { origin: string; - runId: string; traceId: string; - spanId: string; }; -export function useSyncRunPage({ origin, runId, traceId, spanId }: Params) { - const { isUpToDate: isRunUpToDate, run } = useSyncRun({ origin, runId }); - const { isUpToDate: isTraceUpToDate, events } = useSyncTrace({ origin, traceId, spanId }); +export function useSyncRunPage({ origin, traceId }: Params) { + const { isUpToDate: isRunUpToDate, runs } = useSyncTraceRuns({ origin, traceId }); + const { isUpToDate: isTraceUpToDate, events } = useSyncTrace({ origin, traceId }); const isUpToDate = isRunUpToDate && isTraceUpToDate; - return { isUpToDate, isRunUpToDate, isTraceUpToDate, run, events }; + return { isUpToDate, isRunUpToDate, isTraceUpToDate, runs, events }; } diff --git a/apps/webapp/app/hooks/useSyncTrace.ts b/apps/webapp/app/hooks/useSyncTrace.ts index 5e4c29469..111d5eb33 100644 --- a/apps/webapp/app/hooks/useSyncTrace.ts +++ b/apps/webapp/app/hooks/useSyncTrace.ts @@ -5,16 +5,15 @@ import { createTraceTreeFromEvents } from "~/utils/taskEvent"; type Params = { origin: string; traceId: string; - spanId: string; }; export type Trace = ReturnType; -export type TraceEvent = NonNullable; +export type TraceEvent = Trace["events"][number]; -export function useSyncTrace({ origin, traceId, spanId }: Params) { +export function useSyncTrace({ origin, traceId }: Params) { const { isUpToDate, data } = useShape({ url: `${origin}/sync/traces/${traceId}`, }); - return { isUpToDate, events: data ? (data as unknown as TaskEvent[]) : undefined }; + return { isUpToDate, events: data ? (data as any as TaskEvent[]) : undefined }; } diff --git a/apps/webapp/app/hooks/useSyncTraceRuns.ts b/apps/webapp/app/hooks/useSyncTraceRuns.ts new file mode 100644 index 000000000..6411b2c0d --- /dev/null +++ b/apps/webapp/app/hooks/useSyncTraceRuns.ts @@ -0,0 +1,19 @@ +import { useShape } from "@electric-sql/react"; +import { TaskRun } from "@trigger.dev/database"; +import { SyncedShapeData, useSyncedShape } from "./useSyncedShape"; +import { Prettify } from "@trigger.dev/core"; + +type Params = { + origin: string; + traceId: string; +}; + +export type RawRun = Prettify>; + +export function useSyncTraceRuns({ origin, traceId }: Params) { + const { isUpToDate, data } = useSyncedShape({ + url: `${origin}/sync/traces/runs/${traceId}`, + }); + + return { isUpToDate, runs: data }; +} diff --git a/apps/webapp/app/hooks/useSyncedShape.ts b/apps/webapp/app/hooks/useSyncedShape.ts new file mode 100644 index 000000000..93e014133 --- /dev/null +++ b/apps/webapp/app/hooks/useSyncedShape.ts @@ -0,0 +1,69 @@ +import { useShape } from "@electric-sql/react"; + +export type ShapeInput = Parameters[0]; +export type ShapeOutput = { + isError: boolean; + isUpToDate: boolean; + data: S[]; +}; + +export type SyncedShapeData = { + [K in keyof T]: T[K] extends Date + ? string + : T[K] extends Date | null + ? string | null + : T[K] extends BigInt + ? number + : T[K] extends BigInt | null + ? number | null + : T[K] extends object + ? SyncedShapeData + : T[K]; +}; + +export function useSyncedShape(props: ShapeInput): ShapeOutput> { + const output = useShape(props) as any; + + return { + isError: output.isError, + isUpToDate: output.isUpToDate, + data: transformInput(output.data as InputObject) as SyncedShapeData[], + }; +} + +type InputObject = { + [key: string]: Value; +}[]; + +type Value = + | string + | number + | boolean + | bigint + | null + | Value[] + | { + [key: string]: Value; + }; + +function transformInput(input: InputObject): SyncedShapeData { + return input.map((value) => transformValue(value)); +} + +function transformValue(value: Value): Value { + if (Array.isArray(value)) { + return value.map(transformValue); + } else if (typeof value === "object" && value !== null) { + const result: { [key: string]: Value } = {}; + + for (const key in value) { + result[key] = transformValue(value[key]); + } + + return result; + } else if (typeof value === "bigint") { + return Number(value); + } else { + return value; + } +} diff --git a/apps/webapp/app/presenters/v3/RunPresenter.server.ts b/apps/webapp/app/presenters/v3/RunPresenter.server.ts index 2527c455a..7e1e3ef1b 100644 --- a/apps/webapp/app/presenters/v3/RunPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/RunPresenter.server.ts @@ -72,6 +72,7 @@ export class RunPresenter { isFinished: isFinalRunStatus(run.status), completedAt: run.completedAt, logsDeletedAt: run.logsDeletedAt, + environment: { id: run.runtimeEnvironment.id, organizationId: run.runtimeEnvironment.organizationId, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx index ef5d715a6..622e3b1f6 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam/route.tsx @@ -17,7 +17,7 @@ import { millisecondsToNanoseconds, nanosecondsToMilliseconds, } from "@trigger.dev/core/v3"; -import { RuntimeEnvironmentType, TaskRun } from "@trigger.dev/database"; +import { RuntimeEnvironmentType } from "@trigger.dev/database"; import { motion } from "framer-motion"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; @@ -55,6 +55,8 @@ import { NodesState } from "~/components/primitives/TreeView/reducer"; import { CancelRunDialog } from "~/components/runs/v3/CancelRunDialog"; import { ReplayRunDialog } from "~/components/runs/v3/ReplayRunDialog"; import { RunIcon } from "~/components/runs/v3/RunIcon"; +import { RunInspector } from "~/components/runs/v3/RunInspector"; +import { SpanInspector } from "~/components/runs/v3/SpanInspector"; import { SpanTitle, eventBackgroundClassName } from "~/components/runs/v3/SpanTitle"; import { TaskRunStatusIcon, runStatusClassNameColor } from "~/components/runs/v3/TaskRunStatus"; import { useAppOrigin } from "~/hooks/useAppOrigin"; @@ -64,9 +66,11 @@ import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useReplaceLocation } from "~/hooks/useReplaceLocation"; import { Shortcut, useShortcutKeys } from "~/hooks/useShortcutKeys"; +import { useSyncRunPage } from "~/hooks/useSyncRunPage"; import { Trace, TraceEvent } from "~/hooks/useSyncTrace"; +import { RawRun } from "~/hooks/useSyncTraceRuns"; import { useUser } from "~/hooks/useUser"; -import { RunPresenter } from "~/presenters/v3/RunPresenter.server"; +import { Run, RunPresenter } from "~/presenters/v3/RunPresenter.server"; import { getResizableSnapshot } from "~/services/resizablePanel.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; @@ -78,11 +82,13 @@ import { v3RunSpanPath, v3RunsPath, } from "~/utils/pathBuilder"; +import { + TraceSpan, + createSpanFromEvents, + createTraceTreeFromEvents, + prepareTrace, +} from "~/utils/taskEvent"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; -import { SpanView } from "../resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route"; -import { SpanInspector } from "~/components/runs/v3/SpanInspector"; -import { createSpanFromEvents, createTraceTreeFromEvents, prepareTrace } from "~/utils/taskEvent"; -import { useSyncRunPage } from "~/hooks/useSyncRunPage"; const resizableSettings = { parent: { @@ -246,21 +252,28 @@ export default function Page() { ); } +type InspectorState = + | { + type: "span"; + span?: TraceSpan; + } + | { + type: "run"; + run?: RawRun; + } + | undefined; + function Panels({ resizable, run: originalRun }: LoaderData) { const { location, replaceSearchParam } = useReplaceLocation(); const selectedSpanId = getSpanId(location); const appOrigin = useAppOrigin(); - const { isUpToDate, events, run } = useSyncRunPage({ + const { isUpToDate, events, runs } = useSyncRunPage({ origin: appOrigin, - runId: originalRun.id, traceId: originalRun.traceId, - spanId: originalRun.spanId, }); - const initialLoad = !isUpToDate || !run; - - console.log(run); + const initialLoad = !isUpToDate || !runs; const trace = useMemo(() => { if (!events) return undefined; @@ -269,16 +282,39 @@ function Panels({ resizable, run: originalRun }: LoaderData) { return createTraceTreeFromEvents(preparedEvents, originalRun.spanId); }, [events]); - const inspectorSpanId = selectedSpanId - ? selectedSpanId - : originalRun.logsDeletedAt - ? originalRun.spanId - : undefined; + const inspectorState = useMemo(() => { + if (originalRun.logsDeletedAt) { + return { + type: "run", + run: runs?.find((r) => r.friendlyId === originalRun.friendlyId), + }; + } - const selectedSpan = useMemo(() => { - if (!selectedSpanId || !events) return undefined; - return createSpanFromEvents(events, selectedSpanId); - }, [selectedSpanId]); + if (selectedSpanId) { + if (runs && runs.length > 0) { + const spanRun = runs.find((r) => r.spanId === selectedSpanId); + if (spanRun) { + return { + type: "run", + run: spanRun, + }; + } + } + + if (!events) { + return { + type: "span", + span: undefined, + }; + } + + const span = createSpanFromEvents(events, selectedSpanId); + return { + type: "span", + span, + }; + } + }, [selectedSpanId, runs, events]); return ( ) : ( - {inspectorSpanId ? ( + {inspectorState ? ( - {selectedSpan && run ? ( + {inspectorState.type === "span" ? ( replaceSearchParam("span") : undefined} + runParam={originalRun.friendlyId} + span={inspectorState.span} + closePanel={!originalRun.logsDeletedAt ? () => replaceSearchParam("span") : undefined} + /> + ) : inspectorState.type === "run" ? ( + replaceSearchParam("span") : undefined} /> ) : null} @@ -321,7 +363,7 @@ function Panels({ resizable, run: originalRun }: LoaderData) { } type TraceData = { - run: TaskRun; + run: Run; environmentType: RuntimeEnvironmentType; trace?: Trace; selectedSpanId: string | undefined; @@ -362,7 +404,7 @@ function TraceView({ run, environmentType, trace, selectedSpanId, replaceSearchP ); } -function NoLogsView({ run }: { run: TaskRun }) { +function NoLogsView({ run }: { run: Run }) { const plan = useCurrentPlan(); const organization = useOrganization(); diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx deleted file mode 100644 index cfdf46e2a..000000000 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route.tsx +++ /dev/null @@ -1,780 +0,0 @@ -import { CheckIcon, ClockIcon, CloudArrowDownIcon, QueueListIcon } from "@heroicons/react/20/solid"; -import { Link } from "@remix-run/react"; -import { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { - formatDuration, - formatDurationMilliseconds, - nanosecondsToMilliseconds, - TaskRunError, -} from "@trigger.dev/core/v3"; -import { ReactNode, useEffect } from "react"; -import { typedjson, useTypedFetcher } from "remix-typedjson"; -import { ExitIcon } from "~/assets/icons/ExitIcon"; -import { CodeBlock } from "~/components/code/CodeBlock"; -import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; -import { Button, LinkButton } from "~/components/primitives/Buttons"; -import { Callout } from "~/components/primitives/Callout"; -import { DateTime, DateTimeAccurate } from "~/components/primitives/DateTime"; -import { Header2, Header3 } from "~/components/primitives/Headers"; -import { Paragraph } from "~/components/primitives/Paragraph"; -import * as Property from "~/components/primitives/PropertyTable"; -import { Spinner } from "~/components/primitives/Spinner"; -import { TabButton, TabContainer } from "~/components/primitives/Tabs"; -import { TextLink } from "~/components/primitives/TextLink"; -import { InfoIconTooltip, SimpleTooltip } from "~/components/primitives/Tooltip"; -import { LiveTimer } from "~/components/runs/v3/LiveTimer"; -import { RunIcon } from "~/components/runs/v3/RunIcon"; -import { RunTag } from "~/components/runs/v3/RunTag"; -import { SpanEvents } from "~/components/runs/v3/SpanEvents"; -import { SpanTitle } from "~/components/runs/v3/SpanTitle"; -import { TaskRunAttemptStatusCombo } from "~/components/runs/v3/TaskRunAttemptStatus"; -import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus"; -import { useOrganization } from "~/hooks/useOrganizations"; -import { useProject } from "~/hooks/useProject"; -import { useSearchParams } from "~/hooks/useSearchParam"; -import { redirectWithErrorMessage } from "~/models/message.server"; -import { Span, SpanPresenter, SpanRun } from "~/presenters/v3/SpanPresenter.server"; -import { logger } from "~/services/logger.server"; -import { requireUserId } from "~/services/session.server"; -import { cn } from "~/utils/cn"; -import { formatCurrencyAccurate } from "~/utils/numberFormatter"; -import { - v3RunDownloadLogsPath, - v3RunPath, - v3RunSpanPath, - v3RunsPath, - v3SchedulePath, - v3SpanParamsSchema, - v3TraceSpanPath, -} from "~/utils/pathBuilder"; -import { SpanLink } from "~/v3/eventRepository.server"; - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const userId = await requireUserId(request); - const { projectParam, organizationSlug, runParam, spanParam } = v3SpanParamsSchema.parse(params); - - const presenter = new SpanPresenter(); - - try { - const result = await presenter.call({ - userId, - organizationSlug, - projectSlug: projectParam, - spanId: spanParam, - runFriendlyId: runParam, - }); - - return typedjson(result); - } catch (error) { - logger.error("Error loading span", { - projectParam, - organizationSlug, - runParam, - spanParam, - error, - }); - return redirectWithErrorMessage( - v3RunPath({ slug: organizationSlug }, { slug: projectParam }, { friendlyId: runParam }), - request, - `Event not found.` - ); - } -}; - -export function SpanView({ - runParam, - spanId, - closePanel, -}: { - runParam: string; - spanId: string | undefined; - closePanel?: () => void; -}) { - return <>; - // const organization = useOrganization(); - // const project = useProject(); - // const fetcher = useTypedFetcher(); - - // useEffect(() => { - // if (spanId === undefined) return; - // fetcher.load( - // `/resources/orgs/${organization.slug}/projects/v3/${project.slug}/runs/${runParam}/spans/${spanId}` - // ); - // }, [organization.slug, project.slug, runParam, spanId]); - - // if (spanId === undefined) { - // return null; - // } - - // if (fetcher.state !== "idle" || fetcher.data === undefined) { - // return ( - //
- //
- //
- //
- //
- //
- // - //
- //
- // ); - // } - - // const { type } = fetcher.data; - - // switch (type) { - // case "run": { - // return ( - // - // ); - // } - // case "span": { - // return ; - // } - // } -} - -// function RunBody({ -// run, -// runParam, -// spanId, -// closePanel, -// }: { -// run: SpanRun; -// runParam: string; -// spanId: string; -// closePanel?: () => void; -// }) { -// const organization = useOrganization(); -// const project = useProject(); -// const { value, replace } = useSearchParams(); -// const tab = value("tab"); - -// const environment = project.environments.find((e) => e.id === run.environmentId); - -// return ( -//
-//
-//
-// -// -// {run.taskIdentifier} -// -//
-// {runParam && closePanel && ( -//
-//
-// -// { -// replace({ tab: "overview" }); -// }} -// shortcut={{ key: "o" }} -// > -// Overview -// -// { -// replace({ tab: "detail" }); -// }} -// shortcut={{ key: "d" }} -// > -// Detail -// -// { -// replace({ tab: "context" }); -// }} -// shortcut={{ key: "c" }} -// > -// Context -// -// -//
-//
-//
-// {tab === "detail" ? ( -//
-// -// -// Status -// -// -// -// -// -// Task -// -// -// {run.taskIdentifier} -// -// } -// content={`Filter runs by ${run.taskIdentifier}`} -// /> -// -// -// -// Version -// -// {run.version ? ( -// run.version -// ) : ( -// -// Never started -// -// -// )} -// -// -// -// SDK version -// -// {run.sdkVersion ? ( -// run.sdkVersion -// ) : ( -// -// Never started -// -// -// )} -// -// -// -// Test run -// -// {run.isTest ? : "–"} -// -// -// {environment && ( -// -// Environment -// -// -// -// -// )} -// {run.schedule && ( -// -// Schedule -// -//
-//
-// {run.schedule.generatorExpression} -// ({run.schedule.timezone}) -//
-// -// {run.schedule.description} -// -// } -// content={`Go to schedule ${run.schedule.friendlyId}`} -// /> -//
-//
-//
-// )} -// -// Queue -// -//
Name: {run.queue.name}
-//
-// Concurrency key: {run.queue.concurrencyKey ? run.queue.concurrencyKey : "–"} -//
-//
-//
-// -// Time to live (TTL) -// {run.ttl ?? "–"} -// -// -// Tags -// -// {run.tags.length === 0 ? ( -// "–" -// ) : ( -//
-// {run.tags.map((tag) => ( -// -// -// -// } -// content={`Filter runs by ${tag}`} -// /> -// ))} -//
-// )} -//
-//
-// {run.links && run.links.length > 0 && ( -// -// Links -// -//
-// {run.links.map((link, index) => ( -// -// ))} -//
-//
-//
-// )} -// -// Run invocation cost -// -// {run.baseCostInCents > 0 -// ? formatCurrencyAccurate(run.baseCostInCents / 100) -// : "–"} -// -// -// -// Compute cost -// -// {run.costInCents > 0 ? formatCurrencyAccurate(run.costInCents / 100) : "–"} -// -// -// -// Total cost -// -// {run.costInCents > 0 -// ? formatCurrencyAccurate((run.baseCostInCents + run.costInCents) / 100) -// : "–"} -// -// -// -// Usage duration -// -// {run.usageDurationMs > 0 -// ? formatDurationMilliseconds(run.usageDurationMs, { style: "short" }) -// : "–"} -// -// -//
-//
-// ) : tab === "context" ? ( -//
-// -//
-// ) : ( -//
-//
-// -//
-// -// {run.payload !== undefined && ( -// -// )} -// {run.error !== undefined ? ( -// -// ) : run.output !== undefined ? ( -// -// ) : null} -//
-// )} -//
-//
-//
-//
-// {run.friendlyId !== runParam && ( -// -// Focus on run -// -// )} -//
-//
-// {run.logsDeletedAt === null ? ( -// -// Download logs -// -// ) : null} -//
-//
-//
-// ); -// } - -// function RunTimeline({ run }: { run: SpanRun }) { -// return ( -//
-// } -// state="complete" -// /> -// {run.delayUntil && !run.expiredAt ? ( -// {formatDuration(run.createdAt, run.delayUntil)} delay -// ) : ( -// -// -// -// Delayed until {run.ttl && <>(TTL {run.ttl})} -// -// -// ) -// } -// state={run.startedAt ? "complete" : "delayed"} -// /> -// ) : run.startedAt ? ( -// -// ) : ( -// -// {" "} -// {run.ttl && <>(TTL {run.ttl})} -// -// } -// state={run.startedAt || run.expiredAt ? "complete" : "inprogress"} -// /> -// )} -// {run.expiredAt ? ( -// } -// state="error" -// /> -// ) : run.startedAt ? ( -// <> -// } -// state="complete" -// /> -// {run.isFinished ? ( -// <> -// -// } -// state="complete" -// /> -// -// ) : ( -// -// -// -// -// -// -// } -// state={"inprogress"} -// /> -// )} -// -// ) : null} -//
-// ); -// } - -// type RunTimelineItemProps = { -// title: ReactNode; -// subtitle?: ReactNode; -// state: "complete" | "error"; -// }; - -// function RunTimelineEvent({ title, subtitle, state }: RunTimelineItemProps) { -// return ( -//
-//
-//
-//
-//
-// {title} -// {subtitle ? {subtitle} : null} -//
-//
-// ); -// } - -// type RunTimelineLineProps = { -// title: ReactNode; -// state: "complete" | "delayed" | "inprogress"; -// }; - -// function RunTimelineLine({ title, state }: RunTimelineLineProps) { -// return ( -//
-//
-//
-//
-//
-// {title} -//
-//
-// ); -// } - -// function RunError({ error }: { error: TaskRunError }) { -// switch (error.type) { -// case "STRING_ERROR": -// case "CUSTOM_ERROR": { -// return ( -//
-// -//
-// ); -// } -// case "BUILT_IN_ERROR": -// case "INTERNAL_ERROR": { -// const name = "name" in error ? error.name : error.code; -// return ( -//
-// {name} -// {error.message && {error.message}} -// {error.stackTrace && ( -// -// )} -//
-// ); -// } -// } -// } - -// function PacketDisplay({ -// data, -// dataType, -// title, -// }: { -// data: string; -// dataType: string; -// title: string; -// }) { -// switch (dataType) { -// case "application/store": { -// return ( -//
-// -// {title} -// -// -// Download -// -//
-// ); -// } -// case "text/plain": { -// return ( -// -// ); -// } -// default: { -// return ( -// -// ); -// } -// } -// } - -// type TimelineProps = { -// startTime: Date; -// duration: number; -// inProgress: boolean; -// isError: boolean; -// }; - -// type TimelineState = "error" | "pending" | "complete"; - -// function SpanTimeline({ startTime, duration, inProgress, isError }: TimelineProps) { -// const state = isError ? "error" : inProgress ? "pending" : "complete"; -// return ( -// <> -//
-// } -// state="complete" -// /> -// {state === "pending" ? ( -// -// -// -// -// -// -// } -// state={"inprogress"} -// /> -// ) : ( -// <> -// -// -// } -// state={isError ? "error" : "complete"} -// /> -// -// )} -//
-// -// ); -// } - -// function VerticalBar({ state }: { state: TimelineState }) { -// return
; -// } - -// function DottedLine() { -// return ( -//
-//
-//
-//
-//
-//
-// ); -// } - -// function classNameForState(state: TimelineState) { -// switch (state) { -// case "pending": { -// return "bg-pending"; -// } -// case "complete": { -// return "bg-success"; -// } -// case "error": { -// return "bg-error"; -// } -// } -// } - -// function SpanLinkElement({ link }: { link: SpanLink }) { -// const organization = useOrganization(); -// const project = useProject(); - -// switch (link.type) { -// case "run": { -// return ( -// -// {link.title} -// -// ); -// } -// case "span": { -// return ( -// -// {link.title} -// -// ); -// } -// } - -// return null; -// } diff --git a/apps/webapp/app/routes/sync.traces.$traceId.ts b/apps/webapp/app/routes/sync.traces.$traceId.ts index 123fc4ad6..f28e3f791 100644 --- a/apps/webapp/app/routes/sync.traces.$traceId.ts +++ b/apps/webapp/app/routes/sync.traces.$traceId.ts @@ -2,7 +2,7 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; import { $replica } from "~/db.server"; import { env } from "~/env.server"; import { logger } from "~/services/logger.server"; -import { getUserId, requireUserId } from "~/services/session.server"; +import { getUserId } from "~/services/session.server"; import { longPollingFetch } from "~/utils/longPollingFetch"; export async function loader({ params, request }: LoaderFunctionArgs) { diff --git a/apps/webapp/app/routes/sync.runs.$runId.ts b/apps/webapp/app/routes/sync.traces.runs.$traceId.ts similarity index 83% rename from apps/webapp/app/routes/sync.runs.$runId.ts rename to apps/webapp/app/routes/sync.traces.runs.$traceId.ts index c8aff9358..ae4f0b57d 100644 --- a/apps/webapp/app/routes/sync.runs.$runId.ts +++ b/apps/webapp/app/routes/sync.traces.runs.$traceId.ts @@ -1,14 +1,20 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; +import { z } from "zod"; import { $replica } from "~/db.server"; import { env } from "~/env.server"; import { logger } from "~/services/logger.server"; import { getUserId } from "~/services/session.server"; import { longPollingFetch } from "~/utils/longPollingFetch"; +const Params = z.object({ + traceId: z.string(), +}); + export async function loader({ params, request }: LoaderFunctionArgs) { const userId = await getUserId(request); + const { traceId } = Params.parse(params); - logger.log(`/sync/runs/${params.runId}`, { userId }); + logger.log(`/sync/runs/${traceId}`, { userId }); if (!userId) { return new Response("No user found in cookie", { status: 401 }); @@ -23,7 +29,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { }, }, where: { - id: params.runId, + traceId, }, }); @@ -48,7 +54,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { originUrl.searchParams.set(key, value); }); - originUrl.searchParams.set("where", `"id"='${params.runId}'`); + originUrl.searchParams.set("where", `"traceId"='${traceId}'`); return longPollingFetch(originUrl.toString()); }