diff --git a/src/api-engine/chaincode/serializers.py b/src/api-engine/chaincode/serializers.py index 8e1667b30..6b4a725df 100644 --- a/src/api-engine/chaincode/serializers.py +++ b/src/api-engine/chaincode/serializers.py @@ -7,7 +7,7 @@ from chaincode.models import Chaincode from chaincode.service import ChaincodeAction, create_chaincode, get_chaincode, install_chaincode, \ approve_chaincode, commit_chaincode, send_chaincode_request, metadata_exists, get_chaincode_status, \ - get_chaincode_commit_readiness + get_chaincode_commit_readiness, ChaincodeTransactionError from channel.models import Channel from channel.serializers import ChannelID from common.serializers import ListResponseSerializer @@ -172,11 +172,15 @@ class Meta: } def update(self, instance: Chaincode, validated_data: Dict[str, Any]) -> Chaincode: - send_chaincode_request( - self.context["organization"], - instance, - ChaincodeAction[validated_data["action"]], - validated_data["function"], - *validated_data["arguments"] - ) - return instance + try: + result = send_chaincode_request( + self.context["organization"], + instance, + ChaincodeAction[validated_data["action"]], + validated_data["function"], + *validated_data["arguments"] + ) + instance.result = result + return instance + except ChaincodeTransactionError as e: + raise serializers.ValidationError(str(e)) diff --git a/src/api-engine/chaincode/service.py b/src/api-engine/chaincode/service.py index dc68330dc..67f69e279 100644 --- a/src/api-engine/chaincode/service.py +++ b/src/api-engine/chaincode/service.py @@ -21,6 +21,10 @@ LOG = logging.getLogger(__name__) + +class ChaincodeTransactionError(Exception): + pass + peer_command = os.path.join(FABRIC_TOOL, "peer") @@ -183,7 +187,7 @@ def send_chaincode_request( chaincode: Chaincode, action: ChaincodeAction, function: str, - *args: str): + *args: str) -> str: # Pick any organization peer peer_env: Dict[str, str] = get_peers_root_certs_and_addresses_and_envs( organization.name, @@ -204,7 +208,7 @@ def send_chaincode_request( *args ] LOG.info(" ".join(command)) - LOG.info(subprocess.run( + result = subprocess.run( command, env={ **peer_env, @@ -224,9 +228,12 @@ def send_chaincode_request( cwd=os.path.join(CELLO_HOME, "application-gateway"), check=True, capture_output=True, - text=True).stdout) + text=True) + LOG.info(result.stdout) + return result.stdout except subprocess.CalledProcessError as e: LOG.error(e.stderr) + raise ChaincodeTransactionError(e.stderr) def get_peers_root_certs_and_addresses_and_envs( diff --git a/src/api-engine/chaincode/views.py b/src/api-engine/chaincode/views.py index d8342e82d..5d1c3a8b2 100644 --- a/src/api-engine/chaincode/views.py +++ b/src/api-engine/chaincode/views.py @@ -133,7 +133,7 @@ def commit(self, request, pk=None): operation_summary="Invoke/Query a chaincode for the current organization", request_body=ChaincodeRequestBody(), responses=with_common_response( - {status.HTTP_204_NO_CONTENT: None} + {status.HTTP_200_OK: None} ), ) @action(detail=True, methods=["PUT"]) @@ -145,7 +145,9 @@ def transact(self, request, pk=None): }, context={"organization": request.user.organization}) serializer.is_valid(raise_exception=True) - serializer.save() + instance = serializer.save() + result = getattr(instance, "result", "") return Response( - status=status.HTTP_204_NO_CONTENT, + status=status.HTTP_200_OK, + data=ok({"result": result}), ) diff --git a/src/dashboard/src/locales/en-US/Chaincode.js b/src/dashboard/src/locales/en-US/Chaincode.js index 397dc25e6..2e49c6e59 100755 --- a/src/dashboard/src/locales/en-US/Chaincode.js +++ b/src/dashboard/src/locales/en-US/Chaincode.js @@ -52,4 +52,15 @@ export default { 'app.chainCode.form.commit.header.status': 'Approvement Status', 'app.chainCode.form.commit.header.title': 'Commit Chaincode', 'app.chainCode.form.commit.channels': 'Please select channels', + 'app.chainCode.table.operate.interact': 'Interact', + 'app.chainCode.form.transact.header.title': 'Interact with Chaincode', + 'app.chainCode.form.transact.execute': 'Execute', + 'app.chainCode.form.transact.action': 'Action Type', + 'app.chainCode.form.transact.function': 'Function Name', + 'app.chainCode.form.transact.checkFunction': 'Please enter the function name', + 'app.chainCode.form.transact.arguments': 'Arguments', + 'app.chainCode.form.transact.checkArgument': 'Please input argument or delete this field', + 'app.chainCode.form.transact.addArgument': 'Add Argument', + 'app.chainCode.form.transact.result': 'Execution Result', + 'app.chainCode.form.transact.success': 'Transaction executed successfully', }; diff --git a/src/dashboard/src/locales/zh-CN/Chaincode.js b/src/dashboard/src/locales/zh-CN/Chaincode.js index 0ff8ccee6..a4c06677d 100755 --- a/src/dashboard/src/locales/zh-CN/Chaincode.js +++ b/src/dashboard/src/locales/zh-CN/Chaincode.js @@ -52,4 +52,15 @@ export default { 'app.chainCode.form.commit.header.status': '批准状态', 'app.chainCode.form.commit.header.title': '提交链码', 'app.chainCode.form.commit.channels': '请选择通道', + 'app.chainCode.table.operate.interact': '交互', + 'app.chainCode.form.transact.header.title': '与链码交互', + 'app.chainCode.form.transact.execute': '执行', + 'app.chainCode.form.transact.action': '动作类型', + 'app.chainCode.form.transact.function': '函数名称', + 'app.chainCode.form.transact.checkFunction': '请输入函数名称', + 'app.chainCode.form.transact.arguments': '参数列表', + 'app.chainCode.form.transact.checkArgument': '请输入参数或删除此字段', + 'app.chainCode.form.transact.addArgument': '添加参数', + 'app.chainCode.form.transact.result': '执行结果', + 'app.chainCode.form.transact.success': '交易成功执行', }; diff --git a/src/dashboard/src/models/chaincode.js b/src/dashboard/src/models/chaincode.js index 834743346..15921f509 100644 --- a/src/dashboard/src/models/chaincode.js +++ b/src/dashboard/src/models/chaincode.js @@ -8,6 +8,7 @@ import { installChainCode, approveChainCode, commitChainCode, + transactChainCode, } from '@/services/chaincode'; import { createModel, createListEffect, createSimpleEffect } from '@/utils/modelFactory'; @@ -49,5 +50,9 @@ export default createModel({ includePayloadInCallback: false, getServiceParams: payload => ({ id: payload.id }), }), + + transactChainCode: createSimpleEffect(transactChainCode, { + includePayloadInCallback: false, + }), }, }); diff --git a/src/dashboard/src/pages/ChainCode/ChainCode.js b/src/dashboard/src/pages/ChainCode/ChainCode.js index 610ea6948..f6269a6f6 100644 --- a/src/dashboard/src/pages/ChainCode/ChainCode.js +++ b/src/dashboard/src/pages/ChainCode/ChainCode.js @@ -8,10 +8,11 @@ import { PlusOutlined, FunctionOutlined } from '@ant-design/icons'; import PageHeaderWrapper from '@/components/PageHeaderWrapper'; import StandardTable from '@/components/StandardTable'; import UploadForm from '@/pages/ChainCode/forms/UploadForm'; +import InteractForm from '@/pages/ChainCode/forms/InteractForm'; import { useTableManagement } from '@/hooks'; import styles from './styles.less'; -const ChainCode = ({ dispatch, chainCode = {}, loadingChainCodes, uploading }) => { +const ChainCode = ({ dispatch, chainCode = {}, loadingChainCodes, uploading, transacting }) => { const intl = useIntl(); const { chainCodes = [], paginations = {} } = chainCode; @@ -24,6 +25,8 @@ const ChainCode = ({ dispatch, chainCode = {}, loadingChainCodes, uploading }) = const [newFile, setFile] = useState(null); const [operatingId, setOperatingId] = useState(null); const [operationType, setOperationType] = useState(null); + const [interactModalVisible, setInteractModalVisible] = useState(false); + const [currentRecord, setCurrentRecord] = useState(null); useEffect(() => { dispatch({ type: 'chainCode/listChainCode' }); @@ -106,6 +109,19 @@ const ChainCode = ({ dispatch, chainCode = {}, loadingChainCodes, uploading }) = [dispatch] ); + const handleTransact = useCallback( + (values, callback) => { + dispatch({ + type: 'chainCode/transactChainCode', + payload: values, + callback: response => { + if (callback) callback(response); + }, + }); + }, + [dispatch] + ); + const onUploadChainCode = useCallback(() => { handleModalVisible(true); }, [handleModalVisible]); @@ -290,7 +306,23 @@ const ChainCode = ({ dispatch, chainCode = {}, loadingChainCodes, uploading }) = ); } - // COMMITTED 或其他状态不显示按钮 + if (record.status === 'COMMITTED') { + return ( + { + setCurrentRecord(record); + setInteractModalVisible(true); + }} + style={{ cursor: 'pointer' }} + > + {intl.formatMessage({ + id: 'app.chainCode.table.operate.interact', + defaultMessage: 'Interact', + })} + + ); + } + return null; }, }, @@ -337,6 +369,13 @@ const ChainCode = ({ dispatch, chainCode = {}, loadingChainCodes, uploading }) = + ); }; @@ -345,4 +384,5 @@ export default connect(({ chainCode, loading }) => ({ chainCode, loadingChainCodes: loading.effects['chainCode/listChainCode'], uploading: loading.effects['chainCode/uploadChainCode'], + transacting: loading.effects['chainCode/transactChainCode'], }))(ChainCode); diff --git a/src/dashboard/src/pages/ChainCode/forms/InteractForm.js b/src/dashboard/src/pages/ChainCode/forms/InteractForm.js new file mode 100644 index 000000000..d530c155d --- /dev/null +++ b/src/dashboard/src/pages/ChainCode/forms/InteractForm.js @@ -0,0 +1,215 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import { useState, useEffect } from 'react'; +import { injectIntl, useIntl } from 'umi'; +import { Modal, Form, Select, Input, Button, Card } from 'antd'; +import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'; + +const FormItem = Form.Item; +const { Option } = Select; +const { TextArea } = Input; + +const InteractForm = props => { + const [form] = Form.useForm(); + const intl = useIntl(); + const [result, setResult] = useState(''); + + const { modalVisible, handleTransact, handleModalVisible, transacting, record } = props; + + useEffect(() => { + if (modalVisible) { + setResult(''); + form.resetFields(); + } + }, [modalVisible, form]); + + const onSubmit = () => { + form.submit(); + }; + + const transactCallback = response => { + if (response && response.status === 'successful') { + const output = response.data?.result; + setResult( + typeof output === 'object' ? JSON.stringify(output, null, 2) : String(output || '') + ); + } + }; + + const onFinish = values => { + // Arguments could be undefined if empty, default to empty array + const args = values.arguments + ? values.arguments.filter(arg => arg !== undefined && arg !== null) + : []; + handleTransact( + { + id: record.id, + action: values.action, + function: values.function, + arguments: args, + }, + transactCallback + ); + }; + + const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 6 }, + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 16 }, + }, + }; + + const tailFormItemLayout = { + wrapperCol: { + xs: { span: 24, offset: 0 }, + sm: { span: 16, offset: 6 }, + }, + }; + + return ( + handleModalVisible(false)} + width={640} + okText={intl.formatMessage({ + id: 'app.chainCode.form.transact.execute', + defaultMessage: 'Execute', + })} + > +
+ + + + + + + + + + {(fields, { add, remove }) => ( + <> + {fields.map((field, index) => ( + +
+ + + + remove(field.name)} + /> +
+
+ ))} + + + + + )} +
+ + {result && ( + +