Skip to content

feat: migrate Alert component from MUI to shadcn #18412

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 84 additions & 46 deletions site/src/components/Alert/Alert.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,121 @@
import MuiAlert, {
type AlertColor as MuiAlertColor,
type AlertProps as MuiAlertProps,
// biome-ignore lint/nursery/noRestrictedImports: Used as base component
} from "@mui/material/Alert";
import Collapse from "@mui/material/Collapse";
import { type VariantProps, cva } from "class-variance-authority";
import { Button } from "components/Button/Button";
import {
type FC,
type PropsWithChildren,
type ReactNode,
forwardRef,
useState,
} from "react";
import { cn } from "utils/cn";

export type AlertColor = MuiAlertColor;
const alertVariants = cva(
"relative w-full rounded-lg border border-solid p-4 text-left",
{
variants: {
variant: {
default: "border-border-default",
info: "border-highlight-sky",
success: "border-surface-green",
warning: "border-border-warning",
error: "border-border-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);

export type AlertProps = MuiAlertProps & {
// Map MUI severity to our variant
const severityToVariant = {
info: "info",
success: "success",
warning: "warning",
error: "error",
} as const;

export type AlertColor = "info" | "success" | "warning" | "error";

export type AlertProps = {
actions?: ReactNode;
dismissible?: boolean;
onDismiss?: () => void;
};
severity?: AlertColor;
children?: ReactNode;
className?: string;
} & VariantProps<typeof alertVariants>;

export const Alert: FC<AlertProps> = ({
children,
actions,
dismissible,
severity = "info",
onDismiss,
...alertProps
className,
variant,
...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;
}

// Use severity to determine variant if variant is not explicitly provided
const finalVariant =
variant ||
(severity in severityToVariant ? severityToVariant[severity] : "default");

return (
<Collapse in>
<MuiAlert
{...alertProps}
css={{ textAlign: "left" }}
severity={severity}
action={
<>
{/* CTAs passed in by the consumer */}
{actions}
<div
role="alert"
className={cn(alertVariants({ variant: finalVariant }), className)}
{...props}
>
<div className="flex items-start justify-between text-sm">
<div className="flex-1">{children}</div>
<div className="flex items-center gap-2 ml-4">
{/* CTAs passed in by the consumer */}
{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="sm"
onClick={() => {
setOpen(false);
onDismiss?.();
}}
data-testid="dismiss-banner-btn"
>
Dismiss
</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="text-sm opacity-75" data-chromatic="ignore">
{children}
</span>
);
};

// Export AlertTitle and AlertDescription for compatibility
export const AlertTitle = forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
));

AlertTitle.displayName = "AlertTitle";
3 changes: 1 addition & 2 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";

export const ErrorAlert: FC<
Omit<AlertProps, "severity" | "children"> & { error: unknown }
Expand Down
4 changes: 2 additions & 2 deletions site/src/components/GitDeviceAuth/GitDeviceAuth.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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
9 changes: 6 additions & 3 deletions site/src/modules/provisioners/ProvisionerAlert.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { Theme } from "@emotion/react";
import AlertTitle from "@mui/material/AlertTitle";
import { Alert, type AlertColor } from "components/Alert/Alert";
import { 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";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
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
@@ -1,4 +1,3 @@
import AlertTitle from "@mui/material/AlertTitle";
import type {
DAUsResponse,
Experiment,
Expand All @@ -15,7 +14,7 @@ import { Stack } from "components/Stack/Stack";
import type { FC } from "react";
import { useDeploymentOptions } from "utils/deployOptions";
import { docs } from "utils/docs";
import { Alert } from "../../../components/Alert/Alert";
import { Alert, AlertTitle } from "../../../components/Alert/Alert";
import OptionsTable from "../OptionsTable";
import { UserEngagementChart } from "./UserEngagementChart";

Expand Down
4 changes: 2 additions & 2 deletions site/src/pages/SetupPage/SetupPageView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import GitHubIcon from "@mui/icons-material/GitHub";
import AlertTitle from "@mui/material/AlertTitle";

import Autocomplete from "@mui/material/Autocomplete";
import MuiButton from "@mui/material/Button";
import Checkbox from "@mui/material/Checkbox";
Expand All @@ -9,7 +9,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 { FormFields, VerticalForm } from "components/Form/Form";
import { CoderIcon } from "components/Icons/CoderIcon";
Expand Down
4 changes: 2 additions & 2 deletions site/src/pages/WorkspacePage/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import type { Interpolation, Theme } from "@emotion/react";
import { useTheme } from "@emotion/react";
import HistoryOutlined from "@mui/icons-material/HistoryOutlined";
import HubOutlined from "@mui/icons-material/HubOutlined";
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 { useSearchParamsKey } from "hooks/useSearchParamsKey";
import { ProvisionerStatusAlert } from "modules/provisioners/ProvisionerStatusAlert";
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy