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:
mixeme
2026-06-24 20:36:41 +03:00
parent 07c0995935
commit cf276fc8ec
2 changed files with 62 additions and 5 deletions
+32 -5
View File
@@ -19,6 +19,16 @@ const allFolders = "All"
const noFolder = "No folder"
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.
// 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
@@ -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.
commandOutputScroll.SetMinSize(fyne.NewSize(520, 160))
selectedLogs := append([]event(nil), selectedRuntime.Logs...)
selectedLogs := lastJobLogs(selectedRuntime.Logs)
jobLogs := widget.NewList(
func() int { return len(selectedLogs) },
func() fyne.CanvasObject {
@@ -120,7 +130,7 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) {
stateLabel.SetText(rt.LastState)
statsLabel.SetText(app.DisplayStats(rt))
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
@@ -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)
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,
widget.NewSeparator(),
detailRow("Folder", folderLabel),
@@ -349,11 +362,13 @@ func newJobsView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func()) {
detailRow("Statistics", statsLabel),
widget.NewSeparator(),
widget.NewLabelWithStyle("Command output", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
commandOutputScroll,
)
activitySection := container.NewVBox(
widget.NewSeparator(),
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)
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 {
indexes := make([]int, 0, len(jobs))
for index, current := range jobs {
+30
View File
@@ -35,3 +35,33 @@ func (l minWidthLayout) Layout(objects []fyne.CanvasObject, size fyne.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))
}
}