wenlele 2 years ago
parent
commit
78471887f2
  1. 1
      api/.vscode/launch.json
  2. 163
      api/app/lib/controllers/backups/index.js
  3. 13
      api/app/lib/controllers/metadataSearch/index.js
  4. 89
      api/app/lib/models/backups.js
  5. 25
      api/app/lib/routes/backups/index.js
  6. 7
      api/config.js
  7. 25
      scripts/0.0.7/03_create_backups.sql
  8. 4
      web/client/assets/files/backups/1.sql
  9. 2
      web/client/src/app.js
  10. 68
      web/client/src/sections/backups/actions/backups.js
  11. 6
      web/client/src/sections/backups/actions/index.js
  12. 95
      web/client/src/sections/backups/components/backupsModal.js
  13. 6
      web/client/src/sections/backups/components/style.less
  14. 248
      web/client/src/sections/backups/containers/backupTask.js
  15. 5
      web/client/src/sections/backups/containers/index.js
  16. 5
      web/client/src/sections/backups/containers/style.less
  17. 15
      web/client/src/sections/backups/index.js
  18. 17
      web/client/src/sections/backups/nav-item.js
  19. 5
      web/client/src/sections/backups/reducers/index.js
  20. 18
      web/client/src/sections/backups/routes.js
  21. 2
      web/client/src/sections/homePage/nav-item.js
  22. 2
      web/client/src/sections/homePage/routes.js
  23. 69
      web/client/src/sections/memberManagement/actions/task.js
  24. 4
      web/client/src/sections/memberManagement/containers/index.js
  25. 4
      web/client/src/sections/memberManagement/nav-item.js
  26. 4
      web/client/src/sections/memberManagement/routes.js
  27. 71
      web/client/src/sections/resourceRetrieval/containers/retrieval.js
  28. 8
      web/client/src/utils/webapi.js

1
api/.vscode/launch.json

