Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/admin/WorkspacesSearch.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 {
8
AdminGetListResult,
9
AdminGetWorkspacesQuery,
10
ContextURL,
11
User,
12
WorkspaceAndInstance,
13
} from "@gitpod/gitpod-protocol";
14
import {
15
matchesInstanceIdOrLegacyWorkspaceIdExactly,
16
matchesNewWorkspaceIdExactly,
17
} from "@gitpod/gitpod-protocol/lib/util/parse-workspace-id";
18
import dayjs from "dayjs";
19
import { useEffect, useState } from "react";
20
import { useLocation } from "react-router";
21
import { Link } from "react-router-dom";
22
import Pagination from "../Pagination/Pagination";
23
import { getGitpodService } from "../service/service";
24
import { getProjectPath } from "../workspaces/WorkspaceEntry";
25
import WorkspaceDetail from "./WorkspaceDetail";
26
import { AdminPageHeader } from "./AdminPageHeader";
27
import Alert from "../components/Alert";
28
import { isGitpodIo } from "../utils";
29
import { SpinnerLoader } from "../components/Loader";
30
import { WorkspaceStatusIndicator } from "../workspaces/WorkspaceStatusIndicator";
31
import searchIcon from "../icons/search.svg";
32
import Tooltip from "../components/Tooltip";
33
import { converter } from "../service/public-api";
34
35
interface Props {
36
user?: User;
37
}
38
39
export default function WorkspaceSearchPage() {
40
return (
41
<AdminPageHeader title="Admin" subtitle="Configure and manage instance settings.">
42
<WorkspaceSearch />
43
</AdminPageHeader>
44
);
45
}
46
47
export function WorkspaceSearch(props: Props) {
48
const location = useLocation();
49
const [searchResult, setSearchResult] = useState<AdminGetListResult<WorkspaceAndInstance>>({ rows: [], total: 0 });
50
const [queryTerm, setQueryTerm] = useState("");
51
const [searching, setSearching] = useState(false);
52
const [currentWorkspace, setCurrentWorkspaceState] = useState<WorkspaceAndInstance | undefined>(undefined);
53
const pageLength = 50;
54
const [currentPage, setCurrentPage] = useState(1);
55
56
useEffect(() => {
57
const workspaceId = location.pathname.split("/")[3];
58
if (workspaceId) {
59
let user = searchResult.rows.find((ws) => ws.workspaceId === workspaceId);
60
if (user) {
61
setCurrentWorkspaceState(user);
62
} else {
63
getGitpodService()
64
.server.adminGetWorkspace(workspaceId)
65
.then((ws) => setCurrentWorkspaceState(ws))
66
.catch((e) => console.error(e));
67
}
68
} else {
69
setCurrentWorkspaceState(undefined);
70
}
71
// eslint-disable-next-line react-hooks/exhaustive-deps
72
}, [location]);
73
74
useEffect(() => {
75
if (props.user) {
76
search();
77
}
78
// eslint-disable-next-line react-hooks/exhaustive-deps
79
}, [props.user]);
80
81
if (currentWorkspace) {
82
return <WorkspaceDetail workspace={currentWorkspace} />;
83
}
84
85
const search = async (page: number = 1) => {
86
// Disables empty search on the workspace search page
87
if (isGitpodIo() && !props.user && queryTerm.length === 0) {
88
return;
89
}
90
91
setSearching(true);
92
try {
93
const query: AdminGetWorkspacesQuery = {
94
ownerId: props?.user?.id, // Workspace search in admin user detail
95
};
96
if (matchesInstanceIdOrLegacyWorkspaceIdExactly(queryTerm)) {
97
query.instanceIdOrWorkspaceId = queryTerm;
98
} else if (matchesNewWorkspaceIdExactly(queryTerm)) {
99
query.workspaceId = queryTerm;
100
}
101
if (isGitpodIo() && !query.ownerId && !query.instanceIdOrWorkspaceId && !query.workspaceId) {
102
return;
103
}
104
105
const result = await getGitpodService().server.adminGetWorkspaces({
106
limit: pageLength,
107
orderBy: "instanceCreationTime",
108
offset: (page - 1) * pageLength,
109
orderDir: "desc",
110
...query,
111
});
112
setCurrentPage(page);
113
setSearchResult(result);
114
} finally {
115
setSearching(false);
116
}
117
};
118
return (
119
<div className="app-container">
120
<div className="mt-3 mb-3 flex">
121
<div className="flex justify-between w-full">
122
<div className="flex relative h-10 my-auto">
123
{searching ? (
124
<span className="filter-grayscale absolute top-3 left-3">
125
<SpinnerLoader small={true} />
126
</span>
127
) : (
128
<img
129
src={searchIcon}
130
title="Search"
131
className="filter-grayscale absolute top-3 left-3"
132
alt="search icon"
133
/>
134
)}
135
<input
136
className="w-64 pl-9 border-0"
137
type="search"
138
placeholder="Search Workspace IDs"
139
onKeyDown={(ke) => ke.key === "Enter" && search()}
140
onChange={(v) => {
141
setQueryTerm(v.target.value.trim());
142
}}
143
/>
144
</div>
145
</div>
146
</div>
147
<Alert type={"info"} closable={false} showIcon={true} className="flex rounded p-2 mb-2 w-full">
148
Search workspaces using workspace ID.
149
</Alert>
150
<div className="flex flex-col space-y-2">
151
<div className="px-6 py-3 flex justify-between text-sm text-gray-400 border-t border-b border-gray-200 dark:border-gray-800 mb-2">
152
<div className="w-4/12">Name</div>
153
<div className="w-6/12">Context</div>
154
<div className="w-2/12">Last Started</div>
155
</div>
156
{searchResult.rows.map((ws) => (
157
<WorkspaceEntry key={ws.workspaceId} ws={ws} />
158
))}
159
</div>
160
<Pagination
161
currentPage={currentPage}
162
setPage={search}
163
totalNumberOfPages={Math.ceil(searchResult.total / pageLength)}
164
/>
165
</div>
166
);
167
}
168
169
function WorkspaceEntry(p: { ws: WorkspaceAndInstance }) {
170
const workspace = converter.toWorkspace({
171
workspace: WorkspaceAndInstance.toWorkspace(p.ws),
172
latestInstance: WorkspaceAndInstance.toInstance(p.ws),
173
});
174
return (
175
<Link
176
key={"ws-" + p.ws.workspaceId}
177
to={"/admin/workspaces/" + p.ws.workspaceId}
178
data-analytics='{"button_type":"sidebar_menu"}'
179
>
180
<div className="rounded-xl whitespace-nowrap flex py-6 px-6 w-full justify-between hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-kumquat-light group">
181
<div className="pr-3 self-center w-8">
182
<WorkspaceStatusIndicator status={workspace.status} />
183
</div>
184
<div className="flex flex-col w-5/12 truncate">
185
<div className="font-medium text-gray-800 dark:text-gray-100 truncate hover:text-blue-600 dark:hover:text-blue-400 truncate">
186
{p.ws.workspaceId}
187
</div>
188
<div className="text-sm overflow-ellipsis truncate text-gray-400 truncate">
189
{getProjectPath(workspace)}
190
</div>
191
</div>
192
<div className="flex flex-col w-5/12 self-center truncate">
193
<div className="text-gray-500 overflow-ellipsis truncate">{p.ws.description}</div>
194
<div className="text-sm text-gray-400 overflow-ellipsis truncate">
195
{ContextURL.getNormalizedURL(p.ws)?.toString()}
196
</div>
197
</div>
198
<div className="flex w-2/12 self-center">
199
<Tooltip
200
content={dayjs(p.ws.instanceCreationTime || p.ws.workspaceCreationTime).format("MMM D, YYYY")}
201
>
202
<div className="text-sm w-full text-gray-400 truncate">
203
{dayjs(p.ws.instanceCreationTime || p.ws.workspaceCreationTime).fromNow()}
204
</div>
205
</Tooltip>
206
</div>
207
</div>
208
</Link>
209
);
210
}
211
212