Thanks to visit codestin.com
Credit goes to github.com

Skip to content
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
6 changes: 4 additions & 2 deletions docs/user-guides/workspace-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,10 @@ Admins may apply bulk operations (update, delete, start, stop) in the
checkboxes on the left, then use the top-right **Actions** dropdown to apply the
operation.

The start and stop operations can only be applied to a set of workspaces which
are all in the same state. For update and delete, the user will be prompted for
The start and stop operations can be applied even when the selected workspaces
are not all in the same state. Bulk start will only apply to selected workspaces
that are currently stopped, and bulk stop will only apply to selected workspaces
that are currently running. For update and delete, the user will be prompted for
confirmation before any action is taken.

![Bulk workspace actions](../images/user-guides/workspace-bulk-actions.png)
Expand Down
103 changes: 103 additions & 0 deletions site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,12 @@ const stoppedWorkspaces: Workspace[] = [
{ ...MockStoppedWorkspace, id: "3" },
];

const mixedStateWorkspaces: Workspace[] = [
{ ...MockStoppedWorkspace, id: "1" },
{ ...MockWorkspace, id: "2" },
{ ...MockStoppedWorkspace, id: "3" },
];

export const StartsOnlySelectedWorkspaces: Story = {
beforeEach: () => {
spyOn(API, "getWorkspaces").mockResolvedValue({
Expand Down Expand Up @@ -491,6 +497,103 @@ export const StartsOnlySelectedWorkspaces: Story = {
},
};

export const StartIgnoresAlreadyRunningWorkspaces: Story = {
beforeEach: () => {
spyOn(API, "getWorkspaces").mockResolvedValue({
workspaces: mixedStateWorkspaces,
count: mixedStateWorkspaces.length,
});
spyOn(API, "startWorkspace").mockResolvedValue(MockWorkspaceBuild);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(document.body);
const user = userEvent.setup();

await selectWorkspaces(canvas, user, ["1", "2", "3"]);
await openBulkActions(canvas, user);

const startItem = await body.findByRole("menuitem", { name: /start/i });
expect(startItem).not.toHaveAttribute("data-disabled");
await user.click(startItem);

await waitFor(() => expect(API.startWorkspace).toHaveBeenCalledTimes(2));
expect(API.startWorkspace).toHaveBeenCalledWith(
"1",
MockStoppedWorkspace.latest_build.template_version_id,
);
expect(API.startWorkspace).toHaveBeenCalledWith(
"3",
MockStoppedWorkspace.latest_build.template_version_id,
);
},
};

export const StopIgnoresAlreadyStoppedWorkspaces: Story = {
beforeEach: () => {
spyOn(API, "getWorkspaces").mockResolvedValue({
workspaces: mixedStateWorkspaces,
count: mixedStateWorkspaces.length,
});
spyOn(API, "stopWorkspace").mockResolvedValue(MockWorkspaceBuild);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(document.body);
const user = userEvent.setup();

await selectWorkspaces(canvas, user, ["1", "2", "3"]);
await openBulkActions(canvas, user);

const stopItem = await body.findByRole("menuitem", { name: /stop/i });
expect(stopItem).not.toHaveAttribute("data-disabled");
await user.click(stopItem);

await waitFor(() => expect(API.stopWorkspace).toHaveBeenCalledTimes(1));
expect(API.stopWorkspace).toHaveBeenCalledWith("2");
},
};

export const StartDisabledWhenNoWorkspacesAreStartable: Story = {
beforeEach: () => {
spyOn(API, "getWorkspaces").mockResolvedValue({
workspaces: runningWorkspaces,
count: runningWorkspaces.length,
});
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(document.body);
const user = userEvent.setup();

await selectWorkspaces(canvas, user, ["1", "2", "3"]);
await openBulkActions(canvas, user);

const startItem = await body.findByRole("menuitem", { name: /start/i });
expect(startItem).toHaveAttribute("data-disabled");
},
};

export const StopDisabledWhenNoWorkspacesAreStoppable: Story = {
beforeEach: () => {
spyOn(API, "getWorkspaces").mockResolvedValue({
workspaces: stoppedWorkspaces,
count: stoppedWorkspaces.length,
});
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(document.body);
const user = userEvent.setup();

await selectWorkspaces(canvas, user, ["1", "2", "3"]);
await openBulkActions(canvas, user);

const stopItem = await body.findByRole("menuitem", { name: /stop/i });
expect(stopItem).toHaveAttribute("data-disabled");
},
};

const appHealthStatuses: [WorkspaceAppHealth, boolean][] = [
["healthy", true],
["disabled", true],
Expand Down
4 changes: 2 additions & 2 deletions site/src/pages/WorkspacesPage/WorkspacesPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
<DropdownMenuContent align="end">
<DropdownMenuItem
disabled={
!checkedWorkspaces?.every(
!checkedWorkspaces?.some(
(w) =>
w.latest_build.status === "stopped" &&
!mustUpdateWorkspace(w, canChangeVersions),
Expand All @@ -157,7 +157,7 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
</DropdownMenuItem>
<DropdownMenuItem
disabled={
!checkedWorkspaces?.every(
!checkedWorkspaces?.some(
(w) => w.latest_build.status === "running",
)
}
Expand Down
14 changes: 10 additions & 4 deletions site/src/pages/WorkspacesPage/batchActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ export function useBatchActions(
const startAllMutation = useMutation({
mutationFn: (workspaces: readonly Workspace[]) => {
return Promise.all(
workspaces.map((w) =>
API.startWorkspace(w.id, w.latest_build.template_version_id),
),
workspaces
.filter((w) => w.latest_build.status === "stopped")
.map((w) =>
API.startWorkspace(w.id, w.latest_build.template_version_id),
),
);
},
onSuccess,
Expand All @@ -48,7 +50,11 @@ export function useBatchActions(

const stopAllMutation = useMutation({
mutationFn: (workspaces: readonly Workspace[]) => {
return Promise.all(workspaces.map((w) => API.stopWorkspace(w.id)));
return Promise.all(
workspaces
.filter((w) => w.latest_build.status === "running")
.map((w) => API.stopWorkspace(w.id)),
);
},
onSuccess,
onError: (error) => {
Expand Down
Loading