Skip to content

Commit 30a7ad1

Browse files
committed
Logs page MVP
1 parent 06cbe6e commit 30a7ad1

19 files changed

Lines changed: 1943 additions & 2 deletions
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
import { XMarkIcon, ArrowTopRightOnSquareIcon, ClockIcon } from "@heroicons/react/20/solid";
2+
import { Link } from "@remix-run/react";
3+
import { formatDurationNanoseconds } from "@trigger.dev/core/v3";
4+
import { useEffect } from "react";
5+
import { useTypedFetcher } from "remix-typedjson";
6+
import { cn } from "~/utils/cn";
7+
import { Button } from "~/components/primitives/Buttons";
8+
import { DateTime } from "~/components/primitives/DateTime";
9+
import { Header2, Header3 } from "~/components/primitives/Headers";
10+
import { Paragraph } from "~/components/primitives/Paragraph";
11+
import { Spinner } from "~/components/primitives/Spinner";
12+
import { useEnvironment } from "~/hooks/useEnvironment";
13+
import { useOrganization } from "~/hooks/useOrganizations";
14+
import { useProject } from "~/hooks/useProject";
15+
import type { LogEntry } from "~/presenters/v3/LogsListPresenter.server";
16+
import { v3RunSpanPath } from "~/utils/pathBuilder";
17+
import type { loader as logDetailLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs.$logId";
18+
19+
type LogDetailViewProps = {
20+
logId: string;
21+
// If we have the log entry from the list, we can display it immediately
22+
initialLog?: LogEntry;
23+
onClose: () => void;
24+
};
25+
26+
// Level badge color styles
27+
function getLevelColor(level: string): string {
28+
switch (level) {
29+
case "ERROR":
30+
return "text-error bg-error/10 border-error/20";
31+
case "WARN":
32+
return "text-warning bg-warning/10 border-warning/20";
33+
case "DEBUG":
34+
return "text-charcoal-400 bg-charcoal-700 border-charcoal-600";
35+
case "INFO":
36+
return "text-blue-400 bg-blue-500/10 border-blue-500/20";
37+
case "TRACE":
38+
return "text-charcoal-500 bg-charcoal-800 border-charcoal-700";
39+
case "LOG":
40+
default:
41+
return "text-text-dimmed bg-charcoal-750 border-charcoal-700";
42+
}
43+
}
44+
45+
// Event kind badge color styles
46+
function getKindColor(kind: string): string {
47+
if (kind === "SPAN") {
48+
return "text-purple-400 bg-purple-500/10 border-purple-500/20";
49+
}
50+
if (kind === "SPAN_EVENT") {
51+
return "text-amber-400 bg-amber-500/10 border-amber-500/20";
52+
}
53+
if (kind.startsWith("LOG_")) {
54+
return "text-blue-400 bg-blue-500/10 border-blue-500/20";
55+
}
56+
return "text-charcoal-400 bg-charcoal-700 border-charcoal-600";
57+
}
58+
59+
// Get human readable kind label
60+
function getKindLabel(kind: string): string {
61+
switch (kind) {
62+
case "SPAN":
63+
return "Span";
64+
case "SPAN_EVENT":
65+
return "Event";
66+
case "LOG_DEBUG":
67+
return "Log";
68+
case "LOG_INFO":
69+
return "Log";
70+
case "LOG_WARN":
71+
return "Log";
72+
case "LOG_ERROR":
73+
return "Log";
74+
case "LOG_LOG":
75+
return "Log";
76+
case "DEBUG_EVENT":
77+
return "Debug";
78+
case "ANCESTOR_OVERRIDE":
79+
return "Override";
80+
default:
81+
return kind;
82+
}
83+
}
84+
85+
// Status badge color styles
86+
function getStatusColor(status: string): string {
87+
switch (status) {
88+
case "OK":
89+
return "text-success bg-success/10 border-success/20";
90+
case "ERROR":
91+
return "text-error bg-error/10 border-error/20";
92+
case "CANCELLED":
93+
return "text-charcoal-400 bg-charcoal-700 border-charcoal-600";
94+
case "PARTIAL":
95+
return "text-pending bg-pending/10 border-pending/20";
96+
default:
97+
return "text-text-dimmed bg-charcoal-750 border-charcoal-700";
98+
}
99+
}
100+
101+
export function LogDetailView({ logId, initialLog, onClose }: LogDetailViewProps) {
102+
const organization = useOrganization();
103+
const project = useProject();
104+
const environment = useEnvironment();
105+
const fetcher = useTypedFetcher<typeof logDetailLoader>();
106+
107+
// Fetch full log details when logId changes
108+
useEffect(() => {
109+
if (!logId) return;
110+
111+
fetcher.load(
112+
`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/logs/${encodeURIComponent(logId)}`
113+
);
114+
}, [organization.slug, project.slug, environment.slug, logId]);
115+
116+
const isLoading = fetcher.state === "loading";
117+
const log = fetcher.data ?? initialLog;
118+
119+
// Handle Escape key to close panel
120+
useEffect(() => {
121+
const handleKeyDown = (e: KeyboardEvent) => {
122+
if (e.key === "Escape") {
123+
onClose();
124+
}
125+
};
126+
window.addEventListener("keydown", handleKeyDown);
127+
return () => window.removeEventListener("keydown", handleKeyDown);
128+
}, [onClose]);
129+
130+
if (isLoading && !log) {
131+
return (
132+
<div className="flex h-full items-center justify-center">
133+
<Spinner />
134+
</div>
135+
);
136+
}
137+
138+
if (!log) {
139+
return (
140+
<div className="flex h-full flex-col">
141+
<div className="flex items-center justify-between border-b border-grid-dimmed p-4">
142+
<Header2>Log Details</Header2>
143+
<Button variant="minimal/small" onClick={onClose}>
144+
<XMarkIcon className="size-5" />
145+
</Button>
146+
</div>
147+
<div className="flex flex-1 items-center justify-center">
148+
<Paragraph className="text-text-dimmed">Log not found</Paragraph>
149+
</div>
150+
</div>
151+
);
152+
}
153+
154+
const runPath = v3RunSpanPath(
155+
organization,
156+
project,
157+
environment,
158+
{ friendlyId: log.runId },
159+
{ spanId: log.spanId }
160+
);
161+
162+
return (
163+
<div className="flex h-full flex-col overflow-hidden">
164+
{/* Header */}
165+
<div className="flex items-center justify-between border-b border-grid-dimmed px-4 py-3">
166+
<div className="flex items-center gap-2">
167+
<span
168+
className={cn(
169+
"inline-flex items-center rounded border px-1.5 py-0.5 text-xs font-medium",
170+
getKindColor(log.kind)
171+
)}
172+
>
173+
{getKindLabel(log.kind)}
174+
</span>
175+
<span
176+
className={cn(
177+
"inline-flex items-center rounded border px-1.5 py-0.5 text-xs font-medium uppercase",
178+
getLevelColor(log.level)
179+
)}
180+
>
181+
{log.level}
182+
</span>
183+
<span className="text-text-dimmed">·</span>
184+
<DateTime date={log.startTime} />
185+
</div>
186+
<Button variant="minimal/small" onClick={onClose} shortcut={{ key: "esc" }}>
187+
<XMarkIcon className="size-5" />
188+
</Button>
189+
</div>
190+
191+
{/* Content */}
192+
<div className="flex-1 overflow-y-auto p-4">
193+
{/* Message */}
194+
<div className="mb-6">
195+
<Header3 className="mb-2">Message</Header3>
196+
<div className="rounded-md border border-grid-dimmed bg-charcoal-850 p-3">
197+
<pre className="whitespace-pre-wrap break-words font-mono text-sm text-text-bright">
198+
{log.message}
199+
</pre>
200+
</div>
201+
</div>
202+
203+
{/* Run Link */}
204+
<div className="mb-6">
205+
<Header3 className="mb-2">Run</Header3>
206+
<div className="flex items-center gap-3">
207+
<span className="font-mono text-sm text-text-bright">{log.runId}</span>
208+
<Link
209+
to={runPath}
210+
target="_blank"
211+
rel="noopener noreferrer"
212+
>
213+
<Button variant="tertiary/small" LeadingIcon={ArrowTopRightOnSquareIcon}>
214+
View in Run
215+
</Button>
216+
</Link>
217+
</div>
218+
</div>
219+
220+
{/* Details Grid */}
221+
<div className="mb-6">
222+
<Header3 className="mb-2">Details</Header3>
223+
<div className="grid grid-cols-2 gap-4 rounded-md border border-grid-dimmed bg-charcoal-850 p-3">
224+
<DetailItem label="Task" value={log.taskIdentifier} mono />
225+
<DetailItem label="Kind" value={log.kind} />
226+
<DetailItem label="Status" value={log.status} />
227+
<DetailItem
228+
label="Duration"
229+
value={
230+
log.duration > 0
231+
? formatDurationNanoseconds(log.duration, { style: "short" })
232+
: "–"
233+
}
234+
icon={<ClockIcon className="size-4 text-text-dimmed" />}
235+
/>
236+
<DetailItem label="Trace ID" value={log.traceId} mono small />
237+
<DetailItem label="Span ID" value={log.spanId} mono small />
238+
{log.parentSpanId && (
239+
<DetailItem label="Parent Span ID" value={log.parentSpanId} mono small />
240+
)}
241+
</div>
242+
</div>
243+
244+
{/* Metadata - only available in full log detail */}
245+
{"rawMetadata" in log &&
246+
(log as { rawMetadata?: string }).rawMetadata &&
247+
(log as { rawMetadata?: string }).rawMetadata !== "{}" && (
248+
<div className="mb-6">
249+
<Header3 className="mb-2">Metadata</Header3>
250+
<div className="rounded-md border border-grid-dimmed bg-charcoal-850 p-3">
251+
<pre className="whitespace-pre-wrap break-words font-mono text-xs text-text-dimmed">
252+
{JSON.stringify(
253+
"metadata" in log
254+
? (log as { metadata: Record<string, unknown> }).metadata
255+
: JSON.parse((log as { rawMetadata: string }).rawMetadata),
256+
null,
257+
2
258+
)}
259+
</pre>
260+
</div>
261+
</div>
262+
)}
263+
264+
{/* Attributes - only available in full log detail */}
265+
{"rawAttributes" in log &&
266+
(log as { rawAttributes?: string }).rawAttributes &&
267+
(log as { rawAttributes?: string }).rawAttributes !== "{}" && (
268+
<div className="mb-6">
269+
<Header3 className="mb-2">Attributes</Header3>
270+
<div className="rounded-md border border-grid-dimmed bg-charcoal-850 p-3">
271+
<pre className="whitespace-pre-wrap break-words font-mono text-xs text-text-dimmed">
272+
{JSON.stringify(
273+
"attributes" in log
274+
? (log as { attributes: Record<string, unknown> }).attributes
275+
: JSON.parse((log as { rawAttributes: string }).rawAttributes),
276+
null,
277+
2
278+
)}
279+
</pre>
280+
</div>
281+
</div>
282+
)}
283+
</div>
284+
</div>
285+
);
286+
}
287+
288+
function DetailItem({
289+
label,
290+
value,
291+
mono = false,
292+
small = false,
293+
icon,
294+
}: {
295+
label: string;
296+
value: string;
297+
mono?: boolean;
298+
small?: boolean;
299+
icon?: React.ReactNode;
300+
}) {
301+
return (
302+
<div>
303+
<Paragraph variant="extra-small" className="mb-1 text-text-dimmed">
304+
{label}
305+
</Paragraph>
306+
<div className="flex items-center gap-1">
307+
{icon}
308+
<span
309+
className={cn(
310+
"text-text-bright",
311+
mono && "font-mono",
312+
small ? "text-xs" : "text-sm"
313+
)}
314+
>
315+
{value}
316+
</span>
317+
</div>
318+
</div>
319+
);
320+
}

0 commit comments

Comments
 (0)