ui/jobs_view: cap Selected job activity to 3 recent rows, anchor at bottom
Show only the latest 3 activity entries for the selected job instead of the full in-memory list, and pin the panel to the bottom of the details pane via a Border so the command output absorbs the remaining vertical space. Adds fixedHeightLayout (mirroring minWidthLayout) to reserve a stable height for the activity list. The full per-job history (maxJobLogs) is untouched and still available in the History view. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+32
-5
@@ -19,6 +19,16 @@ const allFolders = "All"
|
|||||||
const noFolder = "No folder"
|
const noFolder = "No folder"
|
||||||
const minJobsSidebarWidth float32 = 480
|
const minJobsSidebarWidth float32 = 480
|
||||||
|
|
||||||
|
// maxJobActivityRows caps the "Selected job activity" panel to the most recent
|
||||||
|
// entries. The full per-job history (up to maxJobLogs) remains in the History
|
||||||
|
// view; this panel is a quick at-a-glance summary anchored below the output.
|
||||||
|
const maxJobActivityRows = 3
|
||||||
|
|
||||||
|
// jobActivityHeight is the fixed height reserved for the activity panel at the
|
||||||
|
// bottom of the details pane, sized for maxJobActivityRows rows so the command
|
||||||
|
// output above it can claim the remaining vertical space.
|
||||||
|
const jobActivityHeight float32 = 120
|
||||||
|
|
||||||
// newJobsView builds the Jobs tab: list sidebar, details panel, and toolbar.
|
// newJobsView builds the Jobs tab: list sidebar, details panel, and toolbar.
|
||||||
// It returns the assembled panel and a refresh function the caller invokes
|
// It returns the assembled panel and a refresh function the caller invokes
|
||||||
// whenever the service state may have changed (e.g., from the event subscriber
|
// whenever the service state may have changed (e.g., from the event subscriber
|
||||||
@@ -75,7 +85,7 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) {
|
|||||||
// against the theme when it is placed inside a scroll container.
|
// against the theme when it is placed inside a scroll container.
|
||||||
commandOutputScroll.SetMinSize(fyne.NewSize(520, 160))
|
commandOutputScroll.SetMinSize(fyne.NewSize(520, 160))
|
||||||
|
|
||||||
selectedLogs := append([]event(nil), selectedRuntime.Logs...)
|
selectedLogs := lastJobLogs(selectedRuntime.Logs)
|
||||||
jobLogs := widget.NewList(
|
jobLogs := widget.NewList(
|
||||||
func() int { return len(selectedLogs) },
|
func() int { return len(selectedLogs) },
|
||||||
func() fyne.CanvasObject {
|
func() fyne.CanvasObject {
|
||||||
@@ -120,7 +130,7 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) {
|
|||||||
stateLabel.SetText(rt.LastState)
|
stateLabel.SetText(rt.LastState)
|
||||||
statsLabel.SetText(app.DisplayStats(rt))
|
statsLabel.SetText(app.DisplayStats(rt))
|
||||||
commandOutput.SetText(rt.Output)
|
commandOutput.SetText(rt.Output)
|
||||||
selectedLogs = append(selectedLogs[:0], rt.Logs...)
|
selectedLogs = lastJobLogs(rt.Logs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// list and folderSelect are declared early so closures below can reference
|
// list and folderSelect are declared early so closures below can reference
|
||||||
@@ -335,7 +345,10 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) {
|
|||||||
sidebarHeader := container.NewVBox(globalControls, widget.NewSeparator(), widget.NewLabelWithStyle("Folder", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), folderSelect, toolbar)
|
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)
|
sidebar := container.NewBorder(sidebarHeader, nil, nil, nil, list)
|
||||||
|
|
||||||
details := container.NewVBox(
|
// The details pane is a Border: the fixed metadata rows pin to the top, the
|
||||||
|
// activity panel pins to the bottom, and the command output fills whatever
|
||||||
|
// vertical space is left in between so long output stays readable.
|
||||||
|
topDetails := container.NewVBox(
|
||||||
title,
|
title,
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
detailRow("Folder", folderLabel),
|
detailRow("Folder", folderLabel),
|
||||||
@@ -349,11 +362,13 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) {
|
|||||||
detailRow("Statistics", statsLabel),
|
detailRow("Statistics", statsLabel),
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
widget.NewLabelWithStyle("Command output", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
widget.NewLabelWithStyle("Command output", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
commandOutputScroll,
|
)
|
||||||
|
activitySection := container.NewVBox(
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
widget.NewLabelWithStyle("Selected job activity", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
widget.NewLabelWithStyle("Selected job activity", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
jobLogs,
|
container.New(fixedHeightLayout{height: jobActivityHeight}, jobLogs),
|
||||||
)
|
)
|
||||||
|
details := container.NewBorder(topDetails, activitySection, nil, nil, commandOutputScroll)
|
||||||
|
|
||||||
fixedSidebar := container.New(minWidthLayout{width: minJobsSidebarWidth}, sidebar)
|
fixedSidebar := container.New(minWidthLayout{width: minJobsSidebarWidth}, sidebar)
|
||||||
panel := container.NewBorder(nil, nil, fixedSidebar, nil, container.NewPadded(details))
|
panel := container.NewBorder(nil, nil, fixedSidebar, nil, container.NewPadded(details))
|
||||||
@@ -361,6 +376,18 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// lastJobLogs returns a fresh slice of the most recent activity entries for the
|
||||||
|
// "Selected job activity" panel. Logs are stored newest-first (see
|
||||||
|
// app.Service.recordRun), so the leading entries are the latest; the result is
|
||||||
|
// capped at maxJobActivityRows.
|
||||||
|
func lastJobLogs(logs []event) []event {
|
||||||
|
n := len(logs)
|
||||||
|
if n > maxJobActivityRows {
|
||||||
|
n = maxJobActivityRows
|
||||||
|
}
|
||||||
|
return append([]event(nil), logs[:n]...)
|
||||||
|
}
|
||||||
|
|
||||||
func filteredJobIndexes(jobs []job, folder string) []int {
|
func filteredJobIndexes(jobs []job, folder string) []int {
|
||||||
indexes := make([]int, 0, len(jobs))
|
indexes := make([]int, 0, len(jobs))
|
||||||
for index, current := range jobs {
|
for index, current := range jobs {
|
||||||
|
|||||||
@@ -35,3 +35,33 @@ func (l minWidthLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
|
|||||||
object.Resize(size)
|
object.Resize(size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fixedHeightLayout forces its contents to a fixed height while leaving the
|
||||||
|
// width to the parent container. It is used to reserve a stable amount of space
|
||||||
|
// for the activity panel so a neighbouring widget can absorb the rest.
|
||||||
|
type fixedHeightLayout struct {
|
||||||
|
height float32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l fixedHeightLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
|
||||||
|
var width float32
|
||||||
|
for _, object := range objects {
|
||||||
|
if !object.Visible() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if min := object.MinSize(); min.Width > width {
|
||||||
|
width = min.Width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fyne.NewSize(width, l.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l fixedHeightLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
|
||||||
|
for _, object := range objects {
|
||||||
|
if !object.Visible() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
object.Move(fyne.NewPos(0, 0))
|
||||||
|
object.Resize(fyne.NewSize(size.Width, l.height))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user