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