Files
Web-Frontend/client/src/pages/data_explorer/MyQueryBuilder.jsx
T

273 lines
10 KiB
React
Raw Normal View History

// 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} />
))}
<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, filterID)}
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>
);
};