Skip to content

Commit e63d267

Browse files
committed
WIP: 4.2 (needs testing with deployed spicedb, also can use individual stop buttons on workspaces)
1 parent 1e57920 commit e63d267

File tree

3 files changed

+202
-15
lines changed

3 files changed

+202
-15
lines changed

components/dashboard/src/org-admin/RunningWorkspacesCard.tsx

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { FC, useEffect, useMemo } from "react";
7+
import { FC, useEffect, useMemo, useState } from "react";
88
import dayjs from "dayjs";
99
import { WorkspaceSession, WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
10+
import { workspaceClient } from "../service/public-api";
1011
import { useWorkspaceSessions } from "../data/insights/list-workspace-sessions-query";
12+
import { Button } from "@podkit/buttons/Button";
13+
import ConfirmationModal from "../components/ConfirmationModal";
14+
import { useToast } from "../components/toasts/Toasts";
1115
import { Item, ItemField, ItemsList } from "../components/ItemsList";
1216
import Alert from "../components/Alert";
1317
import Spinner from "../icons/Spinner.svg";
@@ -24,10 +28,14 @@ const isWorkspaceNotStopped = (session: WorkspaceSession): boolean => {
2428

2529
export const RunningWorkspacesCard: FC<RunningWorkspacesCardProps> = () => {
2630
const lookbackHours = 48;
31+
const [isStopAllModalOpen, setIsStopAllModalOpen] = useState(false);
32+
const [isStoppingAll, setIsStoppingAll] = useState(false);
33+
const toast = useToast();
2734

28-
const { data, fetchNextPage, hasNextPage, isLoading, isError, error, isFetchingNextPage } = useWorkspaceSessions({
29-
from: Timestamp.fromDate(dayjs().subtract(lookbackHours, "hours").startOf("day").toDate()),
30-
});
35+
const { data, fetchNextPage, hasNextPage, isLoading, isError, error, isFetchingNextPage, refetch } =
36+
useWorkspaceSessions({
37+
from: Timestamp.fromDate(dayjs().subtract(lookbackHours, "hours").startOf("day").toDate()),
38+
});
3139

3240
useEffect(() => {
3341
if (hasNextPage && !isFetchingNextPage) {
@@ -43,7 +51,51 @@ export const RunningWorkspacesCard: FC<RunningWorkspacesCardProps> = () => {
4351
return allSessions.filter(isWorkspaceNotStopped);
4452
}, [data]);
4553

46-
if (isLoading) {
54+
const handleStopAllWorkspaces = async () => {
55+
if (runningWorkspaces.length === 0) {
56+
toast.toast({ type: "error", message: "No running workspaces to stop." });
57+
setIsStopAllModalOpen(false);
58+
return;
59+
}
60+
61+
setIsStoppingAll(true);
62+
let successCount = 0;
63+
let errorCount = 0;
64+
65+
const stopPromises = runningWorkspaces.map(async (session) => {
66+
if (session.workspace?.id) {
67+
try {
68+
await workspaceClient.stopWorkspace({ workspaceId: session.workspace.id });
69+
successCount++;
70+
} catch (e) {
71+
console.error(`Failed to stop workspace ${session.workspace.id}:`, e);
72+
errorCount++;
73+
}
74+
}
75+
});
76+
77+
await Promise.allSettled(stopPromises);
78+
79+
setIsStoppingAll(false);
80+
setIsStopAllModalOpen(false);
81+
82+
if (errorCount > 0) {
83+
toast.toast({
84+
type: "error",
85+
message: `Failed to stop all workspaces`,
86+
description: `Attempted to stop ${runningWorkspaces.length} workspaces. ${successCount} stopped, ${errorCount} failed.`,
87+
});
88+
} else {
89+
toast.toast({
90+
type: "success",
91+
message: `Stop command sent`,
92+
description: `Successfully sent stop command for ${successCount} workspaces.`,
93+
});
94+
}
95+
refetch();
96+
};
97+
98+
if (isLoading && !isStoppingAll) {
4799
return (
48100
<div className="flex items-center justify-center w-full space-x-2 text-gray-400 text-sm p-8">
49101
<img alt="Loading Spinner" className="h-4 w-4 animate-spin" src={Spinner} />
@@ -63,10 +115,19 @@ export const RunningWorkspacesCard: FC<RunningWorkspacesCardProps> = () => {
63115

64116
return (
65117
<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg p-4 mt-6">
66-
<h3 className="text-lg font-semibold text-gray-700 dark:text-gray-200 mb-3">
67-
Currently Running Workspaces ({runningWorkspaces.length})
68-
</h3>
69-
{runningWorkspaces.length === 0 ? (
118+
<div className="flex justify-between items-center mb-3">
119+
<h3 className="text-lg font-semibold text-gray-700 dark:text-gray-200">
120+
Currently Running Workspaces ({runningWorkspaces.length})
121+
</h3>
122+
<Button
123+
variant="destructive"
124+
onClick={() => setIsStopAllModalOpen(true)}
125+
disabled={isStoppingAll || isLoading || runningWorkspaces.length === 0}
126+
>
127+
Stop All Workspaces
128+
</Button>
129+
</div>
130+
{runningWorkspaces.length === 0 && !isLoading ? (
70131
<p className="text-gray-500 dark:text-gray-400">No workspaces are currently running.</p>
71132
) : (
72133
<ItemsList className="text-gray-400 dark:text-gray-500">
@@ -116,6 +177,21 @@ export const RunningWorkspacesCard: FC<RunningWorkspacesCardProps> = () => {
116177
})}
117178
</ItemsList>
118179
)}
180+
<ConfirmationModal
181+
title="Confirm Stop All Workspaces"
182+
visible={isStopAllModalOpen}
183+
onClose={() => setIsStopAllModalOpen(false)}
184+
onConfirm={handleStopAllWorkspaces}
185+
buttonText={isStoppingAll ? "Stopping..." : "Confirm Stop All"}
186+
buttonType="destructive"
187+
buttonDisabled={isStoppingAll}
188+
>
189+
<p className="text-sm text-gray-600 dark:text-gray-300">
190+
Are you sure you want to stop all {runningWorkspaces.length} currently running workspaces in this
191+
organization? Workspaces will be backed up before stopping. This action cannot be undone for the
192+
stop process itself.
193+
</p>
194+
</ConfirmationModal>
119195
</div>
120196
);
121197
};

prd/001-infra-rollout-4.2.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# PDD: Infrastructure Rollout - Task 4.2: Stop All Running Workspaces
2+
3+
- **Associated PRD**: [prd/001-infra-rollout.md](./001-infra-rollout.md) - Section 4.2
4+
- **Date**: May 8, 2025
5+
- **Author**: Cline
6+
- **Version**: 1.0
7+
- **Status**: Draft
8+
9+
## 1. Overview
10+
This document outlines the design for implementing the "Stop All Running Workspaces" feature for administrators. This feature allows an administrator to initiate a stop command for all currently running workspaces within their organization. This implementation will leverage frontend orchestration, using existing APIs.
11+
12+
## 2. Background
13+
As part of improving the infrastructure update rollout experience (PRD Ref: [CLC-1275](https://linear.app/gitpod/issue/CLC-1275/admin-stop-all-running-workspaces-button-for-infra-update)), administrators need a way to ensure all workspaces are safely stopped (and thus backed up) before an update. This feature provides that capability.
14+
15+
## 3. Proposed Design & Implementation
16+
17+
### 3.1. Approach: Frontend Orchestration
18+
This feature will be implemented primarily in the frontend (`components/dashboard/`). The frontend will:
19+
1. Fetch the list of currently running/active workspaces.
20+
2. Upon admin confirmation, iterate through this list.
21+
3. For each workspace, call the existing public API `workspaceClient.stopWorkspace()` to request a graceful stop.
22+
23+
This approach is viable because the SpiceDB schema (`components/spicedb/schema/schema.yaml`) confirms that an organization owner (`org->owner`) has the `stop` permission on workspaces belonging to their organization, which is enforced by the backend's `WorkspaceService.stopWorkspace` method.
24+
25+
### 3.2. Affected Code Units (Frontend - `components/dashboard/`)
26+
27+
- **`src/org-admin/AdminPage.tsx`**:
28+
* This page already hosts the `RunningWorkspacesCard.tsx` (as per PDD `001-infra-rollout-4.1.md`). No direct changes are needed here for this specific feature, as the functionality will be encapsulated within `RunningWorkspacesCard.tsx`.
29+
- **`src/org-admin/RunningWorkspacesCard.tsx`**:
30+
* This existing component (detailed in PDD `001-infra-rollout-4.1.md`) already fetches and displays running/active workspace sessions using the `useWorkspaceSessions` hook.
31+
* It will be **enhanced** to include the "Stop All Running Workspaces" button and its associated logic.
32+
33+
### 3.3. Key Modifications to `RunningWorkspacesCard.tsx`
34+
35+
- **New UI Elements:**
36+
* **"Stop All Running Workspaces" Button:**
37+
* To be placed prominently within the card (e.g., in the card header or a dedicated action row).
38+
* Label: "Stop All Running Workspaces".
39+
* **Confirmation Dialog:**
40+
* A modal dialog will appear upon clicking the button.
41+
* It will clearly explain the action (e.g., "This will attempt to stop all currently running workspaces in your organization. Workspaces are backed up before stopping. This action cannot be undone for the stop process itself.") and require explicit confirmation (e.g., "Confirm Stop All" button).
42+
43+
- **New Logic:**
44+
1. **Handle "Stop All" Action (on confirmed dialog):**
45+
* The `useWorkspaceSessions` hook's data (`data.pages`) is already available within this component.
46+
* Flatten the session pages: `const allSessions = data.pages.flatMap(page => page);`
47+
* Filter for "not stopped" workspaces (this filtering logic may already exist for display purposes):
48+
```typescript
49+
const notStoppedSessions = allSessions.filter(session =>
50+
session.workspace?.status?.phase?.name !== WorkspacePhase_Phase.STOPPED
51+
);
52+
```
53+
* Iterate through `notStoppedSessions`. For each `session` where `session.workspace?.id` is valid:
54+
* Call `workspaceClient.stopWorkspace({ workspaceId: session.workspace.id })`.
55+
* The `workspaceClient` is imported from `../../service/public-api` (as seen in `list-workspace-sessions-query.ts`).
56+
* Handle individual API call responses:
57+
* Track successes and failures.
58+
* Update the UI to provide feedback (e.g., a progress indicator, a list of workspaces being processed, or a summary toast/notification).
59+
* Provide overall feedback to the administrator (e.g., "Stop command sent for X workspaces. Successes: Y, Failures: Z.").
60+
* The list of running workspaces displayed by this card should update automatically as `useWorkspaceSessions` refetches or its cache is updated by `react-query` after the stop actions. A manual refetch can also be triggered if necessary.
61+
62+
### 3.4. Backend Interaction (`components/server/`)
63+
- **No new dedicated backend API endpoint** is required for the "stop-all" action itself.
64+
- The frontend will use the existing `workspaceClient.stopWorkspace()` method, which calls the public `StopWorkspace` RPC.
65+
- The backend's `WorkspaceService.stopWorkspace` method, along with the `Authorizer` and SpiceDB schema, already handles the necessary permission checks to ensure an organization owner can stop workspaces within their organization.
66+
- The interlock with Maintenance Mode (i.e., disabling this button if Maintenance Mode is not active) will be handled as part of Feature 4.3's implementation.
67+
68+
### 3.5. Diagram of "Stop All" Flow
69+
70+
```mermaid
71+
graph TD
72+
Admin[Admin User] -- Clicks --> Btn["Stop All Workspaces Button (in RunningWorkspacesCard)"]
73+
Admin -- Confirms --> ConfirmDlg["Confirmation Dialog"]
74+
ConfirmDlg -- Triggers --> Iterate[Iterate Filtered Workspaces (from useWorkspaceSessions)]
75+
Iterate -- For Each Workspace --> CallAPI["Call workspaceClient.stopWorkspace({workspaceId})"]
76+
CallAPI -- Interacts With --> BackendAPI["Existing Public StopWorkspace RPC (Server)"]
77+
BackendAPI -- Checks Permissions --> SpiceDB[(SpiceDB: org_owner can stop)]
78+
BackendAPI -- Executes Stop --> WSStop[Workspace Stop Logic (ws-manager, etc.)]
79+
CallAPI -- Updates --> UIFeedback[UI Feedback (Progress, Success/Failure)]
80+
WSStop -- Eventually Updates --> WSList[Running Workspaces List (Refreshed)]
81+
```
82+
83+
## 4. Advantages of this Approach
84+
- **Reduced Backend Complexity:** No need to design, implement, and test a new backend API endpoint specifically for stopping all workspaces.
85+
- **Leverages Existing Infrastructure:** Utilizes the existing, tested `StopWorkspace` public API and its permission model.
86+
- **Clear Permission Model:** Relies on the confirmed SpiceDB definition where organization owners can stop workspaces in their org.
87+
88+
## 5. Testing Strategy
89+
- **Manual Testing:**
90+
* Verify the "Stop All Running Workspaces" button is present in the `RunningWorkspacesCard`.
91+
* Verify clicking the button shows a confirmation dialog with appropriate explanatory text.
92+
* Verify that confirming the dialog triggers calls to `workspaceClient.stopWorkspace()` for all displayed "not stopped" workspaces.
93+
* Verify appropriate UI feedback during and after the stop operations (e.g., progress, success/error messages for individual stops or overall).
94+
* Verify the list of running workspaces in `RunningWorkspacesCard` updates correctly after workspaces are stopped.
95+
* Test with various scenarios: no running workspaces, a few running workspaces, many running workspaces (if feasible in a test environment).
96+
* Test error handling if individual `stopWorkspace` calls fail.
97+
98+
## 6. Rollout Plan
99+
- This enhancement to `RunningWorkspacesCard.tsx` will be part of a standard `components/dashboard/` component update, released alongside the other "Infrastructure Rollout" features.
100+
101+
## 7. Open Questions & Risks
102+
- **UI Feedback for Batch Operation:** Resolved. Assumed stopping will be quick. A toast notification will be shown once `stopWorkspace` has been called on all targeted workspaces.
103+
- **Rate Limiting/Concurrency:** Resolved. Considered not a problem at this time.
104+
- **Dependency on Maintenance Mode API:** The requirement for this button to be disabled if Maintenance Mode is not active (R3.4 in the main PRD) has been moved to be implemented as part of Feature 4.3 (Maintenance Mode Toggle). This PDD assumes the button is always enabled for an admin, and the interlock will be added later.
105+
106+
## 8. Future Considerations
107+
- If performance issues arise with stopping a very large number of workspaces via individual frontend calls, a backend batch operation could be reconsidered in the future, but the current approach is preferred for its simplicity.

prd/001-infra-rollout.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ An "Admin" section will be added to the Gitpod organization menu. This section w
3737
* **R3.1:** Administrators must have an option (e.g., a button) to stop all currently running workspaces within their organization.
3838
* **R3.2:** This action is intended to ensure all running workspaces are backed up before an infrastructure update.
3939
* **R3.3:** The UI should provide a clear explanation of what this action does and its implications.
40-
* **R3.4:** This functionality must be disabled if Maintenance Mode is not active. It should only be usable when Maintenance Mode is enabled.
4140

4241
### 4.3. Maintenance Mode Toggle (Ref: [CLC-1273](https://linear.app/gitpod/issue/CLC-1273/admin-maintenance-mode-toggle))
4342
* **R2.1:** Administrators must be able to manually enable or disable a "Maintenance Mode" for their Gitpod instance.
4443
* **R2.2:** When Maintenance Mode is enabled:
4544
* Users must be prevented from starting new workspaces.
4645
* A clear warning or notification must be displayed on the dashboard indicating that the system is in maintenance.
46+
* The "Stop All Running Workspaces" button (from feature 4.2) must be enabled; otherwise, it must be disabled.
4747
* **R2.3:** This toggle allows administrators to control the state before, during, and after an update.
4848

4949
### 4.4. Schedule Maintenance Notification (Optional) (Ref: [CLC-1274](https://linear.app/gitpod/issue/CLC-1274/admin-schedule-maintenance-notification))
@@ -84,14 +84,15 @@ An "Admin" section will be added to the Gitpod organization menu. This section w
8484
| **4.1 View Running Workspaces** | Done | Cline | [001-infra-rollout-4.1.md](pdd/001-infra-rollout-4.1.md) |
8585
| - API: Fetch running workspaces | | | |
8686
| - UI: Display running workspaces | | | |
87-
| **4.2 Stop All Running Workspaces** | | | |
88-
| - API: Trigger stop all workspaces | | | |
89-
| - Logic: Iterate and stop workspaces | | | |
90-
| - UI: Button (disabled if Maint. Mode off) & Confirm | | | |
87+
| **4.2 Stop All Running Workspaces** | | | [001-infra-rollout-4.2.md](pdd/001-infra-rollout-4.2.md) |
88+
| - API: Trigger stop all workspaces | Done (N/A) | Cline | (Leverages existing StopWorkspace API) |
89+
| - Logic: Iterate and stop workspaces | | | (Frontend orchestration) |
90+
| - UI: Button & Confirm | | | (Integrated into RunningWorkspacesCard) |
9191
| **4.3 Maintenance Mode Toggle** | | | |
9292
| - API: Get/Set Maintenance Mode | | | |
9393
| - Logic: Prevent new workspace starts | | | |
9494
| - UI: Toggle & Dashboard Banner | | | |
95+
| - UI: Enable/disable "Stop All Workspaces" button | | | (Logic within RunningWorkspacesCard to check Maint. Mode) |
9596
| **4.4 Schedule Maintenance Notification (Optional)** | | | |
9697
| - API: Get/Set Notification | | | |
9798
| - UI: Form for scheduling & Dashboard Banner | | | |
@@ -103,4 +104,7 @@ An "Admin" section will be added to the Gitpod organization menu. This section w
103104

104105
## 10. Open Questions
105106

106-
No open questions at this time.
107+
- **Resolved (for 4.2):**
108+
- UI Feedback for "Stop All": A toast notification after all `stopWorkspace` calls are initiated.
109+
- Rate Limiting for "Stop All": Considered not an issue for now.
110+
- Dependency of "Stop All" (4.2) on Maintenance Mode API (4.3): The "Stop All" button's enabled/disabled state (based on Maintenance Mode) is now a requirement of feature 4.3. Feature 4.2 will implement the button, and feature 4.3 will implement the logic to enable/disable it based on Maintenance Mode status.

0 commit comments

Comments
 (0)