refactor(site): convert workspace batch delete dialog to Tailwind CSS (#20946)

Converts from Emotion to Tailwind CSS, based on the tasks batch delete
dialog implementation.

Also propagates simplifications back to the tasks dialog:
- Use `border-border` instead of hardcoded color variants
- Use `max-h-48` instead of specific `max-h-[184px]`
- Add cancel button to workspaces dialog

Refs #20905
This commit is contained in:
Mathias Fredriksson
2025-12-04 20:10:21 +02:00
committed by GitHub
parent c4bf5a2d81
commit 0ab23abb19
3 changed files with 38 additions and 110 deletions
@@ -113,11 +113,11 @@ const Tasks: FC<TasksStageProps> = ({ tasks }) => {
return (
<>
<ul className="list-none p-0 border border-solid border-zinc-200 dark:border-zinc-700 rounded-lg overflow-x-hidden overflow-y-auto max-h-[184px]">
<ul className="list-none p-0 border border-solid border-border rounded-lg overflow-x-hidden overflow-y-auto max-h-48">
{tasks.map((task) => (
<li
key={task.id}
className="py-2 px-4 border-solid border-0 border-b border-zinc-200 dark:border-zinc-700 last:border-b-0"
className="py-2 px-4 border-solid border-0 border-b border-border last:border-b-0"
>
<div className="flex items-center justify-between gap-6">
<span className="font-medium text-content-primary max-w-[400px] overflow-hidden text-ellipsis whitespace-nowrap">
@@ -1,9 +1,6 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import { visuallyHidden } from "@mui/utils";
import type { Workspace } from "api/typesGenerated";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { Stack } from "components/Stack/Stack";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { ClockIcon, UserIcon } from "lucide-react";
@@ -67,7 +64,7 @@ export const BatchDeleteConfirmation: FC<BatchDeleteConfirmationProps> = ({
);
}
// The flicker of these icons is quit noticeable if they aren't loaded in advance,
// The flicker of these icons is quite noticeable if they aren't loaded in advance,
// so we insert them into the document without actually displaying them yet.
const resourceIconPreloads = [
...new Set(
@@ -78,7 +75,7 @@ export const BatchDeleteConfirmation: FC<BatchDeleteConfirmationProps> = ({
),
),
].map((url) => (
<img key={url} alt="" aria-hidden css={{ ...visuallyHidden }} src={url} />
<img key={url} alt="" aria-hidden className="sr-only" src={url} />
));
return (
@@ -90,7 +87,6 @@ export const BatchDeleteConfirmation: FC<BatchDeleteConfirmationProps> = ({
onClose();
}}
title={`Delete ${workspaceCount}`}
hideCancel
confirmLoading={isLoading}
confirmText={confirmText}
onConfirm={onProceed}
@@ -118,7 +114,7 @@ const Consequences: FC = () => {
return (
<>
<p>Deleting workspaces is irreversible!</p>
<ul css={styles.consequences}>
<ul className="flex flex-col gap-2 pl-4 mb-0">
<li>
Terraform resources belonging to deleted workspaces will be destroyed.
</li>
@@ -129,8 +125,6 @@ const Consequences: FC = () => {
};
const Workspaces: FC<StageProps> = ({ workspaces }) => {
const theme = useTheme();
const mostRecent = workspaces.reduce(
(latestSoFar, against) => {
if (!latestSoFar) {
@@ -150,69 +144,47 @@ const Workspaces: FC<StageProps> = ({ workspaces }) => {
return (
<>
<ul css={styles.workspacesList}>
<ul className="list-none p-0 border border-solid border-border rounded-lg overflow-x-hidden overflow-y-auto max-h-48">
{workspaces.map((workspace) => (
<li key={workspace.id} css={styles.workspace}>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
spacing={3}
>
<span
css={{ fontWeight: 500, color: theme.experimental.l1.text }}
>
<li
key={workspace.id}
className="py-2 px-4 border-solid border-0 border-b border-border last:border-b-0"
>
<div className="flex items-center justify-between gap-6">
<span className="font-medium text-content-primary max-w-[400px] overflow-hidden text-ellipsis whitespace-nowrap">
{workspace.name}
</span>
<Stack css={{ gap: 0, fontSize: 14 }} justifyContent="flex-end">
<Stack
direction="row"
alignItems="center"
justifyContent="flex-end"
spacing={1}
>
<span
css={{ whiteSpace: "nowrap", textOverflow: "ellipsis" }}
>
<div className="flex flex-col text-sm items-end">
<div className="flex items-center gap-2">
<span className="whitespace-nowrap">
{workspace.owner_name}
</span>
<PersonIcon />
</Stack>
<Stack
direction="row"
alignItems="center"
spacing={1}
justifyContent="flex-end"
>
<span
css={{ whiteSpace: "nowrap", textOverflow: "ellipsis" }}
>
<UserIcon className="size-icon-sm -m-px" />
</div>
<div className="flex items-center gap-2">
<span className="whitespace-nowrap">
{dayjs(workspace.last_used_at).fromNow()}
</span>
<ClockIcon className="size-icon-xs" />
</Stack>
</Stack>
</Stack>
</div>
</div>
</div>
</li>
))}
</ul>
<Stack
justifyContent="center"
direction="row"
wrap="wrap"
css={{ gap: "6px 20px", fontSize: 14 }}
>
<Stack direction="row" alignItems="center" spacing={1}>
<PersonIcon />
<div className="flex flex-wrap justify-center gap-x-5 gap-y-1.5 text-sm">
<div className="flex items-center gap-2">
<UserIcon className="size-icon-sm -m-px" />
<span>{ownersCount}</span>
</Stack>
</div>
{mostRecent && (
<Stack direction="row" alignItems="center" spacing={1}>
<div className="flex items-center gap-2">
<ClockIcon className="size-icon-xs" />
<span>Last used {dayjs(mostRecent.last_used_at).fromNow()}</span>
</Stack>
</div>
)}
</Stack>
</div>
</>
);
};
@@ -233,66 +205,22 @@ const Resources: FC<StageProps> = ({ workspaces }) => {
}
return (
<Stack>
<div className="flex flex-col gap-4">
<p>
Deleting{" "}
{workspaces.length === 1 ? "this workspace" : "these workspaces"} will
also permanently destroy&hellip;
</p>
<Stack
direction="row"
justifyContent="center"
wrap="wrap"
css={{ gap: "6px 20px", fontSize: 14 }}
>
<div className="flex flex-wrap justify-center gap-x-5 gap-y-1.5 text-sm">
{Object.entries(resources).map(([type, summary]) => (
<Stack key={type} direction="row" alignItems="center" spacing={1}>
<ExternalImage
src={summary.icon}
width={styles.summaryIcon.width}
height={styles.summaryIcon.height}
/>
<div key={type} className="flex items-center gap-2">
<ExternalImage src={summary.icon} width={16} height={16} />
<span>
{summary.count} <code>{type}</code>
</span>
</Stack>
</div>
))}
</Stack>
</Stack>
</div>
</div>
);
};
const PersonIcon: FC = () => {
// Using the Lucide icon with appropriate size class
return <UserIcon className="size-icon-sm" css={{ margin: -1 }} />;
};
const styles = {
summaryIcon: { width: 16, height: 16 },
consequences: {
display: "flex",
flexDirection: "column",
gap: 8,
paddingLeft: 16,
marginBottom: 0,
},
workspacesList: (theme) => ({
listStyleType: "none",
padding: 0,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8,
overflow: "hidden auto",
maxHeight: 184,
}),
workspace: (theme) => ({
padding: "8px 16px",
borderBottom: `1px solid ${theme.palette.divider}`,
"&:last-child": {
border: "none",
},
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -146,7 +146,7 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
disabled={isRunningBatchAction}
variant="outline"
size="sm"
css={{ borderRadius: 9999, marginLeft: "auto" }}
className="ml-auto"
>
Bulk actions
<Spinner loading={isRunningBatchAction}>