Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/admin/UserSearch.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 { AdminGetListResult, User } from "@gitpod/gitpod-protocol";
8
import dayjs from "dayjs";
9
import { useEffect, useState } from "react";
10
import { useLocation } from "react-router";
11
import { Link } from "react-router-dom";
12
import { SpinnerLoader } from "../components/Loader";
13
import Pagination from "../Pagination/Pagination";
14
import { getGitpodService } from "../service/service";
15
import { AdminPageHeader } from "./AdminPageHeader";
16
import UserDetail from "./UserDetail";
17
import searchIcon from "../icons/search.svg";
18
import Tooltip from "../components/Tooltip";
19
import { getPrimaryEmail } from "@gitpod/public-api-common/lib/user-utils";
20
21
export default function UserSearch() {
22
const location = useLocation();
23
const [searchResult, setSearchResult] = useState<AdminGetListResult<User>>({ rows: [], total: 0 });
24
const [searchTerm, setSearchTerm] = useState("");
25
const [searching, setSearching] = useState(false);
26
const [currentUser, setCurrentUserState] = useState<User | undefined>(undefined);
27
const pageLength = 50;
28
const [currentPage, setCurrentPage] = useState(1);
29
30
useEffect(() => {
31
const userId = location.pathname.split("/")[3];
32
if (userId) {
33
let user = searchResult.rows.find((u) => u.id === userId);
34
if (user) {
35
setCurrentUserState(user);
36
} else {
37
getGitpodService()
38
.server.adminGetUser(userId)
39
.then((user) => setCurrentUserState(user))
40
.catch((e) => console.error(e));
41
}
42
} else {
43
setCurrentUserState(undefined);
44
}
45
// eslint-disable-next-line react-hooks/exhaustive-deps
46
}, [location]);
47
48
if (currentUser) {
49
return <UserDetail user={currentUser} />;
50
}
51
52
const search = async (page: number = 1) => {
53
setSearching(true);
54
try {
55
const result = await getGitpodService().server.adminGetUsers({
56
searchTerm,
57
limit: pageLength,
58
orderBy: "creationDate",
59
offset: (page - 1) * pageLength,
60
orderDir: "desc",
61
});
62
setSearchResult(result);
63
setCurrentPage(page);
64
} finally {
65
setSearching(false);
66
}
67
};
68
return (
69
<AdminPageHeader title="Admin" subtitle="Configure and manage instance settings.">
70
<div className="app-container">
71
<div className="mb-3 mt-3 flex">
72
<div className="flex justify-between w-full">
73
<div className="flex relative h-10 my-auto">
74
{searching ? (
75
<span className="filter-grayscale absolute top-3 left-3">
76
<SpinnerLoader small={true} />
77
</span>
78
) : (
79
<img
80
src={searchIcon}
81
title="Search"
82
className="filter-grayscale absolute top-3 left-3"
83
alt="search icon"
84
/>
85
)}
86
<input
87
className="w-64 pl-9 border-0"
88
type="search"
89
placeholder="Search Users"
90
onKeyDown={(ke) => ke.key === "Enter" && search()}
91
onChange={(v) => {
92
setSearchTerm(v.target.value.trim());
93
}}
94
/>
95
</div>
96
</div>
97
</div>
98
<div className="flex flex-col space-y-2">
99
<div className="px-6 py-3 flex justify-between space-x-2 text-sm text-gray-400 border-t border-b border-gray-200 dark:border-gray-800 mb-2">
100
<div className="w-7/12">Name</div>
101
<div className="w-5/12 flex items-center">
102
<span>Created</span>
103
<svg xmlns="http://www.w3.org/2000/svg" fill="none" className="h-4 w-4" viewBox="0 0 16 16">
104
<path
105
fill="#A8A29E"
106
fillRule="evenodd"
107
d="M13.366 8.234a.8.8 0 010 1.132l-4.8 4.8a.8.8 0 01-1.132 0l-4.8-4.8a.8.8 0 111.132-1.132L7.2 11.67V2.4a.8.8 0 111.6 0v9.269l3.434-3.435a.8.8 0 011.132 0z"
108
clipRule="evenodd"
109
/>
110
</svg>
111
</div>
112
</div>
113
{searchResult.rows
114
.filter((u) => u.identities.length > 0)
115
.map((u) => (
116
<UserEntry user={u} />
117
))}
118
</div>
119
<Pagination
120
currentPage={currentPage}
121
setPage={search}
122
totalNumberOfPages={Math.ceil(searchResult.total / pageLength)}
123
/>
124
</div>
125
</AdminPageHeader>
126
);
127
}
128
129
function UserEntry(p: { user: User }) {
130
if (!p) {
131
return <></>;
132
}
133
const email = getPrimaryEmail(p.user) || "---";
134
return (
135
<Link key={p.user.id} to={"/admin/users/" + p.user.id} data-analytics='{"button_type":"sidebar_menu"}'>
136
<div className="rounded-xl whitespace-nowrap flex space-x-2 py-6 px-6 w-full justify-between hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-kumquat-light group">
137
<div className="pr-3 self-center w-1/12">
138
<img className="rounded-full" src={p.user.avatarUrl} alt={p.user.fullName || p.user.name} />
139
</div>
140
<div className="flex flex-col w-6/12">
141
<div className="font-medium text-gray-800 dark:text-gray-100 truncate hover:text-blue-600 dark:hover:text-blue-400">
142
{p.user.fullName}
143
</div>
144
<div className="text-sm overflow-ellipsis truncate text-gray-400 hover:text-blue-600 dark:hover:text-blue-400">
145
{email}
146
</div>
147
</div>
148
<div className="flex w-5/12 self-center">
149
<Tooltip content={dayjs(p.user.creationDate).format("MMM D, YYYY")}>
150
<div className="text-sm w-full text-gray-400 truncate">
151
{dayjs(p.user.creationDate).fromNow()}
152
</div>
153
</Tooltip>
154
</div>
155
</div>
156
</Link>
157
);
158
}
159
160