Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/org-admin/RunningWorkspacesCard.tsx
2499 views
1
/**
2
* Copyright (c) Gitpod GmbH. All rights reserved.
3
* Licensed under the GNU Affero General Public License (AGPL).
4
* See License.AGPL.txt in the project root for license information.
5
*/
6
7
import { FC, useEffect, useMemo, useState } from "react";
8
import dayjs from "dayjs";
9
import { WorkspaceSession, WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
10
import { workspaceClient } from "../service/public-api";
11
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";
15
import { useMaintenanceMode } from "../data/maintenance-mode/maintenance-mode-query";
16
import { Item, ItemField, ItemsList } from "../components/ItemsList";
17
import Alert from "../components/Alert";
18
import Spinner from "../icons/Spinner.svg";
19
import { toRemoteURL } from "../projects/render-utils";
20
import { displayTime } from "../usage/UsageEntry";
21
import { Timestamp } from "@bufbuild/protobuf";
22
import { WorkspaceStatusIndicator } from "../workspaces/WorkspaceStatusIndicator";
23
import { ConfigurationSettingsField } from "../repositories/detail/ConfigurationSettingsField";
24
import { Heading3 } from "../components/typography/headings";
25
import Tooltip from "../components/Tooltip";
26
27
const isWorkspaceNotStopped = (session: WorkspaceSession): boolean => {
28
return session.workspace?.status?.phase?.name !== WorkspacePhase_Phase.STOPPED;
29
};
30
31
export const RunningWorkspacesCard: FC<{}> = () => {
32
const lookbackHours = 48;
33
const [isStopAllModalOpen, setIsStopAllModalOpen] = useState(false);
34
const [isStoppingAll, setIsStoppingAll] = useState(false);
35
const toast = useToast();
36
const { isMaintenanceMode } = useMaintenanceMode();
37
38
const { data, fetchNextPage, hasNextPage, isLoading, isError, error, isFetchingNextPage, refetch } =
39
useWorkspaceSessions({
40
from: Timestamp.fromDate(dayjs().subtract(lookbackHours, "hours").startOf("day").toDate()),
41
});
42
43
useEffect(() => {
44
if (hasNextPage && !isFetchingNextPage) {
45
fetchNextPage();
46
}
47
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
48
49
const runningWorkspaces = useMemo(() => {
50
if (!data?.pages) {
51
return [];
52
}
53
const allSessions = data.pages.flatMap((page) => page);
54
return allSessions.filter(isWorkspaceNotStopped);
55
}, [data]);
56
57
const handleStopAllWorkspaces = async () => {
58
if (runningWorkspaces.length === 0) {
59
toast.toast({ type: "error", message: "No running workspaces to stop." });
60
setIsStopAllModalOpen(false);
61
return;
62
}
63
64
setIsStoppingAll(true);
65
let successCount = 0;
66
let errorCount = 0;
67
68
const stopPromises = runningWorkspaces.map(async (session) => {
69
if (session.workspace?.id) {
70
try {
71
await workspaceClient.stopWorkspace({ workspaceId: session.workspace.id });
72
successCount++;
73
} catch (e) {
74
console.error(`Failed to stop workspace ${session.workspace.id}:`, e);
75
errorCount++;
76
}
77
}
78
});
79
80
await Promise.allSettled(stopPromises);
81
82
setIsStoppingAll(false);
83
setIsStopAllModalOpen(false);
84
85
if (errorCount > 0) {
86
toast.toast({
87
type: "error",
88
message: `Failed to stop all workspaces`,
89
description: `Attempted to stop ${runningWorkspaces.length} workspaces. ${successCount} stopped, ${errorCount} failed.`,
90
});
91
} else {
92
toast.toast({
93
type: "success",
94
message: `Stop command sent`,
95
description: `Successfully sent stop command for ${successCount} workspaces.`,
96
});
97
}
98
refetch();
99
};
100
101
if (isLoading && !isStoppingAll) {
102
return (
103
<div className="flex items-center justify-center w-full space-x-2 text-gray-400 text-sm p-8">
104
<img alt="Loading Spinner" className="h-4 w-4 animate-spin" src={Spinner} />
105
<span>Loading running workspaces...</span>
106
</div>
107
);
108
}
109
110
if (isError && error) {
111
return (
112
<Alert type="error" className="m-4">
113
<p>Error loading running workspaces:</p>
114
<pre>{error instanceof Error ? error.message : String(error)}</pre>
115
</Alert>
116
);
117
}
118
119
const stopAllWorkspacesButton = (
120
<Button
121
variant="destructive"
122
onClick={() => setIsStopAllModalOpen(true)}
123
disabled={!isMaintenanceMode || isStoppingAll || isLoading || runningWorkspaces.length === 0}
124
>
125
Stop All Workspaces
126
</Button>
127
);
128
129
return (
130
<ConfigurationSettingsField>
131
<div className="flex justify-between items-center mb-3">
132
<Heading3>Currently Running Workspaces ({runningWorkspaces.length})</Heading3>
133
{!isMaintenanceMode ? (
134
<Tooltip content="Enable maintenance mode to stop all workspaces">
135
{stopAllWorkspacesButton}
136
</Tooltip>
137
) : (
138
stopAllWorkspacesButton
139
)}
140
</div>
141
{runningWorkspaces.length === 0 && !isLoading ? (
142
<p className="text-pk-content-tertiary">No workspaces are currently running.</p>
143
) : (
144
<ItemsList className="text-gray-400 dark:text-gray-500">
145
<Item
146
header={true}
147
className="grid grid-cols-[1fr_3fr_3fr_3fr_3fr] gap-x-3 bg-pk-surface-secondary dark:bg-gray-700"
148
>
149
<ItemField className="my-auto font-semibold">Status</ItemField>
150
<ItemField className="my-auto font-semibold">Workspace ID</ItemField>
151
<ItemField className="my-auto font-semibold">User</ItemField>
152
<ItemField className="my-auto font-semibold">Project</ItemField>
153
<ItemField className="my-auto font-semibold">Started</ItemField>
154
</Item>
155
{runningWorkspaces.map((session) => {
156
const workspace = session.workspace;
157
const owner = session.owner;
158
const context = session.context;
159
const status = workspace?.status;
160
161
const startedTimeString = session.startedTime
162
? displayTime(session.startedTime.toDate().getTime())
163
: "-";
164
const projectContextURL =
165
context?.repository?.cloneUrl || workspace?.metadata?.originalContextUrl;
166
167
return (
168
<Item
169
key={session.id}
170
className="grid grid-cols-[1fr_3fr_3fr_3fr_3fr] gap-x-3 hover:bg-gray-50 dark:hover:bg-gray-750"
171
>
172
<ItemField className="my-auto truncate">
173
<WorkspaceStatusIndicator status={status} />
174
</ItemField>
175
<ItemField className="my-auto truncate font-mono text-xs">
176
<span title={workspace?.id}>{workspace?.id || "-"}</span>
177
</ItemField>
178
<ItemField className="my-auto truncate">
179
<span title={owner?.name}>{owner?.name || "-"}</span>
180
</ItemField>
181
<ItemField className="my-auto truncate">
182
<span title={projectContextURL ? toRemoteURL(projectContextURL) : ""}>
183
{projectContextURL ? toRemoteURL(projectContextURL) : "-"}
184
</span>
185
</ItemField>
186
<ItemField className="my-auto truncate">
187
<span title={startedTimeString}>{startedTimeString}</span>
188
</ItemField>
189
</Item>
190
);
191
})}
192
</ItemsList>
193
)}
194
<ConfirmationModal
195
title="Confirm Stop All Workspaces"
196
visible={isStopAllModalOpen}
197
onClose={() => setIsStopAllModalOpen(false)}
198
onConfirm={handleStopAllWorkspaces}
199
buttonText={isStoppingAll ? "Stopping..." : "Confirm Stop All"}
200
buttonType="destructive"
201
buttonDisabled={isStoppingAll}
202
>
203
<p className="text-sm text-pk-content-secondary">
204
Are you sure you want to stop all {runningWorkspaces.length} currently running workspaces in this
205
organization? Workspaces will be backed up before stopping. This action cannot be undone.
206
</p>
207
</ConfirmationModal>
208
</ConfigurationSettingsField>
209
);
210
};
211
212