From 6a07c7219998adcfb5ad6e2b53e75f786062f163 Mon Sep 17 00:00:00 2001 From: Haitao Yue Date: Mon, 25 Jun 2018 14:06:10 +0800 Subject: [PATCH] [CE-369] Implement invoke/query smart contract Add running smart contract page for show all running chain code. Can invoke/query smart contract in running chain code detail page. Fix chain root path not clean when chain is released. Change-Id: I1ab6af5bc4023182f28e03651e30f282d5d2502e Signed-off-by: Haitao Yue --- .../src/app/assets/src/common/menu.js | 12 +- .../src/app/assets/src/common/router.js | 10 + .../src/app/assets/src/models/deploy.js | 86 ++++++ .../app/assets/src/models/smartContract.js | 10 +- .../src/routes/SmartContract/Info/detail.js | 69 ++++- .../src/routes/SmartContract/Info/index.js | 10 +- .../src/routes/SmartContract/Info/index.less | 4 + .../routes/SmartContract/InvokeQuery/index.js | 206 ++++++++++++++ .../SmartContract/InvokeQuery/index.less | 113 ++++++++ .../SmartContract/InvokeQuery/operate.js | 118 ++++++++ .../src/routes/SmartContract/New/code.js | 4 +- .../src/routes/SmartContract/Running/index.js | 159 +++++++++++ .../routes/SmartContract/Running/index.less | 179 ++++++++++++ .../src/app/assets/src/services/deploy.js | 21 ++ .../src/app/assets/src/utils/config.js | 5 + user-dashboard/src/app/controller/deploy.js | 44 +++ user-dashboard/src/app/extend/context.js | 6 + user-dashboard/src/app/lib/fabric/index.js | 258 +++++++++++++++++- user-dashboard/src/app/router/api.js | 3 + user-dashboard/src/app/service/chain.js | 9 +- user-dashboard/src/app/service/deploy.js | 65 +++++ .../src/app/service/smart_contract.js | 6 +- user-dashboard/src/config/config.default.js | 6 + 23 files changed, 1389 insertions(+), 14 deletions(-) create mode 100644 user-dashboard/src/app/assets/src/models/deploy.js create mode 100644 user-dashboard/src/app/assets/src/routes/SmartContract/InvokeQuery/index.js create mode 100644 user-dashboard/src/app/assets/src/routes/SmartContract/InvokeQuery/index.less create mode 100644 user-dashboard/src/app/assets/src/routes/SmartContract/InvokeQuery/operate.js create mode 100644 user-dashboard/src/app/assets/src/routes/SmartContract/Running/index.js create mode 100644 user-dashboard/src/app/assets/src/routes/SmartContract/Running/index.less create mode 100644 user-dashboard/src/app/assets/src/services/deploy.js create mode 100644 user-dashboard/src/app/controller/deploy.js create mode 100644 user-dashboard/src/app/service/deploy.js diff --git a/user-dashboard/src/app/assets/src/common/menu.js b/user-dashboard/src/app/assets/src/common/menu.js index 355350a9..fc40bfa6 100644 --- a/user-dashboard/src/app/assets/src/common/menu.js +++ b/user-dashboard/src/app/assets/src/common/menu.js @@ -21,9 +21,19 @@ const menuData = [ icon: "code-o", children: [ { - name: "List", + name: "Templates", path: "index", }, + { + name: "Running", + path: "running", + }, + { + name: "Invoke/Query", + path: "invoke-query/:id", + hideInMenu: true, + hideInBreadcrumb: false, + }, { name: "Info", path: "info/:id", diff --git a/user-dashboard/src/app/assets/src/common/router.js b/user-dashboard/src/app/assets/src/common/router.js index afb0bb69..6f84f544 100644 --- a/user-dashboard/src/app/assets/src/common/router.js +++ b/user-dashboard/src/app/assets/src/common/router.js @@ -130,6 +130,16 @@ export const getRouterData = app => { import("../routes/SmartContract/New/code") ), }, + "/smart-contract/running": { + component: dynamicWrapper(app, ["deploy"], () => + import("../routes/SmartContract/Running") + ), + }, + "/smart-contract/invoke-query/:id": { + component: dynamicWrapper(app, ["deploy"], () => + import("../routes/SmartContract/InvokeQuery") + ), + }, }; // Get name from ./menu.js or just set it in the router data. const menuData = getFlatMenuData(getMenuData()); diff --git a/user-dashboard/src/app/assets/src/models/deploy.js b/user-dashboard/src/app/assets/src/models/deploy.js new file mode 100644 index 00000000..e304d5b5 --- /dev/null +++ b/user-dashboard/src/app/assets/src/models/deploy.js @@ -0,0 +1,86 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import { queryDeploys, queryDeploy, operateDeploy } from "../services/deploy"; + +export default { + namespace: "deploy", + + state: { + deploys: [], + currentDeploy: {}, + total: 0, + instantiatedCount: 0, + instantiatingCount: 0, + errorCount: 0, + }, + + effects: { + *fetch({ payload }, { call, put }) { + const response = yield call(queryDeploys, payload); + const { + data, + total, + instantiatedCount, + instantiatingCount, + errorCount, + } = response.data; + yield put({ + type: "setDeploys", + payload: { + deploys: data, + total, + instantiatingCount, + instantiatedCount, + errorCount, + }, + }); + }, + *queryDeploy({ payload }, { call, put }) { + const response = yield call(queryDeploy, payload.id); + const { deploy } = response; + yield put({ + type: "setDeploy", + payload: { + currentDeploy: deploy, + }, + }); + }, + *operateDeploy({ payload }, { call }) { + const response = yield call(operateDeploy, payload); + if (payload.callback) { + yield call(payload.callback, { + request: payload, + response, + }); + } + }, + }, + + reducers: { + setDeploys(state, action) { + const { + deploys, + total, + instantiatedCount, + instantiatingCount, + errorCount, + } = action.payload; + return { + ...state, + deploys, + total, + instantiatedCount, + instantiatingCount, + errorCount, + }; + }, + setDeploy(state, action) { + const { currentDeploy } = action.payload; + return { + ...state, + currentDeploy, + }; + }, + }, +}; diff --git a/user-dashboard/src/app/assets/src/models/smartContract.js b/user-dashboard/src/app/assets/src/models/smartContract.js index 970b6b96..a3471b86 100644 --- a/user-dashboard/src/app/assets/src/models/smartContract.js +++ b/user-dashboard/src/app/assets/src/models/smartContract.js @@ -17,6 +17,7 @@ export default { smartContracts: [], currentSmartContract: {}, codes: [], + deploys: [], newOperations: [], }, @@ -51,6 +52,7 @@ export default { payload: { currentSmartContract: response.info, codes: response.codes, + deploys: response.deploys, newOperations: response.newOperations, }, }); @@ -78,11 +80,17 @@ export default { }; }, setCurrentSmartContract(state, action) { - const { currentSmartContract, codes, newOperations } = action.payload; + const { + currentSmartContract, + codes, + newOperations, + deploys, + } = action.payload; return { ...state, currentSmartContract, codes, + deploys, newOperations, }; }, diff --git a/user-dashboard/src/app/assets/src/routes/SmartContract/Info/detail.js b/user-dashboard/src/app/assets/src/routes/SmartContract/Info/detail.js index 599c0718..8ffcd273 100644 --- a/user-dashboard/src/app/assets/src/routes/SmartContract/Info/detail.js +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/Info/detail.js @@ -7,6 +7,7 @@ import { Button, Table, Divider, + Badge, } from 'antd'; import moment from 'moment'; import styles from './index.less'; @@ -15,7 +16,7 @@ import styles from './index.less'; export default class Detail extends Component { render() { - const { codes, loadingInfo, onAddNewCode, onDeploy } = this.props; + const { codes, loadingInfo, onAddNewCode, onDeploy, deploys, onInvokeQuery } = this.props; const codeColumns = [ { title: 'Version', @@ -39,10 +40,76 @@ export default class Detail extends Component { ), }, ]; + const deployColumns = [ + { + title: 'Chain', + dataIndex: 'chain', + key: 'chain', + render: text => text.name, + }, + { + title: 'Code Version', + dataIndex: 'smartContractCode', + key: 'smartContractCode', + render: text => text.version, + }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + render: ( text ) => { + let status = "default"; + switch (text) { + case 'installed': + case 'instantiated': + status = "success"; + break; + case 'instantiating': + status = "processing"; + break; + case 'error': + status = "error"; + break; + default: + break; + } + + return {text}} />; + }, + }, + { + title: 'Deploy Time', + dataIndex: 'deployTime', + key: 'deployTime', + render: text => moment(text).format("YYYY-MM-DD HH:mm:ss"), + }, + { + title: 'Operate', + render: ( text, record ) => ( + + + + ), + }, + ]; return (
+ +
+ + +
diff --git a/user-dashboard/src/app/assets/src/routes/SmartContract/Info/index.js b/user-dashboard/src/app/assets/src/routes/SmartContract/Info/index.js index 5ea85fea..0b153ac3 100644 --- a/user-dashboard/src/app/assets/src/routes/SmartContract/Info/index.js +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/Info/index.js @@ -126,6 +126,12 @@ export default class AdvancedProfile extends Component { this.onOperationTabChange('detail'); }; + onInvokeQuery = (item) => { + this.props.dispatch(routerRedux.push({ + pathname: `/smart-contract/invoke-query/${item._id}`, + })); + }; + @Bind() @Debounce(200) setStepDirection() { @@ -143,16 +149,18 @@ export default class AdvancedProfile extends Component { } render() { const { smartContract, chain: { chains }, loadingInfo } = this.props; - const { currentSmartContract, codes, newOperations } = smartContract; + const { currentSmartContract, codes, deploys, newOperations } = smartContract; const { deployStep, selectedVersion, selectedVersionId } = this.state; const versions = codes.map(code => code.version); const versionTags = versions.map(version => {version}); const detailProps = { codes, + deploys, loadingInfo, onAddNewCode: this.onAddNewCode, onDeploy: this.onClickDeploy, + onInvokeQuery: this.onInvokeQuery, }; const deployProps = { version: selectedVersion, diff --git a/user-dashboard/src/app/assets/src/routes/SmartContract/Info/index.less b/user-dashboard/src/app/assets/src/routes/SmartContract/Info/index.less index 160b0263..e6beba3c 100644 --- a/user-dashboard/src/app/assets/src/routes/SmartContract/Info/index.less +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/Info/index.less @@ -107,3 +107,7 @@ align-content: center; text-align: center; } + +.status-text { + text-transform: capitalize; +} diff --git a/user-dashboard/src/app/assets/src/routes/SmartContract/InvokeQuery/index.js b/user-dashboard/src/app/assets/src/routes/SmartContract/InvokeQuery/index.js new file mode 100644 index 00000000..e22da616 --- /dev/null +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/InvokeQuery/index.js @@ -0,0 +1,206 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React, { Component, Fragment } from 'react'; +import Debounce from 'lodash-decorators/debounce'; +import Bind from 'lodash-decorators/bind'; +import pathToRegexp from 'path-to-regexp'; +import { connect } from 'dva'; +import moment from 'moment'; +import { + Button, + Icon, + message, + Badge, +} from 'antd'; +import DescriptionList from 'components/DescriptionList'; +import PageHeaderLayout from '../../../layouts/PageHeaderLayout'; +import styles from './index.less'; +import OperateDeploy from './operate'; + +const { Description } = DescriptionList; + +const getWindowWidth = () => window.innerWidth || document.documentElement.clientWidth; + +const action = ( + + + +); + +const tabList = [ + { + key: 'invoke', + tab: 'Invoke', + }, + { + key: 'query', + tab: 'Query', + }, +]; + +@connect(({ deploy, loading }) => ({ + deploy, + loadingInfo: loading.effects['deploy/queryDeploy'], + operating: loading.effects['deploy/operateDeploy'], +})) +export default class AdvancedProfile extends Component { + state = { + deployId: '', + operationKey: 'invoke', + stepDirection: 'horizontal', + queryResult: '', + }; + + componentDidMount() { + const { location, dispatch } = this.props; + const info = pathToRegexp('/smart-contract/invoke-query/:id').exec(location.pathname); + if (info) { + const id = info[1]; + dispatch({ + type: 'deploy/queryDeploy', + payload: { + id, + }, + }); + this.setState({ + deployId: id, + }) + } + this.setStepDirection(); + window.addEventListener('resize', this.setStepDirection); + } + + componentWillUnmount() { + window.removeEventListener('resize', this.setStepDirection); + this.setStepDirection.cancel(); + } + + onOperationTabChange = key => { + this.setState({ + operationKey: key, + }); + }; + + + @Bind() + @Debounce(200) + setStepDirection() { + const { stepDirection } = this.state; + const w = getWindowWidth(); + if (stepDirection !== 'vertical' && w <= 576) { + this.setState({ + stepDirection: 'vertical', + }); + } else if (stepDirection !== 'horizontal' && w > 576) { + this.setState({ + stepDirection: 'horizontal', + }); + } + } + operateCallback = (data) => { + const { request, response } = data; + const { operation } = request; + let messageStatus = 'success'; + let successText = 'successfully'; + if (!response.success) { + messageStatus = 'error'; + successText = 'failed'; + } + switch (operation) { + case 'invoke': + message[messageStatus](`${operation} operation ${successText}, transaction ID ${response.transactionID}`); + break; + case 'query': + message[messageStatus](`${operation} operation ${successText}, result ${response.result}`); + this.setState({ + queryResult: response.result, + }); + break; + default: + break; + } + }; + operateAPI = (data) => { + const { deployId } = this.state; + this.props.dispatch({ + type: 'deploy/operateDeploy', + payload: { + ...data, + id: deployId, + callback: this.operateCallback, + }, + }); + }; + render() { + const { deploy, loadingInfo, operating } = this.props; + const { operationKey, queryResult } = this.state; + const { currentDeploy } = deploy; + + const invokeProps = { + operation: 'invoke', + onSubmit: this.operateAPI, + submitting: operating, + }; + const queryProps = { + operation: 'query', + onSubmit: this.operateAPI, + submitting: operating, + result: queryResult, + }; + + const contentList = { + invoke: ( + operationKey === 'invoke' && + ), + query: ( + operationKey === 'query' && + ), + }; + + function getStatus(text) { + let status = "default"; + switch (text) { + case 'installed': + case 'instantiated': + status = "success"; + break; + case 'instantiating': + status = "processing"; + break; + case 'error': + status = "error"; + break; + default: + break; + } + return status; + } + + const description = ( + + {currentDeploy.smartContract && currentDeploy.smartContract.name} / {currentDeploy.smartContractCode && currentDeploy.smartContractCode.version} + {currentDeploy.chain && currentDeploy.chain.name} + {currentDeploy.status && } + {currentDeploy.deployTime && moment(currentDeploy.deployTime).format("YYYY-MM-DD HH:mm")} + + ); + + return ( + + } + loading={loadingInfo} + action={action} + content={description} + tabList={tabList} + tabActiveKey={this.state.operationKey} + onTabChange={this.onOperationTabChange} + > + {contentList[this.state.operationKey]} + + ); + } +} diff --git a/user-dashboard/src/app/assets/src/routes/SmartContract/InvokeQuery/index.less b/user-dashboard/src/app/assets/src/routes/SmartContract/InvokeQuery/index.less new file mode 100644 index 00000000..e6beba3c --- /dev/null +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/InvokeQuery/index.less @@ -0,0 +1,113 @@ +@import '~antd/lib/style/themes/default.less'; +@import "../../../utils/utils"; + +.headerList { + margin-bottom: 4px; +} + +.tabsCard { + :global { + .ant-card-head { + padding: 0 16px; + } + } +} + +.noData { + color: @disabled-color; + text-align: center; + line-height: 64px; + font-size: 16px; + i { + font-size: 24px; + margin-right: 16px; + position: relative; + top: 3px; + } +} + +.heading { + color: @heading-color; + font-size: 20px; +} + +.stepDescription { + font-size: 14px; + position: relative; + left: 38px; + & > div { + margin-top: 8px; + margin-bottom: 4px; + } +} + +.textSecondary { + color: @text-color-secondary; +} + +@media screen and (max-width: @screen-sm) { + .stepDescription { + left: 8px; + } +} + +.tableList { + .tableListOperator { + margin-bottom: 16px; + button { + margin-right: 8px; + } + } +} + +.tableListForm { + :global { + .ant-form-item { + margin-bottom: 24px; + margin-right: 0; + display: flex; + > .ant-form-item-label { + width: auto; + line-height: 32px; + padding-right: 8px; + } + .ant-form-item-control { + line-height: 32px; + } + } + .ant-form-item-control-wrapper { + flex: 1; + } + } + .submitButtons { + white-space: nowrap; + margin-bottom: 24px; + } +} + +@media screen and (max-width: @screen-lg) { + .tableListForm :global(.ant-form-item) { + margin-right: 24px; + } +} + +@media screen and (max-width: @screen-md) { + .tableListForm :global(.ant-form-item) { + margin-right: 8px; + } +} + +.step-content { + margin-top: 18px; +} + +.step-button { + margin-top: 18px; + padding: 5px 20px; + align-content: center; + text-align: center; +} + +.status-text { + text-transform: capitalize; +} diff --git a/user-dashboard/src/app/assets/src/routes/SmartContract/InvokeQuery/operate.js b/user-dashboard/src/app/assets/src/routes/SmartContract/InvokeQuery/operate.js new file mode 100644 index 00000000..4fd0684d --- /dev/null +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/InvokeQuery/operate.js @@ -0,0 +1,118 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React, { Component } from 'react'; +import { + Card, + Form, + Input, + Button, + Row, + Col, +} from 'antd'; +import styles from './index.less'; + +const FormItem = Form.Item; + +@Form.create() +export default class OperateDeploy extends Component { + state = { + }; + shouldComponentUpdate(nextProps) { + if (this.props.operation !== nextProps.operation) { + this.props.form.resetFields(['functionName', 'args']); + } + return true; + } + handleSubmit = e => { + const { onSubmit, operation } = this.props; + e.preventDefault(); + this.props.form.validateFieldsAndScroll({ force: true }, (err, values) => { + if (!err) { + onSubmit({ + ...values, + operation, + }); + } + }); + }; + render() { + const { form: { getFieldDecorator }, operation, submitting, result } = this.props; + const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 10 }, + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 14 }, + }, + }; + const tailFormItemLayout = { + wrapperCol: { + xs: { + span: 24, + offset: 0, + }, + sm: { + span: 14, + offset: 10, + }, + }, + }; + return ( + + +
+ {operation}} bordered={false}> +
+ + {getFieldDecorator('functionName', { + initialValue: '', + rules: [ + { + required: true, + message: 'Must input function name', + }, + ], + })()} + + + {getFieldDecorator('args', { + initialValue: '', + rules: [ + { + required: true, + message: 'Must input arguments', + }, + ], + })()} + + + + + +
+ + {operation === 'query' && ( + + + {result} + + +)} + + + ); + } +} diff --git a/user-dashboard/src/app/assets/src/routes/SmartContract/New/code.js b/user-dashboard/src/app/assets/src/routes/SmartContract/New/code.js index 38e1021e..7a691e7a 100644 --- a/user-dashboard/src/app/assets/src/routes/SmartContract/New/code.js +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/New/code.js @@ -79,7 +79,7 @@ class NewSmartContractCode extends PureComponent { } }; clickCancel = () => { - const { smartContractCodeId } = this.state; + const { smartContractCodeId, smartContractId } = this.state; if (smartContractCodeId !== '') { this.props.dispatch({ type: 'smartContract/deleteSmartContractCode', @@ -90,7 +90,7 @@ class NewSmartContractCode extends PureComponent { } this.props.dispatch( routerRedux.push({ - pathname: '/smart-contract', + pathname: `/smart-contract/info/${smartContractId}`, }) ); }; diff --git a/user-dashboard/src/app/assets/src/routes/SmartContract/Running/index.js b/user-dashboard/src/app/assets/src/routes/SmartContract/Running/index.js new file mode 100644 index 00000000..11db2408 --- /dev/null +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/Running/index.js @@ -0,0 +1,159 @@ +import React, { PureComponent } from 'react'; +import moment from 'moment'; +import { connect } from 'dva'; +import { routerRedux, Link } from 'dva/router'; +import { + List, + Card, + Row, + Col, + Radio, + Button, + Avatar, + Badge, +} from 'antd'; + +import PageHeaderLayout from '../../../layouts/PageHeaderLayout'; + +import styles from './index.less'; + +const RadioButton = Radio.Button; +const RadioGroup = Radio.Group; + +@connect(({ deploy, loading }) => ({ + deploy, + loadingDeploys: loading.effects['deploy/fetch'], +})) +export default class BasicList extends PureComponent { + state = { + pageSize: 5, + }; + componentDidMount() { + this.props.dispatch({ + type: 'deploy/fetch', + payload: { + count: 5, + }, + }); + } + changeStatus = e => { + this.props.dispatch({ + type: 'deploy/fetch', + payload: { + status: e.target.value, + }, + }) + }; + invokeQuery = (deploy) => { + this.props.dispatch(routerRedux.push({ + pathname: `/smart-contract/invoke-query/${deploy._id}`, + })); + }; + + render() { + const { deploy: { deploys, total, instantiatedCount, errorCount }, loadingDeploys } = this.props; + const { pageSize } = this.state; + + const Info = ({ title, value, bordered }) => ( +
+ {title} +

{value}

+ {bordered && } +
+ ); + + const extraContent = ( +
+ + All + Instantiating + Instantiated + Error + +
+ ); + + const paginationProps = { + showSizeChanger: true, + showQuickJumper: true, + pageSize, + total, + }; + + function getStatus(status) { + switch (status) { + case 'installed': + case 'instantiated': + return "success"; + case 'instantiating': + return "processing"; + case 'error': + return "error"; + default: + return "default"; + } + } + + const ListContent = ({ data: { chain, deployTime, status } }) => ( +
+
+ Chain Name/Size +

{chain.name} / {chain.size}

+
+
+ Deploy Time +

{moment(deployTime).format('YYYY-MM-DD HH:mm')}

+
+
+ +
+
+ ); + + return ( + +
+ + +
+ + + + + + + + + + + + + ( + this.invokeQuery(item)} disabled={item.status !== 'instantiated'} size="small" icon="api" type="primary">Invoke/Query]}> + } + title={{item.smartContract.name} / {item.smartContractCode.version}} + /> + + + )} + /> + + + + ); + } +} diff --git a/user-dashboard/src/app/assets/src/routes/SmartContract/Running/index.less b/user-dashboard/src/app/assets/src/routes/SmartContract/Running/index.less new file mode 100644 index 00000000..83908317 --- /dev/null +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/Running/index.less @@ -0,0 +1,179 @@ +@import '~antd/lib/style/themes/default.less'; +@import '../../../utils/utils.less'; + +.status-text { + text-transform: capitalize; +} +.standardList { + :global { + .ant-card-head { + border-bottom: none; + } + .ant-card-head-title { + line-height: 32px; + padding: 24px 0; + } + .ant-card-extra { + padding: 24px 0; + } + .ant-list-pagination { + text-align: right; + margin-top: 24px; + } + .ant-avatar-lg { + width: 48px; + height: 48px; + line-height: 48px; + } + } + .headerInfo { + position: relative; + text-align: center; + & > span { + color: @text-color-secondary; + display: inline-block; + font-size: @font-size-base; + line-height: 22px; + margin-bottom: 4px; + } + & > p { + color: @heading-color; + font-size: 24px; + line-height: 32px; + margin: 0; + } + & > em { + background-color: @border-color-split; + position: absolute; + height: 56px; + width: 1px; + top: 0; + right: 0; + } + } + .listContent { + font-size: 0; + .listContentItem { + color: @text-color-secondary; + display: inline-block; + vertical-align: middle; + font-size: @font-size-base; + margin-left: 40px; + > span { + line-height: 20px; + } + > p { + margin-top: 4px; + margin-bottom: 0; + line-height: 22px; + } + } + } + .extraContentSearch { + margin-left: 16px; + width: 272px; + } +} + +@media screen and (max-width: @screen-xs) { + .standardList { + :global { + .ant-list-item-content { + display: block; + flex: none; + width: 100%; + } + .ant-list-item-action { + margin-left: 0; + } + } + .listContent { + margin-left: 0; + & > div { + margin-left: 0; + } + } + .listCard { + :global { + .ant-card-head-title { + overflow: visible; + } + } + } + } +} + +@media screen and (max-width: @screen-sm) { + .standardList { + .extraContentSearch { + margin-left: 0; + width: 100%; + } + .headerInfo { + margin-bottom: 16px; + & > em { + display: none; + } + } + } +} + +@media screen and (max-width: @screen-md) { + .standardList { + .listContent { + & > div { + display: block; + } + & > div:last-child { + top: 0; + width: 100%; + } + } + } + .listCard { + :global { + .ant-radio-group { + display: block; + margin-bottom: 8px; + } + } + } +} + +@media screen and (max-width: @screen-lg) and (min-width: @screen-md) { + .standardList { + .listContent { + & > div { + display: block; + } + & > div:last-child { + top: 0; + width: 100%; + } + } + } +} + +@media screen and (max-width: @screen-xl) { + .standardList { + .listContent { + & > div { + margin-left: 24px; + } + & > div:last-child { + top: 0; + } + } + } +} + +@media screen and (max-width: 1400px) { + .standardList { + .listContent { + text-align: right; + & > div:last-child { + top: 0; + } + } + } +} diff --git a/user-dashboard/src/app/assets/src/services/deploy.js b/user-dashboard/src/app/assets/src/services/deploy.js new file mode 100644 index 00000000..ffdefebb --- /dev/null +++ b/user-dashboard/src/app/assets/src/services/deploy.js @@ -0,0 +1,21 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import { stringify } from "qs"; +import request from "../utils/request"; +import config from "../utils/config"; + +export async function queryDeploys(params) { + return request(`${config.url.deploy.list}?${stringify(params)}`); +} + +export async function queryDeploy(id) { + return request(`${config.url.deploy.query.format({ id })}`); +} + +export async function operateDeploy(params) { + return request(config.url.deploy.operate.format({ id: params.id }), { + method: "POST", + body: params, + }); +} diff --git a/user-dashboard/src/app/assets/src/utils/config.js b/user-dashboard/src/app/assets/src/utils/config.js index 11e769b2..db3961a1 100644 --- a/user-dashboard/src/app/assets/src/utils/config.js +++ b/user-dashboard/src/app/assets/src/utils/config.js @@ -21,5 +21,10 @@ export default { operate: `${urlBase}api/smart-contract/{id}`, codeDeploy: `${urlBase}api/smart-contract/deploy-code/{id}`, }, + deploy: { + list: `${urlBase}api/deploy`, + query: `${urlBase}api/deploy/{id}`, + operate: `${urlBase}api/deploy/operate/{id}`, + }, }, }; diff --git a/user-dashboard/src/app/controller/deploy.js b/user-dashboard/src/app/controller/deploy.js new file mode 100644 index 00000000..9738aa34 --- /dev/null +++ b/user-dashboard/src/app/controller/deploy.js @@ -0,0 +1,44 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +'use strict'; + +const Controller = require('egg').Controller; + +class DeployController extends Controller { + async list() { + const { ctx } = this; + ctx.body = { + data: await ctx.service.deploy.list(), + }; + } + async query() { + const { ctx } = this; + const id = ctx.params.id; + ctx.logger.debug('in deploy query ', id); + ctx.body = await ctx.service.deploy.query(id); + } + async operate() { + const { ctx } = this; + const id = ctx.params.id; + const { functionName, operation } = ctx.request.body; + let { args } = ctx.request.body; + args = args.split(','); + switch (operation) { + case 'invoke': + ctx.body = await ctx.service.deploy.invoke(functionName, args, id); + break; + case 'query': + ctx.body = await ctx.service.deploy.queryChainCode(functionName, args, id); + break; + default: + ctx.body = { + success: false, + message: 'Must input valid operation', + }; + break; + } + } +} + +module.exports = DeployController; diff --git a/user-dashboard/src/app/extend/context.js b/user-dashboard/src/app/extend/context.js index 72079319..6cf89093 100644 --- a/user-dashboard/src/app/extend/context.js +++ b/user-dashboard/src/app/extend/context.js @@ -25,6 +25,12 @@ module.exports = { get installSmartContract() { return this.app.installSmartContract; }, + get invokeChainCode() { + return this.app.invokeChainCode; + }, + get queryChainCode() { + return this.app.queryChainCode; + }, get instantiateSmartContract() { return this.app.instantiateSmartContract; }, diff --git a/user-dashboard/src/app/lib/fabric/index.js b/user-dashboard/src/app/lib/fabric/index.js index 91670bf1..1295c15a 100644 --- a/user-dashboard/src/app/lib/fabric/index.js +++ b/user-dashboard/src/app/lib/fabric/index.js @@ -1,6 +1,7 @@ 'use strict'; const hfc = require('fabric-client'); +const User = require('fabric-client/lib/User.js'); const copService = require('fabric-ca-client'); const fs = require('fs-extra'); const path = require('path'); @@ -19,6 +20,16 @@ module.exports = app => { 'ssl-target-name-override': network.orderer['server-hostname'], }); } + async function buildTarget(helper, peer, org) { + let target = null; + const { network } = helper; + if (typeof peer !== 'undefined') { + const targets = await newPeers(network, [peer], org, helper.clients); + if (targets && targets.length > 0) target = targets[0]; + } + + return target; + } function setupPeers(network, channel, org, client) { for (const key in network[org].peers) { const data = fs.readFileSync(network[org].peers[key].tls_cacerts); @@ -112,7 +123,6 @@ module.exports = app => { const store = await hfc.newDefaultKeyValueStore({ path: getKeyStoreForOrg(keyValueStore, getOrgName(userOrg, network)), }); - app.logger.debug('store ', store); client.setStateStore(store); const user = client.createUser({ username: 'peer' + userOrg + 'Admin', @@ -124,6 +134,42 @@ module.exports = app => { }); return user; } + async function getAdminUser(helper, org) { + const { keyValueStore, network } = helper; + const users = [ + { + username: 'admin', + secret: 'adminpw', + }, + ]; + const username = users[0].username; + const password = users[0].secret; + const client = await getClientForOrg(org, helper.clients); + + const store = await hfc.newDefaultKeyValueStore({ + path: getKeyStoreForOrg(keyValueStore, getOrgName(org, network)), + }); + client.setStateStore(store); + // clearing the user context before switching + client._userContext = null; + const user = await client.getUserContext(username, true); + if (user && user.isEnrolled()) { + app.logger.debug('Successfully loaded member from persistence'); + return user; + } + const caClient = helper.caClients[org]; + const enrollment = await caClient.enroll({ + enrollmentID: username, + enrollmentSecret: password, + }); + app.logger.info('Successfully enrolled user \'' + username + '\''); + const member = new User(username); + member.setCryptoSuite(client.getCryptoSuite()); + await member.setEnrollment(enrollment.key, enrollment.certificate, getMspID(org, network)); + await client.setUserContext(member); + return member; + + } async function createChannel(network, keyValueStorePath, channelName, channelConfigPath) { const helper = await fabricHelper(network, keyValueStorePath); const client = await getClientForOrg('org1', helper.clients); @@ -280,6 +326,7 @@ module.exports = app => { smartContract: smartContractCode.smartContract, name: chainCodeName, chain: chainId, + user: userId, }, { status: 'installed', }, { upsert: true, new: true }); @@ -355,7 +402,7 @@ module.exports = app => { const deployId = await txId.getTransactionID(); const eh = await client.newEventHub(); - const data = fs.readFileSync(path.join(__dirname, network[org].peers.peer1.tls_cacerts)); + const data = fs.readFileSync(network[org].peers.peer1.tls_cacerts); await eh.setPeerAddr(network[org].peers.peer1.events, { pem: Buffer.from(data).toString(), 'ssl-target-name-override': network[org].peers.peer1['server-hostname'], @@ -398,6 +445,209 @@ module.exports = app => { return 'Failed to send instantiate Proposal or receive valid response. Response null or status is not 200. exiting...'; } + async function getRegisteredUsers(helper, username, org) { + // const helper = await fabricHelper(network, keyValueStorePath); + const { keyValueStore, network } = helper; + const client = await getClientForOrg(org, helper.clients); + let member; + let enrollmentSecret = null; + + const store = await hfc.newDefaultKeyValueStore({ + path: getKeyStoreForOrg(keyValueStore, getOrgName(org, network)), + }); + client.setStateStore(store); + client._userContext = null; + const user = await client.getUserContext(username, true); + if (user && user.isEnrolled()) { + app.logger.debug('Successfully loaded member from persistence'); + return user; + } + const caClient = helper.caClients[org]; + member = await getAdminUser(helper, org); + enrollmentSecret = await caClient.register({ + enrollmentID: username, + affiliation: org + '.department1', + }, member); + app.logger.debug(username + ' registered successfully'); + const message = await caClient.enroll({ + enrollmentID: username, + enrollmentSecret, + }); + if (message && typeof message === 'string' && message.includes( + 'Error:')) { + app.logger.error(username + ' enrollment failed'); + return message; + } + app.logger.debug(username + ' enrolled successfully'); + + member = new User(username); + member._enrollmentSecret = enrollmentSecret; + await member.setEnrollment(message.key, message.certificate, getMspID(org, network)); + await client.setUserContext(member); + return member; + + } + async function invokeChainCode(network, keyValueStorePath, peerNames, channelName, chainCodeName, fcn, args, username, org) { + const helper = await fabricHelper(network, keyValueStorePath); + const client = await getClientForOrg(org, helper.clients); + const channel = await getChannelForOrg(org, helper.channels); + const targets = (peerNames) ? await newPeers(network, peerNames, org, helper.clients) : undefined; + await getRegisteredUsers(helper, username, org); + const txId = client.newTransactionID(); + app.logger.debug(util.format('Sending transaction "%j"', txId)); + // send proposal to endorser + const request = { + chaincodeId: chainCodeName, + fcn, + args, + chainId: channelName, + txId, + }; + + if (targets) { request.targets = targets; } + const results = await channel.sendTransactionProposal(request); + const proposalResponses = results[0]; + const proposal = results[1]; + let all_good = true; + for (const i in proposalResponses) { + let one_good = false; + if (proposalResponses && proposalResponses[i].response && + proposalResponses[i].response.status === 200) { + one_good = true; + app.logger.debug('transaction proposal was good'); + } else { + app.logger.error('transaction proposal was bad'); + } + all_good = all_good & one_good; + } + if (all_good) { + app.logger.debug(util.format( + 'Successfully sent Proposal and received ProposalResponse: Status - %s, message - "%s", metadata - "%s", endorsement signature: %s', + proposalResponses[0].response.status, proposalResponses[0].response.message, + proposalResponses[0].response.payload, proposalResponses[0].endorsement + .signature)); + const transactionRequest = { + proposalResponses, + proposal, + }; + // set the transaction listener and set a timeout of 30sec + // if the transaction did not get committed within the timeout period, + // fail the test + const transactionID = txId.getTransactionID(); + const eventPromises = []; + + if (!peerNames) { + peerNames = channel.getPeers().map(function(peer) { + return peer.getName(); + }); + } + + const eventhubs = await newEventHubs(network, peerNames, org, helper.clients); + for (const key in eventhubs) { + const eh = eventhubs[key]; + eh.connect(); + + const txPromise = new Promise((resolve, reject) => { + const handle = setTimeout(() => { + eh.disconnect(); + reject(); + }, 30000); + + eh.registerTxEvent(transactionID, (tx, code) => { + clearTimeout(handle); + eh.unregisterTxEvent(transactionID); + eh.disconnect(); + + if (code !== 'VALID') { + app.logger.error( + 'The balance transfer transaction was invalid, code = ' + code); + reject(); + } else { + app.logger.info( + 'The balance transfer transaction has been committed on peer ' + + eh._ep._endpoint.addr); + resolve(); + } + }); + }); + eventPromises.push(txPromise); + } + const sendPromise = channel.sendTransaction(transactionRequest); + try { + const promiseResults = await Promise.all([sendPromise].concat(eventPromises)); + const response = promiseResults[0]; + if (response.status === 'SUCCESS') { + app.logger.info('Successfully sent transaction to the orderer.'); + return { + transactionID: txId.getTransactionID(), + success: true, + }; + } + app.logger.error('Failed to order the transaction. Error code: ' + response.status); + return { + success: false, + message: 'Failed to order the transaction. Error code: ' + response.status, + }; + + } catch (err) { + app.logger.error( + 'Failed to send transaction and get notifications within the timeout period.' + ); + return { + success: false, + message: 'Failed to send transaction and get notifications within the timeout period.', + }; + } + } else { + app.logger.error( + 'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...' + ); + return { + success: false, + message: 'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...', + }; + } + } + async function queryChainCode(network, keyValueStorePath, peer, channelName, chainCodeName, fcn, args, username, org) { + const helper = await fabricHelper(network, keyValueStorePath); + const client = await getClientForOrg(org, helper.clients); + const channel = await getChannelForOrg(org, helper.channels); + const target = await buildTarget(helper, peer, org); + await getRegisteredUsers(helper, username, org); + const txId = client.newTransactionID(); + // send query + const request = { + chaincodeId: chainCodeName, + txId, + fcn, + args, + }; + try { + const responsePayloads = await channel.queryByChaincode(request, target); + if (responsePayloads) { + for (let i = 0; i < responsePayloads.length; i++) { + app.logger.debug('response payloads ', i, responsePayloads[i].toString('utf8')); + } + for (let i = 0; i < responsePayloads.length; i++) { + return { + success: true, + result: responsePayloads[i].toString('utf8'), + }; + } + } else { + app.logger.error('response_payloads is null'); + return { + success: false, + message: 'response_payloads is null', + }; + } + } catch (err) { + return { + success: false, + message: 'Failed to send query due to error: ' + err.stack ? err.stack : err, + }; + } + } async function fabricHelper(network, keyValueStore) { const helper = { network, @@ -422,7 +672,7 @@ module.exports = app => { setupPeers(network, channel, key, client); const caUrl = network[key].ca; - caClients[key] = new copService(caUrl, null, cryptoSuite); + caClients[key] = new copService(caUrl, null, '', cryptoSuite); } } helper.clients = clients; @@ -441,6 +691,8 @@ module.exports = app => { app.joinChannel = joinChannel; app.installSmartContract = installSmartContract; app.instantiateSmartContract = instantiateSmartContract; + app.invokeChainCode = invokeChainCode; + app.queryChainCode = queryChainCode; app.sleep = sleep; hfc.setLogger(app.logger); }; diff --git a/user-dashboard/src/app/router/api.js b/user-dashboard/src/app/router/api.js index 050eaadf..c4928ed2 100644 --- a/user-dashboard/src/app/router/api.js +++ b/user-dashboard/src/app/router/api.js @@ -16,4 +16,7 @@ module.exports = app => { app.router.delete('/api/smart-contract/:id', app.controller.smartContract.deleteSmartContract); app.router.get('/api/smart-contract/:id', app.controller.smartContract.querySmartContract); app.router.post('/api/smart-contract/deploy-code/:id', app.controller.smartContract.deploySmartContractCode); + app.router.get('/api/deploy', app.controller.deploy.list); + app.router.get('/api/deploy/:id', app.controller.deploy.query); + app.router.post('/api/deploy/operate/:id', app.controller.deploy.operate); }; diff --git a/user-dashboard/src/app/service/chain.js b/user-dashboard/src/app/service/chain.js index d7128d40..8d75b76e 100644 --- a/user-dashboard/src/app/service/chain.js +++ b/user-dashboard/src/app/service/chain.js @@ -37,6 +37,10 @@ class ChainService extends Service { const networkConfig = await ctx.model.NetworkConfig.findOne({ chain: chain._id.toString() }); const networkConfigId = networkConfig._id.toString(); const orderers = await ctx.model.OrdererConfig.find({ networkConfig: networkConfigId }); + const deploys = await ctx.model.SmartContractDeploy.find({ chain }); + deploys.map(deploy => { + return deploy.remove(); + }); orderers.map(ordererItem => { return ordererItem.remove(); }); @@ -64,6 +68,7 @@ class ChainService extends Service { const { ctx, config } = this; const operateUrl = config.operator.url.cluster.operate; const clusterId = ctx.params.id; + const chain = await ctx.model.Chain.findOne({ chainId: clusterId }); const response = await ctx.curl(operateUrl, { method: 'POST', data: { @@ -75,7 +80,7 @@ class ChainService extends Service { }); if (response.status === 200) { await this.cleanDB(clusterId); - await this.cleanStore(clusterId); + await this.cleanStore(chain._id.toString()); } } async findRegex(regex, value) { @@ -165,7 +170,7 @@ class ChainService extends Service { } async initialFabric(chain) { const { ctx, config } = this; - const chainRootDir = `${config.dataDir}/${ctx.user.id}/chains/${chain.chainId}`; + const chainRootDir = `${config.dataDir}/${ctx.user.id}/chains/${chain._id.toString()}`; const channelConfigPath = `${chainRootDir}/tx`; const keyValueStorePath = `${chainRootDir}/client-kvs`; fs.ensureDirSync(channelConfigPath); diff --git a/user-dashboard/src/app/service/deploy.js b/user-dashboard/src/app/service/deploy.js new file mode 100644 index 00000000..636a6a5c --- /dev/null +++ b/user-dashboard/src/app/service/deploy.js @@ -0,0 +1,65 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +'use strict'; + +const Service = require('egg').Service; + +class DeployService extends Service { + async list() { + const { ctx } = this; + const status = ctx.query.status || ''; + const total = await ctx.model.SmartContractDeploy.count({ user: ctx.user.id }); + const instantiatedCount = await ctx.model.SmartContractDeploy.count({ user: ctx.user.id, status: 'instantiated' }); + const instantiatingCount = await ctx.model.SmartContractDeploy.count({ user: ctx.user.id, status: 'instantiating' }); + const errorCount = await ctx.model.SmartContractDeploy.count({ user: ctx.user.id, status: 'error' }); + let data = []; + if (status !== '') { + data = await ctx.model.SmartContractDeploy.find({ user: ctx.user.id, status }, '_id name status deployTime').populate('smartContractCode chain smartContract', '_id version name chainId type size').sort('-deployTime'); + } else { + data = await ctx.model.SmartContractDeploy.find({ user: ctx.user.id }, '_id name status deployTime').populate('smartContractCode chain smartContract', '_id version name chainId type size').sort('-deployTime'); + } + return { + total, + instantiatedCount, + instantiatingCount, + errorCount, + data, + }; + } + async query(id) { + const { ctx } = this; + const deploy = await ctx.model.SmartContractDeploy.findOne({ _id: id }, '_id name status deployTime').populate('smartContractCode chain smartContract', '_id version name chainId type size'); + if (!deploy) { + return { + success: false, + message: 'Deploy not found.', + }; + } + return { + success: true, + deploy, + }; + } + async invoke(functionName, args, deployId) { + const { ctx, config } = this; + const deploy = await ctx.model.SmartContractDeploy.findOne({ _id: deployId }).populate('smartContractCode smartContract chain'); + const chainId = deploy.chain._id.toString(); + const chainRootDir = `${config.dataDir}/${ctx.user.id}/chains/${chainId}`; + const keyValueStorePath = `${chainRootDir}/client-kvs`; + const network = await ctx.service.chain.generateNetwork(chainId); + return await ctx.invokeChainCode(network, keyValueStorePath, ['peer1'], config.default.channelName, deploy.name, functionName, args, ctx.user.username, 'org1'); + } + // async function queryChainCode(network, keyValueStorePath, peer, channelName, chainCodeName, fcn, args, username, org) { + async queryChainCode(functionName, args, deployId) { + const { ctx, config } = this; + const deploy = await ctx.model.SmartContractDeploy.findOne({ _id: deployId }).populate('smartContractCode smartContract chain'); + const chainId = deploy.chain._id.toString(); + const chainRootDir = `${config.dataDir}/${ctx.user.id}/chains/${chainId}`; + const keyValueStorePath = `${chainRootDir}/client-kvs`; + const network = await ctx.service.chain.generateNetwork(chainId); + return await ctx.queryChainCode(network, keyValueStorePath, 'peer1', config.default.channelName, deploy.name, functionName, args, ctx.user.username, 'org1'); + } +} + +module.exports = DeployService; diff --git a/user-dashboard/src/app/service/smart_contract.js b/user-dashboard/src/app/service/smart_contract.js index 5d88b0d0..bcacdef2 100644 --- a/user-dashboard/src/app/service/smart_contract.js +++ b/user-dashboard/src/app/service/smart_contract.js @@ -151,9 +151,9 @@ class SmartContractService extends Service { success: false, }; } - const codes = await ctx.model.SmartContractCode.find({ smartContract }, '_id version createTime'); - const newOperations = await ctx.model.SmartContractOperateHistory.find({ smartContract }, '_id operateTime status').populate('smartContractCode', 'version'); - const deploys = await ctx.model.SmartContractDeploy.find({ smartContract }, '_id status deployTime'); + const codes = await ctx.model.SmartContractCode.find({ smartContract }, '_id version createTime').sort('-createTime'); + const newOperations = await ctx.model.SmartContractOperateHistory.find({ smartContract }, '_id operateTime status').populate('smartContractCode', 'version').sort('-operateTime'); + const deploys = await ctx.model.SmartContractDeploy.find({ smartContract }, '_id name status deployTime').populate('smartContractCode chain', '_id version name chainId type size').sort('-deployTime'); return { success: true, info: smartContract, diff --git a/user-dashboard/src/config/config.default.js b/user-dashboard/src/config/config.default.js index 497f81ac..56930ba3 100644 --- a/user-dashboard/src/config/config.default.js +++ b/user-dashboard/src/config/config.default.js @@ -46,6 +46,12 @@ module.exports = appInfo => { }, ], }, + admins: [ + { + username: 'admin', + secret: 'adminpw', + }, + ], }, dataDir: '/opt/data', };