Browse Source

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

* [Feature][UI Next][V1.0.0-Alpha]: Refactor the user manage page under security.

* [Feature][UI Next][V1.0.0-Alpha]: Add license into types file.
3.0.0/version-upgrade
Amy0104 2 years ago committed by GitHub
parent
commit
e0ee6f1f2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 21
      dolphinscheduler-ui-next/src/locales/modules/en_US.ts
  2. 20
      dolphinscheduler-ui-next/src/locales/modules/zh_CN.ts
  3. 4
      dolphinscheduler-ui-next/src/service/modules/data-source/index.ts
  4. 4
      dolphinscheduler-ui-next/src/service/modules/projects/index.ts
  5. 8
      dolphinscheduler-ui-next/src/service/modules/resources/index.ts
  6. 2
      dolphinscheduler-ui-next/src/service/modules/users/index.ts
  7. 8
      dolphinscheduler-ui-next/src/service/modules/users/types.ts
  8. 31
      dolphinscheduler-ui-next/src/utils/tree-format.ts
  9. 2
      dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-flink.ts
  10. 2
      dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-mr.ts
  11. 18
      dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-sea-tunnel.ts
  12. 13
      dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-shell.ts
  13. 2
      dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-spark.ts
  14. 13
      dolphinscheduler-ui-next/src/views/projects/task/components/node/fields/use-sql.ts
  15. 159
      dolphinscheduler-ui-next/src/views/security/user-manage/components/authorize-modal.tsx
  16. 196
      dolphinscheduler-ui-next/src/views/security/user-manage/components/use-authorize.ts
  17. 496
      dolphinscheduler-ui-next/src/views/security/user-manage/components/use-modal.ts
  18. 177
      dolphinscheduler-ui-next/src/views/security/user-manage/components/use-user-detail.ts
  19. 177
      dolphinscheduler-ui-next/src/views/security/user-manage/components/user-detail-modal.tsx
  20. 209
      dolphinscheduler-ui-next/src/views/security/user-manage/components/user-modal.tsx
  21. 20
      dolphinscheduler-ui-next/src/views/security/user-manage/index.module.scss
  22. 174
      dolphinscheduler-ui-next/src/views/security/user-manage/index.tsx
  23. 63
      dolphinscheduler-ui-next/src/views/security/user-manage/types.ts
  24. 219
      dolphinscheduler-ui-next/src/views/security/user-manage/use-columns.ts
  25. 112
      dolphinscheduler-ui-next/src/views/security/user-manage/use-table.ts
  26. 255
      dolphinscheduler-ui-next/src/views/security/user-manage/use-table.tsx

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

20
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: '请输入关键字',

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

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

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

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

8
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

31
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)
})
}

2
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'

2
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'

18
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()
})

13
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)
})
}

2
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,

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

159
dolphinscheduler-ui-next/src/views/security/user-manage/components/authorize-modal.tsx

@ -0,0 +1,159 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineComponent, PropType, toRefs, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import {
NTransfer,
NSpace,
NRadioGroup,
NRadioButton,
NTreeSelect
} from 'naive-ui'
import { useAuthorize } from './use-authorize'
import Modal from '@/components/modal'
import styles from '../index.module.scss'
import type { TAuthType } from '../types'
const props = {
show: {
type: Boolean as PropType<boolean>,
default: false
},
userId: {
type: Number,
default: 0
},
type: {
type: String as PropType<TAuthType>,
default: 'auth_project'
}
}
export const AuthorizeModal = defineComponent({
name: 'authorize-project-modal',
props,
emits: ['cancel'],
setup(props, ctx) {
const { t } = useI18n()
const { state, onInit, onSave } = useAuthorize()
const onCancel = () => {
ctx.emit('cancel')
}
const onConfirm = async () => {
const result = await onSave(props.type, props.userId)
if (result) onCancel()
}
watch(
() => props.show,
() => {
if (props.show) {
onInit(props.type, props.userId)
}
}
)
return {
t,
...toRefs(state),
onCancel,
onConfirm
}
},
render(props: { type: TAuthType }) {
const { t } = this
const { type } = props
return (
<Modal
show={this.show}
title={t(`security.user.${type}`)}
onCancel={this.onCancel}
confirmLoading={this.loading}
onConfirm={this.onConfirm}
confirmClassName='btn-submit'
cancelClassName='btn-cancel'
>
{type === 'authorize_project' && (
<NTransfer
virtualScroll
options={this.unauthorizedProjects}
filterable
v-model={[this.authorizedProjects, 'value']}
class={styles.transfer}
/>
)}
{type === 'authorize_datasource' && (
<NTransfer
virtualScroll
options={this.unauthorizedDatasources}
filterable
v-model:value={this.authorizedDatasources}
class={styles.transfer}
/>
)}
{type === 'authorize_udf' && (
<NTransfer
virtualScroll
options={this.unauthorizedUdfs}
filterable
v-model:value={this.authorizedUdfs}
class={styles.transfer}
/>
)}
{type === 'authorize_resource' && (
<NSpace vertical>
<NRadioGroup v-model:value={this.resourceType}>
<NRadioButton key='file' value='file'>
{t('security.user.file_resource')}
</NRadioButton>
<NRadioButton key='udf' value='udf'>
{t('security.user.udf_resource')}
</NRadioButton>
</NRadioGroup>
<NTreeSelect
v-show={this.resourceType === 'file'}
filterable
multiple
cascade
checkable
checkStrategy='child'
key-field='id'
label-field='fullName'
options={this.fileResources}
v-model:value={this.authorizedFileResources}
/>
<NTreeSelect
v-show={this.resourceType === 'udf'}
filterable
multiple
cascade
checkable
checkStrategy='child'
key-field='id'
label-field='fullName'
options={this.udfResources}
v-model:value={this.authorizedUdfResources}
/>
</NSpace>
)}
</Modal>
)
}
})
export default AuthorizeModal

