More major overhauls to get the synced version of the run page working…
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
+71
-29
@@ -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();
|
||||
|
||||
|
||||
-780
@@ -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) {
|
||||
|
||||
+9
-3
@@ -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());
|
||||
}
|
||||
Reference in New Issue
Block a user