syncable_cli/agent/ui/
prometheus_display.rs

1//! Prometheus Discovery & Connection Display
2//!
3//! Elegant terminal UI for Prometheus operations:
4//! - Service discovery in Kubernetes cluster
5//! - Port-forward connection establishment
6//! - Connection status and health checks
7//!
8//! Uses a visual style consistent with other tool displays.
9
10use crate::agent::ui::colors::{ansi, icons};
11use colored::Colorize;
12use std::io::{self, Write};
13
14/// Icon for Prometheus (fire/metrics theme)
15pub const PROMETHEUS_ICON: &str = "🔥";
16/// Icon for Kubernetes
17pub const K8S_ICON: &str = "☸";
18/// Icon for network/connection
19pub const NETWORK_ICON: &str = "🔗";
20/// Icon for port-forward
21pub const PORT_FORWARD_ICON: &str = "🚇";
22/// Icon for search/discovery
23pub const SEARCH_ICON: &str = "🔍";
24
25/// Display for Prometheus discovery operations
26pub struct PrometheusDiscoveryDisplay {
27    started: bool,
28}
29
30impl PrometheusDiscoveryDisplay {
31    pub fn new() -> Self {
32        Self { started: false }
33    }
34
35    /// Show discovery started
36    pub fn start(&mut self, namespace: Option<&str>) {
37        self.started = true;
38        let scope = namespace.unwrap_or("all namespaces");
39
40        println!();
41        println!(
42            "{}{}  Prometheus Discovery{}",
43            ansi::BOLD,
44            PROMETHEUS_ICON,
45            ansi::RESET
46        );
47        println!(
48            "{}├─{} {} Searching for Prometheus services in {}...{}",
49            ansi::DIM,
50            ansi::RESET,
51            SEARCH_ICON,
52            scope.cyan(),
53            ansi::RESET
54        );
55        let _ = io::stdout().flush();
56    }
57
58    /// Show services found
59    pub fn found_services(&self, services: &[DiscoveredService]) {
60        if services.is_empty() {
61            println!(
62                "{}├─{} {} {}{}",
63                ansi::DIM,
64                ansi::RESET,
65                icons::WARNING.yellow(),
66                "No Prometheus services found".yellow(),
67                ansi::RESET
68            );
69        } else {
70            println!(
71                "{}├─{} {} Found {} service(s):{}",
72                ansi::DIM,
73                ansi::RESET,
74                icons::SUCCESS.green(),
75                services.len().to_string().green().bold(),
76                ansi::RESET
77            );
78
79            for (i, svc) in services.iter().enumerate() {
80                let is_last = i == services.len() - 1;
81                let prefix = if is_last { "└─" } else { "├─" };
82
83                println!(
84                    "{}│  {}─{} {} {}/{} {}:{}{}",
85                    ansi::DIM,
86                    prefix,
87                    ansi::RESET,
88                    K8S_ICON,
89                    svc.namespace.cyan(),
90                    svc.name.cyan().bold(),
91                    "port".dimmed(),
92                    svc.port.to_string().yellow(),
93                    ansi::RESET
94                );
95            }
96        }
97        let _ = io::stdout().flush();
98    }
99
100    /// Show suggestion for next step
101    pub fn show_suggestion(&self, service: &DiscoveredService) {
102        println!("{}│{}", ansi::DIM, ansi::RESET);
103        println!(
104            "{}└─{} {} Next: Use {} to connect{}",
105            ansi::DIM,
106            ansi::RESET,
107            icons::ARROW.cyan(),
108            "prometheus_connect".cyan().bold(),
109            ansi::RESET
110        );
111        println!(
112            "   {} service: {}, namespace: {}, port: {}",
113            "→".dimmed(),
114            service.name.green(),
115            service.namespace.green(),
116            service.port.to_string().yellow()
117        );
118        let _ = io::stdout().flush();
119    }
120
121    /// Show fallback to all namespaces
122    pub fn searching_all_namespaces(&self) {
123        println!(
124            "{}├─{} {} {}{}",
125            ansi::DIM,
126            ansi::RESET,
127            SEARCH_ICON,
128            "Not found in specified namespace, searching all namespaces...".yellow(),
129            ansi::RESET
130        );
131        let _ = io::stdout().flush();
132    }
133
134    /// Show error
135    pub fn error(&self, message: &str) {
136        println!(
137            "{}└─{} {} {}{}",
138            ansi::DIM,
139            ansi::RESET,
140            icons::ERROR.red(),
141            message.red(),
142            ansi::RESET
143        );
144        let _ = io::stdout().flush();
145    }
146}
147
148impl Default for PrometheusDiscoveryDisplay {
149    fn default() -> Self {
150        Self::new()
151    }
152}
153
154/// A discovered Prometheus service (for display)
155#[derive(Debug, Clone)]
156pub struct DiscoveredService {
157    pub name: String,
158    pub namespace: String,
159    pub port: u16,
160    pub service_type: String,
161}
162
163/// Display for Prometheus connection operations
164pub struct PrometheusConnectionDisplay {
165    mode: ConnectionMode,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq)]
169pub enum ConnectionMode {
170    PortForward,
171    DirectUrl,
172}
173
174impl PrometheusConnectionDisplay {
175    pub fn new(mode: ConnectionMode) -> Self {
176        Self { mode }
177    }
178
179    /// Show connection started
180    pub fn start(&self, target: &str) {
181        println!();
182        println!(
183            "{}{}  Prometheus Connection{}",
184            ansi::BOLD,
185            NETWORK_ICON,
186            ansi::RESET
187        );
188
189        match self.mode {
190            ConnectionMode::PortForward => {
191                println!(
192                    "{}├─{} {} Establishing port-forward to {}...{}",
193                    ansi::DIM,
194                    ansi::RESET,
195                    PORT_FORWARD_ICON,
196                    target.cyan(),
197                    ansi::RESET
198                );
199            }
200            ConnectionMode::DirectUrl => {
201                println!(
202                    "{}├─{} {} Connecting to {}...{}",
203                    ansi::DIM,
204                    ansi::RESET,
205                    NETWORK_ICON,
206                    target.cyan(),
207                    ansi::RESET
208                );
209            }
210        }
211        let _ = io::stdout().flush();
212    }
213
214    /// Show port-forward established
215    pub fn port_forward_established(&self, local_port: u16, service: &str, namespace: &str) {
216        println!(
217            "{}├─{} {} Port-forward active on localhost:{}{}",
218            ansi::DIM,
219            ansi::RESET,
220            icons::SUCCESS.green(),
221            local_port.to_string().green().bold(),
222            ansi::RESET
223        );
224        println!(
225            "{}│  {} {} {}/{} {}",
226            ansi::DIM,
227            ansi::RESET,
228            "→".dimmed(),
229            namespace.dimmed(),
230            service.dimmed(),
231            "(no auth needed)".dimmed()
232        );
233        let _ = io::stdout().flush();
234    }
235
236    /// Show testing connection
237    pub fn testing_connection(&self) {
238        print!(
239            "{}├─{} {} Testing Prometheus API...{}",
240            ansi::DIM,
241            ansi::RESET,
242            icons::EXECUTING.cyan(),
243            ansi::RESET
244        );
245        let _ = io::stdout().flush();
246    }
247
248    /// Show connection successful
249    pub fn connected(&self, url: &str, authenticated: bool) {
250        // Clear the "Testing..." line
251        print!("\r{}", ansi::CLEAR_LINE);
252
253        println!(
254            "{}├─{} {} Connection established{}",
255            ansi::DIM,
256            ansi::RESET,
257            icons::SUCCESS.green(),
258            ansi::RESET
259        );
260
261        let auth_status = if authenticated {
262            "(authenticated)".green()
263        } else {
264            "(no auth)".dimmed()
265        };
266
267        println!(
268            "{}│  {} URL: {} {}{}",
269            ansi::DIM,
270            ansi::RESET,
271            url.cyan(),
272            auth_status,
273            ansi::RESET
274        );
275        let _ = io::stdout().flush();
276    }
277
278    /// Show connection ready for use
279    pub fn ready_for_use(&self, url: &str) {
280        println!("{}│{}", ansi::DIM, ansi::RESET);
281        println!(
282            "{}└─{} {} Ready! Use with {}{}",
283            ansi::DIM,
284            ansi::RESET,
285            PROMETHEUS_ICON,
286            "k8s_optimize".cyan().bold(),
287            ansi::RESET
288        );
289        println!("   {} prometheus: \"{}\"", "→".dimmed(), url.green());
290        let _ = io::stdout().flush();
291    }
292
293    /// Show connection failed
294    pub fn connection_failed(&self, error: &str, suggestions: &[&str]) {
295        // Clear any pending line
296        print!("\r{}", ansi::CLEAR_LINE);
297
298        println!(
299            "{}├─{} {} Connection failed: {}{}",
300            ansi::DIM,
301            ansi::RESET,
302            icons::ERROR.red(),
303            error.red(),
304            ansi::RESET
305        );
306
307        if !suggestions.is_empty() {
308            println!("{}│{}", ansi::DIM, ansi::RESET);
309            println!("{}├─{} Suggestions:{}", ansi::DIM, ansi::RESET, ansi::RESET);
310
311            for (i, suggestion) in suggestions.iter().enumerate() {
312                let is_last = i == suggestions.len() - 1;
313                let prefix = if is_last { "└─" } else { "├─" };
314
315                println!(
316                    "{}│  {}─{} {}{}",
317                    ansi::DIM,
318                    prefix,
319                    ansi::RESET,
320                    suggestion.yellow(),
321                    ansi::RESET
322                );
323            }
324        }
325        let _ = io::stdout().flush();
326    }
327
328    /// Show auth required message
329    pub fn auth_required(&self) {
330        println!(
331            "{}├─{} {} {}{}",
332            ansi::DIM,
333            ansi::RESET,
334            icons::SECURITY.yellow(),
335            "Authentication may be required for external Prometheus".yellow(),
336            ansi::RESET
337        );
338        println!(
339            "{}│  {} Provide auth_type: \"basic\" or \"bearer\"{}",
340            ansi::DIM,
341            "→".dimmed(),
342            ansi::RESET
343        );
344        let _ = io::stdout().flush();
345    }
346
347    /// Show background process info
348    pub fn background_process_info(&self, process_id: &str) {
349        println!(
350            "{}│  {} Background process: {} {}",
351            ansi::DIM,
352            ansi::RESET,
353            process_id.dimmed(),
354            "(will auto-cleanup)".dimmed()
355        );
356        let _ = io::stdout().flush();
357    }
358}
359
360/// Compact inline display for tool calls
361pub struct PrometheusInlineDisplay;
362
363impl PrometheusInlineDisplay {
364    /// Show discovery inline
365    pub fn discovery_start() {
366        print!(
367            "{} {} Discovering Prometheus services...",
368            icons::EXECUTING.cyan(),
369            PROMETHEUS_ICON
370        );
371        let _ = io::stdout().flush();
372    }
373
374    /// Update discovery result
375    pub fn discovery_result(count: usize) {
376        print!("\r{}", ansi::CLEAR_LINE);
377        if count > 0 {
378            println!(
379                "{} {} Found {} Prometheus service(s)",
380                icons::SUCCESS.green(),
381                PROMETHEUS_ICON,
382                count.to_string().green().bold()
383            );
384        } else {
385            println!(
386                "{} {} No Prometheus services found",
387                icons::WARNING.yellow(),
388                PROMETHEUS_ICON
389            );
390        }
391        let _ = io::stdout().flush();
392    }
393
394    /// Show connection inline
395    pub fn connect_start(target: &str) {
396        print!(
397            "{} {} Connecting to {}...",
398            icons::EXECUTING.cyan(),
399            NETWORK_ICON,
400            target.cyan()
401        );
402        let _ = io::stdout().flush();
403    }
404
405    /// Update connection result
406    pub fn connect_result(success: bool, url: &str) {
407        print!("\r{}", ansi::CLEAR_LINE);
408        if success {
409            println!(
410                "{} {} Connected: {}",
411                icons::SUCCESS.green(),
412                NETWORK_ICON,
413                url.green()
414            );
415        } else {
416            println!(
417                "{} {} Connection failed to {}",
418                icons::ERROR.red(),
419                NETWORK_ICON,
420                url
421            );
422        }
423        let _ = io::stdout().flush();
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    fn test_discovered_service() {
433        let svc = DiscoveredService {
434            name: "prometheus-server".to_string(),
435            namespace: "monitoring".to_string(),
436            port: 9090,
437            service_type: "ClusterIP".to_string(),
438        };
439        assert_eq!(svc.name, "prometheus-server");
440        assert_eq!(svc.port, 9090);
441    }
442
443    #[test]
444    fn test_connection_mode() {
445        let display = PrometheusConnectionDisplay::new(ConnectionMode::PortForward);
446        assert_eq!(display.mode, ConnectionMode::PortForward);
447    }
448}