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