diff --git a/src/api/management/src/request/dashboards/timed_annotations.rs b/src/api/management/src/request/dashboards/timed_annotations.rs index 185917140b..614aa944e9 100644 --- a/src/api/management/src/request/dashboards/timed_annotations.rs +++ b/src/api/management/src/request/dashboards/timed_annotations.rs @@ -303,7 +303,13 @@ pub async fn delete_annotation_panels( if panels.is_empty() { return MetaHttpResponse::bad_request("panels cannot be empty".to_string()); } - match timed_annotations::delete_timed_annotation_panels(&timed_annotation_id, panels).await { + match timed_annotations::delete_timed_annotation_panels( + &dashboard_id, + &timed_annotation_id, + panels, + ) + .await + { Ok(_) => (StatusCode::OK, "").into_response(), Err(e) => { log::error!("Error deleting timed annotation panels: {e}"); diff --git a/src/core/src/dashboards/timed_annotations.rs b/src/core/src/dashboards/timed_annotations.rs index 1166adf0da..02359739d3 100644 --- a/src/core/src/dashboards/timed_annotations.rs +++ b/src/core/src/dashboards/timed_annotations.rs @@ -95,13 +95,19 @@ pub async fn update_timed_annotations( #[tracing::instrument] pub async fn delete_timed_annotation_panels( + dashboard_id: &str, timed_annotation_id: &str, panels: Vec, ) -> Result<(), anyhow::Error> { if panels.is_empty() { return Err(anyhow::anyhow!("panels cannot be empty")); } - table::timed_annotation_panels::delete_many_panels(timed_annotation_id, panels.clone()).await?; + table::timed_annotation_panels::delete_many_panels_in_dashboard( + dashboard_id, + timed_annotation_id, + panels.clone(), + ) + .await?; #[cfg(feature = "enterprise")] super_cluster::emit_timed_annotation_panels_delete_event(timed_annotation_id, panels).await?; diff --git a/src/infra/src/table/timed_annotation_panels.rs b/src/infra/src/table/timed_annotation_panels.rs index 2726668890..86f5f8c4b5 100644 --- a/src/infra/src/table/timed_annotation_panels.rs +++ b/src/infra/src/table/timed_annotation_panels.rs @@ -16,7 +16,7 @@ use config::ider; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, Set, TransactionTrait}; -use super::entity::timed_annotation_panels; +use super::entity::{dashboards, timed_annotation_panels, timed_annotations}; use crate::{ db::{get_orm_client_ro, get_orm_client_rw}, errors, @@ -62,6 +62,8 @@ pub async fn insert_many_panels( Ok(()) } +/// Unscoped: only for replaying a delete the originating cluster already authorized. Callers +/// serving a user request must use [`delete_many_panels_in_dashboard`]. pub async fn delete_many_panels( timed_annotation_id: &str, panel_ids: Vec, @@ -83,3 +85,38 @@ pub async fn delete_many_panels( Ok(()) } + +/// Removes panel associations only when the annotation belongs to the given dashboard. +pub async fn delete_many_panels_in_dashboard( + dashboard_id: &str, + timed_annotation_id: &str, + panel_ids: Vec, +) -> Result<(), errors::Error> { + if panel_ids.is_empty() { + return Ok(()); + } + + let client = get_orm_client_rw().await; + let dashboard_record = dashboards::Entity::find() + .filter(dashboards::Column::DashboardId.eq(dashboard_id)) + .one(client) + .await? + .ok_or_else(|| { + errors::Error::DbError(errors::DbError::KeyNotExists(format!( + "Dashboard '{dashboard_id}' not found" + ))) + })?; + + timed_annotations::Entity::find() + .filter(timed_annotations::Column::Id.eq(timed_annotation_id)) + .filter(timed_annotations::Column::DashboardId.eq(dashboard_record.id)) + .one(client) + .await? + .ok_or_else(|| { + errors::Error::DbError(errors::DbError::KeyNotExists(format!( + "TimedAnnotation with ID {timed_annotation_id} not found in dashboard {dashboard_id}" + ))) + })?; + + delete_many_panels(timed_annotation_id, panel_ids).await +}