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.
This commit is contained in:
Patrick Sullivan
2025-02-18 21:01:03 -06:00
committed by GitHub
parent 5ce73b2697
commit 9575fdb322
3 changed files with 102 additions and 83 deletions
+73 -49
View File
@@ -166,6 +166,7 @@ pub async fn list_all() -> Result<Vec<(String, Dashboard)>, errors::Error> {
pub async fn put(
org_id: &str,
folder_id: &str,
new_folder_id: Option<&str>,
mut dashboard: Dashboard,
clone: bool,
) -> Result<Dashboard, errors::Error> {
@@ -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::<i16>(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)
}
+20 -33
View File
@@ -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<Dashboard, DashboardError> {
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<Dashboard, DashboardError> {
@@ -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)
}
+9 -1
View File
@@ -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,