1use crate::platform::api::{
4 types::{
5 CloudProvider, ClusterStatus, ClusterSummary, ProviderDeploymentStatus, RegistryStatus,
6 RegistrySummary,
7 },
8 PlatformApiClient,
9};
10use crate::wizard::render::{display_step_header, status_indicator, wizard_render_config};
11use colored::Colorize;
12use inquire::{InquireError, Select};
13use std::collections::HashMap;
14
15pub async fn get_provider_deployment_statuses(
20 client: &PlatformApiClient,
21 project_id: &str,
22) -> Result<Vec<ProviderDeploymentStatus>, crate::platform::api::PlatformApiError> {
23 let credentials = client
25 .list_cloud_credentials_for_project(project_id)
26 .await
27 .unwrap_or_default();
28
29 let connected_providers: std::collections::HashSet<String> = credentials
31 .iter()
32 .map(|c| c.provider.to_lowercase())
33 .collect();
34
35 let clusters = client
37 .list_clusters_for_project(project_id)
38 .await
39 .unwrap_or_default();
40 let registries = client
41 .list_registries_for_project(project_id)
42 .await
43 .unwrap_or_default();
44
45 let mut provider_clusters: HashMap<CloudProvider, Vec<ClusterSummary>> = HashMap::new();
47 let mut provider_registries: HashMap<CloudProvider, Vec<RegistrySummary>> = HashMap::new();
48
49 for cluster in clusters {
50 let summary = ClusterSummary {
51 id: cluster.id,
52 name: cluster.name,
53 region: cluster.region,
54 is_healthy: cluster.status == ClusterStatus::Running,
55 };
56 provider_clusters
57 .entry(cluster.provider)
58 .or_default()
59 .push(summary);
60 }
61
62 for registry in registries {
63 let summary = RegistrySummary {
64 id: registry.id,
65 name: registry.name,
66 region: registry.region,
67 is_ready: registry.status == RegistryStatus::Ready,
68 };
69 provider_registries
70 .entry(registry.cloud_provider)
71 .or_default()
72 .push(summary);
73 }
74
75 let providers = [
78 CloudProvider::Gcp,
79 CloudProvider::Hetzner,
80 CloudProvider::Aws,
81 CloudProvider::Azure,
82 CloudProvider::Scaleway,
83 CloudProvider::Cyso,
84 ];
85 let mut statuses = Vec::new();
86
87 for provider in providers {
88 let clusters = provider_clusters.remove(&provider).unwrap_or_default();
89 let registries = provider_registries.remove(&provider).unwrap_or_default();
90
91 let is_connected = connected_providers.contains(provider.as_str());
93
94 let cloud_runner_available =
96 is_connected && matches!(provider, CloudProvider::Gcp | CloudProvider::Hetzner);
97
98 let summary = build_status_summary(&clusters, ®istries, cloud_runner_available);
99
100 statuses.push(ProviderDeploymentStatus {
101 provider,
102 is_connected,
103 clusters,
104 registries,
105 cloud_runner_available,
106 summary,
107 });
108 }
109
110 Ok(statuses)
111}
112
113fn build_status_summary(
115 clusters: &[ClusterSummary],
116 registries: &[RegistrySummary],
117 cloud_runner: bool,
118) -> String {
119 let mut parts = Vec::new();
120
121 if cloud_runner {
122 parts.push("Cloud Run".to_string());
123 }
124
125 let healthy_clusters = clusters.iter().filter(|c| c.is_healthy).count();
126 if healthy_clusters > 0 {
127 parts.push(format!(
128 "{} cluster{}",
129 healthy_clusters,
130 if healthy_clusters == 1 { "" } else { "s" }
131 ));
132 }
133
134 let ready_registries = registries.iter().filter(|r| r.is_ready).count();
135 if ready_registries > 0 {
136 parts.push(format!(
137 "{} registr{}",
138 ready_registries,
139 if ready_registries == 1 { "y" } else { "ies" }
140 ));
141 }
142
143 if parts.is_empty() {
144 "Not connected".to_string()
145 } else {
146 parts.join(", ")
147 }
148}
149
150#[derive(Debug, Clone)]
152pub enum ProviderSelectionResult {
153 Selected(CloudProvider),
155 Cancelled,
157}
158
159pub fn select_provider(statuses: &[ProviderDeploymentStatus]) -> ProviderSelectionResult {
161 display_step_header(
162 1,
163 "Select Provider",
164 "Choose which cloud provider to deploy to. You'll need to connect providers in the platform settings first.",
165 );
166
167 let options: Vec<String> = statuses
169 .iter()
170 .map(|s| {
171 let name = format!("{:?}", s.provider);
172 if !s.provider.is_available() {
174 format!("○ {} {}", name.dimmed(), "(Coming Soon)".yellow())
175 } else {
176 let indicator = status_indicator(s.is_connected);
177 if s.is_connected {
178 format!("{} {} {}", indicator, name, s.summary.dimmed())
179 } else {
180 format!("{} {} {}", indicator, name.dimmed(), "Not connected".dimmed())
181 }
182 }
183 })
184 .collect();
185
186 let available_connected_indices: Vec<usize> = statuses
188 .iter()
189 .enumerate()
190 .filter(|(_, s)| s.provider.is_available() && s.is_connected)
191 .map(|(i, _)| i)
192 .collect();
193
194 if available_connected_indices.is_empty() {
195 println!(
196 "\n{}",
197 "No providers connected. Connect a cloud provider in platform settings first.".red()
198 );
199 println!(
200 " {}",
201 "Visit: https://app.syncable.dev/integrations".dimmed()
202 );
203 println!(
204 " {}",
205 "Note: GCP and Hetzner are currently available. AWS, Azure, Scaleway, and Cyso Cloud are coming soon.".dimmed()
206 );
207 return ProviderSelectionResult::Cancelled;
208 }
209
210 let selection = Select::new("Select a provider:", options)
211 .with_render_config(wizard_render_config())
212 .with_help_message("↑↓ to move, Enter to select, Esc to cancel")
213 .with_page_size(6)
214 .prompt();
215
216 match selection {
217 Ok(answer) => {
218 let selected_idx = statuses
220 .iter()
221 .position(|s| {
222 let display = format!("{:?}", s.provider);
223 answer.contains(&display)
224 })
225 .unwrap_or(0);
226
227 let selected_status = &statuses[selected_idx];
228
229 if !selected_status.provider.is_available() {
231 println!(
232 "\n{}",
233 format!(
234 "{} is coming soon! Currently only GCP and Hetzner are available.",
235 selected_status.provider.display_name()
236 )
237 .yellow()
238 );
239 return ProviderSelectionResult::Cancelled;
240 }
241
242 if !selected_status.is_connected {
243 println!(
244 "\n{}",
245 format!(
246 "{:?} is not connected. Please connect it in platform settings first.",
247 selected_status.provider
248 )
249 .yellow()
250 );
251 return ProviderSelectionResult::Cancelled;
252 }
253
254 println!(
255 "\n{} Selected: {:?}",
256 "✓".green(),
257 selected_status.provider
258 );
259 ProviderSelectionResult::Selected(selected_status.provider.clone())
260 }
261 Err(InquireError::OperationCanceled) | Err(InquireError::OperationInterrupted) => {
262 println!("\n{}", "Wizard cancelled.".dimmed());
263 ProviderSelectionResult::Cancelled
264 }
265 Err(_) => ProviderSelectionResult::Cancelled,
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn test_build_status_summary_cloud_runner_only() {
275 let summary = build_status_summary(&[], &[], true);
276 assert_eq!(summary, "Cloud Run");
277 }
278
279 #[test]
280 fn test_build_status_summary_full() {
281 let clusters = vec![
282 ClusterSummary {
283 id: "c1".to_string(),
284 name: "prod".to_string(),
285 region: "us-central1".to_string(),
286 is_healthy: true,
287 },
288 ClusterSummary {
289 id: "c2".to_string(),
290 name: "staging".to_string(),
291 region: "us-east1".to_string(),
292 is_healthy: false,
293 },
294 ];
295 let registries = vec![RegistrySummary {
296 id: "r1".to_string(),
297 name: "main".to_string(),
298 region: "us-central1".to_string(),
299 is_ready: true,
300 }];
301 let summary = build_status_summary(&clusters, ®istries, true);
302 assert_eq!(summary, "Cloud Run, 1 cluster, 1 registry");
303 }
304
305 #[test]
306 fn test_build_status_summary_not_connected() {
307 let summary = build_status_summary(&[], &[], false);
308 assert_eq!(summary, "Not connected");
309 }
310
311 #[test]
312 fn test_provider_selection_result_variants() {
313 let _ = ProviderSelectionResult::Selected(CloudProvider::Gcp);
314 let _ = ProviderSelectionResult::Cancelled;
315 }
316}