diff --git a/user-dashboard/Makefile b/user-dashboard/Makefile index 6a038273c..aca3fa65d 100644 --- a/user-dashboard/Makefile +++ b/user-dashboard/Makefile @@ -1,5 +1,5 @@ build-js: - docker-compose -f docker-compose-files/docker-compose-build-js.yml up + docker-compose -f docker-compose-files/docker-compose-build-js.yaml up npm-install: - docker-compose -f docker-compose-files/docker-compose-npm-install.yml up + docker-compose -f docker-compose-files/docker-compose-npm-install.yaml up diff --git a/user-dashboard/docker-compose-files/docker-compose-build-js.yml b/user-dashboard/docker-compose-files/docker-compose-build-js.yaml similarity index 80% rename from user-dashboard/docker-compose-files/docker-compose-build-js.yml rename to user-dashboard/docker-compose-files/docker-compose-build-js.yaml index d6a514814..e135cf1cf 100644 --- a/user-dashboard/docker-compose-files/docker-compose-build-js.yml +++ b/user-dashboard/docker-compose-files/docker-compose-build-js.yaml @@ -18,5 +18,7 @@ services: volumes: # This should be removed in product env - $ROOT_PATH/user-dashboard/js:/app - $ROOT_PATH/user-dashboard:/usr/app/src - command: bash -c "cd /app && npm run build && cd /app/home && npm run build && + command: bash -c "cd /app && npm run build && + cd /app/home && npm run build && + cd /app/dashboard && npm run build && cd /usr/app/src && npm run build" diff --git a/user-dashboard/docker-compose-files/docker-compose-npm-install.yaml b/user-dashboard/docker-compose-files/docker-compose-npm-install.yaml new file mode 100644 index 000000000..0add359c6 --- /dev/null +++ b/user-dashboard/docker-compose-files/docker-compose-npm-install.yaml @@ -0,0 +1,17 @@ +version: "2" + +# Copyright IBM Corp., All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# +services: + install-npm: + image: node + volumes: + - $ROOT_PATH/user-dashboard/js:/reactjs + environment: + - NPM_REGISTRY=$NPM_REGISTRY + command: bash -c "npm config set registry '$NPM_REGISTRY' && + cd /reactjs && npm install && + cd /reactjs/home && npm install && + cd /reactjs/dashboard && npm install" diff --git a/user-dashboard/docker-compose-files/docker-compose-npm-install.yml b/user-dashboard/docker-compose-files/docker-compose-npm-install.yml deleted file mode 100644 index a42c3ca88..000000000 --- a/user-dashboard/docker-compose-files/docker-compose-npm-install.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: "2" - -# Copyright IBM Corp., All Rights Reserved. -# -# SPDX-License-Identifier: Apache-2.0 -# -services: - install-npm: - image: node - volumes: - - $ROOT_PATH/user-dashboard:/usr/app/src - - $ROOT_PATH/user-dashboard/js:/reactjs - command: bash -c "cd /usr/app/src && npm install && npm install requirejs && - cd /reactjs && npm install && cd /reactjs/home && npm install" \ No newline at end of file diff --git a/user-dashboard/js/dashboard/.editorconfig b/user-dashboard/js/dashboard/.editorconfig new file mode 100644 index 000000000..7e3649acc --- /dev/null +++ b/user-dashboard/js/dashboard/.editorconfig @@ -0,0 +1,16 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/user-dashboard/js/dashboard/.eslintignore b/user-dashboard/js/dashboard/.eslintignore new file mode 100644 index 000000000..9b4ba98e1 --- /dev/null +++ b/user-dashboard/js/dashboard/.eslintignore @@ -0,0 +1,2 @@ +src/**/*-test.js +src/public diff --git a/user-dashboard/js/dashboard/.eslintrc b/user-dashboard/js/dashboard/.eslintrc new file mode 100644 index 000000000..e02cdc05c --- /dev/null +++ b/user-dashboard/js/dashboard/.eslintrc @@ -0,0 +1,53 @@ +{ + "extends": "airbnb", + "rules": { + "semi": [2, "never"], + "no-console": 0, + "comma-dangle": [2, "always-multiline"], + "max-len": 0, + "react/jsx-first-prop-new-line": 0, + "react/jsx-filename-extension": 0, + "space-before-function-paren": [2, "always"], + "no-unused-expressions": [0, { + "allowShortCircuit": true, + "allowTernary": true + }], + "arrow-body-style": [0, "never"], + "func-names": 0, + "prefer-const": 0, + "no-extend-native": 0, + "no-param-reassign": 0, + "no-restricted-syntax": 0, + "no-eval": 0, + "no-continue": 0, + "react/jsx-no-bind": 0, + "no-unused-vars": [2, { "ignoreRestSiblings": true }], + "no-underscore-dangle": 0, + "global-require": 0, + "import/no-unresolved": 0, + "import/extensions": 0, + "jsx-a11y/href-no-hash": 0, + "react/no-array-index-key": 0, + "react/require-default-props": 0, + "react/forbid-prop-types": 0, + "react/no-string-refs": 0, + "react/no-find-dom-node": 0, + "react/prefer-stateless-function": 0, + "import/no-extraneous-dependencies": 0, + "import/prefer-default-export": 0, + "react/no-danger": 0, + "jsx-a11y/no-static-element-interactions": 0, + }, + "parser": "babel-eslint", + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 8, + "ecmaFeatures": { + "jsx": true, + "experimentalObjectRestSpread": true + } + }, + "settings": { + "import/resolver": "node" + } +} diff --git a/user-dashboard/js/dashboard/.roadhogrc b/user-dashboard/js/dashboard/.roadhogrc new file mode 100755 index 000000000..6ba86ecd3 --- /dev/null +++ b/user-dashboard/js/dashboard/.roadhogrc @@ -0,0 +1,26 @@ +{ + "entry": ["src/index.js", "src/locales/en-US.js", "src/locales/zh-CN.js"], + "publicPath": "/static/js/dist/dashboard/", + "outputPath": "/usr/app/src/src/public/js/dist/dashboard", + "extraBabelPlugins": [ + "transform-runtime", + "transform-decorators-legacy", + "transform-class-properties", + ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }] + ], + "env": { + "development": { + "extraBabelPlugins": [ + "dva-hmr" + ] + } + }, + "externals": { + "react": "React", + "g2": "G2", + "g-cloud": "Cloud", + "g2-plugin-slider": "G2.Plugin.slider" + }, + "ignoreMomentLocale": true, + "theme": "./src/theme.js" +} diff --git a/user-dashboard/js/dashboard/package.json b/user-dashboard/js/dashboard/package.json new file mode 100644 index 000000000..5ade13f0c --- /dev/null +++ b/user-dashboard/js/dashboard/package.json @@ -0,0 +1,114 @@ +{ + "name": "user-dashboard", + "version": "1.0.0", + "description": "Cello baas user dashboard", + "private": true, + "scripts": { + "precommit": "npm run lint-staged", + "start": "roadhog server", + "start:no-proxy": "NO_PROXY=true roadhog server", + "build": "roadhog build", + "site": "roadhog-api-doc static", + "analyze": "roadhog build --analyze", + "lint:style": "stylelint \"src/**/*.less\" --syntax less", + "lint": "eslint --ext .js src mock tests && npm run lint:style", + "lint:fix": "eslint --fix --ext .js src mock tests && npm run lint:style", + "lint-staged": "lint-staged", + "lint-staged:js": "eslint --ext .js", + "test": "jest", + "test:all": "node ./tests/run-tests.js" + }, + "dependencies": { + "antd": "^3.0.0", + "classnames": "^2.2.5", + "core-js": "^2.5.1", + "dva": "^2.0.3", + "g-cloud": "^1.0.2-beta", + "g2": "^2.3.13", + "g2-plugin-slider": "^1.2.1", + "lodash": "^4.17.4", + "lodash-decorators": "^4.4.1", + "lodash.clonedeep": "^4.5.0", + "moment": "^2.19.1", + "numeral": "^2.0.6", + "prop-types": "^15.5.10", + "js-cookie": "^2.1.3", + "qs": "^6.5.0", + "react": "^16.0.0", + "react-document-title": "^2.0.3", + "react-intl": "^2.4.0", + "string-format": "^0.5.0", + "validator": "^9.1.1", + "react-dom": "^16.0.0", + "socket.io-client": "^2.0.4", + "localStorage": "^1.0.3", + "react-fittext": "^1.0.0" + }, + "devDependencies": { + "babel-eslint": "^8.0.1", + "babel-jest": "^21.0.0", + "babel-plugin-dva-hmr": "^0.3.2", + "babel-plugin-import": "^1.2.1", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-decorators-legacy": "^1.3.4", + "babel-plugin-transform-runtime": "^6.9.0", + "babel-preset-env": "^1.6.1", + "babel-preset-react": "^6.24.1", + "babel-runtime": "^6.9.2", + "cross-port-killer": "^1.0.1", + "enzyme": "^3.1.0", + "enzyme-adapter-react-16": "^1.0.2", + "eslint": "^4.8.0", + "eslint-config-airbnb": "^16.0.0", + "eslint-plugin-babel": "^4.0.0", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jsx-a11y": "^6.0.0", + "eslint-plugin-markdown": "^1.0.0-beta.6", + "eslint-plugin-react": "^7.0.1", + "gh-pages": "^1.0.0", + "husky": "^0.14.3", + "jest": "^21.0.1", + "lint-staged": "^4.3.0", + "mockjs": "^1.0.1-beta3", + "react-container-query": "^0.9.1", + "react-test-renderer": "^16.0.0", + "redbox-react": "^1.3.2", + "roadhog": "^1.3.1", + "roadhog-api-doc": "^0.2.0", + "stylelint": "^8.1.0", + "stylelint-config-standard": "^17.0.0" + }, + "optionalDependencies": { + "nightmare": "^2.10.0" + }, + "babel": { + "presets": [ + "env", + "react" + ], + "plugins": [ + "transform-decorators-legacy", + "transform-class-properties" + ] + }, + "jest": { + "setupFiles": [ + "/tests/setupTests.js" + ], + "testMatch": [ + "**/?(*.)(spec|test|e2e).js?(x)" + ], + "setupTestFrameworkScriptFile": "/tests/jasmine.js", + "moduleFileExtensions": [ + "js", + "jsx" + ], + "moduleNameMapper": { + "\\.(css|less)$": "/tests/styleMock.js" + } + }, + "lint-staged": { + "**/*.{js,jsx}": "lint-staged:js", + "**/*.less": "stylelint --syntax less" + } +} diff --git a/user-dashboard/js/dashboard/src/common/nav.js b/user-dashboard/js/dashboard/src/common/nav.js new file mode 100644 index 000000000..753c55790 --- /dev/null +++ b/user-dashboard/js/dashboard/src/common/nav.js @@ -0,0 +1,33 @@ +import BasicLayout from '../layouts/BasicLayout'; + +import Chain from '../routes/Chain' +import NewChain from '../routes/Chain/New' + +const data = [ + { + component: BasicLayout, + layout: 'BasicLayout', + name: 'Home', // for breadcrumb + path: '', + children: [ + { + name: 'Chain', + messageId: "Chain", + icon: 'link', + path: 'chain', + component: Chain, + }, + { + messageId: "ChainNew", + path: 'chain/new', + component: NewChain + } + ], + } +]; + +export function getNavData() { + return data; +} + +export default data; diff --git a/user-dashboard/js/dashboard/src/components/Charts/ChartCard/index.js b/user-dashboard/js/dashboard/src/components/Charts/ChartCard/index.js new file mode 100644 index 000000000..b874f8c7b --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/Charts/ChartCard/index.js @@ -0,0 +1,47 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React from 'react'; +import { Card, Spin } from 'antd'; + +import styles from './index.less'; + +const ChartCard = ({ + loading = false, contentHeight, title, action, total, footer, children, ...rest +}) => { + const content = ( +
+
+ {title} + {action} +
+ { + // eslint-disable-next-line + total &&

+ } +

