1
- import { DeploymentStats , WorkspaceStatus } from "api/typesGenerated" ;
2
- import { FC , useMemo , useEffect , useState } from "react" ;
1
+ import type { Health } from "api/api" ;
2
+ import type { DeploymentStats , WorkspaceStatus } from "api/typesGenerated" ;
3
+ import {
4
+ type FC ,
5
+ useMemo ,
6
+ useEffect ,
7
+ useState ,
8
+ PropsWithChildren ,
9
+ } from "react" ;
3
10
import prettyBytes from "pretty-bytes" ;
4
11
import BuildingIcon from "@mui/icons-material/Build" ;
5
- import { RocketIcon } from "components/Icons/RocketIcon" ;
6
- import { MONOSPACE_FONT_FAMILY } from "theme/constants" ;
7
12
import Tooltip from "@mui/material/Tooltip" ;
8
13
import { Link as RouterLink } from "react-router-dom" ;
9
14
import Link from "@mui/material/Link" ;
@@ -12,13 +17,26 @@ import DownloadIcon from "@mui/icons-material/CloudDownload";
12
17
import UploadIcon from "@mui/icons-material/CloudUpload" ;
13
18
import LatencyIcon from "@mui/icons-material/SettingsEthernet" ;
14
19
import WebTerminalIcon from "@mui/icons-material/WebAsset" ;
15
- import { TerminalIcon } from "components/Icons/TerminalIcon" ;
16
- import dayjs from "dayjs" ;
17
20
import CollectedIcon from "@mui/icons-material/Compare" ;
18
21
import RefreshIcon from "@mui/icons-material/Refresh" ;
19
22
import Button from "@mui/material/Button" ;
23
+ import { css as className } from "@emotion/css" ;
24
+ import {
25
+ css ,
26
+ type CSSObject ,
27
+ type Theme ,
28
+ type Interpolation ,
29
+ useTheme ,
30
+ } from "@emotion/react" ;
31
+ import dayjs from "dayjs" ;
32
+ import { TerminalIcon } from "components/Icons/TerminalIcon" ;
33
+ import { RocketIcon } from "components/Icons/RocketIcon" ;
34
+ import ErrorIcon from "@mui/icons-material/ErrorOutline" ;
35
+ import { MONOSPACE_FONT_FAMILY } from "theme/constants" ;
20
36
import { getDisplayWorkspaceStatus } from "utils/workspace" ;
21
- import { css , type Theme , type Interpolation , useTheme } from "@emotion/react" ;
37
+ import { colors } from "theme/colors" ;
38
+ import { HelpTooltipTitle } from "components/HelpTooltip/HelpTooltip" ;
39
+ import { Stack } from "components/Stack/Stack" ;
22
40
23
41
export const bannerHeight = 36 ;
24
42
@@ -49,14 +67,13 @@ const styles = {
49
67
} satisfies Record < string , Interpolation < Theme > > ;
50
68
51
69
export interface DeploymentBannerViewProps {
52
- fetchStats ?: ( ) => void ;
70
+ health ?: Health ;
53
71
stats ?: DeploymentStats ;
72
+ fetchStats ?: ( ) => void ;
54
73
}
55
74
56
- export const DeploymentBannerView : FC < DeploymentBannerViewProps > = ( {
57
- stats,
58
- fetchStats,
59
- } ) => {
75
+ export const DeploymentBannerView : FC < DeploymentBannerViewProps > = ( props ) => {
76
+ const { health, stats, fetchStats } = props ;
60
77
const theme = useTheme ( ) ;
61
78
const aggregatedMinutes = useMemo ( ( ) => {
62
79
if ( ! stats ) {
@@ -105,14 +122,43 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
105
122
// eslint-disable-next-line react-hooks/exhaustive-deps -- We want this to periodically update!
106
123
} , [ timeUntilRefresh , stats ] ) ;
107
124
125
+ const unhealthy = health && ! health . healthy ;
126
+
127
+ const statusBadgeStyle = css `
128
+ display : flex;
129
+ align-items : center;
130
+ justify-content : center;
131
+ background-color : ${ unhealthy ? colors . red [ 10 ] : undefined } ;
132
+ padding : ${ theme . spacing ( 0 , 1.5 ) } ;
133
+ height : ${ bannerHeight } px;
134
+ color : # fff ;
135
+
136
+ & svg {
137
+ width : 16px ;
138
+ height : 16px ;
139
+ }
140
+ ` ;
141
+
142
+ const statusSummaryStyle = className `
143
+ ${ theme . typography . body2 as CSSObject }
144
+
145
+ margin : ${ theme . spacing ( 0 , 0 , 0.5 , 1.5 ) } ;
146
+ width : ${ theme . spacing ( 50 ) } ;
147
+ padding : ${ theme . spacing ( 2 ) } ;
148
+ color : ${ theme . palette . text . primary } ;
149
+ background-color : ${ theme . palette . background . paper } ;
150
+ border : 1px solid ${ theme . palette . divider } ;
151
+ pointer-events : none;
152
+ ` ;
153
+
108
154
return (
109
155
< div
110
156
css = { {
111
157
position : "sticky" ,
112
158
height : bannerHeight ,
113
159
bottom : 0 ,
114
160
zIndex : 1 ,
115
- padding : theme . spacing ( 0 , 2 ) ,
161
+ paddingRight : theme . spacing ( 2 ) ,
116
162
backgroundColor : theme . palette . background . paper ,
117
163
display : "flex" ,
118
164
alignItems : "center" ,
@@ -124,24 +170,51 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
124
170
whiteSpace : "nowrap" ,
125
171
} }
126
172
>
127
- < Tooltip title = "Status of your Coder deployment. Only visible for admins!" >
128
- < div
129
- css = { css `
130
- display : flex;
131
- align-items : center;
132
-
133
- & svg {
134
- width : 16px ;
135
- height : 16px ;
136
- }
137
-
138
- ${ theme . breakpoints . down ( "lg" ) } {
139
- display : none;
140
- }
141
- ` }
142
- >
143
- < RocketIcon />
144
- </ div >
173
+ < Tooltip
174
+ classes = { { tooltip : statusSummaryStyle } }
175
+ title = {
176
+ unhealthy ? (
177
+ < >
178
+ < HelpTooltipTitle >
179
+ We have detected problems with your Coder deployment.
180
+ </ HelpTooltipTitle >
181
+ < Stack spacing = { 1 } >
182
+ { health . access_url && (
183
+ < HealthIssue >
184
+ Your access URL may be configured incorrectly.
185
+ </ HealthIssue >
186
+ ) }
187
+ { health . database && (
188
+ < HealthIssue > Your database is unhealthy.</ HealthIssue >
189
+ ) }
190
+ { health . derp && (
191
+ < HealthIssue >
192
+ We're noticing DERP proxy issues.
193
+ </ HealthIssue >
194
+ ) }
195
+ { health . websocket && (
196
+ < HealthIssue >
197
+ We're noticing websocket issues.
198
+ </ HealthIssue >
199
+ ) }
200
+ </ Stack >
201
+ </ >
202
+ ) : (
203
+ < > Status of your Coder deployment. Only visible for admins!</ >
204
+ )
205
+ }
206
+ open = { process . env . STORYBOOK === "true" ? true : undefined }
207
+ css = { { marginRight : theme . spacing ( - 2 ) } }
208
+ >
209
+ { unhealthy ? (
210
+ < Link component = { RouterLink } to = "/health" css = { statusBadgeStyle } >
211
+ < ErrorIcon />
212
+ </ Link >
213
+ ) : (
214
+ < div css = { statusBadgeStyle } >
215
+ < RocketIcon />
216
+ </ div >
217
+ ) }
145
218
</ Tooltip >
146
219
< div css = { styles . group } >
147
220
< div css = { styles . category } > Workspaces</ div >
@@ -330,3 +403,12 @@ const WorkspaceBuildValue: FC<{
330
403
</ Tooltip >
331
404
) ;
332
405
} ;
406
+
407
+ const HealthIssue : FC < PropsWithChildren > = ( { children } ) => {
408
+ return (
409
+ < Stack direction = "row" spacing = { 1 } >
410
+ < ErrorIcon fontSize = "small" htmlColor = { colors . red [ 10 ] } />
411
+ { children }
412
+ </ Stack >
413
+ ) ;
414
+ } ;
0 commit comments