Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/dashboard/src/admin/BlockedEmailDomains.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 { EmailDomainFilterEntry } from "@gitpod/gitpod-protocol";
8
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
9
import { useEffect, useMemo, useRef, useState } from "react";
10
import Alert from "../components/Alert";
11
import { ContextMenuEntry } from "../components/ContextMenu";
12
import { ItemFieldContextMenu } from "../components/ItemsList";
13
import Modal, { ModalBody, ModalFooter, ModalHeader } from "../components/Modal";
14
import { CheckboxInputField } from "../components/forms/CheckboxInputField";
15
import searchIcon from "../icons/search.svg";
16
import { AdminPageHeader } from "./AdminPageHeader";
17
import Pagination from "../Pagination/Pagination";
18
import { Button } from "@podkit/buttons/Button";
19
import { installationClient } from "../service/public-api";
20
import { ListBlockedEmailDomainsResponse } from "@gitpod/public-api/lib/gitpod/v1/installation_pb";
21
import { TextInputField } from "../components/forms/TextInputField";
22
23
export function BlockedEmailDomains() {
24
return (
25
<AdminPageHeader title="Admin" subtitle="Block email domains.">
26
<BlockedEmailDomainsList />
27
</AdminPageHeader>
28
);
29
}
30
31
function useBlockedEmailDomains() {
32
return useQuery(["blockedEmailDomains"], () => installationClient.listBlockedEmailDomains({}), {
33
staleTime: 1000 * 60 * 5, // 5min
34
});
35
}
36
37
function useUpdateBlockedEmailDomainMutation() {
38
const queryClient = useQueryClient();
39
const blockedEmailDomains = useBlockedEmailDomains();
40
return useMutation(
41
async (blockedDomain: EmailDomainFilterEntry) => {
42
await installationClient.createBlockedEmailDomain({
43
domain: blockedDomain.domain,
44
negative: blockedDomain.negative ?? false,
45
});
46
},
47
{
48
onSuccess: (_, blockedDomain) => {
49
const data = new ListBlockedEmailDomainsResponse(blockedEmailDomains.data);
50
data.blockedEmailDomains.map((entry) => {
51
if (entry.domain !== blockedDomain.domain) {
52
return entry;
53
}
54
return blockedDomain;
55
});
56
queryClient.setQueryData(["blockedEmailDomains"], data);
57
blockedEmailDomains.refetch();
58
},
59
},
60
);
61
}
62
63
interface Props {}
64
65
export function BlockedEmailDomainsList(props: Props) {
66
const blockedEmailDomains = useBlockedEmailDomains();
67
const updateBlockedEmailDomainMutation = useUpdateBlockedEmailDomainMutation();
68
const [searchTerm, setSearchTerm] = useState("");
69
const pageSize = 50;
70
const [isAddModalVisible, setAddModalVisible] = useState(false);
71
const [currentPage, setCurrentPage] = useState(1);
72
const [currentBlockedDomain, setCurrentBlockedDomain] = useState<EmailDomainFilterEntry>({
73
domain: "",
74
negative: false,
75
});
76
77
const searchResult = useMemo(() => {
78
if (!blockedEmailDomains.data) {
79
return [];
80
}
81
return blockedEmailDomains.data.blockedEmailDomains.filter((entry) =>
82
entry.domain.toLowerCase().includes(searchTerm.toLowerCase()),
83
);
84
}, [blockedEmailDomains.data, searchTerm]);
85
86
const add = () => {
87
setCurrentBlockedDomain({
88
domain: "",
89
negative: false,
90
});
91
setAddModalVisible(true);
92
};
93
94
const save = async (blockedDomain: EmailDomainFilterEntry) => {
95
updateBlockedEmailDomainMutation.mutateAsync(blockedDomain);
96
setAddModalVisible(false);
97
};
98
99
const validate = (blockedDomain: EmailDomainFilterEntry): string | undefined => {
100
if (blockedDomain.domain === "" || blockedDomain.domain.trim() === "%") {
101
return "Domain can not be empty";
102
}
103
};
104
105
return (
106
<div className="app-container">
107
{isAddModalVisible && (
108
<AddBlockedDomainModal
109
blockedDomain={currentBlockedDomain}
110
validate={validate}
111
save={save}
112
onClose={() => setAddModalVisible(false)}
113
/>
114
)}
115
<div className="pb-3 mt-3 flex">
116
<div className="flex justify-between w-full">
117
<div className="flex relative h-10 my-auto">
118
<img
119
src={searchIcon}
120
title="Search"
121
className="filter-grayscale absolute top-3 left-3"
122
alt="search icon"
123
/>
124
<input
125
className="w-64 pl-9 border-0"
126
type="search"
127
placeholder="Search by domain"
128
onChange={(v) => {
129
setSearchTerm(v.target.value.trim());
130
}}
131
/>
132
</div>
133
<div className="flex space-x-2">
134
<Button onClick={add}>Add Domain</Button>
135
</div>
136
</div>
137
</div>
138
139
<div className="flex flex-col space-y-2">
140
<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">
141
<div className="w-9/12">Domain</div>
142
<div className="w-1/12">Block Users</div>
143
<div className="w-1/12"></div>
144
</div>
145
{searchResult.slice((currentPage - 1) * pageSize, currentPage * pageSize).map((br) => (
146
<BlockedDomainEntry
147
key={br.domain}
148
br={br}
149
toggleBlockUser={async () => {
150
br.negative = !br.negative;
151
updateBlockedEmailDomainMutation.mutateAsync(br);
152
}}
153
/>
154
))}
155
<Pagination
156
currentPage={currentPage}
157
setPage={setCurrentPage}
158
totalNumberOfPages={Math.ceil(searchResult.length / pageSize)}
159
/>
160
</div>
161
</div>
162
);
163
}
164
165
function BlockedDomainEntry(props: {
166
br: EmailDomainFilterEntry;
167
toggleBlockUser: (br: EmailDomainFilterEntry) => void;
168
}) {
169
const menuEntries: ContextMenuEntry[] = [
170
{
171
title: "Toggle Block User",
172
onClick: () => props.toggleBlockUser(props.br),
173
customFontStyle: "text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300",
174
},
175
];
176
return (
177
<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">
178
<div className="flex flex-col w-9/12 truncate">
179
<span className="mr-3 text-lg text-gray-600 truncate">{props.br.domain}</span>
180
</div>
181
<div className="flex flex-col self-center w-1/12">
182
<span className="mr-3 text-lg text-gray-600 truncate">{props.br.negative ? "Yes" : "No"}</span>
183
</div>
184
<div className="flex flex-col w-1/12">
185
<ItemFieldContextMenu menuEntries={menuEntries} />
186
</div>
187
</div>
188
);
189
}
190
191
interface AddBlockedDomainModalProps {
192
blockedDomain: EmailDomainFilterEntry;
193
validate: (blockedDomain: EmailDomainFilterEntry) => string | undefined;
194
save: (br: EmailDomainFilterEntry) => void;
195
onClose: () => void;
196
}
197
198
function AddBlockedDomainModal(p: AddBlockedDomainModalProps) {
199
const [br, setBr] = useState({ ...p.blockedDomain });
200
const [error, setError] = useState("");
201
const ref = useRef(br);
202
203
const update = (previous: Partial<EmailDomainFilterEntry>) => {
204
const newEnv = { ...ref.current, ...previous };
205
setBr(newEnv);
206
ref.current = newEnv;
207
};
208
209
useEffect(() => {
210
setBr({ ...p.blockedDomain });
211
setError("");
212
}, [p.blockedDomain]);
213
214
const save = () => {
215
const v = ref.current;
216
const newError = p.validate(v);
217
if (!!newError) {
218
setError(newError);
219
}
220
221
p.save(v);
222
p.onClose();
223
};
224
225
return (
226
<Modal visible={true} onClose={p.onClose} onSubmit={save}>
227
<ModalHeader>New Blocked Domain</ModalHeader>
228
<ModalBody>
229
<Alert type={"warning"} closable={false} showIcon={true} className="flex rounded p-2 w-2/3 mb-2 w-full">
230
Entries in this table have an immediate effect on all new users. Please use it carefully.
231
</Alert>
232
<Alert type={"message"} closable={false} showIcon={true} className="flex rounded p-2 w-2/3 mb-2 w-full">
233
Users are blocked by matching their email domain.
234
</Alert>
235
<Details br={br} update={update} error={error} />
236
</ModalBody>
237
<ModalFooter>
238
<Button variant="secondary" onClick={p.onClose}>
239
Cancel
240
</Button>
241
<Button type="submit">Add Blocked Domain</Button>
242
</ModalFooter>
243
</Modal>
244
);
245
}
246
247
function Details(props: {
248
br: EmailDomainFilterEntry;
249
error?: string;
250
update?: (pev: Partial<EmailDomainFilterEntry>) => void;
251
}) {
252
return (
253
<div className="border-gray-200 dark:border-gray-800 -mx-6 px-6 py-4 flex flex-col">
254
{props.error ? (
255
<div className="bg-kumquat-light rounded-md p-3 text-gitpod-red text-sm mb-2">{props.error}</div>
256
) : null}
257
<TextInputField
258
label="Domain (may contain '%' as wild card)"
259
autoFocus
260
type="text"
261
value={props.br.domain}
262
placeholder={'e.g. "mailicous-domain.com"'}
263
disabled={!props.update}
264
onChange={(val) => {
265
if (!!props.update) {
266
props.update({ domain: val });
267
}
268
}}
269
/>
270
271
<CheckboxInputField
272
label="Block Users"
273
hint="Block any user that tries to sign up with this email domain."
274
checked={props.br.negative}
275
disabled={!props.update}
276
onChange={(checked) => {
277
if (!!props.update) {
278
props.update({ negative: checked });
279
}
280
}}
281
/>
282
</div>
283
);
284
}
285
286