@ -16,6 +16,7 @@
"-p 4400", "-p 4400",
// //
"-g postgres://FashionAdmin:123456@10.8.30.39:5432/GovernmentDataResourceCenter", "-g postgres://FashionAdmin:123456@10.8.30.39:5432/GovernmentDataResourceCenter",
"-b http://10.8.30.160:8085"
] ]
}, },
{ {

163
api/app/lib/controllers/backups/index.js

@ -0,0 +1,163 @@
'use strict';
const request = require('superagent')
const moment = require('moment');
function getBackupsList(opts) {
return async function (ctx, next) {
const models = ctx.fs.dc.models;
const { page, limit, name } = 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.note = { $like: '%' + name + '%' };
}
option.where = searchWhere
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.Backups.findAndCount(option);
res.time = moment()
ctx.status = 200;
ctx.body = res;
} catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = errMsg
}
}
}
// 新增数据备份
function addBackups(opts) {
return async function (ctx, next) {
const { backupsUrl } = opts;
const models = ctx.fs.dc.models;
try {
let rslt = ctx.request.body;
const { database, host, password, port, user } = ctx.request.body.databases
let backup = await models.Backups.create(Object.assign({}, rslt))
//调用后端备份接口
// const url = '10.8.30.160:8085/dumpDB?dbHost=10.8.30.75&dbPort=5432&user=postgres&password=1234&dbName=Anxinyun0916'//测试使用
const url = backupsUrl + `/dumpDB?dbHost=${host}&dbPort=${port}&user=${user}&password=${password}&dbName=${database}`;
request.post(url).then(res => {
const { fileInfo: { name, size }, code } = res.body
models.Backups.update({
size, source: name, state: code == 200 ? '备份成功' : '备份失败', completeTime: moment()
}, { where: { id: backup.id } })
if (code != 200) ctx.fs.logger.error(`path: ${ctx.path}, error: ${message}`);
})
ctx.status = 204;
ctx.body = { message: '新建数据备份成功' }
} catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = { message: '新建数据备份失败' }
}
}
}
// 修改数据备份
function editBackups(opts) {
return async function (ctx, next) {
try {
const models = ctx.fs.dc.models;
const { id } = ctx.params;
const body = ctx.request.body;
await models.Backups.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 deleteBackups(opts) {
return async function (ctx, next) {
try {
const models = ctx.fs.dc.models;
const { id } = ctx.params;
await models.Backups.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: '删除数据备份失败' }
}
}
}
//备份恢复
function restore(opts) {
return async function (ctx, next) {
const { backupsUrl } = opts;
const models = ctx.fs.dc.models;
try {
let rslt = ctx.request.body;
const { id, source, databases: { database, host, password, port, user } } = ctx.request.body
await models.Backups.update({
state: '恢复中',
}, { where: { id: id } })
//调用后端备份接口
const url = backupsUrl + `/restoreDB?dbHost=${host}&dbPort=${port}&user=${user}&password=${password}&dbName=${database}&backFileName=${source}`;
request.post(url).then(res => {
const { code, message } = res.body
models.Backups.update({
state: code == 200 ? '恢复成功' : '恢复失败',
}, { where: { id: id } })
if (code != 200) ctx.fs.logger.error(`path: ${ctx.path}, error: ${message}`);
})
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 = {
getBackupsList,
addBackups,
editBackups,
deleteBackups,
restore
}

13
api/app/lib/controllers/metadataSearch/index.js

@ -74,7 +74,11 @@ function getTableData(opts) {
const models = ctx.fs.dc.models; const models = ctx.fs.dc.models;
try { try {
const { id } = ctx.query; const { id } = ctx.query;
let { user, host, database, password, port } = ctx.request.body; const metaData = await models.MetadataDatabase.findOne({ where: { id: id } })
let rslt = [], metaDataChildren = []
if (metaData && metaData.datasourceConfig) {
metaDataChildren = await models.MetadataDatabase.findAll({ where: { parent: id, type: '字段' } })
let { user, host, database, password, port } = metaData.datasourceConfig;
const pool = new Pool({ const pool = new Pool({
user: user, user: user,
host: host, host: host,
@ -84,12 +88,13 @@ function getTableData(opts) {
}) })
const client = await pool.connect() const client = await pool.connect()
const tableName = "user" const tableName = metaData.name;
const ress = await client.query(`SELECT * from "${tableName}"`, []) const ress = await client.query(`SELECT * from "${tableName}"`, [])
console.log(ress.rows) rslt = ress.rows
}
ctx.status = 200; ctx.status = 200;
ctx.body = { rslt: ress.rows } ctx.body = { rslt: rslt, metaDataChildren: metaDataChildren.map(s => s.code) }
} catch (error) { } catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 200; ctx.status = 200;

89
api/app/lib/models/backups.js

@ -0,0 +1,89 @@
/* eslint-disable*/
'use strict';
module.exports = dc => {
const DataTypes = dc.ORM;
const sequelize = dc.orm;
const Backups = sequelize.define("backups", {
id: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: null,
comment: null,
primaryKey: true,
field: "id",
autoIncrement: true,
unique: "backups_id_uindex"
},
note: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: null,
comment: "备注信息",
primaryKey: false,
field: "note",
autoIncrement: false
},
databases: {
type: DataTypes.JSONB,
allowNull: true,
defaultValue: null,
comment: null,
primaryKey: false,
field: "databases",
autoIncrement: false
},
size: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: null,
primaryKey: false,
field: "size",
autoIncrement: false
},
createTime: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
comment: null,
primaryKey: false,
field: "create_time",
autoIncrement: false
},
completeTime: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
comment: "备份完成时间",
primaryKey: false,
field: "complete_time",
autoIncrement: false
},
state: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: null,
primaryKey: false,
field: "state",
autoIncrement: false
},
source: {
type: DataTypes.TEXT,
allowNull: true,
defaultValue: null,
comment: "备份文件路径",
primaryKey: false,
field: "source",
autoIncrement: false
}
}, {
tableName: "backups",
comment: "",
indexes: []
});
dc.models.Backups = Backups;
return Backups;
};

25
api/app/lib/routes/backups/index.js

@ -0,0 +1,25 @@
'use strict';
const backups = require('../../controllers/backups/index');
module.exports = function (app, router, opts, AuthCode) {
app.fs.api.logAttr['POST/meta/backups'] = { content: '增加数据备份', visible: true };
router.post('/meta/backups', backups.addBackups(opts))
// 修改数据备份信息
app.fs.api.logAttr['PUT/meta/backups/:id'] = { content: '修改数据备份信息', visible: true };
router.put('/meta/backups/:id', backups.editBackups(opts))
// 删除数据备份信息
app.fs.api.logAttr['DEL/meta/backups/:id'] = { content: '删除数据备份信息', visible: true };
router.del('/meta/backups/:id', backups.deleteBackups(opts))
//获取数据备份信息列表
app.fs.api.logAttr['GET/meta/backups'] = { content: '获取数据备份信息列表', visible: true };
router.get('/meta/backups', backups.getBackupsList(opts));
//恢复备份
app.fs.api.logAttr['POST/backups/restore'] = { content: '恢复备份', visible: true };
router.post('/backups/restore', backups.restore(opts))
};

7
api/config.js

@ -10,6 +10,7 @@ const dev = process.env.NODE_ENV == 'development';
// 启动参数 // 启动参数
args.option(['p', 'port'], '启动端口'); args.option(['p', 'port'], '启动端口');
args.option(['g', 'pg'], 'postgre 服务 URL'); args.option(['g', 'pg'], 'postgre 服务 URL');
args.option(['b', 'backups'], '后端数据库备份恢复接口地址');
const flags = args.parse(process.argv); const flags = args.parse(process.argv);
@ -21,7 +22,8 @@ const QINIU_BUCKET_RESOURCE = process.env.ANXINCLOUD_QINIU_BUCKET_RESOURCE || fl
const QINIU_AK = process.env.ANXINCLOUD_QINIU_ACCESSKEY || flags.qnak; const QINIU_AK = process.env.ANXINCLOUD_QINIU_ACCESSKEY || flags.qnak;
const QINIU_SK = process.env.ANXINCLOUD_QINIU_SECRETKEY || flags.qnsk; const QINIU_SK = process.env.ANXINCLOUD_QINIU_SECRETKEY || flags.qnsk;
if (!DB) { const BACKUPS_URL = process.env.BACKUPS_URL || flags.backups;
if (!DB || !BACKUPS_URL) {
console.log('缺少启动参数,异常退出'); console.log('缺少启动参数,异常退出');
args.showHelp(); args.showHelp();
process.exit(-1); process.exit(-1);
@ -65,7 +67,8 @@ const product = {
password: 'Fs2689' password: 'Fs2689'
} }
}, },
pssaRequest: [] pssaRequest: [],
backupsUrl: BACKUPS_URL
} }
} }
], ],

