Skip to content

Commit 03b3226

Browse files
feat: dashboards APIs (#854)
feature to support various operations of dashboards APIs - POST /dashboards - to create dashboard - GET /dashboards/<user-id> - to list dashboards for a user - GET /dashboards/dashboard/<dashboard-id> - to get dashboard - PUT /dashboards/dashboard/<dashboard-id> - to update dashboard - DELETE /dashboards/dashboard/<dashboard-id> - to delete dashboard sample JSON - ` { "name": "Backend dashboard", "description": "This is a description for the dashboard", "user_id": "nikhil", "time_filter": { "to": "2024-07-09T07:10:00.000Z", "from": "2024-07-09T07:00:00.000Z" }, "refresh_interval": 60, "tiles": [ { "name": "Donut Tile", "description": "Description for the tile", "stream": "backend", "query": "SELECT level, COUNT(*) AS level_count FROM teststream GROUP BY level;", "visualization": { "visualization_type": "donut-chart", "circular_chart_config": { "name_key": "level", "value_key": "level_count" }, "size": "sm", "color_config": [{ "field_name": "level_count", "color_palette": "red"} ] } }, { "name": "Line Chart", "description": "Description for the tile", "stream": "backend", "query": "SELECT level, COUNT(*) AS level_count FROM teststream GROUP BY level;", "visualization": { "visualization_type": "line-chart", "graph_config": { "x_key": "level", "y_key": ["level_count"] }, "size": "sm", "color_config": [ { "field_name": "level_count", "color_palette": "red" } ] } } ] } ` Also added new field to maintain the order of the tiles for dashboard generate id for each tile in the dashboard
1 parent 72a29df commit 03b3226

File tree

6 files changed

+206
-111
lines changed

6 files changed

+206
-111
lines changed

server/src/handlers/http/modal/server.rs

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -156,36 +156,43 @@ impl Server {
156156

157157
// get the dashboards web scope
158158
pub fn get_dashboards_webscope() -> Scope {
159-
web::scope("/dashboards").service(
160-
web::scope("/{user_id}")
161-
.service(
159+
web::scope("/dashboards")
160+
.service(
161+
web::resource("").route(
162+
web::post()
163+
.to(dashboards::post)
164+
.authorize(Action::CreateDashboard),
165+
),
166+
)
167+
.service(
168+
web::scope("/dashboard").service(
169+
web::resource("/{dashboard_id}")
170+
.route(
171+
web::get()
172+
.to(dashboards::get)
173+
.authorize(Action::GetDashboard),
174+
)
175+
.route(
176+
web::delete()
177+
.to(dashboards::delete)
178+
.authorize(Action::DeleteDashboard),
179+
)
180+
.route(
181+
web::put()
182+
.to(dashboards::update)
183+
.authorize(Action::CreateDashboard),
184+
),
185+
),
186+
)
187+
.service(
188+
web::scope("/{user_id}").service(
162189
web::resource("").route(
163190
web::get()
164191
.to(dashboards::list)
165192
.authorize(Action::ListDashboard),
166193
),
167-
)
168-
.service(
169-
web::scope("/{dashboard_id}").service(
170-
web::resource("")
171-
.route(
172-
web::get()
173-
.to(dashboards::get)
174-
.authorize(Action::GetDashboard),
175-
)
176-
.route(
177-
web::post()
178-
.to(dashboards::post)
179-
.authorize(Action::CreateDashboard),
180-
)
181-
.route(
182-
web::delete()
183-
.to(dashboards::delete)
184-
.authorize(Action::DeleteDashboard),
185-
),
186-
),
187194
),
188-
)
195+
)
189196
}
190197

191198
// get the filters web scope

server/src/handlers/http/users/dashboards.rs

Lines changed: 55 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -20,103 +20,103 @@ use crate::{
2020
handlers::http::ingest::PostError,
2121
option::CONFIG,
2222
storage::{object_storage::dashboard_path, ObjectStorageError},
23-
users::dashboards::{Dashboard, DASHBOARDS},
23+
users::dashboards::{Dashboard, CURRENT_DASHBOARD_VERSION, DASHBOARDS},
2424
};
2525
use actix_web::{http::header::ContentType, web, HttpRequest, HttpResponse, Responder};
2626
use bytes::Bytes;
2727

28+
use chrono::Utc;
2829
use http::StatusCode;
29-
use serde_json::{Error as SerdeError, Value as JsonValue};
30+
use serde_json::Error as SerdeError;
3031

3132
pub async fn list(req: HttpRequest) -> Result<impl Responder, DashboardError> {
3233
let user_id = req
3334
.match_info()
3435
.get("user_id")
3536
.ok_or(DashboardError::Metadata("No User Id Provided"))?;
37+
let dashboards = DASHBOARDS.list_dashboards_by_user(user_id);
3638

37-
// .users/user_id/dashboards/
38-
let path = dashboard_path(user_id, "");
39-
40-
let store = CONFIG.storage().get_object_store();
41-
let dashboards = store
42-
.get_objects(
43-
Some(&path),
44-
Box::new(|file_name: String| file_name.ends_with("json")),
45-
)
46-
.await?;
47-
48-
let mut dash = vec![];
49-
for dashboard in dashboards {
50-
dash.push(serde_json::from_slice::<JsonValue>(&dashboard)?)
51-
}
52-
53-
Ok((web::Json(dash), StatusCode::OK))
39+
Ok((web::Json(dashboards), StatusCode::OK))
5440
}
5541

5642
pub async fn get(req: HttpRequest) -> Result<impl Responder, DashboardError> {
57-
let user_id = req
58-
.match_info()
59-
.get("user_id")
60-
.ok_or(DashboardError::Metadata("No User Id Provided"))?;
61-
62-
let dash_id = req
43+
let dashboard_id = req
6344
.match_info()
6445
.get("dashboard_id")
6546
.ok_or(DashboardError::Metadata("No Dashboard Id Provided"))?;
6647

67-
if let Some(dashboard) = DASHBOARDS.find(dash_id) {
48+
if let Some(dashboard) = DASHBOARDS.get_dashboard(dashboard_id) {
6849
return Ok((web::Json(dashboard), StatusCode::OK));
6950
}
7051

71-
//if dashboard is not in memory fetch from s3
72-
let dash_file_path = dashboard_path(user_id, &format!("{}.json", dash_id));
73-
let resource = CONFIG
74-
.storage()
75-
.get_object_store()
76-
.get_object(&dash_file_path)
52+
Err(DashboardError::Metadata("Dashboard does not exist"))
53+
}
54+
55+
pub async fn post(body: Bytes) -> Result<impl Responder, PostError> {
56+
let mut dashboard: Dashboard = serde_json::from_slice(&body)?;
57+
let dashboard_id = format!("{}.{}", &dashboard.user_id, Utc::now().timestamp_millis());
58+
dashboard.dashboard_id = Some(dashboard_id.clone());
59+
dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string());
60+
DASHBOARDS.update(&dashboard);
61+
for tile in dashboard.tiles.iter_mut() {
62+
tile.tile_id = Some(format!(
63+
"{}.{}",
64+
&dashboard.user_id,
65+
Utc::now().timestamp_micros()
66+
));
67+
}
68+
69+
let path = dashboard_path(&dashboard.user_id, &format!("{}.json", dashboard_id));
70+
71+
let store = CONFIG.storage().get_object_store();
72+
let dashboard_bytes = serde_json::to_vec(&dashboard)?;
73+
store
74+
.put_object(&path, Bytes::from(dashboard_bytes))
7775
.await?;
78-
let resource = serde_json::from_slice::<Dashboard>(&resource)?;
7976

80-
Ok((web::Json(resource), StatusCode::OK))
77+
Ok((web::Json(dashboard), StatusCode::OK))
8178
}
8279

83-
pub async fn post(req: HttpRequest, body: Bytes) -> Result<HttpResponse, PostError> {
84-
let user_id = req
85-
.match_info()
86-
.get("user_id")
87-
.ok_or(DashboardError::Metadata("No User Id Provided"))?;
88-
89-
let dash_id = req
80+
pub async fn update(req: HttpRequest, body: Bytes) -> Result<HttpResponse, PostError> {
81+
let dashboard_id = req
9082
.match_info()
9183
.get("dashboard_id")
9284
.ok_or(DashboardError::Metadata("No Dashboard Id Provided"))?;
85+
if DASHBOARDS.get_dashboard(dashboard_id).is_none() {
86+
return Err(PostError::DashboardError(DashboardError::Metadata(
87+
"Dashboard does not exist",
88+
)));
89+
}
90+
let mut dashboard: Dashboard = serde_json::from_slice(&body)?;
91+
dashboard.dashboard_id = Some(dashboard_id.to_string());
92+
dashboard.version = Some(CURRENT_DASHBOARD_VERSION.to_string());
93+
DASHBOARDS.update(&dashboard);
9394

94-
let dash_file_path = dashboard_path(user_id, &format!("{}.json", dash_id));
95-
96-
let dashboard = serde_json::from_slice::<Dashboard>(&body)?;
97-
DASHBOARDS.update(dashboard);
95+
let path = dashboard_path(&dashboard.user_id, &format!("{}.json", dashboard_id));
9896

9997
let store = CONFIG.storage().get_object_store();
100-
store.put_object(&dash_file_path, body).await?;
98+
let dashboard_bytes = serde_json::to_vec(&dashboard)?;
99+
store
100+
.put_object(&path, Bytes::from(dashboard_bytes))
101+
.await?;
101102

102103
Ok(HttpResponse::Ok().finish())
103104
}
104105

105106
pub async fn delete(req: HttpRequest) -> Result<HttpResponse, PostError> {
106-
let user_id = req
107-
.match_info()
108-
.get("user_id")
109-
.ok_or(DashboardError::Metadata("No User Id Provided"))?;
110-
111-
let dash_id = req
107+
let dashboard_id = req
112108
.match_info()
113109
.get("dashboard_id")
114110
.ok_or(DashboardError::Metadata("No Dashboard Id Provided"))?;
111+
let dashboard = DASHBOARDS
112+
.get_dashboard(dashboard_id)
113+
.ok_or(DashboardError::Metadata("Dashboard does not exist"))?;
115114

116-
let dash_file_path = dashboard_path(user_id, &format!("{}.json", dash_id));
117-
115+
let path = dashboard_path(&dashboard.user_id, &format!("{}.json", dashboard_id));
118116
let store = CONFIG.storage().get_object_store();
119-
store.delete_object(&dash_file_path).await?;
117+
store.delete_object(&path).await?;
118+
119+
DASHBOARDS.delete_dashboard(dashboard_id);
120120

121121
Ok(HttpResponse::Ok().finish())
122122
}

server/src/storage/localfs.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,26 @@ impl ObjectStorage for LocalFS {
350350
Ok(dirs)
351351
}
352352

353+
async fn get_all_dashboards(&self) -> Result<Vec<Bytes>, ObjectStorageError> {
354+
let mut dashboards = vec![];
355+
let users_root_path = self.root.join(USERS_ROOT_DIR);
356+
let directories = ReadDirStream::new(fs::read_dir(&users_root_path).await?);
357+
let users: Vec<DirEntry> = directories.try_collect().await?;
358+
for user in users {
359+
if !user.path().is_dir() {
360+
continue;
361+
}
362+
let dashboards_path = users_root_path.join(user.path()).join("dashboards");
363+
let directories = ReadDirStream::new(fs::read_dir(&dashboards_path).await?);
364+
let dashboards_files: Vec<DirEntry> = directories.try_collect().await?;
365+
for dashboard in dashboards_files {
366+
let file = fs::read(dashboard.path()).await?;
367+
dashboards.push(file.into());
368+
}
369+
}
370+
Ok(dashboards)
371+
}
372+
353373
async fn get_all_saved_filters(&self) -> Result<Vec<Bytes>, ObjectStorageError> {
354374
let mut filters = vec![];
355375
let users_root_path = self.root.join(USERS_ROOT_DIR);

server/src/storage/object_storage.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ pub trait ObjectStorage: Sync + 'static {
8585
async fn list_old_streams(&self) -> Result<Vec<LogStream>, ObjectStorageError>;
8686
async fn list_dirs(&self) -> Result<Vec<String>, ObjectStorageError>;
8787
async fn get_all_saved_filters(&self) -> Result<Vec<Bytes>, ObjectStorageError>;
88+
async fn get_all_dashboards(&self) -> Result<Vec<Bytes>, ObjectStorageError>;
8889
async fn list_dates(&self, stream_name: &str) -> Result<Vec<String>, ObjectStorageError>;
8990
async fn upload_file(&self, key: &str, path: &Path) -> Result<(), ObjectStorageError>;
9091
async fn delete_object(&self, path: &RelativePath) -> Result<(), ObjectStorageError>;

server/src/storage/s3.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,38 @@ impl ObjectStorage for S3 {
640640
.collect::<Vec<_>>())
641641
}
642642

643+
async fn get_all_dashboards(&self) -> Result<Vec<Bytes>, ObjectStorageError> {
644+
let mut dashboards = vec![];
645+
let users_root_path = object_store::path::Path::from(USERS_ROOT_DIR);
646+
let resp = self
647+
.client
648+
.list_with_delimiter(Some(&users_root_path))
649+
.await?;
650+
651+
let users = resp
652+
.common_prefixes
653+
.iter()
654+
.flat_map(|path| path.parts())
655+
.filter(|name| name.as_ref() != USERS_ROOT_DIR)
656+
.map(|name| name.as_ref().to_string())
657+
.collect::<Vec<_>>();
658+
for user in users {
659+
let user_dashboard_path = object_store::path::Path::from(format!(
660+
"{}/{}/{}",
661+
USERS_ROOT_DIR, user, "dashboards"
662+
));
663+
let dashboards_path = RelativePathBuf::from(&user_dashboard_path);
664+
let dashboard_bytes = self
665+
.get_objects(
666+
Some(&dashboards_path),
667+
Box::new(|file_name| file_name.ends_with(".json")),
668+
)
669+
.await?;
670+
dashboards.extend(dashboard_bytes);
671+
}
672+
Ok(dashboards)
673+
}
674+
643675
async fn get_all_saved_filters(&self) -> Result<Vec<Bytes>, ObjectStorageError> {
644676
let mut filters = vec![];
645677
let users_root_path = object_store::path::Path::from(USERS_ROOT_DIR);

0 commit comments

Comments
 (0)