@geekmidas/studio 9.0.2 → 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 -82
  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,564 +0,0 @@
1
- import { useCallback, useEffect, useState } from 'react';
2
- import * as api from '../api';
3
- import type {
4
- FilterConfig,
5
- QueryResult,
6
- SortConfig,
7
- TableInfo,
8
- } from '../types';
9
- import { FilterPanel } from './FilterPanel';
10
-
11
- export interface ForeignKeyClickInfo {
12
- targetTable: string;
13
- targetColumn: string;
14
- value: unknown;
15
- }
16
-
17
- export interface TableViewProps {
18
- tableName: string;
19
- tableInfo: TableInfo | null;
20
- onRowSelect: (row: Record<string, unknown>) => void;
21
- onForeignKeyClick?: (info: ForeignKeyClickInfo) => void;
22
- initialFilters?: FilterConfig[];
23
- }
24
-
25
- const PAGE_SIZES = [25, 50, 100];
26
-
27
- // Column type icons
28
- function ColumnTypeIcon({ type }: { type: string }) {
29
- switch (type) {
30
- case 'string':
31
- return (
32
- <svg
33
- className="w-3.5 h-3.5"
34
- fill="none"
35
- stroke="currentColor"
36
- viewBox="0 0 24 24"
37
- >
38
- <path
39
- strokeLinecap="round"
40
- strokeLinejoin="round"
41
- strokeWidth={2}
42
- d="M4 6h16M4 12h16M4 18h7"
43
- />
44
- </svg>
45
- );
46
- case 'number':
47
- return (
48
- <svg
49
- className="w-3.5 h-3.5"
50
- fill="none"
51
- stroke="currentColor"
52
- viewBox="0 0 24 24"
53
- >
54
- <path
55
- strokeLinecap="round"
56
- strokeLinejoin="round"
57
- strokeWidth={2}
58
- d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"
59
- />
60
- </svg>
61
- );
62
- case 'boolean':
63
- return (
64
- <svg
65
- className="w-3.5 h-3.5"
66
- fill="none"
67
- stroke="currentColor"
68
- viewBox="0 0 24 24"
69
- >
70
- <path
71
- strokeLinecap="round"
72
- strokeLinejoin="round"
73
- strokeWidth={2}
74
- d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
75
- />
76
- </svg>
77
- );
78
- case 'date':
79
- case 'datetime':
80
- return (
81
- <svg
82
- className="w-3.5 h-3.5"
83
- fill="none"
84
- stroke="currentColor"
85
- viewBox="0 0 24 24"
86
- >
87
- <path
88
- strokeLinecap="round"
89
- strokeLinejoin="round"
90
- strokeWidth={2}
91
- d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
92
- />
93
- </svg>
94
- );
95
- case 'json':
96
- return (
97
- <svg
98
- className="w-3.5 h-3.5"
99
- fill="none"
100
- stroke="currentColor"
101
- viewBox="0 0 24 24"
102
- >
103
- <path
104
- strokeLinecap="round"
105
- strokeLinejoin="round"
106
- strokeWidth={2}
107
- d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
108
- />
109
- </svg>
110
- );
111
- case 'uuid':
112
- return (
113
- <svg
114
- className="w-3.5 h-3.5"
115
- fill="none"
116
- stroke="currentColor"
117
- viewBox="0 0 24 24"
118
- >
119
- <path
120
- strokeLinecap="round"
121
- strokeLinejoin="round"
122
- strokeWidth={2}
123
- d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"
124
- />
125
- </svg>
126
- );
127
- default:
128
- return (
129
- <svg
130
- className="w-3.5 h-3.5"
131
- fill="none"
132
- stroke="currentColor"
133
- viewBox="0 0 24 24"
134
- >
135
- <path
136
- strokeLinecap="round"
137
- strokeLinejoin="round"
138
- strokeWidth={2}
139
- d="M4 6h16M4 12h16m-7 6h7"
140
- />
141
- </svg>
142
- );
143
- }
144
- }
145
-
146
- export function TableView({
147
- tableName,
148
- tableInfo,
149
- onRowSelect,
150
- onForeignKeyClick,
151
- initialFilters,
152
- }: TableViewProps) {
153
- const [data, setData] = useState<QueryResult | null>(null);
154
- const [loading, setLoading] = useState(true);
155
- const [error, setError] = useState<string | null>(null);
156
- const [sort, setSort] = useState<SortConfig[]>([]);
157
- const [filters, setFilters] = useState<FilterConfig[]>(initialFilters ?? []);
158
- const [showFilters, setShowFilters] = useState(false);
159
- const [pageSize, setPageSize] = useState(50);
160
- const [cursors, setCursors] = useState<string[]>([]);
161
- const [currentPage, setCurrentPage] = useState(0);
162
-
163
- const loadData = useCallback(
164
- async (cursor?: string) => {
165
- setLoading(true);
166
- setError(null);
167
- try {
168
- // Filter out filters with empty values (except is_null/is_not_null which don't need values)
169
- const validFilters = filters.filter(
170
- (f) =>
171
- f.operator === 'is_null' ||
172
- f.operator === 'is_not_null' ||
173
- (f.value && f.value.trim() !== ''),
174
- );
175
-
176
- const result = await api.queryTable(tableName, {
177
- pageSize,
178
- cursor,
179
- sort: sort.length > 0 ? sort : undefined,
180
- filters: validFilters.length > 0 ? validFilters : undefined,
181
- });
182
- setData(result);
183
- } catch (err) {
184
- setError(err instanceof Error ? err.message : 'Failed to load data');
185
- } finally {
186
- setLoading(false);
187
- }
188
- },
189
- [tableName, pageSize, sort, filters],
190
- );
191
-
192
- // Reset and reload when table, sort, or filters change
193
- useEffect(() => {
194
- setCursors([]);
195
- setCurrentPage(0);
196
- loadData();
197
- }, [loadData]);
198
-
199
- // Update filters when initialFilters changes (e.g., FK navigation)
200
- useEffect(() => {
201
- if (initialFilters) {
202
- setFilters(initialFilters);
203
- setShowFilters(initialFilters.length > 0);
204
- }
205
- }, [initialFilters]);
206
-
207
- const handleSort = useCallback((column: string) => {
208
- setSort((prev) => {
209
- const existing = prev.find((s) => s.column === column);
210
- if (!existing) {
211
- return [{ column, direction: 'asc' }];
212
- }
213
- if (existing.direction === 'asc') {
214
- return [{ column, direction: 'desc' }];
215
- }
216
- return [];
217
- });
218
- }, []);
219
-
220
- const handleNextPage = useCallback(() => {
221
- if (data?.nextCursor) {
222
- setCursors((prev) => [...prev, data.nextCursor!]);
223
- setCurrentPage((prev) => prev + 1);
224
- loadData(data.nextCursor);
225
- }
226
- }, [data, loadData]);
227
-
228
- const handlePrevPage = useCallback(() => {
229
- if (currentPage > 0) {
230
- const newCursors = cursors.slice(0, -1);
231
- setCursors(newCursors);
232
- setCurrentPage((prev) => prev - 1);
233
- loadData(newCursors[newCursors.length - 1] || undefined);
234
- }
235
- }, [currentPage, cursors, loadData]);
236
-
237
- const formatValue = (value: unknown): string => {
238
- if (value === null) return 'NULL';
239
- if (value === undefined) return '';
240
- if (typeof value === 'boolean') return value ? 'true' : 'false';
241
- if (value instanceof Date) return value.toISOString();
242
- if (typeof value === 'object') return JSON.stringify(value);
243
- return String(value);
244
- };
245
-
246
- if (!tableInfo) {
247
- return (
248
- <div className="flex-1 flex items-center justify-center text-slate-500">
249
- <div className="flex items-center gap-2">
250
- <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
251
- <circle
252
- className="opacity-25"
253
- cx="12"
254
- cy="12"
255
- r="10"
256
- stroke="currentColor"
257
- strokeWidth="4"
258
- />
259
- <path
260
- className="opacity-75"
261
- fill="currentColor"
262
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
263
- />
264
- </svg>
265
- Loading table info...
266
- </div>
267
- </div>
268
- );
269
- }
270
-
271
- const columns = tableInfo.columns;
272
- const startRow = currentPage * pageSize + 1;
273
- const endRow = startRow + (data?.rows.length || 0) - 1;
274
-
275
- return (
276
- <div className="flex-1 flex flex-col overflow-hidden bg-studio-bg">
277
- {/* Toolbar */}
278
- <div className="bg-studio-surface border-b border-studio-border px-4 py-2 flex items-center justify-between shrink-0">
279
- <div className="flex items-center gap-2">
280
- {/* Filter button */}
281
- <button
282
- onClick={() => setShowFilters(!showFilters)}
283
- className={`btn btn-default ${showFilters || filters.length > 0 ? 'text-emerald-400' : ''}`}
284
- >
285
- <svg
286
- className="w-4 h-4"
287
- fill="none"
288
- stroke="currentColor"
289
- viewBox="0 0 24 24"
290
- >
291
- <path
292
- strokeLinecap="round"
293
- strokeLinejoin="round"
294
- strokeWidth={2}
295
- d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
296
- />
297
- </svg>
298
- Filter
299
- {filters.length > 0 && (
300
- <span className="badge">{filters.length}</span>
301
- )}
302
- </button>
303
-
304
- {/* Sort indicator */}
305
- {sort.length > 0 && (
306
- <div className="flex items-center gap-1 text-sm text-slate-400">
307
- <span>Sorted by</span>
308
- <span className="text-emerald-400">{sort[0].column}</span>
309
- <span>({sort[0].direction})</span>
310
- <button
311
- onClick={() => setSort([])}
312
- className="p-0.5 hover:bg-studio-hover rounded"
313
- >
314
- <svg
315
- className="w-3.5 h-3.5"
316
- fill="none"
317
- stroke="currentColor"
318
- viewBox="0 0 24 24"
319
- >
320
- <path
321
- strokeLinecap="round"
322
- strokeLinejoin="round"
323
- strokeWidth={2}
324
- d="M6 18L18 6M6 6l12 12"
325
- />
326
- </svg>
327
- </button>
328
- </div>
329
- )}
330
- </div>
331
-
332
- <div className="flex items-center gap-3 text-sm text-slate-400">
333
- {tableInfo.estimatedRowCount !== undefined && (
334
- <span>{tableInfo.estimatedRowCount.toLocaleString()} rows</span>
335
- )}
336
- </div>
337
- </div>
338
-
339
- {/* Filter Panel */}
340
- {showFilters && (
341
- <FilterPanel
342
- columns={columns}
343
- filters={filters}
344
- onFiltersChange={setFilters}
345
- onClose={() => setShowFilters(false)}
346
- />
347
- )}
348
-
349
- {/* Error state */}
350
- {error && (
351
- <div className="p-4 bg-red-500/10 border-b border-red-500/20 text-red-400 text-sm">
352
- {error}
353
- </div>
354
- )}
355
-
356
- {/* Data Grid */}
357
- <div className="flex-1 overflow-auto">
358
- <table className="data-grid">
359
- <thead>
360
- <tr>
361
- {/* Row number header */}
362
- <th className="w-12 px-3 py-2 text-xs font-normal text-slate-500 bg-studio-surface">
363
- #
364
- </th>
365
- {columns.map((col) => (
366
- <th
367
- key={col.name}
368
- onClick={() => handleSort(col.name)}
369
- className="px-3 py-2 text-left cursor-pointer hover:bg-studio-hover transition-colors bg-studio-surface"
370
- >
371
- <div className="flex items-center gap-2">
372
- <span className="text-slate-500">
373
- <ColumnTypeIcon type={col.type} />
374
- </span>
375
- <span className="text-sm font-medium text-slate-300 truncate">
376
- {col.name}
377
- </span>
378
- {col.isPrimaryKey && (
379
- <span className="text-[10px] px-1 py-0.5 rounded bg-amber-500/20 text-amber-400">
380
- PK
381
- </span>
382
- )}
383
- {col.isForeignKey && (
384
- <span className="text-[10px] px-1 py-0.5 rounded bg-blue-500/20 text-blue-400">
385
- FK
386
- </span>
387
- )}
388
- {sort.find((s) => s.column === col.name) && (
389
- <span className="text-emerald-400">
390
- {sort[0].direction === 'asc' ? '↑' : '↓'}
391
- </span>
392
- )}
393
- </div>
394
- </th>
395
- ))}
396
- </tr>
397
- </thead>
398
- <tbody>
399
- {loading && !data ? (
400
- <tr>
401
- <td
402
- colSpan={columns.length + 1}
403
- className="text-center py-8 text-slate-500"
404
- >
405
- <div className="flex items-center justify-center gap-2">
406
- <svg
407
- className="w-5 h-5 animate-spin"
408
- fill="none"
409
- viewBox="0 0 24 24"
410
- >
411
- <circle
412
- className="opacity-25"
413
- cx="12"
414
- cy="12"
415
- r="10"
416
- stroke="currentColor"
417
- strokeWidth="4"
418
- />
419
- <path
420
- className="opacity-75"
421
- fill="currentColor"
422
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
423
- />
424
- </svg>
425
- Loading...
426
- </div>
427
- </td>
428
- </tr>
429
- ) : data?.rows.length === 0 ? (
430
- <tr>
431
- <td
432
- colSpan={columns.length + 1}
433
- className="text-center py-8 text-slate-500"
434
- >
435
- No data found
436
- </td>
437
- </tr>
438
- ) : (
439
- data?.rows.map((row, idx) => (
440
- <tr
441
- key={idx}
442
- onClick={() => onRowSelect(row)}
443
- className="cursor-pointer"
444
- >
445
- {/* Row number */}
446
- <td className="row-number px-3 py-2 text-right">
447
- {startRow + idx}
448
- </td>
449
- {columns.map((col) => {
450
- const value = row[col.name];
451
- const isFkClickable =
452
- col.isForeignKey &&
453
- col.foreignKeyTable &&
454
- col.foreignKeyColumn &&
455
- value !== null &&
456
- onForeignKeyClick;
457
-
458
- return (
459
- <td
460
- key={col.name}
461
- className={`px-3 py-2 text-sm max-w-xs truncate ${
462
- value === null ? 'cell-null' : 'text-slate-300'
463
- }`}
464
- >
465
- {isFkClickable ? (
466
- <button
467
- type="button"
468
- onClick={(e) => {
469
- e.stopPropagation();
470
- onForeignKeyClick({
471
- targetTable: col.foreignKeyTable!,
472
- targetColumn: col.foreignKeyColumn!,
473
- value,
474
- });
475
- }}
476
- className="text-blue-400 hover:text-blue-300 hover:underline transition-colors"
477
- >
478
- {formatValue(value)}
479
- </button>
480
- ) : (
481
- formatValue(value)
482
- )}
483
- </td>
484
- );
485
- })}
486
- </tr>
487
- ))
488
- )}
489
- </tbody>
490
- </table>
491
- </div>
492
-
493
- {/* Pagination */}
494
- <div className="bg-studio-surface border-t border-studio-border px-4 py-2 flex items-center justify-between shrink-0">
495
- <div className="flex items-center gap-4 text-sm text-slate-400">
496
- <div className="flex items-center gap-2">
497
- <span>Rows per page:</span>
498
- <select
499
- value={pageSize}
500
- onChange={(e) => setPageSize(Number(e.target.value))}
501
- className="select py-1"
502
- >
503
- {PAGE_SIZES.map((size) => (
504
- <option key={size} value={size}>
505
- {size}
506
- </option>
507
- ))}
508
- </select>
509
- </div>
510
- {data && data.rows.length > 0 && (
511
- <span>
512
- Showing {startRow} - {endRow}
513
- {tableInfo.estimatedRowCount !== undefined &&
514
- ` of ${tableInfo.estimatedRowCount.toLocaleString()}`}
515
- </span>
516
- )}
517
- </div>
518
-
519
- <div className="flex items-center gap-2">
520
- <button
521
- onClick={handlePrevPage}
522
- disabled={currentPage === 0 || loading}
523
- className="btn btn-default disabled:opacity-50 disabled:cursor-not-allowed"
524
- >
525
- <svg
526
- className="w-4 h-4"
527
- fill="none"
528
- stroke="currentColor"
529
- viewBox="0 0 24 24"
530
- >
531
- <path
532
- strokeLinecap="round"
533
- strokeLinejoin="round"
534
- strokeWidth={2}
535
- d="M15 19l-7-7 7-7"
536
- />
537
- </svg>
538
- Previous
539
- </button>
540
- <button
541
- onClick={handleNextPage}
542
- disabled={!data?.hasMore || loading}
543
- className="btn btn-default disabled:opacity-50 disabled:cursor-not-allowed"
544
- >
545
- Next
546
- <svg
547
- className="w-4 h-4"
548
- fill="none"
549
- stroke="currentColor"
550
- viewBox="0 0 24 24"
551
- >
552
- <path
553
- strokeLinecap="round"
554
- strokeLinejoin="round"
555
- strokeWidth={2}
556
- d="M9 5l7 7-7 7"
557
- />
558
- </svg>
559
- </button>
560
- </div>
561
- </div>
562
- </div>
563
- );
564
- }
package/ui/src/main.tsx DELETED
@@ -1,10 +0,0 @@
1
- import { StrictMode } from 'react';
2
- import { createRoot } from 'react-dom/client';
3
- import { App } from './App';
4
- import './styles.css';
5
-
6
- createRoot(document.getElementById('root')!).render(
7
- <StrictMode>
8
- <App />
9
- </StrictMode>,
10
- );