You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dolphinscheduler.apache.org by so...@apache.org on 2022/03/11 12:41:37 UTC

[dolphinscheduler] branch dev updated: [Feature][UI Next][V1.0.0-Alpha]: Refactor the user manage page unde… (#8839)

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

songjian 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 e0ee6f1   [Feature][UI Next][V1.0.0-Alpha]: Refactor the user manage page unde… (#8839)
e0ee6f1 is described below

commit e0ee6f1f2f3f38e5bd5df832e1069c80789808a8
Author: Amy0104 <97...@users.noreply.github.com>
AuthorDate: Fri Mar 11 20:35:11 2022 +0800

     [Feature][UI Next][V1.0.0-Alpha]: Refactor the user manage page unde… (#8839)
    
    * [Feature][UI Next][V1.0.0-Alpha]: Refactor the user manage page under security.
    
    * [Feature][UI Next][V1.0.0-Alpha]: Add license into types file.
---
 .../src/locales/modules/en_US.ts                   |  21 +-
 .../src/locales/modules/zh_CN.ts                   |  20 +-
 .../src/service/modules/data-source/index.ts       |   4 +-
 .../src/service/modules/projects/index.ts          |   4 +-
 .../src/service/modules/resources/index.ts         |   8 +-
 .../src/service/modules/users/index.ts             |   2 +-
 .../src/service/modules/users/types.ts             |   8 +-
 dolphinscheduler-ui-next/src/utils/tree-format.ts  |  31 ++
 .../task/components/node/fields/use-flink.ts       |   2 +-
 .../projects/task/components/node/fields/use-mr.ts |   2 +-
 .../task/components/node/fields/use-sea-tunnel.ts  |  18 +-
 .../task/components/node/fields/use-shell.ts       |  13 +-
 .../task/components/node/fields/use-spark.ts       |   2 +-
 .../task/components/node/fields/use-sql.ts         |  13 +-
 .../user-manage/components/authorize-modal.tsx     | 159 +++++++
 .../user-manage/components/use-authorize.ts        | 196 ++++++++
 .../security/user-manage/components/use-modal.ts   | 496 ---------------------
 .../user-manage/components/use-user-detail.ts      | 177 ++++++++
 .../user-manage/components/user-detail-modal.tsx   | 177 ++++++++
 .../security/user-manage/components/user-modal.tsx | 209 ---------
 .../views/security/user-manage/index.module.scss   |  20 +
 .../src/views/security/user-manage/index.tsx       | 174 ++++----
 .../src/views/security/user-manage/types.ts        |  63 +++
 .../src/views/security/user-manage/use-columns.ts  | 219 +++++++++
 .../src/views/security/user-manage/use-table.ts    | 112 +++++
 .../src/views/security/user-manage/use-table.tsx   | 255 -----------
 26 files changed, 1277 insertions(+), 1128 deletions(-)

diff --git a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
index 50f44bd..2982407 100644
--- a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
+++ b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts
@@ -1014,18 +1014,23 @@ const security = {
     authorize_udf: 'UDF Function Authorize',
     username: 'Username',
     username_exists: 'The username already exists',
-    username_rule_msg: 'Please enter username',
-    user_password: 'Please enter password',
-    user_password_rule_msg:
+    username_tips: 'Please enter username',
+    user_password: 'Password',
+    user_password_tips:
       'Please enter a password containing letters and numbers with a length between 6 and 20',
     user_type: 'User Type',
+    ordinary_user: 'Ordinary users',
+    administrator: 'Administrator',
     tenant_code: 'Tenant',
-    tenant_id_rule_msg: 'Please select tenant',
+    tenant_id_tips: 'Please select tenant',
     queue: 'Queue',
+    queue_tips: 'Please select a queue',
     email: 'Email',
-    email_rule_msg: 'Please enter valid email',
+    email_empty_tips: 'Please enter email',
+    emial_correct_tips: 'Please enter the correct email format',
     phone: 'Phone',
-    phone_rule_msg: 'Please enter valid phone number',
+    phone_empty_tips: 'Please enter phone number',
+    phone_correct_tips: 'Please enter the correct mobile phone format',
     state: 'State',
     state_enabled: 'Enabled',
     state_disabled: 'Disabled',
@@ -1038,7 +1043,9 @@ const security = {
     save_error_msg: 'Failed to save, please retry',
     delete_error_msg: 'Failed to delete, please retry',
     auth_error_msg: 'Failed to authorize, please retry',
-    auth_success_msg: 'Authorize succeeded'
+    auth_success_msg: 'Authorize succeeded',
+    enable: 'Enable',
+    disable: 'Disable'
   },
   alarm_instance: {
     search_input_tips: 'Please input the keywords',
diff --git a/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts b/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
index bb65aec..9222de7 100644
--- a/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
+++ b/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
@@ -990,7 +990,6 @@ const security = {
     update_user: '更新用户',
     delete_user: '删除用户',
     delete_confirm: '确定删除吗?',
-    delete_confirm_tip: '删除用户属于危险操作,请谨慎操作!',
     project: '项目',
     resource: '资源',
     file_resource: '文件资源',
@@ -1003,17 +1002,22 @@ const security = {
     authorize_udf: 'UDF函数授权',
     username: '用户名',
     username_exists: '用户名已存在',
-    username_rule_msg: '请输入用户名',
+    username_tips: '请输入用户名',
     user_password: '密码',
-    user_password_rule_msg: '请输入包含字母和数字,长度在6~20之间的密码',
+    user_password_tips: '请输入包含字母和数字,长度在6~20之间的密码',
     user_type: '用户类型',
+    ordinary_user: '普通用户',
+    administrator: '管理员',
     tenant_code: '租户',
-    tenant_id_rule_msg: '请选择租户',
+    tenant_id_tips: '请选择租户',
     queue: '队列',
+    queue_tips: '默认为租户关联队列',
     email: '邮件',
-    email_rule_msg: '请输入正确的邮箱',
+    email_empty_tips: '请输入邮箱',
+    emial_correct_tips: '请输入正确的邮箱格式',
     phone: '手机',
-    phone_rule_msg: '请输入正确的手机号',
+    phone_empty_tips: '请输入手机号码',
+    phone_correct_tips: '请输入正确的手机格式',
     state: '状态',
     state_enabled: '启用',
     state_disabled: '停用',
@@ -1026,7 +1030,9 @@ const security = {
     save_error_msg: '保存失败,请重试',
     delete_error_msg: '删除失败,请重试',
     auth_error_msg: '授权失败,请重试',
-    auth_success_msg: '授权成功'
+    auth_success_msg: '授权成功',
+    enable: '启用',
+    disable: '停用'
   },
   alarm_instance: {
     search_input_tips: '请输入关键字',
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 956ff3d..7a13cd9 100644
--- a/dolphinscheduler-ui-next/src/service/modules/data-source/index.ts
+++ b/dolphinscheduler-ui-next/src/service/modules/data-source/index.ts
@@ -45,7 +45,7 @@ export function createDataSource(data: IDataSource): any {
   })
 }
 
-export function authedDatasource(params: UserIdReq) {
+export function authedDatasource(params: UserIdReq): any {
   return axios({
     url: '/datasources/authed-datasource',
     method: 'get',
@@ -80,7 +80,7 @@ export function queryDataSourceList(params: TypeReq): any {
   })
 }
 
-export function unAuthDatasource(params: UserIdReq) {
+export function unAuthDatasource(params: UserIdReq): any {
   return axios({
     url: '/datasources/unauth-datasource',
     method: 'get',
diff --git a/dolphinscheduler-ui-next/src/service/modules/projects/index.ts b/dolphinscheduler-ui-next/src/service/modules/projects/index.ts
index 6a8de0e..555fec1 100644
--- a/dolphinscheduler-ui-next/src/service/modules/projects/index.ts
+++ b/dolphinscheduler-ui-next/src/service/modules/projects/index.ts
@@ -34,7 +34,7 @@ export function createProject(data: ProjectsReq): any {
   })
 }
 
-export function queryAuthorizedProject(params: UserIdReq) {
+export function queryAuthorizedProject(params: UserIdReq): any {
   return axios({
     url: '/projects/authed-project',
     method: 'get',
@@ -56,7 +56,7 @@ export function queryAllProjectList(): any {
   })
 }
 
-export function queryUnauthorizedProject(params: UserIdReq) {
+export function queryUnauthorizedProject(params: UserIdReq): any {
   return axios({
     url: '/projects/unauth-project',
     method: 'get',
diff --git a/dolphinscheduler-ui-next/src/service/modules/resources/index.ts b/dolphinscheduler-ui-next/src/service/modules/resources/index.ts
index dc06961..b4a32bf 100644
--- a/dolphinscheduler-ui-next/src/service/modules/resources/index.ts
+++ b/dolphinscheduler-ui-next/src/service/modules/resources/index.ts
@@ -65,7 +65,7 @@ export function createResource(
   })
 }
 
-export function authorizedFile(params: UserIdReq) {
+export function authorizedFile(params: UserIdReq): any {
   return axios({
     url: '/resources/authed-file',
     method: 'get',
@@ -73,7 +73,7 @@ export function authorizedFile(params: UserIdReq) {
   })
 }
 
-export function authorizeResourceTree(params: UserIdReq) {
+export function authorizeResourceTree(params: UserIdReq): any {
   return axios({
     url: '/resources/authed-resource-tree',
     method: 'get',
@@ -81,7 +81,7 @@ export function authorizeResourceTree(params: UserIdReq) {
   })
 }
 
-export function authUDFFunc(params: UserIdReq) {
+export function authUDFFunc(params: UserIdReq): any {
   return axios({
     url: '/resources/authed-udf-func',
     method: 'get',
@@ -158,7 +158,7 @@ export function deleteUdfFunc(id: number): any {
   })
 }
 
-export function unAuthUDFFunc(params: UserIdReq) {
+export function unAuthUDFFunc(params: UserIdReq): any {
   return axios({
     url: '/resources/unauth-udf-func',
     method: 'get',
diff --git a/dolphinscheduler-ui-next/src/service/modules/users/index.ts b/dolphinscheduler-ui-next/src/service/modules/users/index.ts
index 2a9c4c4..bd42f40 100644
--- a/dolphinscheduler-ui-next/src/service/modules/users/index.ts
+++ b/dolphinscheduler-ui-next/src/service/modules/users/index.ts
@@ -135,7 +135,7 @@ export function listAll(params?: ListAllReq): any {
   })
 }
 
-export function queryUserList(params: ListReq) {
+export function queryUserList(params: ListReq): any {
   return axios({
     url: '/users/list-paging',
     method: 'get',
diff --git a/dolphinscheduler-ui-next/src/service/modules/users/types.ts b/dolphinscheduler-ui-next/src/service/modules/users/types.ts
index 0a71e12..3768d22 100644
--- a/dolphinscheduler-ui-next/src/service/modules/users/types.ts
+++ b/dolphinscheduler-ui-next/src/service/modules/users/types.ts
@@ -28,10 +28,10 @@ interface AlertGroupIdReq {
 }
 
 interface UserReq {
-  email?: string
-  tenantId?: number
-  userName?: string
-  userPassword?: string
+  email: string
+  tenantId: number
+  userName: string
+  userPassword: string
   phone?: string
   queue?: string
   state?: number
diff --git a/dolphinscheduler-ui-next/src/utils/tree-format.ts b/dolphinscheduler-ui-next/src/utils/tree-format.ts
new file mode 100644
index 0000000..a27f04b
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/utils/tree-format.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 function removeUselessChildren(
+  list: { children?: []; dirctory?: boolean; disabled?: boolean }[]
+) {
+  if (!list.length) return
+  list.forEach((item) => {
+    if (item.dirctory) item.disabled = true
+    if (!item.children) return
+    if (item.children.length === 0) {
+      delete item.children
+      return
+    }
+    removeUselessChildren(item.children)
+  })
+}
diff --git a/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-flink.ts b/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-flink.ts
index bbe160b..38563cc 100644
--- a/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-flink.ts
+++ b/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-flink.ts
@@ -17,7 +17,7 @@
 import { ref, onMounted, computed } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { queryResourceByProgramType } from '@/service/modules/resources'
-import { removeUselessChildren } from './use-shell'
+import { removeUselessChildren } from '@/utils/tree-format'
 import { useCustomParams, useDeployMode } from '.'
 import type { IJsonItem, ProgramType } from '../types'
 
diff --git a/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-mr.ts b/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-mr.ts
index 6831344..aa0b825 100644
--- a/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-mr.ts
+++ b/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-mr.ts
@@ -17,7 +17,7 @@
 import { ref, onMounted, computed } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { queryResourceByProgramType } from '@/service/modules/resources'
-import { removeUselessChildren } from './use-shell'
+import { removeUselessChildren } from '@/utils/tree-format'
 import { PROGRAM_TYPES } from './use-spark'
 import { useCustomParams } from '.'
 import type { IJsonItem, ProgramType } from '../types'
diff --git a/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-sea-tunnel.ts b/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-sea-tunnel.ts
index b67112c..ee2d132 100644
--- a/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-sea-tunnel.ts
+++ b/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-sea-tunnel.ts
@@ -18,6 +18,7 @@ import { ref, onMounted, watch, computed } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { queryResourceList } from '@/service/modules/resources'
 import { useDeployMode } from '.'
+import { removeUselessChildren } from '@/utils/tree-format'
 import type { IJsonItem } from '../types'
 
 export function useSeaTunnel(model: { [field: string]: any }): IJsonItem[] {
@@ -62,23 +63,6 @@ export function useSeaTunnel(model: { [field: string]: any }): IJsonItem[] {
     loading.value = false
   }
 
-  function removeUselessChildren(
-    list: { children?: []; fullName: string; id: number }[]
-  ) {
-    if (!list.length) return
-    list.forEach((item) => {
-      if (!item.children) {
-        return
-      }
-      if (item.children.length === 0) {
-        model.resourceFiles.push({ id: item.id, fullName: item.fullName })
-        delete item.children
-        return
-      }
-      removeUselessChildren(item.children)
-    })
-  }
-
   onMounted(() => {
     getResourceList()
   })
diff --git a/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-shell.ts b/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-shell.ts
index 3b54567..09e0a03 100644
--- a/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-shell.ts
+++ b/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-shell.ts
@@ -18,6 +18,7 @@ import { ref, onMounted } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { queryResourceList } from '@/service/modules/resources'
 import { useCustomParams } from './use-custom-params'
+import { removeUselessChildren } from '@/utils/tree-format'
 import type { IJsonItem } from '../types'
 
 export function useShell(model: { [field: string]: any }): IJsonItem[] {
@@ -70,15 +71,3 @@ export function useShell(model: { [field: string]: any }): IJsonItem[] {
     ...useCustomParams({ model, field: 'localParams', isSimple: true })
   ]
 }
-
-export function removeUselessChildren(list: { children?: [] }[]) {
-  if (!list.length) return
-  list.forEach((item) => {
-    if (!item.children) return
-    if (item.children.length === 0) {
-      delete item.children
-      return
-    }
-    removeUselessChildren(item.children)
-  })
-}
diff --git a/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-spark.ts b/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-spark.ts
index 4eddb71..ec2a660 100644
--- a/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-spark.ts
+++ b/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-spark.ts
@@ -17,7 +17,7 @@
 import { ref, onMounted, computed } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { queryResourceByProgramType } from '@/service/modules/resources'
-import { removeUselessChildren } from './use-shell'
+import { removeUselessChildren } from '@/utils/tree-format'
 import {
   useCustomParams,
   useDeployMode,
diff --git a/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-sql.ts b/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-sql.ts
index fc10d08..213f5f4 100644
--- a/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-sql.ts
+++ b/dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-sql.ts
@@ -17,6 +17,7 @@
 import { ref, onMounted } from 'vue'
 import { useI18n } from 'vue-i18n'
 import { queryResourceList } from '@/service/modules/resources'
+import { removeUselessChildren } from '@/utils/tree-format'
 import type { IJsonItem } from '../types'
 
 export function useSql(model: { [field: string]: any }): IJsonItem[] {
@@ -147,18 +148,6 @@ export function useSql(model: { [field: string]: any }): IJsonItem[] {
   ]
 }
 
-function removeUselessChildren(list: { children?: [] }[]) {
-  if (!list.length) return
-  list.forEach((item) => {
-    if (!item.children) return
-    if (item.children.length === 0) {
-      delete item.children
-      return
-    }
-    removeUselessChildren(item.children)
-  })
-}
-
 export const TYPE_LIST = [
   {
     value: 'VARCHAR',
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/components/authorize-modal.tsx b/dolphinscheduler-ui-next/src/views/security/user-manage/components/authorize-modal.tsx
new file mode 100644
index 0000000..0118299
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/security/user-manage/components/authorize-modal.tsx
@@ -0,0 +1,159 @@
+/*
+ * 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 { useI18n } from 'vue-i18n'
+import {
+  NTransfer,
+  NSpace,
+  NRadioGroup,
+  NRadioButton,
+  NTreeSelect
+} from 'naive-ui'
+import { useAuthorize } from './use-authorize'
+import Modal from '@/components/modal'
+import styles from '../index.module.scss'
+import type { TAuthType } from '../types'
+
+const props = {
+  show: {
+    type: Boolean as PropType<boolean>,
+    default: false
+  },
+  userId: {
+    type: Number,
+    default: 0
+  },
+  type: {
+    type: String as PropType<TAuthType>,
+    default: 'auth_project'
+  }
+}
+
+export const AuthorizeModal = defineComponent({
+  name: 'authorize-project-modal',
+  props,
+  emits: ['cancel'],
+  setup(props, ctx) {
+    const { t } = useI18n()
+    const { state, onInit, onSave } = useAuthorize()
+    const onCancel = () => {
+      ctx.emit('cancel')
+    }
+    const onConfirm = async () => {
+      const result = await onSave(props.type, props.userId)
+      if (result) onCancel()
+    }
+
+    watch(
+      () => props.show,
+      () => {
+        if (props.show) {
+          onInit(props.type, props.userId)
+        }
+      }
+    )
+
+    return {
+      t,
+      ...toRefs(state),
+      onCancel,
+      onConfirm
+    }
+  },
+  render(props: { type: TAuthType }) {
+    const { t } = this
+    const { type } = props
+    return (
+      <Modal
+        show={this.show}
+        title={t(`security.user.${type}`)}
+        onCancel={this.onCancel}
+        confirmLoading={this.loading}
+        onConfirm={this.onConfirm}
+        confirmClassName='btn-submit'
+        cancelClassName='btn-cancel'
+      >
+        {type === 'authorize_project' && (
+          <NTransfer
+            virtualScroll
+            options={this.unauthorizedProjects}
+            filterable
+            v-model={[this.authorizedProjects, 'value']}
+            class={styles.transfer}
+          />
+        )}
+        {type === 'authorize_datasource' && (
+          <NTransfer
+            virtualScroll
+            options={this.unauthorizedDatasources}
+            filterable
+            v-model:value={this.authorizedDatasources}
+            class={styles.transfer}
+          />
+        )}
+        {type === 'authorize_udf' && (
+          <NTransfer
+            virtualScroll
+            options={this.unauthorizedUdfs}
+            filterable
+            v-model:value={this.authorizedUdfs}
+            class={styles.transfer}
+          />
+        )}
+        {type === 'authorize_resource' && (
+          <NSpace vertical>
+            <NRadioGroup v-model:value={this.resourceType}>
+              <NRadioButton key='file' value='file'>
+                {t('security.user.file_resource')}
+              </NRadioButton>
+              <NRadioButton key='udf' value='udf'>
+                {t('security.user.udf_resource')}
+              </NRadioButton>
+            </NRadioGroup>
+            <NTreeSelect
+              v-show={this.resourceType === 'file'}
+              filterable
+              multiple
+              cascade
+              checkable
+              checkStrategy='child'
+              key-field='id'
+              label-field='fullName'
+              options={this.fileResources}
+              v-model:value={this.authorizedFileResources}
+            />
+            <NTreeSelect
+              v-show={this.resourceType === 'udf'}
+              filterable
+              multiple
+              cascade
+              checkable
+              checkStrategy='child'
+              key-field='id'
+              label-field='fullName'
+              options={this.udfResources}
+              v-model:value={this.authorizedUdfResources}
+            />
+          </NSpace>
+        )}
+      </Modal>
+    )
+  }
+})
+
+export default AuthorizeModal
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/components/use-authorize.ts b/dolphinscheduler-ui-next/src/views/security/user-manage/components/use-authorize.ts
new file mode 100644
index 0000000..9c43b98
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/security/user-manage/components/use-authorize.ts
@@ -0,0 +1,196 @@
+/*
+ * 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 { useI18n } from 'vue-i18n'
+import {
+  queryAuthorizedProject,
+  queryUnauthorizedProject
+} from '@/service/modules/projects'
+import {
+  authedDatasource,
+  unAuthDatasource
+} from '@/service/modules/data-source'
+import {
+  authorizedFile,
+  authorizeResourceTree,
+  authUDFFunc,
+  unAuthUDFFunc
+} from '@/service/modules/resources'
+import {
+  grantProject,
+  grantResource,
+  grantDataSource,
+  grantUDFFunc
+} from '@/service/modules/users'
+import { removeUselessChildren } from '@/utils/tree-format'
+import type { TAuthType, IResourceOption, IOption } from '../types'
+
+export function useAuthorize() {
+  const { t } = useI18n()
+
+  const state = reactive({
+    saving: false,
+    loading: false,
+    authorizedProjects: [] as number[],
+    unauthorizedProjects: [] as IOption[],
+    authorizedDatasources: [] as number[],
+    unauthorizedDatasources: [] as IOption[],
+    authorizedUdfs: [] as number[],
+    unauthorizedUdfs: [] as IOption[],
+    resourceType: 'file',
+    fileResources: [] as IResourceOption[],
+    udfResources: [] as IResourceOption[],
+    authorizedFileResources: [] as number[],
+    authorizedUdfResources: [] as number[]
+  })
+
+  const getProjects = async (userId: number) => {
+    if (state.loading) return
+    state.loading = true
+    const projects = await Promise.all([
+      queryAuthorizedProject({ userId }),
+      queryUnauthorizedProject({ userId })
+    ])
+    state.loading = false
+    state.authorizedProjects = projects[0].map(
+      (item: { name: string; id: number }) => item.id
+    )
+    state.unauthorizedProjects = [...projects[0], ...projects[1]].map(
+      (item: { name: string; id: number }) => ({
+        label: item.name,
+        value: item.id
+      })
+    )
+  }
+
+  const getDatasources = async (userId: number) => {
+    if (state.loading) return
+    state.loading = true
+    const datasources = await Promise.all([
+      authedDatasource({ userId }),
+      unAuthDatasource({ userId })
+    ])
+    state.loading = false
+    state.authorizedDatasources = datasources[0].map(
+      (item: { name: string; id: number }) => item.id
+    )
+    state.unauthorizedDatasources = [...datasources[0], ...datasources[1]].map(
+      (item: { name: string; id: number }) => ({
+        label: item.name,
+        value: item.id
+      })
+    )
+  }
+
+  const getUdfs = async (userId: number) => {
+    if (state.loading) return
+    state.loading = true
+    const udfs = await Promise.all([
+      authUDFFunc({ userId }),
+      unAuthUDFFunc({ userId })
+    ])
+    state.loading = false
+    state.authorizedUdfs = udfs[0].map(
+      (item: { name: string; id: number }) => item.id
+    )
+    state.unauthorizedUdfs = [...udfs[0], ...udfs[1]].map(
+      (item: { name: string; id: number }) => ({
+        label: item.name,
+        value: item.id
+      })
+    )
+  }
+
+  const getResources = async (userId: number) => {
+    if (state.loading) return
+    state.loading = true
+    const resources = await Promise.all([
+      authorizeResourceTree({ userId }),
+      authorizedFile({ userId })
+    ])
+    state.loading = false
+    removeUselessChildren(resources[0])
+    let udfResources = [] as IResourceOption[]
+    let fileResources = [] as IResourceOption[]
+    resources[0].forEach((item: IResourceOption) => {
+      item.type === 'FILE' ? fileResources.push(item) : udfResources.push(item)
+    })
+    let udfTargets = [] as number[]
+    let fileTargets = [] as number[]
+    resources[1].forEach((item: { type: string; id: number }) => {
+      item.type === 'FILE'
+        ? fileTargets.push(item.id)
+        : udfTargets.push(item.id)
+    })
+    state.fileResources = fileResources
+    state.udfResources = udfResources
+    console.log(fileResources)
+    state.authorizedFileResources = fileTargets
+    state.authorizedUdfResources = fileTargets
+  }
+
+  const onInit = (type: TAuthType, userId: number) => {
+    if (type === 'authorize_project') {
+      getProjects(userId)
+    }
+    if (type === 'authorize_datasource') {
+      getDatasources(userId)
+    }
+    if (type === 'authorize_udf') {
+      getUdfs(userId)
+    }
+    if (type === 'authorize_resource') {
+      getResources(userId)
+    }
+  }
+
+  const onSave = async (type: TAuthType, userId: number) => {
+    if (state.saving) return false
+    state.saving = true
+    if (type === 'authorize_project') {
+      await grantProject({
+        userId,
+        projectIds: state.authorizedProjects.join(',')
+      })
+    }
+    if (type === 'authorize_datasource') {
+      await grantDataSource({
+        userId,
+        datasourceIds: state.authorizedDatasources.join(',')
+      })
+    }
+    if (type === 'authorize_udf') {
+      await grantUDFFunc({
+        userId,
+        udfIds: state.authorizedUdfResources.join(',')
+      })
+    }
+    if (type === 'authorize_resource') {
+      await grantResource({
+        userId,
+        resourceIds:
+          state.resourceType === 'file'
+            ? state.authorizedFileResources.join(',')
+            : state.authorizedUdfResources.join(',')
+      })
+    }
+    state.saving = false
+    return true
+  }
+
+  return { state, onInit, onSave }
+}
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/components/use-modal.ts b/dolphinscheduler-ui-next/src/views/security/user-manage/components/use-modal.ts
deleted file mode 100644
index fe9507e..0000000
--- a/dolphinscheduler-ui-next/src/views/security/user-manage/components/use-modal.ts
+++ /dev/null
@@ -1,496 +0,0 @@
-/*
- * 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 { ref, watch, computed, InjectionKey } from 'vue'
-import { useI18n } from 'vue-i18n'
-import { useMessage } from 'naive-ui'
-import { queryTenantList } from '@/service/modules/tenants'
-import { queryList } from '@/service/modules/queues'
-import {
-  createUser,
-  updateUser,
-  delUserById,
-  verifyUserName,
-  grantProject,
-  grantResource,
-  grantDataSource,
-  grantUDFFunc
-} from '@/service/modules/users'
-import {
-  queryAuthorizedProject,
-  queryUnauthorizedProject
-} from '@/service/modules/projects'
-import {
-  authorizedFile,
-  authorizeResourceTree,
-  authUDFFunc,
-  unAuthUDFFunc
-} from '@/service/modules/resources'
-import {
-  authedDatasource,
-  unAuthDatasource
-} from '@/service/modules/data-source'
-import regexUtils from '@/utils/regex'
-export type Mode =
-  | 'add'
-  | 'edit'
-  | 'delete'
-  | 'auth_project'
-  | 'auth_resource'
-  | 'auth_datasource'
-  | 'auth_udf'
-
-export type UserModalSharedStateType = ReturnType<
-  typeof useSharedUserModalState
-> & {
-  onSuccess?: (mode: Mode) => void
-}
-
-export const UserModalSharedStateKey: InjectionKey<UserModalSharedStateType> =
-  Symbol()
-
-export function useSharedUserModalState() {
-  return {
-    show: ref(false),
-    mode: ref<Mode>('add'),
-    user: ref()
-  }
-}
-
-export function useModal({
-  onSuccess,
-  show,
-  mode,
-  user
-}: UserModalSharedStateType) {
-  const message = useMessage()
-  const { t } = useI18n()
-  const formRef = ref()
-  const formValues = ref({
-    userName: '',
-    userPassword: '',
-    tenantId: 0,
-    email: '',
-    queue: '',
-    phone: '',
-    state: 1
-  })
-  const tenants = ref<any[]>([])
-  const queues = ref<any[]>([])
-  const authorizedProjects = ref<string[]>([])
-  const projects = ref<any[]>([])
-  const authorizedFiles = ref<string[]>([])
-  const originResourceTree = ref<any[]>([])
-  const resourceType = ref<'file' | 'udf'>()
-  const authorizedUDF = ref<string[]>([])
-  const UDFs = ref<any[]>([])
-  const authorizedDatasource = ref<string[]>([])
-  const datasource = ref<any[]>([])
-  const optionsLoading = ref(false)
-  const confirmLoading = ref(false)
-
-  const formRules = computed(() => {
-    return {
-      userName: {
-        required: true,
-        message: t('security.user.username_rule_msg'),
-        trigger: 'blur'
-      },
-      userPassword: {
-        required: mode.value === 'add',
-        validator(rule: any, value?: string) {
-          if (mode.value !== 'add' && !value) {
-            return true
-          }
-          const msg = t('security.user.user_password_rule_msg')
-          if (!value || !regexUtils.password.test(value)) {
-            return new Error(msg)
-          }
-          return true
-        },
-        trigger: ['blur', 'input']
-      },
-      tenantId: {
-        required: true,
-        validator(rule: any, value?: number) {
-          const msg = t('security.user.tenant_id_rule_msg')
-          if (typeof value === 'number') {
-            return true
-          }
-          return new Error(msg)
-        },
-        trigger: 'blur'
-      },
-      email: {
-        required: true,
-        validator(rule: any, value?: string) {
-          const msg = t('security.user.email_rule_msg')
-          if (!value || !regexUtils.email.test(value)) {
-            return new Error(msg)
-          }
-          return true
-        },
-        trigger: ['blur', 'input']
-      },
-      phone: {
-        validator(rule: any, value?: string) {
-          const msg = t('security.user.phone_rule_msg')
-          if (value && !regexUtils.phone.test(value)) {
-            return new Error(msg)
-          }
-          return true
-        },
-        trigger: ['blur', 'input']
-      }
-    }
-  })
-
-  const resourceTree = computed(() => {
-    const loopTree = (arr: any[]): any[] =>
-      arr
-        .map((d) => {
-          if (
-            (resourceType.value &&
-              `${d.type}`.toLowerCase() === resourceType.value) ||
-            !resourceType.value
-          ) {
-            const obj = { key: `${d.pid}-${d.id}`, label: d.name }
-            const children = d.children
-            if (children instanceof Array && children.length > 0) {
-              return {
-                ...obj,
-                children: loopTree(children)
-              }
-            }
-            return obj
-          }
-          return null
-        })
-        .filter((f) => f)
-    const data = loopTree(originResourceTree.value)
-    return data
-  })
-
-  const titleMap = computed(() => {
-    const titles: Record<Mode, string> = {
-      add: t('security.user.create_user'),
-      edit: t('security.user.update_user'),
-      delete: t('security.user.delete_user'),
-      auth_project: t('security.user.authorize_project'),
-      auth_resource: t('security.user.authorize_resource'),
-      auth_datasource: t('security.user.authorize_datasource'),
-      auth_udf: t('security.user.authorize_udf')
-    }
-    return titles
-  })
-
-  const setFormValues = () => {
-    const defaultValues = {
-      userName: '',
-      userPassword: '',
-      tenantId: tenants.value[0]?.value,
-      email: '',
-      queue: queues.value[0]?.value,
-      phone: '',
-      state: 1
-    }
-    if (!user.value) {
-      formValues.value = defaultValues
-    } else {
-      const v: any = {}
-      Object.keys(defaultValues).map((k) => {
-        v[k] = user.value[k]
-      })
-      v.userPassword = ''
-      formValues.value = v
-    }
-  }
-
-  const prepareOptions = async () => {
-    optionsLoading.value = true
-    Promise.all([queryTenantList(), queryList()])
-      .then((res) => {
-        tenants.value =
-          res[0]?.map((d: any) => ({
-            label: d.tenantCode,
-            value: d.id
-          })) || []
-        queues.value =
-          res[1]?.map((d: any) => ({
-            label: d.queueName,
-            value: d.queue
-          })) || []
-      })
-      .finally(() => {
-        optionsLoading.value = false
-      })
-  }
-
-  const fetchProjects = async () => {
-    optionsLoading.value = true
-    Promise.all([
-      queryAuthorizedProject({ userId: user.value.id }),
-      queryUnauthorizedProject({ userId: user.value.id })
-    ])
-      .then((res: any[]) => {
-        const ids: string[] = []
-        res[0]?.forEach((d: any) => {
-          if (!ids.includes(d.id)) {
-            ids.push(d.id)
-          }
-        })
-        authorizedProjects.value = ids
-        projects.value =
-          res?.flat().map((d: any) => ({ label: d.name, value: d.id })) || []
-      })
-      .finally(() => {
-        optionsLoading.value = false
-      })
-  }
-
-  const fetchResources = async () => {
-    optionsLoading.value = true
-    Promise.all([
-      authorizedFile({ userId: user.value.id }),
-      authorizeResourceTree({ userId: user.value.id })
-    ])
-      .then((res: any[]) => {
-        const ids: string[] = []
-        const getIds = (arr: any[]) => {
-          arr.forEach((d) => {
-            const children = d.children
-            if (children instanceof Array && children.length > 0) {
-              getIds(children)
-            } else {
-              ids.push(`${d.pid}-${d.id}`)
-            }
-          })
-        }
-        getIds(res[0] || [])
-        authorizedFiles.value = ids
-        originResourceTree.value = res[1] || []
-      })
-      .finally(() => {
-        optionsLoading.value = false
-      })
-  }
-
-  const fetchDatasource = async () => {
-    optionsLoading.value = true
-    Promise.all([
-      authedDatasource({ userId: user.value.id }),
-      unAuthDatasource({ userId: user.value.id })
-    ])
-      .then((res: any[]) => {
-        const ids: string[] = []
-        res[0]?.forEach((d: any) => {
-          if (!ids.includes(d.id)) {
-            ids.push(d.id)
-          }
-        })
-        authorizedDatasource.value = ids
-        datasource.value =
-          res?.flat().map((d: any) => ({ label: d.name, value: d.id })) || []
-      })
-      .finally(() => {
-        optionsLoading.value = false
-      })
-  }
-
-  const fetchUDFs = async () => {
-    optionsLoading.value = true
-    Promise.all([
-      authUDFFunc({ userId: user.value.id }),
-      unAuthUDFFunc({ userId: user.value.id })
-    ])
-      .then((res: any[]) => {
-        const ids: string[] = []
-        res[0]?.forEach((d: any) => {
-          if (!ids.includes(d.id)) {
-            ids.push(d.id)
-          }
-        })
-        authorizedUDF.value = ids
-        UDFs.value =
-          res?.flat().map((d: any) => ({ label: d.name, value: d.id })) || []
-      })
-      .finally(() => {
-        optionsLoading.value = false
-      })
-  }
-
-  const onModalCancel = () => {
-    show.value = false
-  }
-
-  const onDelete = () => {
-    confirmLoading.value = true
-    delUserById({ id: user.value.id })
-      .then(
-        () => {
-          onSuccess?.(mode.value)
-          onModalCancel()
-        },
-        () => {
-          message.error(t('security.user.delete_error_msg'))
-        }
-      )
-      .finally(() => {
-        confirmLoading.value = false
-      })
-  }
-
-  const onCreateUser = () => {
-    confirmLoading.value = true
-    verifyUserName({ userName: formValues.value.userName })
-      .then(
-        () => createUser(formValues.value),
-        (error) => {
-          if (`${error.message}`.includes('exists')) {
-            message.error(t('security.user.username_exists'))
-          }
-          return false
-        }
-      )
-      .then(
-        (res) => {
-          if (res) {
-            onSuccess?.(mode.value)
-            onModalCancel()
-          }
-        },
-        () => {
-          message.error(t('security.user.save_error_msg'))
-        }
-      )
-      .finally(() => {
-        confirmLoading.value = false
-      })
-  }
-
-  const onUpdateUser = () => {
-    confirmLoading.value = true
-    updateUser({ id: user.value.id, ...formValues.value })
-      .then(
-        () => {
-          onSuccess?.(mode.value)
-          onModalCancel()
-        },
-        () => {
-          message.error(t('security.user.save_error_msg'))
-        }
-      )
-      .finally(() => {
-        confirmLoading.value = false
-      })
-  }
-
-  const onGrant = (grantReq: () => Promise<any>) => {
-    confirmLoading.value = true
-    grantReq()
-      .then(
-        () => {
-          onSuccess?.(mode.value)
-          onModalCancel()
-          message.success(t('security.user.auth_success_msg'))
-        },
-        () => {
-          message.error(t('security.user.auth_error_msg'))
-        }
-      )
-      .finally(() => {
-        confirmLoading.value = false
-      })
-  }
-
-  const onConfirm = () => {
-    if (mode.value === 'add' || mode.value === 'edit') {
-      formRef.value.validate((errors: any) => {
-        if (!errors) {
-          user.value ? onUpdateUser() : onCreateUser()
-        }
-      })
-    } else {
-      mode.value === 'delete' && onDelete()
-      mode.value === 'auth_project' &&
-        onGrant(() =>
-          grantProject({
-            userId: user.value.id,
-            projectIds: authorizedProjects.value.join(',')
-          })
-        )
-      mode.value === 'auth_resource' &&
-        onGrant(() =>
-          grantResource({
-            userId: user.value.id,
-            resourceIds: authorizedFiles.value.join(',')
-          })
-        )
-      mode.value === 'auth_datasource' &&
-        onGrant(() =>
-          grantDataSource({
-            userId: user.value.id,
-            datasourceIds: authorizedDatasource.value.join(',')
-          })
-        )
-      mode.value === 'auth_udf' &&
-        onGrant(() =>
-          grantUDFFunc({
-            userId: user.value.id,
-            udfIds: authorizedUDF.value.join(',')
-          })
-        )
-    }
-  }
-
-  watch([show, mode], () => {
-    show.value && ['add', 'edit'].includes(mode.value) && prepareOptions()
-    show.value && mode.value === 'auth_project' && fetchProjects()
-    show.value && mode.value === 'auth_resource' && fetchResources()
-    show.value && mode.value === 'auth_datasource' && fetchDatasource()
-    show.value && mode.value === 'auth_udf' && fetchUDFs()
-  })
-
-  watch([queues, tenants, user], () => {
-    setFormValues()
-  })
-
-  return {
-    show,
-    mode,
-    user,
-    titleMap,
-    onModalCancel,
-    formRef,
-    formValues,
-    formRules,
-    tenants,
-    queues,
-    authorizedProjects,
-    projects,
-    authorizedDatasource,
-    datasource,
-    authorizedUDF,
-    UDFs,
-    authorizedFiles,
-    resourceTree,
-    resourceType,
-    optionsLoading,
-    onConfirm,
-    confirmLoading
-  }
-}
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/components/use-user-detail.ts b/dolphinscheduler-ui-next/src/views/security/user-manage/components/use-user-detail.ts
new file mode 100644
index 0000000..d436716
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/security/user-manage/components/use-user-detail.ts
@@ -0,0 +1,177 @@
+/*
+ * 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 { onMounted, reactive, ref } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { pick } from 'lodash'
+import { queryTenantList } from '@/service/modules/tenants'
+import { queryList } from '@/service/modules/queues'
+import { verifyUserName, createUser, updateUser } from '@/service/modules/users'
+import { useUserStore } from '@/store/user/user'
+import type { IRecord, UserReq, UserInfoRes } from '../types'
+
+export function useUserDetail() {
+  const { t } = useI18n()
+  const userStore = useUserStore()
+  const userInfo = userStore.getUserInfo as UserInfoRes
+  const IS_ADMIN = userInfo.userType === 'ADMIN_USER'
+
+  const initialValues = {
+    userName: '',
+    userPassword: '',
+    tenantId: 0,
+    email: '',
+    queue: '',
+    phone: '',
+    state: 1
+  } as UserReq
+
+  let PREV_NAME: string
+
+  const state = reactive({
+    formRef: ref(),
+    formData: { ...initialValues },
+    saving: false,
+    loading: false,
+    queues: [] as { label: string; value: string }[],
+    tenants: [] as { label: string; value: number }[]
+  })
+
+  const formRules = {
+    userName: {
+      trigger: ['input', 'blur'],
+      required: true,
+      validator(validator: any, value: string) {
+        if (!value.trim()) {
+          return new Error(t('security.user.username_tips'))
+        }
+      }
+    },
+    userPassword: {
+      trigger: ['input', 'blur'],
+      required: true,
+      validator(validator: any, value: string) {
+        if (
+          !value ||
+          !/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?![`~!@#$%^&*()_\-+=<>?:"{}|,./;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、]+$)[`~!@#$%^&*()_\-+=<>?:"{}|,./;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、0-9A-Za-z]{6,22}$/.test(
+            value
+          )
+        ) {
+          return new Error(t('security.user.user_password_tips'))
+        }
+      }
+    },
+    tenantId: {
+      trigger: ['input', 'blur'],
+      required: true,
+      validator(validator: any, value: string) {
+        if (IS_ADMIN && !value) {
+          return new Error(t('security.user.tenant_id_tips'))
+        }
+      }
+    },
+    email: {
+      trigger: ['input', 'blur'],
+      required: true,
+      validator(validator: any, value: string) {
+        if (!value) {
+          return new Error(t('security.user.email_empty_tips'))
+        }
+        if (
+          !/^([a-zA-Z0-9]+[_|\-|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\-|\.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{2,}$/.test(
+            value
+          )
+        ) {
+          return new Error(t('security.user.emial_correct_tips'))
+        }
+      }
+    },
+    phone: {
+      trigger: ['input', 'blur'],
+      validator(validator: any, value: string) {
+        if (value && !/^1(3|4|5|6|7|8)\d{9}$/.test(value)) {
+          return new Error(t('security.user.phone_correct_tips'))
+        }
+      }
+    }
+  }
+
+  const getQueues = async () => {
+    const result = await queryList()
+    state.queues = result.map((queue: { queueName: string; id: string }) => ({
+      label: queue.queueName,
+      value: queue.id
+    }))
+    if (state.queues.length) {
+      initialValues.queue = state.queues[0].value
+      state.formData.queue = state.queues[0].value
+    }
+  }
+  const getTenants = async () => {
+    const result = await queryTenantList()
+    state.tenants = result.map(
+      (tenant: { tenantCode: string; id: number }) => ({
+        label: tenant.tenantCode,
+        value: tenant.id
+      })
+    )
+    if (state.tenants.length) {
+      initialValues.tenantId = state.tenants[0].value
+      state.formData.tenantId = state.tenants[0].value
+    }
+  }
+  const onReset = () => {
+    state.formData = { ...initialValues }
+  }
+  const onSave = async (id?: number): Promise<boolean> => {
+    await state.formRef.validate()
+    if (state.saving) return false
+    state.saving = true
+    if (PREV_NAME !== state.formData.userName) {
+      await verifyUserName({ userName: state.formData.userName })
+    }
+
+    id
+      ? await updateUser({ id, ...state.formData })
+      : await createUser(state.formData)
+
+    state.saving = false
+    return true
+  }
+  const onSetValues = (record: IRecord) => {
+    state.formData = {
+      ...pick(record, [
+        'userName',
+        'tenantId',
+        'email',
+        'queue',
+        'phone',
+        'state'
+      ]),
+      userPassword: ''
+    } as UserReq
+    PREV_NAME = state.formData.userName
+  }
+
+  onMounted(async () => {
+    if (IS_ADMIN) {
+      getQueues()
+      getTenants()
+    }
+  })
+
+  return { state, formRules, IS_ADMIN, onReset, onSave, onSetValues }
+}
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/components/user-detail-modal.tsx b/dolphinscheduler-ui-next/src/views/security/user-manage/components/user-detail-modal.tsx
new file mode 100644
index 0000000..1db19ca
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/security/user-manage/components/user-detail-modal.tsx
@@ -0,0 +1,177 @@
+/*
+ * 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 { useI18n } from 'vue-i18n'
+import {
+  NInput,
+  NForm,
+  NFormItem,
+  NSelect,
+  NRadio,
+  NRadioGroup,
+  NSpace
+} from 'naive-ui'
+import { useUserDetail } from './use-user-detail'
+import Modal from '@/components/modal'
+import type { IRecord } from '../types'
+
+const props = {
+  show: {
+    type: Boolean as PropType<boolean>,
+    default: false
+  },
+  currentRecord: {
+    type: Object as PropType<IRecord | null>,
+    default: {}
+  }
+}
+
+export const UserModal = defineComponent({
+  name: 'user-modal',
+  props,
+  emits: ['cancel', 'update'],
+  setup(props, ctx) {
+    const { t } = useI18n()
+    const { state, IS_ADMIN, formRules, onReset, onSave, onSetValues } =
+      useUserDetail()
+    const onCancel = () => {
+      onReset()
+      ctx.emit('cancel')
+    }
+    const onConfirm = async () => {
+      const result = await onSave(props.currentRecord?.id)
+      if (!result) return
+      onCancel()
+      ctx.emit('update')
+    }
+
+    watch(
+      () => props.show,
+      () => {
+        if (props.show && props.currentRecord?.id) {
+          onSetValues(props.currentRecord)
+        }
+      }
+    )
+
+    return {
+      t,
+      ...toRefs(state),
+      IS_ADMIN,
+      formRules,
+      onCancel,
+      onConfirm
+    }
+  },
+  render(props: { currentRecord: IRecord }) {
+    const { t } = this
+    const { currentRecord } = props
+    return (
+      <Modal
+        show={this.show}
+        title={`${t(
+          currentRecord?.id
+            ? 'security.user.update_user'
+            : 'security.user.create_user'
+        )}`}
+        onCancel={this.onCancel}
+        confirmLoading={this.loading}
+        onConfirm={this.onConfirm}
+        confirmClassName='btn-submit'
+        cancelClassName='btn-cancel'
+      >
+        <NForm
+          ref='formRef'
+          model={this.formData}
+          rules={this.formRules}
+          labelPlacement='left'
+          labelAlign='left'
+          labelWidth={80}
+        >
+          <NFormItem label={t('security.user.username')} path='userName'>
+            <NInput
+              class='input-username'
+              v-model:value={this.formData.userName}
+              minlength={3}
+              maxlength={39}
+              placeholder={t('security.user.username_tips')}
+            />
+          </NFormItem>
+          <NFormItem
+            label={t('security.user.user_password')}
+            path='userPassword'
+          >
+            <NInput
+              class='input-password'
+              type='password'
+              v-model:value={this.formData.userPassword}
+              placeholder={t('security.user.user_password_tips')}
+            />
+          </NFormItem>
+          {this.IS_ADMIN && (
+            <NFormItem label={t('security.user.tenant_code')} path='tenantId'>
+              <NSelect
+                class='select-tenant'
+                options={this.tenants}
+                v-model:value={this.formData.tenantId}
+              />
+            </NFormItem>
+          )}
+          {this.IS_ADMIN && (
+            <NFormItem label={t('security.user.queue')} path='queue'>
+              <NSelect
+                class='select-queue'
+                options={this.queues}
+                v-model:value={this.formData.queue}
+                placeholder={t('security.user.queue_tips')}
+              />
+            </NFormItem>
+          )}
+          <NFormItem label={t('security.user.email')} path='email'>
+            <NInput
+              class='input-email'
+              v-model:value={this.formData.email}
+              placeholder={t('security.user.email_empty_tips')}
+            />
+          </NFormItem>
+          <NFormItem label={t('security.user.phone')} path='phone'>
+            <NInput
+              class='input-phone'
+              v-model:value={this.formData.phone}
+              placeholder={t('security.user.phone_empty_tips')}
+            />
+          </NFormItem>
+          <NFormItem label={t('security.user.state')} path='state'>
+            <NRadioGroup v-model:value={this.formData.state}>
+              <NSpace>
+                <NRadio value={1} class='radio-state-enable'>
+                  {this.t('security.user.enable')}
+                </NRadio>
+                <NRadio value={0} class='radio-state-disable'>
+                  {this.t('security.user.disable')}
+                </NRadio>
+              </NSpace>
+            </NRadioGroup>
+          </NFormItem>
+        </NForm>
+      </Modal>
+    )
+  }
+})
+
+export default UserModal
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/components/user-modal.tsx b/dolphinscheduler-ui-next/src/views/security/user-manage/components/user-modal.tsx
deleted file mode 100644
index e966095..0000000
--- a/dolphinscheduler-ui-next/src/views/security/user-manage/components/user-modal.tsx
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * 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, inject } from 'vue'
-import { useI18n } from 'vue-i18n'
-import {
-  NInput,
-  NForm,
-  NFormItem,
-  NSelect,
-  NRadio,
-  NRadioGroup,
-  NRadioButton,
-  NSpace,
-  NAlert,
-  NTransfer,
-  NTreeSelect
-} from 'naive-ui'
-
-import Modal from '@/components/modal'
-import {
-  useModal,
-  useSharedUserModalState,
-  UserModalSharedStateKey
-} from './use-modal'
-
-export const UserModal = defineComponent({
-  name: 'user-modal',
-  setup() {
-    const { t } = useI18n()
-    const sharedState =
-      inject(UserModalSharedStateKey) || useSharedUserModalState()
-    const modalState = useModal(sharedState)
-
-    return {
-      t,
-      ...modalState
-    }
-  },
-  render() {
-    const { t } = this
-    return (
-      <Modal
-        show={this.show}
-        title={this.titleMap?.[this.mode || 'add']}
-        onCancel={this.onModalCancel}
-        confirmDisabled={this.optionsLoading}
-        confirmLoading={this.confirmLoading}
-        onConfirm={this.onConfirm}
-        confirmClassName='btn-submit'
-        cancelClassName='btn-cancel'
-      >
-        {{
-          default: () => {
-            if (this.mode === 'delete') {
-              return (
-                <NAlert type='error' title={t('security.user.delete_confirm')}>
-                  {t('security.user.delete_confirm_tip')}
-                </NAlert>
-              )
-            }
-            if (this.mode === 'auth_project') {
-              return (
-                <NTransfer
-                  virtualScroll
-                  options={this.projects}
-                  filterable
-                  v-model:value={this.authorizedProjects}
-                  style={{ margin: '0 auto' }}
-                />
-              )
-            }
-            if (this.mode === 'auth_datasource') {
-              return (
-                <NTransfer
-                  virtualScroll
-                  options={this.datasource}
-                  filterable
-                  v-model:value={this.authorizedDatasource}
-                  style={{ margin: '0 auto' }}
-                />
-              )
-            }
-            if (this.mode === 'auth_udf') {
-              return (
-                <NTransfer
-                  virtualScroll
-                  options={this.UDFs}
-                  filterable
-                  v-model:value={this.authorizedUDF}
-                  style={{ margin: '0 auto' }}
-                />
-              )
-            }
-            if (this.mode === 'auth_resource') {
-              return (
-                <NSpace vertical>
-                  <NRadioGroup v-model:value={this.resourceType}>
-                    <NRadioButton key='file' value='file'>
-                      {t('security.user.file_resource')}
-                    </NRadioButton>
-                    <NRadioButton key='udf' value='udf'>
-                      {t('security.user.udf_resource')}
-                    </NRadioButton>
-                  </NRadioGroup>
-                  <NTreeSelect
-                    multiple
-                    cascade
-                    checkable
-                    checkStrategy='child'
-                    defaultExpandAll
-                    options={this.resourceTree}
-                    v-model:value={this.authorizedFiles}
-                  />
-                </NSpace>
-              )
-            }
-            return (
-              <NForm
-                ref='formRef'
-                model={this.formValues}
-                rules={this.formRules}
-                labelPlacement='left'
-                labelAlign='left'
-                labelWidth={80}
-              >
-                <NFormItem label={t('security.user.username')} path='userName'>
-                  <NInput
-                    class='input-username'
-                    inputProps={{ autocomplete: 'off' }}
-                    v-model:value={this.formValues.userName}
-                  />
-                </NFormItem>
-                <NFormItem
-                  label={t('security.user.user_password')}
-                  path='userPassword'
-                >
-                  <NInput
-                    class='input-password'
-                    inputProps={{ autocomplete: 'off' }}
-                    type='password'
-                    v-model:value={this.formValues.userPassword}
-                  />
-                </NFormItem>
-                <NFormItem
-                  label={t('security.user.tenant_code')}
-                  path='tenantId'
-                >
-                  <NSelect
-                    class='select-tenant'
-                    options={this.tenants}
-                    v-model:value={this.formValues.tenantId}
-                  />
-                </NFormItem>
-                <NFormItem label={t('security.user.queue')} path='queue'>
-                  <NSelect
-                    class='select-queue'
-                    options={this.queues}
-                    v-model:value={this.formValues.queue}
-                  />
-                </NFormItem>
-                <NFormItem label={t('security.user.email')} path='email'>
-                  <NInput
-                    class='input-email'
-                    v-model:value={this.formValues.email}
-                  />
-                </NFormItem>
-                <NFormItem label={t('security.user.phone')} path='phone'>
-                  <NInput
-                    class='input-phone'
-                    v-model:value={this.formValues.phone}
-                  />
-                </NFormItem>
-                <NFormItem label={t('security.user.state')} path='state'>
-                  <NRadioGroup v-model:value={this.formValues.state}>
-                    <NSpace>
-                      <NRadio value={1} class='radio-state-enable'>
-                        启用
-                      </NRadio>
-                      <NRadio value={0} class='radio-state-disable'>
-                        停用
-                      </NRadio>
-                    </NSpace>
-                  </NRadioGroup>
-                </NFormItem>
-              </NForm>
-            )
-          }
-        }}
-      </Modal>
-    )
-  }
-})
-
-export default UserModal
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/index.module.scss b/dolphinscheduler-ui-next/src/views/security/user-manage/index.module.scss
new file mode 100644
index 0000000..68d62fd
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/security/user-manage/index.module.scss
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.transfer {
+  width: 100%;
+}
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/index.tsx b/dolphinscheduler-ui-next/src/views/security/user-manage/index.tsx
index 628ac3b..4277701 100644
--- a/dolphinscheduler-ui-next/src/views/security/user-manage/index.tsx
+++ b/dolphinscheduler-ui-next/src/views/security/user-manage/index.tsx
@@ -15,132 +15,112 @@
  * limitations under the License.
  */
 
-import { defineComponent, provide } from 'vue'
+import { defineComponent, toRefs, watch } from 'vue'
 import {
-  NCard,
   NButton,
-  NInputGroup,
   NInput,
   NIcon,
   NSpace,
-  NGrid,
-  NGridItem,
   NDataTable,
-  NPagination,
-  NSkeleton
+  NPagination
 } from 'naive-ui'
+import Card from '@/components/card'
+import UserDetailModal from './components/user-detail-modal'
+import AuthorizeModal from './components/authorize-modal'
 import { useI18n } from 'vue-i18n'
 import { SearchOutlined } from '@vicons/antd'
+import { useColumns } from './use-columns'
 import { useTable } from './use-table'
-import UserModal from './components/user-modal'
-import {
-  useSharedUserModalState,
-  UserModalSharedStateKey,
-  Mode
-} from './components/use-modal'
 
 const UsersManage = defineComponent({
   name: 'user-manage',
   setup() {
     const { t } = useI18n()
-    const { show, mode, user } = useSharedUserModalState()
-    const tableState = useTable({
-      onEdit: (u, m: Mode) => {
-        show.value = true
-        mode.value = m
-        user.value = u
-      },
-      onDelete: (u) => {
-        show.value = true
-        mode.value = 'delete'
-        user.value = u
-      }
-    })
-
-    const onSuccess = (mode: Mode) => {
-      if (!mode.startsWith('auth')) {
-        mode === 'add' && tableState.resetPage()
-        tableState.getUserList()
-      }
-    }
+    const { state, changePage, changePageSize, updateList, onOperationClick } =
+      useTable()
+    const { columnsRef } = useColumns(onOperationClick)
 
     const onAddUser = () => {
-      show.value = true
-      mode.value = 'add'
-      user.value = undefined
+      state.detailModalShow = true
+      state.currentRecord = null
+    }
+    const onDetailModalCancel = () => {
+      state.detailModalShow = false
+    }
+    const onAuthorizeModalCancel = () => {
+      state.authorizeModalShow = false
     }
-
-    provide(UserModalSharedStateKey, { show, mode, user, onSuccess })
 
     return {
       t,
+      columnsRef,
+      ...toRefs(state),
+      changePage,
+      changePageSize,
       onAddUser,
-      ...tableState
+      onUpdatedList: updateList,
+      onDetailModalCancel,
+      onAuthorizeModalCancel
     }
   },
   render() {
-    const { t, onSearchValOk, onSearchValClear, userListLoading } = this
     return (
       <>
-        <NGrid cols={1} yGap={16}>
-          <NGridItem>
-            <NCard>
-              <NSpace justify='space-between'>
-                <NButton
-                  onClick={this.onAddUser}
-                  type='primary'
-                  class='btn-create-user'
-                >
-                  {t('security.user.create_user')}
+        <NSpace vertical>
+          <Card>
+            <NSpace justify='space-between'>
+              <NButton
+                onClick={this.onAddUser}
+                type='primary'
+                class='btn-create-user'
+              >
+                {this.t('security.user.create_user')}
+              </NButton>
+              <NSpace>
+                <NInput v-model:value={this.searchVal} clearable />
+                <NButton type='primary' onClick={this.onUpdatedList}>
+                  <NIcon>
+                    <SearchOutlined />
+                  </NIcon>
                 </NButton>
-                <NInputGroup>
-                  <NInput
-                    v-model:value={this.searchInputVal}
-                    clearable
-                    onClear={onSearchValClear}
-                    onKeyup={(e) => {
-                      if (e.key === 'Enter') {
-                        onSearchValOk()
-                      }
-                    }}
-                  />
-                  <NButton type='primary' onClick={onSearchValOk}>
-                    <NIcon>
-                      <SearchOutlined />
-                    </NIcon>
-                  </NButton>
-                </NInputGroup>
               </NSpace>
-            </NCard>
-          </NGridItem>
-          <NGridItem>
-            <NCard>
-              {userListLoading ? (
-                <NSkeleton text repeat={6} />
-              ) : (
-                <NSpace v-show={!userListLoading} vertical size={20}>
-                  <NDataTable
-                    row-class-name='items'
-                    columns={this.columns}
-                    data={this.userList}
-                    scrollX={this.scrollX}
-                    bordered={false}
-                  />
-                  <NSpace justify='center'>
-                    <NPagination
-                      v-model:page={this.page}
-                      v-model:page-size={this.pageSize}
-                      pageCount={this.pageCount}
-                      pageSizes={this.pageSizes}
-                      showSizePicker
-                    />
-                  </NSpace>
-                </NSpace>
-              )}
-            </NCard>
-          </NGridItem>
-        </NGrid>
-        <UserModal />
+            </NSpace>
+          </Card>
+          <Card>
+            <NSpace vertical>
+              <NDataTable
+                row-class-name='items'
+                columns={this.columnsRef}
+                data={this.list}
+                loading={this.loading}
+              />
+              <NSpace justify='center'>
+                <NPagination
+                  v-model:page={this.page}
+                  v-model:page-size={this.pageSize}
+                  item-count={this.itemCount}
+                  show-size-picker
+                  page-sizes={[10, 30, 50]}
+                  show-quick-jumper
+                  on-update:page={this.changePage}
+                  on-update:page-size={this.changePageSize}
+                />
+              </NSpace>
+            </NSpace>
+          </Card>
+        </NSpace>
+        <UserDetailModal
+          show={this.detailModalShow}
+          currentRecord={this.currentRecord}
+          onCancel={this.onDetailModalCancel}
+          onUpdate={this.onUpdatedList}
+        />
+        <AuthorizeModal
+          show={this.authorizeModalShow}
+          type={this.authorizeType}
+          userId={this.currentRecord?.id}
+          onCancel={this.onAuthorizeModalCancel}
+        />
       </>
     )
   }
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/types.ts b/dolphinscheduler-ui-next/src/views/security/user-manage/types.ts
new file mode 100644
index 0000000..ac8c1fc
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/security/user-manage/types.ts
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import type {
+  TableColumns,
+  InternalRowData
+} from 'naive-ui/es/data-table/src/interface'
+import { UserReq } from '@/service/modules/users/types'
+export type { UserInfoRes } from '@/service/modules/users/types'
+
+type TUserType = 'GENERAL_USER' | ''
+type TAuthType =
+  | 'authorize_project'
+  | 'authorize_resource'
+  | 'authorize_datasource'
+  | 'authorize_udf'
+
+interface IRecord {
+  id: number
+  userName: string
+  userType: TUserType
+  tenantCode: string
+  queueName: string
+  email: string
+  phone: string
+  state: 0 | 1
+  createTime: string
+  updateTime: string
+}
+
+interface IResourceOption {
+  id: number
+  fullName: string
+  type: string
+}
+
+interface IOption {
+  value: number
+  label: string
+}
+
+export {
+  IRecord,
+  IResourceOption,
+  IOption,
+  TAuthType,
+  UserReq,
+  TableColumns,
+  InternalRowData
+}
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/use-columns.ts b/dolphinscheduler-ui-next/src/views/security/user-manage/use-columns.ts
new file mode 100644
index 0000000..1d508d6
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/security/user-manage/use-columns.ts
@@ -0,0 +1,219 @@
+/*
+ * 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, watch, onMounted, Ref } from 'vue'
+import { useI18n } from 'vue-i18n'
+import {
+  NSpace,
+  NTooltip,
+  NButton,
+  NIcon,
+  NTag,
+  NDropdown,
+  NPopconfirm
+} from 'naive-ui'
+import { EditOutlined, DeleteOutlined, UserOutlined } from '@vicons/antd'
+import { TableColumns, InternalRowData } from './types'
+
+export function useColumns(onCallback: Function) {
+  const { t } = useI18n()
+
+  const columnsRef = ref([]) as Ref<TableColumns>
+
+  const createColumns = () => {
+    columnsRef.value = [
+      {
+        title: '#',
+        key: 'index',
+        render: (rowData: InternalRowData, rowIndex: number) => rowIndex + 1
+      },
+      {
+        title: t('security.user.username'),
+        key: 'userName'
+      },
+      {
+        title: t('security.user.user_type'),
+        key: 'userType',
+        render: (rowData: InternalRowData) =>
+          rowData.userType === 'GENERAL_USER'
+            ? t('security.user.ordinary_user')
+            : t('security.user.administrator')
+      },
+      {
+        title: t('security.user.tenant_code'),
+        key: 'tenantCode'
+      },
+      {
+        title: t('security.user.queue'),
+        key: 'queue'
+      },
+      {
+        title: t('security.user.email'),
+        key: 'email'
+      },
+      {
+        title: t('security.user.phone'),
+        key: 'phone'
+      },
+      {
+        title: t('security.user.state'),
+        key: 'state',
+        render: (rowData: any, unused: number) =>
+          h(
+            NTag,
+            { type: rowData.state === 1 ? 'success' : 'error' },
+            {
+              default: () =>
+                t(
+                  `security.user.state_${
+                    rowData.state === 1 ? 'enabled' : 'disabled'
+                  }`
+                )
+            }
+          )
+      },
+      {
+        title: t('security.user.create_time'),
+        key: 'createTime'
+      },
+      {
+        title: t('security.user.update_time'),
+        key: 'updateTime'
+      },
+      {
+        title: t('security.user.operation'),
+        key: 'operation',
+        render: (rowData: any, unused: number) => {
+          return h(NSpace, null, {
+            default: () => [
+              h(
+                NDropdown,
+                {
+                  trigger: 'click',
+                  options: [
+                    {
+                      label: t('security.user.project'),
+                      key: 'authorize_project'
+                    },
+                    {
+                      label: t('security.user.resource'),
+                      key: 'authorize_resource'
+                    },
+                    {
+                      label: t('security.user.datasource'),
+                      key: 'authorize_datasource'
+                    },
+                    { label: t('security.user.udf'), key: 'authorize_udf' }
+                  ],
+                  onSelect: (key) =>
+                    void onCallback({ rowData, key }, 'authorize')
+                },
+                () =>
+                  h(
+                    NTooltip,
+                    {
+                      trigger: 'hover'
+                    },
+                    {
+                      trigger: () =>
+                        h(
+                          NButton,
+                          {
+                            circle: true,
+                            type: 'warning',
+                            size: 'small',
+                            class: 'authorize'
+                          },
+                          {
+                            icon: () => h(NIcon, null, () => h(UserOutlined))
+                          }
+                        ),
+                      default: () => t('security.user.authorize')
+                    }
+                  )
+              ),
+              h(
+                NTooltip,
+                { trigger: 'hover' },
+                {
+                  trigger: () =>
+                    h(
+                      NButton,
+                      {
+                        circle: true,
+                        type: 'info',
+                        size: 'small',
+                        onClick: () => void onCallback({ rowData }, 'edit')
+                      },
+                      () => h(NIcon, null, () => h(EditOutlined))
+                    ),
+                  default: () => t('security.user.edit')
+                }
+              ),
+              h(
+                NPopconfirm,
+                {
+                  onPositiveClick: () => void onCallback({ rowData }, 'delete')
+                },
+                {
+                  trigger: () =>
+                    h(
+                      NTooltip,
+                      {},
+                      {
+                        trigger: () =>
+                          h(
+                            NButton,
+                            {
+                              circle: true,
+                              type: 'error',
+                              size: 'small',
+                              class: 'delete'
+                            },
+                            {
+                              icon: () =>
+                                h(NIcon, null, {
+                                  default: () => h(DeleteOutlined)
+                                })
+                            }
+                          ),
+                        default: () => t('security.user.delete')
+                      }
+                    ),
+                  default: () => t('security.user.delete_confirm')
+                }
+              )
+            ]
+          })
+        }
+      }
+    ]
+  }
+
+  onMounted(() => {
+    createColumns()
+  })
+
+  watch(useI18n().locale, () => {
+    createColumns()
+  })
+
+  return {
+    columnsRef,
+    createColumns
+  }
+}
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/use-table.ts b/dolphinscheduler-ui-next/src/views/security/user-manage/use-table.ts
new file mode 100644
index 0000000..1e0c9a3
--- /dev/null
+++ b/dolphinscheduler-ui-next/src/views/security/user-manage/use-table.ts
@@ -0,0 +1,112 @@
+/*
+ * 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, onMounted } from 'vue'
+import { queryUserList, delUserById } from '@/service/modules/users'
+import { format } from 'date-fns'
+import { parseTime } from '@/utils/common'
+import type { IRecord, TAuthType } from './types'
+
+export function useTable() {
+  const state = reactive({
+    page: 1,
+    pageSize: 10,
+    itemCount: 0,
+    searchVal: '',
+    list: [],
+    loading: false,
+    currentRecord: {} as IRecord | null,
+    authorizeType: 'authorize_project' as TAuthType,
+    detailModalShow: false,
+    authorizeModalShow: false
+  })
+
+  const getList = async () => {
+    if (state.loading) return
+    state.loading = true
+
+    const { totalList, total } = await queryUserList({
+      pageNo: state.page,
+      pageSize: state.pageSize,
+      searchVal: state.searchVal
+    })
+    state.loading = false
+    if (!totalList) throw Error()
+    state.list = totalList.map((record: IRecord) => {
+      record.createTime = record.createTime
+        ? format(parseTime(record.createTime), 'yyyy-MM-dd HH:mm:ss')
+        : ''
+      record.updateTime = record.updateTime
+        ? format(parseTime(record.updateTime), 'yyyy-MM-dd HH:mm:ss')
+        : ''
+      return record
+    })
+
+    state.itemCount = total
+  }
+
+  const updateList = () => {
+    if (state.list.length === 1 && state.page > 1) {
+      --state.page
+    }
+    getList()
+  }
+
+  const deleteUser = async (userId: number) => {
+    await delUserById({ id: userId })
+    updateList()
+  }
+
+  const onOperationClick = (
+    data: { rowData: IRecord; key?: TAuthType },
+    type: 'authorize' | 'edit' | 'delete'
+  ) => {
+    state.currentRecord = data.rowData
+    if (type === 'edit') {
+      state.detailModalShow = true
+    }
+    if (type === 'authorize' && data.key) {
+      state.authorizeModalShow = true
+      state.authorizeType = data.key
+    }
+    if (type === 'delete') {
+      deleteUser(data.rowData.id)
+    }
+  }
+
+  // const deleteRecord = async (id: number) => {
+  //   const ignored = await deleteAlertPluginInstance(id)
+  //   updateList()
+  // }
+
+  const changePage = (page: number) => {
+    state.page = page
+    getList()
+  }
+
+  const changePageSize = (pageSize: number) => {
+    state.page = 1
+    state.pageSize = pageSize
+    getList()
+  }
+
+  onMounted(() => {
+    getList()
+  })
+
+  return { state, changePage, changePageSize, updateList, onOperationClick }
+}
diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/use-table.tsx b/dolphinscheduler-ui-next/src/views/security/user-manage/use-table.tsx
deleted file mode 100644
index 903468e..0000000
--- a/dolphinscheduler-ui-next/src/views/security/user-manage/use-table.tsx
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- * 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 { ref, watch, onBeforeMount, computed } from 'vue'
-import { NSpace, NTooltip, NButton, NIcon, NTag, NDropdown } from 'naive-ui'
-import { EditOutlined, DeleteOutlined, UserOutlined } from '@vicons/antd'
-import { queryUserList } from '@/service/modules/users'
-import { useI18n } from 'vue-i18n'
-import { Mode } from './components/use-modal'
-
-type UseTableProps = {
-  onEdit: (user: any, mode: Mode) => void
-  onDelete: (user: any) => void
-}
-
-function useColumns({ onEdit, onDelete }: UseTableProps) {
-  const { t } = useI18n()
-  const columns = computed(() =>
-    [
-      {
-        title: '#',
-        key: 'index',
-        width: 80,
-        render: (rowData: any, rowIndex: number) => rowIndex + 1
-      },
-      {
-        title: t('security.user.username'),
-        key: 'userName',
-        className: 'name'
-      },
-      {
-        title: t('security.user.tenant_code'),
-        key: 'tenantCode'
-      },
-      {
-        title: t('security.user.queue'),
-        key: 'queue'
-      },
-      {
-        title: t('security.user.email'),
-        key: 'email'
-      },
-      {
-        title: t('security.user.phone'),
-        key: 'phone'
-      },
-      {
-        title: t('security.user.state'),
-        key: 'state',
-        render: (rowData: any, unused: number) => {
-          return rowData.state === 1 ? (
-            <NTag type='success'>{t('security.user.state_enabled')}</NTag>
-          ) : (
-            <NTag type='error'>{t('security.user.state_disabled')}</NTag>
-          )
-        }
-      },
-      {
-        title: t('security.user.create_time'),
-        key: 'createTime',
-        width: 200
-      },
-      {
-        title: t('security.user.update_time'),
-        key: 'updateTime',
-        width: 200
-      },
-      {
-        title: t('security.user.operation'),
-        key: 'operation',
-        fixed: 'right',
-        width: 140,
-        render: (rowData: any, unused: number) => {
-          return (
-            <NSpace>
-              <NDropdown
-                trigger='click'
-                options={[
-                  { label: t('security.user.project'), key: 'auth_project' },
-                  { label: t('security.user.resource'), key: 'auth_resource' },
-                  {
-                    label: t('security.user.datasource'),
-                    key: 'auth_datasource'
-                  },
-                  { label: t('security.user.udf'), key: 'auth_udf' }
-                ]}
-                onSelect={(key) => {
-                  onEdit(rowData, key)
-                }}
-              >
-                <NTooltip trigger='hover'>
-                  {{
-                    trigger: () => (
-                      <NButton
-                        circle
-                        type='warning'
-                        size='small'
-                        class='authorize'
-                      >
-                        {{
-                          icon: () => (
-                            <NIcon>
-                              <UserOutlined />
-                            </NIcon>
-                          )
-                        }}
-                      </NButton>
-                    ),
-                    default: () => t('security.user.authorize')
-                  }}
-                </NTooltip>
-              </NDropdown>
-              <NTooltip trigger='hover'>
-                {{
-                  trigger: () => (
-                    <NButton
-                      circle
-                      type='info'
-                      size='small'
-                      class='edit'
-                      onClick={() => {
-                        onEdit(rowData, 'edit')
-                      }}
-                    >
-                      {{
-                        icon: () => (
-                          <NIcon>
-                            <EditOutlined />
-                          </NIcon>
-                        )
-                      }}
-                    </NButton>
-                  ),
-                  default: () => t('security.user.edit')
-                }}
-              </NTooltip>
-              <NTooltip trigger='hover'>
-                {{
-                  trigger: () => (
-                    <NButton
-                      circle
-                      type='error'
-                      size='small'
-                      class='delete'
-                      onClick={() => {
-                        onDelete(rowData)
-                      }}
-                    >
-                      {{
-                        icon: () => (
-                          <NIcon>
-                            <DeleteOutlined />
-                          </NIcon>
-                        )
-                      }}
-                    </NButton>
-                  ),
-                  default: () => t('security.user.delete')
-                }}
-              </NTooltip>
-            </NSpace>
-          )
-        }
-      }
-    ].map((d: any) => ({ ...d, width: d.width || 160 }))
-  )
-
-  const scrollX = columns.value.reduce((p, c) => p + c.width, 0)
-
-  return {
-    columns,
-    scrollX
-  }
-}
-
-export function useTable(props: UseTableProps) {
-  const page = ref(1)
-  const pageCount = ref(0)
-  const pageSize = ref(10)
-  const searchInputVal = ref()
-  const searchVal = ref('')
-  const pageSizes = [10, 30, 50]
-  const userListLoading = ref(false)
-  const userList = ref([])
-  const { columns, scrollX } = useColumns(props)
-
-  const getUserList = () => {
-    userListLoading.value = true
-    queryUserList({
-      pageNo: page.value,
-      pageSize: pageSize.value,
-      searchVal: searchVal.value
-    })
-      .then((res: any) => {
-        userList.value = res?.totalList || []
-        pageCount.value = res?.totalPage || 0
-      })
-      .finally(() => {
-        userListLoading.value = false
-      })
-  }
-
-  const resetPage = () => {
-    page.value = 1
-  }
-
-  const onSearchValOk = () => {
-    resetPage()
-    searchVal.value = searchInputVal.value
-  }
-
-  const onSearchValClear = () => {
-    resetPage()
-    searchVal.value = ''
-  }
-
-  onBeforeMount(() => {
-    getUserList()
-  })
-
-  watch([page, pageSize, searchVal], () => {
-    getUserList()
-  })
-
-  return {
-    userList,
-    userListLoading,
-    getUserList,
-    page,
-    pageCount,
-    pageSize,
-    searchVal,
-    searchInputVal,
-    pageSizes,
-    columns,
-    scrollX,
-    onSearchValOk,
-    onSearchValClear,
-    resetPage
-  }
-}