Skip to content

Commit 9068661

Browse files
author
Dylan Huang
committed
DRY things
1 parent 1fe9c5a commit 9068661

File tree

4 files changed

+241
-137
lines changed

4 files changed

+241
-137
lines changed

vite-app/src/GlobalState.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
11
import { makeAutoObservable } from "mobx";
22
import type { EvaluationRow } from "./types/eval-protocol";
3+
import type { PivotConfig } from "./types/filters";
34
import flattenJson from "./util/flatten-json";
45

5-
// Pivot configuration interface
6-
export interface PivotConfig {
7-
selectedRowFields: string[];
8-
selectedColumnFields: string[];
9-
selectedValueField: string;
10-
selectedAggregator: string;
11-
filters: Array<{ field: string; operator: string; value: string }>;
12-
}
13-
146
// Default pivot configuration
157
const DEFAULT_PIVOT_CONFIG: PivotConfig = {
168
selectedRowFields: ["$.eval_metadata.name"],

vite-app/src/components/PivotTab.tsx

Lines changed: 108 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import PivotTable from "./PivotTable";
33
import Select from "./Select";
44
import Button from "./Button";
55
import { state } from "../App";
6-
import { useEffect } from "react";
6+
import { type FilterConfig } from "../types/filters";
7+
import {
8+
getFieldType,
9+
getOperatorsForField,
10+
createFilterFunction,
11+
} from "../util/filter-utils";
712

813
interface FieldSelectorProps {
914
title: string;
@@ -126,112 +131,132 @@ const AggregatorSelector = ({
126131
</div>
127132
);
128133

134+
// Reusable filter input component
135+
const FilterInput = ({
136+
filter,
137+
index,
138+
onUpdate,
139+
}: {
140+
filter: FilterConfig;
141+
index: number;
142+
onUpdate: (updates: Partial<FilterConfig>) => void;
143+
}) => {
144+
const fieldType = filter.type || getFieldType(filter.field);
145+
146+
if (fieldType === "date") {
147+
return (
148+
<div className="flex space-x-2">
149+
<input
150+
type="date"
151+
value={filter.value}
152+
onChange={(e) => onUpdate({ value: e.target.value })}
153+
className="px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:border-gray-500 min-w-32"
154+
/>
155+
{filter.operator === "between" && (
156+
<input
157+
type="date"
158+
value={filter.value2 || ""}
159+
onChange={(e) => onUpdate({ value2: e.target.value })}
160+
className="px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:border-gray-500 min-w-32"
161+
placeholder="End date"
162+
/>
163+
)}
164+
</div>
165+
);
166+
}
167+
168+
return (
169+
<input
170+
type="text"
171+
value={filter.value}
172+
onChange={(e) => onUpdate({ value: e.target.value })}
173+
placeholder="Value"
174+
className="px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:border-gray-500 min-w-32"
175+
/>
176+
);
177+
};
178+
129179
const FilterSelector = ({
130180
filters,
131181
onFiltersChange,
132182
availableKeys,
133183
}: {
134-
filters: Array<{ field: string; operator: string; value: string }>;
135-
onFiltersChange: (
136-
filters: Array<{ field: string; operator: string; value: string }>
137-
) => void;
184+
filters: FilterConfig[];
185+
onFiltersChange: (filters: FilterConfig[]) => void;
138186
availableKeys: string[];
139187
}) => {
140188
const addFilter = () => {
141189
onFiltersChange([
142190
...filters,
143-
{ field: "", operator: "contains", value: "" },
191+
{ field: "", operator: "contains", value: "", type: "text" },
144192
]);
145193
};
146194

147195
const removeFilter = (index: number) => {
148196
onFiltersChange(filters.filter((_, i) => i !== index));
149197
};
150198

151-
const updateFilter = (
152-
index: number,
153-
field: string,
154-
operator: string,
155-
value: string
156-
) => {
199+
const updateFilter = (index: number, updates: Partial<FilterConfig>) => {
157200
const newFilters = [...filters];
158-
newFilters[index] = { field, operator, value };
201+
newFilters[index] = { ...newFilters[index], ...updates };
159202
onFiltersChange(newFilters);
160203
};
161204

162-
const operators = [
163-
{ value: "==", label: "equals" },
164-
{ value: "!=", label: "not equals" },
165-
{ value: ">", label: "greater than" },
166-
{ value: "<", label: "less than" },
167-
{ value: ">=", label: "greater than or equal" },
168-
{ value: "<=", label: "less than or equal" },
169-
{ value: "contains", label: "contains" },
170-
{ value: "!contains", label: "not contains" },
171-
];
172-
173205
return (
174206
<div className="mb-4">
175207
<div className="text-xs font-medium text-gray-700 mb-2">Filters:</div>
176208
<div className="space-y-2">
177-
{filters.map((filter, index) => (
178-
<div key={index} className="flex items-center space-x-2">
179-
<Select
180-
value={filter.field}
181-
onChange={(e) =>
182-
updateFilter(
183-
index,
184-
e.target.value,
185-
filter.operator,
186-
filter.value
187-
)
188-
}
189-
size="sm"
190-
className="min-w-48"
191-
>
192-
<option value="">Select a field...</option>
193-
{availableKeys?.map((key) => (
194-
<option key={key} value={key}>
195-
{key}
196-
</option>
197-
))}
198-
</Select>
199-
<Select
200-
value={filter.operator}
201-
onChange={(e) =>
202-
updateFilter(index, filter.field, e.target.value, filter.value)
203-
}
204-
size="sm"
205-
className="min-w-32"
206-
>
207-
{operators.map((op) => (
208-
<option key={op.value} value={op.value}>
209-
{op.label}
210-
</option>
211-
))}
212-
</Select>
213-
<input
214-
type="text"
215-
value={filter.value}
216-
onChange={(e) =>
217-
updateFilter(
218-
index,
219-
filter.field,
220-
filter.operator,
221-
e.target.value
222-
)
223-
}
224-
placeholder="Value"
225-
className="px-2 py-1 text-xs border border-gray-300 rounded focus:outline-none focus:border-gray-500 min-w-32"
226-
/>
227-
<button
228-
onClick={() => removeFilter(index)}
229-
className="text-xs text-red-600 hover:text-red-800 px-2 py-1"
230-
>
231-
Remove
232-
</button>
233-
</div>
234-
))}
209+
{filters.map((filter, index) => {
210+
const fieldType = filter.type || getFieldType(filter.field);
211+
const operators = getOperatorsForField(filter.field, fieldType);
212+
213+
return (
214+
<div key={index} className="flex items-center space-x-2">
215+
<Select
216+
value={filter.field}
217+
onChange={(e) => {
218+
const newField = e.target.value;
219+
const newType = getFieldType(newField);
220+
updateFilter(index, { field: newField, type: newType });
221+
}}
222+
size="sm"
223+
className="min-w-48"
224+
>
225+
<option value="">Select a field...</option>
226+
{availableKeys?.map((key) => (
227+
<option key={key} value={key}>
228+
{key}
229+
</option>
230+
))}
231+
</Select>
232+
<Select
233+
value={filter.operator}
234+
onChange={(e) =>
235+
updateFilter(index, { operator: e.target.value })
236+
}
237+
size="sm"
238+
className="min-w-32"
239+
>
240+
{operators.map((op) => (
241+
<option key={op.value} value={op.value}>
242+
{op.label}
243+
</option>
244+
))}
245+
</Select>
246+
<FilterInput
247+
filter={filter}
248+
index={index}
249+
onUpdate={(updates) => updateFilter(index, updates)}
250+
/>
251+
<button
252+
onClick={() => removeFilter(index)}
253+
className="text-xs text-red-600 hover:text-red-800 px-2 py-1"
254+
>
255+
Remove
256+
</button>
257+
</div>
258+
);
259+
})}
235260
<button
236261
onClick={addFilter}
237262
className="text-xs text-blue-600 hover:text-blue-800 px-2 py-1"
@@ -244,10 +269,8 @@ const FilterSelector = ({
244269
};
245270

246271
const PivotTab = observer(() => {
247-
// Use global state instead of local state
248272
const { pivotConfig } = state;
249273

250-
// Update global state when configuration changes
251274
const updateRowFields = (index: number, value: string) => {
252275
const newRowFields = [...pivotConfig.selectedRowFields];
253276
newRowFields[index] = value;
@@ -268,9 +291,7 @@ const PivotTab = observer(() => {
268291
state.updatePivotConfig({ selectedAggregator: value });
269292
};
270293

271-
const updateFilters = (
272-
filters: Array<{ field: string; operator: string; value: string }>
273-
) => {
294+
const updateFilters = (filters: FilterConfig[]) => {
274295
state.updatePivotConfig({ filters });
275296
};
276297

@@ -304,47 +325,6 @@ const PivotTab = observer(() => {
304325

305326
const availableKeys = state.flattenedDatasetKeys;
306327

307-
// Create filter function from filter configuration
308-
const createFilterFunction = (
309-
filters: Array<{ field: string; operator: string; value: string }>
310-
) => {
311-
if (filters.length === 0) return undefined;
312-
313-
return (record: any) => {
314-
return filters.every((filter) => {
315-
if (!filter.field || !filter.value) return true; // Skip incomplete filters
316-
317-
const fieldValue = record[filter.field];
318-
const filterValue = filter.value;
319-
320-
switch (filter.operator) {
321-
case "==":
322-
return String(fieldValue) === filterValue;
323-
case "!=":
324-
return String(fieldValue) !== filterValue;
325-
case ">":
326-
return Number(fieldValue) > Number(filterValue);
327-
case "<":
328-
return Number(fieldValue) < Number(filterValue);
329-
case ">=":
330-
return Number(fieldValue) >= Number(filterValue);
331-
case "<=":
332-
return Number(fieldValue) <= Number(filterValue);
333-
case "contains":
334-
return String(fieldValue)
335-
.toLowerCase()
336-
.includes(filterValue.toLowerCase());
337-
case "!contains":
338-
return !String(fieldValue)
339-
.toLowerCase()
340-
.includes(filterValue.toLowerCase());
341-
default:
342-
return true;
343-
}
344-
});
345-
};
346-
};
347-
348328
return (
349329
<div>
350330
<div className="text-xs text-gray-600 mb-2 max-w-2xl">

vite-app/src/types/filters.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Filter configuration interface
2+
export interface FilterConfig {
3+
field: string;
4+
operator: string;
5+
value: string;
6+
value2?: string; // For filtering between dates
7+
type?: "text" | "date" | "date-range";
8+
}
9+
10+
export interface FilterOperator {
11+
value: string;
12+
label: string;
13+
}
14+
15+
// Pivot configuration interface
16+
export interface PivotConfig {
17+
selectedRowFields: string[];
18+
selectedColumnFields: string[];
19+
selectedValueField: string;
20+
selectedAggregator: string;
21+
filters: FilterConfig[];
22+
}

0 commit comments

Comments
 (0)