diff --git a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts index f90db5dc1aa7d0556af9518886361efa727fc716..ca0b64e64f9227d936af728d9f3a3c7756ac5dc6 100644 --- a/dolphinscheduler-ui-next/src/locales/modules/en_US.ts +++ b/dolphinscheduler-ui-next/src/locales/modules/en_US.ts @@ -434,6 +434,38 @@ const security = { edit: 'Edit', delete: 'Delete', delete_confirm: 'Delete?' + }, + user: { + user_manage: 'User Manage', + create_user: 'Create User', + update_user: 'Update User', + delete_user: 'Delete User', + delete_confirm: 'Are you sure to delete?', + delete_confirm_tip: + 'Deleting user is a dangerous operation,please be careful', + index: 'Index', + username: 'Username', + username_exists: 'The username already exists', + username_rule_msg: 'Please enter username', + user_password: 'Please enter password', + user_password_rule_msg: + 'Please enter a password containing letters and numbers with a length between 6 and 20', + user_type: 'User Type', + tenant_code: 'Tenant', + tenant_id_rule_msg: 'Please select tenant', + queue: 'Queue', + email: 'Email', + email_rule_msg: 'Please enter valid email', + phone: 'Phone', + phone_rule_msg: 'Please enter valid phone number', + state: 'State', + create_time: 'Create Time', + update_time: 'Update Time', + operation: 'Operation', + edit: 'Edit', + delete: 'Delete', + save_error_msg: 'Failed to save, please retry', + delete_error_msg: 'Failed to delete, please retry' } } diff --git a/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts b/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts index 1ee6f60d6e8db03e59558b0619c0939c0ba47a17..0b827182401db858e202ff33a525038a481a7992 100644 --- a/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts +++ b/dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts @@ -433,6 +433,36 @@ const security = { edit: '编辑', delete: '删除', delete_confirm: '确定删除吗?' + }, + user: { + user_manage: '用户管理', + create_user: '创建用户', + update_user: '更新用户', + delete_user: '删除用户', + delete_confirm: '确定删除吗?', + delete_confirm_tip: '删除用户属于危险操作,请谨慎操作!', + index: '序号', + username: '用户名', + username_exists: '用户名已存在', + username_rule_msg: '请输入用户名', + user_password: '密码', + user_password_rule_msg: '请输入包含字母和数字,长度在6~20之间的密码', + user_type: '用户类型', + tenant_code: '租户', + tenant_id_rule_msg: '请选择租户', + queue: '队列', + email: '邮件', + email_rule_msg: '请输入正确的邮箱', + phone: '手机', + phone_rule_msg: '请输入正确的手机号', + state: '状态', + create_time: '创建时间', + update_time: '更新时间', + operation: '操作', + edit: '编辑', + delete: '删除', + save_error_msg: '保存失败,请重试', + delete_error_msg: '删除失败,请重试' } } diff --git a/dolphinscheduler-ui-next/src/router/modules/security.ts b/dolphinscheduler-ui-next/src/router/modules/security.ts index 44774e40b3a35ad90e9ad53658164c7630a0db4d..b50e3fb38f5e0141300e4af9c1ffd652f091f58f 100644 --- a/dolphinscheduler-ui-next/src/router/modules/security.ts +++ b/dolphinscheduler-ui-next/src/router/modules/security.ts @@ -39,9 +39,9 @@ export default { } }, { - path: '/security/users', + path: '/security/user-manage', name: 'users-manage', - component: components['home'], + component: components['user-manage'], meta: { title: '用户管理', showSide: true diff --git a/dolphinscheduler-ui-next/src/service/modules/users/index.ts b/dolphinscheduler-ui-next/src/service/modules/users/index.ts index 1e9157161cbe49214cfbdd9b8efd371b83903384..7d566197830045ccd796bf43d7d2a7e013f7b68e 100644 --- a/dolphinscheduler-ui-next/src/service/modules/users/index.ts +++ b/dolphinscheduler-ui-next/src/service/modules/users/index.ts @@ -65,7 +65,7 @@ export function createUser(data: UserReq): any { }) } -export function delUserById(data: IdReq): any { +export function delUserById(data: IdReq) { return axios({ url: '/users/delete', method: 'post', @@ -135,7 +135,7 @@ export function listAll(params?: ListAllReq): any { }) } -export function queryUserList(params: ListReq): any { +export function queryUserList(params: ListReq) { return axios({ url: '/users/list-paging', method: 'get', @@ -167,7 +167,7 @@ export function unauthorizedUser(params: AlertGroupIdReq): any { }) } -export function updateUser(data: IdReq & UserReq): any { +export function updateUser(data: IdReq & UserReq) { return axios({ url: '/users/update', method: 'post', @@ -175,7 +175,7 @@ export function updateUser(data: IdReq & UserReq): any { }) } -export function verifyUserName(params: UserNameReq): any { +export function verifyUserName(params: UserNameReq) { return axios({ url: '/users/verify-user-name', method: 'get', diff --git a/dolphinscheduler-ui-next/src/utils/regex.ts b/dolphinscheduler-ui-next/src/utils/regex.ts index c660e8d127e9e1606365d63f8b89537218a71aae..aa8715eeb1a1aad9d1f77270ed36021255bb5071 100644 --- a/dolphinscheduler-ui-next/src/utils/regex.ts +++ b/dolphinscheduler-ui-next/src/utils/regex.ts @@ -16,7 +16,9 @@ */ const regex = { - email: /^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/ // support Chinese mailbox + email: /^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/, // support Chinese mailbox + phone: /^1\d{10}$/, + password: /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,20}$/ } export default regex 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 new file mode 100644 index 0000000000000000000000000000000000000000..e5f50aee6dbf1847af07310c75ad42218365924c --- /dev/null +++ b/dolphinscheduler-ui-next/src/views/security/user-manage/components/use-modal.ts @@ -0,0 +1,277 @@ +/* + * 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 +} from '@/service/modules/users' +import regexUtils from '@/utils/regex' +export type Mode = 'add' | 'edit' | 'delete' + +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 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 titleMap: Record = { + add: t('security.user.create_user'), + edit: t('security.user.update_user'), + delete: t('security.user.delete_user') + } + + 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 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 onConfirm = () => { + if (mode.value === 'delete') { + onDelete() + } else { + formRef.value.validate((errors: any) => { + if (!errors) { + user.value ? onUpdateUser() : onCreateUser() + } + }) + } + } + + const onModalCancel = () => { + show.value = false + } + + watch([show, mode], () => { + show.value && mode.value !== 'delete' && prepareOptions() + }) + + watch([queues, tenants, user], () => { + setFormValues() + }) + + return { + show, + mode, + user, + titleMap, + onModalCancel, + formRef, + formValues, + formRules, + tenants, + queues, + optionsLoading, + onConfirm, + confirmLoading + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..d23a1e25e86965ea051662ce356d481bf2cfd0f1 --- /dev/null +++ b/dolphinscheduler-ui-next/src/views/security/user-manage/components/user-modal.tsx @@ -0,0 +1,133 @@ +/* + * 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, + NSpace, + NAlert +} 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')} + + ) + } + return ( + + + + + + + + + + + + + + + + + + + + + + + 启用 + 停用 + + + + + ) + } + }} + + ) + } +}) + +export default UserModal diff --git a/dolphinscheduler-ui-next/src/views/security/user-manage/index.tsx b/dolphinscheduler-ui-next/src/views/security/user-manage/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..018c77f706afaeb014e6a450c4b8de26d97b5977 --- /dev/null +++ b/dolphinscheduler-ui-next/src/views/security/user-manage/index.tsx @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineComponent, provide } from 'vue' +import { + NCard, + NButton, + NInputGroup, + NInput, + NIcon, + NSpace, + NGrid, + NGridItem, + NDataTable, + NPagination, + NSkeleton +} from 'naive-ui' +import { useI18n } from 'vue-i18n' +import { SearchOutlined } from '@vicons/antd' +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) => { + show.value = true + mode.value = 'edit' + user.value = u + }, + onDelete: (u) => { + show.value = true + mode.value = 'delete' + user.value = u + } + }) + + const onSuccess = (mode: Mode) => { + if (mode === 'add') { + tableState.resetPage() + } + tableState.getUserList() + } + + const onAddUser = () => { + show.value = true + mode.value = 'add' + user.value = undefined + } + + provide(UserModalSharedStateKey, { show, mode, user, onSuccess }) + + return { + t, + onAddUser, + ...tableState + } + }, + render() { + const { t, onSearchValOk, onSearchValClear, userListLoading } = this + return ( + <> + + + + + + {t('security.user.create_user')} + + + { + if (e.key === 'Enter') { + onSearchValOk() + } + }} + /> + + + + + + + + + + + + {userListLoading ? ( + + ) : ( + + + + + + + )} + + + + + + ) + } +}) + +export default UsersManage 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 new file mode 100644 index 0000000000000000000000000000000000000000..10b334b30d457a44763bd656fbca5802f7a0d016 --- /dev/null +++ b/dolphinscheduler-ui-next/src/views/security/user-manage/use-table.tsx @@ -0,0 +1,212 @@ +/* + * 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 } from 'vue' +import { NSpace, NTooltip, NButton, NIcon, NTag } from 'naive-ui' +import { EditOutlined, DeleteOutlined } from '@vicons/antd' +import { queryUserList } from '@/service/modules/users' +import { useI18n } from 'vue-i18n' + +type UseTableProps = { + onEdit: (user: any) => void + onDelete: (user: any) => void +} + +function useColumns({ onEdit, onDelete }: UseTableProps) { + const { t } = useI18n() + const columns: any[] = [ + { + title: t('security.user.index'), + key: 'index', + width: 80, + render: (rowData: any, rowIndex: number) => rowIndex + 1 + }, + { + title: t('security.user.username'), + key: 'userName' + }, + { + 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, rowIndex: number) => { + return rowData.state === 1 ? ( + 启用 + ) : ( + 停用 + ) + } + }, + { + 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: 120, + render: (rowData: any, rowIndex: number) => { + return ( + + + {{ + trigger: () => ( + { + onEdit(rowData) + }} + > + {{ + 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.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 + } +}