@geekmidas/studio 0.2.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 +5 -5
  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,226 @@
1
+ import { NoData } from '@geekmidas/ui';
2
+ import { Database, Search } from 'lucide-react';
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
5
+ import * as api from '../api';
6
+ import { RowDetail } from '../components/RowDetail';
7
+ import type { ForeignKeyClickInfo } from '../components/TableView';
8
+ import { TableView } from '../components/TableView';
9
+ import type { FilterConfig, TableInfo, TableSummary } from '../types';
10
+
11
+ export function DatabasePage() {
12
+ const { table: tableParam } = useParams<{ table: string }>();
13
+ const [searchParams] = useSearchParams();
14
+ const navigate = useNavigate();
15
+
16
+ const [tables, setTables] = useState<TableSummary[]>([]);
17
+ const [selectedTable, setSelectedTable] = useState<string | null>(
18
+ tableParam || null,
19
+ );
20
+ const [tableInfo, setTableInfo] = useState<TableInfo | null>(null);
21
+ const [selectedRow, setSelectedRow] = useState<Record<
22
+ string,
23
+ unknown
24
+ > | null>(null);
25
+ const [loading, setLoading] = useState(true);
26
+ const [error, setError] = useState<string | null>(null);
27
+ const [searchTerm, setSearchTerm] = useState('');
28
+ const [initialFilters, setInitialFilters] = useState<FilterConfig[]>([]);
29
+
30
+ // Parse filter from URL query params
31
+ useEffect(() => {
32
+ const filterColumn = searchParams.get('filterColumn');
33
+ const filterValue = searchParams.get('filterValue');
34
+
35
+ if (filterColumn && filterValue) {
36
+ setInitialFilters([
37
+ {
38
+ column: filterColumn,
39
+ operator: 'eq',
40
+ value: filterValue,
41
+ },
42
+ ]);
43
+ } else {
44
+ setInitialFilters([]);
45
+ }
46
+ }, [searchParams]);
47
+
48
+ // Update selected table when URL param changes
49
+ useEffect(() => {
50
+ if (tableParam) {
51
+ setSelectedTable(tableParam);
52
+ }
53
+ }, [tableParam]);
54
+
55
+ // Load tables on mount
56
+ useEffect(() => {
57
+ async function loadTables() {
58
+ try {
59
+ const data = await api.getTables();
60
+ setTables(data.tables);
61
+ // Auto-select first table if none selected
62
+ if (data.tables.length > 0 && !selectedTable) {
63
+ const firstTable = data.tables[0].name;
64
+ setSelectedTable(firstTable);
65
+ navigate(`/database/${firstTable}`, { replace: true });
66
+ }
67
+ } catch (err) {
68
+ setError(err instanceof Error ? err.message : 'Failed to load tables');
69
+ } finally {
70
+ setLoading(false);
71
+ }
72
+ }
73
+ loadTables();
74
+ }, [navigate, selectedTable]);
75
+
76
+ // Load table info when selected
77
+ useEffect(() => {
78
+ if (!selectedTable) {
79
+ setTableInfo(null);
80
+ return;
81
+ }
82
+
83
+ async function loadTableInfo() {
84
+ try {
85
+ const info = await api.getTableInfo(selectedTable!);
86
+ setTableInfo(info);
87
+ } catch (err) {
88
+ setError(
89
+ err instanceof Error ? err.message : 'Failed to load table info',
90
+ );
91
+ }
92
+ }
93
+ loadTableInfo();
94
+ }, [selectedTable]);
95
+
96
+ const handleRefresh = useCallback(async () => {
97
+ setLoading(true);
98
+ try {
99
+ await api.getSchema(true);
100
+ const data = await api.getTables();
101
+ setTables(data.tables);
102
+ } catch (err) {
103
+ setError(err instanceof Error ? err.message : 'Failed to refresh');
104
+ } finally {
105
+ setLoading(false);
106
+ }
107
+ }, []);
108
+
109
+ const handleSelectTable = useCallback(
110
+ (tableName: string) => {
111
+ setSelectedTable(tableName);
112
+ setSelectedRow(null);
113
+ navigate(`/database/${tableName}`);
114
+ },
115
+ [navigate],
116
+ );
117
+
118
+ const handleForeignKeyClick = useCallback(
119
+ (info: ForeignKeyClickInfo) => {
120
+ const valueStr = String(info.value);
121
+ navigate(
122
+ `/database/${info.targetTable}?filterColumn=${encodeURIComponent(info.targetColumn)}&filterValue=${encodeURIComponent(valueStr)}`,
123
+ );
124
+ },
125
+ [navigate],
126
+ );
127
+
128
+ // Filter tables by search term
129
+ const filteredTables = tables.filter((t) =>
130
+ t.name.toLowerCase().includes(searchTerm.toLowerCase()),
131
+ );
132
+
133
+ return (
134
+ <div className="flex h-full">
135
+ {/* Table Sidebar */}
136
+ <aside className="w-64 bg-surface border-r border-border flex flex-col shrink-0">
137
+ <div className="p-3 border-b border-border">
138
+ <div className="relative">
139
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
140
+ <input
141
+ type="text"
142
+ placeholder="Search tables..."
143
+ value={searchTerm}
144
+ onChange={(e) => setSearchTerm(e.target.value)}
145
+ className="w-full bg-background border border-border rounded px-3 py-1.5 pl-9 text-sm placeholder-muted-foreground focus:outline-none focus:border-accent"
146
+ />
147
+ </div>
148
+ </div>
149
+
150
+ <div className="flex-1 overflow-auto">
151
+ {loading && tables.length === 0 ? (
152
+ <div className="p-4 text-center text-muted-foreground text-sm">
153
+ Loading...
154
+ </div>
155
+ ) : (
156
+ <div className="py-1">
157
+ {filteredTables.map((table) => (
158
+ <button
159
+ key={table.name}
160
+ onClick={() => handleSelectTable(table.name)}
161
+ className={`w-full flex items-center gap-2 px-3 py-2 text-sm transition-colors ${
162
+ selectedTable === table.name
163
+ ? 'bg-accent/10 text-accent'
164
+ : 'text-foreground hover:bg-surface-hover'
165
+ }`}
166
+ >
167
+ <Database className="h-4 w-4 shrink-0" />
168
+ <span className="truncate">{table.name}</span>
169
+ {table.estimatedRowCount !== undefined && (
170
+ <span className="ml-auto text-xs text-muted-foreground">
171
+ {table.estimatedRowCount.toLocaleString()}
172
+ </span>
173
+ )}
174
+ </button>
175
+ ))}
176
+ </div>
177
+ )}
178
+ </div>
179
+
180
+ <div className="p-3 border-t border-border flex items-center justify-between text-xs text-muted-foreground">
181
+ <span>{tables.length} tables</span>
182
+ <button
183
+ onClick={handleRefresh}
184
+ disabled={loading}
185
+ className="hover:text-foreground transition-colors"
186
+ >
187
+ Refresh
188
+ </button>
189
+ </div>
190
+ </aside>
191
+
192
+ {/* Main Area */}
193
+ <div className="flex-1 flex flex-col overflow-hidden">
194
+ {error ? (
195
+ <div className="flex-1 flex items-center justify-center text-red-400">
196
+ <div className="text-center">
197
+ <p>{error}</p>
198
+ </div>
199
+ </div>
200
+ ) : !selectedTable ? (
201
+ <NoData
202
+ title="Select a table"
203
+ description="Choose a table from the sidebar to view its data."
204
+ />
205
+ ) : (
206
+ <TableView
207
+ tableName={selectedTable}
208
+ tableInfo={tableInfo}
209
+ onRowSelect={setSelectedRow}
210
+ onForeignKeyClick={handleForeignKeyClick}
211
+ initialFilters={initialFilters}
212
+ />
213
+ )}
214
+ </div>
215
+
216
+ {/* Row Detail Panel */}
217
+ {selectedRow && tableInfo && (
218
+ <RowDetail
219
+ row={selectedRow}
220
+ columns={tableInfo.columns}
221
+ onClose={() => setSelectedRow(null)}
222
+ />
223
+ )}
224
+ </div>
225
+ );
226
+ }
@@ -0,0 +1,288 @@
1
+ import {
2
+ AreaTimeSeriesChart,
3
+ Badge,
4
+ createTimeRange,
5
+ LatencyPercentilesChart,
6
+ MetricCard,
7
+ StatusDistributionChart,
8
+ type TimeRange,
9
+ TimeRangeSelector,
10
+ } from '@geekmidas/ui';
11
+ import { ArrowLeft } from 'lucide-react';
12
+ import { useCallback, useEffect, useState } from 'react';
13
+ import { Link, useSearchParams } from 'react-router-dom';
14
+ import { getEndpointDetails, getRequests } from '../api';
15
+ import type { EndpointDetails, RequestEntry } from '../types';
16
+
17
+ function formatDuration(ms: number): string {
18
+ if (ms < 1) return '<1ms';
19
+ if (ms < 1000) return `${Math.round(ms)}ms`;
20
+ return `${(ms / 1000).toFixed(2)}s`;
21
+ }
22
+
23
+ function formatNumber(n: number): string {
24
+ if (n < 1000) return String(Math.round(n));
25
+ if (n < 1000000) return `${(n / 1000).toFixed(1)}k`;
26
+ return `${(n / 1000000).toFixed(1)}M`;
27
+ }
28
+
29
+ function formatPercent(p: number): string {
30
+ return `${p.toFixed(1)}%`;
31
+ }
32
+
33
+ function formatTimestamp(ts: number | string): string {
34
+ return new Date(ts).toLocaleString();
35
+ }
36
+
37
+ function getMethodVariant(
38
+ method: string,
39
+ ): 'get' | 'post' | 'put' | 'patch' | 'delete' | 'default' {
40
+ const m = method.toLowerCase();
41
+ if (
42
+ m === 'get' ||
43
+ m === 'post' ||
44
+ m === 'put' ||
45
+ m === 'patch' ||
46
+ m === 'delete'
47
+ ) {
48
+ return m;
49
+ }
50
+ return 'default';
51
+ }
52
+
53
+ function getStatusColor(status: number): string {
54
+ if (status >= 200 && status < 300) return 'text-green-500';
55
+ if (status >= 300 && status < 400) return 'text-blue-500';
56
+ if (status >= 400 && status < 500) return 'text-amber-500';
57
+ return 'text-red-500';
58
+ }
59
+
60
+ export function EndpointDetailsPage() {
61
+ const [searchParams] = useSearchParams();
62
+ const method = searchParams.get('method') ?? '';
63
+ const path = searchParams.get('path') ?? '';
64
+
65
+ const [details, setDetails] = useState<EndpointDetails | null>(null);
66
+ const [recentRequests, setRecentRequests] = useState<RequestEntry[]>([]);
67
+ const [loading, setLoading] = useState(true);
68
+ const [error, setError] = useState<string | null>(null);
69
+ const [timeRange, setTimeRange] = useState<TimeRange>(() =>
70
+ createTimeRange('1h'),
71
+ );
72
+
73
+ const fetchData = useCallback(async () => {
74
+ if (!method || !path) {
75
+ setError('Missing method or path');
76
+ setLoading(false);
77
+ return;
78
+ }
79
+
80
+ try {
81
+ setLoading(true);
82
+ const timeRangeParams = {
83
+ start: timeRange.start.toISOString(),
84
+ end: timeRange.end.toISOString(),
85
+ };
86
+
87
+ const [detailsData, requestsData] = await Promise.all([
88
+ getEndpointDetails(method, path, timeRangeParams),
89
+ getRequests({ method, search: path, limit: 20 }),
90
+ ]);
91
+
92
+ setDetails(detailsData);
93
+ setRecentRequests(requestsData);
94
+ setError(null);
95
+ } catch (err) {
96
+ setError(
97
+ err instanceof Error ? err.message : 'Failed to load endpoint details',
98
+ );
99
+ } finally {
100
+ setLoading(false);
101
+ }
102
+ }, [method, path, timeRange.start, timeRange.end]);
103
+
104
+ useEffect(() => {
105
+ fetchData();
106
+ }, [fetchData]);
107
+
108
+ if (!method || !path) {
109
+ return (
110
+ <div className="flex-1 flex items-center justify-center text-muted-foreground">
111
+ Missing endpoint method or path
112
+ </div>
113
+ );
114
+ }
115
+
116
+ if (loading && !details) {
117
+ return (
118
+ <div className="flex-1 flex items-center justify-center text-muted-foreground">
119
+ Loading endpoint details...
120
+ </div>
121
+ );
122
+ }
123
+
124
+ if (error) {
125
+ return (
126
+ <div className="flex-1 flex items-center justify-center text-red-500">
127
+ {error}
128
+ </div>
129
+ );
130
+ }
131
+
132
+ return (
133
+ <div className="p-6 space-y-6">
134
+ {/* Header */}
135
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
136
+ <div className="flex items-center gap-4">
137
+ <Link
138
+ to="/performance"
139
+ className="p-2 rounded-md hover:bg-muted transition-colors"
140
+ >
141
+ <ArrowLeft className="w-5 h-5" />
142
+ </Link>
143
+ <div>
144
+ <div className="flex items-center gap-2">
145
+ <Badge variant={getMethodVariant(method)}>{method}</Badge>
146
+ <h1 className="text-xl font-semibold font-mono">{path}</h1>
147
+ </div>
148
+ <p className="text-sm text-muted-foreground mt-1">
149
+ Last seen: {details ? formatTimestamp(details.lastSeen) : 'N/A'}
150
+ </p>
151
+ </div>
152
+ </div>
153
+ <TimeRangeSelector value={timeRange} onChange={setTimeRange} />
154
+ </div>
155
+
156
+ {details && (
157
+ <>
158
+ {/* Key Metrics */}
159
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
160
+ <MetricCard
161
+ title="Total Requests"
162
+ value={formatNumber(details.count)}
163
+ trend="neutral"
164
+ />
165
+ <MetricCard
166
+ title="Avg Duration"
167
+ value={formatDuration(details.avgDuration)}
168
+ trend="neutral"
169
+ trendValue={`p95: ${formatDuration(details.p95Duration)}`}
170
+ />
171
+ <MetricCard
172
+ title="Success Rate"
173
+ value={formatPercent(details.successRate)}
174
+ trend={
175
+ details.successRate >= 99
176
+ ? 'up'
177
+ : details.successRate >= 95
178
+ ? 'neutral'
179
+ : 'down'
180
+ }
181
+ />
182
+ <MetricCard
183
+ title="Error Rate"
184
+ value={formatPercent(details.errorRate)}
185
+ trend={
186
+ details.errorRate <= 1
187
+ ? 'up'
188
+ : details.errorRate <= 5
189
+ ? 'neutral'
190
+ : 'down'
191
+ }
192
+ />
193
+ </div>
194
+
195
+ {/* Charts Row */}
196
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
197
+ {/* Status Distribution */}
198
+ <div className="bg-card rounded-lg border p-4">
199
+ <h2 className="text-sm font-medium mb-4">Status Distribution</h2>
200
+ <StatusDistributionChart data={details.statusDistribution} />
201
+ </div>
202
+
203
+ {/* Latency Percentiles */}
204
+ <div className="bg-card rounded-lg border p-4">
205
+ <h2 className="text-sm font-medium mb-4">Latency Percentiles</h2>
206
+ <LatencyPercentilesChart
207
+ p50={details.p50Duration}
208
+ p95={details.p95Duration}
209
+ p99={details.p99Duration}
210
+ />
211
+ </div>
212
+ </div>
213
+
214
+ {/* Request Volume Time Series */}
215
+ <div className="bg-card rounded-lg border p-4">
216
+ <h2 className="text-sm font-medium mb-4">Request Volume</h2>
217
+ <AreaTimeSeriesChart
218
+ data={details.timeSeries.map((p) => ({
219
+ timestamp: p.timestamp,
220
+ value: p.count,
221
+ secondaryValue: p.errorCount,
222
+ }))}
223
+ primaryLabel="Requests"
224
+ secondaryLabel="Errors"
225
+ primaryColor="blue"
226
+ secondaryColor="red"
227
+ />
228
+ </div>
229
+ </>
230
+ )}
231
+
232
+ {/* Recent Requests */}
233
+ <div className="bg-card rounded-lg border">
234
+ <div className="p-4 border-b">
235
+ <h2 className="text-sm font-medium">Recent Requests</h2>
236
+ </div>
237
+ <div className="overflow-x-auto">
238
+ <table className="w-full text-sm">
239
+ <thead className="bg-muted/50">
240
+ <tr>
241
+ <th className="text-left px-4 py-2 font-medium">Status</th>
242
+ <th className="text-left px-4 py-2 font-medium">Duration</th>
243
+ <th className="text-left px-4 py-2 font-medium">Timestamp</th>
244
+ <th className="text-left px-4 py-2 font-medium">ID</th>
245
+ </tr>
246
+ </thead>
247
+ <tbody>
248
+ {recentRequests.length === 0 ? (
249
+ <tr>
250
+ <td
251
+ colSpan={4}
252
+ className="text-center py-8 text-muted-foreground"
253
+ >
254
+ No recent requests
255
+ </td>
256
+ </tr>
257
+ ) : (
258
+ recentRequests.map((req) => (
259
+ <tr key={req.id} className="border-t hover:bg-muted/30">
260
+ <td className="px-4 py-2">
261
+ <span className={getStatusColor(req.status)}>
262
+ {req.status}
263
+ </span>
264
+ </td>
265
+ <td className="px-4 py-2 font-mono">
266
+ {formatDuration(req.duration)}
267
+ </td>
268
+ <td className="px-4 py-2 text-muted-foreground">
269
+ {formatTimestamp(req.timestamp)}
270
+ </td>
271
+ <td className="px-4 py-2">
272
+ <Link
273
+ to={`/monitoring/requests/${req.id}`}
274
+ className="font-mono text-xs text-primary hover:underline"
275
+ >
276
+ {req.id.slice(0, 8)}...
277
+ </Link>
278
+ </td>
279
+ </tr>
280
+ ))
281
+ )}
282
+ </tbody>
283
+ </table>
284
+ </div>
285
+ </div>
286
+ </div>
287
+ );
288
+ }