diff --git a/Makefile b/Makefile index c4b02483..65d7a393 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,9 @@ WHITE := $(shell tput -Txterm setaf 7) YELLOW := $(shell tput -Txterm setaf 3) RESET := $(shell tput -Txterm sgr0) -STATIC_FOLDER?=themes\/react\/static -TEMPLATE_FOLDER?=themes\/react\/templates +THEME?=basic +STATIC_FOLDER?=themes\/${THEME}\/static +TEMPLATE_FOLDER?=themes\/${THEME}\/templates .PHONY: \ all \ @@ -52,6 +53,15 @@ restart: ##@Service Restart service setup: ##@Environment Setup dependency for service environment bash scripts/setup.sh +build-js: ##@Nodejs Build js files for react + bash scripts/build_reactjs.sh + +watch-mode: ##@Nodejs Run watch mode with js files for react + bash scripts/watch_mode.sh + +npm-install: ##@Nodejs Install modules with npm package management + bash scripts/npm_install.sh + HELP_FUN = \ %help; \ while(<>) { push @{$$help{$$2 // 'options'}}, [$$1, $$3] if /^([a-zA-Z\-]+)\s*:.*\#\#(?:@([a-zA-Z\-]+))?\s(.*)$$/ }; \ diff --git a/README.md b/README.md index 1f090cf6..1c6e61fb 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,10 @@ You can also find more [scenarios](docs/scenario.md). * [Architecture Design](docs/arch.md) * [Database Model](docs/db.md) * [API](api/restserver_v2.md) +* [Develop react js](docs/reactjs.md) ## Why named Cello? Can u find anyone better at playing chains? :) ## License -The Hyperledger Project uses the [Apache License Version 2.0](LICENSE) software license. \ No newline at end of file +The Hyperledger Project uses the [Apache License Version 2.0](LICENSE) software license. diff --git a/docker-compose-build-js.yml b/docker-compose-build-js.yml new file mode 100644 index 00000000..41de6b00 --- /dev/null +++ b/docker-compose-build-js.yml @@ -0,0 +1,16 @@ +# This compose file will deploy the services, and bootup a mongo server. +# Local `/opt/cello/mongo` will be used for the db storage. +# dashbard: dashbard service of cello, listen on 8080 +# app: app service of cello, listen on 80 +# nginx: front end +# mongo: mongo db + +version: '2' +services: + # cello dashbard service + build-js: + image: node + container_name: build-js + volumes: # This should be removed in product env + - ./src/themes/react/static:/app + command: bash -c "cd /app && npm run build" diff --git a/docker-compose-npm-install.yml b/docker-compose-npm-install.yml new file mode 100644 index 00000000..e411fafa --- /dev/null +++ b/docker-compose-npm-install.yml @@ -0,0 +1,16 @@ +# This compose file will deploy the services, and bootup a mongo server. +# Local `/opt/cello/mongo` will be used for the db storage. +# dashbard: dashbard service of cello, listen on 8080 +# app: app service of cello, listen on 80 +# nginx: front end +# mongo: mongo db + +version: '2' +services: + # cello dashbard service + npm-install: + image: node + container_name: npm-install + volumes: # This should be removed in product env + - ./src/themes/react/static:/app + command: bash -c "cd /app && npm install --loglevel http" diff --git a/docker-compose-watch-mode.yml b/docker-compose-watch-mode.yml new file mode 100644 index 00000000..b11eb686 --- /dev/null +++ b/docker-compose-watch-mode.yml @@ -0,0 +1,16 @@ +# This compose file will deploy the services, and bootup a mongo server. +# Local `/opt/cello/mongo` will be used for the db storage. +# dashbard: dashbard service of cello, listen on 8080 +# app: app service of cello, listen on 80 +# nginx: front end +# mongo: mongo db + +version: '2' +services: + # cello dashbard service + watch-mode: + image: node + container_name: watch-mode + volumes: # This should be removed in product env + - ./src/themes/react/static:/app + command: bash -c "cd /app && npm run watch-mode" diff --git a/docs/reactjs.md b/docs/reactjs.md new file mode 100644 index 00000000..69ac5bb4 --- /dev/null +++ b/docs/reactjs.md @@ -0,0 +1,33 @@ +** **This is another Front-end implementation for cello dashboard, if you want to use this version, must change the theme into reactjs. + +How to start service with react theme? +-------------------------------------- + +```sh +$ THEME=react make start +``` + +If you want to develop original js code for react, you must install node modules, and rebuild js after you change the js code. + +In the initialized state, must install node modules, the command is + +```sh +$ make npm-install +``` + +If you want to add extra node modules, you need change the package.json file in src/themes/react/static directory, then rerun the command “make npm-install”. + +How to build react js? +---------------------- + +In the development phase + +```sh +$ make watch-mode +``` + +In the production environment + +```sh +$ make build-js +``` \ No newline at end of file diff --git a/scripts/build_reactjs.sh b/scripts/build_reactjs.sh new file mode 100755 index 00000000..ab88f3f7 --- /dev/null +++ b/scripts/build_reactjs.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# This script will (re)start all services. +# It should be triggered at the upper directory, and safe to repeat. + +source scripts/header.sh + +echo_b "Start build react js files..." +docker-compose -f docker-compose-build-js.yml up --no-recreate + +#echo "Restarting mongo_express" +#[[ "$(docker ps -q --filter='name=mongo_express')" != "" ]] && docker restart mongo_express \ No newline at end of file diff --git a/scripts/npm_install.sh b/scripts/npm_install.sh new file mode 100755 index 00000000..7d04f4a3 --- /dev/null +++ b/scripts/npm_install.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# This script will (re)start all services. +# It should be triggered at the upper directory, and safe to repeat. + +source scripts/header.sh + +echo_b "Start install npm packages..." +docker-compose -f docker-compose-npm-install.yml up --no-recreate + +#echo "Restarting mongo_express" +#[[ "$(docker ps -q --filter='name=mongo_express')" != "" ]] && docker restart mongo_express \ No newline at end of file diff --git a/scripts/watch_mode.sh b/scripts/watch_mode.sh new file mode 100755 index 00000000..36c9f025 --- /dev/null +++ b/scripts/watch_mode.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# This script will (re)start all services. +# It should be triggered at the upper directory, and safe to repeat. + +source scripts/header.sh + +echo_b "Run watch mode for react js files..." +docker-compose -f docker-compose-watch-mode.yml up --no-recreate + +#echo "Restarting mongo_express" +#[[ "$(docker ps -q --filter='name=mongo_express')" != "" ]] && docker restart mongo_express \ No newline at end of file diff --git a/src/themes/react/static/img/favicon.ico b/src/themes/react/static/img/favicon.ico new file mode 100644 index 00000000..c6d2e93f Binary files /dev/null and b/src/themes/react/static/img/favicon.ico differ diff --git a/src/themes/react/static/js/components/layout/bread.js b/src/themes/react/static/js/components/layout/bread.js new file mode 100644 index 00000000..1397892c --- /dev/null +++ b/src/themes/react/static/js/components/layout/bread.js @@ -0,0 +1,62 @@ +import React, { PropTypes } from 'react' +import { Breadcrumb, Icon } from 'antd' +import styles from './main.less' +import { menu } from '../../utils' + +let pathSet = [] +const getPathSet = function (menuArray, parentPath) { + parentPath = parentPath || '/' + menuArray.map(item => { + pathSet[(parentPath + item.key).replace(/\//g, '-').hyphenToHump()] = { + path: parentPath + item.key, + name: item.name, + icon: item.icon || '', + clickable: item.clickable === undefined + } + if (item.child) { + getPathSet(item.child, parentPath + item.key + '/') + } + }) +} +getPathSet(menu) + +function Bread ({ location }) { + let pathNames = [] + location.pathname.substr(1).split('/').map((item, key) => { + if (key > 0) { + pathNames.push((pathNames[key - 1] + '-' + item).hyphenToHump()) + } else { + pathNames.push(('-' + item).hyphenToHump()) + } + }) + const breads = pathNames.map((item, key) => { + if (!(item in pathSet)) { + item = 'Overview' + } + return ( + + {pathSet[item].icon + ? + : ''} + {pathSet[item].name} + + ) + }) + + return ( +
+ + + Home + + {breads} + +
+ ) +} + +Bread.propTypes = { + location: PropTypes.object +} + +export default Bread diff --git a/src/themes/react/static/js/components/layout/common.less b/src/themes/react/static/js/components/layout/common.less new file mode 100644 index 00000000..8c56a010 --- /dev/null +++ b/src/themes/react/static/js/components/layout/common.less @@ -0,0 +1,102 @@ +body { + height: 100%; + overflow-y: hidden; + background-color: #f8f8f8; +} +// scrollbar + +::-webkit-scrollbar-corner { + background-color: transparent; +} + +::-webkit-scrollbar-button { + width: 0; + height: 0; + display: none; +} + +::-webkit-scrollbar-thumb { + width: 2px; + background-color: rgba(0,0,0,.2); + border-radius: 2px; +} + +::-webkit-scrollbar { + width: 2px; + height: 2px; +} + +::-webkit-scrollbar-track { + width: 5px; +} + +::-webkit-scrollbar:hover { + background-color: transparent; +} + +:global .ant-breadcrumb { + & > span { + &:last-child { + color: #999; + font-weight: normal; + } + } +} + +:global .ant-breadcrumb-link { + .anticon + span { + margin-left: 4px; + } +} + +:global .ant-table { + .ant-table-thead > tr > th { + text-align: center; + } + + .ant-table-tbody > tr > td { + text-align: center; + } + + &.ant-table-small { + .ant-table-thead > tr > th { + background: #f7f7f7; + } + + .ant-table-body > table { + padding: 0; + } + } +} + +:global .ant-table-pagination { + float: none!important; + display: table; + margin: 16px auto !important; +} + +:global .ant-popover-inner { + border: none; + border-radius: 0; + box-shadow: 0 0 20px rgba(100, 100, 100, 0.2); +} + +:global .vertical-center-modal { + display: flex; + align-items: center; + justify-content: center; + + .ant-modal { + top: 0; + + .ant-modal-body { + max-height: 500px; + overflow-y: auto; + } + } +} + +:global .ant-form-item-control { + vertical-align: middle; +} +@import "../skin.less"; diff --git a/src/themes/react/static/js/components/layout/footer.js b/src/themes/react/static/js/components/layout/footer.js new file mode 100644 index 00000000..5b7332e3 --- /dev/null +++ b/src/themes/react/static/js/components/layout/footer.js @@ -0,0 +1,9 @@ +import React from 'react' +import styles from './main.less' +import { config } from '../../utils' + +const Footer = () =>
+ {config.footerText} +
+ +export default Footer diff --git a/src/themes/react/static/js/components/layout/header.js b/src/themes/react/static/js/components/layout/header.js new file mode 100644 index 00000000..0c2c38b1 --- /dev/null +++ b/src/themes/react/static/js/components/layout/header.js @@ -0,0 +1,38 @@ +import React from 'react' +import { Menu, Icon, Popover } from 'antd' +import styles from './main.less' +import Menus from './menu' + +const SubMenu = Menu.SubMenu + +function Header ({user, switchSider, siderFold, isNavbar, menuPopoverVisible, location, switchMenuPopover}) { + const menusProps = { + siderFold: false, + darkTheme: false, + isNavbar, + handleClickNavMenu: switchMenuPopover, + location + } + return ( +
+ {isNavbar + ? }> +
+ +
+
+ :
+ +
} + + + + + +
+ ) +} + +export default Header diff --git a/src/themes/react/static/js/components/layout/main.less b/src/themes/react/static/js/components/layout/main.less new file mode 100644 index 00000000..2d5a23a5 --- /dev/null +++ b/src/themes/react/static/js/components/layout/main.less @@ -0,0 +1,301 @@ +@import "../vars.less"; + +.layout { + position: relative; + height: 100vh; + + &.withnavbar { + .main { + margin-left: 0; + } + } + + &.fold { + .sider { + width: 42px; + + .logo { + img { + width: 28px; + margin: 6px 7px; + } + } + + :global { + .ant-menu-root { + width: 100%; + + & > .ant-menu-item { + padding: 0; + text-align: center; + + .anticon { + font-size: 14px; + margin-right: 0; + } + } + + & > .ant-menu-submenu { + & > .ant-menu-submenu-title { + padding: 0; + text-align: center; + + .anticon { + font-size: 14px; + margin-right: 0; + } + + &::after { + display: none; + } + } + } + } + } + } + + .main { + margin-left: 42px; + } + } + + .sider { + width: 224px; + background: #3e3e3e; + position: absolute; + overflow: visible; + padding-bottom: 24px; + height: 100vh; + transition: @transition-ease-out; + box-shadow: @shadow-1; + z-index: 1994; + color: #999; + + &.light { + background: #fff; + + .switchtheme { + background: #fff; + border-top: solid 1px #f8f8f8; + } + } + + .logo { + text-align: center; + height: 40px; + line-height: 40px; + cursor: pointer; + margin: 28px 0; + transition: @transition-ease-out; + overflow: hidden; + + img { + width: 40px; + margin-right: 8px; + transition: @transition-ease-out; + } + + span { + vertical-align: text-bottom; + font-size: 16px; + text-transform: uppercase; + display: inline-block; + } + + .anticon { + transition: @transition-ease-out; + } + } + + .switchtheme { + width: 100%; + position: absolute; + bottom: 0; + height: 48px; + background-color: @dark-half; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 16px 0 24px; + overflow: hidden; + z-index: 1000; + + span { + white-space: nowrap; + overflow: hidden; + } + + :global { + .anticon { + min-width: 14px; + margin-right: 8px; + font-size: 14px; + } + } + } + + :global { + .ant-menu-dark, + .ant-menu-dark .ant-menu-sub { + color: #999; + } + + .ant-menu { + transition: @transition-ease-out; + + .ant-menu-item, + .ant-menu-submenu-title { + overflow: auto; + white-space: normal; + } + } + } + } + + .header { + :global .ant-menu-horizontal { + & > .ant-menu-submenu { + float: right; + } + border: none; + } + box-shadow: @shadow-2; + position: relative; + + .siderbutton { + height: 47px; + width: 47px; + line-height: 47px; + text-align: center; + font-size: 18px; + cursor: pointer; + position: absolute; + transition: @transition-ease-in; + + &:hover { + color: @primary-color; + background-color: fade(@primary-color, 15%); + } + } + } + + .bread { + height: 64px; + line-height: 64px; + padding: 0 24px; + margin-bottom: -24px; + } + + .main { + margin-left: 224px; + overflow: auto; + height: 100vh; + transition: @transition-ease-out; + + .container { + margin: 24px; + + .content { + min-height: e("calc(100vh - 184px)"); + position: relative; + + :global .content-inner { + background: #fff; + padding: 24px; + box-shadow: @shadow-1; + min-height: e("calc(100vh - 184px)"); + } + } + } + } + + .footer { + height: 48px; + line-height: 48px; + text-align: center; + font-size: 12px; + color: #999; + background: #fff; + box-shadow: @shadow-2; + width: 100%; + } +} + +:global .ant-menu-dark { + color: #999; + background-color: #3e3e3e; + + .ant-menu-submenu-title:hover, + .ant-menu-submenu:hover { + color: @primary-color; + } + + .ant-menu-sub { + color: #999; + } + + .ant-menu-submenu-selected { + color: @primary-color; + + &:not(.ant-menu-submenu-open) { + background-color: @dark-half; + } + } + + .ant-menu-item, + .ant-menu-submenu-title { + & > a { + color: #999; + } + + &:hover > a { + color: @primary-color; + } + } + + &.ant-menu-inline { + .ant-menu-item-selected { + background-color: @dark-half; + } + } + + .ant-menu-item-selected { + color: @primary-color; + background-color: @dark-half; + + & > a { + color: @primary-color; + } + + &:hover { + background-color: @dark-half; + } + } +} + +.popovermenu { + width: 280px; + left: 12px!important; + + :global .ant-popover-inner-content { + padding: 0; + + .ant-menu-inline .ant-menu-item, + .ant-menu-vertical .ant-menu-item { + border-right: 0; + height: 48px; + line-height: 48px; + } + + .ant-menu-inline .ant-menu-item-selected, + .ant-menu-inline .ant-menu-selected { + border-right: 0; + } + } +} + +.spin { + :global .ant-spin-container { + height: 100vh; + } +} diff --git a/src/themes/react/static/js/components/layout/menu.js b/src/themes/react/static/js/components/layout/menu.js new file mode 100644 index 00000000..9344ccdb --- /dev/null +++ b/src/themes/react/static/js/components/layout/menu.js @@ -0,0 +1,43 @@ +import React from 'react' +import { Menu, Icon } from 'antd' +import { Link } from 'dva/router' +import { menu } from '../../utils' + +const topMenus = menu.map(item => item.key) +const getMenus = function (menuArray, siderFold, parentPath) { + parentPath = parentPath || '/' + return menuArray.map(item => { + if (item.child) { + return ( + {item.icon ? : ''}{siderFold && topMenus.indexOf(item.key) >= 0 ? '' : item.name}}> + {getMenus(item.child, siderFold, parentPath + item.key + '/')} + + ) + } else { + return ( + + + {item.icon ? : ''} + {siderFold && topMenus.indexOf(item.key) >= 0 ? '' : item.name} + + + ) + } + }) +} + +function Menus ({ siderFold, darkTheme, location, isNavbar, handleClickNavMenu }) { + const menuItems = getMenus(menu, siderFold) + return ( + item.key) : []} + defaultSelectedKeys={[location.pathname.split('/')[location.pathname.split('/').length - 1] || 'dashboard']}> + {menuItems} + + ) +} + +export default Menus diff --git a/src/themes/react/static/js/components/layout/sider.js b/src/themes/react/static/js/components/layout/sider.js new file mode 100644 index 00000000..083cc37b --- /dev/null +++ b/src/themes/react/static/js/components/layout/sider.js @@ -0,0 +1,27 @@ +import React from 'react' +import { Icon, Switch } from 'antd' +import styles from './main.less' +import { config } from '../../utils' +import Menus from './menu' + +function Sider ({ siderFold, darkTheme, location, changeTheme }) { + const menusProps = { + siderFold, + darkTheme, + location + } + return ( +
+
+ {siderFold ? '' : {config.logoText}} +
+ + {!siderFold ?
+ Theme + +
: ''} +
+ ) +} + +export default Sider diff --git a/src/themes/react/static/js/components/skin.less b/src/themes/react/static/js/components/skin.less new file mode 100644 index 00000000..d6db52e6 --- /dev/null +++ b/src/themes/react/static/js/components/skin.less @@ -0,0 +1,21 @@ +@import "./vars.less"; + +:global { + .ant-modal-mask { + background-color: rgba(55, 55, 55, 0.2); + } + + .ant-modal-content { + box-shadow: none; + } + + .ant-card { + &:hover { + overflow: auto; + } + } + + .ant-pagination-item { + margin-bottom: 8px; + } +} diff --git a/src/themes/react/static/js/components/vars.less b/src/themes/react/static/js/components/vars.less new file mode 100644 index 00000000..db294191 --- /dev/null +++ b/src/themes/react/static/js/components/vars.less @@ -0,0 +1,188 @@ +@import "../../node_modules/antd/lib/style/themes/default.less"; // // Prefix +// @ant-prefix : ant; +// +// // Color +// @primary-color : #2db7f5; +// @info-color : #2db7f5; +// @success-color : #87d068; +// @error-color : #f50; +// @warning-color : #fa0; +// @normal-color : #d9d9d9; +// +// // ------ Base & Require ------ +// @body-background : #fff; +// @font-family : -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif; +// @code-family : Consolas,Menlo,Courier,monospace; +// @text-color : #666; +// @heading-color : #404040; +// @font-size-base : 12px; +// @line-height-base : 1.5; +// @line-height-computed : floor((@font-size-base * @line-height-base)); +// @border-radius-base : 6px; +// @border-radius-sm : 4px; +// +// // ICONFONT +// @iconfont-css-prefix : anticon; +// @icon-url : "https://at.alicdn.com/t/font_1473840929_824008"; +// +// // LINK +// @link-color : #2db7f5; +// @link-hover-color : tint(@link-color, 20%); +// @link-active-color : shade(@link-color, 5%); +// @link-hover-decoration : none; +// +// // Disabled cursor for form controls and buttons. +// @cursor-disabled : not-allowed; +// +// // Animation +// @ease-out : cubic-bezier(0.215, 0.61, 0.355, 1); +// @ease-in : cubic-bezier(0.55, 0.055, 0.675, 0.19); +// @ease-in-out : cubic-bezier(0.645, 0.045, 0.355, 1); +// @ease-out-back : cubic-bezier(0.12, 0.4, 0.29, 1.46); +// @ease-in-back : cubic-bezier(0.71, -0.46, 0.88, 0.6); +// @ease-in-out-back : cubic-bezier(0.71, -0.46, 0.29, 1.46); +// @ease-out-circ : cubic-bezier(0.08, 0.82, 0.17, 1); +// @ease-in-circ : cubic-bezier(0.6, 0.04, 0.98, 0.34); +// @ease-in-out-circ : cubic-bezier(0.78, 0.14, 0.15, 0.86); +// @ease-out-quint : cubic-bezier(0.23, 1, 0.32, 1); +// @ease-in-quint : cubic-bezier(0.755, 0.05, 0.855, 0.06); +// @ease-in-out-quint : cubic-bezier(0.86, 0, 0.07, 1); +// +// // Border color +// @border-color-base : #d9d9d9; // base border outline a component +// @border-color-split : #e9e9e9; // split border inside a component +// +// // Outline +// @outline-blur-size : 0; +// @outline-width : 2px; +// @outline-color : @primary-color; +// +// // Background color +// @background-color-base : #f7f7f7; // basic gray background +// +// // Shadow +// @shadow-color : rgba(100, 100, 100, .2); +// @box-shadow-base : @shadow-1-down; +// @shadow-1-up : 0 -1px 6px @shadow-color; +// @shadow-1-down : 0 1px 6px @shadow-color; +// @shadow-1-left : -1px 0 6px @shadow-color; +// @shadow-1-right : 1px 0 6px @shadow-color; +// @shadow-2 : 0 1px 8px @shadow-color; +// +// // Buttons +// @btn-font-weight : 500; +// @btn-border-radius-base : @border-radius-base; +// @btn-border-radius-sm : @border-radius-sm; +// +// @btn-primary-color : #fff; +// @btn-primary-bg : @primary-color; +// @btn-group-border : shade(@primary-color, 5%); +// +// @btn-default-color : @text-color; +// @btn-default-bg : @background-color-base; +// @btn-default-border : @border-color-base; +// +// @btn-ghost-color : @text-color; +// @btn-ghost-bg : transparent; +// @btn-ghost-border : @border-color-base; +// +// @btn-disable-color : #ccc; +// @btn-disable-bg : @background-color-base; +// @btn-disable-border : @border-color-base; +// +// @btn-padding-base : 4px 15px; +// +// @btn-font-size-lg : 14px; +// @btn-padding-lg : 4px 15px 5px 15px; +// +// @btn-padding-sm : 1px 7px; +// +// @btn-circle-size : 28px; +// @btn-circle-size-lg : 32px; +// @btn-circle-size-sm : 22px; +// +// // Media queries breakpoints +// // Extra small screen / phone +// @screen-xs : 480px; +// @screen-xs-min : @screen-xs; +// @screen-xs-max : (@screen-xs-min - 1); +// +// // Small screen / tablet +// @screen-sm : 768px; +// @screen-sm-min : @screen-sm; +// @screen-sm-max : (@screen-sm-min - 1); +// +// // Medium screen / desktop +// @screen-md : 992px; +// @screen-md-min : @screen-md; +// @screen-md-max : (@screen-md-min - 1); +// +// // Large screen / wide desktop +// @screen-lg : 1200px; +// @screen-lg-min : @screen-lg; +// @screen-lg-max : (@screen-lg-min - 1); +// +// // Layout and Grid system +// @grid-columns : 24; +// @grid-gutter-width : 0; +// +// // Container sizes +// @container-sm : (720px + @grid-gutter-width); +// @container-md : (940px + @grid-gutter-width); +// @container-lg : (1140px + @grid-gutter-width); +// +// // z-index list +// @zindex-affix : 10; +// @zindex-back-top : 10; +// @zindex-modal-mask : 1000; +// @zindex-modal : 1000; +// @zindex-notification : 1010; +// @zindex-message : 1010; +// @zindex-popover : 1030; +// @zindex-picker : 1050; +// @zindex-dropdown : 1050; +// @zindex-tooltip : 1060; +// +// // Form +// // -------------------------------- +// // Legend +// @legend-color : #999; +// @legend-border-color : @border-color-base; +// // Label +// @label-required-color : #f50; +// @label-color : @text-color; +// // Input +// @input-height-base: 28px; +// @input-height-lg: 32px; +// @input-height-sm: 22px; +// +// @input-padding-horizontal : 7px; +// @input-padding-vertical-base : 4px; +// @input-padding-vertical-sm : 1px; +// @input-padding-vertical-lg : 6px; +// +// @input-placeholder-color : #ccc; +// @input-color : @text-color; +// @input-border-color : @border-color-base; +// @input-bg : #fff; +// +// @input-hover-border-color : @primary-color; +// @input-disabled-bg : @background-color-base; +// +// @form-item-margin-bottom : 24px; +/*========================*/ +/*=======Antd Admin=======*/ +/*========================*/ +//Color +@dark-half: #494949; +@purple: #d897eb; // Shadow +@shadow-1: 4px 4px 20px 0 rgba(0,0,0,0.01); +@shadow-2: 4px 4px 40px 0 rgba(0,0,0,0.05); //transition +@transition-ease-in: all 0.3s @ease-in; +@transition-ease-out: all 0.3s @ease-out; //mix + +.text-overflow() { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/themes/react/static/js/index.js b/src/themes/react/static/js/index.js new file mode 100644 index 00000000..cd42a915 --- /dev/null +++ b/src/themes/react/static/js/index.js @@ -0,0 +1,14 @@ +import dva from 'dva' + +// 1. Initialize +const app = dva() + +// 2. Model + +app.model(require('./models/app')) + +// 3. Router +app.router(require('./router')) + +// 4. Start +app.start('#root') diff --git a/src/themes/react/static/js/models/app.js b/src/themes/react/static/js/models/app.js new file mode 100644 index 00000000..b9b9d1ba --- /dev/null +++ b/src/themes/react/static/js/models/app.js @@ -0,0 +1,102 @@ +import {parse} from 'qs' + +export default { + namespace: 'app', + state: { + login: false, + loading: false, + user: { + name: 'admin' + }, + loginButtonLoading: false, + menuPopoverVisible: false, + siderFold: localStorage.getItem('antdAdminSiderFold') === 'true', + darkTheme: localStorage.getItem('antdAdminDarkTheme') !== 'false', + isNavbar: document.body.clientWidth < 769 + }, + subscriptions: { + setup ({dispatch}) { + window.onresize = function () { + dispatch({type: 'changeNavbar'}) + } + } + }, + effects: { + *switchSider ({ + payload + }, {put}) { + yield put({ + type: 'handleSwitchSider' + }) + }, + *changeTheme ({ + payload + }, {put}) { + yield put({ + type: 'handleChangeTheme' + }) + }, + *changeNavbar ({ + payload + }, {put}) { + if (document.body.clientWidth < 769) { + yield put({type: 'showNavbar'}) + } else { + yield put({type: 'hideNavbar'}) + } + }, + *switchMenuPopver ({ + payload + }, {put}) { + yield put({ + type: 'handleSwitchMenuPopver' + }) + } + }, + reducers: { + showLoading (state) { + return { + ...state, + loading: true + } + }, + hideLoading (state) { + return { + ...state, + loading: false + } + }, + handleSwitchSider (state) { + localStorage.setItem('antdAdminSiderFold', !state.siderFold) + return { + ...state, + siderFold: !state.siderFold + } + }, + handleChangeTheme (state) { + localStorage.setItem('antdAdminDarkTheme', !state.darkTheme) + return { + ...state, + darkTheme: !state.darkTheme + } + }, + showNavbar (state) { + return { + ...state, + isNavbar: true + } + }, + hideNavbar (state) { + return { + ...state, + isNavbar: false + } + }, + handleSwitchMenuPopver (state) { + return { + ...state, + menuPopoverVisible: !state.menuPopoverVisible + } + } + } +} diff --git a/src/themes/react/static/js/router.js b/src/themes/react/static/js/router.js new file mode 100644 index 00000000..7cc7f35a --- /dev/null +++ b/src/themes/react/static/js/router.js @@ -0,0 +1,78 @@ +import React from 'react' +import {Router} from 'dva/router' +import App from './routes/app' + +export default function ({history, app}) { + const routes = [ + { + path: '/', + component: App, + getIndexRoute (nextState, cb) { + require.ensure([], require => { + cb(null, {component: require('./routes/overview')}) + }) + }, + childRoutes: [ + { + path: 'overview', + name: 'overview', + getComponent (nextState, cb) { + require.ensure([], require => { + cb(null, require('./routes/overview')) + }) + } + }, { + path: 'hosts', + name: 'hosts', + getComponent (nextState, cb) { + require.ensure([], require => { + cb(null, require('./routes/hosts')) + }) + } + }, { + path: 'chains/active', + name: 'chains/active', + getComponent (nextState, cb) { + require.ensure([], require => { + cb(null, require('./routes/chains/active')) + }) + } + }, { + path: 'chains/inused', + name: 'chains/inused', + getComponent (nextState, cb) { + require.ensure([], require => { + cb(null, require('./routes/chains/inused')) + }) + } + }, { + path: 'release', + name: 'release', + getComponent (nextState, cb) { + require.ensure([], require => { + cb(null, require("./routes/release")) + }) + } + }, { + path: 'about', + name: 'about', + getComponent (nextState, cb) { + require.ensure([], require => { + cb(null, require("./routes/about")) + }) + } + }, { + path: '*', + name: 'error', + getComponent (nextState, cb) { + require.ensure([], require => { + cb(null, require('./routes/error')) + }) + } + } + ] + } + ] + + return +} diff --git a/src/themes/react/static/js/routes/about.js b/src/themes/react/static/js/routes/about.js new file mode 100644 index 00000000..fba66f05 --- /dev/null +++ b/src/themes/react/static/js/routes/about.js @@ -0,0 +1,9 @@ +import React from 'react' + +const About = () =>
+
+

