16
16
*
17
17
*/
18
18
19
+ use std:: collections:: HashMap ;
20
+
19
21
use crate :: {
20
22
handlers:: http:: rbac:: RBACError ,
21
23
storage:: ObjectStorageError ,
@@ -68,37 +70,88 @@ pub async fn create_dashboard(
68
70
pub async fn update_dashboard (
69
71
req : HttpRequest ,
70
72
dashboard_id : Path < String > ,
71
- Json ( mut dashboard) : Json < Dashboard > ,
73
+ dashboard : Option < Json < Dashboard > > ,
72
74
) -> Result < impl Responder , DashboardError > {
73
75
let user_id = get_hash ( & get_user_from_request ( & req) ?) ;
74
76
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
+ ) ) ?;
75
83
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
+ ) ) ;
81
97
}
82
98
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" ) ;
94
103
}
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
+ } ;
96
149
97
150
DASHBOARDS
98
- . update ( & user_id, dashboard_id, & mut dashboard )
151
+ . update ( & user_id, dashboard_id, & mut final_dashboard )
99
152
. await ?;
100
153
101
- Ok ( ( web:: Json ( dashboard ) , StatusCode :: OK ) )
154
+ Ok ( ( web:: Json ( final_dashboard ) , StatusCode :: OK ) )
102
155
}
103
156
104
157
pub async fn delete_dashboard (
@@ -145,6 +198,26 @@ pub async fn add_tile(
145
198
Ok ( ( web:: Json ( dashboard) , StatusCode :: OK ) )
146
199
}
147
200
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
+
148
221
#[ derive( Debug , thiserror:: Error ) ]
149
222
pub enum DashboardError {
150
223
#[ error( "Failed to connect to storage: {0}" ) ]
@@ -159,6 +232,8 @@ pub enum DashboardError {
159
232
Custom ( String ) ,
160
233
#[ error( "Dashboard does not exist or is not accessible" ) ]
161
234
Unauthorized ,
235
+ #[ error( "Invalid query parameter" ) ]
236
+ InvalidQueryParameter ,
162
237
}
163
238
164
239
impl actix_web:: ResponseError for DashboardError {
@@ -170,6 +245,7 @@ impl actix_web::ResponseError for DashboardError {
170
245
Self :: UserDoesNotExist ( _) => StatusCode :: NOT_FOUND ,
171
246
Self :: Custom ( _) => StatusCode :: INTERNAL_SERVER_ERROR ,
172
247
Self :: Unauthorized => StatusCode :: UNAUTHORIZED ,
248
+ Self :: InvalidQueryParameter => StatusCode :: BAD_REQUEST ,
173
249
}
174
250
}
175
251
0 commit comments