@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
package/ui/node_modules/.bin/tsc
CHANGED
|
@@ -10,9 +10,9 @@ case `uname` in
|
|
|
10
10
|
esac
|
|
11
11
|
|
|
12
12
|
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/
|
|
13
|
+
export NODE_PATH="/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/node_modules"
|
|
14
14
|
else
|
|
15
|
-
export NODE_PATH="/
|
|
15
|
+
export NODE_PATH="/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
16
|
fi
|
|
17
17
|
if [ -x "$basedir/node" ]; then
|
|
18
18
|
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
|
|
@@ -10,9 +10,9 @@ case `uname` in
|
|
|
10
10
|
esac
|
|
11
11
|
|
|
12
12
|
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/
|
|
13
|
+
export NODE_PATH="/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/node_modules"
|
|
14
14
|
else
|
|
15
|
-
export NODE_PATH="/
|
|
15
|
+
export NODE_PATH="/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/bin/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules/typescript/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/typescript@5.8.2/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
16
|
fi
|
|
17
17
|
if [ -x "$basedir/node" ]; then
|
|
18
18
|
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
|
|
@@ -10,9 +10,9 @@ case `uname` in
|
|
|
10
10
|
esac
|
|
11
11
|
|
|
12
12
|
if [ -z "$NODE_PATH" ]; then
|
|
13
|
-
export NODE_PATH="/
|
|
13
|
+
export NODE_PATH="/home/runner/work/toolbox/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/bin/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/node_modules"
|
|
14
14
|
else
|
|
15
|
-
export NODE_PATH="/
|
|
15
|
+
export NODE_PATH="/home/runner/work/toolbox/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/bin/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules/vite/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/vite@6.3.5_@types+node@24.9.1_jiti@2.6.1_lightningcss@1.30.2_terser@5.43.1_tsx@4.20.6/node_modules:/home/runner/work/toolbox/toolbox/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
16
|
fi
|
|
17
17
|
if [ -x "$basedir/node" ]; then
|
|
18
18
|
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
|
package/ui/src/App.tsx
CHANGED
|
@@ -15,6 +15,7 @@ export function App() {
|
|
|
15
15
|
> | null>(null);
|
|
16
16
|
const [loading, setLoading] = useState(true);
|
|
17
17
|
const [error, setError] = useState<string | null>(null);
|
|
18
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
18
19
|
|
|
19
20
|
// Load tables on mount
|
|
20
21
|
useEffect(() => {
|
|
@@ -22,6 +23,10 @@ export function App() {
|
|
|
22
23
|
try {
|
|
23
24
|
const data = await api.getTables();
|
|
24
25
|
setTables(data.tables);
|
|
26
|
+
// Auto-select first table if none selected
|
|
27
|
+
if (data.tables.length > 0 && !selectedTable) {
|
|
28
|
+
setSelectedTable(data.tables[0].name);
|
|
29
|
+
}
|
|
25
30
|
} catch (err) {
|
|
26
31
|
setError(err instanceof Error ? err.message : 'Failed to load tables');
|
|
27
32
|
} finally {
|
|
@@ -69,73 +74,167 @@ export function App() {
|
|
|
69
74
|
setSelectedRow(null);
|
|
70
75
|
}, []);
|
|
71
76
|
|
|
72
|
-
const handleBack = useCallback(() => {
|
|
73
|
-
setSelectedTable(null);
|
|
74
|
-
setTableInfo(null);
|
|
75
|
-
setSelectedRow(null);
|
|
76
|
-
}, []);
|
|
77
|
-
|
|
78
77
|
return (
|
|
79
|
-
<div className="flex flex-col
|
|
78
|
+
<div className="flex flex-col h-screen bg-studio-bg text-slate-100">
|
|
80
79
|
{/* Header */}
|
|
81
|
-
<header className="bg-
|
|
82
|
-
<div className="flex items-center gap-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
<header className="bg-studio-surface border-b border-studio-border px-4 py-3 flex items-center justify-between shrink-0">
|
|
81
|
+
<div className="flex items-center gap-3">
|
|
82
|
+
<button
|
|
83
|
+
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
84
|
+
className="p-1.5 hover:bg-studio-hover rounded transition-colors"
|
|
85
|
+
title={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
86
|
+
>
|
|
87
|
+
<svg
|
|
88
|
+
className="w-5 h-5 text-slate-400"
|
|
89
|
+
fill="none"
|
|
90
|
+
stroke="currentColor"
|
|
91
|
+
viewBox="0 0 24 24"
|
|
87
92
|
>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
<path
|
|
94
|
+
strokeLinecap="round"
|
|
95
|
+
strokeLinejoin="round"
|
|
96
|
+
strokeWidth={2}
|
|
97
|
+
d="M4 6h16M4 12h16M4 18h16"
|
|
98
|
+
/>
|
|
99
|
+
</svg>
|
|
100
|
+
</button>
|
|
101
|
+
<h1 className="text-lg font-semibold flex items-center gap-2">
|
|
102
|
+
<span className="text-emerald-400">
|
|
103
|
+
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
104
|
+
<path d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
|
105
|
+
</svg>
|
|
106
|
+
</span>
|
|
107
|
+
Studio
|
|
96
108
|
</h1>
|
|
97
109
|
</div>
|
|
98
|
-
<div className="flex items-center gap-
|
|
99
|
-
<span className="text-sm text-slate-400">{tables.length} tables</span>
|
|
110
|
+
<div className="flex items-center gap-3">
|
|
100
111
|
<button
|
|
101
112
|
onClick={handleRefresh}
|
|
102
113
|
disabled={loading}
|
|
103
|
-
className="px-3 py-1 text-sm bg-
|
|
114
|
+
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-studio-hover hover:bg-studio-active rounded transition-colors disabled:opacity-50"
|
|
104
115
|
>
|
|
105
|
-
|
|
116
|
+
<svg
|
|
117
|
+
className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`}
|
|
118
|
+
fill="none"
|
|
119
|
+
stroke="currentColor"
|
|
120
|
+
viewBox="0 0 24 24"
|
|
121
|
+
>
|
|
122
|
+
<path
|
|
123
|
+
strokeLinecap="round"
|
|
124
|
+
strokeLinejoin="round"
|
|
125
|
+
strokeWidth={2}
|
|
126
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
127
|
+
/>
|
|
128
|
+
</svg>
|
|
129
|
+
Refresh
|
|
106
130
|
</button>
|
|
107
131
|
</div>
|
|
108
132
|
</header>
|
|
109
133
|
|
|
110
134
|
{/* Main Content */}
|
|
111
135
|
<main className="flex-1 flex overflow-hidden">
|
|
112
|
-
{
|
|
113
|
-
|
|
114
|
-
|
|
136
|
+
{/* Sidebar */}
|
|
137
|
+
<aside
|
|
138
|
+
className={`${sidebarCollapsed ? 'w-0' : 'w-64'} bg-studio-surface border-r border-studio-border flex flex-col shrink-0 transition-all duration-200 overflow-hidden`}
|
|
139
|
+
>
|
|
140
|
+
<div className="p-3 border-b border-studio-border">
|
|
141
|
+
<div className="relative">
|
|
142
|
+
<svg
|
|
143
|
+
className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500"
|
|
144
|
+
fill="none"
|
|
145
|
+
stroke="currentColor"
|
|
146
|
+
viewBox="0 0 24 24"
|
|
147
|
+
>
|
|
148
|
+
<path
|
|
149
|
+
strokeLinecap="round"
|
|
150
|
+
strokeLinejoin="round"
|
|
151
|
+
strokeWidth={2}
|
|
152
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
153
|
+
/>
|
|
154
|
+
</svg>
|
|
155
|
+
<input
|
|
156
|
+
type="text"
|
|
157
|
+
placeholder="Search tables..."
|
|
158
|
+
className="w-full bg-studio-bg border border-studio-border rounded px-3 py-1.5 pl-9 text-sm placeholder-slate-500 focus:outline-none focus:border-emerald-500/50"
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<div className="flex-1 overflow-auto">
|
|
163
|
+
{loading && tables.length === 0 ? (
|
|
164
|
+
<div className="p-4 text-center text-slate-500 text-sm">
|
|
165
|
+
Loading...
|
|
166
|
+
</div>
|
|
167
|
+
) : (
|
|
168
|
+
<TableList
|
|
169
|
+
tables={tables}
|
|
170
|
+
selectedTable={selectedTable}
|
|
171
|
+
onSelect={handleSelectTable}
|
|
172
|
+
/>
|
|
173
|
+
)}
|
|
115
174
|
</div>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
Loading...
|
|
175
|
+
<div className="p-3 border-t border-studio-border text-xs text-slate-500">
|
|
176
|
+
{tables.length} tables
|
|
119
177
|
</div>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
178
|
+
</aside>
|
|
179
|
+
|
|
180
|
+
{/* Main Area */}
|
|
181
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
182
|
+
{error ? (
|
|
183
|
+
<div className="flex-1 flex items-center justify-center text-red-400">
|
|
184
|
+
<div className="text-center">
|
|
185
|
+
<svg
|
|
186
|
+
className="w-12 h-12 mx-auto mb-3 text-red-400/50"
|
|
187
|
+
fill="none"
|
|
188
|
+
stroke="currentColor"
|
|
189
|
+
viewBox="0 0 24 24"
|
|
190
|
+
>
|
|
191
|
+
<path
|
|
192
|
+
strokeLinecap="round"
|
|
193
|
+
strokeLinejoin="round"
|
|
194
|
+
strokeWidth={2}
|
|
195
|
+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
196
|
+
/>
|
|
197
|
+
</svg>
|
|
198
|
+
<p>{error}</p>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
) : !selectedTable ? (
|
|
202
|
+
<div className="flex-1 flex items-center justify-center text-slate-500">
|
|
203
|
+
<div className="text-center">
|
|
204
|
+
<svg
|
|
205
|
+
className="w-12 h-12 mx-auto mb-3 text-slate-600"
|
|
206
|
+
fill="none"
|
|
207
|
+
stroke="currentColor"
|
|
208
|
+
viewBox="0 0 24 24"
|
|
209
|
+
>
|
|
210
|
+
<path
|
|
211
|
+
strokeLinecap="round"
|
|
212
|
+
strokeLinejoin="round"
|
|
213
|
+
strokeWidth={2}
|
|
214
|
+
d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"
|
|
215
|
+
/>
|
|
216
|
+
</svg>
|
|
217
|
+
<p>Select a table to view data</p>
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
) : (
|
|
221
|
+
<TableView
|
|
222
|
+
tableName={selectedTable}
|
|
223
|
+
tableInfo={tableInfo}
|
|
224
|
+
onRowSelect={setSelectedRow}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Row Detail Panel */}
|
|
230
|
+
{selectedRow && tableInfo && (
|
|
231
|
+
<RowDetail
|
|
232
|
+
row={selectedRow}
|
|
233
|
+
columns={tableInfo.columns}
|
|
234
|
+
onClose={() => setSelectedRow(null)}
|
|
127
235
|
/>
|
|
128
236
|
)}
|
|
129
237
|
</main>
|
|
130
|
-
|
|
131
|
-
{/* Row Detail Panel */}
|
|
132
|
-
{selectedRow && tableInfo && (
|
|
133
|
-
<RowDetail
|
|
134
|
-
row={selectedRow}
|
|
135
|
-
columns={tableInfo.columns}
|
|
136
|
-
onClose={() => setSelectedRow(null)}
|
|
137
|
-
/>
|
|
138
|
-
)}
|
|
139
238
|
</div>
|
|
140
239
|
);
|
|
141
240
|
}
|
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
}
|