seer-cli 0.51.0

Interactive CLI for Seer domain name utilities
//! DNSSEC tab renderer โ€” state header + KV summary + DS records table.
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Paragraph, Row, Table};
use ratatui::Frame;

use crate::tui::action::LensData;
use crate::tui::theme::Theme;
use crate::tui::widgets::{kv, panel};

pub fn render(f: &mut Frame, area: Rect, theme: &Theme, data: &LensData) {
    let LensData::Dnssec(r) = data else {
        return;
    };

    let status_color = if r.chain_valid || r.status == "signed" {
        theme.green
    } else if r.status == "partial" || r.status == "misconfigured" {
        theme.yellow
    } else {
        theme.red
    };

    let inner = panel::render(f, area, theme, "DNSSEC", status_color, false);

    // Split: status header + kv + issues + DS table
    let has_ds = !r.ds_records.is_empty();
    let constraints = if has_ds {
        vec![
            Constraint::Length(1),
            Constraint::Length(7),
            Constraint::Length(2),
            Constraint::Min(0),
        ]
    } else {
        vec![
            Constraint::Length(1),
            Constraint::Length(7),
            Constraint::Min(0),
        ]
    };
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints(constraints)
        .split(inner);

    // Status header
    f.render_widget(
        Paragraph::new(Line::from(Span::styled(
            r.status.to_uppercase(),
            Style::default()
                .fg(status_color)
                .add_modifier(Modifier::BOLD),
        ))),
        chunks[0],
    );

    // KV summary
    let rows = [
        ("enabled", r.enabled.to_string()),
        ("status", r.status.clone()),
        ("chain valid", r.chain_valid.to_string()),
        ("DS records", r.ds_records.len().to_string()),
        ("DNSKEY records", r.dnskey_records.len().to_string()),
    ];
    kv::render(f, chunks[1], theme, status_color, &rows);

    // Issues line
    if !r.issues.is_empty() {
        f.render_widget(
            Paragraph::new(Line::from(Span::styled(
                r.issues.join("  ยท  "),
                Style::default().fg(theme.yellow),
            ))),
            chunks[2],
        );
    }

    // DS records table
    if has_ds {
        let ds_area = chunks[chunks.len() - 1];
        let inner2 = panel::render(f, ds_area, theme, "DS Records", theme.overlay0, false);

        let header = Row::new(["KEY TAG", "ALGORITHM", "DIGEST TYPE", "MATCHED"])
            .style(Style::default().fg(theme.overlay0));
        let ds_rows = r.ds_records.iter().map(|ds| {
            let matched = if ds.matched_key { "โœ“" } else { "โœ—" };
            Row::new(vec![
                ds.key_tag.to_string(),
                ds.algorithm_name.clone(),
                ds.digest_type_name.clone(),
                matched.to_string(),
            ])
            .style(Style::default().fg(theme.text))
        });
        let table = Table::new(
            ds_rows,
            [
                Constraint::Length(10),
                Constraint::Percentage(35),
                Constraint::Percentage(35),
                Constraint::Length(8),
            ],
        )
        .header(header)
        .column_spacing(1);
        f.render_widget(table, inner2);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tui::test_util::render_text;
    use seer_core::DnssecReport;

    #[test]
    fn renders_status_field() {
        let theme = Theme::frappe();
        let data = LensData::Dnssec(Box::new(DnssecReport {
            domain: "x".into(),
            enabled: true,
            has_ds_records: true,
            has_dnskey_records: true,
            ds_records: vec![],
            dnskey_records: vec![],
            issues: vec![],
            status: "signed".into(),
            chain_valid: true,
            authentication_tier: seer_core::AuthenticationTier::DigestOnly,
            rrsig_records: vec![],
        }));
        let text = render_text(70, 14, |f| render(f, f.area(), &theme, &data));
        assert!(text.contains("signed"));
    }
}