Reload job in ui

master
bel 2020-03-15 23:04:48 +00:00
parent 19d4b645b8
commit 8138e31e53
12 changed files with 275 additions and 93 deletions

24
TODO.md
View File

@ -9,29 +9,29 @@
x last output
x add titles to jobs
x job title includes last pass/fail icon
1. button to modify (copies to upsert form)
1. button to delete
x button to modify (copies to upsert form)
x button to delete
1. UI to mutate
1. submit job
1. delete job
1. pause jobs
x submit job
x delete job
x pause jobs
1. interrupt job
1. force run
1. JS
x JS
x ajax for json calls
# Backend
x load from file
1. interrupt running jobs
1. temporarily disable jobs
1. disable jobs
1. json API
1. list
1. last run output
1. last run pass/fail bool
1. last run timestamp
x last run output
x last run pass/fail bool
x last run timestamp
1. next run
1. upsert
x upsert
1. delete job
1. pause/disable job
1. running job
@ -41,4 +41,4 @@ x load from file
x add optional second for test main
1. test main
x add titles to jobs
1. namespace for jobs, output, lastrun, laststatus
x namespace for jobs, output, lastrun, laststatus

View File

@ -14,17 +14,14 @@ import (
)
func main() {
var err error
s := scheduler.New()
if config.Config != "" {
var err error
s, err = scheduler.NewFromFile(config.Config)
scheduler.Schedule, err = scheduler.NewFromFile(config.Config)
if err != nil {
panic(err)
}
}
err = s.Start()
if err != nil {
if err := scheduler.Schedule.Start(); err != nil {
panic(err)
}
@ -46,7 +43,7 @@ func main() {
stop := make(chan os.Signal)
signal.Notify(stop, os.Interrupt)
<-stop
s.Stop()
scheduler.Schedule.Stop()
}
func EnqueueBackups() {

View File

@ -10,11 +10,12 @@
<summary>Upsert Job</summary>
<form id="upsert" action="#" method="get" onsubmit="upsert(); return false;">
<input type="text" name="id" placeholder="id"/>
<input type="text" name="title" placeholder="title"/>
<label><input type="checkbox" name="disabled"/> Disabled</label>
<select name="language" required>
<option value="bash" selected>bash</option>
</select>
<input type="text" name="cron" placeholder="cron" value="@daily"/>
<input type="text" name="cron" placeholder="cron" value="@daily" required/>
<textarea name="script" placeholder="script" required rows="5"></textarea>
<button type="submit">Upsert</button>
</form>

View File

@ -14,7 +14,11 @@ function http(method, remote, callback, body) {
function upsert() {
function cb(body, status) {
console.log(status, body)
if (status == 200) {
init()
} else {
console.log("error upserting:", status, body)
}
}
http("POST", "/api/job/upsert", cb, jsonifyForm("upsert"))
}
@ -28,66 +32,74 @@ function jsonifyForm(id) {
}
function init() {
var table = document.getElementById("jobs").getElementsByTagName("tbody")[0]
function cb(body, status) {
var jobs = JSON.parse(body)
getJobsTable().innerHTML = ""
jobs.forEach(function(job) {
var s = format(job)
inject(s)
})
}
function format(job) {
var pause = "&#9208"
var passing = "9711"
if (job.last.status != 0) {
passing = "129314"
passing = "129324"
}
passing = `&#${passing};`
var buttons = ""
var btns = [
{"name":"disable", "icon":"11035"},
{"name":"enable", "icon":"9654"},
{"name":"modify", "icon":"9999"},
{"name":"delete", "icon":"10006"}
]
btns.forEach(function(e) {
buttons += `
<span>
<input type="button" onclick="job${e.name}(this);" value="&#${e.icon};" alt="${e.name}" title="${e.name}" job="${btoa(JSON.stringify(job))}"/>
</span>
`
})
return `<tr><td><details>
<summary name="${job.id}">
<span>${job.title}</span>
<span>${passing}</span>
${buttons}
</summary>
<table>
<tr><td>
<code>${job.cron}</code>
<code>${job.language}</code>
</td></tr>
<tr><td>
<code>${job.last.run}</code>
<code>${job.last.runtime}</code>
</td></tr>
<tr><td>
<code>${job.script}</code>
</td></tr>
<tr><td>
<code>${job.last.output}</code>
</td></tr>
</table>
</details></td></tr>`
}
function inject(s) {
var table = document.getElementById("jobs").getElementsByTagName("tbody")[0]
table.innerHTML += s
}
http("GET", "/api/job/list", cb, null)
}
function format(job) {
var pause = "&#9208"
var passing = "9711"
if (job.last.status != 0) {
passing = "129314"
passing = "129324"
}
passing = `&#${passing};`
var buttons = ""
var btns = [
{"name":"refresh", "icon":"8635"},
{"name":"disable", "icon":"11035"},
{"name":"enable", "icon":"9654"},
{"name":"modify", "icon":"9999"},
{"name":"delete", "icon":"10006"}
]
btns.forEach(function(e) {
buttons += `
<span>
<input type="button" onclick="job${e.name}(this);" value="&#${e.icon};" alt="${e.name}" title="${e.name}" job="${btoa(JSON.stringify(job))}"/>
</span>
`
})
return `<tr><td><details>
<summary name="${job.id}">
<span>${job.title}</span>
<span>${passing}</span>
${buttons}
</summary>
<table>
<tr><td>
<code>${job.cron}</code>
<code>${job.language}</code>
</td></tr>
<tr><td>
<code>${job.last.run}</code>
<code>${job.last.runtime}</code>
</td></tr>
<tr><td>
<code>${job.script}</code>
</td></tr>
<tr><td>
<code>${job.last.output}</code>
</td></tr>
</table>
</details></td></tr>`
}
function inject(s) {
getJobsTable().innerHTML += s
}
function getJobsTable() {
return document.getElementById("jobs").getElementsByTagName("tbody")[0]
}
init()
function jobdisable(input) {
@ -103,7 +115,7 @@ function jobenable(input) {
function jobmodify(input) {
var form = getForm()
var job = jobFromInput(input)
var fields = ["id", "language", "cron", "script", "disabled"]
var fields = ["id", "language", "cron", "script", "disabled", "title"]
fields.forEach(function(field) {
var e = getField(field)
e.checked = job[field]
@ -117,6 +129,31 @@ function jobdelete(input) {
http("DELETE", "/api/job/delete", cb, null)
}
function jobrefresh(input) {
var job = jobFromInput(input)
function cb(body, status) {
var table = getJobsTable()
var summaries = Array.from(table.getElementsByTagName("summary"))
summaries.forEach(function(e) {
if (e.getAttribute("name") == job.id) {
e = e.parentElement
var showing = false
if (e.getAttribute("open") != null) {
showing = true
}
e = e.parentElement.parentElement
var table = document.createElement("table")
table.innerHTML = format(JSON.parse(body))
if (showing) {
table.getElementsByTagName("details")[0].setAttribute("open", "")
}
e.innerHTML = table.innerHTML
}
})
}
http("GET", "/api/job/get/"+job.id, cb, null)
}
function jobFromInput(input) {
var b64 = input.getAttribute("job")
var json = atob(b64)

View File

@ -20,12 +20,12 @@ type Job struct {
Schedule string
Raw string
Runner Runner
Disabled bool
foo func()
LastStatus int
LastOutput string
LastRuntime time.Duration
LastRun time.Time
Disabled bool
}
func NewJob(runner Runner, schedule, raw string) (*Job, error) {
@ -59,7 +59,7 @@ func newBashJob(schedule, sh string, title ...string) (*Job, error) {
out, err := cmd.CombinedOutput()
j.LastRuntime = time.Since(start)
if err != nil {
out = []byte(fmt.Sprintf("error running command: %v: %v", err, out))
out = []byte(fmt.Sprintf("error running command: %v: %s", err, out))
}
j.LastOutput = strings.TrimSpace(string(out))
if cmd != nil && cmd.ProcessState != nil {

View File

@ -15,6 +15,8 @@ import (
cron "github.com/robfig/cron/v3"
)
var Schedule *Scheduler = New()
type Scheduler struct {
cron *cron.Cron
running map[string]cron.EntryID
@ -154,6 +156,40 @@ func (s *Scheduler) loadJobFromStore(k string) (*Job, error) {
return j, err
}
func (s *Scheduler) Update(j *Job) error {
entryID, ok := s.getEntry(j)
if !ok {
return errors.New("job not found in storage")
}
i, err := s.loadJobFromStore(j.Name)
if err != nil {
return err
}
j.LastStatus = i.LastStatus
j.LastOutput = i.LastOutput
j.LastRuntime = i.LastRuntime
j.LastRun = i.LastRun
b, err := j.Encode()
if err != nil {
return err
}
if err := config.Store.Set(j.Name, b, ns.Jobs...); err != nil {
return err
}
s.cron.Remove(entryID)
entryID, err = s.cron.AddJob(j.Schedule, j)
if err != nil {
return err
}
s.running[j.Name] = entryID
return nil
}
func (s *Scheduler) Add(j *Job) error {
if _, ok := s.getEntry(j); ok {
return ErrDuplicateJob

View File

@ -2,11 +2,13 @@ package scheduler
import (
"bytes"
"fmt"
"io/ioutil"
"local/firestormy/config"
"local/storage"
"os"
"testing"
"time"
)
func TestSchedulerAddRemove(t *testing.T) {
@ -192,3 +194,49 @@ func TestSplitScheduleCommandTitle(t *testing.T) {
})
}
}
func TestSchedulerUpdate(t *testing.T) {
config.Store, _ = storage.New(storage.MAP)
was := config.Store
defer func() {
config.Store = was
}()
s := New()
j, err := NewJob(Bash, "* * * * *", "hostname")
if err != nil {
t.Fatal(err)
}
if err := s.Add(j); err != nil {
t.Fatal(err)
}
if list, err := s.List(); err != nil {
t.Fatal(err)
} else if len(list) != 1 {
t.Fatal(err)
}
time.Sleep(time.Millisecond * 1500)
j.Raw = "echo 2"
j.Title = "title 2"
if err := s.Update(j); err != nil {
t.Fatal(err)
}
if list, err := s.List(); err != nil {
t.Fatal(err)
} else if len(list) != 1 {
t.Fatal(err)
} else if j, err := s.loadJobFromStore(j.Name); err != nil {
t.Fatal(err)
} else if j.Raw != "echo 2" {
t.Error(j.Raw)
} else if j.Title != "title 2" {
t.Error(j.Title)
} else if entry := s.cron.Entry(s.running[j.Name]); entry == s.cron.Entry(-99) {
t.Error(entry)
} else if entries := s.cron.Entries(); len(entries) != 1 {
t.Error(entries)
} else if job, ok := entries[0].Job.(*Job); !ok {
t.Error(fmt.Sprintf("%T", entries[0].Job))
} else if fmt.Sprintf("%+v", job) != fmt.Sprintf("%+v", j) {
t.Error(job)
}
}

28
server/get.go Normal file
View File

@ -0,0 +1,28 @@
package server
import (
"encoding/json"
"local/firestormy/config"
"local/firestormy/config/ns"
"local/firestormy/scheduler"
"net/http"
"strings"
)
func (s *Server) get(w http.ResponseWriter, r *http.Request) {
keys := strings.Split(r.URL.Path, "/")
key := keys[len(keys)-1]
j := &scheduler.Job{}
b, err := config.Store.Get(key, ns.Jobs...)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
if err := j.Decode(b); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(toMap(j))
}

28
server/job.go Normal file
View File

@ -0,0 +1,28 @@
package server
import (
"local/firestormy/scheduler"
"strings"
"time"
)
func toMap(j *scheduler.Job) map[string]interface{} {
tz, err := time.LoadLocation("America/Denver")
if err != nil {
panic(err)
}
out := make(map[string]interface{})
out["disabled"] = j.Disabled
out["id"] = j.Name
out["title"] = j.Title
out["cron"] = j.Schedule
out["language"] = j.Runner.String()
out["script"] = j.Raw
out["last"] = map[string]interface{}{
"run": j.LastRun.In(tz).Format(`2006-01-02 15:04:05 MST`),
"runtime": j.LastRuntime.String(),
"output": strings.ReplaceAll(j.LastOutput, "\n", "<br>"),
"status": j.LastStatus,
}
return out
}

View File

@ -7,7 +7,6 @@ import (
"local/firestormy/scheduler"
"net/http"
"sort"
"time"
)
func (s *Server) list(w http.ResponseWriter, r *http.Request) {
@ -29,22 +28,7 @@ func (s *Server) list(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tz, err := time.LoadLocation("America/Denver")
if err != nil {
panic(err)
}
out[i]["disabled"] = j.Disabled
out[i]["id"] = j.Name
out[i]["title"] = j.Title
out[i]["cron"] = j.Schedule
out[i]["language"] = j.Runner.String()
out[i]["script"] = j.Raw
out[i]["last"] = map[string]interface{}{
"run": j.LastRun.In(tz).Format(`2006-01-02 15:04:05 MST`),
"runtime": j.LastRuntime.String(),
"output": string(j.LastOutput),
"status": j.LastStatus,
}
out[i] = toMap(j)
}
sort.Slice(out, func(i, j int) bool {
return out[i]["title"].(string) < out[j]["title"].(string)

View File

@ -12,6 +12,10 @@ func (s *Server) Routes() error {
path string
handler http.HandlerFunc
}{
{
path: fmt.Sprintf("/api/job/get/%s", router.Wildcard),
handler: s.gzip(s.authenticate(s.get)),
},
{
path: fmt.Sprintf("/api/job/upsert"),
handler: s.gzip(s.authenticate(s.upsert)),

View File

@ -7,7 +7,7 @@ import (
"io"
"local/firestormy/config"
"local/firestormy/config/ns"
"log"
"local/firestormy/scheduler"
"net/http"
"github.com/google/uuid"
@ -52,12 +52,31 @@ func (u *upsertRequest) validate() error {
return nil
}
func (u *upsertRequest) toJob() (*scheduler.Job, error) {
j, err := scheduler.NewJob(scheduler.Bash, u.Cron, u.Script)
if err != nil {
return nil, err
}
j.Title = u.Title
j.Name = u.ID
j.Disabled = u.Disabled
return j, err
}
func (s *Server) upsert(w http.ResponseWriter, r *http.Request) {
upsert, err := newUpsertRequest(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
log.Println("received", upsert)
http.Error(w, "not impl", http.StatusNotImplemented)
job, err := upsert.toJob()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := scheduler.Schedule.Update(job); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(map[string]interface{}{})
}