@geekmidas/studio 0.1.0 → 0.4.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/dist/{DataBrowser-hGwiTffZ.d.cts → DataBrowser-B-jz8KBR.d.mts} +5 -2
- package/dist/DataBrowser-B-jz8KBR.d.mts.map +1 -0
- package/dist/{DataBrowser-SOcqmZb2.d.mts → DataBrowser-BTe9HWJy.d.cts} +5 -2
- package/dist/DataBrowser-BTe9HWJy.d.cts.map +1 -0
- package/dist/{DataBrowser-c-Gs6PZB.cjs → DataBrowser-D8c_pBf4.cjs} +4 -4
- package/dist/DataBrowser-D8c_pBf4.cjs.map +1 -0
- package/dist/{DataBrowser-DQ3-ZxdV.mjs → DataBrowser-kgcI9ApJ.mjs} +4 -4
- package/dist/DataBrowser-kgcI9ApJ.mjs.map +1 -0
- package/dist/Studio-CYzz3wD2.d.cts +152 -0
- package/dist/Studio-CYzz3wD2.d.cts.map +1 -0
- package/dist/Studio-D5yGscb8.d.mts +152 -0
- package/dist/Studio-D5yGscb8.d.mts.map +1 -0
- package/dist/data/index.cjs +1 -1
- package/dist/data/index.d.cts +1 -1
- package/dist/data/index.d.mts +1 -1
- package/dist/data/index.mjs +1 -1
- package/dist/index.cjs +33 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -131
- package/dist/index.d.mts +4 -131
- package/dist/index.mjs +33 -3
- package/dist/index.mjs.map +1 -1
- package/dist/server/hono.cjs +168 -21
- package/dist/server/hono.cjs.map +1 -1
- package/dist/server/hono.d.cts +13 -2
- package/dist/server/hono.d.cts.map +1 -0
- package/dist/server/hono.d.mts +13 -2
- package/dist/server/hono.d.mts.map +1 -0
- package/dist/server/hono.mjs +168 -21
- package/dist/server/hono.mjs.map +1 -1
- package/dist/types-BZv87Ikv.mjs.map +1 -1
- package/dist/types-CMttUZYk.cjs.map +1 -1
- package/package.json +14 -6
- package/src/Studio.ts +341 -292
- package/src/__tests__/Studio.spec.ts +447 -0
- package/src/data/DataBrowser.ts +147 -143
- package/src/data/__tests__/DataBrowser.integration.spec.ts +404 -404
- package/src/data/__tests__/filtering.integration.spec.ts +726 -726
- package/src/data/__tests__/introspection.integration.spec.ts +340 -340
- package/src/data/__tests__/pagination.spec.ts +123 -0
- package/src/data/filtering.ts +154 -154
- package/src/data/introspection.ts +141 -141
- package/src/data/pagination.ts +15 -15
- package/src/index.ts +22 -24
- package/src/server/__tests__/hono.integration.spec.ts +605 -347
- package/src/server/hono.ts +392 -190
- package/src/types.ts +138 -138
- package/src/ui-assets.ts +10 -13
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +9 -9
- package/ui/package.json +28 -22
- package/ui/src/App.tsx +95 -235
- package/ui/src/api.ts +184 -42
- package/ui/src/components/FilterPanel.tsx +198 -198
- package/ui/src/components/NavRail.tsx +183 -0
- package/ui/src/components/RowDetail.tsx +106 -106
- package/ui/src/components/StudioHeader.tsx +109 -0
- package/ui/src/components/TableList.tsx +49 -49
- package/ui/src/components/TableView.tsx +530 -485
- package/ui/src/main.tsx +3 -3
- package/ui/src/pages/DashboardPage.tsx +500 -0
- package/ui/src/pages/DatabasePage.tsx +226 -0
- package/ui/src/pages/EndpointDetailsPage.tsx +288 -0
- package/ui/src/pages/ExceptionsPage.tsx +268 -0
- package/ui/src/pages/LogsPage.tsx +228 -0
- package/ui/src/pages/MonitoringPage.tsx +46 -0
- package/ui/src/pages/PerformancePage.tsx +307 -0
- package/ui/src/pages/RequestsPage.tsx +379 -0
- package/ui/src/providers/StudioProvider.tsx +194 -0
- package/ui/src/styles.css +53 -142
- package/ui/src/types.ts +154 -30
- package/ui/tsconfig.tsbuildinfo +1 -1
- package/ui/vite.config.ts +6 -6
- package/dist/DataBrowser-DQ3-ZxdV.mjs.map +0 -1
- package/dist/DataBrowser-c-Gs6PZB.cjs.map +0 -1
package/ui/src/api.ts
CHANGED
|
@@ -1,71 +1,213 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
EndpointDetails,
|
|
3
|
+
EndpointMetrics,
|
|
4
|
+
ExceptionEntry,
|
|
5
|
+
FilterConfig,
|
|
6
|
+
LogEntry,
|
|
7
|
+
QueryResult,
|
|
8
|
+
RequestEntry,
|
|
9
|
+
RequestMetrics,
|
|
10
|
+
SchemaInfo,
|
|
11
|
+
SortConfig,
|
|
12
|
+
StatusDistribution,
|
|
13
|
+
StudioStats,
|
|
14
|
+
TableInfo,
|
|
15
|
+
TableSummary,
|
|
8
16
|
} from './types';
|
|
9
17
|
|
|
10
18
|
const BASE_URL = '/__studio';
|
|
11
19
|
|
|
12
20
|
async function fetchJson<T>(url: string): Promise<T> {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
const res = await fetch(`${BASE_URL}${url}`);
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
24
|
+
}
|
|
25
|
+
return res.json();
|
|
18
26
|
}
|
|
19
27
|
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Database API
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
20
32
|
export async function getSchema(refresh = false): Promise<SchemaInfo> {
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
const url = refresh ? '/api/schema?refresh=true' : '/api/schema';
|
|
34
|
+
return fetchJson(url);
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
export async function getTables(): Promise<{ tables: TableSummary[] }> {
|
|
26
|
-
|
|
38
|
+
return fetchJson('/api/tables');
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
export async function getTableInfo(tableName: string): Promise<TableInfo> {
|
|
30
|
-
|
|
42
|
+
return fetchJson(`/api/tables/${encodeURIComponent(tableName)}`);
|
|
31
43
|
}
|
|
32
44
|
|
|
33
45
|
export interface QueryOptions {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
pageSize?: number;
|
|
47
|
+
cursor?: string;
|
|
48
|
+
filters?: FilterConfig[];
|
|
49
|
+
sort?: SortConfig[];
|
|
38
50
|
}
|
|
39
51
|
|
|
40
52
|
export async function queryTable(
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
tableName: string,
|
|
54
|
+
options: QueryOptions = {},
|
|
43
55
|
): Promise<QueryResult> {
|
|
44
|
-
|
|
56
|
+
const params = new URLSearchParams();
|
|
57
|
+
|
|
58
|
+
if (options.pageSize) {
|
|
59
|
+
params.set('pageSize', String(options.pageSize));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (options.cursor) {
|
|
63
|
+
params.set('cursor', options.cursor);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (options.filters) {
|
|
67
|
+
for (const filter of options.filters) {
|
|
68
|
+
params.set(`filter[${filter.column}][${filter.operator}]`, filter.value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (options.sort && options.sort.length > 0) {
|
|
73
|
+
const sortStr = options.sort
|
|
74
|
+
.map((s) => `${s.column}:${s.direction}`)
|
|
75
|
+
.join(',');
|
|
76
|
+
params.set('sort', sortStr);
|
|
77
|
+
}
|
|
45
78
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
79
|
+
const queryStr = params.toString();
|
|
80
|
+
const url = `/api/tables/${encodeURIComponent(tableName)}/rows${queryStr ? `?${queryStr}` : ''}`;
|
|
49
81
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
82
|
+
return fetchJson(url);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Monitoring API (from Telescope)
|
|
87
|
+
// ============================================================================
|
|
53
88
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
89
|
+
export async function getStats(): Promise<StudioStats> {
|
|
90
|
+
return fetchJson('/api/stats');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface MonitoringQueryOptions {
|
|
94
|
+
limit?: number;
|
|
95
|
+
search?: string;
|
|
96
|
+
method?: string;
|
|
97
|
+
status?: string;
|
|
98
|
+
level?: string;
|
|
99
|
+
}
|
|
59
100
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
101
|
+
export async function getRequests(
|
|
102
|
+
options: MonitoringQueryOptions = {},
|
|
103
|
+
): Promise<RequestEntry[]> {
|
|
104
|
+
const params = new URLSearchParams();
|
|
105
|
+
if (options.limit) params.set('limit', String(options.limit));
|
|
106
|
+
if (options.search) params.set('search', options.search);
|
|
107
|
+
if (options.method) params.set('method', options.method);
|
|
108
|
+
if (options.status) params.set('status', options.status);
|
|
109
|
+
|
|
110
|
+
const queryStr = params.toString();
|
|
111
|
+
return fetchJson(`/api/requests${queryStr ? `?${queryStr}` : ''}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function getRequest(id: string): Promise<RequestEntry> {
|
|
115
|
+
return fetchJson(`/api/requests/${encodeURIComponent(id)}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function getExceptions(
|
|
119
|
+
options: MonitoringQueryOptions = {},
|
|
120
|
+
): Promise<ExceptionEntry[]> {
|
|
121
|
+
const params = new URLSearchParams();
|
|
122
|
+
if (options.limit) params.set('limit', String(options.limit));
|
|
123
|
+
if (options.search) params.set('search', options.search);
|
|
124
|
+
|
|
125
|
+
const queryStr = params.toString();
|
|
126
|
+
return fetchJson(`/api/exceptions${queryStr ? `?${queryStr}` : ''}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function getException(id: string): Promise<ExceptionEntry> {
|
|
130
|
+
return fetchJson(`/api/exceptions/${encodeURIComponent(id)}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function getLogs(
|
|
134
|
+
options: MonitoringQueryOptions = {},
|
|
135
|
+
): Promise<LogEntry[]> {
|
|
136
|
+
const params = new URLSearchParams();
|
|
137
|
+
if (options.limit) params.set('limit', String(options.limit));
|
|
138
|
+
if (options.search) params.set('search', options.search);
|
|
139
|
+
if (options.level) params.set('level', options.level);
|
|
140
|
+
|
|
141
|
+
const queryStr = params.toString();
|
|
142
|
+
return fetchJson(`/api/logs${queryStr ? `?${queryStr}` : ''}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// Metrics API
|
|
147
|
+
// ============================================================================
|
|
148
|
+
|
|
149
|
+
export interface MetricsQueryOptions {
|
|
150
|
+
start?: string;
|
|
151
|
+
end?: string;
|
|
152
|
+
bucketSize?: number;
|
|
153
|
+
limit?: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function getMetrics(
|
|
157
|
+
options: MetricsQueryOptions = {},
|
|
158
|
+
): Promise<RequestMetrics> {
|
|
159
|
+
const params = new URLSearchParams();
|
|
160
|
+
if (options.start) params.set('start', options.start);
|
|
161
|
+
if (options.end) params.set('end', options.end);
|
|
162
|
+
if (options.bucketSize) params.set('bucketSize', String(options.bucketSize));
|
|
163
|
+
|
|
164
|
+
const queryStr = params.toString();
|
|
165
|
+
return fetchJson(`/api/metrics${queryStr ? `?${queryStr}` : ''}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function getEndpointMetrics(
|
|
169
|
+
options: MetricsQueryOptions = {},
|
|
170
|
+
): Promise<EndpointMetrics[]> {
|
|
171
|
+
const params = new URLSearchParams();
|
|
172
|
+
if (options.start) params.set('start', options.start);
|
|
173
|
+
if (options.end) params.set('end', options.end);
|
|
174
|
+
if (options.limit) params.set('limit', String(options.limit));
|
|
175
|
+
|
|
176
|
+
const queryStr = params.toString();
|
|
177
|
+
return fetchJson(`/api/metrics/endpoints${queryStr ? `?${queryStr}` : ''}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function getEndpointDetails(
|
|
181
|
+
method: string,
|
|
182
|
+
path: string,
|
|
183
|
+
options: MetricsQueryOptions = {},
|
|
184
|
+
): Promise<EndpointDetails> {
|
|
185
|
+
const params = new URLSearchParams();
|
|
186
|
+
params.set('method', method);
|
|
187
|
+
params.set('path', path);
|
|
188
|
+
if (options.start) params.set('start', options.start);
|
|
189
|
+
if (options.end) params.set('end', options.end);
|
|
190
|
+
|
|
191
|
+
return fetchJson(`/api/metrics/endpoint?${params.toString()}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function getStatusDistribution(
|
|
195
|
+
options: MetricsQueryOptions = {},
|
|
196
|
+
): Promise<StatusDistribution> {
|
|
197
|
+
const params = new URLSearchParams();
|
|
198
|
+
if (options.start) params.set('start', options.start);
|
|
199
|
+
if (options.end) params.set('end', options.end);
|
|
200
|
+
|
|
201
|
+
const queryStr = params.toString();
|
|
202
|
+
return fetchJson(`/api/metrics/status${queryStr ? `?${queryStr}` : ''}`);
|
|
203
|
+
}
|
|
66
204
|
|
|
67
|
-
|
|
68
|
-
|
|
205
|
+
// ============================================================================
|
|
206
|
+
// WebSocket
|
|
207
|
+
// ============================================================================
|
|
69
208
|
|
|
70
|
-
|
|
209
|
+
export function createWebSocket(): WebSocket {
|
|
210
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
211
|
+
const wsUrl = `${protocol}//${window.location.host}${BASE_URL}/ws`;
|
|
212
|
+
return new WebSocket(wsUrl);
|
|
71
213
|
}
|
|
@@ -2,212 +2,212 @@ import { useCallback } from 'react';
|
|
|
2
2
|
import type { ColumnInfo, FilterConfig } from '../types';
|
|
3
3
|
|
|
4
4
|
interface FilterPanelProps {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
columns: ColumnInfo[];
|
|
6
|
+
filters: FilterConfig[];
|
|
7
|
+
onFiltersChange: (filters: FilterConfig[]) => void;
|
|
8
|
+
onClose: () => void;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const OPERATORS: { value: string; label: string; types?: string[] }[] = [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
30
|
];
|
|
31
31
|
|
|
32
32
|
function getOperatorsForColumn(column: ColumnInfo) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
return OPERATORS.filter(
|
|
34
|
+
(op) => !op.types || op.types.includes(column.type || 'string'),
|
|
35
|
+
);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export function FilterPanel({
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
columns,
|
|
40
|
+
filters,
|
|
41
|
+
onFiltersChange,
|
|
42
|
+
onClose,
|
|
43
43
|
}: FilterPanelProps) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
213
|
}
|