@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.
Files changed (75) hide show
  1. package/dist/{DataBrowser-hGwiTffZ.d.cts → DataBrowser-B-jz8KBR.d.mts} +5 -2
  2. package/dist/DataBrowser-B-jz8KBR.d.mts.map +1 -0
  3. package/dist/{DataBrowser-SOcqmZb2.d.mts → DataBrowser-BTe9HWJy.d.cts} +5 -2
  4. package/dist/DataBrowser-BTe9HWJy.d.cts.map +1 -0
  5. package/dist/{DataBrowser-c-Gs6PZB.cjs → DataBrowser-D8c_pBf4.cjs} +4 -4
  6. package/dist/DataBrowser-D8c_pBf4.cjs.map +1 -0
  7. package/dist/{DataBrowser-DQ3-ZxdV.mjs → DataBrowser-kgcI9ApJ.mjs} +4 -4
  8. package/dist/DataBrowser-kgcI9ApJ.mjs.map +1 -0
  9. package/dist/Studio-CYzz3wD2.d.cts +152 -0
  10. package/dist/Studio-CYzz3wD2.d.cts.map +1 -0
  11. package/dist/Studio-D5yGscb8.d.mts +152 -0
  12. package/dist/Studio-D5yGscb8.d.mts.map +1 -0
  13. package/dist/data/index.cjs +1 -1
  14. package/dist/data/index.d.cts +1 -1
  15. package/dist/data/index.d.mts +1 -1
  16. package/dist/data/index.mjs +1 -1
  17. package/dist/index.cjs +33 -3
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +4 -131
  20. package/dist/index.d.mts +4 -131
  21. package/dist/index.mjs +33 -3
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/server/hono.cjs +168 -21
  24. package/dist/server/hono.cjs.map +1 -1
  25. package/dist/server/hono.d.cts +13 -2
  26. package/dist/server/hono.d.cts.map +1 -0
  27. package/dist/server/hono.d.mts +13 -2
  28. package/dist/server/hono.d.mts.map +1 -0
  29. package/dist/server/hono.mjs +168 -21
  30. package/dist/server/hono.mjs.map +1 -1
  31. package/dist/types-BZv87Ikv.mjs.map +1 -1
  32. package/dist/types-CMttUZYk.cjs.map +1 -1
  33. package/package.json +14 -6
  34. package/src/Studio.ts +341 -292
  35. package/src/__tests__/Studio.spec.ts +447 -0
  36. package/src/data/DataBrowser.ts +147 -143
  37. package/src/data/__tests__/DataBrowser.integration.spec.ts +404 -404
  38. package/src/data/__tests__/filtering.integration.spec.ts +726 -726
  39. package/src/data/__tests__/introspection.integration.spec.ts +340 -340
  40. package/src/data/__tests__/pagination.spec.ts +123 -0
  41. package/src/data/filtering.ts +154 -154
  42. package/src/data/introspection.ts +141 -141
  43. package/src/data/pagination.ts +15 -15
  44. package/src/index.ts +22 -24
  45. package/src/server/__tests__/hono.integration.spec.ts +605 -347
  46. package/src/server/hono.ts +392 -190
  47. package/src/types.ts +138 -138
  48. package/src/ui-assets.ts +10 -13
  49. package/tsconfig.json +9 -0
  50. package/tsdown.config.ts +9 -9
  51. package/ui/package.json +28 -22
  52. package/ui/src/App.tsx +95 -235
  53. package/ui/src/api.ts +184 -42
  54. package/ui/src/components/FilterPanel.tsx +198 -198
  55. package/ui/src/components/NavRail.tsx +183 -0
  56. package/ui/src/components/RowDetail.tsx +106 -106
  57. package/ui/src/components/StudioHeader.tsx +109 -0
  58. package/ui/src/components/TableList.tsx +49 -49
  59. package/ui/src/components/TableView.tsx +530 -485
  60. package/ui/src/main.tsx +3 -3
  61. package/ui/src/pages/DashboardPage.tsx +500 -0
  62. package/ui/src/pages/DatabasePage.tsx +226 -0
  63. package/ui/src/pages/EndpointDetailsPage.tsx +288 -0
  64. package/ui/src/pages/ExceptionsPage.tsx +268 -0
  65. package/ui/src/pages/LogsPage.tsx +228 -0
  66. package/ui/src/pages/MonitoringPage.tsx +46 -0
  67. package/ui/src/pages/PerformancePage.tsx +307 -0
  68. package/ui/src/pages/RequestsPage.tsx +379 -0
  69. package/ui/src/providers/StudioProvider.tsx +194 -0
  70. package/ui/src/styles.css +53 -142
  71. package/ui/src/types.ts +154 -30
  72. package/ui/tsconfig.tsbuildinfo +1 -1
  73. package/ui/vite.config.ts +6 -6
  74. package/dist/DataBrowser-DQ3-ZxdV.mjs.map +0 -1
  75. 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
+ }