More major overhauls to get the synced version of the run page working…

This commit is contained in:
Matt Aitken
2024-08-30 17:35:33 +01:00
parent 25d13048b8
commit 3d8ea667c8
12 changed files with 898 additions and 842 deletions
@@ -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 (
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr] overflow-hidden bg-background-bright">
<div className="mx-3 flex items-center justify-between gap-2 overflow-x-hidden">
<div className="flex items-center gap-1 overflow-x-hidden">
<RunIcon name={"task"} spanName="" className="h-4 min-h-4 w-4 min-w-4" />
<Header2 className={cn("overflow-x-hidden text-blue-500")}>
<span className="truncate"></span>
</Header2>
</div>
{closePanel && (
<Button
onClick={closePanel}
variant="minimal/medium"
LeadingIcon={ExitIcon}
shortcut={{ key: "esc" }}
/>
)}
</div>
<div />
</div>
);
}
const environment = project.environments.find((e) => e.id === run.runtimeEnvironmentId);
return (
<div className="grid h-full max-h-full grid-rows-[2.5rem_2rem_1fr_3.25rem] overflow-hidden bg-background-bright">
<div className="mx-3 flex items-center justify-between gap-2 overflow-x-hidden">
<div className="flex items-center gap-1 overflow-x-hidden">
<RunIcon
name={"task"}
spanName={run.taskIdentifier}
className="h-4 min-h-4 w-4 min-w-4"
/>
<Header2 className={cn("overflow-x-hidden text-blue-500")}>
<span className="truncate">{run.taskIdentifier}</span>
</Header2>
</div>
{closePanel && (
<Button
onClick={closePanel}
variant="minimal/medium"
LeadingIcon={ExitIcon}
shortcut={{ key: "esc" }}
/>
)}
</div>
<div className="px-3">
<TabContainer>
<TabButton
isActive={!tab || tab === "overview"}
layoutId="span-run"
onClick={() => {
replace({ tab: "overview" });
}}
shortcut={{ key: "o" }}
>
Overview
</TabButton>
<TabButton
isActive={tab === "detail"}
layoutId="span-run"
onClick={() => {
replace({ tab: "detail" });
}}
shortcut={{ key: "d" }}
>
Detail
</TabButton>
<TabButton
isActive={tab === "context"}
layoutId="span-run"
onClick={() => {
replace({ tab: "context" });
}}
shortcut={{ key: "c" }}
>
Context
</TabButton>
</TabContainer>
</div>
<div className="overflow-y-auto px-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
<div>
{tab === "detail" ? (
<div className="flex flex-col gap-4 py-3">
<Property.Table>
<Property.Item>
<Property.Label>Status</Property.Label>
<Property.Value>
{run ? <TaskRunStatusCombo status={run.status} /> : <PropertyLoading />}
</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Task</Property.Label>
<Property.Value>
<SimpleTooltip
button={
<TextLink
to={v3RunsPath(organization, project, {
tasks: [run.taskIdentifier],
})}
>
{run.taskIdentifier}
</TextLink>
}
content={`Filter runs by ${run.taskIdentifier}`}
/>
</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Version</Property.Label>
{/* <Property.Value>
{run.version ? (
run.version
) : (
<span className="flex items-center gap-1">
<span>Never started</span>
<InfoIconTooltip
content={"Runs get locked to the latest version when they start."}
contentClassName="normal-case tracking-normal"
/>
</span>
)}
</Property.Value> */}
</Property.Item>
<Property.Item>
<Property.Label>SDK version</Property.Label>
{/* <Property.Value>
{run.sdkVersion ? (
run.sdkVersion
) : (
<span className="flex items-center gap-1">
<span>Never started</span>
<InfoIconTooltip
content={"Runs get locked to the latest version when they start."}
contentClassName="normal-case tracking-normal"
/>
</span>
)}
</Property.Value> */}
</Property.Item>
<Property.Item>
<Property.Label>Test run</Property.Label>
<Property.Value>
{run.isTest ? <CheckIcon className="size-4 text-text-dimmed" /> : ""}
</Property.Value>
</Property.Item>
{environment && (
<Property.Item>
<Property.Label>Environment</Property.Label>
<Property.Value>
<EnvironmentLabel environment={environment} />
</Property.Value>
</Property.Item>
)}
{/* {run.schedule && (
<Property.Item>
<Property.Label>Schedule</Property.Label>
<Property.Value>
<div>
<div className="flex items-center gap-1">
<span className="font-mono">{run.schedule.generatorExpression}</span>
<span>({run.schedule.timezone})</span>
</div>
<SimpleTooltip
button={
<TextLink to={v3SchedulePath(organization, project, run.schedule)}>
{run.schedule.description}
</TextLink>
}
content={`Go to schedule ${run.schedule.friendlyId}`}
/>
</div>
</Property.Value>
</Property.Item>
)} */}
<Property.Item>
<Property.Label>Queue</Property.Label>
{/* <Property.Value>
<div>Name: {run.queue.name}</div>
<div>
Concurrency key: {run.queue.concurrencyKey ? run.queue.concurrencyKey : ""}
</div>
</Property.Value> */}
</Property.Item>
<Property.Item>
<Property.Label>Time to live (TTL)</Property.Label>
<Property.Value>{run.ttl ?? ""}</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Tags</Property.Label>
{/* <Property.Value>
{run.tags.length === 0 ? (
""
) : (
<div className="mt-1 flex flex-wrap items-center gap-1 text-xs">
{run.tags.map((tag) => (
<SimpleTooltip
key={tag}
button={
<Link to={v3RunsPath(organization, project, { tags: [tag] })}>
<RunTag tag={tag} />
</Link>
}
content={`Filter runs by ${tag}`}
/>
))}
</div>
)}
</Property.Value> */}
</Property.Item>
{/* {run.links && run.links.length > 0 && (
<Property.Item>
<Property.Label>Links</Property.Label>
<Property.Value>
<div className="space-y-1">
{run.links.map((link, index) => (
<SpanLinkElement key={index} link={link} />
))}
</div>
</Property.Value>
</Property.Item>
)} */}
<Property.Item>
<Property.Label>Run invocation cost</Property.Label>
<Property.Value>
{run.baseCostInCents > 0
? formatCurrencyAccurate(run.baseCostInCents / 100)
: ""}
</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Compute cost</Property.Label>
<Property.Value>
{run.costInCents > 0 ? formatCurrencyAccurate(run.costInCents / 100) : ""}
</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Total cost</Property.Label>
<Property.Value>
{run.costInCents > 0
? formatCurrencyAccurate((run.baseCostInCents + run.costInCents) / 100)
: ""}
</Property.Value>
</Property.Item>
<Property.Item>
<Property.Label>Usage duration</Property.Label>
<Property.Value>
{run.usageDurationMs > 0
? formatDurationMilliseconds(run.usageDurationMs, { style: "short" })
: ""}
</Property.Value>
</Property.Item>
</Property.Table>
</div>
) : tab === "context" ? (
<div className="flex flex-col gap-4 py-3">
{/* <CodeBlock code={run.context} showLineNumbers={false} /> */}
</div>
) : (
<div className="flex flex-col gap-4 pt-3">
<div className="border-b border-grid-bright pb-3">
<TaskRunStatusCombo status={run.status} className="text-sm" />
</div>
<RunTimeline run={run} />
{run.payload !== undefined && (
<PacketDisplay data={run.payload} dataType={run.payloadType} title="Payload" />
)}
{/* {run.error !== undefined ? (
<RunError error={run.error} />
) : run.output !== undefined ? (
<PacketDisplay data={run.output} dataType={run.outputType} title="Output" />
) : null} */}
</div>
)}
</div>
</div>
<div className="flex items-center justify-between gap-2 border-t border-grid-dimmed px-2">
<div className="flex items-center gap-4">
{run.friendlyId !== runParam && (
<LinkButton
to={v3RunSpanPath(
organization,
project,
{ friendlyId: run.friendlyId },
{ spanId: run.spanId }
)}
variant="minimal/medium"
LeadingIcon={QueueListIcon}
shortcut={{ key: "f" }}
>
Focus on run
</LinkButton>
)}
</div>
<div className="flex items-center gap-4">
{run.logsDeletedAt === null ? (
<LinkButton
to={v3RunDownloadLogsPath({ friendlyId: runParam })}
LeadingIcon={CloudArrowDownIcon}
variant="tertiary/medium"
target="_blank"
download
>
Download logs
</LinkButton>
) : null}
</div>
</div>
</div>
);
}
function PropertyLoading() {
return <Spinner className="size-4" color="muted" />;
}
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 (
<div className="min-w-fit max-w-80">
<RunTimelineEvent
title="Triggered"
subtitle={<DateTimeAccurate date={run.createdAt} />}
state="complete"
/>
{delayUntil && !expiredAt ? (
<RunTimelineLine
title={
run.startedAt ? (
<>{formatDuration(createdAt, delayUntil)} delay</>
) : (
<span className="flex items-center gap-1">
<ClockIcon className="size-4" />
<span>
Delayed until <DateTime date={delayUntil} /> {run.ttl && <>(TTL {run.ttl})</>}
</span>
</span>
)
}
state={run.startedAt ? "complete" : "delayed"}
/>
) : startedAt ? (
<RunTimelineLine title={formatDuration(createdAt, startedAt)} state={"complete"} />
) : (
<RunTimelineLine
title={
<>
<LiveTimer startTime={createdAt} endTime={startedAt ?? expiredAt ?? undefined} />{" "}
{run.ttl && <>(TTL {run.ttl})</>}
</>
}
state={run.startedAt || run.expiredAt ? "complete" : "inprogress"}
/>
)}
{expiredAt ? (
<RunTimelineEvent
title="Expired"
subtitle={<DateTimeAccurate date={expiredAt} />}
state="error"
/>
) : startedAt ? (
<>
<RunTimelineEvent
title="Started"
subtitle={<DateTimeAccurate date={startedAt} />}
state="complete"
/>
{isFinished ? (
<>
<RunTimelineLine title={formatDuration(startedAt, updatedAt)} state={"complete"} />
<RunTimelineEvent
title="Finished"
subtitle={<DateTimeAccurate date={updatedAt} />}
state="complete"
/>
</>
) : (
<RunTimelineLine
title={
<span className="flex items-center gap-1">
<Spinner className="size-4" />
<span>
<LiveTimer startTime={startedAt} />
</span>
</span>
}
state={"inprogress"}
/>
)}
</>
) : null}
</div>
);
}
type RunTimelineItemProps = {
title: ReactNode;
subtitle?: ReactNode;
state: "complete" | "error";
};
function RunTimelineEvent({ title, subtitle, state }: RunTimelineItemProps) {
return (
<div className="grid h-5 grid-cols-[1.125rem_1fr] text-sm">
<div className="flex items-center justify-center">
<div
className={cn(
"size-[0.3125rem] rounded-full",
state === "complete" ? "bg-success" : "bg-error"
)}
></div>
</div>
<div className="flex items-baseline justify-between gap-3">
<span className="font-medium text-text-bright">{title}</span>
{subtitle ? <span className="text-xs text-text-dimmed">{subtitle}</span> : null}
</div>
</div>
);
}
type RunTimelineLineProps = {
title: ReactNode;
state: "complete" | "delayed" | "inprogress";
};
function RunTimelineLine({ title, state }: RunTimelineLineProps) {
return (
<div className="grid h-6 grid-cols-[1.125rem_1fr] text-xs">
<div className="flex items-stretch justify-center">
<div
className={cn(
"w-px",
state === "complete" ? "bg-success" : state === "delayed" ? "bg-text-dimmed" : ""
)}
style={
state === "inprogress"
? {
width: "1px",
height: "100%",
background:
"repeating-linear-gradient(to bottom, #3B82F6 0%, #3B82F6 50%, transparent 50%, transparent 100%)",
backgroundSize: "1px 6px",
maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
}
: undefined
}
></div>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-text-dimmed">{title}</span>
</div>
</div>
);
}
function RunError({ error }: { error: TaskRunError }) {
switch (error.type) {
case "STRING_ERROR":
case "CUSTOM_ERROR": {
return (
<div className="flex flex-col gap-2 rounded-sm border border-rose-500/50 px-3 pb-3 pt-2">
<CodeBlock
showCopyButton={false}
showLineNumbers={false}
code={error.raw}
maxLines={20}
/>
</div>
);
}
case "BUILT_IN_ERROR":
case "INTERNAL_ERROR": {
const name = "name" in error ? error.name : error.code;
return (
<div className="flex flex-col gap-2 rounded-sm border border-rose-500/50 px-3 pb-3 pt-2">
<Header3 className="text-rose-500">{name}</Header3>
{error.message && <Callout variant="error">{error.message}</Callout>}
{error.stackTrace && (
<CodeBlock
showCopyButton={false}
showLineNumbers={false}
code={error.stackTrace}
maxLines={20}
/>
)}
</div>
);
}
}
}
function PacketDisplay({
data,
dataType,
title,
}: {
data: string;
dataType: string;
title: string;
}) {
switch (dataType) {
case "application/store": {
return (
<div className="flex flex-col">
<Paragraph variant="base/bright" className="w-full border-b border-grid-dimmed py-2.5">
{title}
</Paragraph>
<LinkButton LeadingIcon={CloudArrowDownIcon} to={data} variant="tertiary/medium" download>
Download
</LinkButton>
</div>
);
}
case "text/plain": {
return (
<CodeBlock
language="markdown"
rowTitle={title}
code={data}
maxLines={20}
showLineNumbers={false}
/>
);
}
default: {
return (
<CodeBlock
language="json"
rowTitle={title}
code={data}
maxLines={20}
showLineNumbers={false}
/>
);
}
}
}
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 (
<>
<div className="min-w-fit max-w-80">
<RunTimelineEvent
title="Started"
subtitle={<DateTimeAccurate date={startTime} />}
state="complete"
/>
{state === "pending" ? (
<RunTimelineLine
title={
<span className="flex items-center gap-1">
<Spinner className="size-4" />
<span>
<LiveTimer startTime={startTime} />
</span>
</span>
}
state={"inprogress"}
/>
) : (
<>
<RunTimelineLine
title={formatDuration(
startTime,
new Date(startTime.getTime() + nanosecondsToMilliseconds(duration))
)}
state={"complete"}
/>
<RunTimelineEvent
title="Finished"
subtitle={
<DateTimeAccurate
date={new Date(startTime.getTime() + nanosecondsToMilliseconds(duration))}
/>
}
state={isError ? "error" : "complete"}
/>
</>
)}
</div>
</>
);
}
function VerticalBar({ state }: { state: TimelineState }) {
return <div className={cn("h-3 w-0.75 rounded-full", classNameForState(state))}></div>;
}
function DottedLine() {
return (
<div className="flex h-0.75 flex-1 items-center justify-evenly">
<div className="h-0.75 w-0.75 bg-pending" />
<div className="h-0.75 w-0.75 bg-pending" />
<div className="h-0.75 w-0.75 bg-pending" />
<div className="h-0.75 w-0.75 bg-pending" />
</div>
);
}
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 (
<TextLink to={v3RunPath(organization, project, { friendlyId: link.runId })}>
{link.title}
</TextLink>
);
}
case "span": {
return (
<TextLink to={v3TraceSpanPath(organization, project, link.traceId, link.spanId)}>
{link.title}
</TextLink>
);
}
}
return null;
}
@@ -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 (
<div className="grid h-full max-h-full grid-rows-[2.5rem_2rem_1fr] overflow-hidden bg-background-bright">
<div className="mx-3 flex items-center justify-between gap-2 overflow-x-hidden">
-17
View File
@@ -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 };
}
+5 -7
View File
@@ -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 };
}
+3 -4
View File
@@ -5,16 +5,15 @@ import { createTraceTreeFromEvents } from "~/utils/taskEvent";
type Params = {
origin: string;
traceId: string;
spanId: string;
};
export type Trace = ReturnType<typeof createTraceTreeFromEvents>;
export type TraceEvent = NonNullable<Trace["events"][number]>;
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 };
}
+19
View File
@@ -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<SyncedShapeData<TaskRun>>;
export function useSyncTraceRuns({ origin, traceId }: Params) {
const { isUpToDate, data } = useSyncedShape<TaskRun>({
url: `${origin}/sync/traces/runs/${traceId}`,
});
return { isUpToDate, runs: data };
}
+69
View File
@@ -0,0 +1,69 @@
import { useShape } from "@electric-sql/react";
export type ShapeInput = Parameters<typeof useShape>[0];
export type ShapeOutput<S> = {
isError: boolean;
isUpToDate: boolean;
data: S[];
};
export type SyncedShapeData<T> = {
[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]>
: T[K];
};
export function useSyncedShape<S>(props: ShapeInput): ShapeOutput<SyncedShapeData<S>> {
const output = useShape(props) as any;
return {
isError: output.isError,
isUpToDate: output.isUpToDate,
data: transformInput(output.data as InputObject) as SyncedShapeData<S>[],
};
}
type InputObject = {
[key: string]: Value;
}[];
type Value =
| string
| number
| boolean
| bigint
| null
| Value[]
| {
[key: string]: Value;
};
function transformInput(input: InputObject): SyncedShapeData<Value> {
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;
}
}
@@ -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,
@@ -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<InspectorState>(() => {
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 (
<ResizablePanelGroup
@@ -291,7 +327,7 @@ function Panels({ resizable, run: originalRun }: LoaderData) {
<Loading />
) : (
<TraceView
run={run}
run={originalRun}
environmentType={originalRun.environment.type}
trace={trace}
selectedSpanId={selectedSpanId}
@@ -300,18 +336,24 @@ function Panels({ resizable, run: originalRun }: LoaderData) {
)}
</ResizablePanel>
<ResizableHandle id={resizableSettings.parent.handleId} />
{inspectorSpanId ? (
{inspectorState ? (
<ResizablePanel
id={resizableSettings.parent.inspector.id}
default={resizableSettings.parent.inspector.default}
min={resizableSettings.parent.inspector.min}
isStaticAtRest
>
{selectedSpan && run ? (
{inspectorState.type === "span" ? (
<SpanInspector
runParam={run.friendlyId}
span={selectedSpan}
closePanel={!run.logsDeletedAt ? () => replaceSearchParam("span") : undefined}
runParam={originalRun.friendlyId}
span={inspectorState.span}
closePanel={!originalRun.logsDeletedAt ? () => replaceSearchParam("span") : undefined}
/>
) : inspectorState.type === "run" ? (
<RunInspector
runParam={originalRun.friendlyId}
run={inspectorState.run}
closePanel={!originalRun.logsDeletedAt ? () => replaceSearchParam("span") : undefined}
/>
) : null}
</ResizablePanel>
@@ -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();
@@ -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<typeof loader>();
// 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 (
// <div
// className={cn(
// "grid h-full max-h-full grid-rows-[2.5rem_1fr] overflow-hidden bg-background-bright"
// )}
// >
// <div className="mx-3 flex items-center gap-2 overflow-x-hidden border-b border-grid-dimmed">
// <div className="size-4 bg-grid-dimmed" />
// <div className="h-6 w-[60%] bg-grid-dimmed" />
// </div>
// <div className="flex items-center justify-center">
// <Spinner />
// </div>
// </div>
// );
// }
// const { type } = fetcher.data;
// switch (type) {
// case "run": {
// return (
// <RunBody
// run={fetcher.data.run}
// runParam={runParam}
// spanId={spanId}
// closePanel={closePanel}
// />
// );
// }
// case "span": {
// return <SpanBody span={fetcher.data.span} runParam={runParam} closePanel={closePanel} />;
// }
// }
}
// 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 (
// <div className="grid h-full max-h-full grid-rows-[2.5rem_2rem_1fr_3.25rem] overflow-hidden bg-background-bright">
// <div className="mx-3 flex items-center justify-between gap-2 overflow-x-hidden">
// <div className="flex items-center gap-1 overflow-x-hidden">
// <RunIcon
// name={"task"}
// spanName={run.taskIdentifier}
// className="h-4 min-h-4 w-4 min-w-4"
// />
// <Header2 className={cn("overflow-x-hidden text-blue-500")}>
// <span className="truncate">{run.taskIdentifier}</span>
// </Header2>
// </div>
// {runParam && closePanel && (
// <Button
// onClick={closePanel}
// variant="minimal/medium"
// LeadingIcon={ExitIcon}
// shortcut={{ key: "esc" }}
// />
// )}
// </div>
// <div className="px-3">
// <TabContainer>
// <TabButton
// isActive={!tab || tab === "overview"}
// layoutId="span-run"
// onClick={() => {
// replace({ tab: "overview" });
// }}
// shortcut={{ key: "o" }}
// >
// Overview
// </TabButton>
// <TabButton
// isActive={tab === "detail"}
// layoutId="span-run"
// onClick={() => {
// replace({ tab: "detail" });
// }}
// shortcut={{ key: "d" }}
// >
// Detail
// </TabButton>
// <TabButton
// isActive={tab === "context"}
// layoutId="span-run"
// onClick={() => {
// replace({ tab: "context" });
// }}
// shortcut={{ key: "c" }}
// >
// Context
// </TabButton>
// </TabContainer>
// </div>
// <div className="overflow-y-auto px-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
// <div>
// {tab === "detail" ? (
// <div className="flex flex-col gap-4 py-3">
// <Property.Table>
// <Property.Item>
// <Property.Label>Status</Property.Label>
// <Property.Value>
// <TaskRunStatusCombo status={run.status} />
// </Property.Value>
// </Property.Item>
// <Property.Item>
// <Property.Label>Task</Property.Label>
// <Property.Value>
// <SimpleTooltip
// button={
// <TextLink
// to={v3RunsPath(organization, project, { tasks: [run.taskIdentifier] })}
// >
// {run.taskIdentifier}
// </TextLink>
// }
// content={`Filter runs by ${run.taskIdentifier}`}
// />
// </Property.Value>
// </Property.Item>
// <Property.Item>
// <Property.Label>Version</Property.Label>
// <Property.Value>
// {run.version ? (
// run.version
// ) : (
// <span className="flex items-center gap-1">
// <span>Never started</span>
// <InfoIconTooltip
// content={"Runs get locked to the latest version when they start."}
// contentClassName="normal-case tracking-normal"
// />
// </span>
// )}
// </Property.Value>
// </Property.Item>
// <Property.Item>
// <Property.Label>SDK version</Property.Label>
// <Property.Value>
// {run.sdkVersion ? (
// run.sdkVersion
// ) : (
// <span className="flex items-center gap-1">
// <span>Never started</span>
// <InfoIconTooltip
// content={"Runs get locked to the latest version when they start."}
// contentClassName="normal-case tracking-normal"
// />
// </span>
// )}
// </Property.Value>
// </Property.Item>
// <Property.Item>
// <Property.Label>Test run</Property.Label>
// <Property.Value>
// {run.isTest ? <CheckIcon className="size-4 text-text-dimmed" /> : ""}
// </Property.Value>
// </Property.Item>
// {environment && (
// <Property.Item>
// <Property.Label>Environment</Property.Label>
// <Property.Value>
// <EnvironmentLabel environment={environment} />
// </Property.Value>
// </Property.Item>
// )}
// {run.schedule && (
// <Property.Item>
// <Property.Label>Schedule</Property.Label>
// <Property.Value>
// <div>
// <div className="flex items-center gap-1">
// <span className="font-mono">{run.schedule.generatorExpression}</span>
// <span>({run.schedule.timezone})</span>
// </div>
// <SimpleTooltip
// button={
// <TextLink to={v3SchedulePath(organization, project, run.schedule)}>
// {run.schedule.description}
// </TextLink>
// }
// content={`Go to schedule ${run.schedule.friendlyId}`}
// />
// </div>
// </Property.Value>
// </Property.Item>
// )}
// <Property.Item>
// <Property.Label>Queue</Property.Label>
// <Property.Value>
// <div>Name: {run.queue.name}</div>
// <div>
// Concurrency key: {run.queue.concurrencyKey ? run.queue.concurrencyKey : ""}
// </div>
// </Property.Value>
// </Property.Item>
// <Property.Item>
// <Property.Label>Time to live (TTL)</Property.Label>
// <Property.Value>{run.ttl ?? ""}</Property.Value>
// </Property.Item>
// <Property.Item>
// <Property.Label>Tags</Property.Label>
// <Property.Value>
// {run.tags.length === 0 ? (
// ""
// ) : (
// <div className="mt-1 flex flex-wrap items-center gap-1 text-xs">
// {run.tags.map((tag) => (
// <SimpleTooltip
// key={tag}
// button={
// <Link to={v3RunsPath(organization, project, { tags: [tag] })}>
// <RunTag tag={tag} />
// </Link>
// }
// content={`Filter runs by ${tag}`}
// />
// ))}
// </div>
// )}
// </Property.Value>
// </Property.Item>
// {run.links && run.links.length > 0 && (
// <Property.Item>
// <Property.Label>Links</Property.Label>
// <Property.Value>
// <div className="space-y-1">
// {run.links.map((link, index) => (
// <SpanLinkElement key={index} link={link} />
// ))}
// </div>
// </Property.Value>
// </Property.Item>
// )}
// <Property.Item>
// <Property.Label>Run invocation cost</Property.Label>
// <Property.Value>
// {run.baseCostInCents > 0
// ? formatCurrencyAccurate(run.baseCostInCents / 100)
// : ""}
// </Property.Value>
// </Property.Item>
// <Property.Item>
// <Property.Label>Compute cost</Property.Label>
// <Property.Value>
// {run.costInCents > 0 ? formatCurrencyAccurate(run.costInCents / 100) : ""}
// </Property.Value>
// </Property.Item>
// <Property.Item>
// <Property.Label>Total cost</Property.Label>
// <Property.Value>
// {run.costInCents > 0
// ? formatCurrencyAccurate((run.baseCostInCents + run.costInCents) / 100)
// : ""}
// </Property.Value>
// </Property.Item>
// <Property.Item>
// <Property.Label>Usage duration</Property.Label>
// <Property.Value>
// {run.usageDurationMs > 0
// ? formatDurationMilliseconds(run.usageDurationMs, { style: "short" })
// : ""}
// </Property.Value>
// </Property.Item>
// </Property.Table>
// </div>
// ) : tab === "context" ? (
// <div className="flex flex-col gap-4 py-3">
// <CodeBlock code={run.context} showLineNumbers={false} />
// </div>
// ) : (
// <div className="flex flex-col gap-4 pt-3">
// <div className="border-b border-grid-bright pb-3">
// <TaskRunStatusCombo status={run.status} className="text-sm" />
// </div>
// <RunTimeline run={run} />
// {run.payload !== undefined && (
// <PacketDisplay data={run.payload} dataType={run.payloadType} title="Payload" />
// )}
// {run.error !== undefined ? (
// <RunError error={run.error} />
// ) : run.output !== undefined ? (
// <PacketDisplay data={run.output} dataType={run.outputType} title="Output" />
// ) : null}
// </div>
// )}
// </div>
// </div>
// <div className="flex items-center justify-between gap-2 border-t border-grid-dimmed px-2">
// <div className="flex items-center gap-4">
// {run.friendlyId !== runParam && (
// <LinkButton
// to={v3RunSpanPath(organization, project, { friendlyId: run.friendlyId }, { spanId })}
// variant="minimal/medium"
// LeadingIcon={QueueListIcon}
// shortcut={{ key: "f" }}
// >
// Focus on run
// </LinkButton>
// )}
// </div>
// <div className="flex items-center gap-4">
// {run.logsDeletedAt === null ? (
// <LinkButton
// to={v3RunDownloadLogsPath({ friendlyId: runParam })}
// LeadingIcon={CloudArrowDownIcon}
// variant="tertiary/medium"
// target="_blank"
// download
// >
// Download logs
// </LinkButton>
// ) : null}
// </div>
// </div>
// </div>
// );
// }
// function RunTimeline({ run }: { run: SpanRun }) {
// return (
// <div className="min-w-fit max-w-80">
// <RunTimelineEvent
// title="Triggered"
// subtitle={<DateTimeAccurate date={run.createdAt} />}
// state="complete"
// />
// {run.delayUntil && !run.expiredAt ? (
// <RunTimelineLine
// title={
// run.startedAt ? (
// <>{formatDuration(run.createdAt, run.delayUntil)} delay</>
// ) : (
// <span className="flex items-center gap-1">
// <ClockIcon className="size-4" />
// <span>
// Delayed until <DateTime date={run.delayUntil} /> {run.ttl && <>(TTL {run.ttl})</>}
// </span>
// </span>
// )
// }
// state={run.startedAt ? "complete" : "delayed"}
// />
// ) : run.startedAt ? (
// <RunTimelineLine title={formatDuration(run.createdAt, run.startedAt)} state={"complete"} />
// ) : (
// <RunTimelineLine
// title={
// <>
// <LiveTimer
// startTime={run.createdAt}
// endTime={run.startedAt ?? run.expiredAt ?? undefined}
// />{" "}
// {run.ttl && <>(TTL {run.ttl})</>}
// </>
// }
// state={run.startedAt || run.expiredAt ? "complete" : "inprogress"}
// />
// )}
// {run.expiredAt ? (
// <RunTimelineEvent
// title="Expired"
// subtitle={<DateTimeAccurate date={run.expiredAt} />}
// state="error"
// />
// ) : run.startedAt ? (
// <>
// <RunTimelineEvent
// title="Started"
// subtitle={<DateTimeAccurate date={run.startedAt} />}
// state="complete"
// />
// {run.isFinished ? (
// <>
// <RunTimelineLine
// title={formatDuration(run.startedAt, run.updatedAt)}
// state={"complete"}
// />
// <RunTimelineEvent
// title="Finished"
// subtitle={<DateTimeAccurate date={run.updatedAt} />}
// state="complete"
// />
// </>
// ) : (
// <RunTimelineLine
// title={
// <span className="flex items-center gap-1">
// <Spinner className="size-4" />
// <span>
// <LiveTimer startTime={run.startedAt} />
// </span>
// </span>
// }
// state={"inprogress"}
// />
// )}
// </>
// ) : null}
// </div>
// );
// }
// type RunTimelineItemProps = {
// title: ReactNode;
// subtitle?: ReactNode;
// state: "complete" | "error";
// };
// function RunTimelineEvent({ title, subtitle, state }: RunTimelineItemProps) {
// return (
// <div className="grid h-5 grid-cols-[1.125rem_1fr] text-sm">
// <div className="flex items-center justify-center">
// <div
// className={cn(
// "size-[0.3125rem] rounded-full",
// state === "complete" ? "bg-success" : "bg-error"
// )}
// ></div>
// </div>
// <div className="flex items-baseline justify-between gap-3">
// <span className="font-medium text-text-bright">{title}</span>
// {subtitle ? <span className="text-xs text-text-dimmed">{subtitle}</span> : null}
// </div>
// </div>
// );
// }
// type RunTimelineLineProps = {
// title: ReactNode;
// state: "complete" | "delayed" | "inprogress";
// };
// function RunTimelineLine({ title, state }: RunTimelineLineProps) {
// return (
// <div className="grid h-6 grid-cols-[1.125rem_1fr] text-xs">
// <div className="flex items-stretch justify-center">
// <div
// className={cn(
// "w-px",
// state === "complete" ? "bg-success" : state === "delayed" ? "bg-text-dimmed" : ""
// )}
// style={
// state === "inprogress"
// ? {
// width: "1px",
// height: "100%",
// background:
// "repeating-linear-gradient(to bottom, #3B82F6 0%, #3B82F6 50%, transparent 50%, transparent 100%)",
// backgroundSize: "1px 6px",
// maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
// WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)",
// }
// : undefined
// }
// ></div>
// </div>
// <div className="flex items-center justify-between gap-3">
// <span className="text-text-dimmed">{title}</span>
// </div>
// </div>
// );
// }
// function RunError({ error }: { error: TaskRunError }) {
// switch (error.type) {
// case "STRING_ERROR":
// case "CUSTOM_ERROR": {
// return (
// <div className="flex flex-col gap-2 rounded-sm border border-rose-500/50 px-3 pb-3 pt-2">
// <CodeBlock
// showCopyButton={false}
// showLineNumbers={false}
// code={error.raw}
// maxLines={20}
// />
// </div>
// );
// }
// case "BUILT_IN_ERROR":
// case "INTERNAL_ERROR": {
// const name = "name" in error ? error.name : error.code;
// return (
// <div className="flex flex-col gap-2 rounded-sm border border-rose-500/50 px-3 pb-3 pt-2">
// <Header3 className="text-rose-500">{name}</Header3>
// {error.message && <Callout variant="error">{error.message}</Callout>}
// {error.stackTrace && (
// <CodeBlock
// showCopyButton={false}
// showLineNumbers={false}
// code={error.stackTrace}
// maxLines={20}
// />
// )}
// </div>
// );
// }
// }
// }
// function PacketDisplay({
// data,
// dataType,
// title,
// }: {
// data: string;
// dataType: string;
// title: string;
// }) {
// switch (dataType) {
// case "application/store": {
// return (
// <div className="flex flex-col">
// <Paragraph variant="base/bright" className="w-full border-b border-grid-dimmed py-2.5">
// {title}
// </Paragraph>
// <LinkButton LeadingIcon={CloudArrowDownIcon} to={data} variant="tertiary/medium" download>
// Download
// </LinkButton>
// </div>
// );
// }
// case "text/plain": {
// return (
// <CodeBlock
// language="markdown"
// rowTitle={title}
// code={data}
// maxLines={20}
// showLineNumbers={false}
// />
// );
// }
// default: {
// return (
// <CodeBlock
// language="json"
// rowTitle={title}
// code={data}
// maxLines={20}
// showLineNumbers={false}
// />
// );
// }
// }
// }
// 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 (
// <>
// <div className="min-w-fit max-w-80">
// <RunTimelineEvent
// title="Started"
// subtitle={<DateTimeAccurate date={startTime} />}
// state="complete"
// />
// {state === "pending" ? (
// <RunTimelineLine
// title={
// <span className="flex items-center gap-1">
// <Spinner className="size-4" />
// <span>
// <LiveTimer startTime={startTime} />
// </span>
// </span>
// }
// state={"inprogress"}
// />
// ) : (
// <>
// <RunTimelineLine
// title={formatDuration(
// startTime,
// new Date(startTime.getTime() + nanosecondsToMilliseconds(duration))
// )}
// state={"complete"}
// />
// <RunTimelineEvent
// title="Finished"
// subtitle={
// <DateTimeAccurate
// date={new Date(startTime.getTime() + nanosecondsToMilliseconds(duration))}
// />
// }
// state={isError ? "error" : "complete"}
// />
// </>
// )}
// </div>
// </>
// );
// }
// function VerticalBar({ state }: { state: TimelineState }) {
// return <div className={cn("h-3 w-0.75 rounded-full", classNameForState(state))}></div>;
// }
// function DottedLine() {
// return (
// <div className="flex h-0.75 flex-1 items-center justify-evenly">
// <div className="h-0.75 w-0.75 bg-pending" />
// <div className="h-0.75 w-0.75 bg-pending" />
// <div className="h-0.75 w-0.75 bg-pending" />
// <div className="h-0.75 w-0.75 bg-pending" />
// </div>
// );
// }
// 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 (
// <TextLink to={v3RunPath(organization, project, { friendlyId: link.runId })}>
// {link.title}
// </TextLink>
// );
// }
// case "span": {
// return (
// <TextLink to={v3TraceSpanPath(organization, project, link.traceId, link.spanId)}>
// {link.title}
// </TextLink>
// );
// }
// }
// return null;
// }
@@ -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) {
@@ -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());
}