@geekmidas/studio 2.0.1 → 10.0.0-alpha.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 (50) hide show
  1. package/package.json +8 -5
  2. package/CHANGELOG.md +0 -60
  3. package/src/Studio.ts +0 -367
  4. package/src/__tests__/Studio.spec.ts +0 -447
  5. package/src/data/DataBrowser.ts +0 -170
  6. package/src/data/__tests__/DataBrowser.integration.spec.ts +0 -418
  7. package/src/data/__tests__/filtering.integration.spec.ts +0 -741
  8. package/src/data/__tests__/introspection.integration.spec.ts +0 -352
  9. package/src/data/__tests__/pagination.spec.ts +0 -123
  10. package/src/data/filtering.ts +0 -191
  11. package/src/data/index.ts +0 -1
  12. package/src/data/introspection.ts +0 -220
  13. package/src/data/pagination.ts +0 -33
  14. package/src/index.ts +0 -29
  15. package/src/server/__tests__/hono.integration.spec.ts +0 -619
  16. package/src/server/hono.ts +0 -427
  17. package/src/types.ts +0 -278
  18. package/src/ui-assets.ts +0 -37
  19. package/tsconfig.json +0 -9
  20. package/tsdown.config.ts +0 -13
  21. package/ui/CHANGELOG.md +0 -26
  22. package/ui/index.html +0 -12
  23. package/ui/node_modules/.bin/tsc +0 -21
  24. package/ui/node_modules/.bin/tsserver +0 -21
  25. package/ui/node_modules/.bin/vite +0 -21
  26. package/ui/package.json +0 -30
  27. package/ui/src/App.tsx +0 -100
  28. package/ui/src/api.ts +0 -213
  29. package/ui/src/components/FilterPanel.tsx +0 -213
  30. package/ui/src/components/NavRail.tsx +0 -183
  31. package/ui/src/components/RowDetail.tsx +0 -119
  32. package/ui/src/components/StudioHeader.tsx +0 -109
  33. package/ui/src/components/TableList.tsx +0 -58
  34. package/ui/src/components/TableView.tsx +0 -564
  35. package/ui/src/main.tsx +0 -10
  36. package/ui/src/pages/DashboardPage.tsx +0 -500
  37. package/ui/src/pages/DatabasePage.tsx +0 -226
  38. package/ui/src/pages/EndpointDetailsPage.tsx +0 -288
  39. package/ui/src/pages/ExceptionsPage.tsx +0 -268
  40. package/ui/src/pages/LogsPage.tsx +0 -228
  41. package/ui/src/pages/MonitoringPage.tsx +0 -46
  42. package/ui/src/pages/PerformancePage.tsx +0 -307
  43. package/ui/src/pages/RequestsPage.tsx +0 -379
  44. package/ui/src/providers/StudioProvider.tsx +0 -194
  45. package/ui/src/styles.css +0 -105
  46. package/ui/src/types.ts +0 -174
  47. package/ui/src/vite-env.d.ts +0 -1
  48. package/ui/tsconfig.json +0 -21
  49. package/ui/tsconfig.tsbuildinfo +0 -1
  50. package/ui/vite.config.ts +0 -12
@@ -1,226 +0,0 @@
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
- }
@@ -1,288 +0,0 @@
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
- }