25
scripts/0.0.7/03_create_backups.sql

@ -0,0 +1,25 @@
create table backups
(
id serial
constraint backups_pk
primary key,
note varchar(255) not null,
databases jsonb,
size varchar(255),
create_time timestamp with time zone,
complete_time timestamp with time zone,
state enum_task_state,
source text
);
comment on table backups is '备份任务表';
comment on column backups.note is '备注信息';
comment on column backups.complete_time is '备份完成时间';
comment on column backups.source is '备份文件路径';
create unique index backups_id_uindex
on backups (id);

4
web/client/assets/files/backups/1.sql

@ -0,0 +1,4 @@
alter table t_resource_consumption
add resource_id int;
comment on column t_resource_consumption.resource_id is '元数据id';

2
web/client/src/app.js

@ -11,6 +11,7 @@ import resourceRetrieval from './sections/resourceRetrieval';
import memberManagement from './sections/memberManagement'; import memberManagement from './sections/memberManagement';
import dataQuality from './sections/dataQuality'; import dataQuality from './sections/dataQuality';
import safetySpecification from './sections/safetySpecification'; import safetySpecification from './sections/safetySpecification';
import backups from './sections/backups';
const App = props => { const App = props => {
const { projectName } = props const { projectName } = props
@ -31,6 +32,7 @@ const App = props => {
dataQuality, dataQuality,
safetySpecification, safetySpecification,
memberManagement, memberManagement,
backups
]} ]}
/> />
) )

