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