diff --git a/client/src/pages/data_explorer/DataExplorer.jsx b/client/src/pages/data_explorer/DataExplorer.jsx
index 8c70177..9193cda 100644
--- a/client/src/pages/data_explorer/DataExplorer.jsx
+++ b/client/src/pages/data_explorer/DataExplorer.jsx
@@ -1,9 +1,11 @@
import React, { useState, useRef, useEffect } from "react";
import { Box, Stack, Separator, Center, Spinner, Alert, Flex } from "@chakra-ui/react";
import { toaster } from "../../components/ui/toaster.jsx"; // Adjust path to your v3 toaster if needed
+import VdapApi from "../../hooks/useVdapApi.js";
// Import our newly consolidated engine and presentation components
import QueryBuilder from "./QueryBuilder.jsx";
+import MyQueryBuilder from "./MyQueryBuilder.jsx";
import DataTable from "./DataTable.jsx";
// import { exportToCSV } from "../utils/csvExport.js";
@@ -51,6 +53,7 @@ export default function DataExplorer() {
return params.toString();
};
+ // pulls list of column names and datatypes of said columns
const getColumns = async (endpoint) => {
setError(null);
isReadyRef.current = false; // does this need to be set to true at the end?
@@ -69,6 +72,8 @@ export default function DataExplorer() {
}
};
+
+ // pulls the data
const pullTable = async () => {
setIsLoading(true);
setError(null);
@@ -80,7 +85,6 @@ export default function DataExplorer() {
}
try {
- // 🚨 Relative pathing for your production API
const response = await fetch(`/api/v3/table_view?${buildUrlParams()}`);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
@@ -95,10 +99,11 @@ export default function DataExplorer() {
// Auto-fetch when pagination, columns, or sorting changes (but skip first render)
useEffect(() => {
- if (! isReadyRef.current) {
- isReadyRef.current = false; // why are we resetting the readyRef back to false since it already is false?
- return;
- }
+ if (! isReadyRef.current) return;
+ // {
+ // isReadyRef.current = false; // why are we resetting the readyRef back to false since it already is false?
+ // return;
+ // }
pullTable();
}, [url.page, url.columns, url.ordering]);
@@ -122,69 +127,62 @@ export default function DataExplorer() {
}
};
- // 5. Render Shell
return (
- // 🚨 Stripped hardcoded #FFFBFA background.
// This Flex container will perfectly fill your new Sidebar Route canvas.
-
-
+
+ {/* The Engine */}
+
- {/* The Engine */}
-
+
- {results && }
+ {isLoading && (
+
+
+
+ )}
- {/* Global Loading & Error States */}
- {isLoading && (
-
-
-
- )}
+ {error && (
+
+
+
+ Error Fetching Data
+ {error}
+
+
+ )}
- {error && (
-
-
-
- Error Fetching Data
- {error}
-
-
- )}
-
- {/* The Display (Table, Pagination, Download, Columns) */}
- {results && !isLoading && !error && (
-
- {results.results.length > 0 ? (
-
- ) : (
-
-
-
- No Results Found
- Try loosening your filter criteria.
-
-
- )}
-
- )}
-
-
+ {/* The Display (Table, Pagination, Download, Columns) */}
+ {results && (
+
+ {results.results.length > 0 ? (
+
+ ) : (
+
+
+
+ No Results Found
+ Try loosening your filter criteria.
+
+
+ )}
+
+ )}
);
};
\ No newline at end of file
diff --git a/client/src/pages/data_explorer/DataTable.jsx b/client/src/pages/data_explorer/DataTable.jsx
index 361a949..c1aa508 100644
--- a/client/src/pages/data_explorer/DataTable.jsx
+++ b/client/src/pages/data_explorer/DataTable.jsx
@@ -31,7 +31,7 @@ const ColumnSelector = ({ allColumns, url, setUrl, isLoading }) => {
-
+
Available Columns
{Object.keys(allColumns).map((colName) => (
@@ -161,7 +161,7 @@ const DataTable = React.memo(({ data, count, url, setUrl, allColumns, isLoading,
-
+
{columns.map((col) => (
state.addNewGroup);
+ const filterGroups = useDataFilterStore(state => state.filterGroups);
+
+ console.log("filter groups", filterGroups);
+
+ return (
+
+ {/* Top Controls: Source & Execute */}
+
+ filter(e.inputValue)}
+ value={value}
+ onValueChange={(e) => {
+ setValue(e.value);
+ const selectedView = e.value;
+ setUrl(prev => ({ ...prev, source: selectedView }));
+ // need to get all columns as default in param building
+ const endpoint = "/api/v3/view_dash_event_search_columns";
+ getColumns(endpoint);
+ }}
+ width="320px"
+ >
+ Select Table
+
+
+
+
+
+
+
+
+
+
+ No items found
+ {collection.items.map((item) => (
+
+ {item.label}
+
+
+ ))}
+
+
+
+
+
+ {
+ isReadyRef.current = true;
+ pullTable();
+ }}
+ >
+ Execute Search
+
+
+
+ {/* Filter Builder Engine */}
+ {url.source && allColumns?.types && (
+
+ Filters
+
+
+
+ {filterGroups.map((group) => (
+
+ ))}
+ addNewGroup()} variant="outline" borderColor="widgetBadgeBg" color="text" _hover={{ bg: "widgetBadgeBg" }}>
+ New Filter Group
+
+
+
+
+ )}
+
+ );
+}
+
+// LIST OF TABLES USER CAN EXPLORE
+const tables = [
+ { label: "Events Table", value: "ViewDashEventSearch" },
+];
+
+// ============================================================================
+// 2. FILTERS GROUPS (The AND logic groups)
+// ============================================================================
+function FilterGroup({ groupID, filters, columns }) {
+ const removeGroup = useDataFilterStore(state => state.removeGroup);
+ const addFilter = useDataFilterStore(state => state.addFilter);
+
+ console.log("in filter group. GroupID: ", groupID, "filters: ", filters);
+
+ return (
+
+
+
+ removeGroup(groupID)} variant="ghost" size="sm" color="red.400">
+
+
+
+
+
+ {filters?.map((filter) => (
+
+ ))}
+ addFilter(groupID)} variant="outline" size="sm" color="text" borderColor="widgetBadgeBg" _hover={{ bg: "widgetBadgeBg" }}>
+ Add Filter
+
+
+
+ );
+};
+
+// ============================================================================
+// 3. FILTER ROW (The individual criteria inputs)
+// ============================================================================
+function FilterRow({ groupID, filterID, filter, columns }) {
+ // STATE MANAGEMENT
+ const removeFilter = useDataFilterStore(state => state.removeFilter);
+ const updateFilterColumn = useDataFilterStore(state => state.updateFilterColumn);
+ const updateFilterOp = useDataFilterStore(state => state.updateFilterOps);
+ const updateFilterValue = useDataFilterStore(state => state.updateFilterValue);
+ const copyFilter = useDataFilterStore(state => state.copyFilter);
+ const pasteFilter = useDataFilterStore(state => state.pasteFilter);
+
+ // CONSTANTS
+ const types = {
+ AutoField: ["=", "<", ">", "<=", ">="],
+ IntegerField: ["=", "<", ">", "<=", ">="],
+ CharField: ["=", "contains"],
+ DateTimeField: ["range"],
+ };
+
+ return (
+
+ {/* Remove button */}
+
+
+ removeFilter(groupID, filterID)} variant="ghost" size="sm" color="red.400">
+
+
+
+
+ Remove Filter
+
+
+
+ {/* Copy Button */}
+
+
+ copyFilter(filter)} variant="ghost" size="sm" color="text">
+
+
+
+
+ Copy Filter
+
+
+
+ {/* paste button */}
+
+
+ pasteFilter(groupID)}
+ variant="ghost"
+ size="sm"
+ color="text"
+ >
+
+
+
+
+ Paste Filter
+
+
+
+ {/* Column selector */}
+
+
+ updateFilterColumn(groupID, filterID, e.target.value, columns[e.target.value], null, null)} // e.target.value is the new column
+ >
+ Select Column
+ {Object.keys(columns).map((colName) => (
+ {colName}
+ ))}
+
+
+
+
+
+ {/* Operator selector or start date for range */}
+
+ {filter?.type === "DateTimeField" ? (
+ updateFilterValue(groupID, filterID, e.target.value, "s")} />
+ ) : (
+
+ updateFilterOp(groupID, filterID, e.target.value)}>
+ Operator
+ {filter?.type && types[filter?.type].map((op) => {op} )} {/* maps over the 'filter.type' corresponding ops from the types dictionary */}
+
+
+
+ )}
+
+
+ {/* Value input or end date for range */}
+
+ {filter?.type === "DateTimeField" ? (
+ updateFilterValue(groupID, filterID, e.target.value, "e")} />
+ ) : filter?.type === "IntegerField" || filter?.type === "AutoField" ? (
+ updateFilterValue(groupID, filterID, e.value)}>
+
+
+ ) : (
+ updateFilterValue(groupID, filterID, e.target.value)} />
+ )}
+
+
+ );
+};
diff --git a/client/src/pages/data_explorer/QueryBuilder.jsx b/client/src/pages/data_explorer/QueryBuilder.jsx
index c35e4d9..77c253e 100644
--- a/client/src/pages/data_explorer/QueryBuilder.jsx
+++ b/client/src/pages/data_explorer/QueryBuilder.jsx
@@ -1,8 +1,24 @@
import React, { useRef } from "react";
-import { Button, Stack, Flex, Field, NativeSelect, Input, HoverCard, Text, NumberInput, Box } from "@chakra-ui/react";
+import {
+ Button,
+ Stack,
+ Flex,
+ Field,
+ NativeSelect,
+ Input,
+ HoverCard,
+ Text,
+ NumberInput,
+ Box,
+ useSlotRecipe,
+ Combobox,
+ Portal,
+ useFilter,
+ useListCollection
+} from "@chakra-ui/react";
import { Plus, CircleMinus, Files, ClipboardPaste, Trash } from 'lucide-react';
-// import { GrAdd, GrSubtractCircle } from "react-icons/gr";
-// import { FaCopy, FaPaste, FaTrash } from "react-icons/fa";
+import { useState } from 'react';
+import useDataFilterStore from '../../store/useDataFilterStore.jsx';
// ============================================================================
// 1. FILTER ROW (The individual criteria inputs)
@@ -80,7 +96,7 @@ const FilterRow = React.memo(({ filter, columns, setUrl, bundleId, copiedFilter
- Remove Filter
+ Remove Filter
@@ -92,7 +108,7 @@ const FilterRow = React.memo(({ filter, columns, setUrl, bundleId, copiedFilter
- Copy Filter
+ Copy Filter
@@ -124,7 +140,7 @@ const FilterRow = React.memo(({ filter, columns, setUrl, bundleId, copiedFilter
- Paste Filter
+ Paste Filter
@@ -207,6 +223,8 @@ const FilterBundle = ({ bundle, columns, url, setUrl, copiedFilter }) => {
}) : (b)
),
}));
+
+
};
return (
@@ -234,8 +252,25 @@ const FilterBundle = ({ bundle, columns, url, setUrl, copiedFilter }) => {
// 3. MAIN QUERY BUILDER (The Engine Wrapper)
// ============================================================================
export default function QueryBuilder({ url, setUrl, allColumns, getColumns, pullTable, isLoading, isReadyRef }) {
- const copiedFilter = useRef(null);
+ // STYLING
+ const dataExplorerRecipe = useSlotRecipe({ key: "dataExplorer" });
+ const dataExplorerStyle = dataExplorerRecipe();
+ // SELECT DROPDOWN MENU
+ const { contains } = useFilter({ sensitivity: "base" })
+
+ const { collection, filter } = useListCollection({
+ initialItems: tables,
+ filter: contains,
+ })
+
+ // STATE MANAGEMENT
+ const copiedFilter = useRef(null);
+ const [value, setValue] = useState([]);
+ const addNewGroup = useDataFilterStore(state => state.addNewGroup);
+ const filterGroups = useDataFilterStore(state => state.filterGroups);
+
+ // FUNCTIONS
const handleAddBundle = () => {
setUrl(prev => ({
...prev,
@@ -253,39 +288,56 @@ export default function QueryBuilder({ url, setUrl, allColumns, getColumns, pull
},
],
}));
+
+ // adding zustand state management as a test here
+ addNewGroup();
};
+ console.log("filter groups", filterGroups);
+
return (
-
+
{/* Top Controls: Source & Execute */}
-
-
- Data Source
-
- {
- const selectedView = e.target.value;
- setUrl(prev => ({ ...prev, source: selectedView }));
- // 🚨 FIXED: Now uses relative pathing for the production API
- const endpoint = "/api/v3/view_dash_event_search_columns";
- getColumns(endpoint);
- }}
- >
- Select View
- Main View
- {/*Test View */}
-
-
-
-
+
+ filter(e.inputValue)}
+ value={value}
+ onValueChange={(e) => {
+ setValue(e.value);
+ const selectedView = e.value;
+ setUrl(prev => ({ ...prev, source: selectedView }));
+ // need to get all columns as default in param building
+ const endpoint = "/api/v3/view_dash_event_search_columns";
+ getColumns(endpoint);
+ }}
+ width="320px"
+ >
+ Select Table
+
+
+
+
+
+
+
+
+
+
+ No items found
+ {collection.items.map((item) => (
+
+ {item.label}
+
+
+ ))}
+
+
+
+
+ {/**/}
{
isReadyRef.current = true;
@@ -298,11 +350,11 @@ export default function QueryBuilder({ url, setUrl, allColumns, getColumns, pull
{/* Filter Builder Engine */}
{url.source && allColumns?.types && (
-
- Filters
+
+ Filters
-
+
{url.bundles.map((bundle) => (
))}
@@ -315,4 +367,9 @@ export default function QueryBuilder({ url, setUrl, allColumns, getColumns, pull
)}
);
-}
\ No newline at end of file
+}
+
+// LIST OF TABLES USER CAN EXPLORE
+const tables = [
+ { label: "Events Table", value: "ViewDashEventSearch" },
+]
\ No newline at end of file
diff --git a/client/src/store/useDataFilterStore.jsx b/client/src/store/useDataFilterStore.jsx
new file mode 100644
index 0000000..1400aa5
--- /dev/null
+++ b/client/src/store/useDataFilterStore.jsx
@@ -0,0 +1,148 @@
+import { create } from 'zustand';
+
+const useDataFilterStore = create((set, get) => ({
+ filterGroups: [],
+ copiedFilter: {},
+
+ addNewGroup: () => {
+ const newGroup = {
+ id: crypto.randomUUID(),
+ filters: []
+ }
+
+ set((state) => ({filterGroups: [...state.filterGroups, newGroup]}));
+ },
+
+ removeGroup: (groupID) => set((state) => ({
+ filterGroups: state.filterGroups.filter((group) => group.id !== groupID),
+ })),
+
+ addFilter: (groupID) => set((state) => {
+ return {
+ filterGroups: state.filterGroups.map(filterGroup => {
+ return filterGroup.id === groupID ?
+ {
+ ...filterGroup,
+ filters: [
+ ...filterGroup.filters,
+ {
+ id: crypto.randomUUID(),
+ col: null,
+ type: null,
+ op: null,
+ value: null
+ }
+ ]
+ }
+ : filterGroup
+ })
+ }
+ }),
+
+ updateFilterColumn: (groupID, filterID, col, type, op, value) => set((state) => {
+ return {
+ filterGroups: state.filterGroups.map((group) => {
+ return groupID === group.id ?
+ {
+ ...group,
+ filters: group.filters.map(filter => {
+ return filterID === filter.id ?
+ {
+ ...filter,
+ col: col,
+ type: type,
+ op: op,
+ value: value
+ }
+ : filter
+ })
+ }
+ : group
+ }),
+ }
+ }),
+
+ updateFilterOps: (groupID, filterID, op) => set((state) => {
+ return {
+ filterGroups: state.filterGroups.map((group) => {
+ return groupID === group.id ?
+ {
+ ...group,
+ filters: group.filters.map(filter => {
+ return filterID === filter.id ?
+ {
+ ...filter,
+ op: op,
+ }
+ : filter
+ })
+ }
+ : group
+ }),
+ }
+ }),
+
+ updateFilterValue: (groupID, filterID, value, dateType = undefined) => set((state) => {
+ return {
+ filterGroups: state.filterGroups.map((group) => {
+ return groupID === group.id ?
+ {
+ ...group,
+ filters: group.filters.map(filter => {
+ return filterID === filter.id ?
+ {
+ ...filter,
+ value: (dateType === "s") ? {...filter.value, start: value} : (dateType === "e") ? {...filter.value, end: value } : value
+ }
+ : filter
+ })
+ }
+ : group
+ }),
+ }
+ }),
+
+ removeFilter: (groupID, filterID) => set((state) => ({
+ filterGroups: state.filterGroups.map((group) => {
+ return group.id === groupID ?
+ {
+ ...group,
+ filters: group.filters.filter((f) => f.id !== filterID)
+ }
+ : group
+ }),
+ })),
+
+ copyFilter: (filter) => set((state) => ({
+ copiedFilter: state.copiedFilter = {
+ col: filter.col,
+ type: filter.type,
+ op: filter.op,
+ value: filter.value
+ }
+ })),
+
+ pasteFilter: (groupID) => set((state) => {
+ state.filterGroups.map((group) => {
+ return groupID === group.id ?
+ {
+ ...group,
+ filters: [
+ ...group.filters,
+ {
+ id: crypto.randomUUID(),
+ col: state.copiedFilter.col,
+ type: state.copiedFilter.type,
+ op: state.copiedFilter.op,
+ value: state.copiedFilter.value,
+ }
+ ]
+ }
+ : group
+ })
+ })
+
+}));
+
+export default useDataFilterStore;
+