Use react in admin dashboard

This commit is contained in:
jysperm
2015-08-23 20:06:57 +08:00
parent 333dfe223d
commit 91ed883209
19 changed files with 351 additions and 113 deletions

View File

@@ -1,7 +1,7 @@
_ = require 'lodash'
Q = require 'q'
{Component} = root
{Account, Component} = root
###
Class: Billing Plan, Managed by {BillingManager}.
@@ -63,6 +63,9 @@ class BillingPlan
@state(account).removePlan().then =>
@manager.destroyOverflowedComponents account
membersInPlan: ->
Account.find _.zipObject ["plans.#{@name}"], [$exists: true]
setupDefaultComponents: (account) ->
Q.all _.values(@components).map ({type, defaults}) ->
if defaults

View File

@@ -11,12 +11,6 @@ $ ->
, ->
location.reload()
$('.action-details').click ->
request "/admin/account_details?account_id=#{$(@).parents('tr').data 'id'}", {}, {method: 'get'}, (account) ->
$('.account-details-modal .label-account-id').text account._id
$('.account-details-modal .label-details').html JSON.stringify account, null, ' '
$('.account-details-modal').modal 'show'
$('.confirm-payment-modal .action-confirm-payment').click ->
request '/admin/confirm_payment',
account_id: $('.input-account-id').text()

View File

@@ -8,10 +8,6 @@
}
}
.tab-pane .row > header {
margin-left: 15px;
}
pre {
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
}

View File

@@ -11,6 +11,8 @@ fs = require 'q-io/fs'
_ = require 'lodash'
Q = require 'q'
require('node-jsx').install harmony: true
###
Class: Root object for control RootPanel, An instance is always available as the `root` global.
###

View File

@@ -1,4 +1,5 @@
{Router} = require 'express'
React = require 'react'
_ = require 'lodash'
Q = require 'q'
@@ -25,11 +26,42 @@ router.get '/dashboard', (req, res, next) ->
limit: 10
closed:
limit: 10
]).done ([accounts, components, tickets]) ->
res.render 'admin',
Q.all root.billing.all().map (plan) ->
plan.membersInPlan().then (accounts) ->
return {
name: plan.name
count: accounts.length
}
.then (result) ->
return _.mapValues _.indexBy(result, 'name'), 'count'
]).done ([accounts, components, tickets, accountsInPlan]) ->
props =
accounts: accounts
components: components
tickets: tickets
package: root.package
plans: root.billing.all().map (plan) ->
plan = _.extend {}, plan,
users: accountsInPlan[plan.name]
return _.pick plan, 'components', 'join_freely', 'billing', 'name', 'users'
plugins: root.plugins.all().map (plugin) ->
return _.extend {}, _.pick(plugin, 'dependencies', 'name'),
registered: do ->
{routers, hooks, views, widgets, components, couponTypes, paymentProviders} = root.plugins.getRegisteredExtends plugin
return {
routers: _.pluck routers, 'path'
hooks: _.pluck hooks, 'path'
views: _.pluck views, 'view'
widgets: _.pluck widgets, 'view'
components: _.pluck components, 'name'
couponTypes: _.pluck couponTypes, 'name'
paymentProviders: _.pluck paymentProviders, 'name'
}
res.render 'admin/layout',
mainBlock: React.renderToString React.createElement require('../view/admin/dashboard.jsx'), props
initializeProps: props
, next
###
@@ -46,7 +78,7 @@ router.use '/users', do (router = new Router) ->
router.param 'id', (req, res, next, user_id) ->
Account.findById(user_id).then (user) ->
if req.user = user
next
next()
else
next new Error 'user not found'
.catch next
@@ -66,19 +98,29 @@ router.use '/users', do (router = new Router) ->
res.json req.user.pick 'admin'
###
Router: GET /admin/users/:id/plans/join
Router: POST /admin/users/:id/plans/join
Request {Object}
* `plan` {String}
###
router.post '/:id/plans/join', (req, res, next) ->
req.plan.addMember(req.account).done ->
res.sendStatus 204
root.billing.byName(req.body.plan).addMember(req.user).done ->
res.json req.user
, next
###
Router: GET /admin/users/:id/plans/leave
Router: POST /admin/users/:id/plans/leave
Request {Object}
* `plan` {String}
###
router.post '/:id/plans/leave', (req, res, next) ->
req.plan.removeMember(req.account).done ->
res.sendStatus 204
root.billing.byName(req.body.plan).removeMember(req.user).done ->
res.json req.user
, next
###

View File

