mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
fix: display health alert in DeploymentBannerView
(#10193)
This commit is contained in:
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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 />}
|
||||
|
||||
|
@ -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()}
|
||||
/>
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -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're noticing DERP proxy issues.
|
||||
</HealthIssue>
|
||||
)}
|
||||
{health.websocket && (
|
||||
<HealthIssue>
|
||||
We'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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
@ -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,
|
||||
}),
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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 },
|
||||
};
|
||||
|
Reference in New Issue
Block a user