Refactor mvu Spinner for better readability, add also some styling to it
- commit
- 08459e54daacea3f36a7e509a050014aa78cbb9a
- author
- bnema · · 2023-11-08 12:31 UTC
- parent
- 8d6a219
- signature
- unsigned
2 files · +71 -70
M internal/cli/cmd/deploy.go +3 -3
| @@ -125,13 +125,13 @@ func NewDeployCommand(a *cli.App) *cobra.Command { | |||
| 125 | 125 | ||
| 126 | 126 | // Check if the deployment was successful | |
| 127 | 127 | if !finalModel.DeploymentDone { | |
| 128 | - | fmt.Println("Deployment failed.") | |
| 128 | + | fmt.Println("Deployment failed check your configuration and try again.") | |
| 129 | 129 | return | |
| 130 | 130 | } | |
| 131 | 131 | ||
| 132 | 132 | // Print the final message | |
| 133 | - | color.Green("Deployment successful!") | |
| 134 | - | fmt.Println("Your application is now available at:", targetDomain) | |
| 133 | + | color.Green(" Deployment successful!") | |
| 134 | + | fmt.Println(" Your application is now available at:", targetDomain) | |
| 135 | 135 | }, | |
| 136 | 136 | } | |
| 137 | 137 | ||
| @@ -125,13 +125,13 @@ func NewDeployCommand(a *cli.App) *cobra.Command { | |||||
| 125 | 125 | ||||
| 126 | // Check if the deployment was successful | 126 | // Check if the deployment was successful | ||
| 127 | if !finalModel.DeploymentDone { | 127 | if !finalModel.DeploymentDone { | ||
| 128 | - | fmt.Println("Deployment failed.") | 128 | + | fmt.Println("Deployment failed check your configuration and try again.") |
| 129 | return | 129 | return | ||
| 130 | } | 130 | } | ||
| 131 | 131 | ||||
| 132 | // Print the final message | 132 | // Print the final message | ||
| 133 | - | color.Green("Deployment successful!") | 133 | + | color.Green(" Deployment successful!") |
| 134 | - | fmt.Println("Your application is now available at:", targetDomain) | 134 | + | fmt.Println(" Your application is now available at:", targetDomain) |
| 135 | }, | 135 | }, | ||
| 136 | } | 136 | } | ||
| 137 | 137 | ||||
M internal/cli/mvu/spinner.go +68 -67
| @@ -11,40 +11,90 @@ import ( | |||
| 11 | 11 | "github.com/charmbracelet/lipgloss" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| 14 | + | const ( | |
| 15 | + | defaultMaxRetries = 20 | |
| 16 | + | httpTimeout = 10 * time.Second | |
| 17 | + | retryInterval = 1 * time.Second | |
| 18 | + | ) | |
| 19 | + | ||
| 14 | 20 | type SpinnerModel struct { | |
| 15 | 21 | spinner spinner.Model | |
| 16 | 22 | quitting bool | |
| 17 | 23 | err error | |
| 18 | - | imageName string | |
| 19 | - | targetDomain string | |
| 20 | - | port string | |
| 21 | - | client *http.Client | |
| 22 | - | retryCount int | |
| 23 | - | maxRetries int | |
| 24 | + | deployment DeploymentInfo | |
| 24 | 25 | DeploymentDone bool | |
| 25 | 26 | } | |
| 26 | 27 | ||
| 28 | + | type DeploymentInfo struct { | |
| 29 | + | imageName string | |
| 30 | + | targetDomain string | |
| 31 | + | port string | |
| 32 | + | client *http.Client | |
| 33 | + | retryCount int | |
| 34 | + | maxRetries int | |
| 35 | + | } | |
| 36 | + | ||
| 27 | 37 | type urlCheckedMsg struct { | |
| 28 | 38 | success bool | |
| 29 | 39 | err error | |
| 30 | 40 | } | |
| 31 | 41 | ||
| 42 | + | // NewSpinnerModel returns a new SpinnerModel | |
| 32 | 43 | func NewSpinnerModel() SpinnerModel { | |
| 33 | 44 | s := spinner.New() | |
| 34 | 45 | s.Spinner = spinner.Dot | |
| 35 | - | spinnerStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("205")) | |
| 36 | - | s.Style = spinnerStyle | |
| 46 | + | s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#54baff")) | |
| 37 | 47 | ||
| 38 | 48 | return SpinnerModel{ | |
| 39 | - | spinner: s, | |
| 40 | - | maxRetries: 20, | |
| 49 | + | spinner: s, | |
| 50 | + | deployment: DeploymentInfo{ | |
| 51 | + | maxRetries: defaultMaxRetries, | |
| 52 | + | client: newHTTPClient(), | |
| 53 | + | }, | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | func newHTTPClient() *http.Client { | |
| 58 | + | return &http.Client{ | |
| 59 | + | Timeout: httpTimeout, | |
| 60 | + | Transport: &http.Transport{ | |
| 61 | + | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, | |
| 62 | + | }, | |
| 41 | 63 | } | |
| 42 | 64 | } | |
| 43 | 65 | ||
| 44 | 66 | func (m SpinnerModel) Init() tea.Cmd { | |
| 45 | - | return tea.Batch(m.spinner.Tick, checkURL(m)) | |
| 67 | + | return tea.Batch(m.spinner.Tick, m.checkURL()) | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | // checkURL returns a command that checks the URL and returns a urlCheckedMsg | |
| 71 | + | func (m *SpinnerModel) checkURL() tea.Cmd { | |
| 72 | + | return func() tea.Msg { | |
| 73 | + | success, err := attemptURLCheck(m.deployment) | |
| 74 | + | if err != nil { | |
| 75 | + | return urlCheckedMsg{success: false, err: err} | |
| 76 | + | } | |
| 77 | + | return urlCheckedMsg{success: success, err: nil} | |
| 78 | + | } | |
| 46 | 79 | } | |
| 47 | 80 | ||
| 81 | + | // attemptURLCheck attempts to check the URL and returns a bool and error | |
| 82 | + | func attemptURLCheck(deployment DeploymentInfo) (bool, error) { | |
| 83 | + | for deployment.retryCount = 0; deployment.retryCount < deployment.maxRetries; deployment.retryCount++ { | |
| 84 | + | resp, err := deployment.client.Get(deployment.targetDomain) | |
| 85 | + | if err != nil { | |
| 86 | + | return false, fmt.Errorf("error checking URL: %w", err) | |
| 87 | + | } | |
| 88 | + | resp.Body.Close() | |
| 89 | + | if resp.StatusCode == http.StatusOK { | |
| 90 | + | return true, nil | |
| 91 | + | } | |
| 92 | + | time.Sleep(retryInterval) | |
| 93 | + | } | |
| 94 | + | return false, fmt.Errorf("the URL %s could not be reached after %d attempts", deployment.targetDomain, deployment.retryCount) | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // Update handles the messages sent to the model | |
| 48 | 98 | func (m SpinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |
| 49 | 99 | switch msg := msg.(type) { | |
| 50 | 100 | case tea.KeyMsg: | |
| @@ -73,6 +123,7 @@ func (m SpinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 73 | 123 | } | |
| 74 | 124 | } | |
| 75 | 125 | ||
| 126 | + | // View returns the view for the model | |
| 76 | 127 | func (m SpinnerModel) View() string { | |
| 77 | 128 | if m.quitting { | |
| 78 | 129 | if m.err != nil { | |
| @@ -82,66 +133,16 @@ func (m SpinnerModel) View() string { | |||
| 82 | 133 | } | |
| 83 | 134 | ||
| 84 | 135 | // Use the textStyle to format the entire string | |
| 85 | - | return fmt.Sprintf("\n\n %s Wait while Traefik is setting up the domain and certificates...\n\n", m.spinner.View()) | |
| 86 | - | } | |
| 87 | - | ||
| 88 | - | func checkURL(m SpinnerModel) tea.Cmd { | |
| 89 | - | return func() tea.Msg { | |
| 90 | - | ch := make(chan urlCheckedMsg) | |
| 91 | - | ||
| 92 | - | go func() { | |
| 93 | - | defer close(ch) // Ensure the channel is closed when the goroutine exits | |
| 94 | - | // Initialize the HTTP client if not already done | |
| 95 | - | if m.client == nil { | |
| 96 | - | m.client = &http.Client{ | |
| 97 | - | Timeout: time.Second * 10, // Set a timeout for the request | |
| 98 | - | Transport: &http.Transport{ | |
| 99 | - | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // Skip TLS verification, not recommended for production | |
| 100 | - | }, | |
| 101 | - | } | |
| 102 | - | } | |
| 103 | - | ||
| 104 | - | // Try to check the URL with retries | |
| 105 | - | for i := 0; i < m.maxRetries; i++ { | |
| 106 | - | resp, err := m.client.Get(m.targetDomain) // Send the request | |
| 107 | - | if err != nil { | |
| 108 | - | return | |
| 109 | - | } else { | |
| 110 | - | // Print the response status code | |
| 111 | - | resp.Body.Close() // Close the body to avoid resource leaks | |
| 112 | - | if resp.StatusCode == http.StatusOK { | |
| 113 | - | ch <- urlCheckedMsg{success: true, err: nil} | |
| 114 | - | return | |
| 115 | - | } | |
| 116 | - | } | |
| 117 | - | ||
| 118 | - | m.retryCount++ | |
| 119 | - | time.Sleep(time.Second * 1) // Wait for some time before retrying | |
| 120 | - | } | |
| 121 | - | ||
| 122 | - | // If the code reaches here (maxRetries exceeded) then send a message to the channel | |
| 123 | - | ch <- urlCheckedMsg{ | |
| 124 | - | success: false, | |
| 125 | - | err: fmt.Errorf("the URL %s could not be reached after %d attempts"+m.targetDomain, m.retryCount), | |
| 126 | - | } | |
| 127 | - | }() | |
| 128 | - | ||
| 129 | - | return waitForURLCheckedMsg(ch) | |
| 130 | - | } | |
| 131 | - | } | |
| 132 | - | ||
| 133 | - | func waitForURLCheckedMsg(ch chan urlCheckedMsg) tea.Msg { | |
| 134 | - | msg := <-ch // Wait for a message from the channel | |
| 135 | - | return msg // Return the message to the model's update function | |
| 136 | + | return fmt.Sprintf("\n\n %s Wait while Traefik is setting up the domain and certificates... \n\n", m.spinner.View()) | |
| 136 | 137 | } | |
| 137 | 138 | ||
| 138 | - | // Extracted function to run the TUI program and handle the final model | |
| 139 | + | // RunDeploymentTUI runs the complete deployment TUI and returns the final model | |
| 139 | 140 | func RunDeploymentTUI(client *http.Client, imageName, targetDomain, port string) (SpinnerModel, error) { | |
| 140 | 141 | m := NewSpinnerModel() | |
| 141 | - | m.imageName = imageName | |
| 142 | - | m.targetDomain = targetDomain | |
| 143 | - | m.port = port | |
| 144 | - | m.client = client | |
| 142 | + | m.deployment.imageName = imageName | |
| 143 | + | m.deployment.targetDomain = targetDomain | |
| 144 | + | m.deployment.port = port | |
| 145 | + | m.deployment.client = client | |
| 145 | 146 | ||
| 146 | 147 | p := tea.NewProgram(m) | |
| 147 | 148 | modelInterface, err := p.Run() | |
| @@ -11,40 +11,90 @@ import ( | |||||
| 11 | "github.com/charmbracelet/lipgloss" | 11 | "github.com/charmbracelet/lipgloss" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
| 14 | + | const ( | |||
| 15 | + | defaultMaxRetries = 20 | |||
| 16 | + | httpTimeout = 10 * time.Second | |||
| 17 | + | retryInterval = 1 * time.Second | |||
| 18 | + | ) | |||
| 19 | + | ||||
| 14 | type SpinnerModel struct { | 20 | type SpinnerModel struct { | ||
| 15 | spinner spinner.Model | 21 | spinner spinner.Model | ||
| 16 | quitting bool | 22 | quitting bool | ||
| 17 | err error | 23 | err error | ||
| 18 | - | imageName string | 24 | + | deployment DeploymentInfo |
| 19 | - | targetDomain string | |||
| 20 | - | port string | |||
| 21 | - | client *http.Client | |||
| 22 | - | retryCount int | |||
| 23 | - | maxRetries int | |||
| 24 | DeploymentDone bool | 25 | DeploymentDone bool | ||
| 25 | } | 26 | } | ||
| 26 | 27 | ||||
| 28 | + | type DeploymentInfo struct { | |||
| 29 | + | imageName string | |||
| 30 | + | targetDomain string | |||
| 31 | + | port string | |||
| 32 | + | client *http.Client | |||
| 33 | + | retryCount int | |||
| 34 | + | maxRetries int | |||
| 35 | + | } | |||
| 36 | + | ||||
| 27 | type urlCheckedMsg struct { | 37 | type urlCheckedMsg struct { | ||
| 28 | success bool | 38 | success bool | ||
| 29 | err error | 39 | err error | ||
| 30 | } | 40 | } | ||
| 31 | 41 | ||||
| 42 | + | // NewSpinnerModel returns a new SpinnerModel | |||
| 32 | func NewSpinnerModel() SpinnerModel { | 43 | func NewSpinnerModel() SpinnerModel { | ||
| 33 | s := spinner.New() | 44 | s := spinner.New() | ||
| 34 | s.Spinner = spinner.Dot | 45 | s.Spinner = spinner.Dot | ||
| 35 | - | spinnerStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("205")) | 46 | + | s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#54baff")) |
| 36 | - | s.Style = spinnerStyle | |||
| 37 | 47 | ||||
| 38 | return SpinnerModel{ | 48 | return SpinnerModel{ | ||
| 39 | - | spinner: s, | 49 | + | spinner: s, |
| 40 | - | maxRetries: 20, | 50 | + | deployment: DeploymentInfo{ |
| 51 | + | maxRetries: defaultMaxRetries, | |||
| 52 | + | client: newHTTPClient(), | |||
| 53 | + | }, | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | func newHTTPClient() *http.Client { | |||
| 58 | + | return &http.Client{ | |||
| 59 | + | Timeout: httpTimeout, | |||
| 60 | + | Transport: &http.Transport{ | |||
| 61 | + | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, | |||
| 62 | + | }, | |||
| 41 | } | 63 | } | ||
| 42 | } | 64 | } | ||
| 43 | 65 | ||||
| 44 | func (m SpinnerModel) Init() tea.Cmd { | 66 | func (m SpinnerModel) Init() tea.Cmd { | ||
| 45 | - | return tea.Batch(m.spinner.Tick, checkURL(m)) | 67 | + | return tea.Batch(m.spinner.Tick, m.checkURL()) |
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | // checkURL returns a command that checks the URL and returns a urlCheckedMsg | |||
| 71 | + | func (m *SpinnerModel) checkURL() tea.Cmd { | |||
| 72 | + | return func() tea.Msg { | |||
| 73 | + | success, err := attemptURLCheck(m.deployment) | |||
| 74 | + | if err != nil { | |||
| 75 | + | return urlCheckedMsg{success: false, err: err} | |||
| 76 | + | } | |||
| 77 | + | return urlCheckedMsg{success: success, err: nil} | |||
| 78 | + | } | |||
| 46 | } | 79 | } | ||
| 47 | 80 | ||||
| 81 | + | // attemptURLCheck attempts to check the URL and returns a bool and error | |||
| 82 | + | func attemptURLCheck(deployment DeploymentInfo) (bool, error) { | |||
| 83 | + | for deployment.retryCount = 0; deployment.retryCount < deployment.maxRetries; deployment.retryCount++ { | |||
| 84 | + | resp, err := deployment.client.Get(deployment.targetDomain) | |||
| 85 | + | if err != nil { | |||
| 86 | + | return false, fmt.Errorf("error checking URL: %w", err) | |||
| 87 | + | } | |||
| 88 | + | resp.Body.Close() | |||
| 89 | + | if resp.StatusCode == http.StatusOK { | |||
| 90 | + | return true, nil | |||
| 91 | + | } | |||
| 92 | + | time.Sleep(retryInterval) | |||
| 93 | + | } | |||
| 94 | + | return false, fmt.Errorf("the URL %s could not be reached after %d attempts", deployment.targetDomain, deployment.retryCount) | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // Update handles the messages sent to the model | |||
| 48 | func (m SpinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | 98 | func (m SpinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | ||
| 49 | switch msg := msg.(type) { | 99 | switch msg := msg.(type) { | ||
| 50 | case tea.KeyMsg: | 100 | case tea.KeyMsg: | ||
| @@ -73,6 +123,7 @@ func (m SpinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||||
| 73 | } | 123 | } | ||
| 74 | } | 124 | } | ||
| 75 | 125 | ||||
| 126 | + | // View returns the view for the model | |||
| 76 | func (m SpinnerModel) View() string { | 127 | func (m SpinnerModel) View() string { | ||
| 77 | if m.quitting { | 128 | if m.quitting { | ||
| 78 | if m.err != nil { | 129 | if m.err != nil { | ||
| @@ -82,66 +133,16 @@ func (m SpinnerModel) View() string { | |||||
| 82 | } | 133 | } | ||
| 83 | 134 | ||||
| 84 | // Use the textStyle to format the entire string | 135 | // Use the textStyle to format the entire string | ||
| 85 | - | return fmt.Sprintf("\n\n %s Wait while Traefik is setting up the domain and certificates...\n\n", m.spinner.View()) | 136 | + | return fmt.Sprintf("\n\n %s Wait while Traefik is setting up the domain and certificates... \n\n", m.spinner.View()) |
| 86 | - | } | |||
| 87 | - | ||||
| 88 | - | func checkURL(m SpinnerModel) tea.Cmd { | |||
| 89 | - | return func() tea.Msg { | |||
| 90 | - | ch := make(chan urlCheckedMsg) | |||
| 91 | - | ||||
| 92 | - | go func() { | |||
| 93 | - | defer close(ch) // Ensure the channel is closed when the goroutine exits | |||
| 94 | - | // Initialize the HTTP client if not already done | |||
| 95 | - | if m.client == nil { | |||
| 96 | - | m.client = &http.Client{ | |||
| 97 | - | Timeout: time.Second * 10, // Set a timeout for the request | |||
| 98 | - | Transport: &http.Transport{ | |||
| 99 | - | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // Skip TLS verification, not recommended for production | |||
| 100 | - | }, | |||
| 101 | - | } | |||
| 102 | - | } | |||
| 103 | - | ||||
| 104 | - | // Try to check the URL with retries | |||
| 105 | - | for i := 0; i < m.maxRetries; i++ { | |||
| 106 | - | resp, err := m.client.Get(m.targetDomain) // Send the request | |||
| 107 | - | if err != nil { | |||
| 108 | - | return | |||
| 109 | - | } else { | |||
| 110 | - | // Print the response status code | |||
| 111 | - | resp.Body.Close() // Close the body to avoid resource leaks | |||
| 112 | - | if resp.StatusCode == http.StatusOK { | |||
| 113 | - | ch <- urlCheckedMsg{success: true, err: nil} | |||
| 114 | - | return | |||
| 115 | - | } | |||
| 116 | - | } | |||
| 117 | - | ||||
| 118 | - | m.retryCount++ | |||
| 119 | - | time.Sleep(time.Second * 1) // Wait for some time before retrying | |||
| 120 | - | } | |||
| 121 | - | ||||
| 122 | - | // If the code reaches here (maxRetries exceeded) then send a message to the channel | |||
| 123 | - | ch <- urlCheckedMsg{ | |||
| 124 | - | success: false, | |||
| 125 | - | err: fmt.Errorf("the URL %s could not be reached after %d attempts"+m.targetDomain, m.retryCount), | |||
| 126 | - | } | |||
| 127 | - | }() | |||
| 128 | - | ||||
| 129 | - | return waitForURLCheckedMsg(ch) | |||
| 130 | - | } | |||
| 131 | - | } | |||
| 132 | - | ||||
| 133 | - | func waitForURLCheckedMsg(ch chan urlCheckedMsg) tea.Msg { | |||
| 134 | - | msg := <-ch // Wait for a message from the channel | |||
| 135 | - | return msg // Return the message to the model's update function | |||
| 136 | } | 137 | } | ||
| 137 | 138 | ||||
| 138 | - | // Extracted function to run the TUI program and handle the final model | 139 | + | // RunDeploymentTUI runs the complete deployment TUI and returns the final model |
| 139 | func RunDeploymentTUI(client *http.Client, imageName, targetDomain, port string) (SpinnerModel, error) { | 140 | func RunDeploymentTUI(client *http.Client, imageName, targetDomain, port string) (SpinnerModel, error) { | ||
| 140 | m := NewSpinnerModel() | 141 | m := NewSpinnerModel() | ||
| 141 | - | m.imageName = imageName | 142 | + | m.deployment.imageName = imageName |
| 142 | - | m.targetDomain = targetDomain | 143 | + | m.deployment.targetDomain = targetDomain |
| 143 | - | m.port = port | 144 | + | m.deployment.port = port |
| 144 | - | m.client = client | 145 | + | m.deployment.client = client |
| 145 | 146 | ||||
| 146 | p := tea.NewProgram(m) | 147 | p := tea.NewProgram(m) | ||
| 147 | modelInterface, err := p.Run() | 148 | modelInterface, err := p.Run() | ||