fix: display health alert in DeploymentBannerView (#10193)

This commit is contained in:
Kayla Washburn
2023-10-13 10:39:20 -06:00
committed by GitHub
parent def980b973
commit cbc0c39792
12 changed files with 177 additions and 111 deletions

View File

@ -1516,14 +1516,17 @@ export const getInsightsTemplate = async (
return response.data;
};
export const getHealth = () => {
return axios.get<{
healthy: boolean;
time: string;
coder_version: string;
derp: { healthy: boolean };
access_url: { healthy: boolean };
websocket: { healthy: boolean };
database: { healthy: boolean };
}>("/api/v2/debug/health");
export interface Health {
healthy: boolean;
time: string;
coder_version: string;
access_url: { healthy: boolean };
database: { healthy: boolean };
derp: { healthy: boolean };
websocket: { healthy: boolean };
}
export const getHealth = async () => {
const response = await axios.get<Health>("/api/v2/debug/health");
return response.data;
};

View File

@ -17,6 +17,13 @@ export const deploymentDAUs = () => {
export const deploymentStats = () => {
return {
queryKey: ["deployment", "stats"],
queryFn: () => API.getDeploymentStats(),
queryFn: API.getDeploymentStats,
};
};
export const health = () => {
return {
queryKey: ["deployment", "health"],
queryFn: API.getHealth,
};
};

View File

@ -15,7 +15,6 @@ import Box, { BoxProps } from "@mui/material/Box";
import InfoOutlined from "@mui/icons-material/InfoOutlined";
import Button from "@mui/material/Button";
import { docs } from "utils/docs";
import { HealthBanner } from "./HealthBanner";
export const DashboardLayout: FC = () => {
const permissions = usePermissions();
@ -29,7 +28,6 @@ export const DashboardLayout: FC = () => {
return (
<>
<HealthBanner />
<ServiceBanner />
{canViewDeployment && <LicenseBanner />}

View File

@ -1,11 +1,18 @@
import { type FC } from "react";
import { useQuery } from "react-query";
import { deploymentStats, health } from "api/queries/deployment";
import { usePermissions } from "hooks/usePermissions";
import { DeploymentBannerView } from "./DeploymentBannerView";
import { useQuery } from "react-query";
import { deploymentStats } from "api/queries/deployment";
import { useDashboard } from "../DashboardProvider";
export const DeploymentBanner: React.FC = () => {
export const DeploymentBanner: FC = () => {
const dashboard = useDashboard();
const permissions = usePermissions();
const deploymentStatsQuery = useQuery(deploymentStats());
const healthQuery = useQuery({
...health(),
enabled: dashboard.experiments.includes("deployment_health_page"),
});
if (!permissions.viewDeploymentValues || !deploymentStatsQuery.data) {
return null;
@ -13,6 +20,7 @@ export const DeploymentBanner: React.FC = () => {
return (
<DeploymentBannerView
health={healthQuery.data}
stats={deploymentStatsQuery.data}
fetchStats={() => deploymentStatsQuery.refetch()}
/>

View File

@ -1,5 +1,8 @@
import type { Meta, StoryObj } from "@storybook/react";
import { MockDeploymentStats } from "testHelpers/entities";
import {
DeploymentHealthUnhealthy,
MockDeploymentStats,
} from "testHelpers/entities";
import { DeploymentBannerView } from "./DeploymentBannerView";
const meta: Meta<typeof DeploymentBannerView> = {
@ -13,4 +16,10 @@ const meta: Meta<typeof DeploymentBannerView> = {
export default meta;
type Story = StoryObj<typeof DeploymentBannerView>;
export const Preview: Story = {};
export const Example: Story = {};
export const WithHealthIssues: Story = {
args: {
health: DeploymentHealthUnhealthy,
},
};

View File

@ -1,9 +1,14 @@
import { DeploymentStats, WorkspaceStatus } from "api/typesGenerated";
import { FC, useMemo, useEffect, useState } from "react";
import type { Health } from "api/api";
import type { DeploymentStats, WorkspaceStatus } from "api/typesGenerated";
import {
type FC,
useMemo,
useEffect,
useState,
PropsWithChildren,
} from "react";
import prettyBytes from "pretty-bytes";
import BuildingIcon from "@mui/icons-material/Build";
import { RocketIcon } from "components/Icons/RocketIcon";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import Tooltip from "@mui/material/Tooltip";
import { Link as RouterLink } from "react-router-dom";
import Link from "@mui/material/Link";
@ -12,13 +17,26 @@ import DownloadIcon from "@mui/icons-material/CloudDownload";
import UploadIcon from "@mui/icons-material/CloudUpload";
import LatencyIcon from "@mui/icons-material/SettingsEthernet";
import WebTerminalIcon from "@mui/icons-material/WebAsset";
import { TerminalIcon } from "components/Icons/TerminalIcon";
import dayjs from "dayjs";
import CollectedIcon from "@mui/icons-material/Compare";
import RefreshIcon from "@mui/icons-material/Refresh";
import Button from "@mui/material/Button";
import { css as className } from "@emotion/css";
import {
css,
type CSSObject,
type Theme,
type Interpolation,
useTheme,
} from "@emotion/react";
import dayjs from "dayjs";
import { TerminalIcon } from "components/Icons/TerminalIcon";
import { RocketIcon } from "components/Icons/RocketIcon";
import ErrorIcon from "@mui/icons-material/ErrorOutline";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
import { getDisplayWorkspaceStatus } from "utils/workspace";
import { css, type Theme, type Interpolation, useTheme } from "@emotion/react";
import { colors } from "theme/colors";
import { HelpTooltipTitle } from "components/HelpTooltip/HelpTooltip";
import { Stack } from "components/Stack/Stack";
export const bannerHeight = 36;
@ -49,14 +67,13 @@ const styles = {
} satisfies Record<string, Interpolation<Theme>>;
export interface DeploymentBannerViewProps {
fetchStats?: () => void;
health?: Health;
stats?: DeploymentStats;
fetchStats?: () => void;
}
export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
stats,
fetchStats,
}) => {
export const DeploymentBannerView: FC<DeploymentBannerViewProps> = (props) => {
const { health, stats, fetchStats } = props;
const theme = useTheme();
const aggregatedMinutes = useMemo(() => {
if (!stats) {
@ -105,6 +122,35 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps -- We want this to periodically update!
}, [timeUntilRefresh, stats]);
const unhealthy = health && !health.healthy;
const statusBadgeStyle = css`
display: flex;
align-items: center;
justify-content: center;
background-color: ${unhealthy ? colors.red[10] : undefined};
padding: ${theme.spacing(0, 1.5)};
height: ${bannerHeight}px;
color: #fff;
& svg {
width: 16px;
height: 16px;
}
`;
const statusSummaryStyle = className`
${theme.typography.body2 as CSSObject}
margin: ${theme.spacing(0, 0, 0.5, 1.5)};
width: ${theme.spacing(50)};
padding: ${theme.spacing(2)};
color: ${theme.palette.text.primary};
background-color: ${theme.palette.background.paper};
border: 1px solid ${theme.palette.divider};
pointer-events: none;
`;
return (
<div
css={{
@ -112,7 +158,7 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
height: bannerHeight,
bottom: 0,
zIndex: 1,
padding: theme.spacing(0, 2),
paddingRight: theme.spacing(2),
backgroundColor: theme.palette.background.paper,
display: "flex",
alignItems: "center",
@ -124,24 +170,51 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
whiteSpace: "nowrap",
}}
>
<Tooltip title="Status of your Coder deployment. Only visible for admins!">
<div
css={css`
display: flex;
align-items: center;
& svg {
width: 16px;
height: 16px;
}
${theme.breakpoints.down("lg")} {
display: none;
}
`}
>
<RocketIcon />
</div>
<Tooltip
classes={{ tooltip: statusSummaryStyle }}
title={
unhealthy ? (
<>
<HelpTooltipTitle>
We have detected problems with your Coder deployment.
</HelpTooltipTitle>
<Stack spacing={1}>
{health.access_url && (
<HealthIssue>
Your access URL may be configured incorrectly.
</HealthIssue>
)}
{health.database && (
<HealthIssue>Your database is unhealthy.</HealthIssue>
)}
{health.derp && (
<HealthIssue>
We&apos;re noticing DERP proxy issues.
</HealthIssue>
)}
{health.websocket && (
<HealthIssue>
We&apos;re noticing websocket issues.
</HealthIssue>
)}
</Stack>
</>
) : (
<>Status of your Coder deployment. Only visible for admins!</>
)
}
open={process.env.STORYBOOK === "true" ? true : undefined}
css={{ marginRight: theme.spacing(-2) }}
>
{unhealthy ? (
<Link component={RouterLink} to="/health" css={statusBadgeStyle}>
<ErrorIcon />
</Link>
) : (
<div css={statusBadgeStyle}>
<RocketIcon />
</div>
)}
</Tooltip>
<div css={styles.group}>
<div css={styles.category}>Workspaces</div>
@ -330,3 +403,12 @@ const WorkspaceBuildValue: FC<{
</Tooltip>
);
};
const HealthIssue: FC<PropsWithChildren> = ({ children }) => {
return (
<Stack direction="row" spacing={1}>
<ErrorIcon fontSize="small" htmlColor={colors.red[10]} />
{children}
</Stack>
);
};

View File

@ -1,45 +0,0 @@
import { Alert } from "components/Alert/Alert";
import { Link as RouterLink } from "react-router-dom";
import Link from "@mui/material/Link";
import { colors } from "theme/colors";
import { useQuery } from "react-query";
import { getHealth } from "api/api";
import { useDashboard } from "./DashboardProvider";
export const HealthBanner = () => {
const { data: healthStatus } = useQuery({
queryKey: ["health"],
queryFn: () => getHealth(),
});
const dashboard = useDashboard();
const hasHealthIssues = healthStatus && !healthStatus.data.healthy;
if (
dashboard.experiments.includes("deployment_health_page") &&
hasHealthIssues
) {
return (
<Alert
severity="error"
variant="filled"
sx={{
border: 0,
borderRadius: 0,
backgroundColor: colors.red[10],
}}
>
We have detected problems with your Coder deployment. Please{" "}
<Link
component={RouterLink}
to="/health"
sx={{ fontWeight: 600, color: "inherit" }}
>
inspect the health status
</Link>
.
</Alert>
);
}
return null;
};

View File

@ -159,9 +159,7 @@ export const HelpTooltip: FC<PropsWithChildren<HelpTooltipProps>> = ({
);
};
export const HelpTooltipTitle: FC<PropsWithChildren<unknown>> = ({
children,
}) => {
export const HelpTooltipTitle: FC<PropsWithChildren> = ({ children }) => {
return <h4 css={styles.title}>{children}</h4>;
};
@ -242,7 +240,7 @@ const styles = {
marginBottom: theme.spacing(1),
color: theme.palette.text.primary,
fontSize: 14,
lineHeight: "120%",
lineHeight: "150%",
fontWeight: 600,
}),

View File

@ -2,11 +2,6 @@ import { Meta, StoryObj } from "@storybook/react";
import { PopoverContainer } from "./PopoverContainer";
import Button from "@mui/material/Button";
const numbers: number[] = [];
for (let i = 0; i < 20; i++) {
numbers.push(i + 1);
}
const meta: Meta<typeof PopoverContainer> = {
title: "components/PopoverContainer",
component: PopoverContainer,

View File

@ -1,11 +1,11 @@
import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout";
import { FC } from "react";
import { type FC } from "react";
import { Helmet } from "react-helmet-async";
import { pageTitle } from "utils/page";
import { GeneralSettingsPageView } from "./GeneralSettingsPageView";
import { useQuery } from "react-query";
import { pageTitle } from "utils/page";
import { deploymentDAUs } from "api/queries/deployment";
import { entitlements } from "api/queries/entitlements";
import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout";
import { GeneralSettingsPageView } from "./GeneralSettingsPageView";
const GeneralSettingsPage: FC = () => {
const { deploymentValues } = useDeploySettings();

View File

@ -42,7 +42,7 @@ export default function HealthPage() {
</Helmet>
{healthStatus ? (
<HealthPageView healthStatus={healthStatus.data} tab={tab} />
<HealthPageView healthStatus={healthStatus} tab={tab} />
) : (
<Loader />
)}
@ -54,7 +54,7 @@ export function HealthPageView({
healthStatus,
tab,
}: {
healthStatus: Awaited<ReturnType<typeof getHealth>>["data"];
healthStatus: Awaited<ReturnType<typeof getHealth>>;
tab: ReturnType<typeof useTab>;
}) {
const styles = useStyles();

View File

@ -1,7 +1,8 @@
import {
withDefaultFeatures,
GetLicensesResponse,
DeploymentConfig,
type GetLicensesResponse,
type DeploymentConfig,
type Health,
} from "api/api";
import { FieldError } from "api/errors";
import { everyOneGroup } from "utils/groups";
@ -2752,3 +2753,13 @@ export const MockListeningPortsResponse: TypesGen.WorkspaceAgentListeningPortsRe
{ process_name: "", network: "", port: 8081 },
],
};
export const DeploymentHealthUnhealthy: Health = {
healthy: false,
time: "2023-10-12T23:15:00.000000000Z",
coder_version: "v2.3.0-devel+8cca4915a",
access_url: { healthy: false },
database: { healthy: false },
derp: { healthy: false },
websocket: { healthy: false },
};