You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dolphinscheduler.apache.org by zh...@apache.org on 2022/01/17 02:43:09 UTC

[dolphinscheduler] branch dev updated: [Feature][UI Next] Add Udf resource manage (#8076)

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

zhongjiajie 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 7f36246  [Feature][UI Next] Add Udf resource manage (#8076)
7f36246 is described below

commit 7f362464b326aafae53121d87276a34ec38064f0
Author: Devosend <de...@gmail.com>
AuthorDate: Mon Jan 17 10:43:03 2022 +0800

    [Feature][UI Next] Add Udf resource manage (#8076)
    
    * add resource manange
    
    * add resource delete
    
    * modify rename
    
    * support sub udf resource mannage
    
    * add Bread
    
    * adjust style
---
 .../src/layouts/content/index.tsx                  |   2 +-
 .../src/locales/modules/en_US.ts                   |  28 +++
 .../src/locales/modules/zh_CN.ts                   |  28 +++
 .../src/router/modules/resources.ts                |  16 ++
 .../src/service/modules/resources/index.ts         |  11 +
 .../src/views/datasource/datasource-list/index.tsx |   4 +-
 .../datasource/datasource-list/json-highlight.tsx  |   2 +-
 .../udf/resource/components/folder-modal.tsx       | 106 ++++++++
 .../udf/resource/components/upload-modal.tsx       | 106 ++++++++
 .../resource/udf/resource/components/use-form.ts   |  73 ++++++
 .../resource/udf/resource/components/use-modal.ts  | 114 +++++++++
 .../views/resource/udf/resource/index.module.scss  |  81 ++++++
 .../src/views/resource/udf/resource/index.tsx      | 197 +++++++++++++++
 .../src/views/resource/udf/resource/types.ts       |  38 +++
 .../src/views/resource/udf/resource/use-table.ts   | 279 +++++++++++++++++++++
 15 files changed, 1081 insertions(+), 4 deletions(-)

diff --git a/dolphinscheduler-ui-next/src/layouts/content/index.tsx b/dolphinscheduler-ui-next/src/layouts/content/index.tsx
index 402ce44..88a7402 100644
--- a/dolphinscheduler-ui-next/src/layouts/content/index.tsx
+++ b/dolphinscheduler-ui-next/src/layouts/content/index.tsx
@@ -93,7 +93,7 @@ const Content = defineComponent({
             <SideBar sideMenuOptions={this.sideMenuOptions} />
           )}
           <NLayoutContent native-scrollbar={false} style='padding: 16px 22px'>
-            <router-view />
+            <router-view key={this.$route.fullPath} />
           </NLayoutContent>
         </NLayout>
       </NLayout>
diff --git a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
index 8216a03..700d04d 100644
--- a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
+++ b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
@@ -187,6 +187,34 @@ const resource = {
     return: 'Return',
     save: 'Save'
   },
+  udf: {
+    udf_resources: 'UDF resources',
+    create_folder: 'Create Folder',
+    upload_udf_resources: 'Upload UDF Resources',
+    id: '#',
+    udf_source_name: 'UDF Resource Name',
+    whether_directory: 'Whether directory',
+    file_name: 'File Name',
+    file_size: 'File Size',
+    description: 'Description',
+    create_time: 'Create Time',
+    update_time: 'Update Time',
+    operation: 'Operation',
+    yes: 'Yes',
+    no: 'No',
+    edit: 'Edit',
+    download: 'Download',
+    delete: 'Delete',
+    delete_confirm: 'Delete?',
+    success: 'Success',
+    folder_name: 'Folder Name',
+    upload: 'Upload',
+    upload_files: 'Upload Files',
+    file_upload: 'File Upload',
+    enter_keyword_tips: 'Please enter keyword',
+    enter_name_tips: 'Please enter name',
+    enter_description_tips: 'Please enter description'
+  },
   task_group_option: {
     id: 'No.',
     manage: 'Task group manage',
diff --git a/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts b/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
index 2fc5a88..dfc5035 100644
--- a/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
+++ b/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
@@ -189,6 +189,34 @@ const resource = {
     return: '返回',
     save: '保存'
   },
+  udf: {
+    udf_resources: 'UDF资源',
+    create_folder: '创建文件夹',
+    upload_udf_resources: '上传UDF资源',
+    id: '编号',
+    udf_source_name: 'UDF资源名称',
+    whether_directory: '是否文件夹',
+    file_name: '文件名称',
+    file_size: '文件大小',
+    description: '描述',
+    create_time: '创建时间',
+    update_time: '更新时间',
+    operation: '操作',
+    yes: '是',
+    no: '否',
+    edit: '编辑',
+    download: '下载',
+    delete: '删除',
+    success: '成功',
+    folder_name: '文件夹名称',
+    upload: '上传',
+    upload_files: '上传文件',
+    file_upload: '文件上传',
+    delete_confirm: '确定删除吗?',
+    enter_keyword_tips: '请输入关键词',
+    enter_name_tips: '请输入名称',
+    enter_description_tips: '请输入描述'
+  },
   task_group_option: {
     id: '编号',
     manage: '任务组管理',
diff --git a/dolphinscheduler-ui-next/src/router/modules/resources.ts b/dolphinscheduler-ui-next/src/router/modules/resources.ts
index d6e0f4b..38147ad 100644
--- a/dolphinscheduler-ui-next/src/router/modules/resources.ts
+++ b/dolphinscheduler-ui-next/src/router/modules/resources.ts
@@ -78,6 +78,22 @@ export default {
       }
     },
     {
+      path: '/resource/resource-manage',
+      name: 'resource-manage',
+      component: components['resource'],
+      meta: {
+        title: '资源管理'
+      }
+    },
+    {
+      path: '/resource/resource-manage/:id',
+      name: 'resource-sub-manage',
+      component: components['resource'],
+      meta: {
+        title: '资源管理'
+      }
+    },
+    {
       path: '/resource/task-group',
       name: 'task-group-manage',
       component: components['taskGroupOption'],
diff --git a/dolphinscheduler-ui-next/src/service/modules/resources/index.ts b/dolphinscheduler-ui-next/src/service/modules/resources/index.ts
index c3f915f..c6f44f2 100644
--- a/dolphinscheduler-ui-next/src/service/modules/resources/index.ts
+++ b/dolphinscheduler-ui-next/src/service/modules/resources/index.ts
@@ -45,6 +45,17 @@ export function queryResourceListPaging(
   })
 }
 
+export function queryResourceById(
+  params: ResourceTypeReq & FullNameReq & IdReq,
+  id: number
+): any {
+  return axios({
+    url: `/resources/${id}`,
+    method: 'get',
+    params
+  })
+}
+
 export function createResource(
   data: CreateReq & FileNameReq & NameReq & ResourceTypeReq
 ): any {
diff --git a/dolphinscheduler-ui-next/src/views/datasource/datasource-list/index.tsx b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/index.tsx
index 01d6d2e..a59396b 100644
--- a/dolphinscheduler-ui-next/src/views/datasource/datasource-list/index.tsx
+++ b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/index.tsx
@@ -36,8 +36,8 @@ const list = defineComponent({
   name: 'list',
   setup() {
     const { t } = useI18n()
-    let showDetailModal = ref(false)
-    let selectId = ref()
+    const showDetailModal = ref(false)
+    const selectId = ref()
 
     const { columnsRef } = useColumns((id: number, type: 'edit' | 'delete') => {
       if (type === 'edit') {
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
index 162bcd6..c69cf00 100644
--- a/dolphinscheduler-ui-next/src/views/datasource/datasource-list/json-highlight.tsx
+++ b/dolphinscheduler-ui-next/src/views/datasource/datasource-list/json-highlight.tsx
@@ -45,7 +45,7 @@ const syntaxHighlight = (json: string) => {
   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 = ''
+    let type = ''
     if (isBoolean(value) || value === null) {
       type = 'info'
     } else if (isNumber(value)) {
diff --git a/dolphinscheduler-ui-next/src/views/resource/udf/resource/components/folder-modal.tsx b/dolphinscheduler-ui-next/src/views/resource/udf/resource/components/folder-modal.tsx
new file mode 100644
index 0000000..ef509cb
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/resource/udf/resource/components/folder-modal.tsx
@@ -0,0 +1,106 @@
+/*
+ * 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 } from 'vue'
+import { NForm, NFormItem, NInput } from 'naive-ui'
+import { useI18n } from 'vue-i18n'
+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 { folderState: state } = useForm()
+
+    const { handleCreateResource, handleRenameResource } = useModal(state, ctx)
+
+    const hideModal = () => {
+      ctx.emit('update:show')
+    }
+
+    const handleCreate = () => {
+      handleCreateResource()
+    }
+
+    const handleRename = () => {
+      handleRenameResource(props.row.id)
+    }
+
+    watch(
+      () => props.row,
+      () => {
+        state.folderForm.name = props.row.alias
+        state.folderForm.description = props.row.description
+      }
+    )
+
+    return {
+      hideModal,
+      handleCreate,
+      handleRename,
+      ...toRefs(state)
+    }
+  },
+  render() {
+    const { t } = useI18n()
+
+    return (
+      <Modal
+        show={this.$props.show}
+        title={t('resource.udf.create_folder')}
+        onCancel={this.hideModal}
+        onConfirm={this.row.id ? this.handleRename : this.handleCreate}
+      >
+        <NForm
+          rules={this.rules}
+          ref='folderFormRef'
+          label-placement='left'
+          label-width='160'
+        >
+          <NFormItem label={t('resource.udf.folder_name')} path='name'>
+            <NInput
+              v-model={[this.folderForm.name, 'value']}
+              placeholder={t('resource.udf.enter_name_tips')}
+            />
+          </NFormItem>
+          <NFormItem label={t('resource.udf.description')} path='description'>
+            <NInput
+              type='textarea'
+              v-model={[this.folderForm.description, 'value']}
+              placeholder={t('resource.udf.enter_description_tips')}
+            />
+          </NFormItem>
+        </NForm>
+      </Modal>
+    )
+  }
+})
diff --git a/dolphinscheduler-ui-next/src/views/resource/udf/resource/components/upload-modal.tsx b/dolphinscheduler-ui-next/src/views/resource/udf/resource/components/upload-modal.tsx
new file mode 100644
index 0000000..1379970
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/resource/udf/resource/components/upload-modal.tsx
@@ -0,0 +1,106 @@
+/*
+ * 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 } from 'vue'
+import { NForm, NFormItem, NInput, NUpload, NButton, NIcon } from 'naive-ui'
+import { useI18n } from 'vue-i18n'
+import Modal from '@/components/modal'
+import { useForm } from './use-form'
+import { useModal } from './use-modal'
+import { CloudUploadOutlined } from '@vicons/antd'
+
+const props = {
+  show: {
+    type: Boolean as PropType<boolean>,
+    default: false
+  }
+}
+
+export default defineComponent({
+  name: 'ResourceFileFolder',
+  props,
+  emits: ['update:show', 'updateList'],
+  setup(props, ctx) {
+    const { uploadState: state } = useForm()
+    const { handleUploadFile } = useModal(state, ctx)
+
+    const hideModal = () => {
+      ctx.emit('update:show')
+    }
+
+    const handleFolder = () => {
+      handleUploadFile()
+    }
+
+    const customRequest = ({ file }: any) => {
+      state.uploadForm.name = file.name
+      state.uploadForm.file = file.file
+    }
+
+    return {
+      hideModal,
+      handleFolder,
+      customRequest,
+      ...toRefs(state)
+    }
+  },
+  render() {
+    const { t } = useI18n()
+    return (
+      <Modal
+        show={this.$props.show}
+        title={t('resource.udf.file_upload')}
+        onCancel={this.hideModal}
+        onConfirm={this.handleFolder}
+      >
+        <NForm
+          rules={this.rules}
+          ref='uploadFormRef'
+          label-placement='left'
+          label-width='160'
+        >
+          <NFormItem label={t('resource.udf.file_name')} path='name'>
+            <NInput
+              v-model={[this.uploadForm.name, 'value']}
+              placeholder={t('resource.udf.enter_name_tips')}
+            />
+          </NFormItem>
+          <NFormItem label={t('resource.udf.description')} path='description'>
+            <NInput
+              type='textarea'
+              v-model={[this.uploadForm.description, 'value']}
+              placeholder={t('resource.udf.enter_description_tips')}
+            />
+          </NFormItem>
+          <NFormItem label={t('resource.udf.upload_files')} path='file'>
+            <NUpload
+              v-model={[this.uploadForm.file, 'value']}
+              customRequest={this.customRequest}
+            >
+              <NButton>
+                {t('resource.udf.upload')}
+                <NIcon>
+                  <CloudUploadOutlined />
+                </NIcon>
+              </NButton>
+            </NUpload>
+          </NFormItem>
+        </NForm>
+      </Modal>
+    )
+  }
+})
diff --git a/dolphinscheduler-ui-next/src/views/resource/udf/resource/components/use-form.ts b/dolphinscheduler-ui-next/src/views/resource/udf/resource/components/use-form.ts
new file mode 100644
index 0000000..4241523
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/resource/udf/resource/components/use-form.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 { reactive, ref } from 'vue'
+import { useI18n } from 'vue-i18n'
+import type { FormRules } from 'naive-ui'
+
+export const useForm = () => {
+  const { t } = useI18n()
+
+  const folderState = reactive({
+    folderFormRef: ref(),
+    folderForm: {
+      pid: -1,
+      type: 'UDF',
+      name: '',
+      description: '',
+      currentDir: '/'
+    },
+    rules: {
+      name: {
+        required: true,
+        trigger: ['input', 'blur'],
+        validator() {
+          if (folderState.folderForm.name === '') {
+            return new Error(t('resource.udf.enter_name_tips'))
+          }
+        }
+      }
+    } as FormRules
+  })
+
+  const uploadState = reactive({
+    uploadFormRef: ref(),
+    uploadForm: {
+      name: '',
+      file: '',
+      description: '',
+      pid: -1,
+      currentDir: '/'
+    },
+    rules: {
+      name: {
+        required: true,
+        trigger: ['input', 'blur'],
+        validator() {
+          if (uploadState.uploadForm.name === '') {
+            return new Error(t('resource.udf.enter_name_tips'))
+          }
+        }
+      }
+    } as FormRules
+  })
+
+  return {
+    folderState,
+    uploadState
+  }
+}
diff --git a/dolphinscheduler-ui-next/src/views/resource/udf/resource/components/use-modal.ts b/dolphinscheduler-ui-next/src/views/resource/udf/resource/components/use-modal.ts
new file mode 100644
index 0000000..c9210bd
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/resource/udf/resource/components/use-modal.ts
@@ -0,0 +1,114 @@
+/*
+ * 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 { SetupContext } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { useRouter } from 'vue-router'
+import type { Router } from 'vue-router'
+import { useFileStore } from '@/store/file/file'
+import {
+  createDirectory,
+  createResource,
+  updateResource
+} from '@/service/modules/resources'
+
+export function useModal(
+  state: any,
+  ctx: SetupContext<('update:show' | 'updateList')[]>
+) {
+  const { t } = useI18n()
+  const router: Router = useRouter()
+  const fileStore = useFileStore()
+
+  const handleCreateResource = async () => {
+    const pid = router.currentRoute.value.params.id || -1
+    const currentDir = pid === -1 ? '/' : fileStore.getCurrentDir || '/'
+
+    submitRequest(
+      async () =>
+        await createDirectory({
+          ...state.folderForm,
+          ...{ pid, currentDir }
+        })
+    )
+  }
+
+  const handleRenameResource = async (id: number) => {
+    submitRequest(async () => {
+      await updateResource(
+        {
+          ...state.folderForm,
+          ...{ id }
+        },
+        id
+      )
+    })
+  }
+
+  const submitRequest = (serviceHandle: any) => {
+    state.folderFormRef.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 resetUploadForm = () => {
+    state.uploadForm.name = ''
+    state.uploadForm.file = ''
+    state.uploadForm.description = ''
+  }
+
+  const handleUploadFile = () => {
+    state.uploadFormRef.validate(async (valid: any) => {
+      const pid = router.currentRoute.value.params.id || -1
+      const currentDir = pid === -1 ? '/' : fileStore.getCurrentDir || '/'
+      if (!valid) {
+        const formData = new FormData()
+        formData.append('file', state.uploadForm.file)
+        formData.append('type', 'UDF')
+        formData.append('name', state.uploadForm.name)
+        formData.append('pid', String(pid))
+        formData.append('currentDir', currentDir)
+        formData.append('description', state.uploadForm.description)
+
+        try {
+          await createResource(formData as any)
+          window.$message.success(t('resource.udf.success'))
+          ctx.emit('updateList')
+          ctx.emit('update:show')
+          resetUploadForm()
+        } catch (error: any) {
+          window.$message.error(error.message)
+        }
+      }
+    })
+  }
+
+  return {
+    handleCreateResource,
+    handleRenameResource,
+    handleUploadFile
+  }
+}
diff --git a/dolphinscheduler-ui-next/src/views/resource/udf/resource/index.module.scss b/dolphinscheduler-ui-next/src/views/resource/udf/resource/index.module.scss
new file mode 100644
index 0000000..db59460
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/resource/udf/resource/index.module.scss
@@ -0,0 +1,81 @@
+/*
+ * 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;
+  }
+
+  .links {
+    color: #2080f0;
+    text-decoration: none;
+    &:hover {
+      text-decoration: underline;
+    }
+  }  
+}
+
diff --git a/dolphinscheduler-ui-next/src/views/resource/udf/resource/index.tsx b/dolphinscheduler-ui-next/src/views/resource/udf/resource/index.tsx
new file mode 100644
index 0000000..bed26cc
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/resource/udf/resource/index.tsx
@@ -0,0 +1,197 @@
+/*
+ * 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,
+  NBreadcrumb,
+  NBreadcrumbItem
+} from 'naive-ui'
+import { useI18n } from 'vue-i18n'
+import { SearchOutlined } from '@vicons/antd'
+import Card from '@/components/card'
+import FolderModal from './components/folder-modal'
+import UploadModal from './components/upload-modal'
+import { useTable } from './use-table'
+import styles from './index.module.scss'
+
+export default defineComponent({
+  name: 'resource-manage',
+  setup() {
+    const { variables, getTableData, goUdfManage, goBread } = 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, 'folderShowRef'))
+    }
+
+    const handleUploadFile = () => {
+      handleShowModal(toRef(variables, 'uploadShowRef'))
+    }
+
+    const handleBread = (index: number) => {
+      let breadName = ''
+      variables.breadList.forEach((item, i) => {
+        if (i <= index) {
+          breadName = breadName + '/' + item
+        }
+      })
+      goBread(breadName)
+    }
+
+    onMounted(() => {
+      requestData()
+    })
+
+    return {
+      goUdfManage,
+      handleBread,
+      requestData,
+      handleSearch,
+      handleUpdateList,
+      handleCreateFolder,
+      handleUploadFile,
+      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.udf.create_folder')}
+              </NButton>
+              <NButton strong secondary onClick={this.handleUploadFile}>
+                {t('resource.udf.upload_udf_resources')}
+              </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.udf.enter_keyword_tips')}
+                    v-model={[this.searchVal, 'value']}
+                  />
+                </div>
+              </div>
+            </div>
+          </div>
+        </Card>
+        <Card title={t('resource.udf.udf_resources')}>
+          {{
+            default: () => (
+              <div>
+                <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>
+              </div>
+            ),
+            header: () => (
+              <NBreadcrumb separator='>'>
+                <NBreadcrumbItem>
+                  <NButton text onClick={() => this.goUdfManage()}>
+                    {t('resource.udf.udf_resources')}
+                  </NButton>
+                </NBreadcrumbItem>
+                {this.breadList.map((item, index) => (
+                  <NBreadcrumbItem>
+                    <NButton
+                      text
+                      disabled={index === this.breadList.length - 1}
+                      onClick={() => this.handleBread(index)}
+                    >
+                      {item}
+                    </NButton>
+                  </NBreadcrumbItem>
+                ))}
+              </NBreadcrumb>
+            )
+          }}
+        </Card>
+        <FolderModal
+          v-model:row={this.row}
+          v-model:show={this.folderShowRef}
+          onUpdateList={this.handleUpdateList}
+        />
+        <UploadModal
+          v-model:show={this.uploadShowRef}
+          onUpdateList={this.handleUpdateList}
+        />
+      </div>
+    )
+  }
+})
diff --git a/dolphinscheduler-ui-next/src/views/resource/udf/resource/types.ts b/dolphinscheduler-ui-next/src/views/resource/udf/resource/types.ts
new file mode 100644
index 0000000..b2cfbe5
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/resource/udf/resource/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 IUdfResourceParam {
+  id: number
+  pageSize: number
+  pageNo: number
+  searchVal: string | undefined
+}
+
+export interface IUdf {
+  id: number
+  pid: number
+  userId: number
+  fileName: string
+  fullName: string
+  alias: string
+  directory: boolean
+  size: number
+  type: 'UDF'
+  description: string
+  createTime: string
+  updateTime: string
+}
diff --git a/dolphinscheduler-ui-next/src/views/resource/udf/resource/use-table.ts b/dolphinscheduler-ui-next/src/views/resource/udf/resource/use-table.ts
new file mode 100644
index 0000000..b7f7243
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/resource/udf/resource/use-table.ts
@@ -0,0 +1,279 @@
+/*
+ * 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 { bytesToSize } from '@/utils/common'
+import { useFileStore } from '@/store/file/file'
+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, DownloadOutlined } from '@vicons/antd'
+import { useAsyncState } from '@vueuse/core'
+import {
+  queryResourceListPaging,
+  downloadResource,
+  deleteResource,
+  queryResourceById
+} from '@/service/modules/resources'
+import { IUdfResourceParam } from './types'
+import styles from './index.module.scss'
+
+const goSubFolder = (router: Router, item: any) => {
+  const fileStore = useFileStore()
+  fileStore.setFileInfo(`${item.alias}|${item.size}`)
+
+  if (item.directory) {
+    fileStore.setCurrentDir(`${item.fullName}`)
+    router.push({ name: 'resource-sub-manage', params: { id: item.id } })
+  }
+}
+
+export function useTable() {
+  const { t } = useI18n()
+  const router: Router = useRouter()
+  const fileStore = useFileStore()
+
+  const columns: TableColumns<any> = [
+    {
+      title: t('resource.udf.id'),
+      key: 'id',
+      width: 50,
+      render: (_row, index) => index + 1
+    },
+    {
+      title: t('resource.udf.udf_source_name'),
+      key: 'alias',
+      render: (row) => {
+        if (!row.directory) {
+          return row.alias
+        } else {
+          return h(
+            'a',
+            {
+              href: 'javascript:',
+              class: styles.links,
+              onClick: () => goSubFolder(router, row)
+            },
+            {
+              default: () => {
+                return row.alias
+              }
+            }
+          )
+        }
+      }
+    },
+    {
+      title: t('resource.udf.whether_directory'),
+      key: 'whether_directory',
+      render: (row) =>
+        row.directory ? t('resource.file.yes') : t('resource.file.no')
+    },
+    {
+      title: t('resource.udf.file_name'),
+      key: 'fileName'
+    },
+    {
+      title: t('resource.udf.file_size'),
+      key: 'size',
+      render: (row) => bytesToSize(row.size)
+    },
+    {
+      title: t('resource.udf.description'),
+      key: 'description'
+    },
+    {
+      title: t('resource.udf.create_time'),
+      key: 'createTime'
+    },
+    {
+      title: t('resource.udf.update_time'),
+      key: 'updateTime'
+    },
+    {
+      title: t('resource.udf.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.udf.edit')
+              }
+            ),
+            h(
+              NTooltip,
+              {},
+              {
+                trigger: () =>
+                  h(
+                    NButton,
+                    {
+                      circle: true,
+                      type: 'info',
+                      size: 'tiny',
+                      disabled: row?.directory ? true : false,
+                      onClick: () => downloadResource(row.id)
+                    },
+                    {
+                      icon: () => h(DownloadOutlined)
+                    }
+                  ),
+                default: () => t('resource.udf.download')
+              }
+            ),
+            h(
+              NPopconfirm,
+              {
+                onPositiveClick: () => {
+                  handleDelete(row.id)
+                }
+              },
+              {
+                trigger: () =>
+                  h(
+                    NTooltip,
+                    {},
+                    {
+                      trigger: () =>
+                        h(
+                          NButton,
+                          {
+                            circle: true,
+                            type: 'error',
+                            size: 'tiny'
+                          },
+                          {
+                            icon: () => h(DeleteOutlined)
+                          }
+                        ),
+                      default: () => t('resource.udf.delete')
+                    }
+                  ),
+                default: () => t('resource.udf.delete_confirm')
+              }
+            )
+          ]
+        })
+      }
+    }
+  ]
+
+  const variables = reactive({
+    columns,
+    row: {},
+    tableData: [],
+    breadList: [],
+    id: ref(Number(router.currentRoute.value.params.id) || -1),
+    page: ref(1),
+    pageSize: ref(10),
+    searchVal: ref(),
+    totalPage: ref(1),
+    folderShowRef: ref(false),
+    uploadShowRef: ref(false)
+  })
+
+  const getTableData = (params: IUdfResourceParam) => {
+    const { state } = useAsyncState(
+      queryResourceListPaging({ ...params, type: 'UDF' }).then((res: any) => {
+        const breadList =
+          variables.id === -1
+            ? []
+            : (fileStore.getCurrentDir.split('/') as Array<never>)
+        breadList.shift()
+
+        variables.breadList = breadList
+        variables.totalPage = res.totalPage
+        variables.tableData = res.totalList.map((item: any) => {
+          return { ...item }
+        })
+      }),
+      { total: 0, table: [] }
+    )
+    return state
+  }
+
+  const handleEdit = (row: any) => {
+    variables.folderShowRef = 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
+    }
+
+    deleteResource(id).then(() =>
+      getTableData({
+        id: variables.id,
+        pageSize: variables.pageSize,
+        pageNo: variables.page,
+        searchVal: variables.searchVal
+      })
+    )
+  }
+
+  const goUdfManage = () => {
+    router.push({ name: 'resource-manage' })
+  }
+
+  const goBread = (fullName: string) => {
+    const { id } = variables
+    queryResourceById(
+      {
+        id,
+        type: 'UDF',
+        fullName
+      },
+      id
+    )
+      .then((res: any) => {
+        fileStore.setCurrentDir(res.fullName)
+        router.push({ name: 'resource-sub-manage', params: { id: res.id } })
+      })
+      .catch((error: any) => {
+        window.$message.error(error.message)
+      })
+  }
+
+  return {
+    variables,
+    getTableData,
+    goUdfManage,
+    goBread
+  }
+}