@@ -9,20 +9,30 @@ import { useIsWorkspaceActionsEnabled } from "components/Dashboard/DashboardProv
9
9
import formatDistanceToNow from "date-fns/formatDistanceToNow" ;
10
10
import { Pill } from "components/Pill/Pill" ;
11
11
import InfoOutlined from "@mui/icons-material/InfoOutlined" ;
12
- import ErrorOutline from "@mui/icons-material/ErrorOutline" ;
12
+ import {
13
+ Popover ,
14
+ PopoverContent ,
15
+ PopoverTrigger ,
16
+ } from "components/Popover/Popover" ;
17
+ import { Interpolation , Theme , useTheme } from "@emotion/react" ;
18
+ import Button , { ButtonProps } from "@mui/material/Button" ;
19
+ import { ThemeRole } from "theme/experimental" ;
20
+ import WarningRounded from "@mui/icons-material/WarningRounded" ;
13
21
14
22
type Notification = {
15
23
title : string ;
16
24
severity : AlertProps [ "severity" ] ;
17
25
detail ?: ReactNode ;
18
- actions ?: { label : string ; onClick : ( ) => void } [ ] ;
26
+ actions ?: ReactNode ;
19
27
} ;
20
28
21
29
type WorkspaceNotificationsProps = {
22
30
workspace : Workspace ;
23
31
template : Template ;
24
32
permissions : WorkspacePermissions ;
25
33
onRestartWorkspace : ( ) => void ;
34
+ onUpdateWorkspace : ( ) => void ;
35
+ onActivateWorkspace : ( ) => void ;
26
36
latestVersion ?: TemplateVersion ;
27
37
} ;
28
38
@@ -35,6 +45,8 @@ export const WorkspaceNotifications: FC<WorkspaceNotificationsProps> = (
35
45
latestVersion,
36
46
permissions,
37
47
onRestartWorkspace,
48
+ onUpdateWorkspace,
49
+ onActivateWorkspace,
38
50
} = props ;
39
51
const notifications : Notification [ ] = [ ] ;
40
52
@@ -51,18 +63,26 @@ export const WorkspaceNotifications: FC<WorkspaceNotificationsProps> = (
51
63
const requiresManualUpdate = updateRequired && autoStartFailing ;
52
64
53
65
if ( workspace . outdated && latestVersion ) {
66
+ const actions = (
67
+ < NotificationActionButton onClick = { onUpdateWorkspace } >
68
+ Update
69
+ </ NotificationActionButton >
70
+ ) ;
54
71
if ( requiresManualUpdate ) {
55
72
notifications . push ( {
56
73
title : "Autostart has been disabled for your workspace." ,
57
74
severity : "warning" ,
58
75
detail :
59
76
"Autostart is unable to automatically update your workspace. Manually update your workspace to reenable Autostart." ,
77
+
78
+ actions,
60
79
} ) ;
61
80
} else {
62
81
notifications . push ( {
63
82
title : "An update is available for your workspace" ,
64
83
severity : "info" ,
65
84
detail : latestVersion . message ,
85
+ actions,
66
86
} ) ;
67
87
}
68
88
}
@@ -84,14 +104,11 @@ export const WorkspaceNotifications: FC<WorkspaceNotificationsProps> = (
84
104
.
85
105
</ >
86
106
) ,
87
- actions : permissions . updateWorkspace
88
- ? [
89
- {
90
- label : "Restart" ,
91
- onClick : onRestartWorkspace ,
92
- } ,
93
- ]
94
- : undefined ,
107
+ actions : permissions . updateWorkspace ? (
108
+ < NotificationActionButton onClick = { onRestartWorkspace } >
109
+ Restart
110
+ </ NotificationActionButton >
111
+ ) : undefined ,
95
112
} ) ;
96
113
}
97
114
@@ -107,7 +124,13 @@ export const WorkspaceNotifications: FC<WorkspaceNotificationsProps> = (
107
124
...( timestamp ? { hour : "numeric" , minute : "numeric" } : { } ) ,
108
125
} ) ;
109
126
} ;
127
+ const actions = (
128
+ < NotificationActionButton onClick = { onActivateWorkspace } >
129
+ Activate
130
+ </ NotificationActionButton >
131
+ ) ;
110
132
notifications . push ( {
133
+ actions,
111
134
title : "Workspace is dormant" ,
112
135
severity : "warning" ,
113
136
detail : workspace . deleting_at ? (
@@ -197,24 +220,124 @@ export const WorkspaceNotifications: FC<WorkspaceNotificationsProps> = (
197
220
} ) ;
198
221
}
199
222
223
+ const infoNotifications = notifications . filter ( ( n ) => n . severity === "info" ) ;
224
+ const warningNotifications = notifications . filter (
225
+ ( n ) => n . severity === "warning" ,
226
+ ) ;
227
+
200
228
return (
201
229
< div
202
230
css = { {
203
231
display : "flex" ,
204
232
alignItems : "center" ,
205
- gap : 8 ,
233
+ gap : 12 ,
206
234
position : "fixed" ,
207
235
bottom : 48 ,
208
236
right : 48 ,
209
237
zIndex : 10 ,
210
238
} }
211
239
>
212
- < Pill type = "info" icon = { < InfoOutlined /> } >
213
- 2
214
- </ Pill >
215
- < Pill type = "warning" icon = { < ErrorOutline /> } >
216
- 4
217
- </ Pill >
240
+ { infoNotifications . length > 0 && (
241
+ < NotificationPill
242
+ notifications = { infoNotifications }
243
+ type = "info"
244
+ icon = { < InfoOutlined /> }
245
+ />
246
+ ) }
247
+
248
+ { warningNotifications . length > 0 && (
249
+ < NotificationPill
250
+ notifications = { warningNotifications }
251
+ type = "warning"
252
+ icon = { < WarningRounded /> }
253
+ />
254
+ ) }
218
255
</ div >
219
256
) ;
220
257
} ;
258
+
259
+ type NotificationPillProps = {
260
+ notifications : Notification [ ] ;
261
+ type : ThemeRole ;
262
+ icon : ReactNode ;
263
+ } ;
264
+
265
+ const NotificationPill : FC < NotificationPillProps > = ( props ) => {
266
+ const { notifications, type, icon } = props ;
267
+ const theme = useTheme ( ) ;
268
+
269
+ return (
270
+ < Popover mode = "hover" >
271
+ < PopoverTrigger >
272
+ < div css = { [ styles . pillContainer ] } >
273
+ < Pill type = { type } icon = { icon } >
274
+ { notifications . length }
275
+ </ Pill >
276
+ </ div >
277
+ </ PopoverTrigger >
278
+ < PopoverContent
279
+ transformOrigin = { {
280
+ horizontal : "right" ,
281
+ vertical : "bottom" ,
282
+ } }
283
+ anchorOrigin = { { horizontal : "right" , vertical : "top" } }
284
+ css = { {
285
+ "& .MuiPaper-root" : {
286
+ borderColor : theme . experimental . roles [ type ] . outline ,
287
+ maxWidth : 400 ,
288
+ } ,
289
+ } }
290
+ >
291
+ { notifications . map ( ( n ) => (
292
+ < NotificationItem notification = { n } key = { n . title } />
293
+ ) ) }
294
+ </ PopoverContent >
295
+ </ Popover >
296
+ ) ;
297
+ } ;
298
+
299
+ const NotificationItem : FC < { notification : Notification } > = ( props ) => {
300
+ const { notification } = props ;
301
+ const theme = useTheme ( ) ;
302
+
303
+ return (
304
+ < article css = { { padding : 16 } } >
305
+ < h4 css = { { margin : 0 , fontWeight : 500 } } > { notification . title } </ h4 >
306
+ { notification . detail && (
307
+ < p
308
+ css = { {
309
+ margin : 0 ,
310
+ color : theme . palette . text . secondary ,
311
+ lineHeight : 1.6 ,
312
+ } }
313
+ >
314
+ { notification . detail }
315
+ </ p >
316
+ ) }
317
+ < div css = { { marginTop : 8 } } > { notification . actions } </ div >
318
+ </ article >
319
+ ) ;
320
+ } ;
321
+
322
+ const NotificationActionButton : FC < ButtonProps > = ( props ) => {
323
+ return (
324
+ < Button
325
+ variant = "text"
326
+ css = { {
327
+ textDecoration : "underline" ,
328
+ padding : 0 ,
329
+ height : "auto" ,
330
+ minWidth : "auto" ,
331
+ "&:hover" : { background : "none" , textDecoration : "underline" } ,
332
+ } }
333
+ { ...props }
334
+ />
335
+ ) ;
336
+ } ;
337
+
338
+ const styles = {
339
+ // Adds some spacing from the popover content
340
+ pillContainer : {
341
+ paddingTop : 8 ,
342
+ } ,
343
+ } satisfies Record < string , Interpolation < Theme > > ;
0 commit comments