273 lines
10 KiB
React
273 lines
10 KiB
React
// 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>
|
|
);
|
|
};
|