diff --git a/js/src/components/query/QueryForm.tsx b/js/src/components/query/QueryForm.tsx index 1f13f5e0..1c51592f 100644 --- a/js/src/components/query/QueryForm.tsx +++ b/js/src/components/query/QueryForm.tsx @@ -1,4 +1,5 @@ import { useLineageGraphContext } from "@/lib/hooks/LineageGraphContext"; +import { DropdownValuesInput } from "@/utils/DropdownValuesInput"; import { InfoIcon } from "@chakra-ui/icons"; import { Flex, @@ -7,17 +8,7 @@ import { FormLabel, Tooltip, } from "@chakra-ui/react"; -import { - AutoComplete, - AutoCompleteCreatable, - AutoCompleteInput, - AutoCompleteItem, - AutoCompleteList, - AutoCompleteTag, - Item, - ItemTag, -} from "@choc-ui/chakra-autocomplete"; -import { useMemo } from "react"; +import { useMemo, useRef, useState } from "react"; interface QueryFormProps extends FlexProps { defaultPrimaryKeys: string[] | undefined; @@ -30,13 +21,14 @@ export const QueryForm = ({ ...prob }: QueryFormProps) => { const { lineageGraph } = useLineageGraphContext(); + const labelInfo = + "Provide a primary key to perform query diff in data warehouse and only return changed rows."; - const columns = useMemo(() => { + const availableColumns = useMemo(() => { if (!lineageGraph) { return []; } - - const columnSet = new Set(); + const columnSet = new Set(); for (const modelName in lineageGraph.nodes) { const model = lineageGraph.nodes[modelName]; const baseColumns = model.data.base?.columns; @@ -53,57 +45,24 @@ export const QueryForm = ({ return Array.from(columnSet).sort(); }, [lineageGraph]); - const labelInfo = - "Provide a primary key to perform query diff in data warehouse and only return changed rows."; - return ( - - Primary key{" "} + + Diff with Primary Key(s) (suggested){" "} - { - return optionValue.startsWith(query); - }} - onChange={(vals: string[]) => onPrimaryKeysChange(vals)} - defaultValues={ - defaultPrimaryKeys !== undefined && defaultPrimaryKeys.length !== 0 - ? defaultPrimaryKeys - : undefined - } - > - - {({ tags }: { tags: ItemTag[] }) => - tags.map((tag, tid) => ( - - )) - } - - - {columns.map((column, cid) => ( - - {column} - - ))} - - {({ value }) => Add '{value}' to List} - - - + ); diff --git a/js/src/components/query/QueryPage.tsx b/js/src/components/query/QueryPage.tsx index 5d8b31ac..5fc8a2b9 100644 --- a/js/src/components/query/QueryPage.tsx +++ b/js/src/components/query/QueryPage.tsx @@ -122,63 +122,61 @@ export const QueryPage = () => { }); return ( - - - + + + + + + - - - - {isCustomQueries ? ( - runQuery("query")} - onRunBase={() => runQuery("query_base")} - onRunDiff={() => runQuery("query_diff")} - /> - ) : ( - runQuery("query")} - onRunDiff={() => runQuery("query_diff")} - /> - )} - + Run Diff + - - + + + {isCustomQueries ? ( + runQuery("query")} + onRunBase={() => runQuery("query_base")} + onRunDiff={() => runQuery("query_diff")} + /> + ) : ( + runQuery("query")} + onRunDiff={() => runQuery("query_diff")} + /> + )} + + + /* */ ); }; diff --git a/js/src/components/query/SqlEditor.tsx b/js/src/components/query/SqlEditor.tsx index 257e14fd..7bd24e51 100644 --- a/js/src/components/query/SqlEditor.tsx +++ b/js/src/components/query/SqlEditor.tsx @@ -1,6 +1,19 @@ -import { Flex, Text, Stack, Badge, Spacer, IconButton } from "@chakra-ui/react"; +import { + Flex, + Text, + Stack, + Badge, + Spacer, + IconButton, + Button, + Icon, + Box, +} from "@chakra-ui/react"; import { EditorProps, DiffEditor, Editor } from "@monaco-editor/react"; import { on } from "events"; +import { BiLogoXing } from "react-icons/bi"; +import { FaPlay } from "react-icons/fa6"; +import { RiPlayMiniFill } from "react-icons/ri"; import { VscDebugStart } from "react-icons/vsc"; interface SqlEditorProps { @@ -39,26 +52,27 @@ const SqlEditor: React.FC = ({ <> {(label || onRun || onRunBase) && ( {label ? label.toUpperCase() : ""} {(onRun || onRunBase) && ( - } + )} )} @@ -120,23 +134,29 @@ export const DualSqlEditor: React.FC = ({ }: SqlEditorProps) => { return ( <> - - + + - + diff --git a/js/src/utils/DropdownValuesInput.tsx b/js/src/utils/DropdownValuesInput.tsx new file mode 100644 index 00000000..7a67fe2d --- /dev/null +++ b/js/src/utils/DropdownValuesInput.tsx @@ -0,0 +1,184 @@ +import React, { useState, useRef } from "react"; +import { + Button, + Input, + InputGroup, + InputProps, + InputRightElement, + Menu, + MenuButton, + MenuDivider, + MenuGroup, + MenuItem, + MenuList, + Portal, + Tag, + TagCloseButton, + TagLabel, + Wrap, + WrapItem, +} from "@chakra-ui/react"; + +export interface DropdownValuesInputProps extends InputProps { + unitName: string; + suggestionList?: string[]; + defaultValues?: string[]; + onValuesChange: (values: string[]) => void; +} + +export const DropdownValuesInput = (props: DropdownValuesInputProps) => { + const { defaultValues, suggestionList, onValuesChange } = props; + const [values, setValues] = useState(defaultValues || []); + const [filter, setFilter] = useState(""); + const [isTyping, setIsTyping] = useState(false); + const inputRef = useRef(null); + + const showNumberOfValuesSelected = (tags: string[]) => { + if (tags.length > 1) { + return `${tags.length} ${props.unitName}s selected`; + } else if (tags.length === 1) { + return `${tags.length} ${props.unitName} selected`; + } + return ""; + }; + + const handleSelect = (value: string) => { + if (!values.includes(value)) { + setFilter(""); + setValues([...values, value]); + onValuesChange([...values, value]); + } + }; + + const handleClear = () => { + setFilter(""); + setValues([]); + onValuesChange([]); + }; + + return ( + + + + + + + + {/* Input Filter & Show Tags */} + + + {values.map((value, cid) => ( + + + {value} + { + setValues(values.filter((v) => v !== value)); + onValuesChange(values.filter((v) => v !== value)); + }} + /> + + + ))} + + { + setFilter(e.target.value); + setIsTyping(true); + }} + onKeyDown={(e) => { + const newText = e.currentTarget.value + .trim() + .replace(",", ""); + switch (e.key) { + case ",": + case "Enter": + handleSelect(newText); + setFilter(""); + break; + case "Backspace": + if ( + e.currentTarget.value === "" && + values.length > 0 + ) { + setValues(values.slice(0, -1)); + onValuesChange(values.slice(0, -1)); + } + break; + default: + break; + } + }} + onBlur={() => { + if (inputRef && isTyping) inputRef?.current?.focus(); + }} + /> + + + + + {/* Suggestion List */} + + {filter !== "" && !suggestionList?.includes(filter) && ( + { + handleSelect(filter); + setIsTyping(false); + }} + > + Add '{filter}' to the list + + )} + {suggestionList + ?.filter((value) => filter === "" || value.includes(filter)) + .filter((value) => !values.includes(value)) + ?.map((value, cid) => ( + handleSelect(value)} + > + {value} + + ))} + + + + + + + + + ); +};