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

This commit is contained in:
2026-11-12 09:59:00 -08:00
parent 39ee717a6c
commit 34a2501e5e
5 changed files with 577 additions and 102 deletions
@@ -0,0 +1,272 @@
// CHAKRA COMPONENTS
import {
Button,
Stack,
Flex,
Field,
NativeSelect,
Input,
HoverCard,
Text,
NumberInput,
Box,
useSlotRecipe,
Combobox,
Portal,
useFilter,
useListCollection
} from "@chakra-ui/react";
// ICONS
import {
Plus,
CircleMinus,
Files,
ClipboardPaste,
Trash
} from 'lucide-react';
// OTHER
import { useState } from 'react';
import useDataFilterStore from '../../store/useDataFilterStore.jsx';
// ============================================================================
// 1. MAIN QUERY BUILDER (The Engine Wrapper)
// ============================================================================
export default function MyQueryBuilder({ url, setUrl, allColumns, getColumns, pullTable, isLoading, isReadyRef }) {
// STYLING
const dataExplorerRecipe = useSlotRecipe({ key: "dataExplorer" });
const dataExplorerStyle = dataExplorerRecipe();
// THE SELECT DROPDOWN MENU
const { contains } = useFilter({ sensitivity: "base" })
const { collection, filter } = useListCollection({
initialItems: tables,
filter: contains,
})
// STATE MANAGEMENT
const [value, setValue] = useState([]);
const addNewGroup = useDataFilterStore(state => state.addNewGroup);
const filterGroups = useDataFilterStore(state => state.filterGroups);
console.log("filter groups", filterGroups);
return (
<Flex direction="column" gap={2} w="100%" bg="transparent" css={dataExplorerStyle.query_builder}>
{/* Top Controls: Source & Execute */}
<Flex gap={2} align="flex-end">
<Combobox.Root
collection={collection}
onInputValueChange={(e) => 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"
>
<Combobox.Label>Select Table</Combobox.Label>
<Combobox.Control>
<Combobox.Input placeholder="Type to search" />
<Combobox.IndicatorGroup>
<Combobox.ClearTrigger />
<Combobox.Trigger />
</Combobox.IndicatorGroup>
</Combobox.Control>
<Portal>
<Combobox.Positioner>
<Combobox.Content>
<Combobox.Empty>No items found</Combobox.Empty>
{collection.items.map((item) => (
<Combobox.Item item={item} key={item.value}>
{item.label}
<Combobox.ItemIndicator />
</Combobox.Item>
))}
</Combobox.Content>
</Combobox.Positioner>
</Portal>
</Combobox.Root>
<Button
loading={isLoading}
onClick={() => {
isReadyRef.current = true;
pullTable();
}}
>
Execute Search
</Button>
</Flex>
{/* Filter Builder Engine */}
{url.source && allColumns?.types && (
<Stack w="100%" gap="2">
<Text fontSize="sm" fontWeight="bold" color="text">Filters</Text>
<Box w="100%" maxH="400px" overflow="auto">
<Stack w="100%" gap="2" pr="4">
{filterGroups.map((group) => (
<FilterGroup key={group.id} groupID={group.id} filters={group.filters} columns={allColumns.types} />
))}
<Button onClick={() => addNewGroup()} variant="outline" borderColor="widgetBadgeBg" color="text" _hover={{ bg: "widgetBadgeBg" }}>
New Filter Group <Plus />
</Button>
</Stack>
</Box>
</Stack>
)}
</Flex>
);
}
// 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 (
<Stack bg="widgetItemBg" borderWidth="1px" borderColor="widgetBadgeBg" borderRadius="lg" p="3" spacing="3">
<Flex justify="space-between" align="center">
<Input placeholder="Group Name (Optional)" maxW="200px" size="sm" variant="flushed" color="text" />
<Button onClick={() => removeGroup(groupID)} variant="ghost" size="sm" color="red.400">
<Trash />
</Button>
</Flex>
<Stack spacing="2">
{filters?.map((filter) => (
<FilterRow key={filter.id} groupID={groupID} filterID={filter.id} filter={filter} columns={columns} setUrl={setUrl} copiedFilter={copiedFilter} />
))}
<Button onClick={() => addFilter(groupID)} variant="outline" size="sm" color="text" borderColor="widgetBadgeBg" _hover={{ bg: "widgetBadgeBg" }}>
Add Filter <Plus />
</Button>
</Stack>
</Stack>
);
};
// ============================================================================
// 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 (
<Flex gap="2" align="center">
{/* Remove button */}
<HoverCard.Root positioning={{ placement: "top" }}>
<HoverCard.Trigger asChild>
<Button onClick={() => removeFilter(groupID, filterID)} variant="ghost" size="sm" color="red.400">
<CircleMinus />
</Button>
</HoverCard.Trigger>
<HoverCard.Positioner>
<HoverCard.Content p="2" bg="white"><Text fontSize="sm">Remove Filter</Text></HoverCard.Content>
</HoverCard.Positioner>
</HoverCard.Root>
{/* Copy Button */}
<HoverCard.Root positioning={{ placement: "top" }}>
<HoverCard.Trigger asChild>
<Button onClick={() => copyFilter(filter)} variant="ghost" size="sm" color="text">
<Files />
</Button>
</HoverCard.Trigger>
<HoverCard.Positioner>
<HoverCard.Content p="2" bg="white"><Text fontSize="sm">Copy Filter</Text></HoverCard.Content>
</HoverCard.Positioner>
</HoverCard.Root>
{/* paste button */}
<HoverCard.Root positioning={{ placement: "top" }}>
<HoverCard.Trigger asChild>
<Button
onClick={() => pasteFilter(groupID)}
variant="ghost"
size="sm"
color="text"
>
<ClipboardPaste />
</Button>
</HoverCard.Trigger>
<HoverCard.Positioner>
<HoverCard.Content p="2" bg="white"><Text fontSize="sm">Paste Filter</Text></HoverCard.Content>
</HoverCard.Positioner>
</HoverCard.Root>
{/* Column selector */}
<Field.Root w={filter?.type === "DateTimeField" ? "30%" : "30%"}>
<NativeSelect.Root>
<NativeSelect.Field
bg="widgetItemBg"
color="text"
value={filter.col ?? ""}
onChange={(e) => updateFilterColumn(groupID, filterID, e.target.value, columns[e.target.value], null, null)} // e.target.value is the new column
>
<option value="" disabled hidden>Select Column</option>
{Object.keys(columns).map((colName) => (
<option key={colName} value={colName}>{colName}</option>
))}
</NativeSelect.Field>
<NativeSelect.Indicator />
</NativeSelect.Root>
</Field.Root>
{/* Operator selector or start date for range */}
<Field.Root w={filter?.type === "DateTimeField" ? "20%" : "20%"}>
{filter?.type === "DateTimeField" ? (
<Input type="date" bg="widgetItemBg" color="text" value={filter.value?.start || ""} onChange={(e) => updateFilterValue(groupID, filterID, e.target.value, "s")} />
) : (
<NativeSelect.Root>
<NativeSelect.Field bg="widgetItemBg" color="text" value={filter.op ?? ""} onChange={(e) => updateFilterOp(groupID, filterID, e.target.value)}>
<option value="" disabled hidden>Operator</option>
{filter?.type && types[filter?.type].map((op) => <option key={op} value={op}>{op}</option>)} {/* maps over the 'filter.type' corresponding ops from the types dictionary */}
</NativeSelect.Field>
<NativeSelect.Indicator />
</NativeSelect.Root>
)}
</Field.Root>
{/* Value input or end date for range */}
<Field.Root w={filter.type === "DateTimeField" ? "20%" : "50%"}>
{filter?.type === "DateTimeField" ? (
<Input type="date" bg="widgetItemBg" color="text" value={filter.value?.end || ""} onChange={(e) => updateFilterValue(groupID, filterID, e.target.value, "e")} />
) : filter?.type === "IntegerField" || filter?.type === "AutoField" ? (
<NumberInput.Root value={filter.value ?? ""} onValueChange={(e) => updateFilterValue(groupID, filterID, e.value)}>
<NumberInput.Input placeholder="Value" bg="widgetItemBg" color="text" />
</NumberInput.Root>
) : (
<Input placeholder="Value" bg="widgetItemBg" color="text" value={filter.value ?? ""} onChange={(e) => updateFilterValue(groupID, filterID, e.target.value)} />
)}
</Field.Root>
</Flex>
);
};