@@ -1,36 +0,0 @@
table.table.table-hover
thead
tr
th= t('account.username')
th= t('account.email')
th= t('plan.')
th= t('common.amount')
th= t('common.actions')
tbody
for account in accounts
tr(data-id='#{account._id}')
td= account.username
td= account.email
td= _.keys(account.plans).join(', ')
td= account.balance.toFixed(2)
td
button.btn.btn-info.btn-sm.action-details(type='button')= t('common.details')
.btn-group
button(type='button', data-toggle='dropdown').btn.btn-warning.btn-sm.dropdown-toggle
| 计划  
span.caret
ul.dropdown-menu
li
a(href='#') 加入套餐 A
li
a(href='#') 离开套餐 B
.btn-group
button(type='button', data-toggle='dropdown').btn.btn-primary.btn-sm.dropdown-toggle
| #{t('common.actions')}  
span.caret
ul.dropdown-menu
li
a.action-confirm-payment(href='#')= t('view.admin.confirm_payment')
if account.balance <= 0 && !_.isEmpty(account.plans)
li
a.action-delete-account(href='#')= t('view.admin.delete_account')

View File

@@ -0,0 +1,119 @@
var React = require('react');
var {Table, Button, DropdownButton, MenuItem, Modal} = require('react-bootstrap');
var _ = require('lodash');
var agent = require('../scripts/agent.coffee');
var Cookies = require('js-cookie');
var $ = require('jquery');
module.exports = AdminAccounts = React.createClass({
getInitialState: function() {
return {
accounts: this.props.accounts
};
},
componentDidMount: function() {
$.ajaxSetup({
headers: {'X-Token': Cookies.get('token')}
});
},
showAccountDetails: function(account_id) {
this.setState({
detailsModal: account_id
});
},
closeAccountDetails: function() {
this.setState({
detailsModal: null
});
},
joinPlan: function(account_id, plan_name) {
agent.post(`/admin/users/${account_id}/plans/join`, {
plan: plan_name
}).then( account => {
this.updateAccount(account);
});
},
leavePlan: function(account_id, plan_name) {
agent.post(`/admin/users/${account_id}/plans/leave`, {
plan: plan_name
}).then( account => {
this.updateAccount(account);
});
},
updateAccount: function(account) {
this.setState({
accounts: this.state.accounts.map(function(originalAccount) {
if (originalAccount._id == account._id) {
return account;
} else {
return originalAccount;
}
})
});
},
render: function() {
return (
<Table hover>
<thead>
<tr>
<th>用户名</th>
<th>邮箱</th>
<th>付费计划</th>
<th>余额</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{this.state.accounts.map( account => {
return (
<tr key={account._id}>
<td>{account.username}</td>
<td>{account.email}</td>
<td>{_.keys(account.plans).join()}</td>
<td>{account.balance.toFixed(2)}</td>
<td>
<Button bsStyle='info' bsSize='small' onClick={this.showAccountDetails.bind(this, account._id)}>
详情
</Button>
{this.state.detailsModal == account._id && (
<Modal show={true} onHide={this.closeAccountDetails} bsSize='large'>
<Modal.Header closeButton>
<Modal.Title>{account._id}</Modal.Title>
</Modal.Header>
<Modal.Body>
<pre>{JSON.stringify(account, null, ' ')}</pre>
</Modal.Body>
<Modal.Footer>
<Button onClick={this.closeAccountDetails}>关闭</Button>
</Modal.Footer>
</Modal>
)}
<DropdownButton title='付费计划' bsStyle='warning' bsSize='small'>
{this.props.plans.map( plan => {
if (account.plans[plan.name]) {
return <MenuItem key={plan.name} className='bg-danger' eventKey={plan.name} onSelect={this.leavePlan.bind(this, account._id)}>离开计划 {plan.name}</MenuItem>;
} else {
return <MenuItem key={plan.name} className='bg-success' eventKey={plan.name} onSelect={this.joinPlan.bind(this, account._id)}>加入计划 {plan.name}</MenuItem>;
}
})}
</DropdownButton>
<DropdownButton title='操作' bsStyle='primary' bsSize='small'>
<MenuItem>确认充值</MenuItem>
<MenuItem>删除账号</MenuItem>
</DropdownButton>
</td>
</tr>
)
})}
</tbody>
</Table>
)
}
});

View File

@@ -0,0 +1,17 @@
Backbone = require 'backbone'
React = require 'react'
AdminDashboard = require './dashboard.jsx'
getInitializeProps = ->
return JSON.parse $('#initialize-props').html()
AdminRouter = Backbone.Router.extend
routes:
'admin/dashboard': 'dashboard'
dashboard: ->
React.render React.createElement(AdminDashboard, getInitializeProps()), document.querySelector('#main-block')
new AdminRouter()
Backbone.history.loadUrl location.pathname

View File

View File

