From 34a2501e5edeee6169879ce7ba0eeeb65e432e08 Mon Sep 17 00:00:00 2001 From: vkuchenik Date: Thu, 12 Nov 2026 09:59:00 -0800 Subject: [PATCH] refactor: now using MyQueryBuilder (temp name) in Data Explorer pg as it implements zustand state management to aid the transition from retriggering an api call on every new filter added --- .../src/pages/data_explorer/DataExplorer.jsx | 122 ++++---- client/src/pages/data_explorer/DataTable.jsx | 4 +- .../pages/data_explorer/MyQueryBuilder.jsx | 272 ++++++++++++++++++ .../src/pages/data_explorer/QueryBuilder.jsx | 133 ++++++--- client/src/store/useDataFilterStore.jsx | 148 ++++++++++ 5 files changed, 577 insertions(+), 102 deletions(-) create mode 100644 client/src/pages/data_explorer/MyQueryBuilder.jsx create mode 100644 client/src/store/useDataFilterStore.jsx 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) => ( + + + {/* Filter Builder Engine */} + {url.source && allColumns?.types && ( + + Filters + + + + {filterGroups.map((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 ( + + + + + + + + {filters?.map((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 */} + + + + + + Remove Filter + + + + {/* Copy Button */} + + + + + + Copy Filter + + + + {/* paste button */} + + + + + + Paste Filter + + + + {/* Column selector */} + + + updateFilterColumn(groupID, filterID, e.target.value, columns[e.target.value], null, null)} // e.target.value is the new column + > + + {Object.keys(columns).map((colName) => ( + + ))} + + + + + + {/* Operator selector or start date for range */} + + {filter?.type === "DateTimeField" ? ( + updateFilterValue(groupID, filterID, e.target.value, "s")} /> + ) : ( + + updateFilterOp(groupID, filterID, e.target.value)}> + + {filter?.type && types[filter?.type].map((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); - }} - > - - - {/**/} - - - - + + 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} + + + ))} + + + + + {/**/}