diff --git a/site/src/components/Alert/Alert.stories.tsx b/site/src/components/Alert/Alert.stories.tsx index e122c0c07c5a6..979a0b6a9a5a8 100644 --- a/site/src/components/Alert/Alert.stories.tsx +++ b/site/src/components/Alert/Alert.stories.tsx @@ -54,3 +54,37 @@ export const WarningWithActionAndDismiss: Story = { severity: "warning", }, }; + +export const Info: Story = { + args: { + children: "This is an informational message", + severity: "info", + }, +}; + +export const ErrorSeverity: Story = { + args: { + children: "This is an error message", + severity: "error", + }, +}; + +export const WarningProminent: Story = { + args: { + children: + "This is a high risk warning. Use this design only for high risk warnings.", + severity: "warning", + prominent: true, + dismissible: true, + }, +}; + +export const ErrorProminent: Story = { + args: { + children: + "This is a crucial error. Use this design only for crucial errors.", + severity: "error", + prominent: true, + dismissible: true, + }, +}; diff --git a/site/src/components/Alert/Alert.tsx b/site/src/components/Alert/Alert.tsx index 2673cd9bc4f8a..6db0a99ff2d13 100644 --- a/site/src/components/Alert/Alert.tsx +++ b/site/src/components/Alert/Alert.tsx @@ -1,21 +1,81 @@ -import MuiAlert, { - type AlertColor as MuiAlertColor, - type AlertProps as MuiAlertProps, -} from "@mui/material/Alert"; -import Collapse from "@mui/material/Collapse"; +import { cva } from "class-variance-authority"; import { Button } from "components/Button/Button"; +import { + CircleAlertIcon, + CircleCheckIcon, + InfoIcon, + TriangleAlertIcon, + XIcon, +} from "lucide-react"; import { type FC, + forwardRef, type PropsWithChildren, type ReactNode, useState, } from "react"; -export type AlertColor = MuiAlertColor; +import { cn } from "utils/cn"; + +const alertVariants = cva( + "relative w-full rounded-lg border border-solid p-4 text-left", + { + variants: { + severity: { + info: "", + success: "", + warning: "", + error: "", + }, + prominent: { + true: "", + false: "", + }, + }, + compoundVariants: [ + { + prominent: false, + className: "border-border-default bg-surface-secondary", + }, + { + severity: "success", + prominent: true, + className: "border-border-success bg-surface-green", + }, + { + severity: "warning", + prominent: true, + className: "border-border-warning bg-surface-orange", + }, + { + severity: "error", + prominent: true, + className: "border-border-destructive bg-surface-red", + }, + ], + defaultVariants: { + severity: "info", + prominent: false, + }, + }, +); + +const severityIcons = { + info: { icon: InfoIcon, className: "text-highlight-sky" }, + success: { icon: CircleCheckIcon, className: "text-content-success" }, + warning: { icon: TriangleAlertIcon, className: "text-content-warning" }, + error: { icon: CircleAlertIcon, className: "text-content-destructive" }, +} as const; + +export type AlertColor = "info" | "success" | "warning" | "error"; -export type AlertProps = MuiAlertProps & { +export type AlertProps = { actions?: ReactNode; dismissible?: boolean; onDismiss?: () => void; + severity?: AlertColor; + prominent?: boolean; + children?: ReactNode; + className?: string; }; export const Alert: FC = ({ @@ -23,59 +83,69 @@ export const Alert: FC = ({ actions, dismissible, severity = "info", + prominent = false, onDismiss, - ...alertProps + className, + ...props }) => { const [open, setOpen] = useState(true); - // Can't only rely on MUI's hiding behavior inside flex layouts, because even - // though MUI will make a dismissed alert have zero height, the alert will - // still behave as a flex child and introduce extra row/column gaps if (!open) { return null; } + const { icon: Icon, className: iconClassName } = severityIcons[severity]; + return ( - - - {/* CTAs passed in by the consumer */} - {actions} +
+
+
+ +
{children}
+
+
+ {actions} - {/* close CTA */} - {dismissible && ( - - )} - - } - > - {children} - - + {dismissible && ( + + )} +
+
+
); }; export const AlertDetail: FC = ({ children }) => { return ( - ({ color: theme.palette.text.secondary, fontSize: 13 })} - data-chromatic="ignore" - > + {children} ); }; + +export const AlertTitle = forwardRef< + HTMLHeadingElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); diff --git a/site/src/components/Alert/ErrorAlert.tsx b/site/src/components/Alert/ErrorAlert.tsx index 2a8da27e035ba..04da586f829e3 100644 --- a/site/src/components/Alert/ErrorAlert.tsx +++ b/site/src/components/Alert/ErrorAlert.tsx @@ -1,8 +1,7 @@ -import AlertTitle from "@mui/material/AlertTitle"; import { getErrorDetail, getErrorMessage, getErrorStatus } from "api/errors"; import type { FC } from "react"; import { Link } from "../Link/Link"; -import { Alert, AlertDetail, type AlertProps } from "./Alert"; +import { Alert, AlertDetail, type AlertProps, AlertTitle } from "./Alert"; type ErrorAlertProps = Readonly< Omit & { error: unknown } @@ -18,7 +17,7 @@ export const ErrorAlert: FC = ({ error, ...alertProps }) => { const shouldDisplayDetail = message !== detail; return ( - + { // When the error is a Forbidden response we include a link for the user to // go back to a known viewable page. diff --git a/site/src/components/Badge/Badge.tsx b/site/src/components/Badge/Badge.tsx index ca6a08eb6040a..317f1e04e481a 100644 --- a/site/src/components/Badge/Badge.tsx +++ b/site/src/components/Badge/Badge.tsx @@ -24,7 +24,7 @@ const badgeVariants = cva( "border border-solid border-border-destructive bg-surface-red text-highlight-red shadow", green: "border border-solid border-border-green bg-surface-green text-highlight-green shadow", - info: "border border-solid border-border-sky bg-surface-sky text-highlight-sky shadow", + info: "border border-solid border-border-pending bg-surface-sky text-highlight-sky shadow", }, size: { xs: "text-2xs font-regular h-5 [&_svg]:hidden rounded px-1.5", diff --git a/site/src/components/GitDeviceAuth/GitDeviceAuth.tsx b/site/src/components/GitDeviceAuth/GitDeviceAuth.tsx index 7b3d8091abfeb..6fca3c52d59d0 100644 --- a/site/src/components/GitDeviceAuth/GitDeviceAuth.tsx +++ b/site/src/components/GitDeviceAuth/GitDeviceAuth.tsx @@ -1,11 +1,10 @@ import type { Interpolation, Theme } from "@emotion/react"; -import AlertTitle from "@mui/material/AlertTitle"; import CircularProgress from "@mui/material/CircularProgress"; import Link from "@mui/material/Link"; import type { ApiErrorResponse } from "api/errors"; import type { ExternalAuthDevice } from "api/typesGenerated"; import { isAxiosError } from "axios"; -import { Alert, AlertDetail } from "components/Alert/Alert"; +import { Alert, AlertDetail, AlertTitle } from "components/Alert/Alert"; import { CopyButton } from "components/CopyButton/CopyButton"; import { ExternalLinkIcon } from "lucide-react"; import type { FC } from "react"; @@ -102,7 +101,9 @@ export const GitDeviceAuth: FC = ({ break; case DeviceExchangeError.AccessDenied: status = ( - Access to the Git provider was denied. + + Access to the Git provider was denied. + ); break; default: diff --git a/site/src/modules/provisioners/ProvisionerAlert.tsx b/site/src/modules/provisioners/ProvisionerAlert.tsx index 2160b4e7b3ebf..298b785887038 100644 --- a/site/src/modules/provisioners/ProvisionerAlert.tsx +++ b/site/src/modules/provisioners/ProvisionerAlert.tsx @@ -1,8 +1,13 @@ -import type { Theme } from "@emotion/react"; -import AlertTitle from "@mui/material/AlertTitle"; -import { Alert, type AlertColor, AlertDetail } from "components/Alert/Alert"; +import { + Alert, + type AlertColor, + AlertDetail, + AlertTitle, +} from "components/Alert/Alert"; import { ProvisionerTag } from "modules/provisioners/ProvisionerTag"; import type { FC } from "react"; +import { cn } from "utils/cn"; + export enum AlertVariant { // Alerts are usually styled with a full rounded border and meant to use as a visually distinct element of the page. // The Standalone variant conforms to this styling. @@ -21,20 +26,21 @@ interface ProvisionerAlertProps { variant?: AlertVariant; } -const getAlertStyles = (variant: AlertVariant, severity: AlertColor) => { - switch (variant) { - case AlertVariant.Inline: - return { - css: (theme: Theme) => ({ - borderRadius: 0, - border: 0, - borderBottom: `1px solid ${theme.palette.divider}`, - borderLeft: `2px solid ${theme.palette[severity].main}`, - }), - }; - default: - return {}; +const severityBorderColors: Record = { + info: "border-l-highlight-sky", + success: "border-l-content-success", + warning: "border-l-content-warning", + error: "border-l-content-destructive", +}; + +const getAlertClassName = (variant: AlertVariant, severity: AlertColor) => { + if (variant === AlertVariant.Inline) { + return cn( + "rounded-none border-0 border-b border-l-2 border-solid border-b-border-default", + severityBorderColors[severity], + ); } + return undefined; }; export const ProvisionerAlert: FC = ({ @@ -45,7 +51,7 @@ export const ProvisionerAlert: FC = ({ variant = AlertVariant.Standalone, }) => { return ( - + {title}
{detail}
diff --git a/site/src/modules/resources/WildcardHostnameWarning.tsx b/site/src/modules/resources/WildcardHostnameWarning.tsx index 2210575ae430f..e9aa9d3ad8334 100644 --- a/site/src/modules/resources/WildcardHostnameWarning.tsx +++ b/site/src/modules/resources/WildcardHostnameWarning.tsx @@ -40,6 +40,7 @@ export const WildcardHostnameWarning: FC = ({ return ( +
This template is using the classic parameter flow, which will be{" "} deprecated and removed in a future release. Please diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx index ecedc5aef6b5f..2a0d89878d0a1 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx @@ -1,11 +1,10 @@ import { css } from "@emotion/css"; -import AlertTitle from "@mui/material/AlertTitle"; import Autocomplete from "@mui/material/Autocomplete"; import CircularProgress from "@mui/material/CircularProgress"; import TextField from "@mui/material/TextField"; import { templateVersions } from "api/queries/templates"; import type { TemplateVersion, Workspace } from "api/typesGenerated"; -import { Alert } from "components/Alert/Alert"; +import { Alert, AlertTitle } from "components/Alert/Alert"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx index 4a825ee6c3b68..95d54555592cd 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx @@ -146,7 +146,7 @@ export const DownloadLogsDialog: FC = ({

{!isWorkspaceHealthy && isLoadingFiles && ( - + Your workspace is unhealthy. Some logs may be unavailable for download. diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx index e3528e9b707be..bd0fcd9db56e6 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx @@ -428,7 +428,7 @@ const fillNameAndDisplayWithFilename = async ( const ProvisionerWarning: FC = () => { return ( - + This organization does not have any provisioners. Before you create a template, you'll need to configure a provisioner.{" "} diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index f27ab1df3cbbc..ec5eadcefddca 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -520,7 +520,7 @@ export const CreateWorkspacePageView: FC = ({
{Boolean(error) && !hasAllRequiredExternalAuth && ( - + To create a workspace using this template, please connect to all required external authentication providers listed below. diff --git a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.tsx b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.tsx index d116718be1898..95fe96a2accf0 100644 --- a/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.tsx +++ b/site/src/pages/DeploymentSettingsPage/NotificationsPage/NotificationEvents.tsx @@ -69,6 +69,7 @@ export const NotificationEvents: FC = ({ {hasWebhookNotifications && !isWebhookConfigured && ( = ({ {hasSMTPNotifications && !isSMTPConfigured && ( { actions={} key={warning.code} severity="warning" + prominent > {warning.message} diff --git a/site/src/pages/HealthPage/DERPPage.tsx b/site/src/pages/HealthPage/DERPPage.tsx index 3a913f23e05d5..36e0e922a7a53 100644 --- a/site/src/pages/HealthPage/DERPPage.tsx +++ b/site/src/pages/HealthPage/DERPPage.tsx @@ -67,6 +67,7 @@ const DERPPage: FC = () => { actions={} key={warning.code} severity="warning" + prominent > {warning.message} diff --git a/site/src/pages/HealthPage/DERPRegionPage.tsx b/site/src/pages/HealthPage/DERPRegionPage.tsx index bc0830fbf5fb8..a3aaea54259d7 100644 --- a/site/src/pages/HealthPage/DERPRegionPage.tsx +++ b/site/src/pages/HealthPage/DERPRegionPage.tsx @@ -81,6 +81,7 @@ const DERPRegionPage: FC = () => { actions={} key={warning.code} severity="warning" + prominent > {warning.message} diff --git a/site/src/pages/HealthPage/DatabasePage.tsx b/site/src/pages/HealthPage/DatabasePage.tsx index 7837b5aae9b8c..8724ab34a3ffe 100644 --- a/site/src/pages/HealthPage/DatabasePage.tsx +++ b/site/src/pages/HealthPage/DatabasePage.tsx @@ -37,6 +37,7 @@ const DatabasePage = () => { actions={} key={warning.code} severity="warning" + prominent > {warning.message} diff --git a/site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx b/site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx index 19a2a0f9f43de..32fb7f01eca60 100644 --- a/site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx +++ b/site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx @@ -30,7 +30,11 @@ const ProvisionerDaemonsPage: FC = () => {
- {daemons.error && {daemons.error}} + {daemons.error && ( + + {daemons.error} + + )} {daemons.warnings.map((warning) => { return ( {
- {websocket.error && {websocket.error}} + {websocket.error && ( + + {websocket.error} + + )} {websocket.warnings.map((warning) => { return ( - + {warning.message} ); diff --git a/site/src/pages/HealthPage/WorkspaceProxyPage.tsx b/site/src/pages/HealthPage/WorkspaceProxyPage.tsx index e80786e1dffa0..29dfff0472c1d 100644 --- a/site/src/pages/HealthPage/WorkspaceProxyPage.tsx +++ b/site/src/pages/HealthPage/WorkspaceProxyPage.tsx @@ -42,7 +42,9 @@ const WorkspaceProxyPage: FC = () => {
{workspace_proxy.error && ( - {workspace_proxy.error} + + {workspace_proxy.error} + )} {workspace_proxy.warnings.map((warning) => { return ( @@ -50,6 +52,7 @@ const WorkspaceProxyPage: FC = () => { actions={} key={warning.code} severity="warning" + prominent > {warning.message} diff --git a/site/src/pages/LoginPage/SignInForm.tsx b/site/src/pages/LoginPage/SignInForm.tsx index 8bee2fb7405ab..c70873586dc85 100644 --- a/site/src/pages/LoginPage/SignInForm.tsx +++ b/site/src/pages/LoginPage/SignInForm.tsx @@ -114,7 +114,9 @@ export const SignInForm: FC = ({ )} {!passwordEnabled && !oAuthEnabled && ( - No authentication methods configured! + + No authentication methods configured! + )}
); diff --git a/site/src/pages/SetupPage/SetupPageView.tsx b/site/src/pages/SetupPage/SetupPageView.tsx index 28e750bfb31dc..754345c180950 100644 --- a/site/src/pages/SetupPage/SetupPageView.tsx +++ b/site/src/pages/SetupPage/SetupPageView.tsx @@ -1,4 +1,3 @@ -import AlertTitle from "@mui/material/AlertTitle"; import Autocomplete from "@mui/material/Autocomplete"; import Checkbox from "@mui/material/Checkbox"; import Link from "@mui/material/Link"; @@ -7,7 +6,7 @@ import TextField from "@mui/material/TextField"; import { countries } from "api/countriesGenerated"; import type * as TypesGen from "api/typesGenerated"; import { isAxiosError } from "axios"; -import { Alert, AlertDetail } from "components/Alert/Alert"; +import { Alert, AlertDetail, AlertTitle } from "components/Alert/Alert"; import { Button } from "components/Button/Button"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { FormFields, VerticalForm } from "components/Form/Form"; @@ -352,7 +351,7 @@ export const SetupPageView: FC = ({ )} {isAxiosError(error) && error.response?.data?.message && ( - + {error.response.data.message} {error.response.data.detail && ( diff --git a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx index 93ed0219890d7..bc7c9e70fb6d8 100644 --- a/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx +++ b/site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx @@ -334,6 +334,7 @@ export const TemplateVersionEditor: FC = ({ > { ); }; +const severityBorderColors: Record = { + info: "border-l-highlight-sky", + success: "border-l-content-success", + warning: "border-l-content-warning", + error: "border-l-content-destructive", +}; + const TerminalAlert: FC = (props) => { + const severity = props.severity ?? "info"; return ( ({ - borderRadius: 0, - borderWidth: 0, - borderBottomWidth: 1, - borderBottomColor: theme.palette.divider, - backgroundColor: theme.palette.background.paper, - borderLeft: `3px solid ${theme.palette[props.severity!].light}`, - marginBottom: 1, - })} + className={cn( + "rounded-none border-0 border-b border-l-[3px] border-b-border-default bg-surface-primary mb-px [&>div]:items-center", + severityBorderColors[severity], + )} /> ); }; diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx index c1d95e6d49470..b6d2f0d825f4e 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx @@ -174,6 +174,7 @@ export const WorkspaceBuildPageView: FC = ({ {build.transition === "delete" && build.job.status === "failed" && (
@@ -190,6 +191,7 @@ export const WorkspaceBuildPageView: FC = ({ {build?.job?.logs_overflowed && ( Provisioner logs exceeded the max size of 1MB. Will not continue diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 83b8a6397104c..79d17098945b6 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -1,7 +1,7 @@ -import AlertTitle from "@mui/material/AlertTitle"; import type * as TypesGen from "api/typesGenerated"; -import { Alert, AlertDetail } from "components/Alert/Alert"; +import { Alert, AlertDetail, AlertTitle } from "components/Alert/Alert"; import { SidebarIconButton } from "components/FullPageLayout/Sidebar"; +import { Link } from "components/Link/Link"; import { useSearchParamsKey } from "hooks/useSearchParamsKey"; import { BlocksIcon, HistoryIcon } from "lucide-react"; import { ProvisionerStatusAlert } from "modules/provisioners/ProvisionerStatusAlert"; @@ -193,14 +193,14 @@ export const Workspace: FC = ({ )} {workspace.latest_build.job.error && ( - + Workspace build failed {workspace.latest_build.job.error} )} {!workspace.health.healthy && ( - + Workspace is unhealthy

@@ -208,7 +208,12 @@ export const Workspace: FC = ({ {workspace.health.failing_agents.length > 1 ? `${workspace.health.failing_agents.length} agents are unhealthy` : "1 agent is unhealthy"} - . + .{" "} + {troubleshootingURL && ( + + View docs to troubleshoot + + )}

{hasActions && (
@@ -219,15 +224,6 @@ export const Workspace: FC = ({ Restart )} - {troubleshootingURL && ( - - window.open(troubleshootingURL, "_blank") - } - > - Troubleshooting - - )}
)}
diff --git a/site/src/pages/WorkspacePage/WorkspaceDeletedBanner.tsx b/site/src/pages/WorkspacePage/WorkspaceDeletedBanner.tsx index dee02d4a2aa98..1ec9e887b9eb0 100644 --- a/site/src/pages/WorkspacePage/WorkspaceDeletedBanner.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceDeletedBanner.tsx @@ -16,7 +16,7 @@ export const WorkspaceDeletedBanner: FC = ({ ); return ( - + This workspace has been deleted and cannot be edited. ); diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications/Notifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications/Notifications.tsx index 308a763716969..b2989fab130db 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications/Notifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications/Notifications.tsx @@ -105,7 +105,7 @@ const NotificationItem: FC = ({ notification }) => { }; export const NotificationActionButton: FC = (props) => { - return