About

+
+
+ +export default About diff --git a/src/themes/react/static/js/routes/app.js b/src/themes/react/static/js/routes/app.js new file mode 100644 index 00000000..7fe79500 --- /dev/null +++ b/src/themes/react/static/js/routes/app.js @@ -0,0 +1,68 @@ +import React, { PropTypes } from 'react' +import { connect } from 'dva' +import Header from '../components/layout/header' +import Bread from '../components/layout/bread' +import Footer from '../components/layout/footer' +import Sider from '../components/layout/sider' +import styles from '../components/layout/main.less' +import { Spin } from 'antd' +import { classnames } from '../utils' +import '../components/layout/common.less' + +function App ({children, location, dispatch, app}) { + const {user, siderFold, darkTheme, isNavbar, menuPopoverVisible} = app + + const headerProps = { + user, + siderFold, + location, + isNavbar, + menuPopoverVisible, + switchMenuPopover () { + dispatch({type: 'app/switchMenuPopver'}) + }, + switchSider () { + dispatch({type: 'app/switchSider'}) + } + } + + const siderProps = { + siderFold, + darkTheme, + location, + changeTheme () { + dispatch({type: 'app/changeTheme'}) + } + } + + return ( +
+
+ {!isNavbar ? : ''} +
+
+ +
+
+ {children} +
+
+
+
+
+
+ ) +} + +App.propTypes = { + children: PropTypes.element.isRequired, + location: PropTypes.object, + dispatch: PropTypes.func, + user: PropTypes.object, + siderFold: PropTypes.bool, + darkTheme: PropTypes.bool +} + +export default connect(({app}) => ({app}))(App) diff --git a/src/themes/react/static/js/routes/chains/active.js b/src/themes/react/static/js/routes/chains/active.js new file mode 100644 index 00000000..5ec9220b --- /dev/null +++ b/src/themes/react/static/js/routes/chains/active.js @@ -0,0 +1,9 @@ +import React from 'react' + +const Active = () =>
+
+

