@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.
- package/package.json +8 -5
- package/CHANGELOG.md +0 -60
- package/src/Studio.ts +0 -367
- package/src/__tests__/Studio.spec.ts +0 -447
- package/src/data/DataBrowser.ts +0 -170
- package/src/data/__tests__/DataBrowser.integration.spec.ts +0 -418
- package/src/data/__tests__/filtering.integration.spec.ts +0 -741
- package/src/data/__tests__/introspection.integration.spec.ts +0 -352
- package/src/data/__tests__/pagination.spec.ts +0 -123
- package/src/data/filtering.ts +0 -191
- package/src/data/index.ts +0 -1
- package/src/data/introspection.ts +0 -220
- package/src/data/pagination.ts +0 -33
- package/src/index.ts +0 -29
- package/src/server/__tests__/hono.integration.spec.ts +0 -619
- package/src/server/hono.ts +0 -427
- package/src/types.ts +0 -278
- package/src/ui-assets.ts +0 -37
- package/tsconfig.json +0 -9
- package/tsdown.config.ts +0 -13
- package/ui/CHANGELOG.md +0 -26
- package/ui/index.html +0 -12
- package/ui/node_modules/.bin/tsc +0 -21
- package/ui/node_modules/.bin/tsserver +0 -21
- package/ui/node_modules/.bin/vite +0 -21
- package/ui/package.json +0 -30
- package/ui/src/App.tsx +0 -100
- package/ui/src/api.ts +0 -213
- package/ui/src/components/FilterPanel.tsx +0 -213
- package/ui/src/components/NavRail.tsx +0 -183
- package/ui/src/components/RowDetail.tsx +0 -119
- package/ui/src/components/StudioHeader.tsx +0 -109
- package/ui/src/components/TableList.tsx +0 -58
- package/ui/src/components/TableView.tsx +0 -564
- package/ui/src/main.tsx +0 -10
- package/ui/src/pages/DashboardPage.tsx +0 -500
- package/ui/src/pages/DatabasePage.tsx +0 -226
- package/ui/src/pages/EndpointDetailsPage.tsx +0 -288
- package/ui/src/pages/ExceptionsPage.tsx +0 -268
- package/ui/src/pages/LogsPage.tsx +0 -228
- package/ui/src/pages/MonitoringPage.tsx +0 -46
- package/ui/src/pages/PerformancePage.tsx +0 -307
- package/ui/src/pages/RequestsPage.tsx +0 -379
- package/ui/src/providers/StudioProvider.tsx +0 -194
- package/ui/src/styles.css +0 -105
- package/ui/src/types.ts +0 -174
- package/ui/src/vite-env.d.ts +0 -1
- package/ui/tsconfig.json +0 -21
- package/ui/tsconfig.tsbuildinfo +0 -1
- 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