196
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 }
}

496
dolphinscheduler-ui-next/src/views/security/user-manage/components/use-modal.ts

@ -1,496 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ref, watch, computed, InjectionKey } from 'vue'
import { useI18n } from 'vue-i18n'
import { useMessage } from 'naive-ui'
import { queryTenantList } from '@/service/modules/tenants'
import { queryList } from '@/service/modules/queues'
import {
createUser,
updateUser,
delUserById,
verifyUserName,
grantProject,
grantResource,
grantDataSource,
grantUDFFunc
} from '@/service/modules/users'
import {
queryAuthorizedProject,
queryUnauthorizedProject
} from '@/service/modules/projects'
import {
authorizedFile,
authorizeResourceTree,
authUDFFunc,
unAuthUDFFunc
} from '@/service/modules/resources'
import {
authedDatasource,
unAuthDatasource
} from '@/service/modules/data-source'
import regexUtils from '@/utils/regex'
export type Mode =
| 'add'
| 'edit'
| 'delete'
| 'auth_project'
| 'auth_resource'
| 'auth_datasource'
| 'auth_udf'
export type UserModalSharedStateType = ReturnType<
typeof useSharedUserModalState
> & {
onSuccess?: (mode: Mode) => void
}
export const UserModalSharedStateKey: InjectionKey<UserModalSharedStateType> =
Symbol()
export function useSharedUserModalState() {
return {
show: ref(false),
mode: ref<Mode>('add'),
user: ref()
}
}
export function useModal({
onSuccess,
show,
mode,
user
}: UserModalSharedStateType) {
const message = useMessage()
const { t } = useI18n()
const formRef = ref()
const formValues = ref({
userName: '',
userPassword: '',
tenantId: 0,
email: '',
queue: '',
phone: '',
state: 1
})
const tenants = ref<any[]>([])
const queues = ref<any[]>([])
const authorizedProjects = ref<string[]>([])
const projects = ref<any[]>([])
const authorizedFiles = ref<string[]>([])
const originResourceTree = ref<any[]>([])
const resourceType = ref<'file' | 'udf'>()
const authorizedUDF = ref<string[]>([])
const UDFs = ref<any[]>([])
const authorizedDatasource = ref<string[]>([])
const datasource = ref<any[]>([])
const optionsLoading = ref(false)
const confirmLoading = ref(false)
const formRules = computed(() => {
return {
userName: {
required: true,
message: t('security.user.username_rule_msg'),
trigger: 'blur'
},
userPassword: {
required: mode.value === 'add',
validator(rule: any, value?: string) {
if (mode.value !== 'add' && !value) {
return true
}
const msg = t('security.user.user_password_rule_msg')
if (!value || !regexUtils.password.test(value)) {
return new Error(msg)
}
return true
},
trigger: ['blur', 'input']
},
tenantId: {
required: true,
validator(rule: any, value?: number) {
const msg = t('security.user.tenant_id_rule_msg')
if (typeof value === 'number') {
return true
}
return new Error(msg)
},
trigger: 'blur'
},
email: {
required: true,
validator(rule: any, value?: string) {
const msg = t('security.user.email_rule_msg')
if (!value || !regexUtils.email.test(value)) {
return new Error(msg)
}
return true
},
trigger: ['blur', 'input']
},
phone: {
validator(rule: any, value?: string) {
const msg = t('security.user.phone_rule_msg')
if (value && !regexUtils.phone.test(value)) {
return new Error(msg)
}
return true
},
trigger: ['blur', 'input']
}
}
})
const resourceTree = computed(() => {
const loopTree = (arr: any[]): any[] =>
arr
.map((d) => {
if (
(resourceType.value &&
`${d.type}`.toLowerCase() === resourceType.value) ||
!resourceType.value
) {
const obj = { key: `${d.pid}-${d.id}`, label: d.name }
const children = d.children
if (children instanceof Array && children.length > 0) {
return {
...obj,
children: loopTree(children)
}
}
return obj
}
return null
})
.filter((f) => f)
const data = loopTree(originResourceTree.value)
return data
})
const titleMap = computed(() => {
const titles: Record<Mode, string> = {
add: t('security.user.create_user'),
edit: t('security.user.update_user'),
delete: t('security.user.delete_user'),
auth_project: t('security.user.authorize_project'),
auth_resource: t('security.user.authorize_resource'),
auth_datasource: t('security.user.authorize_datasource'),
auth_udf: t('security.user.authorize_udf')
}
return titles
})
const setFormValues = () => {
const defaultValues = {
userName: '',
userPassword: '',
tenantId: tenants.value[0]?.value,
email: '',
queue: queues.value[0]?.value,
phone: '',
state: 1
}
if (!user.value) {
formValues.value = defaultValues
} else {
const v: any = {}
Object.keys(defaultValues).map((k) => {
v[k] = user.value[k]
})
v.userPassword = ''
formValues.value = v
}
}
const prepareOptions = async () => {
optionsLoading.value = true
Promise.all([queryTenantList(), queryList()])
.then((res) => {
tenants.value =
res[0]?.map((d: any) => ({
label: d.tenantCode,
value: d.id
})) || []
queues.value =
res[1]?.map((d: any) => ({
label: d.queueName,
value: d.queue
})) || []
})
.finally(() => {
optionsLoading.value = false
})
}
const fetchProjects = async () => {
optionsLoading.value = true
Promise.all([
queryAuthorizedProject({ userId: user.value.id }),
queryUnauthorizedProject({ userId: user.value.id })
])
.then((res: any[]) => {
const ids: string[] = []
res[0]?.forEach((d: any) => {
if (!ids.includes(d.id)) {
ids.push(d.id)
}
})
authorizedProjects.value = ids
projects.value =
res?.flat().map((d: any) => ({ label: d.name, value: d.id })) || []
})
.finally(() => {
optionsLoading.value = false
})
}
const fetchResources = async () => {
optionsLoading.value = true
Promise.all([
authorizedFile({ userId: user.value.id }),
authorizeResourceTree({ userId: user.value.id })
])
.then((res: any[]) => {
const ids: string[] = []
const getIds = (arr: any[]) => {
arr.forEach((d) => {
const children = d.children
if (children instanceof Array && children.length > 0) {
getIds(children)
} else {
ids.push(`${d.pid}-${d.id}`)
}
})
}
getIds(res[0] || [])
authorizedFiles.value = ids
originResourceTree.value = res[1] || []
})
.finally(() => {
optionsLoading.value = false
})
}
const fetchDatasource = async () => {
optionsLoading.value = true
Promise.all([
authedDatasource({ userId: user.value.id }),
unAuthDatasource({ userId: user.value.id })
])
.then((res: any[]) => {
const ids: string[] = []
res[0]?.forEach((d: any) => {
if (!ids.includes(d.id)) {
ids.push(d.id)
}
})
authorizedDatasource.value = ids
datasource.value =
res?.flat().map((d: any) => ({ label: d.name, value: d.id })) || []
})
.finally(() => {
optionsLoading.value = false
})
}
const fetchUDFs = async () => {
optionsLoading.value = true
Promise.all([
authUDFFunc({ userId: user.value.id }),
unAuthUDFFunc({ userId: user.value.id })
])
.then((res: any[]) => {
const ids: string[] = []
res[0]?.forEach((d: any) => {
if (!ids.includes(d.id)) {
ids.push(d.id)
}
})
authorizedUDF.value = ids
UDFs.value =
res?.flat().map((d: any) => ({ label: d.name, value: d.id })) || []
})
.finally(() => {
optionsLoading.value = false
})
}
const onModalCancel = () => {
show.value = false
}
const onDelete = () => {
confirmLoading.value = true
delUserById({ id: user.value.id })
.then(
() => {
onSuccess?.(mode.value)
onModalCancel()
},
() => {
message.error(t('security.user.delete_error_msg'))
}
)
.finally(() => {
confirmLoading.value = false
})
}
const onCreateUser = () => {
confirmLoading.value = true
verifyUserName({ userName: formValues.value.userName })
.then(
() => createUser(formValues.value),
(error) => {
if (`${error.message}`.includes('exists')) {
message.error(t('security.user.username_exists'))
}
return false
}
)
.then(
(res) => {
if (res) {
onSuccess?.(mode.value)
onModalCancel()
}
},
() => {
message.error(t('security.user.save_error_msg'))
}
)
.finally(() => {
confirmLoading.value = false
})
}
const onUpdateUser = () => {
confirmLoading.value = true
updateUser({ id: user.value.id, ...formValues.value })
.then(
() => {
onSuccess?.(mode.value)
onModalCancel()
},
() => {
message.error(t('security.user.save_error_msg'))
}
)
.finally(() => {
confirmLoading.value = false
})
}
const onGrant = (grantReq: () => Promise<any>) => {
confirmLoading.value = true
grantReq()
.then(
() => {
onSuccess?.(mode.value)
onModalCancel()
message.success(t('security.user.auth_success_msg'))
},
() => {
message.error(t('security.user.auth_error_msg'))
}
)
.finally(() => {
confirmLoading.value = false
})
}
const onConfirm = () => {
if (mode.value === 'add' || mode.value === 'edit') {
formRef.value.validate((errors: any) => {
if (!errors) {
user.value ? onUpdateUser() : onCreateUser()
}
})
} else {
mode.value === 'delete' && onDelete()
mode.value === 'auth_project' &&
onGrant(() =>
grantProject({
userId: user.value.id,
projectIds: authorizedProjects.value.join(',')
})
)
mode.value === 'auth_resource' &&
onGrant(() =>
grantResource({
userId: user.value.id,
resourceIds: authorizedFiles.value.join(',')
})
)
mode.value === 'auth_datasource' &&
onGrant(() =>
grantDataSource({
userId: user.value.id,
datasourceIds: authorizedDatasource.value.join(',')
})
)
mode.value === 'auth_udf' &&
onGrant(() =>
grantUDFFunc({
userId: user.value.id,
udfIds: authorizedUDF.value.join(',')
})
)
}
}
watch([show, mode], () => {
show.value && ['add', 'edit'].includes(mode.value) && prepareOptions()
show.value && mode.value === 'auth_project' && fetchProjects()
show.value && mode.value === 'auth_resource' && fetchResources()
show.value && mode.value === 'auth_datasource' && fetchDatasource()
show.value && mode.value === 'auth_udf' && fetchUDFs()
})
watch([queues, tenants, user], () => {
setFormValues()
})
return {
show,
mode,
user,
titleMap,
onModalCancel,
formRef,
formValues,
formRules,
tenants,
queues,
authorizedProjects,
projects,
authorizedDatasource,
datasource,
authorizedUDF,
UDFs,
authorizedFiles,
resourceTree,
resourceType,
optionsLoading,
onConfirm,
confirmLoading
}
}