Active chains

+
+
+ +export default Active diff --git a/src/themes/react/static/js/routes/chains/inused.js b/src/themes/react/static/js/routes/chains/inused.js new file mode 100644 index 00000000..9e24bc6a --- /dev/null +++ b/src/themes/react/static/js/routes/chains/inused.js @@ -0,0 +1,9 @@ +import React from 'react' + +const Inused = () =>
+
+

Inused Chains

+
+
+ +export default Inused diff --git a/src/themes/react/static/js/routes/error.js b/src/themes/react/static/js/routes/error.js new file mode 100644 index 00000000..52854786 --- /dev/null +++ b/src/themes/react/static/js/routes/error.js @@ -0,0 +1,12 @@ +import React from 'react' +import {Icon} from 'antd' +import styles from './error.less' + +const Error = () =>
+
+ +

404 Not Found

+
+
+ +export default Error diff --git a/src/themes/react/static/js/routes/error.less b/src/themes/react/static/js/routes/error.less new file mode 100644 index 00000000..f5300b1f --- /dev/null +++ b/src/themes/react/static/js/routes/error.less @@ -0,0 +1,19 @@ +.error { + color: black; + text-align: center; + position: absolute; + top: 30%; + margin-top: -50px; + left: 50%; + margin-left: -100px; + width: 200px; + + :global .anticon { + font-size: 48px; + margin-bottom: 16px; + } + + h1 { + font-family: cursive; + } +} diff --git a/src/themes/react/static/js/routes/hosts.js b/src/themes/react/static/js/routes/hosts.js new file mode 100644 index 00000000..63b58211 --- /dev/null +++ b/src/themes/react/static/js/routes/hosts.js @@ -0,0 +1,9 @@ +import React from 'react' + +const Hosts = () =>
+
+

