Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/admin/BlockedRepositories.tsx
2500 views
1
/**
2
* Copyright (c) 2022 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 { useCallback, useEffect, useRef, useState } from "react";
8
import { AdminPageHeader } from "./AdminPageHeader";
9
import ConfirmationModal from "../components/ConfirmationModal";
10
import Modal, { ModalBody, ModalFooter, ModalHeader } from "../components/Modal";
11
import { CheckboxInputField } from "../components/forms/CheckboxInputField";
12
import { ItemFieldContextMenu } from "../components/ItemsList";
13
import { ContextMenuEntry } from "../components/ContextMenu";
14
import Alert from "../components/Alert";
15
import { SpinnerLoader } from "../components/Loader";
16
import searchIcon from "../icons/search.svg";
17
import { Button } from "@podkit/buttons/Button";
18
import { installationClient } from "../service/public-api";
19
import { Sort, SortOrder } from "@gitpod/public-api/lib/gitpod/v1/sorting_pb";
20
import { BlockedRepository, ListBlockedRepositoriesResponse } from "@gitpod/public-api/lib/gitpod/v1/installation_pb";
21
import { TextInputField } from "../components/forms/TextInputField";
22
23
export function BlockedRepositories() {
24
return (
25
<AdminPageHeader title="Admin" subtitle="Configure and manage instance settings.">
26
<BlockedRepositoriesList />
27
</AdminPageHeader>
28
);
29
}
30
31
type NewBlockedRepository = Pick<BlockedRepository, "urlRegexp" | "blockUser" | "blockFreeUsage">;
32
type ExistingBlockedRepository = Pick<BlockedRepository, "id" | "urlRegexp" | "blockUser" | "blockFreeUsage">;
33
34
interface Props {}
35
36
export function BlockedRepositoriesList(props: Props) {
37
const [searchResult, setSearchResult] = useState<ListBlockedRepositoriesResponse>(
38
new ListBlockedRepositoriesResponse({
39
blockedRepositories: [],
40
}),
41
);
42
const [queryTerm, setQueryTerm] = useState("");
43
const [searching, setSearching] = useState(false);
44
45
const [isAddModalVisible, setAddModalVisible] = useState(false);
46
const [isDeleteModalVisible, setDeleteModalVisible] = useState(false);
47
48
const [currentBlockedRepository, setCurrentBlockedRepository] = useState<BlockedRepository>(
49
new BlockedRepository({
50
id: 0,
51
urlRegexp: "",
52
blockUser: false,
53
blockFreeUsage: false,
54
}),
55
);
56
57
const search = async () => {
58
setSearching(true);
59
try {
60
const result = await installationClient.listBlockedRepositories({
61
// Don't need, added it in json-rpc implement to make life easier.
62
// pagination: new PaginationRequest({
63
// token: Buffer.from(JSON.stringify({ offset: 0 })).toString("base64"),
64
// pageSize: 100,
65
// }),
66
sort: [
67
new Sort({
68
field: "urlRegexp",
69
order: SortOrder.ASC,
70
}),
71
],
72
searchTerm: queryTerm,
73
});
74
setSearchResult(result);
75
} finally {
76
setSearching(false);
77
}
78
};
79
useEffect(() => {
80
search(); // Initial list
81
// eslint-disable-next-line react-hooks/exhaustive-deps
82
}, []);
83
84
const add = () => {
85
setCurrentBlockedRepository(
86
new BlockedRepository({
87
id: 0,
88
urlRegexp: "",
89
blockUser: false,
90
blockFreeUsage: false,
91
}),
92
);
93
setAddModalVisible(true);
94
};
95
96
const save = async (blockedRepository: NewBlockedRepository) => {
97
await installationClient.createBlockedRepository({
98
urlRegexp: blockedRepository.urlRegexp ?? "",
99
blockUser: blockedRepository.blockUser ?? false,
100
blockFreeUsage: blockedRepository.blockFreeUsage ?? false,
101
});
102
setAddModalVisible(false);
103
search();
104
};
105
106
const validate = (blockedRepository: NewBlockedRepository): string | undefined => {
107
if (blockedRepository.urlRegexp === "") {
108
return "Repository URL can not be empty";
109
}
110
};
111
112
const deleteBlockedRepository = async (blockedRepository: ExistingBlockedRepository) => {
113
await installationClient.deleteBlockedRepository({
114
blockedRepositoryId: blockedRepository.id,
115
});
116
search();
117
};
118
119
const confirmDeleteBlockedRepository = (blockedRepository: BlockedRepository) => {
120
setCurrentBlockedRepository(blockedRepository);
121
setAddModalVisible(false);
122
setDeleteModalVisible(true);
123
};
124
125
return (
126
<div className="app-container">
127
{isAddModalVisible && (
128
<AddBlockedRepositoryModal
129
blockedRepository={currentBlockedRepository}
130
validate={validate}
131
save={save}
132
onClose={() => setAddModalVisible(false)}
133
/>
134
)}
135
{isDeleteModalVisible && (
136
<DeleteBlockedRepositoryModal
137
blockedRepository={currentBlockedRepository}
138
deleteBlockedRepository={async () => await deleteBlockedRepository(currentBlockedRepository)}
139
onClose={() => setDeleteModalVisible(false)}
140
/>
141
)}
142
<div className="pb-3 mt-3 flex">
143
<div className="flex justify-between w-full">
144
<div className="flex relative h-10 my-auto">
145
{searching ? (
146
<span className="filter-grayscale absolute top-3 left-3">
147
<SpinnerLoader small={true} />
148
</span>
149
) : (
150
<img
151
src={searchIcon}
152
title="Search"
153
className="filter-grayscale absolute top-3 left-3"
154
alt="search icon"
155
/>
156
)}
157
<input
158
className="w-64 pl-9 border-0"
159
type="search"
160
placeholder="Search by URL RegEx"
161
onKeyDown={(ke) => ke.key === "Enter" && search()}
162
onChange={(v) => {
163
setQueryTerm(v.target.value.trim());
164
}}
165
/>
166
</div>
167
<div className="flex space-x-2">
168
<Button onClick={add}>New Blocked Repository</Button>
169
</div>
170
</div>
171
</div>
172
173
<Alert type={"info"} closable={false} showIcon={true} className="flex rounded p-2 mb-2 w-full">
174
Search by repository URL using <abbr title="regular expression">RegEx</abbr>.
175
</Alert>
176
<div className="flex flex-col space-y-2">
177
<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">
178
<div className="w-9/12">Repository URL (RegEx)</div>
179
<div className="w-1/12">Block Users</div>
180
<div className="w-2/12">Block Free Usage</div>
181
<div className="w-1/12"></div>
182
</div>
183
{searchResult.blockedRepositories.map((br) => (
184
<BlockedRepositoryEntry br={br} confirmedDelete={confirmDeleteBlockedRepository} />
185
))}
186
</div>
187
</div>
188
);
189
}
190
191
function BlockedRepositoryEntry(props: { br: BlockedRepository; confirmedDelete: (br: BlockedRepository) => void }) {
192
const menuEntries: ContextMenuEntry[] = [
193
{
194
title: "Delete",
195
onClick: () => props.confirmedDelete(props.br),
196
customFontStyle: "text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300",
197
},
198
];
199
return (
200
<div className="rounded 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">
201
<div className="flex flex-col w-9/12 truncate">
202
<span className="mr-3 text-lg text-gray-600 truncate">{props.br.urlRegexp}</span>
203
</div>
204
<div className="flex flex-col self-center w-1/12">
205
<span className="mr-3 text-lg text-gray-600 truncate">{props.br.blockUser ? "Yes" : "No"}</span>
206
</div>
207
<div className="flex flex-col self-center w-2/12">
208
<span className="mr-3 text-lg text-gray-600 truncate">{props.br.blockFreeUsage ? "Yes" : " "}</span>
209
</div>
210
<div className="flex flex-col w-1/12">
211
<ItemFieldContextMenu menuEntries={menuEntries} />
212
</div>
213
</div>
214
);
215
}
216
217
interface AddBlockedRepositoryModalProps {
218
blockedRepository: NewBlockedRepository;
219
validate: (blockedRepository: NewBlockedRepository) => string | undefined;
220
save: (br: NewBlockedRepository) => void;
221
onClose: () => void;
222
}
223
224
function AddBlockedRepositoryModal(p: AddBlockedRepositoryModalProps) {
225
const [br, setBr] = useState({ ...p.blockedRepository });
226
const [error, setError] = useState("");
227
const ref = useRef(br);
228
229
const update = (previous: Partial<NewBlockedRepository>) => {
230
const newEnv = { ...ref.current, ...previous };
231
setBr(newEnv);
232
ref.current = newEnv;
233
};
234
235
useEffect(() => {
236
setBr({ ...p.blockedRepository });
237
setError("");
238
}, [p.blockedRepository]);
239
240
const save = useCallback(() => {
241
const v = ref.current;
242
const newError = p.validate(v);
243
if (!!newError) {
244
setError(newError);
245
}
246
247
p.save(v);
248
}, [p]);
249
250
return (
251
<Modal visible onClose={p.onClose} onSubmit={save}>
252
<ModalHeader>New Blocked Repository</ModalHeader>
253
<ModalBody>
254
<Alert type={"warning"} closable={false} showIcon={true} className="flex rounded p-2 w-2/3 mb-2 w-full">
255
Entries in this table have an immediate effect on all users. Please use it carefully.
256
</Alert>
257
<Alert type={"message"} closable={false} showIcon={true} className="flex rounded p-2 w-2/3 mb-2 w-full">
258
Repositories are blocked by matching their URL against this regular expression.
259
</Alert>
260
<Details br={br} update={update} error={error} />
261
</ModalBody>
262
<ModalFooter>
263
<Button variant="secondary" onClick={p.onClose}>
264
Cancel
265
</Button>
266
<Button type="submit">Add Blocked Repository</Button>
267
</ModalFooter>
268
</Modal>
269
);
270
}
271
272
function DeleteBlockedRepositoryModal(props: {
273
blockedRepository: ExistingBlockedRepository;
274
deleteBlockedRepository: () => void;
275
onClose: () => void;
276
}) {
277
return (
278
<ConfirmationModal
279
title="Delete Blocked Repository"
280
areYouSureText="Are you sure you want to delete this repository from the list?"
281
buttonText="Delete Blocked Repository"
282
onClose={props.onClose}
283
onConfirm={async () => {
284
await props.deleteBlockedRepository();
285
props.onClose();
286
}}
287
>
288
<Details br={props.blockedRepository} />
289
</ConfirmationModal>
290
);
291
}
292
293
function Details(props: {
294
br: NewBlockedRepository;
295
error?: string;
296
update?: (pev: Partial<NewBlockedRepository>) => void;
297
}) {
298
return (
299
<div className="border-gray-200 dark:border-gray-800 -mx-6 px-6 py-4 flex flex-col">
300
{props.error ? (
301
<div className="bg-kumquat-light rounded-md p-3 text-gitpod-red text-sm mb-2">{props.error}</div>
302
) : null}
303
<TextInputField
304
label="Repository URL RegEx"
305
autoFocus
306
type="text"
307
value={props.br.urlRegexp}
308
placeholder={'e.g. "https://github.com/malicious-user/*"'}
309
disabled={!props.update}
310
onChange={(val) => {
311
if (!!props.update) {
312
props.update({ urlRegexp: val });
313
}
314
}}
315
/>
316
317
<CheckboxInputField
318
label="Block Users"
319
hint="Block any user that tries to open a workspace for a repository URL that matches this RegEx."
320
checked={props.br.blockUser}
321
disabled={!props.update}
322
onChange={(checked) => {
323
if (!!props.update) {
324
props.update({ blockUser: checked });
325
}
326
}}
327
/>
328
329
<CheckboxInputField
330
label="Block Free Usage"
331
hint="Block workspace start for a repository URL that matches this RegEx if user is on free tier."
332
checked={props.br.blockFreeUsage}
333
disabled={!props.update}
334
onChange={(checked) => {
335
if (!!props.update) {
336
props.update({ blockFreeUsage: checked });
337
}
338
}}
339
/>
340
</div>
341
);
342
}
343
344