Skip to main content

raps_cli/commands/
interactive.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2025 Dmytro Yemelianov
3
4//! Shared interactive dialoguer prompt wrappers for CLI dropdowns.
5
6use anyhow::{Context, Result};
7use dialoguer::Select;
8use raps_acc::RfiClient;
9use raps_dm::DataManagementClient;
10pub use raps_kernel::interactive::is_non_interactive;
11use raps_kernel::{api_health, progress};
12
13pub async fn prompt_for_hub(client: &DataManagementClient) -> Result<String> {
14    if is_non_interactive() {
15        anyhow::bail!(
16            "Hub ID is required in non-interactive mode. Please provide it as an argument."
17        );
18    }
19
20    let spinner = progress::spinner("Fetching hubs...");
21    let start = std::time::Instant::now();
22    let hubs = client.list_hubs().await.context(
23        "Failed to list hubs. This requires 3-legged auth \u{2014} run 'raps auth login' first",
24    );
25    let elapsed = start.elapsed();
26    let snap = api_health::snapshot();
27    let suffix = if snap.sample_count > 0 {
28        format!(
29            " ({}, avg: {}, API: {})",
30            api_health::format_duration_ms(elapsed),
31            api_health::format_duration_ms(snap.avg_latency),
32            snap.health_status,
33        )
34    } else {
35        format!(" ({})", api_health::format_duration_ms(elapsed))
36    };
37    match &hubs {
38        Ok(_) => spinner.finish_with_message(format!("\u{2713} Fetching hubs{}", suffix)),
39        Err(_) => spinner.finish_with_message(format!(
40            "\u{2717} Fetching hubs (after {})",
41            api_health::format_duration_ms(elapsed)
42        )),
43    }
44    let hubs = hubs?;
45
46    if hubs.is_empty() {
47        anyhow::bail!("No hubs found. Make sure you're logged in with 3-legged auth.");
48    }
49
50    let hub_names: Vec<String> = hubs
51        .iter()
52        .map(|h| format!("{} ({})", h.attributes.name, h.id))
53        .collect();
54
55    let selection = Select::new()
56        .with_prompt("Select a Hub")
57        .items(&hub_names)
58        .interact()?;
59
60    Ok(hubs[selection].id.clone())
61}
62
63pub async fn prompt_for_project(client: &DataManagementClient, hub_id: &str) -> Result<String> {
64    if is_non_interactive() {
65        anyhow::bail!(
66            "Project ID is required in non-interactive mode. Please provide it as an argument."
67        );
68    }
69
70    let spinner = progress::spinner("Fetching projects...");
71    let start = std::time::Instant::now();
72    let projects = client
73        .list_projects(hub_id)
74        .await
75        .context(format!("Failed to list projects in hub '{}'", hub_id));
76    let elapsed = start.elapsed();
77    let snap = api_health::snapshot();
78    let suffix = if snap.sample_count > 0 {
79        format!(
80            " ({}, avg: {}, API: {})",
81            api_health::format_duration_ms(elapsed),
82            api_health::format_duration_ms(snap.avg_latency),
83            snap.health_status,
84        )
85    } else {
86        format!(" ({})", api_health::format_duration_ms(elapsed))
87    };
88    match &projects {
89        Ok(_) => spinner.finish_with_message(format!("\u{2713} Fetching projects{}", suffix)),
90        Err(_) => spinner.finish_with_message(format!(
91            "\u{2717} Fetching projects (after {})",
92            api_health::format_duration_ms(elapsed)
93        )),
94    }
95    let projects = projects?;
96
97    if projects.is_empty() {
98        anyhow::bail!("No projects found in this hub.");
99    }
100
101    let project_names: Vec<String> = projects
102        .iter()
103        .map(|p| format!("{} ({})", p.attributes.name, p.id))
104        .collect();
105
106    let selection = Select::new()
107        .with_prompt("Select a Project")
108        .items(&project_names)
109        .interact()?;
110
111    Ok(projects[selection].id.clone())
112}
113
114pub async fn prompt_for_folder(
115    client: &DataManagementClient,
116    hub_id: &str,
117    project_id: &str,
118) -> Result<String> {
119    if is_non_interactive() {
120        anyhow::bail!(
121            "Folder ID is required in non-interactive mode. Please provide it as an argument."
122        );
123    }
124
125    let spinner = progress::spinner("Fetching top folders...");
126    let start = std::time::Instant::now();
127    let folders = client
128        .get_top_folders(hub_id, project_id)
129        .await
130        .context(format!(
131            "Failed to get top folders for project '{}'",
132            project_id
133        ));
134    let elapsed = start.elapsed();
135    let snap = api_health::snapshot();
136    let suffix = if snap.sample_count > 0 {
137        format!(
138            " ({}, avg: {}, API: {})",
139            api_health::format_duration_ms(elapsed),
140            api_health::format_duration_ms(snap.avg_latency),
141            snap.health_status,
142        )
143    } else {
144        format!(" ({})", api_health::format_duration_ms(elapsed))
145    };
146    match &folders {
147        Ok(_) => spinner.finish_with_message(format!("\u{2713} Fetching top folders{}", suffix)),
148        Err(_) => spinner.finish_with_message(format!(
149            "\u{2717} Fetching top folders (after {})",
150            api_health::format_duration_ms(elapsed)
151        )),
152    }
153    let folders = folders?;
154
155    if folders.is_empty() {
156        anyhow::bail!("No folders found in this project.");
157    }
158
159    let folder_names: Vec<String> = folders
160        .iter()
161        .map(|f| {
162            let name = f
163                .attributes
164                .display_name
165                .as_deref()
166                .unwrap_or(f.attributes.name.as_str());
167            format!("{} ({})", name, f.id)
168        })
169        .collect();
170
171    let selection = Select::new()
172        .with_prompt("Select a Folder")
173        .items(&folder_names)
174        .interact()?;
175
176    Ok(folders[selection].id.clone())
177}
178
179pub async fn prompt_for_rfi(client: &RfiClient, project_id: &str) -> Result<String> {
180    if is_non_interactive() {
181        anyhow::bail!(
182            "RFI ID is required in non-interactive mode. Please provide it as an argument."
183        );
184    }
185
186    let spinner = progress::spinner("Fetching RFIs...");
187    let start = std::time::Instant::now();
188    let rfis = client
189        .list_rfis(project_id)
190        .await
191        .context(format!("Failed to list RFIs for project '{}'", project_id));
192    let elapsed = start.elapsed();
193    let snap = api_health::snapshot();
194    let suffix = if snap.sample_count > 0 {
195        format!(
196            " ({}, avg: {}, API: {})",
197            api_health::format_duration_ms(elapsed),
198            api_health::format_duration_ms(snap.avg_latency),
199            snap.health_status,
200        )
201    } else {
202        format!(" ({})", api_health::format_duration_ms(elapsed))
203    };
204    match &rfis {
205        Ok(_) => spinner.finish_with_message(format!("\u{2713} Fetching RFIs{}", suffix)),
206        Err(_) => spinner.finish_with_message(format!(
207            "\u{2717} Fetching RFIs (after {})",
208            api_health::format_duration_ms(elapsed)
209        )),
210    }
211    let rfis = rfis?;
212
213    if rfis.is_empty() {
214        anyhow::bail!("No RFIs found in this project.");
215    }
216
217    let rfi_names: Vec<String> = rfis
218        .iter()
219        .map(|r| {
220            let num = r.number.as_deref().unwrap_or("-");
221            format!("[{}] {} ({}) - {}", num, r.title, r.id, r.status)
222        })
223        .collect();
224
225    let selection = Select::new()
226        .with_prompt("Select an RFI")
227        .items(&rfi_names)
228        .interact()?;
229
230    Ok(rfis[selection].id.clone())
231}