|
| 1 | +// SPDX-FileCopyrightText: 2025 OVH SAS <opensource@ovh.net> |
| 2 | +// |
| 3 | +// SPDX-License-Identifier: Apache-2.0 |
| 4 | + |
| 5 | +//go:build !(js && wasm) |
| 6 | + |
| 7 | +package browser |
| 8 | + |
| 9 | +import ( |
| 10 | + "fmt" |
| 11 | + "net/url" |
| 12 | + "strings" |
| 13 | + |
| 14 | + tea "github.com/charmbracelet/bubbletea" |
| 15 | + "github.com/charmbracelet/lipgloss" |
| 16 | + httpLib "github.com/ovh/ovhcloud-cli/internal/http" |
| 17 | +) |
| 18 | + |
| 19 | +// gatewayModels lists the available OVHcloud gateway sizes. |
| 20 | +var gatewayModels = []string{"s", "m", "l", "xl", "2xl", "3xl"} |
| 21 | + |
| 22 | +// ─── API call ───────────────────────────────────────────────────────────────── |
| 23 | + |
| 24 | +// createGatewayFromWizard sends the POST request to create a gateway linked to the |
| 25 | +// private network/subnet stored in the wizard state. |
| 26 | +func (m Model) createGatewayFromWizard() tea.Cmd { |
| 27 | + return func() tea.Msg { |
| 28 | + if m.cloudProject == "" { |
| 29 | + return gwCreatedMsg{err: fmt.Errorf("aucun projet cloud sélectionné")} |
| 30 | + } |
| 31 | + |
| 32 | + model := gatewayModels[m.wizard.gwModelIdx] |
| 33 | + body := map[string]interface{}{ |
| 34 | + "model": model, |
| 35 | + "name": m.wizard.gwName, |
| 36 | + } |
| 37 | + |
| 38 | + endpoint := fmt.Sprintf( |
| 39 | + "/v1/cloud/project/%s/region/%s/network/%s/subnet/%s/gateway", |
| 40 | + m.cloudProject, |
| 41 | + url.PathEscape(m.wizard.gwRegion), |
| 42 | + url.PathEscape(m.wizard.gwNetworkID), |
| 43 | + url.PathEscape(m.wizard.gwSubnetID), |
| 44 | + ) |
| 45 | + |
| 46 | + var result map[string]interface{} |
| 47 | + if err := httpLib.Client.Post(endpoint, body, &result); err != nil { |
| 48 | + return gwCreatedMsg{err: fmt.Errorf("failed to create gateway: %w", err)} |
| 49 | + } |
| 50 | + return gwCreatedMsg{gateway: result} |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +// ─── Render functions ───────────────────────────────────────────────────────── |
| 55 | + |
| 56 | +func (m Model) renderGwWizardModelStep(width int) string { |
| 57 | + var content strings.Builder |
| 58 | + titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FFFFFF")) |
| 59 | + selectedStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#00FF7F")).Padding(0, 1) |
| 60 | + dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#CCCCCC")) |
| 61 | + descStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")) |
| 62 | + |
| 63 | + content.WriteString(titleStyle.Render("Choisir le modèle de la Gateway :") + "\n\n") |
| 64 | + content.WriteString(descStyle.Render( |
| 65 | + fmt.Sprintf("Réseau : %s • Région : %s", m.wizard.gwNetworkName, m.wizard.gwRegion), |
| 66 | + ) + "\n\n") |
| 67 | + |
| 68 | + for i, model := range gatewayModels { |
| 69 | + if i == m.wizard.gwModelIdx { |
| 70 | + content.WriteString(selectedStyle.Render("▶ " + strings.ToUpper(model)) + "\n") |
| 71 | + } else { |
| 72 | + content.WriteString(dimStyle.Render(" "+strings.ToUpper(model)) + "\n") |
| 73 | + } |
| 74 | + } |
| 75 | + |
| 76 | + content.WriteString("\n" + lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")). |
| 77 | + Render("↑↓ Naviguer • Enter : Sélectionner • Esc : Annuler")) |
| 78 | + return content.String() |
| 79 | +} |
| 80 | + |
| 81 | +func (m Model) renderGwWizardNameStep(width int) string { |
| 82 | + var content strings.Builder |
| 83 | + titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FFFFFF")) |
| 84 | + descStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")) |
| 85 | + |
| 86 | + content.WriteString(titleStyle.Render("Nom de la Gateway :") + "\n\n") |
| 87 | + content.WriteString(descStyle.Render( |
| 88 | + fmt.Sprintf("Modèle : %s • Réseau : %s", strings.ToUpper(gatewayModels[m.wizard.gwModelIdx]), m.wizard.gwNetworkName), |
| 89 | + ) + "\n\n") |
| 90 | + |
| 91 | + if m.wizard.errorMsg != "" { |
| 92 | + content.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#FF6B6B")).Render("Erreur : "+m.wizard.errorMsg) + "\n\n") |
| 93 | + } |
| 94 | + |
| 95 | + inputStyle := lipgloss.NewStyle(). |
| 96 | + Border(lipgloss.RoundedBorder()). |
| 97 | + BorderForeground(lipgloss.Color("#00FF7F")). |
| 98 | + Padding(0, 1).Width(40) |
| 99 | + content.WriteString(inputStyle.Render(m.wizard.gwNameInput+"▌") + "\n\n") |
| 100 | + content.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")). |
| 101 | + Render("Tapez le nom • Enter : Continuer • ← : Retour • Esc : Annuler")) |
| 102 | + return content.String() |
| 103 | +} |
| 104 | + |
| 105 | +func (m Model) renderGwWizardConfirmStep(width int) string { |
| 106 | + var content strings.Builder |
| 107 | + titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FFFFFF")) |
| 108 | + labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#888888")).Width(22) |
| 109 | + valueStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")) |
| 110 | + |
| 111 | + content.WriteString(titleStyle.Render("Confirmer la création de la Gateway :") + "\n\n") |
| 112 | + content.WriteString(labelStyle.Render(" Réseau :") + valueStyle.Render(m.wizard.gwNetworkName) + "\n") |
| 113 | + content.WriteString(labelStyle.Render(" Région :") + valueStyle.Render(m.wizard.gwRegion) + "\n") |
| 114 | + content.WriteString(labelStyle.Render(" Modèle :") + valueStyle.Render(strings.ToUpper(gatewayModels[m.wizard.gwModelIdx])) + "\n") |
| 115 | + content.WriteString(labelStyle.Render(" Nom :") + valueStyle.Render(m.wizard.gwName) + "\n") |
| 116 | + content.WriteString("\n") |
| 117 | + |
| 118 | + if m.wizard.isLoading { |
| 119 | + content.WriteString(loadingStyle.Render("⏳ Création en cours...")) |
| 120 | + return content.String() |
| 121 | + } |
| 122 | + if m.wizard.errorMsg != "" { |
| 123 | + content.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#FF6B6B")).Render("Erreur : "+m.wizard.errorMsg) + "\n\n") |
| 124 | + } |
| 125 | + |
| 126 | + btnCreate := lipgloss.NewStyle().Background(lipgloss.Color("#00FF7F")).Foreground(lipgloss.Color("#000000")).Bold(true).Padding(0, 2).Render(" Créer ") |
| 127 | + btnCancel := lipgloss.NewStyle().Background(lipgloss.Color("#333333")).Foreground(lipgloss.Color("#CCCCCC")).Padding(0, 2).Render(" Annuler ") |
| 128 | + if m.wizard.gwConfirmBtnIdx == 1 { |
| 129 | + btnCreate = lipgloss.NewStyle().Background(lipgloss.Color("#333333")).Foreground(lipgloss.Color("#CCCCCC")).Padding(0, 2).Render(" Créer ") |
| 130 | + btnCancel = lipgloss.NewStyle().Background(lipgloss.Color("#FF6B6B")).Foreground(lipgloss.Color("#000000")).Bold(true).Padding(0, 2).Render(" Annuler ") |
| 131 | + } |
| 132 | + content.WriteString(btnCreate + " " + btnCancel + "\n\n") |
| 133 | + content.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")). |
| 134 | + Render("←→ : Sélectionner • Enter : Confirmer • Esc : Annuler")) |
| 135 | + return content.String() |
| 136 | +} |
| 137 | + |
| 138 | +// ─── Key handlers ───────────────────────────────────────────────────────────── |
| 139 | + |
| 140 | +func (m Model) handleGwWizardModelKeys(key string) (tea.Model, tea.Cmd) { |
| 141 | + switch key { |
| 142 | + case "up", "k": |
| 143 | + if m.wizard.gwModelIdx > 0 { |
| 144 | + m.wizard.gwModelIdx-- |
| 145 | + } |
| 146 | + case "down", "j": |
| 147 | + if m.wizard.gwModelIdx < len(gatewayModels)-1 { |
| 148 | + m.wizard.gwModelIdx++ |
| 149 | + } |
| 150 | + case "enter": |
| 151 | + m.wizard.step = GwWizardStepName |
| 152 | + } |
| 153 | + return m, nil |
| 154 | +} |
| 155 | + |
| 156 | +func (m Model) handleGwWizardNameKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) { |
| 157 | + key := msg.String() |
| 158 | + switch key { |
| 159 | + case "enter": |
| 160 | + name := strings.TrimSpace(m.wizard.gwNameInput) |
| 161 | + if name == "" { |
| 162 | + m.wizard.errorMsg = "Le nom ne peut pas être vide" |
| 163 | + return m, nil |
| 164 | + } |
| 165 | + m.wizard.gwName = name |
| 166 | + m.wizard.errorMsg = "" |
| 167 | + m.wizard.step = GwWizardStepConfirm |
| 168 | + case "left": |
| 169 | + m.wizard.step = GwWizardStepModel |
| 170 | + case "backspace": |
| 171 | + if len(m.wizard.gwNameInput) > 0 { |
| 172 | + m.wizard.gwNameInput = m.wizard.gwNameInput[:len(m.wizard.gwNameInput)-1] |
| 173 | + } |
| 174 | + default: |
| 175 | + if len(msg.Runes) > 0 { |
| 176 | + m.wizard.gwNameInput += string(msg.Runes) |
| 177 | + } |
| 178 | + } |
| 179 | + return m, nil |
| 180 | +} |
| 181 | + |
| 182 | +func (m Model) handleGwWizardConfirmKeys(key string) (tea.Model, tea.Cmd) { |
| 183 | + switch key { |
| 184 | + case "left", "h": |
| 185 | + m.wizard.gwConfirmBtnIdx = 0 |
| 186 | + case "right", "l": |
| 187 | + m.wizard.gwConfirmBtnIdx = 1 |
| 188 | + case "enter": |
| 189 | + if m.wizard.gwConfirmBtnIdx == 1 { |
| 190 | + // Cancel → go back to name step |
| 191 | + m.wizard.step = GwWizardStepName |
| 192 | + return m, nil |
| 193 | + } |
| 194 | + m.wizard.isLoading = true |
| 195 | + m.wizard.loadingMessage = "Création de la Gateway..." |
| 196 | + return m, m.createGatewayFromWizard() |
| 197 | + } |
| 198 | + return m, nil |
| 199 | +} |
0 commit comments