68
web/client/src/sections/backups/actions/backups.js

@ -0,0 +1,68 @@
'use strict';
import { basicAction } from '@peace/utils'
import { ApiTable } from '$utils'
export function getBackupsList(query) {
return dispatch => basicAction({
type: 'get',
dispatch: dispatch,
query: query || {},
actionType: 'GET_BACKUPS_REPORT',
url: `${ApiTable.getBackupsList}`,
msg: { error: '获取数据备份列表失败' },
reducer: { name: 'backups' }
});
}
export function addBackups(params) {
return (dispatch) => basicAction({
type: 'post',
data: params,
dispatch,
actionType: 'ADD_BACKUPS_REPORT',
url: ApiTable.addBackups,
msg: {
option: '新增数据备份下发',
},
});
}
export function deleteBackups(id) {
return (dispatch) => basicAction({
type: 'del',
dispatch,
actionType: 'DELETE_BACKUPS_REPORT',
url: ApiTable.modifyBackups.replace('{id}', id),
msg: {
option: '数据备份删除',
},
});
}
export function modifyBackups(id, params, msg) {
return (dispatch) => basicAction({
type: 'put',
data: params,
dispatch,
actionType: 'MODIFY_BACKUPS_REPORT',
url: ApiTable.modifyBackups.replace('{id}', id),
msg: {
option: msg || '数据备份编辑',
},
});
}
export function restoreBackups(params) {
return (dispatch) => basicAction({
type: 'post',
data: params,
dispatch,
actionType: 'RESTORE_BACKUPS_REPORT',
url: ApiTable.restoreBackups,
msg: {
option: '备份恢复下发',
},
});
}

6
web/client/src/sections/backups/actions/index.js

@ -0,0 +1,6 @@
'use strict';
import * as backups from './backups';
export default {
...backups
}

95
web/client/src/sections/backups/components/backupsModal.js

@ -0,0 +1,95 @@
import React, { useRef } from 'react';
import { Button, Form } from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
import {
ModalForm,
ProFormTreeSelect,
ProFormText,
} from '@ant-design/pro-form';
import moment from 'moment';
export default (props) => {
const { title, triggerRender, editData = null, onFinish, dataSources } = props;
const formItemLayout = { labelCol: { span: 6 }, wrapperCol: { span: 16 } };
const initialValues = editData ? {
...editData,
} : {};
const [form] = Form.useForm();
const formRef = useRef();
return (
<ModalForm
formRef={formRef}
title={title || ''}
initialValues={initialValues}
trigger={
triggerRender ? triggerRender : <Button type="primary" >
{title || ''}
</Button>
}
layout="horizontal"
grid={true}
{...formItemLayout}
modalProps={{
destroyOnClose: true,
onCancel: () => { },
}}
onFinish={async (values) => {
values.databases = dataSources?.rows?.find(s => s.id == values?.databases?.value)?.config;
values.createTime = moment();
values.state = '备份中';
values.title = title;
return onFinish && await onFinish(values, editData, form)
// return true;
}}
width={500}
>
{title != '恢复数据备份' && <ProFormText
rules={[{ required: true, message: '请输入姓名' },
{ max: 255, message: '姓名长度不能大于255个字符' },
]}
name="note"
label="备份信息"
/>}
<ProFormTreeSelect
name="databases"
label='数据源'
placeholder="请选择数据源"
tooltip={title == '恢复数据备份' ? '恢复前请确保恢复数据源数据库为空数据库' : ''}
allowClear
secondary
request={async () => {
return [
{
title: 'postgre',
disabled: true,
value: '0-0',
children: dataSources?.rows?.filter(s => (title != '恢复数据备份' && s?.type != '备份数据库') || (title == '恢复数据备份' && s?.type == '备份数据库'))?.map(s => {
return {
title: s.name,
value: s.id,
}
})
},
];
}}
rules={[{ required: true, message: '请选择数据源' }]}
// tree-select args
fieldProps={{
showArrow: false,
filterTreeNode: true,
showSearch: true,
popupMatchSelectWidth: false,
labelInValue: true,
autoClearSearchValue: true,
multiple: false,
treeDefaultExpandAll: true,
treeNodeFilterProp: 'title',
fieldNames: {
label: 'title',
},
}}
/>
</ModalForm>
);
};