+
+ {children} +
+
+ { + footer && ( +
+ {footer} +
+ ) + } +
+ ); + + return ( + + {{content}} + + ); +}; + +export default ChartCard; diff --git a/user-dashboard/js/dashboard/src/components/Charts/ChartCard/index.less b/user-dashboard/js/dashboard/src/components/Charts/ChartCard/index.less new file mode 100644 index 000000000..2d60257ba --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/Charts/ChartCard/index.less @@ -0,0 +1,52 @@ +@import "~antd/lib/style/themes/default.less"; + +.textOverflow() { + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + white-space: nowrap; +} + +.chartCard { + position: relative; + .meta { + color: @text-color-secondary; + font-size: @font-size-base; + position: relative; + line-height: 22px; + height: 22px; + } + .action { + cursor: pointer; + position: absolute; + top: 0; + right: 0; + } + .total { + .textOverflow(); + color: @heading-color; + margin-top: 4px; + font-size: 30px; + line-height: 38px; + height: 38px; + } + .content { + margin-bottom: 12px; + position: relative; + width: 100%; + } + .contentFixed { + position: absolute; + left: 0; + bottom: 0; + width: 100%; + } + .footer { + border-top: 1px solid @border-color-split; + padding-top: 9px; + margin-top: 8px; + & > * { + position: relative; + } + } +} diff --git a/user-dashboard/js/dashboard/src/components/Charts/equal.js b/user-dashboard/js/dashboard/src/components/Charts/equal.js new file mode 100644 index 000000000..ee5286792 --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/Charts/equal.js @@ -0,0 +1,20 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +/* eslint eqeqeq: 0 */ + +function equal(old, target) { + let r = true; + for (const prop in old) { + if (typeof old[prop] === 'function' && typeof target[prop] === 'function') { + if (old[prop].toString() != target[prop].toString()) { + r = false; + } + } else if (old[prop] != target[prop]) { + r = false; + } + } + return r; +} + +export default equal; diff --git a/user-dashboard/js/dashboard/src/components/Charts/index.js b/user-dashboard/js/dashboard/src/components/Charts/index.js new file mode 100644 index 000000000..c00291f9a --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/Charts/index.js @@ -0,0 +1,10 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import ChartCard from './ChartCard'; + +const yuan = val => `¥ ${numeral(val).format('0,0')}`; + +export default { + ChartCard, +}; diff --git a/user-dashboard/js/dashboard/src/components/Charts/index.less b/user-dashboard/js/dashboard/src/components/Charts/index.less new file mode 100644 index 000000000..52f97c4c9 --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/Charts/index.less @@ -0,0 +1,19 @@ +.miniChart { + position: relative; + width: 100%; + .chartContent { + position: absolute; + bottom: -34px; + width: 100%; + & > div { + margin: 0 -5px; + overflow: hidden; + } + } + .chartLoading { + position: absolute; + top: 16px; + left: 50%; + margin-left: -7px; + } +} diff --git a/user-dashboard/js/dashboard/src/components/Ellipsis/index.js b/user-dashboard/js/dashboard/src/components/Ellipsis/index.js new file mode 100644 index 000000000..d19001e45 --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/Ellipsis/index.js @@ -0,0 +1,213 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React, { Component } from 'react'; +import { Tooltip } from 'antd'; +import classNames from 'classnames'; +import styles from './index.less'; + +/* eslint react/no-did-mount-set-state: 0 */ +/* eslint no-param-reassign: 0 */ + +const isSupportLineClamp = (document.body.style.webkitLineClamp !== undefined); + +const EllipsisText = ({ text, length, tooltip, ...other }) => { + if (typeof text !== 'string') { + throw new Error('Ellipsis children must be string.'); + } + if (text.length <= length || length < 0) { + return {text}; + } + const tail = '...'; + let displayText; + if (length - tail.length <= 0) { + displayText = ''; + } else { + displayText = text.slice(0, (length - tail.length)); + } + + if (tooltip) { + return {displayText}{tail}; + } + + return ( + + {displayText}{tail} + + ); +}; + +export default class Ellipsis extends Component { + state = { + text: '', + targetCount: 0, + } + + componentDidMount() { + if (this.node) { + this.computeLine(); + } + } + + componentWillReceiveProps(nextProps) { + if (this.props.lines !== nextProps.lines) { + this.computeLine(); + } + } + + computeLine = () => { + const { lines } = this.props; + if (lines && !isSupportLineClamp) { + const text = this.shadowChildren.innerText; + const lineHeight = parseInt(getComputedStyle(this.root).lineHeight, 10); + const targetHeight = lines * lineHeight; + this.content.style.height = `${targetHeight}px`; + const totalHeight = this.shadowChildren.offsetHeight; + const shadowNode = this.shadow.firstChild; + + if (totalHeight <= targetHeight) { + this.setState({ + text, + targetCount: text.length, + }); + return; + } + + // bisection + const len = text.length; + const mid = Math.floor(len / 2); + + const count = this.bisection(targetHeight, mid, 0, len, text, shadowNode); + + this.setState({ + text, + targetCount: count, + }); + } + } + + bisection = (th, m, b, e, text, shadowNode) => { + const suffix = '...'; + let mid = m; + let end = e; + let begin = b; + shadowNode.innerHTML = text.substring(0, mid) + suffix; + let sh = shadowNode.offsetHeight; + + if (sh <= th) { + shadowNode.innerHTML = text.substring(0, mid + 1) + suffix; + sh = shadowNode.offsetHeight; + if (sh > th) { + return mid; + } else { + begin = mid; + mid = Math.floor((end - begin) / 2) + begin; + return this.bisection(th, mid, begin, end, text, shadowNode); + } + } else { + if (mid - 1 < 0) { + return mid; + } + shadowNode.innerHTML = text.substring(0, mid - 1) + suffix; + sh = shadowNode.offsetHeight; + if (sh <= th) { + return mid - 1; + } else { + end = mid; + mid = Math.floor((end - begin) / 2) + begin; + return this.bisection(th, mid, begin, end, text, shadowNode); + } + } + } + + handleRoot = (n) => { + this.root = n; + } + + handleContent = (n) => { + this.content = n; + } + + handleNode = (n) => { + this.node = n; + } + + handleShadow = (n) => { + this.shadow = n; + } + + handleShadowChildren = (n) => { + this.shadowChildren = n; + } + + render() { + const { text, targetCount } = this.state; + const { + children, + lines, + length, + className, + tooltip, + ...restProps + } = this.props; + + const cls = classNames(styles.ellipsis, className, { + [styles.lines]: (lines && !isSupportLineClamp), + [styles.lineClamp]: (lines && isSupportLineClamp), + }); + + if (!lines && !length) { + return ({children}); + } + + // length + if (!lines) { + return (); + } + + const id = `antd-pro-ellipsis-${`${new Date().getTime()}${Math.floor(Math.random() * 100)}`}`; + + // support document.body.style.webkitLineClamp + if (isSupportLineClamp) { + const style = `#${id}{-webkit-line-clamp:${lines};}`; + return ( +
+ + { + tooltip ? ({children}) : children + } +
); + } + + const childNode = ( + + { + (targetCount > 0) && text.substring(0, targetCount) + } + { + (targetCount > 0) && (targetCount < text.length) && '...' + } + + ); + + return ( +
+
+ { + tooltip ? ( + {childNode} + ) : childNode + } +
{children}
+
{text}
+
+
+ ); + } +} diff --git a/user-dashboard/js/dashboard/src/components/Ellipsis/index.less b/user-dashboard/js/dashboard/src/components/Ellipsis/index.less new file mode 100644 index 000000000..acd837899 --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/Ellipsis/index.less @@ -0,0 +1,25 @@ +.ellipsis { + overflow: hidden; + display: inline-block; + word-break: break-all; + width: 100%; +} + +.lines { + position: relative; + .shadow { + display: block; + position: relative; + color: transparent; + opacity: 0; + z-index: -999; + } +} + +.lineClamp { + position: relative; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; +} diff --git a/user-dashboard/js/dashboard/src/components/Exception/index.js b/user-dashboard/js/dashboard/src/components/Exception/index.js new file mode 100644 index 000000000..f9d458b32 --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/Exception/index.js @@ -0,0 +1,36 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React, { createElement } from 'react'; +import classNames from 'classnames'; +import { Button } from 'antd'; +import config from './typeConfig'; +import styles from './index.less'; + +export default ({ className, linkElement = 'a', type, title, desc, img, actions, ...rest }) => { + const pageType = type in config ? type : '404'; + const clsString = classNames(styles.exception, className); + return ( +
+
+
+
+
+

{title || config[pageType].title}

+
{desc || config[pageType].desc}
+
+ { + actions || + createElement(linkElement, { + to: '/', + href: '/', + }, ) + } +
+
+
+ ); +}; diff --git a/user-dashboard/js/dashboard/src/components/Exception/index.less b/user-dashboard/js/dashboard/src/components/Exception/index.less new file mode 100644 index 000000000..22e0c1ee2 --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/Exception/index.less @@ -0,0 +1,78 @@ +@import "~antd/lib/style/themes/default.less"; +@import "~antd/lib/style/mixins/clearfix.less"; + +.exception { + display: flex; + align-items: center; + height: 100%; + + .imgBlock { + flex: 0 0 62.5%; + width: 62.5%; + padding-right: 152px; + .clearfix(); + } + + .imgEle { + height: 360px; + width: 100%; + max-width: 430px; + float: right; + background-repeat: no-repeat; + background-position: 50% 50%; + background-size: 100% 100%; + } + + .content { + flex: auto; + + h1 { + color: #434e59; + font-size: 72px; + font-weight: 600; + line-height: 72px; + margin-bottom: 24px; + } + + .desc { + color: @text-color-secondary; + font-size: 20px; + line-height: 28px; + margin-bottom: 16px; + } + + .actions { + button:not(:last-child) { + margin-right: 8px; + } + } + } +} + +@media screen and (max-width: @screen-xl) { + .exception { + .imgBlock { + padding-right: 88px; + } + } +} + +@media screen and (max-width: @screen-sm) { + .exception { + display: block; + text-align: center; + .imgBlock { + padding-right: 0; + margin: 0 auto 24px; + } + } +} + +@media screen and (max-width: @screen-xs) { + .exception { + .imgBlock { + margin-bottom: -24px; + overflow: hidden; + } + } +} diff --git a/user-dashboard/js/dashboard/src/components/Exception/typeConfig.js b/user-dashboard/js/dashboard/src/components/Exception/typeConfig.js new file mode 100644 index 000000000..84dce2b8e --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/Exception/typeConfig.js @@ -0,0 +1,22 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +const config = { + 403: { + img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg', + title: '403', + desc: 'Sorry, you have no permission.', + }, + 404: { + img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg', + title: '404', + desc: 'Sorry, page not found', + }, + 500: { + img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg', + title: '500', + desc: 'Sorry, something wrong with server', + }, +}; + +export default config; diff --git a/user-dashboard/js/dashboard/src/components/FooterToolbar/index.js b/user-dashboard/js/dashboard/src/components/FooterToolbar/index.js new file mode 100644 index 000000000..f49959d63 --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/FooterToolbar/index.js @@ -0,0 +1,21 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React, { Component } from 'react'; +import classNames from 'classnames'; +import styles from './index.less'; + +export default class FooterToolbar extends Component { + render() { + const { children, className, extra, ...restProps } = this.props; + return ( +
+
{extra}
+
{children}
+
+ ); + } +} diff --git a/user-dashboard/js/dashboard/src/components/FooterToolbar/index.less b/user-dashboard/js/dashboard/src/components/FooterToolbar/index.less new file mode 100644 index 000000000..a0bbb52e3 --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/FooterToolbar/index.less @@ -0,0 +1,33 @@ +@import "~antd/lib/style/themes/default.less"; + +.toolbar { + position: fixed; + width: 100%; + bottom: 0; + right: 0; + height: 56px; + line-height: 56px; + box-shadow: 0 -1px 2px rgba(0, 0, 0, .03); + background: #fff; + border-top: 1px solid @border-color-split; + padding: 0 24px; + transition: all .3s; + + &:after { + content: ""; + display: block; + clear: both; + } + + .left { + float: left; + } + + .right { + float: right; + } + + button + button { + margin-left: 8px; + } +} diff --git a/user-dashboard/js/dashboard/src/components/GlobalFooter/index.js b/user-dashboard/js/dashboard/src/components/GlobalFooter/index.js new file mode 100644 index 000000000..43454c30d --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/GlobalFooter/index.js @@ -0,0 +1,30 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React from 'react'; +import classNames from 'classnames'; +import styles from './index.less'; + +export default ({ className, links, copyright }) => { + const clsString = classNames(styles.globalFooter, className); + return ( +
+ { + links && ( +
+ {links.map(link => ( + + {link.title} + + ))} +
+ ) + } + {copyright &&
{copyright}
} +
+ ); +}; diff --git a/user-dashboard/js/dashboard/src/components/GlobalFooter/index.less b/user-dashboard/js/dashboard/src/components/GlobalFooter/index.less new file mode 100644 index 000000000..7fce60042 --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/GlobalFooter/index.less @@ -0,0 +1,29 @@ +@import "~antd/lib/style/themes/default.less"; + +.globalFooter { + padding: 0 16px; + margin: 48px 0 24px 0; + text-align: center; + + .links { + margin-bottom: 8px; + + a { + color: @text-color-secondary; + transition: all .3s; + + &:not(:last-child) { + margin-right: 40px; + } + + &:hover { + color: @text-color; + } + } + } + + .copyright { + color: @text-color-secondary; + font-size: @font-size-base; + } +} diff --git a/user-dashboard/js/dashboard/src/components/PageHeader/index.js b/user-dashboard/js/dashboard/src/components/PageHeader/index.js new file mode 100644 index 000000000..c89e187b7 --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/PageHeader/index.js @@ -0,0 +1,203 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React, { PureComponent, createElement } from 'react'; +import PropTypes from 'prop-types'; +import { Breadcrumb, Tabs } from 'antd'; +import classNames from 'classnames'; +import styles from './index.less'; +import { injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; + +const messages = defineMessages({ + home: { + id: "Home", + defaultMessage: "Home" + }, + menu: { + Chain: { + id: "Menu.Chain", + defaultMessage: "Chain" + }, + ChainNew: { + id: "Path.ChainNew", + defaultMessage: "New Chain" + }, + SmartContract: { + id: "Menu.SmartContract", + defaultMessage: "Smart Contract" + }, + API: { + id: "Menu.API", + defaultMessage: "API" + }, + Token: { + id: "Menu.Token", + defaultMessage: "Token" + }, + Account: { + id: "Menu.Account", + defaultMessage: "Account" + }, + Asset: { + id: "Menu.Asset", + defaultMessage: "Asset" + }, + Log: { + id: "Menu.Log", + defaultMessage: "Log" + }, + SmartContractStore: { + id: "Menu.SmartContractStore", + defaultMessage: "Smart Contract Store" + }, + }, +}) + +const { TabPane } = Tabs; + +function getBreadcrumbNameWithParams(breadcrumbNameMap, url) { + let name = ''; + Object.keys(breadcrumbNameMap).forEach((item) => { + const itemRegExpStr = `^${item.replace(/:[\w-]+/g, '[\\w-]+')}$`; + const itemRegExp = new RegExp(itemRegExpStr); + if (itemRegExp.test(url)) { + // name = breadcrumbNameMap[item]; + name = + } + }); + return name; +} + +class PageHeader extends PureComponent { + static contextTypes = { + routes: PropTypes.array, + params: PropTypes.object, + location: PropTypes.object, + breadcrumbNameMap: PropTypes.object, + }; + onChange = (key) => { + if (this.props.onTabChange) { + this.props.onTabChange(key); + } + }; + getBreadcrumbProps = () => { + return { + routes: this.props.routes || this.context.routes, + params: this.props.params || this.context.params, + location: this.props.location || this.context.location, + breadcrumbNameMap: this.props.breadcrumbNameMap || this.context.breadcrumbNameMap, + }; + }; + itemRender = (route, params, routes, paths) => { + const { linkElement = 'a' } = this.props; + const last = routes.indexOf(route) === routes.length - 1; + return (last || !route.component) + ? {route.breadcrumbName} + : createElement(linkElement, { + href: paths.join('/') || '/', + to: paths.join('/') || '/', + }, route.breadcrumbName); + } + render() { + const { routes, params, location, breadcrumbNameMap } = this.getBreadcrumbProps(); + const { + title, logo, action, content, extraContent, intl, + breadcrumbList, tabList, className, linkElement = 'a', + } = this.props; + const clsString = classNames(styles.pageHeader, className); + let breadcrumb; + if (routes && params) { + breadcrumb = ( + route.breadcrumbName)} + params={params} + itemRender={this.itemRender} + /> + ); + } else if (location && location.pathname) { + const pathSnippets = location.pathname.split('/').filter(i => i); + const extraBreadcrumbItems = pathSnippets.map((_, index) => { + const url = `/${pathSnippets.slice(0, index + 1).join('/')}`; + return ( + + {createElement( + index === pathSnippets.length - 1 ? 'span' : linkElement, + { [linkElement === 'a' ? 'href' : 'to']: url }, + intl.formatMessage(messages.menu[breadcrumbNameMap[url] || + breadcrumbNameMap[url.replace('/', '')] || + getBreadcrumbNameWithParams(breadcrumbNameMap, url) || + url]) + )} + + ); + }); + const breadcrumbItems = [( + + {createElement(linkElement, { + [linkElement === 'a' ? 'href' : 'to']: '/', + }, intl.formatMessage(messages.home))} + + )].concat(extraBreadcrumbItems); + breadcrumb = ( + + {breadcrumbItems} + + ); + } else if (breadcrumbList && breadcrumbList.length) { + breadcrumb = ( + + { + breadcrumbList.map(item => ( + + {item.href ? ( + createElement(linkElement, { + [linkElement === 'a' ? 'href' : 'to']: item.href, + }, '首页') + ) : item.title} + ) + ) + } + + ); + } else { + breadcrumb = null; + } + + const tabDefaultValue = tabList && (tabList.filter(item => item.default)[0] || tabList[0]); + + return ( +
+ {breadcrumb} +
+ {logo &&
{logo}
} +
+
+ {title &&

{title}

} + {action &&
{action}
} +
+
+ {content &&
{content}
} + {extraContent &&
{extraContent}
} +
+
+
+ { + tabList && + tabList.length && + + { + tabList.map(item => ) + } + + } +
+ ); + } +} + +export default injectIntl(PageHeader) diff --git a/user-dashboard/js/dashboard/src/components/PageHeader/index.less b/user-dashboard/js/dashboard/src/components/PageHeader/index.less new file mode 100644 index 000000000..a1e3b2e91 --- /dev/null +++ b/user-dashboard/js/dashboard/src/components/PageHeader/index.less @@ -0,0 +1,138 @@ +@import "~antd/lib/style/themes/default.less"; + +.pageHeader { + background: @component-background; + padding: 16px 32px 0 32px; + border-bottom: @border-width-base @border-style-base @border-color-split; + + .detail { + display: flex; + } + + .row { + display: flex; + } + + .breadcrumb { + margin-bottom: 16px; + } + + .tabs { + margin: 0 0 -17px -8px; + + :global { + .ant-tabs-bar { + border-bottom: @border-width-base @border-style-base @border-color-split; + } + } + } + + .logo { + flex: 0 1 auto; + margin-right: 16px; + padding-top: 1px; + > img { + width: 28px; + height: 28px; + border-radius: @border-radius-base; + display: block; + } + } + + .title { + font-size: 20px; + font-weight: 500; + color: @heading-color; + } + + .action { + margin-left: 56px; + min-width: 266px; + + :global { + .ant-btn-group:not(:last-child), + .ant-btn:not(:last-child) { + margin-right: 8px; + } + + .ant-btn-group > .ant-btn { + margin-right: 0; + } + } + } + + .title, .action, .content, .extraContent, .main { + flex: auto; + } + + .title, .action { + margin-bottom: 16px; + } + + .logo, .content, .extraContent { + margin-bottom: 16px; + } + + .action, .extraContent { + text-align: right; + } + + .extraContent { + margin-left: 88px; + min-width: 242px; + } +} + +@media screen and (max-width: @screen-xl) { + .pageHeader { + .extraContent { + margin-left: 44px; + } + } +} + +@media screen and (max-width: @screen-lg) { + .pageHeader { + .extraContent { + margin-left: 20px; + } + } +} + +@media screen and (max-width: @screen-md) { + .pageHeader { + .row { + display: block; + } + + .action, .extraContent { + margin-left: 0; + text-align: left; + } + } +} + +@media screen and (max-width: @screen-sm) { + .pageHeader { + .detail { + display: block; + } + } +} + +@media screen and (max-width: @screen-xs) { + .pageHeader { + .action { + :global { + .ant-btn-group, .ant-btn { + display: block; + margin-bottom: 8px; + } + .ant-btn-group > .ant-btn { + display: inline-block; + margin-bottom: 0; + } + } + } + } +} diff --git a/user-dashboard/js/dashboard/src/g2.js b/user-dashboard/js/dashboard/src/g2.js new file mode 100644 index 000000000..f6520c329 --- /dev/null +++ b/user-dashboard/js/dashboard/src/g2.js @@ -0,0 +1,28 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import G2 from 'g2'; + +G2.track(false); + +const colors = [ + '#8543E0', '#F04864', '#FACC14', '#1890FF', '#13C2C2', '#2FC25B', '#fa8c16', '#a0d911', +]; + +const config = { + ...G2.Theme, + defaultColor: '#1089ff', + colors: { + default: colors, + intervalStack: colors, + }, + tooltip: { + background: { + radius: 4, + fill: '#000', + fillOpacity: 0.75, + }, + }, +}; + +G2.Global.setTheme(config); diff --git a/user-dashboard/js/dashboard/src/index.js b/user-dashboard/js/dashboard/src/index.js new file mode 100644 index 000000000..525bb685a --- /dev/null +++ b/user-dashboard/js/dashboard/src/index.js @@ -0,0 +1,29 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import dva from 'dva'; +import 'moment/locale/zh-cn'; +import models from './models'; +import './polyfill'; +import './g2'; +// import { browserHistory } from 'dva/router'; +import './index.less'; + +// 1. Initialize +const app = dva({ + // history: browserHistory, +}); + +// 2. Plugins +// app.use({}); + +// 3. Model move to router +models.forEach((m) => { + app.model(m); +}); + +// 4. Router +app.router(require('./router')); + +// 5. Start +app.start('#root'); diff --git a/user-dashboard/js/dashboard/src/index.less b/user-dashboard/js/dashboard/src/index.less new file mode 100644 index 000000000..5da336e8c --- /dev/null +++ b/user-dashboard/js/dashboard/src/index.less @@ -0,0 +1,11 @@ +@import '~antd/lib/style/v2-compatible-reset.less'; + +html, body, :global(#root) { + height: 100%; +} + +body { + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/user-dashboard/js/dashboard/src/layouts/BasicLayout.js b/user-dashboard/js/dashboard/src/layouts/BasicLayout.js new file mode 100644 index 000000000..6b02c5f56 --- /dev/null +++ b/user-dashboard/js/dashboard/src/layouts/BasicLayout.js @@ -0,0 +1,261 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Layout, Menu, Icon, Avatar, Dropdown, Tag, message, Spin, Button } from 'antd'; +import DocumentTitle from 'react-document-title'; +import { connect } from 'dva'; +import { Link, Route, Redirect, Switch } from 'dva/router'; +import { injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; +import { ContainerQuery } from 'react-container-query'; +import classNames from 'classnames'; +import styles from './BasicLayout.less'; +import GlobalFooter from '../components/GlobalFooter'; +import { getNavData } from '../common/nav'; +import { getRouteData } from '../utils/utils'; +import messages from './BasicLayoutMessages' + +const { Header, Sider, Content } = Layout; +const { SubMenu } = Menu; + +const query = { + 'screen-xs': { + maxWidth: 575, + }, + 'screen-sm': { + minWidth: 576, + maxWidth: 767, + }, + 'screen-md': { + minWidth: 768, + maxWidth: 991, + }, + 'screen-lg': { + minWidth: 992, + maxWidth: 1199, + }, + 'screen-xl': { + minWidth: 1200, + }, +}; + +class BasicLayout extends React.PureComponent { + static childContextTypes = { + location: PropTypes.object, + breadcrumbNameMap: PropTypes.object, + } + constructor(props) { + super(props); + // 把一级 Layout 的 children 作为菜单项 + this.menus = getNavData().reduce((arr, current) => arr.concat(current.children), []); + this.state = { + openKeys: this.getDefaultCollapsedSubMenus(props), + }; + } + getChildContext() { + const { location } = this.props; + const routeData = getRouteData('BasicLayout'); + const menuData = getNavData().reduce((arr, current) => arr.concat(current.children), []); + const breadcrumbNameMap = {}; + routeData.concat(menuData).forEach((item) => { + breadcrumbNameMap[item.path] = item.messageId; + }); + return { location, breadcrumbNameMap }; + } + componentDidMount() { + this.props.dispatch({ + type: 'user/fetchCurrent', + }); + } + componentWillUnmount() { + clearTimeout(this.resizeTimeout); + } + onCollapse = (collapsed) => { + this.props.dispatch({ + type: 'global/changeLayoutCollapsed', + payload: collapsed, + }); + } + onMenuClick = ({ key }) => { + if (key === 'logout') { + window.location.href = "/logout" + } + } + getDefaultCollapsedSubMenus(props) { + const currentMenuSelectedKeys = [...this.getCurrentMenuSelectedKeys(props)]; + currentMenuSelectedKeys.splice(-1, 1); + if (currentMenuSelectedKeys.length === 0) { + return ['chain']; + } + return currentMenuSelectedKeys; + } + getCurrentMenuSelectedKeys(props) { + const { location: { pathname } } = props || this.props; + const keys = pathname.split('/').slice(1); + if (keys.length === 1 && keys[0] === '') { + return [this.menus[0].key]; + } + return keys; + } + getNavMenuItems(menusData, parentPath = '') { + if (!menusData) { + return []; + } + return menusData.map((item) => { + if (!item.name) { + return null; + } + let itemPath; + if (item.path.indexOf('http') === 0) { + itemPath = item.path; + } else { + itemPath = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/'); + } + if (item.children && item.children.some(child => child.name)) { + return ( + + + {item.name} + + ) : item.name + } + key={item.key || item.path} + > + {this.getNavMenuItems(item.children, itemPath)} + + ); + } + const icon = item.icon && ; + return ( + + { + /^https?:\/\//.test(itemPath) ? ( + + {icon}{item.name} + + ) : ( + + {icon} + + ) + } + + ); + }); + } + getPageTitle() { + const { location, intl } = this.props; + const { pathname } = location; + let title = intl.formatMessage(messages.menu.Chain); + return title; + } + changeLanguage () { + if (window.language === "zh-CN") { + window.location.href = "/languages/en" + } else { + window.location.href = "/languages/zh-CN" + } + } + render() { + const { collapsed, intl } = this.props; + + const menu = ( + + + + ); + + // Don't show popup menu when it is been collapsed + const menuProps = collapsed ? {} : { + openKeys: this.state.openKeys, + }; + + const layout = ( + + +
+ + logo +

Cello Baas

+ +
+ + {this.getNavMenuItems(this.menus)} + +
+ +
+ +
+ + + + {window.username} + + +
+
+ + + { + getRouteData('BasicLayout').map(item => + ( + + ) + ) + } + + + + Copyright Cello +
+ } + /> + + + + ); + + return ( + + + {params =>
{layout}
} +
+
+ ); + } +} + +export default connect(state => ({ + collapsed: state.global.collapsed, +}))(injectIntl(BasicLayout)); diff --git a/user-dashboard/js/dashboard/src/layouts/BasicLayout.less b/user-dashboard/js/dashboard/src/layouts/BasicLayout.less new file mode 100644 index 000000000..d59a45d9e --- /dev/null +++ b/user-dashboard/js/dashboard/src/layouts/BasicLayout.less @@ -0,0 +1,113 @@ +@import "~antd/lib/style/themes/default.less"; + +.header { + padding: 0 12px 0 0; + background: #fff; + box-shadow: 0 1px 4px rgba(0, 21, 41, .08); + position: relative; +} + +.logo { + height: 64px; + position: relative; + line-height: 64px; + padding-left: 24px; + transition: all .3s; + background: #002140; + overflow: hidden; + img { + display: inline-block; + vertical-align: middle; + height: 32px; + } + h1 { + color: #fff; + display: inline-block; + vertical-align: middle; + font-size: 20px; + margin-left: 12px; + font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif; + font-weight: 600; + } +} + +:global(.ant-layout-sider-collapsed) .logo { + padding-left: 24px; + > a { + width: 32px; + } +} + +.trigger { + font-size: 20px; + line-height: 64px; + cursor: pointer; + transition: all .3s; + padding: 0 24px; + &:hover { + background: @primary-1; + } +} + +@media screen and (max-width: @screen-xs) { + .trigger { + display: none; + } +} + +.right { + float: right; + height: 100%; + .action { + cursor: pointer; + padding: 0 12px; + display: inline-block; + transition: all .3s; + height: 100%; + > i { + font-size: 16px; + vertical-align: middle; + } + &:global(.ant-popover-open), + &:hover { + background: @primary-1; + } + } + .search { + padding: 0; + margin: 0 12px; + &:hover { + background: transparent; + } + } + .account { + .avatar { + margin: 20px 8px 20px 0; + color: @primary-color; + background: rgba(255, 255, 255, .85); + vertical-align: middle; + } + } +} + +.menu { + :global(.anticon) { + margin-right: 8px; + } + :global(.ant-dropdown-menu-item) { + width: 160px; + } +} + +:global { + .ant-layout { + overflow-x: hidden; + } +} + +.sider { + min-height: 100vh; + box-shadow: 2px 0 6px rgba(0, 21, 41, .35); + position: relative; + z-index: 10; +} diff --git a/user-dashboard/js/dashboard/src/layouts/BasicLayoutMessages.js b/user-dashboard/js/dashboard/src/layouts/BasicLayoutMessages.js new file mode 100644 index 000000000..8c53897f3 --- /dev/null +++ b/user-dashboard/js/dashboard/src/layouts/BasicLayoutMessages.js @@ -0,0 +1,53 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import { defineMessages } from 'react-intl'; + +const messages = defineMessages({ + dropdownMenu: { + logout: { + id: "Header.DropdownMenu.Logout", + defaultMessage: "Logout" + } + }, + menu: { + Chain: { + id: "Menu.Chain", + defaultMessage: "Chain" + }, + ChainNew: { + id: "Path.ChainNew", + defaultMessage: "Apply New Chain" + }, + SmartContract: { + id: "Menu.SmartContract", + defaultMessage: "Smart Contract" + }, + API: { + id: "Menu.API", + defaultMessage: "API" + }, + Token: { + id: "Menu.Token", + defineMessage: "Token" + }, + Account: { + id: "Menu.Account", + defineMessage: "Account" + }, + Asset: { + id: "Menu.Asset", + defineMessage: "Asset" + }, + Log: { + id: "Menu.Log", + defaultMessage: "Log" + }, + SmartContractStore: { + id: "Menu.SmartContractStore", + defaultMessage: "Smart Contract Store" + }, + }, +}); + +export default messages diff --git a/user-dashboard/js/dashboard/src/layouts/PageHeaderLayout.js b/user-dashboard/js/dashboard/src/layouts/PageHeaderLayout.js new file mode 100644 index 000000000..39abf5d46 --- /dev/null +++ b/user-dashboard/js/dashboard/src/layouts/PageHeaderLayout.js @@ -0,0 +1,15 @@ +/* + 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'; + +export default ({ children, wrapperClassName, top, ...restProps }) => ( +
+ {top} + + {children ?
{children}
: null} +
+); diff --git a/user-dashboard/js/dashboard/src/layouts/PageHeaderLayout.less b/user-dashboard/js/dashboard/src/layouts/PageHeaderLayout.less new file mode 100644 index 000000000..a0c0a6efe --- /dev/null +++ b/user-dashboard/js/dashboard/src/layouts/PageHeaderLayout.less @@ -0,0 +1,11 @@ +@import "~antd/lib/style/themes/default.less"; + +.content { + margin: 24px 24px 0; +} + +@media screen and (max-width: @screen-sm) { + .content { + margin: 24px 0 0; + } +} diff --git a/user-dashboard/js/dashboard/src/locales/en-US.js b/user-dashboard/js/dashboard/src/locales/en-US.js new file mode 100644 index 000000000..4bc2d24e1 --- /dev/null +++ b/user-dashboard/js/dashboard/src/locales/en-US.js @@ -0,0 +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'; + +window.appLocale = { + messages: { + ...enMessages, + }, + antd: antdEn, + locale: 'en-US', + data: appLocaleData, +}; diff --git a/user-dashboard/js/dashboard/src/locales/en.json b/user-dashboard/js/dashboard/src/locales/en.json new file mode 100644 index 000000000..f3c197231 --- /dev/null +++ b/user-dashboard/js/dashboard/src/locales/en.json @@ -0,0 +1,106 @@ +{ + "Home": "Home", + "Menu.Chain": "Chain", + "Menu.SmartContract": "Smart Contract", + "Menu.API": "API", + "Menu.Token": "Token", + "Menu.Account": "Account", + "Menu.Asset": "Asset", + "Menu.Log": "Log", + "Menu.SmartContractStore": "Smart Contract Store", + "Path.ChainNew": "Apply New Chain", + "Header.DropdownMenu.Logout": "Logout", + "Button.Submit": "Submit", + "Button.Confirm": "Confirm", + "Button.Cancel": "Cancel", + "Global.message.success.newTransaction": "Got new transaction {name}", + "Chain.Index.PageHeader.Content.Title.createTime": "Create Time", + "Chain.Index.PageHeader.Content.Button.changeName": "Change Name", + "Chain.Index.PageHeader.Content.Button.release": "Release", + "Chain.Index.PageHeader.Extra.Title.status": "Status", + "Chain.Index.PageHeader.Extra.Content.status.running": "Running", + "Chain.Index.PageHeader.Extra.Content.status.initializing": "Initializing", + "Chain.Index.PageHeader.Extra.Content.status.releasing": "Releasing", + "Chain.Index.PageHeader.Extra.Content.status.error": "Error", + "Chain.Index.PageHeader.Extra.Title.type": "Type", + "Chain.Index.PageHeader.Extra.Title.running": "Running Hours", + "Chain.Index.Modal.Confirm.Title.release": "Do you want to release chain {name}?", + "Chain.Index.Overview.Title.peer": "Peer", + "Chain.Index.Overview.Title.block": "Block", + "Chain.Index.Overview.Title.smartContract": "Smart Contract", + "Chain.Index.Overview.Title.transaction": "Transaction", + "Chain.Index.Overview.Help.peer": "Peer Number of Chain", + "Chain.Index.Overview.Help.block": "Block Number of Chain", + "Chain.Index.Overview.Help.smartContract": "Smart Contract of Chain", + "Chain.Index.Overview.Help.transaction": "Transaction of Chain", + "Chain.EmptyView.Title": "You did not add any block chains", + "Chain.EmptyView.Content": "Speed, free, stable, and so what, hurry up to apply it", + "Chain.EmptyView.ApplyButton": "Apply Now", + "Chain.Edit.form.title": "Update chain name", + "Chain.New.configuration.basic.title": "{type} - Basic Config", + "Chain.New.configuration.basic.content": "Free, suitable for {type} base developers", + "Chain.New.configuration.advance.title": "{type} - Advance Config", + "Chain.New.configuration.advance.content": "Limited time free, suitable for {type} advanced developers", + "Chain.New.form.placeholder.chainType": "Please select a chain type", + "Chain.New.form.label.name": "Name", + "Chain.New.form.label.chainType": "Chain Type", + "Chain.New.form.label.configuration": "Configuration", + "Chain.New.form.validate.name.required": "Must input chain name", + "Chain.New.form.validate.name.invalidName": "Please input valid Name", + "Chain.New.form.validate.name.invalidLength": "Name max length must be less than 20", + "Chain.New.form.validate.chainType.required": "Please select a chain type", + "Chain.New.form.validate.configuration.required": "Please select a configuration", + "Chain.message.success.applyChain": "Apply Chain {name} success.", + "Chain.message.success.releaseChain": "Release Chain {name} success.", + "Chain.message.fail.applyChain": "Apply Chain {name} fail.", + "Chain.message.fail.releaseChain": "Release Chain {name} fail.", + "ChainCode.button.newSmartContract": "Add New Smart Contract", + "ChainCode.button.install": "Install", + "ChainCode.button.instantiate": "Instantiate", + "ChainCode.button.delete": "Delete", + "ChainCode.tooltip.edit": "Edit", + "ChainCode.confirm.deleteChainCode": "Do you want to delete smart contract code {name}?", + "ChainCode.message.success.deleteChainCode": "Delete smart contract code {name} success.", + "ChainCode.message.success.updateChainCode": "Update Smart contract code success.", + "ChainCode.message.success.installChainCode": "Install Smart contract code success.", + "ChainCode.message.success.instantiateChainCode": "Deploy Smart contract code task created success.", + "ChainCode.message.success.instantiateChainCodeDone": "Deploy Smart contract code is done.", + "ChainCode.message.fail.deleteChainCode": "Delete smart contract code {name} failed.", + "ChainCode.message.fail.updateChainCode": "Update Smart contract code failed.", + "ChainCode.message.fail.installChainCode": "Install Smart contract code failed.", + "ChainCode.message.fail.instantiateChainCode": "Deploy Smart contract code task created failed.", + "ChainCode.message.fail.listChainCode": "List Smart contract failed.", + "ChainCode.message.test": "test", + "ChainCode.Edit.form.title": "Update smart contract name", + "ChainCode.modal.title.installChainCode": "Install Smart Contract", + "ChainCode.modal.title.instantiateChainCode": "Deploy Smart Contract", + "ChainCode.form.validate.chain.required": "Must select a chain", + "ChainCode.status.uploaded": "Uploaded and not deployed", + "ChainCode.status.installed": "Uploaded and installed", + "ChainCode.status.instantiated": "Deployed at {name}", + "ChainCode.status.instantiating": "Deploying Smart Contract", + "ChainCode.status.error": "Error", + "API.pageHeader.content": "Contract call", + "API.button.newParam": "New Parameter", + "API.button.call": "Call", + "API.form.label.smartContract": "Smart Contract", + "API.form.label.function": "Function Name", + "API.form.label.parameter": "Parameter", + "API.form.label.method": "Method", + "API.form.validate.required.chainCode": "Must select a Smart contract", + "API.form.validate.required.parameter": "Must input parameter", + "API.form.validate.required.function": "Must input function name", + "API.form.validate.required.method": "Must select a method", + "API.form.options.method.invoke": "Invoke", + "API.form.options.method.query": "Query", + "API.form.info.parameter": "Use , to separate parameters. Example: move, a, b, 100", + "Token.modal.issueToken.title": "Issue Token", + "Token.modal.issueToken.form.label.name": "Token Name", + "Token.modal.issueToken.form.required.name": "Must input token name", + "Token.modal.issueToken.form.required.address": "Must input issue address", + "Token.modal.issueToken.form.required.initialCount": "Must input initial token count", + "Token.modal.issueToken.form.required.maxFee": "Must input max fee", + "Token.modal.issueToken.form.label.maxFee": "Max Fee", + "Token.modal.issueToken.form.label.address": "Issue Address", + "Token.modal.issueToken.form.label.initialCount": "Initial Token Count" +} diff --git a/user-dashboard/js/dashboard/src/locales/zh-CN.js b/user-dashboard/js/dashboard/src/locales/zh-CN.js new file mode 100644 index 000000000..8087496d4 --- /dev/null +++ b/user-dashboard/js/dashboard/src/locales/zh-CN.js @@ -0,0 +1,15 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import appLocaleData from 'react-intl/locale-data/zh'; +import zhMessages from './zh.json'; +import zhCN from 'antd/lib/locale-provider/zh_CN'; + +window.appLocale = { + messages: { + ...zhMessages, + }, + antd: zhCN, + locale: 'zh-Hans', + data: appLocaleData, +}; diff --git a/user-dashboard/js/dashboard/src/locales/zh.json b/user-dashboard/js/dashboard/src/locales/zh.json new file mode 100644 index 000000000..53b19f199 --- /dev/null +++ b/user-dashboard/js/dashboard/src/locales/zh.json @@ -0,0 +1,106 @@ +{ + "Home": "首页", + "Menu.Chain": "链路", + "Menu.SmartContract": "智能合约", + "Menu.API": "接口", + "Menu.Token": "代币", + "Menu.Account": "账户", + "Menu.Asset": "资产", + "Menu.Log": "日志", + "Menu.SmartContractStore": "智能合约市场", + "Header.DropdownMenu.Logout": "退出", + "Path.ChainNew": "申请新的链路", + "Button.Submit": "提交", + "Button.Confirm": "确认", + "Button.Cancel": "取消", + "Global.message.success.newTransaction": "收到新的交易 {name}", + "Chain.Index.PageHeader.Content.Button.changeName": "修改名称", + "Chain.Index.PageHeader.Content.Button.release": "释放", + "Chain.Index.PageHeader.Content.Title.createTime": "创建时间", + "Chain.Index.PageHeader.Extra.Content.status.running": "运行中", + "Chain.Index.PageHeader.Extra.Content.status.initializing": "正在初始化", + "Chain.Index.PageHeader.Extra.Content.status.releasing": "正在释放", + "Chain.Index.PageHeader.Extra.Content.status.error": "出错", + "Chain.Index.PageHeader.Extra.Title.status": "状态", + "Chain.Index.PageHeader.Extra.Title.type": "类型", + "Chain.Index.PageHeader.Extra.Title.running": "运行时间 (小时)", + "Chain.Index.Modal.Confirm.Title.release": "是否要释放链 {name}?", + "Chain.Index.Overview.Title.peer": "节点", + "Chain.Index.Overview.Title.block": "区块", + "Chain.Index.Overview.Title.smartContract": "智能合约", + "Chain.Index.Overview.Title.transaction": "交易", + "Chain.Index.Overview.Help.peer": "链的节点个数", + "Chain.Index.Overview.Help.block": "链的区块个数", + "Chain.Index.Overview.Help.smartContract": "链的智能合约个数", + "Chain.Index.Overview.Help.transaction": "链的交易数", + "Chain.EmptyView.Title": "您还没添加任何区块链", + "Chain.EmptyView.Content": "极速、免费、稳定,还等什么,赶快申请吧", + "Chain.EmptyView.ApplyButton": "立即申请", + "Chain.Edit.form.title": "更新链名称", + "Chain.New.configuration.basic.title": "{type} - 基础配置", + "Chain.New.configuration.basic.content": "免费, 适用于 {type} 基础开发者使用", + "Chain.New.configuration.advance.title": "{type} - 高级配置", + "Chain.New.configuration.advance.content": "限时免费, 适用于 {type} 高级开发者使用", + "Chain.New.form.placeholder.chainType": "请选择一个链类型", + "Chain.New.form.label.name": "名称", + "Chain.New.form.label.chainType": "链类型", + "Chain.New.form.label.configuration": "配置", + "Chain.New.form.validate.name.required": "必须输入链名称", + "Chain.New.form.validate.name.invalidName": "请输入有效的名字", + "Chain.New.form.validate.name.invalidLength": "名字的最大长度必须小于20", + "Chain.New.form.validate.chainType.required": "请选择一个链类型", + "Chain.New.form.validate.configuration.required": "请选择一个配置选项", + "Chain.message.success.applyChain": "申请链路 {name} 成功.", + "Chain.message.success.releaseChain": "释放链路 {name} 成功.", + "Chain.message.fail.applyChain": "申请链路 {name} 失败.", + "Chain.message.fail.releaseChain": "释放链路 {name} 失败.", + "ChainCode.Edit.form.title": "更新智能合约名称", + "ChainCode.button.newSmartContract": "新增智能合约", + "ChainCode.button.install": "安装", + "ChainCode.button.instantiate": "部署", + "ChainCode.button.delete": "删除", + "ChainCode.tooltip.edit": "编辑", + "ChainCode.message.success.deleteChainCode": "删除智能合约 {name} 成功.", + "ChainCode.message.success.updateChainCode": "更新智能合约成功.", + "ChainCode.message.success.installChainCode": "安装智能合约成功.", + "ChainCode.message.success.instantiateChainCode": "部署智能合约任务提交成功.", + "ChainCode.message.success.instantiateChainCodeDone": "部署智能合约完成.", + "ChainCode.message.fail.deleteChainCode": "删除智能合约 {name} 失败.", + "ChainCode.message.fail.updateChainCode": "更新智能合约失败.", + "ChainCode.message.fail.installChainCode": "安装智能合约失败.", + "ChainCode.message.fail.instantiateChainCode": "部署智能合约任务提交失败.", + "ChainCode.message.fail.listChainCode": "列取智能合约失败.", + "ChainCode.message.test": "测试", + "ChainCode.confirm.deleteChainCode": "您是否确认删除智能合约 {name}?", + "ChainCode.modal.title.installChainCode": "安装智能合约", + "ChainCode.modal.title.instantiateChainCode": "部署智能合约", + "ChainCode.form.validate.chain.required": "必须选择一个链路", + "ChainCode.status.uploaded": "新上传未部署", + "ChainCode.status.installed": "上传并已安装", + "ChainCode.status.instantiated": "已部署在 {name}", + "ChainCode.status.instantiating": "正在部署智能合约", + "ChainCode.status.error": "出错", + "API.pageHeader.content": "合约调用", + "API.button.newParam": "新参数", + "API.button.call": "发起", + "API.form.label.smartContract": "智能合约", + "API.form.label.function": "函数名称", + "API.form.label.parameter": "参数", + "API.form.label.method": "方法", + "API.form.validate.required.chainCode": "必须选择一个智能合约", + "API.form.validate.required.parameter": "必须输入参数", + "API.form.validate.required.function": "必须输入函数名称", + "API.form.validate.required.method": "必须选择一个方式", + "API.form.options.method.invoke": "调用", + "API.form.options.method.query": "查询", + "API.form.info.parameter": "使用 , 分割参数. 例如 move, a, b, 100", + "Token.modal.issueToken.title": "发行代币", + "Token.modal.issueToken.form.label.name": "代币名称", + "Token.modal.issueToken.form.required.name": "必须输入代币名称", + "Token.modal.issueToken.form.required.initialCount": "必须输入初始化代币个数", + "Token.modal.issueToken.form.required.maxFee": "必须输入最大手续费", + "Token.modal.issueToken.form.required.address": "必须输入发行地址", + "Token.modal.issueToken.form.label.maxFee": "最大手续费", + "Token.modal.issueToken.form.label.address": "发行地址", + "Token.modal.issueToken.form.label.initialCount": "初始化代币个数" +} diff --git a/user-dashboard/js/dashboard/src/models/chain.js b/user-dashboard/js/dashboard/src/models/chain.js new file mode 100644 index 000000000..0f7e5939a --- /dev/null +++ b/user-dashboard/js/dashboard/src/models/chain.js @@ -0,0 +1,197 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import {applyChain, listChain, releaseChain, editChain, listDBChain} from '../services/chain' +import {message} from 'antd' +import { routerRedux } from 'dva/router' +import localStorage from 'localStorage' +import { FormattedMessage, IntlProvider, defineMessages } from 'react-intl'; +const appLocale = window.appLocale; +const intlProvider = new IntlProvider({ locale: appLocale.locale, messages: appLocale.messages }, {}); +const { intl } = intlProvider.getChildContext(); + +const messages = defineMessages({ + success: { + applyChain: { + id: "Chain.message.success.applyChain", + defaultMessage: "申请链路 {name} 成功." + }, + releaseChain: { + id: "Chain.message.success.releaseChain", + defaultMessage: "释放链路 {name} 成功." + }, + editChain: { + id: "Chain.message.success.editChain", + defaultMessage: "编辑链路成功." + } + }, + fail: { + applyChain: { + id: "Chain.message.fail.applyChain", + defaultMessage: "申请链路 {name} 失败." + }, + releaseChain: { + id: "Chain.message.fail.releaseChain", + defaultMessage: "释放链路 {name} 失败." + }, + editChain: { + id: "Chain.message.fail.editChain", + defaultMessage: "编辑链路失败." + } + } +}) + +export default { + namespace: 'chain', + + state: { + chains: [], + dbChains: [], + chainLimit: 1, + loadingDBChains: false, + currentChainId: "", + currentChain: null, + modalVisible: false, + applying: false, + releasing: false, + editing: false, + editModalVisible: false, + }, + + effects: { + * queryChains ({payload}, {call, put}) { + const data = yield call(listChain) + if (data && data.success) { + yield put({type: 'setChains', payload: {chains: data.chains, chainLimit: data.limit}}) + } else { + message.error("Query chains failed!") + yield put({type: 'setChains', payload: {chains: []}}) + } + }, + * listDBChain ({payload}, {call, put}) { + yield put({type: 'showLoadingDBChains'}) + const data = yield call(listDBChain) + if (data && data.success) { + yield put({type: 'setDBChains', payload: {dbChains: data.chains}}) + } + }, + * applyChain({payload}, {call, put}) { + yield put({type: 'setApplying', payload: {applying: true}}) + const data = yield call(applyChain, payload) + if (data && data.success) { + yield put({type: 'setApplying', payload: {applying: false}}) + yield put({type: 'hideModal'}) + message.success(intl.formatMessage(messages.success.applyChain, {name: payload.name})) + localStorage.setItem(`${window.apikey}-chainId`, data.dbId) + yield put(routerRedux.push({ + pathname: '/chain' + })) + } else { + yield put({type: 'setApplying', payload: {applying: false}}) + message.error(intl.formatMessage(messages.fail.applyChain, {name: payload.name})) + } + }, + *releaseChain({payload}, {call, put}) { + yield put({type: 'setReleasing', payload: {releasing: true}}) + const data = yield call(releaseChain, payload) + if (data && data.success) { + localStorage.removeItem(`${window.apikey}-chainId`) + message.success(intl.formatMessage(messages.success.releaseChain, {name: payload.name})) + yield put({ + type: 'queryChains' + }) + } else { + message.error(intl.formatMessage(messages.fail.releaseChain, {name: payload.name})) + } + yield put({type: 'setReleasing', payload: {releasing: false}}) + }, + *editChain({payload}, {call, put}) { + yield put({type: 'setEditing', payload: {editing: true}}) + const data = yield call(editChain, payload) + if (data && data.success) { + yield put({type: 'updateCurrentChainName', payload: {name: payload.name}}) + yield put({type: 'hideEditModal'}) + message.success(intl.formatMessage(messages.success.editChain)) + } else { + message.error(intl.formatMessage(messages.fail.editChain)) + } + yield put({type: 'setEditing', payload: {editing: false}}) + }, + }, + + reducers: { + showEditModal (state) { + return {...state, editModalVisible: true} + }, + hideEditModal (state) { + return {...state, editModalVisible: false} + }, + changeChainType (state, {payload: {chainType}}) { + return {...state, chainType, selectedConfig: null, selectedConfigId: 0} + }, + setSelectedConfig (state, {payload: {selectedConfig, selectedConfigId}}) { + return {...state, selectedConfig, selectedConfigId} + }, + setChains (state, {payload: {chains, chainLimit}}) { + const currentChainId = localStorage.getItem(`${window.apikey}-chainId`) + let currentChain = null; + if (!currentChainId && chains.length) { + localStorage.setItem(`${window.apikey}-chainId`, chains[0].dbId) + currentChain = chains[0] + } else { + chains.forEach(function (chain, index, _ary) { + if (currentChainId === chain.dbId) { + currentChain = chain + return false + } else { + return true + } + }) + } + return {...state, chains, currentChain, loadingChains: false, chainLimit, currentChainId} + }, + setApplying (state, {payload: {applying}}) { + return {...state, applying} + }, + setEditing (state, {payload: {editing}}) { + return {...state, editing} + }, + setReleasing (state, {payload: {releasing}}) { + let {currentChain} = state; + if (currentChain) { + currentChain.releasing = releasing + currentChain.status = "releasing" + } + return {...state, currentChain} + }, + updateCurrentChainName (state, {payload: {name}}) { + let {currentChain} = state; + currentChain.name = name + return {...state, currentChain} + }, + showLoadingDBChains(state) { + return {...state, loadingDBChains: true} + }, + setDBChains(state, {payload: {dbChains}}) { + return {...state, dbChains, loadingDBChains: false} + }, + changeChainId(state, {payload: {currentChainId}}) { + let {chains, currentChain} = state; + localStorage.setItem(`${window.apikey}-chainId`, currentChainId) + chains.forEach(function (chain, index, _ary) { + if (currentChainId === chain.dbId) { + currentChain = chain + return false + } else { + return true + } + }) + return {...state, currentChainId, currentChain} + } + }, + + subscriptions: { + setup({ history }) { + }, + }, +}; diff --git a/user-dashboard/js/dashboard/src/models/chainCode.js b/user-dashboard/js/dashboard/src/models/chainCode.js new file mode 100644 index 000000000..32be8c8f3 --- /dev/null +++ b/user-dashboard/js/dashboard/src/models/chainCode.js @@ -0,0 +1,247 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import {listChainCodes, deleteChainCode, instantiateChainCode, callChainCode, + editChainCode, installChainCode} from '../services/chainCode' +import {message} from 'antd' +import { routerRedux } from 'dva/router' +import { FormattedMessage, IntlProvider, defineMessages } from 'react-intl'; +const appLocale = window.appLocale; +const intlProvider = new IntlProvider({ locale: appLocale.locale, messages: appLocale.messages }, {}); +const { intl } = intlProvider.getChildContext(); + +const messages = defineMessages({ + success: { + deleteChainCode: { + id: "ChainCode.message.success.deleteChainCode", + defaultMessage: "删除智能合约 {name} 成功." + }, + updateChainCode: { + id: "ChainCode.message.success.updateChainCode", + defaultMessage: "更新智能合约成功" + }, + installChainCode: { + id: "ChainCode.message.success.installChainCode", + defaultMessage: "安装智能合约成功" + }, + instantiateChainCode: { + id: "ChainCode.message.success.instantiateChainCode", + defaultMessage: "部署智能合约任务提交成功" + }, + instantiateChainCodeDone: { + id: "ChainCode.message.success.instantiateChainCodeDone", + defaultMessage: "部署智能合约任务完成" + } + }, + fail: { + deleteChainCode: { + id: "ChainCode.message.fail.deleteChainCode", + defaultMessage: "删除智能合约 {name} 失败." + }, + updateChainCode: { + id: "ChainCode.message.fail.updateChainCode", + defaultMessage: "更新智能合约失败" + }, + installChainCode: { + id: "ChainCode.message.fail.installChainCode", + defaultMessage: "安装智能合约失败" + }, + instantiateChainCode: { + id: "ChainCode.message.fail.instantiateChainCode", + defaultMessage: "部署智能合约失败" + }, + listChainCode: { + id: "ChainCode.message.fail.listChainCode", + defaultMessage: "列取智能合约失败" + } + } +}) + +export default { + namespace: 'chainCode', + + state: { + chainCodes: [], + loadingChainCodes: false, + calling: false, + callResult: { + success: true, + message: "" + }, + editing: false, + installing: false, + editModalVisible: false, + installModalVisible: false, + instantiateModalVisible: false, + currentChainCode: null + }, + + effects: { + * queryChainCodes ({payload}, {call, put}) { + const data = yield call(listChainCodes, payload) + if (data && data.success) { + yield put({type: 'setChainCodes', payload: {chainCodes: data.chainCodes}}) + } else { + message.error(intl.formatMessage(messages.fail.listChainCode)) + yield put({type: 'setChainCodes', payload: {chains: []}}) + } + }, + *deleteChainCode ({payload}, {call, put}) { + const data = yield call(deleteChainCode, payload) + if (data && data.success) { + yield put({ + type: 'removeChainCode', + payload: { + id: payload.id + } + }) + message.success(intl.formatMessage(messages.success.deleteChainCode, {name: payload.name})) + } + }, + *editChainCode ({payload}, {call, put}) { + const data = yield call(editChainCode, payload) + if (data && data.success) { + yield put({ + type: 'updateChainCode', + payload + }) + yield put({ + type: 'hideEditModal' + }) + message.success(intl.formatMessage(messages.success.updateChainCode)) + } else { + message.error(intl.formatMessage(messages.fail.updateChainCode)) + } + }, + *installChainCode ({payload}, {call, put}) { + const data = yield call(installChainCode, payload) + if (data && data.success) { + yield put({ + type: 'updateChainCode', + payload: { + ...payload, + status: "installed" + } + }) + yield put({ + type: 'hideInstallModal' + }) + message.success(intl.formatMessage(messages.success.installChainCode)) + } else { + message.error(intl.formatMessage(messages.fail.installChainCode)) + } + }, + *instantiateChainCode ({payload}, {call, put}) { + const data = yield call(instantiateChainCode, payload) + if (data && data.success) { + yield put({ + type: 'updateChainCode', + payload: { + ...payload, + status: "instantiating" + } + }) + yield put({ + type: 'hideInstantiateModal' + }) + message.success(intl.formatMessage(messages.success.instantiateChainCode)) + } else { + message.error(intl.formatMessage(messages.fail.instantiateChainCode)) + } + }, + *instantiateDone ({payload}, {call, put}) { + yield put({ + type: 'updateChainCode', + payload: { + id: payload.id, + chainName: payload.chainName, + status: "instantiated" + } + }) + message.success(intl.formatMessage(messages.success.instantiateChainCodeDone)) + }, + *callChainCode ({payload}, {call, put}) { + yield put({type: 'setCalling'}) + const data = yield call(callChainCode, payload) + yield put({ + type: 'setCallResult', + payload: { + callResult: data + } + }) + } + }, + + reducers: { + showLoading(state) { + return {...state, loadingChainCodes: true} + }, + setChainCodes(state, {payload: {chainCodes}}) { + return {...state, chainCodes, loadingChainCodes: false} + }, + removeChainCode(state, {payload: {id}}) { + const {chainCodes} = state; + const remainChainCodes = chainCodes.filter(item => item.id !== id); + return {...state, chainCodes: remainChainCodes} + }, + updateChainCode(state, {payload: {id, name, status, chainName}}) { + let {chainCodes} = state; + chainCodes.forEach(function (chainCode, index, _ary) { + if (chainCode.id === id) { + if (name) { + chainCode.name = name; + } + if (status) { + chainCode.status = status; + } + if (chainName) { + chainCode.chainName = chainName; + } + chainCodes[index] = chainCode + return false + } else { + return true + } + }) + return {...state, chainCodes} + }, + showEditModal(state, {payload: {currentChainCode}}) { + return {...state, currentChainCode, editModalVisible: true} + }, + hideEditModal(state) { + return {...state, editModalVisible: false, currentChainCode: null} + }, + setInstalling(state) { + return {...state, installing: true} + }, + showInstallModal(state, {payload: {currentChainCode}}) { + return {...state, currentChainCode, installModalVisible: true} + }, + hideInstallModal(state) { + return {...state, installModalVisible: false, currentChainCode: null, installing: false} + }, + showInstantiateModal(state, {payload: {currentChainCode}}) { + return {...state, currentChainCode, instantiateModalVisible: true} + }, + hideInstantiateModal(state) { + return {...state, instantiateModalVisible: false, currentChainCode: null} + }, + setCalling(state) { + return {...state, calling: true} + }, + setCallResult(state, {payload: {callResult}}) { + return {...state, callResult, calling: false} + }, + clear(state) { + return {...state, callResult: { + success: true, + message: "" + }, chainCodes: [], currentChainCode: null} + } + }, + + subscriptions: { + setup({ history }) { + }, + }, +}; diff --git a/user-dashboard/js/dashboard/src/models/fabric.js b/user-dashboard/js/dashboard/src/models/fabric.js new file mode 100644 index 000000000..59de35aaa --- /dev/null +++ b/user-dashboard/js/dashboard/src/models/fabric.js @@ -0,0 +1,44 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import {queryChannelHeight} from '../services/fabric' +import {message} from 'antd' +import { routerRedux } from 'dva/router' + +export default { + namespace: 'fabric', + + state: { + channelHeight: 0, + queryingChannelHeight: false + }, + + effects: { + * queryChannelHeight ({payload}, {select, call, put}) { + yield put({type: 'setQueryingHeight'}) + const data = yield call(queryChannelHeight, payload) + if (data && data.success) { + yield put({ + type: 'setHeight', + payload: { + channelHeight: parseInt(data.height) + } + }) + } + }, + }, + + reducers: { + setQueryingHeight (state) { + return {...state, queryChannelHeight: true} + }, + setHeight (state, action) { + return {...state, ...action.payload, queryChannelHeight: false} + }, + }, + + subscriptions: { + setup({ history }) { + }, + }, +}; diff --git a/user-dashboard/js/dashboard/src/models/global.js b/user-dashboard/js/dashboard/src/models/global.js new file mode 100644 index 000000000..8ea0434be --- /dev/null +++ b/user-dashboard/js/dashboard/src/models/global.js @@ -0,0 +1,87 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import { queryNotices } from '../services/api'; +import { routerRedux } from 'dva/router' +import io from 'socket.io-client'; +import {message} from 'antd' +import { FormattedMessage, IntlProvider, defineMessages } from 'react-intl'; +const appLocale = window.appLocale; +const intlProvider = new IntlProvider({ locale: appLocale.locale, messages: appLocale.messages }, {}); +const { intl } = intlProvider.getChildContext(); + +const messages = defineMessages({ + success: { + newTransaction: { + id: "Global.message.success.newTransaction", + defaultMessage: "收到新的交易 {name}" + } + } +}) + +export default { + namespace: 'global', + + state: { + collapsed: false, + }, + + effects: { + }, + + reducers: { + changeLayoutCollapsed(state, { payload }) { + return { + ...state, + collapsed: payload, + }; + }, + }, + + subscriptions: { + 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); + } + }); + }, + socketIO({dispatch}) { + const socket = io(); + socket.emit('join', {id: window.apikey}) + socket.on('update chain', (data) => { + console.log(data) + setTimeout(function () { + dispatch({ + type: 'chain/queryChains' + }) + }, 1000) + }) + socket.on('instantiate done', (data) => { + dispatch({ + type: 'chainCode/instantiateDone', + payload: data + }) + }) + socket.on('new transaction', (data) => { + dispatch({ + type: 'chain/queryChains' + }) + message.success(intl.formatMessage(messages.success.newTransaction, {name: data.id})) + }) + socket.on('issue token done', (data) => { + message.success(`issue token done ${data.address}`) + dispatch({ + type: 'token/issueTokenDone' + }) + }) + socket.on('transfer token done', (data) => { + message.success(`transfer ${data.count} from ${data.from} to ${data.to} successfully`) + dispatch({ + type: 'account/reloadAsset' + }) + }) + } + }, +}; diff --git a/user-dashboard/js/dashboard/src/models/index.js b/user-dashboard/js/dashboard/src/models/index.js new file mode 100644 index 000000000..8b4d32893 --- /dev/null +++ b/user-dashboard/js/dashboard/src/models/index.js @@ -0,0 +1,14 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +// Use require.context to require reducers automatically +// Ref: https://webpack.github.io/docs/context.html +const context = require.context('./', false, /\.js$/); +const keys = context.keys().filter(item => item !== './index.js'); + +const models = []; +for (let i = 0; i < keys.length; i += 1) { + models.push(context(keys[i])); +} + +export default models; diff --git a/user-dashboard/js/dashboard/src/polyfill.js b/user-dashboard/js/dashboard/src/polyfill.js new file mode 100644 index 000000000..4feeb3bac --- /dev/null +++ b/user-dashboard/js/dashboard/src/polyfill.js @@ -0,0 +1,10 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import 'core-js/es6/map'; +import 'core-js/es6/set'; + +global.requestAnimationFrame = + global.requestAnimationFrame || function requestAnimationFrame(callback) { + setTimeout(callback, 0); + }; diff --git a/user-dashboard/js/dashboard/src/router.js b/user-dashboard/js/dashboard/src/router.js new file mode 100644 index 000000000..b7c5e27f4 --- /dev/null +++ b/user-dashboard/js/dashboard/src/router.js @@ -0,0 +1,28 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React from 'react'; +import { Router, Route, Switch, Redirect } from 'dva/router'; +import { LocaleProvider } from 'antd'; +import BasicLayout from './layouts/BasicLayout'; +import { addLocaleData, IntlProvider } from 'react-intl'; + +const appLocale = window.appLocale; +addLocaleData(appLocale.data); + +function RouterConfig({ history }) { + return ( + + + + + + + + + + + ); +} + +export default RouterConfig; diff --git a/user-dashboard/js/dashboard/src/routes/Chain/New/index.js b/user-dashboard/js/dashboard/src/routes/Chain/New/index.js new file mode 100644 index 000000000..ceaade4f4 --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/New/index.js @@ -0,0 +1,255 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { routerRedux } from 'dva/router'; +import { + Form, Input, Select, Button, Card, Icon, Tooltip, + Row, Col +} from 'antd'; +import { injectIntl, intlShape, FormattedMessage} from 'react-intl'; +import PageHeaderLayout from '../../../layouts/PageHeaderLayout'; +import {isAscii, isByteLength} from 'validator'; +import classNames from 'classnames/bind'; +import styles from './style.less'; +import messages from './messages' + +let cx = classNames.bind(styles); + +const FormItem = Form.Item; +const { Option } = Select; + +@connect(state => ({ + chain: state.chain, +})) +@Form.create() +class NewChain extends PureComponent { + constructor (props) { + super(props) + this.state = { + selectedConfigId: 0, + selectedConfig: null, + chainType: '', + configs: [ + { + id: 1, + type: "fabric", + configName: "Fabric", + configType: 'basic', + config: { + size: 1, + org: 1, + peer: 1 + } + }, + { + id: 2, + type: "fabric", + configName: "Fabric", + configType: 'advance', + config: { + size: 4, + org: 2, + peer: 4 + } + }, + { + id: 3, + type: "ink", + configName: "InkChain", + configType: 'basic', + config: { + size: 1, + org: 1, + peer: 2 + } + }, + { + id: 4, + type: "ink", + configName: "InkChain", + configType: 'advance', + config: { + size: 4, + org: 2, + peer: 4 + } + } + ] + } + } + handleSubmit = (e) => { + const {selectedConfig} = this.state; + e.preventDefault(); + this.props.form.validateFieldsAndScroll((err, values) => { + if (err) { + const errorLen = Object.keys(err) + if (errorLen>1) { + return + } else { + if (!("configId" in err) || ("configId" in err && values.configId === 0)) { + return + } + } + } + this.props.dispatch({ + type: 'chain/applyChain', + payload: { + ...values, + ...selectedConfig + }, + }); + }); + } + configIdValue = (value, prevValue = []) => { + return this.state.selectedConfigId; + } + validateName = (rule, value, callback) => { + const {intl} = this.props; + + const invalidName = intl.formatMessage(messages.form.validate.name.invalidName) + const invalidLength = intl.formatMessage(messages.form.validate.name.invalidLength) + if (value) { + if (!isAscii(value)) { + callback(invalidName) + } else { + if (!isByteLength(value, {max: 20})) { + callback(invalidLength) + } + } + } + callback() + } + validateConfig = (rule, value, callback) => { + const {intl} = this.props; + if (value === 0) { + callback(intl.formatMessage(messages.form.validate.configuration.required)) + } + callback() + } + onTypeChange = (value) => { + this.setState({ + chainType: value, + selectedConfig: null, + selectedConfigId: 0 + }) + } + setSelectedConfig = (config) => { + this.setState({ + selectedConfig: config, + selectedConfigId: config.id + }) + } + clickCancel = () => { + const {dispatch} = this.props; + dispatch(routerRedux.push('/chain')) + } + render() { + const { submitting, intl } = this.props; + const {configs, chainType, selectedConfigId, selectedConfig} = this.state + const { getFieldDecorator, getFieldValue } = this.props.form; + + 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 title = intl.formatMessage(messages.title) + const config = configs.filter(item => item.type === chainType) + const configCards = config.map((item, i) => + + this.setSelectedConfig(item)} className={cx({configCard: true, selected: item.id === selectedConfigId})} bordered={true}> +

