talos/internal/pkg/dashboard/configurl.go
Utku Ozdemir e1d38b6feb
feat: show template URL in dashboard config URL tab
Show the config URL template that will be populated when the code is entered. Closes siderolabs/talos#7092.

Clear the form when the tab is exited & do not display "Saved successfully" message when the code is saved, as we navigate to the summary tab afterward anyway. Closes siderolabs/talos#7093.

Signed-off-by: Utku Ozdemir <utku.ozdemir@siderolabs.com>
2023-04-19 11:27:47 +02:00

206 lines
5.2 KiB
Go

// 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 dashboard
import (
"context"
"fmt"
"strings"
"github.com/rivo/tview"
"github.com/siderolabs/go-procfs/procfs"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/siderolabs/talos/internal/pkg/dashboard/resourcedata"
"github.com/siderolabs/talos/internal/pkg/meta"
"github.com/siderolabs/talos/pkg/machinery/constants"
runtimeres "github.com/siderolabs/talos/pkg/machinery/resources/runtime"
)
const unset = "[gray](unset)[-]"
type configURLData struct {
existingCode string
}
// ConfigURLGrid represents the config URL grid.
type ConfigURLGrid struct {
tview.Grid
dashboard *Dashboard
form *tview.Form
template *tview.TextView
existingCode *tview.TextView
newCodeField *tview.InputField
infoView *tview.TextView
selectedNode string
nodeMap map[string]*configURLData
}
// NewConfigURLGrid returns a new config URL grid.
func NewConfigURLGrid(ctx context.Context, dashboard *Dashboard) *ConfigURLGrid {
grid := &ConfigURLGrid{
Grid: *tview.NewGrid(),
dashboard: dashboard,
form: tview.NewForm(),
template: tview.NewTextView().SetDynamicColors(true).SetLabel("Template").SetScrollable(false),
existingCode: tview.NewTextView().SetDynamicColors(true).SetLabel("Existing Code").SetText(unset).SetSize(1, 0).SetScrollable(false),
newCodeField: tview.NewInputField().SetLabel("New Code"),
infoView: tview.NewTextView().SetDynamicColors(true).SetSize(2, 0).SetScrollable(false),
nodeMap: make(map[string]*configURLData),
}
grid.template.SetText(grid.readTemplateFromKernelArgs())
grid.SetRows(-1, 18, -1).SetColumns(-1, 72, -1)
grid.form.SetBorder(true)
grid.form.AddFormItem(grid.template)
grid.form.AddFormItem(grid.existingCode)
grid.form.AddFormItem(grid.newCodeField)
grid.form.AddButton("Save", func() {
ctx = nodeContext(ctx, grid.selectedNode)
value := grid.newCodeField.GetText()
if value == "" {
grid.infoView.SetText("[red]Error: No code entered[-]")
return
}
err := dashboard.cli.MetaWrite(ctx, meta.DownloadURLCode, []byte(value))
if err != nil {
grid.infoView.SetText(fmt.Sprintf("[red]Error: %v[-]", err))
return
}
grid.clearForm()
grid.dashboard.selectScreen(ScreenSummary)
})
grid.form.AddButton("Delete", func() {
ctx = nodeContext(ctx, grid.selectedNode)
err := dashboard.cli.MetaDelete(ctx, meta.DownloadURLCode)
if err != nil {
if status.Code(err) == codes.NotFound {
grid.clearForm()
grid.infoView.SetText("[green]Already deleted[-]")
return
}
grid.infoView.SetText(fmt.Sprintf("[red]Error: %v[-]", err))
return
}
grid.clearForm()
grid.infoView.SetText("[green]Deleted successfully[-]")
})
grid.form.AddFormItem(grid.infoView)
grid.AddItem(tview.NewBox(), 0, 0, 1, 3, 0, 0, false)
grid.AddItem(tview.NewBox(), 1, 0, 1, 1, 0, 0, false)
grid.AddItem(grid.form, 1, 1, 1, 1, 0, 0, false)
grid.AddItem(tview.NewBox(), 1, 2, 1, 1, 0, 0, false)
grid.AddItem(tview.NewBox(), 2, 0, 1, 3, 0, 0, false)
return grid
}
func (widget *ConfigURLGrid) readTemplateFromKernelArgs() string {
option := procfs.ProcCmdline().Get(constants.KernelParamConfig).First()
if option == nil {
return unset
}
codeVar := fmt.Sprintf("${%s}", constants.CodeKey)
return strings.ReplaceAll(tview.Escape(*option), codeVar, fmt.Sprintf("[green]%s[-]", codeVar))
}
// OnScreenSelect implements the screenSelectListener interface.
func (widget *ConfigURLGrid) onScreenSelect(active bool) {
if active {
widget.dashboard.app.SetFocus(widget.form)
} else {
widget.clearForm()
}
}
// OnNodeSelect implements the NodeSelectListener interface.
func (widget *ConfigURLGrid) OnNodeSelect(node string) {
if node != widget.selectedNode {
widget.selectedNode = node
widget.clearForm()
widget.redraw()
}
}
// OnResourceDataChange implements the ResourceDataListener interface.
func (widget *ConfigURLGrid) OnResourceDataChange(data resourcedata.Data) {
widget.updateNodeData(data)
if data.Node == widget.selectedNode {
widget.redraw()
}
}
func (widget *ConfigURLGrid) updateNodeData(data resourcedata.Data) {
nodeData := widget.getOrCreateNodeData(data.Node)
//nolint:gocritic
switch res := data.Resource.(type) {
case *runtimeres.MetaKey:
if res.Metadata().ID() == runtimeres.MetaKeyTagToID(meta.DownloadURLCode) {
if data.Deleted {
nodeData.existingCode = unset
} else {
val := res.TypedSpec().Value
if val == "" {
val = "(empty)"
}
nodeData.existingCode = fmt.Sprintf("[blue]%s[-]", val)
}
}
}
}
func (widget *ConfigURLGrid) redraw() {
data := widget.getOrCreateNodeData(widget.selectedNode)
widget.existingCode.SetText(data.existingCode)
}
func (widget *ConfigURLGrid) getOrCreateNodeData(node string) *configURLData {
nodeData, ok := widget.nodeMap[node]
if !ok {
nodeData = &configURLData{
existingCode: unset,
}
widget.nodeMap[node] = nodeData
}
return nodeData
}
func (widget *ConfigURLGrid) clearForm() {
widget.form.SetFocus(0)
widget.infoView.SetText("")
widget.newCodeField.SetText("")
}