mirror of
https://github.com/siderolabs/talos.git
synced 2025-10-13 16:41:20 +02:00
feat: change UI component for disks selector
Implemented a new component based on `tview.Table` that shows all available block devices as a table. Had to stop using standard form control, because it doesn't really handle multiline elements. Signed-off-by: Artem Chernyshev <artem.0xD2@gmail.com>
This commit is contained in:
parent
dd810d0514
commit
07b7a8c103
76
internal/pkg/tui/components/form.go
Normal file
76
internal/pkg/tui/components/form.go
Normal file
@ -0,0 +1,76 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package components
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
// NewForm creates a new form.
|
||||
func NewForm() *Form {
|
||||
return &Form{
|
||||
Flex: tview.NewFlex().SetDirection(tview.FlexRow),
|
||||
formItems: []tview.FormItem{},
|
||||
}
|
||||
}
|
||||
|
||||
// Form is a more flexible form component for tview lib.
|
||||
type Form struct {
|
||||
*tview.Flex
|
||||
formItems []tview.FormItem
|
||||
maxLabelLen int
|
||||
}
|
||||
|
||||
// AddFormItem adds a new item to the form.
|
||||
func (f *Form) AddFormItem(item tview.Primitive) {
|
||||
if formItem, ok := item.(tview.FormItem); ok {
|
||||
f.formItems = append(f.formItems, formItem)
|
||||
labelLen := len(formItem.GetLabel()) + 1
|
||||
|
||||
if labelLen > f.maxLabelLen {
|
||||
for _, item := range f.formItems[:len(f.formItems)-1] {
|
||||
item.SetFormAttributes(
|
||||
labelLen,
|
||||
tview.Styles.PrimaryTextColor,
|
||||
f.GetBackgroundColor(),
|
||||
tview.Styles.PrimaryTextColor,
|
||||
tview.Styles.ContrastBackgroundColor,
|
||||
)
|
||||
}
|
||||
|
||||
f.maxLabelLen = labelLen
|
||||
}
|
||||
|
||||
formItem.SetFormAttributes(
|
||||
f.maxLabelLen,
|
||||
tview.Styles.PrimaryTextColor,
|
||||
f.GetBackgroundColor(),
|
||||
tview.Styles.PrimaryTextColor,
|
||||
tview.Styles.ContrastBackgroundColor,
|
||||
)
|
||||
} else if box, ok := item.(Box); ok {
|
||||
box.SetBackgroundColor(f.GetBackgroundColor())
|
||||
}
|
||||
|
||||
height := 1
|
||||
multiline, ok := item.(Multiline)
|
||||
|
||||
if ok {
|
||||
height = multiline.GetHeight()
|
||||
}
|
||||
|
||||
f.AddItem(item, height+1, 1, false)
|
||||
}
|
||||
|
||||
// Multiline interface represents elements that can occupy more than one line.
|
||||
type Multiline interface {
|
||||
GetHeight() int
|
||||
}
|
||||
|
||||
// Box interface that has just SetBackgroundColor.
|
||||
type Box interface {
|
||||
SetBackgroundColor(tcell.Color) *tview.Box
|
||||
}
|
@ -5,7 +5,6 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
@ -15,8 +14,6 @@ func NewFormLabel(label string) *FormLabel {
|
||||
tview.NewTextView().SetText(label),
|
||||
}
|
||||
|
||||
res.SetWordWrap(true)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
@ -24,25 +21,3 @@ func NewFormLabel(label string) *FormLabel {
|
||||
type FormLabel struct {
|
||||
*tview.TextView
|
||||
}
|
||||
|
||||
// GetLabel implements FormItem interface.
|
||||
func (fl *FormLabel) GetLabel() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetFormAttributes implements FormItem interface.
|
||||
func (fl *FormLabel) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) tview.FormItem {
|
||||
fl.SetBackgroundColor(bgColor)
|
||||
|
||||
return fl
|
||||
}
|
||||
|
||||
// GetFieldWidth implements FormItem interface.
|
||||
func (fl *FormLabel) GetFieldWidth() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// SetFinishedFunc implements FormItem interface.
|
||||
func (fl *FormLabel) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem {
|
||||
return fl
|
||||
}
|
||||
|
210
internal/pkg/tui/components/table.go
Normal file
210
internal/pkg/tui/components/table.go
Normal file
@ -0,0 +1,210 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package components
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
var (
|
||||
backgroundColor = tcell.Color235
|
||||
textNormalColor = tcell.ColorIvory
|
||||
selectedTextColor = tview.Styles.PrimaryTextColor
|
||||
selectedBackgroundColor = tview.Styles.ContrastBackgroundColor
|
||||
)
|
||||
|
||||
// NewTable creates new table.
|
||||
func NewTable() *Table {
|
||||
t := &Table{
|
||||
Table: tview.NewTable(),
|
||||
selectedRow: -1,
|
||||
hoveredRow: -1,
|
||||
rows: [][]interface{}{},
|
||||
}
|
||||
|
||||
hasFocus := false
|
||||
|
||||
t.SetDrawFunc(func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {
|
||||
changed := hasFocus != t.HasFocus()
|
||||
if !changed {
|
||||
return x, y, width, height
|
||||
}
|
||||
|
||||
hasFocus = t.HasFocus()
|
||||
|
||||
if hasFocus {
|
||||
if t.selectedRow != -1 {
|
||||
t.HoverRow(t.selectedRow)
|
||||
} else {
|
||||
t.HoverRow(1)
|
||||
}
|
||||
} else {
|
||||
t.HoverRow(-1)
|
||||
}
|
||||
|
||||
return x, y, width, height
|
||||
})
|
||||
|
||||
t.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
||||
// nolint:exhaustive
|
||||
switch e.Key() {
|
||||
case tcell.KeyUp:
|
||||
if t.hoveredRow > 0 {
|
||||
t.HoverRow(t.hoveredRow - 1)
|
||||
}
|
||||
case tcell.KeyDown:
|
||||
if t.hoveredRow < t.GetRowCount() {
|
||||
t.HoverRow(t.hoveredRow + 1)
|
||||
}
|
||||
case tcell.KeyEnter:
|
||||
if t.hoveredRow != -1 {
|
||||
t.SelectRow(t.hoveredRow)
|
||||
}
|
||||
}
|
||||
|
||||
return e
|
||||
})
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Table list of choices represented in table format.
|
||||
type Table struct {
|
||||
*tview.Table
|
||||
selectedRow int
|
||||
hoveredRow int
|
||||
onRowSelected func(row int)
|
||||
rows [][]interface{}
|
||||
}
|
||||
|
||||
// SetHeader sets table header.
|
||||
func (t *Table) SetHeader(keys ...interface{}) {
|
||||
t.AddRow(keys...)
|
||||
}
|
||||
|
||||
// AddRow adds a new row to the table.
|
||||
func (t *Table) AddRow(columns ...interface{}) {
|
||||
row := t.GetRowCount()
|
||||
col := backgroundColor
|
||||
textColor := tview.Styles.PrimaryTextColor
|
||||
|
||||
if row == 0 {
|
||||
col = tcell.ColorSilver
|
||||
textColor = tview.Styles.InverseTextColor
|
||||
} else {
|
||||
t.rows = append(t.rows, columns)
|
||||
}
|
||||
|
||||
cell := tview.NewTableCell(" ").
|
||||
SetAlign(tview.AlignCenter).
|
||||
SetBackgroundColor(col).
|
||||
SetTextColor(textColor)
|
||||
|
||||
if row > 0 {
|
||||
cell.SetClickedFunc(func() bool {
|
||||
t.HoverRow(row)
|
||||
t.SelectRow(row)
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
t.SetCell(row, 0, cell)
|
||||
|
||||
for i, text := range columns {
|
||||
cell = tview.NewTableCell(text.(string))
|
||||
cell.SetExpansion(1)
|
||||
cell.SetTextColor(textColor)
|
||||
|
||||
if i == len(columns)-1 {
|
||||
cell.SetAlign(tview.AlignRight)
|
||||
}
|
||||
|
||||
cell.SetBackgroundColor(col)
|
||||
cell.SetSelectable(true)
|
||||
t.SetCell(row, i+1, cell)
|
||||
|
||||
if row > 0 {
|
||||
cell.SetClickedFunc(func() bool {
|
||||
return t.HoverRow(row)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SelectRow selects the row in the table.
|
||||
func (t *Table) SelectRow(row int) bool {
|
||||
// don't select the header
|
||||
if row < 2 {
|
||||
row = 1
|
||||
}
|
||||
|
||||
if row < t.GetRowCount() {
|
||||
if t.selectedRow != -1 {
|
||||
t.GetCell(t.selectedRow, 0).SetText(" ")
|
||||
}
|
||||
|
||||
t.GetCell(row, 0).SetText("►")
|
||||
t.selectedRow = row
|
||||
|
||||
if t.onRowSelected != nil {
|
||||
t.onRowSelected(row)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// HoverRow highlights the row in the table.
|
||||
func (t *Table) HoverRow(row int) bool {
|
||||
updateRowStyle := func(r int, foregroundColor, backgroundColor tcell.Color) {
|
||||
for i := 0; i < t.GetColumnCount(); i++ {
|
||||
t.GetCell(r, i).SetBackgroundColor(backgroundColor).SetTextColor(foregroundColor)
|
||||
}
|
||||
}
|
||||
|
||||
// don't select the header
|
||||
if row == 0 {
|
||||
row = 1
|
||||
}
|
||||
|
||||
if row < t.GetRowCount() {
|
||||
if t.hoveredRow != -1 {
|
||||
updateRowStyle(t.hoveredRow, textNormalColor, backgroundColor)
|
||||
}
|
||||
|
||||
if row != -1 {
|
||||
updateRowStyle(row, selectedTextColor, selectedBackgroundColor)
|
||||
}
|
||||
|
||||
t.hoveredRow = row
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetHeight implements Multiline interface.
|
||||
func (t *Table) GetHeight() int {
|
||||
return t.GetRowCount()
|
||||
}
|
||||
|
||||
// SetRowSelectedFunc called when selected row is updated.
|
||||
func (t *Table) SetRowSelectedFunc(callback func(row int)) {
|
||||
t.onRowSelected = callback
|
||||
}
|
||||
|
||||
// GetValue returns value in row/column.
|
||||
func (t *Table) GetValue(row, column int) interface{} {
|
||||
if row < len(t.rows) && column < len(t.rows[row]) {
|
||||
return t.rows[row][column]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
@ -45,6 +45,14 @@ type Item struct {
|
||||
options []interface{}
|
||||
}
|
||||
|
||||
// TableHeaders represents table headers list for item options which are using table representation.
|
||||
type TableHeaders []interface{}
|
||||
|
||||
// NewTableHeaders creates TableHeaders object.
|
||||
func NewTableHeaders(headers ...interface{}) TableHeaders {
|
||||
return TableHeaders(headers)
|
||||
}
|
||||
|
||||
// NewItem creates new form item.
|
||||
func NewItem(name, description string, dest interface{}, options ...interface{}) *Item {
|
||||
return &Item{
|
||||
@ -62,8 +70,8 @@ func (item *Item) assign(value string) error {
|
||||
|
||||
// createFormItems dynamically creates tview.FormItem list based on the wrapped type.
|
||||
// nolint:gocyclo
|
||||
func (item *Item) createFormItems() ([]tview.FormItem, error) {
|
||||
res := []tview.FormItem{}
|
||||
func (item *Item) createFormItems() ([]tview.Primitive, error) {
|
||||
res := []tview.Primitive{}
|
||||
|
||||
v := reflect.ValueOf(item.dest)
|
||||
if v.Kind() == reflect.Ptr {
|
||||
@ -77,7 +85,7 @@ func (item *Item) createFormItems() ([]tview.FormItem, error) {
|
||||
}
|
||||
}
|
||||
|
||||
var formItem tview.FormItem
|
||||
var formItem tview.Primitive
|
||||
|
||||
// nolint:exhaustive
|
||||
switch v.Kind() {
|
||||
@ -92,37 +100,69 @@ func (item *Item) createFormItems() ([]tview.FormItem, error) {
|
||||
formItem = checkbox
|
||||
default:
|
||||
if len(item.options) > 0 {
|
||||
dropdown := tview.NewDropDown()
|
||||
tableHeaders, ok := item.options[0].(TableHeaders)
|
||||
if ok {
|
||||
table := components.NewTable()
|
||||
table.SetHeader(tableHeaders...)
|
||||
|
||||
if len(item.options)%2 != 0 {
|
||||
return nil, fmt.Errorf("wrong amount of arguments for options: should be even amount of key, value pairs")
|
||||
}
|
||||
data := item.options[1:]
|
||||
numColumns := len(tableHeaders)
|
||||
|
||||
for i := 0; i < len(item.options); i += 2 {
|
||||
if optionName, ok := item.options[i].(string); ok {
|
||||
selected := -1
|
||||
|
||||
func(index int) {
|
||||
dropdown.AddOption(optionName, func() {
|
||||
v.Set(reflect.ValueOf(item.options[index]))
|
||||
})
|
||||
|
||||
if v.Interface() == item.options[index] {
|
||||
selected = i / 2
|
||||
}
|
||||
}(i + 1)
|
||||
|
||||
if selected != -1 {
|
||||
dropdown.SetCurrentOption(selected)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("expected string option name, got %s", item.options[i])
|
||||
if len(data)%numColumns != 0 {
|
||||
return nil, fmt.Errorf("incorrect amount of data provided for the table")
|
||||
}
|
||||
|
||||
selected := -1
|
||||
|
||||
for i := 0; i < len(data); i += numColumns {
|
||||
table.AddRow(data[i : i+numColumns]...)
|
||||
|
||||
if v.Interface() == data[i] {
|
||||
selected = i / numColumns
|
||||
}
|
||||
}
|
||||
|
||||
if selected != -1 {
|
||||
table.SelectRow(selected)
|
||||
}
|
||||
|
||||
formItem = table
|
||||
table.SetRowSelectedFunc(func(row int) {
|
||||
v.Set(reflect.ValueOf(table.GetValue(row, 0))) // always pick the first column
|
||||
})
|
||||
} else {
|
||||
dropdown := tview.NewDropDown()
|
||||
|
||||
if len(item.options)%2 != 0 {
|
||||
return nil, fmt.Errorf("wrong amount of arguments for options: should be even amount of key, value pairs")
|
||||
}
|
||||
|
||||
for i := 0; i < len(item.options); i += 2 {
|
||||
if optionName, ok := item.options[i].(string); ok {
|
||||
selected := -1
|
||||
|
||||
func(index int) {
|
||||
dropdown.AddOption(optionName, func() {
|
||||
v.Set(reflect.ValueOf(item.options[index]))
|
||||
})
|
||||
|
||||
if v.Interface() == item.options[index] {
|
||||
selected = i / 2
|
||||
}
|
||||
}(i + 1)
|
||||
|
||||
if selected != -1 {
|
||||
dropdown.SetCurrentOption(selected)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("expected string option name, got %s", item.options[i])
|
||||
}
|
||||
}
|
||||
|
||||
dropdown.SetLabel(item.name)
|
||||
|
||||
formItem = dropdown
|
||||
}
|
||||
|
||||
dropdown.SetLabel(item.name)
|
||||
|
||||
formItem = dropdown
|
||||
} else {
|
||||
input := tview.NewInputField()
|
||||
formItem = input
|
||||
@ -375,7 +415,8 @@ func (installer *Installer) configure() error {
|
||||
groups = append(groups, eg)
|
||||
|
||||
err = func(index int) error {
|
||||
form := tview.NewForm()
|
||||
form := components.NewForm()
|
||||
form.SetBackgroundColor(color)
|
||||
|
||||
for _, item := range p.items {
|
||||
formItems, e := item.createFormItems()
|
||||
|
@ -46,7 +46,9 @@ func NewState(ctx context.Context, conn *Connection) (*State, error) {
|
||||
opts.ClusterConfig.ControlPlane.Endpoint = fmt.Sprintf("https://%s:%d", conn.nodeEndpoint, constants.DefaultControlPlanePort)
|
||||
}
|
||||
|
||||
diskInstallOptions := []interface{}{}
|
||||
diskInstallOptions := []interface{}{
|
||||
NewTableHeaders("device name", "model name", "size"),
|
||||
}
|
||||
|
||||
disks, err := conn.Disks()
|
||||
if err != nil {
|
||||
@ -58,8 +60,7 @@ func NewState(ctx context.Context, conn *Connection) (*State, error) {
|
||||
opts.MachineConfig.InstallConfig.InstallDisk = disk.DeviceName
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("%s %s %s", disk.DeviceName, disk.Model, humanize.Bytes(disk.Size))
|
||||
diskInstallOptions = append(diskInstallOptions, name, disk.DeviceName)
|
||||
diskInstallOptions = append(diskInstallOptions, disk.DeviceName, disk.Model, humanize.Bytes(disk.Size))
|
||||
}
|
||||
|
||||
var machineTypes []interface{}
|
||||
@ -80,7 +81,26 @@ func NewState(ctx context.Context, conn *Connection) (*State, error) {
|
||||
conn: conn,
|
||||
opts: opts,
|
||||
pages: []*Page{
|
||||
NewPage("Installer Params",
|
||||
NewItem(
|
||||
"image",
|
||||
v1alpha1.InstallConfigDoc.Describe("image", true),
|
||||
&opts.MachineConfig.InstallConfig.InstallImage,
|
||||
),
|
||||
NewItem(
|
||||
"install disk",
|
||||
v1alpha1.InstallConfigDoc.Describe("disk", true),
|
||||
&opts.MachineConfig.InstallConfig.InstallDisk,
|
||||
diskInstallOptions...,
|
||||
),
|
||||
),
|
||||
NewPage("Machine Config",
|
||||
NewItem(
|
||||
"machine type",
|
||||
v1alpha1.MachineConfigDoc.Describe("type", true),
|
||||
&opts.MachineConfig.Type,
|
||||
machineTypes...,
|
||||
),
|
||||
NewItem(
|
||||
"cluster name",
|
||||
v1alpha1.ClusterConfigDoc.Describe("clusterName", true),
|
||||
@ -91,28 +111,11 @@ func NewState(ctx context.Context, conn *Connection) (*State, error) {
|
||||
v1alpha1.ControlPlaneConfigDoc.Describe("endpoint", true),
|
||||
&opts.ClusterConfig.ControlPlane.Endpoint,
|
||||
),
|
||||
NewItem(
|
||||
"machine type",
|
||||
v1alpha1.MachineConfigDoc.Describe("type", true),
|
||||
&opts.MachineConfig.Type,
|
||||
machineTypes...,
|
||||
),
|
||||
NewItem(
|
||||
"kubernetes version",
|
||||
"Kubernetes version to install.",
|
||||
&opts.MachineConfig.KubernetesVersion,
|
||||
),
|
||||
NewItem(
|
||||
"install disk",
|
||||
v1alpha1.InstallConfigDoc.Describe("disk", true),
|
||||
&opts.MachineConfig.InstallConfig.InstallDisk,
|
||||
diskInstallOptions...,
|
||||
),
|
||||
NewItem(
|
||||
"image",
|
||||
v1alpha1.InstallConfigDoc.Describe("image", true),
|
||||
&opts.MachineConfig.InstallConfig.InstallImage,
|
||||
),
|
||||
),
|
||||
NewPage("Network Config",
|
||||
NewItem(
|
||||
|
Loading…
x
Reference in New Issue
Block a user