Skip to content

Commit 8965968

Browse files
Aerexsteveclay
andauthoredJan 28, 2025
Merge pull request #426 from IBM-Cloud/dev
* feat: added AlphaCommandsEnabled configuration * chore: move up dependencies (#423) * feat: increase token expiration check visibility (#424) * feat: used go-pretty to improve table formatting on screen width (#414) G * chore: bumped version to v1.6.0 * chore: updated golang.org/x/crypto to v0.32.0 --------- Co-authored-by: steveclay <steveclay@users.noreply.github.com>
2 parents 4a59214 + 9d8ea65 commit 8965968

File tree

9 files changed

+244
-70
lines changed

9 files changed

+244
-70
lines changed
 

‎bluemix/configuration/core_config/bx_config.go

+17-3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type BXConfigData struct {
5252
LastSessionUpdateTime int64
5353
Trace string
5454
ColorEnabled string
55+
AlphaCommandsEnabled string
5556
HTTPTimeout int
5657
TypeOfSSO string
5758
FallbackIAMTokens struct {
@@ -312,18 +313,18 @@ func (c *bxConfig) IAMID() (guid string) {
312313
func (c *bxConfig) IsLoggedIn() bool {
313314
if token, refresh := c.IAMToken(), c.IAMRefreshToken(); token != "" || refresh != "" {
314315
iamTokenInfo := NewIAMTokenInfo(token)
315-
if iamTokenInfo.hasExpired() && refresh != "" {
316+
if iamTokenInfo.HasExpired() && refresh != "" {
316317
repo := newRepository(c)
317318
if _, err := repo.RefreshIAMToken(); err != nil {
318319
return false
319320
}
320321
// Check again to make sure that the new token has not expired
321-
if iamTokenInfo = NewIAMTokenInfo(c.IAMToken()); iamTokenInfo.hasExpired() {
322+
if iamTokenInfo = NewIAMTokenInfo(c.IAMToken()); iamTokenInfo.HasExpired() {
322323
return false
323324
}
324325

325326
return true
326-
} else if iamTokenInfo.hasExpired() && refresh == "" {
327+
} else if iamTokenInfo.HasExpired() && refresh == "" {
327328
return false
328329
} else {
329330
return true
@@ -415,6 +416,13 @@ func (c *bxConfig) ColorEnabled() (enabled string) {
415416
return
416417
}
417418

419+
func (c *bxConfig) AlphaCommandsEnabled() (enabled string) {
420+
c.read(func() {
421+
enabled = c.data.AlphaCommandsEnabled
422+
})
423+
return
424+
}
425+
418426
func (c *bxConfig) TypeOfSSO() (style string) {
419427
c.read(func() {
420428
style = c.data.TypeOfSSO
@@ -723,6 +731,12 @@ func (c *bxConfig) SetColorEnabled(enabled string) {
723731
})
724732
}
725733

734+
func (c *bxConfig) SetAlphaCommandsEnabled(enabled string) {
735+
c.write(func() {
736+
c.data.AlphaCommandsEnabled = enabled
737+
})
738+
}
739+
726740
func (c *bxConfig) SetLocale(locale string) {
727741
c.write(func() {
728742
c.data.Locale = locale

‎bluemix/configuration/core_config/iam_token.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func (t IAMTokenInfo) exists() bool {
9393
return t.ID != ""
9494
}
9595

96-
func (t IAMTokenInfo) hasExpired() bool {
96+
func (t IAMTokenInfo) HasExpired() bool {
9797
if !t.exists() {
9898
return true
9999
}

‎bluemix/configuration/core_config/iam_token_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func TestIAMTokenHasExpired(t *testing.T) {
9898
for _, testCase := range TestIAMTokenHasExpiredTestCases {
9999
t.Run(testCase.name, func(t *testing.T) {
100100
tokenInfo := NewIAMTokenInfo(testCase.token)
101-
assert.Equal(t, testCase.isExpired, tokenInfo.hasExpired())
101+
assert.Equal(t, testCase.isExpired, tokenInfo.HasExpired())
102102
})
103103
}
104104
}

‎bluemix/configuration/core_config/repository.go

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type Repository interface {
5252
PluginRepo(string) (models.PluginRepo, bool)
5353
IsSSLDisabled() bool
5454
TypeOfSSO() string
55+
AlphaCommandsEnabled() string
5556
AssumedTrustedProfileId() string
5657
FallbackIAMToken() string
5758
FallbackIAMRefreshToken() string
@@ -118,6 +119,7 @@ type Repository interface {
118119
SetLocale(string)
119120
SetTrace(string)
120121
SetColorEnabled(string)
122+
SetAlphaCommandsEnabled(string)
121123

122124
CheckMessageOfTheDay() bool
123125
SetMessageOfTheDayTime()

‎bluemix/terminal/table.go

+146-37
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,18 @@ package terminal
33
import (
44
"encoding/csv"
55
"fmt"
6-
"io"
6+
"os"
7+
"strconv"
78
"strings"
89

10+
"golang.org/x/term"
11+
912
. "github.com/IBM-Cloud/ibm-cloud-cli-sdk/i18n"
13+
14+
"io"
15+
16+
"github.com/jedib0t/go-pretty/v6/table"
17+
"github.com/jedib0t/go-pretty/v6/text"
1018
"github.com/mattn/go-runewidth"
1119
)
1220

@@ -24,11 +32,10 @@ type Table interface {
2432
}
2533

2634
type PrintableTable struct {
27-
writer io.Writer
28-
headers []string
29-
headerPrinted bool
30-
maxSizes []int
31-
rows [][]string //each row is single line
35+
writer io.Writer
36+
headers []string
37+
maxSizes []int
38+
rows [][]string //each row is single line
3239
}
3340

3441
func NewTable(w io.Writer, headers []string) Table {
@@ -69,58 +76,160 @@ func (t *PrintableTable) Add(row ...string) {
6976
}
7077
}
7178

79+
func isWideColumn(col string) bool {
80+
// list of common columns that are usually wide
81+
largeColumnTypes := []string{T("ID"), T("Description")}
82+
83+
for _, largeColn := range largeColumnTypes {
84+
if strings.Contains(largeColn, col) {
85+
return true
86+
}
87+
}
88+
89+
return false
90+
91+
}
92+
93+
func terminalWidth() int {
94+
var err error
95+
terminalWidth, _, err := term.GetSize(int(os.Stdin.Fd()))
96+
97+
if err != nil {
98+
// Assume normal 80 char width line
99+
terminalWidth = 80
100+
}
101+
102+
testTerminalWidth, envSet := os.LookupEnv("TEST_TERMINAL_WIDTH")
103+
if envSet {
104+
envWidth, err := strconv.Atoi(testTerminalWidth)
105+
if err == nil {
106+
terminalWidth = envWidth
107+
}
108+
}
109+
return terminalWidth
110+
}
111+
72112
func (t *PrintableTable) Print() {
73113
for _, row := range append(t.rows, t.headers) {
74114
t.calculateMaxSize(row)
75115
}
76116

77-
if t.headerPrinted == false {
78-
t.printHeader()
79-
t.headerPrinted = true
117+
tbl := table.NewWriter()
118+
tbl.SetOutputMirror(t.writer)
119+
tbl.SuppressTrailingSpaces()
120+
// remove padding from the left to keep the table aligned to the left
121+
tbl.Style().Box.PaddingLeft = ""
122+
tbl.Style().Box.PaddingRight = strings.Repeat(" ", minSpace)
123+
// remove all border and column and row separators
124+
tbl.Style().Options.DrawBorder = false
125+
tbl.Style().Options.SeparateColumns = false
126+
tbl.Style().Options.SeparateFooter = false
127+
tbl.Style().Options.SeparateHeader = false
128+
tbl.Style().Options.SeparateRows = false
129+
tbl.Style().Format.Header = text.FormatDefault
130+
131+
headerRow, rows := t.createPrettyRowsAndHeaders()
132+
columnConfig := t.createColumnConfigs()
133+
134+
tbl.SetColumnConfigs(columnConfig)
135+
tbl.AppendHeader(headerRow)
136+
tbl.AppendRows(rows)
137+
tbl.Render()
138+
}
139+
140+
func (t *PrintableTable) createColumnConfigs() []table.ColumnConfig {
141+
// there must be at row in order to configure column
142+
if len(t.rows) == 0 {
143+
return []table.ColumnConfig{}
80144
}
81145

82-
for _, line := range t.rows {
83-
t.printRow(line)
146+
colCount := len(t.rows[0])
147+
var (
148+
widestColIndicies []int
149+
terminalWidth = terminalWidth()
150+
// total amount padding space that a row will take up
151+
totalPaddingSpace = (colCount - 1) * minSpace
152+
remainingSpace = max(0, terminalWidth-totalPaddingSpace)
153+
// the estimated max column width by dividing the remaining space evenly across the columns
154+
maxColWidth = remainingSpace / colCount
155+
)
156+
columnConfig := make([]table.ColumnConfig, colCount)
157+
158+
for colIndex := range columnConfig {
159+
columnConfig[colIndex] = table.ColumnConfig{
160+
AlignHeader: text.AlignLeft,
161+
Align: text.AlignLeft,
162+
WidthMax: maxColWidth,
163+
Number: colIndex + 1,
164+
}
165+
166+
// assuming the table has headers: store columns with wide content where the max width may need to be adjusted
167+
// using the remaining space
168+
if t.maxSizes[colIndex] > maxColWidth && (colIndex < len(t.headers) && isWideColumn(t.headers[colIndex])) {
169+
widestColIndicies = append(widestColIndicies, colIndex)
170+
} else if t.maxSizes[colIndex] < maxColWidth {
171+
// use the max column width instead of the estimated max column width
172+
// if it is shorter
173+
columnConfig[colIndex].WidthMax = t.maxSizes[colIndex]
174+
remainingSpace -= t.maxSizes[colIndex]
175+
} else {
176+
remainingSpace -= maxColWidth
177+
}
84178
}
85179

86-
t.rows = [][]string{}
87-
}
180+
// if only one wide column use the remaining space as the max column width
181+
if len(widestColIndicies) == 1 {
182+
widestColIndx := widestColIndicies[0]
183+
columnConfig[widestColIndx].WidthMax = remainingSpace
184+
}
88185

89-
func (t *PrintableTable) calculateMaxSize(row []string) {
90-
for index, value := range row {
91-
cellLength := runewidth.StringWidth(Decolorize(value))
92-
if t.maxSizes[index] < cellLength {
93-
t.maxSizes[index] = cellLength
186+
// if more than one wide column, spread the remaining space between the columns
187+
if len(widestColIndicies) > 1 {
188+
remainingSpace /= len(widestColIndicies)
189+
for _, columnCfgIdx := range widestColIndicies {
190+
columnConfig[columnCfgIdx].WidthMax = remainingSpace
191+
}
192+
193+
origRemainingSpace := remainingSpace
194+
moreRemainingSpace := origRemainingSpace % len(widestColIndicies)
195+
if moreRemainingSpace != 0 {
196+
columnConfig[0].WidthMax += moreRemainingSpace
94197
}
95198
}
199+
200+
return columnConfig
96201
}
97202

98-
func (t *PrintableTable) printHeader() {
99-
output := ""
100-
for col, value := range t.headers {
101-
output = output + t.cellValue(col, HeaderColor(value))
203+
func (t *PrintableTable) createPrettyRowsAndHeaders() (headerRow table.Row, rows []table.Row) {
204+
for _, header := range t.headers {
205+
headerRow = append(headerRow, header)
102206
}
103-
fmt.Fprintln(t.writer, output)
104-
}
105207

106-
func (t *PrintableTable) printRow(row []string) {
107-
output := ""
108-
for columnIndex, value := range row {
109-
if columnIndex == 0 {
110-
value = TableContentHeaderColor(value)
208+
for i := range t.rows {
209+
var row, emptyRow table.Row
210+
for j, cell := range t.rows[i] {
211+
if j == 0 {
212+
cell = TableContentHeaderColor(cell)
213+
}
214+
row = append(row, cell)
215+
emptyRow = append(emptyRow, "")
111216
}
112-
113-
output = output + t.cellValue(columnIndex, value)
217+
if i == 0 && len(t.headers) == 0 {
218+
rows = append(rows, emptyRow)
219+
}
220+
rows = append(rows, row)
114221
}
115-
fmt.Fprintln(t.writer, output)
222+
223+
return
116224
}
117225

118-
func (t *PrintableTable) cellValue(col int, value string) string {
119-
padding := ""
120-
if col < len(t.maxSizes)-1 {
121-
padding = strings.Repeat(" ", t.maxSizes[col]-runewidth.StringWidth(Decolorize(value))+minSpace)
226+
func (t *PrintableTable) calculateMaxSize(row []string) {
227+
for index, value := range row {
228+
cellLength := runewidth.StringWidth(Decolorize(value))
229+
if t.maxSizes[index] < cellLength {
230+
t.maxSizes[index] = cellLength
231+
}
122232
}
123-
return fmt.Sprintf("%s%s", value, padding)
124233
}
125234

126235
// Prints out a nicely/human formatted Json string instead of a table structure

‎bluemix/terminal/table_test.go

+45-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package terminal_test
22

33
import (
44
"bytes"
5+
"os"
56
"strings"
67
"testing"
78

@@ -38,7 +39,7 @@ func TestEmptyHeaderTable(t *testing.T) {
3839
testTable.Add("row1", "row2")
3940
testTable.Print()
4041
assert.Contains(t, buf.String(), "row1")
41-
assert.Equal(t, " \nrow1 row2\n", buf.String())
42+
assert.Equal(t, "\nrow1 row2\n", buf.String())
4243
}
4344

4445
func TestEmptyHeaderTableJson(t *testing.T) {
@@ -79,7 +80,49 @@ func TestNotEnoughRowEntires(t *testing.T) {
7980
testTable.Add("", "row2")
8081
testTable.Print()
8182
assert.Contains(t, buf.String(), "row1")
82-
assert.Equal(t, "col1 col2\nrow1 \n row2\n", buf.String())
83+
assert.Equal(t, "col1 col2\nrow1\n row2\n", buf.String())
84+
}
85+
86+
func TestMoreColThanTerminalWidth(t *testing.T) {
87+
os.Setenv("TEST_TERMINAL_WIDTH", "1")
88+
buf := bytes.Buffer{}
89+
testTable := NewTable(&buf, []string{"col1"})
90+
testTable.Add("row1", "row2")
91+
testTable.Print()
92+
assert.Contains(t, buf.String(), "row1")
93+
assert.Equal(t, "col1\nrow1 row2\n", buf.String())
94+
os.Unsetenv("TEST_TERMINAL_WIDTH")
95+
}
96+
97+
func TestWideHeaderNames(t *testing.T) {
98+
buf := bytes.Buffer{}
99+
testTable := NewTable(&buf, []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt u", "NAME"})
100+
testTable.Add("col1", "col2")
101+
testTable.Print()
102+
assert.Contains(t, buf.String(), "Lorem ipsum dolor sit amet, consectetu")
103+
assert.Equal(t, "Lorem ipsum dolor sit amet, consectetu NAME\nr adipiscing elit, sed do eiusmod temp\nor incididunt u\ncol1 col2\n", buf.String())
104+
}
105+
106+
func TestWidestColumn(t *testing.T) {
107+
buf := bytes.Buffer{}
108+
id := "ABCDEFG-9b8babbd-f2ed-4371-b817-a839e4130332"
109+
testTable := NewTable(&buf, []string{"ID", "Name"})
110+
testTable.Add(id, "row2")
111+
testTable.Print()
112+
assert.Contains(t, buf.String(), id)
113+
assert.Equal(t, buf.String(), "ID Name\nABCDEFG-9b8babbd-f2ed-4371-b817-a839e4130332 row2\n")
114+
}
115+
116+
func TestMultiWideColumns(t *testing.T) {
117+
buf := bytes.Buffer{}
118+
id := "ABCDEFG-9b8babbd-f2ed-4371-b817-a839e4130332"
119+
desc := "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut"
120+
testTable := NewTable(&buf, []string{"ID", "Description", "Name"})
121+
testTable.Add(id, desc, "col3")
122+
testTable.Print()
123+
assert.Contains(t, buf.String(), "ABCDEFG-9b8babbd-f2ed-4371-b817-a839")
124+
assert.Contains(t, buf.String(), "e4130332")
125+
assert.Equal(t, buf.String(), "ID Description Name\nABCDEFG-9b8babbd-f2ed-4371-b817-a839 Lorem ipsum dolor sit amet, consect col3\ne4130332 etur adipiscing elit, sed do eiusmo\n d tempor incididunt ut\n")
83126
}
84127

85128
func TestNotEnoughRowEntiresJson(t *testing.T) {

‎bluemix/version.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package bluemix
33
import "fmt"
44

55
// Version is the SDK version
6-
var Version = VersionType{Major: 1, Minor: 5, Build: 0}
6+
var Version = VersionType{Major: 1, Minor: 6, Build: 0}
77

88
// VersionType describe version info
99
type VersionType struct {

0 commit comments

Comments
 (0)
Failed to load comments.