detached 08459e5

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 {
125125
126126 // Check if the deployment was successful
127127 if !finalModel.DeploymentDone {
128- fmt.Println("Deployment failed.")
128+ fmt.Println("Deployment failed check your configuration and try again.")
129129 return
130130 }
131131
132132 // 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)
135135 },
136136 }
137137
@@ -125,13 +125,13 @@ func NewDeployCommand(a *cli.App) *cobra.Command {
125 125
126 // Check if the deployment was successful126 // 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 return129 return
130 }130 }
131 131
132 // Print the final message132 // 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 (
1111 "github.com/charmbracelet/lipgloss"
1212 )
1313
14+const (
15+ defaultMaxRetries = 20
16+ httpTimeout = 10 * time.Second
17+ retryInterval = 1 * time.Second
18+)
19+
1420 type SpinnerModel struct {
1521 spinner spinner.Model
1622 quitting bool
1723 err error
18- imageName string
19- targetDomain string
20- port string
21- client *http.Client
22- retryCount int
23- maxRetries int
24+ deployment DeploymentInfo
2425 DeploymentDone bool
2526 }
2627
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+
2737 type urlCheckedMsg struct {
2838 success bool
2939 err error
3040 }
3141
42+// NewSpinnerModel returns a new SpinnerModel
3243 func NewSpinnerModel() SpinnerModel {
3344 s := spinner.New()
3445 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"))
3747
3848 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+ },
4163 }
4264 }
4365
4466 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+ }
4679 }
4780
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
4898 func (m SpinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
4999 switch msg := msg.(type) {
50100 case tea.KeyMsg:
@@ -73,6 +123,7 @@ func (m SpinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
73123 }
74124 }
75125
126+// View returns the view for the model
76127 func (m SpinnerModel) View() string {
77128 if m.quitting {
78129 if m.err != nil {
@@ -82,66 +133,16 @@ func (m SpinnerModel) View() string {
82133 }
83134
84135 // 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())
136137 }
137138
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
139140 func RunDeploymentTUI(client *http.Client, imageName, targetDomain, port string) (SpinnerModel, error) {
140141 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
145146
146147 p := tea.NewProgram(m)
147148 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.Model21 spinner spinner.Model
16 quitting bool22 quitting bool
17 err error23 err error
18- imageName string24+ deployment DeploymentInfo
19- targetDomain string
20- port string
21- client *http.Client
22- retryCount int
23- maxRetries int
24 DeploymentDone bool25 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 bool38 success bool
29 err error39 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.Dot45 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 string135 // 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 model139+// 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 = imageName142+ m.deployment.imageName = imageName
142- m.targetDomain = targetDomain143+ m.deployment.targetDomain = targetDomain
143- m.port = port144+ m.deployment.port = port
144- m.client = client145+ 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()

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close