Pārlūkot izejas kodu

[Feature][UI Next] Add udf function manage (#8126)

Devosend 3 gadi atpakaļ
vecāks
revīzija
62198fda24

+ 32 - 0
dolphinscheduler-ui-next/src/locales/modules/en_US.ts

@@ -215,6 +215,38 @@ const resource = {
     enter_name_tips: 'Please enter name',
     enter_description_tips: 'Please enter description'
   },
+  function: {
+    udf_function: 'UDF Function',
+    create_udf_function: 'Create UDF Function',
+    edit_udf_function: 'Create UDF Function',
+    id: '#',
+    udf_function_name: 'UDF Function Name',
+    class_name: 'Class Name',
+    type: 'Type',
+    description: 'Description',
+    jar_package: 'Jar Package',
+    update_time: 'Update Time',
+    operation: 'Operation',
+    rename: 'Rename',
+    edit: 'Edit',
+    delete: 'Delete',
+    success: 'Success',
+    package_name: 'Package Name',
+    udf_resources: 'UDF Resources',
+    instructions: 'Instructions',
+    upload_resources: 'Upload Resources',
+    udf_resources_directory: 'UDF resources directory',
+    delete_confirm: 'Delete?',
+    enter_keyword_tips: 'Please enter keyword',
+    enter_udf_unction_name_tips: 'Please enter a UDF function name',
+    enter_package_name_tips: 'Please enter a Package name',
+    enter_select_udf_resources_tips: 'Please select UDF resources',
+    enter_select_udf_resources_directory_tips:
+      'Please select UDF resources directory',
+    enter_instructions_tips: 'Please enter a instructions',
+    enter_name_tips: 'Please enter name',
+    enter_description_tips: 'Please enter description'
+  },
   task_group_option: {
     id: 'No.',
     manage: 'Task group manage',

+ 31 - 0
dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts

@@ -217,6 +217,37 @@ const resource = {
     enter_name_tips: '请输入名称',
     enter_description_tips: '请输入描述'
   },
+  function: {
+    udf_function: 'UDF函数',
+    create_udf_function: '创建UDF函数',
+    edit_udf_function: '编辑UDF函数',
+    id: '编号',
+    udf_function_name: 'UDF函数名称',
+    class_name: '类名',
+    type: '类型',
+    description: '描述',
+    jar_package: 'jar包',
+    update_time: '更新时间',
+    operation: '操作',
+    rename: '重命名',
+    edit: '编辑',
+    delete: '删除',
+    success: '成功',
+    package_name: '包名类名',
+    udf_resources: 'UDF资源',
+    instructions: '使用说明',
+    upload_resources: '上传资源',
+    udf_resources_directory: 'UDF资源目录',
+    delete_confirm: '确定删除吗?',
+    enter_keyword_tips: '请输入关键词',
+    enter_udf_unction_name_tips: '请输入UDF函数名称',
+    enter_package_name_tips: '请输入包名类名',
+    enter_select_udf_resources_tips: '请选择UDF资源',
+    enter_select_udf_resources_directory_tips: '请选择UDF资源目录',
+    enter_instructions_tips: '请输入使用说明',
+    enter_name_tips: '请输入名称',
+    enter_description_tips: '请输入描述'
+  },
   task_group_option: {
     id: '编号',
     manage: '任务组管理',

+ 8 - 0
dolphinscheduler-ui-next/src/router/modules/resources.ts

@@ -101,6 +101,14 @@ export default {
         showSide: true
       }
     },
+    {
+      path: '/resource/function-manage',
+      name: 'function-manage',
+      component: components['function'],
+      meta: {
+        title: '函数管理'
+      }
+    },
     {
       path: '/resource/task-group',
       name: 'task-group-manage',

+ 4 - 6
dolphinscheduler-ui-next/src/service/modules/resources/index.ts

@@ -118,9 +118,7 @@ export function onlineCreateResource(
   })
 }
 
-export function queryResourceByProgramType(
-  params: ResourceTypeReq & ProgramTypeReq
-): any {
+export function queryResourceByProgramType(params: ResourceTypeReq): any {
   return axios({
     url: '/resources/query-by-type',
     method: 'get',
@@ -136,7 +134,7 @@ export function queryUdfFuncListPaging(params: ListReq): any {
   })
 }
 
-export function queryUdfFuncList(params: UdfTypeReq): any {
+export function queryUdfFuncList(params: IdReq & ListReq): any {
   return axios({
     url: '/resources/udf-func/list',
     method: 'get',
@@ -152,7 +150,7 @@ export function verifyUdfFuncName(params: NameReq): any {
   })
 }
 
-export function deleteUdfFunc(id: IdReq): any {
+export function deleteUdfFunc(id: number): any {
   return axios({
     url: `/resources/udf-func/${id}`,
     method: 'delete'
@@ -245,7 +243,7 @@ export function createUdfFunc(
 export function updateUdfFunc(
   data: UdfFuncReq,
   resourceId: ResourceIdReq,
-  id: IdReq
+  id: number
 ): any {
   return axios({
     url: `/resources/${resourceId}/udf-func/${id}`,

+ 1 - 1
dolphinscheduler-ui-next/src/service/modules/resources/types.ts

@@ -83,7 +83,7 @@ interface ResourceIdReq {
   resourceId: number
 }
 
-interface UdfFuncReq extends UdfTypeReq, DescriptionReq {
+interface UdfFuncReq extends UdfTypeReq, DescriptionReq, ResourceIdReq {
   className: string
   funcName: string
   argTypes?: string

+ 269 - 0
dolphinscheduler-ui-next/src/views/resource/udf/function/components/function-modal.tsx

@@ -0,0 +1,269 @@
+/*
+ * 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, toRefs, PropType, watch, onMounted, ref } from 'vue'
+import {
+  NUpload,
+  NIcon,
+  NForm,
+  NFormItem,
+  NInput,
+  NInputGroup,
+  NRadio,
+  NTreeSelect,
+  NButton,
+  NRadioGroup
+} from 'naive-ui'
+import { useI18n } from 'vue-i18n'
+import { CloudUploadOutlined } from '@vicons/antd'
+import Modal from '@/components/modal'
+import { useForm } from './use-form'
+import { useModal } from './use-modal'
+import type { IUdf } from '../types'
+
+const props = {
+  row: {
+    type: Object as PropType<IUdf>,
+    default: {}
+  },
+  show: {
+    type: Boolean as PropType<boolean>,
+    default: false
+  }
+}
+
+export default defineComponent({
+  name: 'ResourceFileFolder',
+  props,
+  emits: ['update:show', 'updateList'],
+  setup(props, ctx) {
+    const treeRef = ref()
+    const { state, uploadState } = useForm()
+
+    const {
+      variables,
+      handleCreateFunc,
+      handleRenameFunc,
+      getUdfList,
+      handleUploadFile
+    } = useModal(state, uploadState, ctx)
+
+    const hideModal = () => {
+      ctx.emit('update:show')
+    }
+
+    const handleCreate = () => {
+      handleCreateFunc()
+    }
+
+    const handleRename = () => {
+      handleRenameFunc(props.row.id)
+    }
+
+    const handleUpload = () => {
+      uploadState.uploadForm.currentDir = `/${treeRef.value.selectedOption?.fullName}`
+      handleUploadFile()
+    }
+
+    const customRequest = ({ file }: any) => {
+      uploadState.uploadForm.name = file.name
+      uploadState.uploadForm.file = file.file
+    }
+
+    onMounted(() => {
+      getUdfList()
+    })
+
+    watch(
+      () => props.row,
+      () => {
+        variables.uploadShow = false
+        state.functionForm.type = props.row.type
+        state.functionForm.funcName = props.row.funcName
+        state.functionForm.className = props.row.className
+        state.functionForm.resourceId = props.row.resourceId || -1
+        state.functionForm.description = props.row.description
+      }
+    )
+    return {
+      treeRef,
+      hideModal,
+      handleCreate,
+      handleRename,
+      customRequest,
+      handleUpload,
+      ...toRefs(state),
+      ...toRefs(uploadState),
+      ...toRefs(variables)
+    }
+  },
+  render() {
+    const { t } = useI18n()
+
+    return (
+      <Modal
+        show={this.$props.show}
+        title={
+          this.row.id
+            ? t('resource.function.edit_udf_function')
+            : t('resource.function.create_udf_function')
+        }
+        onCancel={this.hideModal}
+        onConfirm={this.row.id ? this.handleRename : this.handleCreate}
+      >
+        <NForm
+          rules={this.rules}
+          ref='functionFormRef'
+          label-placement='left'
+          label-width='160'
+        >
+          <NFormItem label={t('resource.function.type')} path='type'>
+            <NRadioGroup
+              v-model={[this.functionForm.type, 'value']}
+              name='type'
+            >
+              <NRadio value='HIVE'>HIVE UDF</NRadio>
+            </NRadioGroup>
+          </NFormItem>
+          <NFormItem
+            label={t('resource.function.udf_function_name')}
+            path='funcName'
+          >
+            <NInput
+              v-model={[this.functionForm.funcName, 'value']}
+              placeholder={t('resource.function.enter_udf_unction_name_tips')}
+            />
+          </NFormItem>
+          <NFormItem
+            label={t('resource.function.package_name')}
+            path='className'
+          >
+            <NInput
+              v-model={[this.functionForm.className, 'value']}
+              placeholder={t('resource.function.enter_package_name_tips')}
+            />
+          </NFormItem>
+          <NFormItem
+            label={t('resource.function.udf_resources')}
+            path='resourceId'
+          >
+            <NInputGroup>
+              <NTreeSelect
+                options={this.udfResourceList}
+                label-field='fullName'
+                key-field='id'
+                v-model={[this.functionForm.resourceId, 'value']}
+                placeholder={t(
+                  'resource.function.enter_select_udf_resources_tips'
+                )}
+                defaultValue={this.functionForm.resourceId}
+                disabled={this.uploadShow}
+                showPath={false}
+              ></NTreeSelect>
+              <NButton
+                type='primary'
+                ghost
+                onClick={() => (this.uploadShow = !this.uploadShow)}
+              >
+                {t('resource.function.upload_resources')}
+              </NButton>
+            </NInputGroup>
+          </NFormItem>
+          {this.uploadShow && (
+            <NForm
+              rules={this.uploadRules}
+              ref='uploadFormRef'
+              label-placement='left'
+              label-width='160'
+            >
+              <NFormItem
+                label={t('resource.function.udf_resources_directory')}
+                path='pid'
+                show-feedback={false}
+                style={{ marginBottom: '5px' }}
+              >
+                <NTreeSelect
+                  ref='treeRef'
+                  options={this.udfResourceDirList}
+                  label-field='fullName'
+                  key-field='id'
+                  v-model={[this.uploadForm.pid, 'value']}
+                  placeholder={t(
+                    'resource.function.enter_select_udf_resources_directory_tips'
+                  )}
+                  defaultValue={this.uploadForm.pid}
+                ></NTreeSelect>
+              </NFormItem>
+              <NFormItem
+                label=' '
+                show-feedback={false}
+                style={{ marginBottom: '5px' }}
+              >
+                <NInputGroup>
+                  <NInput
+                    v-model={[this.uploadForm.name, 'value']}
+                    placeholder={t('resource.function.enter_name_tips')}
+                  />
+                  <NButton>
+                    <NUpload
+                      v-model={[this.uploadForm.file, 'value']}
+                      customRequest={this.customRequest}
+                      showFileList={false}
+                    >
+                      <NButton text>
+                        上传
+                        <NIcon>
+                          <CloudUploadOutlined />
+                        </NIcon>
+                      </NButton>
+                    </NUpload>
+                  </NButton>
+                </NInputGroup>
+              </NFormItem>
+              <NFormItem
+                label=' '
+                path='description'
+                show-feedback={false}
+                style={{ marginBottom: '5px' }}
+              >
+                <NInput
+                  type='textarea'
+                  v-model={[this.uploadForm.description, 'value']}
+                  placeholder={t('resource.function.enter_description_tips')}
+                />
+              </NFormItem>
+              <NFormItem label=' '>
+                <NButton onClick={this.handleUpload}>上传UDF资源</NButton>
+              </NFormItem>
+            </NForm>
+          )}
+
+          <NFormItem
+            label={t('resource.function.instructions')}
+            path='description'
+          >
+            <NInput
+              type='textarea'
+              v-model={[this.functionForm.description, 'value']}
+              placeholder={t('resource.function.enter_instructions_tips')}
+            />
+          </NFormItem>
+        </NForm>
+      </Modal>
+    )
+  }
+})

+ 102 - 0
dolphinscheduler-ui-next/src/views/resource/udf/function/components/use-form.ts

@@ -0,0 +1,102 @@
+/*
+ * 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 type { FormRules } from 'naive-ui'
+
+export const useForm = () => {
+  const { t } = useI18n()
+
+  const state = reactive({
+    functionFormRef: ref(),
+    functionForm: {
+      type: 'HIVE',
+      funcName: '',
+      className: '',
+      argTypes: '',
+      database: '',
+      description: '',
+      resourceId: -1
+    },
+    rules: {
+      type: {
+        required: true,
+        trigger: ['input', 'blur'],
+        validator() {
+          if (!state.functionForm.type) {
+            return new Error(t('resource.function.enter_name_tips'))
+          }
+        }
+      },
+      funcName: {
+        required: true,
+        trigger: ['input', 'blur'],
+        validator() {
+          if (!state.functionForm.funcName) {
+            return new Error(t('resource.function.enter_name_tips'))
+          }
+        }
+      },
+      className: {
+        required: true,
+        trigger: ['input', 'blur'],
+        validator() {
+          if (!state.functionForm.className) {
+            return new Error(t('resource.function.enter_name_tips'))
+          }
+        }
+      },
+      resourceId: {
+        required: true,
+        trigger: ['input', 'blur'],
+        validator() {
+          if (state.functionForm.resourceId === -1) {
+            return new Error(t('resource.function.enter_name_tips'))
+          }
+        }
+      }
+    } as FormRules
+  })
+
+  const uploadState = reactive({
+    uploadFormRef: ref(),
+    uploadForm: {
+      name: '',
+      file: '',
+      description: '',
+      pid: -1,
+      currentDir: '/'
+    },
+    uploadRules: {
+      pid: {
+        required: true,
+        trigger: ['input', 'blur'],
+        validator() {
+          if (uploadState.uploadForm.pid === -1) {
+            return new Error(t('resource.function.enter_name_tips'))
+          }
+        }
+      }
+    } as FormRules
+  })
+
+  return {
+    state,
+    uploadState
+  }
+}

+ 184 - 0
dolphinscheduler-ui-next/src/views/resource/udf/function/components/use-modal.ts

@@ -0,0 +1,184 @@
+/*
+ * 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 _ from 'lodash'
+import { ref, reactive, SetupContext } from 'vue'
+import { useI18n } from 'vue-i18n'
+import {
+  createResource,
+  createUdfFunc,
+  queryResourceList,
+  updateUdfFunc
+} from '@/service/modules/resources'
+import { useAsyncState } from '@vueuse/core'
+
+export function useModal(
+  state: any,
+  uploadState: any,
+  ctx: SetupContext<('update:show' | 'updateList')[]>
+) {
+  const { t } = useI18n()
+
+  const handleCreateFunc = async () => {
+    submitRequest(
+      async () =>
+        await createUdfFunc(
+          {
+            ...state.functionForm
+          },
+          state.functionForm.resourceId
+        )
+    )
+  }
+
+  const handleRenameFunc = async (id: number) => {
+    submitRequest(async () => {
+      await updateUdfFunc(
+        {
+          ...state.functionForm,
+          id
+        },
+        state.functionForm.resourceId,
+        id
+      )
+    })
+  }
+
+  const submitRequest = (serviceHandle: any) => {
+    state.functionFormRef.validate(async (valid: any) => {
+      if (!valid) {
+        try {
+          await serviceHandle()
+          window.$message.success(t('resource.udf.success'))
+          ctx.emit('updateList')
+          ctx.emit('update:show')
+        } catch (error: any) {
+          window.$message.error(error.message)
+        }
+      }
+    })
+  }
+
+  const variables = reactive({
+    uploadShow: ref(false),
+    udfResourceList: [],
+    udfResourceDirList: []
+  })
+
+  const filterEmptyDirectory = (list: any) => {
+    for (const item of list) {
+      if (item.children) {
+        if (!/\.jar$/.test(item.name)) {
+          item.disabled = true
+        }
+        filterEmptyDirectory(item.children)
+      }
+    }
+    return list.filter(
+      (n: any) =>
+        (/\.jar$/.test(n.name) && n.children.length === 0) ||
+        (!/\.jar$/.test(n.name) && n.children.length > 0)
+    )
+  }
+
+  // filterJarFile
+  const filterJarFile = (array: any) => {
+    for (const item of array) {
+      if (item.children) {
+        item.children = filterJarFile(item.children)
+      }
+    }
+    return array.filter((n: any) => !/\.jar$/.test(n.name))
+  }
+
+  // recursiveTree
+  const recursiveTree = (item: any) => {
+    // Recursive convenience tree structure
+    item.forEach((item: any) => {
+      item.children === '' ||
+      item.children === undefined ||
+      item.children === null ||
+      item.children.length === 0
+        ? delete item.children
+        : recursiveTree(item.children)
+    })
+  }
+
+  const getUdfList = () => {
+    const { state } = useAsyncState(
+      queryResourceList({ type: 'UDF' }).then((res: any) => {
+        let item = res
+        let item1 = _.cloneDeep(res)
+
+        filterEmptyDirectory(item)
+        item = filterEmptyDirectory(item)
+        recursiveTree(item)
+        recursiveTree(filterJarFile(item1))
+        item1 = item1.filter((item: any) => {
+          if (item.dirctory) {
+            return item
+          }
+        })
+        variables.udfResourceList = item
+        variables.udfResourceDirList = item1
+      }),
+      {}
+    )
+    return state
+  }
+
+  const resetUploadForm = () => {
+    uploadState.uploadForm.name = ''
+    uploadState.uploadForm.file = ''
+    uploadState.uploadForm.pid = -1
+    uploadState.uploadForm.currentDir = '/'
+    uploadState.uploadForm.description = ''
+  }
+
+  const handleUploadFile = () => {
+    uploadState.uploadFormRef.validate(async (valid: any) => {
+      if (!valid) {
+        const formData = new FormData()
+        formData.append('file', uploadState.uploadForm.file)
+        formData.append('type', 'UDF')
+        formData.append('name', uploadState.uploadForm.name)
+        formData.append('pid', uploadState.uploadForm.pid)
+        formData.append('currentDir', uploadState.uploadForm.currentDir)
+        formData.append('description', uploadState.uploadForm.description)
+
+        try {
+          const res = await createResource(formData as any)
+          window.$message.success(t('resource.function.success'))
+          variables.uploadShow = false
+          resetUploadForm()
+          getUdfList()
+          state.functionForm.resourceId = res.id
+        } catch (error: any) {
+          window.$message.error(error.message)
+        }
+      }
+    })
+  }
+
+  return {
+    variables,
+    getUdfList,
+    handleCreateFunc,
+    handleRenameFunc,
+    handleUploadFile
+  }
+}

+ 73 - 0
dolphinscheduler-ui-next/src/views/resource/udf/function/index.module.scss

@@ -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.
+ */
+
+.content {
+  width: 100%;
+
+  .card {
+    margin-bottom: 8px;
+  }
+
+  .header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin: 10px 0;
+    .right {
+      > .search {
+        .list {
+          float: right;
+          margin: 3px 0 3px 4px;
+        }
+      }
+    }
+  }
+
+  .table {
+    table {
+      width: 100%;
+      tr {
+        height: 40px;
+        font-size: 12px;
+        th,
+        td {
+          &:nth-child(1) {
+            width: 50px;
+            text-align: center;
+          }
+        }
+        th {
+          &:nth-child(1) {
+            width: 60px;
+            text-align: center;
+          }
+          > span {
+            font-size: 12px;
+            color: #555;
+          }
+        }
+      }
+    }
+  }
+
+  .pagination {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-top: 20px;
+  }
+}

+ 144 - 0
dolphinscheduler-ui-next/src/views/resource/udf/function/index.tsx

@@ -0,0 +1,144 @@
+/*
+ * 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, Ref, toRefs, onMounted, toRef } from 'vue'
+import {
+  NIcon,
+  NSpace,
+  NDataTable,
+  NButton,
+  NPagination,
+  NInput
+} from 'naive-ui'
+import { useI18n } from 'vue-i18n'
+import { SearchOutlined } from '@vicons/antd'
+import Card from '@/components/card'
+import FolderModal from './components/function-modal'
+import { useTable } from './use-table'
+import styles from './index.module.scss'
+
+export default defineComponent({
+  name: 'function-manage',
+  setup() {
+    const { variables, getTableData } = useTable()
+
+    const requestData = () => {
+      getTableData({
+        id: variables.id,
+        pageSize: variables.pageSize,
+        pageNo: variables.page,
+        searchVal: variables.searchVal
+      })
+    }
+
+    const handleUpdateList = () => {
+      requestData()
+    }
+
+    const handleChangePageSize = () => {
+      variables.page = 1
+      requestData()
+    }
+
+    const handleSearch = () => {
+      variables.page = 1
+      requestData()
+    }
+
+    const handleShowModal = (showRef: Ref<Boolean>) => {
+      showRef.value = true
+    }
+
+    const handleCreateFolder = () => {
+      variables.row = {}
+      handleShowModal(toRef(variables, 'showRef'))
+    }
+
+    onMounted(() => {
+      requestData()
+    })
+
+    return {
+      requestData,
+      handleSearch,
+      handleUpdateList,
+      handleCreateFolder,
+      handleChangePageSize,
+      ...toRefs(variables)
+    }
+  },
+  render() {
+    const { t } = useI18n()
+
+    return (
+      <div class={styles.content}>
+        <Card class={styles.card}>
+          <div class={styles.header}>
+            <NSpace>
+              <NButton type='primary' onClick={this.handleCreateFolder}>
+                {t('resource.function.create_udf_function')}
+              </NButton>
+            </NSpace>
+            <div class={styles.right}>
+              <div class={styles.search}>
+                <div class={styles.list}>
+                  <NButton type='primary' onClick={this.handleSearch}>
+                    <NIcon>
+                      <SearchOutlined />
+                    </NIcon>
+                  </NButton>
+                </div>
+                <div class={styles.list}>
+                  <NInput
+                    placeholder={t('resource.function.enter_keyword_tips')}
+                    v-model={[this.searchVal, 'value']}
+                  />
+                </div>
+              </div>
+            </div>
+          </div>
+        </Card>
+        <Card title={t('resource.function.udf_function')}>
+          <NDataTable
+            columns={this.columns}
+            data={this.tableData}
+            striped
+            size={'small'}
+            class={styles.table}
+          />
+          <div class={styles.pagination}>
+            <NPagination
+              v-model:page={this.page}
+              v-model:page-size={this.pageSize}
+              page-count={this.totalPage}
+              show-size-picker
+              page-sizes={[10, 30, 50]}
+              show-quick-jumper
+              onUpdatePage={this.requestData}
+              onUpdatePageSize={this.handleChangePageSize}
+            />
+          </div>
+        </Card>
+        <FolderModal
+          v-model:row={this.row}
+          v-model:show={this.showRef}
+          onUpdateList={this.handleUpdateList}
+        />
+      </div>
+    )
+  }
+})

+ 38 - 0
dolphinscheduler-ui-next/src/views/resource/udf/function/types.ts

@@ -0,0 +1,38 @@
+/*
+ * 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 interface IUdfFunctionParam {
+  id: number
+  pageSize: number
+  pageNo: number
+  searchVal: string | undefined
+}
+
+export interface IUdf {
+  id: number
+  userId: number
+  type: 'HIVE'
+  funcName: string
+  className: string
+  resourceId: number
+  resourceName: string
+  argTypes: string
+  database: string
+  description: string
+  createTime: string
+  updateTime: string
+}

+ 182 - 0
dolphinscheduler-ui-next/src/views/resource/udf/function/use-table.ts

@@ -0,0 +1,182 @@
+/*
+ * 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, ref, reactive } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { useRouter } from 'vue-router'
+import type { Router } from 'vue-router'
+import type { TableColumns } from 'naive-ui/es/data-table/src/interface'
+import { NSpace, NTooltip, NButton, NPopconfirm } from 'naive-ui'
+import { EditOutlined, DeleteOutlined } from '@vicons/antd'
+import { useAsyncState } from '@vueuse/core'
+import {
+  queryUdfFuncListPaging,
+  deleteUdfFunc
+} from '@/service/modules/resources'
+import { IUdfFunctionParam } from './types'
+
+export function useTable() {
+  const { t } = useI18n()
+  const router: Router = useRouter()
+
+  const columns: TableColumns<any> = [
+    {
+      title: t('resource.function.id'),
+      key: 'id',
+      width: 50,
+      render: (_row, index) => index + 1
+    },
+    {
+      title: t('resource.function.udf_function_name'),
+      key: 'funcName'
+    },
+    {
+      title: t('resource.function.class_name'),
+      key: 'className'
+    },
+    {
+      title: t('resource.function.type'),
+      key: 'type'
+    },
+    {
+      title: t('resource.function.description'),
+      key: 'description'
+    },
+    {
+      title: t('resource.function.jar_package'),
+      key: 'resourceName'
+    },
+    {
+      title: t('resource.function.update_time'),
+      key: 'updateTime'
+    },
+    {
+      title: t('resource.function.operation'),
+      key: 'operation',
+      render: (row) => {
+        return h(NSpace, null, {
+          default: () => [
+            h(
+              NTooltip,
+              {},
+              {
+                trigger: () =>
+                  h(
+                    NButton,
+                    {
+                      circle: true,
+                      type: 'info',
+                      size: 'tiny',
+                      onClick: () => {
+                        handleEdit(row)
+                      }
+                    },
+                    {
+                      icon: () => h(EditOutlined)
+                    }
+                  ),
+                default: () => t('resource.function.edit')
+              }
+            ),
+            h(
+              NPopconfirm,
+              {
+                onPositiveClick: () => {
+                  handleDelete(row.id)
+                }
+              },
+              {
+                trigger: () =>
+                  h(
+                    NTooltip,
+                    {},
+                    {
+                      trigger: () =>
+                        h(
+                          NButton,
+                          {
+                            circle: true,
+                            type: 'error',
+                            size: 'tiny'
+                          },
+                          {
+                            icon: () => h(DeleteOutlined)
+                          }
+                        ),
+                      default: () => t('resource.function.delete')
+                    }
+                  ),
+                default: () => t('resource.function.delete_confirm')
+              }
+            )
+          ]
+        })
+      }
+    }
+  ]
+
+  const variables = reactive({
+    columns,
+    row: {},
+    tableData: [],
+    id: ref(Number(router.currentRoute.value.params.id) || -1),
+    page: ref(1),
+    pageSize: ref(10),
+    searchVal: ref(),
+    totalPage: ref(1),
+    showRef: ref(false)
+  })
+
+  const getTableData = (params: IUdfFunctionParam) => {
+    const { state } = useAsyncState(
+      queryUdfFuncListPaging({ ...params }).then((res: any) => {
+        variables.totalPage = res.totalPage
+        variables.tableData = res.totalList.map((item: any) => {
+          return { ...item }
+        })
+      }),
+      { total: 0, table: [] }
+    )
+    return state
+  }
+
+  const handleEdit = (row: any) => {
+    variables.showRef = true
+    variables.row = row
+  }
+
+  const handleDelete = (id: number) => {
+    /* after deleting data from the current page, you need to jump forward when the page is empty. */
+    if (variables.tableData.length === 1 && variables.page > 1) {
+      variables.page -= 1
+    }
+
+    deleteUdfFunc(id).then(() =>
+      getTableData({
+        id: variables.id,
+        pageSize: variables.pageSize,
+        pageNo: variables.page,
+        searchVal: variables.searchVal
+      })
+    )
+  }
+
+  return {
+    variables,
+    getTableData
+  }
+}