Skip to content

Commit

Permalink
Generalize UI events for cataloging tasks (#2369)
Browse files Browse the repository at this point in the history
* generalize ui events for cataloging tasks

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* moderate review comments

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* incorporate review comments

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* rename cataloger task progress object

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* migrate cataloger task fn to bus helper

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
  • Loading branch information
wagoodman committed Nov 30, 2023
1 parent b943da6 commit 4adfbeb
Show file tree
Hide file tree
Showing 28 changed files with 427 additions and 807 deletions.
16 changes: 12 additions & 4 deletions cmd/syft/cli/ui/__snapshots__/handle_cataloger_task_test.snap
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@

[TestHandler_handleCatalogerTaskStarted/cataloging_task_in_progress - 1]
some task title [some value]
Cataloging contents
some task title ━━━━━━━━━━━━━━━━━━━━ [some stage]
---

[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_in_progress - 1]
└── some task title [some value]
Cataloging contents
└── ⠙ some task title ━━━━━━━━━━━━━━━━━━━━ [some stage]
---

[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_complete - 1]
✔ └── some task done [some value]
Cataloging contents
└── ✔ some task done [some stage]
---

[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_complete_with_removal - 1]
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_complete_--_hide_stage - 1]
Cataloging contents
└── ✔ some task done
---

[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_complete_with_removal - 1]
Cataloging contents
---
13 changes: 10 additions & 3 deletions cmd/syft/cli/ui/handle_attestation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,30 +100,37 @@ func TestHandler_handleAttestationStarted(t *testing.T) {
Height: 80,
}

models := handler.Handle(event)
models, _ := handler.Handle(event)
require.Len(t, models, 2)

t.Run("task line", func(t *testing.T) {
tsk, ok := models[0].(taskprogress.Model)
require.True(t, ok)

got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
gotModel := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
Time: time.Now(),
Sequence: tsk.Sequence(),
ID: tsk.ID(),
})

got := gotModel.View()

t.Log(got)
snaps.MatchSnapshot(t, got)
})

