Skip to content

Commit adc1627

Browse files
digocorbelliniRodrigo Okamoto
and
Rodrigo Okamoto
authored
Interactive table sorting, trimming, and filtering (#152)
* implemented filtering * styled filter text input * fixed bug with filtering and then going to verbose view * fixed bug with expanding with no rows selected * working trim feature * fixed trim while filtering bug * moved sorting shorthands to sorter.go * created sorting view (Non fuctional) * fixed row selection toggle * added sorting by json path * added sorting by shorthand values * added sort direction toggle * updated third party licenses * moved all sorting constants to sorter.go * removed unecessary print in sortingView view() * renamed initSortingView to initSortingModel * use 'esc' instead of 'e' to exit verbose view * Added interactive output demo to readme Co-authored-by: Rodrigo Okamoto <rodocp@amazon.com>
1 parent 98c3555 commit adc1627

File tree

10 files changed

+683
-106
lines changed

10 files changed

+683
-106
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@ t3.medium 2 4 nitro true true
147147
t3a.medium 2 4 nitro true true x86_64 Up to 5 Gigabit 3 0 0 none -Not Fetched- $0.01246
148148
```
149149

150+
**Interactive Output**
151+
```
152+
$ ec2-instance-selector -o interactive
153+
```
154+
https://user-images.githubusercontent.com/68402662/184218343-6b236d4a-3fe6-42ae-9fe3-3fd3ee92a4b5.mov
155+
150156
**Sort by memory in ascending order using shorthand**
151157
```
152158
$ ec2-instance-selector -r us-east-1 -o table-wide --max-results 10 --sort-by memory --sort-direction asc

THIRD_PARTY_LICENSES

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,6 +1445,33 @@ furnished to do so, subject to the following conditions:
14451445
The above copyright notice and this permission notice shall be included in all
14461446
copies or substantial portions of the Software.
14471447

1448+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1449+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1450+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1451+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1452+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1453+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1454+
SOFTWARE.
1455+
1456+
------
1457+
1458+
** github.com/sahilm/fuzzy; v0.1.0 --
1459+
https://github.com/sahilm/fuzzy
1460+
1461+
The MIT License (MIT)
1462+
1463+
Copyright (c) 2017 Sahil Muthoo
1464+
1465+
Permission is hereby granted, free of charge, to any person obtaining a copy
1466+
of this software and associated documentation files (the "Software"), to deal
1467+
in the Software without restriction, including without limitation the rights
1468+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1469+
copies of the Software, and to permit persons to whom the Software is
1470+
furnished to do so, subject to the following conditions:
1471+
1472+
The above copyright notice and this permission notice shall be included in all
1473+
copies or substantial portions of the Software.
1474+
14481475
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14491476
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14501477
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

cmd/main.go

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ const (
4949
tableWideOutput = "table-wide"
5050
oneLine = "one-line"
5151
bubbleTeaOutput = "interactive"
52+
53+
// Sort filter default
54+
instanceNamePath = ".InstanceType"
5255
)
5356

5457
// Filter Flag Constants
@@ -120,33 +123,6 @@ const (
120123
sortBy = "sort-by"
121124
)
122125

123-
// Sorting Constants
124-
const (
125-
// Direction
126-
127-
sortAscending = "ascending"
128-
sortAsc = "asc"
129-
sortDescending = "descending"
130-
sortDesc = "desc"
131-
132-
// Sorting Fields
133-
spotPrice = "spot-price"
134-
odPrice = "on-demand-price"
135-
136-
// JSON field paths
137-
instanceNamePath = ".InstanceType"
138-
vcpuPath = ".VCpuInfo.DefaultVCpus"
139-
memoryPath = ".MemoryInfo.SizeInMiB"
140-
gpuMemoryTotalPath = ".GpuInfo.TotalGpuMemoryInMiB"
141-
networkInterfacesPath = ".NetworkInfo.MaximumNetworkInterfaces"
142-
spotPricePath = ".SpotPrice"
143-
odPricePath = ".OndemandPricePerHour"
144-
instanceStoragePath = ".InstanceStorageInfo.TotalSizeInGB"
145-
ebsOptimizedBaselineBandwidthPath = ".EbsInfo.EbsOptimizedInfo.BaselineBandwidthInMbps"
146-
ebsOptimizedBaselineThroughputPath = ".EbsInfo.EbsOptimizedInfo.BaselineThroughputInMBps"
147-
ebsOptimizedBaselineIOPSPath = ".EbsInfo.EbsOptimizedInfo.BaselineIops"
148-
)
149-
150126
var (
151127
// versionID is overridden at compilation with the version based on the git tag
152128
versionID = "dev"
@@ -177,26 +153,10 @@ Full docs can be found at github.com/aws/amazon-` + binName
177153
resultsOutputFn := outputs.SimpleInstanceTypeOutput
178154

179155
cliSortDirections := []string{
180-
sortAscending,
181-
sortAsc,
182-
sortDescending,
183-
sortDesc,
184-
}
185-
186-
// map quantity cli flags to json paths for easier cli sorting
187-
sortingKeysMap := map[string]string{
188-
vcpus: vcpuPath,
189-
memory: memoryPath,
190-
gpuMemoryTotal: gpuMemoryTotalPath,
191-
networkInterfaces: networkInterfacesPath,
192-
spotPrice: spotPricePath,
193-
odPrice: odPricePath,
194-
instanceStorage: instanceStoragePath,
195-
ebsOptimizedBaselineBandwidth: ebsOptimizedBaselineBandwidthPath,
196-
ebsOptimizedBaselineThroughput: ebsOptimizedBaselineThroughputPath,
197-
ebsOptimizedBaselineIOPS: ebsOptimizedBaselineIOPSPath,
198-
gpus: gpus,
199-
inferenceAccelerators: inferenceAccelerators,
156+
sorter.SortAscending,
157+
sorter.SortAsc,
158+
sorter.SortDescending,
159+
sorter.SortDesc,
200160
}
201161

202162
// Registers flags with specific input types from the cli pkg
@@ -263,7 +223,7 @@ Full docs can be found at github.com/aws/amazon-` + binName
263223
cli.ConfigBoolFlag(verbose, cli.StringMe("v"), nil, "Verbose - will print out full instance specs")
264224
cli.ConfigBoolFlag(help, cli.StringMe("h"), nil, "Help")
265225
cli.ConfigBoolFlag(version, nil, nil, "Prints CLI version")
266-
cli.ConfigStringOptionsFlag(sortDirection, nil, cli.StringMe(sortAscending), fmt.Sprintf("Specify the direction to sort in (%s)", strings.Join(cliSortDirections, ", ")), cliSortDirections)
226+
cli.ConfigStringOptionsFlag(sortDirection, nil, cli.StringMe(sorter.SortAscending), fmt.Sprintf("Specify the direction to sort in (%s)", strings.Join(cliSortDirections, ", ")), cliSortDirections)
267227
cli.ConfigStringFlag(sortBy, nil, cli.StringMe(instanceNamePath), "Specify the field to sort by. Quantity flags present in this CLI (memory, gpus, etc.) or a JSON path to the appropriate instance type field (Ex: \".MemoryInfo.SizeInMiB\") is acceptable.", nil)
268228

269229
// Parses the user input with the registered flags and runs type specific validation on the user input
@@ -419,11 +379,6 @@ Full docs can be found at github.com/aws/amazon-` + binName
419379
}
420380
}
421381

422-
// determine if user used a shorthand for sorting flag
423-
if sortFieldShorthandPath, ok := sortingKeysMap[*sortField]; ok {
424-
sortField = &sortFieldShorthandPath
425-
}
426-
427382
// fetch instance types without truncating results
428383
prevMaxResults := filters.MaxResults
429384
filters.MaxResults = nil

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.18
55
require (
66
github.com/aws/aws-sdk-go v1.44.59
77
github.com/blang/semver/v4 v4.0.0
8-
github.com/charmbracelet/bubbles v0.11.0
8+
github.com/charmbracelet/bubbles v0.13.0
99
github.com/charmbracelet/bubbletea v0.21.0
1010
github.com/charmbracelet/lipgloss v0.5.0
1111
github.com/evertras/bubble-table v0.14.4
@@ -32,6 +32,7 @@ require (
3232
github.com/muesli/cancelreader v0.2.0 // indirect
3333
github.com/muesli/reflow v0.3.0 // indirect
3434
github.com/rivo/uniseg v0.2.0 // indirect
35+
github.com/sahilm/fuzzy v0.1.0 // indirect
3536
github.com/smartystreets/goconvey v1.6.4 // indirect
3637
go.uber.org/atomic v1.4.0 // indirect
3738
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2y
1515
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
1616
github.com/charmbracelet/bubbles v0.11.0 h1:fBLyY0PvJnd56Vlu5L84JJH6f4axhgIJ9P3NET78f0Q=
1717
github.com/charmbracelet/bubbles v0.11.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
18+
github.com/charmbracelet/bubbles v0.13.0 h1:zP/ROH3wJEBqZWKIsD50ZKKlx3ydLInq3LdD/Nrlb8w=
19+
github.com/charmbracelet/bubbles v0.13.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
1820
github.com/charmbracelet/bubbletea v0.21.0 h1:f3y+kanzgev5PA916qxmDybSHU3N804uOnKnhRPXTcI=
1921
github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
2022
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
@@ -126,6 +128,7 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
126128
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
127129
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
128130
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
131+
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
129132
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
130133
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
131134
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=

pkg/selector/outputs/bubbletea.go

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,29 @@ package outputs
1515

1616
import (
1717
"github.com/aws/amazon-ec2-instance-selector/v2/pkg/instancetypes"
18+
"github.com/aws/amazon-ec2-instance-selector/v2/pkg/sorter"
1819
tea "github.com/charmbracelet/bubbletea"
20+
"github.com/charmbracelet/lipgloss"
1921
"github.com/muesli/termenv"
2022
)
2123

2224
const (
2325
// can't get terminal dimensions on startup, so use this
2426
initialDimensionVal = 30
27+
28+
instanceTypeKey = "instance type"
29+
selectedKey = "selected"
2530
)
2631

2732
const (
2833
// table states
2934
stateTable = "table"
3035
stateVerbose = "verbose"
36+
stateSorting = "sorting"
37+
)
38+
39+
var (
40+
controlsStyle = lipgloss.NewStyle().Faint(true)
3141
)
3242

3343
// BubbleTeaModel is used to hold the state of the bubble tea TUI
@@ -40,6 +50,9 @@ type BubbleTeaModel struct {
4050

4151
// holds state for the verbose view
4252
verboseModel verboseModel
53+
54+
// holds the state for the sorting view
55+
sortingModel sortingModel
4356
}
4457

4558
// NewBubbleTeaModel initializes a new bubble tea Model which represents
@@ -49,6 +62,7 @@ func NewBubbleTeaModel(instanceTypes []*instancetypes.Details) BubbleTeaModel {
4962
currentState: stateTable,
5063
tableModel: *initTableModel(instanceTypes),
5164
verboseModel: *initVerboseModel(instanceTypes),
65+
sortingModel: *initSortingModel(instanceTypes),
5266
}
5367
}
5468

@@ -62,28 +76,87 @@ func (m BubbleTeaModel) Init() tea.Cmd {
6276
func (m BubbleTeaModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6377
switch msg := msg.(type) {
6478
case tea.KeyMsg:
79+
// don't listen for input if currently typing into text field
80+
if m.tableModel.filterTextInput.Focused() {
81+
break
82+
} else if m.sortingModel.sortTextInput.Focused() {
83+
// see if we should sort and switch states to table
84+
if m.currentState == stateSorting && msg.String() == "enter" {
85+
jsonPath := m.sortingModel.sortTextInput.Value()
86+
87+
sortDirection := sorter.SortAscending
88+
if m.sortingModel.isDescending {
89+
sortDirection = sorter.SortDescending
90+
}
91+
92+
var err error
93+
m.tableModel, err = m.tableModel.sortTable(jsonPath, sortDirection)
94+
if err != nil {
95+
m.sortingModel.sortTextInput.SetValue(jsonPathError)
96+
break
97+
}
98+
99+
m.currentState = stateTable
100+
101+
m.sortingModel.sortTextInput.Blur()
102+
}
103+
104+
break
105+
}
106+
65107
// check for quit or change in state
66108
switch msg.String() {
67109
case "ctrl+c", "q":
68110
return m, tea.Quit
69-
case "enter":
70-
switch m.currentState {
71-
case stateTable:
72-
// switch from table state to verbose state
73-
m.currentState = stateVerbose
74-
111+
case "e":
112+
// switch from table state to verbose state
113+
if m.currentState == stateTable {
75114
// get focused instance type
76-
rowIndex := m.tableModel.table.GetHighlightedRowIndex()
77-
focusedInstance := m.verboseModel.instanceTypes[rowIndex]
115+
focusedRow := m.tableModel.table.HighlightedRow()
116+
focusedInstance, ok := focusedRow.Data[instanceTypeKey].(*instancetypes.Details)
117+
if !ok {
118+
break
119+
}
78120

79121
// set content of view
80122
m.verboseModel.focusedInstanceName = focusedInstance.InstanceType
81123
m.verboseModel.viewport.SetContent(VerboseInstanceTypeOutput([]*instancetypes.Details{focusedInstance})[0])
82124

83125
// move viewport to top of printout
84126
m.verboseModel.viewport.SetYOffset(0)
85-
case stateVerbose:
86-
// switch from verbose state to table state
127+
128+
// switch from table state to verbose state
129+
m.currentState = stateVerbose
130+
}
131+
case "s":
132+
// switch from table view to sorting view
133+
if m.currentState == stateTable {
134+
m.currentState = stateSorting
135+
}
136+
case "enter":
137+
// sort and switch states to table
138+
if m.currentState == stateSorting {
139+
sortFilter := string(m.sortingModel.shorthandList.SelectedItem().(item))
140+
141+
sortDirection := sorter.SortAscending
142+
if m.sortingModel.isDescending {
143+
sortDirection = sorter.SortDescending
144+
}
145+
146+
var err error
147+
m.tableModel, err = m.tableModel.sortTable(sortFilter, sortDirection)
148+
if err != nil {
149+
m.sortingModel.sortTextInput.SetValue("INVALID SHORTHAND VALUE")
150+
break
151+
}
152+
153+
m.currentState = stateTable
154+
155+
m.sortingModel.sortTextInput.Blur()
156+
}
157+
case "esc":
158+
// switch from sorting state or verbose state to table state
159+
if m.currentState == stateSorting || m.currentState == stateVerbose {
87160
m.currentState = stateTable
88161
}
89162
}
@@ -95,23 +168,21 @@ func (m BubbleTeaModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
95168
// handle screen resizing
96169
m.tableModel = m.tableModel.resizeView(msg)
97170
m.verboseModel = m.verboseModel.resizeView(msg)
171+
m.sortingModel = m.sortingModel.resizeView(msg)
98172
}
99173

174+
var cmd tea.Cmd
175+
// update currently active state
100176
switch m.currentState {
101177
case stateTable:
102-
// update table
103-
var cmd tea.Cmd
104178
m.tableModel, cmd = m.tableModel.update(msg)
105-
106-
return m, cmd
107179
case stateVerbose:
108-
// update viewport
109-
var cmd tea.Cmd
110180
m.verboseModel, cmd = m.verboseModel.update(msg)
111-
return m, cmd
181+
case stateSorting:
182+
m.sortingModel, cmd = m.sortingModel.update(msg)
112183
}
113184

114-
return m, nil
185+
return m, cmd
115186
}
116187

117188
// View is used by bubble tea to render the bubble tea model
@@ -121,6 +192,8 @@ func (m BubbleTeaModel) View() string {
121192
return m.tableModel.view()
122193
case stateVerbose:
123194
return m.verboseModel.view()
195+
case stateSorting:
196+
return m.sortingModel.view()
124197
}
125198

126199
return ""

0 commit comments

Comments
 (0)