Skip to content

feat: batch workspace updates #11583

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

Merged
merged 14 commits into from
Jan 18, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const styles = {
}),
dialogContent: (theme) => ({
color: theme.palette.text.secondary,
padding: 40,
padding: "40px 40px 20px",
}),
dialogTitle: (theme) => ({
margin: 0,
Expand Down
4 changes: 2 additions & 2 deletions site/src/components/Dialogs/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ const styles = {
},

"&:hover:not(:disabled)": {
backgroundColor: theme.experimental.roles.danger.disabled.fill,
borderColor: theme.experimental.roles.danger.disabled.outline,
backgroundColor: theme.experimental.roles.danger.hover.fill,
borderColor: theme.experimental.roles.danger.hover.outline,
},

"&.Mui-disabled": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@ export const WorkspaceOutdatedTooltip: FC<TooltipProps> = (props) => {
);
};

export const WorkspaceOutdatedTooltipContent = (props: TooltipProps) => {
export const WorkspaceOutdatedTooltipContent: FC<TooltipProps> = ({
onUpdateVersion,
ariaLabel,
latestVersionId,
templateName,
}) => {
const popover = usePopover();
const { onUpdateVersion, ariaLabel, latestVersionId, templateName } = props;
const { data: activeVersion } = useQuery({
...templateVersion(latestVersionId),
enabled: popover.isOpen,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ export const DeleteDialog: Story = {
args: {
queryKey: ["tokens"],
token: MockToken,
setToken: () => {
return null;
},
setToken: () => null,
},
};
6 changes: 3 additions & 3 deletions site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useDashboard } from "components/Dashboard/DashboardProvider";
import { useFeatureVisibility } from "hooks/useFeatureVisibility";
import { FC, useEffect, useState } from "react";
import { type FC, useEffect, useState } from "react";
import { Helmet } from "react-helmet-async";
import { useNavigate } from "react-router-dom";
import { Workspace } from "./Workspace";
Expand Down Expand Up @@ -42,11 +42,11 @@ interface WorkspaceReadyPageProps {
permissions: WorkspacePermissions;
}

export const WorkspaceReadyPage = ({
export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
workspace,
template,
permissions,
}: WorkspaceReadyPageProps): JSX.Element => {
}) => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { buildInfo } = useDashboard();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { action } from "@storybook/addon-actions";
import type { Meta, StoryObj } from "@storybook/react";
import { chromatic } from "testHelpers/chromatic";
import { MockWorkspace, MockUser2 } from "testHelpers/entities";
import { BatchDeleteConfirmation } from "./BatchActions";
import { BatchDeleteConfirmation } from "./BatchDeleteConfirmation";

const meta: Meta<typeof BatchDeleteConfirmation> = {
title: "pages/WorkspacesPage/BatchDelete",
title: "pages/WorkspacesPage/BatchDeleteConfirmation",
parameters: { chromatic },
component: BatchDeleteConfirmation,
args: {
onClose: action("onClose"),
Expand Down Expand Up @@ -35,4 +37,4 @@ type Story = StoryObj<typeof BatchDeleteConfirmation>;

const Example: Story = {};

export { Example as BatchDelete };
export { Example as BatchDeleteConfirmation };
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,15 @@ import PersonOutlinedIcon from "@mui/icons-material/PersonOutlined";
import ScheduleIcon from "@mui/icons-material/Schedule";
import { visuallyHidden } from "@mui/utils";
import dayjs from "dayjs";
import "dayjs/plugin/relativeTime";
import { type Interpolation, type Theme } from "@emotion/react";
import relativeTime from "dayjs/plugin/relativeTime";
import { useTheme, type Interpolation, type Theme } from "@emotion/react";
import { type FC, type ReactNode, useState } from "react";
import { useMutation } from "react-query";
import { deleteWorkspace, startWorkspace, stopWorkspace } from "api/api";
import type { Workspace } from "api/typesGenerated";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { displayError } from "components/GlobalSnackbar/utils";
import { getResourceIconPath } from "utils/workspace";
import { Stack } from "components/Stack/Stack";
import { getResourceIconPath } from "utils/workspace";

interface UseBatchActionsProps {
onSuccess: () => Promise<void>;
}

export function useBatchActions(options: UseBatchActionsProps) {
const { onSuccess } = options;

const startAllMutation = useMutation({
mutationFn: async (workspaces: Workspace[]) => {
return Promise.all(
workspaces.map((w) =>
startWorkspace(w.id, w.latest_build.template_version_id),
),
);
},
onSuccess,
onError: () => {
displayError("Failed to start workspaces");
},
});

const stopAllMutation = useMutation({
mutationFn: async (workspaces: Workspace[]) => {
return Promise.all(workspaces.map((w) => stopWorkspace(w.id)));
},
onSuccess,
onError: () => {
displayError("Failed to stop workspaces");
},
});

const deleteAllMutation = useMutation({
mutationFn: async (workspaces: Workspace[]) => {
return Promise.all(workspaces.map((w) => deleteWorkspace(w.id)));
},
onSuccess,
onError: () => {
displayError("Failed to delete workspaces");
},
});

return {
startAll: startAllMutation.mutateAsync,
stopAll: stopAllMutation.mutateAsync,
deleteAll: deleteAllMutation.mutateAsync,
isLoading:
startAllMutation.isLoading ||
stopAllMutation.isLoading ||
deleteAllMutation.isLoading,
};
}
dayjs.extend(relativeTime);

type BatchDeleteConfirmationProps = {
checkedWorkspaces: Workspace[];
Expand Down Expand Up @@ -182,6 +129,8 @@ const Consequences: FC = () => {
};

const Workspaces: FC<StageProps> = ({ workspaces }) => {
const theme = useTheme();

const mostRecent = workspaces.reduce(
(latestSoFar, against) => {
if (!latestSoFar) {
Expand Down Expand Up @@ -209,7 +158,9 @@ const Workspaces: FC<StageProps> = ({ workspaces }) => {
alignItems="center"
justifyContent="space-between"
>
<span css={{ fontWeight: 500, color: "#fff" }}>
<span
css={{ fontWeight: 500, color: theme.experimental.l1.text }}
>
{workspace.name}
</span>
<Stack css={{ gap: 0, fontSize: 14, width: 128 }}>
Expand All @@ -234,7 +185,12 @@ const Workspaces: FC<StageProps> = ({ workspaces }) => {
</li>
))}
</ul>
<Stack justifyContent="center" direction="row" css={{ fontSize: 14 }}>
<Stack
justifyContent="center"
direction="row"
wrap="wrap"
css={{ gap: "6px 20px", fontSize: 14 }}
>
<Stack direction="row" alignItems="center" spacing={1}>
<PersonIcon />
<span>{ownersCount}</span>
Expand Down
75 changes: 75 additions & 0 deletions site/src/pages/WorkspacesPage/BatchUpdateConfirmation.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { action } from "@storybook/addon-actions";
import type { Meta, StoryObj } from "@storybook/react";
import { useQueryClient } from "react-query";
import { chromatic } from "testHelpers/chromatic";
import {
MockWorkspace,
MockRunningOutdatedWorkspace,
MockDormantOutdatedWorkspace,
MockOutdatedWorkspace,
MockTemplateVersion,
MockUser2,
} from "testHelpers/entities";
import {
BatchUpdateConfirmation,
type Update,
} from "./BatchUpdateConfirmation";

const workspaces = [
{ ...MockRunningOutdatedWorkspace, id: "1" },
{ ...MockDormantOutdatedWorkspace, id: "2" },
{ ...MockOutdatedWorkspace, id: "3" },
{ ...MockOutdatedWorkspace, id: "4" },
{ ...MockWorkspace, id: "5" },
{
...MockRunningOutdatedWorkspace,
id: "6",
owner_id: MockUser2.id,
owner_name: MockUser2.username,
},
];

const updates = new Map<string, Update>();
for (const it of workspaces) {
const versionId = it.template_active_version_id;
const version = updates.get(versionId);

if (version) {
version.affected_workspaces.push(it);
continue;
}

updates.set(versionId, {
...MockTemplateVersion,
template_display_name: it.template_display_name,
affected_workspaces: [it],
});
}

const meta: Meta<typeof BatchUpdateConfirmation> = {
title: "pages/WorkspacesPage/BatchUpdateConfirmation",
parameters: { chromatic },
component: BatchUpdateConfirmation,
decorators: [
(Story) => {
const queryClient = useQueryClient();
for (const [id, it] of updates) {
queryClient.setQueryData(["batchUpdate", id], it);
}
return <Story />;
},
],
args: {
onClose: action("onClose"),
onConfirm: action("onConfirm"),
open: true,
checkedWorkspaces: workspaces,
},
};

export default meta;
type Story = StoryObj<typeof BatchUpdateConfirmation>;

const Example: Story = {};

export { Example as BatchUpdateConfirmation };
Loading