Hosts

+
+
+ +export default Hosts diff --git a/src/themes/react/static/js/routes/overview.js b/src/themes/react/static/js/routes/overview.js new file mode 100644 index 00000000..00a5c80f --- /dev/null +++ b/src/themes/react/static/js/routes/overview.js @@ -0,0 +1,9 @@ +import React from 'react' + +const Overview = () =>
+
+

Overview

+
+
+ +export default Overview diff --git a/src/themes/react/static/js/routes/release.js b/src/themes/react/static/js/routes/release.js new file mode 100644 index 00000000..3a98f35b --- /dev/null +++ b/src/themes/react/static/js/routes/release.js @@ -0,0 +1,9 @@ +import React from 'react' + +const Release = () =>
+
+

Release History

+
+
+ +export default Release diff --git a/src/themes/react/static/js/routes/users.js b/src/themes/react/static/js/routes/users.js new file mode 100644 index 00000000..b4be09b4 --- /dev/null +++ b/src/themes/react/static/js/routes/users.js @@ -0,0 +1,100 @@ +import React, { PropTypes } from 'react' +import { routerRedux } from 'dva/router' +import { connect } from 'dva' +import UserList from '../components/users/list' +import UserSearch from '../components/users/search' +import UserModal from '../components/users/modal' + +function Users ({ location, dispatch, users }) { + const { loading, list, pagination, currentItem, modalVisible, modalType } = users + const { field, keyword } = location.query + + const userModalProps = { + item: modalType === 'create' ? {} : currentItem, + type: modalType, + visible: modalVisible, + onOk (data) { + dispatch({ + type: `users/${modalType}`, + payload: data + }) + }, + onCancel () { + dispatch({ + type: 'users/hideModal' + }) + } + } + + const userListProps = { + dataSource: list, + loading, + pagination: pagination, + onPageChange (page) { + dispatch(routerRedux.push({ + pathname: '/users', + query: { + page: page.current, + pageSize: page.pageSize + } + })) + }, + onDeleteItem (id) { + dispatch({ + type: 'users/delete', + payload: id + }) + }, + onEditItem (item) { + dispatch({ + type: 'users/showModal', + payload: { + modalType: 'update', + currentItem: item + } + }) + } + } + + const userSearchProps = { + field, + keyword, + onSearch (fieldsValue) { + dispatch({ + type: 'users/query', + payload: fieldsValue + }) + }, + onAdd () { + dispatch({ + type: 'users/showModal', + payload: { + modalType: 'create' + } + }) + } + } + + const UserModalGen = () => + + + return ( +
+ + + +
+ ) +} + +Users.propTypes = { + users: PropTypes.object, + location: PropTypes.object, + dispatch: PropTypes.func +} + +function mapStateToProps ({ users }) { + return { users } +} + +export default connect(mapStateToProps)(Users) diff --git a/src/themes/react/static/js/theme.js b/src/themes/react/static/js/theme.js new file mode 100644 index 00000000..82ec4559 --- /dev/null +++ b/src/themes/react/static/js/theme.js @@ -0,0 +1,13 @@ +module.exports = () => { + return { + '@border-radius-base': '3px', + '@border-radius-sm': '2px', + '@shadow-color': 'rgba(0,0,0,0.05)', + '@shadow-1-down': '4px 4px 40px @shadow-color', + '@border-color-split': '#f4f4f4', + '@border-color-base': '#e5e5e5', + '@menu-dark-bg': '#3e3e3e', + '@text-color': '#666', + '@font-family': 'monospace,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", SimSun, sans-serif' + } +} diff --git a/src/themes/react/static/js/utils/config.js b/src/themes/react/static/js/utils/config.js new file mode 100644 index 00000000..4d33982a --- /dev/null +++ b/src/themes/react/static/js/utils/config.js @@ -0,0 +1,6 @@ +module.exports = { + name: 'Cello Dashboard', + prefix: 'cello', + footerText: 'Cello Dashboard', + logoText: 'Cello' +} diff --git a/src/themes/react/static/js/utils/index.js b/src/themes/react/static/js/utils/index.js new file mode 100644 index 00000000..02406440 --- /dev/null +++ b/src/themes/react/static/js/utils/index.js @@ -0,0 +1,48 @@ +import config from './config' +import menu from './menu' +import request from './request' +import classnames from 'classnames' +import {color} from './theme' + +// 连字符转驼峰 +String.prototype.hyphenToHump = function () { + return this.replace(/-(\w)/g, function () { + return arguments[1].toUpperCase() + }) +} + +// 驼峰转连字符 +String.prototype.humpToHyphen = function () { + return this.replace(/([A-Z])/g, '-$1').toLowerCase() +} + +// 日期格式化 +Date.prototype.format = function (format) { + var o = { + 'M+': this.getMonth() + 1, + 'd+': this.getDate(), + 'h+': this.getHours(), + 'H+': this.getHours(), + 'm+': this.getMinutes(), + 's+': this.getSeconds(), + 'q+': Math.floor((this.getMonth() + 3) / 3), + 'S': this.getMilliseconds() + } + if (/(y+)/.test(format)) { format = format.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length)) } + for (var k in o) { + if (new RegExp('(' + k + ')').test(format)) { + format = format.replace(RegExp.$1, RegExp.$1.length === 1 + ? o[k] + : ('00' + o[k]).substr(('' + o[k]).length)) + } + } + return format +} + +module.exports = { + config, + menu, + request, + color, + classnames +} diff --git a/src/themes/react/static/js/utils/menu.js b/src/themes/react/static/js/utils/menu.js new file mode 100644 index 00000000..c3a9d371 --- /dev/null +++ b/src/themes/react/static/js/utils/menu.js @@ -0,0 +1,38 @@ +module.exports = [ + { + key: 'overview', + name: 'Overview', + icon: 'laptop' + }, + { + key: 'hosts', + name: 'Hosts', + icon: 'user' + }, + { + key: 'chains', + name: 'Chains', + icon: 'camera-o', + clickable: false, + child: [ + { + key: 'active', + name: 'Active Chains' + }, + { + key: 'inused', + name: 'Inused Chains' + } + ] + }, + { + key: 'release', + name: 'Release History', + icon: 'user' + }, + { + key: 'about', + name: 'About', + icon: 'user' + } +] diff --git a/src/themes/react/static/js/utils/request.js b/src/themes/react/static/js/utils/request.js new file mode 100644 index 00000000..279b9f75 --- /dev/null +++ b/src/themes/react/static/js/utils/request.js @@ -0,0 +1,27 @@ +const Ajax = require('robe-ajax') + +/** + * 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) { + if (options.cross) { + return Ajax.getJSON('http://query.yahooapis.com/v1/public/yql', { + q: "select * from json where url='" + url + '?' + Ajax.param(options.data) + "'", + format: 'json' + }) + } else { + return Ajax.ajax({ + url: url, + method: options.method || 'get', + data: options.data || {}, + processData: options.method === 'get', + dataType: 'JSON' + }).done((data) => { + return data + }) + } +} diff --git a/src/themes/react/static/js/utils/theme.js b/src/themes/react/static/js/utils/theme.js new file mode 100644 index 00000000..85ed8b4f --- /dev/null +++ b/src/themes/react/static/js/utils/theme.js @@ -0,0 +1,14 @@ +module.exports = { + color: { + green: '#64ea91', + blue: '#8fc9fb', + purple: '#d897eb', + red: '#f69899', + yellow: '#f8c82e', + peach: '#f797d6', + borderBase: '#e5e5e5', + borderSplit: '#f4f4f4', + grass: '#d6fbb5', + sky: '#c1e0fc' + } +} diff --git a/src/themes/react/static/package.json b/src/themes/react/static/package.json new file mode 100644 index 00000000..9f9271a7 --- /dev/null +++ b/src/themes/react/static/package.json @@ -0,0 +1,56 @@ +{ + "private": true, + "entry": { + "index": "./js/index.js" + }, + "dependencies": { + "antd": "^2.6.0", + "classnames": "^2.2.5", + "dva": "^1.1.0", + "dva-loading": "^0.2.0", + "js-cookie": "^2.1.3", + "qs": "^6.2.0", + "react": "^15.4.1", + "react-countup": "^1.3.0", + "react-dom": "^15.4.1", + "recharts": "^0.19.0" + }, + "devDependencies": { + "atool-build": "^0.7.6", + "babel-eslint": "^6.0.4", + "babel-plugin-dev-expression": "^0.2.1", + "babel-plugin-dva-hmr": "^0.1.0", + "babel-plugin-import": "^1.0.1", + "babel-plugin-transform-runtime": "^6.9.0", + "babel-runtime": "^6.9.2", + "dora": "0.3.x", + "dora-plugin-proxy": "^0.7.0", + "dora-plugin-webpack": "0.6.x", + "dora-plugin-webpack-hmr": "^0.1.0", + "eslint": "^2.13.1", + "eslint-config-airbnb": "^9.0.1", + "eslint-plugin-import": "^1.8.1", + "eslint-plugin-jsx-a11y": "^1.4.2", + "eslint-plugin-react": "^5.1.1", + "glob": "^7.0.5", + "mockjs": "^1.0.1-beta3", + "redbox-react": "^1.2.10", + "robe-ajax": "^1.0.0", + "watchjs": "^0.0.0" + }, + "scripts": { + "start": "dora --plugins \"proxy,webpack,webpack-hmr\"", + "dev": "dora --plugins \"webpack,webpack-hmr\"", + "lint": "eslint --fix --ext .js,.jsx src/components/layout", + "build": "atool-build -o /app/js/dist --publicPath /static/js/dist/", + "watch-mode": "atool-build -w --no-compress -o /app/js/dist --publicPath /static/js/dist/" + }, + "theme": "./js/theme.js", + "standard": { + "parser": "babel-eslint", + "globals": [ + "location", + "localStorage" + ] + } +} diff --git a/src/themes/react/static/webpack.config.js b/src/themes/react/static/webpack.config.js new file mode 100644 index 00000000..5b3521c5 --- /dev/null +++ b/src/themes/react/static/webpack.config.js @@ -0,0 +1,49 @@ +const webpack = require('atool-build/lib/webpack') + +module.exports = function (webpackConfig, env) { + webpackConfig.babel.plugins.push('transform-runtime') + webpackConfig.babel.plugins.push(['import', { + libraryName: 'antd', + style: true + }]) + + // Support hmr + if (env === 'development') { + webpackConfig.devtool = '#eval' + webpackConfig.babel.plugins.push(['dva-hmr', { + entries: [ + './js/index.js' + ] + }]) + } else { + webpackConfig.babel.plugins.push('dev-expression') + } + + // Don't extract common.js and common.css + webpackConfig.plugins = webpackConfig.plugins.filter(function (plugin) { + return !(plugin instanceof webpack.optimize.CommonsChunkPlugin) + }) + + // Support CSS Modules + // Parse all less files as css module. + webpackConfig.module.loaders.forEach(function (loader, index) { + if (typeof loader.test === 'function' && loader.test.toString().indexOf('\\.less$') > -1) { + loader.include = /node_modules/ + loader.test = /\.less$/ + } + if (loader.test.toString() === '/\\.module\\.less$/') { + loader.exclude = /node_modules/ + loader.test = /\.less$/ + } + if (typeof loader.test === 'function' && loader.test.toString().indexOf('\\.css$') > -1) { + loader.include = /node_modules/ + loader.test = /\.css$/ + } + if (loader.test.toString() === '/\\.module\\.css$/') { + loader.exclude = /node_modules/ + loader.test = /\.css$/ + } + }) + + return webpackConfig +} diff --git a/src/themes/react/templates/index.html b/src/themes/react/templates/index.html new file mode 100644 index 00000000..575af242 --- /dev/null +++ b/src/themes/react/templates/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + {% block head %} + Cello Dashboard + {% endblock %} + + + + + +
+ + + +