Improve scheduler GUI controls
This commit is contained in:
@@ -1,4 +1,8 @@
|
|||||||
# ---> Python
|
# ---> Python
|
||||||
|
# ---> Go
|
||||||
|
bin/
|
||||||
|
*.exe
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
+263
-27
@@ -9,25 +9,33 @@ import (
|
|||||||
"fyne.io/fyne/v2/app"
|
"fyne.io/fyne/v2/app"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/dialog"
|
"fyne.io/fyne/v2/dialog"
|
||||||
|
"fyne.io/fyne/v2/driver/desktop"
|
||||||
"fyne.io/fyne/v2/layout"
|
"fyne.io/fyne/v2/layout"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
)
|
)
|
||||||
|
|
||||||
const appID = "io.github.pysentry.desktop"
|
const appID = "io.github.pysentry.desktop"
|
||||||
|
const allFolders = "All"
|
||||||
|
const noFolder = "No folder"
|
||||||
|
|
||||||
type job struct {
|
type job struct {
|
||||||
|
ID int
|
||||||
Name string
|
Name string
|
||||||
|
Folder string
|
||||||
Schedule string
|
Schedule string
|
||||||
Command string
|
Command string
|
||||||
Enabled bool
|
Enabled bool
|
||||||
LastRun string
|
LastRun string
|
||||||
NextRun string
|
NextRun string
|
||||||
LastState string
|
LastState string
|
||||||
|
Logs []event
|
||||||
|
Output string
|
||||||
}
|
}
|
||||||
|
|
||||||
type event struct {
|
type event struct {
|
||||||
Time string
|
Time string
|
||||||
|
JobID int
|
||||||
JobName string
|
JobName string
|
||||||
State string
|
State string
|
||||||
Detail string
|
Detail string
|
||||||
@@ -38,32 +46,70 @@ func Run() {
|
|||||||
a.SetIcon(theme.ComputerIcon())
|
a.SetIcon(theme.ComputerIcon())
|
||||||
|
|
||||||
w := a.NewWindow("PySentry")
|
w := a.NewWindow("PySentry")
|
||||||
|
configureSystemTray(a, w)
|
||||||
w.Resize(fyne.NewSize(1120, 720))
|
w.Resize(fyne.NewSize(1120, 720))
|
||||||
w.SetContent(newMainView(w))
|
w.SetContent(newMainView(w))
|
||||||
w.ShowAndRun()
|
w.ShowAndRun()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configureSystemTray(a fyne.App, w fyne.Window) {
|
||||||
|
desk, ok := a.(desktop.App)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
menu := fyne.NewMenu("PySentry",
|
||||||
|
fyne.NewMenuItem("Show", func() {
|
||||||
|
w.Show()
|
||||||
|
w.RequestFocus()
|
||||||
|
}),
|
||||||
|
fyne.NewMenuItemSeparator(),
|
||||||
|
fyne.NewMenuItem("Quit", func() {
|
||||||
|
a.Quit()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
desk.SetSystemTrayMenu(menu)
|
||||||
|
w.SetCloseIntercept(func() {
|
||||||
|
w.Hide()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func newMainView(w fyne.Window) fyne.CanvasObject {
|
func newMainView(w fyne.Window) fyne.CanvasObject {
|
||||||
jobs := []job{
|
jobs := []job{
|
||||||
{
|
{
|
||||||
|
ID: 1,
|
||||||
Name: "Nightly backup",
|
Name: "Nightly backup",
|
||||||
|
Folder: "Maintenance",
|
||||||
Schedule: "0 2 * * *",
|
Schedule: "0 2 * * *",
|
||||||
Command: "python scripts/backup.py",
|
Command: "python scripts/backup.py",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
LastRun: "Today 02:00",
|
LastRun: "Today 02:00",
|
||||||
NextRun: "Tomorrow 02:00",
|
NextRun: "Tomorrow 02:00",
|
||||||
LastState: "OK",
|
LastState: "OK",
|
||||||
|
Output: "stdout: backup archive created\nstderr: <empty>",
|
||||||
|
Logs: []event{
|
||||||
|
{Time: "Today 02:00", JobID: 1, JobName: "Nightly backup", State: "OK", Detail: "Completed in 42 s"},
|
||||||
|
{Time: "Yesterday 02:00", JobID: 1, JobName: "Nightly backup", State: "OK", Detail: "Completed in 39 s"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
ID: 2,
|
||||||
Name: "Health check",
|
Name: "Health check",
|
||||||
|
Folder: "Monitoring",
|
||||||
Schedule: "*/15 * * * *",
|
Schedule: "*/15 * * * *",
|
||||||
Command: "curl -fsS https://example.test/health",
|
Command: "curl -fsS https://example.test/health",
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
LastRun: "21:00",
|
LastRun: "21:00",
|
||||||
NextRun: "21:15",
|
NextRun: "21:15",
|
||||||
LastState: "OK",
|
LastState: "OK",
|
||||||
|
Output: "stdout: HTTP 200 OK\nstderr: <empty>",
|
||||||
|
Logs: []event{
|
||||||
|
{Time: "21:00", JobID: 2, JobName: "Health check", State: "OK", Detail: "Completed in 184 ms"},
|
||||||
|
{Time: "20:45", JobID: 2, JobName: "Health check", State: "OK", Detail: "Completed in 201 ms"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
ID: 3,
|
||||||
Name: "Rotate logs",
|
Name: "Rotate logs",
|
||||||
Schedule: "30 1 * * 1",
|
Schedule: "30 1 * * 1",
|
||||||
Command: "pysentry rotate-logs",
|
Command: "pysentry rotate-logs",
|
||||||
@@ -71,63 +117,81 @@ func newMainView(w fyne.Window) fyne.CanvasObject {
|
|||||||
LastRun: "Monday 01:30",
|
LastRun: "Monday 01:30",
|
||||||
NextRun: "Paused",
|
NextRun: "Paused",
|
||||||
LastState: "Paused",
|
LastState: "Paused",
|
||||||
|
Output: "No command output captured yet.",
|
||||||
|
Logs: []event{
|
||||||
|
{Time: "Yesterday 01:30", JobID: 3, JobName: "Rotate logs", State: "Paused", Detail: "Skipped because the job is paused"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
events := []event{
|
events := []event{
|
||||||
{Time: "21:00", JobName: "Health check", State: "OK", Detail: "Completed in 184 ms"},
|
{Time: "21:00", JobID: 2, JobName: "Health check", State: "OK", Detail: "Completed in 184 ms"},
|
||||||
{Time: "20:45", JobName: "Health check", State: "OK", Detail: "Completed in 201 ms"},
|
{Time: "20:45", JobID: 2, JobName: "Health check", State: "OK", Detail: "Completed in 201 ms"},
|
||||||
{Time: "02:00", JobName: "Nightly backup", State: "OK", Detail: "Completed in 42 s"},
|
{Time: "02:00", JobID: 1, 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"},
|
{Time: "Yesterday 01:30", JobID: 3, JobName: "Rotate logs", State: "Paused", Detail: "Skipped because the job is paused"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextJobID := 4
|
||||||
selected := 0
|
selected := 0
|
||||||
|
selectedFolder := allFolders
|
||||||
|
schedulerPaused := false
|
||||||
|
filteredJobs := filteredJobIndexes(jobs, selectedFolder)
|
||||||
title := widget.NewLabelWithStyle(jobs[selected].Name, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
title := widget.NewLabelWithStyle(jobs[selected].Name, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||||
|
folder := widget.NewLabel(jobs[selected].Folder)
|
||||||
schedule := widget.NewLabel(jobs[selected].Schedule)
|
schedule := widget.NewLabel(jobs[selected].Schedule)
|
||||||
command := widget.NewLabel(jobs[selected].Command)
|
command := widget.NewLabel(jobs[selected].Command)
|
||||||
lastRun := widget.NewLabel(jobs[selected].LastRun)
|
lastRun := widget.NewLabel(jobs[selected].LastRun)
|
||||||
nextRun := widget.NewLabel(jobs[selected].NextRun)
|
nextRun := widget.NewLabel(jobs[selected].NextRun)
|
||||||
state := widget.NewLabel(jobs[selected].LastState)
|
state := widget.NewLabel(jobs[selected].LastState)
|
||||||
recentEvents := widget.NewList(
|
schedulerState := widget.NewLabel("Scheduler running")
|
||||||
|
commandOutput := widget.NewMultiLineEntry()
|
||||||
|
commandOutput.SetText(jobs[selected].Output)
|
||||||
|
commandOutput.Disable()
|
||||||
|
history := newHistoryView(&events)
|
||||||
|
jobLogs := widget.NewList(
|
||||||
func() int {
|
func() int {
|
||||||
if len(events) < 5 {
|
if selected < 0 || selected >= len(jobs) {
|
||||||
return len(events)
|
return 0
|
||||||
}
|
}
|
||||||
return 5
|
return len(jobs[selected].Logs)
|
||||||
},
|
},
|
||||||
func() fyne.CanvasObject { return widget.NewLabel("event") },
|
func() fyne.CanvasObject { return widget.NewLabel("log") },
|
||||||
func(id widget.ListItemID, item fyne.CanvasObject) {
|
func(id widget.ListItemID, item fyne.CanvasObject) {
|
||||||
item.(*widget.Label).SetText(eventText(events[id]))
|
item.(*widget.Label).SetText(eventText(jobs[selected].Logs[id]))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
history := newHistoryView(&events)
|
|
||||||
|
|
||||||
updateDetails := func(index int) {
|
updateDetails := func(index int) {
|
||||||
if index < 0 || index >= len(jobs) {
|
if index < 0 || index >= len(jobs) {
|
||||||
title.SetText("No job selected")
|
title.SetText("No job selected")
|
||||||
|
folder.SetText("")
|
||||||
schedule.SetText("")
|
schedule.SetText("")
|
||||||
command.SetText("")
|
command.SetText("")
|
||||||
lastRun.SetText("")
|
lastRun.SetText("")
|
||||||
nextRun.SetText("")
|
nextRun.SetText("")
|
||||||
state.SetText("")
|
state.SetText("")
|
||||||
|
commandOutput.SetText("")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selected = index
|
selected = index
|
||||||
current := jobs[selected]
|
current := jobs[selected]
|
||||||
title.SetText(current.Name)
|
title.SetText(current.Name)
|
||||||
|
folder.SetText(displayFolder(current.Folder))
|
||||||
schedule.SetText(current.Schedule)
|
schedule.SetText(current.Schedule)
|
||||||
command.SetText(current.Command)
|
command.SetText(current.Command)
|
||||||
lastRun.SetText(current.LastRun)
|
lastRun.SetText(current.LastRun)
|
||||||
nextRun.SetText(current.NextRun)
|
nextRun.SetText(current.NextRun)
|
||||||
state.SetText(current.LastState)
|
state.SetText(current.LastState)
|
||||||
|
commandOutput.SetText(current.Output)
|
||||||
}
|
}
|
||||||
refresh := func() {
|
refresh := func() {
|
||||||
|
filteredJobs = filteredJobIndexes(jobs, selectedFolder)
|
||||||
updateDetails(selected)
|
updateDetails(selected)
|
||||||
recentEvents.Refresh()
|
jobLogs.Refresh()
|
||||||
history.Refresh()
|
history.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
list := widget.NewList(
|
list := widget.NewList(
|
||||||
func() int { return len(jobs) },
|
func() int { return len(filteredJobs) },
|
||||||
func() fyne.CanvasObject {
|
func() fyne.CanvasObject {
|
||||||
name := widget.NewLabelWithStyle("Job name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
name := widget.NewLabelWithStyle("Job name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||||
meta := widget.NewLabel("schedule")
|
meta := widget.NewLabel("schedule")
|
||||||
@@ -140,22 +204,58 @@ func newMainView(w fyne.Window) fyne.CanvasObject {
|
|||||||
meta := row.Objects[1].(*widget.Label)
|
meta := row.Objects[1].(*widget.Label)
|
||||||
status := row.Objects[2].(*widget.Label)
|
status := row.Objects[2].(*widget.Label)
|
||||||
|
|
||||||
current := jobs[id]
|
current := jobs[filteredJobs[id]]
|
||||||
name.SetText(current.Name)
|
name.SetText(current.Name)
|
||||||
meta.SetText(current.Schedule + " " + current.Command)
|
meta.SetText(displayFolder(current.Folder) + " " + current.Schedule + " " + current.Command)
|
||||||
status.SetText(statusText(current))
|
status.SetText(statusText(current))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
list.OnSelected = updateDetails
|
list.OnSelected = func(id widget.ListItemID) {
|
||||||
|
if id < 0 || id >= len(filteredJobs) {
|
||||||
|
updateDetails(-1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateDetails(filteredJobs[id])
|
||||||
|
}
|
||||||
list.Select(selected)
|
list.Select(selected)
|
||||||
|
|
||||||
|
folderSelect := widget.NewSelect(folderOptions(jobs), func(value string) {
|
||||||
|
if value == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedFolder = value
|
||||||
|
filteredJobs = filteredJobIndexes(jobs, selectedFolder)
|
||||||
|
list.Refresh()
|
||||||
|
if len(filteredJobs) == 0 {
|
||||||
|
selected = -1
|
||||||
|
updateDetails(-1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selected = filteredJobs[0]
|
||||||
|
list.Select(0)
|
||||||
|
refresh()
|
||||||
|
})
|
||||||
|
folderSelect.SetSelected(selectedFolder)
|
||||||
|
|
||||||
addButton := widget.NewButtonWithIcon("New job", theme.ContentAddIcon(), func() {
|
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) {
|
showJobDialog(w, "New job", job{Enabled: true, LastRun: "Never", NextRun: "After save", LastState: "Ready"}, func(saved job) {
|
||||||
|
saved.ID = nextJobID
|
||||||
|
nextJobID++
|
||||||
jobs = append(jobs, saved)
|
jobs = append(jobs, saved)
|
||||||
selected = len(jobs) - 1
|
selected = len(jobs) - 1
|
||||||
events = append([]event{newEvent(saved.Name, "Created", "Job was added")}, events...)
|
created := newEvent(saved.ID, saved.Name, "Created", "Job was added")
|
||||||
|
jobs[selected].Logs = append([]event{created}, jobs[selected].Logs...)
|
||||||
|
events = append([]event{created}, events...)
|
||||||
|
folderSelect.Options = folderOptions(jobs)
|
||||||
|
folderSelect.Refresh()
|
||||||
|
targetFolder := filterValue(saved.Folder)
|
||||||
|
if selectedFolder != allFolders && selectedFolder != targetFolder {
|
||||||
|
selectedFolder = targetFolder
|
||||||
|
folderSelect.SetSelected(targetFolder)
|
||||||
|
}
|
||||||
|
filteredJobs = filteredJobIndexes(jobs, selectedFolder)
|
||||||
list.Refresh()
|
list.Refresh()
|
||||||
list.Select(selected)
|
list.Select(displayIndex(filteredJobs, selected))
|
||||||
refresh()
|
refresh()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -164,8 +264,15 @@ func newMainView(w fyne.Window) fyne.CanvasObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
showJobDialog(w, "Edit job", jobs[selected], func(saved job) {
|
showJobDialog(w, "Edit job", jobs[selected], func(saved job) {
|
||||||
|
saved.ID = jobs[selected].ID
|
||||||
|
saved.Logs = jobs[selected].Logs
|
||||||
|
saved.Output = jobs[selected].Output
|
||||||
jobs[selected] = saved
|
jobs[selected] = saved
|
||||||
events = append([]event{newEvent(saved.Name, "Updated", "Job settings changed")}, events...)
|
updated := newEvent(saved.ID, saved.Name, "Updated", "Job settings changed")
|
||||||
|
jobs[selected].Logs = append([]event{updated}, jobs[selected].Logs...)
|
||||||
|
events = append([]event{updated}, events...)
|
||||||
|
folderSelect.Options = folderOptions(jobs)
|
||||||
|
folderSelect.Refresh()
|
||||||
list.Refresh()
|
list.Refresh()
|
||||||
refresh()
|
refresh()
|
||||||
})
|
})
|
||||||
@@ -174,15 +281,49 @@ func newMainView(w fyne.Window) fyne.CanvasObject {
|
|||||||
if selected < 0 || selected >= len(jobs) {
|
if selected < 0 || selected >= len(jobs) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if schedulerPaused {
|
||||||
|
dialog.ShowInformation("Scheduler paused", "Global pause is active. Resume the scheduler before running jobs.", w)
|
||||||
|
return
|
||||||
|
}
|
||||||
jobs[selected].LastRun = "Just now"
|
jobs[selected].LastRun = "Just now"
|
||||||
jobs[selected].LastState = "OK"
|
jobs[selected].LastState = "OK"
|
||||||
|
jobs[selected].Output = "stdout: manual run simulated\nstderr: <empty>"
|
||||||
if jobs[selected].Enabled {
|
if jobs[selected].Enabled {
|
||||||
jobs[selected].NextRun = "Waiting for scheduler"
|
jobs[selected].NextRun = "Waiting for scheduler"
|
||||||
}
|
}
|
||||||
events = append([]event{newEvent(jobs[selected].Name, "OK", "Manual run simulated")}, events...)
|
ran := newEvent(jobs[selected].ID, jobs[selected].Name, "OK", "Manual run simulated")
|
||||||
|
jobs[selected].Logs = append([]event{ran}, jobs[selected].Logs...)
|
||||||
|
events = append([]event{ran}, events...)
|
||||||
list.Refresh()
|
list.Refresh()
|
||||||
refresh()
|
refresh()
|
||||||
})
|
})
|
||||||
|
stopAllButton := widget.NewButtonWithIcon("Pause all", theme.MediaStopIcon(), nil)
|
||||||
|
stopAllButton.OnTapped = func() {
|
||||||
|
schedulerPaused = !schedulerPaused
|
||||||
|
if schedulerPaused {
|
||||||
|
schedulerState.SetText("Scheduler paused")
|
||||||
|
stopAllButton.SetText("Resume all")
|
||||||
|
stopAllButton.SetIcon(theme.MediaPlayIcon())
|
||||||
|
for index := range jobs {
|
||||||
|
if jobs[index].Enabled {
|
||||||
|
jobs[index].NextRun = "Scheduler paused"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
events = append([]event{newEvent(0, "Scheduler", "Paused", "All job execution paused")}, events...)
|
||||||
|
} else {
|
||||||
|
schedulerState.SetText("Scheduler running")
|
||||||
|
stopAllButton.SetText("Pause all")
|
||||||
|
stopAllButton.SetIcon(theme.MediaStopIcon())
|
||||||
|
for index := range jobs {
|
||||||
|
if jobs[index].Enabled && jobs[index].NextRun == "Scheduler paused" {
|
||||||
|
jobs[index].NextRun = "Waiting for scheduler"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
events = append([]event{newEvent(0, "Scheduler", "Resumed", "All job execution resumed")}, events...)
|
||||||
|
}
|
||||||
|
list.Refresh()
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
pauseButton := widget.NewButtonWithIcon("Pause", theme.MediaPauseIcon(), func() {
|
pauseButton := widget.NewButtonWithIcon("Pause", theme.MediaPauseIcon(), func() {
|
||||||
if selected < 0 || selected >= len(jobs) {
|
if selected < 0 || selected >= len(jobs) {
|
||||||
return
|
return
|
||||||
@@ -192,30 +333,71 @@ func newMainView(w fyne.Window) fyne.CanvasObject {
|
|||||||
if current.Enabled {
|
if current.Enabled {
|
||||||
current.LastState = "Ready"
|
current.LastState = "Ready"
|
||||||
current.NextRun = "Waiting for scheduler"
|
current.NextRun = "Waiting for scheduler"
|
||||||
events = append([]event{newEvent(current.Name, "Resumed", "Job was enabled")}, events...)
|
resumed := newEvent(current.ID, current.Name, "Resumed", "Job was enabled")
|
||||||
|
current.Logs = append([]event{resumed}, current.Logs...)
|
||||||
|
events = append([]event{resumed}, events...)
|
||||||
} else {
|
} else {
|
||||||
current.LastState = "Paused"
|
current.LastState = "Paused"
|
||||||
current.NextRun = "Paused"
|
current.NextRun = "Paused"
|
||||||
events = append([]event{newEvent(current.Name, "Paused", "Job was disabled")}, events...)
|
paused := newEvent(current.ID, current.Name, "Paused", "Job was disabled")
|
||||||
|
current.Logs = append([]event{paused}, current.Logs...)
|
||||||
|
events = append([]event{paused}, events...)
|
||||||
}
|
}
|
||||||
list.Refresh()
|
list.Refresh()
|
||||||
refresh()
|
refresh()
|
||||||
})
|
})
|
||||||
|
deleteButton := widget.NewButtonWithIcon("Delete", theme.DeleteIcon(), func() {
|
||||||
|
if selected < 0 || selected >= len(jobs) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deleted := jobs[selected]
|
||||||
|
dialog.ShowConfirm("Delete job", fmt.Sprintf("Delete %q?", deleted.Name), func(confirm bool) {
|
||||||
|
if !confirm {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jobs = append(jobs[:selected], jobs[selected+1:]...)
|
||||||
|
folderSelect.Options = folderOptions(jobs)
|
||||||
|
folderSelect.Refresh()
|
||||||
|
filteredJobs = filteredJobIndexes(jobs, selectedFolder)
|
||||||
|
if len(filteredJobs) == 0 && selectedFolder != allFolders {
|
||||||
|
selectedFolder = allFolders
|
||||||
|
folderSelect.SetSelected(allFolders)
|
||||||
|
filteredJobs = filteredJobIndexes(jobs, selectedFolder)
|
||||||
|
}
|
||||||
|
if len(filteredJobs) == 0 {
|
||||||
|
selected = -1
|
||||||
|
} else {
|
||||||
|
selected = filteredJobs[0]
|
||||||
|
}
|
||||||
|
events = append([]event{newEvent(deleted.ID, deleted.Name, "Deleted", "Job was removed")}, events...)
|
||||||
|
list.Refresh()
|
||||||
|
if selected >= 0 {
|
||||||
|
list.Select(displayIndex(filteredJobs, selected))
|
||||||
|
}
|
||||||
|
refresh()
|
||||||
|
}, w)
|
||||||
|
})
|
||||||
|
|
||||||
toolbar := container.NewHBox(addButton, editButton, runButton, pauseButton, layout.NewSpacer())
|
toolbar := container.NewHBox(addButton, editButton, runButton, pauseButton, deleteButton, layout.NewSpacer())
|
||||||
sidebar := container.NewBorder(toolbar, nil, nil, nil, list)
|
globalControls := container.NewHBox(stopAllButton, schedulerState, layout.NewSpacer())
|
||||||
|
sidebarHeader := container.NewVBox(globalControls, widget.NewSeparator(), widget.NewLabelWithStyle("Folder", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), folderSelect, toolbar)
|
||||||
|
sidebar := container.NewBorder(sidebarHeader, nil, nil, nil, list)
|
||||||
|
|
||||||
details := container.NewVBox(
|
details := container.NewVBox(
|
||||||
title,
|
title,
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
|
detailRow("Folder", folder),
|
||||||
detailRow("Schedule", schedule),
|
detailRow("Schedule", schedule),
|
||||||
detailRow("Command", command),
|
detailRow("Command", command),
|
||||||
detailRow("Last run", lastRun),
|
detailRow("Last run", lastRun),
|
||||||
detailRow("Next run", nextRun),
|
detailRow("Next run", nextRun),
|
||||||
detailRow("State", state),
|
detailRow("State", state),
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
widget.NewLabelWithStyle("Recent events", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
widget.NewLabelWithStyle("Command output", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
recentEvents,
|
commandOutput,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
widget.NewLabelWithStyle("Selected job activity", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
|
jobLogs,
|
||||||
)
|
)
|
||||||
|
|
||||||
tabs := container.NewAppTabs(
|
tabs := container.NewAppTabs(
|
||||||
@@ -235,9 +417,10 @@ func statusText(j job) string {
|
|||||||
return j.LastState
|
return j.LastState
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEvent(jobName string, state string, detail string) event {
|
func newEvent(jobID int, jobName string, state string, detail string) event {
|
||||||
return event{
|
return event{
|
||||||
Time: time.Now().Format("15:04:05"),
|
Time: time.Now().Format("15:04:05"),
|
||||||
|
JobID: jobID,
|
||||||
JobName: jobName,
|
JobName: jobName,
|
||||||
State: state,
|
State: state,
|
||||||
Detail: detail,
|
Detail: detail,
|
||||||
@@ -254,10 +437,60 @@ func detailRow(label string, value fyne.CanvasObject) fyne.CanvasObject {
|
|||||||
return container.NewGridWithColumns(2, caption, value)
|
return container.NewGridWithColumns(2, caption, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func filteredJobIndexes(jobs []job, folder string) []int {
|
||||||
|
indexes := make([]int, 0, len(jobs))
|
||||||
|
for index, current := range jobs {
|
||||||
|
if folder == allFolders || filterValue(current.Folder) == folder {
|
||||||
|
indexes = append(indexes, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return indexes
|
||||||
|
}
|
||||||
|
|
||||||
|
func folderOptions(jobs []job) []string {
|
||||||
|
options := []string{allFolders, noFolder}
|
||||||
|
seen := map[string]bool{allFolders: true, noFolder: true}
|
||||||
|
for _, current := range jobs {
|
||||||
|
folder := strings.TrimSpace(current.Folder)
|
||||||
|
if folder == "" || seen[folder] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[folder] = true
|
||||||
|
options = append(options, folder)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterValue(folder string) string {
|
||||||
|
if strings.TrimSpace(folder) == "" {
|
||||||
|
return noFolder
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayFolder(folder string) string {
|
||||||
|
if strings.TrimSpace(folder) == "" {
|
||||||
|
return "(" + noFolder + ")"
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayIndex(indexes []int, jobIndex int) int {
|
||||||
|
for display, index := range indexes {
|
||||||
|
if index == jobIndex {
|
||||||
|
return display
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
|
func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
|
||||||
name := widget.NewEntry()
|
name := widget.NewEntry()
|
||||||
name.SetPlaceHolder("Nightly backup")
|
name.SetPlaceHolder("Nightly backup")
|
||||||
name.SetText(current.Name)
|
name.SetText(current.Name)
|
||||||
|
folder := widget.NewEntry()
|
||||||
|
folder.SetPlaceHolder("Maintenance")
|
||||||
|
folder.SetText(current.Folder)
|
||||||
schedule := widget.NewEntry()
|
schedule := widget.NewEntry()
|
||||||
schedule.SetPlaceHolder("0 2 * * *")
|
schedule.SetPlaceHolder("0 2 * * *")
|
||||||
schedule.SetText(current.Schedule)
|
schedule.SetText(current.Schedule)
|
||||||
@@ -273,6 +506,7 @@ func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
|
|||||||
"Cancel",
|
"Cancel",
|
||||||
[]*widget.FormItem{
|
[]*widget.FormItem{
|
||||||
widget.NewFormItem("Name", name),
|
widget.NewFormItem("Name", name),
|
||||||
|
widget.NewFormItem("Folder", folder),
|
||||||
widget.NewFormItem("Schedule", schedule),
|
widget.NewFormItem("Schedule", schedule),
|
||||||
widget.NewFormItem("Command", command),
|
widget.NewFormItem("Command", command),
|
||||||
widget.NewFormItem("", enabled),
|
widget.NewFormItem("", enabled),
|
||||||
@@ -286,6 +520,7 @@ func showJobDialog(w fyne.Window, title string, current job, onSave func(job)) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
current.Name = strings.TrimSpace(name.Text)
|
current.Name = strings.TrimSpace(name.Text)
|
||||||
|
current.Folder = strings.TrimSpace(folder.Text)
|
||||||
current.Schedule = strings.TrimSpace(schedule.Text)
|
current.Schedule = strings.TrimSpace(schedule.Text)
|
||||||
current.Command = strings.TrimSpace(command.Text)
|
current.Command = strings.TrimSpace(command.Text)
|
||||||
current.Enabled = enabled.Checked
|
current.Enabled = enabled.Checked
|
||||||
@@ -323,6 +558,7 @@ func newHistoryView(events *[]event) *fyne.Container {
|
|||||||
func settingsView() fyne.CanvasObject {
|
func settingsView() fyne.CanvasObject {
|
||||||
runOnStartup := widget.NewCheck("Start PySentry when I sign in", nil)
|
runOnStartup := widget.NewCheck("Start PySentry when I sign in", nil)
|
||||||
minimizeToTray := widget.NewCheck("Keep running in the system tray", nil)
|
minimizeToTray := widget.NewCheck("Keep running in the system tray", nil)
|
||||||
|
minimizeToTray.SetChecked(true)
|
||||||
notifications := widget.NewCheck("Show desktop notifications for failed jobs", nil)
|
notifications := widget.NewCheck("Show desktop notifications for failed jobs", nil)
|
||||||
notifications.SetChecked(true)
|
notifications.SetChecked(true)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user