- Added settings sidebar;
	- Added IP for node shows;
pull/98/merge v0.3.1
Doflatango 2018-03-19 15:36:10 +08:00
parent 75b49bddae
commit 7c61222b68
19 changed files with 243 additions and 102 deletions

View File

@ -31,13 +31,20 @@ var UpgradeCmd = &cobra.Command{
ea.Exit("invalid version number")
}
nodesById := getIPMapper(ea)
if prever < "0.3.0" {
fmt.Println("upgrading data to version 0.3.0")
nodesById := getIPMapper(ea)
if to_0_3_0(ea, nodesById) {
return
}
}
if prever < "0.3.1" {
fmt.Println("upgrading data to version 0.3.1")
if to_0_3_1(ea, nodesById) {
return
}
}
},
}
@ -132,10 +139,8 @@ func to_0_3_0(ea *ExitAction, nodesById map[string]*cronsun.Node) (shouldStop bo
cronsun.GetDb().WithC(cronsun.Coll_JobLog, func(c *mgo.Collection) error {
for ip, node := range nodesById {
_, err = c.UpdateAll(bson.M{"node": ip}, bson.M{"$set": bson.M{"node": node.ID, "hostname": node.Hostname}})
if err != nil {
if err != nil {
fmt.Println("failed to upgrade job logs: ", err.Error())
}
break
}
}
@ -147,10 +152,39 @@ func to_0_3_0(ea *ExitAction, nodesById map[string]*cronsun.Node) (shouldStop bo
cronsun.GetDb().WithC(cronsun.Coll_JobLatestLog, func(c *mgo.Collection) error {
for ip, node := range nodesById {
_, err = c.UpdateAll(bson.M{"node": ip}, bson.M{"$set": bson.M{"node": node.ID, "hostname": node.Hostname}})
if err != nil {
if err != nil {
fmt.Println("failed to upgrade job latest logs: ", err.Error())
}
break
}
}
shouldStop = true
return err
})
return
}
// to_0_3_0 can be run many times
func to_0_3_1(ea *ExitAction, nodesById map[string]*cronsun.Node) (shouldStop bool) {
// upgrade logs
var err error
cronsun.GetDb().WithC(cronsun.Coll_JobLog, func(c *mgo.Collection) error {
for _, node := range nodesById {
_, err = c.UpdateAll(bson.M{"node": node.ID}, bson.M{"$set": bson.M{"ip": node.IP}})
if err != nil {
fmt.Println("failed to upgrade job logs: ", err.Error())
break
}
}
shouldStop = true
return err
})
cronsun.GetDb().WithC(cronsun.Coll_JobLatestLog, func(c *mgo.Collection) error {
for _, node := range nodesById {
_, err = c.UpdateAll(bson.M{"node": node.ID}, bson.M{"$set": bson.M{"ip": node.IP}})
if err != nil {
fmt.Println("failed to upgrade job latest logs: ", err.Error())
break
}
}

5
job.go
View File

@ -69,6 +69,7 @@ type Job struct {
// 执行任务的结点,用于记录 job log
runOn string
hostname string
ip string
// 用于存储分隔后的任务
cmd []string
// 控制同时执行任务数
@ -186,9 +187,9 @@ func (j *Job) unlimit() {
atomic.AddInt64(j.Count, -1)
}
func (j *Job) Init(nodeID, hostname string) {
func (j *Job) Init(nodeID, hostname, ip string) {
var c int64
j.Count, j.runOn, j.hostname = &c, nodeID, hostname
j.Count, j.runOn, j.hostname, j.ip = &c, nodeID, hostname, ip
}
func (c *Cmd) lockTtl() int64 {

View File

@ -25,6 +25,7 @@ type JobLog struct {
Name string `bson:"name" json:"name"` // 任务名称
Node string `bson:"node" json:"node"` // 运行此次任务的节点 id索引
Hostname string `bson:"hostname" json:"hostname"` // 运行此次任务的节点主机名称,索引
IP string `bson:"ip" json:"ip"` // 运行此次任务的节点主机IP索引
Command string `bson:"command" json:"command,omitempty"` // 执行的命令,包括参数
Output string `bson:"output" json:"output,omitempty"` // 任务输出的所有内容
Success bool `bson:"success" json:"success"` // 是否执行成功
@ -98,6 +99,7 @@ func CreateJobLog(j *Job, t time.Time, rs string, success bool) {
Node: j.runOn,
Hostname: j.hostname,
IP: j.ip,
Command: j.Command,
Output: rs,
@ -126,7 +128,7 @@ func CreateJobLog(j *Job, t time.Time, rs string, success bool) {
JobLog: jl,
}
latestLog.Id = ""
if err := mgoDB.Upsert(Coll_JobLatestLog, bson.M{"node": jl.Node, "hostname": jl.Hostname, "jobId": jl.JobId, "jobGroup": jl.JobGroup}, latestLog); err != nil {
if err := mgoDB.Upsert(Coll_JobLatestLog, bson.M{"node": jl.Node, "hostname": jl.Hostname, "ip": jl.IP, "jobId": jl.JobId, "jobGroup": jl.JobGroup}, latestLog); err != nil {
log.Errorf(err.Error())
}

View File

@ -149,7 +149,7 @@ func (n *Node) loadJobs() (err error) {
}
for _, job := range jobs {
job.Init(n.ID, n.Hostname)
job.Init(n.ID, n.Hostname, n.IP)
n.addJob(job, false)
}
@ -338,7 +338,7 @@ func (n *Node) groupAddNode(g *cronsun.Group) {
continue
}
job.Init(n.ID, n.Hostname)
job.Init(n.ID, n.Hostname, n.IP)
}
cmds := job.Cmds(n.ID, n.groups)
@ -394,7 +394,7 @@ func (n *Node) watchJobs() {
continue
}
job.Init(n.ID, n.Hostname)
job.Init(n.ID, n.Hostname, n.IP)
n.addJob(job, true)
case ev.IsModify():
job, err := cronsun.GetJobFromKv(ev.Kv.Key, ev.Kv.Value)
@ -403,7 +403,7 @@ func (n *Node) watchJobs() {
continue
}
job.Init(n.ID, n.Hostname)
job.Init(n.ID, n.Hostname, n.IP)
n.modJob(job)
case ev.Type == client.EventTypeDelete:
n.delJob(cronsun.GetIDFromKey(string(ev.Kv.Key)))

View File

@ -5,7 +5,7 @@ import (
"runtime"
)
const VersionNumber = "0.3.0"
const VersionNumber = "0.3.1"
var (
Version = fmt.Sprintf("v%s (build %s)", VersionNumber, runtime.Version())

View File

@ -15,9 +15,17 @@ import (
func EnsureJobLogIndex() {
cronsun.GetDb().WithC(cronsun.Coll_JobLog, func(c *mgo.Collection) error {
return c.EnsureIndex(mgo.Index{
c.EnsureIndex(mgo.Index{
Key: []string{"beginTime"},
})
c.EnsureIndex(mgo.Index{
Key: []string{"hostname"},
})
c.EnsureIndex(mgo.Index{
Key: []string{"ip"},
})
return nil
})
}
@ -50,8 +58,21 @@ func (jl *JobLog) GetDetail(ctx *Context) {
outJSON(ctx.W, logDetail)
}
func searchText(field string, keywords []string) (q []bson.M) {
for _, k := range keywords {
k = strings.TrimSpace(k)
if len(k) == 0 {
continue
}
q = append(q, bson.M{field: bson.M{"$regex": bson.RegEx{Pattern: k, Options: "i"}}})
}
return q
}
func (jl *JobLog) GetList(ctx *Context) {
hostnames := getStringArrayFromQuery("hostnames", ",", ctx.R)
ips := getStringArrayFromQuery("ips", ",", ctx.R)
names := getStringArrayFromQuery("names", ",", ctx.R)
ids := getStringArrayFromQuery("ids", ",", ctx.R)
begin := getTime(ctx.R.FormValue("begin"))
@ -62,26 +83,18 @@ func (jl *JobLog) GetList(ctx *Context) {
orderBy := "-beginTime"
query := bson.M{}
if len(hostnames) > 0 {
query["hostname"] = bson.M{"$in": hostnames}
var textSearch = make([]bson.M, 0, 2)
textSearch = append(textSearch, searchText("hostname", hostnames)...)
textSearch = append(textSearch, searchText("name", names)...)
if len(ips) > 0 {
query["ip"] = bson.M{"$in": ips}
}
if len(ids) > 0 {
query["jobId"] = bson.M{"$in": ids}
}
if len(names) > 0 {
var search []bson.M
for _, k := range names {
k = strings.TrimSpace(k)
if len(k) == 0 {
continue
}
search = append(search, bson.M{"name": bson.M{"$regex": bson.RegEx{Pattern: k, Options: "i"}}})
}
query["$or"] = search
}
if !begin.IsZero() {
query["beginTime"] = bson.M{"$gte": begin}
}
@ -93,6 +106,10 @@ func (jl *JobLog) GetList(ctx *Context) {
query["success"] = false
}
if len(textSearch) > 0 {
query["$or"] = textSearch
}
var pager struct {
Total int `json:"total"`
List []*cronsun.JobLog `json:"list"`

File diff suppressed because one or more lines are too long

View File

@ -61,7 +61,7 @@
<div id="app">
<div id="initloader"></div>
</div>
<script src="build.js?v=36c2198"></script>
<script src="build.js?v=75b49bd"></script>
</body>
</html>

View File

@ -1,3 +1,11 @@
<style scoped>
.ui.vertical.menu h4 {
background: rgba(0,0,0,.05);
margin-bottom: 0px;
margin-top: 0px;
}
</style>
<template>
<div id="app">
<div class="ui blue inverted menu fixed">
@ -7,25 +15,35 @@
<router-link v-if="shouldOpen" class="item" to="/job" v-bind:class="{active: this.$route.path.indexOf('/job') === 0}"><i class="calendar icon"></i> {{$L('job')}}</router-link>
<router-link v-if="shouldOpen" class="item" to="/node" v-bind:class="{active: this.$route.path.indexOf('/node') === 0}"><i class="server icon"></i> {{$L('node')}}</router-link>
<router-link v-if="$store.getters.enabledAuth && $store.getters.role === 1" class="item" to="/admin/account/list" v-bind:class="{active: this.$route.path.indexOf('/admin/account') === 0}"><i class="user icon"></i> {{$L('account')}}</router-link>
<a class="item" href="https://github.com/shunfei/cronsun/wiki" target="_blank"><i class="external alternate icon"></i> Docs</a>
<div class="right menu">
<router-link to="/user/setpwd" class="item" v-if="this.$store.getters.email"><i class="user icon"></i> {{this.$store.getters.email}}</router-link>
<a class="item" v-if="this.$store.getters.email" href="#" v-on:click="logout"><i class="sign out icon"></i></a>
<div ref="langSelection" class="ui right icon dropdown item">
<i class="world icon" style="margin-left:-1px; margin-right: 8px;"></i>
<span class="text">Language</span>
<i class="dropdown icon"></i>
<div class="menu">
<div class="item" v-for="lang in $Lang.supported" :data-value="lang.code">{{lang.name}}</div>
</div>
</div>
<a class="item" href="#" @click.prevent="toggleSetting"><i class="settings icon"></i></a>
</div>
</div>
<div style="height: 55px;"></div>
<div class="ui container">
<div class="ui container pusher">
<router-view></router-view>
</div>
<div ref="sidebar" class="ui sidebar right vertical menu">
<h4 class="item">{{$store.getters.version}}</h4>
<h4 class="item">Language</h4>
<div class="item">
<div class="menu">
<a class="item" :class="{active: locale === lang.code}" href="#" v-for="lang in $Lang.supported" @click.prevent="selectLanguage(lang.code)">{{lang.name}}</a>
</div>
</div>
<h4 class="item">{{$L('node show as')}}</h4>
<div class="item">
<div class="menu">
<a class="item" :class="{active: hostshow === h.value}" href="#" v-for="h in hostshowsList" @click.prevent="selectHostshows(h.value)">{{h.name}}</a>
</div>
</div>
</div>
<Messager/>
</div>
</template>
@ -39,21 +57,25 @@ export default {
name: 'app',
store,
mounted: function(){
$(this.$refs.langSelection).dropdown({
onChange: function(value, text){
var old = window.$.cookie('locale');
if (old !== value) {
window.$.cookie('locale', value)
window.location.reload()
data: function() {
return {
hostshowsList: [{name: this.$L('hostname'), value: 'hostname'}, {name: 'IP', value: 'ip'}],
locale: ''
}
}
});
},
created: function() {
this.locale = window.$.cookie('locale');
this.$store.commit('setShowWithHostname', window.$.cookie('hostshows') === 'hostname');
},
computed: {
shouldOpen() {
return !this.$store.getters.enabledAuth || (this.$store.getters.enabledAuth && this.$store.getters.email)
},
hostshow() {
return this.$store.getters.showWithHostname ? 'hostname' : 'ip';
}
},
@ -67,6 +89,28 @@ export default {
vm.$router.push('/login');
}).
do();
},
toggleSetting() {
$(this.$refs.sidebar).sidebar('toggle');
},
selectLanguage(code) {
if (this.locale !== code) {
window.$.cookie('locale', code);
window.location.reload();
return;
}
this.toggleSetting();
},
selectHostshows(v) {
if (this.hostshow !== v) {
window.$.cookie('hostshows', v);
window.location.reload();
return;
}
this.toggleSetting();
}
},

View File

@ -28,8 +28,6 @@ export default {
}
},
mounted(){},
methods: {
show(jobName, jobGroup, jobId){
this.jobName = jobName;
@ -53,7 +51,7 @@ export default {
onsucceed(200, (resp)=>{
var nodes = [{value: 'all nodes', name: vm.$L('all nodes')}];
for (var i in resp) {
nodes.push({value: resp[i], name: vm.$store.getters.getHostnameByID(resp[i])})
nodes.push({value: resp[i], name: vm.$store.getters.hostshows(resp[i])})
}
vm.nodes = nodes;
}).

View File

@ -183,7 +183,7 @@ export default {
},
formatLatest: function(latest){
return this.$L('on {node} took {times}, {begin ~ end}', latest.hostname, formatDuration(latest.beginTime, latest.endTime), formatTime(latest.beginTime, latest.endTime));
return this.$L('on {node} took {times}, {begin ~ end}', this.$store.getters.hostshowsWithoutTip(latest.node), formatDuration(latest.beginTime, latest.endTime), formatTime(latest.beginTime, latest.endTime));
},
showExecuteJobModal: function(jobName, jobGroup, jobId){

View File

@ -19,7 +19,7 @@
</div>
<div class="field">
<label>{{$L('select nodes')}}</label>
<Dropdown :title="$L('select nodes')" v-bind:items="prefetchs.nodes" v-on:change="changeNodes" :selected="nodes" :multiple="true"/>
<Dropdown :title="$L('select nodes')" v-bind:items="$store.getters.dropdownNodes" v-on:change="changeNodes" :selected="nodes" :multiple="true"/>
</div>
<div class="field">
<button class="fluid ui button" type="button" v-on:click="submit">{{$L('submit query')}}</button>
@ -39,7 +39,7 @@
<tr v-for="(proc, index) in executings">
<td class="center aligned"><router-link :to="'/job/edit/'+proc.group+'/'+proc.jobId">{{proc.jobId}}</router-link></td>
<td class="center aligned">{{proc.group}}</td>
<td class="center aligned">{{$store.getters.getHostnameByID(proc.nodeId)}}</td>
<td class="center aligned">{{$store.getters.hostshows(proc.nodeId)}}</td>
<td class="center aligned">{{proc.id}}</td>
<td class="center aligned">{{proc.time}}</td>
</tr>
@ -56,7 +56,7 @@ export default {
name: 'job-executing',
data(){
return {
prefetchs: {groups: [], nodes: []},
prefetchs: {groups: []},
loading: false,
groups: [],
ids: '',
@ -76,12 +76,6 @@ export default {
vm.prefetchs.groups = resp;
this.fetchList(this.buildQuery());
}).do();
this.$rest.GET('nodes').onsucceed(200, (resp)=>{
for (var i in resp) {
vm.prefetchs.nodes.push(resp[i].id);
}
}).do();
},
watch: {

View File

@ -1,6 +1,6 @@
<template>
<div>
<form class="ui form" method="GET" v-bind:class="{loading:loading}" v-on:submit.prevent>
<form class="ui form" method="GET" v-bind:class="{loading:loading}" v-on:submit.prevent v-on:keyup.enter="submit">
<div class="two fields">
<div class="field">
<label>{{$L('job name')}}</label>
@ -13,7 +13,8 @@
</div>
<div class="field">
<label>{{$L('node')}}</label>
<input type="text" v-model="hostnames" :placeholder="$L('multiple Hostnames can separated by commas')">
<input v-if="$store.getters.showWithHostname" type="text" v-model="hostnames" :placeholder="$L('multiple Hostnames can separated by commas')">
<input v-else type="text" v-model="ips" :placeholder="$L('multiple IPs can separated by commas')">
</div>
<div class="two fields">
<div class="field">
@ -56,7 +57,7 @@
<tbody>
<tr v-for="log in list">
<td><router-link class="item" :to="'/job/edit/'+log.jobGroup+'/'+log.jobId">{{log.name}}</router-link></td>
<td :title="log.node">{{$store.getters.getHostnameByID(log.node)}}</td>
<td :title="log.node">{{$store.getters.hostshows(log.node)}}</td>
<td>{{log.user}}</td>
<td :class="{warning: durationAttention(log.beginTime, log.endTime)}"><i class="attention icon" v-if="durationAttention(log.beginTime, log.endTime)"></i> {{formatTime(log)}}</td>
<td :class="{error: !log.success}">
@ -84,6 +85,7 @@ export default {
names: '',
ids: '',
hostnames: '',
ips: '',
begin: '',
end: '',
latest: false,
@ -115,6 +117,7 @@ export default {
this.names = this.$route.query.names || '';
this.ids = this.$route.query.ids || '';
this.hostnames = this.$route.query.hostnames || '';
this.ips = this.$route.query.ips || '';
this.begin = this.$route.query.begin || '';
this.end = this.$route.query.end || '';
this.page = this.$route.query.page || 1;
@ -138,8 +141,9 @@ export default {
buildQuery(){
var params = [];
if (this.names) params.push('names='+this.names);
if (this.ids) params.push('ids='+this.ids);
if (this.hostnames) params.push('hostnames='+this.hostnames);
if (!this.$store.getters.showWithHostname && this.ids) params.push('ids='+this.ids);
if (this.$store.getters.showWithHostname && this.hostnames) params.push('hostnames='+this.hostnames);
if (this.ips) params.push('ips='+this.ips);
if (this.begin) params.push('begin='+this.begin);
if (this.end) params.push('end='+this.end);
if (this.failedOnly) params.push('failedOnly=true');

View File

@ -41,7 +41,7 @@
<div v-for="(node, nodeIndex) in group.nodes" class="node" v-bind:title="node.title">
<router-link class="item" :to="'/job?node='+node.id">
<i class="red icon fork" v-if="node.version !== version" :title="$L('version inconsistent, node: {version}', node.version)"></i>
{{node.hostname || node.id+"(need to upgrade)"}}
{{$store.getters.hostshows(node.id)}}
</router-link>
<i v-if="groupIndex != 2" v-on:click="removeConfirm(groupIndex, nodeIndex, node.id)" class="icon remove"></i>
</div>
@ -59,16 +59,12 @@ export default {
{nodes: [], name: 'node offline', title: 'node is in maintenance or is shutdown manually', css:''},
{nodes: [], name: 'node normaly', title: 'node is running', css:'green'}
],
count: 0,
version: ''
count: 0
}
},
mounted: function(){
var vm = this;
this.$rest.GET('version').onsucceed(200, (resp)=>{
vm.version = resp;
}).do();
var nodes = this.$store.getters.nodes;
for (var id in nodes) {
@ -86,6 +82,12 @@ export default {
vm.count = Object.keys(nodes).length;
},
computed: {
version: function(){
return this.$store.getters.version;
}
},
methods: {
removeConfirm: function(groupIndex, nodeIndex, nodeId){
if (!confirm(this.$L('are you sure to remove the node {nodeId}?', nodeId))) return;

View File

@ -31,7 +31,7 @@
<div class="description">
<div class="ui middle large aligned divided list">
<div class="item" v-for="nodeID in g.nids">
<span v-if="nodes[nodeID]">{{nodes[nodeID].hostname || nodes[nodeID].id}}
<span v-if="nodes[nodeID]">{{$store.getters.hostshows(nodeID)}}
<i class="arrow circle up icon red" v-if="nodes[nodeID].hostname == ''"></i>
<i v-if="nodes[nodeID].hostname == ''">(need to upgrade)</i>
</span>

View File

@ -24,6 +24,8 @@ var language = {
'total number of executeds': 'Total executeds',
'total number of nodes': 'Total nodes',
'job executed in past 7 days': 'Job executed in past 7 days',
'node show as': 'Node show as',
'hostname': 'Hostname',
'batch': 'Batch',
'job name': 'Job name',
@ -31,6 +33,7 @@ var language = {
'job ID': 'Job ID',
'multiple IDs can separated by commas': 'Multiple IDs can separated by commas',
'multiple Hostnames can separated by commas': 'Multiple Hostnames can separated by commas',
'multiple IPs can separated by commas': 'Multiple IPs can separated by commas',
'starting date': 'Starting date',
'end date': 'End date',
'failure only': 'Failure only',

View File

@ -24,6 +24,8 @@ var language = {
'total number of executeds': '执行任务总次数',
'total number of nodes': '节点总数',
'job executed in past 7 days': '过去 7 天任务统计',
'node show as': '节点显示为',
'hostname': '主机名称',
'batch': '批量',
'job name': '任务名称',
@ -31,6 +33,7 @@ var language = {
'job ID': '任务 ID',
'multiple IDs can separated by commas': '多个 ID 用英文逗号分隔',
'multiple Hostnames can separated by commas': '多个主机名称用英文逗号分隔',
'multiple IPs can separated by commas': '多个 IP 用英文逗号分隔',
'starting date': '起始日期',
'end date': '截至日期',
'failure only': '只看失败的任务',

View File

@ -124,6 +124,10 @@ var initConf = new Promise((resolve) => {
store.commit('setEmail', resp.email);
store.commit('setRole', resp.role);
restApi.GET('version').onsucceed(200, (resp)=>{
store.commit('setVersion', resp);
}).do();
restApi.GET('configurations').
onsucceed(200, (resp) => {
Vue.use((Vue) => Vue.prototype.$appConfig = resp);

View File

@ -5,16 +5,21 @@ Vue.use(Vuex);
const store = new Vuex.Store({
state: {
version: '',
enabledAuth: false,
user: {
email: '',
role: 0
},
nodes: {},
dropdownNodes: []
showWithHostname: false
},
getters: {
version: function (state) {
return state.version;
},
email: function (state) {
return state.user.email;
},
@ -31,11 +36,16 @@ const store = new Vuex.Store({
return state.nodes;
},
getHostnameByID: function (state) {
return (id) => {
if (!state.nodes[id]) return id + '(node not found)';
return state.nodes[id].hostname || id + '(need to upgrade)';
}
showWithHostname: function (state) {
return state.showWithHostname;
},
hostshows: function (state) {
return (id) => _hostshows(id, state, true);
},
hostshowsWithoutTip: function (state) {
return (id) => _hostshows(id, state, false);
},
getNodeByID: function (state) {
@ -45,11 +55,23 @@ const store = new Vuex.Store({
},
dropdownNodes: function (state) {
return state.dropdownNodes;
var dn = [];
var nodes = state.nodes;
for (var i in nodes) {
dn.push({
value: nodes[i].id,
name: _hostshows(nodes[i].id, state, true)
});
}
return dn;
}
},
mutations: {
setVersion: function (state, v) {
state.version = v;
},
setEmail: function (state, email) {
state.user.email = email;
},
@ -64,13 +86,26 @@ const store = new Vuex.Store({
setNodes: function (state, nodes) {
state.nodes = nodes;
var dn = []
for (var i in nodes) {
dn.push({value: nodes[i].id, name: nodes[i].hostname || nodes[i].id + '(need to upgrade)'})
}
state.dropdownNodes = dn;
},
setShowWithHostname: function (state, b) {
state.showWithHostname = b;
}
}
})
function _hostshows(id, state, tip) {
if (!state.nodes[id]) {
if (tip) id += '(node not found)';
return id;
}
var show = state.showWithHostname ? state.nodes[id].hostname : state.nodes[id].ip;
if (!show) {
show = id
if (tip) show += '(need to upgrade)';
}
return show;
}
export default store