@@ -1,4 +0,0 @@
.page-header
h1
| RootPanel &nbsp;
small= root.package.version

View File

@@ -0,0 +1,26 @@
var React = require('react');
var {TabbedArea, TabPane} = require('react-bootstrap');
var AdminExtensions = require('./extensions.jsx');
var AdminAccounts = require('./accounts.jsx');
module.exports = AdminDashboard = React.createClass({
render: function() {
return (
<TabbedArea defaultActiveKey='dashboard'>
<TabPane eventKey='dashboard' tab='仪表盘'>
<h1>RootPanel <small>{this.props.package.version}</small></h1>
</TabPane>
<TabPane eventKey='extensions' tab='插件和拓展'>
<AdminExtensions {...this.props} />
</TabPane>
<TabPane eventKey='accounts' tab='用户'>
<AdminAccounts {...this.props} />
</TabPane>
<TabPane eventKey='tickets' tab='工单'></TabPane>
<TabPane eventKey='coupons' tab='优惠和兑换'></TabPane>
<TabPane eventKey='compontents' tab='元件'></TabPane>
<TabPane eventKey='logs' tab='系统日志'></TabPane>
</TabbedArea>
);
}
});

View File

@@ -1,33 +0,0 @@
.row
header 付费方案
for plan in root.billing.all()
.col-md-3
.panel.panel-success
.panel-heading
strong= plan.name
.panel-body
p join_freely: #{plan.join_freely}
p components: #{_.keys(plan.components).join()}
p billing: #{_.keys(plan.billing).join()}
p 用户数量0
.row
header 已加载的插件
for plugin in root.plugins.all()
- registered = root.plugins.getRegisteredExtends(plugin)
.col-md-6
.panel.panel-default
.panel-heading
strong= plugin.name
.panel-body
p dependencies: #{_.keys(plugin.dependencies).join()}
p routes: #{_.pluck(registered.routers, 'path').join()}
p hooks: #{_.pluck(registered.hooks, 'path').join()}
p views: #{_.pluck(registered.views, 'view').join()}
p widgets: #{_.pluck(registered.widgets, 'view').join()}
p components: #{_.pluck(registered.components, 'name').join()}
p couponTypes: #{_.pluck(registered.couponTypes, 'name').join()}
p paymentProviders: #{_.pluck(registered.paymentProviders, 'name').join()}

View File

@@ -0,0 +1,48 @@
var {Row, Col, Panel} = require('react-bootstrap');
var React = require('react');
var _ = require('lodash');
module.exports = AdminExtensions = React.createClass({
render: function() {
var {plugins, plans} = this.props;
return (
<div>
<Row>
<header>付费方案</header>
{plans.map(function(plan) {
return (
<Col md={3} key={plan.name}>
<Panel header={plan.name}>
<p>join_freely: {plan.join_freely.toString()}</p>
<p>components: {_.keys(plan.components).join()}</p>
<p>billing: {_.keys(plan.billing).join()}</p>
<p>users: {plan.users}</p>
</Panel>
</Col>
);
})}
</Row>
<Row>
<header>插件</header>
{plugins.map(function(plugin) {
return (
<Col md={6} key={plugin.name}>
<Panel header={plugin.name}>
<p>dependencies: {_.keys(plugin.dependencies).join()}</p>
<p>routes: {_.pluck(plugin.registered.routers, 'path').join()}</p>
<p>hooks: {_.pluck(plugin.registered.hooks, 'path').join()}</p>
<p>views: {_.pluck(plugin.registered.views, 'view').join()}</p>
<p>widgets: {_.pluck(plugin.registered.widgets, 'view').join()}</p>
<p>components: {_.pluck(plugin.registered.components, 'name').join()}</p>
<p>couponTypes: {_.pluck(plugin.registered.couponTypes, 'name').join()}</p>
<p>paymentProviders: {_.pluck(plugin.registered.paymentProviders, 'name').join()}</p>
</Panel>
</Col>
);
})}
</Row>
</div>
);
}
});

View File

@@ -0,0 +1,15 @@
extends ../layout
prepend header
title= helpers.title('admin.admin_panel')
block main
!= mainBlock
prepend sidebar
.row
a.btn.btn-lg.btn-success(href='/admin/ticket/')= t('ticket.ticket_list')
append footer
script(id='initialize-props', type='application/json')!= JSON.stringify(initializeProps)
script(src='/public/admin.js')

View File

@@ -3,7 +3,7 @@ html
head
meta(charset='utf-8')
block header
link(rel='stylesheet', href='/public/vendor/vendor.css')
link(rel='stylesheet', href='/public/bootstrap.css')
link(rel='stylesheet', href='/public/core.css')
body(data-username="#{account ? account.username : ''}")
@@ -50,8 +50,9 @@ html
block content
#content.container
.row
.col-md-9
#main-block.col-md-9
block main
!= mainBlock
#sidebar.col-md-3
block sidebar

