[feature] Init sql editor  (#11)

* [feature] init sql editor
diff --git a/frontend/package.json b/frontend/package.json
index 3d04969..b0b96d5 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -19,10 +19,12 @@
     "@ant-design/pro-list": "^1.17.7",
     "@ant-design/pro-table": "^2.56.7",
     "@babel/core": "^7.12.10",
+    "@monaco-editor/react": "^4.3.1",
     "ahooks": "^3.1.10",
     "antd": "^4.16.11",
     "axios": "^0.24.0",
     "babel-loader": "^8.2.2",
+    "bignumber.js": "^9.0.2",
     "buffer": "^6.0.3",
     "cross-env": "^7.0.3",
     "crypto-browserify": "^3.12.0",
@@ -32,6 +34,7 @@
     "highlight.js": "^11.0.1",
     "i18next": "^20.4.0",
     "i18next-browser-languagedetector": "^6.1.2",
+    "immer": "^9.0.12",
     "js-cookie": "^3.0.1",
     "lodash-es": "^4.17.21",
     "password-generator": "^2.3.2",
@@ -45,6 +48,7 @@
     "react-i18next": "^11.11.4",
     "react-router-cache-route": "^1.12.1",
     "react-router-dom": "^5.2.0",
+    "react-spring": "^9.4.4",
     "recoil": "^0.5.0",
     "stream-browserify": "^3.0.0",
     "sweetalert2": "^11.1.9",
@@ -52,6 +56,7 @@
     "swr": "^0.5.6",
     "ttag": "1.7.15",
     "typescript": "^4.3.5",
+    "use-immer": "^0.6.0",
     "webpack": "^5.11.0",
     "webpack-merge": "^5.7.3"
   },
@@ -93,7 +98,7 @@
     "postcss-flexbugs-fixes": "^5.0.2",
     "postcss-loader": "^6.1.1",
     "postcss-normalize": "^10.0.0",
-    "postcss-preset-env": "^6.7.0",
+    "postcss-preset-env": "^7.4.3",
     "prettier": "^2.3.2",
     "react-router-config": "^5.1.1",
     "style-loader": "^2.0.0",
diff --git a/frontend/src/routes/container.tsx b/frontend/src/routes/container.tsx
index 4849d8a..6792a7d 100644
--- a/frontend/src/routes/container.tsx
+++ b/frontend/src/routes/container.tsx
@@ -37,6 +37,7 @@
 import { Cluster } from './cluster/cluster';
 import { UserSetting } from './user-setting';
 import { useUserInfo } from '@src/hooks/use-userinfo.hooks';
