Skip to content

Commit 0629a3f

Browse files
feat: add dashboard changes (#1362)
1. add below fields to dashboard creation - a. tags - list of strings b. created - created datetime c. is favorite - true/false, default false 2. ensure title is unique 3. add API to get all tags - `GET /api/v1/dashboards/list_tags` 4. is_favorite=true/false -- to set dashboard to favorite 5. rename_to=<updated title> -- to update the title of the dashboard 6. tags=<comma separated tags> -- to update tags
1 parent 5d34b02 commit 0629a3f

File tree

5 files changed

+225
-33
lines changed

5 files changed

+225
-33
lines changed

src/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ pub struct Options {
451451
help = "Object store sync threshold in seconds"
452452
)]
453453
pub object_store_sync_threshold: u64,
454-
// the oidc scope
454+
// the oidc scope
455455
#[arg(
456456
long = "oidc-scope",
457457
name = "oidc-scope",

src/handlers/http/modal/server.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,20 @@ impl Server {
300300
.authorize(Action::ListDashboard),
301301
),
302302
)
303+
.service(
304+
web::resource("/list_tags").route(
305+
web::get()
306+
.to(dashboards::list_tags)
307+
.authorize(Action::ListDashboard),
308+
),
309+
)
310+
.service(
311+
web::resource("/list_by_tag/{tag}").route(
312+
web::get()
313+
.to(dashboards::list_dashboards_by_tag)
314+
.authorize(Action::ListDashboard),
315+
),
316+
)
303317
.service(
304318
web::scope("/{dashboard_id}")
305319
.service(

src/handlers/http/oidc.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,13 @@ pub async fn login(
7777
let session_key = extract_session_key_from_req(&req).ok();
7878
let (session_key, oidc_client) = match (session_key, oidc_client) {
7979
(None, None) => return Ok(redirect_no_oauth_setup(query.redirect.clone())),
80-
(None, Some(client)) => return Ok(redirect_to_oidc(query, client, PARSEABLE.options.scope.to_string().as_str())),
80+
(None, Some(client)) => {
81+
return Ok(redirect_to_oidc(
82+
query,
83+
client,
84+
PARSEABLE.options.scope.to_string().as_str(),
85+
))
86+
}
8187
(Some(session_key), client) => (session_key, client),
8288
};
8389
// try authorize
@@ -113,7 +119,11 @@ pub async fn login(
113119
} else {
114120
Users.remove_session(&key);
115121
if let Some(oidc_client) = oidc_client {
116-
redirect_to_oidc(query, oidc_client, PARSEABLE.options.scope.to_string().as_str())
122+
redirect_to_oidc(
123+
query,
124+
oidc_client,
125+
PARSEABLE.options.scope.to_string().as_str(),
126+
)
117127
} else {
118128
redirect_to_client(query.redirect.as_str(), None)
119129
}

src/handlers/http/users/dashboards.rs

Lines changed: 96 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*
1717
*/
1818

19+
use std::collections::HashMap;
20+
1921
use crate::{
2022
handlers::http::rbac::RBACError,
2123
storage::ObjectStorageError,
@@ -68,37 +70,88 @@ pub async fn create_dashboard(
6870
pub async fn update_dashboard(
6971
req: HttpRequest,
7072
dashboard_id: Path<String>,
71-
Json(mut dashboard): Json<Dashboard>,
73+
dashboard: Option<Json<Dashboard>>,
7274
) -> Result<impl Responder, DashboardError> {
7375
let user_id = get_hash(&get_user_from_request(&req)?);
7476
let dashboard_id = validate_dashboard_id(dashboard_id.into_inner())?;
77+
let mut existing_dashboard = DASHBOARDS
78+
.get_dashboard_by_user(dashboard_id, &user_id)
79+
.await
80+
.ok_or(DashboardError::Metadata(
81+
"Dashboard does not exist or user is not authorized",
82+
))?;
7583

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"));
80-
}
84+
let query_map = web::Query::<HashMap<String, String>>::from_query(req.query_string())
85+
.map_err(|_| DashboardError::InvalidQueryParameter)?;
86+
87+
// Validate: either query params OR body, not both
88+
let has_query_params = !query_map.is_empty();
89+
let has_body_update = dashboard
90+
.as_ref()
91+
.is_some_and(|d| d.title != existing_dashboard.title || d.tiles.is_some());
92+
93+
if has_query_params && has_body_update {
94+
return Err(DashboardError::Metadata(
95+
"Cannot use both query parameters and request body for updates",
96+
));
8197
}
8298

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"));
99+
let mut final_dashboard = if has_query_params {
100+
// Apply partial updates from query parameters
101+
if let Some(is_favorite) = query_map.get("isFavorite") {
102+
existing_dashboard.is_favorite = Some(is_favorite == "true");
94103
}
95-
}
104+
if let Some(tags) = query_map.get("tags") {
105+
let parsed_tags: Vec<String> = tags
106+
.split(',')
107+
.map(|s| s.trim())
108+
.filter(|s| !s.is_empty())
109+
.map(|s| s.to_string())
110+
.collect();
111+
existing_dashboard.tags = if parsed_tags.is_empty() {
112+
None
113+
} else {
114+
Some(parsed_tags)
115+
};
116+
}
117+
if let Some(rename_to) = query_map.get("renameTo") {
118+
let trimmed = rename_to.trim();
119+
if trimmed.is_empty() {
120+
return Err(DashboardError::Metadata("Rename to cannot be empty"));
121+
}
122+
existing_dashboard.title = trimmed.to_string();
123+
}
124+
existing_dashboard
125+
} else {
126+
let dashboard = dashboard
127+
.ok_or(DashboardError::Metadata("Request body is required"))?
128+
.into_inner();
129+
if let Some(tiles) = &dashboard.tiles {
130+
if tiles.iter().any(|tile| tile.tile_id.is_nil()) {
131+
return Err(DashboardError::Metadata("Tile ID must be provided"));
132+
}
133+
134+
// Check if tile_id are unique
135+
let unique_tiles: Vec<_> = tiles
136+
.iter()
137+
.map(|tile| tile.tile_id)
138+
.collect::<std::collections::HashSet<_>>()
139+
.into_iter()
140+
.collect();
141+
142+
if unique_tiles.len() != tiles.len() {
143+
return Err(DashboardError::Metadata("Tile IDs must be unique"));
144+
}
145+
}
146+
147+
dashboard
148+
};
96149

97150
DASHBOARDS
98-
.update(&user_id, dashboard_id, &mut dashboard)
151+
.update(&user_id, dashboard_id, &mut final_dashboard)
99152
.await?;
100153

101-
Ok((web::Json(dashboard), StatusCode::OK))
154+
Ok((web::Json(final_dashboard), StatusCode::OK))
102155
}
103156

104157
pub async fn delete_dashboard(
@@ -145,6 +198,26 @@ pub async fn add_tile(
145198
Ok((web::Json(dashboard), StatusCode::OK))
146199
}
147200

201+
pub async fn list_tags() -> Result<impl Responder, DashboardError> {
202+
let tags = DASHBOARDS.list_tags().await;
203+
Ok((web::Json(tags), StatusCode::OK))
204+
}
205+
206+
pub async fn list_dashboards_by_tag(tag: Path<String>) -> Result<impl Responder, DashboardError> {
207+
let tag = tag.into_inner();
208+
if tag.is_empty() {
209+
return Err(DashboardError::Metadata("Tag cannot be empty"));
210+
}
211+
212+
let dashboards = DASHBOARDS.list_dashboards_by_tag(&tag).await;
213+
let dashboard_summaries = dashboards
214+
.iter()
215+
.map(|dashboard| dashboard.to_summary())
216+
.collect::<Vec<_>>();
217+
218+
Ok((web::Json(dashboard_summaries), StatusCode::OK))
219+
}
220+
148221
#[derive(Debug, thiserror::Error)]
149222
pub enum DashboardError {
150223
#[error("Failed to connect to storage: {0}")]
@@ -159,6 +232,8 @@ pub enum DashboardError {
159232
Custom(String),
160233
#[error("Dashboard does not exist or is not accessible")]
161234
Unauthorized,
235+
#[error("Invalid query parameter")]
236+
InvalidQueryParameter,
162237
}
163238

164239
impl actix_web::ResponseError for DashboardError {
@@ -170,6 +245,7 @@ impl actix_web::ResponseError for DashboardError {
170245
Self::UserDoesNotExist(_) => StatusCode::NOT_FOUND,
171246
Self::Custom(_) => StatusCode::INTERNAL_SERVER_ERROR,
172247
Self::Unauthorized => StatusCode::UNAUTHORIZED,
248+
Self::InvalidQueryParameter => StatusCode::BAD_REQUEST,
173249
}
174250
}
175251

0 commit comments

Comments
 (0)