From 215f5f740b817ea426df49ceddd9bed77bf5f8f8 Mon Sep 17 00:00:00 2001 From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Date: Tue, 12 Dec 2023 11:43:41 -0500 Subject: [PATCH] feat: small ui for list and check (#23) Signed-off-by: Christopher Phillips --- cmd/grant/cli/command/check.go | 22 +++++ cmd/grant/cli/command/list.go | 26 +++++- cmd/grant/cli/internal/check/report.go | 13 ++- cmd/grant/cli/tui/handle_check.go | 109 ----------------------- cmd/grant/cli/tui/handle_task_started.go | 37 ++++++++ cmd/grant/cli/tui/handler.go | 2 +- event/parsers.go | 14 ++- go.mod | 4 +- go.sum | 4 + grant/evalutation/result.go | 8 ++ 10 files changed, 117 insertions(+), 122 deletions(-) delete mode 100644 cmd/grant/cli/tui/handle_check.go create mode 100644 cmd/grant/cli/tui/handle_task_started.go diff --git a/cmd/grant/cli/command/check.go b/cmd/grant/cli/command/check.go index 672d2e0..724e778 100644 --- a/cmd/grant/cli/command/check.go +++ b/cmd/grant/cli/command/check.go @@ -12,7 +12,9 @@ import ( "github.com/anchore/clio" "github.com/anchore/grant/cmd/grant/cli/internal/check" "github.com/anchore/grant/cmd/grant/cli/option" + "github.com/anchore/grant/event" "github.com/anchore/grant/grant" + "github.com/anchore/grant/internal/bus" "github.com/anchore/grant/internal/input" ) @@ -87,6 +89,25 @@ func runCheck(cfg *CheckConfig, userInput []string) (errs error) { return errors.Wrap(err, fmt.Sprintf("could not check licenses; could not build rules from config: %s", cfg.Config)) } + monitor := bus.PublishTask( + event.Title{ + Default: "Check licenses", + WhileRunning: "Checking licenses", + OnSuccess: "Checked licenses", + }, + "", + len(userInput), + ) + + defer func() { + if errs != nil { + monitor.SetError(errs) + } else { + monitor.AtomicStage.Set(strings.Join(userInput, ", ")) + monitor.SetCompleted() + } + }() + policy, err := grant.NewPolicy(cfg.CheckNonSPDX, rules...) if err != nil { return errors.Wrap(err, fmt.Sprintf("could not check licenses; could not build policy from config: %s", cfg.Config)) @@ -98,6 +119,7 @@ func runCheck(cfg *CheckConfig, userInput []string) (errs error) { ShowPackages: cfg.ShowPackages, CheckNonSPDX: cfg.CheckNonSPDX, OsiApproved: cfg.OsiApproved, + Monitor: monitor, } rep, err := check.NewReport(reportConfig, userInput...) if err != nil { diff --git a/cmd/grant/cli/command/list.go b/cmd/grant/cli/command/list.go index 6e69310..64b78d6 100644 --- a/cmd/grant/cli/command/list.go +++ b/cmd/grant/cli/command/list.go @@ -2,13 +2,16 @@ package command import ( "slices" + "strings" "github.com/spf13/cobra" "github.com/anchore/clio" "github.com/anchore/grant/cmd/grant/cli/internal/check" "github.com/anchore/grant/cmd/grant/cli/option" + "github.com/anchore/grant/event" "github.com/anchore/grant/grant" + "github.com/anchore/grant/internal/bus" "github.com/anchore/grant/internal/input" ) @@ -38,7 +41,7 @@ func List(app clio.Application) *cobra.Command { }, cfg) } -func runList(cfg *ListConfig, userInput []string) error { +func runList(cfg *ListConfig, userInput []string) (errs error) { // check if user provided source by stdin // note: cat sbom.json | grant check spdx.json - is supported // it will generate results for both stdin and spdx.json @@ -46,11 +49,32 @@ func runList(cfg *ListConfig, userInput []string) error { if isStdin && !slices.Contains(userInput, "-") { userInput = append(userInput, "-") } + + monitor := bus.PublishTask( + event.Title{ + Default: "List licenses", + WhileRunning: "Looking up licenses", + OnSuccess: "Found licenses", + }, + "", + len(userInput), + ) + + defer func() { + if errs != nil { + monitor.SetError(errs) + } else { + monitor.AtomicStage.Set(strings.Join(userInput, ", ")) + monitor.SetCompleted() + } + }() + reportConfig := check.ReportConfig{ Format: check.Format(cfg.Format), ShowPackages: cfg.ShowPackages, CheckNonSPDX: cfg.CheckNonSPDX, Policy: grant.DefaultPolicy(), + Monitor: monitor, } rep, err := check.NewReport(reportConfig, userInput...) if err != nil { diff --git a/cmd/grant/cli/internal/check/report.go b/cmd/grant/cli/internal/check/report.go index 4049f5e..35a1924 100644 --- a/cmd/grant/cli/internal/check/report.go +++ b/cmd/grant/cli/internal/check/report.go @@ -2,11 +2,13 @@ package check import ( "errors" + "strings" "time" "github.com/gookit/color" list "github.com/jedib0t/go-pretty/v6/list" + "github.com/anchore/grant/event" "github.com/anchore/grant/grant" "github.com/anchore/grant/grant/evalutation" "github.com/anchore/grant/internal/bus" @@ -23,6 +25,7 @@ type Report struct { Results evalutation.Results Config ReportConfig Timestamp string + Monitor *event.ManualStagedProgress errors []error } @@ -32,6 +35,7 @@ type ReportConfig struct { ShowPackages bool CheckNonSPDX bool OsiApproved bool + Monitor *event.ManualStagedProgress } // NewReport will generate a new report for the given format. @@ -59,6 +63,7 @@ func NewReport(rc ReportConfig, userRequests ...string) (*Report, error) { Results: results, Config: rc, Timestamp: time.Now().Format(time.RFC3339), + Monitor: rc.Monitor, }, nil } @@ -86,6 +91,8 @@ func (r *Report) RenderList() error { func (r *Report) renderCheckTree() error { var uiLists []list.Writer for _, res := range r.Results { + r.Monitor.Increment() + r.Monitor.AtomicStage.Set(res.Case.UserInput) resulList := newList() uiLists = append(uiLists, resulList) resulList.AppendItem(color.Primary.Sprintf("%s", res.Case.UserInput)) @@ -118,9 +125,10 @@ func (r *Report) renderCheckTree() error { renderOrphanPackages(resulList, res, false) // keep primary coloring for tree } } - + r.Monitor.AtomicStage.Set(strings.Join(r.Results.UserInputs(), ", ")) // segment the results into lists by user input // lists can optionally show the packages that were evaluated + for _, l := range uiLists { bus.Report(l.Render()) } @@ -130,6 +138,8 @@ func (r *Report) renderCheckTree() error { func (r *Report) renderList() error { var uiLists []list.Writer for _, res := range r.Results { + r.Monitor.Increment() + r.Monitor.AtomicStage.Set(res.Case.UserInput) resulList := newList() uiLists = append(uiLists, resulList) resulList.AppendItem(color.Primary.Sprintf("%s", res.Case.UserInput)) @@ -152,6 +162,7 @@ func (r *Report) renderList() error { renderOrphanPackages(resulList, res, true) } } + r.Monitor.AtomicStage.Set(strings.Join(r.Results.UserInputs(), ", ")) // segment the results into lists by user input // lists can optionally show the packages that were evaluated diff --git a/cmd/grant/cli/tui/handle_check.go b/cmd/grant/cli/tui/handle_check.go deleted file mode 100644 index 6c94794..0000000 --- a/cmd/grant/cli/tui/handle_check.go +++ /dev/null @@ -1,109 +0,0 @@ -package tui - -import ( - "strings" - "time" - - "github.com/charmbracelet/bubbles/spinner" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/go-progress" - - "github.com/anchore/grant/event" - "github.com/anchore/grant/internal/log" -) - -var _ tea.Model = (*checkViewModel)(nil) - -func (m *Handler) handleCLICheckCmdStarted(e partybus.Event) ([]tea.Model, tea.Cmd) { - sourceNames, prog, err := event.ParseCheckCommandStarted(e) - if err != nil { - log.WithFields("error", err).Warn("unable to parse event") - return nil, nil - } - - return []tea.Model{newCheckViewModel(sourceNames, prog, m.WindowSize)}, nil -} - -type checkViewModel struct { - SourceNames []string - Total progress.StagedProgressable - Progress map[string]progress.StagedProgressable - - WindowSize tea.WindowSizeMsg - Spinner spinner.Model - - SourceNameStyle lipgloss.Style - TitleStyle lipgloss.Style - WaitingStyle lipgloss.Style - CheckingStyle lipgloss.Style - DoneStyle lipgloss.Style - ErrorStyle lipgloss.Style -} - -func newCheckViewModel(sourceNames []string, total progress.StagedProgressable, windowSize tea.WindowSizeMsg) checkViewModel { - padding := 0 - for _, name := range sourceNames { - if len(name) > padding { - padding = len(name) - } - } - - return checkViewModel{ - SourceNames: sourceNames, - Total: total, - Progress: make(map[string]progress.StagedProgressable), - - Spinner: spinner.New( - spinner.WithSpinner( - // matches the same spinner as syft/grype - spinner.Spinner{ - Frames: strings.Split("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏", ""), - FPS: 150 * time.Millisecond, - }, - ), - spinner.WithStyle( - lipgloss.NewStyle().Foreground(lipgloss.Color("13")), // 13 = high intensity magenta (ANSI 16-bit color code) - ), - ), - - WindowSize: windowSize, - - SourceNameStyle: lipgloss.NewStyle().Width(padding), - TitleStyle: lipgloss.NewStyle().Bold(true), - WaitingStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")), - CheckingStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("214")), - DoneStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("10")), - ErrorStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("9")), - } -} - -func (m checkViewModel) Init() tea.Cmd { - return nil -} - -func (m checkViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.WindowSize = msg - return m, nil - case spinner.TickMsg: - spinModel, spinCmd := m.Spinner.Update(msg) - m.Spinner = spinModel - return m, spinCmd - case partybus.Event: - log.WithFields("component", "ui").Tracef("event: %q", msg.Type) - // TODO: handle source check started event - } - - return m, nil -} - -func (m checkViewModel) View() string { - isCompleted := progress.IsCompleted(m.Total) - if isCompleted { - return "done" - } - return "not done" -} diff --git a/cmd/grant/cli/tui/handle_task_started.go b/cmd/grant/cli/tui/handle_task_started.go new file mode 100644 index 0000000..193c316 --- /dev/null +++ b/cmd/grant/cli/tui/handle_task_started.go @@ -0,0 +1,37 @@ +package tui + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/wagoodman/go-partybus" + + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grant/event" +) + +func (m *Handler) handleTaskStarted(e partybus.Event) ([]tea.Model, tea.Cmd) { + cmd, prog, err := event.ParseTaskStarted(e) + if err != nil { + //log.Warnf("unable to parse event: %+v", err) + return nil, nil + } + + tsk := taskprogress.New( + m.Running, + taskprogress.WithStagedProgressable(prog), + ) + + tsk.HideProgressOnSuccess = true + tsk.HideOnSuccess = true + tsk.TitleWidth = len(cmd.Title.WhileRunning) + tsk.HintEndCaps = nil + tsk.TitleOptions = taskprogress.Title{ + Default: cmd.Title.Default, + Running: cmd.Title.WhileRunning, + Success: cmd.Title.OnSuccess, + Failed: cmd.Title.OnFail, + } + tsk.Context = []string{cmd.Context} + tsk.WindowSize = m.WindowSize + + return []tea.Model{tsk}, nil +} diff --git a/cmd/grant/cli/tui/handler.go b/cmd/grant/cli/tui/handler.go index 25ccda1..6a464cc 100644 --- a/cmd/grant/cli/tui/handler.go +++ b/cmd/grant/cli/tui/handler.go @@ -45,7 +45,7 @@ func New(cfg HandlerConfig) *Handler { // register all supported event types with the respective handler functions d.AddHandlers(map[partybus.EventType]bubbly.EventHandlerFn{ - event.CLICheckCmdStarted: h.handleCLICheckCmdStarted, + event.TaskStartedEvent: h.handleTaskStarted, }) return h diff --git a/event/parsers.go b/event/parsers.go index f924294..efc1eb2 100644 --- a/event/parsers.go +++ b/event/parsers.go @@ -32,26 +32,22 @@ func checkEventType(actual, expected partybus.EventType) error { return nil } -func ParseCheckCommandStarted(e partybus.Event) ([]string, progress.StagedProgressable, error) { - if err := checkEventType(e.Type, CLICheckCmdStarted); err != nil { +func ParseTaskStarted(e partybus.Event) (*Task, progress.StagedProgressable, error) { + if err := checkEventType(e.Type, TaskStartedEvent); err != nil { return nil, nil, err } - return parseSourcesAndStagedProgressable(e) -} - -func parseSourcesAndStagedProgressable(e partybus.Event) ([]string, progress.StagedProgressable, error) { - sources, ok := e.Source.([]string) + cmd, ok := e.Source.(Task) if !ok { return nil, nil, newPayloadErr(e.Type, "Source", e.Source) } - prog, ok := e.Value.(progress.StagedProgressable) + p, ok := e.Value.(progress.StagedProgressable) if !ok { return nil, nil, newPayloadErr(e.Type, "Value", e.Value) } - return sources, prog, nil + return &cmd, p, nil } func ParseCLIReport(e partybus.Event) (string, string, error) { diff --git a/go.mod b/go.mod index 2746ee3..86a10df 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/anchore/clio v0.0.0-20231128152715-767f62261f13 github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a github.com/anchore/syft v0.98.0 - github.com/charmbracelet/bubbles v0.16.1 github.com/charmbracelet/bubbletea v0.24.2 github.com/charmbracelet/lipgloss v0.9.1 github.com/github/go-spdx/v2 v2.2.0 @@ -36,6 +35,7 @@ require ( github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/hcsshim v0.11.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/acobaugh/osrelease v0.1.0 // indirect github.com/adrg/xdg v0.4.0 // indirect github.com/anchore/fangs v0.0.0-20231103141714-84c94dc43a2e // indirect @@ -49,6 +49,8 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/becheran/wildmatch-go v1.0.0 // indirect github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect + github.com/charmbracelet/bubbles v0.16.1 // indirect + github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/cloudflare/circl v1.3.3 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect diff --git a/go.sum b/go.sum index 3210c3f..dd93e1d 100644 --- a/go.sum +++ b/go.sum @@ -73,6 +73,8 @@ github.com/Microsoft/hcsshim v0.11.1/go.mod h1:nFJmaO4Zr5Y7eADdFOpYswDDlNVbvcIJJ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE= github.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzdVasCEGHTWY= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= @@ -141,6 +143,8 @@ github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5 github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= diff --git a/grant/evalutation/result.go b/grant/evalutation/result.go index e076e15..ea93411 100644 --- a/grant/evalutation/result.go +++ b/grant/evalutation/result.go @@ -37,6 +37,14 @@ func (rs Results) IsFailed() bool { return false } +func (rs Results) UserInputs() []string { + inputs := make([]string, 0) + for _, r := range rs { + inputs = append(inputs, r.Case.UserInput) + } + return inputs +} + // GetFailedEvaluations returns a map of user input to slice of failed license evaluations for that input func (rs Results) GetFailedEvaluations(userInput string, rule grant.Rule) LicenseEvaluations { failed := make(LicenseEvaluations, 0)