Skip to content

Commit 41ea0de

Browse files
committed
fix: change emotion to Tailwind
1 parent 7e1a09c commit 41ea0de

File tree

3 files changed

+55
-193
lines changed

3 files changed

+55
-193
lines changed

site/src/pages/TasksPage/BatchDeleteConfirmation.stories.tsx

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { chromatic } from "testHelpers/chromatic";
2-
import { MockTask, MockWorkspace } from "testHelpers/entities";
2+
import { MockTask } from "testHelpers/entities";
33
import type { Meta, StoryObj } from "@storybook/react-vite";
44
import { action } from "storybook/actions";
55
import { userEvent, within } from "storybook/test";
@@ -39,23 +39,16 @@ const meta: Meta<typeof BatchDeleteConfirmation> = {
3939
).toISOString(),
4040
},
4141
],
42-
workspaces: [
43-
MockWorkspace,
44-
{
45-
...MockWorkspace,
46-
id: "workspace-2",
47-
name: "bob-workspace",
48-
},
49-
],
42+
workspaceCount: 2,
5043
},
5144
};
5245

5346
export default meta;
5447
type Story = StoryObj<typeof BatchDeleteConfirmation>;
5548

56-
const Stage1_Consequences: Story = {};
49+
export const Consequences: Story = {};
5750

