Browse Source

(*)数据备份功能完善

master
peng.peng 2 years ago
parent
commit
1aa4564f98
  1. 41
      api/app/lib/controllers/backups/index.js
  2. 4
      api/app/lib/routes/backups/index.js
  3. 12
      web/client/src/sections/backups/actions/backups.js
  4. 7
      web/client/src/sections/backups/components/backupsModal.js
  5. 26
      web/client/src/sections/backups/components/taskModal.js
  6. 103
      web/client/src/sections/backups/containers/backupTask.js
  7. 2
      web/client/src/sections/homePage/nav-item.js
  8. 2
      web/client/src/sections/homePage/routes.js
  9. 2
      web/client/src/sections/resourceRetrieval/containers/retrieval.js

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

@ -33,6 +33,7 @@ function getBackupsList(opts) {
} }
const res = await models.Backups.findAndCount(option); const res = await models.Backups.findAndCount(option);
res.time = moment()
ctx.status = 200; ctx.status = 200;
ctx.body = res; ctx.body = res;
} catch (error) { } catch (error) {
@ -56,11 +57,13 @@ function addBackups(opts) {
//调用后端备份接口 //调用后端备份接口
// const url = '10.8.30.160:8085/dumpDB?dbHost=10.8.30.75&dbPort=5432&user=postgres&password=1234&dbName=Anxinyun0916'//测试使用 // 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}`; const url = backupsUrl + `/dumpDB?dbHost=${host}&dbPort=${port}&user=${user}&password=${password}&dbName=${database}`;
const res = await request.post(url) request.post(url).then(res => {
const { fileInfo: { name, size }, message } = res.body const { fileInfo: { name, size }, message } = res.body
await models.Backups.update({ models.Backups.update({
size, source: name, state: message, completeTime: moment() size, source: name, state: message, completeTime: moment()
}, { where: { id: backup.id } }) }, { where: { id: backup.id } })
})
ctx.status = 204; ctx.status = 204;
ctx.body = { message: '新建数据备份成功' } ctx.body = { message: '新建数据备份成功' }
} catch (error) { } catch (error) {
@ -119,12 +122,38 @@ function deleteBackups(opts) {
} }
} }
// 新增数据备份
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
//调用后端备份接口
const url = backupsUrl + `/restoreDB?dbHost=${host}&dbPort=${port}&user=${user}&password=${password}&dbName=${database}&backFileName=${source}`;
request.post(url).then(res => {
const { fileInfo: { name, size }, message } = res.body
models.Backups.update({
size, source: name, state: message, completeTime: moment()
}, { 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 = { module.exports = {
getBackupsList, getBackupsList,
addBackups, addBackups,
editBackups, editBackups,
deleteBackups, deleteBackups,
restore
} }

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

@ -19,5 +19,7 @@ module.exports = function (app, router, opts, AuthCode) {
app.fs.api.logAttr['GET/meta/backups'] = { content: '获取数据备份信息列表', visible: true }; app.fs.api.logAttr['GET/meta/backups'] = { content: '获取数据备份信息列表', visible: true };
router.get('/meta/backups', backups.getBackupsList(opts)); router.get('/meta/backups', backups.getBackupsList(opts));
//恢复备份
app.fs.api.logAttr['POST/backups/restore'] = { content: '恢复备份', visible: true };
router.post('/backups/restore', backups.restore(opts))
}; };

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

@ -54,3 +54,15 @@ export function modifyBackups(id, params, msg) {
}); });
} }
export function restoreBackups(params) {
return (dispatch) => basicAction({
type: 'post',
data: params,
dispatch,
actionType: 'RESTORE_BACKUPS_REPORT',
url: ApiTable.restoreBackups,
msg: {
option: '备份恢复',
},
});
}

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