6
web/client/src/sections/backups/components/style.less

@ -0,0 +1,6 @@
.step-footer {
display: flex;
justify-content: flex-end;
margin-top: 20px;
width: 100%;
}

248
web/client/src/sections/backups/containers/backupTask.js

@ -0,0 +1,248 @@
import React, { useEffect, useState, useMemo } 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 BackupsModal from '../components/backupsModal';
import './style.less';
import { ApiTable, useFsRequest } from '$utils';
function Member(props) {
const { loading, clientHeight, actions, dispatch, backups, backupsIsRequesting, dataSources } = props;
const [pageSize, setPageSize] = useState(10);
const [currentPage, setCurrentPage] = useState(1);
const [searchValue, setSearchValue] = useState('');
const [addLoading, setAddLoading] = useState(false)
const [autoSearchValue, setAutoSearchValue] = useState('');
const queryData = (search) => {
const query = {
limit: search ? 10 : pageSize || 10,
page: search ? 1 : currentPage || 1,
name: searchValue,
}
dispatch(actions.backups.getBackupsList(query));
}
const { data: tableData = {} } = useFsRequest({
url: ApiTable.getBackupsList,
query: {
limit: pageSize || 10,
page: currentPage || 1,
name: autoSearchValue,
},
ready: !backupsIsRequesting,
refreshDeps: [pageSize, currentPage, autoSearchValue],
pollingInterval: 1000
});
useEffect(() => {
dispatch(actions.metadataAcquisition.getDataSources());
}, [])
useEffect(() => {
queryData();
}, [pageSize, currentPage]);
const columns = [
{
title: '序号',
dataIndex: 'index',
render: (text, record, index) => { return index + 1 }
},
{
title: '备份信息',
dataIndex: 'note',
},
{
title: '备份大小',
dataIndex: 'size',
},
{
title: '备份时间',
dataIndex: 'createTime',
render: (text, record) => { return moment(record?.createTime).format('YYYY-MM-DD HH:mm:ss') }
},
{
title: '备份完成时间',
dataIndex: 'completeTime',
render: (text, record) => { return record?.completeTime ? moment(record?.completeTime).format('YYYY-MM-DD HH:mm:ss') : '-' }
},
{
title: '状态',
dataIndex: 'state',
},
{
title: '操作',
width: 160,
key: 'option',
valueType: 'option',
render: (text, record) => {
const options = [];
options.push(
(record?.state != '恢复中' && record?.source) ? <BackupsModal
editData={record}
dataSources={dataSources}
triggerRender={<a type='primary'>恢复</a>}
title="恢复数据备份"
onFinish={onFinish}
key="addModel"
/> : <a style={{ color: 'gray' }}></a>)
options.push(
record?.source ?
<a a onClick={() => { window.open(record?.source) }}> 下载</a> : <a style={{ color: 'gray' }}></a>
)
if (record?.state != '备份中' && record?.state != '恢复中') {
options.push(
<Popconfirm
key="del"
placement="top"
title={<><div>是否确认删除该数据备份</div>
</>}
onConfirm={() => handleDelete(record.id)}
okText="是"
cancelText="否"
>
<a>删除</a>
</Popconfirm>)
}
return options;
},
},
];
const handleDelete = (id) => {
dispatch(actions.backups.deleteBackups(id)).then(() => {
queryData();
});
};
const onFinish = async (values, editData) => {
if (values?.title == '恢复数据备份') {
return dispatch(actions.backups.restoreBackups({
id: editData.id,
source: editData.source,
databases: values.databases
})).then(res => {
setAddLoading(false)
if (res.success) {
queryData();
return true;
} else {
return false;
}
});
} else {
setAddLoading(true)
return dispatch(actions.backups.addBackups({
...values,
})).then(res => {
setAddLoading(false)
if (res.success) {
queryData();
return true;
} else {
return false;
}
});
}
};
const tableDataFilter = useMemo(() => {
if (tableData?.count && backups?.count) {
return tableData.time > backups.time ? tableData : backups;
} else {
return backups;
}
}, [tableData, backups])
return <Spin spinning={loading || addLoading}>
<Row className='protable-title'>
<Col span={12}>
<BackupsModal
dataSources={dataSources}
triggerRender={<Button type='primary'>新建</Button>}
title="新建数据备份"
onFinish={onFinish}
key="addModel"
/>
</Col>
<Col span={12} style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}>
<span>备份信息 </span> <Input
value={searchValue} onChange={e => { setSearchValue(e.target.value) }}
style={{ width: 220, marginRight: 15 }} placeholder="请输入" />
<Button onClick={() => {
setAutoSearchValue(searchValue)
setCurrentPage(1)
setPageSize(10)
queryData(true)
}} type='primary'>查询</Button>
</Col>
</Row>
<ProTable
columns={columns}
dateFormatter="string"
search={false}
scroll={
{
scrollToFirstRowOnChange: true,
y: clientHeight - 260
}
}
pagination={{
size: 'large',
total: tableDataFilter?.count,
showSizeChanger: true,
showQuickJumper: true,
current: currentPage,
pageSize: pageSize || 10,
defaultPageSize: 10,
pageSizeOptions: [10, 20, 50],
showTotal: (total) => {
return <span style={{ fontSize: 15 }}>{`${Math.ceil(total / pageSize)}页,${total}`}</span>
},
onShowSizeChange: (currentPage, pageSize) => {
setCurrentPage(currentPage);
setPageSize(pageSize);
},
onChange: (page, pageSize) => {
setCurrentPage(page);
setPageSize(pageSize);
}
}}
dataSource={tableDataFilter?.rows || []}
options={false}
/>
</Spin>
}
function mapStateToProps(state) {
const {
auth, global, datasources, backups
} = state;
return {
loading: backups.isRequesting || datasources.isRequesting,
clientHeight: global.clientHeight,
actions: global.actions,
backups: backups?.data || {},
user: auth.user,
dataSources: datasources?.data || {},
backupsIsRequesting: backups.isRequesting
};
}
export default connect(mapStateToProps)(Member);

