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 }