+import VisualQuery from './visual-query/visual-query';
 export function Container(props: any) {
     const [userInfo] = useUserInfo();
     const history = useHistory();
@@ -62,6 +63,7 @@
                                     <Route path="/query" component={Query} />
                                     <Route path="/details/:queryId" component={QueryDetails} />
                                     <Route path="/user-setting" component={UserSetting} />
+                                    <Route path="/visual-query" component={VisualQuery} />
                                     <Redirect to="/cluster" />
                                 </Switch>
                             </div>
diff --git a/frontend/src/routes/visual-query/components/loading-layout/index.tsx b/frontend/src/routes/visual-query/components/loading-layout/index.tsx
new file mode 100644
index 0000000..2cea538
--- /dev/null
+++ b/frontend/src/routes/visual-query/components/loading-layout/index.tsx
@@ -0,0 +1,28 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { PropsWithChildren } from 'react';
+import ScreenLoading from './screen-loading';
+
+interface LoadingLayoutProps {
+    loading: boolean;
+}
+
+export default function LoadingLayout(props: PropsWithChildren<LoadingLayoutProps>) {
+    const { loading, children } = props;
+    return <>{loading ? <ScreenLoading /> : children}</>;
+}
diff --git a/frontend/src/routes/visual-query/components/loading-layout/screen-loading.tsx b/frontend/src/routes/visual-query/components/loading-layout/screen-loading.tsx
new file mode 100644
index 0000000..573e2c6
--- /dev/null
+++ b/frontend/src/routes/visual-query/components/loading-layout/screen-loading.tsx
@@ -0,0 +1,28 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { Spin } from 'antd';
+import styles from './style.module.less';
+
+export default function ScreenLoading() {
+    return (
+        <div className={styles.loadingWrapper}>
+            <Spin />
+        </div>
+    );
+}
diff --git a/frontend/src/routes/visual-query/components/loading-layout/style.module.less b/frontend/src/routes/visual-query/components/loading-layout/style.module.less
new file mode 100644
index 0000000..e90e951
--- /dev/null
+++ b/frontend/src/routes/visual-query/components/loading-layout/style.module.less
@@ -0,0 +1,27 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.loadingWrapper {
+    position: fixed;
+    display: flex;
+    top: 65px;
+    left: 0;
+    width: 100%;
+    height: 80%;
+    align-items: center;
+    justify-content: center;
+}
diff --git a/frontend/src/routes/visual-query/components/save-dashboard-modal/index.module.less b/frontend/src/routes/visual-query/components/save-dashboard-modal/index.module.less
new file mode 100644
index 0000000..39a2ce6
--- /dev/null
+++ b/frontend/src/routes/visual-query/components/save-dashboard-modal/index.module.less
@@ -0,0 +1,20 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+.cascaderInput {
+    width: 100%;
+}
diff --git a/frontend/src/routes/visual-query/components/save-dashboard-modal/index.tsx b/frontend/src/routes/visual-query/components/save-dashboard-modal/index.tsx
new file mode 100644
index 0000000..000826f
--- /dev/null
+++ b/frontend/src/routes/visual-query/components/save-dashboard-modal/index.tsx
@@ -0,0 +1,75 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useEffect } from 'react';
+import { useImmer } from 'use-immer';
+import { Modal, Form, Cascader } from 'antd';
+import styles from './index.module.less';
+import LoadingLayout from '@src/components/loading-layout';
+import { CascaderItem } from '../../types';
+import { processCollections } from '../../utils';
+import { fetchChildOfCollectionAPI } from '../../visual-query.api';
+
+interface SaveDashboardModalProps {
+    visible: boolean;
+    loading?: boolean;
+    collections: CascaderItem[];
+    setSaveDashboardModalVisible: (v: boolean) => void;
+}
+
+export default function SaveDashboardModal(props: SaveDashboardModalProps) {
+    const { visible, loading = false, collections: defaultCollections, setSaveDashboardModalVisible } = props;
+    const [collections, setCollections] = useImmer([...processCollections(defaultCollections)]);
+
+    useEffect(() => {
+        setCollections([...processCollections(defaultCollections)]);
+    }, [defaultCollections]);
+
+    const [form] = Form.useForm();
+
+    const handleCancel = () => {
+        setSaveDashboardModalVisible(false);
+    };
+
+    const loadData = (path: any[]) => {
+        const currentCollection = path[path.length - 1];
+        currentCollection.loading = true;
+        fetchChildOfCollectionAPI({
+            collectionId: currentCollection.id,
+            model: 'dashboard',
+        }).then(res => {
+            // Todo
+        });
+    };
+
+    return (
+        <Modal visible={visible} onCancel={handleCancel} title="把查询添加到仪表盘">
+            <LoadingLayout loading={loading} wrapperStyle={{ textAlign: 'center' }}>
+                <Form form={form} autoComplete="off" labelCol={{ span: 5 }} wrapperCol={{ span: 17 }}>
+                    <Form.Item
+                        name="dashboard"
+                        label="仪表盘"
+                        required
+                        rules={[{ required: true, message: '请选择仪表盘' }]}
+                    >
+                        <Cascader options={collections} fieldNames={{ value: 'id' }} loadData={loadData} />
+                    </Form.Item>
+                </Form>
+            </LoadingLayout>
+        </Modal>
+    );
+}
diff --git a/frontend/src/routes/visual-query/components/save-query-modal/index.tsx b/frontend/src/routes/visual-query/components/save-query-modal/index.tsx
new file mode 100644
index 0000000..d091b63
--- /dev/null
+++ b/frontend/src/routes/visual-query/components/save-query-modal/index.tsx
@@ -0,0 +1,155 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext } from 'react';
+import { Modal, Form, Input, Cascader, message } from 'antd';
+import LoadingLayout from '@src/components/loading-layout';
+import { CascaderItem } from '../../types';
+import { DataContext } from '../../context';
+import { useAsync } from '@src/hooks/use-async';
+import { addCardAPI } from '../../visual-query.api';
+
+interface SaveQueryModalProps {
+    visible: boolean;
+    loading?: boolean;
+    collections: CascaderItem[];
+    setSaveQueryModalVisible: (v: boolean) => void;
+    setSaveDashboardModalVisible: (v: boolean) => void;
+}
+
+interface FormProps {
+    name: string;
+    description?: string;
+    collection: number[];
+}
+
+export default function SaveQueryModal(props: SaveQueryModalProps) {
+    const { visible, setSaveQueryModalVisible, loading = false, collections, setSaveDashboardModalVisible } = props;
+    const { columns, data } = useContext(DataContext);
+
+    const [form] = Form.useForm<FormProps>();
+
+    const { loading: confirmLoading, run: runAddCard } = useAsync();
+
+    const handleCancel = () => {
+        setSaveQueryModalVisible(false);
+    };
+
+    const filter = (inputValue: string, path: any[]) => {
+        return path.some(option => option.name.toLowerCase().indexOf(inputValue.toLowerCase()) > -1);
+    };
+
+    const displayRender = (label: string[], path?: any[]) => {
+        return path?.map(item => item.name).join(' / ') || '';
+    };
+
+    const handleOk = async () => {
+        try {
+            const { collection, name, description } = await form.validateFields();
+            const resultData = data!;
+            const visualization_settings = {
+                columns,
+            }
+            try {
+                await runAddCard(
+                    addCardAPI({
+                        collection_id: collection[collection.length - 1],
+                        dataset_query: {
+                            database: resultData.database_id,
+                            native: {
+                                query: resultData.data.native_form.query,
+                            },
+                            type: 'NATIVE',
+                        },
+                        description: description || null,
+                        display: 'table',
+                        name,
+                        result_metadata: {
+                            columns: resultData.data.cols,
+                        },
+                        original_definition: {
+                            database: resultData.database_id,
+                            native: {
+                                query: resultData.data.native_form.query,
+                            },
+                            type: 'NATIVE',
+                        },
+                        visualization_settings,
+                    }),
+                );
+                message.success('保存查询成功');
+                setSaveQueryModalVisible(false);
+                Modal.confirm({
+                    title: '是否将此查询保存到dashboard',
+                    onOk: () => {
+                        setSaveDashboardModalVisible(true);
+                    },
+                });
+            } catch (e) {
+                message.error('保存查询失败');
+            }
+        } catch (e) {
+            console.log(e);
+        }
+    };
+
+    return (
+        <Modal
+            title="保存查询"
+            visible={visible}
+            onCancel={handleCancel}
+            onOk={handleOk}
+            confirmLoading={confirmLoading}
+            destroyOnClose
+        >
+            <LoadingLayout loading={loading} wrapperStyle={{ textAlign: 'center' }}>
+                <Form form={form} autoComplete="off" labelCol={{ span: 5 }} wrapperCol={{ span: 17 }}>
+                    <Form.Item
+                        name="name"
+                        label="名字"
+                        rules={[{ required: true, message: '请为该查询命名' }]}
+                        required
+                    >
+                        <Input placeholder="请为该查询命名" />
+                    </Form.Item>
+                    <Form.Item name="description" label="描述">
+                        <Input.TextArea />
+                    </Form.Item>
+                    <Form.Item
+                        name="collection"
+                        label="所属文件夹"
+                        required
+                        rules={[{ required: true, message: '请选择所属文件夹' }]}
+                    >
+                        <Cascader
+                            options={collections}
+                            fieldNames={{ value: 'id' }}
+                            showSearch={{
+                                filter,
+                                render: (inputValue: string, path: any[]) => {
+                                    return path.map(item => item.name).join(' / ');
+                                },
+                            }}
+                            displayRender={displayRender}
+                            changeOnSelect
+                        />
+                    </Form.Item>
+                </Form>
+            </LoadingLayout>
+        </Modal>
+    );
+}
diff --git a/frontend/src/routes/visual-query/components/visual-content/constants.ts b/frontend/src/routes/visual-query/components/visual-content/constants.ts
new file mode 100644
index 0000000..85a2cc7
--- /dev/null
+++ b/frontend/src/routes/visual-query/components/visual-content/constants.ts
@@ -0,0 +1,221 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export const MONACO_EDITOR_THEME = {
+    base: 'vs',
+    inherit: true,
+    rules: [],
+    colors: {
+        'editor.background': '#F9FBFC',
+    },
+};
+
+export const RESIZEABLE_ENABLE = {
+    top: false,
+    right: false,
+    bottom: true,
+    left: false,
+    topRight: false,
+    bottomRight: false,
+    bottomLeft: false,
+    topLeft: false,
+};
+
+export const THEME_NAME = 'theme-name';
+
+export const DEFAULT_SUGGESTIONS = [
+    { label: 'add', insertText: 'add', detail: '提示' },
+    { label: 'after', insertText: 'after', detail: '提示' },
+    { label: 'all', insertText: 'all', detail: '提示' },
+    { label: 'alter', insertText: 'alter', detail: '提示' },
+    { label: 'and', insertText: 'and', detail: '提示' },
+    { label: 'append', insertText: 'append', detail: '提示' },
+    { label: 'as', insertText: 'as', detail: '提示' },
+    { label: 'authors', insertText: 'authors', detail: '提示' },
+    { label: 'begin', insertText: 'begin', detail: '提示' },
+    { label: 'between', insertText: 'between', detail: '提示' },
+    { label: 'bigint', insertText: 'bigint', detail: '提示' },
+    { label: 'boolean', insertText: 'boolean', detail: '提示' },
+    { label: 'hdfs', insertText: 'hdfs', detail: '提示' },
+    { label: 'by', insertText: 'by', detail: '提示' },
+    { label: 'char', insertText: 'char', detail: '提示' },
+    { label: 'character', insertText: 'character', detail: '提示' },
+    { label: 'column', insertText: 'column', detail: '提示' },
+    { label: 'columns', insertText: 'columns', detail: '提示' },
+    { label: 'comment', insertText: 'comment', detail: '提示' },
+    { label: 'commit', insertText: 'commit', detail: '提示' },
+    { label: 'committed', insertText: 'committed', detail: '提示' },
+    { label: 'config', insertText: 'config', detail: '提示' },
+    { label: 'count', insertText: 'count', detail: '提示' },
+    { label: 'create', insertText: 'create', detail: '提示' },
+    { label: 'cross', insertText: 'cross', detail: '提示' },
+    { label: 'database', insertText: 'database', detail: '提示' },
+    { label: 'databases', insertText: 'databases', detail: '提示' },
+    { label: 'date', insertText: 'date', detail: '提示' },
+    { label: 'datetime', insertText: 'datetime', detail: '提示' },
+    { label: 'decimal', insertText: 'decimal', detail: '提示' },
+    { label: 'delete', insertText: 'delete', detail: '提示' },
+    { label: 'desc', insertText: 'desc', detail: '提示' },
+    { label: 'double', insertText: 'double', detail: '提示' },
+    { label: 'drop', insertText: 'drop', detail: '提示' },
+    { label: 'else', insertText: 'else', detail: '提示' },
+    { label: 'enable', insertText: 'enable', detail: '提示' },
+    { label: 'end', insertText: 'end', detail: '提示' },
+    { label: 'engine', insertText: 'engine', detail: '提示' },
+    { label: 'engines', insertText: 'engines', detail: '提示' },
+    { label: 'except', insertText: 'except', detail: '提示' },
+    { label: 'exclude', insertText: 'exclude', detail: '提示' },
+    { label: 'exists', insertText: 'exists', detail: '提示' },
+    { label: 'explain', insertText: 'explain', detail: '提示' },
+    { label: 'external', insertText: 'external', detail: '提示' },
+    { label: 'float', insertText: 'float', detail: '提示' },
+    { label: 'format', insertText: 'format', detail: '提示' },
+    { label: 'from', insertText: 'from', detail: '提示' },
+    { label: 'grant', insertText: 'grant', detail: '提示' },
+    { label: 'grants', insertText: 'grants', detail: '提示' },
+    { label: 'group', insertText: 'group', detail: '提示' },
+    { label: 'hash', insertText: 'hash', detail: '提示' },
+    { label: 'having', insertText: 'having', detail: '提示' },
+    { label: 'inner', insertText: 'inner', detail: '提示' },
+    { label: 'insert', insertText: 'insert', detail: '提示' },
+    { label: 'int', insertText: 'int', detail: '提示' },
+    { label: 'integer', insertText: 'integer', detail: '提示' },
+    { label: 'into', insertText: 'into', detail: '提示' },
+    { label: 'is', insertText: 'is', detail: '提示' },
+    { label: 'isnull', insertText: 'isnull', detail: '提示' },
+    { label: 'join', insertText: 'join', detail: '提示' },
+    { label: 'key', insertText: 'key', detail: '提示' },
+    { label: 'keys', insertText: 'keys', detail: '提示' },
+    { label: 'last', insertText: 'last', detail: '提示' },
+    { label: 'left', insertText: 'left', detail: '提示' },
+    { label: 'less', insertText: 'less', detail: '提示' },
+    { label: 'like', insertText: 'like', detail: '提示' },
+    { label: 'limit', insertText: 'limit', detail: '提示' },
+    { label: 'list', insertText: 'list', detail: '提示' },
+    { label: 'load', insertText: 'load', detail: '提示' },
+    { label: 'max', insertText: 'max', detail: '提示' },
+    { label: 'merge', insertText: 'merge', detail: '提示' },
+    { label: 'min', insertText: 'min', detail: '提示' },
+    { label: 'modify', insertText: 'modify', detail: '提示' },
+    { label: 'month', insertText: 'month', detail: '提示' },
+    { label: 'no', insertText: 'no', detail: '提示' },
+    { label: 'not', insertText: 'not', detail: '提示' },
+    { label: 'null', insertText: 'null', detail: '提示' },
+    { label: 'nulls', insertText: 'nulls', detail: '提示' },
+    { label: 'offset', insertText: 'offset', detail: '提示' },
+    { label: 'on', insertText: 'on', detail: '提示' },
+    { label: 'only', insertText: 'only', detail: '提示' },
+    { label: 'open', insertText: 'open', detail: '提示' },
+    { label: 'or', insertText: 'or', detail: '提示' },
+    { label: 'order', insertText: 'order', detail: '提示' },
+    { label: 'partition', insertText: 'partition', detail: '提示' },
+    { label: 'partitions', insertText: 'partitions', detail: '提示' },
+    { label: 'properties', insertText: 'properties', detail: '提示' },
+    { label: 'property', insertText: 'property', detail: '提示' },
+    { label: 'range', insertText: 'range', detail: '提示' },
+    { label: 'range', insertText: 'range', detail: '提示' },
+    { label: 'revoke', insertText: 'revoke', detail: '提示' },
+    { label: 'right', insertText: 'right', detail: '提示' },
+    { label: 'role', insertText: 'role', detail: '提示' },
+    { label: 'roles', insertText: 'roles', detail: '提示' },
+    { label: 'row', insertText: 'row', detail: '提示' },
+    { label: 'rows', insertText: 'rows', detail: '提示' },
+    { label: 'schema', insertText: 'schema', detail: '提示' },
+    { label: 'schemas', insertText: 'schemas', detail: '提示' },
+    { label: 'second', insertText: 'second', detail: '提示' },
+    { label: 'select', insertText: 'select', detail: '提示' },
+    { label: 'set', insertText: 'set', detail: '提示' },
+    { label: 'sets', insertText: 'sets', detail: '提示' },
+    { label: 'show', insertText: 'show', detail: '提示' },
+    { label: 'stream', insertText: 'stream', detail: '提示' },
+    { label: 'string', insertText: 'string', detail: '提示' },
+    { label: 'sum', insertText: 'sum', detail: '提示' },
+    { label: 'table', insertText: 'table', detail: '提示' },
+    { label: 'tables', insertText: 'tables', detail: '提示' },
+    { label: 'than', insertText: 'than', detail: '提示' },
+    { label: 'then', insertText: 'then', detail: '提示' },
+    { label: 'timestamp', insertText: 'timestamp', detail: '提示' },
+    { label: 'true', insertText: 'true', detail: '提示' },
+    { label: 'truncate', insertText: 'truncate', detail: '提示' },
+    { label: 'type', insertText: 'type', detail: '提示' },
+    { label: 'types', insertText: 'types', detail: '提示' },
+    { label: 'union', insertText: 'union', detail: '提示' },
+    { label: 'unique', insertText: 'unique', detail: '提示' },
+    { label: 'unsigned', insertText: 'unsigned', detail: '提示' },
+    { label: 'use', insertText: 'use', detail: '提示' },
+    { label: 'user', insertText: 'user', detail: '提示' },
+    { label: 'using', insertText: 'using', detail: '提示' },
+    { label: 'update', insertText: 'update', detail: '提示' },
+    { label: 'value', insertText: 'value', detail: '提示' },
+    { label: 'values', insertText: 'values', detail: '提示' },
+    { label: 'varchar', insertText: 'varchar', detail: '提示' },
+    { label: 'variables', insertText: 'variables', detail: '提示' },
+    { label: 'view', insertText: 'view', detail: '提示' },
+    { label: 'when', insertText: 'when', detail: '提示' },
+    { label: 'where', insertText: 'where', detail: '提示' },
+    { label: 'with', insertText: 'with', detail: '提示' },
+    { label: 'write', insertText: 'write', detail: '提示' },
+    { label: 'year', insertText: 'year', detail: '提示' },
+    { label: 'matched', insertText: 'matched', detail: '提示' },
+    { label: 'run', insertText: 'run', detail: '提示' },
+    { label: 'tasks', insertText: 'tasks', detail: '提示' },
+    { label: 'update', insertText: 'update', detail: '提示' },
+    { label: 'stage', insertText: 'stage', detail: '提示' },
+    { label: 'stages', insertText: 'stages', detail: '提示' },
+    { label: 'url', insertText: 'url', detail: '提示' },
+    { label: 'storage_options', insertText: 'storage_options', detail: '提示' },
+    { label: 'file_format', insertText: 'file_format', detail: '提示' },
+    { label: 'copy_options', insertText: 'copy_options', detail: '提示' },
+    { label: 'jobflow', insertText: 'jobflow', detail: '提示' },
+    { label: 'schedule', insertText: 'schedule', detail: '提示' },
+    { label: 'copy', insertText: 'copy', detail: '提示' },
+    { label: 'grants', insertText: 'grants', detail: '提示' },
+    { label: 'cron', insertText: 'cron', detail: '提示' },
+    { label: 'options', insertText: 'options', detail: '提示' },
+    { label: 'job', insertText: 'job', detail: '提示' },
+    { label: 'unset', insertText: 'unset', detail: '提示' },
+    { label: 'remove', insertText: 'remove', detail: '提示' },
+    { label: 'terse', insertText: 'terse', detail: '提示' },
+    { label: 'jobs', insertText: 'jobs', detail: '提示' },
+    { label: 'starts', insertText: 'starts', detail: '提示' },
+    { label: 'account', insertText: 'account', detail: '提示' },
+    { label: 'jobflows', insertText: 'jobflows', detail: '提示' },
+    { label: 'call', insertText: 'call', detail: '提示' },
+    { label: 'at', insertText: 'at', detail: '提示' },
+    { label: 'history', insertText: 'history', detail: '提示' },
+    { label: 'snapshots', insertText: 'snapshots', detail: '提示' },
+    { label: 'clone', insertText: 'clone', detail: '提示' },
+    { label: 'stream', insertText: 'stream', detail: '提示' },
+    { label: 'before', insertText: 'before', detail: '提示' },
+    { label: 'append_only', insertText: 'append_only', detail: '提示' },
+    { label: 'streams', insertText: 'streams', detail: '提示' },
+    { label: 'dropped', insertText: 'dropped', detail: '提示' },
+    { label: 'array', insertText: 'array', detail: '提示' },
+    { label: 'map', insertText: 'map', detail: '提示' },
+    { label: 'struct', insertText: 'struct', detail: '提示' },
+    { label: 'field', insertText: 'field', detail: '提示' },
+    { label: 'refresh', insertText: 'refresh', detail: '提示' },
+    { label: 'mount', insertText: 'mount', detail: '提示' },
+    { label: 'files', insertText: 'files', detail: '提示' },
+    { label: 'cache', insertText: 'cache', detail: '提示' },
+    { label: 'granted', insertText: 'granted', detail: '提示' },
+    { label: 'cached', insertText: 'cached', detail: '提示' },
+    { label: 'queue', insertText: 'queue', detail: '提示' },
+    { label: 'queues', insertText: 'queues', detail: '提示' },
+    { label: 'sessionlabel', insertText: 'sessionlabel', detail: '提示' },
+    { label: 'extended', insertText: 'extended', detail: '提示' },
+];
diff --git a/frontend/src/routes/visual-query/components/visual-content/hooks.ts b/frontend/src/routes/visual-query/components/visual-content/hooks.ts
new file mode 100644
index 0000000..ee48d41
--- /dev/null
+++ b/frontend/src/routes/visual-query/components/visual-content/hooks.ts
@@ -0,0 +1,73 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { useEffect, useContext, useCallback, useRef } from 'react';
+import { useMonaco } from '@monaco-editor/react';
+import { THEME_NAME, MONACO_EDITOR_THEME, DEFAULT_SUGGESTIONS } from './constants';
+import { DatabasesContext } from '../../context';
+import { useCallbackRef } from '../../hooks';
+
+export function useMonacoEditor() {
+    const { selectedDatabaseId, databases } = useContext(DatabasesContext);
+    const monaco = useMonaco();
+
+    useEffect(() => {
+        if (monaco) {
+            monaco.editor.defineTheme(THEME_NAME, MONACO_EDITOR_THEME as any);
+            monaco.editor.setTheme(THEME_NAME);
+        }
+    }, [monaco]);
+
+    const provideCompletionItems = useCallbackRef((model: any, position: any) => {
+        const textUntilPosition = model.getValueInRange({
+            startLineNumber: position.lineNumber,
+            startColumn: 1,
+            endLineNumber: position.lineNumber,
+            endColumn: position.column,
+        });
+        const textArr = textUntilPosition.split(' ');
+        const lastText = textArr[textArr.length - 1];
+        const currentDatabase = databases?.find(database => database.id === selectedDatabaseId);
+        const SUGGESTIONS = [
+            ...DEFAULT_SUGGESTIONS,
+            ...currentDatabase.tables.map((table: any) => ({
+                label: table.name,
+                insertText: table.name,
+                detail: '表',
+            })),
+        ];
+        const match = SUGGESTIONS.some((item: any) => {
+            // 循环判断是否包含在补全数组中
+            return item.label.indexOf(lastText) > 0;
+        });
+        const suggestion = match ? SUGGESTIONS : [];
+        return {
+            suggestions: suggestion.map(item => ({
+                ...item,
+                kind: monaco?.languages.CompletionItemKind.Variable,
+            })),
+        } as any;
+    });
+
+    useEffect(() => {
+        if (monaco) {
+            monaco.languages.registerCompletionItemProvider('sql', {
+                provideCompletionItems,
+            });
+        }
+    }, [monaco, provideCompletionItems]);
+}
diff --git a/frontend/src/routes/visual-query/components/visual-content/index.tsx b/frontend/src/routes/visual-query/components/visual-content/index.tsx
new file mode 100644
index 0000000..aebd5e6
--- /dev/null
+++ b/frontend/src/routes/visual-query/components/visual-content/index.tsx
@@ -0,0 +1,91 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext, useImperativeHandle, useMemo, useRef } from 'react';
+import * as _ from 'lodash-es';
+import classnames from 'classnames';
+import { Typography, Row, Spin } from 'antd';
+import styles from './style.module.less';
+import VisualEditor from './visual-editor';
+import { DatabasesContext, DataContext } from '../../context';
+import LoadingLayout from '../loading-layout';
+
+function VisualContent(props: any, ref: any) {
+    const { databasesLoading } = useContext(DatabasesContext);
+    const { data: resultData, dataLoading, dataError } = useContext(DataContext);
+    const visualWrapperRef = useRef<HTMLDivElement | null>(null);
+    const visualTableRef =
+        useRef<{ refreshTableHeight: (val: number) => void; refreshTableWidth: (val: number) => void }>(null);
+
+    const isFetchingError = dataError != null || (resultData && resultData.error);
+
+    const content = useMemo(() => {
+        if (dataLoading)
+            return (
+                <Row align="middle" justify="center" style={{ height: '100%' }}>
+                    <Spin />
+                </Row>
+            );
+        if (isFetchingError)
+            return (
+                <Row align="middle" justify="center" style={{ height: '100%' }}>
+                    <Typography.Text type="danger">
+                        您的查询出现错误:{dataError ? '请检查您的网络' : resultData?.error}
+                    </Typography.Text>
+                </Row>
+            );
+        if (resultData == null)
+            return (
+                <Row align="middle" justify="center" style={{ height: '100%' }}>
+                    <Typography.Text>您的查询结果将在这里展示</Typography.Text>
+                </Row>
+            );
+        return (
+            <>
+            </>
+        );
+    }, [dataLoading, isFetchingError, resultData, dataError]);
+
+    const onResize = _.throttle(() => {
+        if (visualTableRef.current && visualWrapperRef.current) {
+            visualTableRef.current.refreshTableHeight(visualWrapperRef.current.clientHeight * 0.8 - 120);
+        }
+    });
+
+    const refreshTableWidth = () => {
+        if (visualTableRef.current && visualWrapperRef.current) {
+            visualTableRef.current.refreshTableWidth(visualWrapperRef.current.clientWidth);
+        }
+    };
+
+    useImperativeHandle(ref, () => ({
+        refreshTableWidth,
+    }));
+
+    return (
+        <LoadingLayout loading={databasesLoading}>
+            <div className={classnames(styles.content)}>
+                <VisualEditor onResize={onResize} />
+                <div className={styles.visualWrapper} ref={visualWrapperRef}>
+                    {content}
+                </div>
+            </div>
+        </LoadingLayout>
+    );
+}
+
+export default React.forwardRef(VisualContent);
diff --git a/frontend/src/routes/visual-query/components/visual-content/style.module.less b/frontend/src/routes/visual-query/components/visual-content/style.module.less
new file mode 100644
index 0000000..bc4139b
--- /dev/null
+++ b/frontend/src/routes/visual-query/components/visual-content/style.module.less
@@ -0,0 +1,51 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+.content {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+    position: relative;
+    height: calc(100vh - 128px);
+    &::before {
+        content: '';
+        width: 1px;
+        background-color: #ddd;
+        position: absolute;
+        top: 0;
+        left: -20px;
+        height: calc(100% + 40px);
+        z-index: 99;
+    }
+    &.contentRight {
+        &::before {
+            content: '';
+            width: 1px;
+            background-color: #ddd;
+            position: absolute;
+            top: 0;
+            left: 0;
+            height: calc(100% + 40px);
+            z-index: 99;
+        }
+    }
+}
+
+.visualWrapper {
+    flex: 1;
+    width: 95%;
+}
diff --git a/frontend/src/routes/visual-query/components/visual-content/visual-editor.tsx b/frontend/src/routes/visual-query/components/visual-content/visual-editor.tsx
new file mode 100644
index 0000000..2e247da
--- /dev/null
+++ b/frontend/src/routes/visual-query/components/visual-content/visual-editor.tsx
@@ -0,0 +1,104 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext, useState } from 'react';
+import { Select, Row, Col, Button, message } from 'antd';
+import { Resizable } from 're-resizable';
+import Editor from '@monaco-editor/react';
+import { DatabasesContext, DataContext, EditorContext } from '../../context';
+import { THEME_NAME, RESIZEABLE_ENABLE } from './constants';
+import { useMonacoEditor } from './hooks';
+import { fetchData } from '../../visual-query.api';
+
+const { Option } = Select;
+
+interface VisualEditorProps {
+    onResize: () => void;
+}
+
+export default function VisualEditor(props: VisualEditorProps) {
+    const { onResize } = props;
+    const { selectedDatabaseId, setSelectedDatabaseId, databases } = useContext(DatabasesContext);
+    const { runFetchData } = useContext(DataContext);
+    const { editorValue, setEditorValue } = useContext(EditorContext);
+    useMonacoEditor();
+
+    const runSqlQuery = () => {
+        if (selectedDatabaseId == null) {
+            message.error('请选择一个数据库');
+            return;
+        }
+        if (editorValue === '') {
+            message.error('请输入查询语句');
+            return;
+        }
+        runFetchData(fetchData({ database: selectedDatabaseId, native: { query: editorValue }, type: 'NATIVE' })).catch(
+            () => message.error('查询失败'),
+        );
+    };
+
+    return (
+        <>
+            <Select
+                value={selectedDatabaseId || undefined}
+                style={{ width: 200, margin: '20px 0 0 20px' }}
+                placeholder="请选择一个数据库"
+                onChange={v => setSelectedDatabaseId(v)}
+            >
+                {databases?.map(database => (
+                    <Option value={database.id} key={database.id}>
+                        {database.name}
+                    </Option>
+                ))}
+            </Select>
+            <Resizable
+                style={{ border: '1px solid #ccc', margin: 20 }}
+                enable={RESIZEABLE_ENABLE}
+                defaultSize={{
+                    width: '95%',
+                    height: 150,
+                }}
+                minHeight={150}
+                onResize={onResize}
+            >
+                <Editor
+                    height="100%"
+                    defaultLanguage="sql"
+                    theme={THEME_NAME}
+                    options={{
+                        minimap: {
+                            enabled: false,
+                        },
+                    }}
+                    value={editorValue}
+                    onChange={v => setEditorValue(v || '')}
+                />
+            </Resizable>
+            <Row justify="end">
+                <Col style={{ marginRight: '4%' }}>
+                    <Button
+                        type="primary"
+                        disabled={selectedDatabaseId == null || editorValue === ''}
+                        onClick={runSqlQuery}
+                    >
+                        运行
+                    </Button>
+                </Col>
+            </Row>
+        </>
+    );
+}
diff --git a/frontend/src/routes/visual-query/components/visual-content/visual-table.tsx b/frontend/src/routes/visual-query/components/visual-content/visual-table.tsx
new file mode 100644
index 0000000..53a25cc
--- /dev/null
+++ b/frontend/src/routes/visual-query/components/visual-content/visual-table.tsx
@@ -0,0 +1,58 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext, useEffect, useImperativeHandle, useRef, useState } from 'react';
+import { Table } from 'antd';
+import { DataContext } from '../../context';
+
+function VisualTable(props: any, ref: any) {
+    const { columns, dataSource } = useContext(DataContext);
+
+    const tableWrapperRef = useRef<HTMLDivElement | null>(null);
+
+    const [tableWidth, setTableWidth] = useState(1100);
+    const [scrollY, setScrollY] = useState(200);
+
+    useEffect(() => {
+        if (tableWrapperRef.current) {
+            setTableWidth(tableWrapperRef.current.clientWidth);
+            setScrollY(tableWrapperRef.current.clientHeight - 120);
+        }
+    }, []);
+
+    useImperativeHandle(ref, () => ({
+        refreshTableHeight: setScrollY,
+        refreshTableWidth: setTableWidth,
+    }));
+
+    return (
+        <div style={{ height: '100%', margin: '20px 0 0 20px' }} ref={tableWrapperRef}>
+            <div style={{ width: tableWidth, height: '100%' }}>
+                <Table
+                    style={{ width: '100%', height: '100%' }}
+                    columns={columns}
+                    dataSource={dataSource}
+                    scroll={{ y: scrollY, x: tableWidth - 20 }}
+                    pagination={{ size: 'small', pageSize: 50, showSizeChanger: false }}
+                    size="small"
+                />
+            </div>
+        </div>
+    );
+}
+
+export default React.forwardRef(VisualTable);
diff --git a/frontend/src/routes/visual-query/components/visual-header/index.module.less b/frontend/src/routes/visual-query/components/visual-header/index.module.less
new file mode 100644
index 0000000..4456d55
--- /dev/null
+++ b/frontend/src/routes/visual-query/components/visual-header/index.module.less
@@ -0,0 +1,22 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+.container {
+    height: 62px;
+    border-bottom: 1px solid #f0f0f0;
+    background-color: #fff;
+    padding: 0 20px;
+}
diff --git a/frontend/src/routes/visual-query/components/visual-header/index.tsx b/frontend/src/routes/visual-query/components/visual-header/index.tsx
new file mode 100644
index 0000000..3b82a9b
--- /dev/null
+++ b/frontend/src/routes/visual-query/components/visual-header/index.tsx
@@ -0,0 +1,48 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useContext } from 'react';
+import { Button, message, Row, Typography } from 'antd';
+import styles from './index.module.less';
+import { DataContext } from '../../context';
+
+interface VisualHeaderProps {
+    setSaveQueryModalVisible: (v: boolean) => void;
+}
+
+export default function VisualHeader(props: VisualHeaderProps) {
+    const { setSaveQueryModalVisible } = props;
+    const { data } = useContext(DataContext);
+
+    const handleSave = () => {
+        if (data == null || data.error != null) {
+            return message.info('请先进行有效查询');
+        }
+        setSaveQueryModalVisible(true);
+    };
+
+    return (
+        <Row justify="space-between" align="middle" className={styles.container}>
+            <Typography.Title level={4}>SQL编辑区</Typography.Title>
+            <div>
+                <Button type="link" onClick={handleSave}>
+                    保存
+                </Button>
+            </div>
+        </Row>
+    );
+}
diff --git a/frontend/src/routes/visual-query/context/data-context.tsx b/frontend/src/routes/visual-query/context/data-context.tsx
new file mode 100644
index 0000000..040255a
--- /dev/null
+++ b/frontend/src/routes/visual-query/context/data-context.tsx
@@ -0,0 +1,100 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { PropsWithChildren, useEffect, useState, useMemo } from 'react';
+import { useAsync } from '../hooks';
+import { DataType } from '../types';
+
+export interface Column {
+    dataIndex: string;
+    title: string;
+}
+
+interface ResultData {
+    data: DataType;
+    error?: any;
+    database_id: number;
+}
+
+interface DataContextProps {
+    columns: Column[];
+    dataSource: Record<string, any>[];
+    setColumns: (val: Column[]) => void;
+    data: ResultData | null;
+    dataLoading: boolean;
+    dataError: Error | null;
+    runFetchData: (promise: Promise<ResultData>) => Promise<ResultData>;
+}
+
+export const DataContext = React.createContext<DataContextProps>({
+    columns: [],
+    dataSource: [],
+    setColumns: () => {},
+    data: null,
+    dataLoading: false,
+    dataError: null,
+    runFetchData: () => Promise.resolve({ data: { cols: [], rows: [], native_form: { query: '' } }, database_id: 0 }),
+});
+
+export default function DataContextProvider(props: PropsWithChildren<{}>) {
+    const [columns, setColumns] = useState<{ dataIndex: string; title: string }[]>([]);
+    const { data: resultData, loading: dataLoading, error: dataError, run: runFetchData } = useAsync<ResultData>();
+
+    const isFetchingError = dataError != null || (resultData && resultData.error);
+
+    const dataSource = useMemo(() => {
+        if (!resultData || isFetchingError) return [];
+        return resultData.data.rows.map((item, index) => {
+            return columns.reduce(
+                (memo, current) => {
+                    const columnIndex = resultData.data.cols.findIndex(col => col.name === current.dataIndex);
+                    memo[current.dataIndex] = item[columnIndex];
+                    return memo;
+                },
+                { key: index } as Record<string, any>,
+            );
+        });
+    }, [resultData, columns, isFetchingError]);
+
+    useEffect(() => {
+        if (isFetchingError || resultData == null) {
+            setColumns([]);
+            return;
+        }
+        const newColumns = resultData.data.cols.map(item => ({
+            title: item.display_name,
+            dataIndex: item.name,
+        }));
+        setColumns(newColumns);
+    }, [resultData, isFetchingError]);
+
+    return (
+        <DataContext.Provider
+            value={{
+                columns,
+                dataSource,
+                setColumns,
+                data: resultData,
+                dataLoading,
+                dataError,
+                runFetchData,
+            }}
+        >
+            {props.children}
+        </DataContext.Provider>
+    );
+}
diff --git a/frontend/src/routes/visual-query/context/databases-context.tsx b/frontend/src/routes/visual-query/context/databases-context.tsx
new file mode 100644
index 0000000..4befc27
--- /dev/null
+++ b/frontend/src/routes/visual-query/context/databases-context.tsx
@@ -0,0 +1,63 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { PropsWithChildren, useEffect, useState } from 'react';
+import { message } from 'antd';
+import { fetchDatabases } from '../visual-query.api';
+import { useAsync } from '../hooks';
+
+interface DatabasesContextProps {
+    databases: any[] | null;
+    databasesLoading: boolean;
+    selectedDatabaseId: number | null;
+    setSelectedDatabaseId: (id: number) => void;
+}
+
+export const DatabasesContext = React.createContext<DatabasesContextProps>({
+    databases: [],
+    databasesLoading: true,
+    selectedDatabaseId: null,
+    setSelectedDatabaseId: () => {},
+});
+
+export default function QueryContextProvider(props: PropsWithChildren<{}>) {
+    const {
+        data: databases,
+        loading: databasesLoading,
+        run: runFetchDatabases,
+    } = useAsync<any[]>({ loading: true, data: [] });
+    const [selectedDatabaseId, setSelectedDatabaseId] = useState<number | null>(null);
+
+    useEffect(() => {
+        runFetchDatabases(fetchDatabases())
+            .then(res => res && res.length > 0 && setSelectedDatabaseId(res[0].id))
+            .catch(() => message.error('获取数据库列表失败'));
+    }, [runFetchDatabases]);
+
+    return (
+        <DatabasesContext.Provider
+            value={{
+                databases,
+                databasesLoading,
+                selectedDatabaseId,
+                setSelectedDatabaseId,
+            }}
+        >
+            {props.children}
+        </DatabasesContext.Provider>
+    );
+}
diff --git a/frontend/src/routes/visual-query/context/editor-context.tsx b/frontend/src/routes/visual-query/context/editor-context.tsx
new file mode 100644
index 0000000..7a39f2d
--- /dev/null
+++ b/frontend/src/routes/visual-query/context/editor-context.tsx
@@ -0,0 +1,33 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { PropsWithChildren, useState } from 'react';
+
+interface EditorContextProps {
+    editorValue: string;
+    setEditorValue: (v: string) => void;
+}
+
+export const EditorContext = React.createContext<EditorContextProps>({
+    editorValue: '',
+    setEditorValue: () => {},
+});
+
+export default function EditorContextProvider(props: PropsWithChildren<{}>) {
+    const [editorValue, setEditorValue] = useState('');
+    return <EditorContext.Provider value={{ editorValue, setEditorValue }}>{props.children}</EditorContext.Provider>;
+}
diff --git a/frontend/src/routes/visual-query/context/index.tsx b/frontend/src/routes/visual-query/context/index.tsx
new file mode 100644
index 0000000..19e0c4e
--- /dev/null
+++ b/frontend/src/routes/visual-query/context/index.tsx
@@ -0,0 +1,41 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { PropsWithChildren } from 'react';
+import DatabasesContextProvider from './databases-context';
+import DataContextProvider from './data-context';
+import EditorContextProvider from './editor-context';
+
+const providers = [
+    DatabasesContextProvider,
+    DataContextProvider,
+    EditorContextProvider,
+];
+
+export default function VisualQueryProvider(props: PropsWithChildren<{}>) {
+    return (
+        <>
+            {providers.reduce((memo, Provider) => {
+                return <Provider>{memo}</Provider>;
+            }, props.children)}
+        </>
+    );
+}
+
+export * from './databases-context';
+export * from './data-context';
+export * from './editor-context';
diff --git a/frontend/src/routes/visual-query/hooks/index.ts b/frontend/src/routes/visual-query/hooks/index.ts
new file mode 100644
index 0000000..dd0e843
--- /dev/null
+++ b/frontend/src/routes/visual-query/hooks/index.ts
@@ -0,0 +1,21 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export * from './use-async';
+export * from './use-callback-ref';
+export * from './use-click-outside';
+export * from './use-collections';
diff --git a/frontend/src/routes/visual-query/hooks/use-async.ts b/frontend/src/routes/visual-query/hooks/use-async.ts
new file mode 100644
index 0000000..2164ced
--- /dev/null
+++ b/frontend/src/routes/visual-query/hooks/use-async.ts
@@ -0,0 +1,59 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { useReducer, useCallback } from 'react';
+
+interface State<T> {
+    loading: boolean;
+    data: T | null;
+    error: Error | null;
+}
+
+const defaultState = {
+    loading: false,
+    data: null,
+    error: null,
+};
+
+export function useAsync<T>(initialState?: Partial<State<T>>) {
+    const [state, dispatch] = useReducer((state: State<T>, action: Partial<State<T>>) => ({ ...state, ...action }), {
+        ...defaultState,
+        ...initialState,
+    });
+
+    const run = useCallback(
+        (promise: Promise<T>) => {
+            dispatch({ loading: true });
+            return promise
+                .then(data => {
+                    dispatch({ loading: false, data, error: null });
+                    return data;
+                })
+                .catch(error => {
+                    dispatch({ loading: false, data: null, error });
+                    return Promise.reject(error);
+                });
+        },
+        [dispatch],
+    );
+
+    return {
+        run,
+        dispatch,
+        ...state,
+    };
+}
diff --git a/frontend/src/routes/visual-query/hooks/use-callback-ref.ts b/frontend/src/routes/visual-query/hooks/use-callback-ref.ts
new file mode 100644
index 0000000..bf274b9
--- /dev/null
+++ b/frontend/src/routes/visual-query/hooks/use-callback-ref.ts
@@ -0,0 +1,24 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { useRef, useCallback } from 'react';
+
+export function useCallbackRef(fn: (...args: any[]) => any) {
+    const fnRef = useRef(fn);
+    fnRef.current = fn;
+    return useCallback((...args: any[]) => fnRef.current(...args), []);
+}
diff --git a/frontend/src/routes/visual-query/hooks/use-click-outside.ts b/frontend/src/routes/visual-query/hooks/use-click-outside.ts
new file mode 100644
index 0000000..b299278
--- /dev/null
+++ b/frontend/src/routes/visual-query/hooks/use-click-outside.ts
@@ -0,0 +1,34 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { useEffect } from 'react';
+
+export function useClickOutside(domRef: React.MutableRefObject<HTMLElement | null>, callback: () => void) {
+    useEffect(() => {
+        const dom = domRef.current;
+        const handler = (e: MouseEvent) => {
+            if (dom && dom.contains(e.target as HTMLElement)) {
+                return;
+            }
+            callback();
+        };
+        document.addEventListener('click', handler);
+        return () => {
+            document.removeEventListener('click', handler);
+        };
+    }, []);
+}
diff --git a/frontend/src/routes/visual-query/hooks/use-collections.ts b/frontend/src/routes/visual-query/hooks/use-collections.ts
new file mode 100644
index 0000000..e171310
--- /dev/null
+++ b/frontend/src/routes/visual-query/hooks/use-collections.ts
@@ -0,0 +1,42 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { useEffect, useMemo } from 'react';
+import { message } from 'antd';
+import { useAsync } from '@src/hooks/use-async';
+import { CascaderItem } from '../types';
+import { fetchCollectionAPI } from '../visual-query.api';
+import { getCollections } from '../utils';
+
+export function useCollections() {
+    const { data, loading, run: runFetchCollections } = useAsync<CascaderItem[]>({ data: [] });
+
+    useEffect(() => {
+        runFetchCollections(fetchCollectionAPI()).catch(() => {
+            message.error('获取文件夹列表失败');
+        });
+    }, [runFetchCollections]);
+
+    const collections = useMemo(() => {
+        return getCollections(data as CascaderItem[]);
+    }, [data]);
+
+    return {
+        loading,
+        collections,
+    };
+}
diff --git a/frontend/src/routes/visual-query/types/common.ts b/frontend/src/routes/visual-query/types/common.ts
new file mode 100644
index 0000000..a361dbb
--- /dev/null
+++ b/frontend/src/routes/visual-query/types/common.ts
@@ -0,0 +1,64 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+
+export interface DataType {
+    cols: {
+        source: string;
+        name: string;
+        display_name: string;
+        base_type: string;
+    }[];
+    rows: any[][];
+    native_form: {
+        query: string;
+    };
+}
+
+export type Dataset = { source: any[][] }[];
+
+export const enum SidebarTypeEnum {
+    TYPE = 'echartsType',
+    SETTINGS = 'echartsSettings',
+}
+
+export const enum ChartTypeEnum {
+    BAR_CHART = 'bar',
+    LINE_CHART = 'line',
+    AREA_CHART = 'area',
+    SCATTER_CHART = 'scatter',
+    HORIZONTAL_BAR_CHART = 'horizontal-bar',
+    TABLE = 'table',
+}
+
+export const enum AxisTypeEnum {
+    CATEGORY = 'category',
+    VALUE = 'value',
+}
+
+export interface CascaderItem {
+    name: string;
+    id: number;
+    archived: boolean;
+    can_write: boolean;
+    location: string;
+    children: CascaderItem[];
+    label: React.ReactNode;
+    isLeaf?: boolean;
+    isFetched?: boolean;
+}
diff --git a/frontend/src/routes/visual-query/types/index.ts b/frontend/src/routes/visual-query/types/index.ts
new file mode 100644
index 0000000..6aaf4db
--- /dev/null
+++ b/frontend/src/routes/visual-query/types/index.ts
@@ -0,0 +1,18 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export * from './common';
\ No newline at end of file
diff --git a/frontend/src/routes/visual-query/utils/color-picker.ts b/frontend/src/routes/visual-query/utils/color-picker.ts
new file mode 100644
index 0000000..69a0db5
--- /dev/null
+++ b/frontend/src/routes/visual-query/utils/color-picker.ts
@@ -0,0 +1,59 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+const COLORS = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'];
+
+const COLORS_MAP = COLORS.reduce((memo, current) => {
+    memo[current] = 0;
+    return memo;
+}, {});
+
+class ColorPicker {
+    colors: string[] = COLORS;
+    colorsMap: Record<string, number> = COLORS_MAP;
+    index: number = 0;
+    restCount: number = COLORS.length;
+    pickInOrder() {
+        if (this.restCount === 0) {
+            const color = this.colors[this.index % this.colors.length];
+            this.colorsMap[color]++;
+            this.index++;
+            return color;
+        }
+        const color = Object.keys(this.colorsMap).find(color => this.colorsMap[color] === 0) as string;
+        this.colorsMap[color]++;
+        this.restCount--;
+        this.index = this.colors.indexOf(color) + 1;
+        return color;
+    }
+    pick(color: string) {
+        this.colorsMap[color]++;
+        if (this.colorsMap[color] === 1) {
+            this.restCount--;
+        }
+        this.index = this.colors.indexOf(color) + 1;
+    }
+    reset() {
+        Object.keys(this.colorsMap).forEach(key => {
+            this.colorsMap[key] = 0;
+        });
+        this.index = 0;
+        this.restCount = this.colors.length;
+    }
+}
+
+export const colorPicker = new ColorPicker();
diff --git a/frontend/src/routes/visual-query/utils/get-collections.tsx b/frontend/src/routes/visual-query/utils/get-collections.tsx
new file mode 100644
index 0000000..e2777b3
--- /dev/null
+++ b/frontend/src/routes/visual-query/utils/get-collections.tsx
@@ -0,0 +1,60 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React from 'react';
+import { FolderOutlined } from '@ant-design/icons';
+import { CascaderItem } from '../types';
+
+function getLocationPaths(location: string) {
+    return location
+        .split('/')
+        .filter(item => item !== '')
+        .map(item => Number(item));
+}
+
+function getParent(map: Map<number, CascaderItem>, locationPaths: number[]) {
+    return locationPaths.reduce((memo, current) => {
+        return memo.children.find(item => item.id === current)!;
+    }, map.get(locationPaths.shift()!) as CascaderItem);
+}
+
+export function getCollections(data: CascaderItem[]) {
+    const filteredData = data
+        .filter(item => !item.archived && item.can_write)
+        .map(item => ({
+            ...item,
+            children: [] as CascaderItem[],
+            label: (
+                <div>
+                    <FolderOutlined style={{ marginRight: 5 }} />
+                    {item.name}
+                </div>
+            ),
+        }))
+        .sort((a, b) => getLocationPaths(a.location).length - getLocationPaths(b.location).length);
+    const map = new Map<number, CascaderItem>();
+    filteredData.forEach(item => {
+        const locationPaths = getLocationPaths(item.location);
+        if (locationPaths.length === 0) {
+            map.set(item.id, item);
+            return;
+        }
+        const parent = getParent(map, locationPaths);
+        parent.children.push(item);
+    });
+    return Array.from(map).map(item => item[1]);
+}
diff --git a/frontend/src/routes/visual-query/utils/index.ts b/frontend/src/routes/visual-query/utils/index.ts
new file mode 100644
index 0000000..b453c0b
--- /dev/null
+++ b/frontend/src/routes/visual-query/utils/index.ts
@@ -0,0 +1,20 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+export * from './color-picker';
+export * from './get-collections';
+export * from './process-collections';
diff --git a/frontend/src/routes/visual-query/utils/process-collections.ts b/frontend/src/routes/visual-query/utils/process-collections.ts
new file mode 100644
index 0000000..3526f6b
--- /dev/null
+++ b/frontend/src/routes/visual-query/utils/process-collections.ts
@@ -0,0 +1,29 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { CascaderItem } from '../types';
+
+export function processCollections(collections: CascaderItem[]): CascaderItem[] {
+    return collections.map(collection => {
+        return {
+            ...collection,
+            isLeaf: false,
+            isFetched: false,
+            children: processCollections(collection.children),
+        };
+    });
+}
diff --git a/frontend/src/routes/visual-query/visual-query.api.ts b/frontend/src/routes/visual-query/visual-query.api.ts
new file mode 100644
index 0000000..e312395
--- /dev/null
+++ b/frontend/src/routes/visual-query/visual-query.api.ts
@@ -0,0 +1,91 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import { http, isSuccess } from '@src/utils/http';
+import { DataType } from './types';
+
+export function fetchDatabases() {
+    return http.get('/api/database/', { include: 'tables' }).then(res => res.data) as Promise<any[]>;
+}
+
+interface NativeSqlQueryParams {
+    database: number;
+    native: { query: string };
+    type: 'NATIVE';
+}
+
+export function fetchData(params: NativeSqlQueryParams) {
+    return http.post('/api/dataset/', params).then(res => res.data) as Promise<{
+        data: DataType;
+        error?: any;
+        database_id: number;
+    }>;
+}
+
+export function fetchCollectionAPI() {
+    return http.get('/api/collection/').then(res => {
+        if (isSuccess(res)) {
+            return res.data;
+        }
+        return Promise.reject(res);
+    });
+}
+
+interface AddCardParams {
+    collection_id: number;
+    dataset_query: {
+        database: number;
+        native: {
+            query: string;
+        };
+        type: 'NATIVE';
+    };
+    description: string | null;
+    display: 'table' | 'chart';
+    name: string;
+    result_metadata: {
+        columns: any[];
+    };
+    original_definition: {
+        database: number;
+        native: {
+            query: string;
+        };
+        type: 'NATIVE';
+    };
+    visualization_settings: any;
+}
+
+export function addCardAPI(params: AddCardParams) {
+    return http.post('/api/card/', params).then(res => {
+        if (isSuccess(res)) return res.data;
+        return Promise.reject(res);
+    });
+}
+
+interface FetchChildOfCollectionParams {
+    collectionId: number;
+    model: 'collection' | 'dashboard' | 'card';
+}
+
+export function fetchChildOfCollectionAPI(params: FetchChildOfCollectionParams) {
+    const { collectionId, ...restParams } = params;
+    return http.get(`/api/collection/${collectionId}/items`, restParams).then(res => {
+        if (isSuccess(res)) return res.data;
+        return Promise.reject(res);
+    });
+}
diff --git a/frontend/src/routes/visual-query/visual-query.tsx b/frontend/src/routes/visual-query/visual-query.tsx
new file mode 100644
index 0000000..97ce4a0
--- /dev/null
+++ b/frontend/src/routes/visual-query/visual-query.tsx
@@ -0,0 +1,46 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import React, { useRef, useState } from 'react';
+import VisualQueryProvider from './context';
+import VisualContent from './components/visual-content';
+import VisualHeader from './components/visual-header';
+import SaveQueryModal from './components/save-query-modal';
+import SaveDashboardModal from './components/save-dashboard-modal';
+import { useCollections } from './hooks';
+
+export default function VisualQuery() {
+    const [saveQueryModalVisible, setSaveQueryModalVisible] = useState(false);
+    const [saveDashboardModalVisible, setSaveDashboardModalVisible] = useState(false);
+    const visualContentRef = useRef<{ refreshTableWidth: () => void }>(null);
+    const { collections, loading } = useCollections();
+
+    const onAnimated = () => {
+        if (visualContentRef.current) {
+            visualContentRef.current.refreshTableWidth();
+        }
+    };
+
+    return (
+        <VisualQueryProvider>
+            <VisualHeader setSaveQueryModalVisible={setSaveQueryModalVisible} />
+            <div style={{ display: 'flex' }}>
+                <VisualContent ref={visualContentRef} />
+            </div>
+        </VisualQueryProvider>
+    );
+}