{intl.formatMessage(messages.configuration[item.configType].title, {type: item.configName})}

+

{intl.formatMessage(messages.configuration[item.configType].content, {type: item.configName})}

+
+ + ) + return ( + + +
+ + {getFieldDecorator('name', { + initialValue: '', + rules: [ + { + required: true, + message: intl.formatMessage(messages.form.validate.name.invalidName), + }, + { + validator: this.validateName + } + ], + })()} + + + {getFieldDecorator('type', { + rules: [ + { required: true, message: intl.formatMessage(messages.form.validate.chainType.required) }, + ], + })( + + )} + + + {getFieldDecorator('configId', { + validateTrigger: ['onBlur', 'onValidate'], + trigger: 'onBlur', + normalize: this.configIdValue, + rules: [ + { + required: true, + message: intl.formatMessage(messages.form.validate.configuration.required), + }, + { + validator: this.validateConfig + } + ] + })( + + {configCards} + + )} + + + + + +
+
+
+ ); + } +} + +export default injectIntl(NewChain) diff --git a/user-dashboard/js/dashboard/src/routes/Chain/New/messages.js b/user-dashboard/js/dashboard/src/routes/Chain/New/messages.js new file mode 100644 index 000000000..77bb108eb --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/New/messages.js @@ -0,0 +1,101 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import { defineMessages } from 'react-intl'; + +const messages = defineMessages({ + button: { + submit: { + id: "Button.Submit", + defaultMessage: "Submit" + }, + cancel: { + id: "Button.Cancel", + defaultMessage: "Cancel" + } + }, + title: { + id: "Path.ChainNew", + defaultMessage: "Apply New Chain" + }, + configuration: { + basic: { + title: { + id: "Chain.New.configuration.basic.title", + defaultMessage: "{type} - Advance Config" + }, + content: { + id: "Chain.New.configuration.basic.content", + defaultMessage: "Limited time free, suitable for {type} advanced developers" + } + }, + advance: { + title: { + id: "Chain.New.configuration.advance.title", + defaultMessage: "{type} - Advance Config" + }, + content: { + id: "Chain.New.configuration.advance.content", + defaultMessage: "Limited time free, suitable for {type} advanced developers" + } + } + }, + form: { + title: { + updateChain: { + id: "Chain.Edit.form.title", + defaultMessage: "Update chain name" + } + }, + label: { + name: { + id: "Chain.New.form.label.name", + defaultMessage: "Name" + }, + chainType: { + id: "Chain.New.form.label.chainType", + defaultMessage: "Chain Type" + }, + configuration: { + id: "Chain.New.form.label.configuration", + defaultMessage: "Configuration" + } + }, + validate: { + name: { + required: { + id: "Chain.New.form.validate.name.required", + defaultMessage: "Must input chain name" + }, + invalidName: { + id: "Chain.New.form.validate.name.invalidName", + defaultMessage: "Please input valid name" + }, + invalidLength: { + id: "Chain.New.form.validate.name.invalidLength", + defaultMessage: "Name max length must be less than 20" + } + }, + chainType: { + required: { + id: "Chain.New.form.validate.chainType.required", + defaultMessage: "Please select a chain type" + } + }, + configuration: { + required: { + id: "Chain.New.form.validate.configuration.required", + defaultMessage: "Please select a configuration" + } + } + }, + placeholder: { + chainType: { + id: "Chain.New.form.placeholder.chainType", + defaultMessage: "Please select a chain type" + } + } + } +}); + +export default messages diff --git a/user-dashboard/js/dashboard/src/routes/Chain/New/style.less b/user-dashboard/js/dashboard/src/routes/Chain/New/style.less new file mode 100644 index 000000000..eff12f716 --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/New/style.less @@ -0,0 +1,129 @@ +@import "~antd/lib/style/themes/default.less"; + +.card { + margin-bottom: 24px; +} + +.heading { + font-size: 14px; + line-height: 22px; + margin: 0 0 16px 0; +} + +.steps { + max-width: 750px; + margin: 16px auto; +} + +.errorIcon { + cursor: pointer; + color: @error-color; + margin-right: 24px; + i { + margin-right: 4px; + } +} + +.errorPopover { + :global { + .ant-popover-inner-content { + padding: 0; + max-height: 290px; + overflow: auto; + min-width: 256px; + } + } +} + +.errorListItem { + list-style: none; + border-bottom: 1px solid @border-color-split; + padding: 8px 16px; + cursor: pointer; + transition: all .3s; + &:hover { + background: @primary-1; + } + &:last-child { + border: 0; + } + .errorIcon { + color: @error-color; + float: left; + margin-top: 4px; + margin-right: 12px; + padding-bottom: 22px; + } + .errorField { + font-size: 12px; + color: @text-color-secondary; + margin-top: 2px; + } +} + +.editable { + td { + padding-top: 13px !important; + padding-bottom: 12.5px !important; + } +} + +// custom footer for fixed footer toolbar +.advancedForm + div { + padding-bottom: 64px; +} + +.advancedForm { + :global { + .ant-form .ant-row:last-child .ant-form-item { + margin-bottom: 24px; + } + .ant-table td { + transition: none !important; + } + } +} + +.optional { + color: @text-color-secondary; + font-style: normal; +} + +.configuration { + background-color: #f8f8f8; + border-radius: 4px; + padding: 16px; + min-height: 160px; +} + +.modalRow { + min-height: 200px; + padding: 10px 30px; +} + +.configCard { + border-radius: 4px; + margin-bottom: 10px; +} + +.configCard.selected { + border-color: #40a9ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + -webkit-box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); +} + +.configCard:hover { + border-color: #40a9ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + -webkit-box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + cursor: pointer; +} + +.cardTitle { + font-weight: 500; + margin-top: -10px; +} + +.cardContent { + line-height: 1.4; +} diff --git a/user-dashboard/js/dashboard/src/routes/Chain/components/chainView.js b/user-dashboard/js/dashboard/src/routes/Chain/components/chainView.js new file mode 100644 index 000000000..0e1ca91c0 --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/components/chainView.js @@ -0,0 +1,49 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React from 'react' +import PropTypes from 'prop-types' +import { Row, Col, Icon, Button, Modal, Badge } from 'antd' +import Overview from './overview' +import styles from './chainView.less' + +const confirm = Modal.confirm; + +class ChainView extends React.Component { + constructor (props) { + super(props) + } + showReleaseConfirm = () => { + const {chain, onRelease} = this.props; + const chainId = chain ? chain.id || "" : "" + const chainName = chain ? chain.name || "" : "" + confirm({ + title: `Do you want to release chain ${chain && chain.name || ""}?`, + content: This operation can not be resumed!, + onOk() { + onRelease({ + id: chainId, + name: chainName + }) + }, + onCancel() {}, + okText: 'Ok', + cancelText: 'Cancel' + }); + } + render() { + const {chain, currentChain, onUpdate, channelHeight} = this.props; + const overviewProps = { + chain, + channelHeight + } + const chainStatus = chain ? chain.status || "" : "" + return ( +
+ +
+ ) + } +} + +export default ChainView diff --git a/user-dashboard/js/dashboard/src/routes/Chain/components/chainView.less b/user-dashboard/js/dashboard/src/routes/Chain/components/chainView.less new file mode 100644 index 000000000..fac2be8cc --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/components/chainView.less @@ -0,0 +1,8 @@ +.ghostBtn { + color: rgba(0,0,0,.50); + border-color: rgba(0,0,0,.50); +} + +.firstUpper { + text-transform: capitalize; +} diff --git a/user-dashboard/js/dashboard/src/routes/Chain/components/editModal.js b/user-dashboard/js/dashboard/src/routes/Chain/components/editModal.js new file mode 100644 index 000000000..bb67f3f05 --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/components/editModal.js @@ -0,0 +1,86 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React, {PropTypes} from 'react' +import { Modal, Form, Row, Col, Input } from 'antd' +const FormItem = Form.Item +import {isAscii, isByteLength} from 'validator'; +import { injectIntl, intlShape, FormattedMessage} from 'react-intl'; +import messages from '../New/messages' + +const formItemLayout = { + labelCol: { + span: 6, + }, + wrapperCol: { + span: 14, + }, +} + +@Form.create() +class EditModal extends React.Component { + constructor (props) { + super(props) + } + onOk = (e) => { + const {form: {validateFields}, onOk} = this.props + e.preventDefault() + validateFields((errors, values) => { + if (errors) { + return + } + onOk(values) + }) + } + validateName = (rule, value, callback) => { + const {intl} = this.props; + if (value) { + if (!isAscii(value)) { + callback(intl.formatMessage(messages.form.validate.name.invalidName)) + } else { + if (!isByteLength(value, {max: 20})) { + callback(intl.formatMessage(messages.form.validate.name.invalidLength)) + } + } + } + callback() + } + render () { + const {onCancel, visible, loading, name, form: {getFieldDecorator}, intl} = this.props; + const title = intl.formatMessage(messages.form.title.updateChain) + const okText = intl.formatMessage(messages.button.submit) + const cancelText = intl.formatMessage(messages.button.cancel) + const modalProps = { + title, + visible, + onOk: this.onOk, + onCancel, + confirmLoading: loading, + okText, + cancelText, + style: { top: 50 } + } + return ( + +
+ + {getFieldDecorator('name', { + initialValue: name, + rules: [ + { + required: true, + message: intl.formatMessage(messages.form.validate.name.required), + }, + { + validator: this.validateName + } + ], + })()} + +
+
+ ) + } +} + +export default injectIntl(EditModal) diff --git a/user-dashboard/js/dashboard/src/routes/Chain/components/emptyView.js b/user-dashboard/js/dashboard/src/routes/Chain/components/emptyView.js new file mode 100644 index 000000000..4e3d1ce1b --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/components/emptyView.js @@ -0,0 +1,45 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React from 'react' +import { Row, Col, Icon, Button, Card } from 'antd' +import { injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; + +const messages = defineMessages({ + title: { + id: "Chain.EmptyView.Title", + defaultMessage: "You did not add any block chains" + }, + content: { + id: "Chain.EmptyView.Content", + defaultMessage: "Speed, free, stable, and so what, hurry up to apply it" + }, + applyButton: { + id: "Chain.EmptyView.ApplyButton", + defaultMessage: "Apply Now" + } +}) + +function EmptyView ({ onClickButton }) { + + return ( + + + + + + + +

+

+
+ + + + +
+
+ ) +} + +export default EmptyView diff --git a/user-dashboard/js/dashboard/src/routes/Chain/components/emptyView.less b/user-dashboard/js/dashboard/src/routes/Chain/components/emptyView.less new file mode 100644 index 000000000..efca545bf --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/components/emptyView.less @@ -0,0 +1,6 @@ +.viewCenter { + align-items: center; + justify-content: center; + -webkit-align-items: center; + -webkit-align-content: center; +} diff --git a/user-dashboard/js/dashboard/src/routes/Chain/components/index.js b/user-dashboard/js/dashboard/src/routes/Chain/components/index.js new file mode 100644 index 000000000..816fbee20 --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/components/index.js @@ -0,0 +1,9 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import EmptyView from './emptyView' +// import ChainModal from './chainModal' +import ChainView from './chainView' +import EditModal from './editModal' + +export {EmptyView, ChainView, EditModal} diff --git a/user-dashboard/js/dashboard/src/routes/Chain/components/overview.js b/user-dashboard/js/dashboard/src/routes/Chain/components/overview.js new file mode 100644 index 000000000..55e372144 --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/components/overview.js @@ -0,0 +1,71 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React from 'react' +import { Row, Col, Icon, Button, Tooltip } from 'antd' +import { + ChartCard +} from '../../../components/Charts' +import { injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl'; +import messages from './overviewMessages' + +class Overview extends React.Component { + constructor (props) { + super(props) + } + render() { + const {chain, intl, channelHeight} = this.props; + const topColResponsiveProps = { + xs: 24, + sm: 12, + md: 12, + lg: 12, + xl: 6, + style: { marginBottom: 24 }, + }; + return ( +
+ + + } + total={chain ? chain.peerNum || 0 : 0} + contentHeight={46} + /> + + + } + total={chain ? chain.blocks || 0 : 0} + contentHeight={46} + /> + + + } + total={chain ? chain.scNum || 0 : 0} + contentHeight={46} + /> + + + } + total={chain ? chain.transactionNum || 0 : 0} + contentHeight={46} + /> + + +
+ ) + } +} + +export default injectIntl(Overview) diff --git a/user-dashboard/js/dashboard/src/routes/Chain/components/overview.less b/user-dashboard/js/dashboard/src/routes/Chain/components/overview.less new file mode 100644 index 000000000..ab62385e9 --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/components/overview.less @@ -0,0 +1,178 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.iconGroup { + i { + transition: color 0.32s; + color: @text-color-secondary; + cursor: pointer; + margin-left: 16px; + &:hover { + color: @text-color; + } + } +} + +.rankingList { + margin-top: 25px; + li { + .clearfix(); + margin-top: 16px; + span { + color: @text-color; + font-size: 14px; + line-height: 22px; + } + span:first-child { + background-color: @background-color-base; + border-radius: 20px; + display: inline-block; + font-size: 12px; + font-weight: 600; + margin-right: 24px; + height: 20px; + line-height: 20px; + width: 20px; + text-align: center; + } + span.active { + //background-color: @primary-color; + background-color: #314659; + color: #fff; + } + span:last-child { + float: right; + } + } +} + +.salesExtra { + display: inline-block; + margin-right: 24px; + a { + color: @text-color; + margin-left: 24px; + &:hover { + color: @primary-color; + } + &.currentDate { + color: @primary-color; + } + } +} + +.salesCard { + .salesBar { + padding: 0 0 32px 32px; + } + .salesRank { + padding: 0 32px 32px 72px; + } + :global { + .ant-tabs-bar { + padding-left: 16px; + .ant-tabs-nav .ant-tabs-tab { + padding-top: 16px; + padding-bottom: 14px; + line-height: 24px; + } + } + .ant-tabs-extra-content { + padding-right: 24px; + line-height: 55px; + } + } +} + +.salesCard { + :global { + .ant-card-head { + position: relative; + } + } +} + +.salesCardExtra { + height: 68px; +} + +.salesTypeRadio { + position: absolute; + left: 24px; + bottom: 15px; +} + +.offlineCard { + :global { + .ant-tabs-ink-bar { + bottom: auto; + } + .ant-tabs-bar { + border-bottom: none; + } + .ant-tabs-nav-container-scrolling { + padding-left: 40px; + padding-right: 40px; + } + .ant-tabs-tab-prev-icon:before { + position: relative; + left: 6px; + } + .ant-tabs-tab-next-icon:before { + position: relative; + right: 6px; + } + } + + :global(.ant-tabs-tab-active) h4 { + color: @primary-color; + } +} + +td.alignRight, +th.alignRight { + text-align: right!important; +} + +.trendText { + margin-left: 8px; + color: @heading-color; +} + +@media screen and (max-width: @screen-lg) { + .salesExtra { + display: none; + } + + .rankingList { + li { + span:first-child { + margin-right: 8px; + } + } + } +} + +@media screen and (max-width: @screen-md) { + .rankingTitle { + margin-top: 16px; + } + + .salesCard .salesBar { + padding: 16px; + } +} + +@media screen and (max-width: @screen-sm) { + .salesExtraWrap { + display: none; + } + + .salesCard { + :global { + .ant-tabs-content { + padding-top: 30px; + } + } + } +} diff --git a/user-dashboard/js/dashboard/src/routes/Chain/components/overviewCard.js b/user-dashboard/js/dashboard/src/routes/Chain/components/overviewCard.js new file mode 100644 index 000000000..4c613c0ef --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/components/overviewCard.js @@ -0,0 +1,35 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React from 'react' +import PropTypes from 'prop-types' +import { Card, Icon, Row, Col, Avatar } from 'antd' +import classNames from 'classnames/bind'; +import styles from './overviewCard.less' +let cx = classNames.bind(styles); + +class OverviewCard extends React.Component { + constructor (props) { + super(props) + } + render() { + const {iconType, colorClass, title, number} = this.props; + const iconBackgroundClass = {} + iconBackgroundClass[colorClass] = true + return ( + + + + + + +

{title}

+

{number}

+ +
+
+ ) + } +} + +export default OverviewCard diff --git a/user-dashboard/js/dashboard/src/routes/Chain/components/overviewCard.less b/user-dashboard/js/dashboard/src/routes/Chain/components/overviewCard.less new file mode 100644 index 000000000..26ed2735c --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/components/overviewCard.less @@ -0,0 +1,42 @@ +.card { + padding: 5px; +} +.card:hover { + cursor: pointer; +} +.iconBackground { + border-radius: 50%; + padding-bottom: 100%; + height: 0; +} + +.blue { + background-color: #00a8e6; +} +.green { + background-color: #00a854; +} +.yellow { + background-color: gold; +} +.pink { + background-color: deeppink; +} + +.cardIcon { + font-size: 3em; + color: white; +} + +.cardTitle { + color: rgba(0,0,0,.50) +} + +.cardNum { + font-size: 1.8em; + font-weight: 600; +} + +.cardContent { + padding-left: 10px; +} diff --git a/user-dashboard/js/dashboard/src/routes/Chain/components/overviewMessages.js b/user-dashboard/js/dashboard/src/routes/Chain/components/overviewMessages.js new file mode 100644 index 000000000..bc10a5e01 --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/components/overviewMessages.js @@ -0,0 +1,45 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import { defineMessages } from 'react-intl'; + +const messages = defineMessages({ + title: { + peer: { + id: "Chain.Index.Overview.Title.peer", + defaultMessage: "Peer" + }, + block: { + id: "Chain.Index.Overview.Title.block", + defaultMessage: "Block" + }, + smartContract: { + id: "Chain.Index.Overview.Title.smartContract", + defaultMessage: "Smart Contract" + }, + transaction: { + id: "Chain.Index.Overview.Title.transaction", + defaultMessage: "Transaction" + }, + }, + help: { + peer: { + id: "Chain.Index.Overview.Help.peer", + defaultMessage: "Peer Number of Chain" + }, + block: { + id: "Chain.Index.Overview.Help.block", + defaultMessage: "Block Number of Chain" + }, + smartContract: { + id: "Chain.Index.Overview.Help.smartContract", + defaultMessage: "Smart Contract of Chain" + }, + transaction: { + id: "Chain.Index.Overview.Help.transaction", + defaultMessage: "Transaction of Chain" + }, + } +}); + +export default messages diff --git a/user-dashboard/js/dashboard/src/routes/Chain/index.js b/user-dashboard/js/dashboard/src/routes/Chain/index.js new file mode 100644 index 000000000..acd6cd82d --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/index.js @@ -0,0 +1,216 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { Link } from 'dva/router'; +import { Row, Col, Card, List, Avatar, Badge, Button, Modal, Select } from 'antd'; +import { routerRedux } from 'dva/router'; +import {EmptyView, ChainView, EditModal} from './components' +import { injectIntl, intlShape, FormattedMessage} from 'react-intl'; +import messages from './messages' +import localStorage from 'localStorage' + +const confirm = Modal.confirm +const Option = Select.Option; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; + +import styles from './index.less'; + +@connect(state => ({ + chain: state.chain, + fabric: state.fabric +})) +class Chain extends PureComponent { + state = { + loading: true, + editModalVisible: false, + editing: false + } + componentDidMount() { + this.props.dispatch({ + type: 'chain/queryChains' + }).then(() => this.setState({ + loading: false + })) + } + + componentWillUnmount() { + } + + showEditModal = () => { + const {dispatch} = this.props; + dispatch({ + type: 'chain/showEditModal' + }) + } + + showReleaseConfirm = () => { + const {chain: {currentChain}, intl, dispatch} = this.props; + const chainId = currentChain ? currentChain.id || "" : "" + const chainName = currentChain ? currentChain.name || "" : "" + const title = intl.formatMessage(messages.modal.confirm.release.title, {name: currentChain && currentChain.name || ""}) + confirm({ + title, + onOk() { + dispatch({ + type: 'chain/releaseChain', + payload: { + id: chainId, + name: chainName + } + }) + }, + onCancel() {}, + okText: intl.formatMessage(messages.button.confirm), + cancelText: intl.formatMessage(messages.button.cancel) + }); + } + applyNewChain = () => { + const {dispatch} = this.props; + dispatch(routerRedux.push("/chain/new")) + } + + changeChain = (chainId) => { + const {dispatch} = this.props; + dispatch({ + type: 'chain/changeChainId', + payload: { + currentChainId: chainId + } + }) + } + + render() { + const {dispatch, chain: {chains, currentChain, currentChainId, editModalVisible, editing, chainLimit}, + fabric: {channelHeight, queryingChannelHeight}, intl} = this.props; + const {loading} = this.state; + + const emptyViewProps = { + onClickButton() { + dispatch(routerRedux.push('/chain/new')); + } + } + const chainViewProps = { + chain: currentChain, + onRelease (data) { + dispatch({ + type: 'chain/releaseChain', + payload: data + }) + }, + onUpdate () { + dispatch({ + type: 'chain/editModalVisible' + }) + }, + currentChain, + channelHeight + } + const editModalProps = { + visible: editModalVisible, + name: currentChain ? currentChain.name : "", + loading: editing, + onCancel () { + dispatch({ + type: 'chain/hideEditModal' + }) + }, + onOk (data) { + dispatch({ + type: 'chain/editChain', + payload: { + id: currentChain ? currentChain.id : "", + ...data + } + }) + }, + } + + const pageHeaderContent = chains && chains.length ? ( +
+
+

+ {chains.length < chainLimit && + + } + {chains.length > 1 && + + } + {currentChain && currentChain.name || ""} +

+

+ : {currentChain && currentChain.createTime || ""}    +

+

+ + +

+
+
+ ) : ""; + + const chainStatus = currentChain ? currentChain.status || "error" : "error" + function statusBadge (status) { + switch (status) { + case "running": + return "success" + case "initializing": + return "processing" + case "releasing": + return "processing" + case "error": + default: + return "error" + } + } + const pageHeaderExtra = chains && chains.length ? ( +
+
+

+

+ +

+
+
+

+

+ {currentChain && currentChain.type || ""} +

+
+
+

+

{currentChain && currentChain.runningHours || 0}

+
+
+ ) : ""; + + return ( + + {chains && chains.length ? + : + + + + } + {editModalVisible && } + + ); + } +} + +export default injectIntl(Chain) diff --git a/user-dashboard/js/dashboard/src/routes/Chain/index.less b/user-dashboard/js/dashboard/src/routes/Chain/index.less new file mode 100644 index 000000000..6c05f0f3e --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/index.less @@ -0,0 +1,239 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.activitiesList { + padding: 0 24px 8px 24px; + .username { + color: @text-color; + } + .event { + font-weight: normal; + } +} + +.pageHeaderContent { + display: flex; + .avatar { + flex: 0 1 72px; + margin-bottom: 8px; + & > span { + border-radius: 72px; + display: block; + width: 72px; + height: 72px; + } + } + .content { + position: relative; + top: 4px; + margin-left: 24px; + flex: 1 1 auto; + color: @text-color-secondary; + line-height: 22px; + .contentTitle { + font-size: 20px; + line-height: 28px; + font-weight: 500; + color: @heading-color; + margin-bottom: 12px; + } + } +} + +.pageHeaderExtra { + .clearfix(); + float: right; + & > div { + padding: 0 32px; + position: relative; + float: left; + & > p:first-child { + color: @text-color-secondary; + font-size: @font-size-base; + line-height: 22px; + margin-bottom: 4px; + } + & > p { + color: @heading-color; + font-size: 30px; + line-height: 38px; + & > span { + color: @text-color-secondary; + font-size: 20px; + } + } + &:after { + background-color: @border-color-split; + position: absolute; + top: 8px; + right: 0; + width: 1px; + height: 40px; + content: ''; + } + } + & > div:last-child { + padding-right: 0; + &:after { + display: none; + } + } +} + +.members { + a { + display: block; + margin: 12px 0; + line-height: 24px; + height: 24px; + .textOverflow(); + .member { + font-size: @font-size-base; + color: @text-color; + line-height: 24px; + max-width: 100px; + vertical-align: top; + margin-left: 12px; + transition: all .3s; + display: inline-block; + .textOverflow(); + } + &:hover { + span { + color: @primary-color; + } + } + } +} + +.projectList { + :global { + .ant-card-meta-description { + color: @text-color-secondary; + height: 44px; + line-height: 22px; + overflow: hidden; + } + } + .cardTitle { + font-size: 0; + a { + color: @heading-color; + margin-left: 12px; + line-height: 24px; + height: 24px; + display: inline-block; + vertical-align: top; + font-size: @font-size-base; + &:hover { + color: @primary-color; + } + } + } + .projectGrid { + width: 33.33%; + } + .projectItemContent { + display: flex; + margin-top: 8px; + overflow: hidden; + font-size: 12px; + height: 20px; + line-height: 20px; + .textOverflow(); + a { + color: @text-color-secondary; + display: inline-block; + flex: 1 1 0; + .textOverflow(); + &:hover { + color: @primary-color; + } + } + .datetime { + color: @disabled-color; + flex: 0 0 auto; + float: right; + } + } +} + +.datetime { + color: @disabled-color; +} + +@media screen and (max-width: @screen-xl) and (min-width: @screen-lg) { + .activeCard { + margin-bottom: 24px; + } + .members { + margin-bottom: 0; + } + .pageHeaderExtra { + margin-left: -44px; + & > div { + padding: 0 16px; + } + } +} + +@media screen and (max-width: @screen-lg) { + .activeCard { + margin-bottom: 24px; + } + .members { + margin-bottom: 0; + } + .pageHeaderExtra { + float: none; + margin-right: 0; + & > div { + padding: 0 16px; + text-align: left; + &:after { + display: none; + } + } + } +} + +@media screen and (max-width: @screen-md) { + .pageHeaderExtra { + margin-left: -16px; + } + .projectList { + .projectGrid { + width: 50%; + } + } +} + +@media screen and (max-width: @screen-sm) { + .pageHeaderContent { + display: block; + .content { + margin-left: 0; + } + } + .pageHeaderExtra { + & > div { + float: none; + } + } +} + +@media screen and (max-width: @screen-xs) { + .projectList { + .projectGrid { + width: 100%; + } + } +} + +.firstUpper { + text-transform: capitalize; +} +.ghostBtn { + color: rgba(0,0,0,.50); + border-color: rgba(0,0,0,.50); +} diff --git a/user-dashboard/js/dashboard/src/routes/Chain/messages.js b/user-dashboard/js/dashboard/src/routes/Chain/messages.js new file mode 100644 index 000000000..c7cb4dcfc --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Chain/messages.js @@ -0,0 +1,85 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import { defineMessages } from 'react-intl'; + +const messages = defineMessages({ + pageHeader: { + content: { + title: { + createTime: { + id: "Chain.Index.PageHeader.Content.Title.createTime", + defaultMessage: "Create Time" + } + }, + button: { + changeName: { + id: "Chain.Index.PageHeader.Content.Button.changeName", + defaultMessage: "Change Name" + }, + release: { + id: "Chain.Index.PageHeader.Content.Button.release", + defaultMessage: "Release" + } + } + }, + extra: { + title: { + status: { + id: "Chain.Index.PageHeader.Extra.Title.status", + defaultMessage: "Status" + }, + type: { + id: "Chain.Index.PageHeader.Extra.Title.type", + defaultMessage: "Type" + }, + running: { + id: "Chain.Index.PageHeader.Extra.Title.running", + defaultMessage: "Running Hours" + } + }, + content: { + status: { + running: { + id: "Chain.Index.PageHeader.Extra.Content.status.running", + defaultMessage: "Running" + }, + initializing: { + id: "Chain.Index.PageHeader.Extra.Content.status.initializing", + defaultMessage: "Initializing" + }, + releasing: { + id: "Chain.Index.PageHeader.Extra.Content.status.releasing", + defaultMessage: "Releasing" + }, + error: { + id: "Chain.Index.PageHeader.Extra.Content.status.error", + defaultMessage: "Error" + } + } + } + } + }, + modal: { + confirm: { + release: { + title: { + id: "Chain.Index.Modal.Confirm.Title.release", + defaultMessage: "Do you want to release chain {name}?" + } + } + } + }, + button: { + confirm: { + id: "Button.Confirm", + defaultMessage: "Confirm" + }, + cancel: { + id: "Button.Cancel", + defaultMessage: "Cancel" + } + } +}); + +export default messages diff --git a/user-dashboard/js/dashboard/src/routes/Exception/403.js b/user-dashboard/js/dashboard/src/routes/Exception/403.js new file mode 100644 index 000000000..650bd48ab --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Exception/403.js @@ -0,0 +1,10 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React from 'react'; +import { Link } from 'dva/router'; +import Exception from '../../components/Exception'; + +export default () => ( + +); diff --git a/user-dashboard/js/dashboard/src/routes/Exception/404.js b/user-dashboard/js/dashboard/src/routes/Exception/404.js new file mode 100644 index 000000000..0a942a511 --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Exception/404.js @@ -0,0 +1,10 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React from 'react'; +import { Link } from 'dva/router'; +import Exception from '../../components/Exception'; + +export default () => ( + +); diff --git a/user-dashboard/js/dashboard/src/routes/Exception/500.js b/user-dashboard/js/dashboard/src/routes/Exception/500.js new file mode 100644 index 000000000..ed44ff64e --- /dev/null +++ b/user-dashboard/js/dashboard/src/routes/Exception/500.js @@ -0,0 +1,10 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import React from 'react'; +import { Link } from 'dva/router'; +import Exception from '../../components/Exception'; + +export default () => ( + +); diff --git a/user-dashboard/js/dashboard/src/services/api.js b/user-dashboard/js/dashboard/src/services/api.js new file mode 100644 index 000000000..bc8224363 --- /dev/null +++ b/user-dashboard/js/dashboard/src/services/api.js @@ -0,0 +1,89 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import { stringify } from 'qs'; +import request from '../utils/request'; + +export async function queryProjectNotice() { + return request('/api/project/notice'); +} + +export async function queryActivities() { + return request('/api/activities'); +} + +export async function queryRule(params) { + return request(`/api/rule?${stringify(params)}`); +} + +export async function removeRule(params) { + return request('/api/rule', { + method: 'POST', + body: { + ...params, + method: 'delete', + }, + }); +} + +export async function addRule(params) { + return request('/api/rule', { + method: 'POST', + body: { + ...params, + method: 'post', + }, + }); +} + +export async function fakeSubmitForm(params) { + return request('/api/forms', { + method: 'POST', + body: params, + }); +} + +export async function fakeChartData() { + return request('/api/fake_chart_data'); +} + +export async function queryTags() { + return request('/api/tags'); +} + +export async function queryBasicProfile() { + return request('/api/profile/basic'); +} + +export async function queryAdvancedProfile() { + return request('/api/profile/advanced'); +} + +export async function queryFakeList(params) { + return request(`/api/fake_list?${stringify(params)}`); +} + +export async function fakeAccountLogin(params) { + return request('/api/login/account', { + method: 'POST', + body: params, + }); +} + +export async function fakeMobileLogin(params) { + return request('/api/login/mobile', { + method: 'POST', + body: params, + }); +} + +export async function fakeRegister(params) { + return request('/api/register', { + method: 'POST', + body: params, + }); +} + +export async function queryNotices() { + return request('/api/notices'); +} diff --git a/user-dashboard/js/dashboard/src/services/chain.js b/user-dashboard/js/dashboard/src/services/chain.js new file mode 100644 index 000000000..f13d9e517 --- /dev/null +++ b/user-dashboard/js/dashboard/src/services/chain.js @@ -0,0 +1,38 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import request from '../utils/request' +import config from '../utils/config' +import { stringify } from 'qs'; + +const { api } = config +const { chain } = api + +export async function applyChain(params) { + return request(chain.apply.format({apikey: window.apikey}), { + method: "POST", + body: params + }) +} + +export async function listChain(params) { + return request(`${chain.list.format({apikey: window.apikey})}?${stringify(params)}`) +} + +export async function listDBChain(params) { + return request(`${chain.dbList.format({apikey: window.apikey})}?${stringify(params)}`) +} + +export async function releaseChain(params) { + return request(chain.release.format({apikey: window.apikey, id: params.id}), { + method: "POST", + body: params + }) +} + +export async function editChain(params) { + return request(chain.edit.format({apikey: window.apikey, id: params.id}), { + method: "POST", + body: params + }) +} diff --git a/user-dashboard/js/dashboard/src/services/chainCode.js b/user-dashboard/js/dashboard/src/services/chainCode.js new file mode 100644 index 000000000..b2858fa22 --- /dev/null +++ b/user-dashboard/js/dashboard/src/services/chainCode.js @@ -0,0 +1,47 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import request from '../utils/request' +import config from '../utils/config' +import { stringify } from 'qs'; + +const { api } = config +const { chainCodes } = api + +export async function listChainCodes(params) { + return request(`${chainCodes.list}?${stringify(params)}`) +} + +export async function deleteChainCode(params) { + return request(chainCodes.delete.format({chainCodeId: params.id}), { + method: "DELETE" + }) +} + +export async function editChainCode(params) { + return request(chainCodes.edit.format({chainCodeId: params.id}), { + method: "PUT", + body: params + }) +} + +export async function installChainCode(params) { + return request(chainCodes.install, { + method: "POST", + body: params + }) +} + +export async function instantiateChainCode(params) { + return request(chainCodes.instantiate, { + method: "POST", + body: params + }) +} + +export async function callChainCode (params) { + return request(chainCodes.call, { + method: "POST", + body: params + }) +} diff --git a/user-dashboard/js/dashboard/src/services/fabric.js b/user-dashboard/js/dashboard/src/services/fabric.js new file mode 100644 index 000000000..783ac165b --- /dev/null +++ b/user-dashboard/js/dashboard/src/services/fabric.js @@ -0,0 +1,13 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import request from '../utils/request' +import config from '../utils/config' +import { stringify } from 'qs'; + +const { api } = config +const { fabric } = api + +export async function queryChannelHeight(params) { + return request(`${fabric.channelHeight.format({id: params.id})}?${stringify(params)}`) +} diff --git a/user-dashboard/js/dashboard/src/services/user.js b/user-dashboard/js/dashboard/src/services/user.js new file mode 100644 index 000000000..3066d5cf2 --- /dev/null +++ b/user-dashboard/js/dashboard/src/services/user.js @@ -0,0 +1,12 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import request from '../utils/request'; + +export async function query() { + return request('/api/users'); +} + +export async function queryCurrent() { + return request('/api/currentUser'); +} diff --git a/user-dashboard/js/dashboard/src/theme.js b/user-dashboard/js/dashboard/src/theme.js new file mode 100644 index 000000000..376dadf03 --- /dev/null +++ b/user-dashboard/js/dashboard/src/theme.js @@ -0,0 +1,8 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +// https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less +module.exports = { + // 'primary-color': '#10e99b', + 'card-actions-background': '#f5f8fa', +}; diff --git a/user-dashboard/js/dashboard/src/utils/config.js b/user-dashboard/js/dashboard/src/utils/config.js new file mode 100644 index 000000000..e778b3e54 --- /dev/null +++ b/user-dashboard/js/dashboard/src/utils/config.js @@ -0,0 +1,50 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +const API = '/api' +const format = require('string-format') +format.extend(String.prototype) + +module.exports = { + name: 'Cello Baas', + prefix: 'celloBaas', + footerText: 'Cello Baas © 2017', + logo: '/static/images/logo.svg', + iconFontCSS: '/static/js/dist/iconfont.css', + iconFontJS: '/static/js/dist/iconfont.js', + CORS: [], + openPages: ['/login'], + apiPrefix: '/api/v1', + API, + api: { + chain: { + apply: `${API}/chain/{apikey}/apply`, + list: `${API}/chain/{apikey}/list`, + dbList: `${API}/chain/{apikey}/db-list`, + release: `${API}/chain/{apikey}/{id}/release`, + edit: `${API}/chain/{apikey}/{id}/edit` + }, + fabric: { + channelHeight: `${API}/fabric/{id}/channelHeight` + }, + chainCodes: { + list: `${API}/chain-code`, + delete: `${API}/chain-code/{chainCodeId}`, + edit: `${API}/chain-code/{chainCodeId}`, + install: `${API}/chain-code/install`, + instantiate: `${API}/chain-code/instantiate`, + call: `${API}/chain-code/call` + }, + token: { + issue: `${API}/token/issue`, + address: `${API}/token/address`, + tokens: `${API}/token/tokens` + }, + account: { + list: `${API}/account/`, + assets: `${API}/account/assets/{accountId}`, + new: `${API}/account/new`, + transferToken: `${API}/account/transferToken` + } + }, +} diff --git a/user-dashboard/js/dashboard/src/utils/request.js b/user-dashboard/js/dashboard/src/utils/request.js new file mode 100644 index 000000000..e59142d7c --- /dev/null +++ b/user-dashboard/js/dashboard/src/utils/request.js @@ -0,0 +1,65 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import fetch from 'dva/fetch'; +import { notification } from 'antd'; +const Cookies = require('js-cookie') + +function checkStatus(response) { + if (response.status >= 200 && response.status < 300) { + return response; + } + notification.error({ + message: `请求错误 ${response.status}: ${response.url}`, + description: response.statusText, + }); + const error = new Error(response.statusText); + error.response = response; + throw error; +} + +/** + * Requests a URL, returning a promise. + * + * @param {string} url The URL we want to request + * @param {object} [options] The options we want to pass to "fetch" + * @return {object} An object containing either "data" or "err" + */ +export default function request(url, options) { + const token = Cookies.get('CelloToken') + const defaultOptions = { + credentials: 'include', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + authorization: `Bearer ${token}` + }, + }; + const newOptions = { ...defaultOptions, ...options }; + if (newOptions.method === 'POST' || newOptions.method === 'PUT') { + newOptions.headers = { + Accept: 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + ...newOptions.headers, + }; + newOptions.body = JSON.stringify(newOptions.body); + } + + return fetch(url, newOptions) + .then(checkStatus) + .then(response => response.json()) + .catch((error) => { + if (error.code) { + notification.error({ + message: error.name, + description: error.message, + }); + } + if ('stack' in error && 'message' in error) { + notification.error({ + message: `请求错误: ${url}`, + description: error.message, + }); + } + return error; + }); +} diff --git a/user-dashboard/js/dashboard/src/utils/utils.js b/user-dashboard/js/dashboard/src/utils/utils.js new file mode 100644 index 000000000..11a9f204a --- /dev/null +++ b/user-dashboard/js/dashboard/src/utils/utils.js @@ -0,0 +1,110 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +import moment from 'moment'; +import cloneDeep from 'lodash/cloneDeep'; +import navData from '../common/nav'; + + +export function fixedZero(val) { + return val * 1 < 10 ? `0${val}` : val; +} + +export function getTimeDistance(type) { + const now = new Date(); + const oneDay = 1000 * 60 * 60 * 24; + + if (type === 'today') { + now.setHours(0); + now.setMinutes(0); + now.setSeconds(0); + return [moment(now), moment(now.getTime() + (oneDay - 1000))]; + } + + if (type === 'week') { + let day = now.getDay(); + now.setHours(0); + now.setMinutes(0); + now.setSeconds(0); + + if (day === 0) { + day = 6; + } else { + day -= 1; + } + + const beginTime = now.getTime() - (day * oneDay); + + return [moment(beginTime), moment(beginTime + ((7 * oneDay) - 1000))]; + } + + if (type === 'month') { + const year = now.getFullYear(); + const month = now.getMonth(); + 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)]; + } + + if (type === 'year') { + const year = now.getFullYear(); + + return [moment(`${year}-01-01 00:00:00`), moment(`${year}-12-31 23:59:59`)]; + } +} + +function getPlainNode(nodeList, parentPath = '') { + const arr = []; + nodeList.forEach((node) => { + const item = node; + item.path = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/'); + item.exact = true; + if (item.children && !item.component) { + arr.push(...getPlainNode(item.children, item.path)); + } else { + if (item.children && item.component) { + item.exact = false; + } + arr.push(item); + } + }); + return arr; +} + +export function getRouteData(path) { + if (!navData.some(item => item.layout === path) || + !(navData.filter(item => item.layout === path)[0].children)) { + return null; + } + const dataList = cloneDeep(navData.filter(item => item.layout === path)[0]); + const nodeList = getPlainNode(dataList.children); + return nodeList; +} + +export function digitUppercase(n) { + const fraction = ['角', '分']; + const digit = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']; + const unit = [ + ['元', '万', '亿'], + ['', '拾', '佰', '仟'], + ]; + let num = Math.abs(n); + let s = ''; + fraction.forEach((item, index) => { + s += (digit[Math.floor(num * 10 * (10 ** index)) % 10] + item).replace(/零./, ''); + }); + s = s || '整'; + num = Math.floor(num); + for (let i = 0; i < unit[0].length && num > 0; i += 1) { + let p = ''; + for (let j = 0; j < unit[1].length && num > 0; j += 1) { + p = digit[num % 10] + unit[1][j] + p; + num = Math.floor(num / 10); + } + s = p.replace(/(零.)*零$/, '').replace(/^$/, '零') + unit[0][i] + s; + } + + return s.replace(/(零.)*零元/, '元').replace(/(零.)+/g, '零').replace(/^整$/, '零元整'); +} diff --git a/user-dashboard/js/dashboard/src/utils/utils.less b/user-dashboard/js/dashboard/src/utils/utils.less new file mode 100644 index 000000000..1ec1efbd5 --- /dev/null +++ b/user-dashboard/js/dashboard/src/utils/utils.less @@ -0,0 +1,50 @@ +.textOverflow() { + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + white-space: nowrap; +} + +.textOverflowMulti(@line: 3, @bg: #fff) { + overflow: hidden; + position: relative; + line-height: 1.5em; + max-height: @line * 1.5em; + text-align: justify; + margin-right: -1em; + padding-right: 1em; + &:before { + background: @bg; + content: '...'; + padding: 0 1px; + position: absolute; + right: 14px; + bottom: 0; + } + &:after { + background: white; + content: ''; + margin-top: 0.2em; + position: absolute; + right: 14px; + width: 1em; + height: 1em; + } +} + +// mixins for clearfix +// ------------------------ +.clearfix() { + zoom: 1; + &:before, + &:after { + content: " "; + display: table; + } + &:after { + clear: both; + visibility: hidden; + font-size: 0; + height: 0; + } +} diff --git a/user-dashboard/js/dashboard/theme.config.js b/user-dashboard/js/dashboard/theme.config.js new file mode 100644 index 000000000..0f0dbb9e7 --- /dev/null +++ b/user-dashboard/js/dashboard/theme.config.js @@ -0,0 +1,11 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +const fs = require('fs') +const path = require('path') +const lessToJs = require('less-vars-to-js') + +module.exports = () => { + const themePath = path.join(__dirname, './src/themes/default.less') + return lessToJs(fs.readFileSync(themePath, 'utf8')) +} diff --git a/user-dashboard/js/dashboard/version.js b/user-dashboard/js/dashboard/version.js new file mode 100644 index 000000000..da1e8e89e --- /dev/null +++ b/user-dashboard/js/dashboard/version.js @@ -0,0 +1,85 @@ +/* + SPDX-License-Identifier: Apache-2.0 +*/ +const fs = require('fs') +const path = require('path') +const beautify = require('js-beautify').js_beautify +const config = require('./package.json') + +const dist = path.join(`${__dirname}/dist`) +const maxVersion = 5 + +const writeVersion = () => new Promise((resolve, reject) => { + const { version } = config + const numbers = version.split('.') + numbers[2] = Number(numbers[2]) + 1 + config.version = numbers.join('.') + + fs.writeFile(path.join(__dirname, 'package.json'), beautify(JSON.stringify(config), { indent_size: 2 }), (err) => { + if (err) { + reject() + } + resolve() + console.log(`version: ${config.version}`) + }) +}) + +const removeFolder = (folderPath) => { + let files = [] + if (fs.existsSync(folderPath)) { + files = fs.readdirSync(folderPath) + files.forEach((file) => { + const curPath = `${folderPath}/${file}` + if (fs.statSync(curPath).isDirectory()) { + removeFolder(curPath) + } else { + fs.unlinkSync(curPath) + } + }) + fs.rmdirSync(folderPath) + } +} + +const start = async () => { + const files = fs.readdirSync(dist) + const promises = files.map(file => new Promise((resolve, reject) => { + fs.stat(`${dist}/${file}`, (err, stats) => { + if (err) { + reject() + } else { + resolve(!stats.isFile() + ? file + : null) + } + }) + })) + + const result = await Promise.all(promises) + + const folders = result.filter((item) => { + if (item) { + return item.split('.').length === 3 + } + return false + }).sort((a, b) => { + const an = a.split('.').map(_ => Number(_)) + const bn = b.split('.').map(_ => Number(_)) + if (an[0] === bn[0]) { + if (an[1] === bn[1]) { + return an[2] < bn[2] + } + return an[1] < bn[1] + } + return an[0] < bn[0] + }).filter((item, index) => { + return index > (maxVersion - 1) + }) + + for (const item of folders) { + await removeFolder(`${dist}/${item}`) + } + + await writeVersion() +} + +start()