diff --git a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts index 50f44bd7f0..2982407339 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 bb65aecff8..9222de77b7 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 956ff3d3fb..7a13cd9633 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 6a8de0e073..555fec1ab6 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 dc06961cf0..b4a32bf5fd 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 2a9c4c44ac..bd42f40a65 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 0a71e12394..3768d22b14 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 0000000000..a27f04b6ea --- /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 bbe160b757..38563ccfac 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 68313443e0..aa0b8250fa 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 b67112c515..ee2d132c60 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 3b54567d0d..09e0a03d8e 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 4eddb7173b..ec2a660e93 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 fc10d08119..213f5f4e7e 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 0000000000..0118299f40 --- /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, + default: false + }, + userId: { + type: Number, + default: 0 + }, + type: { + type: String as PropType, + 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 ( + + {type === 'authorize_project' && ( + + )} + {type === 'authorize_datasource' && ( + + )} + {type === 'authorize_udf' && ( + + )} + {type === 'authorize_resource' && ( + + + + {t('security.user.file_resource')} + + + {t('security.user.udf_resource')} + + + + + + )} + + ) + } +}) + +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 0000000000..9c43b98299 --- /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 fe9507e2cc..0000000000 --- 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 = - Symbol() - -export function useSharedUserModalState() { - return { - show: ref(false), - mode: ref('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([]) - const queues = ref([]) - const authorizedProjects = ref([]) - const projects = ref([]) - const authorizedFiles = ref([]) - const originResourceTree = ref([]) - const resourceType = ref<'file' | 'udf'>() - const authorizedUDF = ref([]) - const UDFs = ref([]) - const authorizedDatasource = ref([]) - const datasource = ref([]) - 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 = { - 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) => { - 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 0000000000..d436716b20 --- /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 => { + 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 0000000000..1db19ca7b4 --- /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, + default: false + }, + currentRecord: { + type: Object as PropType, + 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 ( + + + + + + + + + {this.IS_ADMIN && ( + + + + )} + {this.IS_ADMIN && ( + + + + )} + + + + + + + + + + + {this.t('security.user.enable')} + + + {this.t('security.user.disable')} + + + + + + + ) + } +}) + +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 e966095d7d..0000000000 --- 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 ( - - {{ - default: () => { - if (this.mode === 'delete') { - return ( - - {t('security.user.delete_confirm_tip')} - - ) - } - if (this.mode === 'auth_project') { - return ( - - ) - } - if (this.mode === 'auth_datasource') { - return ( - - ) - } - if (this.mode === 'auth_udf') { - return ( - - ) - } - if (this.mode === 'auth_resource') { - return ( - - - - {t('security.user.file_resource')} - - - {t('security.user.udf_resource')} - - - - - ) - } - return ( - - - - - - - - - - - - - - - - - - - - - - - - 启用 - - - 停用 - - - - - - ) - } - }} - - ) - } -}) - -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 0000000000..68d62fdd11 --- /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 628ac3b557..4277701233 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 ( <> - - - - - - {t('security.user.create_user')} + + + + + {this.t('security.user.create_user')} + + + + + + + - - { - if (e.key === 'Enter') { - onSearchValOk() - } - }} - /> - - - - - - - - - - - {userListLoading ? ( - - ) : ( - - - - - - - )} - - - - + + + + + + + + + + + + + ) } 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 0000000000..ac8c1fc8a6 --- /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 0000000000..1d508d6e6d --- /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 + + 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 0000000000..1e0c9a39dc --- /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 903468e382..0000000000 --- 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 ? ( - {t('security.user.state_enabled')} - ) : ( - {t('security.user.state_disabled')} - ) - } - }, - { - 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 ( - - { - onEdit(rowData, key) - }} - > - - {{ - trigger: () => ( - - {{ - icon: () => ( - - - - ) - }} - - ), - default: () => t('security.user.authorize') - }} - - - - {{ - trigger: () => ( - { - onEdit(rowData, 'edit') - }} - > - {{ - icon: () => ( - - - - ) - }} - - ), - default: () => t('security.user.edit') - }} - - - {{ - trigger: () => ( - { - onDelete(rowData) - }} - > - {{ - icon: () => ( - - - - ) - }} - - ), - default: () => t('security.user.delete') - }} - - - ) - } - } - ].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 - } -}