Skip to content

Commit 848a594

Browse files
author
Dylan Huang
committed
add filtering to table
1 parent 309c216 commit 848a594

5 files changed

Lines changed: 420 additions & 185 deletions

File tree

vite-app/src/GlobalState.tsx

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { makeAutoObservable, runInAction } from "mobx";
22
import type { EvaluationRow } from "./types/eval-protocol";
3-
import type { PivotConfig } from "./types/filters";
3+
import type { PivotConfig, FilterGroup } from "./types/filters";
44
import flattenJson from "./util/flatten-json";
55

66
// Default pivot configuration
@@ -12,6 +12,9 @@ const DEFAULT_PIVOT_CONFIG: PivotConfig = {
1212
filters: [],
1313
};
1414

15+
// Default table filter configuration
16+
const DEFAULT_TABLE_FILTER_CONFIG: FilterGroup[] = [];
17+
1518
// Default pagination configuration
1619
const DEFAULT_PAGINATION_CONFIG = {
1720
currentPage: 1,
@@ -26,6 +29,8 @@ export class GlobalState {
2629
expandedRows: Record<string, boolean> = {};
2730
// Pivot configuration
2831
pivotConfig: PivotConfig;
32+
// Table filter configuration
33+
tableFilterConfig: FilterGroup[];
2934
// Pagination configuration
3035
currentPage: number;
3136
pageSize: number;
@@ -35,6 +40,8 @@ export class GlobalState {
3540
constructor() {
3641
// Load pivot config from localStorage or use defaults
3742
this.pivotConfig = this.loadPivotConfig();
43+
// Load table filter config from localStorage or use defaults
44+
this.tableFilterConfig = this.loadTableFilterConfig();
3845
// Load pagination config from localStorage or use defaults
3946
const paginationConfig = this.loadPaginationConfig();
4047
this.currentPage = paginationConfig.currentPage;
@@ -57,6 +64,23 @@ export class GlobalState {
5764
return { ...DEFAULT_PIVOT_CONFIG };
5865
}
5966

67+
// Load table filter configuration from localStorage
68+
private loadTableFilterConfig(): FilterGroup[] {
69+
try {
70+
const stored = localStorage.getItem("tableFilterConfig");
71+
if (stored) {
72+
const parsed = JSON.parse(stored);
73+
return Array.isArray(parsed) ? parsed : DEFAULT_TABLE_FILTER_CONFIG;
74+
}
75+
} catch (error) {
76+
console.warn(
77+
"Failed to load table filter config from localStorage:",
78+
error
79+
);
80+
}
81+
return DEFAULT_TABLE_FILTER_CONFIG;
82+
}
83+
6084
// Load pagination configuration from localStorage
6185
private loadPaginationConfig() {
6286
try {
@@ -84,6 +108,21 @@ export class GlobalState {
84108
}
85109
}
86110

111+
// Save table filter configuration to localStorage
112+
private saveTableFilterConfig() {
113+
try {
114+
localStorage.setItem(
115+
"tableFilterConfig",
116+
JSON.stringify(this.tableFilterConfig)
117+
);
118+
} catch (error) {
119+
console.warn(
120+
"Failed to save table filter config to localStorage:",
121+
error
122+
);
123+
}
124+
}
125+
87126
// Save pagination configuration to localStorage
88127
private savePaginationConfig() {
89128
try {
@@ -105,6 +144,12 @@ export class GlobalState {
105144
this.savePivotConfig();
106145
}
107146

147+
// Update table filter configuration and save to localStorage
148+
updateTableFilterConfig(filters: FilterGroup[]) {
149+
this.tableFilterConfig = filters;
150+
this.saveTableFilterConfig();
151+
}
152+
108153
// Update pagination configuration and save to localStorage
109154
updatePaginationConfig(
110155
updates: Partial<{ currentPage: number; pageSize: number }>
@@ -127,6 +172,12 @@ export class GlobalState {
127172
this.savePivotConfig();
128173
}
129174

175+
// Reset table filter configuration to defaults
176+
resetTableFilterConfig() {
177+
this.tableFilterConfig = [...DEFAULT_TABLE_FILTER_CONFIG];
178+
this.saveTableFilterConfig();
179+
}
180+
130181
// Reset pagination configuration to defaults
131182
resetPaginationConfig() {
132183
this.currentPage = DEFAULT_PAGINATION_CONFIG.currentPage;
@@ -213,6 +264,27 @@ export class GlobalState {
213264
return this.sortedDataset.map((row) => flattenJson(row));
214265
}
215266

267+
get filteredFlattenedDataset() {
268+
if (this.tableFilterConfig.length === 0) {
269+
return this.flattenedDataset;
270+
}
271+
272+
const filterFunction = this.createFilterFunction(this.tableFilterConfig);
273+
return this.flattenedDataset.filter(filterFunction);
274+
}
275+
276+
get filteredOriginalDataset() {
277+
if (this.tableFilterConfig.length === 0) {
278+
return this.sortedDataset;
279+
}
280+
281+
const filterFunction = this.createFilterFunction(this.tableFilterConfig);
282+
return this.sortedDataset.filter((row) => {
283+
const flattened = flattenJson(row);
284+
return filterFunction(flattened);
285+
});
286+
}
287+
216288
get flattenedDatasetKeys() {
217289
const keySet = new Set<string>();
218290
this.flattenedDataset.forEach((row) => {
@@ -222,7 +294,7 @@ export class GlobalState {
222294
}
223295

224296
get totalCount() {
225-
return Object.keys(this.dataset).length;
297+
return this.filteredFlattenedDataset.length;
226298
}
227299

228300
get totalPages() {
@@ -236,4 +308,93 @@ export class GlobalState {
236308
get endRow() {
237309
return Math.min(this.currentPage * this.pageSize, this.totalCount);
238310
}
311+
312+
// Create filter function from filter group configuration
313+
private createFilterFunction(filterGroups: FilterGroup[]) {
314+
if (filterGroups.length === 0) return () => true;
315+
316+
return (record: any) => {
317+
return filterGroups.every((group) => {
318+
if (group.filters.length === 0) return true;
319+
320+
if (group.logic === "OR") {
321+
// For OR logic, at least one filter must pass
322+
return group.filters.some((filter) =>
323+
this.evaluateFilter(filter, record)
324+
);
325+
} else {
326+
// For AND logic, all filters must pass
327+
return group.filters.every((filter) =>
328+
this.evaluateFilter(filter, record)
329+
);
330+
}
331+
});
332+
};
333+
}
334+
335+
// Helper function to evaluate a single filter
336+
private evaluateFilter(filter: any, record: any): boolean {
337+
if (!filter.field || !filter.value) return true; // Skip incomplete filters
338+
339+
const fieldValue = record[filter.field];
340+
const filterValue = filter.value;
341+
const filterValue2 = filter.value2;
342+
343+
// Handle date filtering
344+
if (filter.type === "date" || filter.type === "date-range") {
345+
const fieldDate = new Date(fieldValue);
346+
const valueDate = new Date(filterValue);
347+
348+
if (isNaN(fieldDate.getTime()) || isNaN(valueDate.getTime())) {
349+
return true; // Skip invalid dates
350+
}
351+
352+
switch (filter.operator) {
353+
case "==":
354+
return fieldDate.toDateString() === valueDate.toDateString();
355+
case "!=":
356+
return fieldDate.toDateString() !== valueDate.toDateString();
357+
case ">=":
358+
return fieldDate >= valueDate;
359+
case "<=":
360+
return fieldDate <= valueDate;
361+
case "between":
362+
if (filterValue2) {
363+
const valueDate2 = new Date(filterValue2);
364+
if (!isNaN(valueDate2.getTime())) {
365+
return fieldDate >= valueDate && fieldDate <= valueDate2;
366+
}
367+
}
368+
return true; // Skip incomplete between filter
369+
default:
370+
return true;
371+
}
372+
}
373+
374+
// Handle text/numeric filtering
375+
switch (filter.operator) {
376+
case "==":
377+
return String(fieldValue) === filterValue;
378+
case "!=":
379+
return String(fieldValue) !== filterValue;
380+
case ">":
381+
return Number(fieldValue) > Number(filterValue);
382+
case "<":
383+
return Number(fieldValue) < Number(filterValue);
384+
case ">=":
385+
return Number(fieldValue) >= Number(filterValue);
386+
case "<=":
387+
return Number(fieldValue) <= Number(filterValue);
388+
case "contains":
389+
return String(fieldValue)
390+
.toLowerCase()
391+
.includes(filterValue.toLowerCase());
392+
case "!contains":
393+
return !String(fieldValue)
394+
.toLowerCase()
395+
.includes(filterValue.toLowerCase());
396+
default:
397+
return true;
398+
}
399+
}
239400
}

vite-app/src/components/Dashboard.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,15 @@ const Dashboard = observer(({ onRefresh }: DashboardProps) => {
136136
<Button onClick={collapseAll} size="sm" variant="secondary">
137137
Collapse All
138138
</Button>
139+
{state.tableFilterConfig.length > 0 && (
140+
<Button
141+
onClick={() => state.resetTableFilterConfig()}
142+
size="sm"
143+
variant="secondary"
144+
>
145+
Reset Filters
146+
</Button>
147+
)}
139148
</div>
140149
)}
141150
</div>

vite-app/src/components/EvaluationTable.tsx

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { state } from "../App";
33
import { EvaluationRow } from "./EvaluationRow";
44
import Button from "./Button";
55
import Select from "./Select";
6+
import FilterSelector from "./FilterSelector";
67
import {
78
TableHeader,
89
TableHead,
@@ -13,7 +14,9 @@ const TableBody = observer(
1314
({ currentPage, pageSize }: { currentPage: number; pageSize: number }) => {
1415
const startIndex = (currentPage - 1) * pageSize;
1516
const endIndex = startIndex + pageSize;
16-
const paginatedData = state.sortedDataset.slice(startIndex, endIndex);
17+
// Use filtered original data for pagination
18+
const filteredData = state.filteredOriginalDataset;
19+
const paginatedData = filteredData.slice(startIndex, endIndex);
1720

1821
return (
1922
<TableBodyBase>
@@ -31,10 +34,10 @@ const TableBody = observer(
3134

3235
// Dedicated component for rendering the list - following MobX best practices
3336
export const EvaluationTable = observer(() => {
34-
const totalRows = state.sortedDataset.length;
35-
const totalPages = state.totalPages;
36-
const startRow = state.startRow;
37-
const endRow = state.endRow;
37+
const totalRows = state.filteredOriginalDataset.length;
38+
const totalPages = Math.ceil(totalRows / state.pageSize);
39+
const startRow = (state.currentPage - 1) * state.pageSize + 1;
40+
const endRow = Math.min(state.currentPage * state.pageSize, totalRows);
3841

3942
const handlePageChange = (page: number) => {
4043
state.setCurrentPage(Math.max(1, Math.min(page, totalPages)));
@@ -44,8 +47,53 @@ export const EvaluationTable = observer(() => {
4447
state.setPageSize(newPageSize);
4548
};
4649

50+
const handleFiltersChange = (filters: any[]) => {
51+
state.updateTableFilterConfig(filters);
52+
};
53+
54+
const resetFilters = () => {
55+
state.resetTableFilterConfig();
56+
};
57+
4758
return (
4859
<div className="bg-white border border-gray-200">
60+
{/* Filter Controls */}
61+
<div className="px-3 py-3 border-b border-gray-200 bg-white">
62+
<div className="flex items-center justify-between mb-3">
63+
<div className="flex items-center gap-4">
64+
<h3 className="text-sm font-medium text-gray-700">Table Filters</h3>
65+
<div className="text-xs text-gray-600">
66+
{state.tableFilterConfig.length > 0 ? (
67+
<>
68+
Showing {totalRows} of {state.sortedDataset.length} rows
69+
{totalRows !== state.sortedDataset.length && (
70+
<span className="text-blue-600 ml-1">(filtered)</span>
71+
)}
72+
</>
73+
) : (
74+
`Showing all ${state.sortedDataset.length} rows`
75+
)}
76+
</div>
77+
</div>
78+
<Button
79+
onClick={resetFilters}
80+
size="sm"
81+
variant="secondary"
82+
disabled={state.tableFilterConfig.length === 0}
83+
>
84+
Reset Filters
85+
</Button>
86+
</div>
87+
<div className="border border-gray-200 rounded-lg p-4 bg-white">
88+
<FilterSelector
89+
filters={state.tableFilterConfig}
90+
onFiltersChange={handleFiltersChange}
91+
availableKeys={state.flattenedDatasetKeys}
92+
title=""
93+
/>
94+
</div>
95+
</div>
96+
4997
{/* Pagination Controls - Fixed outside scrollable area */}
5098
<div className="px-3 py-2 border-b border-gray-200 bg-gray-50 flex items-center justify-between">
5199
<div className="flex items-center gap-4">

0 commit comments

Comments
 (0)