View File

@@ -0,0 +1,25 @@
$ = require 'jquery'
_ = require 'lodash'
methods = ['get', 'post', 'delete', 'put', 'patch', 'head', 'options']
agent = {}
methods.forEach (method) ->
agent[method] = (url, data = {}, options = {}) ->
unless method == 'get'
data = JSON.stringify data
options.contentType = 'application/json; charset=UTF-8'
_.extend options,
url: url
data: data
type: method.toUpperCase()
$.ajax(options).fail (jqXHR) ->
if jqXHR.responseJSON?.error
alert root.t jqXHR.responseJSON.error
else
alert jqXHR.statusText
module.exports = agent

2
core/view/styles/bootstrap.less vendored Normal file
View File

@@ -0,0 +1,2 @@
@import "less/bootstrap.less";
@icon-font-path: "";

View File

@@ -12,17 +12,34 @@ uglify = require 'gulp-uglify'
minifyCss = require 'gulp-minify-css'
bowerFiles = require 'main-bower-files'
runSequence = require 'run-sequence'
browserify = require 'browserify'
reactify = require 'reactify'
source = require 'vinyl-source-stream'
coffeeify = require 'coffeeify'
gulp.task 'clean', ->
del 'public/*'
gulp.task 'vendor:styles', ->
gulp.src bowerFiles()
.pipe filter '*.less'
.pipe less()
.pipe concat 'vendor.css'
gulp.task 'vendor:bootstrap:styles', ->
gulp.src 'core/view/styles/bootstrap.less'
.pipe less
paths: ['bower_components/bootstrap']
.pipe minifyCss()
.pipe gulp.dest 'public/vendor'
.pipe gulp.dest 'public'
gulp.task 'scripts:admin', ->
browserify 'core/view/admin/admin.coffee'
.transform coffeeify
.transform reactify, es6: true
.bundle()
.pipe source 'admin.js'
.pipe gulp.dest 'public'
gulp.task 'styles:admin', ->
gulp.src 'core/view/admin/admin.less'
.pipe less()
.pipe minifyCss()
.pipe gulp.dest 'public'
gulp.task 'vendor:scripts', ->
gulp.src bowerFiles()
@@ -35,16 +52,9 @@ gulp.task 'vendor:scripts', ->
gulp.task 'vendor:fonts', ->
gulp.src bowerFiles()
.pipe filter ['*.eot', '*.svg', '*.ttf', '*.woff', '*.woff2']
.pipe gulp.dest 'public/fonts'
.pipe gulp.dest 'public'
gulp.task 'build:vendor', ->
runSequence [
'clean'
], [
'vendor:styles'
'vendor:scripts'
'vendor:fonts'
]
gulp.task 'build:vendor', ['vendor:bootstrap:styles', 'vendor:scripts', 'vendor:fonts']
gulp.task 'build:styles', ->
gulp.src 'core/public/style/*.less'
@@ -64,8 +74,9 @@ gulp.task 'build:scripts', ->
gulp.task 'watch', ->
gulp.watch 'core/public/style/*.less', ['build:styles']
gulp.watch 'core/public/script/*.coffee', ['build:scripts']
gulp.watch ['core/view/admin/*.jsx', 'core/view/admin/*.coffee'], ['scripts:admin']
gulp.task 'build', ['build:vendor', 'build:styles', 'build:scripts']
gulp.task 'build', ['build:vendor', 'build:styles', 'build:scripts', 'scripts:admin', 'styles:admin']
gulp.task 'build:docs', shell.task 'node_modules/.bin/endokken --extension html --theme bullet --dest ./docs-public'

View File

@@ -59,7 +59,13 @@
"ssh2": "0.3.6",
"underscore": "1.7.0",
"validator": "3.37.0",
"ioredis": "1.3.6"
"ioredis": "1.3.6",
"react": "0.13.3",
"node-jsx": "0.13.3",
"react-bootstrap": "0.24.5",
"backbone": "1.1.2",
"jquery": "2.1.4",
"js-cookie": "2.0.3"
},
"devDependencies": {
"chai": "1.10.0",
@@ -80,6 +86,10 @@
"mocha": "2.0.1",
"mocha-reporter-cov-summary": "0.1.0",
"run-sequence": "1.1.0",
"supertest": "0.15.0"
"supertest": "0.15.0",
"browserify": "11.0.1",
"reactify": "1.1.1",
"vinyl-source-stream": "1.1.0",
"coffeeify": "1.1.0"
}
}