import type { TaskRunStatus } from "@trigger.dev/database"; import { useEffect, useState } from "react"; import { useTypedFetcher } from "remix-typedjson"; import { ExitIcon } from "~/assets/icons/ExitIcon"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { CopyableText } from "~/components/primitives/CopyableText"; import { DateTimeAccurate } from "~/components/primitives/DateTime"; import { Header2 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import * as Property from "~/components/primitives/PropertyTable"; import { Spinner } from "~/components/primitives/Spinner"; import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { PacketDisplay } from "~/components/runs/v3/PacketDisplay"; import { TaskRunStatusCombo, descriptionForTaskRunStatus, } from "~/components/runs/v3/TaskRunStatus"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import type { LogEntry } from "~/presenters/v3/LogsListPresenter.server"; import type { loader as logDetailLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId"; import { v3RunSpanPath } from "~/utils/pathBuilder"; import { LogLevel } from "./LogLevel"; type LogDetailViewProps = { logId: string; // If we have the log entry from the list, we can display it immediately initialLog?: LogEntry; onClose: () => void; searchTerm?: string; }; type LogAttributes = Record & { error?: { message?: string; }; }; function getDisplayMessage(log: { message: string; level: string; attributes?: LogAttributes; }): string { let message = log.message ?? ""; if (log.level === "ERROR") { const maybeErrorMessage = log.attributes?.error?.message; if (typeof maybeErrorMessage === "string" && maybeErrorMessage.length > 0) { message = maybeErrorMessage; } } return message; } function formatStringJSON(str: string): string { return str .replace(/\\n/g, "\n") // Converts literal "\n" to newline .replace(/\\t/g, "\t"); // Converts literal "\t" to tab } export function LogDetailView({ logId, initialLog, onClose, searchTerm }: LogDetailViewProps) { const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); const fetcher = useTypedFetcher(); const [error, setError] = useState(null); // Fetch full log details when logId changes useEffect(() => { if (!logId) return; setError(null); fetcher.load( `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${ environment.slug }/logs/${encodeURIComponent(logId)}` ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [organization.slug, project.slug, environment.slug, logId]); // Handle fetch errors useEffect(() => { if (fetcher.data && typeof fetcher.data === "object" && "error" in fetcher.data) { setError(fetcher.data.error as string); } else if (fetcher.state === "idle" && fetcher.data === null && !initialLog) { setError("Failed to load log details"); } else { setError(null); } }, [fetcher.data, initialLog, fetcher.state]); const isLoading = fetcher.state === "loading"; const log = fetcher.data ?? initialLog; const runStatus = fetcher.data?.runStatus; const runPath = v3RunSpanPath( organization, project, environment, { friendlyId: log?.runId ?? "" }, { spanId: log?.spanId ?? "" } ); if (isLoading && !log) { return (
); } if (!log) { return (
Log Details
{error ?? "Log not found"}
); } return (
{/* Header */}
{getDisplayMessage(log)}
); } function DetailsTab({ log, runPath, runStatus, searchTerm, }: { log: LogEntry & { attributes?: LogAttributes; }; runPath: string; runStatus?: TaskRunStatus; searchTerm?: string; }) { let beautifiedAttributes: string | null = null; if (log.attributes) { beautifiedAttributes = JSON.stringify(log.attributes, null, 2); beautifiedAttributes = formatStringJSON(beautifiedAttributes); } const showAttributes = beautifiedAttributes && beautifiedAttributes !== "{}"; const message = getDisplayMessage(log); return ( <> Run ID View full run {runStatus && ( Status } content={descriptionForTaskRunStatus(runStatus)} disableHoverableContent className="mt-1" /> )} Task Level Timestamp {/* Message */}
{/* Attributes - only available in full log detail */} {showAttributes && beautifiedAttributes && (
)} ); }