Authored by 姜枫

添加用户管理

@@ -5,6 +5,7 @@ import ServerModel from './server'; @@ -5,6 +5,7 @@ import ServerModel from './server';
5 import BuildingModel from './building'; 5 import BuildingModel from './building';
6 import ProjectModel from './project'; 6 import ProjectModel from './project';
7 import DeployModel from './deploy'; 7 import DeployModel from './deploy';
  8 +import UserModel from './user';
8 9
9 shelljs.mkdir('-p', config.dbDir); 10 shelljs.mkdir('-p', config.dbDir);
10 11
@@ -12,10 +13,12 @@ const Server = new ServerModel(); @@ -12,10 +13,12 @@ const Server = new ServerModel();
12 const Building = new BuildingModel(); 13 const Building = new BuildingModel();
13 const Project = new ProjectModel(); 14 const Project = new ProjectModel();
14 const DeployInfo = new DeployModel(); 15 const DeployInfo = new DeployModel();
  16 +const User = new UserModel();
15 17
16 export { 18 export {
17 Server, 19 Server,
18 Building, 20 Building,
19 Project, 21 Project,
20 - DeployInfo 22 + DeployInfo,
  23 + User
21 }; 24 };
  1 +/**
  2 + *
  3 + * @author: jiangfeng<jeff.jiang@yoho.cn>
  4 + * @date: 16/8/16
  5 + */
  6 +
  7 +'use strict';
  8 +
  9 +import Model from './model';
  10 +
  11 +class User extends Model {
  12 + constructor() {
  13 + super('users');
  14 + }
  15 +
  16 + findByUsername(username) {
  17 + return this.findOne({
  18 + username: username
  19 + });
  20 + }
  21 +}
  22 +
  23 +export default User;
1 'use strict'; 1 'use strict';
2 2
3 import Router from 'koa-router'; 3 import Router from 'koa-router';
  4 +import md5 from 'md5';
  5 +import {User} from '../../models';
4 6
5 let r = new Router(); 7 let r = new Router();
6 8
@@ -12,9 +14,11 @@ const login = { @@ -12,9 +14,11 @@ const login = {
12 let username = ctx.request.body.username; 14 let username = ctx.request.body.username;
13 let password = ctx.request.body.password; 15 let password = ctx.request.body.password;
14 16
15 - if (username === 'yoho' && password === 'yoho9646') { 17 + let user = await User.findByUsername(username);
  18 +
  19 + if (user && password && user.password === md5(password)) {
16 ctx.session = { 20 ctx.session = {
17 - user: 'yoho' 21 + user: user
18 }; 22 };
19 ctx.redirect('/projects'); 23 ctx.redirect('/projects');
20 ctx.status = 301; 24 ctx.status = 301;
@@ -18,6 +18,7 @@ let r = new Router(); @@ -18,6 +18,7 @@ let r = new Router();
18 const colors = ['primary', 'success', 'info', 'warning', 'danger', 'success-alt', 'info-alt', 'warning-alt', 'danger-alt', 'primary-head', 'success-head', 'danger-head']; 18 const colors = ['primary', 'success', 'info', 'warning', 'danger', 'success-alt', 'info-alt', 'warning-alt', 'danger-alt', 'primary-head', 'success-head', 'danger-head'];
19 const envs = { 19 const envs = {
20 production: '线上环境', 20 production: '线上环境',
  21 + preview: '灰度环境',
21 test: '测试环境' 22 test: '测试环境'
22 }; 23 };
23 24
@@ -88,6 +89,7 @@ const p = { @@ -88,6 +89,7 @@ const p = {
88 } 89 }
89 } 90 }
90 }); 91 });
  92 +
