diff --git a/src/api-engine/api/routes/node/views.py b/src/api-engine/api/routes/node/views.py index be95e77fb..fa3bf4e3f 100644 --- a/src/api-engine/api/routes/node/views.py +++ b/src/api-engine/api/routes/node/views.py @@ -6,6 +6,7 @@ import shutil import os import threading +import yaml from django.core.exceptions import ObjectDoesNotExist from django.core.paginator import Paginator @@ -746,6 +747,10 @@ def node_config(self, request, pk=None): # Update yaml, zip files, and the database field try: new_config_file = request.data['file'] + try: + yaml.safe_load(new_config_file) + except yaml.YAMLError: + return Response(err("Unable to parse this YAML file."), status=status.HTTP_400_BAD_REQUEST) if os.path.exists("{}{}".format(dir_node, name)): os.remove("{}{}".format(dir_node, name)) with open("{}{}".format(dir_node, name), 'wb+') as f: diff --git a/src/dashboard/src/locales/en-US/form.js b/src/dashboard/src/locales/en-US/form.js index 6a4fdbb92..f373d0017 100755 --- a/src/dashboard/src/locales/en-US/form.js +++ b/src/dashboard/src/locales/en-US/form.js @@ -5,6 +5,8 @@ export default { 'form.button.new': 'New', 'form.table.header.operation': 'Operation', 'form.menu.item.download': 'Download', + 'form.menu.item.upload': 'Upload', + 'form.menu.item.joinChannel': 'Join Channel', 'form.menu.item.delete': 'Delete', 'form.input.placeholder': 'Please input', 'form.menu.item.update': 'Update', diff --git a/src/dashboard/src/locales/en-US/operatorNode.js b/src/dashboard/src/locales/en-US/operatorNode.js index 69618b369..aefeb6da3 100644 --- a/src/dashboard/src/locales/en-US/operatorNode.js +++ b/src/dashboard/src/locales/en-US/operatorNode.js @@ -23,6 +23,8 @@ export default { 'Deleting node {name} may cause abnormality in the blockchain network. Confirm delete?', 'app.operator.node.delete.success': 'Delete Node Successful.', 'app.operator.node.download.success': 'Download Node Config File Successful.', + 'app.operator.node.upload.success': 'Upload Node Config File Successful.', + 'app.operator.node.joinChannel.success': 'Join Channel Successful', 'app.operator.node.operation.start.success': 'Start Node Successful.', 'app.operator.node.operation.stop.success': 'Stop Node Successful.', 'app.operator.node.operation.restart.success': 'Restart Node Successful.', diff --git a/src/dashboard/src/locales/zh-CN/form.js b/src/dashboard/src/locales/zh-CN/form.js index fd2c0ff2a..e18bae8c7 100755 --- a/src/dashboard/src/locales/zh-CN/form.js +++ b/src/dashboard/src/locales/zh-CN/form.js @@ -5,6 +5,8 @@ export default { 'form.button.new': '新建', 'form.table.header.operation': '操作', 'form.menu.item.download': '下载', + 'form.menu.item.upload': '上传', + 'form.menu.item.joinChannel': '加入通道', 'form.menu.item.delete': '删除', 'form.input.placeholder': '请输入', 'form.menu.item.update': '更新', diff --git a/src/dashboard/src/locales/zh-CN/operatorNode.js b/src/dashboard/src/locales/zh-CN/operatorNode.js index de721a360..f4f515314 100644 --- a/src/dashboard/src/locales/zh-CN/operatorNode.js +++ b/src/dashboard/src/locales/zh-CN/operatorNode.js @@ -22,6 +22,8 @@ export default { 'app.operator.node.delete.confirm': '删除节点 {name} 可能导致区块链网络异常,是否确认删除?', 'app.operator.node.delete.success': '删除节点成功。', 'app.operator.node.download.success': '下载节点配置文件成功。', + 'app.operator.node.upload.success': '上传节点配置文件成功。', + 'app.operator.node.joinChannel.success': '加入通道成功。', 'app.operator.node.operation.start.success': '启动节点成功。', 'app.operator.node.operation.stop.success': '停止节点成功。', 'app.operator.node.operation.restart.success': '重启节点成功。', diff --git a/src/dashboard/src/models/node.js b/src/dashboard/src/models/node.js index e7d90ac25..9da177747 100644 --- a/src/dashboard/src/models/node.js +++ b/src/dashboard/src/models/node.js @@ -6,6 +6,8 @@ import { operateNode, createNode, downloadNodeConfig, + uploadNodeConfig, + nodeJoinChannel, } from '@/services/node'; export default { @@ -97,6 +99,24 @@ export default { callback(response); } }, + *uploadNodeConfig({ payload, callback }, { call }) { + const response = yield call(uploadNodeConfig, payload); + if (callback) { + callback({ + payload, + ...response, + }); + } + }, + *nodeJoinChannel({ payload, callback }, { call }) { + const response = yield call(nodeJoinChannel, payload); + if (callback) { + callback({ + payload, + ...response, + }); + } + }, }, reducers: { save(state, { payload }) { diff --git a/src/dashboard/src/pages/Operator/Node/index.js b/src/dashboard/src/pages/Operator/Node/index.js index 6ab0d0be4..098988e30 100644 --- a/src/dashboard/src/pages/Operator/Node/index.js +++ b/src/dashboard/src/pages/Operator/Node/index.js @@ -16,6 +16,7 @@ import { Select, InputNumber, Badge, + Upload, } from 'antd'; import { DownOutlined, PlusOutlined } from '@ant-design/icons'; import moment from 'moment'; @@ -540,6 +541,52 @@ class Index extends PureComponent { URL.revokeObjectURL(link.href); }; + handleUploadConfig = (row, formData) => { + const { dispatch } = this.props; + const params = { + id: row.id, + form: formData, + }; + dispatch({ + type: 'node/uploadNodeConfig', + payload: params, + callback: this.uploadCallBack, + }); + }; + + uploadCallBack = () => { + const { intl } = this.props; + message.success( + intl.formatMessage({ + id: 'app.operator.node.upload.success', + defaultMessage: 'Upload config file succeed', + }) + ); + }; + + handleJoinChannel = (row, formData) => { + const { dispatch } = this.props; + const params = { + id: row.id, + form: formData, + }; + dispatch({ + type: 'node/nodeJoinChannel', + payload: params, + callback: this.joinCallBack, + }); + }; + + joinCallBack = () => { + const { intl } = this.props; + message.success( + intl.formatMessage({ + id: 'app.operator.node.joinChannel.success', + defaultMessage: 'Join Channel succeed', + }) + ); + }; + render() { const { selectedRows, registerUserFormVisible, targetNodeId, createModalVisible } = this.state; @@ -593,6 +640,13 @@ class Index extends PureComponent { return statusOfBadge; } + // prevent the default http upload request in antd + const dummyRequest = ({ onSuccess }) => { + setTimeout(() => { + onSuccess('ok'); + }, 0); + }; + const menu = record => ( {record.type === 'ca' && ( @@ -612,6 +666,59 @@ class Index extends PureComponent { )} + {(record.type === 'peer' || record.type === 'orderer') && ( + + { + if (info.file.name.split('.').pop() !== 'yaml') { + message.error('Only accept yaml file.'); + return; + } + if (info.file.status === 'done') { + const formData = new FormData(); + formData.append('file', info.fileList[0].originFileObj); + this.handleUploadConfig(record, formData); + } + }, + }} + > + + {intl.formatMessage({ id: 'form.menu.item.upload', defaultMessage: 'Upload' })} + + + + )} + {record.type === 'peer' && ( + + { + if (info.file.name.split('.').pop() !== 'block') { + message.error('Only accept block file.'); + return; + } + if (info.file.status === 'done') { + const formData = new FormData(); + formData.append('file', info.fileList[0].originFileObj); + this.handleJoinChannel(record, formData); + } + }, + }} + > + + {intl.formatMessage({ + id: 'form.menu.item.joinChannel', + defaultMessage: 'Join Channel', + })} + + + + )} this.handleDeleteNode(record)}> {intl.formatMessage({ id: 'form.menu.item.delete', defaultMessage: 'Delete' })} diff --git a/src/dashboard/src/services/node.js b/src/dashboard/src/services/node.js index f0562295f..9db50481e 100644 --- a/src/dashboard/src/services/node.js +++ b/src/dashboard/src/services/node.js @@ -47,3 +47,17 @@ export async function downloadNodeConfig(params) { getResponse: true, }); } + +export async function uploadNodeConfig(params) { + return request(`/api/v1/nodes/${params.id}/config`, { + method: 'POST', + body: params.form, + }); +} + +export async function nodeJoinChannel(params) { + return request(`/api/v1/nodes/${params.id}/block`, { + method: 'POST', + body: params.form, + }); +}