diff --git a/Makefile b/Makefile index 08256d52..824ca12e 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ SOURCE_GIT_COMMIT ?= $(shell git rev-parse --verify 'HEAD^{commit}') BUILD_VERSION ?= $(shell git describe --always --abbrev=40 --dirty) VERSION_URI ?= github.com/openshift/agent-installer-utils/pkg/version -RELEASE_IMAGE ?= quay.io/openshift-release-dev/ocp-release:4.18.4-x86_64 +RELEASE_IMAGE ?= quay.io/openshift-release-dev/ocp-release:4.21.1-x86_64 ARCH ?= x86_64 .PHONY:clean @@ -14,6 +14,10 @@ clean: lint: golangci-lint run -v +.PHONY: test +test: + cd tools/agent_tui && go test -v ./... + .PHONY: build build: clean lint hack/build.sh ${VERSION_URI} ${SOURCE_GIT_COMMIT} ${BUILD_VERSION} diff --git a/tools/agent_tui/app.go b/tools/agent_tui/app.go index 884e08ce..82e0bd1a 100644 --- a/tools/agent_tui/app.go +++ b/tools/agent_tui/app.go @@ -15,7 +15,7 @@ import ( "github.com/sirupsen/logrus" ) -func App(app *tview.Application, rendezvousIP string, config checks.Config, checkFuncs ...checks.CheckFunctions) { +func App(app *tview.Application, rendezvousIP string, interactiveUIMode bool, config checks.Config, checkFuncs ...checks.CheckFunctions) { if err := prepareConfig(&config); err != nil { log.Fatal(err) @@ -34,6 +34,7 @@ func App(app *tview.Application, rendezvousIP string, config checks.Config, chec logger.Infof("Agent TUI git version: %s", version.Commit) logger.Infof("Agent TUI build version: %s", version.Raw) logger.Infof("Rendezvous IP: %s", rendezvousIP) + logger.Infof("Interactive UI Mode: %v", interactiveUIMode) var appUI *ui.UI if app == nil { @@ -54,11 +55,11 @@ func App(app *tview.Application, rendezvousIP string, config checks.Config, chec app = tview.NewApplication() } - appUI = ui.NewUI(app, config, logger) + appUI = ui.NewUI(app, config, logger, rendezvousIP) controller := ui.NewController(appUI) engine := checks.NewEngine(controller.GetChan(), config, logger, checkFuncs...) - controller.Init(engine.Size(), rendezvousIP) + controller.Init(engine.Size(), rendezvousIP, interactiveUIMode) engine.Init() if err := app.Run(); err != nil { log.Fatal(err) diff --git a/tools/agent_tui/apptester_test.go b/tools/agent_tui/apptester_test.go index 18620e7a..8dd367c4 100644 --- a/tools/agent_tui/apptester_test.go +++ b/tools/agent_tui/apptester_test.go @@ -64,7 +64,7 @@ func NewAppTester(t *testing.T, debug ...bool) *AppTester { // Starts a new Agent TUI in background func (a *AppTester) Start(config checks.Config) *AppTester { - go App(a.app, "192.168.111.80", config, checks.CheckFunctions{ + go App(a.app, "192.168.111.80", false, config, checks.CheckFunctions{ checks.CheckTypeReleaseImageHostDNS: a.wrapper, checks.CheckTypeReleaseImageHostPing: a.wrapper, checks.CheckTypeReleaseImageHttp: a.wrapper, diff --git a/tools/agent_tui/main/main.go b/tools/agent_tui/main/main.go index 190c4324..32b39afd 100644 --- a/tools/agent_tui/main/main.go +++ b/tools/agent_tui/main/main.go @@ -13,6 +13,7 @@ import ( const ( RENDEZVOUS_IP_TEMPLATE_VALUE = "{{.RendezvousIP}}" + INTERACTIVE_UI_SENTINEL_PATH = "/etc/assisted/interactive-ui" ) func main() { @@ -29,24 +30,33 @@ func main() { logPath = "/tmp/agent_tui.log" fmt.Printf("AGENT_TUI_LOG_PATH is unspecified, logging to: %v\n", logPath) } - rendezvousIP := getRendezvousIP() + rendezvousIP, interactiveUIMode := getRendezvousIP() config := checks.Config{ ReleaseImageURL: releaseImage, LogPath: logPath, } - agent_tui.App(nil, rendezvousIP, config) + agent_tui.App(nil, rendezvousIP, interactiveUIMode, config) } -// getRendezvousIP reads NODE_ZERO_IP from /etc/assisted/rendezvous-host.env. -func getRendezvousIP() (nodeZeroIP string) { +// getRendezvousIP reads NODE_ZERO_IP from /etc/assisted/rendezvous-host.env +// and checks if the interactive UI sentinel file exists. +// Returns (rendezvousIP, interactiveUIMode) where: +// - rendezvousIP: the IP address from rendezvous-host.env, or empty string +// - interactiveUIMode: true if /etc/assisted/interactive-ui exists +func getRendezvousIP() (nodeZeroIP string, interactiveUIMode bool) { + // Check if the interactive UI sentinel file exists + _, err := os.Stat(INTERACTIVE_UI_SENTINEL_PATH) + interactiveUIMode = (err == nil) + + // Read rendezvous IP from the standard location envMap, err := godotenv.Read(ui.RENDEZVOUS_HOST_ENV_PATH) if err != nil { - return "" + return "", interactiveUIMode } nodeZeroIP = envMap["NODE_ZERO_IP"] if nodeZeroIP == RENDEZVOUS_IP_TEMPLATE_VALUE { nodeZeroIP = "" } - return nodeZeroIP + return nodeZeroIP, interactiveUIMode } diff --git a/tools/agent_tui/ui/check_page_test.go b/tools/agent_tui/ui/check_page_test.go index 5eded1f8..eb1fbc0b 100644 --- a/tools/agent_tui/ui/check_page_test.go +++ b/tools/agent_tui/ui/check_page_test.go @@ -17,7 +17,7 @@ func TestCheckPageNavigation(t *testing.T) { } logger := logrus.New() - ui := NewUI(tview.NewApplication(), config, logger) + ui := NewUI(tview.NewApplication(), config, logger, "") // There are two buttons that the user can navigate between // and @@ -38,8 +38,148 @@ func TestCheckPageNavigation(t *testing.T) { assert.Equal(t, 0, ui.focusedItem) } +func TestRendezvousIPPageNavigation(t *testing.T) { + config := checks.Config{ + ReleaseImageURL: "", + LogPath: "/tmp/agent-tui.log", + } + + logger := logrus.New() + ui := NewUI(tview.NewApplication(), config, logger, "") + + // Focus on rendezvous IP page + ui.setFocusToRendezvousIP() + + // There are four focusable items on the rendezvous IP page: + // 0: Input field (Rendezvous IP) + // 1: Save rendezvous IP button + // 2: This is the rendezvous node button + // 3: Configure Network button + assert.Equal(t, 0, ui.focusedItem) + + // Test TAB navigation (forward) + applyKeyToRendezvousIPPage(ui, tcell.KeyTab, 1) + assert.Equal(t, 1, ui.focusedItem) // Save button + applyKeyToRendezvousIPPage(ui, tcell.KeyTab, 1) + assert.Equal(t, 2, ui.focusedItem) // This is rendezvous node button + applyKeyToRendezvousIPPage(ui, tcell.KeyTab, 1) + assert.Equal(t, 3, ui.focusedItem) // Configure Network button + applyKeyToRendezvousIPPage(ui, tcell.KeyTab, 1) + assert.Equal(t, 0, ui.focusedItem) // Back to input field + + // Test BACKTAB navigation (backward) + applyKeyToRendezvousIPPage(ui, tcell.KeyBacktab, 1) + assert.Equal(t, 3, ui.focusedItem) // Configure Network button + applyKeyToRendezvousIPPage(ui, tcell.KeyBacktab, 1) + assert.Equal(t, 2, ui.focusedItem) // This is rendezvous node button + applyKeyToRendezvousIPPage(ui, tcell.KeyBacktab, 1) + assert.Equal(t, 1, ui.focusedItem) // Save button + applyKeyToRendezvousIPPage(ui, tcell.KeyBacktab, 1) + assert.Equal(t, 0, ui.focusedItem) // Input field +} + +func TestRendezvousIPPageNavigationWithPrefilled(t *testing.T) { + config := checks.Config{ + ReleaseImageURL: "", + LogPath: "/tmp/agent-tui.log", + } + + logger := logrus.New() + prefilledIP := "192.168.111.80" + ui := NewUI(tview.NewApplication(), config, logger, prefilledIP) + + // Verify the input field has the prefilled IP + inputField := ui.rendezvousIPForm.GetFormItemByLabel(FIELD_ENTER_RENDEZVOUS_IP) + assert.NotNil(t, inputField) + + // Verify initial rendezvous IP is set + assert.Equal(t, prefilledIP, ui.initialRendezvousIP) +} + +func TestInteractiveUIModeWithPrefilledIP(t *testing.T) { + config := checks.Config{ + ReleaseImageURL: "", + LogPath: "/tmp/agent-tui.log", + } + + logger := logrus.New() + prefilledIP := "192.168.111.80" + ui := NewUI(tview.NewApplication(), config, logger, prefilledIP) + controller := NewController(ui) + + // Initialize with interactive mode and prefilled IP + controller.Init(1, prefilledIP, true) + + // Verify timeout modal is active + assert.True(t, ui.IsRendezvousIPTimeoutActive()) +} + +func TestInteractiveUIModeWithoutPrefilledIP(t *testing.T) { + config := checks.Config{ + ReleaseImageURL: "", + LogPath: "/tmp/agent-tui.log", + } + + logger := logrus.New() + ui := NewUI(tview.NewApplication(), config, logger, "") + controller := NewController(ui) + + // Initialize with interactive mode but no prefilled IP + controller.Init(1, "", true) + + // Verify timeout modal is NOT active + assert.False(t, ui.IsRendezvousIPTimeoutActive()) +} + +func TestNonInteractiveUIMode(t *testing.T) { + config := checks.Config{ + ReleaseImageURL: "", + LogPath: "/tmp/agent-tui.log", + } + + logger := logrus.New() + ui := NewUI(tview.NewApplication(), config, logger, "") + controller := NewController(ui) + + // Initialize with non-interactive mode + controller.Init(1, "", false) + + // Verify splash screen is shown and timeout modal is NOT active + assert.False(t, ui.IsRendezvousIPTimeoutActive()) +} + +func TestTimeoutModalCancellation(t *testing.T) { + config := checks.Config{ + ReleaseImageURL: "", + LogPath: "/tmp/agent-tui.log", + } + + logger := logrus.New() + prefilledIP := "192.168.111.80" + ui := NewUI(tview.NewApplication(), config, logger, prefilledIP) + + // Show timeout modal + ui.ShowRendezvousIPTimeoutDialog(prefilledIP) + assert.True(t, ui.IsRendezvousIPTimeoutActive()) + + // Cancel the timeout modal + ui.cancelRendezvousIPTimeout() + assert.False(t, ui.IsRendezvousIPTimeoutActive()) +} + func applyKeyToChecks(u *UI, key tcell.Key, numKeyPresses int) { for i := 0; i < numKeyPresses; i++ { u.mainFlex.InputHandler()(tcell.NewEventKey(key, 0, tcell.ModNone), func(p tview.Primitive) {}) } } + +func applyKeyToRendezvousIPPage(u *UI, key tcell.Key, numKeyPresses int) { + // Get the rendezvous IP page from pages + page, _ := u.pages.GetFrontPage() + if page == PAGE_RENDEZVOUS_IP { + for i := 0; i < numKeyPresses; i++ { + // Apply key event through the main flex which has the input capture logic + u.rendezvousIPMainFlex.InputHandler()(tcell.NewEventKey(key, 0, tcell.ModNone), func(p tview.Primitive) {}) + } + } +} diff --git a/tools/agent_tui/ui/controller.go b/tools/agent_tui/ui/controller.go index bc061005..2eb4ab5b 100644 --- a/tools/agent_tui/ui/controller.go +++ b/tools/agent_tui/ui/controller.go @@ -40,12 +40,22 @@ func (c *Controller) receivedPrimaryCheck(numChecks int) bool { return found } -func (c *Controller) Init(numChecks int, rendezvousIP string) { - c.ui.ShowSplashScreen() - - if rendezvousIP == "" { +func (c *Controller) Init(numChecks int, rendezvousIP string, interactiveUIMode bool) { + if interactiveUIMode { + // NoRegistry (OVE) ISO flow c.ui.setFocusToRendezvousIP() + + if rendezvousIP != "" { + // Show timeout modal with the prefilled rendezvous IP + c.ui.logger.Infof("Interactive UI mode with prefilled IP: %s", rendezvousIP) + c.ui.ShowRendezvousIPTimeoutDialog(rendezvousIP) + } else { + // Do not show timeout modal + c.ui.logger.Infof("Interactive UI mode with empty IP form") + } } else { + // Agent ISO flow + c.ui.ShowSplashScreen() c.ui.setFocusToChecks() } diff --git a/tools/agent_tui/ui/rendezvous_ip.go b/tools/agent_tui/ui/rendezvous_ip.go index d0d34f9e..6f04ff18 100644 --- a/tools/agent_tui/ui/rendezvous_ip.go +++ b/tools/agent_tui/ui/rendezvous_ip.go @@ -41,7 +41,7 @@ func (u *UI) createRendezvousIPPage(config checks.Config) { rendezvousTextFlex := u.createTextFlex(rendezvousIPFormDescription) rendezvousTextNumRows := 8 - u.rendezvousIPForm.AddInputField(FIELD_ENTER_RENDEZVOUS_IP, "", 55, nil, nil) + u.rendezvousIPForm.AddInputField(FIELD_ENTER_RENDEZVOUS_IP, u.initialRendezvousIP, 55, nil, nil) u.rendezvousIPForm.SetFieldTextColor(newt.ColorGray) u.rendezvousIPForm.AddButton(SAVE_RENDEZVOUS_IP_BUTTON, func() { @@ -84,31 +84,55 @@ func (u *UI) createRendezvousIPPage(config checks.Config) { u.selectIPForm.SetButtonStyle(tcell.StyleDefault.Background(newt.ColorGray). Foreground(newt.ColorBlack)) - mainFlex := tview.NewFlex(). + // Add a seperator + separator := tview.NewTextView() + separator.SetText("────────────────────────────────────────────────────────────────────────────") + separator.SetTextAlign(tview.AlignCenter) + separator.SetTextColor(newt.ColorBlack) + separator.SetBackgroundColor(newt.ColorGray) + + // Add 'Configure Network' button at bottom right + u.configureNetworkForm = tview.NewForm() + u.configureNetworkForm.SetBorder(false) + u.configureNetworkForm.SetButtonsAlign(tview.AlignRight) + u.configureNetworkForm.AddButton(RENDEZVOUS_CONFIGURE_NETWORK_BUTTON, func() { + u.showNMTUIWithErrorDialog(func() { + u.setFocusToRendezvousIP() + }) + }) + u.configureNetworkForm.SetButtonActivatedStyle(tcell.StyleDefault.Background(newt.ColorRed). + Foreground(newt.ColorGray)) + u.configureNetworkForm.SetButtonStyle(tcell.StyleDefault.Background(newt.ColorGray). + Foreground(newt.ColorBlack)) + + u.rendezvousIPMainFlex = tview.NewFlex(). SetDirection(tview.FlexRow). AddItem(rendezvousTextFlex, rendezvousTextNumRows, 0, false). AddItem(u.rendezvousIPForm, 5, 0, false). AddItem(selectTextFlex, selectTextNumRows, 0, false). - AddItem(u.selectIPForm, 4, 0, false) - mainFlex.SetTitle(" Rendezvous node setup "). + AddItem(u.selectIPForm, 3, 0, false). + AddItem(separator, 1, 0, false). + AddItem(u.configureNetworkForm, 3, 0, false) + u.rendezvousIPMainFlex.SetTitle(" Rendezvous node setup "). SetTitleColor(newt.ColorRed). SetBorder(true) + mainFlexHeight := 5 + 3 + 1 + 3 // form + selectIPForm + separator + configureNetworkForm innerFlex := tview.NewFlex().SetDirection(tview.FlexRow). AddItem(nil, 0, 1, false). - AddItem(mainFlex, mainFlexHeight+1+rendezvousTextNumRows+selectTextNumRows, 0, false). + AddItem(u.rendezvousIPMainFlex, mainFlexHeight+1+rendezvousTextNumRows+selectTextNumRows, 0, false). AddItem(nil, 0, 1, false) // Allow the user to cycle the focus only over the configured items - mainFlex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + u.rendezvousIPMainFlex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { - case tcell.KeyTab, tcell.KeyUp: + case tcell.KeyTab, tcell.KeyDown: u.focusedItem++ if u.focusedItem > len(u.focusableItems)-1 { u.focusedItem = 0 } - case tcell.KeyBacktab, tcell.KeyDown: + case tcell.KeyBacktab, tcell.KeyUp: u.focusedItem-- if u.focusedItem < 0 { u.focusedItem = len(u.focusableItems) - 1 diff --git a/tools/agent_tui/ui/rendezvous_ip_save_success_modal.go b/tools/agent_tui/ui/rendezvous_ip_save_success_modal.go index 51e9adb3..a996ad88 100644 --- a/tools/agent_tui/ui/rendezvous_ip_save_success_modal.go +++ b/tools/agent_tui/ui/rendezvous_ip_save_success_modal.go @@ -9,8 +9,8 @@ import ( const ( PAGE_RENDEZVOUS_IP_SAVE_SUCCESS string = "rendezvousIPSaveSuccessPage" - CONTINUE_BUTTON = "" - BACK_BUTTON = "" + CONTINUE_BUTTON string = "" + BACK_BUTTON string = "" SUCCESS_TEXT_FORMAT = "Successfully saved %s as the rendezvous node IP. " OTHER_NODES_TEXT_FORMAT = "Enter %s as the rendezvous node IP on the other nodes that will form the cluster." diff --git a/tools/agent_tui/ui/rendezvous_ip_select.go b/tools/agent_tui/ui/rendezvous_ip_select.go index 21e5c1b9..7b534838 100644 --- a/tools/agent_tui/ui/rendezvous_ip_select.go +++ b/tools/agent_tui/ui/rendezvous_ip_select.go @@ -135,7 +135,7 @@ func (u *UI) updateSelectIPList(ipAddresses []string) { // only add spacer line if there are IP addresses options = append(options, EMPTY_OPTION) } - options = append(options, backOption, RENDEZVOUS_CONFIGURE_NETWORK_BUTTON) + options = append(options, backOption) for _, selected := range options { u.selectIPList.AddItem(selected, "", rune(0), func() { switch selected { @@ -143,12 +143,6 @@ func (u *UI) updateSelectIPList(ipAddresses []string) { // spacing between IP addresses and buttons case backOption: u.setFocusToRendezvousIP() - case RENDEZVOUS_CONFIGURE_NETWORK_BUTTON: - u.showNMTUIWithErrorDialog(func() { - u.refreshSelectIPList() - u.setFocusToSelectIP() - }) - u.setFocusToSelectIP() default: err := u.saveRendezvousIPAddress(selected) if err != nil { diff --git a/tools/agent_tui/ui/timeout_modal.go b/tools/agent_tui/ui/timeout_modal.go index e1789cf1..f4154bc7 100644 --- a/tools/agent_tui/ui/timeout_modal.go +++ b/tools/agent_tui/ui/timeout_modal.go @@ -4,26 +4,36 @@ import ( "fmt" "time" - "github.com/openshift/agent-installer-utils/tools/agent_tui/checks" "github.com/openshift/agent-installer-utils/tools/agent_tui/newt" "github.com/rivo/tview" ) const ( - YES_BUTTON string = "" - NO_BUTTON string = "" - PAGE_TIMEOUTSCREEN string = "timeout" + YES_BUTTON string = "" + NO_BUTTON string = "" + MODIFY_BUTTON string = "" + QUIT_TIMEOUT_BUTTON string = "" + PAGE_TIMEOUTSCREEN string = "timeout" + PAGE_RENDEZVOUS_IP_TIMEOUT string = "rendezvousIPTimeout" + + timeout = 20 * time.Second - timeout = 20 * time.Second modalText = "Agent-based installer connectivity checks passed. No additional network configuration is required." + "Do you still wish to modify the network configuration for this host?\n\n " + "This prompt will timeout in [red]%.f [white]seconds." + rendezvousIPTimeoutModalText = "Rendezvous IP has been set to [red]%s[white].\n\n" + + "Press to change it, or to exit.\n\n" + + "This prompt will automatically exit in [red]%.f[white] seconds." ) +// ============================================================================ +// Network Configuration Timeout Modal +// ============================================================================ + // Creates the timeout modal but does not add the modal // to pages. The activeUserPrompt function does that // when all checks are successful. -func (u *UI) createTimeoutModal(config checks.Config) { +func (u *UI) createTimeoutModal() { // view is the modal asking the user if they would still // like to change their network configuration. u.timeoutModal = tview.NewModal(). @@ -53,33 +63,121 @@ func (u *UI) ShowTimeoutDialog() { u.app.SetFocus(u.timeoutModal) u.pages.ShowPage(PAGE_TIMEOUTSCREEN) + // Start countdown timer + u.startCountdownTimer(timeout, u.timeoutDialogCancel, func(remaining float64) { + // Update message with remaining time + u.app.QueueUpdateDraw(func() { + u.timeoutModal.SetText(fmt.Sprintf(modalText, remaining)) + }) + }, func() { + // On timeout - exit application + u.app.Stop() + }) +} + +func (u *UI) cancelUserPrompt() { + u.timeoutDialogCancel <- true + u.setIsTimeoutDialogActive(false) + u.setFocusToChecks() +} + +// ============================================================================ +// Rendezvous IP Timeout Modal +// ============================================================================ + +// createRendezvousIPTimeoutModal creates the rendezvous IP timeout modal +func (u *UI) createRendezvousIPTimeoutModal() { + u.rendezvousIPTimeoutModal = tview.NewModal(). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + if buttonIndex == 0 { + // Modify button - close modal and show form with prefilled IP + u.cancelRendezvousIPTimeout() + u.setFocusToRendezvousIP() + } else { + // Quit button - exit the application + u.cancelRendezvousIPTimeout() + u.app.Stop() + } + }). + SetBackgroundColor(newt.ColorGray) + u.rendezvousIPTimeoutModal. + SetBorderColor(newt.ColorBlack). + SetBorder(true) + u.rendezvousIPTimeoutModal. + SetButtonBackgroundColor(newt.ColorGray). + SetButtonTextColor(newt.ColorRed) + + userPromptButtons := []string{MODIFY_BUTTON, QUIT_TIMEOUT_BUTTON} + u.rendezvousIPTimeoutModal.AddButtons(userPromptButtons) + u.pages.AddPage(PAGE_RENDEZVOUS_IP_TIMEOUT, u.rendezvousIPTimeoutModal, true, false) +} + +func (u *UI) ShowRendezvousIPTimeoutDialog(rendezvousIP string) { + u.setIsRendezvousIPTimeoutActive(true) + u.rendezvousIPTimeoutModal.SetText(fmt.Sprintf(rendezvousIPTimeoutModalText, rendezvousIP, timeout.Seconds())) + u.app.SetFocus(u.rendezvousIPTimeoutModal) + u.pages.ShowPage(PAGE_RENDEZVOUS_IP_TIMEOUT) + + // Start countdown timer + u.startCountdownTimer(timeout, u.rendezvousIPTimeoutCancel, func(remaining float64) { + // Update message with remaining time + u.app.QueueUpdateDraw(func() { + u.rendezvousIPTimeoutModal.SetText(fmt.Sprintf(rendezvousIPTimeoutModalText, rendezvousIP, remaining)) + }) + }, func() { + // On timeout - quit the application + u.app.QueueUpdateDraw(func() { + u.setIsRendezvousIPTimeoutActive(false) + u.pages.HidePage(PAGE_RENDEZVOUS_IP_TIMEOUT) + u.logger.Infof("Rendezvous IP timeout expired, exiting application") + u.app.Stop() + }) + }) +} + +func (u *UI) cancelRendezvousIPTimeout() { + u.rendezvousIPTimeoutCancel <- true + u.setIsRendezvousIPTimeoutActive(false) + u.pages.HidePage(PAGE_RENDEZVOUS_IP_TIMEOUT) +} + +// ============================================================================ +// Common Helper Functions +// ============================================================================ + +// startCountdownTimer runs a countdown timer in a goroutine +// duration: how long until timeout +// cancelChan: channel to cancel the timer +// onTick: called every second with remaining time in seconds +// onTimeout: called when timer expires +func (u *UI) startCountdownTimer( + duration time.Duration, + cancelChan chan bool, + onTick func(remaining float64), + onTimeout func(), +) { start := time.Now() ticker := time.NewTicker(1 * time.Second) go func() { + defer ticker.Stop() + for { select { - case <-u.timeoutDialogCancel: - ticker.Stop() + case <-cancelChan: return case t := <-ticker.C: elapsed := t.Sub(start) - if elapsed >= timeout { - ticker.Stop() - u.app.Stop() + if elapsed >= duration { + onTimeout() + return } - u.app.QueueUpdateDraw(func() { - u.timeoutModal.SetText(fmt.Sprintf(modalText, timeout.Seconds()-elapsed.Seconds())) - }) + remaining := duration.Seconds() - elapsed.Seconds() + onTick(remaining) } } }() -} -func (u *UI) cancelUserPrompt() { - u.timeoutDialogCancel <- true - u.setIsTimeoutDialogActive(false) - u.setFocusToChecks() } diff --git a/tools/agent_tui/ui/ui.go b/tools/agent_tui/ui/ui.go index f5b21b76..f76b6c4b 100644 --- a/tools/agent_tui/ui/ui.go +++ b/tools/agent_tui/ui/ui.go @@ -26,12 +26,19 @@ type UI struct { // Rendezvous node IP workflow rendezvousIPForm *tview.Form + rendezvousIPMainFlex *tview.Flex selectIPForm *tview.Form selectIPList *tview.List rendezvousModal *tview.Modal rendezvousIPFormActive atomic.Value rendezvousIPSaveSuccessModal *tview.Modal connectivityFailModal *tview.Modal + configureNetworkForm *tview.Form + + initialRendezvousIP string + rendezvousIPTimeoutModal *tview.Modal + rendezvousIPTimeoutActive atomic.Value + rendezvousIPTimeoutCancel chan bool focusableItems []tview.Primitive // the list of widgets that can be focused focusedItem int // the current focused widget @@ -39,15 +46,18 @@ type UI struct { logger *logrus.Logger } -func NewUI(app *tview.Application, config checks.Config, logger *logrus.Logger) *UI { +func NewUI(app *tview.Application, config checks.Config, logger *logrus.Logger, initialRendezvousIP string) *UI { ui := &UI{ - app: app, - timeoutDialogCancel: make(chan bool), - logger: logger, + app: app, + timeoutDialogCancel: make(chan bool), + rendezvousIPTimeoutCancel: make(chan bool), + logger: logger, + initialRendezvousIP: initialRendezvousIP, } ui.nmtuiActive.Store(false) ui.timeoutDialogActive.Store(false) ui.rendezvousIPFormActive.Store(false) + ui.rendezvousIPTimeoutActive.Store(false) ui.dirty.Store(false) ui.create(config) return ui @@ -80,8 +90,9 @@ func (u *UI) setFocusToRendezvousIP() { // reset u.focusableItems to those on the rendezvous IP page u.focusableItems = []tview.Primitive{ u.rendezvousIPForm.GetFormItemByLabel(FIELD_ENTER_RENDEZVOUS_IP), - u.selectIPForm.GetButton(0), - u.rendezvousIPForm.GetButton(0), + u.rendezvousIPForm.GetButton(0), // Save rendezvous IP + u.selectIPForm.GetButton(0), // This is the rendezvous node + u.configureNetworkForm.GetButton(0), // Configure Network button } u.pages.SwitchToPage(PAGE_RENDEZVOUS_IP) @@ -120,13 +131,22 @@ func (u *UI) IsDirty() bool { return u.dirty.Load().(bool) } +func (u *UI) setIsRendezvousIPTimeoutActive(isActive bool) { + u.rendezvousIPTimeoutActive.Store(isActive) +} + +func (u *UI) IsRendezvousIPTimeoutActive() bool { + return u.rendezvousIPTimeoutActive.Load().(bool) +} + func (u *UI) create(config checks.Config) { u.pages = tview.NewPages() u.createCheckPage(config) - u.createTimeoutModal(config) + u.createTimeoutModal() u.createSplashScreen() u.createRendezvousIPPage(config) u.createRendezvousModal() + u.createRendezvousIPTimeoutModal() u.createSelectHostIPPage() u.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if !u.IsRendezvousIPFormActive() {