1616 *
1717 */
1818
19+ use std:: collections:: HashMap ;
20+
1921use crate :: {
2022 handlers:: http:: rbac:: RBACError ,
2123 storage:: ObjectStorageError ,
@@ -68,37 +70,88 @@ pub async fn create_dashboard(
6870pub 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
104157pub 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 ) ]
149222pub 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
164239impl 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