mirror of https://github.com/shunfei/cronsun
修改任务列表也,日志详情页,添加分页 组件
parent
5991088eda
commit
c1d4a12628
|
@ -10,7 +10,8 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
Coll_JobLog = "job_log"
|
||||
Coll_JobLog = "job_log"
|
||||
Coll_JobLatestLog = "job_latest_log"
|
||||
)
|
||||
|
||||
// 任务执行记录
|
||||
|
@ -27,12 +28,17 @@ type JobLog struct {
|
|||
EndTime time.Time `bson:"endTime" json:"endTime"` // 任务执行完毕时间,精确到毫秒
|
||||
}
|
||||
|
||||
type JobLatestLog struct {
|
||||
JobLog `bson:",inline"`
|
||||
RefLogId string `bson:"refLogId,omitempty" json:"refLogId"`
|
||||
}
|
||||
|
||||
func GetJobLogById(id bson.ObjectId) (l *JobLog, err error) {
|
||||
err = mgoDB.FindId(Coll_JobLog, id, &l)
|
||||
return
|
||||
}
|
||||
|
||||
var projection = bson.M{"command": -1, "output": -1}
|
||||
var selectForJobLogList = bson.M{"command": 0, "output": 0}
|
||||
|
||||
func GetJobLogList(query bson.M, page, size int, sort string) (list []*JobLog, total int, err error) {
|
||||
err = mgoDB.WithC(Coll_JobLog, func(c *mgo.Collection) error {
|
||||
|
@ -40,11 +46,25 @@ func GetJobLogList(query bson.M, page, size int, sort string) (list []*JobLog, t
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Find(query).Select(projection).Sort(sort).Skip((page - 1) * size).Limit(size).All(&list)
|
||||
return c.Find(query).Select(selectForJobLogList).Sort(sort).Skip((page - 1) * size).Limit(size).All(&list)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func GetJobLatestLogListByJobIds(jobIds []string) (m map[string]*JobLatestLog, err error) {
|
||||
var list []*JobLatestLog
|
||||
err = mgoDB.AllSelect(Coll_JobLatestLog, bson.M{"jobId": bson.M{"$in": jobIds}}, selectForJobLogList, &list)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
m = make(map[string]*JobLatestLog, len(list))
|
||||
for i := range list {
|
||||
m[list[i].JobId] = list[i]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func CreateJobLog(j *Job, t time.Time, rs string, success bool) {
|
||||
jl := JobLog{
|
||||
Id: bson.NewObjectId(),
|
||||
|
@ -65,4 +85,13 @@ func CreateJobLog(j *Job, t time.Time, rs string, success bool) {
|
|||
if err := mgoDB.Insert(Coll_JobLog, jl); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
|
||||
latestLog := &JobLatestLog{
|
||||
RefLogId: jl.Id.Hex(),
|
||||
JobLog: jl,
|
||||
}
|
||||
latestLog.Id = ""
|
||||
if err := mgoDB.Upsert(Coll_JobLatestLog, bson.M{"jobId": jl.JobId, "jobGroup": jl.JobGroup}, latestLog); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
|
21
web/job.go
21
web/job.go
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/coreos/etcd/clientv3"
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"sunteng/commons/log"
|
||||
"sunteng/cronsun/conf"
|
||||
"sunteng/cronsun/models"
|
||||
)
|
||||
|
@ -138,6 +139,11 @@ func (j *Job) GetGroups(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (j *Job) GetListByGroupName(w http.ResponseWriter, r *http.Request) {
|
||||
type jobStatus struct {
|
||||
*models.Job
|
||||
LatestStatus *models.JobLatestLog `json:"latestStatus"`
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
resp, err := models.DefalutClient.Get(conf.Config.Cmd+vars["name"], clientv3.WithPrefix(), clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend))
|
||||
if err != nil {
|
||||
|
@ -145,7 +151,8 @@ func (j *Job) GetListByGroupName(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
var jobList = make([]*models.Job, 0, resp.Count)
|
||||
var jobIds []string
|
||||
var jobList = make([]*jobStatus, 0, resp.Count)
|
||||
for i := range resp.Kvs {
|
||||
job := models.Job{}
|
||||
err = json.Unmarshal(resp.Kvs[i].Value, &job)
|
||||
|
@ -153,7 +160,17 @@ func (j *Job) GetListByGroupName(w http.ResponseWriter, r *http.Request) {
|
|||
outJSONError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jobList = append(jobList, &job)
|
||||
jobList = append(jobList, &jobStatus{Job: &job})
|
||||
jobIds = append(jobIds, job.ID)
|
||||
}
|
||||
|
||||
m, err := models.GetJobLatestLogListByJobIds(jobIds)
|
||||
if err != nil {
|
||||
log.Error("GetJobLatestLogListByJobIds error:", err.Error())
|
||||
} else {
|
||||
for i := range jobList {
|
||||
jobList[i].LatestStatus = m[jobList[i].ID]
|
||||
}
|
||||
}
|
||||
|
||||
outJSON(w, jobList)
|
||||
|
|
|
@ -4,13 +4,13 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"gopkg.in/mgo.v2"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
|
||||
"sunteng/cronsun/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type JobLog struct{}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -17,6 +17,8 @@
|
|||
<th class="collapsing center aligned">状态</th>
|
||||
<th width="200px" class="center aligned">分组</th>
|
||||
<th class="center aligned">名称</th>
|
||||
<th class="center aligned">最近执行时间</th>
|
||||
<th class="center aligned">执行结果</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -36,6 +38,14 @@
|
|||
<td class="center aligned"><i class="icon" v-bind:class="{pause: job.pause, play: !job.pause, green: !job.pause}"></i></td>
|
||||
<td>{{job.group}}</td>
|
||||
<td>{{job.name}}</td>
|
||||
<td>
|
||||
<span v-if="!job.latestStatus">-</span>
|
||||
<span v-else>{{formatTime(job.latestStatus.beginTime, job.latestStatus.endTime)}},耗时 {{formatDuration(job.latestStatus.beginTime, job.latestStatus.endTime)}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="!job.latestStatus">-</span>
|
||||
<router-link v-else :to="'/log/'+job.latestStatus.refLogId">{{job.latestStatus.success ? '成功' : '失败'}}</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -45,6 +55,7 @@
|
|||
<script>
|
||||
import JobToolbar from './JobToolbar.vue';
|
||||
import Dropdown from './basic/Dropdown.vue';
|
||||
import Pager from './basic/Pager.vue';
|
||||
|
||||
export default {
|
||||
name: 'job',
|
||||
|
@ -93,12 +104,67 @@ export default {
|
|||
this.$rest.POST('job/'+group+'-'+id, {"pause": isPause}).onsucceed(200, (resp)=>{
|
||||
vm.refreshList();
|
||||
}).do();
|
||||
},
|
||||
|
||||
formatExecResult: function(st){
|
||||
if (!st) return '-';
|
||||
return
|
||||
},
|
||||
|
||||
formatDuration: function(beginTime, endTime){
|
||||
var d = new Date(endTime) - new Date(beginTime);
|
||||
var s = '';
|
||||
var day = d/86400000;
|
||||
if (day >= 1) s += day.toString() + ' 天 ';
|
||||
|
||||
d = d%86400000;
|
||||
var hour = d/3600000;
|
||||
if (hour >= 1) s += hour.toString() + ' 小时 ';
|
||||
|
||||
d = d%3600000;
|
||||
var min = d/60000;
|
||||
if (min >= 1) s += min.toString() + ' 分钟 ';
|
||||
|
||||
d = d%60000;
|
||||
var sec = d/1000;
|
||||
if (sec >= 1) s += sec.toString() + ' 秒 ';
|
||||
|
||||
d = d%1000;
|
||||
if (d >= 1) s = d.toString() + ' 毫秒';
|
||||
|
||||
return s;
|
||||
},
|
||||
|
||||
formatTime: function(beginTime, endTime){
|
||||
var now = new Date();
|
||||
var bt = new Date(beginTime);
|
||||
var et = new Date(endTime);
|
||||
var s = this._formatTime(now, bt) + ' ~ ' + this._formatTime(now, et);
|
||||
return s;
|
||||
},
|
||||
|
||||
_formatTime: function(now, t){
|
||||
var s = '';
|
||||
if (now.getFullYear() != t.getFullYear()) {
|
||||
s += t.getFullYear().toString() + '-';
|
||||
}
|
||||
s += this._formatNumber(t.getMonth()+1, 2).toString() + '-';
|
||||
s += this._formatNumber(t.getDate(), 2) + ' ' + this._formatNumber(t.getHours(), 2) + ':' + this._formatNumber(t.getMinutes(), 2) + ':' + this._formatNumber(t.getSeconds(), 2);
|
||||
return s;
|
||||
},
|
||||
|
||||
// i > 0
|
||||
_formatNumber: function(i, len){
|
||||
var n = Math.ceil(Math.log10(i+1));
|
||||
if (n >= len) return i.toString();
|
||||
return '0'.repeat(len-n) + i.toString();
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
JobToolbar,
|
||||
Dropdown
|
||||
Dropdown,
|
||||
Pager
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -3,7 +3,10 @@
|
|||
<h4 class="ui horizontal divider header">定时器 - {{index}} <a href="#" v-on:click.prevent="remove">删除</a></h4>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<input type="text" v-bind:value="rule.timer" v-on:input="change('timer', $event.target.value)" placeholder="定时 * * * * *(crontab 格式)"/>
|
||||
<div class="ui icon input">
|
||||
<input type="text" v-bind:value="rule.timer" v-on:input="change('timer', $event.target.value)" placeholder="定时 * 5 * * * *"/>
|
||||
<i ref="ruletip" class="large help circle link icon" data-position="top right" data-content="<秒> <分钟> <小时> <日> <月份> <星期>,规则与 crontab 一样" data-variation="wide"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<Dropdown title="节点分组" v-bind:items="nodeGroups" multiple="true"></Dropdown>
|
||||
|
@ -35,7 +38,11 @@ export default {
|
|||
|
||||
mounted: function(){
|
||||
var vm = this;
|
||||
this.$rest.GET('nodes').onsucceed(200, (resp)=>{vm.activityNodes = resp}).do();
|
||||
this.$rest.GET('nodes').onsucceed(200, (resp)=>{
|
||||
for (var i in resp) {
|
||||
vm.activityNodes.push(resp[i].id);
|
||||
}
|
||||
}).do();
|
||||
this.$rest.GET('nodes/groups').onsucceed(200, (resp)=>{
|
||||
var groups = [];
|
||||
for (var i in resp) {
|
||||
|
@ -43,6 +50,8 @@ export default {
|
|||
}
|
||||
vm.nodeGroups = groups;
|
||||
});
|
||||
|
||||
$(this.$refs.ruletip).popup();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
|
@ -54,15 +54,17 @@ export default {
|
|||
nodes: '',
|
||||
begin: '',
|
||||
end: '',
|
||||
list: []
|
||||
list: [],
|
||||
total: 0,
|
||||
currPage: 1
|
||||
}
|
||||
},
|
||||
|
||||
mounted: function(){
|
||||
this.names = this.$route.query.names;
|
||||
this.nodes = this.$route.query.nodes;
|
||||
this.begin = this.$route.query.begin;
|
||||
this.end = this.$route.query.end;
|
||||
this.names = this.$route.query.names || '';
|
||||
this.nodes = this.$route.query.nodes || '';
|
||||
this.begin = this.$route.query.begin || '';
|
||||
this.end = this.$route.query.end || '';
|
||||
|
||||
if (this.names || this.nodes || this.begin || this.end) this.submit();
|
||||
},
|
||||
|
@ -71,8 +73,12 @@ export default {
|
|||
submit: function(){
|
||||
this.loading = true;
|
||||
var vm = this;
|
||||
this.$rest.GET('logs?names='+this.name+'&nodes='+this.nodes+'&begin='+this.begin+'&end='+this.end
|
||||
)
|
||||
var params = [];
|
||||
if (this.name) params.push('names='+this.name);
|
||||
if (this.nodes) params.push('nodes='+this.nodes);
|
||||
if (this.begin) params.push('begin='+this.begin);
|
||||
if (this.end) params.push('end='+this.end);
|
||||
this.$rest.GET('logs?'+params.join('&'))
|
||||
.onsucceed(200, (resp)=>{vm.list = resp})
|
||||
.onfailed((resp)=>{console.log(resp)})
|
||||
.onend(()=>{vm.loading=false})
|
||||
|
@ -122,13 +128,13 @@ export default {
|
|||
s += t.getFullYear().toString() + '-';
|
||||
}
|
||||
s += this._formatNumber(t.getMonth()+1, 2).toString() + '-';
|
||||
s += this._formatNumber(t.getDate(), 2) + ' ' + this._formatNumber(t.getHours(), 2) + ':' + this._formatNumber(t.getMinutes());
|
||||
s += this._formatNumber(t.getDate(), 2) + ' ' + this._formatNumber(t.getHours(), 2) + ':' + this._formatNumber(t.getMinutes(), 2) + ':' + this._formatNumber(t.getSeconds(), 2);
|
||||
return s;
|
||||
},
|
||||
|
||||
// i > 0
|
||||
_formatNumber: function(i, len){
|
||||
var n = Math.ceil(Math.log10(i));
|
||||
var n = Math.ceil(Math.log10(i+1));
|
||||
if (n >= len) return i.toString();
|
||||
return '0'.repeat(len-n) + i.toString();
|
||||
}
|
||||
|
|
|
@ -13,6 +13,9 @@
|
|||
<div class="ui segment">
|
||||
<p>时间:{{log.beginTime}} 到 {{log.endTime}}</p>
|
||||
</div>
|
||||
<div class="ui segment">
|
||||
<p>结果:{{log.success ? '成功' : '失败'}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="ui header">执行的命令</h4>
|
||||
<pre class="ui grey inverted segment">{{log.command}}</pre>
|
||||
|
@ -45,7 +48,7 @@ export default {
|
|||
mounted: function(){
|
||||
var vm = this;
|
||||
this.$rest.GET('log/'+this.$route.params.id).
|
||||
onsucceed(200, (resp)=>{/*vm.log = resp*/}).
|
||||
onsucceed(200, (resp)=>{vm.log = resp}).
|
||||
onfailed((data)=>{vm.error = data.error}).
|
||||
do();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<div style="text-align: center;">
|
||||
<div class="ui icon buttons">
|
||||
<router-link :to="pageURL(startPage-1)" class="ui button" :class="{disabled: startPage<=1}"><i class="angle left icon"></i></router-link>
|
||||
<router-link :to="pageURL(startPage + n - 1)" v-for="n in pageBtnNum" class="ui button" :class="{blue: startPage+n-1 == _current}">{{startPage + n-1}}</router-link>
|
||||
<router-link :to="pageURL(startPage+length)" class="ui button" :class="{disabled: startPage+length>total}"><i class="angle right icon"></i></router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'pager',
|
||||
props: ['total', 'length', 'pageVar'],
|
||||
data: function(){
|
||||
return {
|
||||
_pagevar: '',
|
||||
_current: 1,
|
||||
}
|
||||
},
|
||||
created: function(){
|
||||
this._pagevar = this.pageVar || 'page';
|
||||
this._current = this.$route.query[this._pagevar] || 1;
|
||||
},
|
||||
|
||||
mounted: function(){
|
||||
console.log('mounted');
|
||||
},
|
||||
|
||||
methods: {
|
||||
pageURL: function(page){
|
||||
return this.url + this._pagevar + '=' + page;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route': function(){
|
||||
this._current = this.$route.query[this._pagevar] || 1;
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
pageBtnNum: function(){
|
||||
console.log('pageBtnNum');
|
||||
var remainingPage = this.total - this.startPage;
|
||||
return remainingPage <= this.length ? this.total - this.startPage + 1 : this.length;
|
||||
},
|
||||
|
||||
startPage: function(){
|
||||
console.log('startPage');
|
||||
return Math.floor((this._current-1)/this.length) * this.length+1;
|
||||
},
|
||||
|
||||
url: function(){
|
||||
console.log('url');
|
||||
var query = [];
|
||||
for (var k in this.$route.query) {
|
||||
if (this._pagevar === k) continue;
|
||||
query.push(k+'='+this.$route.query[k]);
|
||||
}
|
||||
|
||||
return this.$route.path+'?'+query.join('&') + '&';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
Loading…
Reference in New Issue