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