diff --git a/site/src/api/api.ts b/site/src/api/api.ts index d1a7bcfe3d..65c2db3283 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1602,3 +1602,19 @@ export const getHealth = async (force: boolean = false) => { ); return response.data; }; + +export const getHealthSettings = async () => { + return ( + await axios.get(`/api/v2/debug/health/settings`) + ).data; +}; + +export const updateHealthSettings = async ( + data: TypesGen.UpdateHealthSettings, +) => { + const response = await axios.put( + `/api/v2/debug/health/settings`, + data, + ); + return response.data; +}; diff --git a/site/src/api/queries/debug.ts b/site/src/api/queries/debug.ts index 5381c6610f..e8a5d447a2 100644 --- a/site/src/api/queries/debug.ts +++ b/site/src/api/queries/debug.ts @@ -1,17 +1,45 @@ import * as API from "api/api"; -import { QueryClient } from "react-query"; +import { HealthSettings, UpdateHealthSettings } from "api/typesGenerated"; +import { QueryClient, UseMutationOptions } from "react-query"; + +export const HEALTH_QUERY_KEY = ["health"]; +export const HEALTH_QUERY_SETTINGS_KEY = ["health", "settings"]; export const health = () => ({ - queryKey: ["health"], + queryKey: HEALTH_QUERY_KEY, queryFn: async () => API.getHealth(), }); export const refreshHealth = (queryClient: QueryClient) => { return { mutationFn: async () => { - await queryClient.cancelQueries(["health"]); + await queryClient.cancelQueries(HEALTH_QUERY_KEY); const newHealthData = await API.getHealth(true); - queryClient.setQueryData(["health"], newHealthData); + queryClient.setQueryData(HEALTH_QUERY_KEY, newHealthData); + }, + }; +}; + +export const healthSettings = () => { + return { + queryKey: HEALTH_QUERY_SETTINGS_KEY, + queryFn: API.getHealthSettings, + }; +}; + +export const updateHealthSettings = ( + queryClient: QueryClient, +): UseMutationOptions< + HealthSettings, + unknown, + UpdateHealthSettings, + unknown +> => { + return { + mutationFn: API.updateHealthSettings, + onSuccess: async (_, newSettings) => { + await queryClient.invalidateQueries(HEALTH_QUERY_KEY); + queryClient.setQueryData(HEALTH_QUERY_SETTINGS_KEY, newSettings); }, }; }; diff --git a/site/src/pages/HealthPage/AccessURLPage.tsx b/site/src/pages/HealthPage/AccessURLPage.tsx index 446fb7657e..0a4c3b7894 100644 --- a/site/src/pages/HealthPage/AccessURLPage.tsx +++ b/site/src/pages/HealthPage/AccessURLPage.tsx @@ -12,6 +12,7 @@ import { HealthcheckReport } from "api/typesGenerated"; import { Alert } from "components/Alert/Alert"; import { Helmet } from "react-helmet-async"; import { pageTitle } from "utils/page"; +import { DismissWarningButton } from "./DismissWarningButton"; export const AccessURLPage = () => { const healthStatus = useOutletContext(); @@ -28,6 +29,7 @@ export const AccessURLPage = () => { Access URL +
diff --git a/site/src/pages/HealthPage/DERPPage.tsx b/site/src/pages/HealthPage/DERPPage.tsx index d5bab7351b..d64547c786 100644 --- a/site/src/pages/HealthPage/DERPPage.tsx +++ b/site/src/pages/HealthPage/DERPPage.tsx @@ -20,6 +20,7 @@ import { Alert } from "components/Alert/Alert"; import { Helmet } from "react-helmet-async"; import { pageTitle } from "utils/page"; import { useTheme } from "@mui/material/styles"; +import { DismissWarningButton } from "./DismissWarningButton"; const flags = [ "UDP", @@ -52,6 +53,7 @@ export const DERPPage = () => { DERP +
diff --git a/site/src/pages/HealthPage/DatabasePage.tsx b/site/src/pages/HealthPage/DatabasePage.tsx index 54772d0a01..62f3d316e3 100644 --- a/site/src/pages/HealthPage/DatabasePage.tsx +++ b/site/src/pages/HealthPage/DatabasePage.tsx @@ -12,6 +12,7 @@ import { HealthcheckReport } from "api/typesGenerated"; import { Alert } from "components/Alert/Alert"; import { Helmet } from "react-helmet-async"; import { pageTitle } from "utils/page"; +import { DismissWarningButton } from "./DismissWarningButton"; export const DatabasePage = () => { const healthStatus = useOutletContext(); @@ -28,6 +29,7 @@ export const DatabasePage = () => { Database +
diff --git a/site/src/pages/HealthPage/DismissWarningButton.tsx b/site/src/pages/HealthPage/DismissWarningButton.tsx new file mode 100644 index 0000000000..558ba04f52 --- /dev/null +++ b/site/src/pages/HealthPage/DismissWarningButton.tsx @@ -0,0 +1,74 @@ +import NotificationOutlined from "@mui/icons-material/NotificationsOutlined"; +import NotificationsOffOutlined from "@mui/icons-material/NotificationsOffOutlined"; +import { healthSettings, updateHealthSettings } from "api/queries/debug"; +import { useMutation, useQuery, useQueryClient } from "react-query"; +import { displaySuccess } from "components/GlobalSnackbar/utils"; +import LoadingButton from "@mui/lab/LoadingButton"; +import Skeleton from "@mui/material/Skeleton"; +import { HealthSection } from "api/typesGenerated"; + +export const DismissWarningButton = (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)); + + if (!healthSettingsQuery.data) { + return ( + + ); + } + + const { dismissed_healthchecks } = healthSettingsQuery.data; + const isDismissed = dismissed_healthchecks.includes(props.healthcheck); + + if (isDismissed) { + return ( + } + onClick={async () => { + const updatedSettings = dismissed_healthchecks.filter( + (dismissedHealthcheck) => + dismissedHealthcheck !== props.healthcheck, + ); + await enableMutation.mutateAsync({ + dismissed_healthchecks: updatedSettings, + }); + displaySuccess("Warnings enabled successfully!"); + }} + > + Enable warnings + + ); + } + + return ( + } + onClick={async () => { + const updatedSettings = [...dismissed_healthchecks, props.healthcheck]; + await dismissMutation.mutateAsync({ + dismissed_healthchecks: updatedSettings, + }); + displaySuccess("Warnings dismissed successfully!"); + }} + > + Dismiss warnings + + ); +}; diff --git a/site/src/pages/HealthPage/HealthLayout.tsx b/site/src/pages/HealthPage/HealthLayout.tsx index 11b80469c7..7944ac122c 100644 --- a/site/src/pages/HealthPage/HealthLayout.tsx +++ b/site/src/pages/HealthPage/HealthLayout.tsx @@ -16,6 +16,7 @@ import { kebabCase } from "lodash/fp"; import { Suspense } from "react"; import { HealthIcon } from "./Content"; import { HealthSeverity } from "api/typesGenerated"; +import NotificationsOffOutlined from "@mui/icons-material/NotificationsOffOutlined"; const sections = { derp: "DERP", @@ -189,6 +190,15 @@ export function HealthLayout() { severity={healthSection.severity as HealthSeverity} /> {label} + {healthSection.dismissed && ( + + )} ); })} diff --git a/site/src/pages/HealthPage/WebsocketPage.tsx b/site/src/pages/HealthPage/WebsocketPage.tsx index 43b1ebd3ee..150432c311 100644 --- a/site/src/pages/HealthPage/WebsocketPage.tsx +++ b/site/src/pages/HealthPage/WebsocketPage.tsx @@ -15,6 +15,7 @@ import { MONOSPACE_FONT_FAMILY } from "theme/constants"; import { Alert } from "components/Alert/Alert"; import { pageTitle } from "utils/page"; import { Helmet } from "react-helmet-async"; +import { DismissWarningButton } from "./DismissWarningButton"; export const WebsocketPage = () => { const healthStatus = useOutletContext(); @@ -32,6 +33,7 @@ export const WebsocketPage = () => { Websocket +
diff --git a/site/src/pages/HealthPage/WorkspaceProxyPage.tsx b/site/src/pages/HealthPage/WorkspaceProxyPage.tsx index 70a389e6e6..8bf2f2a989 100644 --- a/site/src/pages/HealthPage/WorkspaceProxyPage.tsx +++ b/site/src/pages/HealthPage/WorkspaceProxyPage.tsx @@ -16,6 +16,7 @@ import TagOutlined from "@mui/icons-material/TagOutlined"; import { Alert } from "components/Alert/Alert"; import { Helmet } from "react-helmet-async"; import { pageTitle } from "utils/page"; +import { DismissWarningButton } from "./DismissWarningButton"; export const WorkspaceProxyPage = () => { const healthStatus = useOutletContext(); @@ -34,6 +35,7 @@ export const WorkspaceProxyPage = () => { Workspace Proxy +
diff --git a/site/src/pages/HealthPage/storybook.tsx b/site/src/pages/HealthPage/storybook.tsx new file mode 100644 index 0000000000..8a6893ebf3 --- /dev/null +++ b/site/src/pages/HealthPage/storybook.tsx @@ -0,0 +1,33 @@ +import { useQueryClient } from "react-query"; +import { + reactRouterParameters, + reactRouterOutlet, + RouteDefinition, +} from "storybook-addon-react-router-v6"; +import { MockHealth, MockHealthSettings } from "testHelpers/entities"; +import { Meta } from "@storybook/react"; +import { HEALTH_QUERY_KEY, HEALTH_QUERY_SETTINGS_KEY } from "api/queries/debug"; + +type MetaOptions = { + element: RouteDefinition; + path: string; +}; + +export const generateMeta = ({ element, path }: MetaOptions): Meta => { + return { + parameters: { + layout: "fullscreen", + reactRouter: reactRouterParameters({ + routing: reactRouterOutlet({ path: `/health/${path}` }, element), + }), + }, + decorators: [ + (Story) => { + const queryClient = useQueryClient(); + queryClient.setQueryData(HEALTH_QUERY_KEY, MockHealth); + queryClient.setQueryData(HEALTH_QUERY_SETTINGS_KEY, MockHealthSettings); + return ; + }, + ], + }; +}; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 555b21058e..f9dc9f6e55 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -3149,6 +3149,10 @@ export const DeploymentHealthUnhealthy: TypesGen.HealthcheckReport = { }, }; +export const MockHealthSettings: TypesGen.HealthSettings = { + dismissed_healthchecks: [], +}; + export const MockGithubExternalProvider: TypesGen.ExternalAuthLinkProvider = { id: "github", type: "github",