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