@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.
@@ -1,6 +1,12 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import * as api from '../api';
3
- import type { QueryResult, SortConfig, TableInfo } from '../types';
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 [pageSize] = useState(50);
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
- }, [loadData]);
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
- Loading table info...
89
- </div>
90
- );
91
- }
92
-
93
- if (error) {
94
- return (
95
- <div className="flex-1 flex items-center justify-center text-red-400">
96
- {error}
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
- {/* Column Info Bar */}
106
- <div className="bg-bg-secondary border-b border-border px-4 py-2 flex items-center gap-4 text-sm">
107
- <span className="text-slate-400">{columns.length} columns</span>
108
- {tableInfo.estimatedRowCount !== undefined && (
109
- <span className="text-slate-400">
110
- ~{tableInfo.estimatedRowCount.toLocaleString()} rows
111
- </span>
112
- )}
113
- {sort.length > 0 && (
114
- <span className="text-purple-400">
115
- Sorted by: {sort[0].column} ({sort[0].direction})
116
- </span>
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
- {/* Table */}
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="w-full border-collapse">
123
- <thead className="sticky top-0 bg-bg-secondary z-10">
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={`text-left px-3 py-2 text-sm font-medium text-slate-300 border-b border-border cursor-pointer hover:bg-bg-tertiary transition-colors ${getColumnWidth(col.name)}`}
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="truncate">{col.name}</span>
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-xs text-amber-400">PK</span>
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-xs text-blue-400">FK</span>
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-purple-400">
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
- Loading...
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="border-b border-border hover:bg-bg-tertiary cursor-pointer transition-colors"
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 truncate max-w-xs ${
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
- {data && (
200
- <div className="bg-bg-secondary border-t border-border px-4 py-2 flex items-center justify-between text-sm">
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
- {data.hasMore && (
206
- <button
207
- onClick={handleNextPage}
208
- disabled={loading}
209
- className="px-3 py-1 bg-bg-tertiary hover:bg-slate-600 rounded transition-colors disabled:opacity-50"
210
- >
211
- {loading ? 'Loading...' : 'Load More'}
212
- </button>
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
  }