58-
const Stage2_ReviewTasks: Story = {
51+
export const ReviewTasks: Story = {
5952
play: async ({ canvasElement, step }) => {
6053
const body = within(canvasElement.ownerDocument.body);
6154

@@ -68,7 +61,7 @@ const Stage2_ReviewTasks: Story = {
6861
},
6962
};
7063

71-
const Stage3_ReviewResources: Story = {
64+
export const ReviewResources: Story = {
7265
play: async ({ canvasElement, step }) => {
7366
const body = within(canvasElement.ownerDocument.body);
7467

@@ -87,9 +80,3 @@ const Stage3_ReviewResources: Story = {
8780
});
8881
},
8982
};
90-
91-
export {
92-
Stage1_Consequences as Consequences,
93-
Stage2_ReviewTasks as ReviewTasks,
94-
Stage3_ReviewResources as ReviewResources,
95-
};
Lines changed: 44 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
1-
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
2-
import { visuallyHidden } from "@mui/utils";
3-
import type { Task, Workspace } from "api/typesGenerated";
1+
import type { Task } from "api/typesGenerated";
42
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
5-
import { ExternalImage } from "components/ExternalImage/ExternalImage";
6-
import { Stack } from "components/Stack/Stack";
73
import dayjs from "dayjs";
84
import relativeTime from "dayjs/plugin/relativeTime";
95
import { ClockIcon, ServerIcon, UserIcon } from "lucide-react";
106
import { type FC, type ReactNode, useState } from "react";
11-
import { getResourceIconPath } from "utils/workspace";
127

138
dayjs.extend(relativeTime);
149

1510
type BatchDeleteConfirmationProps = {
1611
checkedTasks: readonly Task[];
17-
workspaces: readonly Workspace[];
12+
workspaceCount: number;
1813
open: boolean;
1914
isLoading: boolean;
2015
onClose: () => void;
@@ -23,7 +18,7 @@ type BatchDeleteConfirmationProps = {
2318

2419
export const BatchDeleteConfirmation: FC<BatchDeleteConfirmationProps> = ({
2520
checkedTasks,
26-
workspaces,
21+
workspaceCount,
2722
open,
2823
onClose,
2924
onConfirm,
@@ -56,38 +51,16 @@ export const BatchDeleteConfirmation: FC<BatchDeleteConfirmationProps> = ({
5651
confirmText = <>Confirm {taskCount}&hellip;</>;
5752
}
5853
if (stage === "resources") {
59-
const workspaceCount = workspaces.length;
60-
const resources = workspaces
61-
.map((workspace) => workspace.latest_build.resources.length)
62-
.reduce((a, b) => a + b, 0);
63-
const resourceCount = `${resources} ${
64-
resources === 1 ? "resource" : "resources"
65-
}`;
6654
const workspaceCountText = `${workspaceCount} ${
6755
workspaceCount === 1 ? "workspace" : "workspaces"
6856
}`;
6957
confirmText = (
7058
<>
71-
Delete {taskCount}, {workspaceCountText} and {resourceCount}
59+
Delete {taskCount} and {workspaceCountText}
7260
</>
7361
);
7462
}
7563

76-
// The flicker of these icons is quite noticeable if they aren't
77-
// loaded in advance, so we insert them into the document without
78-
// actually displaying them yet.
79-
const resourceIconPreloads = [
80-
...new Set(
81-
workspaces.flatMap((workspace) =>
82-
workspace.latest_build.resources.map(
83-
(resource) => resource.icon || getResourceIconPath(resource.type),
84-
),
85-
),
86-
),
87-
].map((url) => (
88-
<img key={url} alt="" aria-hidden css={{ ...visuallyHidden }} src={url} />
89-
));
90-
9164
return (
9265
<ConfirmDialog
9366
type="delete"
@@ -106,9 +79,10 @@ export const BatchDeleteConfirmation: FC<BatchDeleteConfirmationProps> = ({
10679
{stage === "consequences" && <Consequences />}
10780
{stage === "tasks" && <Tasks tasks={checkedTasks} />}
10881
{stage === "resources" && (
109-
<Resources tasks={checkedTasks} workspaces={workspaces} />
82+
<Resources tasks={checkedTasks} workspaceCount={workspaceCount} />
11083
)}
111-
{resourceIconPreloads}
84+
{/* Preload ServerIcon to prevent flicker on stage 3 */}
85+
<ServerIcon className="sr-only" aria-hidden />
11286
</>
11387
}
11488
/>
@@ -121,27 +95,24 @@ interface TasksStageProps {
12195

12296
interface ResourcesStageProps {
12397
tasks: readonly Task[];
124-
workspaces: readonly Workspace[];
98+
workspaceCount: number;
12599
}
126100

127101
const Consequences: FC = () => {
128102
return (
129103
<>
130104
<p>Deleting tasks is irreversible!</p>
131-
<ul css={styles.consequences}>
105+
<ul className="flex flex-col gap-2 pl-4 mb-0">
132106
<li>
133107
Tasks with associated workspaces will have those workspaces deleted.
134108
</li>
135-
<li>Terraform resources in task workspaces will be destroyed.</li>
136109
<li>Any data stored in task workspaces will be permanently deleted.</li>
137110
</ul>
138111
</>
139112
);
140113
};
141114

142115
const Tasks: FC<TasksStageProps> = ({ tasks }) => {
143-
const theme = useTheme();
144-
145116
const mostRecent = tasks.reduce(
146117
(latestSoFar, against) => {
147118
if (!latestSoFar) {
@@ -161,156 +132,66 @@ const Tasks: FC<TasksStageProps> = ({ tasks }) => {
161132

162133
return (
163134
<>
164-
<ul css={styles.tasksList}>
135+
<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]">
165136
{tasks.map((task) => (
166-
<li key={task.id} css={styles.task}>
167-
<Stack
168-
direction="row"
169-
alignItems="center"
170-
justifyContent="space-between"
171-
spacing={3}
172-
>
173-
<span
174-
css={{
175-
fontWeight: 500,
176-
color: theme.experimental.l1.text,
177-
maxWidth: 400,
178-
overflow: "hidden",
179-
textOverflow: "ellipsis",
180-
whiteSpace: "nowrap",
181-
}}
182-
>
183-
{task.initial_prompt}
137+
<li
138+
key={task.id}
139+
className="py-2 px-4 border-solid border-0 border-b border-zinc-200 dark:border-zinc-700 last:border-b-0"
140+
>
141+
<div className="flex items-center justify-between gap-6">
142+
<span className="font-medium text-content-primary max-w-[400px] overflow-hidden text-ellipsis whitespace-nowrap">
143+
{task.display_name}
184144
</span>
185145

186-
<Stack css={{ gap: 0, fontSize: 14 }} justifyContent="flex-end">
187-
<Stack
188-
direction="row"
189-
alignItems="center"
190-
justifyContent="flex-end"
191-
spacing={1}
192-
>
193-
<span css={{ whiteSpace: "nowrap" }}>{task.owner_name}</span>
194-
<PersonIcon />
195-
</Stack>
196-
<Stack
197-
direction="row"
198-
alignItems="center"
199-
spacing={1}
200-
justifyContent="flex-end"
201-
>
202-
<span css={{ whiteSpace: "nowrap" }}>
146+
<div className="flex flex-col text-sm items-end">
147+
<div className="flex items-center gap-2">
148+
<span className="whitespace-nowrap">{task.owner_name}</span>
149+
<UserIcon className="size-icon-sm -m-px" />
150+
</div>
151+
<div className="flex items-center gap-2">
152+
<span className="whitespace-nowrap">
203153
{dayjs(task.created_at).fromNow()}
204154
</span>
205155
<ClockIcon className="size-icon-xs" />
206-
</Stack>
207-
</Stack>
208-
</Stack>
156+
</div>
157+
</div>
158+
</div>
209159
</li>
210160
))}
211161
</ul>
212-
<Stack
213-
justifyContent="center"
214-
direction="row"
215-
wrap="wrap"
216-
css={{ gap: "6px 20px", fontSize: 14 }}
217-
>
218-
<Stack direction="row" alignItems="center" spacing={1}>
219-
<PersonIcon />
162+
<div className="flex flex-wrap justify-center gap-x-5 gap-y-1.5 text-sm">
163+
<div className="flex items-center gap-2">
164+
<UserIcon className="size-icon-sm -m-px" />
220165
<span>{ownersCount}</span>
221-
</Stack>
166+
</div>
222167
{mostRecent && (
223-
<Stack direction="row" alignItems="center" spacing={1}>
168+
<div className="flex items-center gap-2">
224169
<ClockIcon className="size-icon-xs" />
225170
<span>Last created {dayjs(mostRecent.created_at).fromNow()}</span>
226-
</Stack>
171+
</div>
227172
)}
228-
</Stack>
173+
</div>
229174
</>
230175
);
231176
};
232177

233-
const Resources: FC<ResourcesStageProps> = ({ tasks, workspaces }) => {
234-
const resources: Record<string, { count: number; icon: string }> = {};
235-
for (const workspace of workspaces) {
236-
for (const resource of workspace.latest_build.resources) {
237-
if (!resources[resource.type]) {
238-
resources[resource.type] = {
239-
count: 0,
240-
icon: resource.icon || getResourceIconPath(resource.type),
241-
};
242-
}
243-
244-
resources[resource.type].count++;
245-
}
246-
}
178+
const Resources: FC<ResourcesStageProps> = ({ tasks, workspaceCount }) => {
179+
const taskCount = tasks.length;
247180

248181
return (
249-
<Stack>
182+
<div className="flex flex-col gap-4">
250183
<p>
251-
Deleting {tasks.length === 1 ? "this task" : "these tasks"} will also
184+
Deleting {taskCount === 1 ? "this task" : "these tasks"} will also
252185
permanently destroy&hellip;
253186
</p>
254-
<Stack
255-
direction="row"
256-
justifyContent="center"
257-
wrap="wrap"
258-
css={{ gap: "6px 20px", fontSize: 14 }}
259-
>
260-
<Stack direction="row" alignItems="center" spacing={1}>
187+
<div className="flex flex-wrap justify-center gap-x-5 gap-y-1.5 text-sm">
188+
<div className="flex items-center gap-2">
261189
<ServerIcon className="size-icon-sm" />
262190
<span>
263-
{workspaces.length}{" "}
264-
{workspaces.length === 1 ? "workspace" : "workspaces"}
191+
{workspaceCount} {workspaceCount === 1 ? "workspace" : "workspaces"}
265192
</span>
266-
</Stack>
267-
{Object.entries(resources).map(([type, summary]) => (
268-
<Stack key={type} direction="row" alignItems="center" spacing={1}>
269-
<ExternalImage
270-
src={summary.icon}
271-
width={styles.summaryIcon.width}
272-
height={styles.summaryIcon.height}
273-
/>
274-
<span>
275-
{summary.count} <code>{type}</code>
276-
</span>
277-
</Stack>
278-
))}
279-
</Stack>
280-
</Stack>
193+
</div>
194+
</div>
195+
</div>
281196
);
282197
};
283-
284-
const PersonIcon: FC = () => {
285-
return <UserIcon className="size-icon-sm" css={{ margin: -1 }} />;
286-
};
287-
288-
const styles = {
289-
summaryIcon: { width: 16, height: 16 },
290-
291-
consequences: {
292-
display: "flex",
293-
flexDirection: "column",
294-
gap: 8,
295-
paddingLeft: 16,
296-
marginBottom: 0,
297-
},
298-
299-
tasksList: (theme) => ({
300-
listStyleType: "none",
301-
padding: 0,
302-
border: `1px solid ${theme.palette.divider}`,
303-
borderRadius: 8,
304-
overflow: "hidden auto",
305-
maxHeight: 184,
306-
}),
307-
308-
task: (theme) => ({
309-
padding: "8px 16px",
310-
borderBottom: `1px solid ${theme.palette.divider}`,
311-
312-
"&:last-child": {
313-
border: "none",
314-
},
315-
}),
316-
} satisfies Record<string, Interpolation<Theme>>;

site/src/pages/TasksPage/TasksPage.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { API } from "api/api";
22
import { templates } from "api/queries/templates";
3-
import { workspaces } from "api/queries/workspaces";
3+
44
import type { TasksFilter } from "api/typesGenerated";
55
import { Badge } from "components/Badge/Badge";
66
import { Button, type ButtonProps } from "components/Button/Button";
@@ -91,16 +91,10 @@ const TasksPage: FC = () => {
9191
const { entitlements } = useDashboard();
9292
const canCheckTasks = entitlements.features.task_batch_actions.enabled;
9393

94-
// Workspaces are fetched lazily only when dialog opens because tasks
95-
// don't always have associated workspaces and we need full resource data
96-
// for the confirmation dialog.
97-
const workspaceIds = checkedTasks
98-
.map((t) => t.workspace_id)
99-
.filter((id): id is string => id !== null);
100-
const workspacesQuery = useQuery({
101-
...workspaces({ q: `id:${workspaceIds.join(",")}` }),
102-
enabled: workspaceIds.length > 0 && isDeleteDialogOpen,
103-
});
94+
// Count workspaces that will be deleted with the selected tasks.
95+
const workspaceCount = checkedTasks.filter(
96+
(t) => t.workspace_id !== null,
97+
).length;
10498

10599
// Clear selections when switching tabs/filters to avoid confusing UX
106100
// where selected tasks might no longer be visible.
@@ -232,7 +226,7 @@ const TasksPage: FC = () => {
232226
<BatchDeleteConfirmation
233227
open={isDeleteDialogOpen}
234228
checkedTasks={checkedTasks}
235-
workspaces={workspacesQuery.data?.workspaces ?? []}
229+
workspaceCount={workspaceCount}
236230
isLoading={batchActions.isProcessing}
237231
onClose={() => setIsDeleteDialogOpen(false)}
238232
onConfirm={handleConfirmDelete}

0 commit comments

Comments
 (0)