Path: blob/main/components/dashboard/src/workspaces/WorkspaceEntry.tsx
2500 views
/**1* Copyright (c) 2021 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 { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";7import { FunctionComponent, useCallback, useMemo, useState } from "react";8import { Item, ItemFieldIcon } from "../components/ItemsList";9import PendingChangesDropdown from "../components/PendingChangesDropdown";10import Tooltip from "../components/Tooltip";11import dayjs from "dayjs";12import { WorkspaceEntryOverflowMenu } from "./WorkspaceOverflowMenu";13import { WorkspaceStatusIndicator } from "./WorkspaceStatusIndicator";14import { Workspace } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";15import { GitBranchIcon, PinIcon } from "lucide-react";16import { useUpdateWorkspaceMutation } from "../data/workspaces/update-workspace-mutation";17import { fromWorkspaceName } from "./RenameWorkspaceModal";18import { Button } from "@podkit/buttons/Button";19import { cn } from "@podkit/lib/cn";2021type Props = {22info: Workspace;23shortVersion?: boolean;24};2526export const WorkspaceEntry: FunctionComponent<Props> = ({ info, shortVersion }) => {27const [menuActive, setMenuActive] = useState(false);28const updateWorkspace = useUpdateWorkspaceMutation();2930const gitStatus = info.status?.gitStatus;3132const workspace = info;33const currentBranch = gitStatus?.branch || "<unknown>";34const project = getProjectPath(workspace);3536const changeMenuState = (state: boolean) => {37setMenuActive(state);38};3940const togglePinned = useCallback(() => {41updateWorkspace.mutate({42workspaceId: workspace.id,43metadata: {44pinned: !workspace.metadata?.pinned,45},46});47}, [updateWorkspace, workspace.id, workspace.metadata?.pinned]);4849// Could this be `/start#${workspace.id}` instead?50const startUrl = useMemo(51() =>52new GitpodHostUrl(window.location.href)53.with({54pathname: "/start/",55hash: "#" + workspace.id,56})57.toString(),58[workspace.id],59);6061let gridCol =62"grid-cols-[minmax(32px,32px),minmax(100px,auto),minmax(100px,300px),minmax(80px,160px),minmax(32px,32px),minmax(32px,32px)]";63if (shortVersion) {64gridCol = "grid-cols-[minmax(32px,32px),minmax(100px,auto)]";65}6667return (68<Item className={`whitespace-nowrap py-6 px-4 gap-3 grid ${gridCol}`} solid={menuActive}>69<ItemFieldIcon className="min-w-8">70<WorkspaceStatusIndicator status={workspace?.status} />71</ItemFieldIcon>72<div className="flex-grow flex flex-col py-auto truncate">73<Tooltip content={info.id} allowWrap={true}>74<a href={startUrl}>75<div className="font-medium text-gray-800 dark:text-gray-200 truncate hover:text-blue-600 dark:hover:text-blue-400">76{fromWorkspaceName(info) || info.id}77</div>78</a>79</Tooltip>80<Tooltip content={project ? "https://" + project : ""} allowWrap={true}>81<a href={project ? "https://" + project : undefined}>82<div className="text-sm overflow-ellipsis truncate text-gray-400 dark:text-gray-500 hover:text-blue-600 dark:hover:text-blue-400">83{project || "Unknown"}84</div>85</a>86</Tooltip>87</div>88{!shortVersion && (89<>90<div className="flex flex-col justify-between">91<div className="text-gray-500 dark:text-gray-400 flex flex-row gap-1 items-center overflow-hidden">92<div className="min-w-4">93<GitBranchIcon className="h-4 w-4" />94</div>95<Tooltip96content={currentBranch}97className="truncate overflow-ellipsis max-w-[120px] w-auto"98>99{currentBranch}100</Tooltip>101</div>102<div className="mr-auto">103<PendingChangesDropdown gitStatus={gitStatus} />104</div>105</div>106<div className="flex items-center">107{/*108* Tooltip for workspace last active time109* Displays relative time (e.g. "2 days ago") as visible text110* Shows exact date and time with GMT offset on hover111* Uses dayjs for date formatting and relative time calculation112* Handles potential undefined dates with fallback to current date113* Removes leading zero from single-digit GMT hour offsets114*/}115<Tooltip116content={`Last active: ${dayjs(117info.status?.phase?.lastTransitionTime?.toDate() ?? new Date(),118).format("MMM D, YYYY, h:mm A")} GMT${dayjs(119info.status?.phase?.lastTransitionTime?.toDate() ?? new Date(),120)121.format("Z")122.replace(/^([+-])0/, "$1")}`}123className="w-full"124>125<div className="text-sm w-full text-gray-400 overflow-ellipsis truncate">126{dayjs(info.status?.phase?.lastTransitionTime?.toDate() ?? new Date()).fromNow()}127</div>128</Tooltip>129</div>130<div className="min-w-8 flex items-center">131<Tooltip content={workspace.metadata?.pinned ? "Unpin" : "Pin"}>132<Button133onClick={togglePinned}134variant={"ghost"}135className={136"group px-2 flex items-center hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md w-8 h-8"137}138>139<PinIcon140className={cn(141"w-4 h-4 self-center",142workspace.metadata?.pinned143? "text-gray-600 dark:text-gray-300"144: "text-gray-300 dark:text-gray-600 group-hover:text-gray-600 dark:group-hover:text-gray-300",145)}146/>147</Button>148</Tooltip>149</div>150<WorkspaceEntryOverflowMenu changeMenuState={changeMenuState} info={info} />151</>152)}153</Item>154);155};156157export function getProjectPath(ws: Workspace) {158// TODO: Remove and call papi ContextService159return ws.metadata!.originalContextUrl.replace("https://", "");160}161162163