add ui for cron
This commit is contained in:
parent
8f7171abc1
commit
6fb7d88306
@ -14,13 +14,13 @@ cron:
|
||||
- spec: 30 * * * *
|
||||
name: named schedule
|
||||
# each hour with custom payload and headers
|
||||
- spec: @hourly
|
||||
- spec: "@hourly"
|
||||
content: |
|
||||
hello world
|
||||
headers:
|
||||
X-Some-Header: test-header
|
||||
# each day with content from file
|
||||
- spec: @daily
|
||||
- spec: "@daily"
|
||||
content_file: /path/to/content
|
||||
```
|
||||
|
||||
|
@ -60,6 +60,8 @@ type CronEntry struct {
|
||||
ID cron.EntryID
|
||||
}
|
||||
|
||||
func (ce *CronEntry) Unit() Unit { return ce.Config }
|
||||
|
||||
// Cron initializes cron engine and registers all required worker schedules to it.
|
||||
func Cron(workers []*worker.Worker, configs []Unit) ([]*CronEntry, *cron.Cron, error) {
|
||||
engine := cron.New()
|
||||
|
File diff suppressed because one or more lines are too long
@ -109,7 +109,7 @@ func (cfg Config) Create(global context.Context) (*Server, error) {
|
||||
gctx.Request = gctx.Request.WithContext(global)
|
||||
gctx.Next()
|
||||
})
|
||||
cfg.installUI(router, units, workers)
|
||||
cfg.installUI(router, units, workers, cronEntries)
|
||||
server.Attach(router.Group("/api/"), units, workers)
|
||||
|
||||
srv := &Server{
|
||||
@ -161,7 +161,7 @@ func (cfg Config) Run(global context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (cfg Config) installUI(router *gin.Engine, units []server.Unit, workers []*worker.Worker) {
|
||||
func (cfg Config) installUI(router *gin.Engine, units []server.Unit, workers []*worker.Worker, cronEntries []*server.CronEntry) {
|
||||
if cfg.DisableUI {
|
||||
log.Println("ui disabled")
|
||||
return
|
||||
@ -177,7 +177,7 @@ func (cfg Config) installUI(router *gin.Engine, units []server.Unit, workers []*
|
||||
router.GET("/", func(gctx *gin.Context) {
|
||||
gctx.Redirect(http.StatusTemporaryRedirect, "ui")
|
||||
})
|
||||
ui.Attach(uiGroup, units, workers, cfg.Auth)
|
||||
ui.Attach(uiGroup, units, workers, cronEntries, cfg.Auth)
|
||||
}
|
||||
|
||||
func (cfg Config) useDirectoryUI(router *gin.Engine, uiGroup gin.IRouter) {
|
||||
|
@ -6,6 +6,8 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/sprig"
|
||||
@ -16,25 +18,38 @@ import (
|
||||
"nano-run/worker"
|
||||
)
|
||||
|
||||
func Expose(units []server.Unit, workers []*worker.Worker, uiDir string, auth Authorization) http.Handler {
|
||||
func Expose(units []server.Unit, workers []*worker.Worker, cronEntries []*server.CronEntry, uiDir string, auth Authorization) http.Handler {
|
||||
router := gin.New()
|
||||
router.SetFuncMap(sprig.HtmlFuncMap())
|
||||
router.LoadHTMLGlob(filepath.Join(uiDir, "*.html"))
|
||||
Attach(router, units, workers, auth)
|
||||
Attach(router, units, workers, cronEntries, auth)
|
||||
return router
|
||||
}
|
||||
|
||||
func Attach(router gin.IRouter, units []server.Unit, workers []*worker.Worker, auth Authorization) {
|
||||
func Attach(router gin.IRouter, units []server.Unit, workers []*worker.Worker, cronEntries []*server.CronEntry, auth Authorization) {
|
||||
ui := &uiRouter{
|
||||
units: make(map[string]unitInfo),
|
||||
}
|
||||
|
||||
var offset int
|
||||
for i := range units {
|
||||
u := units[i]
|
||||
w := workers[i]
|
||||
var ce []*server.CronEntry
|
||||
|
||||
var last int
|
||||
for last = offset; last < len(cronEntries); last++ {
|
||||
if cronEntries[last].Worker != w {
|
||||
break
|
||||
}
|
||||
}
|
||||
ce = cronEntries[offset:last]
|
||||
offset = last
|
||||
|
||||
ui.units[u.Name()] = unitInfo{
|
||||
Unit: u,
|
||||
Worker: w,
|
||||
Unit: u,
|
||||
Worker: w,
|
||||
CronEntries: ce,
|
||||
}
|
||||
}
|
||||
sessions := &memorySessions{}
|
||||
@ -49,20 +64,26 @@ func Attach(router gin.IRouter, units []server.Unit, workers []*worker.Worker, a
|
||||
})
|
||||
auth.attach(router.Group("/auth"), "login.html", sessions)
|
||||
|
||||
restricted := router.Group("/unit/").Use(auth.restrict(func(gctx *gin.Context) string {
|
||||
guard := auth.restrict(func(gctx *gin.Context) string {
|
||||
b := base(gctx)
|
||||
return b.Rel("/auth/")
|
||||
}, sessions))
|
||||
}, sessions)
|
||||
|
||||
restricted.GET("/", ui.listUnits)
|
||||
restricted.GET("/:name/", ui.unitInfo)
|
||||
restricted.POST("/:name/", ui.unitInvoke)
|
||||
restricted.GET("/:name/history", ui.unitHistory)
|
||||
restricted.GET("/:name/request/:request/", ui.unitRequestInfo)
|
||||
restricted.POST("/:name/request/:request/", ui.unitRequestRetry)
|
||||
restricted.GET("/:name/request/:request/payload", ui.unitRequestPayload)
|
||||
restricted.GET("/:name/request/:request/attempt/:attempt/", ui.unitRequestAttemptInfo)
|
||||
restricted.GET("/:name/request/:request/attempt/:attempt/result", ui.unitRequestAttemptResult)
|
||||
unitsRoutes := router.Group("/unit/").Use(guard)
|
||||
|
||||
unitsRoutes.GET("/", ui.listUnits)
|
||||
unitsRoutes.GET("/:name/", ui.unitInfo)
|
||||
unitsRoutes.POST("/:name/", ui.unitInvoke)
|
||||
unitsRoutes.GET("/:name/history", ui.unitHistory)
|
||||
unitsRoutes.GET("/:name/request/:request/", ui.unitRequestInfo)
|
||||
unitsRoutes.POST("/:name/request/:request/", ui.unitRequestRetry)
|
||||
unitsRoutes.GET("/:name/request/:request/payload", ui.unitRequestPayload)
|
||||
unitsRoutes.GET("/:name/request/:request/attempt/:attempt/", ui.unitRequestAttemptInfo)
|
||||
unitsRoutes.GET("/:name/request/:request/attempt/:attempt/result", ui.unitRequestAttemptResult)
|
||||
unitsRoutes.GET("/:name/cron/:index", ui.unitCronInfo)
|
||||
|
||||
cronRoutes := router.Group("/cron/").Use(guard)
|
||||
cronRoutes.GET("/", ui.listCron)
|
||||
}
|
||||
|
||||
type uiRouter struct {
|
||||
@ -271,6 +292,39 @@ func (ui *uiRouter) listUnits(gctx *gin.Context) {
|
||||
gctx.HTML(http.StatusOK, "units-list.html", reply)
|
||||
}
|
||||
|
||||
func (ui *uiRouter) listCron(gctx *gin.Context) {
|
||||
type uiEntry struct {
|
||||
Index int
|
||||
Entry *server.CronEntry
|
||||
}
|
||||
|
||||
var reply struct {
|
||||
baseResponse
|
||||
Entries []uiEntry
|
||||
}
|
||||
|
||||
var specs = make([]uiEntry, 0, len(ui.units))
|
||||
for _, info := range ui.units {
|
||||
for i, spec := range info.CronEntries {
|
||||
specs = append(specs, uiEntry{
|
||||
Index: i,
|
||||
Entry: spec,
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(specs, func(i, j int) bool {
|
||||
if specs[i].Entry.Config.Name() < specs[j].Entry.Config.Name() {
|
||||
return true
|
||||
} else if specs[i].Entry.Config.Name() == specs[j].Entry.Config.Name() && specs[i].Index < specs[j].Index {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
reply.baseResponse = base(gctx)
|
||||
reply.Entries = specs
|
||||
gctx.HTML(http.StatusOK, "cron-list.html", reply)
|
||||
}
|
||||
|
||||
func (ui *uiRouter) unitHistory(gctx *gin.Context) {
|
||||
type viewUnit struct {
|
||||
unitInfo
|
||||
@ -287,3 +341,33 @@ func (ui *uiRouter) unitHistory(gctx *gin.Context) {
|
||||
baseResponse: base(gctx),
|
||||
})
|
||||
}
|
||||
|
||||
func (ui *uiRouter) unitCronInfo(gctx *gin.Context) {
|
||||
type viewUnit struct {
|
||||
unitInfo
|
||||
baseResponse
|
||||
Cron *server.CronEntry
|
||||
Label string
|
||||
}
|
||||
name := gctx.Param("name")
|
||||
info, ok := ui.units[name]
|
||||
if !ok {
|
||||
gctx.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
strIndex := gctx.Param("index")
|
||||
index, err := strconv.Atoi(strIndex)
|
||||
|
||||
if err != nil || index < 0 || index >= len(info.CronEntries) {
|
||||
gctx.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
entry := info.CronEntries[index]
|
||||
gctx.HTML(http.StatusOK, "unit-cron-info.html", viewUnit{
|
||||
unitInfo: info,
|
||||
baseResponse: base(gctx),
|
||||
Cron: entry,
|
||||
Label: entry.Spec.Label(strconv.Itoa(index)),
|
||||
})
|
||||
}
|
||||
|
@ -9,8 +9,9 @@ import (
|
||||
)
|
||||
|
||||
type unitInfo struct {
|
||||
Unit server.Unit
|
||||
Worker *worker.Worker
|
||||
Unit server.Unit
|
||||
Worker *worker.Worker
|
||||
CronEntries []*server.CronEntry
|
||||
}
|
||||
|
||||
type historyRecord struct {
|
||||
|
67
templates/cron-list.html
Normal file
67
templates/cron-list.html
Normal file
@ -0,0 +1,67 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-sm navbar-light bg-light">
|
||||
<a class="navbar-brand" href="{{.Rel "/"}}">Nano-Run</a>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/unit/"}}">All units</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="{{.Rel "/cron/"}}">All schedules</a>
|
||||
</li>
|
||||
{{if .Authorized}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/auth/logout"}}">Logout</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
<span class="navbar-text">{{.Login}}</span>
|
||||
</nav>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">All cron</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Unit</th>
|
||||
<th>Name</th>
|
||||
<th>Spec</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Entries}}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{$.Rel "/unit" .Entry.Unit.Name}}">{{.Entry.Unit.Name}}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{$.Rel "/unit" .Entry.Unit.Name "cron" (print .Index)}}">{{.Entry.Spec.Label .Entry.Name}}</a>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{.Entry.Spec.Spec}}</code>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<link rel="stylesheet" href="{{.Rel "/static" "css" "bootstrap-material-design.min.css"}}">
|
||||
</body>
|
||||
</html>
|
92
templates/unit-cron-info.html
Normal file
92
templates/unit-cron-info.html
Normal file
@ -0,0 +1,92 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-sm navbar-light bg-light">
|
||||
<a class="navbar-brand" href="{{.Rel "/"}}">Nano-Run</a>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/unit/"}}">All units</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/cron/"}}">All schedules</a>
|
||||
</li>
|
||||
{{if .Authorized}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/auth/logout"}}">Logout</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
<span class="navbar-text">{{.Login}}</span>
|
||||
</nav>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{.Rel "/unit/"}}" title="all units">units</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{.Rel "/unit" .Unit.Name}}" title="unit">{{.Unit.Name}}</a></li>
|
||||
<li class="breadcrumb-item active" title="cron">{{.Label}}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Scheduled job {{.Label}}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">Configuration</h6>
|
||||
<div class="card-text">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">Spec</dt>
|
||||
<dd class="col-sm-9">
|
||||
<a href="https://crontab.guru/#{{.Cron.Spec.Spec}}" target="_blank"><code>{{.Cron.Spec.Spec}}</code></a>
|
||||
</dd>
|
||||
{{if .Cron.Spec.Content}}
|
||||
<dt class="col-sm-3">Content</dt>
|
||||
<dd class="col-sm-9"><pre>{{.Cron.Spec.Content}}</pre></dd>
|
||||
{{else if .Cron.Spec.ContentFile}}
|
||||
<dt class="col-sm-3">Content file</dt>
|
||||
<dd class="col-sm-9">{{.Cron.Spec.ContentFile}}</dd>
|
||||
{{end}}
|
||||
</dl>
|
||||
</div>
|
||||
{{if .Cron.Spec.Headers}}
|
||||
<h6 class="card-subtitle mb-2 text-muted">Headers</h6>
|
||||
<div class="card-text">
|
||||
{{with .Cron.Spec.Headers}}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $k,$v := .}}
|
||||
<tr>
|
||||
<td>
|
||||
<pre>{{$k}}</pre>
|
||||
</td>
|
||||
<td>{{$v}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{else}}
|
||||
no custom variables defined
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<link rel="stylesheet" href="{{.Rel "/static" "css" "bootstrap-material-design.min.css"}}">
|
||||
|
||||
</body>
|
||||
</html>
|
@ -12,6 +12,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/unit/"}}">All units</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/cron/"}}">All schedules</a>
|
||||
</li>
|
||||
{{if .Authorized}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/auth/logout"}}">Logout</a>
|
||||
@ -162,6 +165,45 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{with .CronEntries}}
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<form method="post">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Schedules</h5>
|
||||
<div class="card-text">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Spec</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $index, $entry := .}}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="cron/{{$index}}">{{$entry.Spec.Label (print "#" $index)}}</a>
|
||||
</td>
|
||||
<td>
|
||||
<code>{{$entry.Spec.Spec}}</code>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
|
@ -12,6 +12,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/unit/"}}">All units</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/cron/"}}">All schedules</a>
|
||||
</li>
|
||||
{{if .Authorized}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/auth/logout"}}">Logout</a>
|
||||
|
@ -12,6 +12,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/unit/"}}">All units</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/cron/"}}">All schedules</a>
|
||||
</li>
|
||||
{{if .Authorized}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/auth/logout"}}">Logout</a>
|
||||
|
@ -13,6 +13,9 @@
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="{{.Rel "/unit/"}}">All units</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/cron/"}}">All schedules</a>
|
||||
</li>
|
||||
{{if .Authorized}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{.Rel "/auth/logout"}}">Logout</a>
|
||||
|
Loading…
Reference in New Issue
Block a user