diff --git a/user-dashboard/src/.eslintrc b/user-dashboard/src/.eslintrc index b26d137f6..4de5b3da0 100644 --- a/user-dashboard/src/.eslintrc +++ b/user-dashboard/src/.eslintrc @@ -2,7 +2,8 @@ "extends": "eslint-config-egg", "rules": { "array-bracket-spacing": [0], - "no-extend-native": [0] + "no-extend-native": [0], + "no-bitwise": [0] }, "parser": "babel-eslint" } diff --git a/user-dashboard/src/.ui-eslintrc b/user-dashboard/src/.ui-eslintrc index bcd649f49..3879a4fdb 100644 --- a/user-dashboard/src/.ui-eslintrc +++ b/user-dashboard/src/.ui-eslintrc @@ -19,6 +19,7 @@ "react/jsx-no-bind": [0], "react/prop-types": [0], "react/prefer-stateless-function": [0], + "react/no-did-mount-set-state": [0], "react/jsx-wrap-multilines": [ "error", { diff --git a/user-dashboard/src/app/assets/src/common/menu.js b/user-dashboard/src/app/assets/src/common/menu.js index 2b4454013..355350a95 100644 --- a/user-dashboard/src/app/assets/src/common/menu.js +++ b/user-dashboard/src/app/assets/src/common/menu.js @@ -1,34 +1,52 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import { isUrl } from '../utils/utils'; +import { isUrl } from "../utils/utils"; const menuData = [ { - name: 'Chain', - icon: 'link', - path: 'chain', + name: "Chain", + icon: "link", + path: "chain", }, { - name: 'Apply Chain', - path: 'apply-chain', + name: "Apply Chain", + path: "apply-chain", hideInMenu: true, hideInBreadcrumb: false, }, { - name: 'Smart Contract', - path: 'smart-contract', - icon: 'code-o', - }, - { - name: 'Create New Smart Contract', - path: 'new-smart-contract', - hideInMenu: true, - hideInBreadcrumb: false, + name: "Smart Contract", + path: "smart-contract", + icon: "code-o", + children: [ + { + name: "List", + path: "index", + }, + { + name: "Info", + path: "info/:id", + hideInMenu: true, + hideInBreadcrumb: false, + }, + { + name: "Create", + path: "new", + hideInMenu: true, + hideInBreadcrumb: false, + }, + { + name: "New Code", + path: "new-code", + hideInMenu: true, + hideInBreadcrumb: false, + }, + ], }, ]; -function formatter(data, parentPath = '/', parentAuthority) { +function formatter(data, parentPath = "/", parentAuthority) { return data.map(item => { let { path } = item; if (!isUrl(path)) { @@ -40,7 +58,11 @@ function formatter(data, parentPath = '/', parentAuthority) { authority: item.authority || parentAuthority, }; if (item.children) { - result.children = formatter(item.children, `${parentPath}${item.path}/`, item.authority); + result.children = formatter( + item.children, + `${parentPath}${item.path}/`, + item.authority + ); } return result; }); diff --git a/user-dashboard/src/app/assets/src/common/router.js b/user-dashboard/src/app/assets/src/common/router.js index 3f13ee00e..afb0bb69b 100644 --- a/user-dashboard/src/app/assets/src/common/router.js +++ b/user-dashboard/src/app/assets/src/common/router.js @@ -1,24 +1,24 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import { createElement } from 'react'; -import dynamic from 'dva/dynamic'; -import pathToRegexp from 'path-to-regexp'; -import { getMenuData } from './menu'; +import { createElement } from "react"; +import dynamic from "dva/dynamic"; +import pathToRegexp from "path-to-regexp"; +import { getMenuData } from "./menu"; let routerDataCache; const modelNotExisted = (app, model) => // eslint-disable-next-line !app._models.some(({ namespace }) => { - return namespace === model.substring(model.lastIndexOf('/') + 1); + return namespace === model.substring(model.lastIndexOf("/") + 1); }); // wrapper of dynamic const dynamicWrapper = (app, models, component) => { // () => require('module') // transformed by babel-plugin-dynamic-import-node-sync - if (component.toString().indexOf('.then(') < 0) { + if (component.toString().indexOf(".then(") < 0) { models.forEach(model => { if (modelNotExisted(app, model)) { // eslint-disable-next-line @@ -39,7 +39,9 @@ const dynamicWrapper = (app, models, component) => { return dynamic({ app, models: () => - models.filter(model => modelNotExisted(app, model)).map(m => import(`../models/${m}.js`)), + models + .filter(model => modelNotExisted(app, model)) + .map(m => import(`../models/${m}.js`)), // add routerData prop component: () => { if (!routerDataCache) { @@ -72,35 +74,61 @@ function getFlatMenuData(menus) { export const getRouterData = app => { const routerConfig = { - '/': { - component: dynamicWrapper(app, ['user', 'login'], () => import('../layouts/BasicLayout')), + "/": { + component: dynamicWrapper(app, ["user", "login"], () => + import("../layouts/BasicLayout") + ), }, - '/chain': { - component: dynamicWrapper(app, ['chain'], () => import('../routes/Chain')), + "/chain": { + component: dynamicWrapper(app, ["chain"], () => import("../routes/Chain")), }, - '/apply-chain': { - component: dynamicWrapper(app, ['chain'], () => import('../routes/Chain/Apply')), + "/apply-chain": { + component: dynamicWrapper(app, ["chain"], () => + import("../routes/Chain/Apply") + ), }, - '/exception/403': { - component: dynamicWrapper(app, [], () => import('../routes/Exception/403')), + "/exception/403": { + component: dynamicWrapper(app, [], () => + import("../routes/Exception/403") + ), }, - '/exception/404': { - component: dynamicWrapper(app, [], () => import('../routes/Exception/404')), + "/exception/404": { + component: dynamicWrapper(app, [], () => + import("../routes/Exception/404") + ), }, - '/exception/500': { - component: dynamicWrapper(app, [], () => import('../routes/Exception/500')), + "/exception/500": { + component: dynamicWrapper(app, [], () => + import("../routes/Exception/500") + ), }, - '/user': { - component: dynamicWrapper(app, [], () => import('../layouts/UserLayout')), + "/user": { + component: dynamicWrapper(app, [], () => import("../layouts/UserLayout")), }, - '/user/login': { - component: dynamicWrapper(app, ['login'], () => import('../routes/User/Login')), + "/user/login": { + component: dynamicWrapper(app, ["login"], () => + import("../routes/User/Login") + ), }, - '/smart-contract': { - component: dynamicWrapper(app, ['smartContract'], () => import('../routes/SmartContract')), + "/smart-contract/index": { + component: dynamicWrapper(app, ["smartContract"], () => + import("../routes/SmartContract") + ), }, - '/new-smart-contract': { - component: dynamicWrapper(app, ['smartContract'], () => import('../routes/SmartContract/New')), + "/smart-contract/info/:id": { + component: dynamicWrapper(app, ["smartContract", "chain"], () => + import("../routes/SmartContract/Info") + ), + }, + "/smart-contract/new": { + component: dynamicWrapper(app, ["smartContract"], () => + import("../routes/SmartContract/New") + ), + }, + "/smart-contract/new-code/:id": { + component: dynamicWrapper(app, ["smartContract"], () => + import("../routes/SmartContract/New/code") + ), }, }; // Get name from ./menu.js or just set it in the router data. @@ -114,7 +142,9 @@ export const getRouterData = app => { // Regular match item name // eg. router /user/:id === /user/chen const pathRegexp = pathToRegexp(path); - const menuKey = Object.keys(menuData).find(key => pathRegexp.test(`${key}`)); + const menuKey = Object.keys(menuData).find(key => + pathRegexp.test(`${key}`) + ); let menuItem = {}; // If menuKey is not empty if (menuKey) { diff --git a/user-dashboard/src/app/assets/src/components/DescriptionList/Description.js b/user-dashboard/src/app/assets/src/components/DescriptionList/Description.js new file mode 100644 index 000000000..3e41c0717 --- /dev/null +++ b/user-dashboard/src/app/assets/src/components/DescriptionList/Description.js @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { Col } from 'antd'; +import styles from './index.less'; +import responsive from './responsive'; + +const Description = ({ term, column, className, children, ...restProps }) => { + const clsString = classNames(styles.description, className); + return ( + + {term &&
{term}
} + {children !== null && children !== undefined && +
{children}
} + + ); +}; + +Description.defaultProps = { + term: '', +}; + +Description.propTypes = { + term: PropTypes.node, +}; + +export default Description; diff --git a/user-dashboard/src/app/assets/src/components/DescriptionList/DescriptionList.js b/user-dashboard/src/app/assets/src/components/DescriptionList/DescriptionList.js new file mode 100644 index 000000000..382d7e85f --- /dev/null +++ b/user-dashboard/src/app/assets/src/components/DescriptionList/DescriptionList.js @@ -0,0 +1,31 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Row } from 'antd'; +import styles from './index.less'; + +const DescriptionList = ({ + className, + title, + col = 3, + layout = 'horizontal', + gutter = 32, + children, + size, + ...restProps +}) => { + const clsString = classNames(styles.descriptionList, styles[layout], className, { + [styles.small]: size === 'small', + [styles.large]: size === 'large', + }); + const column = col > 4 ? 4 : col; + return ( +
+ {title ?
{title}
: null} + + {React.Children.map(children, child => child ? React.cloneElement(child, { column }) : child)} + +
+ ); +}; + +export default DescriptionList; diff --git a/user-dashboard/src/app/assets/src/components/DescriptionList/index.js b/user-dashboard/src/app/assets/src/components/DescriptionList/index.js new file mode 100644 index 000000000..357f479fd --- /dev/null +++ b/user-dashboard/src/app/assets/src/components/DescriptionList/index.js @@ -0,0 +1,5 @@ +import DescriptionList from './DescriptionList'; +import Description from './Description'; + +DescriptionList.Description = Description; +export default DescriptionList; diff --git a/user-dashboard/src/app/assets/src/components/DescriptionList/index.less b/user-dashboard/src/app/assets/src/components/DescriptionList/index.less new file mode 100644 index 000000000..bcb6fd1da --- /dev/null +++ b/user-dashboard/src/app/assets/src/components/DescriptionList/index.less @@ -0,0 +1,77 @@ +@import '~antd/lib/style/themes/default.less'; + +.descriptionList { + // offset the padding-bottom of last row + :global { + .ant-row { + margin-bottom: -16px; + overflow: hidden; + } + } + + .title { + font-size: 14px; + color: @heading-color; + font-weight: 500; + margin-bottom: 16px; + } + + .term { + // Line-height is 22px IE dom height will calculate error + line-height: 20px; + padding-bottom: 16px; + margin-right: 8px; + color: @heading-color; + white-space: nowrap; + display: table-cell; + + &:after { + content: ':'; + margin: 0 8px 0 2px; + position: relative; + top: -0.5px; + } + } + + .detail { + line-height: 22px; + width: 100%; + padding-bottom: 16px; + color: @text-color; + display: table-cell; + } + + &.small { + // offset the padding-bottom of last row + :global { + .ant-row { + margin-bottom: -8px; + } + } + .title { + margin-bottom: 12px; + color: @text-color; + } + .term, + .detail { + padding-bottom: 8px; + } + } + + &.large { + .title { + font-size: 16px; + } + } + + &.vertical { + .term { + padding-bottom: 8px; + display: block; + } + + .detail { + display: block; + } + } +} diff --git a/user-dashboard/src/app/assets/src/components/DescriptionList/responsive.js b/user-dashboard/src/app/assets/src/components/DescriptionList/responsive.js new file mode 100644 index 000000000..a5aa73f78 --- /dev/null +++ b/user-dashboard/src/app/assets/src/components/DescriptionList/responsive.js @@ -0,0 +1,6 @@ +export default { + 1: { xs: 24 }, + 2: { xs: 24, sm: 12 }, + 3: { xs: 24, sm: 12, md: 8 }, + 4: { xs: 24, sm: 12, md: 6 }, +}; diff --git a/user-dashboard/src/app/assets/src/layouts/BasicLayout.js b/user-dashboard/src/app/assets/src/layouts/BasicLayout.js index 84e8f10dc..f2926b2a7 100644 --- a/user-dashboard/src/app/assets/src/layouts/BasicLayout.js +++ b/user-dashboard/src/app/assets/src/layouts/BasicLayout.js @@ -1,23 +1,23 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { Layout, Icon } from 'antd'; -import DocumentTitle from 'react-document-title'; -import { connect } from 'dva'; -import { Route, Redirect, Switch } from 'dva/router'; -import { ContainerQuery } from 'react-container-query'; -import classNames from 'classnames'; -import { enquireScreen } from 'enquire-js'; -import GlobalHeader from '../components/GlobalHeader'; -import GlobalFooter from '../components/GlobalFooter'; -import SiderMenu from '../components/SiderMenu'; -import NotFound from '../routes/Exception/404'; -import { getRoutes } from '../utils/utils'; -import Authorized from '../utils/Authorized'; -import { getMenuData } from '../common/menu'; -import logo from '../assets/logo.svg'; +import React, { Fragment } from "react"; +import PropTypes from "prop-types"; +import { Layout, Icon } from "antd"; +import DocumentTitle from "react-document-title"; +import { connect } from "dva"; +import { Route, Redirect, Switch } from "dva/router"; +import { ContainerQuery } from "react-container-query"; +import classNames from "classnames"; +import { enquireScreen } from "enquire-js"; +import GlobalHeader from "../components/GlobalHeader"; +import GlobalFooter from "../components/GlobalFooter"; +import SiderMenu from "../components/SiderMenu"; +import NotFound from "../routes/Exception/404"; +import { getRoutes } from "../utils/utils"; +import Authorized from "../utils/Authorized"; +import { getMenuData } from "../common/menu"; +import logo from "../assets/logo.svg"; const { Content, Header, Footer } = Layout; const { check } = Authorized; @@ -53,22 +53,22 @@ const getBreadcrumbNameMap = (menuData, routerData) => { }; const query = { - 'screen-xs': { + "screen-xs": { maxWidth: 575, }, - 'screen-sm': { + "screen-sm": { minWidth: 576, maxWidth: 767, }, - 'screen-md': { + "screen-md": { minWidth: 768, maxWidth: 991, }, - 'screen-lg': { + "screen-lg": { minWidth: 992, maxWidth: 1199, }, - 'screen-xl': { + "screen-xl": { minWidth: 1200, }, }; @@ -103,7 +103,7 @@ class BasicLayout extends React.PureComponent { getPageTitle() { const { routerData, location } = this.props; const { pathname } = location; - let title = 'Cello Operator Dashboard'; + let title = "Cello Operator Dashboard"; if (routerData[pathname] && routerData[pathname].name) { title = `${routerData[pathname].name} - Cello Operator Dashboard`; } @@ -113,16 +113,16 @@ class BasicLayout extends React.PureComponent { // According to the url parameter to redirect const urlParams = new URL(window.location.href); - const redirect = urlParams.searchParams.get('redirect'); + const redirect = urlParams.searchParams.get("redirect"); // Remove the parameters in the url if (redirect) { - urlParams.searchParams.delete('redirect'); - window.history.replaceState(null, 'redirect', urlParams.href); + urlParams.searchParams.delete("redirect"); + window.history.replaceState(null, "redirect", urlParams.href); } else { const { routerData } = this.props; // get the first authorized route path in routerData const authorizedPath = Object.keys(routerData).find( - item => check(routerData[item].authority, item) && item !== '/' + item => check(routerData[item].authority, item) && item !== "/" ); return authorizedPath; } @@ -130,13 +130,13 @@ class BasicLayout extends React.PureComponent { }; handleMenuCollapse = collapsed => { this.props.dispatch({ - type: 'global/changeLayoutCollapsed', + type: "global/changeLayoutCollapsed", payload: collapsed, }); }; handleMenuClick = ({ key }) => { - if (key === 'logout') { - window.location.href = '/logout'; + if (key === "logout") { + window.location.href = "/logout"; } }; render() { @@ -165,7 +165,7 @@ class BasicLayout extends React.PureComponent { onMenuClick={this.handleMenuClick} /> - + {redirectData.map(item => ( @@ -209,6 +209,6 @@ class BasicLayout extends React.PureComponent { export default connect(({ user, global, loading }) => ({ currentUser: user.currentUser, collapsed: global.collapsed, - fetchingNotices: loading.effects['global/fetchNotices'], + fetchingNotices: loading.effects["global/fetchNotices"], notices: global.notices, }))(BasicLayout); diff --git a/user-dashboard/src/app/assets/src/layouts/BlankLayout.js b/user-dashboard/src/app/assets/src/layouts/BlankLayout.js index d83a1b1e8..2a77a6346 100644 --- a/user-dashboard/src/app/assets/src/layouts/BlankLayout.js +++ b/user-dashboard/src/app/assets/src/layouts/BlankLayout.js @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React from "react"; export default props =>
; diff --git a/user-dashboard/src/app/assets/src/layouts/PageHeaderLayout.js b/user-dashboard/src/app/assets/src/layouts/PageHeaderLayout.js index 533f4b862..e37118c03 100644 --- a/user-dashboard/src/app/assets/src/layouts/PageHeaderLayout.js +++ b/user-dashboard/src/app/assets/src/layouts/PageHeaderLayout.js @@ -1,15 +1,18 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { Link } from 'dva/router'; -import PageHeader from '../components/PageHeader'; -import styles from './PageHeaderLayout.less'; +import React from "react"; +import { Link } from "dva/router"; +import { Spin } from "antd"; +import PageHeader from "../components/PageHeader"; +import styles from "./PageHeaderLayout.less"; -export default ({ children, wrapperClassName, top, ...restProps }) => ( -
- {top} - - {children ?
{children}
: null} -
+export default ({ children, loading, wrapperClassName, top, ...restProps }) => ( + +
+ {top} + + {children ?
{children}
: null} +
+
); diff --git a/user-dashboard/src/app/assets/src/layouts/PageHeaderLayout.less b/user-dashboard/src/app/assets/src/layouts/PageHeaderLayout.less index 39a449657..a0c0a6efe 100644 --- a/user-dashboard/src/app/assets/src/layouts/PageHeaderLayout.less +++ b/user-dashboard/src/app/assets/src/layouts/PageHeaderLayout.less @@ -1,4 +1,4 @@ -@import '~antd/lib/style/themes/default.less'; +@import "~antd/lib/style/themes/default.less"; .content { margin: 24px 24px 0; diff --git a/user-dashboard/src/app/assets/src/layouts/UserLayout.js b/user-dashboard/src/app/assets/src/layouts/UserLayout.js index da2bc9d97..5ab656b45 100644 --- a/user-dashboard/src/app/assets/src/layouts/UserLayout.js +++ b/user-dashboard/src/app/assets/src/layouts/UserLayout.js @@ -1,15 +1,15 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import React, { Fragment } from 'react'; -import { Link, Redirect, Switch, Route } from 'dva/router'; -import DocumentTitle from 'react-document-title'; -import Particles from 'react-particles-js'; -import { Icon } from 'antd'; -import GlobalFooter from '../components/GlobalFooter'; -import styles from './UserLayout.less'; -import logo from '../assets/logo.svg'; -import { getRoutes } from '../utils/utils'; +import React, { Fragment } from "react"; +import { Link, Redirect, Switch, Route } from "dva/router"; +import DocumentTitle from "react-document-title"; +import Particles from "react-particles-js"; +import { Icon } from "antd"; +import GlobalFooter from "../components/GlobalFooter"; +import styles from "./UserLayout.less"; +import logo from "../assets/logo.svg"; +import { getRoutes } from "../utils/utils"; const copyright = ( @@ -21,7 +21,7 @@ class UserLayout extends React.PureComponent { getPageTitle() { const { routerData, location } = this.props; const { pathname } = location; - let title = 'Cello User Dashboard'; + let title = "Cello User Dashboard"; if (routerData[pathname] && routerData[pathname].name) { title = `${routerData[pathname].name} - Cello User Dashboard`; } @@ -30,105 +30,105 @@ class UserLayout extends React.PureComponent { render() { const { routerData, match } = this.props; const particlesParams = { - "particles": { - "number": { - "value": 10, - "density": { - "enable": true, - "value_area": 800, + particles: { + number: { + value: 10, + density: { + enable: true, + value_area: 800, }, }, - "color": { - "value": "#40a9ff", + color: { + value: "#40a9ff", }, - "shape": { - "type": "polygon", - "stroke": { - "width": 0, - "color": "#000000", + shape: { + type: "polygon", + stroke: { + width: 0, + color: "#000000", }, - "polygon": { - "nb_sides": 5, + polygon: { + nb_sides: 5, }, }, - "opacity": { - "value": 0.5, - "random": false, - "anim": { - "enable": false, - "speed": 1, - "opacity_min": 0.1, - "sync": false, + opacity: { + value: 0.5, + random: false, + anim: { + enable: false, + speed: 1, + opacity_min: 0.1, + sync: false, }, }, - "size": { - "value": 3, - "random": true, - "anim": { - "enable": false, - "speed": 40, - "size_min": 0.1, - "sync": false, + size: { + value: 3, + random: true, + anim: { + enable: false, + speed: 40, + size_min: 0.1, + sync: false, }, }, - "line_linked": { - "enable": true, - "distance": 150, - "color": "#40a9ff", - "opacity": 0.4, - "width": 1, + line_linked: { + enable: true, + distance: 150, + color: "#40a9ff", + opacity: 0.4, + width: 1, }, - "move": { - "enable": true, - "speed": 6, - "direction": "none", - "random": false, - "straight": false, - "out_mode": "out", - "bounce": false, - "attract": { - "enable": false, - "rotateX": 600, - "rotateY": 1200, + move: { + enable: true, + speed: 6, + direction: "none", + random: false, + straight: false, + out_mode: "out", + bounce: false, + attract: { + enable: false, + rotateX: 600, + rotateY: 1200, }, }, }, - "interactivity": { - "detect_on": "canvas", - "events": { - "onhover": { - "enable": true, - "mode": "repulse", + interactivity: { + detect_on: "canvas", + events: { + onhover: { + enable: true, + mode: "repulse", }, - "onclick": { - "enable": true, - "mode": "push", + onclick: { + enable: true, + mode: "push", }, - "resize": true, + resize: true, }, - "modes": { - "grab": { - "distance": 400, - "line_linked": { - "opacity": 1, + modes: { + grab: { + distance: 400, + line_linked: { + opacity: 1, }, }, - "bubble": { - "distance": 400, - "size": 40, - "duration": 2, - "opacity": 8, - "speed": 3, + bubble: { + distance: 400, + size: 40, + duration: 2, + opacity: 8, + speed: 3, }, - "repulse": { - "distance": 200, - "duration": 0.4, + repulse: { + distance: 200, + duration: 0.4, }, - "push": { - "particles_nb": 4, + push: { + particles_nb: 4, }, - "remove": { - "particles_nb": 2, + remove: { + particles_nb: 2, }, }, }, diff --git a/user-dashboard/src/app/assets/src/layouts/UserLayout.less b/user-dashboard/src/app/assets/src/layouts/UserLayout.less index 4965e7ea7..97537a3c7 100644 --- a/user-dashboard/src/app/assets/src/layouts/UserLayout.less +++ b/user-dashboard/src/app/assets/src/layouts/UserLayout.less @@ -1,4 +1,4 @@ -@import '~antd/lib/style/themes/default.less'; +@import "~antd/lib/style/themes/default.less"; .container { display: flex; @@ -40,7 +40,7 @@ .title { font-size: 33px; color: @heading-color; - font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif; + font-family: "Myriad Pro", "Helvetica Neue", Arial, Helvetica, sans-serif; font-weight: 600; position: relative; top: 2px; @@ -57,4 +57,4 @@ width: 100%; position: fixed; top: 0; -} \ No newline at end of file +} diff --git a/user-dashboard/src/app/assets/src/locales/en-US.js b/user-dashboard/src/app/assets/src/locales/en-US.js index 45e1217c8..b3f111a37 100755 --- a/user-dashboard/src/app/assets/src/locales/en-US.js +++ b/user-dashboard/src/app/assets/src/locales/en-US.js @@ -1,15 +1,15 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import antdEn from 'antd/lib/locale-provider/en_US'; -import appLocaleData from 'react-intl/locale-data/en'; -import enMessages from './en.json'; +import antdEn from "antd/lib/locale-provider/en_US"; +import appLocaleData from "react-intl/locale-data/en"; +import enMessages from "./en.json"; export default { messages: { ...enMessages, }, antd: antdEn, - locale: 'en-US', + locale: "en-US", data: appLocaleData, }; diff --git a/user-dashboard/src/app/assets/src/locales/en.json b/user-dashboard/src/app/assets/src/locales/en.json index cf5b92ec8..157844428 100755 --- a/user-dashboard/src/app/assets/src/locales/en.json +++ b/user-dashboard/src/app/assets/src/locales/en.json @@ -5,20 +5,29 @@ "Menu.Chain": "Chain", "Menu.UserManagement": "User Management", "Messages.RequestError": "Request Error", - "Messages.HttpStatus.200": "The server successfully returned the requested data.", + "Messages.HttpStatus.200": + "The server successfully returned the requested data.", "Messages.HttpStatus.201": "New or modified data is successful.", - "Messages.HttpStatus.202": "A request has entered the background queue (asynchronous task).", + "Messages.HttpStatus.202": + "A request has entered the background queue (asynchronous task).", "Messages.HttpStatus.204": "Delete data successfully.", - "Messages.HttpStatus.400": "The request was issued with an error. The server did not create or modify data.", - "Messages.HttpStatus.401": "User does not have permission (token, username, password is wrong).", + "Messages.HttpStatus.400": + "The request was issued with an error. The server did not create or modify data.", + "Messages.HttpStatus.401": + "User does not have permission (token, username, password is wrong).", "Messages.HttpStatus.403": "User authorized, but access is forbidden.", - "Messages.HttpStatus.404": "The request was issued for a non-existent record and the server did not perform the operation.", + "Messages.HttpStatus.404": + "The request was issued for a non-existent record and the server did not perform the operation.", "Messages.HttpStatus.406": "The requested format is not available.", - "Messages.HttpStatus.410": "The requested resource is permanently deleted and will no longer be available.", - "Messages.HttpStatus.422": "A validation error occurred when creating an object.", - "Messages.HttpStatus.500": "An error occurred on the server. Please check the server.", + "Messages.HttpStatus.410": + "The requested resource is permanently deleted and will no longer be available.", + "Messages.HttpStatus.422": + "A validation error occurred when creating an object.", + "Messages.HttpStatus.500": + "An error occurred on the server. Please check the server.", "Messages.HttpStatus.502": "Bad gateway.", - "Messages.HttpStatus.503": "The service is unavailable and the server is temporarily overloaded or maintained.", + "Messages.HttpStatus.503": + "The service is unavailable and the server is temporarily overloaded or maintained.", "Messages.HttpStatus.504": "Gateway timeout.", "Login.Button.Login": "Login", "Login.Placeholder.Username": "Username", diff --git a/user-dashboard/src/app/assets/src/locales/zh-CN.js b/user-dashboard/src/app/assets/src/locales/zh-CN.js index 90e2ce096..9427d5ce9 100755 --- a/user-dashboard/src/app/assets/src/locales/zh-CN.js +++ b/user-dashboard/src/app/assets/src/locales/zh-CN.js @@ -1,15 +1,15 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import appLocaleData from 'react-intl/locale-data/zh'; -import zhCN from 'antd/lib/locale-provider/zh_CN'; -import zhMessages from './zh.json'; +import appLocaleData from "react-intl/locale-data/zh"; +import zhCN from "antd/lib/locale-provider/zh_CN"; +import zhMessages from "./zh.json"; export default { messages: { ...zhMessages, }, antd: zhCN, - locale: 'zh-Hans', + locale: "zh-Hans", data: appLocaleData, }; diff --git a/user-dashboard/src/app/assets/src/locales/zh.json b/user-dashboard/src/app/assets/src/locales/zh.json index 30fd6628a..c3ea5184b 100755 --- a/user-dashboard/src/app/assets/src/locales/zh.json +++ b/user-dashboard/src/app/assets/src/locales/zh.json @@ -9,10 +9,12 @@ "Messages.HttpStatus.201": "新建或修改数据成功。", "Messages.HttpStatus.202": "一个请求已经进入后台排队(异步任务)。", "Messages.HttpStatus.204": "删除数据成功。", - "Messages.HttpStatus.400": "发出的请求有错误,服务器没有进行新建或修改数据的操作。", + "Messages.HttpStatus.400": + "发出的请求有错误,服务器没有进行新建或修改数据的操作。", "Messages.HttpStatus.401": "用户没有权限(令牌、用户名、密码错误)。", "Messages.HttpStatus.403": "用户得到授权,但是访问是被禁止的。", - "Messages.HttpStatus.404": "发出的请求针对的是不存在的记录,服务器没有进行操作。", + "Messages.HttpStatus.404": + "发出的请求针对的是不存在的记录,服务器没有进行操作。", "Messages.HttpStatus.406": "请求的格式不可得。", "Messages.HttpStatus.410": "请求的资源被永久删除,且不会再得到的。", "Messages.HttpStatus.422": "当创建一个对象时,发生一个验证错误。", diff --git a/user-dashboard/src/app/assets/src/models/chain.js b/user-dashboard/src/app/assets/src/models/chain.js index 459180926..e3c27914b 100644 --- a/user-dashboard/src/app/assets/src/models/chain.js +++ b/user-dashboard/src/app/assets/src/models/chain.js @@ -1,12 +1,12 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import { routerRedux } from 'dva/router'; -import { message } from 'antd'; -import { queryChains, release, apply } from '../services/chain'; +import { routerRedux } from "dva/router"; +import { message } from "antd"; +import { queryChains, release, apply } from "../services/chain"; export default { - namespace: 'chain', + namespace: "chain", state: { chains: [], @@ -16,26 +16,26 @@ export default { *fetch(_, { call, put }) { const response = yield call(queryChains); yield put({ - type: 'setChains', + type: "setChains", payload: response.data, - }) + }); }, *release({ payload }, { call, put }) { const response = yield call(release, payload.id); if (JSON.parse(response).success) { - message.success('Release Chain successfully'); + message.success("Release Chain successfully"); yield put({ - type: 'fetch', - }) + type: "fetch", + }); } }, *apply({ payload }, { call, put }) { - const response = yield call(apply, payload); - if (response.success) { - message.success('Apply Chain successfully'); + const response = yield call(apply, payload); + if (response.success) { + message.success("Apply Chain successfully"); yield put( routerRedux.push({ - pathname: '/chain', + pathname: "/chain", }) ); } diff --git a/user-dashboard/src/app/assets/src/models/error.js b/user-dashboard/src/app/assets/src/models/error.js index 94162c1f9..da1d704a7 100644 --- a/user-dashboard/src/app/assets/src/models/error.js +++ b/user-dashboard/src/app/assets/src/models/error.js @@ -1,14 +1,14 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import { routerRedux } from 'dva/router'; -import { query } from '../services/error'; +import { routerRedux } from "dva/router"; +import { query } from "../services/error"; export default { - namespace: 'error', + namespace: "error", state: { - error: '', + error: "", isloading: false, }, @@ -18,7 +18,7 @@ export default { // redirect on client when network broken yield put(routerRedux.push(`/exception/${payload.code}`)); yield put({ - type: 'trigger', + type: "trigger", payload: payload.code, }); }, diff --git a/user-dashboard/src/app/assets/src/models/global.js b/user-dashboard/src/app/assets/src/models/global.js index 63bfbc800..086f55170 100644 --- a/user-dashboard/src/app/assets/src/models/global.js +++ b/user-dashboard/src/app/assets/src/models/global.js @@ -2,7 +2,7 @@ SPDX-License-Identifier: Apache-2.0 */ export default { - namespace: 'global', + namespace: "global", state: { collapsed: false, @@ -12,12 +12,12 @@ export default { effects: { *clearNotices({ payload }, { put, select }) { yield put({ - type: 'saveClearedNotices', + type: "saveClearedNotices", payload, }); const count = yield select(state => state.global.notices.length); yield put({ - type: 'user/changeNotifyCount', + type: "user/changeNotifyCount", payload: count, }); }, @@ -48,8 +48,8 @@ export default { setup({ history }) { // Subscribe history(url) change, trigger `load` action if pathname is `/` return history.listen(({ pathname, search }) => { - if (typeof window.ga !== 'undefined') { - window.ga('send', 'pageview', pathname + search); + if (typeof window.ga !== "undefined") { + window.ga("send", "pageview", pathname + search); } }); }, diff --git a/user-dashboard/src/app/assets/src/models/index.js b/user-dashboard/src/app/assets/src/models/index.js index 6eb7f2a29..c22dbe41a 100644 --- a/user-dashboard/src/app/assets/src/models/index.js +++ b/user-dashboard/src/app/assets/src/models/index.js @@ -3,8 +3,8 @@ */ // Use require.context to require reducers automatically // Ref: https://webpack.js.org/guides/dependency-management/#require-context -const context = require.context('./', false, /\.js$/); +const context = require.context("./", false, /\.js$/); export default context .keys() - .filter(item => item !== './index.js') + .filter(item => item !== "./index.js") .map(key => context(key)); diff --git a/user-dashboard/src/app/assets/src/models/login.js b/user-dashboard/src/app/assets/src/models/login.js index d20424cdf..d60db1dee 100644 --- a/user-dashboard/src/app/assets/src/models/login.js +++ b/user-dashboard/src/app/assets/src/models/login.js @@ -1,11 +1,11 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import { login } from '../services/user'; -import { setAuthority } from '../utils/authority'; +import { login } from "../services/user"; +import { setAuthority } from "../utils/authority"; export default { - namespace: 'login', + namespace: "login", state: { status: undefined, diff --git a/user-dashboard/src/app/assets/src/models/smartContract.js b/user-dashboard/src/app/assets/src/models/smartContract.js index 777b54829..970b6b964 100644 --- a/user-dashboard/src/app/assets/src/models/smartContract.js +++ b/user-dashboard/src/app/assets/src/models/smartContract.js @@ -1,27 +1,37 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import { querySmartContracts, deleteSmartContractCode, updateSmartContractCode, deleteSmartContract } from '../services/smart_contract'; +import { + querySmartContracts, + deleteSmartContractCode, + updateSmartContractCode, + deleteSmartContract, + querySmartContract, + deploySmartContract, +} from "../services/smart_contract"; export default { - namespace: 'smartContract', + namespace: "smartContract", state: { smartContracts: [], + currentSmartContract: {}, + codes: [], + newOperations: [], }, effects: { *fetch(_, { call, put }) { const response = yield call(querySmartContracts); yield put({ - type: 'setSmartContracts', + type: "setSmartContracts", payload: response.data, - }) + }); }, *deleteSmartContractCode({ payload }, { call }) { yield call(deleteSmartContractCode, payload.id); if (payload.callback) { - yield call(payload.callback) + yield call(payload.callback); } }, *updateSmartContractCode({ payload }, { call }) { @@ -33,6 +43,25 @@ export default { }); } }, + *querySmartContract({ payload }, { call, put }) { + const response = yield call(querySmartContract, payload.id); + if (response.success) { + yield put({ + type: "setCurrentSmartContract", + payload: { + currentSmartContract: response.info, + codes: response.codes, + newOperations: response.newOperations, + }, + }); + } + }, + *deploySmartContract({ payload }, { call }) { + const response = yield call(deploySmartContract, payload); + if (payload.callback) { + yield call(payload.callback, response); + } + }, *deleteSmartContract({ payload }, { call }) { yield call(deleteSmartContract, payload.id); if (payload.callback) { @@ -48,5 +77,14 @@ export default { smartContracts: action.payload, }; }, + setCurrentSmartContract(state, action) { + const { currentSmartContract, codes, newOperations } = action.payload; + return { + ...state, + currentSmartContract, + codes, + newOperations, + }; + }, }, }; diff --git a/user-dashboard/src/app/assets/src/models/user.js b/user-dashboard/src/app/assets/src/models/user.js index 4e9bfa6f8..4d5332ffd 100644 --- a/user-dashboard/src/app/assets/src/models/user.js +++ b/user-dashboard/src/app/assets/src/models/user.js @@ -1,10 +1,10 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import { query as queryUsers, queryCurrent } from '../services/user'; +import { query as queryUsers, queryCurrent } from "../services/user"; export default { - namespace: 'user', + namespace: "user", state: { list: [], @@ -15,14 +15,14 @@ export default { *fetch(_, { call, put }) { const response = yield call(queryUsers); yield put({ - type: 'save', + type: "save", payload: response, }); }, *fetchCurrent(_, { call, put }) { const response = yield call(queryCurrent); yield put({ - type: 'saveCurrentUser', + type: "saveCurrentUser", payload: response, }); }, diff --git a/user-dashboard/src/app/assets/src/routes/SmartContract/Info/deploy.js b/user-dashboard/src/app/assets/src/routes/SmartContract/Info/deploy.js new file mode 100644 index 000000000..e3eb2cc55 --- /dev/null +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/Info/deploy.js @@ -0,0 +1,314 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React, { Component, Fragment } from 'react'; +import { + Card, + Popover, + Steps, + Form, + Select, + Button, + message, + Input, +} from 'antd'; +import classNames from 'classnames'; +import styles from './index.less'; + +const { Step } = Steps; +const FormItem = Form.Item; +const { Option } = Select; + +@Form.create() +export default class Deploy extends Component { + constructor(props) { + super(props); + const current = props.current || 0; + const version = props.version || ''; + const versionId = props.versionId || ''; + this.state = { + stepDirection: 'horizontal', + current, + version, + versionId, + chainName: '', + chainId: '', + deployId: '', + installing: false, + instantiating: false, + } + } + changeVersion = value => { + const { codes } = this.props; + let codeVersion = this.state.version; + codes.forEach(code => { + if (code._id === value) { + codeVersion = code.version; + return false; + } + }); + this.setState({ + versionId: value, + version: codeVersion, + }) + }; + changeChain = value => { + const { chains } = this.props; + let { chainName } = this.state; + chains.forEach(chain => { + if (chain._id === value) { + chainName = chain.name; + return false; + } + }); + this.setState({ + chainId: value, + chainName, + }) + + }; + installCallback = (response) => { + if (response.success) { + message.success('Install chain code successfully.'); + const current = this.state.current + 1; + this.setState({ + current, + deployId: response.deployId, + }); + } else { + message.error('Install chain code failed.'); + } + this.setState({ + installing: false, + }) + }; + instantiateCallback = (response) => { + const { onDeployDone } = this.props; + if (response.success) { + message.success('Instantiate chain code successfully.'); + onDeployDone(); + } else { + message.error('Instantiate chain code failed.'); + } + this.setState({ + instantiating: false, + }) + }; + next = () => { + let { current } = this.state; + const { chainId, versionId, deployId } = this.state; + const { onDeploy } = this.props; + const { installCallback, instantiateCallback } = this; + switch (current) { + case 1: + this.setState({ + installing: true, + }, () => { + onDeploy({ + id: versionId, + chainId, + operation: 'install', + callback: installCallback, + }); + }); + break; + case 2: + this.props.form.validateFieldsAndScroll({ force: true }, (err, values) => { + if (!err) { + let { functionName, args } = values; + functionName = functionName === "" ? null : functionName; + args = args.split(','); + this.setState({ + instantiating: true, + }, () => { + onDeploy({ + id: versionId, + chainId, + operation: 'instantiate', + deployId, + callback: instantiateCallback, + functionName, + args, + }) + }); + } + }); + break; + default: + current += 1; + break; + } + this.setState({ current }); + }; + prev = () => { + const current = this.state.current - 1; + this.setState({ current }); + }; + + render() { + const { stepDirection, current, version, versionId, chainName, chainId, installing, instantiating } = this.state; + const { chains, codes } = this.props; + const { getFieldDecorator } = this.props.form; + const versionOptions = codes.map(code => ); + const chainOptions = chains.map(chain => ); + const versionDesc = ( +
+ + {current > 0 && version} + +
+ ); + const chainDesc = ( +
+ + {current > 1 && chainName} + +
+ ); + const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 4 }, + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 20 }, + }, + }; + + const steps = [ + { + title: 'Version', + content: ( + + + + ), + description: versionDesc, + help: 'Select code version to deploy', + }, + { + title: 'Install', + content: ( + + + + ), + description: chainDesc, + help: 'Select network to install', + }, + { + title: 'Instantiate', + content: ( +
+ + {getFieldDecorator('functionName', { + initialValue: '', + })()} + + + {getFieldDecorator('args', { + initialValue: '', + rules: [ + { + required: true, + message: 'Must input arguments', + }, + ], + })()} + +
+ ), + help: 'Input function & arguments to instantiate', + }, + ]; + + const popoverContent = ( +
+ {steps[current].help} +
+ ); + + const customDot = (dot, { status }) => + status === 'process' ? ( + + {dot} + + ) : ( + dot + ); + + function getNextText() { + switch (current) { + case 0: + return 'Next'; + case 1: + return 'Install'; + case 2: + return 'Instantiate'; + default: + return 'Next'; + } + } + function getLoading() { + switch (current) { + case 1: + return installing; + case 2: + return instantiating; + default: + return false; + } + } + function checkDisabled() { + switch (current) { + case 1: + return chainId === ''; + default: + return false; + } + } + return ( + + + {steps.map(item => )} + +
+
+ {steps[current].content} +
+
+
+ { + current > 0 + && ( + +)} + { + (current < steps.length - 1 || current === steps.length - 1) + && + + } +
+
+ ); + } +} 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 new file mode 100644 index 000000000..599c0718a --- /dev/null +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/Info/detail.js @@ -0,0 +1,65 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React, { Component, Fragment } from 'react'; +import { + Card, + Button, + Table, + Divider, +} from 'antd'; +import moment from 'moment'; +import styles from './index.less'; + + +export default class Detail extends Component { + + render() { + const { codes, loadingInfo, onAddNewCode, onDeploy } = this.props; + const codeColumns = [ + { + title: 'Version', + dataIndex: 'version', + key: 'version', + }, + { + title: 'Create Time', + dataIndex: 'createTime', + key: 'createTime', + render: text => moment(text).format('YYYY-MM-DD HH:mm:ss'), + }, + { + title: 'Operate', + render: ( text, record ) => ( + + onDeploy(record)}>Deploy + + Delete + + ), + }, + ]; + return ( +
+ +
+
+ +
+ + + + + ); + } +} diff --git a/user-dashboard/src/app/assets/src/routes/SmartContract/Info/history.js b/user-dashboard/src/app/assets/src/routes/SmartContract/Info/history.js new file mode 100644 index 000000000..4f42ebded --- /dev/null +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/Info/history.js @@ -0,0 +1,79 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React, { Component } from 'react'; +import moment from 'moment'; +import { + Card, + Badge, + Table, +} from 'antd'; +import styles from './index.less'; + +const operationTabList = [ + { + key: 'newOperations', + tab: 'History of create new code', + }, +]; + +const newOperationColumns = [ + { + title: 'Code Version', + dataIndex: 'smartContractCode', + key: 'smartContractCode', + render: item => item.version, + }, + { + title: 'Operate Time', + dataIndex: 'operateTime', + key: 'operateTime', + render: text => moment(text).format('YYYY-MM-DD HH:mm:ss'), + }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + render: text => + text === 'success' ? ( + + ) : ( + + ), + }, +]; + +export default class History extends Component { + state = { + operationKey: 'newOperations', + }; + + onOperationTabChange = key => { + this.setState({ operationKey: key }); + }; + + render() { + const { loading, newOperations } = this.props; + const contentList = { + newOperations: ( +
+ ), + }; + + return ( + + {contentList[this.state.operationKey]} + + ); + } +} 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 new file mode 100644 index 000000000..5ea85fea9 --- /dev/null +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/Info/index.js @@ -0,0 +1,205 @@ +/* + 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 { routerRedux } from 'dva/router'; +import { + Button, + Icon, + Tag, +} from 'antd'; +import DescriptionList from 'components/DescriptionList'; +import PageHeaderLayout from '../../../layouts/PageHeaderLayout'; +import styles from './index.less'; +import History from './history'; +import Deploy from './deploy'; +import Detail from './detail' + +const { Description } = DescriptionList; + +const getWindowWidth = () => window.innerWidth || document.documentElement.clientWidth; + +const action = ( + + + +); + +const tabList = [ + { + key: 'detail', + tab: 'Detail', + }, + { + key: 'history', + tab: 'History', + }, + { + key: 'deploy', + tab: 'Deploy', + }, +]; + +@connect(({ smartContract, chain, loading }) => ({ + smartContract, + chain, + loadingInfo: loading.effects['smartContract/querySmartContract'], +})) +export default class AdvancedProfile extends Component { + state = { + operationKey: 'detail', + stepDirection: 'horizontal', + selectedVersion: '', + selectedVersionId: '', + deployStep: 0, + }; + + componentDidMount() { + const { location, dispatch } = this.props; + const info = pathToRegexp('/smart-contract/info/:id').exec(location.pathname); + if (info) { + const id = info[1]; + dispatch({ + type: 'smartContract/querySmartContract', + payload: { + id, + }, + }); + } + this.props.dispatch({ + type: 'chain/fetch', + }); + this.setStepDirection(); + window.addEventListener('resize', this.setStepDirection); + } + + componentWillUnmount() { + window.removeEventListener('resize', this.setStepDirection); + this.setStepDirection.cancel(); + } + + onOperationTabChange = key => { + this.setState({ + operationKey: key, + deployStep: 0, + selectedVersion: '', + selectedVersionId: '', + }); + }; + + onAddNewCode = () => { + const { smartContract, dispatch } = this.props; + const { currentSmartContract } = smartContract; + dispatch(routerRedux.push({ + pathname: `/smart-contract/new-code/${currentSmartContract._id}`, + })); + }; + onClickDeploy = (smartContractCode) => { + this.setState({ + deployStep: 1, + selectedVersion: smartContractCode.version, + selectedVersionId: smartContractCode._id, + operationKey: 'deploy', + }); + }; + onDeploy = (payload) => { + this.props.dispatch({ + type: 'smartContract/deploySmartContract', + payload, + }) + }; + onDeployDone = () => { + const { dispatch } = this.props; + const { smartContract } = this.props; + const { currentSmartContract } = smartContract; + dispatch({ + type: 'smartContract/querySmartContract', + payload: { + id: currentSmartContract._id, + }, + }); + this.onOperationTabChange('detail'); + }; + + @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', + }); + } + } + render() { + const { smartContract, chain: { chains }, loadingInfo } = this.props; + const { currentSmartContract, codes, newOperations } = smartContract; + const { deployStep, selectedVersion, selectedVersionId } = this.state; + const versions = codes.map(code => code.version); + const versionTags = versions.map(version => {version}); + + const detailProps = { + codes, + loadingInfo, + onAddNewCode: this.onAddNewCode, + onDeploy: this.onClickDeploy, + }; + const deployProps = { + version: selectedVersion, + versionId: selectedVersionId, + current: deployStep, + chains, + codes, + onDeploy: this.onDeploy, + onDeployDone: this.onDeployDone, + currentSmartContract, + }; + + const contentList = { + detail: ( + + ), + history: ( + + ), + deploy: ( + + ), + }; + + const description = ( + + {currentSmartContract && moment(currentSmartContract.createTime).format('YYYY-MM-DD HH:mm:ss')} + {currentSmartContract && currentSmartContract.description} + {versionTags} + + ); + + 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/Info/index.less b/user-dashboard/src/app/assets/src/routes/SmartContract/Info/index.less new file mode 100644 index 000000000..160b02638 --- /dev/null +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/Info/index.less @@ -0,0 +1,109 @@ +@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; +} 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 new file mode 100644 index 000000000..38e1021e0 --- /dev/null +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/New/code.js @@ -0,0 +1,213 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React, { PureComponent } from 'react'; +import { Card, Form, Input, Button, Upload, Icon, message } from 'antd'; +import { routerRedux } from 'dva/router'; +import { connect } from 'dva'; +import PropTypes from 'prop-types'; +import { injectIntl } from 'react-intl'; +import pathToRegexp from 'path-to-regexp'; +import PageHeaderLayout from '../../../layouts/PageHeaderLayout'; +import config from '../../../utils/config'; + +const FormItem = Form.Item; + +@connect(({ smartContract }) => ({ + smartContract, +})) +@Form.create() +class NewSmartContractCode extends PureComponent { + static contextTypes = { + routes: PropTypes.array, + params: PropTypes.object, + location: PropTypes.object, + }; + state = { + submitting: false, + smartContractId: '', + smartContractCodeId: '', + }; + componentDidMount() { + const { location } = this.props; + const info = pathToRegexp('/smart-contract/new-code/:id').exec(location.pathname); + if (info) { + const id = info[1]; + this.setState({ + smartContractId: id, + }); + } + } + onRemoveFile = () => { + const { smartContractCodeId } = this.state; + this.props.dispatch({ + type: 'smartContract/deleteSmartContractCode', + payload: { + id: smartContractCodeId, + callback: this.deleteCallback, + }, + }) + }; + onUploadFile = info => { + if (info.file.status === 'done') { + const { response } = info.file; + if (response.success) { + this.setState({ + smartContractCodeId: response.id, + }); + } else { + message.error("Upload smart contract file failed"); + } + } else if (info.file.status === 'error') { + message.error('Upload smart contract file failed.') + } + }; + submitCallback = ({ payload, success }) => { + const { smartContractId } = this.state; + this.setState({ + submitting: false, + }); + if (success) { + message.success(`Create new smart contract version ${payload.version} successfully.`); + this.props.dispatch( + routerRedux.push({ + pathname: `/smart-contract/info/${smartContractId}`, + }) + ); + } else { + message.error(`Create new smart contract version ${payload.version} failed.`); + } + }; + clickCancel = () => { + const { smartContractCodeId } = this.state; + if (smartContractCodeId !== '') { + this.props.dispatch({ + type: 'smartContract/deleteSmartContractCode', + payload: { + id: smartContractCodeId, + }, + }); + } + this.props.dispatch( + routerRedux.push({ + pathname: '/smart-contract', + }) + ); + }; + handleSubmit = e => { + const { smartContractCodeId } = this.state; + e.preventDefault(); + this.props.form.validateFieldsAndScroll({ force: true }, (err, values) => { + if (!err) { + this.props.dispatch({ + type: 'smartContract/updateSmartContractCode', + payload: { + id: smartContractCodeId, + ...values, + callback: this.submitCallback, + }, + }) + } + }); + }; + normFile = () => { + return this.state.smartContractCodeId; + }; + validateUpload = (rule, value, callback) => { + const { smartContractCodeId } = this.state; + if (smartContractCodeId === '') { + callback('Must upload smart contract zip file'); + } + callback(); + }; + deleteCallback = () => { + this.setState({ + smartContractCodeId: '', + }); + }; + render() { + const { getFieldDecorator } = this.props.form; + const { submitting, smartContractCodeId, smartContractId } = this.state; + const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 7 }, + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 12 }, + md: { span: 10 }, + }, + }; + + const submitFormLayout = { + wrapperCol: { + xs: { span: 24, offset: 0 }, + sm: { span: 10, offset: 7 }, + }, + }; + const uploadProps = { + name: "smart_contract", + accept: '.zip', + action: `${config.url.smartContract.upload}?_csrf=${window.csrf}&id=${smartContractId}`, + onChange: this.onUploadFile, + onRemove: this.onRemoveFile, + beforeUpload() { + return smartContractCodeId === ''; + }, + }; + + return ( + + +
+ + {getFieldDecorator('version', { + initialValue: '', + rules: [ + { + required: true, + message: 'Must input version of smart contract', + }, + ], + })()} + + + {getFieldDecorator('smartContractCode', { + getValueFromEvent: this.normFile, + trigger: 'onBlur', + rules: [ + { + validator: this.validateUpload, + }, + ], + })( + + + + )} + + + + + + +
+
+ ); + } +} + +export default injectIntl(NewSmartContractCode); diff --git a/user-dashboard/src/app/assets/src/routes/SmartContract/index.js b/user-dashboard/src/app/assets/src/routes/SmartContract/index.js index cc250d55a..8c8aa31a2 100644 --- a/user-dashboard/src/app/assets/src/routes/SmartContract/index.js +++ b/user-dashboard/src/app/assets/src/routes/SmartContract/index.js @@ -25,7 +25,7 @@ export default class SmartContract extends PureComponent { newSmartContract = () => { this.props.dispatch( routerRedux.push({ - pathname: '/new-smart-contract', + pathname: '/smart-contract/new', }) ); }; @@ -53,6 +53,11 @@ export default class SmartContract extends PureComponent { }, }); }; + smartContractInfo = (smartContract) => { + this.props.dispatch(routerRedux.push({ + pathname: `/smart-contract/info/${smartContract._id}`, + })); + }; render() { const { smartContract: { smartContracts }, loadingSmartContracts } = this.props; const content = ( @@ -82,7 +87,7 @@ export default class SmartContract extends PureComponent { renderItem={item => item ? ( - Info, this.deleteSmartContract(item)}>Delete]}> + this.smartContractInfo(item)}>Info, this.deleteSmartContract(item)}>Delete]}> diff --git a/user-dashboard/src/app/assets/src/services/chain.js b/user-dashboard/src/app/assets/src/services/chain.js index 834e9e2ca..a84a49df7 100644 --- a/user-dashboard/src/app/assets/src/services/chain.js +++ b/user-dashboard/src/app/assets/src/services/chain.js @@ -1,22 +1,22 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import request from '../utils/request'; -import config from '../utils/config'; +import request from "../utils/request"; +import config from "../utils/config"; export async function queryChains() { return request(config.url.chain.list); } export async function release(id) { - return request(`${config.url.chain.release.format({id})}`, { - method: 'DELETE', + return request(`${config.url.chain.release.format({ id })}`, { + method: "DELETE", }); } export async function apply(params) { return request(config.url.chain.apply, { - method: 'POST', + method: "POST", body: params, - }) + }); } diff --git a/user-dashboard/src/app/assets/src/services/error.js b/user-dashboard/src/app/assets/src/services/error.js index 687141036..4fe9bfb3e 100644 --- a/user-dashboard/src/app/assets/src/services/error.js +++ b/user-dashboard/src/app/assets/src/services/error.js @@ -1,7 +1,7 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import request from '../utils/request'; +import request from "../utils/request"; export async function query(code) { return request(`/api/${code}`); diff --git a/user-dashboard/src/app/assets/src/services/smart_contract.js b/user-dashboard/src/app/assets/src/services/smart_contract.js index ad887fe30..620082d6c 100644 --- a/user-dashboard/src/app/assets/src/services/smart_contract.js +++ b/user-dashboard/src/app/assets/src/services/smart_contract.js @@ -1,8 +1,8 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import request from '../utils/request'; -import config from '../utils/config'; +import request from "../utils/request"; +import config from "../utils/config"; export async function querySmartContracts() { return request(config.url.smartContract.list); @@ -10,19 +10,36 @@ export async function querySmartContracts() { export async function deleteSmartContractCode(id) { return request(config.url.smartContract.codeOperate.format({ id }), { - method: 'DELETE', + method: "DELETE", }); } export async function updateSmartContractCode(payload) { - return request(config.url.smartContract.codeOperate.format({ id: payload.id }), { - method: 'PUT', - body: payload, - }) + return request( + config.url.smartContract.codeOperate.format({ id: payload.id }), + { + method: "PUT", + body: payload, + } + ); } export async function deleteSmartContract(id) { return request(config.url.smartContract.operate.format({ id }), { - method: 'DELETE', - }) + method: "DELETE", + }); +} + +export async function querySmartContract(id) { + return request(config.url.smartContract.operate.format({ id })); +} + +export async function deploySmartContract(payload) { + return request( + config.url.smartContract.codeDeploy.format({ id: payload.id }), + { + method: "POST", + body: payload, + } + ); } diff --git a/user-dashboard/src/app/assets/src/services/user.js b/user-dashboard/src/app/assets/src/services/user.js index e6ca9d2cb..1f6c2b75e 100644 --- a/user-dashboard/src/app/assets/src/services/user.js +++ b/user-dashboard/src/app/assets/src/services/user.js @@ -1,19 +1,19 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import request from '../utils/request'; +import request from "../utils/request"; export async function query() { - return request('/api/users'); + return request("/api/users"); } export async function queryCurrent() { - return request('/api/currentUser'); + return request("/api/currentUser"); } export async function login(params) { - return request('/login', { - method: 'POST', + return request("/login", { + method: "POST", body: params, }); } diff --git a/user-dashboard/src/app/assets/src/utils/Authorized.js b/user-dashboard/src/app/assets/src/utils/Authorized.js index d15311c22..f98ddf519 100644 --- a/user-dashboard/src/app/assets/src/utils/Authorized.js +++ b/user-dashboard/src/app/assets/src/utils/Authorized.js @@ -1,8 +1,8 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import RenderAuthorized from '../components/Authorized'; -import { getAuthority } from './authority'; +import RenderAuthorized from "../components/Authorized"; +import { getAuthority } from "./authority"; let Authorized = RenderAuthorized(getAuthority()); // eslint-disable-line diff --git a/user-dashboard/src/app/assets/src/utils/authority.js b/user-dashboard/src/app/assets/src/utils/authority.js index 2a8e4e898..95cd94fad 100644 --- a/user-dashboard/src/app/assets/src/utils/authority.js +++ b/user-dashboard/src/app/assets/src/utils/authority.js @@ -3,9 +3,9 @@ */ // use localStorage to store the authority info, which might be sent from server in actual project. export function getAuthority() { - return localStorage.getItem('cello-authority'); + return localStorage.getItem("cello-authority"); } export function setAuthority(authority) { - return localStorage.setItem('cello-authority', authority); + return localStorage.setItem("cello-authority", authority); } diff --git a/user-dashboard/src/app/assets/src/utils/config.js b/user-dashboard/src/app/assets/src/utils/config.js index ab11450ab..11e769b2a 100644 --- a/user-dashboard/src/app/assets/src/utils/config.js +++ b/user-dashboard/src/app/assets/src/utils/config.js @@ -1,7 +1,7 @@ /* SPDX-License-Identifier: Apache-2.0 */ -const format = require('string-format'); +const format = require("string-format"); const urlBase = window.webRoot; format.extend(String.prototype); @@ -19,6 +19,7 @@ export default { upload: `${urlBase}upload-smart-contract`, codeOperate: `${urlBase}api/smart-contract/code/{id}`, operate: `${urlBase}api/smart-contract/{id}`, + codeDeploy: `${urlBase}api/smart-contract/deploy-code/{id}`, }, }, }; diff --git a/user-dashboard/src/app/assets/src/utils/request.js b/user-dashboard/src/app/assets/src/utils/request.js index f8d0b6837..1b587f1d1 100644 --- a/user-dashboard/src/app/assets/src/utils/request.js +++ b/user-dashboard/src/app/assets/src/utils/request.js @@ -1,14 +1,14 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import fetch from 'dva/fetch'; -import { notification } from 'antd'; -import { routerRedux } from 'dva/router'; -import { IntlProvider, defineMessages } from 'react-intl'; -import store from '../index'; -import { getLocale } from '../utils/utils'; +import fetch from "dva/fetch"; +import { notification } from "antd"; +import { routerRedux } from "dva/router"; +import { IntlProvider, defineMessages } from "react-intl"; +import store from "../index"; +import { getLocale } from "../utils/utils"; -const Cookies = require('js-cookie'); +const Cookies = require("js-cookie"); const currentLocale = getLocale(); const intlProvider = new IntlProvider( @@ -109,7 +109,9 @@ function checkStatus(response) { } const errortext = codeMessage[response.status] || response.statusText; notification.error({ - message: `${intl.formatMessage(messages.requestError)} ${response.status}: ${response.url}`, + message: `${intl.formatMessage(messages.requestError)} ${ + response.status + }: ${response.url}`, description: errortext, }); if ([400, 500].indexOf(response.status) < 0) { @@ -131,33 +133,33 @@ function checkStatus(response) { */ export default function request(url, options) { const defaultOptions = { - credentials: 'include', + credentials: "include", }; const newOptions = { ...defaultOptions, ...options }; - const csrftoken = Cookies.get('csrfToken'); - if (newOptions.method === 'POST' || newOptions.method === 'PUT') { + const csrftoken = Cookies.get("csrfToken"); + if (newOptions.method === "POST" || newOptions.method === "PUT") { if (!(newOptions.body instanceof FormData)) { newOptions.headers = { - Accept: 'application/json', - 'Content-Type': 'application/json; charset=utf-8', + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", ...newOptions.headers, }; newOptions.body = JSON.stringify(newOptions.body); } else { // newOptions.body is FormData newOptions.headers = { - Accept: 'application/json', - 'Content-Type': 'multipart/form-data', + Accept: "application/json", + "Content-Type": "multipart/form-data", ...newOptions.headers, }; } newOptions.headers = { - 'x-csrf-token': csrftoken, + "x-csrf-token": csrftoken, ...newOptions.headers, }; - } else if (newOptions.method === 'DELETE') { + } else if (newOptions.method === "DELETE") { newOptions.headers = { - 'x-csrf-token': csrftoken, + "x-csrf-token": csrftoken, ...newOptions.headers, }; } @@ -165,7 +167,7 @@ export default function request(url, options) { return fetch(url, newOptions) .then(checkStatus) .then(response => { - if (newOptions.method === 'DELETE' || response.status === 204) { + if (newOptions.method === "DELETE" || response.status === 204) { return response.text(); } return response.json(); @@ -175,22 +177,22 @@ export default function request(url, options) { const status = e.name; if (status === 401) { dispatch({ - type: 'login/logout', + type: "login/logout", }); return { status, }; } if (status === 403) { - dispatch(routerRedux.push('/exception/403')); + dispatch(routerRedux.push("/exception/403")); return; } if (status <= 504 && status >= 500) { - dispatch(routerRedux.push('/exception/500')); + dispatch(routerRedux.push("/exception/500")); return; } if (status >= 404 && status < 422) { - dispatch(routerRedux.push('/exception/404')); + dispatch(routerRedux.push("/exception/404")); } }); } diff --git a/user-dashboard/src/app/assets/src/utils/utils.js b/user-dashboard/src/app/assets/src/utils/utils.js index 2d5405ed7..490c0ee24 100644 --- a/user-dashboard/src/app/assets/src/utils/utils.js +++ b/user-dashboard/src/app/assets/src/utils/utils.js @@ -1,9 +1,9 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import moment from 'moment'; -import enLocale from '../locales/en-US'; -import zhLocale from '../locales/zh-CN'; +import moment from "moment"; +import enLocale from "../locales/en-US"; +import zhLocale from "../locales/zh-CN"; export function fixedZero(val) { return val * 1 < 10 ? `0${val}` : val; @@ -13,14 +13,14 @@ export function getTimeDistance(type) { const now = new Date(); const oneDay = 1000 * 60 * 60 * 24; - if (type === 'today') { + if (type === "today") { now.setHours(0); now.setMinutes(0); now.setSeconds(0); return [moment(now), moment(now.getTime() + (oneDay - 1000))]; } - if (type === 'week') { + if (type === "week") { let day = now.getDay(); now.setHours(0); now.setMinutes(0); @@ -37,31 +37,35 @@ export function getTimeDistance(type) { return [moment(beginTime), moment(beginTime + (7 * oneDay - 1000))]; } - if (type === 'month') { + if (type === "month") { const year = now.getFullYear(); const month = now.getMonth(); - const nextDate = moment(now).add(1, 'months'); + const nextDate = moment(now).add(1, "months"); const nextYear = nextDate.year(); const nextMonth = nextDate.month(); return [ moment(`${year}-${fixedZero(month + 1)}-01 00:00:00`), - moment(moment(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).valueOf() - 1000), + moment( + moment( + `${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00` + ).valueOf() - 1000 + ), ]; } - if (type === 'year') { + if (type === "year") { const year = now.getFullYear(); return [moment(`${year}-01-01 00:00:00`), moment(`${year}-12-31 23:59:59`)]; } } -export function getPlainNode(nodeList, parentPath = '') { +export function getPlainNode(nodeList, parentPath = "") { const arr = []; nodeList.forEach(node => { const item = node; - item.path = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/'); + item.path = `${parentPath}/${item.path || ""}`.replace(/\/+/g, "/"); item.exact = true; if (item.children && !item.component) { arr.push(...getPlainNode(item.children, item.path)); @@ -77,10 +81,10 @@ export function getPlainNode(nodeList, parentPath = '') { function getRelation(str1, str2) { if (str1 === str2) { - console.warn('Two path are equal!'); // eslint-disable-line + console.warn("Two path are equal!"); // eslint-disable-line } - const arr1 = str1.split('/'); - const arr2 = str2.split('/'); + const arr1 = str1.split("/"); + const arr2 = str2.split("/"); if (arr2.every((item, index) => item === arr1[index])) { return 1; } else if (arr1.every((item, index) => item === arr2[index])) { @@ -114,12 +118,14 @@ export function getRoutes(path, routerData) { routePath => routePath.indexOf(path) === 0 && routePath !== path ); // Replace path to '' eg. path='user' /user/name => name - routes = routes.map(item => item.replace(path, '')); + routes = routes.map(item => item.replace(path, "")); // Get the route to be rendered to remove the deep rendering const renderArr = getRenderArr(routes); // Conversion and stitching parameters const renderRoutes = renderArr.map(item => { - const exact = !routes.some(route => route !== item && getRelation(route, item) === 1); + const exact = !routes.some( + route => route !== item && getRelation(route, item) === 1 + ); return { exact, ...routerData[`${path}${item}`], @@ -138,11 +144,15 @@ export function isUrl(path) { } export function getLocale() { - return ((window.localStorage && localStorage.getItem('language')) || (navigator.language || navigator.browserLanguage).toLowerCase()) === 'en' + return ((window.localStorage && localStorage.getItem("language")) || + (navigator.language || navigator.browserLanguage).toLowerCase()) === "en" ? enLocale : zhLocale; } export function getLang() { - return (window.localStorage && localStorage.getItem('language')) || (navigator.language || navigator.browserLanguage).toLowerCase(); + return ( + (window.localStorage && localStorage.getItem("language")) || + (navigator.language || navigator.browserLanguage).toLowerCase() + ); } diff --git a/user-dashboard/src/app/assets/src/utils/utils.less b/user-dashboard/src/app/assets/src/utils/utils.less index 725792252..ca8a3bbc0 100644 --- a/user-dashboard/src/app/assets/src/utils/utils.less +++ b/user-dashboard/src/app/assets/src/utils/utils.less @@ -15,7 +15,7 @@ padding-right: 1em; &:before { background: @bg; - content: '...'; + content: "..."; padding: 0 1px; position: absolute; right: 14px; @@ -23,7 +23,7 @@ } &:after { background: white; - content: ''; + content: ""; margin-top: 0.2em; position: absolute; right: 14px; @@ -38,7 +38,7 @@ zoom: 1; &:before, &:after { - content: ' '; + content: " "; display: table; } &:after { diff --git a/user-dashboard/src/app/controller/smart_contract.js b/user-dashboard/src/app/controller/smart_contract.js index 64788b81c..5afd261b6 100644 --- a/user-dashboard/src/app/controller/smart_contract.js +++ b/user-dashboard/src/app/controller/smart_contract.js @@ -34,6 +34,27 @@ class SmartContractController extends Controller { await ctx.service.smartContract.deleteSmartContract(id); ctx.status = 204; } + async querySmartContract() { + const { ctx } = this; + const id = ctx.params.id; + ctx.body = await ctx.service.smartContract.querySmartContract(id); + } + async deploySmartContractCode() { + const { ctx } = this; + const id = ctx.params.id; + const { chainId, operation } = ctx.request.body; + switch (operation) { + case 'instantiate': + ctx.service.smartContract.deploySmartContractCode(id, chainId, operation); + ctx.body = { + success: true, + }; + break; + default: + ctx.body = await ctx.service.smartContract.deploySmartContractCode(id, chainId, operation); + break; + } + } } module.exports = SmartContractController; diff --git a/user-dashboard/src/app/extend/context.js b/user-dashboard/src/app/extend/context.js index 12d75ebab..720793194 100644 --- a/user-dashboard/src/app/extend/context.js +++ b/user-dashboard/src/app/extend/context.js @@ -22,6 +22,12 @@ module.exports = { get joinChannel() { return this.app.joinChannel; }, + get installSmartContract() { + return this.app.installSmartContract; + }, + get instantiateSmartContract() { + return this.app.instantiateSmartContract; + }, get sleep() { return this.app.sleep; }, diff --git a/user-dashboard/src/app/lib/fabric/index.js b/user-dashboard/src/app/lib/fabric/index.js index 45e027efd..91670bf1a 100644 --- a/user-dashboard/src/app/lib/fabric/index.js +++ b/user-dashboard/src/app/lib/fabric/index.js @@ -231,6 +231,171 @@ module.exports = app => { return { success: false, }; + } + async function installSmartContract(network, keyValueStorePath, peers, userId, smartContractCodeId, chainId, org) { + const ctx = app.createAnonymousContext(); + const smartContractCode = await ctx.model.SmartContractCode.findOne({ _id: smartContractCodeId }); + const chain = await ctx.model.Chain.findOne({ _id: chainId }); + const chainCodeName = `${chain.chainId}-${smartContractCodeId}`; + const smartContractSourcePath = `github.com/${smartContractCodeId}`; + const chainRootPath = `/opt/data/${userId}/chains/${chainId}`; + process.env.GOPATH = chainRootPath; + fs.ensureDirSync(`${chainRootPath}/src/github.com`); + fs.copySync(smartContractCode.path, `${chainRootPath}/src/${smartContractSourcePath}`); + + const helper = await fabricHelper(network, keyValueStorePath); + const client = await getClientForOrg('org1', helper.clients); + + await getOrgAdmin('org1', helper); + + const request = { + targets: await newPeers(network, peers, org, helper.clients), + chaincodePath: smartContractSourcePath, + chaincodeId: chainCodeName, + chaincodeVersion: smartContractCode.version, + }; + const results = await client.installChaincode(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; + ctx.logger.info('install proposal was good'); + } else { + ctx.logger.error('install proposal was bad'); + } + all_good = all_good & one_good; + } + if (all_good) { + ctx.logger.info(util.format( + 'Successfully sent install Proposal and received ProposalResponse: Status - %s', + proposalResponses[0].response.status)); + ctx.logger.debug('\nSuccessfully Installed chaincode on organization ' + org + + '\n'); + const deploy = await ctx.model.SmartContractDeploy.findOneAndUpdate({ + smartContractCode, + smartContract: smartContractCode.smartContract, + name: chainCodeName, + chain: chainId, + }, { + status: 'installed', + }, { upsert: true, new: true }); + return { + success: true, + deployId: deploy._id.toString(), + message: 'Successfully Installed chaincode on organization ' + org, + }; + } + ctx.logger.error( + 'Failed to send install Proposal or receive valid response. Response null or status is not 200. exiting...' + ); + return { + success: false, + message: 'Failed to send install Proposal or receive valid response. Response null or status is not 200. exiting...', + }; + + } + async function instantiateSmartContract(network, keyValueStorePath, channelName, deployId, functionName, args, org) { + const ctx = app.createAnonymousContext(); + const deploy = await ctx.model.SmartContractDeploy.findOne({ _id: deployId }).populate('smartContractCode'); + deploy.status = 'instantiating'; + deploy.save(); + const helper = await fabricHelper(network, keyValueStorePath); + const client = await getClientForOrg('org1', helper.clients); + const channel = await getChannelForOrg('org1', helper.channels); + + await getOrgAdmin('org1', helper); + await channel.initialize(); + const txId = client.newTransactionID(); + // send proposal to endorser + const request = { + chaincodeId: deploy.name, + chaincodeVersion: deploy.smartContractCode.version, + args, + txId, + }; + + if (functionName) { request.fcn = functionName; } + + const results = await channel.sendInstantiateProposal(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; + ctx.logger.debug('instantiate proposal was good'); + } else { + ctx.logger.error('instantiate proposal was bad'); + } + all_good = all_good & one_good; + } + if (all_good) { + deploy.status = 'instantiated'; + deploy.deployTime = Date.now(); + deploy.save(); + ctx.logger.info(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 promiseRequest = { + 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 deployId = await txId.getTransactionID(); + + const eh = await client.newEventHub(); + const data = fs.readFileSync(path.join(__dirname, 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'], + }); + await eh.connect(); + + const txPromise = new Promise((resolve, reject) => { + const handle = setTimeout(() => { + eh.disconnect(); + reject(); + }, 30000); + + eh.registerTxEvent(deployId, (tx, code) => { + ctx.logger.info( + 'The chaincode instantiate transaction has been committed on peer ' + + eh._ep._endpoint.addr); + clearTimeout(handle); + eh.unregisterTxEvent(deployId); + eh.disconnect(); + + if (code !== 'VALID') { + ctx.logger.error('The chaincode instantiate transaction was invalid, code = ' + code); + reject(); + } else { + ctx.logger.info('The chaincode instantiate transaction was valid.'); + resolve(); + } + }); + }); + + const sendPromise = await channel.sendTransaction(promiseRequest); + const promiseResults = await Promise.all([sendPromise].concat([txPromise])); + return promiseResults[0]; + } + deploy.status = 'error'; + deploy.save(); + ctx.logger.error( + 'Failed to send instantiate Proposal or receive valid response. Response null or status is not 200. exiting...' + ); + return 'Failed to send instantiate Proposal or receive valid response. Response null or status is not 200. exiting...'; } async function fabricHelper(network, keyValueStore) { @@ -274,6 +439,8 @@ module.exports = app => { app.getChannelForOrg = getChannelForOrg; app.createChannel = createChannel; app.joinChannel = joinChannel; + app.installSmartContract = installSmartContract; + app.instantiateSmartContract = instantiateSmartContract; app.sleep = sleep; hfc.setLogger(app.logger); }; diff --git a/user-dashboard/src/app/model/smart-contract-deploy.js b/user-dashboard/src/app/model/smart-contract-deploy.js index 58842af2e..0162fe4aa 100644 --- a/user-dashboard/src/app/model/smart-contract-deploy.js +++ b/user-dashboard/src/app/model/smart-contract-deploy.js @@ -9,6 +9,9 @@ module.exports = app => { const SmartContractDeploySchema = new Schema({ user: { type: Schema.Types.ObjectId, ref: 'User' }, + smartContract: { type: Schema.Types.ObjectId, ref: 'SmartContract' }, + smartContractCode: { type: Schema.Types.ObjectId, ref: 'SmartContractCode' }, + chain: { type: Schema.Types.ObjectId, ref: 'Chain' }, name: { type: String }, status: { type: String, default: 'idle', enum: [ 'idle', 'installed', 'instantiating', 'instantiated', 'error'] }, deployTime: { type: Date, default: Date.now }, diff --git a/user-dashboard/src/app/model/smart-contract-operate-history.js b/user-dashboard/src/app/model/smart-contract-operate-history.js new file mode 100644 index 000000000..0432dd9cc --- /dev/null +++ b/user-dashboard/src/app/model/smart-contract-operate-history.js @@ -0,0 +1,21 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +'use strict'; + +module.exports = app => { + const mongoose = app.mongoose; + const Schema = mongoose.Schema; + + const SmartContractOperateHistorySchema = new Schema({ + user: { type: Schema.Types.ObjectId, ref: 'User' }, + smartContract: { type: Schema.Types.ObjectId, ref: 'SmartContract' }, + smartContractCode: { type: Schema.Types.ObjectId, ref: 'SmartContractCode' }, + chain: { type: Schema.Types.ObjectId, ref: 'Chain' }, + operate: { type: String, enum: ['new', 'deploy', 'delete-code'] }, + status: { type: String, default: 'success', enum: ['success', 'failed'] }, + operateTime: { type: Date, default: Date.now }, + }); + + return mongoose.model('SmartContractOperateHistory', SmartContractOperateHistorySchema); +}; diff --git a/user-dashboard/src/app/router/api.js b/user-dashboard/src/app/router/api.js index eee2a81b7..050eaadf2 100644 --- a/user-dashboard/src/app/router/api.js +++ b/user-dashboard/src/app/router/api.js @@ -14,4 +14,6 @@ module.exports = app => { app.router.delete('/api/smart-contract/code/:id', app.controller.smartContract.removeSmartContractCode); app.router.put('/api/smart-contract/code/:id', app.controller.smartContract.updateSmartContractCode); 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); }; diff --git a/user-dashboard/src/app/service/smart_contract.js b/user-dashboard/src/app/service/smart_contract.js index bed88e986..5d88b0d09 100644 --- a/user-dashboard/src/app/service/smart_contract.js +++ b/user-dashboard/src/app/service/smart_contract.js @@ -48,8 +48,9 @@ class SmartContractService extends Service { } async storeSmartContract(stream) { const { ctx, config } = this; + const id = ctx.query.id; const smartContractRootDir = `${config.dataDir}/${ctx.user.id}/smart_contract`; - const smartContractId = new ObjectID(); + const smartContractId = id || new ObjectID(); const targetFileName = `${smartContractId}${path.extname(stream.filename)}`; const smartContractPath = `${smartContractRootDir}/${smartContractId}`; const smartContractCodePath = `${smartContractRootDir}/${smartContractId}/tmp`; @@ -67,11 +68,13 @@ class SmartContractService extends Service { const zip = AdmZip(zipFile); zip.extractAllTo(smartContractCodePath, true); commonFs.unlinkSync(zipFile); - await ctx.model.SmartContract.create({ - _id: smartContractId, - path: smartContractPath, - user: ctx.user.id, - }); + if (!id) { + await ctx.model.SmartContract.create({ + _id: smartContractId, + path: smartContractPath, + user: ctx.user.id, + }); + } const smartContractCode = await ctx.model.SmartContractCode.create({ smartContract: smartContractId, path: smartContractCodePath, @@ -121,6 +124,12 @@ class SmartContractService extends Service { smartContract.name = name; smartContract.description = description; await smartContract.save(); + await ctx.model.SmartContractOperateHistory.create({ + user: smartContract.user, + smartContract, + smartContractCode, + operate: 'new', + }); return { success: true, }; @@ -134,6 +143,47 @@ class SmartContractService extends Service { } await smartContract.remove(); } + async querySmartContract(id) { + const { ctx } = this; + const smartContract = await ctx.model.SmartContract.findOne({ _id: id }, '_id description name createTime'); + if (!smartContract) { + return { + 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'); + return { + success: true, + info: smartContract, + codes, + newOperations, + deploys, + }; + } + async deploySmartContractCode(id, chainId, operation) { + const { ctx, config } = this; + + const { functionName, args, deployId } = ctx.request.body; + const chainRootDir = `${config.dataDir}/${ctx.user.id}/chains/${chainId}`; + const keyValueStorePath = `${chainRootDir}/client-kvs`; + const network = await ctx.service.chain.generateNetwork(chainId); + switch (operation) { + case 'install': + return await ctx.installSmartContract(network, keyValueStorePath, ['peer1', 'peer2'], ctx.user.id, id, chainId, 'org1'); + case 'instantiate': + ctx.instantiateSmartContract(network, keyValueStorePath, config.default.channelName, deployId, functionName, args, 'org1'); + return { + success: true, + }; + default: + return { + success: false, + message: 'Please input deploy operation', + }; + } + } } module.exports = SmartContractService; diff --git a/user-dashboard/src/package.json b/user-dashboard/src/package.json index 2931d8f62..19ce1d6ba 100644 --- a/user-dashboard/src/package.json +++ b/user-dashboard/src/package.json @@ -118,6 +118,7 @@ "autod": "autod", "build": "if-env DEV=True && npm run build:dev || npm run build:prod", "build:dev": "cross-env ESLINT=none COMPRESS=none roadhog build && cp -r app/assets/src/assets/* app/assets/public/", + "prettier": "prettier --write ./app/**/**/**/*.{js,jsx,less}", "build:prod": "cross-env ESLINT=none roadhog build && cp -r app/assets/src/assets/* app/assets/public/" }, "ci": {