91 }, 93 },
92 edit_page: async(ctx, next) => { 94 edit_page: async(ctx, next) => {
93 let id = ctx.query.id; 95 let id = ctx.query.id;
@@ -10,6 +10,7 @@ const r = new Router; @@ -10,6 +10,7 @@ const r = new Router;
10 10
11 const envs = { 11 const envs = {
12 production: '线上环境', 12 production: '线上环境',
  13 + preview: '灰度环境',
13 test: '测试环境' 14 test: '测试环境'
14 }; 15 };
15 16
  1 +/**
  2 + * 用户管理
  3 + * @author: jiangfeng<jeff.jiang@yoho.cn>
  4 + * @date: 16/8/16
  5 + */
  6 +
  7 +'use strict';
  8 +import Router from 'koa-router';
  9 +import md5 from 'md5';
  10 +import {User} from '../../models';
  11 +
  12 +const r = new Router;
  13 +
  14 +const user = {
  15 + async setting_page(ctx) {
  16 + let users = await User.findAll();
  17 +
  18 + await ctx.render('action/users', {
  19 + users: users
  20 + });
  21 + },
  22 + async new_page(ctx) {
  23 + await ctx.render('action/users_form');
  24 + },
  25 + async edit_page(ctx) {
  26 + let id = ctx.query.id;
  27 + let user = await User.findById(id);
  28 +
  29 + user.password = "******";
  30 + await ctx.render('action/users_form', user);
  31 + },
  32 + async save(ctx) {
  33 + let {_id, username, password, role, state} = ctx.request.body;
  34 + let user = {
  35 + username: username,
  36 + role: role,
  37 + state: state
  38 + };
  39 +
  40 + if (password !== '******') {
  41 + user.password = md5(password);
  42 + }
  43 +
  44 + if (_id) {
  45 + await User.update({
  46 + _id: _id
  47 + }, {
  48 + $set: user
  49 + });
  50 + } else {
  51 + await User.insert(user);
  52 + }
  53 + ctx.redirect('/users/setting');
  54 + ctx.status = 301;
  55 + },
  56 + async del(ctx) {
  57 + let id = ctx.request.body.id;
  58 + await User.removeById(id);
  59 + ctx.body = {
  60 + code: 200
  61 + };
  62 + }
  63 +};
  64 +
  65 +r.get('/setting', user.setting_page);
  66 +r.get('/new', user.new_page);
  67 +r.get('/edit', user.edit_page);
  68 +r.post('/save', user.save);
  69 +r.post('/del', user.del);
  70 +
  71 +export default r;
@@ -19,6 +19,16 @@ app.use(hbs({ @@ -19,6 +19,16 @@ app.use(hbs({
19 helpers: helpers 19 helpers: helpers
20 })); 20 }));
21 21
  22 +const mastersUrl = [
  23 + '/projects/new',
  24 + '/projects/edit',
  25 + '/projects/save',
  26 + '/projects/build',
  27 + '/projects/deploy',
  28 + '/servers',
  29 + '/users'
  30 +];
  31 +
22 app.use(async(ctx, next) => { 32 app.use(async(ctx, next) => {
23 ctx.locals = { 33 ctx.locals = {
24 title: 'Yoho Node.js 持续集成平台' 34 title: 'Yoho Node.js 持续集成平台'
@@ -27,8 +37,23 @@ app.use(async(ctx, next) => { @@ -27,8 +37,23 @@ app.use(async(ctx, next) => {
27 if (pjax) { 37 if (pjax) {
28 ctx.locals.layout = null; 38 ctx.locals.layout = null;
29 } 39 }
30 - await next();  
31 40
  41 + if (ctx.session && ctx.session.user ) {
  42 + ctx.locals.is_master = ctx.session.user.role === '1000';
  43 + ctx.locals.current_user = ctx.session.user;
  44 + }
  45 +
  46 + let needMaster = mastersUrl.some(u => {
  47 + return ctx.request.path.indexOf(u) === 0;
  48 + });
  49 +
  50 + if (needMaster) {
  51 + if (ctx.locals.is_master) {
  52 + await next();
  53 + }
  54 + } else {
  55 + await next();
  56 + }
32 // if (pjax && ctx.status == 301) { 57 // if (pjax && ctx.status == 301) {
33 // let location = ctx.response.get('Location'); 58 // let location = ctx.response.get('Location');
34 // ctx.response.set('X-PJAX-URL', ctx.origin + location); 59 // ctx.response.set('X-PJAX-URL', ctx.origin + location);
@@ -6,6 +6,7 @@ import projects from './actions/projects'; @@ -6,6 +6,7 @@ import projects from './actions/projects';
6 import servers from './actions/servers'; 6 import servers from './actions/servers';
7 import login from './actions/login'; 7 import login from './actions/login';
8 import monitor from './actions/monitor'; 8 import monitor from './actions/monitor';
  9 +import users from './actions/users';
9 10
10 const noAuth = new Router(); 11 const noAuth = new Router();
11 const base = new Router(); 12 const base = new Router();
@@ -28,6 +29,7 @@ export default function (app) { @@ -28,6 +29,7 @@ export default function (app) {
28 base.use('/projects', projects.routes(), projects.allowedMethods()); 29 base.use('/projects', projects.routes(), projects.allowedMethods());
29 base.use('/servers', servers.routes(), servers.allowedMethods()); 30 base.use('/servers', servers.routes(), servers.allowedMethods());
30 base.use('/monitor', monitor.routes(), monitor.allowedMethods()); 31 base.use('/monitor', monitor.routes(), monitor.allowedMethods());
  32 + base.use('/users', users.routes(), users.allowedMethods());
31 33
32 base.use('', index.routes(), index.allowedMethods()); 34 base.use('', index.routes(), index.allowedMethods());
33 35
@@ -56,8 +56,13 @@ @@ -56,8 +56,13 @@
56 <div class="col-sm-8 col-md-9"> 56 <div class="col-sm-8 col-md-9">
57 <div class="panel"> 57 <div class="panel">
58 <div class="panel-heading"> 58 <div class="panel-heading">
  59 + <div class="pull-right">
  60 + <a class="btn btn-success btn-rounded mr20 realtime-btn">开启实时加载</a>
  61 + </div>
59 <h4 class="panel-title">日志</h4> 62 <h4 class="panel-title">日志</h4>
60 <p>可通过滚动鼠标加载</p> 63 <p>可通过滚动鼠标加载</p>
  64 +
  65 +
61 </div><!-- panel-heading --> 66 </div><!-- panel-heading -->
62 <div class="panel-body yoho-log-dark"> 67 <div class="panel-body yoho-log-dark">
63 <div class="results-list "> 68 <div class="results-list ">
@@ -214,6 +219,7 @@ @@ -214,6 +219,7 @@
214 return; 219 return;
215 } else { 220 } else {
216 loading = true; 221 loading = true;
  222 + var scH = $logWrap[0].scrollHeight;
217 $.get('/monitor/log/query', { 223 $.get('/monitor/log/query', {
218 influxName: influxName, 224 influxName: influxName,
219 ip: ip, 225 ip: ip,
@@ -227,20 +233,35 @@ @@ -227,20 +233,35 @@
227 $(data).each(function() { 233 $(data).each(function() {
228 appendLog(this); 234 appendLog(this);
229 }); 235 });
  236 + $logWrap.scrollTop(scH);
230 }); 237 });
231 } 238 }
232 } 239 }
233 240
234 var scrollH, scrollTop = 0; 241 var scrollH, scrollTop = 0;
235 - $logWrap.scroll(function(e) { 242 + $logWrap.scroll(function() {
236 scrollH = $(this)[0].scrollHeight; 243 scrollH = $(this)[0].scrollHeight;
237 scrollTop = $(this)[0].scrollTop; 244 scrollTop = $(this)[0].scrollTop;
238 245
239 if (scrollTop < 10) { 246 if (scrollTop < 10) {
240 loadOld(); 247 loadOld();
241 - } else if (scrollTop + $logWrap.height() + 50 >= scrollH) { 248 + } else if (scrollTop + $logWrap.height() + 40 >= scrollH) {
242 loadNew(); 249 loadNew();
243 } 250 }
244 }); 251 });
  252 +
  253 + var sc;
  254 +
  255 + $('.realtime-btn').click(function() {
  256 + if ($(this).hasClass('active')) {
  257 + clearInterval(sc);
  258 + $(this).removeClass('active');
  259 + $(this).text('开启实时加载');
  260 + } else {
  261 + $(this).addClass('active');
  262 + $(this).text('关闭实时加载');
  263 + sc = setInterval(loadNew, 2000);
  264 + }
  265 + });
245 }); 266 });
246 </script> 267 </script>
@@ -83,12 +83,13 @@ @@ -83,12 +83,13 @@
83 <div class="col-sm-12"> 83 <div class="col-sm-12">
84 <h5 class="lg-title mb10">环境配置</h5> 84 <h5 class="lg-title mb10">环境配置</h5>
85 <ul class="nav nav-tabs nav-primary"> 85 <ul class="nav nav-tabs nav-primary">
86 - <li class="active"><a href="#home4" data-toggle="tab"><strong>线上环境</strong></a></li>  
87 - <li><a href="#profile4" data-toggle="tab"><strong>测试环境</strong></a></li> 86 + <li class="active"><a href="#production4" data-toggle="tab"><strong>线上环境</strong></a></li>
  87 + <li><a href="#preview4" data-toggle="tab"><strong>灰度环境</strong></a></li>
  88 + <li><a href="#test4" data-toggle="tab"><strong>测试环境</strong></a></li>
88 </ul> 89 </ul>
89 <!-- Tab panes --> 90 <!-- Tab panes -->
90 <div class="tab-content tab-content-primary mb30"> 91 <div class="tab-content tab-content-primary mb30">
91 - <div class="tab-pane active" id="home4"> 92 + <div class="tab-pane active" id="production4">
92 <div class="row"> 93 <div class="row">
93 <div class="col-sm-12"> 94 <div class="col-sm-12">
94 <div class="form-group"> 95 <div class="form-group">
@@ -126,8 +127,48 @@ @@ -126,8 +127,48 @@
126 </div> 127 </div>
127 </div> 128 </div>
128 </div><!-- tab-pane --> 129 </div><!-- tab-pane -->
129 -  
130 - <div class="tab-pane" id="profile4"> 130 + <div class="tab-pane" id="preview4">
  131 + <div class="row">
  132 + <div class="col-sm-12">
  133 + <div class="form-group">
  134 + <label class="control-label">目标服务器</label>
  135 + <div class="col-sm-12">
  136 + {{#each servers.preview}}
  137 + <div class="checkbox inline-block mr10">
  138 + <label>
  139 + <input type="checkbox"
  140 + name="deploy[preview][target][{{@index}}]"
  141 + value="{{host}}"
  142 + {{#if checked}}checked=""{{/if}}> {{host}}
  143 + </label>
  144 + </div>
  145 + {{/each}}
  146 + </div>
  147 + </div>
  148 + </div>
  149 + </div>
  150 + <div class="row">
  151 + <div class="col-sm-6">
  152 + <div class="form-group">
  153 + <label class="control-label">对应Git分支</label>
  154 + <input type="text" value="{{project.deploy.preview.branchName}}"
  155 + name="deploy[preview][branchName]" value="" placeholder="master"
  156 + class="form-control">
  157 + </div>
  158 + </div>
  159 + </div>
  160 + <div class="row">
  161 + <div class="col-sm-12">
  162 + <div class="form-group">
  163 + <label class="control-label">测试URL</label>
  164 + <input type="text" value="{{project.deploy.preview.testUrl}}"
  165 + name="deploy[preview][testUrl]" placeholder="ex: http://{host}:8080/test"
  166 + class="form-control">
  167 + </div>
  168 + </div>
  169 + </div>
  170 + </div><!-- tab-pane -->
  171 + <div class="tab-pane" id="test4">
131 <div class="row"> 172 <div class="row">
132 <div class="col-sm-12"> 173 <div class="col-sm-12">
133 <div class="form-group"> 174 <div class="form-group">
@@ -19,8 +19,10 @@ @@ -19,8 +19,10 @@
19 <div class="panel panel-default" data-env='{{deploy.env}}'> 19 <div class="panel panel-default" data-env='{{deploy.env}}'>
20 <div class="panel-heading"> 20 <div class="panel-heading">
21 <div class="pull-right"> 21 <div class="pull-right">
22 - <a class="btn btn-info btn-rounded mr5 log-btn"><i class="fa fa-eye"></i> 查看构建日志</a>  
23 - <a class="btn btn-success btn-rounded mr20 build-btn"><i class="glyphicon glyphicon-plus"></i> 新增构建</a> 22 + {{#if is_master}}
  23 + <a class="btn btn-info btn-rounded mr5 log-btn"><i class="fa fa-eye"></i> 查看构建日志</a>
  24 + <a class="btn btn-success btn-rounded mr20 build-btn"><i class="glyphicon glyphicon-plus"></i> 新增构建</a>
  25 + {{/if}}
24 <a href="" class="tooltips panel-minimize"><i class="fa fa-minus"></i></a> 26 <a href="" class="tooltips panel-minimize"><i class="fa fa-minus"></i></a>
25 </div> 27 </div>
26 <h4 class="panel-title">{{deploy.name}}</h4> 28 <h4 class="panel-title">{{deploy.name}}</h4>
@@ -29,18 +31,18 @@ @@ -29,18 +31,18 @@
29 <div class="panel-body"> 31 <div class="panel-body">
30 <table id="table-{{deploy.env}}" class="table table-striped table-bordered building-table"> 32 <table id="table-{{deploy.env}}" class="table table-striped table-bordered building-table">
31 <thead> 33 <thead>
32 - <tr>  
33 - <th>构建版本</th>  
34 - <th>分支名称</th>  
35 - <th>状态</th>  
36 - <th>构建时间</th>  
37 - <th>操作</th>  
38 - </tr> 34 + <tr>
  35 + <th>构建版本</th>
  36 + <th>分支名称</th>
  37 + <th>状态</th>
  38 + <th>构建时间</th>
  39 + <th>操作</th>
  40 + </tr>
39 </thead> 41 </thead>
40 </table> 42 </table>
41 </div> 43 </div>
42 </div> 44 </div>
43 - 45 +
44 <div class="panel panel-default"> 46 <div class="panel panel-default">
45 <div class="panel-heading"> 47 <div class="panel-heading">
46 <h4>服务器信息</h4> 48 <h4>服务器信息</h4>
@@ -49,29 +51,32 @@ @@ -49,29 +51,32 @@
49 <div class="panel-body"> 51 <div class="panel-body">
50 <div class="row"> 52 <div class="row">
51 {{#each targets}} 53 {{#each targets}}
52 - <div class="col-md-4" id="d-{{hostFm}}">  
53 - <div class="panel panel-info noborder">  
54 - <div class="panel-heading noborder">  
55 - <div class="panel-btns">  
56 - </div><!-- panel-btns -->  
57 - <div class="panel-icon"><i class="fa fa-cloud" style="padding-left:12px;"></i></div>  
58 - <div class="media-body">  
59 - <h2 class="nomargin">{{host}}</h2>  
60 - <h5 class="md-title mt5">当前运行版本:&nbsp;<code>{{#if info}}{{info.building}}{{^}}未知部署{{/if}}</code></h5>  
61 - </div><!-- media-body -->  
62 - <hr class="mt10 mb10">  
63 - <div class="clearfix mt5">  
64 - <div class="col-xs-6 project-env" data-env="test">  
65 - <h5 class="md-title mt10">当前状态</h5>  
66 - <span class="label label-success deploy-log-btn" data-host="{{host}}"><i class="fa fa-spinner fa-spin fa-fw margin-bottom"></i> <b>{{#if info}}{{info.state}}{{^}}未知部署{{/if}}</b></span>  
67 - </div>  
68 - <div class="col-xs-6">  
69 - 54 + <div class="col-md-4" id="d-{{hostFm}}">
  55 + <div class="panel panel-info noborder">
  56 + <div class="panel-heading noborder">
  57 + <div class="panel-btns">
  58 + </div><!-- panel-btns -->
  59 + <div class="panel-icon"><i class="fa fa-cloud" style="padding-left:12px;"></i></div>
  60 + <div class="media-body">
  61 + <h2 class="nomargin">{{host}}</h2>
  62 + <h5 class="md-title mt5">当前运行版本:&nbsp;<code>{{#if info}}{{info.building}}{{^}}
  63 + 未知部署{{/if}}</code></h5>
  64 + </div><!-- media-body -->
  65 + <hr class="mt10 mb10">
  66 + <div class="clearfix mt5">
  67 + <div class="col-xs-6 project-env" data-env="test">
  68 + <h5 class="md-title mt10">当前状态</h5>
  69 + <span class="label label-success deploy-log-btn" data-host="{{host}}"><i
  70 + class="fa fa-spinner fa-spin fa-fw margin-bottom"></i> <b>{{#if info}}{{info.state}}{{^}}
  71 + 未知部署{{/if}}</b></span>
  72 + </div>
  73 + <div class="col-xs-6">
  74 +
  75 + </div>
70 </div> 76 </div>
71 - </div>  
72 - </div><!-- panel-body -->  
73 - </div><!-- panel -->  
74 - </div> 77 + </div><!-- panel-body -->
  78 + </div><!-- panel -->
  79 + </div>
75 {{/each}} 80 {{/each}}
76 </div> 81 </div>
77 </div> 82 </div>
@@ -79,99 +84,103 @@ @@ -79,99 +84,103 @@
79 </div> 84 </div>
80 85
81 <div id="log-area" class="panel panel-default panel-alt" style="display:none;height: 100%;"> 86 <div id="log-area" class="panel panel-default panel-alt" style="display:none;height: 100%;">
82 - <div class="panel-body nopadding" style="height: 100%;"> 87 + <div class="panel-body nopadding" style="height: 100%;">
83 <textarea name="code" id="code" style="height: 100%;"></textarea> 88 <textarea name="code" id="code" style="height: 100%;"></textarea>
84 </div> 89 </div>
85 </div> 90 </div>
86 91
87 <script> 92 <script>
88 -  
89 - $(function(){  
90 - 93 +
  94 + $(function() {
  95 +
91 var tables = {}; 96 var tables = {};
92 - $('.building-table').each(function(){ 97 + $('.building-table').each(function() {
93 var env = $(this).parents('.panel').data('env'); 98 var env = $(this).parents('.panel').data('env');
94 tables[env] = $(this).DataTable({ 99 tables[env] = $(this).DataTable({
95 responsive: true, 100 responsive: true,
96 ajax: '/projects/{{project._id}}/buildings?env=' + env, 101 ajax: '/projects/{{project._id}}/buildings?env=' + env,
97 columns: [ 102 columns: [
98 - { data: "buildTime"},  
99 - { data: "branch"},  
100 - { data: "state"},  
101 - { data: "updatedAt"},  
102 - { data: "_id" } 103 + {data: "buildTime"},
  104 + {data: "branch"},
  105 + {data: "state"},
  106 + {data: "updatedAt"},
  107 + {data: "_id"}
103 ], 108 ],
104 order: [[0, 'desc']], 109 order: [[0, 'desc']],
105 columnDefs: [{ 110 columnDefs: [{
106 - render: function(data, type, row){ 111 + render: function(data, type, row) {
107 var color = 'warning'; 112 var color = 'warning';
108 - if(data == 'success'){ 113 + if (data == 'success') {
109 color = 'success'; 114 color = 'success';
110 - }else if(data == 'fail'){ 115 + } else if (data == 'fail') {
111 color = 'danger'; 116 color = 'danger';
112 } 117 }
113 - var html = '<span id="b-'+row._id+'" class="label label-' + color + '">';  
114 - if(data != 'success' && data != 'fail'){ 118 + var html = '<span id="b-' + row._id + '" class="label label-' + color + '">';
  119 + if (data != 'success' && data != 'fail') {
115 html += '<i class="fa fa-spinner fa-spin fa-fw margin-bottom"></i>'; 120 html += '<i class="fa fa-spinner fa-spin fa-fw margin-bottom"></i>';
116 - } 121 + }
117 html += data + '</span>'; 122 html += data + '</span>';
118 return html; 123 return html;
119 }, 124 },
120 targets: 2 125 targets: 2
121 }, { 126 }, {
122 - render: function(data, type, row){ 127 + render: function(data, type, row) {
123 var disabled = row.state !== 'success'; 128 var disabled = row.state !== 'success';
124 - return '<button '+(disabled? 'disabled' : '')+' class="btn btn-success btn-xs deploy-btn" data-id="'+data+'" data-build='+row.buildTime+'>分发</button>'; 129 + {{#if is_master}}
  130 + return '<button ' + (disabled ? 'disabled' : '') + ' class="btn btn-success btn-xs deploy-btn" data-id="' + data + '" data-build=' + row.buildTime + '>分发</button>';
  131 + {{^}}
  132 + return '';
  133 + {{/if}}
125 }, 134 },
126 targets: 4 135 targets: 4
127 }] 136 }]
128 }); 137 });
129 -  
130 - $(this).on( 'draw.dt', function () {  
131 - $('.deploy-btn').click(function(){  
132 - var id = $(this).data('id');  
133 - var build = $(this).data('build');  
134 - layer.confirm('确定发布版本<code>' + build + '</code>吗?', {  
135 - btn: ['确定', '取消']  
136 - }, function(){  
137 - doDeploy(id);  
138 - }); 138 +
  139 + $(this).on('draw.dt', function() {
  140 + $('.deploy-btn').click(function() {
  141 + var id = $(this).data('id');
  142 + var build = $(this).data('build');
  143 + layer.confirm('确定发布版本<code>' + build + '</code>吗?', {
  144 + btn: ['确定', '取消']
  145 + }, function() {
  146 + doDeploy(id);
139 }); 147 });
140 - } ); 148 + });
  149 + });
141 }); 150 });
142 -  
143 - function doDeploy(build){ 151 +
  152 + function doDeploy(build) {
144 $.post('/projects/deploy/' + build, function(ret) { 153 $.post('/projects/deploy/' + build, function(ret) {
145 - if(ret.code == 200){ 154 + if (ret.code == 200) {
146 layer.msg('正在分发中'); 155 layer.msg('正在分发中');
147 - }else{ 156 + } else {
148 layer.msg(ret.msg, {icon: 5}); 157 layer.msg(ret.msg, {icon: 5});
149 } 158 }
150 }); 159 });
151 } 160 }
152 -  
153 - $('.build-btn').click(function(){ 161 +
  162 + $('.build-btn').click(function() {
154 var env = $(this).parents('.panel').data('env'); 163 var env = $(this).parents('.panel').data('env');
155 - var i = layer.prompt({ 164 + var i = layer.prompt({
156 title: '请输入需要构建的分支,默认为 {{deploy.branchName}}' 165 title: '请输入需要构建的分支,默认为 {{deploy.branchName}}'
157 - }, function(branch){ 166 + }, function(branch) {
158 branch = branch || '{{deploy.branchName}}'; 167 branch = branch || '{{deploy.branchName}}';
159 - $.post('/projects/build/{{project._id}}', {env: env, branch: branch}, function(ret){  
160 - if(ret.code == 200) { 168 + $.post('/projects/build/{{project._id}}', {env: env, branch: branch}, function(ret) {
  169 + if (ret.code == 200) {
161 tables[env].ajax.reload(); 170 tables[env].ajax.reload();
162 layer.close(i); 171 layer.close(i);
163 } 172 }
164 }); 173 });
165 }); 174 });
166 - 175 +
167 }); 176 });
168 -  
169 -  
170 - $('.rollback-btn').click(function(){ 177 +
  178 +
  179 + $('.rollback-btn').click(function() {
171 layer.prompt({ 180 layer.prompt({
172 title: '请输入回滚到哪个版本' 181 title: '请输入回滚到哪个版本'
173 - }, function(build){  
174 - doDeploy(build); 182 + }, function(build) {
  183 + doDeploy(build);
175 }); 184 });
176 }); 185 });
177 186
@@ -181,17 +190,17 @@ @@ -181,17 +190,17 @@
181 }); 190 });
182 191
183 var tag; 192 var tag;
184 -  
185 - $('.log-btn').click(function(){ 193 +
  194 + $('.log-btn').click(function() {
186 viewLog(''); 195 viewLog('');
187 }); 196 });
188 197
189 - $('.deploy-log-btn').click(function(){ 198 + $('.deploy-log-btn').click(function() {
190 var host = $(this).data('host'); 199 var host = $(this).data('host');
191 viewLog(host); 200 viewLog(host);
192 }); 201 });
193 202
194 - function viewLog(tag){ 203 + function viewLog(tag) {
195 tag = tag; 204 tag = tag;
196 $('#log-area').show(); 205 $('#log-area').show();
197 cm.setValue(""); 206 cm.setValue("");
@@ -201,46 +210,46 @@ @@ -201,46 +210,46 @@
201 title: '实时日志', 210 title: '实时日志',
202 content: $('#log-area'), 211 content: $('#log-area'),
203 area: ['1000px', '600px'], 212 area: ['1000px', '600px'],
204 - maxmin:true,  
205 - cancel: function(){ 213 + maxmin: true,
  214 + cancel: function() {
206 $('#log-area').hide(); 215 $('#log-area').hide();
207 } 216 }
208 }); 217 });
209 } 218 }
210 219
211 var ws = io(); 220 var ws = io();
212 - ws.on('connect', function(){  
213 - ws.on('/building/{{project._id}}', function(data){ 221 + ws.on('connect', function() {
  222 + ws.on('/building/{{project._id}}', function(data) {
214 console.log(data); 223 console.log(data);
215 $('#b-' + data.bid).text(data.state); 224 $('#b-' + data.bid).text(data.state);
216 - if(data.state == 'success'){ 225 + if (data.state == 'success') {
217 $('#b-' + data.bid).removeClass('label-warning').addClass('label-success') 226 $('#b-' + data.bid).removeClass('label-warning').addClass('label-success')
218 $('#b-' + data.bid).parent().parent().find('.deploy-btn').removeAttr('disabled'); 227 $('#b-' + data.bid).parent().parent().find('.deploy-btn').removeAttr('disabled');
219 $('#b-' + data.bid).find('i').remove(); 228 $('#b-' + data.bid).find('i').remove();
220 - }else if(data.state == 'fail'){ 229 + } else if (data.state == 'fail') {
221 $('#b-' + data.bid).removeClass('label-warning').addClass('label-danger'); 230 $('#b-' + data.bid).removeClass('label-warning').addClass('label-danger');
222 $('#b-' + data.bid).find('i').remove(); 231 $('#b-' + data.bid).find('i').remove();
223 } 232 }
224 }); 233 });
225 -  
226 - // ws.on('/building/{{project._id}}/log', function(data){  
227 - // if(tag == '') {  
228 - // cm.replaceRange("> " + data + "\n", {line: Infinity});  
229 - // }  
230 - // });  
231 -  
232 - ws.on('/deploy/{{project._id}}', function(data){ 234 +
  235 + // ws.on('/building/{{project._id}}/log', function(data){
  236 + // if(tag == '') {
  237 + // cm.replaceRange("> " + data + "\n", {line: Infinity});
  238 + // }
  239 + // });
  240 +
  241 + ws.on('/deploy/{{project._id}}', function(data) {
233 console.log(data); 242 console.log(data);
234 $('#d-' + data.host.replace(/\./g, '-')).find('b').text(data.state); 243 $('#d-' + data.host.replace(/\./g, '-')).find('b').text(data.state);
235 }); 244 });
236 245
237 - // ws.on('/deploy/{{project._id}}/log', function(data){  
238 - // if(tag == data.host){  
239 - // cm.replaceRange("> " +data.msg+ "\n", {line: Infinity});  
240 - // }  
241 - // }); 246 + // ws.on('/deploy/{{project._id}}/log', function(data){
  247 + // if(tag == data.host){
  248 + // cm.replaceRange("> " +data.msg+ "\n", {line: Infinity});
  249 + // }
  250 + // });
242 }); 251 });
243 - ws.on('error', function(){ 252 + ws.on('error', function() {
244 console.log('connect fail'); 253 console.log('connect fail');
245 }); 254 });
246 }); 255 });
@@ -18,59 +18,68 @@ @@ -18,59 +18,68 @@
18 <div class="contentpanel servers-page"> 18 <div class="contentpanel servers-page">
19 <div class="row row-stat"> 19 <div class="row row-stat">
20 {{#each projects}} 20 {{#each projects}}
21 - <div class="col-md-4">  
22 - <div class="panel panel-{{color}} noborder">  
23 - <div class="panel-heading noborder">  
24 - <div class="panel-btns">  
25 - <a href="/projects/edit?id={{_id}}" class="tooltips" title="设置"><i class="fa fa-gear"></i></a>  
26 - </div><!-- panel-btns -->  
27 - <div class="panel-icon"><i class="fa fa-git" style="padding-left:12px;"></i></div>  
28 - <div class="media-body">  
29 - <h1 class="nomargin">{{name}}</h1>  
30 - <h5 class="md-title mt5">{{subname}}&nbsp;</h5>  
31 - </div><!-- media-body -->  
32 - <hr>  
33 - <div class="clearfix mt20">  
34 - <div class="col-xs-6 project-env" data-id="{{_id}}" data-env="production">  
35 - <h5 class="md-title nomargin">线上环境</h5>  
36 - <h4 class="nomargin">{{deploy.production.target.length}}</h4> 21 + <div class="col-md-4">
  22 + <div class="panel panel-{{color}} noborder">
  23 + <div class="panel-heading noborder">
  24 + <div class="panel-btns">
  25 + {{#if @root.is_master}}
  26 + <a href="/projects/edit?id={{_id}}" class="tooltips" title="设置"><i
  27 + class="fa fa-gear"></i></a>
  28 + {{/if}}
  29 + </div><!-- panel-btns -->
  30 + <div class="panel-icon"><i class="fa fa-git" style="padding-left:12px;"></i></div>
  31 + <div class="media-body">
  32 + <h1 class="nomargin">{{name}}</h1>
  33 + <h5 class="md-title mt5">{{subname}}&nbsp;</h5>
  34 + </div><!-- media-body -->
  35 + <hr>
  36 + <div class="clearfix mt20">
  37 + <div class="col-xs-4 project-env" data-id="{{_id}}" data-env="production">
  38 + <h5 class="md-title nomargin">线上环境</h5>
  39 + <h4 class="nomargin">{{deploy.production.target.length}}</h4>
  40 + </div>
  41 + <div class="col-xs-4 project-env" data-id="{{_id}}" data-env="preview">
  42 + <h5 class="md-title nomargin">灰度环境</h5>
  43 + <h4 class="nomargin">{{deploy.preview.target.length}}</h4>
  44 + </div>
  45 + <div class="col-xs-4 project-env" data-id="{{_id}}" data-env="test">
  46 + <h5 class="md-title nomargin">测试环境</h5>
  47 + <h4 class="nomargin">{{deploy.test.target.length}}</h4>
  48 + </div>
37 </div> 49 </div>
38 - <div class="col-xs-6 project-env" data-id="{{_id}}" data-env="test">  
39 - <h5 class="md-title nomargin">测试环境</h5>  
40 - <h4 class="nomargin">{{deploy.test.target.length}}</h4>  
41 - </div>  
42 - </div>  
43 - </div><!-- panel-body -->  
44 - </div><!-- panel -->  
45 - </div><!-- col-md-4 --> 50 + </div><!-- panel-body -->
  51 + </div><!-- panel -->
  52 + </div><!-- col-md-4 -->
46 {{/each}} 53 {{/each}}
47 - <div class="col-md-4">  
48 - <div class="panel panel-default noborder">  
49 - <div class="panel-heading noborder">  
50 - <div style="text-align: center; font-size: 97px;">  
51 - <a href="/projects/new" class="">  
52 - <i class="fa fa-plus"></i>  
53 - </a> 54 + {{#if is_master}}
  55 + <div class="col-md-4">
  56 + <div class="panel panel-default noborder">
  57 + <div class="panel-heading noborder">
  58 + <div style="text-align: center; font-size: 97px;">
  59 + <a href="/projects/new" class="">
  60 + <i class="fa fa-plus"></i>
  61 + </a>
  62 + </div>
54 </div> 63 </div>
55 </div> 64 </div>
56 </div> 65 </div>
57 - </div> 66 + {{/if}}
58 </div><!-- row --> 67 </div><!-- row -->
59 </div> 68 </div>
60 69
61 70
62 <script> 71 <script>
63 $(function() { 72 $(function() {
64 - $('.servers-page').pjax('a', '#pjax-container');  
65 - $('.project-env').click(function(){  
66 - var id = $(this).data('id');  
67 - var env = $(this).data('env');  
68 - $.pjax({  
69 - url: '/projects/' + id + '?env=' + env,  
70 - container: '#pjax-container'  
71 - });  
72 - });  
73 - 73 + $('.servers-page').pjax('a', '#pjax-container');
  74 + $('.project-env').click(function() {
  75 + var id = $(this).data('id');
  76 + var env = $(this).data('env');
  77 + $.pjax({
  78 + url: '/projects/' + id + '?env=' + env,
  79 + container: '#pjax-container'
  80 + });
  81 + });
  82 +
74 }); 83 });
75 - 84 +
76 </script> 85 </script>
@@ -73,7 +73,14 @@ @@ -73,7 +73,14 @@
73 </div> 73 </div>
74 </div> 74 </div>
75 </div> 75 </div>
76 - <div class="col-sm-4"> 76 + <div class="col-sm-2">
  77 + <label class="control-label">&nbsp;</label>
  78 + <div class="rdio rdio-primary">
  79 + <input type="radio" name="env" id="previewEnv" value="preview" {{#equals env 'preview'}}checked="checked"{{/equals}}>
  80 + <label for="previewEnv">灰度环境</label>
  81 + </div>
  82 + </div>
  83 + <div class="col-sm-2">
77 <label class="control-label">&nbsp;</label> 84 <label class="control-label">&nbsp;</label>
78 <div class="rdio rdio-primary"> 85 <div class="rdio rdio-primary">
79 <input type="radio" name="env" id="testEnv" value="test" {{#equals env 'test'}}checked="checked"{{/equals}}> 86 <input type="radio" name="env" id="testEnv" value="test" {{#equals env 'test'}}checked="checked"{{/equals}}>
@@ -46,7 +46,7 @@ @@ -46,7 +46,7 @@
46 <td>{{host}}</td> 46 <td>{{host}}</td>
47 <td>{{username}}</td> 47 <td>{{username}}</td>
48 <td>{{port}}</td> 48 <td>{{port}}</td>
49 - <td><span class="label label-primary">{{envName}}</span></td> 49 + <td><span class="label label-primary {{env}}">{{envName}}</span></td>
50 <td>{{deployDir}}</td> 50 <td>{{deployDir}}</td>
51 <td data-id='{{_id}}'> 51 <td data-id='{{_id}}'>
52 <button class="btn btn-success btn-xs server-edit">修改</button> &nbsp; 52 <button class="btn btn-success btn-xs server-edit">修改</button> &nbsp;
  1 +<div class="pageheader">
  2 + <div class="media">
  3 + <div class="pageicon pull-left">
  4 + <i class="fa fa-th-list"></i>
  5 + </div>
  6 + <div class="media-body">
  7 + <ul class="breadcrumb">
  8 + <li><a href=""><i class="glyphicon glyphicon-home"></i></a></li>
  9 + <li><a href="">系统设置</a></li>
  10 + <li>用户管理</li>
  11 + </ul>
  12 + <h4>用户管理</h4>
  13 + </div>
  14 + </div>
  15 + <!-- media -->
  16 +</div>
  17 +<!-- pageheader -->
  18 +
  19 +<div class="contentpanel">
  20 + <div class="panel panel-primary-head">
  21 + <div class="panel-heading">
  22 + <div class="pull-right">
  23 + <a id="new-page" href="/users/new" class="btn btn-success btn-rounded"><i class="glyphicon glyphicon-plus"></i> 新增用户</a>
  24 + </div>
  25 + <h4 class="panel-title">用户管理</h4>
  26 + <p>&nbsp;</p>
  27 + </div>
  28 + <!-- panel-heading -->
  29 +
  30 + <table id="basicTable" class="table table-striped table-bordered responsive">
  31 + <thead class="">
  32 + <tr>
  33 + <th>用户名</th>
  34 + <th>角色</th>
  35 + <th>状态</th>
  36 + <th></th>
  37 + </tr>
  38 + </thead>
  39 +
  40 + <tbody>
  41 + {{#each users}}
  42 + <tr>
  43 + <td>{{username}}</td>
  44 + <td>{{#equals role '1000'}}<span class="label label-primary">运维</span>{{^}}<span class="label label-success">开发</span>{{/equals}}</td>
  45 + <td>{{#equals state '1'}}<span class="label label-success">启用</span>{{^}}<span class="label label-danger">禁用</span>{{/equals}}</td>
  46 + <td data-id='{{_id}}'>
  47 + <button class="btn btn-success btn-xs server-edit">修改</button> &nbsp;
  48 + <button class="btn btn-danger btn-xs server-del">删除</button>
  49 + </td>
  50 + </tr>
  51 + {{/each}}
  52 + </tbody>
  53 + </table>
  54 + </div>
  55 + <!-- panel -->
  56 +</div>
  57 +
  58 +
  59 +<script>
  60 + $(function() {
  61 + $("#basicTable").DataTable({
  62 + responsive: true
  63 + });
  64 + $(document).pjax('#new-page', '#pjax-container')
  65 +
  66 + $('.server-del').click(function(){
  67 + var id = $(this).parent().data('id');
  68 + $.post('/users/del', {id: id}, function(ret){
  69 + if(ret.code == 200){
  70 + var i = layer.alert('操作成功', function(){
  71 + layer.close(i);
  72 + $.pjax.reload('#pjax-container');
  73 + });
  74 + }
  75 + });
  76 + });
  77 +
  78 + $('.server-edit').click(function(){
  79 + var id = $(this).parent().data('id');
  80 + $.pjax({
  81 + url: '/users/edit?id=' + id,
  82 + container: '#pjax-container'
  83 + });
  84 + });
  85 + });
  86 +
  87 +</script>
  1 +
  2 +<div class="pageheader">
  3 + <div class="media">
  4 + <div class="pageicon pull-left">
  5 + <i class="fa fa-th-list"></i>
  6 + </div>
  7 + <div class="media-body">
  8 + <ul class="breadcrumb">
  9 + <li><a href=""><i class="glyphicon glyphicon-home"></i></a></li>
  10 + <li><a href="">服务器管理</a></li>
  11 + <li>用户信息</li>
  12 + </ul>
  13 + <h4>用户信息</h4>
  14 + </div>
  15 + </div>
  16 + <!-- media -->
  17 +</div>
  18 +<!-- pageheader -->
  19 +<div class="panel panel-default">
  20 + <div class="panel-heading">
  21 + <p>&nbsp;</p>
  22 + </div>
  23 + <form action="/users/save" method="POST" data-pjax>
  24 + <input type="hidden" name="_id" value="{{_id}}">
  25 + <div class="panel-body">
  26 + <div class="row">
  27 + <div class="col-sm-6">
  28 + <div class="form-group">
  29 + <label class="control-label">Username</label>
  30 + <input type="text" name="username" value="{{username}}" class="form-control" placeholder="用户名">
  31 + </div>
  32 + <!-- form-group -->
  33 + </div>
  34 + <!-- col-sm-6 -->
  35 + <div class="col-sm-6">
  36 + <div class="form-group">
  37 + <label class="control-label">Password</label>
  38 + <input type="text" name="password" value="{{password}}" class="form-control" placeholder="密码">
  39 + </div>
  40 + <!-- form-group -->
  41 + </div>
  42 + <!-- col-sm-6 -->
  43 + </div>
  44 + <!-- row -->
  45 + <div class="row">
  46 + <div class="col-sm-2">
  47 + <div class="form-group">
  48 + <label class="control-label">Role</label>
  49 + <div class="rdio rdio-primary">
  50 + <input type="radio" name="role" id="role1" value="1000" {{#equals role '1000'}}checked="checked"{{/equals}}>
  51 + <label for="role1">运维</label>
  52 + </div>
  53 + </div>
  54 + </div>
  55 + <div class="col-sm-4">
  56 + <label class="control-label">&nbsp;</label>
  57 + <div class="rdio rdio-primary">
  58 + <input type="radio" name="role" id="role2" value="2000" {{#equals role '2000'}}checked="checked"{{/equals}}>
  59 + <label for="role2">开发</label>
  60 + </div>
  61 + </div>
  62 + <div class="col-sm-2">
  63 + <div class="form-group">
  64 + <label class="control-label">状态</label>
  65 + <div class="rdio rdio-primary">
  66 + <input type="radio" name="state" id="state1" value="1" {{#equals state '1'}}checked="checked"{{/equals}}>
  67 + <label for="state1">启用</label>
  68 + </div>
  69 + </div>
  70 + </div>
  71 + <div class="col-sm-4">
  72 + <label class="control-label">&nbsp;</label>
  73 + <div class="rdio rdio-primary">
  74 + <input type="radio" name="state" id="state2" value="0" {{#equals state '0'}}checked="checked"{{/equals}}>
  75 + <label for="state2">禁用</label>
  76 + </div>
  77 + </div>
  78 + </div>
  79 + <!-- row -->
  80 + </div>
  81 + <!-- panel-body -->
  82 + <div class="panel-footer">
  83 + <button type="submit" class="btn btn-primary">保存</button>
  84 + <button type="button" class="btn btn-default ml20 go-back">取消</button>
  85 + </div>
  86 + </form>
  87 + <!-- panel-footer -->
  88 +</div>
  89 +
  90 +<script>
  91 + $(function(){
  92 + $(document.body).off().on('submit', 'form[data-pjax]', function(event) {
  93 + event.preventDefault(); // stop default submit behavior
  94 + $.pjax.submit(event, '#pjax-container', {
  95 + type: 'POST'
  96 + });
  97 + });
  98 +
  99 + $('.go-back').click(function() {
  100 + window.history.go(-1);
  101 + });
  102 + });
  103 +</script>
  104 +
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 <img class="img-circle" src="/images/photos/profile.png" alt=""> 4 <img class="img-circle" src="/images/photos/profile.png" alt="">
5 </a> 5 </a>
6 <div class="media-body"> 6 <div class="media-body">
7 - <h4 class="media-heading">Admin</h4> 7 + <h4 class="media-heading">{{current_user.username}}</h4>
8 <small class="text-muted">yoho-ci</small> 8 <small class="text-muted">yoho-ci</small>
9 </div> 9 </div>
10 </div> 10 </div>
@@ -13,17 +13,20 @@ @@ -13,17 +13,20 @@
13 <h5 class="leftpanel-title">Navigation</h5> 13 <h5 class="leftpanel-title">Navigation</h5>
14 <ul class="nav nav-pills nav-stacked nav-menu"> 14 <ul class="nav nav-pills nav-stacked nav-menu">
15 <li class="active"><a href="/index"><i class="fa fa-home"></i> <span>Dashboard</span></a></li> 15 <li class="active"><a href="/index"><i class="fa fa-home"></i> <span>Dashboard</span></a></li>
16 - <li><a href="/projects"><i class="glyphicon glyphicon-th"></i> <span>Projects</span></a></li> 16 + <li><a href="/projects"><i class="glyphicon glyphicon-th"></i> <span>项目</span></a></li>
17 <li class="parent"><a href=""><i class="fa fa-gears"></i> <span>监控中心</span></a> 17 <li class="parent"><a href=""><i class="fa fa-gears"></i> <span>监控中心</span></a>
18 <ul class="children"> 18 <ul class="children">
19 <li><a href="/monitor/log">实时日志</a></li> 19 <li><a href="/monitor/log">实时日志</a></li>
20 </ul> 20 </ul>
21 </li> 21 </li>
22 - <li class="parent"><a href=""><i class="fa fa-gears"></i> <span>Settings</span></a> 22 + {{#if is_master}}
  23 + <li class="parent"><a href=""><i class="fa fa-gears"></i> <span>系统配置</span></a>
23 <ul class="children"> 24 <ul class="children">
24 - <li><a href="/servers/setting">Servers</a></li> 25 + <li><a href="/servers/setting">服务器配置</a></li>
  26 + <li><a href="/users/setting">用户管理</a></li>
25 </ul> 27 </ul>
26 </li> 28 </li>
  29 + {{/if}}
27 </ul> 30 </ul>
28 31
29 </div> 32 </div>
@@ -47,6 +47,7 @@ @@ -47,6 +47,7 @@
47 "koa-session": "^3.3.1", 47 "koa-session": "^3.3.1",
48 "koa-static": "^3.0.0", 48 "koa-static": "^3.0.0",
49 "lodash": "^4.13.1", 49 "lodash": "^4.13.1",
  50 + "md5": "^2.1.0",
50 "moment": "^2.13.0", 51 "moment": "^2.13.0",
51 "nedb": "^1.8.0", 52 "nedb": "^1.8.0",
52 "nedb-promise": "^2.0.0", 53 "nedb-promise": "^2.0.0",
@@ -79,4 +79,16 @@ body .mainwrapper .leftpanel { @@ -79,4 +79,16 @@ body .mainwrapper .leftpanel {
79 .yoho-log-dark .host { 79 .yoho-log-dark .host {
80 width: 140px; 80 width: 140px;
81 display: inline-block; 81 display: inline-block;
  82 +}
  83 +
  84 +.label.production {
  85 + background-color: #428bca;
  86 +}
  87 +
  88 +.label.preview {
  89 + background-color: #F5A503;
  90 +}
  91 +
  92 +.label.test {
  93 + background-color: #5cb85c;
82 } 94 }