Skip to content

Commit b8a9bd0

Browse files
update dashboard implementation (#1348)
- New Features Added the ability to add tiles to existing dashboards. Introduced support for dashboard types and more flexible tile definitions. Improved error handling with clearer unauthorized access messages. - Bug Fixes Enhanced validation for dashboard and tile IDs to ensure uniqueness and correctness. - Refactor Simplified dashboard data model and removed legacy migration logic. Centralized and streamlined dashboard creation, updating, and deletion processes. Improved ownership enforcement for dashboard modifications. Restructured dashboard API routes for clearer and more organized access. Updated dashboard listing to return summaries instead of full dashboards. Removed manual ID and version assignments, relying on centralized methods. Updated search functionality to use dashboard titles for improved matching. - Style Updated naming conventions for dashboard-related actions for better clarity.
1 parent 32d105e commit b8a9bd0

File tree

4 files changed

+331
-316
lines changed

4 files changed

+331
-316
lines changed

src/handlers/http/modal/server.rs

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -270,31 +270,41 @@ impl Server {
270270
web::resource("")
271271
.route(
272272
web::post()
273-
.to(dashboards::post)
273+
.to(dashboards::create_dashboard)
274274
.authorize(Action::CreateDashboard),
275275
)
276276
.route(
277277
web::get()
278-
.to(dashboards::list)
278+
.to(dashboards::list_dashboards)
279279
.authorize(Action::ListDashboard),
280280
),
281281
)
282282
.service(
283-
web::resource("/{dashboard_id}")
284-
.route(
285-
web::get()
286-
.to(dashboards::get)
287-
.authorize(Action::GetDashboard),
288-
)
289-
.route(
290-
web::delete()
291-
.to(dashboards::delete)
292-
.authorize(Action::DeleteDashboard),
283+
web::scope("/{dashboard_id}")
284+
.service(
285+
web::resource("")
286+
.route(
287+
web::get()
288+
.to(dashboards::get_dashboard)
289+
.authorize(Action::GetDashboard),
290+
)
291+
.route(
292+
web::delete()
293+
.to(dashboards::delete_dashboard)
294+
.authorize(Action::DeleteDashboard),
295+
)
296+
.route(
297+
web::put()
298+
.to(dashboards::update_dashboard)
299+
.authorize(Action::CreateDashboard),
300+
),
293301
)
294-
.route(
295-
web::put()
296-
.to(dashboards::update)
297-
.authorize(Action::CreateDashboard),
302+
.service(
303+
web::resource("/add_tile").route(
304+
web::put()
305+
.to(dashboards::add_tile)
306+
.authorize(Action::CreateDashboard),
307+
),
298308
),
299309
)
300310
}

src/handlers/http/users/dashboards.rs

Lines changed: 81 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -18,140 +18,131 @@
1818

1919
use crate::{
2020
handlers::http::rbac::RBACError,
21-
parseable::PARSEABLE,
22-
storage::{object_storage::dashboard_path, ObjectStorageError},
23-
users::dashboards::{Dashboard, CURRENT_DASHBOARD_VERSION, DASHBOARDS},
24-
utils::{actix::extract_session_key_from_req, get_hash, get_user_from_request},
21+
storage::ObjectStorageError,
22+
users::dashboards::{validate_dashboard_id, Dashboard, Tile, DASHBOARDS},
23+
utils::{get_hash, get_user_from_request},
2524
};
2625
use actix_web::{
2726
http::header::ContentType,
2827
web::{self, Json, Path},
2928
HttpRequest, HttpResponse, Responder,
3029
};
31-
use bytes::Bytes;
32-
use rand::distributions::DistString;
33-
34-
use chrono::Utc;
3530
use http::StatusCode;
3631
use serde_json::Error as SerdeError;
3732

38-
pub async fn list(req: HttpRequest) -> Result<impl Responder, DashboardError> {
39-
let key =
40-
extract_session_key_from_req(&req).map_err(|e| DashboardError::Custom(e.to_string()))?;
41-
let dashboards = DASHBOARDS.list_dashboards(&key).await;
33+
pub async fn list_dashboards() -> Result<impl Responder, DashboardError> {
34+
let dashboards = DASHBOARDS.list_dashboards().await;
35+
let dashboard_summaries = dashboards
36+
.iter()
37+
.map(|dashboard| dashboard.to_summary())
38+
.collect::<Vec<_>>();
4239

43-
Ok((web::Json(dashboards), StatusCode::OK))
40+
Ok((web::Json(dashboard_summaries), StatusCode::OK))
4441
}
4542

46-
pub async fn get(
47-
req: HttpRequest,
48-
dashboard_id: Path<String>,
49-
) -> Result<impl Responder, DashboardError> {
50-
let user_id = get_user_from_request(&req)?;
51-
let dashboard_id = dashboard_id.into_inner();
43+
pub async fn get_dashboard(dashboard_id: Path<String>) -> Result<impl Responder, DashboardError> {
44+
let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?;
5245

53-
if let Some(dashboard) = DASHBOARDS
54-
.get_dashboard(&dashboard_id, &get_hash(&user_id))
46+
let dashboard = DASHBOARDS
47+
.get_dashboard(dashboard_id)
5548
.await
56-
{
57-
return Ok((web::Json(dashboard), StatusCode::OK));
58-
}
49+
.ok_or_else(|| DashboardError::Metadata("Dashboard does not exist"))?;
5950

60-
Err(DashboardError::Metadata("Dashboard does not exist"))
51+
Ok((web::Json(dashboard), StatusCode::OK))
6152
}
6253

63-
pub async fn post(
54+
pub async fn create_dashboard(
6455
req: HttpRequest,
6556
Json(mut dashboard): Json<Dashboard>,
6657
) -> Result<impl Responder, DashboardError> {
67-
let mut user_id = get_user_from_request(&req)?;
68-
user_id = get_hash(&user_id);
69-
let dashboard_id = get_hash(Utc::now().timestamp_micros().to_string().as_str());
70-
dashboard.dashboard_id = Some(dashboard_id.clone());
71-
dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string());
72-
73-
dashboard.user_id = Some(user_id.clone());
74-
for tile in dashboard.tiles.iter_mut() {
75-
tile.tile_id = Some(get_hash(
76-
format!(
77-
"{}{}",
78-
rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 8),
79-
Utc::now().timestamp_micros()
80-
)
81-
.as_str(),
82-
));
58+
if dashboard.title.is_empty() {
59+
return Err(DashboardError::Metadata("Title must be provided"));
8360
}
84-
DASHBOARDS.update(&dashboard).await;
85-
86-
let path = dashboard_path(&user_id, &format!("{dashboard_id}.json"));
8761

88-
let store = PARSEABLE.storage.get_object_store();
89-
let dashboard_bytes = serde_json::to_vec(&dashboard)?;
90-
store
91-
.put_object(&path, Bytes::from(dashboard_bytes))
92-
.await?;
62+
let user_id = get_hash(&get_user_from_request(&req)?);
9363

64+
DASHBOARDS.create(&user_id, &mut dashboard).await?;
9465
Ok((web::Json(dashboard), StatusCode::OK))
9566
}
9667

97-
pub async fn update(
68+
pub async fn update_dashboard(
9869
req: HttpRequest,
9970
dashboard_id: Path<String>,
10071
Json(mut dashboard): Json<Dashboard>,
10172
) -> Result<impl Responder, DashboardError> {
102-
let mut user_id = get_user_from_request(&req)?;
103-
user_id = get_hash(&user_id);
104-
let dashboard_id = dashboard_id.into_inner();
73+
let user_id = get_hash(&get_user_from_request(&req)?);
74+
let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?;
10575

106-
if DASHBOARDS
107-
.get_dashboard(&dashboard_id, &user_id)
108-
.await
109-
.is_none()
110-
{
111-
return Err(DashboardError::Metadata("Dashboard does not exist"));
112-
}
113-
dashboard.dashboard_id = Some(dashboard_id.to_string());
114-
dashboard.user_id = Some(user_id.clone());
115-
dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string());
116-
for tile in dashboard.tiles.iter_mut() {
117-
if tile.tile_id.is_none() {
118-
tile.tile_id = Some(get_hash(Utc::now().timestamp_micros().to_string().as_str()));
76+
// Validate all tiles have valid IDs
77+
if let Some(tiles) = &dashboard.tiles {
78+
if tiles.iter().any(|tile| tile.tile_id.is_nil()) {
79+
return Err(DashboardError::Metadata("Tile ID must be provided"));
11980
}
12081
}
121-
DASHBOARDS.update(&dashboard).await;
12282

123-
let path = dashboard_path(&user_id, &format!("{dashboard_id}.json"));
83+
// Check if tile_id are unique
84+
if let Some(tiles) = &dashboard.tiles {
85+
let unique_tiles: Vec<_> = tiles
86+
.iter()
87+
.map(|tile| tile.tile_id)
88+
.collect::<std::collections::HashSet<_>>()
89+
.into_iter()
90+
.collect();
91+
92+
if unique_tiles.len() != tiles.len() {
93+
return Err(DashboardError::Metadata("Tile IDs must be unique"));
94+
}
95+
}
12496

125-
let store = PARSEABLE.storage.get_object_store();
126-
let dashboard_bytes = serde_json::to_vec(&dashboard)?;
127-
store
128-
.put_object(&path, Bytes::from(dashboard_bytes))
97+
DASHBOARDS
98+
.update(&user_id, dashboard_id, &mut dashboard)
12999
.await?;
130100

131101
Ok((web::Json(dashboard), StatusCode::OK))
132102
}
133103

134-
pub async fn delete(
104+
pub async fn delete_dashboard(
135105
req: HttpRequest,
136106
dashboard_id: Path<String>,
137107
) -> Result<HttpResponse, DashboardError> {
138-
let mut user_id = get_user_from_request(&req)?;
139-
user_id = get_hash(&user_id);
140-
let dashboard_id = dashboard_id.into_inner();
141-
if DASHBOARDS
142-
.get_dashboard(&dashboard_id, &user_id)
108+
let user_id = get_hash(&get_user_from_request(&req)?);
109+
let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?;
110+
111+
DASHBOARDS.delete_dashboard(&user_id, dashboard_id).await?;
112+
113+
Ok(HttpResponse::Ok().finish())
114+
}
115+
116+
pub async fn add_tile(
117+
req: HttpRequest,
118+
dashboard_id: Path<String>,
119+
Json(tile): Json<Tile>,
120+
) -> Result<impl Responder, DashboardError> {
121+
if tile.tile_id.is_nil() {
122+
return Err(DashboardError::Metadata("Tile ID must be provided"));
123+
}
124+
125+
let user_id = get_hash(&get_user_from_request(&req)?);
126+
let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?;
127+
128+
let mut dashboard = DASHBOARDS
129+
.get_dashboard_by_user(dashboard_id, &user_id)
143130
.await
144-
.is_none()
145-
{
146-
return Err(DashboardError::Metadata("Dashboard does not exist"));
131+
.ok_or(DashboardError::Unauthorized)?;
132+
133+
let tiles = dashboard.tiles.get_or_insert_with(Vec::new);
134+
135+
// check if the tile already exists
136+
if tiles.iter().any(|t| t.tile_id == tile.tile_id) {
137+
return Err(DashboardError::Metadata("Tile already exists"));
147138
}
148-
let path = dashboard_path(&user_id, &format!("{dashboard_id}.json"));
149-
let store = PARSEABLE.storage.get_object_store();
150-
store.delete_object(&path).await?;
139+
tiles.push(tile);
151140

152-
DASHBOARDS.delete_dashboard(&dashboard_id).await;
141+
DASHBOARDS
142+
.update(&user_id, dashboard_id, &mut dashboard)
143+
.await?;
153144

154-
Ok(HttpResponse::Ok().finish())
145+
Ok((web::Json(dashboard), StatusCode::OK))
155146
}
156147

157148
#[derive(Debug, thiserror::Error)]
@@ -166,6 +157,8 @@ pub enum DashboardError {
166157
UserDoesNotExist(#[from] RBACError),
167158
#[error("Error: {0}")]
168159
Custom(String),
160+
#[error("Dashboard does not exist or is not accessible")]
161+
Unauthorized,
169162
}
170163

171164
impl actix_web::ResponseError for DashboardError {
@@ -176,6 +169,7 @@ impl actix_web::ResponseError for DashboardError {
176169
Self::Metadata(_) => StatusCode::BAD_REQUEST,
177170
Self::UserDoesNotExist(_) => StatusCode::NOT_FOUND,
178171
Self::Custom(_) => StatusCode::INTERNAL_SERVER_ERROR,
172+
Self::Unauthorized => StatusCode::UNAUTHORIZED,
179173
}
180174
}
181175

src/prism/home/mod.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ pub async fn generate_home_search_response(
270270
let (alert_titles, correlation_titles, dashboard_titles, filter_titles, stream_titles) = tokio::join!(
271271
get_alert_titles(key, query_value),
272272
get_correlation_titles(key, query_value),
273-
get_dashboard_titles(key, query_value),
273+
get_dashboard_titles(query_value),
274274
get_filter_titles(key, query_value),
275275
get_stream_titles(key)
276276
);
@@ -368,22 +368,20 @@ async fn get_correlation_titles(
368368
Ok(correlations)
369369
}
370370

371-
async fn get_dashboard_titles(
372-
key: &SessionKey,
373-
query_value: &str,
374-
) -> Result<Vec<Resource>, PrismHomeError> {
371+
async fn get_dashboard_titles(query_value: &str) -> Result<Vec<Resource>, PrismHomeError> {
375372
let dashboard_titles = DASHBOARDS
376-
.list_dashboards(key)
373+
.list_dashboards()
377374
.await
378375
.iter()
379376
.filter_map(|dashboard| {
380-
let dashboard_id = dashboard.dashboard_id.as_ref().unwrap().clone();
381-
if dashboard.name.to_lowercase().contains(query_value)
377+
let dashboard_id = *dashboard.dashboard_id.as_ref().unwrap();
378+
let dashboard_id = dashboard_id.to_string();
379+
if dashboard.title.to_lowercase().contains(query_value)
382380
|| dashboard_id.to_lowercase().contains(query_value)
383381
{
384382
Some(Resource {
385383
id: dashboard_id,
386-
name: dashboard.name.clone(),
384+
name: dashboard.title.clone(),
387385
resource_type: ResourceType::Dashboard,
388386
})
389387
} else {

0 commit comments

Comments
 (0)