mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
+18
-19
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user