mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
fix: show an error when a user doesn't have permission to view the health page (#16580)
This commit is contained in:
@@ -7,6 +7,7 @@ import IconButton from "@mui/material/IconButton";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { health, refreshHealth } from "api/queries/debug";
|
||||
import type { HealthSeverity } from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { type ClassName, useClassName } from "hooks/useClassName";
|
||||
import kebabCase from "lodash/fp/kebabCase";
|
||||
@@ -22,7 +23,11 @@ import { HealthIcon } from "./Content";
|
||||
export const HealthLayout: FC = () => {
|
||||
const theme = useTheme();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: healthStatus } = useQuery({
|
||||
const {
|
||||
data: healthStatus,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
...health(),
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
@@ -42,161 +47,171 @@ export const HealthLayout: FC = () => {
|
||||
const link = useClassName(classNames.link, []);
|
||||
const activeLink = useClassName(classNames.activeLink, []);
|
||||
|
||||
if (isLoading || !healthStatus) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ErrorAlert error={error} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle("Health")}</title>
|
||||
</Helmet>
|
||||
|
||||
{healthStatus ? (
|
||||
<DashboardFullPage>
|
||||
<DashboardFullPage>
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexBasis: 0,
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexBasis: 0,
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
width: 256,
|
||||
flexShrink: 0,
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
width: 256,
|
||||
flexShrink: 0,
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
fontSize: 14,
|
||||
padding: 24,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
padding: 24,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<HealthIcon size={32} severity={healthStatus.severity} />
|
||||
<div>
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<HealthIcon size={32} severity={healthStatus.severity} />
|
||||
|
||||
<Tooltip title="Refresh health checks">
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={isRefreshing}
|
||||
data-testid="healthcheck-refresh-button"
|
||||
onClick={() => {
|
||||
forceRefresh();
|
||||
}}
|
||||
>
|
||||
{isRefreshing ? (
|
||||
<CircularProgress size={16} />
|
||||
) : (
|
||||
<ReplayIcon css={{ width: 20, height: 20 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div css={{ fontWeight: 500, marginTop: 16 }}>
|
||||
{healthStatus.healthy ? "Healthy" : "Unhealthy"}
|
||||
</div>
|
||||
<div
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "150%",
|
||||
}}
|
||||
>
|
||||
{healthStatus.healthy
|
||||
? Object.keys(visibleSections).some((key) => {
|
||||
const section =
|
||||
healthStatus[key as keyof typeof visibleSections];
|
||||
return (
|
||||
section.warnings && section.warnings.length > 0
|
||||
);
|
||||
})
|
||||
? "All systems operational, but performance might be degraded"
|
||||
: "All systems operational"
|
||||
: "Some issues have been detected"}
|
||||
</div>
|
||||
<Tooltip title="Refresh health checks">
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={isRefreshing}
|
||||
data-testid="healthcheck-refresh-button"
|
||||
onClick={() => {
|
||||
forceRefresh();
|
||||
}}
|
||||
>
|
||||
{isRefreshing ? (
|
||||
<CircularProgress size={16} />
|
||||
) : (
|
||||
<ReplayIcon css={{ width: 20, height: 20 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div css={{ display: "flex", flexDirection: "column" }}>
|
||||
<span css={{ fontWeight: 500 }}>Last check</span>
|
||||
<span
|
||||
data-chromatic="ignore"
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "150%",
|
||||
}}
|
||||
>
|
||||
{createDayString(healthStatus.time)}
|
||||
</span>
|
||||
<div css={{ fontWeight: 500, marginTop: 16 }}>
|
||||
{healthStatus.healthy ? "Healthy" : "Unhealthy"}
|
||||
</div>
|
||||
|
||||
<div css={{ display: "flex", flexDirection: "column" }}>
|
||||
<span css={{ fontWeight: 500 }}>Version</span>
|
||||
<span
|
||||
data-chromatic="ignore"
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "150%",
|
||||
}}
|
||||
>
|
||||
{healthStatus.coder_version}
|
||||
</span>
|
||||
<div
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "150%",
|
||||
}}
|
||||
>
|
||||
{healthStatus.healthy
|
||||
? Object.keys(visibleSections).some((key) => {
|
||||
const section =
|
||||
healthStatus[key as keyof typeof visibleSections];
|
||||
return section.warnings && section.warnings.length > 0;
|
||||
})
|
||||
? "All systems operational, but performance might be degraded"
|
||||
: "All systems operational"
|
||||
: "Some issues have been detected"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav css={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
{Object.entries(visibleSections)
|
||||
.sort()
|
||||
.map(([key, label]) => {
|
||||
const healthSection =
|
||||
healthStatus[key as keyof typeof visibleSections];
|
||||
<div css={{ display: "flex", flexDirection: "column" }}>
|
||||
<span css={{ fontWeight: 500 }}>Last check</span>
|
||||
<span
|
||||
data-chromatic="ignore"
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "150%",
|
||||
}}
|
||||
>
|
||||
{createDayString(healthStatus.time)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
end
|
||||
key={key}
|
||||
to={`/health/${kebabCase(key)}`}
|
||||
className={({ isActive }) =>
|
||||
cx([link, isActive && activeLink])
|
||||
}
|
||||
>
|
||||
<HealthIcon
|
||||
size={16}
|
||||
severity={healthSection.severity as HealthSeverity}
|
||||
<div css={{ display: "flex", flexDirection: "column" }}>
|
||||
<span css={{ fontWeight: 500 }}>Version</span>
|
||||
<span
|
||||
data-chromatic="ignore"
|
||||
css={{
|
||||
color: theme.palette.text.secondary,
|
||||
lineHeight: "150%",
|
||||
}}
|
||||
>
|
||||
{healthStatus.coder_version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav css={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
{Object.entries(visibleSections)
|
||||
.sort()
|
||||
.map(([key, label]) => {
|
||||
const healthSection =
|
||||
healthStatus[key as keyof typeof visibleSections];
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
end
|
||||
key={key}
|
||||
to={`/health/${kebabCase(key)}`}
|
||||
className={({ isActive }) =>
|
||||
cx([link, isActive && activeLink])
|
||||
}
|
||||
>
|
||||
<HealthIcon
|
||||
size={16}
|
||||
severity={healthSection.severity as HealthSeverity}
|
||||
/>
|
||||
{label}
|
||||
{healthSection.dismissed && (
|
||||
<NotificationsOffOutlined
|
||||
css={{
|
||||
fontSize: 14,
|
||||
marginLeft: "auto",
|
||||
color: theme.palette.text.disabled,
|
||||
}}
|
||||
/>
|
||||
{label}
|
||||
{healthSection.dismissed && (
|
||||
<NotificationsOffOutlined
|
||||
css={{
|
||||
fontSize: 14,
|
||||
marginLeft: "auto",
|
||||
color: theme.palette.text.disabled,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div css={{ overflowY: "auto", width: "100%" }}>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet context={healthStatus} />
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</DashboardFullPage>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
|
||||
<div css={{ overflowY: "auto", width: "100%" }}>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Outlet context={healthStatus} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardFullPage>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user