Highly ambitious ATProtocol AppView service and sdks
1import { LogLevelBadge } from "./LogLevelBadge.tsx";
2import { Text } from "./Text.tsx";
3import { Card } from "./Card.tsx";
4import { NetworkSlicesSliceGetJobLogsLogEntry } from "../../client.ts";
5import { formatTimestamp } from "../../utils/time.ts";
6
7interface LogViewerProps {
8 logs: NetworkSlicesSliceGetJobLogsLogEntry[];
9 emptyMessage?: string;
10}
11
12export function LogViewer({
13 logs,
14 emptyMessage = "No logs available.",
15}: LogViewerProps) {
16 if (logs.length === 0) {
17 return (
18 <div className="p-8 text-center">
19 <Text as="p" variant="muted">
20 {emptyMessage}
21 </Text>
22 </div>
23 );
24 }
25
26 const errorCount = logs.filter((l) => l.level === "error").length;
27 const warnCount = logs.filter((l) => l.level === "warn").length;
28 const infoCount = logs.filter((l) => l.level === "info").length;
29
30 return (
31 <>
32 <Card>
33 <div className="bg-zinc-50 dark:bg-zinc-800 px-6 py-3 border-b border-zinc-200 dark:border-zinc-700 rounded-t-sm">
34 <div className="flex items-center gap-4">
35 <Text as="span" size="sm">
36 Total: <strong>{logs.length}</strong>
37 </Text>
38 {errorCount > 0 && (
39 <Text as="span" size="sm" variant="error">
40 Errors: <strong>{errorCount}</strong>
41 </Text>
42 )}
43 {warnCount > 0 && (
44 <Text as="span" size="sm" variant="warning">
45 Warnings: <strong>{warnCount}</strong>
46 </Text>
47 )}
48 <Text
49 as="span"
50 size="sm"
51 className="text-blue-600 dark:text-blue-400"
52 >
53 Info: <strong>{infoCount}</strong>
54 </Text>
55 </div>
56 </div>
57
58 <Card.Content className="divide-y divide-zinc-200 dark:divide-zinc-700">
59 {logs.map((log) => (
60 <div
61 key={log.id}
62 className="p-3 hover:bg-zinc-50 dark:hover:bg-zinc-800 font-mono text-sm"
63 >
64 <div className="flex items-start gap-3">
65 <span className="text-xs text-zinc-500 dark:text-zinc-400">
66 {formatTimestamp(log.createdAt)}
67 </span>
68 <LogLevelBadge level={log.level} />
69 <div className="flex-1">
70 <Text as="div" size="sm">
71 {log.message}
72 </Text>
73 {log.metadata && Object.keys(log.metadata).length > 0 && (
74 <details className="mt-2">
75 <summary
76 className="cursor-pointer hover:text-zinc-700 dark:hover:text-zinc-300"
77 /* @ts-ignore - Hyperscript attribute */
78 _="on click toggle .hidden on next <pre/>"
79 >
80 <Text as="span" size="xs" variant="muted">
81 View metadata
82 </Text>
83 </summary>
84 <pre className="mt-2 p-2 bg-zinc-100 dark:bg-zinc-800 rounded text-xs overflow-x-auto break-words whitespace-pre-wrap hidden">
85 <Text as="span" size="xs">
86 {JSON.stringify(log.metadata, null, 2)}
87 </Text>
88 </pre>
89 </details>
90 )}
91 </div>
92 </div>
93 </div>
94 ))}
95 </Card.Content>
96 </Card>
97 </>
98 );
99}