t.Run("log", func(t *testing.T) {
log, ok := models[1].(attestLogFrame)
require.True(t, ok)
got := runModel(t, log, tt.iterations, attestLogFrameTickMsg{

gotModel := runModel(t, log, tt.iterations, attestLogFrameTickMsg{
Time: time.Now(),
Sequence: log.sequence,
ID: log.id,
}, log.reader.running)

got := gotModel.View()

t.Log(got)
snaps.MatchSnapshot(t, got)
})
Expand Down
131 changes: 91 additions & 40 deletions cmd/syft/cli/ui/handle_cataloger_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,70 +3,121 @@ package ui
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/google/uuid"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"

"github.com/anchore/bubbly/bubbles/taskprogress"
"github.com/anchore/bubbly/bubbles/tree"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/event/monitor"
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
)

var _ progress.Stager = (*catalogerTaskStageAdapter)(nil)
// we standardize how rows are instantiated to ensure consistency in the appearance across the UI
type taskModelFactory func(title taskprogress.Title, opts ...taskprogress.Option) taskprogress.Model

type catalogerTaskStageAdapter struct {
mon *monitor.CatalogerTask
var _ tea.Model = (*catalogerTaskModel)(nil)

type catalogerTaskModel struct {
model tree.Model
modelFactory taskModelFactory
}

func newCatalogerTaskStageAdapter(mon *monitor.CatalogerTask) *catalogerTaskStageAdapter {
return &catalogerTaskStageAdapter{
mon: mon,
func newCatalogerTaskTreeModel(f taskModelFactory) *catalogerTaskModel {
t := tree.NewModel()
t.Padding = " "
t.RootsWithoutPrefix = true
return &catalogerTaskModel{
modelFactory: f,
model: t,
}
}

func (c catalogerTaskStageAdapter) Stage() string {
return c.mon.GetValue()
type newCatalogerTaskRowEvent struct {
info monitor.GenericTask
prog progress.StagedProgressable
}

func (m *Handler) handleCatalogerTaskStarted(e partybus.Event) []tea.Model {
mon, err := syftEventParsers.ParseCatalogerTaskStarted(e)
if err != nil {
log.WithFields("error", err).Warn("unable to parse event")
return nil
}
func (cts catalogerTaskModel) Init() tea.Cmd {
return cts.model.Init()
}

func (cts catalogerTaskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
event, ok := msg.(newCatalogerTaskRowEvent)
if !ok {
model, cmd := cts.model.Update(msg)
cts.model = model.(tree.Model)

var prefix string
if mon.SubStatus {
// TODO: support list of sub-statuses, not just a single leaf
prefix = "└── "
return cts, cmd
}

tsk := m.newTaskProgress(
info, prog := event.info, event.prog

tsk := cts.modelFactory(
taskprogress.Title{
// TODO: prefix should not be part of the title, but instead a separate field that is aware of the tree structure
Default: prefix + mon.Title,
Running: prefix + mon.Title,
Success: prefix + mon.TitleOnCompletion,
Default: info.Title.Default,
Running: info.Title.WhileRunning,
Success: info.Title.OnSuccess,
},
taskprogress.WithStagedProgressable(
struct {
progress.Stager
progress.Progressable
}{
Progressable: mon.GetMonitor(),
Stager: newCatalogerTaskStageAdapter(mon),
},
),
taskprogress.WithStagedProgressable(prog),
)

// TODO: this isn't ideal since the model stays around after it is no longer needed, but it works for now
tsk.HideOnSuccess = mon.RemoveOnCompletion
tsk.HideStageOnSuccess = false
tsk.HideProgressOnSuccess = false
if info.Context != "" {
tsk.Context = []string{info.Context}
}

tsk.HideOnSuccess = info.HideOnSuccess
tsk.HideStageOnSuccess = info.HideStageOnSuccess
tsk.HideProgressOnSuccess = true

if info.ParentID != "" {
tsk.TitleStyle = lipgloss.NewStyle()
}

tsk.TitleStyle = lipgloss.NewStyle()
// TODO: this is a hack to get the spinner to not show up, but ideally the component would support making the spinner optional
tsk.Spinner.Spinner.Frames = []string{" "}
if err := cts.model.Add(info.ParentID, info.ID, tsk); err != nil {
log.WithFields("error", err).Error("unable to add cataloger task to tree model")
}

return cts, tsk.Init()
}

func (cts catalogerTaskModel) View() string {
return cts.model.View()
}

func (m *Handler) handleCatalogerTaskStarted(e partybus.Event) ([]tea.Model, tea.Cmd) {
mon, info, err := syftEventParsers.ParseCatalogerTaskStarted(e)
if err != nil {
log.WithFields("error", err).Warn("unable to parse event")
return nil, nil
}

var models []tea.Model

// only create the new cataloger task tree once to manage all cataloger task events
m.onNewCatalogerTask.Do(func() {
models = append(models, newCatalogerTaskTreeModel(m.newTaskProgress))
})

// we need to update the cataloger task model with a new row. We should never update the model outside of the
// bubbletea update-render event loop. Instead, we return a command that will be executed by the bubbletea runtime,
// producing a message that is passed to the cataloger task model. This is the prescribed way to update models
// in bubbletea.

if info.ID == "" {
// ID is optional from the consumer perspective, but required internally
info.ID = uuid.Must(uuid.NewRandom()).String()
}

cmd := func() tea.Msg {
// this message will cause the cataloger task model to add a new row to the output based on the given task
// information and progress data.
return newCatalogerTaskRowEvent{
info: *info,
prog: mon,
}
}

return []tea.Model{tsk}
return models, cmd
}
Loading

0 comments on commit 4adfbeb

Please sign in to comment.