@ -36,18 +36,19 @@ export default (props) => {
values.databases = dataSources?.rows?.find(s => s.id == values?.databases?.value)?.config; values.databases = dataSources?.rows?.find(s => s.id == values?.databases?.value)?.config;
values.createTime = moment(); values.createTime = moment();
values.state = '备份中'; values.state = '备份中';
values.title = title;
return onFinish && await onFinish(values, editData, form) return onFinish && await onFinish(values, editData, form)
// return true; // return true;
}} }}
width={500} width={500}
> >
<ProFormText {title != '恢复数据备份' && <ProFormText
rules={[{ required: true, message: '请输入姓名' }, rules={[{ required: true, message: '请输入姓名' },
{ max: 255, message: '姓名长度不能大于255个字符' }, { max: 255, message: '姓名长度不能大于255个字符' },
]} ]}
name="note" name="note"
label="备份信息" label="备份信息"
/> />}
<ProFormTreeSelect <ProFormTreeSelect
name="databases" name="databases"
@ -61,7 +62,7 @@ export default (props) => {
title: 'postgre', title: 'postgre',
disabled: true, disabled: true,
value: '0-0', value: '0-0',
children: dataSources?.rows?.filter(s => s?.type != '备份数据库')?.map(s => { children: dataSources?.rows?.filter(s => (title != '恢复数据备份' && s?.type != '备份数据库') || (title == '恢复数据备份' && s?.type == '备份数据库'))?.map(s => {
return { return {
title: s.name, title: s.name,
value: s.id, value: s.id,

26
web/client/src/sections/backups/components/taskModal.js

@ -1,26 +0,0 @@
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 <>
<Modal
title={editData ? '编辑采集任务' : "新建采集任务"}
onCancel={() => { onCancel() }}
open={visible}
footer={null}
width={1200}
destroyOnClose={true}
>
<StepThree next={onFinish} dataSourceFilter={dataSourceFilter} editData={editData} />
</Modal>
</>
}
export default DataSourceModal

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

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { Spin, Popconfirm, Select, Row, Col, Button, Input, Table } from 'antd'; import { Spin, Popconfirm, Select, Row, Col, Button, Input, Table } from 'antd';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ProTable from '@ant-design/pro-table'; import ProTable from '@ant-design/pro-table';
@ -6,12 +6,15 @@ import moment from 'moment';
import BackupsModal from '../components/backupsModal'; import BackupsModal from '../components/backupsModal';
import './style.less'; import './style.less';
import { ApiTable, useFsRequest } from '$utils';
function Member(props) { function Member(props) {
const { loading, clientHeight, actions, dispatch, backups, user, dataSources } = props; const { loading, clientHeight, actions, dispatch, backups, backupsIsRequesting, dataSources } = props;
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [addLoading, setAddLoading] = useState(false) const [addLoading, setAddLoading] = useState(false)
const [autoSearchValue, setAutoSearchValue] = useState('');
const queryData = (search) => { const queryData = (search) => {
const query = { const query = {
limit: search ? 10 : pageSize || 10, limit: search ? 10 : pageSize || 10,
@ -22,6 +25,18 @@ function Member(props) {
dispatch(actions.backups.getBackupsList(query)); 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(() => { useEffect(() => {
dispatch(actions.metadataAcquisition.getDataSources()); dispatch(actions.metadataAcquisition.getDataSources());
}, []) }, [])
@ -66,20 +81,17 @@ function Member(props) {
render: (text, record) => { render: (text, record) => {
const options = []; const options = [];
options.push( options.push(
<Popconfirm record?.source ? <BackupsModal
key="del" dataSources={dataSources}
placement="top" triggerRender={<a type='primary'>恢复</a>}
title={<><div>是否确认重置该数据备份密码</div> title="恢复数据备份"
</>} onFinish={onFinish}
onConfirm={() => { key="addModel"
dispatch(actions.backups.modifyBackups(record.id, { password: 'e10adc3949ba59abbe56e057f20f883e' }, '重置密码')) /> : <a style={{ color: 'gray' }}></a>)
}} options.push(
okText="是" record?.source ?
cancelText="否" <a a onClick={() => { window.open(record?.source) }}> 下载</a> : <a style={{ color: 'gray' }}></a>
> )
<a>恢复</a>
</Popconfirm>)
options.push(<a onClick={() => { window.open('/assets/files/backups/1.sql') }}>下载</a>)
options.push( options.push(
<Popconfirm <Popconfirm
@ -106,20 +118,46 @@ function Member(props) {
}; };
const onFinish = async (values, editData) => { const onFinish = async (values, editData) => {
setAddLoading(true) if (values?.title == '恢复数据备份') {
return dispatch(actions.backups.addBackups({ return dispatch(actions.backups.restoreBackups({
...values, id: editData.id,
})).then(res => { source: editData.source,
setAddLoading(false) databases: values.databases
if (res.success) { })).then(res => {
queryData(); setAddLoading(false)
return true; if (res.success) {
} else { queryData();
return false; 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}> return <Spin spinning={loading || addLoading}>
<Row className='protable-title'> <Row className='protable-title'>
<Col span={12}> <Col span={12}>
@ -137,10 +175,12 @@ function Member(props) {
style={{ width: 220, marginRight: 15 }} placeholder="请输入" /> style={{ width: 220, marginRight: 15 }} placeholder="请输入" />
<Button onClick={() => { <Button onClick={() => {
setAutoSearchValue(searchValue)
setCurrentPage(1) setCurrentPage(1)
setPageSize(10) setPageSize(10)
queryData(true) queryData(true)
}} type='primary'>查询</Button></Col> }} type='primary'>查询</Button>
</Col>
</Row> </Row>
<ProTable <ProTable
@ -155,7 +195,7 @@ function Member(props) {
} }
pagination={{ pagination={{
size: 'large', size: 'large',
total: backups?.count, total: tableDataFilter?.count,
showSizeChanger: true, showSizeChanger: true,
showQuickJumper: true, showQuickJumper: true,
current: currentPage, current: currentPage,
@ -176,7 +216,7 @@ function Member(props) {
} }
}} }}
dataSource={backups?.rows || []} dataSource={tableDataFilter?.rows || []}
options={false} options={false}
/> />
</Spin> </Spin>
@ -194,6 +234,7 @@ function mapStateToProps(state) {
backups: backups?.data || {}, backups: backups?.data || {},
user: auth.user, user: auth.user,
dataSources: datasources?.data || {}, dataSources: datasources?.data || {},
backupsIsRequesting: backups.isRequesting
}; };
} }

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

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

@ -64,7 +64,7 @@ function Retrieval(props) {
id: searchDataId id: searchDataId
}, },
refreshDeps: [searchDataId], refreshDeps: [searchDataId],
ready: !!searchDataId ready: !!searchDataId,
}); });
useEffect(() => { useEffect(() => {

Loading…
Cancel
Save