feat(site): rename "Dismiss warnings" to "Mute warnings" and make health callouts dismissible (#27554)

Fixes a mismatch between the header button's label and its behavior on
the Health pages. Today the button reads **Dismiss warnings**, which
suggests it will close the in-page callout, but it actually toggles
whether the health check surfaces in the top-nav status indicator and
shows a bell-off icon in the sidebar. The callout itself has no way to
be closed.

### Changes
- Rename the toggle to **Mute warnings** / **Unmute warnings** (with
matching toast copy) and rename the component + file from
`DismissWarningButton` to `MuteWarningsButton`.
- Set `dismissible` on the **warning** `<Alert>`s across the Health
pages (Access URL, Database, DERP, DERP region, Provisioner Daemons,
Websocket, Workspace Proxy) so users can close the callout from the
callout itself. `Alert` already supports this via a built-in close
button.
- Error-severity `<Alert>`s are intentionally **not** dismissible:
`HealthLayout` refetches every 30s and reuses the mounted subpage, so
allowing dismissal would suppress subsequent (possibly different) error
messages until reload. Diagnostics pages should not hide active faults.
- Align ProvisionerDaemonsPage's warning callout with the other five
pages by setting `prominent`.

### Notes
- Callout dismissal is client-side only (matches `Alert`'s existing
`useState` behavior). Warning `<Alert>`s are keyed by `warning.code`, so
a dismissed warning reappears on reload/remount but survives a refetch.
The mute toggle continues to persist server-side via
`dismissed_healthchecks`.
- Follow-up filed for a pre-existing UX mismatch: the mute also silently
drops error-severity sections from the top-nav banner (#27557). Kept out
of scope here per requester.
- No API or backend changes.

---

_This PR was generated by Coder Agents on behalf of @tracyjohnsonux._
This commit is contained in:
TJ
2026-07-27 19:56:47 -07:00
committed by GitHub
parent 1ab4ed8db5
commit 6c916629c9
8 changed files with 38 additions and 32 deletions
+3 -2
View File
@@ -12,7 +12,7 @@ import {
HealthyDot,
Main,
} from "./Content";
import { DismissWarningButton } from "./DismissWarningButton";
import { MuteWarningsButton } from "./MuteWarningsButton";
const AccessURLPage = () => {
const healthStatus = useOutletContext<HealthcheckReport>();
@@ -27,7 +27,7 @@ const AccessURLPage = () => {
<HealthyDot severity={accessUrl.severity} />
Access URL
</HeaderTitle>
<DismissWarningButton healthcheck="AccessURL" />
<MuteWarningsButton healthcheck="AccessURL" />
</Header>
<Main>
@@ -40,6 +40,7 @@ const AccessURLPage = () => {
key={warning.code}
severity="warning"
prominent
dismissible
>
{warning.message}
</Alert>
+3 -2
View File
@@ -25,7 +25,7 @@ import {
SectionLabel,
StatusIcon,
} from "./Content";
import { DismissWarningButton } from "./DismissWarningButton";
import { MuteWarningsButton } from "./MuteWarningsButton";
type BooleanKeys<T> = {
[K in keyof T]: T[K] extends boolean | null ? K : never;
@@ -148,7 +148,7 @@ const DERPPage: FC = () => {
<HealthyDot severity={derp.severity as HealthSeverity} />
DERP
</HeaderTitle>
<DismissWarningButton healthcheck="DERP" />
<MuteWarningsButton healthcheck="DERP" />
</Header>
<Main>
@@ -159,6 +159,7 @@ const DERPPage: FC = () => {
key={warning.code}
severity="warning"
prominent
dismissible
>
{warning.message}
</Alert>
@@ -83,6 +83,7 @@ const DERPRegionPage: FC = () => {
key={warning.code}
severity="warning"
prominent
dismissible
>
{warning.message}
</Alert>
+3 -2
View File
@@ -12,7 +12,7 @@ import {
HealthyDot,
Main,
} from "./Content";
import { DismissWarningButton } from "./DismissWarningButton";
import { MuteWarningsButton } from "./MuteWarningsButton";
const DatabasePage = () => {
const healthStatus = useOutletContext<HealthcheckReport>();
@@ -27,7 +27,7 @@ const DatabasePage = () => {
<HealthyDot severity={database.severity} />
Database
</HeaderTitle>
<DismissWarningButton healthcheck="Database" />
<MuteWarningsButton healthcheck="Database" />
</Header>
<Main>
@@ -38,6 +38,7 @@ const DatabasePage = () => {
key={warning.code}
severity="warning"
prominent
dismissible
>
{warning.message}
</Alert>
@@ -7,63 +7,62 @@ import { Button } from "#/components/Button/Button";
import { Skeleton } from "#/components/Skeleton/Skeleton";
import { Spinner } from "#/components/Spinner/Spinner";
export const DismissWarningButton = (props: { healthcheck: HealthSection }) => {
export const MuteWarningsButton = (props: { healthcheck: HealthSection }) => {
const queryClient = useQueryClient();
const healthSettingsQuery = useQuery(healthSettings());
// They call the same mutation but are used in diff contexts so we don't want
// to merge their states. Eg. You dismiss a warning and when it is done it
// will show the enable button but since the mutation is still invalidating
// other queries it will be in the loading state when it should be idle.
const enableMutation = useMutation(updateHealthSettings(queryClient));
const dismissMutation = useMutation(updateHealthSettings(queryClient));
// Separate mutation instances so unmuting isn't stuck pending while
// muting's query invalidation resolves (a shared mutation would share
// isPending and spin the wrong button).
const unmuteMutation = useMutation(updateHealthSettings(queryClient));
const muteMutation = useMutation(updateHealthSettings(queryClient));
if (!healthSettingsQuery.data) {
return <Skeleton height={36} width={170} className="rounded-lg" />;
}
const { dismissed_healthchecks } = healthSettingsQuery.data;
const isDismissed = dismissed_healthchecks.includes(props.healthcheck);
const isMuted = dismissed_healthchecks.includes(props.healthcheck);
if (isDismissed) {
if (isMuted) {
return (
<Button
disabled={healthSettingsQuery.isLoading || enableMutation.isPending}
disabled={healthSettingsQuery.isLoading || unmuteMutation.isPending}
variant="outline"
onClick={async () => {
const updatedSettings = dismissed_healthchecks.filter(
(dismissedHealthcheck) =>
dismissedHealthcheck !== props.healthcheck,
);
await enableMutation.mutateAsync({
await unmuteMutation.mutateAsync({
dismissed_healthchecks: updatedSettings,
});
toast.success("Warnings enabled successfully.");
toast.success("Warnings unmuted successfully.");
}}
>
<Spinner loading={enableMutation.isPending}>
<Spinner loading={unmuteMutation.isPending}>
<BellOffIcon />
</Spinner>
Enable warnings
Unmute warnings
</Button>
);
}
return (
<Button
disabled={healthSettingsQuery.isLoading || dismissMutation.isPending}
disabled={healthSettingsQuery.isLoading || muteMutation.isPending}
variant="outline"
onClick={async () => {
const updatedSettings = [...dismissed_healthchecks, props.healthcheck];
await dismissMutation.mutateAsync({
await muteMutation.mutateAsync({
dismissed_healthchecks: updatedSettings,
});
toast.success("Dismissed warnings successfully.");
toast.success("Warnings muted successfully.");
}}
>
<Spinner loading={dismissMutation.isPending}>
<Spinner loading={muteMutation.isPending}>
<BellIcon />
</Spinner>
Dismiss warnings
Mute warnings
</Button>
);
};
@@ -11,7 +11,7 @@ import {
HealthyDot,
Main,
} from "./Content";
import { DismissWarningButton } from "./DismissWarningButton";
import { MuteWarningsButton } from "./MuteWarningsButton";
const ProvisionerDaemonsPage: FC = () => {
const healthStatus = useOutletContext<HealthcheckReport>();
@@ -26,7 +26,7 @@ const ProvisionerDaemonsPage: FC = () => {
<HealthyDot severity={daemons.severity} />
Provisioner Daemons
</HeaderTitle>
<DismissWarningButton healthcheck="ProvisionerDaemons" />
<MuteWarningsButton healthcheck="ProvisionerDaemons" />
</Header>
<Main>
@@ -41,6 +41,8 @@ const ProvisionerDaemonsPage: FC = () => {
actions={<HealthMessageDocsLink {...warning} />}
key={warning.code}
severity="warning"
prominent
dismissible
>
{warning.message}
</Alert>
+3 -3
View File
@@ -16,7 +16,7 @@ import {
Pill,
SectionLabel,
} from "./Content";
import { DismissWarningButton } from "./DismissWarningButton";
import { MuteWarningsButton } from "./MuteWarningsButton";
const WebsocketPage = () => {
const healthStatus = useOutletContext<HealthcheckReport>();
@@ -31,7 +31,7 @@ const WebsocketPage = () => {
<HealthyDot severity={websocket.severity} />
Websocket
</HeaderTitle>
<DismissWarningButton healthcheck="Websocket" />
<MuteWarningsButton healthcheck="Websocket" />
</Header>
<Main>
@@ -43,7 +43,7 @@ const WebsocketPage = () => {
{websocket.warnings.map((warning) => {
return (
<Alert key={warning.code} severity="warning" prominent>
<Alert key={warning.code} severity="warning" prominent dismissible>
{warning.message}
</Alert>
);
@@ -21,7 +21,7 @@ import {
Main,
Pill,
} from "./Content";
import { DismissWarningButton } from "./DismissWarningButton";
import { MuteWarningsButton } from "./MuteWarningsButton";
const WorkspaceProxyPage: FC = () => {
const healthStatus = useOutletContext<HealthcheckReport>();
@@ -37,7 +37,7 @@ const WorkspaceProxyPage: FC = () => {
<HealthyDot severity={workspace_proxy.severity} />
Workspace Proxy
</HeaderTitle>
<DismissWarningButton healthcheck="WorkspaceProxy" />
<MuteWarningsButton healthcheck="WorkspaceProxy" />
</Header>
<Main>
@@ -53,6 +53,7 @@ const WorkspaceProxyPage: FC = () => {
key={warning.code}
severity="warning"
prominent
dismissible
>
{warning.message}
</Alert>