mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 00:08:55 +00:00
feat: migrate dashboards out of meta table and into their own table (#5146)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes - **New Features** - Enhanced dashboard management with new methods for retrieving properties like ID, owner, title, and description. - Added functionality for creating, updating, listing, and deleting dashboards through a new service module. - Introduced a new `dashboards` table in the database, with migrations for creation and population. - **Bug Fixes** - Improved error handling for dashboard retrieval and management operations, providing clearer responses for various conditions. - **Documentation** - Updated documentation to reflect new methods and error handling improvements. - **Refactor** - Streamlined the dashboard handling logic in service and database modules, reducing complexity and enhancing modularity. - Enhanced error handling logic in dashboard management functions for better clarity and control flow. - Refined the dashboard retrieval process to improve clarity in control flow and error handling. - Updated the logic for dashboard operations to simplify version handling and reduce redundancy. - Improved the overall structure and organization of the dashboard management code. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -40,6 +40,142 @@ pub struct Dashboard {
|
||||
pub hash: String,
|
||||
}
|
||||
|
||||
impl Dashboard {
|
||||
pub fn dashboard_id(&self) -> Option<&str> {
|
||||
match self.version {
|
||||
1 => self.v1.as_ref().map(|inner| inner.dashboard_id.as_str()),
|
||||
2 => self.v2.as_ref().map(|inner| inner.dashboard_id.as_str()),
|
||||
3 => self.v3.as_ref().map(|inner| inner.dashboard_id.as_str()),
|
||||
4 => self.v4.as_ref().map(|inner| inner.dashboard_id.as_str()),
|
||||
5 => self.v5.as_ref().map(|inner| inner.dashboard_id.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dashboard_id(&mut self, dashboard_id: String) {
|
||||
match self {
|
||||
Self {
|
||||
version: 1,
|
||||
v1: Some(inner),
|
||||
..
|
||||
} => inner.dashboard_id = dashboard_id,
|
||||
Self {
|
||||
version: 2,
|
||||
v2: Some(inner),
|
||||
..
|
||||
} => inner.dashboard_id = dashboard_id,
|
||||
Self {
|
||||
version: 3,
|
||||
v3: Some(inner),
|
||||
..
|
||||
} => inner.dashboard_id = dashboard_id,
|
||||
Self {
|
||||
version: 4,
|
||||
v4: Some(inner),
|
||||
..
|
||||
} => inner.dashboard_id = dashboard_id,
|
||||
Self {
|
||||
version: 5,
|
||||
v5: Some(inner),
|
||||
..
|
||||
} => inner.dashboard_id = dashboard_id,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn owner(&self) -> Option<&str> {
|
||||
match self.version {
|
||||
1 => self.v1.as_ref().map(|inner| inner.owner.as_str()),
|
||||
2 => self.v2.as_ref().map(|inner| inner.owner.as_str()),
|
||||
3 => self.v3.as_ref().map(|inner| inner.owner.as_str()),
|
||||
4 => self.v4.as_ref().map(|inner| inner.owner.as_str()),
|
||||
5 => self.v5.as_ref().map(|inner| inner.owner.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn title(&self) -> Option<&str> {
|
||||
match self.version {
|
||||
1 => self.v1.as_ref().map(|inner| inner.title.as_str()),
|
||||
2 => self.v2.as_ref().map(|inner| inner.title.as_str()),
|
||||
3 => self.v3.as_ref().map(|inner| inner.title.as_str()),
|
||||
4 => self.v4.as_ref().map(|inner| inner.title.as_str()),
|
||||
5 => self.v5.as_ref().map(|inner| inner.title.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_title(&mut self, title: String) {
|
||||
match self {
|
||||
Self {
|
||||
version: 1,
|
||||
v1: Some(inner),
|
||||
..
|
||||
} => inner.title = title,
|
||||
Self {
|
||||
version: 2,
|
||||
v2: Some(inner),
|
||||
..
|
||||
} => inner.title = title,
|
||||
Self {
|
||||
version: 3,
|
||||
v3: Some(inner),
|
||||
..
|
||||
} => inner.title = title,
|
||||
Self {
|
||||
version: 4,
|
||||
v4: Some(inner),
|
||||
..
|
||||
} => inner.title = title,
|
||||
Self {
|
||||
version: 5,
|
||||
v5: Some(inner),
|
||||
..
|
||||
} => inner.title = title,
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn description(&self) -> Option<&str> {
|
||||
match self.version {
|
||||
1 => self.v1.as_ref().map(|inner| inner.description.as_str()),
|
||||
2 => self.v2.as_ref().map(|inner| inner.description.as_str()),
|
||||
3 => self.v3.as_ref().map(|inner| inner.description.as_str()),
|
||||
4 => self.v4.as_ref().map(|inner| inner.description.as_str()),
|
||||
5 => self.v5.as_ref().map(|inner| inner.description.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn role(&self) -> Option<&str> {
|
||||
match self.version {
|
||||
1 => self.v1.as_ref().map(|inner| inner.role.as_str()),
|
||||
2 => self.v2.as_ref().map(|inner| inner.role.as_str()),
|
||||
3 => self.v3.as_ref().map(|inner| inner.role.as_str()),
|
||||
4 => self.v4.as_ref().map(|inner| inner.role.as_str()),
|
||||
5 => self.v5.as_ref().map(|inner| inner.role.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the timestamp with timezone of the time at which the dashboard
|
||||
/// was created.
|
||||
///
|
||||
/// This value is stored in JSON for versions 1-5 of the dashboard. However
|
||||
/// future versions of the dashboard should utilize the `created_at` Unix
|
||||
/// timestamp field in the database to represent the creation timestamp.
|
||||
pub fn created_at_deprecated(&self) -> Option<DateTime<FixedOffset>> {
|
||||
match self.version {
|
||||
1 => self.v1.as_ref().map(|inner| inner.created),
|
||||
2 => self.v2.as_ref().map(|inner| inner.created),
|
||||
3 => self.v3.as_ref().map(|inner| inner.created),
|
||||
4 => self.v4.as_ref().map(|inner| inner.created),
|
||||
5 => self.v5.as_ref().map(|inner| inner.created),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct Dashboards {
|
||||
pub dashboards: Vec<Dashboard>,
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
@@ -40,6 +42,27 @@ pub struct Dashboard {
|
||||
pub variables: Option<Variables>,
|
||||
}
|
||||
|
||||
impl From<Dashboard> for super::Dashboard {
|
||||
fn from(value: Dashboard) -> Self {
|
||||
let version: i32 = 1;
|
||||
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
hasher.write_i32(version);
|
||||
value.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
|
||||
Self {
|
||||
v1: Some(value),
|
||||
v2: None,
|
||||
v3: None,
|
||||
v4: None,
|
||||
v5: None,
|
||||
version,
|
||||
hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn datetime_now() -> DateTime<FixedOffset> {
|
||||
Utc::now().with_timezone(&FixedOffset::east_opt(0).expect(
|
||||
"BUG", // This can't possibly fail. Can it?
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
@@ -41,6 +43,27 @@ pub struct Dashboard {
|
||||
pub variables: Option<Variables>,
|
||||
}
|
||||
|
||||
impl From<Dashboard> for super::Dashboard {
|
||||
fn from(value: Dashboard) -> Self {
|
||||
let version: i32 = 2;
|
||||
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
hasher.write_i32(version);
|
||||
value.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
|
||||
Self {
|
||||
v1: None,
|
||||
v2: Some(value),
|
||||
v3: None,
|
||||
v4: None,
|
||||
v5: None,
|
||||
version,
|
||||
hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn datetime_now() -> DateTime<FixedOffset> {
|
||||
Utc::now().with_timezone(&FixedOffset::east_opt(0).expect(
|
||||
"BUG", // This can't possibly fail. Can it?
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
@@ -43,6 +45,26 @@ pub struct Dashboard {
|
||||
pub default_datetime_duration: Option<DateTimeOptions>,
|
||||
}
|
||||
|
||||
impl From<Dashboard> for super::Dashboard {
|
||||
fn from(value: Dashboard) -> Self {
|
||||
let version: i32 = 3;
|
||||
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
hasher.write_i32(version);
|
||||
value.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
|
||||
Self {
|
||||
v1: None,
|
||||
v2: None,
|
||||
v3: Some(value),
|
||||
v4: None,
|
||||
v5: None,
|
||||
version,
|
||||
hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Layout {
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
@@ -43,6 +45,27 @@ pub struct Dashboard {
|
||||
pub default_datetime_duration: Option<DateTimeOptions>,
|
||||
}
|
||||
|
||||
impl From<Dashboard> for super::Dashboard {
|
||||
fn from(value: Dashboard) -> Self {
|
||||
let version: i32 = 4;
|
||||
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
hasher.write_i32(version);
|
||||
value.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
|
||||
Self {
|
||||
v1: None,
|
||||
v2: None,
|
||||
v3: None,
|
||||
v4: Some(value),
|
||||
v5: None,
|
||||
version,
|
||||
hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Layout {
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
@@ -43,6 +45,27 @@ pub struct Dashboard {
|
||||
pub default_datetime_duration: Option<DateTimeOptions>,
|
||||
}
|
||||
|
||||
impl From<Dashboard> for super::Dashboard {
|
||||
fn from(value: Dashboard) -> Self {
|
||||
let version: i32 = 5;
|
||||
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
hasher.write_i32(version);
|
||||
value.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
|
||||
Self {
|
||||
v1: None,
|
||||
v2: None,
|
||||
v3: None,
|
||||
v4: None,
|
||||
v5: Some(value),
|
||||
version,
|
||||
hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Layout {
|
||||
|
||||
@@ -88,6 +88,32 @@ pub enum DbError {
|
||||
UniqueViolation,
|
||||
#[error("SeaORMError# {0}")]
|
||||
SeaORMError(String),
|
||||
#[error("error getting dashboard")]
|
||||
GetDashboardError(#[from] GetDashboardError),
|
||||
#[error("PutDashbord# {0}")]
|
||||
PutDashboard(#[from] PutDashboardError),
|
||||
}
|
||||
|
||||
#[derive(ThisError, Debug)]
|
||||
pub enum GetDashboardError {
|
||||
#[error("dashboard in DB has version {0} which cannot be deserialized")]
|
||||
UnsupportedVersion(i32),
|
||||
}
|
||||
|
||||
#[derive(ThisError, Debug)]
|
||||
pub enum PutDashboardError {
|
||||
#[error("error putting dashboard with folder that does not exist")]
|
||||
FolderDoesNotExist,
|
||||
#[error("error putting dashboard with missing dashboard_id")]
|
||||
MissingDashboardId,
|
||||
#[error("error putting dashboard with missing title")]
|
||||
MissingTitle,
|
||||
#[error("error putting dashboard with missing owner")]
|
||||
MissingOwner,
|
||||
#[error("error putting dashboard with missing inner data for version {0}")]
|
||||
MissingInnerData(i32),
|
||||
#[error("error converting created timestamp with timezone to Unix timestamp")]
|
||||
ConvertingCreatedTimestamp,
|
||||
}
|
||||
|
||||
#[derive(ThisError, Debug)]
|
||||
@@ -111,6 +137,18 @@ impl From<sea_orm::DbErr> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GetDashboardError> for Error {
|
||||
fn from(value: GetDashboardError) -> Self {
|
||||
Error::DbError(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PutDashboardError> for Error {
|
||||
fn from(value: PutDashboardError) -> Self {
|
||||
Error::DbError(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ErrorCodes {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
// Copyright 2024 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use config::meta::dashboards::{
|
||||
v1::Dashboard as DashboardV1, v2::Dashboard as DashboardV2, v3::Dashboard as DashboardV3,
|
||||
v4::Dashboard as DashboardV4, v5::Dashboard as DashboardV5, Dashboard,
|
||||
};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::NotSet, ColumnTrait, DatabaseConnection, EntityTrait,
|
||||
ModelTrait, QueryFilter, QueryOrder, Set, TryIntoModel,
|
||||
};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use super::{
|
||||
entity::{dashboards, folders},
|
||||
folders::FolderType,
|
||||
};
|
||||
use crate::{
|
||||
db::{connect_to_orm, ORM_CLIENT},
|
||||
errors::{self, GetDashboardError, PutDashboardError},
|
||||
};
|
||||
|
||||
impl TryFrom<dashboards::Model> for Dashboard {
|
||||
type Error = errors::Error;
|
||||
|
||||
fn try_from(mut value: dashboards::Model) -> Result<Self, Self::Error> {
|
||||
if let Some(obj) = value.data.as_object_mut() {
|
||||
// The domain model JSON deserialization logic for v1-v5 expects
|
||||
// some or all these fields to be present in the JSON even though we
|
||||
// store them in DB columns. Therefore we add these values back into
|
||||
// the JSON object so that deserializing the JSON can succeed.
|
||||
obj.insert("dashboardId".to_owned(), value.dashboard_id.into());
|
||||
obj.insert("version".to_owned(), value.version.into());
|
||||
obj.insert("owner".to_owned(), value.owner.into());
|
||||
obj.insert("role".to_owned(), value.role.unwrap_or_default().into());
|
||||
obj.insert("title".to_owned(), value.title.into());
|
||||
obj.insert(
|
||||
"description".to_owned(),
|
||||
value.description.unwrap_or_default().into(),
|
||||
);
|
||||
}
|
||||
|
||||
match value.version {
|
||||
1 => {
|
||||
let inner: DashboardV1 = serde_json::from_value(value.data)?;
|
||||
let dash = inner.into();
|
||||
Ok(dash)
|
||||
}
|
||||
2 => {
|
||||
let inner: DashboardV2 = serde_json::from_value(value.data)?;
|
||||
let dash = inner.into();
|
||||
Ok(dash)
|
||||
}
|
||||
3 => {
|
||||
let inner: DashboardV3 = serde_json::from_value(value.data)?;
|
||||
let dash = inner.into();
|
||||
Ok(dash)
|
||||
}
|
||||
4 => {
|
||||
let inner: DashboardV4 = serde_json::from_value(value.data)?;
|
||||
let dash = inner.into();
|
||||
Ok(dash)
|
||||
}
|
||||
5 => {
|
||||
let inner: DashboardV5 = serde_json::from_value(value.data)?;
|
||||
let dash = inner.into();
|
||||
Ok(dash)
|
||||
}
|
||||
_ => Err(GetDashboardError::UnsupportedVersion(value.version).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a dashboard.
|
||||
pub async fn get(
|
||||
org_id: &str,
|
||||
folder_id: &str,
|
||||
dashboard_id: &str,
|
||||
) -> Result<Option<Dashboard>, errors::Error> {
|
||||
let client = ORM_CLIENT.get_or_init(connect_to_orm).await;
|
||||
let model = get_model(client, org_id, folder_id, dashboard_id)
|
||||
.await?
|
||||
.and_then(|(_folder, maybe_dash)| maybe_dash);
|
||||
|
||||
if let Some(model) = model {
|
||||
let dash = model.try_into()?;
|
||||
Ok(Some(dash))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists all dashboards belonging to the given org and folder.
|
||||
pub async fn list(org_id: &str, folder_id: &str) -> Result<Vec<Dashboard>, errors::Error> {
|
||||
let client = ORM_CLIENT.get_or_init(connect_to_orm).await;
|
||||
let dashboards = list_models(client, org_id, folder_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Dashboard::try_from)
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(dashboards)
|
||||
}
|
||||
|
||||
/// Creates a new dashboard or updates an existing dashboard in the database.
|
||||
/// Returns the new or updated dashboard.
|
||||
pub async fn put(
|
||||
org_id: &str,
|
||||
folder_id: &str,
|
||||
dashboard: Dashboard,
|
||||
) -> Result<Dashboard, errors::Error> {
|
||||
let client = ORM_CLIENT.get_or_init(connect_to_orm).await;
|
||||
|
||||
// Get the fields that will be inserted into or updated in the database.
|
||||
let dashboard_id = dashboard
|
||||
.dashboard_id()
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) })
|
||||
.ok_or(errors::PutDashboardError::MissingDashboardId)?
|
||||
.to_owned();
|
||||
let owner = dashboard
|
||||
.owner()
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) })
|
||||
.ok_or(errors::PutDashboardError::MissingOwner)?
|
||||
.to_owned();
|
||||
let title = dashboard
|
||||
.title()
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) })
|
||||
.ok_or(errors::PutDashboardError::MissingTitle)?
|
||||
.to_owned();
|
||||
let role = dashboard
|
||||
.role()
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) })
|
||||
.map(|r| r.to_owned());
|
||||
let description = dashboard
|
||||
.description()
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) })
|
||||
.map(|d| d.to_owned());
|
||||
let version = dashboard.version;
|
||||
let created_at_depricated = dashboard.created_at_deprecated();
|
||||
|
||||
let data = inner_data_as_json(dashboard)?;
|
||||
|
||||
let (folder_m, mut dash_am) = match get_model(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
|
||||
// convert the dashboard model to an active model that will be
|
||||
// updated.
|
||||
Ok((folder_m, dash_m.into()))
|
||||
}
|
||||
Some((folder_m, None)) => {
|
||||
// Destination folder exists but dashboard does not exist, so create
|
||||
// a new dashboard active model that will be inserted.
|
||||
let created_at_unix: i64 = if let Some(created_at_tz) = created_at_depricated {
|
||||
created_at_tz.timestamp()
|
||||
} else {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_err(|_| PutDashboardError::ConvertingCreatedTimestamp)?
|
||||
.as_secs()
|
||||
.try_into()
|
||||
.map_err(|_| PutDashboardError::ConvertingCreatedTimestamp)?
|
||||
};
|
||||
|
||||
let dash_am = dashboards::ActiveModel {
|
||||
id: NotSet, // Set by DB.
|
||||
dashboard_id: Set(dashboard_id.to_owned()),
|
||||
folder_id: NotSet, // Can be updated, so it is set below.
|
||||
owner: NotSet, // Can be updated, so it is set below.
|
||||
role: NotSet, // Can be updated, so it is set below.
|
||||
title: NotSet, // Can be updated, so it is set below.
|
||||
description: NotSet, // Can be updated, so it is set below.
|
||||
data: NotSet, // Can be updated, so it is set below.
|
||||
version: NotSet, // Can be updated, so it is set below.
|
||||
created_at: Set(created_at_unix),
|
||||
};
|
||||
Ok((folder_m, dash_am))
|
||||
}
|
||||
}?;
|
||||
|
||||
// All of the following fields will be set on creation or updated.
|
||||
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);
|
||||
|
||||
let model: dashboards::Model = dash_am.save(client).await?.try_into_model()?;
|
||||
let dash = model.try_into()?;
|
||||
Ok(dash)
|
||||
}
|
||||
|
||||
/// Deletes a dashboard with the given `folder_id` and `dashboard_id` surrogate
|
||||
/// keys.
|
||||
pub async fn delete(
|
||||
org_id: &str,
|
||||
folder_id: &str,
|
||||
dashboard_id: &str,
|
||||
) -> Result<(), errors::Error> {
|
||||
let client = ORM_CLIENT.get_or_init(connect_to_orm).await;
|
||||
let model = get_model(client, org_id, folder_id, dashboard_id)
|
||||
.await?
|
||||
.and_then(|(_folder, maybe_dash)| maybe_dash);
|
||||
|
||||
if let Some(model) = model {
|
||||
let _ = model.delete(client).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes all dashboards.
|
||||
pub async fn delete_all() -> Result<(), errors::Error> {
|
||||
let client = ORM_CLIENT.get_or_init(connect_to_orm).await;
|
||||
dashboards::Entity::delete_many().exec(client).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to get a dashboard ORM entity and its parent folder ORM entity.
|
||||
async fn get_model(
|
||||
db: &DatabaseConnection,
|
||||
org_id: &str,
|
||||
folder_id: &str,
|
||||
dashboard_id: &str,
|
||||
) -> Result<Option<(folders::Model, Option<dashboards::Model>)>, sea_orm::DbErr> {
|
||||
let select_folders = folders::Entity::find()
|
||||
.filter(folders::Column::Org.eq(org_id))
|
||||
.filter(folders::Column::Type.eq::<i16>(FolderType::Dashboards.into()))
|
||||
.filter(folders::Column::FolderId.eq(folder_id));
|
||||
|
||||
let Some(folder) = select_folders.one(db).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let maybe_dashboard = folder
|
||||
.find_related(dashboards::Entity)
|
||||
.filter(dashboards::Column::DashboardId.eq(dashboard_id))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
Ok(Some((folder, maybe_dashboard)))
|
||||
}
|
||||
|
||||
/// Lists all dashboard ORM models that belong to the given org and folder.
|
||||
async fn list_models(
|
||||
db: &DatabaseConnection,
|
||||
org_id: &str,
|
||||
folder_id: &str,
|
||||
) -> Result<Vec<dashboards::Model>, sea_orm::DbErr> {
|
||||
let rslt = folders::Entity::find()
|
||||
.filter(folders::Column::Org.eq(org_id))
|
||||
.filter(folders::Column::Type.eq::<i16>(FolderType::Dashboards.into()))
|
||||
.filter(folders::Column::FolderId.eq(folder_id))
|
||||
.find_with_related(dashboards::Entity)
|
||||
.order_by_asc(dashboards::Column::Title)
|
||||
.all(db)
|
||||
.await?;
|
||||
let dashboards = rslt.into_iter().flat_map(|(_, ds)| ds).collect();
|
||||
Ok(dashboards)
|
||||
}
|
||||
|
||||
/// Converts the [Dashboard] into the JSON represention of the inner data that
|
||||
/// will be stored in the data column of the database.
|
||||
///
|
||||
/// The returned JSON object should exclude any field that will be stored in
|
||||
/// database columns. The schema of the JSON object will vary depending on the
|
||||
/// dashboard version.
|
||||
fn inner_data_as_json(dashboard: Dashboard) -> Result<JsonValue, errors::Error> {
|
||||
let mut data: JsonValue = match dashboard {
|
||||
Dashboard {
|
||||
version: 1,
|
||||
v1: Some(inner),
|
||||
..
|
||||
} => serde_json::to_value(inner).map_err(errors::Error::SerdeJsonError),
|
||||
Dashboard {
|
||||
version: 2,
|
||||
v2: Some(inner),
|
||||
..
|
||||
} => serde_json::to_value(inner).map_err(errors::Error::SerdeJsonError),
|
||||
Dashboard {
|
||||
version: 3,
|
||||
v3: Some(inner),
|
||||
..
|
||||
} => serde_json::to_value(inner).map_err(errors::Error::SerdeJsonError),
|
||||
Dashboard {
|
||||
version: 4,
|
||||
v4: Some(inner),
|
||||
..
|
||||
} => serde_json::to_value(inner).map_err(errors::Error::SerdeJsonError),
|
||||
Dashboard {
|
||||
version: 5,
|
||||
v5: Some(inner),
|
||||
..
|
||||
} => serde_json::to_value(inner).map_err(errors::Error::SerdeJsonError),
|
||||
Dashboard { version: v, .. } => Err(errors::PutDashboardError::MissingInnerData(v).into()),
|
||||
}?;
|
||||
|
||||
// Remove fields that will be saved in DB columns from the JSON blob so that
|
||||
// we are not storing the same value in two different places and risking
|
||||
// desynchronization.
|
||||
if let Some(obj) = data.as_object_mut() {
|
||||
obj.remove("dashboard_id");
|
||||
obj.remove("version");
|
||||
obj.remove("owner");
|
||||
obj.remove("role");
|
||||
obj.remove("title");
|
||||
obj.remove("description");
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "dashboards")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub dashboard_id: String,
|
||||
pub folder_id: i64,
|
||||
pub owner: String,
|
||||
pub role: Option<String>,
|
||||
pub title: String,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub description: Option<String>,
|
||||
pub data: Json,
|
||||
pub version: i32,
|
||||
pub created_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::folders::Entity",
|
||||
from = "Column::FolderId",
|
||||
to = "super::folders::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
)]
|
||||
Folders,
|
||||
}
|
||||
|
||||
impl Related<super::folders::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Folders.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -16,6 +16,15 @@ pub struct Model {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::dashboards::Entity")]
|
||||
Dashboards,
|
||||
}
|
||||
|
||||
impl Related<super::dashboards::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Dashboards.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
@@ -2,4 +2,5 @@
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod dashboards;
|
||||
pub mod folders;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||
|
||||
pub use super::folders::Entity as Folders;
|
||||
pub use super::{dashboards::Entity as Dashboards, folders::Entity as Folders};
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::{
|
||||
|
||||
/// Indicates the type of data that the folder can contain.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum FolderType {
|
||||
pub(crate) enum FolderType {
|
||||
Dashboards,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright 2024 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
const DASHBOARDS_FOLDERS_FK: &str = "dashboards_folders_fk";
|
||||
const DASHBOARDS_FOLDER_ID_DASHBOARD_ID_IDX: &str = "dashboards_folder_id_dashboard_id_idx";
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(create_dashboards_table_statement())
|
||||
.await?;
|
||||
manager
|
||||
.create_index(create_dashboards_folder_id_dashboard_id_idx_stmnt())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name(DASHBOARDS_FOLDER_ID_DASHBOARD_ID_IDX)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
manager
|
||||
.drop_table(Table::drop().table(Dashboards::Table).to_owned())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Statement to create the dashboards table.
|
||||
fn create_dashboards_table_statement() -> TableCreateStatement {
|
||||
Table::create()
|
||||
.table(Dashboards::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Dashboards::Id)
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
// A user-facing ID for the folder. This value can be a 64-bit signed
|
||||
// integer "snowflake".
|
||||
.col(ColumnDef::new(Dashboards::DashboardId).string_len(256).not_null())
|
||||
// Foreign key to the folders table.
|
||||
.col(ColumnDef::new(Dashboards::FolderId).big_integer().not_null())
|
||||
// Identifier of the user that owns the dashboard.
|
||||
.col(ColumnDef::new(Dashboards::Owner).string_len(256).not_null())
|
||||
.col(ColumnDef::new(Dashboards::Role).string_len(256).null())
|
||||
.col(ColumnDef::new(Dashboards::Title).string_len(256).not_null())
|
||||
.col(ColumnDef::new(Dashboards::Description).text().null())
|
||||
.col(ColumnDef::new(Dashboards::Data).json().not_null())
|
||||
.col(ColumnDef::new(Dashboards::Version).integer().not_null())
|
||||
.col(
|
||||
ColumnDef::new(Dashboards::CreatedAt)
|
||||
.big_integer()
|
||||
.not_null(),
|
||||
)
|
||||
.foreign_key(
|
||||
sea_query::ForeignKey::create()
|
||||
.name(DASHBOARDS_FOLDERS_FK)
|
||||
.from(Dashboards::Table, Dashboards::FolderId)
|
||||
.to(Folders::Table, Folders::Id)
|
||||
)
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
/// Statement to create unique index on dashboard_id.
|
||||
fn create_dashboards_folder_id_dashboard_id_idx_stmnt() -> IndexCreateStatement {
|
||||
sea_query::Index::create()
|
||||
.if_not_exists()
|
||||
.name(DASHBOARDS_FOLDER_ID_DASHBOARD_ID_IDX)
|
||||
.table(Dashboards::Table)
|
||||
.col(Dashboards::FolderId)
|
||||
.col(Dashboards::DashboardId)
|
||||
.unique()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
/// Identifiers used in queries on the dashboards table.
|
||||
#[derive(DeriveIden)]
|
||||
enum Dashboards {
|
||||
Table,
|
||||
Id,
|
||||
DashboardId,
|
||||
FolderId,
|
||||
Owner,
|
||||
Role,
|
||||
Title,
|
||||
Description,
|
||||
Data,
|
||||
Version,
|
||||
CreatedAt,
|
||||
}
|
||||
|
||||
/// Identifiers used in queries on the folders table.
|
||||
#[derive(DeriveIden)]
|
||||
enum Folders {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use collapse::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn postgres() {
|
||||
collapsed_eq!(
|
||||
&create_dashboards_table_statement().to_string(PostgresQueryBuilder),
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS "dashboards" (
|
||||
"id" bigserial NOT NULL PRIMARY KEY,
|
||||
"dashboard_id" varchar(256) NOT NULL,
|
||||
"folder_id" bigint NOT NULL,
|
||||
"owner" varchar(256) NOT NULL,
|
||||
"role" varchar(256) NULL,
|
||||
"title" varchar(256) NOT NULL,
|
||||
"description" text NULL,
|
||||
"data" json NOT NULL,
|
||||
"version" integer NOT NULL,
|
||||
"created_at" bigint NOT NULL,
|
||||
CONSTRAINT "dashboards_folders_fk" FOREIGN KEY ("folder_id") REFERENCES "folders" ("id")
|
||||
)"#
|
||||
);
|
||||
assert_eq!(
|
||||
&create_dashboards_folder_id_dashboard_id_idx_stmnt().to_string(PostgresQueryBuilder),
|
||||
r#"CREATE UNIQUE INDEX IF NOT EXISTS "dashboards_folder_id_dashboard_id_idx" ON "dashboards" ("folder_id", "dashboard_id")"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql() {
|
||||
collapsed_eq!(
|
||||
&create_dashboards_table_statement().to_string(MysqlQueryBuilder),
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS `dashboards` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
`dashboard_id` varchar(256) NOT NULL,
|
||||
`folder_id` bigint NOT NULL,
|
||||
`owner` varchar(256) NOT NULL,
|
||||
`role` varchar(256) NULL,
|
||||
`title` varchar(256) NOT NULL,
|
||||
`description` text NULL,
|
||||
`data` json NOT NULL,
|
||||
`version` int NOT NULL,
|
||||
`created_at` bigint NOT NULL,
|
||||
CONSTRAINT `dashboards_folders_fk` FOREIGN KEY (`folder_id`) REFERENCES `folders` (`id`)
|
||||
)"#
|
||||
);
|
||||
assert_eq!(
|
||||
&create_dashboards_folder_id_dashboard_id_idx_stmnt().to_string(MysqlQueryBuilder),
|
||||
r#"CREATE UNIQUE INDEX `dashboards_folder_id_dashboard_id_idx` ON `dashboards` (`folder_id`, `dashboard_id`)"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlite() {
|
||||
collapsed_eq!(
|
||||
&create_dashboards_table_statement().to_string(SqliteQueryBuilder),
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS "dashboards" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"dashboard_id" varchar(256) NOT NULL,
|
||||
"folder_id" bigint NOT NULL,
|
||||
"owner" varchar(256) NOT NULL,
|
||||
"role" varchar(256) NULL,
|
||||
"title" varchar(256) NOT NULL,
|
||||
"description" text NULL,
|
||||
"data" json_text NOT NULL,
|
||||
"version" integer NOT NULL,
|
||||
"created_at" bigint NOT NULL,
|
||||
FOREIGN KEY ("folder_id") REFERENCES "folders" ("id")
|
||||
)"#
|
||||
);
|
||||
assert_eq!(
|
||||
&create_dashboards_folder_id_dashboard_id_idx_stmnt().to_string(SqliteQueryBuilder),
|
||||
r#"CREATE UNIQUE INDEX IF NOT EXISTS "dashboards_folder_id_dashboard_id_idx" ON "dashboards" ("folder_id", "dashboard_id")"#
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// Copyright 2024 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Populates the dashboards table by transforming unstructured dashboard
|
||||
//! records from the meta table.
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use sea_orm::{
|
||||
ActiveValue::NotSet, EntityTrait, FromQueryResult, Paginator, PaginatorTrait, SelectModel, Set,
|
||||
Statement, TransactionTrait,
|
||||
};
|
||||
use sea_orm_migration::prelude::*;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let txn = manager.get_connection().begin().await?;
|
||||
|
||||
// Migrate pages of 100 records at a time to avoid loading too many
|
||||
// records into memory.
|
||||
let mut meta_pages = MetaDashboard::paginate(&txn, 100);
|
||||
|
||||
while let Some(metas) = meta_pages.fetch_and_next().await? {
|
||||
let dashboards_rslt: Result<Vec<_>, DbErr> = metas
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
let m: dashboards::ActiveModel = m.try_into().map_err(DbErr::Migration)?;
|
||||
Ok(m)
|
||||
})
|
||||
.collect();
|
||||
let dashboards = dashboards_rslt?;
|
||||
dashboards::Entity::insert_many(dashboards)
|
||||
.exec(&txn)
|
||||
.await?;
|
||||
}
|
||||
|
||||
txn.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
dashboards::Entity::delete_many().exec(db).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A result from querying for dashboards from the meta table and joining on the
|
||||
/// folders table.
|
||||
#[derive(Debug, FromQueryResult)]
|
||||
pub struct MetaDashboard {
|
||||
folder_id: Option<i64>,
|
||||
dashboard_id: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl MetaDashboard {
|
||||
/// Paginate through the results of querying for dashboards from the meta
|
||||
/// table and joining on the folders table.
|
||||
fn paginate<C>(db: &C, page_size: u64) -> Paginator<'_, C, SelectModel<MetaDashboard>>
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
let backend = db.get_database_backend();
|
||||
let sql = match backend {
|
||||
sea_orm::DatabaseBackend::MySql => {
|
||||
r#"
|
||||
SELECT
|
||||
f.id AS folder_id,
|
||||
SUBSTRING_INDEX(m.key2,'/', -1) AS dashboard_id,
|
||||
m.value AS value
|
||||
FROM meta AS m
|
||||
JOIN folders f ON
|
||||
SUBSTRING_INDEX(m.key2,'/', 1) = f.folder_id AND
|
||||
m.key1 = f.org
|
||||
WHERE m.module = 'dashboard'
|
||||
ORDER BY m.id ASC
|
||||
"#
|
||||
}
|
||||
sea_orm::DatabaseBackend::Postgres => {
|
||||
r#"
|
||||
SELECT
|
||||
f.id AS folder_id,
|
||||
SPLIT_PART(m.key2, '/', 2) AS dashboard_id,
|
||||
m.value AS value
|
||||
FROM meta AS m
|
||||
JOIN folders f ON
|
||||
SPLIT_PART(m.key2, '/', 1) = f.folder_id AND
|
||||
m.key1 = f.org
|
||||
WHERE m.module = 'dashboard'
|
||||
ORDER BY m.id ASC
|
||||
"#
|
||||
}
|
||||
sea_orm::DatabaseBackend::Sqlite => {
|
||||
r#"
|
||||
SELECT
|
||||
f.id AS folder_id,
|
||||
SUBSTR(m.key2, INSTR(m.key2, '/') + 1) AS dashboard_id,
|
||||
m.value AS value
|
||||
FROM meta AS m
|
||||
INNER JOIN folders f ON
|
||||
SUBSTR(m.key2, 1, INSTR(m.key2, '/') - 1) = f.folder_id AND
|
||||
m.key1 = f.org
|
||||
WHERE m.module = 'dashboard'
|
||||
ORDER BY m.id ASC
|
||||
"#
|
||||
}
|
||||
};
|
||||
|
||||
Self::find_by_statement(Statement::from_sql_and_values(backend, sql, []))
|
||||
.paginate(db, page_size)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<MetaDashboard> for dashboards::ActiveModel {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(m: MetaDashboard) -> Result<Self, Self::Error> {
|
||||
let folder_id = m
|
||||
.folder_id
|
||||
.ok_or("Dashboard in meta table references folder that does not exist")?;
|
||||
let dashboard_id = if m.dashboard_id.is_empty() {
|
||||
Err("Dashboard in meta table is missing a dasbhoard ID")
|
||||
} else {
|
||||
Ok(m.dashboard_id)
|
||||
}?;
|
||||
|
||||
let mut value: JsonValue = serde_json::from_str(&m.value)
|
||||
.map_err(|_| "Dashboard in meta table has \"value\" field that is not valid JSON")?;
|
||||
let obj = value
|
||||
.as_object_mut()
|
||||
.ok_or("Dashboard in meta table has \"value\" field that is not a JSON object")?;
|
||||
|
||||
// Remove each of the following fields from the inner JSON since they
|
||||
// will now be stored in table columns and we don't want to keep
|
||||
// duplicate sources of the data that we would need to synchronize.
|
||||
obj.remove("dashboardId");
|
||||
let version: i32 = obj
|
||||
.remove("version")
|
||||
.and_then(|v| v.as_i64())
|
||||
.and_then(|v: i64| v.try_into().ok())
|
||||
.ok_or("Dashboard JSON does not have i32 \"version\" field")?;
|
||||
let owner = obj
|
||||
.remove("owner")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string()))
|
||||
.ok_or("Dashboard JSON does not have string \"owner\" field")?;
|
||||
let title = obj
|
||||
.remove("title")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string()))
|
||||
.ok_or("Dashboard JSON does not have string \"title\" field")?;
|
||||
let role = obj
|
||||
.remove("role")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string()))
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
||||
let description = obj
|
||||
.remove("description")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string()))
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
||||
|
||||
// Since the created field includes a timezone that cannot be
|
||||
// represented in all databases and since the UI expects to receieve a
|
||||
// timestamp with a timezone, we will leave the created field in the
|
||||
// JSON while also migrating its value into the created_at Unix timestamp column.
|
||||
let created_at_tz: DateTime<FixedOffset> = obj
|
||||
.get("created")
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string()))
|
||||
.ok_or("Dashboard JSON does not have string \"created\" field".to_string())
|
||||
.and_then(|s| DateTime::parse_from_rfc3339(&s).map_err(|e| e.to_string()))?;
|
||||
let created_at_unix = created_at_tz.timestamp();
|
||||
|
||||
Ok(dashboards::ActiveModel {
|
||||
id: NotSet, // Set automatically by DB.
|
||||
dashboard_id: Set(dashboard_id),
|
||||
folder_id: Set(folder_id),
|
||||
owner: Set(owner),
|
||||
role: Set(role),
|
||||
title: Set(title),
|
||||
description: Set(description),
|
||||
data: Set(value),
|
||||
version: Set(version),
|
||||
created_at: Set(created_at_unix),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The schemas of tables might change after subsequent migrations. Therefore
|
||||
// this migration only references ORM models in private submodules that should
|
||||
// remain unchanged rather than ORM models in the `entity` module that will be
|
||||
// updated to reflect the latest changes to table schemas.
|
||||
|
||||
/// Representation of the dashboards table at the time this migration executes.
|
||||
mod dashboards {
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "dashboards")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub dashboard_id: String,
|
||||
pub folder_id: i64,
|
||||
pub owner: String,
|
||||
pub role: Option<String>,
|
||||
pub title: String,
|
||||
#[sea_orm(column_type = "Text", nullable)]
|
||||
pub description: Option<String>,
|
||||
pub data: Json,
|
||||
pub version: i32,
|
||||
pub created_at: i64,
|
||||
}
|
||||
|
||||
// There are relations but they are not important to this migration.
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2024 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Deletes dashboard records from the meta table.
|
||||
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, TransactionTrait};
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let txn = manager.get_connection().begin().await?;
|
||||
meta::Entity::delete_many()
|
||||
.filter(meta::Column::Module.eq("dashboard"))
|
||||
.exec(&txn)
|
||||
.await?;
|
||||
txn.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, _: &SchemaManager) -> Result<(), DbErr> {
|
||||
// The deletion of records from the meta table is not reversable.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// The schemas of tables might change after subsequent migrations. Therefore
|
||||
// this migration only references ORM models in private submodules that should
|
||||
// remain unchanged rather than ORM models in the `entity` module that will be
|
||||
// updated to reflect the latest changes to table schemas.
|
||||
|
||||
/// Representation of the meta table at the time this migration executes.
|
||||
mod meta {
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "meta")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub module: String,
|
||||
pub key1: String,
|
||||
pub key2: String,
|
||||
pub start_dt: i64,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
}
|
||||
@@ -19,6 +19,9 @@ mod m20241114_000001_create_folders_table;
|
||||
mod m20241115_150000_populate_folders_table;
|
||||
mod m20241116_000001_delete_metas;
|
||||
mod m20241116_000002_drop_folders_created_at_column;
|
||||
mod m20241119_000001_create_dashboards_table;
|
||||
mod m20241119_000002_populate_dashboards_table;
|
||||
mod m20241119_000003_delete_metas;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -30,6 +33,9 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20241115_150000_populate_folders_table::Migration),
|
||||
Box::new(m20241116_000001_delete_metas::Migration),
|
||||
Box::new(m20241116_000002_drop_folders_created_at_column::Migration),
|
||||
Box::new(m20241119_000001_create_dashboards_table::Migration),
|
||||
Box::new(m20241119_000002_populate_dashboards_table::Migration),
|
||||
Box::new(m20241119_000003_delete_metas::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use sea_orm_migration::MigratorTrait;
|
||||
|
||||
use crate::db::{connect_to_orm, sqlite::CLIENT_RW, ORM_CLIENT, SQLITE_STORE};
|
||||
|
||||
pub mod dashboards;
|
||||
#[allow(unused_imports)]
|
||||
mod entity;
|
||||
pub mod folders;
|
||||
|
||||
@@ -119,9 +119,14 @@ pub async fn update_dashboard(
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn list_dashboards(org_id: &str, folder_id: &str) -> Result<HttpResponse, io::Error> {
|
||||
Ok(HttpResponse::Ok().json(Dashboards {
|
||||
dashboards: db::dashboards::list(org_id, folder_id).await.unwrap(),
|
||||
}))
|
||||
let resp = match db::dashboards::list(org_id, folder_id).await {
|
||||
Ok(dashboards) => HttpResponse::Ok().json(Dashboards { dashboards }),
|
||||
Err(error) => HttpResponse::InternalServerError().json(MetaHttpResponse::error(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR.into(),
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
@@ -130,10 +135,13 @@ pub async fn get_dashboard(
|
||||
dashboard_id: &str,
|
||||
folder_id: &str,
|
||||
) -> Result<HttpResponse, io::Error> {
|
||||
let resp = if let Ok(dashboard) = db::dashboards::get(org_id, dashboard_id, folder_id).await {
|
||||
HttpResponse::Ok().json(dashboard)
|
||||
} else {
|
||||
return Ok(Response::NotFound("Dashboard".to_string()).into());
|
||||
let resp = match db::dashboards::get(org_id, dashboard_id, folder_id).await {
|
||||
Ok(Some(dash)) => HttpResponse::Ok().json(dash),
|
||||
Ok(None) => Response::NotFound("Dashboard".to_string()).into(),
|
||||
Err(error) => HttpResponse::InternalServerError().json(MetaHttpResponse::error(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR.into(),
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
Ok(resp)
|
||||
}
|
||||
@@ -144,15 +152,24 @@ pub async fn delete_dashboard(
|
||||
dashboard_id: &str,
|
||||
folder_id: &str,
|
||||
) -> Result<HttpResponse, io::Error> {
|
||||
if db::dashboards::get(org_id, dashboard_id, folder_id)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Ok(HttpResponse::NotFound().json(MetaHttpResponse::error(
|
||||
http::StatusCode::NOT_FOUND.into(),
|
||||
"Dashboard not found".to_string(),
|
||||
)));
|
||||
}
|
||||
match db::dashboards::get(org_id, dashboard_id, folder_id).await {
|
||||
Ok(Some(_)) => {} // Dashboard exists. Continue with deleting.
|
||||
Ok(None) => {
|
||||
return Ok(HttpResponse::NotFound().json(MetaHttpResponse::error(
|
||||
http::StatusCode::NOT_FOUND.into(),
|
||||
"Dashboard not found".to_string(),
|
||||
)));
|
||||
}
|
||||
Err(error) => {
|
||||
return Ok(
|
||||
HttpResponse::InternalServerError().json(MetaHttpResponse::error(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR.into(),
|
||||
error.to_string(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
match db::dashboards::delete(org_id, dashboard_id, folder_id).await {
|
||||
Ok(_) => {
|
||||
remove_ownership(
|
||||
@@ -209,11 +226,12 @@ pub async fn move_dashboard(
|
||||
from_folder: &str,
|
||||
to_folder: &str,
|
||||
) -> Result<HttpResponse, anyhow::Error> {
|
||||
if let Ok(dashboard) = db::dashboards::get(org_id, dashboard_id, from_folder).await {
|
||||
if let Ok(Some(dashboard)) = db::dashboards::get(org_id, dashboard_id, from_folder).await {
|
||||
// make sure the destination folder exists
|
||||
if !db::folders::exists(org_id, to_folder).await? {
|
||||
return Ok(Response::NotFound("Destination Folder".to_string()).into());
|
||||
}
|
||||
|
||||
let dash = if dashboard.version == 1 {
|
||||
json::to_vec(&dashboard.v1.unwrap()).unwrap()
|
||||
} else if dashboard.version == 2 {
|
||||
|
||||
@@ -141,9 +141,9 @@ pub async fn save(
|
||||
// Supports only one tab for now
|
||||
let tab_id = &dashboard.tabs[0];
|
||||
tasks.push(async move {
|
||||
let dashboard = db::dashboards::get(org_id, dash_id, folder).await?;
|
||||
let maybe_dashboard = db::dashboards::get(org_id, dash_id, folder).await?;
|
||||
// Check if the tab_id exists
|
||||
if let Some(dashboard) = dashboard.v3 {
|
||||
if let Some(dashboard) = maybe_dashboard.and_then(|d| d.v3) {
|
||||
let mut tab_found = false;
|
||||
for tab in dashboard.tabs {
|
||||
if &tab.tab_id == tab_id {
|
||||
|
||||
@@ -13,15 +13,12 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use actix_web::web;
|
||||
use config::{
|
||||
meta::dashboards::{v1, v2, v3, v4, v5, Dashboard, DashboardVersion},
|
||||
utils::json,
|
||||
};
|
||||
|
||||
use crate::service::db;
|
||||
use infra::table::dashboards;
|
||||
|
||||
pub mod reports;
|
||||
|
||||
@@ -29,77 +26,21 @@ pub mod reports;
|
||||
pub(crate) async fn get(
|
||||
org_id: &str,
|
||||
dashboard_id: &str,
|
||||
folder: &str,
|
||||
) -> Result<Dashboard, anyhow::Error> {
|
||||
let key = format!("/dashboard/{org_id}/{folder}/{dashboard_id}");
|
||||
let bytes = db::get(&key).await?;
|
||||
let d_version: DashboardVersion = json::from_slice(&bytes)?;
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
hasher.write_i32(d_version.version);
|
||||
|
||||
if d_version.version == 1 {
|
||||
let dash: v1::Dashboard = json::from_slice(&bytes)?;
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
Ok(Dashboard {
|
||||
v1: Some(dash),
|
||||
version: 1,
|
||||
hash,
|
||||
..Default::default()
|
||||
})
|
||||
} else if d_version.version == 2 {
|
||||
let dash: v2::Dashboard = json::from_slice(&bytes)?;
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
Ok(Dashboard {
|
||||
v2: Some(dash),
|
||||
version: 2,
|
||||
hash,
|
||||
..Default::default()
|
||||
})
|
||||
} else if d_version.version == 3 {
|
||||
let dash: v3::Dashboard = json::from_slice(&bytes)?;
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
Ok(Dashboard {
|
||||
v3: Some(dash),
|
||||
version: 3,
|
||||
hash,
|
||||
..Default::default()
|
||||
})
|
||||
} else if d_version.version == 4 {
|
||||
let dash: v4::Dashboard = json::from_slice(&bytes)?;
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
Ok(Dashboard {
|
||||
v4: Some(dash),
|
||||
version: 4,
|
||||
hash,
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
let dash: v5::Dashboard = json::from_slice(&bytes)?;
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
Ok(Dashboard {
|
||||
v5: Some(dash),
|
||||
version: 5,
|
||||
hash,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
folder_id: &str,
|
||||
) -> Result<Option<Dashboard>, anyhow::Error> {
|
||||
let dash = dashboards::get(org_id, folder_id, dashboard_id).await?;
|
||||
Ok(dash)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(body))]
|
||||
pub(crate) async fn put(
|
||||
org_id: &str,
|
||||
dashboard_id: &str,
|
||||
folder: &str,
|
||||
folder_id: &str,
|
||||
body: web::Bytes,
|
||||
hash: Option<&str>,
|
||||
) -> Result<Dashboard, anyhow::Error> {
|
||||
let key = format!("/dashboard/{org_id}/{folder}/{}", dashboard_id);
|
||||
if let Ok(existing_dash) = get(org_id, dashboard_id, folder).await {
|
||||
if let Some(existing_dash) = get(org_id, dashboard_id, folder_id).await? {
|
||||
let existing_dash_hash = existing_dash.hash;
|
||||
|
||||
let Some(Ok(hash_val)) = hash.map(|hash_str| hash_str.parse::<u64>()) else {
|
||||
@@ -115,186 +56,68 @@ pub(crate) async fn put(
|
||||
};
|
||||
|
||||
let d_version: DashboardVersion = json::from_slice(&body)?;
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
hasher.write_i32(d_version.version);
|
||||
let mut dash = parse_dashboard(body, d_version.version)?;
|
||||
|
||||
if d_version.version == 1 {
|
||||
let mut dash: v1::Dashboard = json::from_slice(&body)?;
|
||||
dash.title = dash.title.trim().to_string();
|
||||
if dash.title.is_empty() {
|
||||
return Err(anyhow::anyhow!("Dashboard should have title"));
|
||||
};
|
||||
dash.dashboard_id = dashboard_id.to_string();
|
||||
let value: bytes::Bytes = json::to_vec(&dash)?.into();
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
let title = dash
|
||||
.title()
|
||||
.map(|t| t.trim().to_string())
|
||||
.and_then(|t| if t.is_empty() { None } else { Some(t) })
|
||||
.ok_or_else(|| anyhow::anyhow!("Dashboard should have title"))?;
|
||||
dash.set_title(title);
|
||||
|
||||
match db::put(&key, value, db::NO_NEED_WATCH, None).await {
|
||||
Ok(_) => Ok(Dashboard {
|
||||
v1: Some(dash),
|
||||
version: 1,
|
||||
hash,
|
||||
..Default::default()
|
||||
}),
|
||||
Err(_) => Err(anyhow::anyhow!("Failed to save Dashboard")),
|
||||
}
|
||||
} else if d_version.version == 2 {
|
||||
let mut dash: v2::Dashboard = json::from_slice(&body)?;
|
||||
dash.title = dash.title.trim().to_string();
|
||||
if dash.title.is_empty() {
|
||||
return Err(anyhow::anyhow!("Dashboard should have title"));
|
||||
};
|
||||
dash.dashboard_id = dashboard_id.to_string();
|
||||
let value: bytes::Bytes = json::to_vec(&dash)?.into();
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
match db::put(&key, value, db::NO_NEED_WATCH, None).await {
|
||||
Ok(_) => Ok(Dashboard {
|
||||
v2: Some(dash),
|
||||
version: 2,
|
||||
hash,
|
||||
..Default::default()
|
||||
}),
|
||||
Err(_) => Err(anyhow::anyhow!("Failed to save Dashboard")),
|
||||
}
|
||||
} else if d_version.version == 3 {
|
||||
let mut dash: v3::Dashboard = json::from_slice(&body)?;
|
||||
dash.title = dash.title.trim().to_string();
|
||||
if dash.title.is_empty() {
|
||||
return Err(anyhow::anyhow!("Dashboard should have title"));
|
||||
};
|
||||
dash.dashboard_id = dashboard_id.to_string();
|
||||
let value: bytes::Bytes = json::to_vec(&dash)?.into();
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
match db::put(&key, value, db::NO_NEED_WATCH, None).await {
|
||||
Ok(_) => Ok(Dashboard {
|
||||
v3: Some(dash),
|
||||
version: 3,
|
||||
hash,
|
||||
..Default::default()
|
||||
}),
|
||||
Err(_) => Err(anyhow::anyhow!("Failed to save Dashboard")),
|
||||
}
|
||||
} else if d_version.version == 4 {
|
||||
let mut dash: v4::Dashboard = json::from_slice(&body)?;
|
||||
dash.title = dash.title.trim().to_string();
|
||||
if dash.title.is_empty() {
|
||||
return Err(anyhow::anyhow!("Dashboard should have title"));
|
||||
};
|
||||
dash.dashboard_id = dashboard_id.to_string();
|
||||
let value: bytes::Bytes = json::to_vec(&dash)?.into();
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
match db::put(&key, value, db::NO_NEED_WATCH, None).await {
|
||||
Ok(_) => Ok(Dashboard {
|
||||
v4: Some(dash),
|
||||
version: 4,
|
||||
hash,
|
||||
..Default::default()
|
||||
}),
|
||||
Err(_) => Err(anyhow::anyhow!("Failed to save Dashboard")),
|
||||
}
|
||||
} else {
|
||||
let mut dash: v5::Dashboard = json::from_slice(&body)?;
|
||||
dash.title = dash.title.trim().to_string();
|
||||
if dash.title.is_empty() {
|
||||
return Err(anyhow::anyhow!("Dashboard should have title"));
|
||||
};
|
||||
dash.dashboard_id = dashboard_id.to_string();
|
||||
let value: bytes::Bytes = json::to_vec(&dash)?.into();
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
match db::put(&key, value, db::NO_NEED_WATCH, None).await {
|
||||
Ok(_) => Ok(Dashboard {
|
||||
v5: Some(dash),
|
||||
version: 5,
|
||||
hash,
|
||||
..Default::default()
|
||||
}),
|
||||
Err(_) => Err(anyhow::anyhow!("Failed to save Dashboard")),
|
||||
}
|
||||
}
|
||||
dash.set_dashboard_id(dashboard_id.to_owned());
|
||||
let dash = dashboards::put(org_id, folder_id, dash).await?;
|
||||
Ok(dash)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub(crate) async fn list(org_id: &str, folder: &str) -> Result<Vec<Dashboard>, anyhow::Error> {
|
||||
let db_key = format!("/dashboard/{org_id}/{folder}/");
|
||||
db::list(&db_key)
|
||||
.await?
|
||||
.into_values()
|
||||
.map(|val| {
|
||||
let d_version: DashboardVersion = json::from_slice(&val).unwrap();
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
hasher.write_i32(d_version.version);
|
||||
|
||||
if d_version.version == 1 {
|
||||
let dash: v1::Dashboard = json::from_slice(&val).unwrap();
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
Ok(Dashboard {
|
||||
v1: Some(dash),
|
||||
version: 1,
|
||||
hash,
|
||||
..Default::default()
|
||||
})
|
||||
} else if d_version.version == 2 {
|
||||
let dash: v2::Dashboard = json::from_slice(&val).unwrap();
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
Ok(Dashboard {
|
||||
v2: Some(dash),
|
||||
version: 2,
|
||||
hash,
|
||||
..Default::default()
|
||||
})
|
||||
} else if d_version.version == 3 {
|
||||
let dash: v3::Dashboard = json::from_slice(&val).unwrap();
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
Ok(Dashboard {
|
||||
v3: Some(dash),
|
||||
version: 3,
|
||||
hash,
|
||||
..Default::default()
|
||||
})
|
||||
} else if d_version.version == 4 {
|
||||
let dash: v4::Dashboard = json::from_slice(&val).unwrap();
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
Ok(Dashboard {
|
||||
v4: Some(dash),
|
||||
version: 4,
|
||||
hash,
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
let dash: v5::Dashboard = json::from_slice(&val).unwrap();
|
||||
dash.hash(&mut hasher);
|
||||
let hash = hasher.finish().to_string();
|
||||
Ok(Dashboard {
|
||||
v5: Some(dash),
|
||||
version: 5,
|
||||
hash,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
pub(crate) async fn list(org_id: &str, folder_id: &str) -> Result<Vec<Dashboard>, anyhow::Error> {
|
||||
let ds = dashboards::list(org_id, folder_id).await?;
|
||||
Ok(ds)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub(crate) async fn delete(
|
||||
org_id: &str,
|
||||
dashboard_id: &str,
|
||||
folder: &str,
|
||||
folder_id: &str,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let key = format!("/dashboard/{org_id}/{folder}/{dashboard_id}");
|
||||
Ok(db::delete(&key, false, db::NO_NEED_WATCH, None).await?)
|
||||
dashboards::delete(org_id, folder_id, dashboard_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn reset() -> Result<(), anyhow::Error> {
|
||||
let key = "/dashboard/";
|
||||
Ok(db::delete(key, true, db::NO_NEED_WATCH, None).await?)
|
||||
dashboards::delete_all().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parses the bytes into a dashboard with the given version.
|
||||
pub(crate) fn parse_dashboard(
|
||||
bytes: web::Bytes,
|
||||
version: i32,
|
||||
) -> Result<Dashboard, serde_json::Error> {
|
||||
let dash = match version {
|
||||
1 => {
|
||||
let inner: v1::Dashboard = json::from_slice(&bytes)?;
|
||||
inner.into()
|
||||
}
|
||||
2 => {
|
||||
let inner: v2::Dashboard = json::from_slice(&bytes)?;
|
||||
inner.into()
|
||||
}
|
||||
3 => {
|
||||
let inner: v3::Dashboard = json::from_slice(&bytes)?;
|
||||
inner.into()
|
||||
}
|
||||
4 => {
|
||||
let inner: v4::Dashboard = json::from_slice(&bytes)?;
|
||||
inner.into()
|
||||
}
|
||||
_ => {
|
||||
let inner: v5::Dashboard = json::from_slice(&bytes)?;
|
||||
inner.into()
|
||||
}
|
||||
};
|
||||
Ok(dash)
|
||||
}
|
||||
|
||||
@@ -102,7 +102,11 @@ def test_e2e_createdeletedashboard(create_session, base_url):
|
||||
session = create_session
|
||||
# Create a dashboard
|
||||
org_id = "default"
|
||||
payload = {"description": "pydashboards", "title": "pytestdashboard"}
|
||||
payload = {
|
||||
"description": "pydashboards",
|
||||
"title": "pytestdashboard",
|
||||
"owner": "user1@alpha1.com",
|
||||
}
|
||||
|
||||
resp_create_dashboard = session.post(
|
||||
f"{base_url}api/{org_id}/dashboards", json=payload
|
||||
@@ -120,7 +124,7 @@ def test_e2e_createdeletedashboard(create_session, base_url):
|
||||
"dashboardId": "",
|
||||
"description": "updatedashboard",
|
||||
"role": "",
|
||||
"owner": "root@alpha1.com",
|
||||
"owner": "user2@alpha1.com",
|
||||
"created": "2023-10-19T14:08:48.090Z",
|
||||
"panels": [],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user