5
web/client/src/sections/backups/containers/index.js

@ -0,0 +1,5 @@
'use strict';
import Restore from './backupTask';
export { Restore };

5
web/client/src/sections/backups/containers/style.less

@ -0,0 +1,5 @@
.protable-title {
margin-bottom: 16px;
padding-left: 24px;
padding-right: 24px;
}

15
web/client/src/sections/backups/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: 'backups',
name: '数据备份恢复',
reducers: reducers,
routes: routes,
actions: actions,
getNavItem: getNavItem
};

17
web/client/src/sections/backups/nav-item.js

@ -0,0 +1,17 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Menu } from 'antd';
import { ControlOutlined } from '@ant-design/icons';
const SubMenu = Menu.SubMenu;
export function getNavItem(user) {
return (
user?.role == '系统管理员' && <SubMenu key="数据备份恢复" icon={<ControlOutlined />} title='数据备份恢复'>
<Menu.Item key="backups">
<Link to="/backups/restore">备份恢复</Link>
</Menu.Item>
</ SubMenu >
)
}

5
web/client/src/sections/backups/reducers/index.js

@ -0,0 +1,5 @@
'use strict';
export default {
}

18
web/client/src/sections/backups/routes.js

@ -0,0 +1,18 @@
'use strict';
import { Restore } from './containers';
export default [{
type: 'inner',
route: {
path: '/backups',
key: 'backups',
breadcrumb: '数据备份恢复',
// 不设置 component 则面包屑禁止跳转
childRoutes: [{
path: '/restore',
key: 'restore',
component: Restore,
breadcrumb: '备份恢复'
}]
}
}];

2
web/client/src/sections/homePage/nav-item.js

@ -5,7 +5,7 @@ import { HomeOutlined } from '@ant-design/icons';
export function getNavItem() { export function getNavItem() {
return ( return (
<Menu.Item key="homePage" icon={<HomeOutlined />}> <Menu.Item key="homePage" icon={<HomeOutlined />}>
<Link to="/homePage">首页</Link> <Link to="/homePage">数据监控平台</Link>
</Menu.Item> </Menu.Item>
); );
} }

