Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit cbc0c39

Browse files
authored
fix: display health alert in DeploymentBannerView (#10193)
1 parent def980b commit cbc0c39

File tree

12 files changed

+177
-111
lines changed

12 files changed

+177
-111
lines changed

site/src/api/api.ts

+13-10
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

+8-1
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

-2
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

+11-3
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

+11-2
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

+113-31
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

-45
This file was deleted.

site/src/components/HelpTooltip/HelpTooltip.tsx

+2-4
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

-5
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)