You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dolphinscheduler.apache.org by le...@apache.org on 2022/01/15 04:01:24 UTC

[dolphinscheduler] branch dev updated: [Feature][UI Next] Add data source (#8058)

This is an automated email from the ASF dual-hosted git repository.

leonbao pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git


The following commit(s) were added to refs/heads/dev by this push:
     new 4ea603b  [Feature][UI Next] Add data source (#8058)
4ea603b is described below

commit 4ea603bb43c5d31cbdc50fa6276ec6a08fe9ae77
Author: Amy0104 <97...@users.noreply.github.com>
AuthorDate: Sat Jan 15 12:01:14 2022 +0800

    [Feature][UI Next] Add data source (#8058)
---
 .../src/components/modal/index.module.scss         |   9 -
 .../src/components/modal/index.tsx                 |  16 +-
 .../src/locales/modules/en_US.ts                   |  51 +++-
 .../src/locales/modules/zh_CN.ts                   |  48 ++-
 .../src/router/modules/datasource.ts               |   2 +-
 .../src/service/modules/data-source/index.ts       |  26 +-
 .../src/service/modules/data-source/types.ts       |  32 +-
 .../views/datasource/datasource-list/detail.tsx    | 332 +++++++++++++++++++++
 .../datasource/datasource-list}/index.module.scss  |  23 +-
 .../src/views/datasource/datasource-list/index.tsx | 150 ++++++++++
 .../datasource-list/json-highlight.module.scss}    |  17 +-
 .../datasource/datasource-list/json-highlight.tsx  |  67 +++++
 .../datasource/datasource-list/types.ts}           |  34 ++-
 .../datasource/datasource-list/use-columns.ts      | 136 +++++++++
 .../views/datasource/datasource-list/use-detail.ts | 103 +++++++
 .../views/datasource/datasource-list/use-form.ts   | 209 +++++++++++++
 .../views/datasource/datasource-list/use-table.ts  |  82 +++++
 17 files changed, 1269 insertions(+), 68 deletions(-)

diff --git a/dolphinscheduler-ui-next/src/components/modal/index.module.scss b/dolphinscheduler-ui-next/src/components/modal/index.module.scss
index 325e7c0..8044aa2 100644
--- a/dolphinscheduler-ui-next/src/components/modal/index.module.scss
+++ b/dolphinscheduler-ui-next/src/components/modal/index.module.scss
@@ -18,12 +18,3 @@
 .container {
   width: 600px;
 }
-
-.btn-box {
-  display: flex;
-  justify-content: flex-end;
-
-  button:last-child {
-    margin-left: 20px;
-  }
-}
diff --git a/dolphinscheduler-ui-next/src/components/modal/index.tsx b/dolphinscheduler-ui-next/src/components/modal/index.tsx
index 7758ddb..c8a09b8 100644
--- a/dolphinscheduler-ui-next/src/components/modal/index.tsx
+++ b/dolphinscheduler-ui-next/src/components/modal/index.tsx
@@ -16,7 +16,7 @@
  */
 
 import { defineComponent, PropType, renderSlot } from 'vue'
-import { NModal, NCard, NButton } from 'naive-ui'
+import { NModal, NCard, NButton, NSpace } from 'naive-ui'
 import { useI18n } from 'vue-i18n'
 import styles from './index.module.scss'
 
@@ -42,6 +42,10 @@ const props = {
   confirmDisabled: {
     type: Boolean as PropType<boolean>,
     default: false
+  },
+  confirmLoading: {
+    type: Boolean as PropType<boolean>,
+    default: false
   }
 }
 
