Skip to content
34 changes: 34 additions & 0 deletions site/src/components/Alert/Alert.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
158 changes: 114 additions & 44 deletions site/src/components/Alert/Alert.tsx
Original file line number Diff line number Diff line change
@@ -1,81 +1,151 @@
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<AlertProps> = ({
children,
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 (
<Collapse in>
<MuiAlert
{...alertProps}
css={{ textAlign: "left" }}
severity={severity}
action={
<>
{/* CTAs passed in by the consumer */}
{actions}
<div
role="alert"
className={cn(alertVariants({ severity, prominent }), className)}
{...props}
>
<div className="flex items-center justify-between gap-4 text-sm">
<div className="flex flex-row items-start gap-3">
<Icon className={cn("size-icon-sm mt-[3px]", iconClassName)} />
<div className="flex-1">{children}</div>
</div>
<div className="flex items-center gap-2">
{actions}

{/* close CTA */}
{dismissible && (
<Button
variant="subtle"
size="sm"
onClick={() => {
setOpen(false);
onDismiss?.();
}}
data-testid="dismiss-banner-btn"
>
Dismiss
</Button>
)}
</>
}
>
{children}
</MuiAlert>
</Collapse>
{dismissible && (
<Button
variant="subtle"
size="icon"
className="!size-auto !min-w-0 !p-0"
onClick={() => {
setOpen(false);
onDismiss?.();
}}
data-testid="dismiss-banner-btn"
aria-label="Dismiss"
>
<XIcon className="!size-icon-sm !p-0" />
</Button>
)}
</div>
</div>
</div>
);
};

export const AlertDetail: FC<PropsWithChildren> = ({ children }) => {
return (
<span
css={(theme) => ({ color: theme.palette.text.secondary, fontSize: 13 })}
data-chromatic="ignore"
>
<span className="m-0 text-sm" data-chromatic="ignore">
{children}
</span>
);
};

export const AlertTitle = forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h1
ref={ref}
className={cn("m-0 mb-1 text-sm font-medium", className)}
{...props}
/>
));
5 changes: 2 additions & 3 deletions site/src/components/Alert/ErrorAlert.tsx
Original file line number Diff line number Diff line change
@@ -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<AlertProps, "severity" | "children"> & { error: unknown }
Expand All @@ -18,7 +17,7 @@ export const ErrorAlert: FC<ErrorAlertProps> = ({ error, ...alertProps }) => {
const shouldDisplayDetail = message !== detail;

return (
<Alert severity="error" {...alertProps}>
<Alert severity="error" prominent {...alertProps}>
{
// When the error is a Forbidden response we include a link for the user to
// go back to a known viewable page.
Expand Down
2 changes: 1 addition & 1 deletion site/src/components/Badge/Badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 4 additions & 3 deletions site/src/components/GitDeviceAuth/GitDeviceAuth.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -102,7 +101,9 @@ export const GitDeviceAuth: FC<GitDeviceAuthProps> = ({
break;
case DeviceExchangeError.AccessDenied:
status = (
<Alert severity="error">Access to the Git provider was denied.</Alert>
<Alert severity="error" prominent>
Access to the Git provider was denied.
</Alert>
);
break;
default:
Expand Down
40 changes: 23 additions & 17 deletions site/src/modules/provisioners/ProvisionerAlert.tsx
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<AlertColor, string> = {
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<ProvisionerAlertProps> = ({
Expand All @@ -45,7 +51,7 @@ export const ProvisionerAlert: FC<ProvisionerAlertProps> = ({
variant = AlertVariant.Standalone,
}) => {
return (
<Alert severity={severity} {...getAlertStyles(variant, severity)}>
<Alert severity={severity} className={getAlertClassName(variant, severity)}>
<AlertTitle>{title}</AlertTitle>
<AlertDetail>
<div>{detail}</div>
Expand Down
1 change: 1 addition & 0 deletions site/src/modules/resources/WildcardHostnameWarning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const WildcardHostnameWarning: FC<WildcardHostnameWarningProps> = ({
return (
<Alert
severity="warning"
prominent
className={
hasResources
? "rounded-none border-0 border-l-2 border-l-warning border-b-divider"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const ClassicParameterFlowDeprecationWarning: FC<
}

return (
<Alert severity="warning" className="mb-2">
<Alert severity="warning" className="mb-2" prominent>
<div>
This template is using the classic parameter flow, which will be{" "}
<strong>deprecated</strong> and removed in a future release. Please
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export const DownloadLogsDialog: FC<DownloadLogsDialogProps> = ({
</p>

{!isWorkspaceHealthy && isLoadingFiles && (
<Alert severity="warning">
<Alert severity="warning" prominent>
Your workspace is unhealthy. Some logs may be unavailable for
download.
</Alert>
Expand Down
2 changes: 1 addition & 1 deletion site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ const fillNameAndDisplayWithFilename = async (

const ProvisionerWarning: FC = () => {
return (
<Alert severity="warning" css={{ marginBottom: 16 }}>
<Alert severity="warning" css={{ marginBottom: 16 }} prominent>
This organization does not have any provisioners. Before you create a
template, you&apos;ll need to configure a provisioner.{" "}
<Link href={docs("/admin/provisioners#organization-scoped-provisioners")}>
Expand Down
Loading
Loading