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:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user