177
dolphinscheduler-ui-next/src/views/security/user-manage/components/use-user-detail.ts

@ -0,0 +1,177 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { onMounted, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { pick } from 'lodash'
import { queryTenantList } from '@/service/modules/tenants'
import { queryList } from '@/service/modules/queues'
import { verifyUserName, createUser, updateUser } from '@/service/modules/users'
import { useUserStore } from '@/store/user/user'
import type { IRecord, UserReq, UserInfoRes } from '../types'
export function useUserDetail() {
const { t } = useI18n()
const userStore = useUserStore()
const userInfo = userStore.getUserInfo as UserInfoRes
const IS_ADMIN = userInfo.userType === 'ADMIN_USER'
const initialValues = {
userName: '',
userPassword: '',
tenantId: 0,
email: '',
queue: '',
phone: '',
state: 1
} as UserReq
let PREV_NAME: string
const state = reactive({
formRef: ref(),
formData: { ...initialValues },
saving: false,
loading: false,
queues: [] as { label: string; value: string }[],
tenants: [] as { label: string; value: number }[]
})
const formRules = {
userName: {
trigger: ['input', 'blur'],
required: true,
validator(validator: any, value: string) {
if (!value.trim()) {
return new Error(t('security.user.username_tips'))
}
}
},
userPassword: {
trigger: ['input', 'blur'],
required: true,
validator(validator: any, value: string) {
if (
!value ||
!/^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?![`~!@#$%^&*()_\-+=<>?:"{}|,./;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、]+$)[`~!@#$%^&*()_\-+=<>?:"{}|,./;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、0-9A-Za-z]{6,22}$/.test(
value
)
) {
return new Error(t('security.user.user_password_tips'))
}
}
},
tenantId: {
trigger: ['input', 'blur'],
required: true,
validator(validator: any, value: string) {
if (IS_ADMIN && !value) {
return new Error(t('security.user.tenant_id_tips'))
}
}
},
email: {
trigger: ['input', 'blur'],
required: true,
validator(validator: any, value: string) {
if (!value) {
return new Error(t('security.user.email_empty_tips'))
}
if (
!/^([a-zA-Z0-9]+[_|\-|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\-|\.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{2,}$/.test(
value
)
) {
return new Error(t('security.user.emial_correct_tips'))
}
}
},
phone: {
trigger: ['input', 'blur'],
validator(validator: any, value: string) {
if (value && !/^1(3|4|5|6|7|8)\d{9}$/.test(value)) {
return new Error(t('security.user.phone_correct_tips'))
}
}
}
}
const getQueues = async () => {
const result = await queryList()
state.queues = result.map((queue: { queueName: string; id: string }) => ({
label: queue.queueName,
value: queue.id
}))
if (state.queues.length) {
initialValues.queue = state.queues[0].value
state.formData.queue = state.queues[0].value
}
}
const getTenants = async () => {
const result = await queryTenantList()
state.tenants = result.map(
(tenant: { tenantCode: string; id: number }) => ({
label: tenant.tenantCode,
value: tenant.id
})
)
if (state.tenants.length) {
initialValues.tenantId = state.tenants[0].value
state.formData.tenantId = state.tenants[0].value
}
}
const onReset = () => {
state.formData = { ...initialValues }
}
const onSave = async (id?: number): Promise<boolean> => {
await state.formRef.validate()
if (state.saving) return false
state.saving = true
if (PREV_NAME !== state.formData.userName) {
await verifyUserName({ userName: state.formData.userName })
}
id
? await updateUser({ id, ...state.formData })
: await createUser(state.formData)
state.saving = false
return true
}
const onSetValues = (record: IRecord) => {
state.formData = {
...pick(record, [
'userName',
'tenantId',
'email',
'queue',
'phone',
'state'
]),
userPassword: ''
} as UserReq
PREV_NAME = state.formData.userName
}
onMounted(async () => {
if (IS_ADMIN) {
getQueues()
getTenants()
}
})
return { state, formRules, IS_ADMIN, onReset, onSave, onSetValues }
}

177
dolphinscheduler-ui-next/src/views/security/user-manage/components/user-detail-modal.tsx

@ -0,0 +1,177 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineComponent, PropType, toRefs, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import {
NInput,
NForm,
NFormItem,
NSelect,
NRadio,
NRadioGroup,
NSpace
} from 'naive-ui'
import { useUserDetail } from './use-user-detail'
import Modal from '@/components/modal'
import type { IRecord } from '../types'
const props = {
show: {
type: Boolean as PropType<boolean>,
default: false
},
currentRecord: {
type: Object as PropType<IRecord | null>,
default: {}
}
}
export const UserModal = defineComponent({
name: 'user-modal',
props,
emits: ['cancel', 'update'],
setup(props, ctx) {
const { t } = useI18n()
const { state, IS_ADMIN, formRules, onReset, onSave, onSetValues } =
useUserDetail()
const onCancel = () => {
onReset()
ctx.emit('cancel')
}
const onConfirm = async () => {
const result = await onSave(props.currentRecord?.id)
if (!result) return
onCancel()
ctx.emit('update')
}
watch(
() => props.show,
() => {
if (props.show && props.currentRecord?.id) {
onSetValues(props.currentRecord)
}
}
)
return {
t,
...toRefs(state),
IS_ADMIN,
formRules,
onCancel,
onConfirm
}
},
render(props: { currentRecord: IRecord }) {
const { t } = this
const { currentRecord } = props
return (
<Modal
show={this.show}
title={`${t(
currentRecord?.id
? 'security.user.update_user'
: 'security.user.create_user'
)}`}
onCancel={this.onCancel}
confirmLoading={this.loading}
onConfirm={this.onConfirm}
confirmClassName='btn-submit'
cancelClassName='btn-cancel'
>
<NForm
ref='formRef'
model={this.formData}
rules={this.formRules}
labelPlacement='left'
labelAlign='left'
labelWidth={80}
>
<NFormItem label={t('security.user.username')} path='userName'>
<NInput
class='input-username'
v-model:value={this.formData.userName}
minlength={3}
maxlength={39}
placeholder={t('security.user.username_tips')}
/>
</NFormItem>
<NFormItem
label={t('security.user.user_password')}
path='userPassword'
>
<NInput
class='input-password'
type='password'
v-model:value={this.formData.userPassword}
placeholder={t('security.user.user_password_tips')}
/>
</NFormItem>
{this.IS_ADMIN && (
<NFormItem label={t('security.user.tenant_code')} path='tenantId'>
<NSelect
class='select-tenant'
options={this.tenants}
v-model:value={this.formData.tenantId}
/>
</NFormItem>
)}
{this.IS_ADMIN && (
<NFormItem label={t('security.user.queue')} path='queue'>
<NSelect
class='select-queue'
options={this.queues}
v-model:value={this.formData.queue}
placeholder={t('security.user.queue_tips')}
/>
</NFormItem>
)}
<NFormItem label={t('security.user.email')} path='email'>
<NInput
class='input-email'
v-model:value={this.formData.email}
placeholder={t('security.user.email_empty_tips')}
/>
</NFormItem>
<NFormItem label={t('security.user.phone')} path='phone'>
<NInput
class='input-phone'
v-model:value={this.formData.phone}
placeholder={t('security.user.phone_empty_tips')}
/>
</NFormItem>
<NFormItem label={t('security.user.state')} path='state'>
<NRadioGroup v-model:value={this.formData.state}>
<NSpace>
<NRadio value={1} class='radio-state-enable'>
{this.t('security.user.enable')}
</NRadio>
<NRadio value={0} class='radio-state-disable'>
{this.t('security.user.disable')}
</NRadio>
</NSpace>
</NRadioGroup>
</NFormItem>
</NForm>
</Modal>
)
}
})
export default UserModal

209
dolphinscheduler-ui-next/src/views/security/user-manage/components/user-modal.tsx

@ -1,209 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineComponent, inject } from 'vue'
import { useI18n } from 'vue-i18n'
import {
NInput,
NForm,
NFormItem,
NSelect,
NRadio,
NRadioGroup,
NRadioButton,
NSpace,
NAlert,
NTransfer,
NTreeSelect
} from 'naive-ui'
import Modal from '@/components/modal'
import {
useModal,
useSharedUserModalState,
UserModalSharedStateKey
} from './use-modal'
export const UserModal = defineComponent({
name: 'user-modal',
setup() {
const { t } = useI18n()
const sharedState =
inject(UserModalSharedStateKey) || useSharedUserModalState()
const modalState = useModal(sharedState)
return {
t,
...modalState
}
},
render() {
const { t } = this
return (
<Modal
show={this.show}
title={this.titleMap?.[this.mode || 'add']}
onCancel={this.onModalCancel}
confirmDisabled={this.optionsLoading}
confirmLoading={this.confirmLoading}
onConfirm={this.onConfirm}
confirmClassName='btn-submit'
cancelClassName='btn-cancel'
>
{{
default: () => {
if (this.mode === 'delete') {
return (
<NAlert type='error' title={t('security.user.delete_confirm')}>
{t('security.user.delete_confirm_tip')}
</NAlert>
)
}
if (this.mode === 'auth_project') {
return (
<NTransfer
virtualScroll
options={this.projects}
filterable
v-model:value={this.authorizedProjects}
style={{ margin: '0 auto' }}
/>
)
}
if (this.mode === 'auth_datasource') {
return (
<NTransfer
virtualScroll
options={this.datasource}
filterable
v-model:value={this.authorizedDatasource}
style={{ margin: '0 auto' }}
/>
)
}
if (this.mode === 'auth_udf') {
return (
<NTransfer
virtualScroll
options={this.UDFs}
filterable
v-model:value={this.authorizedUDF}
style={{ margin: '0 auto' }}
/>
)
}
if (this.mode === 'auth_resource') {
return (
<NSpace vertical>
<NRadioGroup v-model:value={this.resourceType}>
<NRadioButton key='file' value='file'>
{t('security.user.file_resource')}
</NRadioButton>
<NRadioButton key='udf' value='udf'>
{t('security.user.udf_resource')}
</NRadioButton>
</NRadioGroup>
<NTreeSelect
multiple
cascade
checkable
checkStrategy='child'
defaultExpandAll
options={this.resourceTree}
v-model:value={this.authorizedFiles}
/>
</NSpace>
)
}
return (
<NForm
ref='formRef'
model={this.formValues}
rules={this.formRules}
labelPlacement='left'
labelAlign='left'
labelWidth={80}
>
<NFormItem label={t('security.user.username')} path='userName'>
<NInput
class='input-username'
inputProps={{ autocomplete: 'off' }}
v-model:value={this.formValues.userName}
/>
</NFormItem>
<NFormItem
label={t('security.user.user_password')}
path='userPassword'
>
<NInput
class='input-password'
inputProps={{ autocomplete: 'off' }}
type='password'
v-model:value={this.formValues.userPassword}
/>
</NFormItem>
<NFormItem
label={t('security.user.tenant_code')}
path='tenantId'
>
<NSelect
class='select-tenant'
options={this.tenants}
v-model:value={this.formValues.tenantId}
/>
</NFormItem>
<NFormItem label={t('security.user.queue')} path='queue'>
<NSelect
class='select-queue'
options={this.queues}
v-model:value={this.formValues.queue}
/>
</NFormItem>
<NFormItem label={t('security.user.email')} path='email'>
<NInput
class='input-email'
v-model:value={this.formValues.email}
/>
</NFormItem>
<NFormItem label={t('security.user.phone')} path='phone'>
<NInput
class='input-phone'
v-model:value={this.formValues.phone}
/>
</NFormItem>
<NFormItem label={t('security.user.state')} path='state'>
<NRadioGroup v-model:value={this.formValues.state}>
<NSpace>
<NRadio value={1} class='radio-state-enable'>
</NRadio>
<NRadio value={0} class='radio-state-disable'>
</NRadio>
</NSpace>
</NRadioGroup>
</NFormItem>
</NForm>
)
}
}}
</Modal>
)
}
})
export default UserModal

20
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%;
}

174
dolphinscheduler-ui-next/src/views/security/user-manage/index.tsx

@ -15,132 +15,112 @@
* limitations under the License.
*/
import { defineComponent, provide } from 'vue'
import { defineComponent, toRefs, watch } from 'vue'
import {
NCard,
NButton,
NInputGroup,
NInput,
NIcon,
NSpace,
NGrid,
NGridItem,
NDataTable,
NPagination,
NSkeleton
NPagination
} from 'naive-ui'
import Card from '@/components/card'
import UserDetailModal from './components/user-detail-modal'
import AuthorizeModal from './components/authorize-modal'
import { useI18n } from 'vue-i18n'
import { SearchOutlined } from '@vicons/antd'
import { useColumns } from './use-columns'
import { useTable } from './use-table'
import UserModal from './components/user-modal'
import {
useSharedUserModalState,
UserModalSharedStateKey,
Mode
} from './components/use-modal'
const UsersManage = defineComponent({
name: 'user-manage',
setup() {
const { t } = useI18n()
const { show, mode, user } = useSharedUserModalState()
const tableState = useTable({
onEdit: (u, m: Mode) => {
show.value = true
mode.value = m
user.value = u
},
onDelete: (u) => {
show.value = true
mode.value = 'delete'
user.value = u
}
})
const onSuccess = (mode: Mode) => {
if (!mode.startsWith('auth')) {
mode === 'add' && tableState.resetPage()
tableState.getUserList()
}
}
const { state, changePage, changePageSize, updateList, onOperationClick } =
useTable()
const { columnsRef } = useColumns(onOperationClick)
const onAddUser = () => {
show.value = true
mode.value = 'add'
user.value = undefined
state.detailModalShow = true
state.currentRecord = null
}
const onDetailModalCancel = () => {
state.detailModalShow = false
}
const onAuthorizeModalCancel = () => {
state.authorizeModalShow = false
}
provide(UserModalSharedStateKey, { show, mode, user, onSuccess })
return {
t,
columnsRef,
...toRefs(state),
changePage,
changePageSize,
onAddUser,
...tableState
onUpdatedList: updateList,
onDetailModalCancel,
onAuthorizeModalCancel
}
},
render() {
const { t, onSearchValOk, onSearchValClear, userListLoading } = this
return (
<>
<NGrid cols={1} yGap={16}>
<NGridItem>
<NCard>
<NSpace justify='space-between'>
<NButton
onClick={this.onAddUser}
type='primary'
class='btn-create-user'
>
{t('security.user.create_user')}
<NSpace vertical>
<Card>
<NSpace justify='space-between'>
<NButton
onClick={this.onAddUser}
type='primary'
class='btn-create-user'
>
{this.t('security.user.create_user')}
</NButton>
<NSpace>
<NInput v-model:value={this.searchVal} clearable />
<NButton type='primary' onClick={this.onUpdatedList}>
<NIcon>
<SearchOutlined />
</NIcon>
</NButton>
<NInputGroup>
<NInput
v-model:value={this.searchInputVal}
clearable
onClear={onSearchValClear}
onKeyup={(e) => {
if (e.key === 'Enter') {
onSearchValOk()
}
}}
/>
<NButton type='primary' onClick={onSearchValOk}>
<NIcon>
<SearchOutlined />
</NIcon>
</NButton>
</NInputGroup>
</NSpace>
</NCard>
</NGridItem>
<NGridItem>
<NCard>
{userListLoading ? (
<NSkeleton text repeat={6} />
) : (
<NSpace v-show={!userListLoading} vertical size={20}>
<NDataTable
row-class-name='items'
columns={this.columns}
data={this.userList}
scrollX={this.scrollX}
bordered={false}
/>
<NSpace justify='center'>
<NPagination
v-model:page={this.page}
v-model:page-size={this.pageSize}
pageCount={this.pageCount}
pageSizes={this.pageSizes}
showSizePicker
/>
</NSpace>
</NSpace>
)}
</NCard>
</NGridItem>
</NGrid>
<UserModal />
</NSpace>
</Card>
<Card>
<NSpace vertical>
<NDataTable
row-class-name='items'
columns={this.columnsRef}
data={this.list}
loading={this.loading}
/>
<NSpace justify='center'>
<NPagination
v-model:page={this.page}
v-model:page-size={this.pageSize}
item-count={this.itemCount}
show-size-picker
page-sizes={[10, 30, 50]}
show-quick-jumper
on-update:page={this.changePage}
on-update:page-size={this.changePageSize}
/>
</NSpace>
</NSpace>
</Card>
</NSpace>
<UserDetailModal
show={this.detailModalShow}
currentRecord={this.currentRecord}
onCancel={this.onDetailModalCancel}
onUpdate={this.onUpdatedList}
/>
<AuthorizeModal
show={this.authorizeModalShow}
type={this.authorizeType}
userId={this.currentRecord?.id}
onCancel={this.onAuthorizeModalCancel}
/>
</>
)
}

63
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
}

219
dolphinscheduler-ui-next/src/views/security/user-manage/use-columns.ts

@ -0,0 +1,219 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { h, ref, watch, onMounted, Ref } from 'vue'
import { useI18n } from 'vue-i18n'
import {
NSpace,
NTooltip,
NButton,
NIcon,
NTag,
NDropdown,
NPopconfirm
} from 'naive-ui'
import { EditOutlined, DeleteOutlined, UserOutlined } from '@vicons/antd'
import { TableColumns, InternalRowData } from './types'
export function useColumns(onCallback: Function) {
const { t } = useI18n()
const columnsRef = ref([]) as Ref<TableColumns>
const createColumns = () => {
columnsRef.value = [
{
title: '#',
key: 'index',
render: (rowData: InternalRowData, rowIndex: number) => rowIndex + 1
},
{
title: t('security.user.username'),
key: 'userName'
},
{
title: t('security.user.user_type'),
key: 'userType',
render: (rowData: InternalRowData) =>
rowData.userType === 'GENERAL_USER'
? t('security.user.ordinary_user')
: t('security.user.administrator')
},
{
title: t('security.user.tenant_code'),
key: 'tenantCode'
},
{
title: t('security.user.queue'),
key: 'queue'
},
{
title: t('security.user.email'),
key: 'email'
},
{
title: t('security.user.phone'),
key: 'phone'
},
{
title: t('security.user.state'),
key: 'state',
render: (rowData: any, unused: number) =>
h(
NTag,
{ type: rowData.state === 1 ? 'success' : 'error' },
{
default: () =>
t(
`security.user.state_${
rowData.state === 1 ? 'enabled' : 'disabled'
}`
)
}
)
},
{
title: t('security.user.create_time'),
key: 'createTime'
},
{
title: t('security.user.update_time'),
key: 'updateTime'
},
{
title: t('security.user.operation'),
key: 'operation',
render: (rowData: any, unused: number) => {
return h(NSpace, null, {
default: () => [
h(
NDropdown,
{
trigger: 'click',
options: [
{
label: t('security.user.project'),
key: 'authorize_project'
},
{
label: t('security.user.resource'),
key: 'authorize_resource'
},
{
label: t('security.user.datasource'),
key: 'authorize_datasource'
},
{ label: t('security.user.udf'), key: 'authorize_udf' }
],
onSelect: (key) =>
void onCallback({ rowData, key }, 'authorize')
},
() =>
h(
NTooltip,
{
trigger: 'hover'
},
{
trigger: () =>
h(
NButton,
{
circle: true,
type: 'warning',
size: 'small',
class: 'authorize'
},
{
icon: () => h(NIcon, null, () => h(UserOutlined))
}
),
default: () => t('security.user.authorize')
}
)
),
h(
NTooltip,
{ trigger: 'hover' },
{
trigger: () =>
h(
NButton,
{
circle: true,
type: 'info',
size: 'small',
onClick: () => void onCallback({ rowData }, 'edit')
},
() => h(NIcon, null, () => h(EditOutlined))
),
default: () => t('security.user.edit')
}
),
h(
NPopconfirm,
{
onPositiveClick: () => void onCallback({ rowData }, 'delete')
},
{
trigger: () =>
h(
NTooltip,
{},
{
trigger: () =>
h(
NButton,
{
circle: true,
type: 'error',
size: 'small',
class: 'delete'
},
{
icon: () =>
h(NIcon, null, {
default: () => h(DeleteOutlined)
})
}
),
default: () => t('security.user.delete')
}
),
default: () => t('security.user.delete_confirm')
}
)
]
})
}
}
]
}
onMounted(() => {
createColumns()
})
watch(useI18n().locale, () => {
createColumns()
})
return {
columnsRef,
createColumns
}
}

112
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 }
}

