@geekmidas/studio 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataBrowser-hGwiTffZ.d.cts → DataBrowser-B-jz8KBR.d.mts} +5 -2
- package/dist/DataBrowser-B-jz8KBR.d.mts.map +1 -0
- package/dist/{DataBrowser-SOcqmZb2.d.mts → DataBrowser-BTe9HWJy.d.cts} +5 -2
- package/dist/DataBrowser-BTe9HWJy.d.cts.map +1 -0
- package/dist/{DataBrowser-c-Gs6PZB.cjs → DataBrowser-D8c_pBf4.cjs} +4 -4
- package/dist/DataBrowser-D8c_pBf4.cjs.map +1 -0
- package/dist/{DataBrowser-DQ3-ZxdV.mjs → DataBrowser-kgcI9ApJ.mjs} +4 -4
- package/dist/DataBrowser-kgcI9ApJ.mjs.map +1 -0
- package/dist/Studio-CYzz3wD2.d.cts +152 -0
- package/dist/Studio-CYzz3wD2.d.cts.map +1 -0
- package/dist/Studio-D5yGscb8.d.mts +152 -0
- package/dist/Studio-D5yGscb8.d.mts.map +1 -0
- package/dist/data/index.cjs +1 -1
- package/dist/data/index.d.cts +1 -1
- package/dist/data/index.d.mts +1 -1
- package/dist/data/index.mjs +1 -1
- package/dist/index.cjs +33 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -131
- package/dist/index.d.mts +4 -131
- package/dist/index.mjs +33 -3
- package/dist/index.mjs.map +1 -1
- package/dist/server/hono.cjs +168 -21
- package/dist/server/hono.cjs.map +1 -1
- package/dist/server/hono.d.cts +13 -2
- package/dist/server/hono.d.cts.map +1 -0
- package/dist/server/hono.d.mts +13 -2
- package/dist/server/hono.d.mts.map +1 -0
- package/dist/server/hono.mjs +168 -21
- package/dist/server/hono.mjs.map +1 -1
- package/dist/types-BZv87Ikv.mjs.map +1 -1
- package/dist/types-CMttUZYk.cjs.map +1 -1
- package/package.json +14 -6
- package/src/Studio.ts +341 -292
- package/src/__tests__/Studio.spec.ts +447 -0
- package/src/data/DataBrowser.ts +147 -143
- package/src/data/__tests__/DataBrowser.integration.spec.ts +404 -404
- package/src/data/__tests__/filtering.integration.spec.ts +726 -726
- package/src/data/__tests__/introspection.integration.spec.ts +340 -340
- package/src/data/__tests__/pagination.spec.ts +123 -0
- package/src/data/filtering.ts +154 -154
- package/src/data/introspection.ts +141 -141
- package/src/data/pagination.ts +15 -15
- package/src/index.ts +22 -24
- package/src/server/__tests__/hono.integration.spec.ts +605 -347
- package/src/server/hono.ts +392 -190
- package/src/types.ts +138 -138
- package/src/ui-assets.ts +10 -13
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +9 -9
- package/ui/package.json +28 -22
- package/ui/src/App.tsx +95 -235
- package/ui/src/api.ts +184 -42
- package/ui/src/components/FilterPanel.tsx +198 -198
- package/ui/src/components/NavRail.tsx +183 -0
- package/ui/src/components/RowDetail.tsx +106 -106
- package/ui/src/components/StudioHeader.tsx +109 -0
- package/ui/src/components/TableList.tsx +49 -49
- package/ui/src/components/TableView.tsx +530 -485
- package/ui/src/main.tsx +3 -3
- package/ui/src/pages/DashboardPage.tsx +500 -0
- package/ui/src/pages/DatabasePage.tsx +226 -0
- package/ui/src/pages/EndpointDetailsPage.tsx +288 -0
- package/ui/src/pages/ExceptionsPage.tsx +268 -0
- package/ui/src/pages/LogsPage.tsx +228 -0
- package/ui/src/pages/MonitoringPage.tsx +46 -0
- package/ui/src/pages/PerformancePage.tsx +307 -0
- package/ui/src/pages/RequestsPage.tsx +379 -0
- package/ui/src/providers/StudioProvider.tsx +194 -0
- package/ui/src/styles.css +53 -142
- package/ui/src/types.ts +154 -30
- package/ui/tsconfig.tsbuildinfo +1 -1
- package/ui/vite.config.ts +6 -6
- package/dist/DataBrowser-DQ3-ZxdV.mjs.map +0 -1
- package/dist/DataBrowser-c-Gs6PZB.cjs.map +0 -1
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { NoResults } from '@geekmidas/ui';
|
|
2
|
+
import { AlertTriangle, ArrowLeft, X } from 'lucide-react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import { useNavigate, useParams } from 'react-router-dom';
|
|
5
|
+
import * as api from '../api';
|
|
6
|
+
import { useStudio } from '../providers/StudioProvider';
|
|
7
|
+
import type { ExceptionEntry } from '../types';
|
|
8
|
+
|
|
9
|
+
interface ExceptionFilters {
|
|
10
|
+
search: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ExceptionsPage() {
|
|
14
|
+
const { id } = useParams<{ id: string }>();
|
|
15
|
+
const navigate = useNavigate();
|
|
16
|
+
const { exceptions: realtimeExceptions } = useStudio();
|
|
17
|
+
|
|
18
|
+
const [exceptions, setExceptions] = useState<ExceptionEntry[]>([]);
|
|
19
|
+
const [selectedException, setSelectedException] =
|
|
20
|
+
useState<ExceptionEntry | null>(null);
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
const [filters, setFilters] = useState<ExceptionFilters>({
|
|
23
|
+
search: '',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Merge realtime exceptions with fetched exceptions
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
setExceptions((prev) => {
|
|
29
|
+
const existingIds = new Set(prev.map((e) => e.id));
|
|
30
|
+
const newExceptions = realtimeExceptions.filter(
|
|
31
|
+
(e) => !existingIds.has(e.id),
|
|
32
|
+
);
|
|
33
|
+
if (newExceptions.length > 0) {
|
|
34
|
+
return [...newExceptions, ...prev].slice(0, 100);
|
|
35
|
+
}
|
|
36
|
+
return prev;
|
|
37
|
+
});
|
|
38
|
+
}, [realtimeExceptions]);
|
|
39
|
+
|
|
40
|
+
// Load exceptions
|
|
41
|
+
const loadExceptions = useCallback(async () => {
|
|
42
|
+
try {
|
|
43
|
+
setLoading(true);
|
|
44
|
+
const data = await api.getExceptions({
|
|
45
|
+
limit: 100,
|
|
46
|
+
search: filters.search || undefined,
|
|
47
|
+
});
|
|
48
|
+
setExceptions(data);
|
|
49
|
+
} catch (_error) {
|
|
50
|
+
} finally {
|
|
51
|
+
setLoading(false);
|
|
52
|
+
}
|
|
53
|
+
}, [filters]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
loadExceptions();
|
|
57
|
+
}, [loadExceptions]);
|
|
58
|
+
|
|
59
|
+
// Load selected exception detail
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (id) {
|
|
62
|
+
const existing = exceptions.find((e) => e.id === id);
|
|
63
|
+
if (existing) {
|
|
64
|
+
setSelectedException(existing);
|
|
65
|
+
} else {
|
|
66
|
+
api.getException(id).then(setSelectedException).catch(console.error);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
setSelectedException(null);
|
|
70
|
+
}
|
|
71
|
+
}, [id, exceptions]);
|
|
72
|
+
|
|
73
|
+
// Filter exceptions
|
|
74
|
+
const filteredExceptions = useMemo(() => {
|
|
75
|
+
return exceptions.filter((exception) => {
|
|
76
|
+
if (filters.search) {
|
|
77
|
+
const searchLower = filters.search.toLowerCase();
|
|
78
|
+
if (
|
|
79
|
+
!exception.name.toLowerCase().includes(searchLower) &&
|
|
80
|
+
!exception.message.toLowerCase().includes(searchLower)
|
|
81
|
+
) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
});
|
|
87
|
+
}, [exceptions, filters]);
|
|
88
|
+
|
|
89
|
+
const hasFilters = filters.search;
|
|
90
|
+
|
|
91
|
+
const clearFilters = () => {
|
|
92
|
+
setFilters({ search: '' });
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="flex h-full">
|
|
97
|
+
{/* Exception List */}
|
|
98
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
99
|
+
{/* Filter Bar */}
|
|
100
|
+
<div className="p-4 border-b border-border bg-surface flex items-center gap-4">
|
|
101
|
+
<input
|
|
102
|
+
type="text"
|
|
103
|
+
placeholder="Search exceptions..."
|
|
104
|
+
value={filters.search}
|
|
105
|
+
onChange={(e) =>
|
|
106
|
+
setFilters((f) => ({ ...f, search: e.target.value }))
|
|
107
|
+
}
|
|
108
|
+
className="flex-1 bg-background border border-border rounded px-3 py-1.5 text-sm focus:outline-none focus:border-accent"
|
|
109
|
+
/>
|
|
110
|
+
{hasFilters && (
|
|
111
|
+
<button
|
|
112
|
+
onClick={clearFilters}
|
|
113
|
+
className="text-sm text-muted-foreground hover:text-foreground"
|
|
114
|
+
>
|
|
115
|
+
Clear
|
|
116
|
+
</button>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Exception List */}
|
|
121
|
+
<div className="flex-1 overflow-auto p-4">
|
|
122
|
+
{loading ? (
|
|
123
|
+
<div className="flex items-center justify-center py-16 text-muted-foreground">
|
|
124
|
+
Loading...
|
|
125
|
+
</div>
|
|
126
|
+
) : filteredExceptions.length === 0 ? (
|
|
127
|
+
<NoResults
|
|
128
|
+
title={hasFilters ? 'No matching exceptions' : 'No exceptions'}
|
|
129
|
+
description={
|
|
130
|
+
hasFilters
|
|
131
|
+
? 'Try adjusting your filters.'
|
|
132
|
+
: 'Exceptions will appear here when they occur.'
|
|
133
|
+
}
|
|
134
|
+
/>
|
|
135
|
+
) : (
|
|
136
|
+
<div className="flex flex-col gap-2">
|
|
137
|
+
{filteredExceptions.map((exception) => (
|
|
138
|
+
<div
|
|
139
|
+
key={exception.id}
|
|
140
|
+
className={`bg-surface border rounded-lg p-4 cursor-pointer transition-colors hover:border-accent/50 ${
|
|
141
|
+
selectedException?.id === exception.id
|
|
142
|
+
? 'border-accent'
|
|
143
|
+
: 'border-border'
|
|
144
|
+
}`}
|
|
145
|
+
onClick={() =>
|
|
146
|
+
navigate(`/monitoring/exceptions/${exception.id}`)
|
|
147
|
+
}
|
|
148
|
+
>
|
|
149
|
+
<div className="flex justify-between items-center mb-1">
|
|
150
|
+
<div className="flex items-center gap-2">
|
|
151
|
+
<AlertTriangle className="h-4 w-4 text-red-400" />
|
|
152
|
+
<span className="font-medium text-red-400">
|
|
153
|
+
{exception.name}
|
|
154
|
+
</span>
|
|
155
|
+
</div>
|
|
156
|
+
<span className="text-xs text-muted-foreground">
|
|
157
|
+
{formatTime(exception.timestamp)}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
<p className="text-sm text-muted-foreground truncate pl-6">
|
|
161
|
+
{exception.message}
|
|
162
|
+
</p>
|
|
163
|
+
</div>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Exception Detail Panel */}
|
|
171
|
+
{selectedException && (
|
|
172
|
+
<ExceptionDetailPanel
|
|
173
|
+
exception={selectedException}
|
|
174
|
+
onClose={() => navigate('/monitoring/exceptions')}
|
|
175
|
+
/>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function ExceptionDetailPanel({
|
|
182
|
+
exception,
|
|
183
|
+
onClose,
|
|
184
|
+
}: {
|
|
185
|
+
exception: ExceptionEntry;
|
|
186
|
+
onClose: () => void;
|
|
187
|
+
}) {
|
|
188
|
+
return (
|
|
189
|
+
<div className="w-[600px] border-l border-border bg-surface flex flex-col">
|
|
190
|
+
{/* Header */}
|
|
191
|
+
<div className="flex items-center justify-between p-4 border-b border-border">
|
|
192
|
+
<div className="flex items-center gap-3">
|
|
193
|
+
<button
|
|
194
|
+
onClick={onClose}
|
|
195
|
+
className="p-1 hover:bg-surface-hover rounded"
|
|
196
|
+
>
|
|
197
|
+
<ArrowLeft className="h-4 w-4" />
|
|
198
|
+
</button>
|
|
199
|
+
<div>
|
|
200
|
+
<div className="flex items-center gap-2">
|
|
201
|
+
<AlertTriangle className="h-4 w-4 text-red-400" />
|
|
202
|
+
<span className="font-medium text-red-400">{exception.name}</span>
|
|
203
|
+
</div>
|
|
204
|
+
<p className="text-xs text-muted-foreground mt-1">
|
|
205
|
+
{formatTime(exception.timestamp)}
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
<button
|
|
210
|
+
onClick={onClose}
|
|
211
|
+
className="p-1 hover:bg-surface-hover rounded"
|
|
212
|
+
>
|
|
213
|
+
<X className="h-4 w-4" />
|
|
214
|
+
</button>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{/* Content */}
|
|
218
|
+
<div className="flex-1 overflow-auto p-4 space-y-4">
|
|
219
|
+
{/* Message */}
|
|
220
|
+
<div>
|
|
221
|
+
<h4 className="text-sm font-medium mb-2">Message</h4>
|
|
222
|
+
<p className="text-sm bg-background rounded p-3 break-words text-red-300">
|
|
223
|
+
{exception.message}
|
|
224
|
+
</p>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
{/* Request info */}
|
|
228
|
+
{exception.request && (
|
|
229
|
+
<div>
|
|
230
|
+
<h4 className="text-sm font-medium mb-2">Request</h4>
|
|
231
|
+
<p className="text-sm font-mono bg-background rounded p-3">
|
|
232
|
+
{exception.request.method} {exception.request.path}
|
|
233
|
+
</p>
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
{/* Stack trace */}
|
|
238
|
+
{exception.stack && (
|
|
239
|
+
<div>
|
|
240
|
+
<h4 className="text-sm font-medium mb-2">Stack Trace</h4>
|
|
241
|
+
<pre className="bg-background rounded p-3 text-xs overflow-auto max-h-[400px] whitespace-pre-wrap">
|
|
242
|
+
{formatStackTrace(exception.stack)}
|
|
243
|
+
</pre>
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
|
|
247
|
+
{/* Context */}
|
|
248
|
+
{exception.context && Object.keys(exception.context).length > 0 && (
|
|
249
|
+
<div>
|
|
250
|
+
<h4 className="text-sm font-medium mb-2">Context</h4>
|
|
251
|
+
<pre className="bg-background rounded p-3 text-xs overflow-auto max-h-[200px]">
|
|
252
|
+
{JSON.stringify(exception.context, null, 2)}
|
|
253
|
+
</pre>
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function formatStackTrace(stack: string) {
|
|
262
|
+
// Highlight file paths and line numbers
|
|
263
|
+
return stack;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function formatTime(timestamp: string) {
|
|
267
|
+
return new Date(timestamp).toLocaleString();
|
|
268
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { LogLevelBadge, NoResults } from '@geekmidas/ui';
|
|
2
|
+
import { X } from 'lucide-react';
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
|
+
import * as api from '../api';
|
|
5
|
+
import { useStudio } from '../providers/StudioProvider';
|
|
6
|
+
import type { LogEntry } from '../types';
|
|
7
|
+
|
|
8
|
+
interface LogFilters {
|
|
9
|
+
search: string;
|
|
10
|
+
level: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function LogsPage() {
|
|
14
|
+
const { logs: realtimeLogs } = useStudio();
|
|
15
|
+
|
|
16
|
+
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
17
|
+
const [selectedLog, setSelectedLog] = useState<LogEntry | null>(null);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [filters, setFilters] = useState<LogFilters>({
|
|
20
|
+
search: '',
|
|
21
|
+
level: '',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Merge realtime logs with fetched logs
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
setLogs((prev) => {
|
|
27
|
+
const existingIds = new Set(prev.map((l) => l.id));
|
|
28
|
+
const newLogs = realtimeLogs.filter((l) => !existingIds.has(l.id));
|
|
29
|
+
if (newLogs.length > 0) {
|
|
30
|
+
return [...newLogs, ...prev].slice(0, 100);
|
|
31
|
+
}
|
|
32
|
+
return prev;
|
|
33
|
+
});
|
|
34
|
+
}, [realtimeLogs]);
|
|
35
|
+
|
|
36
|
+
// Load logs
|
|
37
|
+
const loadLogs = useCallback(async () => {
|
|
38
|
+
try {
|
|
39
|
+
setLoading(true);
|
|
40
|
+
const data = await api.getLogs({
|
|
41
|
+
limit: 100,
|
|
42
|
+
search: filters.search || undefined,
|
|
43
|
+
level: filters.level || undefined,
|
|
44
|
+
});
|
|
45
|
+
setLogs(data);
|
|
46
|
+
} catch (_error) {
|
|
47
|
+
} finally {
|
|
48
|
+
setLoading(false);
|
|
49
|
+
}
|
|
50
|
+
}, [filters]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
loadLogs();
|
|
54
|
+
}, [loadLogs]);
|
|
55
|
+
|
|
56
|
+
// Filter logs
|
|
57
|
+
const filteredLogs = useMemo(() => {
|
|
58
|
+
return logs.filter((log) => {
|
|
59
|
+
if (
|
|
60
|
+
filters.search &&
|
|
61
|
+
!log.message.toLowerCase().includes(filters.search.toLowerCase())
|
|
62
|
+
) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if (filters.level && log.level !== filters.level) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
}, [logs, filters]);
|
|
71
|
+
|
|
72
|
+
const hasFilters = filters.search || filters.level;
|
|
73
|
+
|
|
74
|
+
const clearFilters = () => {
|
|
75
|
+
setFilters({ search: '', level: '' });
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="flex h-full">
|
|
80
|
+
{/* Log List */}
|
|
81
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
82
|
+
{/* Filter Bar */}
|
|
83
|
+
<div className="p-4 border-b border-border bg-surface flex items-center gap-4">
|
|
84
|
+
<input
|
|
85
|
+
type="text"
|
|
86
|
+
placeholder="Search logs..."
|
|
87
|
+
value={filters.search}
|
|
88
|
+
onChange={(e) =>
|
|
89
|
+
setFilters((f) => ({ ...f, search: e.target.value }))
|
|
90
|
+
}
|
|
91
|
+
className="flex-1 bg-background border border-border rounded px-3 py-1.5 text-sm focus:outline-none focus:border-accent"
|
|
92
|
+
/>
|
|
93
|
+
<select
|
|
94
|
+
value={filters.level}
|
|
95
|
+
onChange={(e) =>
|
|
96
|
+
setFilters((f) => ({ ...f, level: e.target.value }))
|
|
97
|
+
}
|
|
98
|
+
className="bg-background border border-border rounded px-3 py-1.5 text-sm focus:outline-none focus:border-accent"
|
|
99
|
+
>
|
|
100
|
+
<option value="">All Levels</option>
|
|
101
|
+
<option value="trace">Trace</option>
|
|
102
|
+
<option value="debug">Debug</option>
|
|
103
|
+
<option value="info">Info</option>
|
|
104
|
+
<option value="warn">Warn</option>
|
|
105
|
+
<option value="error">Error</option>
|
|
106
|
+
<option value="fatal">Fatal</option>
|
|
107
|
+
</select>
|
|
108
|
+
{hasFilters && (
|
|
109
|
+
<button
|
|
110
|
+
onClick={clearFilters}
|
|
111
|
+
className="text-sm text-muted-foreground hover:text-foreground"
|
|
112
|
+
>
|
|
113
|
+
Clear
|
|
114
|
+
</button>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Log List */}
|
|
119
|
+
<div className="flex-1 overflow-auto p-4">
|
|
120
|
+
{loading ? (
|
|
121
|
+
<div className="flex items-center justify-center py-16 text-muted-foreground">
|
|
122
|
+
Loading...
|
|
123
|
+
</div>
|
|
124
|
+
) : filteredLogs.length === 0 ? (
|
|
125
|
+
<NoResults
|
|
126
|
+
title={hasFilters ? 'No matching logs' : 'No logs yet'}
|
|
127
|
+
description={
|
|
128
|
+
hasFilters
|
|
129
|
+
? 'Try adjusting your filters.'
|
|
130
|
+
: 'Log entries will appear here as they are recorded.'
|
|
131
|
+
}
|
|
132
|
+
/>
|
|
133
|
+
) : (
|
|
134
|
+
<div className="flex flex-col gap-2">
|
|
135
|
+
{filteredLogs.map((log) => (
|
|
136
|
+
<div
|
|
137
|
+
key={log.id}
|
|
138
|
+
className={`bg-surface border rounded-lg p-4 cursor-pointer transition-colors hover:border-accent/50 flex items-start gap-4 ${
|
|
139
|
+
selectedLog?.id === log.id
|
|
140
|
+
? 'border-accent'
|
|
141
|
+
: 'border-border'
|
|
142
|
+
}`}
|
|
143
|
+
onClick={() => setSelectedLog(log)}
|
|
144
|
+
>
|
|
145
|
+
<LogLevelBadge level={log.level as any} size="sm" />
|
|
146
|
+
<span className="flex-1 text-sm break-words">
|
|
147
|
+
{log.message}
|
|
148
|
+
</span>
|
|
149
|
+
<span className="text-xs text-muted-foreground min-w-20 text-right shrink-0">
|
|
150
|
+
{formatTime(log.timestamp)}
|
|
151
|
+
</span>
|
|
152
|
+
</div>
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* Log Detail Panel */}
|
|
160
|
+
{selectedLog && (
|
|
161
|
+
<LogDetailPanel
|
|
162
|
+
log={selectedLog}
|
|
163
|
+
onClose={() => setSelectedLog(null)}
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function LogDetailPanel({
|
|
171
|
+
log,
|
|
172
|
+
onClose,
|
|
173
|
+
}: {
|
|
174
|
+
log: LogEntry;
|
|
175
|
+
onClose: () => void;
|
|
176
|
+
}) {
|
|
177
|
+
return (
|
|
178
|
+
<div className="w-[500px] border-l border-border bg-surface flex flex-col">
|
|
179
|
+
{/* Header */}
|
|
180
|
+
<div className="flex items-center justify-between p-4 border-b border-border">
|
|
181
|
+
<div className="flex items-center gap-3">
|
|
182
|
+
<LogLevelBadge level={log.level as any} size="md" />
|
|
183
|
+
<span className="text-sm text-muted-foreground">
|
|
184
|
+
{formatTime(log.timestamp)}
|
|
185
|
+
</span>
|
|
186
|
+
</div>
|
|
187
|
+
<button
|
|
188
|
+
onClick={onClose}
|
|
189
|
+
className="p-1 hover:bg-surface-hover rounded"
|
|
190
|
+
>
|
|
191
|
+
<X className="h-4 w-4" />
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* Content */}
|
|
196
|
+
<div className="flex-1 overflow-auto p-4 space-y-4">
|
|
197
|
+
<div>
|
|
198
|
+
<h4 className="text-sm font-medium mb-2">Message</h4>
|
|
199
|
+
<p className="text-sm bg-background rounded p-3 break-words">
|
|
200
|
+
{log.message}
|
|
201
|
+
</p>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{log.requestId && (
|
|
205
|
+
<div>
|
|
206
|
+
<h4 className="text-sm font-medium mb-2">Request ID</h4>
|
|
207
|
+
<p className="text-sm font-mono bg-background rounded p-3">
|
|
208
|
+
{log.requestId}
|
|
209
|
+
</p>
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
|
|
213
|
+
{log.context && Object.keys(log.context).length > 0 && (
|
|
214
|
+
<div>
|
|
215
|
+
<h4 className="text-sm font-medium mb-2">Context</h4>
|
|
216
|
+
<pre className="bg-background rounded p-3 text-xs overflow-auto max-h-[400px]">
|
|
217
|
+
{JSON.stringify(log.context, null, 2)}
|
|
218
|
+
</pre>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatTime(timestamp: string) {
|
|
227
|
+
return new Date(timestamp).toLocaleTimeString();
|
|
228
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { AlertTriangle, FileText, Network } from 'lucide-react';
|
|
2
|
+
import { NavLink, Outlet, useLocation } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
const tabs = [
|
|
5
|
+
{ path: '/monitoring/requests', label: 'Requests', icon: Network },
|
|
6
|
+
{ path: '/monitoring/logs', label: 'Logs', icon: FileText },
|
|
7
|
+
{ path: '/monitoring/exceptions', label: 'Exceptions', icon: AlertTriangle },
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
export function MonitoringPage() {
|
|
11
|
+
const location = useLocation();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex flex-col h-full">
|
|
15
|
+
{/* Tab Bar */}
|
|
16
|
+
<div className="bg-surface border-b border-border px-4">
|
|
17
|
+
<nav className="flex gap-1">
|
|
18
|
+
{tabs.map((tab) => {
|
|
19
|
+
const Icon = tab.icon;
|
|
20
|
+
const isActive = location.pathname.startsWith(tab.path);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<NavLink
|
|
24
|
+
key={tab.path}
|
|
25
|
+
to={tab.path}
|
|
26
|
+
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
|
|
27
|
+
isActive
|
|
28
|
+
? 'text-accent border-accent'
|
|
29
|
+
: 'text-muted-foreground border-transparent hover:text-foreground hover:border-border'
|
|
30
|
+
}`}
|
|
31
|
+
>
|
|
32
|
+
<Icon className="h-4 w-4" />
|
|
33
|
+
{tab.label}
|
|
34
|
+
</NavLink>
|
|
35
|
+
);
|
|
36
|
+
})}
|
|
37
|
+
</nav>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{/* Content */}
|
|
41
|
+
<div className="flex-1 overflow-hidden">
|
|
42
|
+
<Outlet />
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|