add ui for cron

This commit is contained in:
Alexander Baryshnikov 2020-11-07 23:04:40 +08:00
parent 8f7171abc1
commit 6fb7d88306
12 changed files with 370 additions and 27 deletions

View File

@ -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
```

View File

@ -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

View File

@ -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) {

View File

@ -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)),
})
}

View File

@ -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
View 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>

View 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>

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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>