339 lines
9.7 KiB
Go
339 lines
9.7 KiB
Go
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/app"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
const appID = "io.github.pysentry.desktop"
|
|
|
|
type job struct {
|
|
Name string
|
|
Schedule string
|
|
Command string
|
|
Enabled bool
|
|
LastRun string
|
|
NextRun string
|
|
LastState string
|
|
}
|
|
|
|
type event struct {
|
|
Time string
|
|
JobName string
|
|
State string
|
|
Detail string
|
|
}
|
|
|
|
func Run() {
|
|
a := app.NewWithID(appID)
|
|
a.SetIcon(theme.ComputerIcon())
|
|
|
|
w := a.NewWindow("PySentry")
|
|
w.Resize(fyne.NewSize(1120, 720))
|
|
w.SetContent(newMainView(w))
|
|
w.ShowAndRun()
|
|
}
|
|
|
|
func newMainView(w fyne.Window) fyne.CanvasObject {
|
|
jobs := []job{
|
|
{
|
|
Name: "Nightly backup",
|
|
Schedule: "0 2 * * *",
|
|
Command: "python scripts/backup.py",
|
|
Enabled: true,
|
|
LastRun: "Today 02:00",
|
|
NextRun: "Tomorrow 02:00",
|
|
LastState: "OK",
|
|
},
|
|
{
|
|
Name: "Health check",
|
|
Schedule: "*/15 * * * *",
|
|
Command: "curl -fsS https://example.test/health",
|
|
Enabled: true,
|
|
LastRun: "21:00",
|
|
NextRun: "21:15",
|
|
LastState: "OK",
|
|
},
|
|
{
|
|
Name: "Rotate logs",
|
|
Schedule: "30 1 * * 1",
|
|
Command: "pysentry rotate-logs",
|
|
Enabled: false,
|
|
LastRun: "Monday 01:30",
|
|
NextRun: "Paused",
|
|
LastState: "Paused",
|
|
},
|
|
}
|
|
events := []event{
|
|
{Time: "21:00", JobName: "Health check", State: "OK", Detail: "Completed in 184 ms"},
|
|
{Time: "20:45", JobName: "Health check", State: "OK", Detail: "Completed in 201 ms"},
|
|
{Time: "02:00", JobName: "Nightly backup", State: "OK", Detail: "Completed in 42 s"},
|
|
{Time: "Yesterday 01:30", JobName: "Rotate logs", State: "Paused", Detail: "Skipped because the job is paused"},
|
|
}
|
|
|
|
selected := 0
|
|
title := widget.NewLabelWithStyle(jobs[selected].Name, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
|
schedule := widget.NewLabel(jobs[selected].Schedule)
|
|
command := widget.NewLabel(jobs[selected].Command)
|
|
lastRun := widget.NewLabel(jobs[selected].LastRun)
|
|
nextRun := widget.NewLabel(jobs[selected].NextRun)
|
|
state := widget.NewLabel(jobs[selected].LastState)
|
|
recentEvents := widget.NewList(
|
|
func() int {
|
|
if len(events) < 5 {
|
|
return len(events)
|
|
}
|
|
return 5
|
|
},
|
|
func() fyne.CanvasObject { return widget.NewLabel("event") },
|
|
func(id widget.ListItemID, item fyne.CanvasObject) {
|
|
item.(*widget.Label).SetText(eventText(events[id]))
|
|
},
|
|
)
|
|
history := newHistoryView(&events)
|
|
|
|
updateDetails := func(index int) {
|
|
if index < 0 || index >= len(jobs) {
|
|
title.SetText("No job selected")
|
|
schedule.SetText("")
|
|
command.SetText("")
|
|
lastRun.SetText("")
|
|
nextRun.SetText("")
|
|
state.SetText("")
|
|
return
|
|
}
|
|
selected = index
|
|
current := jobs[selected]
|
|
title.SetText(current.Name)
|
|
schedule.SetText(current.Schedule)
|
|
command.SetText(current.Command)
|
|
lastRun.SetText(current.LastRun)
|
|
nextRun.SetText(current.NextRun)
|
|
state.SetText(current.LastState)
|
|
}
|
|
refresh := func() {
|
|
updateDetails(selected)
|
|
recentEvents.Refresh()
|
|
history.Refresh()
|
|
}
|
|
|
|
list := widget.NewList(
|
|
func() int { return len(jobs) },
|
|
func() fyne.CanvasObject {
|
|
name := widget.NewLabelWithStyle("Job name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
|
meta := widget.NewLabel("schedule")
|
|
status := widget.NewLabel("status")
|
|
return container.NewVBox(name, meta, status)
|
|
},
|
|
func(id widget.ListItemID, item fyne.CanvasObject) {
|
|
row := item.(*fyne.Container)
|
|
name := row.Objects[0].(*widget.Label)
|
|
meta := row.Objects[1].(*widget.Label)
|
|
status := row.Objects[2].(*widget.Label)
|
|
|
|
current := jobs[id]
|
|
name.SetText(current.Name)
|
|
meta.SetText(current.Schedule + " " + current.Command)
|
|
status.SetText(statusText(current))
|
|
},
|
|
)
|
|
list.OnSelected = updateDetails
|
|
list.Select(selected)
|
|
|
|
addButton := widget.NewButtonWithIcon("New job", theme.ContentAddIcon(), func() {
|
|
showJobDialog(w, "New job", job{Enabled: true, LastRun: "Never", NextRun: "After save", LastState: "Ready"}, func(saved job) {
|
|
jobs = append(jobs, saved)
|
|
selected = len(jobs) - 1
|
|
events = append([]event{newEvent(saved.Name, "Created", "Job was added")}, events...)
|
|
list.Refresh()
|
|
list.Select(selected)
|
|
refresh()
|
|
})
|
|
})
|
|
editButton := widget.NewButtonWithIcon("Edit", theme.DocumentCreateIcon(), func() {
|
|
if selected < 0 || selected >= len(jobs) {
|
|
return
|
|
}
|
|
showJobDialog(w, "Edit job", jobs[selected], func(saved job) {
|
|
jobs[selected] = saved
|
|
events = append([]event{newEvent(saved.Name, "Updated", "Job settings changed")}, events...)
|
|
list.Refresh()
|
|
refresh()
|
|
})
|
|
})
|
|
runButton := widget.NewButtonWithIcon("Run now", theme.MediaPlayIcon(), func() {
|
|
if selected < 0 || selected >= len(jobs) {
|
|
return
|
|
}
|
|
jobs[selected].LastRun = "Just now"
|
|
jobs[selected].LastState = "OK"
|
|
if jobs[selected].Enabled {
|
|
jobs[selected].NextRun = "Waiting for scheduler"
|
|
}
|
|
events = append([]event{newEvent(jobs[selected].Name, "OK", "Manual run simulated")}, events...)
|
|
list.Refresh()
|
|
refresh()
|
|
})
|
|
pauseButton := widget.NewButtonWithIcon("Pause", theme.MediaPauseIcon(), func() {
|
|
if selected < 0 || selected >= len(jobs) {
|
|
return
|
|
}
|
|
current := &jobs[selected]
|
|
current.Enabled = !current.Enabled
|
|
if current.Enabled {
|
|
current.LastState = "Ready"
|
|
current.NextRun = "Waiting for scheduler"
|
|
events = append([]event{newEvent(current.Name, "Resumed", "Job was enabled")}, events...)
|
|
} else {
|
|
current.LastState = "Paused"
|
|
current.NextRun = "Paused"
|
|
events = append([]event{newEvent(current.Name, "Paused", "Job was disabled")}, events...)
|
|
}
|
|
list.Refresh()
|
|
refresh()
|
|
})
|
|
|
|
toolbar := container.NewHBox(addButton, editButton, runButton, pauseButton, layout.NewSpacer())
|
|
sidebar := container.NewBorder(toolbar, nil, nil, nil, list)
|
|
|
|
details := container.NewVBox(
|
|
title,
|
|
widget.NewSeparator(),
|
|
detailRow("Schedule", schedule),
|
|
detailRow("Command", command),
|
|
detailRow("Last run", lastRun),
|
|
detailRow("Next run", nextRun),
|
|
detailRow("State", state),
|
|
widget.NewSeparator(),
|
|
widget.NewLabelWithStyle("Recent events", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
recentEvents,
|
|
)
|
|
|
|
tabs := container.NewAppTabs(
|
|
container.NewTabItemWithIcon("Jobs", theme.ListIcon(), container.NewHSplit(sidebar, container.NewPadded(details))),
|
|
container.NewTabItemWithIcon("History", theme.HistoryIcon(), history),
|
|
container.NewTabItemWithIcon("Settings", theme.SettingsIcon(), settingsView()),
|
|
)
|
|
tabs.SetTabLocation(container.TabLocationTop)
|
|
|
|
return tabs
|
|
}
|
|
|
|
func statusText(j job) string {
|
|
if !j.Enabled {
|
|
return "Paused"
|
|
}
|
|
return j.LastState
|
|
}
|
|
|
|
func newEvent(jobName string, state string, detail string) event {
|
|
return event{
|
|
Time: time.Now().Format("15:04:05"),
|
|
JobName: jobName,
|
|
State: state,
|
|
Detail: detail,
|
|
}
|
|
}
|
|
|
|
func eventText(e event) string {
|
|
return fmt.Sprintf("%s %s %s %s", e.Time, e.JobName, e.State, e.Detail)
|
|
}
|
|
|
|
func detailRow(label string, value fyne.CanvasObject) fyne.CanvasObject {
|
|
caption := widget.NewLabelWithStyle(label, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
|
caption.Wrapping = fyne.TextTruncate
|
|
return container.NewGridWithColumns(2, caption, value)
|
|
}
|
|
|
|
func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
|
|
name := widget.NewEntry()
|
|
name.SetPlaceHolder("Nightly backup")
|
|
name.SetText(current.Name)
|
|
schedule := widget.NewEntry()
|
|
schedule.SetPlaceHolder("0 2 * * *")
|
|
schedule.SetText(current.Schedule)
|
|
command := widget.NewEntry()
|
|
command.SetPlaceHolder("python scripts/backup.py")
|
|
command.SetText(current.Command)
|
|
enabled := widget.NewCheck("Enabled", nil)
|
|
enabled.SetChecked(current.Enabled)
|
|
|
|
form := dialog.NewForm(
|
|
title,
|
|
"Save",
|
|
"Cancel",
|
|
[]*widget.FormItem{
|
|
widget.NewFormItem("Name", name),
|
|
widget.NewFormItem("Schedule", schedule),
|
|
widget.NewFormItem("Command", command),
|
|
widget.NewFormItem("", enabled),
|
|
},
|
|
func(saved bool) {
|
|
if !saved {
|
|
return
|
|
}
|
|
if strings.TrimSpace(name.Text) == "" || strings.TrimSpace(schedule.Text) == "" || strings.TrimSpace(command.Text) == "" {
|
|
dialog.ShowError(fmt.Errorf("name, schedule, and command are required"), w)
|
|
return
|
|
}
|
|
current.Name = strings.TrimSpace(name.Text)
|
|
current.Schedule = strings.TrimSpace(schedule.Text)
|
|
current.Command = strings.TrimSpace(command.Text)
|
|
current.Enabled = enabled.Checked
|
|
if current.LastRun == "" {
|
|
current.LastRun = "Never"
|
|
}
|
|
if current.Enabled {
|
|
current.NextRun = "Waiting for scheduler"
|
|
if current.LastState == "" || current.LastState == "Paused" {
|
|
current.LastState = "Ready"
|
|
}
|
|
} else {
|
|
current.NextRun = "Paused"
|
|
current.LastState = "Paused"
|
|
}
|
|
onSave(current)
|
|
},
|
|
w,
|
|
)
|
|
form.Resize(fyne.NewSize(560, 280))
|
|
form.Show()
|
|
}
|
|
|
|
func newHistoryView(events *[]event) *fyne.Container {
|
|
list := widget.NewList(
|
|
func() int { return len(*events) },
|
|
func() fyne.CanvasObject { return widget.NewLabel("event") },
|
|
func(id widget.ListItemID, item fyne.CanvasObject) {
|
|
item.(*widget.Label).SetText(eventText((*events)[id]))
|
|
},
|
|
)
|
|
return container.NewPadded(list)
|
|
}
|
|
|
|
func settingsView() fyne.CanvasObject {
|
|
runOnStartup := widget.NewCheck("Start PySentry when I sign in", nil)
|
|
minimizeToTray := widget.NewCheck("Keep running in the system tray", nil)
|
|
notifications := widget.NewCheck("Show desktop notifications for failed jobs", nil)
|
|
notifications.SetChecked(true)
|
|
|
|
return container.NewPadded(container.NewVBox(
|
|
widget.NewLabelWithStyle("Application", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
runOnStartup,
|
|
minimizeToTray,
|
|
notifications,
|
|
widget.NewSeparator(),
|
|
widget.NewLabelWithStyle("Scheduler", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
widget.NewLabel("The scheduler service, job storage, and cron parser come next."),
|
|
))
|
|
}
|