Browse Source

[Feature][UI Next] Security User Manage (#8133)

3.0.0/version-upgrade
lilyzhou 2 years ago committed by GitHub
parent
commit
e0dbf3edc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      dolphinscheduler-ui-next/src/locales/modules/en_US.ts
  2. 30
      dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
  3. 4
      dolphinscheduler-ui-next/src/router/modules/security.ts
  4. 8
      dolphinscheduler-ui-next/src/service/modules/users/index.ts
  5. 4
      dolphinscheduler-ui-next/src/utils/regex.ts
  6. 277
      dolphinscheduler-ui-next/src/views/security/user-manage/components/use-modal.ts
  7. 133
      dolphinscheduler-ui-next/src/views/security/user-manage/components/user-modal.tsx
  8. 144
      dolphinscheduler-ui-next/src/views/security/user-manage/index.tsx
  9. 212
      dolphinscheduler-ui-next/src/views/security/user-manage/use-table.tsx

32
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'
}
}

30
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: '删除失败,请重试'
}
}

4
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

8
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',

4
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

277
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<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 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<Mode, string> = {
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
}
}

133
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 (
<Modal
show={this.show}
title={this.titleMap?.[this.mode || 'add']}
onCancel={this.onModalCancel}
confirmLoading={this.confirmLoading}
onConfirm={this.onConfirm}
>
{{
default: () => {
if (this.mode === 'delete') {
return (
<NAlert type='error' title={t('security.user.delete_confirm')}>
{t('security.user.delete_confirm_tip')}
</NAlert>
)
}
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
inputProps={{ autocomplete: 'off' }}
v-model:value={this.formValues.userName}
/>
</NFormItem>
<NFormItem
label={t('security.user.user_password')}
path='userPassword'
>
<NInput
inputProps={{ autocomplete: 'off' }}
type='password'
v-model:value={this.formValues.userPassword}
/>
</NFormItem>
<NFormItem
label={t('security.user.tenant_code')}
path='tenantId'
>
<NSelect
options={this.tenants}
v-model:value={this.formValues.tenantId}
/>
</NFormItem>
<NFormItem label={t('security.user.queue')} path='queue'>
<NSelect
options={this.queues}
v-model:value={this.formValues.queue}
/>
</NFormItem>
<NFormItem label={t('security.user.email')} path='email'>
<NInput v-model:value={this.formValues.email} />
</NFormItem>
<NFormItem label={t('security.user.phone')} path='phone'>
<NInput 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}></NRadio>
<NRadio value={0}></NRadio>
</NSpace>
</NRadioGroup>
</NFormItem>
</NForm>
)
}
}}
</Modal>
)
}
})
export default UserModal

144
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 (
<>
<NGrid cols={1} yGap={16}>
<NGridItem>
<NCard>
<NSpace justify='space-between'>
<NButton onClick={this.onAddUser} type='primary'>
{t('security.user.create_user')}
</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}></NSkeleton>
) : (
<NSpace v-show={!userListLoading} vertical size={20}>
<NDataTable
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 />
</>
)
}
})
export default UsersManage

212
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 ? (
<NTag type='success'></NTag>
) : (
<NTag type='error'></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: 120,
render: (rowData: any, rowIndex: number) => {
return (
<NSpace>
<NTooltip trigger='hover'>
{{
trigger: () => (
<NButton
circle
type='info'
size='small'
onClick={() => {
onEdit(rowData)
}}
>
{{
icon: () => (
<NIcon>
<EditOutlined />
</NIcon>
)
}}
</NButton>
),
default: () => t('security.user.edit')
}}
</NTooltip>
<NTooltip trigger='hover'>
{{
trigger: () => (
<NButton
circle
type='error'
size='small'
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.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
}
}
Loading…
Cancel
Save