github.com/google/osv-scalibr@v0.4.1/guidedremediation/internal/tui/components/in_place_info.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package components
    16  
    17  import (
    18  	"fmt"
    19  	"slices"
    20  
    21  	"github.com/charmbracelet/bubbles/key"
    22  	"github.com/charmbracelet/bubbles/table"
    23  	tea "github.com/charmbracelet/bubbletea"
    24  	"github.com/charmbracelet/lipgloss"
    25  	"github.com/google/osv-scalibr/guidedremediation/internal/resolution"
    26  	"github.com/google/osv-scalibr/guidedremediation/result"
    27  )
    28  
    29  // inPlaceInfo is a ViewModel showing the table of package upgrades and fixed vulnerabilities,
    30  // for in-place upgrades.
    31  // Pressing 'enter' on a row shows the vulnerability details.
    32  type inPlaceInfo struct {
    33  	table.Model
    34  
    35  	vulns           []resolution.Vulnerability
    36  	detailsRenderer DetailsRenderer // renderer for markdown details.
    37  	currVulnInfo    ViewModel
    38  
    39  	width  int
    40  	height int
    41  }
    42  
    43  // NewInPlaceInfo creates a ViewModel showing the table of package upgrades and fixed vulnerabilities,
    44  // for in-place upgrades.
    45  func NewInPlaceInfo(patches []result.Patch, vulns []resolution.Vulnerability, detailsRenderer DetailsRenderer) ViewModel {
    46  	info := inPlaceInfo{
    47  		vulns:           vulns,
    48  		detailsRenderer: detailsRenderer,
    49  		width:           ViewMinWidth,
    50  		height:          ViewMinHeight,
    51  	}
    52  
    53  	cols := []table.Column{
    54  		{Title: "PACKAGE"},
    55  		{Title: "VERSION CHANGE"},
    56  		{Title: "FIXED VULN"},
    57  	}
    58  	for i := range cols {
    59  		cols[i].Width = lipgloss.Width(cols[i].Title)
    60  	}
    61  
    62  	// Have 1 row per vulnerability, but only put the package name on the first vuln it fixes.
    63  	rows := make([]table.Row, 0, len(vulns))
    64  	for _, p := range patches {
    65  		row := table.Row{
    66  			p.PackageUpdates[0].Name,
    67  			fmt.Sprintf("%s → %s", p.PackageUpdates[0].VersionFrom, p.PackageUpdates[0].VersionTo),
    68  			p.Fixed[0].ID,
    69  		}
    70  		// Set each column to their widest element
    71  		for i, s := range row {
    72  			if w := lipgloss.Width(s); w > cols[i].Width {
    73  				cols[i].Width = w
    74  			}
    75  		}
    76  		rows = append(rows, row)
    77  
    78  		// use blank package name / bump for other vulns from same patch
    79  		for _, v := range p.Fixed[1:] {
    80  			row := table.Row{
    81  				"",
    82  				"",
    83  				v.ID,
    84  			}
    85  			rows = append(rows, row)
    86  			if w := lipgloss.Width(row[2]); w > cols[2].Width {
    87  				cols[2].Width = w
    88  			}
    89  		}
    90  	}
    91  
    92  	// center the version change column
    93  	cols[1].Title = lipgloss.PlaceHorizontal(cols[1].Width, lipgloss.Center, cols[1].Title)
    94  	for _, row := range rows {
    95  		row[1] = lipgloss.PlaceHorizontal(cols[1].Width, lipgloss.Center, row[1])
    96  	}
    97  
    98  	st := table.DefaultStyles()
    99  	st.Header = st.Header.Bold(false).BorderStyle(lipgloss.NormalBorder()).BorderBottom(true)
   100  	st.Selected = st.Selected.Foreground(ColorPrimary)
   101  
   102  	info.Model = table.New(
   103  		table.WithColumns(cols),
   104  		table.WithRows(rows),
   105  		table.WithWidth(info.width),
   106  		table.WithHeight(info.height),
   107  		table.WithFocused(true),
   108  		table.WithStyles(st),
   109  		table.WithKeyMap(table.KeyMap{
   110  			LineUp:   Keys.Up,
   111  			LineDown: Keys.Down,
   112  			PageUp:   Keys.Left,
   113  			PageDown: Keys.Right,
   114  		}),
   115  	)
   116  
   117  	return &info
   118  }
   119  
   120  func (ip *inPlaceInfo) Resize(w, h int) ViewModel {
   121  	ip.width = w
   122  	ip.height = h
   123  	ip.SetWidth(w)
   124  	ip.SetHeight(h)
   125  	if ip.currVulnInfo != nil {
   126  		ip.currVulnInfo.Resize(w, h)
   127  	}
   128  
   129  	return ip
   130  }
   131  
   132  func (ip *inPlaceInfo) Update(msg tea.Msg) (ViewModel, tea.Cmd) {
   133  	var cmd tea.Cmd
   134  	if ip.currVulnInfo != nil {
   135  		ip.currVulnInfo, cmd = ip.currVulnInfo.Update(msg)
   136  		return ip, cmd
   137  	}
   138  	if msg, ok := msg.(tea.KeyMsg); ok {
   139  		switch {
   140  		case key.Matches(msg, Keys.Quit):
   141  			return ip, CloseViewModel
   142  		case key.Matches(msg, Keys.Select):
   143  			vID := ip.Rows()[ip.Cursor()][2]
   144  			vIdx := slices.IndexFunc(ip.vulns, func(v resolution.Vulnerability) bool { return v.OSV.Id == vID })
   145  			if vIdx == -1 {
   146  				// something went wrong, just ignore this.
   147  				return ip, nil
   148  			}
   149  			vuln := ip.vulns[vIdx]
   150  			ip.currVulnInfo = NewVulnInfo(vuln, ip.detailsRenderer)
   151  			ip.currVulnInfo = ip.currVulnInfo.Resize(ip.Width(), ip.Height())
   152  
   153  			return ip, nil
   154  		}
   155  	}
   156  	ip.Model, cmd = ip.Model.Update(msg)
   157  
   158  	return ip, cmd
   159  }
   160  
   161  func (ip *inPlaceInfo) View() string {
   162  	if ip.currVulnInfo != nil {
   163  		return ip.currVulnInfo.View()
   164  	}
   165  	// place the table in the center of the view
   166  	return lipgloss.Place(ip.width, ip.height, lipgloss.Center, lipgloss.Center, ip.Model.View())
   167  }
   168  
   169  // GetModel returns the underlying table model.
   170  // This is used by the in-place patches state to get the table model.
   171  func (ip *inPlaceInfo) GetModel() table.Model {
   172  	return ip.Model
   173  }