From 25873a6aee7255389abf7955a183cc621ef0bae7 Mon Sep 17 00:00:00 2001 From: andreashimin Date: Mon, 14 Apr 2025 13:38:19 +0800 Subject: [PATCH 1/9] chore(wren-ui): sync data source type management --- CONTRIBUTING.md | 3 +- .../pages/setup/ConnectDataSource.tsx | 2 +- wren-ui/src/components/pages/setup/utils.tsx | 93 ++++--------------- wren-ui/src/utils/dataSourceType.ts | 92 ++++++++++++++++++ wren-ui/src/utils/enum/dataSources.ts | 2 +- 5 files changed, 113 insertions(+), 79 deletions(-) create mode 100644 wren-ui/src/utils/dataSourceType.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a032e96ab..6642e5ad2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -148,7 +148,8 @@ If you prefer to learn by example, you can refer to this Trino [issue](https://g - Implement the data source form template in this file 3. Set up the data source template: - - Navigate to `wren-ui/src/components/pages/setup/utils` > `DATA_SOURCE_FORM` + - Navigate to `wren-ui/src/utils/dataSourceType.ts` + - Add new data source image, name, properties - Update the necessary files to include the new data source template settings 4. Update the data source list: diff --git a/wren-ui/src/components/pages/setup/ConnectDataSource.tsx b/wren-ui/src/components/pages/setup/ConnectDataSource.tsx index 27d554185a..a95d73c48c 100644 --- a/wren-ui/src/components/pages/setup/ConnectDataSource.tsx +++ b/wren-ui/src/components/pages/setup/ConnectDataSource.tsx @@ -89,7 +89,7 @@ export default function ConnectDataSource(props: Props) { { - return Object.keys(DATA_SOURCE_OPTIONS).map((key) => ({ - ...DATA_SOURCE_OPTIONS[key], - value: key, - })) as ButtonOption[]; + return Object.values(DATA_SOURCE_OPTIONS) as ButtonOption[]; }; export const getDataSource = (dataSource: DATA_SOURCES) => { - const defaultDataSource = merge( - DATA_SOURCE_OPTIONS[DATA_SOURCES.BIG_QUERY], - DATA_SOURCE_FORM[DATA_SOURCES.BIG_QUERY], - ); - return ( - { - [DATA_SOURCES.BIG_QUERY]: defaultDataSource, - [DATA_SOURCES.DUCKDB]: merge( - DATA_SOURCE_OPTIONS[DATA_SOURCES.DUCKDB], - DATA_SOURCE_FORM[DATA_SOURCES.DUCKDB], - ), - [DATA_SOURCES.PG_SQL]: merge( - DATA_SOURCE_OPTIONS[DATA_SOURCES.PG_SQL], - DATA_SOURCE_FORM[DATA_SOURCES.PG_SQL], - ), - [DATA_SOURCES.MYSQL]: merge( - DATA_SOURCE_OPTIONS[DATA_SOURCES.MYSQL], - DATA_SOURCE_FORM[DATA_SOURCES.MYSQL], - ), - [DATA_SOURCES.MSSQL]: merge( - DATA_SOURCE_OPTIONS[DATA_SOURCES.MSSQL], - DATA_SOURCE_FORM[DATA_SOURCES.MSSQL], - ), - [DATA_SOURCES.CLICK_HOUSE]: merge( - DATA_SOURCE_OPTIONS[DATA_SOURCES.CLICK_HOUSE], - DATA_SOURCE_FORM[DATA_SOURCES.CLICK_HOUSE], - ), - [DATA_SOURCES.TRINO]: merge( - DATA_SOURCE_OPTIONS[DATA_SOURCES.TRINO], - DATA_SOURCE_FORM[DATA_SOURCES.TRINO], - ), - [DATA_SOURCES.SNOWFLAKE]: merge( - DATA_SOURCE_OPTIONS[DATA_SOURCES.SNOWFLAKE], - DATA_SOURCE_FORM[DATA_SOURCES.SNOWFLAKE], - ), - }[dataSource] || defaultDataSource + return merge( + DATA_SOURCE_OPTIONS[dataSource], + getDataSourceFormComponent(dataSource), ); }; diff --git a/wren-ui/src/utils/dataSourceType.ts b/wren-ui/src/utils/dataSourceType.ts new file mode 100644 index 0000000000..d36f2e1b2c --- /dev/null +++ b/wren-ui/src/utils/dataSourceType.ts @@ -0,0 +1,92 @@ +import { DATA_SOURCES } from '@/utils/enum'; +import BigQueryProperties from '@/components/pages/setup/dataSources/BigQueryProperties'; +import DuckDBProperties from '@/components/pages/setup/dataSources/DuckDBProperties'; +import MySQLProperties from '@/components/pages/setup/dataSources/MySQLProperties'; +import PostgreSQLProperties from '@/components/pages/setup/dataSources/PostgreSQLProperties'; +import SQLServerProperties from '@/components/pages/setup/dataSources/SQLServerProperties'; +import ClickHouseProperties from '@/components/pages/setup/dataSources/ClickHouseProperties'; +import TrinoProperties from '@/components/pages/setup/dataSources/TrinoProperties'; +import SnowflakeProperties from '@/components/pages/setup/dataSources/SnowflakeProperties'; + +export const getDataSourceImage = (dataSource: DATA_SOURCES | string) => { + switch (dataSource) { + case DATA_SOURCES.BIG_QUERY: + return '/images/dataSource/bigQuery.svg'; + case DATA_SOURCES.POSTGRES: + return '/images/dataSource/postgreSql.svg'; + case DATA_SOURCES.MYSQL: + return '/images/dataSource/mysql.svg'; + case DATA_SOURCES.MSSQL: + return '/images/dataSource/sqlserver.svg'; + case DATA_SOURCES.CLICK_HOUSE: + return '/images/dataSource/clickhouse.svg'; + case DATA_SOURCES.DUCKDB: + return '/images/dataSource/duckdb.svg'; + case DATA_SOURCES.TRINO: + return '/images/dataSource/trino.svg'; + case DATA_SOURCES.SNOWFLAKE: + return '/images/dataSource/snowflake.svg'; + default: + return null; + } +}; + +export const getDataSourceName = (dataSource: DATA_SOURCES | string) => { + switch (dataSource) { + case DATA_SOURCES.BIG_QUERY: + return 'BigQuery'; + case DATA_SOURCES.POSTGRES: + return 'PostgreSQL'; + case DATA_SOURCES.MYSQL: + return 'MySQL'; + case DATA_SOURCES.MSSQL: + return 'SQL Server'; + case DATA_SOURCES.CLICK_HOUSE: + return 'ClickHouse'; + case DATA_SOURCES.DUCKDB: + return 'DuckDB'; + case DATA_SOURCES.TRINO: + return 'Trino'; + case DATA_SOURCES.SNOWFLAKE: + return 'Snowflake'; + default: + return ''; + } +}; + +export const getDataSourceProperties = (dataSource: DATA_SOURCES | string) => { + switch (dataSource) { + case DATA_SOURCES.BIG_QUERY: + return BigQueryProperties; + case DATA_SOURCES.POSTGRES: + return PostgreSQLProperties; + case DATA_SOURCES.MYSQL: + return MySQLProperties; + case DATA_SOURCES.MSSQL: + return SQLServerProperties; + case DATA_SOURCES.CLICK_HOUSE: + return ClickHouseProperties; + case DATA_SOURCES.DUCKDB: + return DuckDBProperties; + case DATA_SOURCES.TRINO: + return TrinoProperties; + case DATA_SOURCES.SNOWFLAKE: + return SnowflakeProperties; + default: + return null; + } +}; + +export const getDataSourceConfig = (dataSource: DATA_SOURCES | string) => { + return { + label: getDataSourceName(dataSource), + logo: getDataSourceImage(dataSource), + value: DATA_SOURCES[dataSource], + }; +}; + +export const getDataSourceFormComponent = ( + dataSource: DATA_SOURCES | string, +) => { + return { component: getDataSourceProperties(dataSource) || (() => null) }; +}; diff --git a/wren-ui/src/utils/enum/dataSources.ts b/wren-ui/src/utils/enum/dataSources.ts index d5b395d406..f7822341b2 100644 --- a/wren-ui/src/utils/enum/dataSources.ts +++ b/wren-ui/src/utils/enum/dataSources.ts @@ -1,7 +1,7 @@ export enum DATA_SOURCES { BIG_QUERY = 'BIG_QUERY', DUCKDB = 'DUCKDB', - PG_SQL = 'POSTGRES', + POSTGRES = 'POSTGRES', MYSQL = 'MYSQL', MSSQL = 'MSSQL', CLICK_HOUSE = 'CLICK_HOUSE', From 6aa1bec99cfc189555cbb6cdad293e9cd26b792b Mon Sep 17 00:00:00 2001 From: andreashimin Date: Tue, 15 Apr 2025 15:51:37 +0800 Subject: [PATCH 2/9] feat(wren-ui): add autocomplete hook with convertor function. replace mentions --- .../src/components/editor/MarkdownEditor.tsx | 2 +- .../modals/AdjustReasoningStepsModal.tsx | 8 +- wren-ui/src/hooks/useAutoComplete.tsx | 90 +++++++++++++++++++ wren-ui/src/hooks/useMentions.tsx | 62 ------------- 4 files changed, 97 insertions(+), 65 deletions(-) create mode 100644 wren-ui/src/hooks/useAutoComplete.tsx delete mode 100644 wren-ui/src/hooks/useMentions.tsx diff --git a/wren-ui/src/components/editor/MarkdownEditor.tsx b/wren-ui/src/components/editor/MarkdownEditor.tsx index 8e6ee4e0c4..f5f42b5de6 100644 --- a/wren-ui/src/components/editor/MarkdownEditor.tsx +++ b/wren-ui/src/components/editor/MarkdownEditor.tsx @@ -5,7 +5,7 @@ import { useState, useContext, useRef } from 'react'; import ReadOutlined from '@ant-design/icons/ReadOutlined'; import EditOutlined from '@ant-design/icons/EditOutlined'; import { nextTick } from '@/utils/time'; -import { Mention } from '@/hooks/useMentions'; +import { Mention } from '@/hooks/useAutoComplete'; import { FormItemInputContext } from 'antd/lib/form/context'; import MarkdownBlock from './MarkdownBlock'; diff --git a/wren-ui/src/components/modals/AdjustReasoningStepsModal.tsx b/wren-ui/src/components/modals/AdjustReasoningStepsModal.tsx index 9f2fc65884..fea6d5a8ef 100644 --- a/wren-ui/src/components/modals/AdjustReasoningStepsModal.tsx +++ b/wren-ui/src/components/modals/AdjustReasoningStepsModal.tsx @@ -4,7 +4,7 @@ import styled from 'styled-components'; import { Form, Modal, Select, Tag } from 'antd'; import QuestionCircleOutlined from '@ant-design/icons/QuestionCircleOutlined'; import { ERROR_TEXTS } from '@/utils/error'; -import useMentions from '@/hooks/useMentions'; +import useAutoComplete, { convertMention } from '@/hooks/useAutoComplete'; import { ModalAction } from '@/hooks/useModalAction'; import MarkdownEditor from '@/components/editor/MarkdownEditor'; import { useListModelsQuery } from '@/apollo/client/graphql/model.generated'; @@ -36,7 +36,11 @@ export default function AdjustReasoningStepsModal(props: Props) { const { visible, defaultValue, loading, onSubmit, onClose } = props; const [form] = Form.useForm(); - const { mentions } = useMentions({ includeColumns: true, skip: !visible }); + const mentions = useAutoComplete({ + convertor: convertMention, + includeColumns: true, + skip: !visible, + }); const listModelsResult = useListModelsQuery({ skip: !visible }); const modelNameMap = keyBy( listModelsResult.data?.listModels, diff --git a/wren-ui/src/hooks/useAutoComplete.tsx b/wren-ui/src/hooks/useAutoComplete.tsx new file mode 100644 index 0000000000..89e8a0ae9e --- /dev/null +++ b/wren-ui/src/hooks/useAutoComplete.tsx @@ -0,0 +1,90 @@ +import { useMemo } from 'react'; +import { capitalize } from 'lodash'; +import { useDiagramQuery } from '@/apollo/client/graphql/diagram.generated'; +import { getNodeTypeIcon } from '@/utils/nodeType'; +import { + DiagramModel, + DiagramView, + DiagramModelField, + DiagramViewField, +} from '@/apollo/client/graphql/__types__'; + +type Model = DiagramModel | DiagramView; +type Field = DiagramModelField | DiagramViewField; +type Convertor = (item: (Model | Field) & { parent?: Model }) => T; + +interface Props { + skip?: boolean; + includeColumns?: boolean; + convertor?: Convertor; +} + +const getDocHTML = (item: (Model | Field) & { parent?: Model }) => { + return [ + '
', + `${item.referenceName}`, + item.description + ? `
${item.description}
` + : null, + '
', + ] + .filter(Boolean) + .join(''); +}; + +// For mention usage +export const convertMention = (item: (Model | Field) & { parent?: Model }) => { + return { + id: `${item.id}-${item.referenceName}`, + label: item.displayName, + value: item.referenceName, + nodeType: capitalize(item.nodeType), + meta: item.parent ? `${item.displayName}.${item.displayName}` : undefined, + icon: getNodeTypeIcon( + { nodeType: item.nodeType, type: (item as Field).type }, + { className: 'gray-8 mr-2' }, + ), + }; +}; + +// For ace completer usage +export const convertCompleter = ( + item: (Model | Field) & { parent?: Model }, +) => { + return { + caption: item.parent + ? `${item.parent.displayName}.${item.displayName}` + : item.displayName, + value: item.referenceName, + meta: item.nodeType.toLowerCase(), + // Higher score for models, views + score: item.parent ? 1 : 10, + docHTML: getDocHTML(item), + }; +}; + +export type Mention = ReturnType; +export type Completer = ReturnType; + +export default function useAutoComplete(props: Props) { + const { includeColumns, skip } = props; + const { data } = useDiagramQuery({ skip }); + + // Defined convertor + const convertor = (props.convertor || convertCompleter) as Convertor; + + return useMemo(() => { + const models = data?.diagram.models || []; + const views = data?.diagram.views || []; + + return [...models, ...views].reduce((result, item) => { + result.push(convertor(item)); + if (includeColumns) { + item.fields.forEach((field) => { + result.push(convertor({ ...field, parent: item })); + }); + } + return result; + }, [] as T[]); + }, [data?.diagram, includeColumns]); +} diff --git a/wren-ui/src/hooks/useMentions.tsx b/wren-ui/src/hooks/useMentions.tsx deleted file mode 100644 index bb47e27abe..0000000000 --- a/wren-ui/src/hooks/useMentions.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useMemo } from 'react'; -import { capitalize } from 'lodash'; -import { useDiagramQuery } from '@/apollo/client/graphql/diagram.generated'; -import { getNodeTypeIcon } from '@/utils/nodeType'; -import { - DiagramModel, - DiagramView, - DiagramModelField, - DiagramViewField, -} from '@/apollo/client/graphql/__types__'; - -type Model = DiagramModel | DiagramView; -type Field = DiagramModelField | DiagramViewField; - -interface Props { - skip?: boolean; - includeColumns?: boolean; -} - -const convertMention = (item: (Model | Field) & { meta?: string }) => { - return { - id: `${item.id}-${item.referenceName}`, - label: item.displayName, - value: item.referenceName, - nodeType: capitalize(item.nodeType), - meta: item.meta, - icon: getNodeTypeIcon( - { nodeType: item.nodeType, type: (item as Field).type }, - { className: 'gray-8 mr-2' }, - ), - }; -}; - -export type Mention = ReturnType; - -export default function useMentions(props: Props) { - const { includeColumns, skip } = props; - const { data } = useDiagramQuery({ skip }); - - // handle mentions data - const mentions = useMemo(() => { - const models = data?.diagram.models || []; - const views = data?.diagram.views || []; - - return [...models, ...views].reduce((result, item) => { - result.push(convertMention(item)); - if (includeColumns) { - item.fields.forEach((field) => { - result.push( - convertMention({ - ...field, - meta: `${item.displayName}.${field.displayName}`, - }), - ); - }); - } - return result; - }, [] as Mention[]); - }, [data?.diagram, includeColumns]); - - return { mentions }; -} From 1841d283882aeda78b7dab809b858c077aaf0458 Mon Sep 17 00:00:00 2001 From: andreashimin Date: Tue, 15 Apr 2025 15:52:25 +0800 Subject: [PATCH 3/9] feat(wren-ui): provide autocomplete to SQL editor --- wren-ui/src/components/editor/SQLEditor.tsx | 116 +++++++++++++++++--- 1 file changed, 98 insertions(+), 18 deletions(-) diff --git a/wren-ui/src/components/editor/SQLEditor.tsx b/wren-ui/src/components/editor/SQLEditor.tsx index 9cb629b0a7..97a2bb77f5 100644 --- a/wren-ui/src/components/editor/SQLEditor.tsx +++ b/wren-ui/src/components/editor/SQLEditor.tsx @@ -1,37 +1,117 @@ -import { useState } from 'react'; +import clsx from 'clsx'; +import { useState, useContext, useRef, useEffect } from 'react'; +import styled from 'styled-components'; import AceEditor from '@/components/editor/AceEditor'; +import { FormItemInputContext } from 'antd/lib/form/context'; +import useAutoComplete from '@/hooks/useAutoComplete'; + +const Wrapper = styled.div` + transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + + &:hover { + border-color: var(--geekblue-5) !important; + } + + &.adm-markdown-editor-error { + border-color: var(--red-5) !important; + + .adm-markdown-editor-length { + color: var(--red-5) !important; + } + } + &:not(.adm-markdown-editor-error).adm-markdown-editor-focused { + border-color: var(--geekblue-5) !important; + box-shadow: 0 0 0 2px rgba(47, 84, 235, 0.2); + } + + &.adm-markdown-editor-focused.adm-markdown-editor-error { + borer-color: var(--red-4) !important; + box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2); + } +`; + +const Toolbar = styled.div` + color: var(--gray-8); + background-color: var(--gray-3); + border-bottom: 1px solid var(--gray-5); + height: 32px; + padding: 4px 8px; + border-radius: 4px 4px 0px 0px; +`; interface Props { value?: string; onChange?: (value: string) => void; autoFocus?: boolean; + autoComplete?: boolean; + toolbar?: React.ReactNode; } +const getLangTools = () => { + const { ace } = window as any; + return ace ? ace.require('ace/ext/language_tools') : null; +}; + export default function SQLEditor(props: Props) { - const { value, onChange, autoFocus } = props; + const { value, onChange, autoFocus, autoComplete, toolbar } = props; + const $wrapper = useRef(null); + const [focused, setFocused] = useState(false); + + const formItemContext = useContext(FormItemInputContext); + const { status } = formItemContext; + + const completers = useAutoComplete({ + includeColumns: true, + skip: !autoComplete, + }); + + useEffect(() => { + if (completers.length === 0) return; + + const langTools = getLangTools(); + const customCompleter = { + getCompletions: (_editor, _session, _pos, _prefix, callback) => { + callback(null, completers); + }, + }; + langTools?.addCompleter(customCompleter); + }, [autoComplete, completers]); const [sql, setSql] = useState(value || ''); const change = (sql) => { setSql(sql); - onChange && onChange(sql); + onChange?.(sql); }; return ( - + + {!!toolbar && {toolbar}} + setFocused(true)} + onBlur={() => setFocused(false)} + name="sql_editor" + editorProps={{ $blockScrolling: true }} + enableLiveAutocompletion + enableBasicAutocompletion + showPrintMargin={false} + focus={autoFocus} + /> + ); } From 8244132d97525360a4eefc06fb3b31c3ed08cd94 Mon Sep 17 00:00:00 2001 From: andreashimin Date: Tue, 15 Apr 2025 16:15:45 +0800 Subject: [PATCH 4/9] feat(wren-ui): add import data source sql modal --- wren-ui/src/components/editor/SQLEditor.tsx | 11 +- .../modals/ImportDataSourceSQLModal.tsx | 97 ++++++ .../modals/QuestionSQLPairModal.tsx | 300 +++++++++++------- wren-ui/src/utils/error/dictionary.ts | 5 + 4 files changed, 291 insertions(+), 122 deletions(-) create mode 100644 wren-ui/src/components/modals/ImportDataSourceSQLModal.tsx diff --git a/wren-ui/src/components/editor/SQLEditor.tsx b/wren-ui/src/components/editor/SQLEditor.tsx index 97a2bb77f5..7e5753449b 100644 --- a/wren-ui/src/components/editor/SQLEditor.tsx +++ b/wren-ui/src/components/editor/SQLEditor.tsx @@ -66,7 +66,7 @@ export default function SQLEditor(props: Props) { }); useEffect(() => { - if (completers.length === 0) return; + if (!autoComplete || completers.length === 0) return; const langTools = getLangTools(); const customCompleter = { @@ -75,6 +75,15 @@ export default function SQLEditor(props: Props) { }, }; langTools?.addCompleter(customCompleter); + + // clear custom completer + return () => { + langTools?.setCompleters([ + langTools.keyWordCompleter, + langTools.snippetCompleter, + langTools.textCompleter, + ]); + }; }, [autoComplete, completers]); const [sql, setSql] = useState(value || ''); diff --git a/wren-ui/src/components/modals/ImportDataSourceSQLModal.tsx b/wren-ui/src/components/modals/ImportDataSourceSQLModal.tsx new file mode 100644 index 0000000000..6aeb1bc86b --- /dev/null +++ b/wren-ui/src/components/modals/ImportDataSourceSQLModal.tsx @@ -0,0 +1,97 @@ +import { Modal, Form, Alert } from 'antd'; +import { ModalAction } from '@/hooks/useModalAction'; +import { getDataSourceImage, getDataSourceName } from '@/utils/dataSourceType'; +import { DATA_SOURCES } from '@/utils/enum'; +import SQLEditor from '@/components/editor/SQLEditor'; +import { ERROR_TEXTS } from '@/utils/error'; +import ErrorCollapse from '../ErrorCollapse'; + +type Props = ModalAction<{ dataSource: DATA_SOURCES }> & { + loading?: boolean; +}; + +const Toolbar = (props) => { + const { dataSource } = props; + if (!dataSource) return null; + const logo = getDataSourceImage(dataSource); + const name = getDataSourceName(dataSource); + return ( + <> + + logo + {name} + + + ); +}; + +export default function ImportDataSourceSQLModal(props: Props) { + const { visible, defaultValue, loading, onSubmit, onClose } = props; + const name = getDataSourceName(defaultValue?.dataSource) || 'data source'; + + const error = { + shortMessage: 'Error', + message: 'Error', + }; + + const [form] = Form.useForm(); + + const reset = () => { + form.resetFields(); + }; + + const submit = async () => { + form + .validateFields() + .then(async (values) => { + onSubmit(values); + onClose(); + }) + .catch(console.error); + }; + + return ( + reset()} + > +
+ + } + autoFocus + /> + +
+ {!!error && ( + } + /> + )} +
+ ); +} diff --git a/wren-ui/src/components/modals/QuestionSQLPairModal.tsx b/wren-ui/src/components/modals/QuestionSQLPairModal.tsx index 9fd5e5f884..35d99ef986 100644 --- a/wren-ui/src/components/modals/QuestionSQLPairModal.tsx +++ b/wren-ui/src/components/modals/QuestionSQLPairModal.tsx @@ -1,18 +1,23 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { Alert, Button, Form, Input, Modal, Typography } from 'antd'; +import { Logo } from '@/components/Logo'; import InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined'; +import SelectOutlined from '@ant-design/icons/SelectOutlined'; import { ERROR_TEXTS } from '@/utils/error'; import { FORM_MODE } from '@/utils/enum'; -import { ModalAction } from '@/hooks/useModalAction'; +import { getDataSourceName } from '@/utils/dataSourceType'; +import useModalAction, { ModalAction } from '@/hooks/useModalAction'; import SQLEditor from '@/components/editor/SQLEditor'; import { parseGraphQLError } from '@/utils/errorHandler'; import { createSQLPairQuestionValidator } from '@/utils/validator'; import ErrorCollapse from '@/components/ErrorCollapse'; import PreviewData from '@/components/dataPreview/PreviewData'; +import ImportDataSourceSQLModal from '@/components/modals/ImportDataSourceSQLModal'; import { usePreviewSqlMutation } from '@/apollo/client/graphql/sql.generated'; import { useGenerateQuestionMutation } from '@/apollo/client/graphql/sql.generated'; import { SqlPair } from '@/apollo/client/graphql/__types__'; +import { useGetSettingsQuery } from '@/apollo/client/graphql/settings.generated'; type Props = ModalAction & { loading?: boolean; @@ -27,6 +32,23 @@ const StyledForm = styled(Form)` } `; +const Toolbar = (props: { dataSource: string; onClick: () => void }) => { + const { dataSource, onClick } = props; + const name = getDataSourceName(dataSource); + return ( +
+ + + Wren SQL + + +
+ ); +}; + export default function QuestionSQLPairModal(props: Props) { const { defaultValue, @@ -40,6 +62,17 @@ export default function QuestionSQLPairModal(props: Props) { // pass payload?.isCreateMode to prevent formMode from being set to Update when passing defaultValue, for the 'Add a SQL pair from an existing answer' scenario use. const isCreateMode = formMode === FORM_MODE.CREATE || payload?.isCreateMode; + const importDataSourceSQLModal = useModalAction(); + + const { data: settingsResult } = useGetSettingsQuery(); + const settings = settingsResult?.settings; + const dataSource = useMemo( + () => ({ + isOwned: !settings?.dataSource?.sampleDataset, + type: settings?.dataSource?.type, + }), + [settings?.dataSource], + ); const [form] = Form.useForm(); const [error, setError] = @@ -151,133 +184,158 @@ export default function QuestionSQLPairModal(props: Props) { const disabled = !sqlValue; return ( - handleReset()} - footer={ -
-
- - + handleReset()} + footer={ +
+
- The SQL statement used here follows Wren SQL, which is - based on ANSI SQL and optimized for Wren AI.{` `} - + - Learn more about the syntax. - - -
-
- - -
-
- } - > - - - Question -
- Let AI create a matching question for this SQL statement. - -
+ Learn more about the syntax. + +
+
+
+ +
- } - name="question" - required - rules={[ - { - validator: createSQLPairQuestionValidator( - ERROR_TEXTS.SQL_PAIR.QUESTION, - ), - }, - ]} - > - - - - - - -
- - Data preview (50 rows) - - - {showPreview && ( -
-
+ } + > + + + Question +
+ Let AI create a matching question for this SQL statement. + +
+
+ } + name="question" + required + rules={[ + { + validator: createSQLPairQuestionValidator( + ERROR_TEXTS.SQL_PAIR.QUESTION, + ), + }, + ]} + > + + + + + importDataSourceSQLModal.openModal({ + dataSource: dataSource.type, + }) + } + /> + ) + } + autoFocus + /> + + +
+ + Data preview (50 rows) + + + {showPreview && ( +
+ +
+ )} +
+ {!!error && ( + } + /> )} -
- {!!error && ( - } +
+ {dataSource.isOwned && ( + { + console.log(values); + }} /> )} - + ); } diff --git a/wren-ui/src/utils/error/dictionary.ts b/wren-ui/src/utils/error/dictionary.ts index 1c264e299b..383ff8f9d0 100644 --- a/wren-ui/src/utils/error/dictionary.ts +++ b/wren-ui/src/utils/error/dictionary.ts @@ -133,4 +133,9 @@ export const ERROR_TEXTS = { MAX_LENGTH: 'Reasoning steps must be 3000 characters or fewer.', }, }, + IMPORT_DATA_SOURCE_SQL: { + SQL: { + REQUIRED: 'Please input SQL statement.', + }, + }, }; From acbba5e5643a3dc48a4a5b4c3f0a2acfc00780eb Mon Sep 17 00:00:00 2001 From: andreashimin Date: Tue, 15 Apr 2025 17:13:10 +0800 Subject: [PATCH 5/9] feat(wren-ui): turn on autoComplete for adjustSQL, fixSQL, QuestionSQLPair --- wren-ui/src/components/modals/AdjustSQLModal.tsx | 2 +- wren-ui/src/components/modals/FixSQLModal.tsx | 2 +- wren-ui/src/components/modals/QuestionSQLPairModal.tsx | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/wren-ui/src/components/modals/AdjustSQLModal.tsx b/wren-ui/src/components/modals/AdjustSQLModal.tsx index 905152ac28..d0a8e77d1a 100644 --- a/wren-ui/src/components/modals/AdjustSQLModal.tsx +++ b/wren-ui/src/components/modals/AdjustSQLModal.tsx @@ -178,7 +178,7 @@ export default function AdjustSQLModal(props: Props) { }, ]} > - +
diff --git a/wren-ui/src/components/modals/FixSQLModal.tsx b/wren-ui/src/components/modals/FixSQLModal.tsx index c541880221..17a89a9579 100644 --- a/wren-ui/src/components/modals/FixSQLModal.tsx +++ b/wren-ui/src/components/modals/FixSQLModal.tsx @@ -98,7 +98,7 @@ export function FixSQLModal(props: Props) { }, ]} > - +
diff --git a/wren-ui/src/components/modals/QuestionSQLPairModal.tsx b/wren-ui/src/components/modals/QuestionSQLPairModal.tsx index 35d99ef986..acd63afc70 100644 --- a/wren-ui/src/components/modals/QuestionSQLPairModal.tsx +++ b/wren-ui/src/components/modals/QuestionSQLPairModal.tsx @@ -293,6 +293,7 @@ export default function QuestionSQLPairModal(props: Props) { /> ) } + autoComplete autoFocus /> From 1a6213d598aacd222472e9fb0a5f7009f7f7fda9 Mon Sep 17 00:00:00 2001 From: andreashimin Date: Tue, 15 Apr 2025 17:01:32 +0800 Subject: [PATCH 6/9] feat(wren-ui): generate model substitude API --- .../src/apollo/client/graphql/__types__.ts | 11 +++++ .../apollo/client/graphql/sql.generated.ts | 40 ++++++++++++++++++- wren-ui/src/apollo/client/graphql/sql.ts | 6 +++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/wren-ui/src/apollo/client/graphql/__types__.ts b/wren-ui/src/apollo/client/graphql/__types__.ts index 18454a756e..388dfa0aff 100644 --- a/wren-ui/src/apollo/client/graphql/__types__.ts +++ b/wren-ui/src/apollo/client/graphql/__types__.ts @@ -11,6 +11,7 @@ export type Scalars = { Boolean: boolean; Int: number; Float: number; + DialectSQL: any; JSON: any; }; @@ -551,6 +552,10 @@ export type ModelInfo = { sourceTableName: Scalars['String']; }; +export type ModelSubstituteInput = { + sql: Scalars['DialectSQL']; +}; + export type ModelSyncResponse = { __typename?: 'ModelSyncResponse'; status: SyncStatus; @@ -592,6 +597,7 @@ export type Mutation = { generateThreadResponseAnswer: ThreadResponse; generateThreadResponseBreakdown: ThreadResponse; generateThreadResponseChart: ThreadResponse; + modelSubstitute: Scalars['String']; previewBreakdownData: Scalars['JSON']; previewData: Scalars['JSON']; previewItemSQL: Scalars['JSON']; @@ -773,6 +779,11 @@ export type MutationGenerateThreadResponseChartArgs = { }; +export type MutationModelSubstituteArgs = { + data: ModelSubstituteInput; +}; + + export type MutationPreviewBreakdownDataArgs = { where: PreviewDataInput; }; diff --git a/wren-ui/src/apollo/client/graphql/sql.generated.ts b/wren-ui/src/apollo/client/graphql/sql.generated.ts index 24fe293d0b..4228cdb601 100644 --- a/wren-ui/src/apollo/client/graphql/sql.generated.ts +++ b/wren-ui/src/apollo/client/graphql/sql.generated.ts @@ -17,6 +17,13 @@ export type GenerateQuestionMutationVariables = Types.Exact<{ export type GenerateQuestionMutation = { __typename?: 'Mutation', generateQuestion: string }; +export type ModelSubstituteMutationVariables = Types.Exact<{ + data: Types.ModelSubstituteInput; +}>; + + +export type ModelSubstituteMutation = { __typename?: 'Mutation', modelSubstitute: string }; + export const PreviewSqlDocument = gql` mutation PreviewSQL($data: PreviewSQLDataInput!) { @@ -79,4 +86,35 @@ export function useGenerateQuestionMutation(baseOptions?: Apollo.MutationHookOpt } export type GenerateQuestionMutationHookResult = ReturnType; export type GenerateQuestionMutationResult = Apollo.MutationResult; -export type GenerateQuestionMutationOptions = Apollo.BaseMutationOptions; \ No newline at end of file +export type GenerateQuestionMutationOptions = Apollo.BaseMutationOptions; +export const ModelSubstituteDocument = gql` + mutation ModelSubstitute($data: ModelSubstituteInput!) { + modelSubstitute(data: $data) +} + `; +export type ModelSubstituteMutationFn = Apollo.MutationFunction; + +/** + * __useModelSubstituteMutation__ + * + * To run a mutation, you first call `useModelSubstituteMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useModelSubstituteMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [modelSubstituteMutation, { data, loading, error }] = useModelSubstituteMutation({ + * variables: { + * data: // value for 'data' + * }, + * }); + */ +export function useModelSubstituteMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(ModelSubstituteDocument, options); + } +export type ModelSubstituteMutationHookResult = ReturnType; +export type ModelSubstituteMutationResult = Apollo.MutationResult; +export type ModelSubstituteMutationOptions = Apollo.BaseMutationOptions; \ No newline at end of file diff --git a/wren-ui/src/apollo/client/graphql/sql.ts b/wren-ui/src/apollo/client/graphql/sql.ts index 7843e77eaf..e09219cabb 100644 --- a/wren-ui/src/apollo/client/graphql/sql.ts +++ b/wren-ui/src/apollo/client/graphql/sql.ts @@ -11,3 +11,9 @@ export const GENERATE_QUESTION = gql` generateQuestion(data: $data) } `; + +export const MODEL_SUBSTITUDE = gql` + mutation ModelSubstitute($data: ModelSubstituteInput!) { + modelSubstitute(data: $data) + } +`; From 17ff4e74cd34e8792e9283dce09518c955eafbda Mon Sep 17 00:00:00 2001 From: andreashimin Date: Tue, 15 Apr 2025 17:03:12 +0800 Subject: [PATCH 7/9] feat(wren-ui): integrate substitude dialect to SQL pair --- .../modals/ImportDataSourceSQLModal.tsx | 37 +++++++++++++++---- .../modals/QuestionSQLPairModal.tsx | 12 +++--- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/wren-ui/src/components/modals/ImportDataSourceSQLModal.tsx b/wren-ui/src/components/modals/ImportDataSourceSQLModal.tsx index 6aeb1bc86b..7a77eb726c 100644 --- a/wren-ui/src/components/modals/ImportDataSourceSQLModal.tsx +++ b/wren-ui/src/components/modals/ImportDataSourceSQLModal.tsx @@ -1,10 +1,14 @@ +import { useMemo } from 'react'; import { Modal, Form, Alert } from 'antd'; import { ModalAction } from '@/hooks/useModalAction'; import { getDataSourceImage, getDataSourceName } from '@/utils/dataSourceType'; import { DATA_SOURCES } from '@/utils/enum'; -import SQLEditor from '@/components/editor/SQLEditor'; import { ERROR_TEXTS } from '@/utils/error'; -import ErrorCollapse from '../ErrorCollapse'; +import { parseGraphQLError } from '@/utils/errorHandler'; +import SQLEditor from '@/components/editor/SQLEditor'; +import ErrorCollapse from '@/components/ErrorCollapse'; +import { useModelSubstituteMutation } from '@/apollo/client/graphql/sql.generated'; +import { DataSource, DataSourceName } from '@/apollo/client/graphql/__types__'; type Props = ModalAction<{ dataSource: DATA_SOURCES }> & { loading?: boolean; @@ -25,26 +29,45 @@ const Toolbar = (props) => { ); }; +export const isSupportSubstitute = (dataSource: DataSource) => { + // DuckDB not supported, sample dataset as well + return ( + !dataSource?.sampleDataset && dataSource?.type !== DataSourceName.DUCKDB + ); +}; + export default function ImportDataSourceSQLModal(props: Props) { const { visible, defaultValue, loading, onSubmit, onClose } = props; const name = getDataSourceName(defaultValue?.dataSource) || 'data source'; - const error = { - shortMessage: 'Error', - message: 'Error', - }; + const [substituteDialectSQL, modelSubstitudeResult] = + useModelSubstituteMutation(); + const error = useMemo( + () => + modelSubstitudeResult.error + ? { + ...parseGraphQLError(modelSubstitudeResult.error), + shortMessage: `Invalid ${name} SQL syntax`, + } + : null, + [modelSubstitudeResult.error], + ); const [form] = Form.useForm(); const reset = () => { form.resetFields(); + modelSubstitudeResult.reset(); }; const submit = async () => { form .validateFields() .then(async (values) => { - onSubmit(values); + const response = await substituteDialectSQL({ + variables: { data: { sql: values.sql } }, + }); + await onSubmit(response.data?.modelSubstitute); onClose(); }) .catch(console.error); diff --git a/wren-ui/src/components/modals/QuestionSQLPairModal.tsx b/wren-ui/src/components/modals/QuestionSQLPairModal.tsx index acd63afc70..cfc8d0a7a5 100644 --- a/wren-ui/src/components/modals/QuestionSQLPairModal.tsx +++ b/wren-ui/src/components/modals/QuestionSQLPairModal.tsx @@ -13,11 +13,13 @@ import { parseGraphQLError } from '@/utils/errorHandler'; import { createSQLPairQuestionValidator } from '@/utils/validator'; import ErrorCollapse from '@/components/ErrorCollapse'; import PreviewData from '@/components/dataPreview/PreviewData'; -import ImportDataSourceSQLModal from '@/components/modals/ImportDataSourceSQLModal'; +import ImportDataSourceSQLModal, { + isSupportSubstitute, +} from '@/components/modals/ImportDataSourceSQLModal'; import { usePreviewSqlMutation } from '@/apollo/client/graphql/sql.generated'; +import { useGetSettingsQuery } from '@/apollo/client/graphql/settings.generated'; import { useGenerateQuestionMutation } from '@/apollo/client/graphql/sql.generated'; import { SqlPair } from '@/apollo/client/graphql/__types__'; -import { useGetSettingsQuery } from '@/apollo/client/graphql/settings.generated'; type Props = ModalAction & { loading?: boolean; @@ -68,7 +70,7 @@ export default function QuestionSQLPairModal(props: Props) { const settings = settingsResult?.settings; const dataSource = useMemo( () => ({ - isOwned: !settings?.dataSource?.sampleDataset, + isSupportSubstitute: isSupportSubstitute(settings?.dataSource), type: settings?.dataSource?.type, }), [settings?.dataSource], @@ -282,7 +284,7 @@ export default function QuestionSQLPairModal(props: Props) { > @@ -328,7 +330,7 @@ export default function QuestionSQLPairModal(props: Props) { /> )} - {dataSource.isOwned && ( + {dataSource.isSupportSubstitute && ( Date: Tue, 15 Apr 2025 17:28:43 +0800 Subject: [PATCH 8/9] feat(wren-ui): integrate form set value. refine reset completer flow --- wren-ui/src/components/editor/SQLEditor.tsx | 22 +++++++++++-------- .../modals/ImportDataSourceSQLModal.tsx | 4 ++-- .../modals/QuestionSQLPairModal.tsx | 4 ++-- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/wren-ui/src/components/editor/SQLEditor.tsx b/wren-ui/src/components/editor/SQLEditor.tsx index 7e5753449b..4341ce4670 100644 --- a/wren-ui/src/components/editor/SQLEditor.tsx +++ b/wren-ui/src/components/editor/SQLEditor.tsx @@ -65,7 +65,18 @@ export default function SQLEditor(props: Props) { skip: !autoComplete, }); + const resetCompleters = () => { + // clear custom completer + const langTools = getLangTools(); + langTools?.setCompleters([ + langTools.keyWordCompleter, + langTools.snippetCompleter, + langTools.textCompleter, + ]); + }; + useEffect(() => { + resetCompleters(); if (!autoComplete || completers.length === 0) return; const langTools = getLangTools(); @@ -76,15 +87,8 @@ export default function SQLEditor(props: Props) { }; langTools?.addCompleter(customCompleter); - // clear custom completer - return () => { - langTools?.setCompleters([ - langTools.keyWordCompleter, - langTools.snippetCompleter, - langTools.textCompleter, - ]); - }; - }, [autoComplete, completers]); + return () => resetCompleters(); + }, [focused, autoComplete, completers]); const [sql, setSql] = useState(value || ''); diff --git a/wren-ui/src/components/modals/ImportDataSourceSQLModal.tsx b/wren-ui/src/components/modals/ImportDataSourceSQLModal.tsx index 7a77eb726c..bb1ef85015 100644 --- a/wren-ui/src/components/modals/ImportDataSourceSQLModal.tsx +++ b/wren-ui/src/components/modals/ImportDataSourceSQLModal.tsx @@ -65,7 +65,7 @@ export default function ImportDataSourceSQLModal(props: Props) { .validateFields() .then(async (values) => { const response = await substituteDialectSQL({ - variables: { data: { sql: values.sql } }, + variables: { data: { sql: values.dialectSql } }, }); await onSubmit(response.data?.modelSubstitute); onClose(); @@ -92,7 +92,7 @@ export default function ImportDataSourceSQLModal(props: Props) { >
{ - console.log(values); + onSubmit={async (convertedSql: string) => { + form.setFieldsValue({ sql: convertedSql }); }} /> )} From 80e33dd8fe859fac22072d218201f3bb5a6217c1 Mon Sep 17 00:00:00 2001 From: andreashimin Date: Wed, 16 Apr 2025 11:28:20 +0800 Subject: [PATCH 9/9] feat(wren-ui): add quote identifier condition for referenceName --- wren-ui/src/hooks/useAutoComplete.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/wren-ui/src/hooks/useAutoComplete.tsx b/wren-ui/src/hooks/useAutoComplete.tsx index 89e8a0ae9e..492e8e21bb 100644 --- a/wren-ui/src/hooks/useAutoComplete.tsx +++ b/wren-ui/src/hooks/useAutoComplete.tsx @@ -32,6 +32,10 @@ const getDocHTML = (item: (Model | Field) & { parent?: Model }) => { .join(''); }; +const shouldQuoteIdentifier = (word: string) => { + return /[^a-z0-9_]/.test(word) || /^\d/.test(word); +}; + // For mention usage export const convertMention = (item: (Model | Field) & { parent?: Model }) => { return { @@ -55,7 +59,9 @@ export const convertCompleter = ( caption: item.parent ? `${item.parent.displayName}.${item.displayName}` : item.displayName, - value: item.referenceName, + value: shouldQuoteIdentifier(item.referenceName) + ? `"${item.referenceName}"` + : item.referenceName, meta: item.nodeType.toLowerCase(), // Higher score for models, views score: item.parent ? 1 : 10,