255
dolphinscheduler-ui-next/src/views/security/user-manage/use-table.tsx

@ -1,255 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ref, watch, onBeforeMount, computed } from 'vue'
import { NSpace, NTooltip, NButton, NIcon, NTag, NDropdown } from 'naive-ui'
import { EditOutlined, DeleteOutlined, UserOutlined } from '@vicons/antd'
import { queryUserList } from '@/service/modules/users'
import { useI18n } from 'vue-i18n'
import { Mode } from './components/use-modal'
type UseTableProps = {
onEdit: (user: any, mode: Mode) => void
onDelete: (user: any) => void
}
function useColumns({ onEdit, onDelete }: UseTableProps) {
const { t } = useI18n()
const columns = computed(() =>
[
{
title: '#',
key: 'index',
width: 80,
render: (rowData: any, rowIndex: number) => rowIndex + 1
},
{
title: t('security.user.username'),
key: 'userName',
className: 'name'
},
{
title: t('security.user.tenant_code'),
key: 'tenantCode'
},
{
title: t('security.user.queue'),
key: 'queue'
},
{
title: t('security.user.email'),
key: 'email'
},
{
title: t('security.user.phone'),
key: 'phone'
},
{
title: t('security.user.state'),
key: 'state',
render: (rowData: any, unused: number) => {
return rowData.state === 1 ? (
<NTag type='success'>{t('security.user.state_enabled')}</NTag>
) : (
<NTag type='error'>{t('security.user.state_disabled')}</NTag>
)
}
},
{
title: t('security.user.create_time'),
key: 'createTime',
width: 200
},
{
title: t('security.user.update_time'),
key: 'updateTime',
width: 200
},
{
title: t('security.user.operation'),
key: 'operation',
fixed: 'right',
width: 140,
render: (rowData: any, unused: number) => {
return (
<NSpace>
<NDropdown
trigger='click'
options={[
{ label: t('security.user.project'), key: 'auth_project' },
{ label: t('security.user.resource'), key: 'auth_resource' },
{
label: t('security.user.datasource'),
key: 'auth_datasource'
},
{ label: t('security.user.udf'), key: 'auth_udf' }
]}
onSelect={(key) => {
onEdit(rowData, key)
}}
>
<NTooltip trigger='hover'>
{{
trigger: () => (
<NButton
circle
type='warning'
size='small'
class='authorize'
>
{{
icon: () => (
<NIcon>
<UserOutlined />
</NIcon>
)
}}
</NButton>
),
default: () => t('security.user.authorize')
}}
</NTooltip>
</NDropdown>
<NTooltip trigger='hover'>
{{
trigger: () => (
<NButton
circle
type='info'
size='small'
class='edit'
onClick={() => {
onEdit(rowData, 'edit')
}}
>
{{
icon: () => (
<NIcon>
<EditOutlined />
</NIcon>
)
}}
</NButton>
),
default: () => t('security.user.edit')
}}
</NTooltip>
<NTooltip trigger='hover'>
{{
trigger: () => (
<NButton
circle
type='error'
size='small'
class='delete'
onClick={() => {
onDelete(rowData)
}}
>
{{
icon: () => (
<NIcon>
<DeleteOutlined />
</NIcon>
)
}}
</NButton>
),
default: () => t('security.user.delete')
}}
</NTooltip>
</NSpace>
)
}
}
].map((d: any) => ({ ...d, width: d.width || 160 }))
)
const scrollX = columns.value.reduce((p, c) => p + c.width, 0)
return {
columns,
scrollX
}
}
export function useTable(props: UseTableProps) {
const page = ref(1)
const pageCount = ref(0)
const pageSize = ref(10)
const searchInputVal = ref()
const searchVal = ref('')
const pageSizes = [10, 30, 50]
const userListLoading = ref(false)
const userList = ref([])
const { columns, scrollX } = useColumns(props)
const getUserList = () => {
userListLoading.value = true
queryUserList({
pageNo: page.value,
pageSize: pageSize.value,
searchVal: searchVal.value
})
.then((res: any) => {
userList.value = res?.totalList || []
pageCount.value = res?.totalPage || 0
})
.finally(() => {
userListLoading.value = false
})
}
const resetPage = () => {
page.value = 1
}
const onSearchValOk = () => {
resetPage()
searchVal.value = searchInputVal.value
}
const onSearchValClear = () => {
resetPage()
searchVal.value = ''
}
onBeforeMount(() => {
getUserList()
})
watch([page, pageSize, searchVal], () => {
getUserList()
})
return {
userList,
userListLoading,
getUserList,
page,
pageCount,
pageSize,
searchVal,
searchInputVal,
pageSizes,
columns,
scrollX,
onSearchValOk,
onSearchValClear,
resetPage
}
}
Loading…
Cancel
Save