use anyhow::Result;
use clap::Parser;
use crate::cli::datadog::dashboard::{render_dashboard_table, DashboardRow};
use crate::cli::datadog::format::{output_as, OutputFormat};
use crate::cli::datadog::helpers::create_client;
use crate::datadog::client::DatadogClient;
use crate::datadog::dashboards_api::{DashboardListFilter, DashboardsApi};
use crate::datadog::types::DashboardSummary;
#[derive(Parser)]
pub struct ListCommand {
#[arg(long = "filter-shared")]
pub filter_shared: bool,
#[arg(short = 'o', long, value_enum, default_value_t = OutputFormat::Table)]
pub output: OutputFormat,
}
impl ListCommand {
pub async fn execute(self) -> Result<()> {
let (client, _site) = create_client()?;
let filter = DashboardListFilter {
filter_shared: if self.filter_shared { Some(true) } else { None },
};
run_list(&client, &filter, &self.output).await
}
}
async fn run_list(
client: &DatadogClient,
filter: &DashboardListFilter,
output: &OutputFormat,
) -> Result<()> {
let dashboards = DashboardsApi::new(client).list(filter).await?;
if output_as(&dashboards, output)? {
return Ok(());
}
let stdout = std::io::stdout();
let mut handle = stdout.lock();
let rows: Vec<DashboardRow<'_>> = dashboards.iter().map(dashboard_row).collect();
render_dashboard_table(&rows, &mut handle)
}
fn dashboard_row(d: &DashboardSummary) -> DashboardRow<'_> {
DashboardRow {
id: d.id.as_str(),
title: d.title.as_str(),
author: d.author_label(),
url: d.url_label(),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn dashboard_json(id: &str) -> serde_json::Value {
serde_json::json!({
"id": id,
"title": format!("Dashboard {id}"),
"author_handle": "alice@example.com",
"url": format!("/dashboard/{id}"),
"is_shared": true
})
}
#[test]
fn dashboard_row_falls_back_to_dash_when_optional_fields_missing() {
let s: DashboardSummary = serde_json::from_value(serde_json::json!({
"id": "x",
"title": "y"
}))
.unwrap();
let row = dashboard_row(&s);
assert_eq!(row.author, "-");
assert_eq!(row.url, "-");
}
#[tokio::test]
async fn run_list_table_path_writes_to_stdout() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/dashboard"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"dashboards": [dashboard_json("abc")]
})),
)
.expect(1)
.mount(&server)
.await;
let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
run_list(
&client,
&DashboardListFilter::default(),
&OutputFormat::Table,
)
.await
.unwrap();
}
#[tokio::test]
async fn run_list_json_path_returns_ok() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/dashboard"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"dashboards": [dashboard_json("abc"), dashboard_json("def")]
})),
)
.mount(&server)
.await;
let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
run_list(
&client,
&DashboardListFilter::default(),
&OutputFormat::Json,
)
.await
.unwrap();
}
#[tokio::test]
async fn run_list_passes_filter_shared_flag_through() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/dashboard"))
.and(wiremock::matchers::query_param("filter_shared", "true"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"dashboards": [dashboard_json("abc")]
})),
)
.expect(1)
.mount(&server)
.await;
let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
run_list(
&client,
&DashboardListFilter {
filter_shared: Some(true),
},
&OutputFormat::Json,
)
.await
.unwrap();
}
#[tokio::test]
async fn run_list_propagates_api_errors() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/dashboard"))
.respond_with(wiremock::ResponseTemplate::new(500).set_body_string("boom"))
.mount(&server)
.await;
let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
let err = run_list(
&client,
&DashboardListFilter::default(),
&OutputFormat::Table,
)
.await
.unwrap_err();
assert!(err.to_string().contains("500"));
}
#[tokio::test]
async fn list_command_execute_errors_when_credentials_missing() {
use crate::datadog::test_support::{with_empty_home, EnvGuard};
let guard = EnvGuard::take();
let _dir = with_empty_home(&guard);
let cmd = ListCommand {
filter_shared: false,
output: OutputFormat::Table,
};
let err = cmd.execute().await.unwrap_err();
assert!(err.to_string().contains("not configured"));
}
#[tokio::test]
async fn list_command_execute_omits_filter_shared_when_flag_unset() {
use std::fs;
use crate::datadog::auth::{DATADOG_API_KEY, DATADOG_API_URL, DATADOG_APP_KEY};
use crate::datadog::test_support::{with_empty_home, EnvGuard};
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/dashboard"))
.and(wiremock::matchers::query_param_is_missing("filter_shared"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.set_body_json(serde_json::json!({"dashboards": []})),
)
.expect(1)
.mount(&server)
.await;
let guard = EnvGuard::take();
let dir = with_empty_home(&guard);
let omni_dir = dir.path().join(".omni-dev");
fs::create_dir_all(&omni_dir).unwrap();
fs::write(
omni_dir.join("settings.json"),
r#"{"env":{"DATADOG_API_KEY":"api","DATADOG_APP_KEY":"app","DATADOG_SITE":"datadoghq.com"}}"#,
)
.unwrap();
std::env::set_var(DATADOG_API_KEY, "api");
std::env::set_var(DATADOG_APP_KEY, "app");
std::env::set_var(DATADOG_API_URL, server.uri());
let cmd = ListCommand {
filter_shared: false,
output: OutputFormat::Json,
};
cmd.execute().await.unwrap();
}
#[tokio::test]
async fn list_command_execute_end_to_end_via_api_url_override() {
use std::fs;
use crate::datadog::auth::{DATADOG_API_KEY, DATADOG_API_URL, DATADOG_APP_KEY};
use crate::datadog::test_support::{with_empty_home, EnvGuard};
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/dashboard"))
.and(wiremock::matchers::query_param("filter_shared", "true"))
.respond_with(
wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
"dashboards": [dashboard_json("abc"), dashboard_json("def")]
})),
)
.expect(1)
.mount(&server)
.await;
let guard = EnvGuard::take();
let dir = with_empty_home(&guard);
let omni_dir = dir.path().join(".omni-dev");
fs::create_dir_all(&omni_dir).unwrap();
fs::write(
omni_dir.join("settings.json"),
r#"{"env":{"DATADOG_API_KEY":"api","DATADOG_APP_KEY":"app","DATADOG_SITE":"datadoghq.com"}}"#,
)
.unwrap();
std::env::set_var(DATADOG_API_KEY, "api");
std::env::set_var(DATADOG_APP_KEY, "app");
std::env::set_var(DATADOG_API_URL, server.uri());
let cmd = ListCommand {
filter_shared: true,
output: OutputFormat::Json,
};
cmd.execute().await.unwrap();
}
}