@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,213 +0,0 @@
1
- import { useCallback } from 'react';
2
- import type { ColumnInfo, FilterConfig } from '../types';
3
-
4
- interface FilterPanelProps {
5
- columns: ColumnInfo[];
6
- filters: FilterConfig[];
7
- onFiltersChange: (filters: FilterConfig[]) => void;
8
- onClose: () => void;
9
- }
10
-
11
- const OPERATORS: { value: string; label: string; types?: string[] }[] = [
12
- { value: 'eq', label: 'equals' },
13
- { value: 'neq', label: 'not equals' },
14
- { value: 'gt', label: 'greater than', types: ['number', 'date', 'datetime'] },
15
- {
16
- value: 'gte',
17
- label: 'greater than or equal',
18
- types: ['number', 'date', 'datetime'],
19
- },
20
- { value: 'lt', label: 'less than', types: ['number', 'date', 'datetime'] },
21
- {
22
- value: 'lte',
23
- label: 'less than or equal',
24
- types: ['number', 'date', 'datetime'],
25
- },
26
- { value: 'like', label: 'contains', types: ['string'] },
27
- { value: 'ilike', label: 'contains (case insensitive)', types: ['string'] },
28
- { value: 'is_null', label: 'is null' },
29
- { value: 'is_not_null', label: 'is not null' },
30
- ];
31
-
32
- function getOperatorsForColumn(column: ColumnInfo) {
33
- return OPERATORS.filter(
34
- (op) => !op.types || op.types.includes(column.type || 'string'),
35
- );
36
- }
37
-
38
- export function FilterPanel({
39
- columns,
40
- filters,
41
- onFiltersChange,
42
- onClose,
43
- }: FilterPanelProps) {
44
- const addFilter = useCallback(() => {
45
- const firstColumn = columns[0];
46
- if (!firstColumn) return;
47
-
48
- onFiltersChange([
49
- ...filters,
50
- { column: firstColumn.name, operator: 'eq', value: '' },
51
- ]);
52
- }, [columns, filters, onFiltersChange]);
53
-
54
- const updateFilter = useCallback(
55
- (index: number, updates: Partial<FilterConfig>) => {
56
- const newFilters = [...filters];
57
- newFilters[index] = { ...newFilters[index], ...updates };
58
- onFiltersChange(newFilters);
59
- },
60
- [filters, onFiltersChange],
61
- );
62
-
63
- const removeFilter = useCallback(
64
- (index: number) => {
65
- onFiltersChange(filters.filter((_, i) => i !== index));
66
- },
67
- [filters, onFiltersChange],
68
- );
69
-
70
- const clearAll = useCallback(() => {
71
- onFiltersChange([]);
72
- }, [onFiltersChange]);
73
-
74
- return (
75
- <div className="filter-panel bg-studio-surface border-b border-studio-border p-4">
76
- <div className="flex items-center justify-between mb-3">
77
- <h3 className="text-sm font-medium text-slate-300">Filters</h3>
78
- <div className="flex items-center gap-2">
79
- {filters.length > 0 && (
80
- <button
81
- onClick={clearAll}
82
- className="text-xs text-slate-500 hover:text-slate-300 transition-colors"
83
- >
84
- Clear all
85
- </button>
86
- )}
87
- <button
88
- onClick={onClose}
89
- className="p-1 hover:bg-studio-hover rounded transition-colors"
90
- >
91
- <svg
92
- className="w-4 h-4 text-slate-400"
93
- fill="none"
94
- stroke="currentColor"
95
- viewBox="0 0 24 24"
96
- >
97
- <path
98
- strokeLinecap="round"
99
- strokeLinejoin="round"
100
- strokeWidth={2}
101
- d="M6 18L18 6M6 6l12 12"
102
- />
103
- </svg>
104
- </button>
105
- </div>
106
- </div>
107
-
108
- <div className="space-y-2">
109
- {filters.map((filter, index) => {
110
- const column = columns.find((c) => c.name === filter.column);
111
- const operators = column ? getOperatorsForColumn(column) : OPERATORS;
112
- const needsValue = !['is_null', 'is_not_null'].includes(
113
- filter.operator,
114
- );
115
-
116
- return (
117
- <div key={index} className="flex items-center gap-2">
118
- {index > 0 && (
119
- <span className="text-xs text-slate-500 w-8">and</span>
120
- )}
121
- {index === 0 && (
122
- <span className="text-xs text-slate-500 w-8">Where</span>
123
- )}
124
-
125
- {/* Column select */}
126
- <select
127
- value={filter.column}
128
- onChange={(e) =>
129
- updateFilter(index, { column: e.target.value })
130
- }
131
- className="select flex-1 min-w-0"
132
- >
133
- {columns.map((col) => (
134
- <option key={col.name} value={col.name}>
135
- {col.name}
136
- </option>
137
- ))}
138
- </select>
139
-
140
- {/* Operator select */}
141
- <select
142
- value={filter.operator}
143
- onChange={(e) =>
144
- updateFilter(index, { operator: e.target.value })
145
- }
146
- className="select w-48"
147
- >
148
- {operators.map((op) => (
149
- <option key={op.value} value={op.value}>
150
- {op.label}
151
- </option>
152
- ))}
153
- </select>
154
-
155
- {/* Value input */}
156
- {needsValue && (
157
- <input
158
- type="text"
159
- value={filter.value}
160
- onChange={(e) =>
161
- updateFilter(index, { value: e.target.value })
162
- }
163
- placeholder="Enter value..."
164
- className="input flex-1 min-w-0"
165
- />
166
- )}
167
-
168
- {/* Remove button */}
169
- <button
170
- onClick={() => removeFilter(index)}
171
- className="p-1.5 hover:bg-studio-hover rounded transition-colors text-slate-500 hover:text-slate-300"
172
- >
173
- <svg
174
- className="w-4 h-4"
175
- fill="none"
176
- stroke="currentColor"
177
- viewBox="0 0 24 24"
178
- >
179
- <path
180
- strokeLinecap="round"
181
- strokeLinejoin="round"
182
- strokeWidth={2}
183
- d="M6 18L18 6M6 6l12 12"
184
- />
185
- </svg>
186
- </button>
187
- </div>
188
- );
189
- })}
190
- </div>
191
-
192
- <button
193
- onClick={addFilter}
194
- className="mt-3 flex items-center gap-2 text-sm text-emerald-400 hover:text-emerald-300 transition-colors"
195
- >
196
- <svg
197
- className="w-4 h-4"
198
- fill="none"
199
- stroke="currentColor"
200
- viewBox="0 0 24 24"
201
- >
202
- <path
203
- strokeLinecap="round"
204
- strokeLinejoin="round"
205
- strokeWidth={2}
206
- d="M12 4v16m8-8H4"
207
- />
208
- </svg>
209
- Add filter
210
- </button>
211
- </div>
212
- );
213
- }
@@ -1,183 +0,0 @@
1
- import {
2
- Tooltip,
3
- TooltipContent,
4
- TooltipProvider,
5
- TooltipTrigger,
6
- } from '@geekmidas/ui';
7
- import { ChevronLeft, ChevronRight, type LucideIcon } from 'lucide-react';
8
- import { createContext, useContext, useEffect, useState } from 'react';
9
- import { Link, useLocation } from 'react-router-dom';
10
-
11
- const STORAGE_KEY = 'studio-nav-collapsed';
12
-
13
- interface NavRailContextValue {
14
- collapsed: boolean;
15
- setCollapsed: (collapsed: boolean) => void;
16
- }
17
-
18
- const NavRailContext = createContext<NavRailContextValue>({
19
- collapsed: false,
20
- setCollapsed: () => {},
21
- });
22
-
23
- export function useNavRail() {
24
- return useContext(NavRailContext);
25
- }
26
-
27
- interface NavRailProps {
28
- children: React.ReactNode;
29
- header?: React.ReactNode;
30
- footer?: React.ReactNode;
31
- }
32
-
33
- export function NavRail({ children, header, footer }: NavRailProps) {
34
- const [collapsed, setCollapsed] = useState(() => {
35
- if (typeof window !== 'undefined') {
36
- const stored = localStorage.getItem(STORAGE_KEY);
37
- return stored === 'true';
38
- }
39
- return false;
40
- });
41
-
42
- useEffect(() => {
43
- localStorage.setItem(STORAGE_KEY, String(collapsed));
44
- }, [collapsed]);
45
-
46
- return (
47
- <NavRailContext.Provider value={{ collapsed, setCollapsed }}>
48
- <TooltipProvider delayDuration={0}>
49
- <nav
50
- className={`
51
- flex flex-col h-full
52
- bg-[#0d0d0d] border-r border-white/[0.06]
53
- transition-all duration-200 ease-out
54
- ${collapsed ? 'w-[52px]' : 'w-[180px]'}
55
- `}
56
- >
57
- {/* Toggle button */}
58
- <div className="flex items-center justify-end h-12 px-2">
59
- <button
60
- onClick={() => setCollapsed(!collapsed)}
61
- className="
62
- p-1.5 rounded-md
63
- text-white/40 hover:text-white/70
64
- hover:bg-white/[0.06]
65
- transition-colors
66
- "
67
- aria-label={
68
- collapsed ? 'Expand navigation' : 'Collapse navigation'
69
- }
70
- >
71
- {collapsed ? (
72
- <ChevronRight className="h-4 w-4" />
73
- ) : (
74
- <ChevronLeft className="h-4 w-4" />
75
- )}
76
- </button>
77
- </div>
78
-
79
- {header && <div className="px-2 mb-2">{header}</div>}
80
-
81
- {/* Navigation items */}
82
- <div className="flex-1 flex flex-col gap-0.5 px-2">{children}</div>
83
-
84
- {footer && (
85
- <div className="px-2 py-2 border-t border-white/[0.06]">
86
- {footer}
87
- </div>
88
- )}
89
- </nav>
90
- </TooltipProvider>
91
- </NavRailContext.Provider>
92
- );
93
- }
94
-
95
- interface NavRailItemProps {
96
- to: string;
97
- icon: LucideIcon;
98
- children: React.ReactNode;
99
- matchPath?: string;
100
- }
101
-
102
- export function NavRailItem({
103
- to,
104
- icon: Icon,
105
- children,
106
- matchPath,
107
- }: NavRailItemProps) {
108
- const { collapsed } = useNavRail();
109
- const location = useLocation();
110
-
111
- // Special handling for root path
112
- const isActive = matchPath
113
- ? location.pathname.startsWith(matchPath)
114
- : to === '/'
115
- ? location.pathname === '/'
116
- : location.pathname === to || location.pathname.startsWith(`${to}/`);
117
-
118
- const content = (
119
- <Link
120
- to={to}
121
- className={`
122
- relative flex items-center gap-3
123
- px-3 py-2 rounded-md
124
- text-sm font-medium
125
- transition-colors duration-150
126
- ${collapsed ? 'justify-center px-0' : ''}
127
- ${
128
- isActive
129
- ? 'text-white bg-white/[0.08]'
130
- : 'text-white/60 hover:text-white/90 hover:bg-white/[0.04]'
131
- }
132
- `}
133
- >
134
- {/* Active indicator bar */}
135
- {isActive && (
136
- <span className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-4 bg-emerald-500 rounded-full" />
137
- )}
138
-
139
- <Icon
140
- className={`h-[18px] w-[18px] shrink-0 ${collapsed ? '' : 'ml-0.5'}`}
141
- />
142
-
143
- {!collapsed && <span className="truncate">{children}</span>}
144
- </Link>
145
- );
146
-
147
- if (collapsed) {
148
- return (
149
- <Tooltip>
150
- <TooltipTrigger asChild>{content}</TooltipTrigger>
151
- <TooltipContent
152
- side="right"
153
- sideOffset={8}
154
- className="bg-[#1a1a1a] text-white border-white/10"
155
- >
156
- {children}
157
- </TooltipContent>
158
- </Tooltip>
159
- );
160
- }
161
-
162
- return content;
163
- }
164
-
165
- interface NavRailSectionProps {
166
- title?: string;
167
- children: React.ReactNode;
168
- }
169
-
170
- export function NavRailSection({ title, children }: NavRailSectionProps) {
171
- const { collapsed } = useNavRail();
172
-
173
- return (
174
- <div className="flex flex-col gap-0.5">
175
- {title && !collapsed && (
176
- <div className="px-3 py-2 text-[11px] font-medium text-white/30 uppercase tracking-wider">
177
- {title}
178
- </div>
179
- )}
180
- {children}
181
- </div>
182
- );
183
- }
@@ -1,119 +0,0 @@
1
- import type { ColumnInfo } from '../types';
2
-
3
- interface RowDetailProps {
4
- row: Record<string, unknown>;
5
- columns: ColumnInfo[];
6
- onClose: () => void;
7
- }
8
-
9
- export function RowDetail({ row, columns, onClose }: RowDetailProps) {
10
- const formatValue = (value: unknown): string => {
11
- if (value === null) return 'NULL';
12
- if (value === undefined) return '';
13
- if (typeof value === 'boolean') return value ? 'true' : 'false';
14
- if (typeof value === 'object') return JSON.stringify(value, null, 2);
15
- return String(value);
16
- };
17
-
18
- const getValueClass = (value: unknown): string => {
19
- if (value === null) return 'text-slate-500 italic';
20
- if (typeof value === 'boolean')
21
- return value ? 'text-emerald-400' : 'text-red-400';
22
- if (typeof value === 'number') return 'text-blue-400';
23
- return 'text-slate-300';
24
- };
25
-
26
- return (
27
- <aside className="w-96 bg-studio-surface border-l border-studio-border flex flex-col shrink-0 overflow-hidden">
28
- {/* Header */}
29
- <div className="flex items-center justify-between px-4 py-3 border-b border-studio-border shrink-0">
30
- <h2 className="text-sm font-medium text-slate-200">Row Details</h2>
31
- <button
32
- onClick={onClose}
33
- className="p-1 hover:bg-studio-hover rounded transition-colors"
34
- >
35
- <svg
36
- className="w-4 h-4 text-slate-400"
37
- fill="none"
38
- stroke="currentColor"
39
- viewBox="0 0 24 24"
40
- >
41
- <path
42
- strokeLinecap="round"
43
- strokeLinejoin="round"
44
- strokeWidth={2}
45
- d="M6 18L18 6M6 6l12 12"
46
- />
47
- </svg>
48
- </button>
49
- </div>
50
-
51
- {/* Content */}
52
- <div className="flex-1 overflow-y-auto">
53
- {columns.map((col) => {
54
- const value = row[col.name];
55
- const isLongValue = typeof value === 'string' && value.length > 50;
56
- const isJson = typeof value === 'object' && value !== null;
57
-
58
- return (
59
- <div
60
- key={col.name}
61
- className="px-4 py-3 border-b border-studio-border"
62
- >
63
- {/* Column name and metadata */}
64
- <div className="flex items-center gap-2 mb-1.5">
65
- <span className="text-sm font-medium text-slate-300">
66
- {col.name}
67
- </span>
68
- <span className="text-xs text-slate-600">{col.rawType}</span>
69
- {col.isPrimaryKey && (
70
- <span className="text-[10px] px-1 py-0.5 rounded bg-amber-500/20 text-amber-400">
71
- PK
72
- </span>
73
- )}
74
- {col.isForeignKey && (
75
- <span className="text-[10px] px-1 py-0.5 rounded bg-blue-500/20 text-blue-400">
76
- FK
77
- </span>
78
- )}
79
- </div>
80
-
81
- {/* Value */}
82
- {isLongValue || isJson ? (
83
- <pre
84
- className={`${getValueClass(value)} bg-studio-bg text-xs p-2 rounded overflow-x-auto whitespace-pre-wrap break-words max-h-48`}
85
- >
86
- {formatValue(value)}
87
- </pre>
88
- ) : (
89
- <div className={`text-sm ${getValueClass(value)}`}>
90
- {formatValue(value)}
91
- </div>
92
- )}
93
-
94
- {/* Foreign key reference */}
95
- {col.isForeignKey && col.foreignKeyTable && (
96
- <div className="mt-1.5 text-xs text-blue-400 flex items-center gap-1">
97
- <svg
98
- className="w-3 h-3"
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="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
108
- />
109
- </svg>
110
- {col.foreignKeyTable}.{col.foreignKeyColumn}
111
- </div>
112
- )}
113
- </div>
114
- );
115
- })}
116
- </div>
117
- </aside>
118
- );
119
- }
@@ -1,109 +0,0 @@
1
- import { RefreshCw } from 'lucide-react';
2
- import { useLocation } from 'react-router-dom';
3
- import { useStudio } from '../providers/StudioProvider';
4
-
5
- const PAGE_TITLES: Record<string, string> = {
6
- '/': 'Dashboard',
7
- '/database': 'Database',
8
- '/monitoring': 'Monitoring',
9
- '/monitoring/requests': 'Requests',
10
- '/monitoring/logs': 'Logs',
11
- '/monitoring/exceptions': 'Exceptions',
12
- '/performance': 'Performance',
13
- };
14
-
15
- function getPageTitle(pathname: string): string {
16
- // Exact match first
17
- if (PAGE_TITLES[pathname]) {
18
- return PAGE_TITLES[pathname];
19
- }
20
-
21
- // Check for partial matches (e.g., /database/users -> Database)
22
- for (const [path, title] of Object.entries(PAGE_TITLES)) {
23
- if (pathname.startsWith(path) && path !== '/') {
24
- return title;
25
- }
26
- }
27
-
28
- return 'Studio';
29
- }
30
-
31
- export function StudioHeader() {
32
- const { connected, stats, loading, refresh } = useStudio();
33
- const location = useLocation();
34
- const pageTitle = getPageTitle(location.pathname);
35
-
36
- return (
37
- <header className="flex items-center justify-between h-14 px-6 border-b border-white/[0.06] bg-[#0a0a0a]">
38
- {/* Page Title */}
39
- <div className="flex items-center gap-3">
40
- <h1 className="text-[15px] font-semibold text-white">{pageTitle}</h1>
41
- </div>
42
-
43
- {/* Right side: Stats + Actions */}
44
- <div className="flex items-center gap-6">
45
- {/* Stats */}
46
- {stats && (
47
- <div className="flex items-center gap-5 text-[13px]">
48
- <div className="flex items-center gap-1.5">
49
- <span className="text-white/40">Requests</span>
50
- <span className="font-medium text-white tabular-nums">
51
- {stats.requests.toLocaleString()}
52
- </span>
53
- </div>
54
- <div className="flex items-center gap-1.5">
55
- <span className="text-white/40">Exceptions</span>
56
- <span
57
- className={`font-medium tabular-nums ${stats.exceptions > 0 ? 'text-red-400' : 'text-white'}`}
58
- >
59
- {stats.exceptions.toLocaleString()}
60
- </span>
61
- </div>
62
- <div className="flex items-center gap-1.5">
63
- <span className="text-white/40">Logs</span>
64
- <span className="font-medium text-white tabular-nums">
65
- {stats.logs.toLocaleString()}
66
- </span>
67
- </div>
68
- </div>
69
- )}
70
-
71
- {/* Divider */}
72
- <div className="h-4 w-px bg-white/[0.08]" />
73
-
74
- {/* Refresh Button */}
75
- <button
76
- onClick={refresh}
77
- disabled={loading}
78
- className="
79
- flex items-center gap-2 px-3 py-1.5
80
- text-[13px] font-medium
81
- text-white/70 hover:text-white
82
- bg-white/[0.04] hover:bg-white/[0.08]
83
- border border-white/[0.08] hover:border-white/[0.12]
84
- rounded-md transition-all
85
- disabled:opacity-50 disabled:cursor-not-allowed
86
- "
87
- >
88
- <RefreshCw
89
- className={`h-3.5 w-3.5 ${loading ? 'animate-spin' : ''}`}
90
- />
91
- Refresh
92
- </button>
93
-
94
- {/* Connection Status */}
95
- <div className="flex items-center gap-2">
96
- <span
97
- className={`
98
- h-2 w-2 rounded-full
99
- ${connected ? 'bg-emerald-500' : 'bg-red-500'}
100
- `}
101
- />
102
- <span className="text-[13px] text-white/50">
103
- {connected ? 'Live' : 'Disconnected'}
104
- </span>
105
- </div>
106
- </div>
107
- </header>
108
- );
109
- }
@@ -1,58 +0,0 @@
1
- import type { TableSummary } from '../types';
2
-
3
- interface TableListProps {
4
- tables: TableSummary[];
5
- selectedTable: string | null;
6
- onSelect: (tableName: string) => void;
7
- }
8
-
9
- export function TableList({ tables, selectedTable, onSelect }: TableListProps) {
10
- if (tables.length === 0) {
11
- return (
12
- <div className="p-4 text-center text-slate-500 text-sm">
13
- <p>No tables found</p>
14
- <p className="mt-1 text-xs">Check your database schema.</p>
15
- </div>
16
- );
17
- }
18
-
19
- return (
20
- <div className="py-1">
21
- {tables.map((table) => (
22
- <button
23
- key={table.name}
24
- onClick={() => onSelect(table.name)}
25
- className={`w-full text-left px-3 py-2 flex items-center gap-3 transition-colors ${
26
- selectedTable === table.name
27
- ? 'bg-studio-hover text-white'
28
- : 'text-slate-400 hover:bg-studio-hover hover:text-slate-200'
29
- }`}
30
- >
31
- {/* Table icon */}
32
- <svg
33
- className={`w-4 h-4 shrink-0 ${selectedTable === table.name ? 'text-emerald-400' : 'text-slate-500'}`}
34
- fill="none"
35
- stroke="currentColor"
36
- viewBox="0 0 24 24"
37
- >
38
- <path
39
- strokeLinecap="round"
40
- strokeLinejoin="round"
41
- strokeWidth={1.5}
42
- d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
43
- />
44
- </svg>
45
-
46
- <div className="flex-1 min-w-0">
47
- <div className="truncate text-sm">{table.name}</div>
48
- {table.estimatedRowCount !== undefined && (
49
- <div className="text-xs text-slate-500">
50
- {table.estimatedRowCount.toLocaleString()} rows
51
- </div>
52
- )}
53
- </div>
54
- </button>
55
- ))}
56
- </div>
57
- );
58
- }