2
web/client/src/sections/homePage/routes.js

@ -6,7 +6,7 @@ export default [{
route: { route: {
path: '/homePage', path: '/homePage',
key: 'homePage', key: 'homePage',
breadcrumb: '首页', breadcrumb: '数据监控平台',
// 不设置 component 则面包屑禁止跳转 // 不设置 component 则面包屑禁止跳转
component: homePage component: homePage
} }

69
web/client/src/sections/memberManagement/actions/task.js

@ -1,69 +0,0 @@
'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 || '任务执行',
},
});
}

4
web/client/src/sections/memberManagement/containers/index.js

@ -1,5 +1,5 @@
'use strict'; 'use strict';
import DataSourceManagement from './member'; import MemberManagement from './member';
export { DataSourceManagement }; export { MemberManagement };

4
web/client/src/sections/memberManagement/nav-item.js

@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Menu } from 'antd'; import { Menu } from 'antd';
import { BarChartOutlined } from '@ant-design/icons'; import { UserOutlined } from '@ant-design/icons';
const SubMenu = Menu.SubMenu; const SubMenu = Menu.SubMenu;
export function getNavItem(user) { export function getNavItem(user) {
return ( return (
user?.role == '系统管理员' && <SubMenu key="memberManagement" icon={<BarChartOutlined />} title='用户管理'> user?.role == '系统管理员' && <SubMenu key="memberManagement" icon={<UserOutlined />} title='用户管理'>
<Menu.Item key="auth"> <Menu.Item key="auth">
<Link to="/memberManagement/auth">用户权限</Link> <Link to="/memberManagement/auth">用户权限</Link>
</Menu.Item> </Menu.Item>

4
web/client/src/sections/memberManagement/routes.js

@ -1,5 +1,5 @@
'use strict'; 'use strict';
import { Adapter, DataSourceManagement, AcquisitionTask, AcquisitionLog, AdapterDetail } from './containers'; import { MemberManagement } from './containers';
export default [{ export default [{
type: 'inner', type: 'inner',
route: { route: {
@ -10,7 +10,7 @@ export default [{
childRoutes: [{ childRoutes: [{
path: '/auth', path: '/auth',
key: 'auth', key: 'auth',
component: DataSourceManagement, component: MemberManagement,
breadcrumb: '权限管理' breadcrumb: '权限管理'
}] }]
} }

71
web/client/src/sections/resourceRetrieval/containers/retrieval.js

@ -5,6 +5,7 @@ import { useFsRequest, ApiTable } from '$utils';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { downloadImg, markRedKeywords } from '../utils/index' import { downloadImg, markRedKeywords } from '../utils/index'
import KeyModal from '../components/keyModal'; import KeyModal from '../components/keyModal';
import xlsx from 'xlsx';
const METADTA_TYPE = { const METADTA_TYPE = {
'库': 'databases', '库': 'databases',
'表': 'databases', '表': 'databases',
@ -26,6 +27,8 @@ function Retrieval(props) {
const [firstInput, setFirstInput] = useState() const [firstInput, setFirstInput] = useState()
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [currentData, setCurrentData] = useState(null) const [currentData, setCurrentData] = useState(null)
const [searchDataId, setSearchDataId] = useState(null)
const formRef = React.createRef(); const formRef = React.createRef();
// const { data: catalogs = [] } = useFsRequest({ // const { data: catalogs = [] } = useFsRequest({
// url: ApiTable.getResourceCatalog, // url: ApiTable.getResourceCatalog,
@ -54,6 +57,58 @@ function Retrieval(props) {
ready: !!(user?.id) ready: !!(user?.id)
}); });
//检索元数据
const { data: tableData = {} } = useFsRequest({
url: 'meta/table/data',
query: {
id: searchDataId
},
refreshDeps: [searchDataId],
ready: !!searchDataId,
});
useEffect(() => {
if (tableData?.rslt && tableData?.metaDataChildren) handleTableExport()
}, [tableData])
const handleTableExport = () => {
const metaData = result?.rows?.find(s => s.id == searchDataId)
if (!metaData) return;
let excelTitle = tableData?.metaDataChildren.map(s => {
return { k: s, v: s }
});
let workBook = {
SheetNames: [], //sheet名称
Sheets: {} //根据SheetNames名称顺序依次添加每个sheet数据
};
if (tableData?.rslt && tableData?.rslt?.length > 0) {
let sheetName = '元数据列表';
let sheetDataMap = new Map();
let sheetData = [excelTitle];
let index = 1;
tableData?.rslt.map(data => {
const arr = []
tableData?.metaDataChildren.map(s => {
arr.push(
{ k: s, v: JSON.stringify(data[s]) },
);
});
sheetData.push(arr)
index = index + 1;
})
sheetDataMap.set(sheetName, sheetData);
sheetDataMap.forEach((values, key) => {
// 写入excel
workBook.Sheets[key] = xlsx.utils.aoa_to_sheet(values);
workBook.Sheets[key]['!cols'] = [{ wpx: 50 }, { wpx: 150 }, { wpx: 180 }, { wpx: 230 }, { wpx: 230 }, { wpx: 230 }]
})
workBook.SheetNames = [sheetName];
}
// 将workBook写入文件
xlsx.writeFile(workBook, `${metaData?.name}-data.xlsx`);
}
const renderIcon = (type) => { const renderIcon = (type) => {
switch (type) { switch (type) {
case '库': case '库':
@ -106,7 +161,13 @@ function Retrieval(props) {
window.open('/assets/files/common/' + s.fileName) window.open('/assets/files/common/' + s.fileName)
} }
} else { } else {
alert('库表下载待开发') if (s.datasourceConfig) {
setSearchDataId(s.id)
if (tableData?.rslt && tableData?.metaDataChildren) handleTableExport()
} else {
message.warning('数据源信息读取失败,无法下载元数据!')
}
} }
} }
@ -120,7 +181,11 @@ function Retrieval(props) {
</div> : </div> :
<> <>
<Input.Search defaultValue={keywords} style={{ width: '30%', marginLeft: 10, marginBottom: 30 }} <Input.Search defaultValue={keywords} style={{ width: '30%', marginLeft: 10, marginBottom: 30 }}
onSearch={value => { setKeywords(value) }}
onSearch={value => {
setPage(1)
setKeywords(value)
}}
/> />
{result?.rows?.slice((page - 1) * 10, (page - 1) * 10 + 10).map(s => { {result?.rows?.slice((page - 1) * 10, (page - 1) * 10 + 10).map(s => {
const catalogText = renderCatalog(s?.catalog).split('/').reverse().toString().replaceAll(',', '/') const catalogText = renderCatalog(s?.catalog).split('/').reverse().toString().replaceAll(',', '/')
@ -151,9 +216,11 @@ function Retrieval(props) {
</div> </div>
})} })}
{result?.rows?.length > 0 && <Pagination {result?.rows?.length > 0 && <Pagination
key={keywords}
defaultCurrent={1} total={result?.rows?.length} defaultCurrent={1} total={result?.rows?.length}
style={{ float: 'right', paddingRight: '5%' }} style={{ float: 'right', paddingRight: '5%' }}
showTotal={total => `${total} 条数据`} showTotal={total => `${total} 条数据`}
showSizeChanger={false}
onChange={(page, pageSize) => { onChange={(page, pageSize) => {
setPage(page) setPage(page)
}} }}

8
web/client/src/utils/webapi.js

@ -96,6 +96,14 @@ export const ApiTable = {
//数据安全规范上传 //数据安全规范上传
specifications:'data-security/specifications', specifications:'data-security/specifications',
delSpecifications:'data-security/specifications/{fileIds}', delSpecifications:'data-security/specifications/{fileIds}',
//元数据检索
searchMetadata: "meta/data/search",
//备份恢复
getBackupsList: 'meta/backups',
addBackups: 'meta/backups',
modifyBackups: 'meta/backups/{id}',
restoreBackups:'backups/restore'
}; };
export const RouteTable = { export const RouteTable = {

Loading…
Cancel
Save