From 9575fdb322db9b0b8bd57de79bc040343c1ae07e Mon Sep 17 00:00:00 2001 From: Patrick Sullivan Date: Tue, 18 Feb 2025 21:01:03 -0600 Subject: [PATCH] feat: Move dashboards by updating the `folder_id` (#6001) Changes the logic to move a dashboard into a different folder. - Before: We created a copy of the dashboard in the new folder and then deleted the old dashboard. - Now: We update the `folder_id` foreign key on the dashboard so that it references the new parent folder. This is the third PR in a series of 3 PRs that should be merged in sequence 1. https://github.com/openobserve/openobserve/pull/5997 - Updates pattern matching on the "put dashboard" message so that new fields can be added to the "put dashboard" message without introducing compiler errors for openobserve when built with enterprise features 2. https://github.com/openobserve/o2-enterprise/pull/350 - Adds the optional `new_folder_id` field to the "put dashboard" message 3. https://github.com/openobserve/openobserve/pull/6001 - Changes the "move dashboard" logic so that instead of creating a copied dashboard and deleting the old one, it updates the dashboard's `folder_id` to reference a new parent folder. In this PR we also emit a "put dashboard" super cluster event that contains the new `folder_id` of the updated dashboard. --- src/infra/src/table/dashboards.rs | 122 +++++++++++++++----------- src/service/dashboards/mod.rs | 53 +++++------ src/super_cluster_queue/dashboards.rs | 10 ++- 3 files changed, 102 insertions(+), 83 deletions(-) diff --git a/src/infra/src/table/dashboards.rs b/src/infra/src/table/dashboards.rs index d9618bec1f..141dd553e0 100644 --- a/src/infra/src/table/dashboards.rs +++ b/src/infra/src/table/dashboards.rs @@ -166,6 +166,7 @@ pub async fn list_all() -> Result, errors::Error> { pub async fn put( org_id: &str, folder_id: &str, + new_folder_id: Option<&str>, mut dashboard: Dashboard, clone: bool, ) -> Result { @@ -201,59 +202,82 @@ pub async fn put( dashboard.set_updated_at(); } let updated_at = dashboard.updated_at; - let data = inner_data_as_json(dashboard)?; - let model = match get_model_from_folder(client, org_id, folder_id, &dashboard_id).await? { - None => { - // Destination folder does not exist so the dashboard can neither be - // created nor updated. - Err(errors::PutDashboardError::FolderDoesNotExist) - } - Some((folder_m, Some(dash_m))) => { - // Destination folder exists and dashboard already exists, so - // update the dashboard. - let mut dash_am = dash_m.into_active_model(); - dash_am.folder_id = Set(folder_m.id); - dash_am.owner = Set(owner); - dash_am.role = Set(role); - dash_am.title = Set(title); - dash_am.description = Set(description); - dash_am.data = Set(data); - dash_am.version = Set(version); - dash_am.updated_at = Set(updated_at); - let model: dashboards::Model = dash_am.update(client).await?.try_into_model()?; - Ok(model) - } - Some((folder_m, None)) => { - // Destination folder exists but dashboard does not exist, so create - // a new dashboard. - // TODO: Use timestamp in microseconds like all other resources - let created_at_unix: i64 = if let Some(created_at_tz) = created_at_depricated { - created_at_tz.timestamp() - } else { - chrono::Utc::now().timestamp() - }; + // Try to get the new folder if the dashboard is being moved to a new folder. + let maybe_new_folder_model = if let Some(new_folder_id) = new_folder_id { + let new_folder_model = folders::Entity::find() + .filter(folders::Column::Org.eq(org_id)) + .filter(folders::Column::Type.eq::(folder_type_into_i16(FolderType::Dashboards))) + .filter(folders::Column::FolderId.eq(new_folder_id)) + .one(client) + .await? + .ok_or(errors::PutDashboardError::FolderDoesNotExist)?; + Some(new_folder_model) + } else { + None + }; - let dash_am = dashboards::ActiveModel { - id: Set(svix_ksuid::Ksuid::new(None, None).to_string()), - dashboard_id: Set(dashboard_id.to_owned()), - folder_id: Set(folder_m.id), - owner: Set(owner), - role: Set(role), - title: Set(title), - description: Set(description), - data: Set(data), - version: Set(version), - created_at: Set(created_at_unix), - updated_at: Set(updated_at), - }; - let model: dashboards::Model = dash_am.insert(client).await?.try_into_model()?; - Ok(model) - } - }?; + let dashboard_model = + match get_model_from_folder(client, org_id, folder_id, &dashboard_id).await? { + None => { + // Destination folder does not exist so the dashboard can neither be + // created nor updated. + Err(errors::PutDashboardError::FolderDoesNotExist) + } + Some((folder_m, Some(dash_m))) => { + // Destination folder exists and dashboard already exists, so + // update the dashboard. - let dash = model.try_into()?; + // If the dashboard is being to a new folder, then update the folder ID foreign key. + // Otherwise keep using the older folder ID foreign key. + let folder_id = if let Some(new_folder_model) = maybe_new_folder_model { + new_folder_model.id + } else { + folder_m.id + }; + + let mut dash_am = dash_m.into_active_model(); + dash_am.folder_id = Set(folder_id); + dash_am.owner = Set(owner); + dash_am.role = Set(role); + dash_am.title = Set(title); + dash_am.description = Set(description); + dash_am.data = Set(data); + dash_am.version = Set(version); + dash_am.updated_at = Set(updated_at); + let model: dashboards::Model = dash_am.update(client).await?.try_into_model()?; + Ok(model) + } + Some((folder_m, None)) => { + // Destination folder exists but dashboard does not exist, so create + // a new dashboard. + // TODO: Use timestamp in microseconds like all other resources + let created_at_unix: i64 = if let Some(created_at_tz) = created_at_depricated { + created_at_tz.timestamp() + } else { + chrono::Utc::now().timestamp() + }; + + let dash_am = dashboards::ActiveModel { + id: Set(svix_ksuid::Ksuid::new(None, None).to_string()), + dashboard_id: Set(dashboard_id.to_owned()), + folder_id: Set(folder_m.id), + owner: Set(owner), + role: Set(role), + title: Set(title), + description: Set(description), + data: Set(data), + version: Set(version), + created_at: Set(created_at_unix), + updated_at: Set(updated_at), + }; + let model: dashboards::Model = dash_am.insert(client).await?.try_into_model()?; + Ok(model) + } + }?; + + let dash = dashboard_model.try_into()?; Ok(dash) } diff --git a/src/service/dashboards/mod.rs b/src/service/dashboards/mod.rs index 3ee78e6f01..7e7217b6b0 100644 --- a/src/service/dashboards/mod.rs +++ b/src/service/dashboards/mod.rs @@ -284,7 +284,7 @@ pub async fn create_dashboard( let dashboard = if table::folders::exists(org_id, folder_id, FolderType::Dashboards).await? { let dashboard_id = ider::generate(); - let saved = put(org_id, &dashboard_id, folder_id, dashboard, None).await?; + let saved = put(org_id, &dashboard_id, folder_id, None, dashboard, None).await?; set_ownership( org_id, "dashboards", @@ -306,7 +306,7 @@ pub async fn create_dashboard( .await .map_err(|_| DashboardError::CreateDefaultFolder)?; let dashboard_id = ider::generate(); - let saved = put(org_id, &dashboard_id, folder_id, dashboard, None).await?; + let saved = put(org_id, &dashboard_id, folder_id, None, dashboard, None).await?; set_ownership( org_id, "dashboards", @@ -343,7 +343,7 @@ pub async fn update_dashboard( dashboard: Dashboard, hash: Option<&str>, ) -> Result { - let dashboard = put(org_id, dashboard_id, folder_id, dashboard, hash).await?; + let dashboard = put(org_id, dashboard_id, folder_id, None, dashboard, hash).await?; #[cfg(feature = "enterprise")] if get_o2_config().super_cluster.enabled { @@ -428,14 +428,25 @@ pub async fn move_dashboard( return Err(DashboardError::MoveDestinationFolderNotFound); }; - // add the dashboard to the destination folder - put(org_id, dashboard_id, to_folder, dashboard.clone(), None).await?; + // update the dashboard so that it's parent folder is the destination folder + put( + org_id, + dashboard_id, + from_folder, + Some(to_folder), + dashboard.clone(), + Some(&dashboard.hash), + ) + .await?; // OFGA ownership #[cfg(feature = "enterprise")] { if get_o2_config().super_cluster.enabled { - let _ = o2_enterprise::enterprise::super_cluster::queue::dashboards_put( - org_id, to_folder, dashboard, + let _ = o2_enterprise::enterprise::super_cluster::queue::dashboards_put_v2( + org_id, + from_folder, + Some(to_folder), + dashboard, ) .await; } @@ -447,31 +458,6 @@ pub async fn move_dashboard( &get_ofga_type("folders"), ) .await; - } - } - - // delete the dashboard from the source folder - table::dashboards::delete_from_folder(org_id, from_folder, dashboard_id) - .await - .map_err(|e| { - DashboardError::MoveDashboardDeleteOld( - dashboard_id.to_string(), - from_folder.to_string(), - e.to_string(), - ) - })?; - - #[cfg(feature = "enterprise")] - { - if get_o2_config().super_cluster.enabled { - let _ = o2_enterprise::enterprise::super_cluster::queue::dashboards_delete( - org_id, - from_folder, - dashboard_id, - ) - .await; - } - if get_openfga_config().enabled { remove_parent_relation( dashboard_id, &get_ofga_type("dashboards"), @@ -489,6 +475,7 @@ async fn put( org_id: &str, dashboard_id: &str, folder_id: &str, + new_folder_id: Option<&str>, mut dashboard: Dashboard, hash: Option<&str>, ) -> Result { @@ -520,7 +507,7 @@ async fn put( dashboard.set_title(title); dashboard.set_dashboard_id(dashboard_id.to_owned()); - let dash = table::dashboards::put(org_id, folder_id, dashboard, false).await?; + let dash = table::dashboards::put(org_id, folder_id, new_folder_id, dashboard, false).await?; Ok(dash) } diff --git a/src/super_cluster_queue/dashboards.rs b/src/super_cluster_queue/dashboards.rs index 924137121a..c884663ff4 100644 --- a/src/super_cluster_queue/dashboards.rs +++ b/src/super_cluster_queue/dashboards.rs @@ -27,11 +27,19 @@ pub(crate) async fn process_msg(msg: DashboardMessage) -> Result<()> { DashboardMessage::Put { org_id, folder_id, + new_folder_id, dashboard, .. } => { // `clone` is always true for super cluster - table::dashboards::put(&org_id, &folder_id, dashboard, true).await?; + table::dashboards::put( + &org_id, + &folder_id, + new_folder_id.as_deref(), + dashboard, + true, + ) + .await?; } DashboardMessage::Delete { org_id,