Skip to content

Commit 2575419

Browse files
aslilacpull[bot]
authored andcommitted
fix: display health alert in DeploymentBannerView (#10193)
1 parent 50cceed commit 2575419

File tree

12 files changed

+177
-111
lines changed

12 files changed

+177
-111
lines changed

site/src/api/api.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,14 +1516,17 @@ export const getInsightsTemplate = async (
15161516
return response.data;
15171517
};
15181518

1519-
export const getHealth = () => {
1520-
return axios.get<{
1521-
healthy: boolean;
1522-
time: string;
1523-
coder_version: string;
1524-
derp: { healthy: boolean };
1525-
access_url: { healthy: boolean };
1526-
websocket: { healthy: boolean };
1527-
database: { healthy: boolean };
1528-
}>("/api/v2/debug/health");
1519+
export interface Health {
1520+
healthy: boolean;
1521+
time: string;
1522+
coder_version: string;
1523+
access_url: { healthy: boolean };
1524+
database: { healthy: boolean };
1525+
derp: { healthy: boolean };
1526+
websocket: { healthy: boolean };
1527+
}
1528+
1529+
export const getHealth = async () => {
1530+
const response = await axios.get<Health>("/api/v2/debug/health");
1531+
return response.data;
15291532
};

site/src/api/queries/deployment.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ export const deploymentDAUs = () => {
1717
export const deploymentStats = () => {
1818
return {
1919
queryKey: ["deployment", "stats"],
20-
queryFn: () => API.getDeploymentStats(),
20+
queryFn: API.getDeploymentStats,
21+
};
22+
};
23+
24+
export const health = () => {
25+
return {
26+
queryKey: ["deployment", "health"],
27+
queryFn: API.getHealth,
2128
};
2229
};

site/src/components/Dashboard/DashboardLayout.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import Box, { BoxProps } from "@mui/material/Box";
1515
import InfoOutlined from "@mui/icons-material/InfoOutlined";
1616
import Button from "@mui/material/Button";
1717
import { docs } from "utils/docs";
18-
import { HealthBanner } from "./HealthBanner";
1918

2019
export const DashboardLayout: FC = () => {
2120
const permissions = usePermissions();
@@ -29,7 +28,6 @@ export const DashboardLayout: FC = () => {
2928

3029
return (
3130
<>
32-
<HealthBanner />
3331
<ServiceBanner />
3432
{canViewDeployment && <LicenseBanner />}
3533

site/src/components/Dashboard/DeploymentBanner/DeploymentBanner.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
1+
import { type FC } from "react";
2+
import { useQuery } from "react-query";
3+
import { deploymentStats, health } from "api/queries/deployment";
14
import { usePermissions } from "hooks/usePermissions";
25
import { DeploymentBannerView } from "./DeploymentBannerView";
3-
import { useQuery } from "react-query";
4-
import { deploymentStats } from "api/queries/deployment";
6+
import { useDashboard } from "../DashboardProvider";
57

6-
export const DeploymentBanner: React.FC = () => {
8+
export const DeploymentBanner: FC = () => {
9+
const dashboard = useDashboard();
710
const permissions = usePermissions();
811
const deploymentStatsQuery = useQuery(deploymentStats());
12+
const healthQuery = useQuery({
13+
...health(),
14+
enabled: dashboard.experiments.includes("deployment_health_page"),
15+
});
916

1017
if (!permissions.viewDeploymentValues || !deploymentStatsQuery.data) {
1118
return null;
1219
}
1320

1421
return (
1522
<DeploymentBannerView
23+
health={healthQuery.data}
1624
stats={deploymentStatsQuery.data}
1725
fetchStats={() => deploymentStatsQuery.refetch()}
1826
/>

site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.stories.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { Meta, StoryObj } from "@storybook/react";
2-
import { MockDeploymentStats } from "testHelpers/entities";
2+
import {
3+
DeploymentHealthUnhealthy,
4+
MockDeploymentStats,
5+
} from "testHelpers/entities";
36
import { DeploymentBannerView } from "./DeploymentBannerView";
47

58
const meta: Meta<typeof DeploymentBannerView> = {
@@ -13,4 +16,10 @@ const meta: Meta<typeof DeploymentBannerView> = {
1316
export default meta;
1417
type Story = StoryObj<typeof DeploymentBannerView>;
1518

16-
export const Preview: Story = {};
19+
export const Example: Story = {};
20+
21+
export const WithHealthIssues: Story = {
22+
args: {
23+
health: DeploymentHealthUnhealthy,
24+
},
25+
};

site/src/components/Dashboard/DeploymentBanner/DeploymentBannerView.tsx

Lines changed: 113 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
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";
310
import prettyBytes from "pretty-bytes";
411
import BuildingIcon from "@mui/icons-material/Build";
5-
import { RocketIcon } from "components/Icons/RocketIcon";
6-
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
712
import Tooltip from "@mui/material/Tooltip";
813
import { Link as RouterLink } from "react-router-dom";
914
import Link from "@mui/material/Link";
@@ -12,13 +17,26 @@ import DownloadIcon from "@mui/icons-material/CloudDownload";
1217
import UploadIcon from "@mui/icons-material/CloudUpload";
1318
import LatencyIcon from "@mui/icons-material/SettingsEthernet";
1419
import WebTerminalIcon from "@mui/icons-material/WebAsset";
15-
import { TerminalIcon } from "components/Icons/TerminalIcon";
16-
import dayjs from "dayjs";
1720
import CollectedIcon from "@mui/icons-material/Compare";
1821
import RefreshIcon from "@mui/icons-material/Refresh";
1922
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";
2036
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";
2240

2341
export const bannerHeight = 36;
2442

@@ -49,14 +67,13 @@ const styles = {
4967
} satisfies Record<string, Interpolation<Theme>>;
5068

5169
export interface DeploymentBannerViewProps {
52-
fetchStats?: () => void;
70+
health?: Health;
5371
stats?: DeploymentStats;
72+
fetchStats?: () => void;
5473
}
5574

56-
export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
57-
stats,
58-
fetchStats,
59-
}) => {
75+
export const DeploymentBannerView: FC<DeploymentBannerViewProps> = (props) => {
76+
const { health, stats, fetchStats } = props;
6077
const theme = useTheme();
6178
const aggregatedMinutes = useMemo(() => {
6279
if (!stats) {
@@ -105,14 +122,43 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
105122
// eslint-disable-next-line react-hooks/exhaustive-deps -- We want this to periodically update!
106123
}, [timeUntilRefresh, stats]);
107124

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+
108154
return (
109155
<div
110156
css={{
111157
position: "sticky",
112158
height: bannerHeight,
113159
bottom: 0,
114160
zIndex: 1,
115-
padding: theme.spacing(0, 2),
161+
paddingRight: theme.spacing(2),
116162
backgroundColor: theme.palette.background.paper,
117163
display: "flex",
118164
alignItems: "center",
@@ -124,24 +170,51 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
124170
whiteSpace: "nowrap",
125171
}}
126172
>
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&apos;re noticing DERP proxy issues.
193+
</HealthIssue>
194+
)}
195+
{health.websocket && (
196+
<HealthIssue>
197+
We&apos;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+
)}
145218
</Tooltip>
146219
<div css={styles.group}>
147220
<div css={styles.category}>Workspaces</div>
@@ -330,3 +403,12 @@ const WorkspaceBuildValue: FC<{
330403
</Tooltip>
331404
);
332405
};
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+
};

site/src/components/Dashboard/HealthBanner.tsx

Lines changed: 0 additions & 45 deletions
This file was deleted.

site/src/components/HelpTooltip/HelpTooltip.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,7 @@ export const HelpTooltip: FC<PropsWithChildren<HelpTooltipProps>> = ({
159159
);
160160
};
161161

162-
export const HelpTooltipTitle: FC<PropsWithChildren<unknown>> = ({
163-
children,
164-
}) => {
162+
export const HelpTooltipTitle: FC<PropsWithChildren> = ({ children }) => {
165163
return <h4 css={styles.title}>{children}</h4>;
166164
};
167165

@@ -242,7 +240,7 @@ const styles = {
242240
marginBottom: theme.spacing(1),
243241
color: theme.palette.text.primary,
244242
fontSize: 14,
245-
lineHeight: "120%",
243+
lineHeight: "150%",
246244
fontWeight: 600,
247245
}),
248246

site/src/components/PopoverContainer/PopoverContainer.stories.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@ import { Meta, StoryObj } from "@storybook/react";
22
import { PopoverContainer } from "./PopoverContainer";
33
import Button from "@mui/material/Button";
44

5-
const numbers: number[] = [];
6-
for (let i = 0; i < 20; i++) {
7-
numbers.push(i + 1);
8-
}
9-
105
const meta: Meta<typeof PopoverContainer> = {
116
title: "components/PopoverContainer",
127
component: PopoverContainer,

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy