diff --git a/api/app/lib/controllers/auth/index.js b/api/app/lib/controllers/auth/index.js index eb7d901..b8d6b8e 100644 --- a/api/app/lib/controllers/auth/index.js +++ b/api/app/lib/controllers/auth/index.js @@ -5,7 +5,7 @@ const MD5 = require('crypto-js/md5'); const moment = require('moment'); const uuid = require('uuid'); -async function login (ctx, next) { +async function login(ctx, next) { // const transaction = await ctx.fs.dc.orm.transaction(); try { const models = ctx.fs.dc.models; @@ -19,7 +19,8 @@ async function login (ctx, next) { attributes: { exclude: ['password'] }, where: { username: params.username, - password: password + password: password, + enabled: true }, }); } @@ -58,7 +59,7 @@ async function login (ctx, next) { } } -async function logout (ctx) { +async function logout(ctx) { try { const models = ctx.fs.dc.models; const params = ctx.request.body; diff --git a/api/app/lib/controllers/member/index.js b/api/app/lib/controllers/member/index.js new file mode 100644 index 0000000..b2f8845 --- /dev/null +++ b/api/app/lib/controllers/member/index.js @@ -0,0 +1,139 @@ +'use strict'; +const moment = require('moment') + +function getUserList(opts) { + return async function (ctx, next) { + + const models = ctx.fs.dc.models; + const { page, limit, name, role } = ctx.query; + const Op = ctx.fs.dc.ORM.Op; + let errMsg = { message: '获取用户失败' } + try { + let searchWhere = {} + let option = { + where: searchWhere, + order: [["id", "desc"]], + attributes: { exclude: ['password'] }, + } + + if (name) { + searchWhere.name = { $like: name }; + } + + if (role) { + searchWhere.role = role; + } + + option.where = searchWhere + let total = await models.User.findAll(option); + let limit_ = limit || 10; + let page_ = page || 1; + let offset = (page_ - 1) * limit_; + if (limit && page) { + option.limit = limit_ + option.offset = offset + } + + const res = await models.User.findAndCount(option); + ctx.status = 200; + ctx.body = res; + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = errMsg + } + } +} + +// 新增用户 +function addUser(opts) { + return async function (ctx, next) { + + const models = ctx.fs.dc.models; + try { + const { attributeName, attributeCode, modelType } = ctx.request.body + const checkName = await models.User.findOne({ where: { attributeName, modelType } }); + const checkCode = await models.User.findOne({ where: { attributeCode, modelType } }); + if (checkName || checkCode) { + ctx.status = 400; + ctx.body = { message: checkName ? '该属性名称已存在' : "该属性代码已存在" } + } else { + let rslt = ctx.request.body; + await models.User.create(Object.assign({}, rslt)) + ctx.status = 204; + ctx.body = { message: '新建用户成功' } + } + + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { message: '新建用户失败' } + } + } +} + +// 修改用户 +function editUser(opts) { + return async function (ctx, next) { + + try { + const models = ctx.fs.dc.models; + const { id } = ctx.params; + const body = ctx.request.body; + let modelFind = await models.User.findOne({ where: { id: id } }); + const { attributeName, attributeCode } = ctx.request.body; + const { modelType } = modelFind; + const checkName = await models.User.findOne({ where: { id: { $not: id }, attributeName, modelType } }); + const checkCode = await models.User.findOne({ where: { id: { $not: id }, attributeCode, modelType } }); + if (checkName || checkCode) { + ctx.status = 400; + ctx.body = { message: checkName ? '该属性名称已存在' : "该属性代码已存在" } + } else { + await models.User.update( + body, + { where: { id: id, } } + ) + ctx.status = 204; + ctx.body = { message: '修改用户成功' } + } + + + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { message: '修改用户失败' } + } + } +} + +// 删除用户 +function deleteUser(opts) { + return async function (ctx, next) { + + try { + const models = ctx.fs.dc.models; + const { id } = ctx.params; + await models.User.destroy({ + where: { + id: id + } + }) + ctx.status = 204; + ctx.body = { message: '删除用户成功' } + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { message: '删除用户失败' } + } + } +} + + + +module.exports = { + getUserList, + addUser, + editUser, + deleteUser, + +} \ No newline at end of file diff --git a/api/app/lib/models/user.js b/api/app/lib/models/user.js index a4b5709..cf38fb6 100644 --- a/api/app/lib/models/user.js +++ b/api/app/lib/models/user.js @@ -51,7 +51,17 @@ module.exports = dc => { primaryKey: false, field: "role", autoIncrement: false + }, + enabled: { + type: DataTypes.BOOLEAN, + allowNull: true, + defaultValue: true, + comment: "是否启用", + primaryKey: false, + field: "enabled", + autoIncrement: false } + }, { tableName: "t_user", comment: "", diff --git a/api/app/lib/routes/member/index.js b/api/app/lib/routes/member/index.js new file mode 100644 index 0000000..db9caad --- /dev/null +++ b/api/app/lib/routes/member/index.js @@ -0,0 +1,23 @@ +'use strict'; + +const member = require('../../controllers/member/index'); + +module.exports = function (app, router, opts, AuthCode) { + + app.fs.api.logAttr['POST/meta/member'] = { content: '增加用户', visible: true }; + router.post('/meta/member', member.addUser(opts)) + + // 修改用户信息 + app.fs.api.logAttr['PUT/meta/member/:id'] = { content: '修改用户信息', visible: true }; + router.put('/meta/member/:id', member.editUser(opts)) + + // 删除用户信息 + app.fs.api.logAttr['DEL/meta/member/:id'] = { content: '删除用户信息', visible: true }; + router.del('/meta/member/:id', member.deleteUser(opts)) + + //获取用户信息列表 + app.fs.api.logAttr['GET/meta/members'] = { content: '获取用户信息列表', visible: true }; + router.get('/meta/members', member.getUserList(opts)); + + +}; diff --git a/web/client/src/app.js b/web/client/src/app.js index e615824..91f27b8 100644 --- a/web/client/src/app.js +++ b/web/client/src/app.js @@ -8,6 +8,7 @@ import MetadataManagement from './sections/metadataManagement'; import MetadataAcquisition from './sections/metadataAcquisition'; import resourceConsumption from './sections/resourceConsumption'; import resourceRetrieval from './sections/resourceRetrieval'; +import memberManagement from './sections/memberManagement'; const App = props => { const { projectName } = props @@ -24,7 +25,9 @@ const App = props => { MetadataManagement, MetadataAcquisition, resourceConsumption, - resourceRetrieval]} + resourceRetrieval, + memberManagement + ]} /> ) diff --git a/web/client/src/sections/memberManagement/actions/adapter.js b/web/client/src/sections/memberManagement/actions/adapter.js new file mode 100644 index 0000000..8dfe530 --- /dev/null +++ b/web/client/src/sections/memberManagement/actions/adapter.js @@ -0,0 +1,44 @@ +'use strict'; + +import { basicAction } from '@peace/utils' +import { ApiTable } from '$utils' + +export function checkPostgreConnect(params) { + return (dispatch) => basicAction({ + type: 'post', + data: params, + dispatch, + actionType: 'CHECK_POSTGRE_CONNECT', + url: ApiTable.pgCheckConnect, + msg: { + option: '', + }, + }); +} + +export function addAdapter(params) { + return (dispatch) => basicAction({ + type: 'post', + data: params, + dispatch, + actionType: 'ADD_ADAPTER', + url: ApiTable.addAdapter, + msg: { + option: '新增适配器', + }, + }); +} + +export function getAdapters(query) { + return dispatch => basicAction({ + type: 'get', + dispatch: dispatch, + query: query || {}, + actionType: 'GET_ACQ_ADAPTERS', + url: `${ApiTable.getAdapters}`, + msg: { error: '获取适配器列表失败' }, + reducer: { name: 'adapters' } + }); +} + + diff --git a/web/client/src/sections/memberManagement/actions/dataSource.js b/web/client/src/sections/memberManagement/actions/dataSource.js new file mode 100644 index 0000000..91f8963 --- /dev/null +++ b/web/client/src/sections/memberManagement/actions/dataSource.js @@ -0,0 +1,56 @@ +'use strict'; + +import { basicAction } from '@peace/utils' +import { ApiTable } from '$utils' + +export function addDataSource(params, msg) { + return (dispatch) => basicAction({ + type: 'post', + data: params, + dispatch, + actionType: 'ADD_DATSOURCE', + url: ApiTable.addDataSource, + msg: { + option: msg == '' ? '' : '新增数据源', + }, + }); +} + +export function getDataSources(query) { + return dispatch => basicAction({ + type: 'get', + dispatch: dispatch, + query: query || {}, + actionType: 'GET_ACQ_DATASOURCES', + url: `${ApiTable.getDataSources}`, + msg: { error: '获取数据源列表失败' }, + reducer: { name: 'datasources' } + }); +} + +export function deleteDataSource(id) { + return (dispatch) => basicAction({ + type: 'del', + dispatch, + actionType: 'DELETE_ACQ_DATASOURCE', + url: ApiTable.modifyDataSource.replace('{id}', id), + msg: { + option: '数据源删除', + }, + }); +} + +export function modifyDataSource(id, params, msg) { + return (dispatch) => basicAction({ + type: 'put', + data: params, + dispatch, + actionType: 'MODIFY_ACQ_DATASOURCE', + url: ApiTable.modifyDataSource.replace('{id}', id), + msg: { + option: msg || '数据源编辑', + }, + }); +} + + diff --git a/web/client/src/sections/memberManagement/actions/example.js b/web/client/src/sections/memberManagement/actions/example.js new file mode 100644 index 0000000..6b3c25d --- /dev/null +++ b/web/client/src/sections/memberManagement/actions/example.js @@ -0,0 +1,15 @@ +'use strict'; + +import { basicAction } from '@peace/utils' +import { ApiTable } from '$utils' + +// export function getMembers(orgId) { +// return dispatch => basicAction({ +// type: 'get', +// dispatch: dispatch, +// actionType: 'GET_MEMBERS', +// url: `${ApiTable.getEnterprisesMembers.replace('{enterpriseId}', orgId)}`, +// msg: { error: '获取用户列表失败' }, +// reducer: { name: 'members' } +// }); +// } diff --git a/web/client/src/sections/memberManagement/actions/index.js b/web/client/src/sections/memberManagement/actions/index.js new file mode 100644 index 0000000..c419cad --- /dev/null +++ b/web/client/src/sections/memberManagement/actions/index.js @@ -0,0 +1,14 @@ +'use strict'; + +import * as example from './example' +import * as adapter from './adapter' +import * as dataSource from './dataSource' +import * as task from './task'; +import * as log from './log'; +export default { + ...example, + ...adapter, + ...dataSource, + ...task, + ...log +} \ No newline at end of file diff --git a/web/client/src/sections/memberManagement/actions/log.js b/web/client/src/sections/memberManagement/actions/log.js new file mode 100644 index 0000000..0fba54a --- /dev/null +++ b/web/client/src/sections/memberManagement/actions/log.js @@ -0,0 +1,16 @@ +'use strict'; + +import { basicAction } from '@peace/utils' +import { ApiTable } from '$utils' + +export function getLogs(query) { + return dispatch => basicAction({ + type: 'get', + dispatch: dispatch, + query: query || {}, + actionType: 'GET_ACQ_LOGS', + url: `${ApiTable.getLogs}`, + msg: { error: '获取采集日志失败' }, + reducer: { name: 'acqlogs' } + }); +} \ No newline at end of file diff --git a/web/client/src/sections/memberManagement/actions/task.js b/web/client/src/sections/memberManagement/actions/task.js new file mode 100644 index 0000000..11d8ce3 --- /dev/null +++ b/web/client/src/sections/memberManagement/actions/task.js @@ -0,0 +1,69 @@ +'use strict'; + +import { basicAction } from '@peace/utils' +import { ApiTable } from '$utils' + +export function addTask(params, msg) { + return (dispatch) => basicAction({ + type: 'post', + data: params, + dispatch, + actionType: 'ADD_ACQ_TASK', + url: ApiTable.addTask, + msg: { + option: msg || '新增采集任务', + }, + }); +} + +export function getTasks(query) { + return dispatch => basicAction({ + type: 'get', + dispatch: dispatch, + query: query || {}, + actionType: 'GET_ACQ_TASKS', + url: `${ApiTable.getTasks}`, + msg: { error: '获取采集任务列表失败' }, + reducer: { name: 'tasks' } + }); +} + +export function deleteTask(id) { + return (dispatch) => basicAction({ + type: 'del', + dispatch, + actionType: 'DELETE_ACQ_TASK', + url: ApiTable.modifyTask.replace('{id}', id), + msg: { + option: '采集任务删除', + }, + }); +} + +export function modifyTask(id, params, msg) { + return (dispatch) => basicAction({ + type: 'put', + data: params, + dispatch, + actionType: 'MODIFY_ACQ_TASK', + url: ApiTable.modifyTask.replace('{id}', id), + msg: { + option: msg || '采集任务编辑', + }, + }); +} + +export function runTask(params, msg) { + return (dispatch) => basicAction({ + type: 'post', + data: params, + dispatch, + actionType: 'RUN_ACQ_TASK', + url: ApiTable.runTask, + msg: { + option: msg || '任务执行', + }, + }); +} + + diff --git a/web/client/src/sections/memberManagement/components/memberModal.js b/web/client/src/sections/memberManagement/components/memberModal.js new file mode 100644 index 0000000..6522f60 --- /dev/null +++ b/web/client/src/sections/memberManagement/components/memberModal.js @@ -0,0 +1,73 @@ +import React, { useRef } from 'react'; +import { Button, Form } from 'antd'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { + ModalForm, + ProFormSelect, + ProFormTextArea, + ProFormDigit, + ProFormText, + ProFormSwitch +} from '@ant-design/pro-form'; + +export default (props) => { + const { title, triggerRender, editData = null, onFinish, paramsName } = props; + const formItemLayout = { labelCol: { span: 6 }, wrapperCol: { span: 16 } }; + const initialValues = editData ? { + ...editData, + } : {}; + const [form] = Form.useForm(); + const formRef = useRef(); + return ( + + {title || ''} + + } + layout="horizontal" + grid={true} + {...formItemLayout} + modalProps={{ + destroyOnClose: true, + onCancel: () => { }, + }} + onFinish={async (values) => { + return onFinish && await onFinish(values, editData, form) + // return true; + }} + width={500} + > + + + + + + + + + ); +}; \ No newline at end of file diff --git a/web/client/src/sections/memberManagement/components/style.less b/web/client/src/sections/memberManagement/components/style.less new file mode 100644 index 0000000..cf451f1 --- /dev/null +++ b/web/client/src/sections/memberManagement/components/style.less @@ -0,0 +1,6 @@ +.step-footer { + display: flex; + justify-content: flex-end; + margin-top: 20px; + width: 100%; +} \ No newline at end of file diff --git a/web/client/src/sections/memberManagement/components/taskModal.js b/web/client/src/sections/memberManagement/components/taskModal.js new file mode 100644 index 0000000..0278fa8 --- /dev/null +++ b/web/client/src/sections/memberManagement/components/taskModal.js @@ -0,0 +1,26 @@ +import React, { useEffect, useState } from 'react' +import { Tabs, Card, Modal } from 'antd' +import AdapterStep from './adapterStep'; +import { STEP_CONFIG } from './steps/index' +function DataSourceModal(props) { + const { visible, editData, onFinish, onCancel, + type = 'postgre',//当前卡片的key (目前只有postgre,支持后续扩展) + dataSourceFilter, + } = props; + const { StepThree } = STEP_CONFIG[type]; + // const onFinish = () => { } + return <> + { onCancel() }} + open={visible} + footer={null} + width={1200} + destroyOnClose={true} + > + + + +} + +export default DataSourceModal \ No newline at end of file diff --git a/web/client/src/sections/memberManagement/containers/index.js b/web/client/src/sections/memberManagement/containers/index.js new file mode 100644 index 0000000..8b9e540 --- /dev/null +++ b/web/client/src/sections/memberManagement/containers/index.js @@ -0,0 +1,5 @@ +'use strict'; + +import DataSourceManagement from './member'; + +export { DataSourceManagement }; diff --git a/web/client/src/sections/memberManagement/containers/member.js b/web/client/src/sections/memberManagement/containers/member.js new file mode 100644 index 0000000..bbd00a6 --- /dev/null +++ b/web/client/src/sections/memberManagement/containers/member.js @@ -0,0 +1,218 @@ +import React, { useEffect, useState } from 'react' +import { Spin, Popconfirm, Select, Row, Col, Button, Input, Table } from 'antd'; +import { connect } from 'react-redux'; +import ProTable from '@ant-design/pro-table'; +import moment from 'moment'; +import MemberModal from '../components/memberModal'; +import { useFsRequest, ApiTable } from '$utils'; +import './style.less'; +function Member(props) { + const { loading, clientHeight, actions, dispatch, dataSources, adapters } = props; + const [pageSize, setPageSize] = useState(10); + const [currentPage, setCurrentPage] = useState(1); + const [searchValue, setSearchValue] = useState('') + const [visible, setVisible] = useState(false);//是否展示新增编辑模态框 + const [editData, setEditData] = useState(null);//模态框编辑数据 + const [refreshTree, setRefreshTree] = useState(1); + const [searchRole, setSearchRole] = useState() + const queryData = (search) => { + const query = { + limit: search ? 10 : pageSize || 10, + page: search ? 1 : currentPage || 1, + name: searchValue + } + + dispatch(actions.metadataAcquisition.getDataSources(query)); + } + const { data: treeData = [] } = useFsRequest({ + url: ApiTable.getResourceCatalog, + refreshDeps: [refreshTree] + }); + + useEffect(() => { + dispatch(actions.metadataAcquisition.getAdapters()) + queryData(); + }, [pageSize, currentPage]); + + const handleDelete = (id) => { + dispatch(actions.metadataAcquisition.deleteDataSource(id)).then(() => { + queryData(); + }); + }; + + const columns = [ + { + title: '序号', + dataIndex: 'index', + render: (text, record, index) => { return index + 1 } + }, + { + title: '姓名', + dataIndex: 'name', + }, + { + title: '用户名', + dataIndex: 'username', + }, + { + title: '角色', + dataIndex: 'role', + }, + { + title: '状态', + dataIndex: 'enabled', + }, + { + title: '操作', + width: 160, + key: 'option', + valueType: 'option', + render: (text, record) => { + const options = []; + options.push( { + setVisible(true) + setEditData(record) + }} style={{ marginRight: 8 }}>编辑) + options.push( +
是否确认删除该数据源?
+
(将同步删除数据源下的元数据)
} + onConfirm={() => handleDelete(record.id)} + okText="是" + cancelText="否" + > + 删除 +
) + return options; + + }, + }, + ]; + + const onFinish = (values) => { + const { stepOneValues, stepTwoValues, } = values; + const adapterInfo = adapters?.find(x => x.adapterName == stepOneValues?.adapterName) + if (adapterInfo) { + const dataToSave = { + name: stepOneValues?.name, + audited: true, + adapterId: adapterInfo?.id, + mountPath: stepOneValues?.mountPath, + catalogKey: stepOneValues?.catalogKey, + description: stepOneValues?.description, + config: stepTwoValues, + time: moment() + } + if (editData) { + dispatch(actions.metadataAcquisition.modifyDataSource(editData?.id, dataToSave)).then(res => { + if (res.success) { + setVisible(false); + setEditData(null); + queryData(); + } + }) + } else { + dispatch(actions.metadataAcquisition.addDataSource(dataToSave)).then(res => { + if (res.success) { + setVisible(false); + setEditData(null); + queryData(); + } + }) + } + } + } + const refresh = () => { + // queryData(); + setRefreshTree(refreshTree + 1) + } + + return + + + 新建} + title="新建用户" + onFinish={onFinish} + key="addModel" + /> + + + 用户姓名: { setSearchValue(e.target.value) }} + style={{ width: 220, marginRight: 15 }} placeholder="请输入" /> + + 角色: + + + + + { + return {`共${Math.ceil(total / pageSize)}页,${total}项`} + }, + onShowSizeChange: (currentPage, pageSize) => { + setCurrentPage(currentPage); + setPageSize(pageSize); + + }, + onChange: (page, pageSize) => { + setCurrentPage(page); + setPageSize(pageSize); + + } + }} + dataSource={dataSources?.rows || []} + options={false} + /> + + +} + +function mapStateToProps(state) { + const { + auth, global, datasources, adapters + } = state; + return { + loading: datasources.isRequesting || adapters?.isRequesting, + clientHeight: global.clientHeight, + actions: global.actions, + dataSources: datasources?.data || {}, + adapters: adapters?.data || [] + }; +} + +export default connect(mapStateToProps)(Member); + + + diff --git a/web/client/src/sections/memberManagement/containers/style.less b/web/client/src/sections/memberManagement/containers/style.less new file mode 100644 index 0000000..8219c4f --- /dev/null +++ b/web/client/src/sections/memberManagement/containers/style.less @@ -0,0 +1,5 @@ +.protable-title { + margin-bottom: 16px; + padding-left: 24px; + padding-right: 24px; +} \ No newline at end of file diff --git a/web/client/src/sections/memberManagement/index.js b/web/client/src/sections/memberManagement/index.js new file mode 100644 index 0000000..ef51db2 --- /dev/null +++ b/web/client/src/sections/memberManagement/index.js @@ -0,0 +1,15 @@ +'use strict'; + +import reducers from './reducers'; +import routes from './routes'; +import actions from './actions'; +import { getNavItem } from './nav-item'; + +export default { + key: 'memberManagement', + name: '元数据采集', + reducers: reducers, + routes: routes, + actions: actions, + getNavItem: getNavItem +}; \ No newline at end of file diff --git a/web/client/src/sections/memberManagement/nav-item.js b/web/client/src/sections/memberManagement/nav-item.js new file mode 100644 index 0000000..520cbdb --- /dev/null +++ b/web/client/src/sections/memberManagement/nav-item.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Menu } from 'antd'; +import { BarChartOutlined } from '@ant-design/icons'; +const SubMenu = Menu.SubMenu; + +export function getNavItem() { + return ( + } title='用户管理'> + + 用户权限 + + + ); +} \ No newline at end of file diff --git a/web/client/src/sections/memberManagement/reducers/index.js b/web/client/src/sections/memberManagement/reducers/index.js new file mode 100644 index 0000000..7ed1088 --- /dev/null +++ b/web/client/src/sections/memberManagement/reducers/index.js @@ -0,0 +1,5 @@ +'use strict'; + +export default { + +} \ No newline at end of file diff --git a/web/client/src/sections/memberManagement/routes.js b/web/client/src/sections/memberManagement/routes.js new file mode 100644 index 0000000..ac7ff7e --- /dev/null +++ b/web/client/src/sections/memberManagement/routes.js @@ -0,0 +1,17 @@ +'use strict'; +import { Adapter, DataSourceManagement, AcquisitionTask, AcquisitionLog, AdapterDetail } from './containers'; +export default [{ + type: 'inner', + route: { + path: '/memberManagement', + key: 'memberManagement', + breadcrumb: '元数据采集', + // 不设置 component 则面包屑禁止跳转 + childRoutes: [{ + path: '/auth', + key: 'auth', + component: DataSourceManagement, + breadcrumb: '权限管理' + }] + } +}]; \ No newline at end of file