@@ -63,7 +67,8 @@ const Modal = defineComponent({
     return { t, onCancel, onConfirm }
   },
   render() {
-    const { $slots, t, onCancel, onConfirm, confirmDisabled } = this
+    const { $slots, t, onCancel, onConfirm, confirmDisabled, confirmLoading } =
+      this
 
     return (
       <NModal
@@ -75,21 +80,24 @@ const Modal = defineComponent({
           {{
             default: () => renderSlot($slots, 'default'),
             footer: () => (
-              <div class={styles['btn-box']}>
+              <NSpace justify='end'>
                 {this.cancelShow && (
                   <NButton quaternary size='small' onClick={onCancel}>
                     {this.cancelText || t('modal.cancel')}
                   </NButton>
                 )}
+                {/* TODO: Add left and right slots later */}
+                {renderSlot($slots, 'btn-middle')}
                 <NButton
                   type='info'
                   size='small'
                   onClick={onConfirm}
                   disabled={confirmDisabled}
+                  loading={confirmLoading}
                 >
                   {this.confirmText || t('modal.confirm')}
                 </NButton>
-              </div>
+              </NSpace>
             )
           }}
         </NCard>
diff --git a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
index 44e1a1a..7a4286e 100644
--- a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
+++ b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
@@ -267,6 +267,54 @@ const security = {
     worker_group_tips: 'Please select worker group'
   }
 }
+const datasource = {
+  datasource: 'DataSource',
+  create_datasource: 'Create DataSource',
+  search_input_tips: 'Please input the keywords',
+  serial_number: '#',
+  datasource_name: 'Datasource Name',
+  datasource_name_tips: 'Please enter datasource name',
+  datasource_user_name: 'Owner',
+  datasource_type: 'Datasource Type',
+  datasource_parameter: 'Datasource Parameter',
+  description: 'Description',
+  description_tips: 'Please enter description',
+  create_time: 'Create Time',
+  update_time: 'Update Time',
+  operation: 'Operation',
+  click_to_view: 'Click to view',
+  delete: 'Delete',
+  confirm: 'Confirm',
+  cancel: 'Cancel',
+  create: 'Create',
+  edit: 'Edit',
+  success: 'Success',
+  test_connect: 'Test Connect',
+  ip: 'IP',
+  ip_tips: 'Please enter IP',
+  port: 'Port',
+  port_tips: 'Please enter port',
+  database_name: 'Database Name',
+  database_name_tips: 'Please enter database name',
+  oracle_connect_type: 'ServiceName or SID',
+  oracle_connect_type_tips: 'Please select serviceName or SID',
+  oracle_service_name: 'ServiceName',
+  oracle_sid: 'SID',
+  jdbc_connect_parameters: 'jdbc connect parameters',
+  principal_tips: 'Please enter Principal',
+  krb5_conf_tips:
+    'Please enter the kerberos authentication parameter java.security.krb5.conf',
+  keytab_username_tips:
+    'Please enter the kerberos authentication parameter login.user.keytab.username',
+  keytab_path_tips:
+    'Please enter the kerberos authentication parameter login.user.keytab.path',
+  format_tips: 'Please enter format',
+  connection_parameter: 'connection parameter',
+  user_name: 'User Name',
+  user_name_tips: 'Please enter your username',
+  user_password: 'Password',
+  user_password_tips: 'Please enter your password'
+}
 
 export default {
   login,
@@ -280,5 +328,6 @@ export default {
   monitor,
   resource,
   project,
-  security
+  security,
+  datasource
 }
diff --git a/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts b/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
index 1050abd..21a6911 100644
--- a/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
+++ b/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
@@ -266,6 +266,51 @@ const security = {
     worker_group_tips: '请选择Worker分组'
   }
 }
+const datasource = {
+  datasource: '数据源',
+  create_datasource: '创建数据源',
+  search_input_tips: '请输入关键字',
+  serial_number: '编号',
+  datasource_name: '数据源名称',
+  datasource_name_tips: '请输入数据源名称',
+  datasource_user_name: '所属用户',
+  datasource_type: '数据源类型',
+  datasource_parameter: '数据源参数',
+  description: '描述',
+  description_tips: '请输入描述',
+  create_time: '创建时间',
+  update_time: '更新时间',
+  operation: '操作',
+  click_to_view: '点击查看',
+  delete: '删除',
+  confirm: '确定',
+  cancel: '取消',
+  create: '创建',
+  edit: '编辑',
+  success: '成功',
+  test_connect: '测试连接',
+  ip: 'IP主机名',
+  ip_tips: '请输入IP主机名',
+  port: '端口',
+  port_tips: '请输入端口',
+  database_name: '数据库名',
+  database_name_tips: '请输入数据库名',
+  oracle_connect_type: '服务名或SID',
+  oracle_connect_type_tips: '请选择服务名或SID',
+  oracle_service_name: '服务名',
+  oracle_sid: 'SID',
+  jdbc_connect_parameters: 'jdbc连接参数',
+  principal_tips: '请输入Principal',
+  krb5_conf_tips: '请输入kerberos认证参数 java.security.krb5.conf',
+  keytab_username_tips: '请输入kerberos认证参数 login.user.keytab.username',
+  keytab_path_tips: '请输入kerberos认证参数 login.user.keytab.path',
+  format_tips: '请输入格式为',
+  connection_parameter: '连接参数',
+  user_name: '用户名',
+  user_name_tips: '请输入用户名',
+  user_password: '密码',
+  user_password_tips: '请输入密码'
+}
 
 export default {
   login,
@@ -279,5 +324,6 @@ export default {
   monitor,
   resource,
   project,
-  security
+  security,
+  datasource
 }
diff --git a/dolphinscheduler-ui-next/src/router/modules/datasource.ts b/dolphinscheduler-ui-next/src/router/modules/datasource.ts
index 1c4e7a9..2c88482 100644
--- a/dolphinscheduler-ui-next/src/router/modules/datasource.ts
+++ b/dolphinscheduler-ui-next/src/router/modules/datasource.ts
@@ -32,7 +32,7 @@ export default {
     {
       path: '/datasource/list',
       name: 'datasource-list',
-      component: components['home'],
+      component: components['datasource-list'],
       meta: {
         title: '数据源中心'
       }
diff --git a/dolphinscheduler-ui-next/src/service/modules/data-source/index.ts b/dolphinscheduler-ui-next/src/service/modules/data-source/index.ts
index 78eb556..b3aae9b 100644
--- a/dolphinscheduler-ui-next/src/service/modules/data-source/index.ts
+++ b/dolphinscheduler-ui-next/src/service/modules/data-source/index.ts
@@ -18,7 +18,7 @@
 import { axios } from '@/service/service'
 import {
   ListReq,
-  DataSourceReq,
+  IDataSource,
   UserIdReq,
   TypeReq,
   NameReq,
@@ -33,11 +33,15 @@ export function queryDataSourceListPaging(params: ListReq): any {
   })
 }
 
-export function createDataSource(data: DataSourceReq): any {
+export function createDataSource(data: IDataSource): any {
   return axios({
     url: '/datasources',
     method: 'post',
-    data
+    data,
+    headers: {
+      'Content-Type': 'application/json;charset=UTF-8'
+    },
+    transformRequest: (params) => JSON.stringify(params)
   })
 }
 
@@ -49,11 +53,15 @@ export function authedDatasource(params: UserIdReq): any {
   })
 }
 
-export function connectDataSource(data: DataSourceReq): any {
+export function connectDataSource(data: IDataSource): any {
   return axios({
     url: '/datasources/connect',
     method: 'post',
-    data
+    data,
+    headers: {
+      'Content-Type': 'application/json;charset=UTF-8'
+    },
+    transformRequest: (params) => JSON.stringify(params)
   })
 }
 
@@ -95,11 +103,15 @@ export function queryDataSource(id: IdReq): any {
   })
 }
 
-export function updateDataSource(data: DataSourceReq, id: IdReq): any {
+export function updateDataSource(data: IDataSource, id: IdReq): any {
   return axios({
     url: `/datasources/${id}`,
     method: 'put',
-    data
+    data,
+    headers: {
+      'Content-Type': 'application/json;charset=UTF-8'
+    },
+    transformRequest: (params) => JSON.stringify(params)
   })
 }
 
diff --git a/dolphinscheduler-ui-next/src/service/modules/data-source/types.ts b/dolphinscheduler-ui-next/src/service/modules/data-source/types.ts
index 212e965..e17c6a2 100644
--- a/dolphinscheduler-ui-next/src/service/modules/data-source/types.ts
+++ b/dolphinscheduler-ui-next/src/service/modules/data-source/types.ts
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-type DataBase =
+type IDataBase =
   | 'MYSQL'
   | 'POSTGRESQL'
   | 'HIVE'
@@ -25,19 +25,23 @@ type DataBase =
   | 'SQLSERVER'
   | 'DB2'
   | 'PRESTO'
-  | 'H2'
 
-interface DataSource {
-  database?: string
-  host?: string
+interface IDataSource {
   id?: number
+  type?: IDataBase
   name?: string
   note?: string
-  other?: object
-  password?: string
+  host?: string
   port?: number
-  type?: DataBase
+  principal?: string
+  javaSecurityKrb5Conf?: string
+  loginUserKeytabUsername?: string
+  loginUserKeytabPath?: string
   userName?: string
+  password?: string
+  database?: string
+  connectType?: string
+  other?: object
 }
 
 interface ListReq {
@@ -46,24 +50,18 @@ interface ListReq {
   searchVal?: string
 }
 
-interface DataSourceReq {
-  dataSourceParam: DataSource
-}
-
 interface UserIdReq {
   userId: number
 }
 
 interface TypeReq {
-  type: DataBase
+  type: IDataBase
 }
 
 interface NameReq {
   name: string
 }
 
-interface IdReq {
-  id: number
-}
+type IdReq = number
 
-export { ListReq, DataSourceReq, UserIdReq, TypeReq, NameReq, IdReq }
+export { ListReq, IDataBase, IDataSource, UserIdReq, TypeReq, NameReq, IdReq }
diff --git a/dolphinscheduler-ui-next/src/views/datasource/datasource-list/detail.tsx b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/detail.tsx
new file mode 100644
index 0000000..93c7a8e
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/detail.tsx
@@ -0,0 +1,332 @@
+/*
+ * 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 { defineComponent, PropType, toRefs, watch } from 'vue'
+import {
+  NButton,
+  NSpin,
+  NForm,
+  NFormItem,
+  NSelect,
+  NInput,
+  NInputNumber,
+  NRadioGroup,
+  NRadio,
+  NSpace
+} from 'naive-ui'
+import Modal from '@/components/modal'
+import { useI18n } from 'vue-i18n'
+import { useForm, datasourceTypeList } from './use-form'
+import { useDetail } from './use-detail'
+
+const props = {
+  show: {
+    type: Boolean as PropType<boolean>,
+    default: false
+  },
+  id: {
+    type: Number as PropType<number>
+  }
+}
+
+const DetailModal = defineComponent({
+  name: 'DetailModal',
+  props,
+  emits: ['cancel', 'update'],
+  setup(props, ctx) {
+    const { t } = useI18n()
+
+    const {
+      state,
+      changeType,
+      changePort,
+      resetFieldsValue,
+      setFieldsValue,
+      getFieldsValue
+    } = useForm(props.id)
+
+    const { status, queryById, testConnect, createOrUpdate } =
+      useDetail(getFieldsValue)
+
+    const onCancel = () => {
+      resetFieldsValue()
+      ctx.emit('cancel')
+    }
+
+    const onSubmit = async () => {
+      await state.detailFormRef.validate()
+      const res = await createOrUpdate(props.id)
+      if (res) {
+        onCancel()
+        ctx.emit('update')
+      }
+    }
+
+    const onTest = async () => {
+      await state.detailFormRef.validate()
+      testConnect()
+    }
+
+    const onChangeType = changeType
+    const onChangePort = changePort
+
+    watch(
+      () => props.show,
+      async () => {
+        props.show && props.id && setFieldsValue(await queryById(props.id))
+      }
+    )
+
+    return {
+      t,
+      ...toRefs(state),
+      ...toRefs(status),
+      onChangeType,
+      onChangePort,
+      onSubmit,
+      onTest,
+      onCancel
+    }
+  },
+  render() {
+    const {
+      show,
+      id,
+      t,
+      detailForm,
+      rules,
+      requiredDataBase,
+      showConnectType,
+      showPrincipal,
+      loading,
+      saving,
+      testing,
+      onChangeType,
+      onChangePort,
+      onCancel,
+      onTest,
+      onSubmit
+    } = this
+    return (
+      <Modal
+        show={show}
+        title={`${t(id ? 'datasource.edit' : 'datasource.create')}${t(
+          'datasource.datasource'
+        )}`}
+        onConfirm={onSubmit}
+        confirmLoading={saving || loading}
+        onCancel={onCancel}
+      >
+        {{
+          default: () => (
+            <NSpin show={loading}>
+              <NForm
+                rules={rules}
+                ref='detailFormRef'
+                require-mark-placement='left'
+                label-placement='left'
+                label-width={180}
+                label-align='right'
+              >
+                <NFormItem
+                  label={t('datasource.datasource')}
+                  path='type'
+                  show-require-mark
+                >
+                  <NSelect
+                    v-model={[detailForm.type, 'value']}
+                    options={datasourceTypeList}
+                    disabled={!!id}
+                    on-update:value={onChangeType}
+                  />
+                </NFormItem>
+                <NFormItem
+                  label={t('datasource.datasource_name')}
+                  path='name'
+                  show-require-mark
+                >
+                  <NInput
+                    v-model={[detailForm.name, 'value']}
+                    maxlength={60}
+                    placeholder={t('datasource.datasource_name_tips')}
+                  />
+                </NFormItem>
+                <NFormItem label={t('datasource.description')} path='note'>
+                  <NInput
+                    v-model={[detailForm.note, 'value']}
+                    type='textarea'
+                    placeholder={t('datasource.description_tips')}
+                  />
+                </NFormItem>
+                <NFormItem
+                  label={t('datasource.ip')}
+                  path='host'
+                  show-require-mark
+                >
+                  <NInput
+                    v-model={[detailForm.host, 'value']}
+                    type='text'
+                    maxlength={255}
+                    placeholder={t('datasource.ip_tips')}
+                  />
+                </NFormItem>
+                <NFormItem
+                  label={t('datasource.port')}
+                  path='port'
+                  show-require-mark
+                >
+                  <NInputNumber
+                    v-model={[detailForm.port, 'value']}
+                    show-button={false}
+                    placeholder={t('datasource.port_tips')}
+                    on-blur={onChangePort}
+                    style={{ width: '100%' }}
+                  />
+                </NFormItem>
+                <NFormItem
+                  v-show={showPrincipal}
+                  label='Principal'
+                  path='principal'
+                  show-require-mark
+                >
+                  <NInput
+                    v-model={[detailForm.principal, 'value']}
+                    type='text'
+                    placeholder={t('datasource.principal_tips')}
+                  />
+                </NFormItem>
+                <NFormItem
+                  v-show={showPrincipal}
+                  label='krb5.conf'
+                  path='javaSecurityKrb5Conf'
+                >
+                  <NInput
+                    v-model={[detailForm.javaSecurityKrb5Conf, 'value']}
+                    type='text'
+                    placeholder={t('datasource.krb5_conf_tips')}
+                  />
+                </NFormItem>
+                <NFormItem
+                  v-show={showPrincipal}
+                  label='keytab.username'
+                  path='loginUserKeytabUsername'
+                >
+                  <NInput
+                    v-model={[detailForm.loginUserKeytabUsername, 'value']}
+                    type='text'
+                    placeholder={t('datasource.keytab_username_tips')}
+                  />
+                </NFormItem>
+                <NFormItem
+                  v-show={showPrincipal}
+                  label='keytab.path'
+                  path='loginUserKeytabPath'
+                >
+                  <NInput
+                    v-model={[detailForm.loginUserKeytabPath, 'value']}
+                    type='text'
+                    placeholder={t('datasource.keytab_path_tips')}
+                  />
+                </NFormItem>
+                <NFormItem
+                  label={t('datasource.user_name')}
+                  path='userName'
+                  show-require-mark
+                >
+                  <NInput
+                    v-model={[detailForm.userName, 'value']}
+                    type='text'
+                    maxlength={60}
+                    placeholder={t('datasource.user_name_tips')}
+                  />
+                </NFormItem>
+                <NFormItem
+                  label={t('datasource.user_password')}
+                  path='password'
+                >
+                  <NInput
+                    v-model={[detailForm.password, 'value']}
+                    type='password'
+                    placeholder={t('datasource.user_password_tips')}
+                  />
+                </NFormItem>
+                <NFormItem
+                  label={t('datasource.database_name')}
+                  path='database'
+                  show-require-mark={requiredDataBase}
+                >
+                  <NInput
+                    v-model={[detailForm.database, 'value']}
+                    type='text'
+                    maxlength={60}
+                    placeholder={t('datasource.database_name_tips')}
+                  />
+                </NFormItem>
+                <NFormItem
+                  v-show={showConnectType}
+                  label={t('datasource.oracle_connect_type')}
+                  path='connectType'
+                  show-require-mark
+                >
+                  <NRadioGroup v-model={[detailForm.connectType, 'value']}>
+                    <NSpace>
+                      <NRadio value='ORACLE_SERVICE_NAME'>
+                        {t('datasource.oracle_service_name')}
+                      </NRadio>
+                      <NRadio value='ORACLE_SID'>
+                        {t('datasource.oracle_sid')}
+                      </NRadio>
+                    </NSpace>
+                  </NRadioGroup>
+                </NFormItem>
+                <NFormItem
+                  label={t('datasource.jdbc_connect_parameters')}
+                  path='other'
+                >
+                  <NInput
+                    v-model={[detailForm.other, 'value']}
+                    type='textarea'
+                    autosize={{
+                      minRows: 2
+                    }}
+                    placeholder={`${t(
+                      'datasource.format_tips'
+                    )} {"key1":"value1","key2":"value2"...} ${t(
+                      'datasource.connection_parameter'
+                    )}`}
+                  />
+                </NFormItem>
+              </NForm>
+            </NSpin>
+          ),
+          'btn-middle': () => (
+            <NButton
+              type='primary'
+              size='small'
+              onClick={onTest}
+              loading={testing || loading}
+            >
+              {t('datasource.test_connect')}
+            </NButton>
+          )
+        }}
+      </Modal>
+    )
+  }
+})
+
+export default DetailModal
diff --git a/dolphinscheduler-ui-next/src/components/modal/index.module.scss b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/index.module.scss
similarity index 79%
copy from dolphinscheduler-ui-next/src/components/modal/index.module.scss
copy to dolphinscheduler-ui-next/src/views/datasource/datasource-list/index.module.scss
index 325e7c0..fc5f4cc 100644
--- a/dolphinscheduler-ui-next/src/components/modal/index.module.scss
+++ b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/index.module.scss
@@ -15,15 +15,18 @@
  * limitations under the License.
  */
 
-.container {
-  width: 600px;
-}
-
-.btn-box {
+.conditions {
   display: flex;
-  justify-content: flex-end;
-
-  button:last-child {
-    margin-left: 20px;
-  }
+  justify-content: space-between;
+  align-items: center;
+}
+.conditions-search-input {
+  width: 250px;
+}
+.pagination {
+  margin-top: 20px;
+  justify-content: center;
+}
+.mt-8 {
+  margin-top: 8px;
 }
diff --git a/dolphinscheduler-ui-next/src/views/datasource/datasource-list/index.tsx b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/index.tsx
new file mode 100644
index 0000000..01d6d2e
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/index.tsx
@@ -0,0 +1,150 @@
+/*
+ * 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 { defineComponent, onMounted, ref, toRefs } from 'vue'
+import {
+  NButton,
+  NInput,
+  NIcon,
+  NDataTable,
+  NPagination,
+  NSpace
+} from 'naive-ui'
+import Card from '@/components/card'
+import DetailModal from './detail'
+import { SearchOutlined } from '@vicons/antd'
+import { useI18n } from 'vue-i18n'
+import { useColumns } from './use-columns'
+import { useTable } from './use-table'
+import styles from './index.module.scss'
+
+const list = defineComponent({
+  name: 'list',
+  setup() {
+    const { t } = useI18n()
+    let showDetailModal = ref(false)
+    let selectId = ref()
+
+    const { columnsRef } = useColumns((id: number, type: 'edit' | 'delete') => {
+      if (type === 'edit') {
+        showDetailModal.value = true
+        selectId.value = id
+      } else {
+        deleteRecord(id)
+      }
+    })
+
+    const { data, changePage, changePageSize, deleteRecord, updateList } =
+      useTable()
+
+    const onCreate = () => {
+      selectId.value = null
+      showDetailModal.value = true
+    }
+
+    onMounted(() => {
+      changePage(1)
+    })
+
+    return {
+      t,
+      showDetailModal,
+      id: selectId,
+      columnsRef,
+      ...toRefs(data),
+      changePage,
+      changePageSize,
+      onCreate,
+      onUpdatedList: updateList
+    }
+  },
+  render() {
+    const {
+      t,
+      id,
+      showDetailModal,
+      columnsRef,
+      list,
+      page,
+      pageSize,
+      itemCount,
+      loading,
+      changePage,
+      changePageSize,
+      onCreate,
+      onUpdatedList
+    } = this
+
+    return (
+      <>
+        <Card title=''>
+          {{
+            default: () => (
+              <div class={styles['conditions']}>
+                <NButton onClick={onCreate} type='primary'>{`${t(
+                  'datasource.create_datasource'
+                )}`}</NButton>
+
+                <NSpace
+                  class={styles['conditions-search']}
+                  justify='end'
+                  wrap={false}
+                >
+                  <div class={styles['conditions-search-input']}>
+                    <NInput
+                      v-model={[this.searchVal, 'value']}
+                      placeholder={`${t('datasource.search_input_tips')}`}
+                    />
+                  </div>
+                  <NButton type='primary' onClick={onUpdatedList}>
+                    <NIcon>
+                      <SearchOutlined />
+                    </NIcon>
+                  </NButton>
+                </NSpace>
+              </div>
+            )
+          }}
+        </Card>
+        <Card title='' class={styles['mt-8']}>
+          <NDataTable
+            columns={columnsRef}
+            data={list}
+            loading={loading}
+            striped
+          />
+          <NPagination
+            page={page}
+            page-size={pageSize}
+            item-count={itemCount}
+            show-quick-jumper
+            class={styles['pagination']}
+            on-update:page={changePage}
+            on-update:page-size={changePageSize}
+          />
+        </Card>
+        <DetailModal
+          show={showDetailModal}
+          id={id}
+          onCancel={() => void (this.showDetailModal = false)}
+          onUpdate={onUpdatedList}
+        />
+      </>
+    )
+  }
+})
+export default list
diff --git a/dolphinscheduler-ui-next/src/components/modal/index.module.scss b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/json-highlight.module.scss
similarity index 85%
copy from dolphinscheduler-ui-next/src/components/modal/index.module.scss
copy to dolphinscheduler-ui-next/src/views/datasource/datasource-list/json-highlight.module.scss
index 325e7c0..a6f09f0 100644
--- a/dolphinscheduler-ui-next/src/components/modal/index.module.scss
+++ b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/json-highlight.module.scss
@@ -15,15 +15,12 @@
  * limitations under the License.
  */
 
-.container {
-  width: 600px;
+.json-highlight {
+  display: block;
+  line-height: 1.5;
+  font-size: 12px;
+  padding: 5px;
 }
-
-.btn-box {
-  display: flex;
-  justify-content: flex-end;
-
-  button:last-child {
-    margin-left: 20px;
-  }
+.line {
+  padding-left: 8px;
 }
diff --git a/dolphinscheduler-ui-next/src/views/datasource/datasource-list/json-highlight.tsx b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/json-highlight.tsx
new file mode 100644
index 0000000..474e3ca
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/json-highlight.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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 { defineComponent, PropType, h } from 'vue'
+import { NText } from 'naive-ui'
+import { isBoolean, isNumber, isPlainObject } from 'lodash'
+import styles from './json-highlight.module.scss'
+
+const props = {
+  json: {
+    type: String as PropType<string>,
+    default: ''
+  }
+}
+
+const JsonHighlight = defineComponent({
+  name: 'JsonHighlight',
+  props,
+  render() {
+    return (
+      <pre class={styles['json-highlight']}>{syntaxHighlight(this.json)}</pre>
+    )
+  }
+})
+
+const syntaxHighlight = (json: string) => {
+  if (!isPlainObject(JSON.parse(json))) return ''
+  const lines = [<NText v-html='{'></NText>]
+  const entries = Object.entries(JSON.parse(json))
+  for (let i = 0, len = entries.length; i < len; i++) {
+    const [key, value] = entries[i]
+    let type: string = ''
+    if (isBoolean(value) || value === null) {
+      type = 'info'
+    } else if (isNumber(value)) {
+      type = 'warning'
+    } else {
+      type = 'success'
+    }
+    lines.push(
+      <NText tag='div' class={styles['line']}>
+        <NText type='error'>"{key}": </NText>
+        <NText type={type}>
+          "{value}"{i !== len - 1 ? ',' : ''}
+        </NText>
+      </NText>
+    )
+  }
+  lines.push(<NText v-html='}'></NText>)
+  return lines
+}
+
+export default JsonHighlight
diff --git a/dolphinscheduler-ui-next/src/components/modal/index.module.scss b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/types.ts
similarity index 56%
copy from dolphinscheduler-ui-next/src/components/modal/index.module.scss
copy to dolphinscheduler-ui-next/src/views/datasource/datasource-list/types.ts
index 325e7c0..a3dc8b3 100644
--- a/dolphinscheduler-ui-next/src/components/modal/index.module.scss
+++ b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/types.ts
@@ -15,15 +15,33 @@
  * limitations under the License.
  */
 
-.container {
-  width: 600px;
+import type {
+  IDataSource,
+  IDataBase
+} from '@/service/modules/data-source/types'
+import type { TableColumns } from 'naive-ui/es/data-table/src/interface'
+import type { SelectBaseOption } from 'naive-ui/es/select/src/interface'
+
+interface IDataSourceDetail extends Omit<IDataSource, 'other'> {
+  other?: string
+}
+
+interface IDataBaseOption extends SelectBaseOption {
+  label: string
+  value: string
+  defaultPort: number
+  previousPort?: number
 }
 
-.btn-box {
-  display: flex;
-  justify-content: flex-end;
+type IDataBaseOptionKeys = {
+  [key in IDataBase]: IDataBaseOption
+}
 
-  button:last-child {
-    margin-left: 20px;
-  }
+export {
+  IDataSource,
+  IDataSourceDetail,
+  IDataBase,
+  IDataBaseOption,
+  IDataBaseOptionKeys,
+  TableColumns
 }
diff --git a/dolphinscheduler-ui-next/src/views/datasource/datasource-list/use-columns.ts b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/use-columns.ts
new file mode 100644
index 0000000..57ec39c
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/use-columns.ts
@@ -0,0 +1,136 @@
+/*
+ * 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 { h } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { NPopover, NButton, NIcon, NPopconfirm, NSpace } from 'naive-ui'
+import { EditOutlined, DeleteOutlined } from '@vicons/antd'
+import JsonHighlight from './json-highlight'
+import styles from './index.module.scss'
+import { TableColumns } from './types'
+
+export function useColumns(onCallback: Function) {
+  const { t } = useI18n()
+
+  const columnsRef: TableColumns = [
+    {
+      title: t('datasource.serial_number'),
+      key: 'index',
+      render: (rowData, rowIndex) => rowIndex + 1
+    },
+    {
+      title: t('datasource.datasource_name'),
+      key: 'name'
+    },
+    {
+      title: t('datasource.datasource_user_name'),
+      key: 'userName'
+    },
+    {
+      title: t('datasource.datasource_type'),
+      key: 'type'
+    },
+    {
+      title: t('datasource.datasource_parameter'),
+      key: 'parameter',
+      render: (rowData) => {
+        return h(
+          NPopover,
+          { trigger: 'click' },
+          {
+            trigger: () =>
+              h(
+                NButton,
+                {
+                  quaternary: true,
+                  type: 'primary'
+                },
+                {
+                  default: () => t('datasource.click_to_view')
+                }
+              ),
+            default: () =>
+              h(JsonHighlight, { json: rowData.connectionParams }, null)
+          }
+        )
+      }
+    },
+    {
+      title: t('datasource.description'),
+      key: 'note'
+    },
+    {
+      title: t('datasource.create_time'),
+      key: 'createTime'
+    },
+    {
+      title: t('datasource.update_time'),
+      key: 'updateTime'
+    },
+    {
+      title: t('datasource.operation'),
+      key: 'operation',
+      width: 150,
+      render: (rowData, rowIndex) => {
+        return h(NSpace, null, {
+          default: () => [
+            h(
+              NButton,
+              {
+                circle: true,
+                class: styles['mr-10'],
+                type: 'info',
+                onClick: () => void onCallback(rowData.id, 'edit')
+              },
+              {
+                default: () =>
+                  h(NIcon, null, { default: () => h(EditOutlined) })
+              }
+            ),
+            h(
+              NPopconfirm,
+              {
+                onPositiveClick: () => void onCallback(rowData.id, 'delete'),
+                negativeText: t('datasource.cancel'),
+                positiveText: t('datasource.confirm')
+              },
+              {
+                trigger: () =>
+                  h(
+                    NButton,
+                    {
+                      circle: true,
+                      type: 'error'
+                    },
+                    {
+                      default: () =>
+                        h(NIcon, null, { default: () => h(DeleteOutlined) })
+                    }
+                  ),
+                default: () => t('datasource.delete')
+              }
+            )
+          ]
+        })
+      }
+    }
+  ]
+
+  return {
+    columnsRef
+  }
+}
diff --git a/dolphinscheduler-ui-next/src/views/datasource/datasource-list/use-detail.ts b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/use-detail.ts
new file mode 100644
index 0000000..a8d0f33
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/use-detail.ts
@@ -0,0 +1,103 @@
+/*
+ * 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 { reactive } from 'vue'
+import {
+  queryDataSource,
+  createDataSource,
+  updateDataSource,
+  connectDataSource,
+  verifyDataSourceName
+} from '@/service/modules/data-source'
+import { useI18n } from 'vue-i18n'
+import type { IDataSource } from './types'
+
+export function useDetail(getFieldsValue: Function) {
+  const { t } = useI18n()
+  const status = reactive({
+    saving: false,
+    testing: false,
+    loading: false
+  })
+
+  let PREV_NAME: string
+
+  const formatParams = (): IDataSource => {
+    const values = getFieldsValue()
+    return {
+      ...values,
+      other: values.other ? JSON.parse(values.other) : null
+    }
+  }
+
+  const queryById = async (id: number) => {
+    if (status.loading) return {}
+    status.loading = true
+    try {
+      const dataSourceRes = await queryDataSource(id)
+      status.loading = false
+      PREV_NAME = dataSourceRes.name
+      return dataSourceRes
+    } catch (e) {
+      window.$message.error((e as Error).message)
+      status.loading = false
+      return {}
+    }
+  }
+
+  const testConnect = async () => {
+    if (status.testing) return
+    status.testing = true
+    try {
+      const res = await connectDataSource(formatParams())
+      window.$message.success(
+        res
+          ? res.msg
+          : `${t('datasource.test_connect')} ${t('datasource.success')}`
+      )
+      status.testing = false
+    } catch (e) {
+      window.$message.error((e as Error).message)
+      status.testing = false
+    }
+  }
+
+  const createOrUpdate = async (id?: number) => {
+    const values = getFieldsValue()
+    if (status.saving || !values.name) return false
+    status.saving = true
+
+    try {
+      if (PREV_NAME !== values.name) {
+        await verifyDataSourceName({ name: values.name })
+      }
+
+      id
+        ? await updateDataSource(formatParams(), id)
+        : await createDataSource(formatParams())
+
+      status.saving = false
+      return true
+    } catch (e) {
+      window.$message.error((e as Error).message)
+      status.saving = false
+      return false
+    }
+  }
+
+  return { status, queryById, testConnect, createOrUpdate }
+}
diff --git a/dolphinscheduler-ui-next/src/views/datasource/datasource-list/use-form.ts b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/use-form.ts
new file mode 100644
index 0000000..7ff6765
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/use-form.ts
@@ -0,0 +1,209 @@
+/*
+ * 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 { reactive, ref } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { getKerberosStartupState } from '@/service/modules/data-source'
+import type { FormRules } from 'naive-ui'
+import type {
+  IDataSourceDetail,
+  IDataBase,
+  IDataBaseOption,
+  IDataBaseOptionKeys
+} from './types'
+
+export function useForm(id?: number) {
+  const { t } = useI18n()
+
+  const initialValues = {
+    type: 'MYSQL',
+    name: '',
+    note: '',
+    host: '',
+    port: datasourceType['MYSQL'].defaultPort,
+    principal: '',
+    javaSecurityKrb5Conf: '',
+    loginUserKeytabUsername: '',
+    loginUserKeytabPath: '',
+    userName: '',
+    password: '',
+    database: '',
+    connectType: '',
+    other: ''
+  } as IDataSourceDetail
+
+  const state = reactive({
+    detailFormRef: ref(),
+    detailForm: { ...initialValues },
+    requiredDataBase: true,
+    showConnectType: false,
+    showPrincipal: false,
+    rules: {
+      name: {
+        trigger: ['input'],
+        validator() {
+          if (!state.detailForm.name) {
+            return new Error(t('datasource.datasource_name_tips'))
+          }
+        }
+      },
+      host: {
+        trigger: ['input'],
+        validator() {
+          if (!state.detailForm.host) {
+            return new Error(t('datasource.ip_tips'))
+          }
+        }
+      },
+      port: {
+        trigger: ['input'],
+        validator() {
+          if (!state.detailForm.port) {
+            return new Error(t('datasource.port_tips'))
+          }
+        }
+      },
+      principal: {
+        trigger: ['input'],
+        validator() {
+          if (!state.detailForm.principal && state.showPrincipal) {
+            return new Error(t('datasource.principal_tips'))
+          }
+        }
+      },
+      userName: {
+        trigger: ['input'],
+        validator() {
+          if (!state.detailForm.userName) {
+            return new Error(t('datasource.user_name_tips'))
+          }
+        }
+      },
+      database: {
+        trigger: ['input'],
+        validator() {
+          if (!state.detailForm.database && state.requiredDataBase) {
+            return new Error(t('datasource.database_name_tips'))
+          }
+        }
+      },
+      connectType: {
+        trigger: ['update'],
+        validator() {
+          if (!state.detailForm.connectType && state.showConnectType) {
+            return new Error(t('datasource.oracle_connect_type_tips'))
+          }
+        }
+      }
+    } as FormRules
+  })
+
+  const changeType = async (type: IDataBase, options: IDataBaseOption) => {
+    state.detailForm.port = options.previousPort || options.defaultPort
+    state.detailForm.type = type
+
+    if (type === 'ORACLE' && !id) {
+      state.detailForm.connectType = 'ORACLE_SERVICE_NAME'
+    }
+    state.requiredDataBase = type !== 'POSTGRESQL'
+    state.showConnectType = type === 'ORACLE'
+
+    if (type === 'HIVE' || type === 'SPARK') {
+      try {
+        state.showPrincipal = await getKerberosStartupState()
+      } catch (e) {
+        window.$message.error((e as Error).message)
+      }
+    } else {
+      state.showPrincipal = false
+    }
+  }
+
+  const changePort = async () => {
+    if (!state.detailForm.type) return
+    const currentDataBaseOption = datasourceType[state.detailForm.type]
+    currentDataBaseOption.previousPort = state.detailForm.port
+  }
+
+  const resetFieldsValue = () => {
+    state.detailForm = { ...initialValues }
+  }
+  const setFieldsValue = (values: object) => {
+    state.detailForm = { ...state.detailForm, ...values }
+  }
+  const getFieldsValue = () => state.detailForm
+
+  return {
+    state,
+    changeType,
+    changePort,
+    resetFieldsValue,
+    setFieldsValue,
+    getFieldsValue
+  }
+}
+
+const datasourceType: IDataBaseOptionKeys = {
+  MYSQL: {
+    value: 'MYSQL',
+    label: 'MYSQL',
+    defaultPort: 3306
+  },
+  POSTGRESQL: {
+    value: 'POSTGRESQL',
+    label: 'POSTGRESQL',
+    defaultPort: 5432
+  },
+  HIVE: {
+    value: 'HIVE',
+    label: 'HIVE/IMPALA',
+    defaultPort: 10000
+  },
+  SPARK: {
+    value: 'SPARK',
+    label: 'SPARK',
+    defaultPort: 10015
+  },
+  CLICKHOUSE: {
+    value: 'CLICKHOUSE',
+    label: 'CLICKHOUSE',
+    defaultPort: 8123
+  },
+  ORACLE: {
+    value: 'ORACLE',
+    label: 'ORACLE',
+    defaultPort: 1521
+  },
+  SQLSERVER: {
+    value: 'SQLSERVER',
+    label: 'SQLSERVER',
+    defaultPort: 1433
+  },
+  DB2: {
+    value: 'DB2',
+    label: 'DB2',
+    defaultPort: 50000
+  },
+  PRESTO: {
+    value: 'PRESTO',
+    label: 'PRESTO',
+    defaultPort: 8080
+  }
+}
+
+export const datasourceTypeList: IDataBaseOption[] =
+  Object.values(datasourceType)
diff --git a/dolphinscheduler-ui-next/src/views/datasource/datasource-list/use-table.ts b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/use-table.ts
new file mode 100644
index 0000000..bcb3c3d
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/use-table.ts
@@ -0,0 +1,82 @@
+/*
+ * 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 { reactive } from 'vue'
+import {
+  queryDataSourceListPaging,
+  deleteDataSource
+} from '@/service/modules/data-source'
+
+export function useTable() {
+  const data = reactive({
+    page: 1,
+    pageSize: 10,
+    itemCount: 0,
+    searchVal: '',
+    list: [],
+    loading: false
+  })
+
+  const getList = async () => {
+    if (data.loading) return
+    data.loading = true
+
+    try {
+      const listRes = await queryDataSourceListPaging({
+        pageNo: data.page,
+        pageSize: data.pageSize,
+        searchVal: data.searchVal
+      })
+      data.loading = false
+      data.list = listRes.totalList
+      data.itemCount = listRes.total
+    } catch (e) {
+      window.$message.error((e as Error).message)
+      data.loading = false
+      data.list = []
+    }
+  }
+
+  const updateList = () => {
+    if (data.list.length === 1 && data.page > 1) {
+      --data.page
+    }
+    getList()
+  }
+
+  const deleteRecord = async (id: number) => {
+    try {
+      const res = await deleteDataSource(id)
+      updateList()
+    } catch (e) {
+      window.$message.error((e as Error).message)
+    }
+  }
+
+  const changePage = (page: number) => {
+    data.page = page
+    getList()
+  }
+
+  const changePageSize = (pageSize: number) => {
+    data.page = 1
+    data.pageSize = pageSize
+    getList()
+  }
+
+  return { data, changePage, changePageSize, deleteRecord, updateList }
+}