Skip to content

Commit 51eae90

Browse files
geroplroboquat
authored andcommitted
[dashboard] BlockedRepo UI
1 parent 3fd02a7 commit 51eae90

File tree

3 files changed

+302
-17
lines changed

3 files changed

+302
-17
lines changed

components/dashboard/src/App.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import { parseProps } from "./start/StartWorkspace";
4848
import SelectIDEModal from "./settings/SelectIDEModal";
4949
import { StartPage, StartPhase } from "./start/StartPage";
5050
import { isGitpodIo } from "./utils";
51-
import { BlockedRepositorySettings } from "./admin/BlockedRepositorySettings";
51+
import { BlockedRepositories } from "./admin/BlockedRepositories";
5252

5353
const Setup = React.lazy(() => import(/* webpackPrefetch: true */ "./Setup"));
5454
const Workspaces = React.lazy(() => import(/* webpackPrefetch: true */ "./workspaces/Workspaces"));
@@ -366,7 +366,7 @@ function App() {
366366
<AdminRoute path="/admin/teams" component={TeamsSearch} />
367367
<AdminRoute path="/admin/workspaces" component={WorkspacesSearch} />
368368
<AdminRoute path="/admin/projects" component={ProjectsSearch} />
369-
<AdminRoute path="/admin/blocked-repositories" component={BlockedRepositorySettings} />
369+
<AdminRoute path="/admin/blocked-repositories" component={BlockedRepositories} />
370370
<AdminRoute path="/admin/license" component={License} />
371371
<AdminRoute path="/admin/settings" component={AdminSettings} />
372372

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
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 { AdminGetListResult } from "@gitpod/gitpod-protocol";
8+
import { useEffect, useRef, useState } from "react";
9+
import { getGitpodService } from "../service/service";
10+
import { PageWithAdminSubMenu } from "./PageWithAdminSubMenu";
11+
import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositories-protocol";
12+
import ConfirmationModal from "../components/ConfirmationModal";
13+
import Modal from "../components/Modal";
14+
import CheckBox from "../components/CheckBox";
15+
import { ItemFieldContextMenu } from "../components/ItemsList";
16+
import { ContextMenuEntry } from "../components/ContextMenu";
17+
18+
export function BlockedRepositories() {
19+
return (
20+
<PageWithAdminSubMenu title="Blocked Repositories" subtitle="Search and manage all blocked repositories.">
21+
<BlockedRepositoriesList />
22+
</PageWithAdminSubMenu>
23+
);
24+
}
25+
26+
type NewBlockedRepository = Pick<BlockedRepository, "urlRegexp" | "blockUser">;
27+
type ExistingBlockedRepository = Pick<BlockedRepository, "id" | "urlRegexp" | "blockUser">;
28+
29+
interface Props {}
30+
31+
export function BlockedRepositoriesList(props: Props) {
32+
const [searchResult, setSearchResult] = useState<AdminGetListResult<BlockedRepository>>({ rows: [], total: 0 });
33+
const [queryTerm, setQueryTerm] = useState("");
34+
const [searching, setSearching] = useState(false);
35+
36+
const [isAddModalVisible, setAddModalVisible] = useState(false);
37+
const [isDeleteModalVisible, setDeleteModalVisible] = useState(false);
38+
39+
const [currentBlockedRepository, setCurrentBlockedRepository] = useState<ExistingBlockedRepository>({
40+
id: 0,
41+
urlRegexp: "",
42+
blockUser: false,
43+
});
44+
45+
const search = async () => {
46+
setSearching(true);
47+
try {
48+
const result = await getGitpodService().server.adminGetBlockedRepositories({
49+
limit: 100,
50+
orderBy: "urlRegexp",
51+
offset: 0,
52+
orderDir: "asc",
53+
searchTerm: queryTerm,
54+
});
55+
setSearchResult(result);
56+
} finally {
57+
setSearching(false);
58+
}
59+
};
60+
useEffect(() => {
61+
search(); // Initial list
62+
}, []);
63+
64+
const add = () => {
65+
setCurrentBlockedRepository({
66+
id: 0,
67+
urlRegexp: "",
68+
blockUser: false,
69+
});
70+
setAddModalVisible(true);
71+
};
72+
73+
const save = async (blockedRepository: NewBlockedRepository) => {
74+
await getGitpodService().server.adminCreateBlockedRepository(
75+
blockedRepository.urlRegexp,
76+
blockedRepository.blockUser,
77+
);
78+
setAddModalVisible(false);
79+
search();
80+
};
81+
82+
const validate = (blockedRepository: NewBlockedRepository): string | undefined => {
83+
if (blockedRepository.urlRegexp === "") {
84+
return "Empty RegEx!";
85+
}
86+
};
87+
88+
const deleteBlockedRepository = async (blockedRepository: ExistingBlockedRepository) => {
89+
await getGitpodService().server.adminDeleteBlockedRepository(blockedRepository.id);
90+
search();
91+
};
92+
93+
const confirmDeleteBlockedRepository = (blockedRepository: ExistingBlockedRepository) => {
94+
setCurrentBlockedRepository(blockedRepository);
95+
setAddModalVisible(false);
96+
setDeleteModalVisible(true);
97+
};
98+
99+
return (
100+
<>
101+
{isAddModalVisible && (
102+
<AddBlockedRepositoryModal
103+
blockedRepository={currentBlockedRepository}
104+
validate={validate}
105+
save={save}
106+
onClose={() => setAddModalVisible(false)}
107+
/>
108+
)}
109+
{isDeleteModalVisible && (
110+
<DeleteBlockedRepositoryModal
111+
blockedRepository={currentBlockedRepository}
112+
deleteBlockedRepository={() => deleteBlockedRepository(currentBlockedRepository)}
113+
onClose={() => setDeleteModalVisible(false)}
114+
/>
115+
)}
116+
<div className="pt-8 flex">
117+
<div className="flex justify-between w-full">
118+
<div className="flex">
119+
<div className="py-4">
120+
<svg
121+
className={searching ? "animate-spin" : ""}
122+
width="16"
123+
height="16"
124+
fill="none"
125+
xmlns="http://www.w3.org/2000/svg"
126+
>
127+
<path
128+
fillRule="evenodd"
129+
clipRule="evenodd"
130+
d="M6 2a4 4 0 100 8 4 4 0 000-8zM0 6a6 6 0 1110.89 3.477l4.817 4.816a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 010 6z"
131+
fill="#A8A29E"
132+
/>
133+
</svg>
134+
</div>
135+
<input
136+
type="search"
137+
placeholder="Search by URL Regex"
138+
onKeyDown={(ke) => ke.key === "Enter" && search()}
139+
onChange={(v) => {
140+
setQueryTerm(v.target.value.trim());
141+
}}
142+
/>
143+
</div>
144+
<div className="flex space-x-2">
145+
<button onClick={add}>New Blocked Repository</button>
146+
</div>
147+
</div>
148+
</div>
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-9/12">Repository URL Regex</div>
152+
<div className="w-1/12">Block user</div>
153+
<div className="w-2/12">Delete</div>
154+
</div>
155+
{searchResult.rows.map((br) => (
156+
<BlockedRepositoryEntry br={br} confirmedDelete={confirmDeleteBlockedRepository} />
157+
))}
158+
</div>
159+
</>
160+
);
161+
}
162+
163+
function BlockedRepositoryEntry(props: { br: BlockedRepository; confirmedDelete: (br: BlockedRepository) => void }) {
164+
const menuEntries: ContextMenuEntry[] = [
165+
{
166+
title: "Delete",
167+
onClick: () => props.confirmedDelete(props.br),
168+
},
169+
];
170+
return (
171+
<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-gitpod-kumquat-light group">
172+
<div className="flex flex-col w-9/12 truncate">
173+
<span className="mr-3 text-lg text-gray-600 truncate">{props.br.urlRegexp}</span>
174+
</div>
175+
<div className="flex flex-col self-center w-1/12">
176+
<CheckBox title={""} desc={""} checked={props.br.blockUser} disabled={true} />
177+
</div>
178+
<div className="flex flex-col w-2/12">
179+
<ItemFieldContextMenu menuEntries={menuEntries} />
180+
</div>
181+
</div>
182+
);
183+
}
184+
185+
interface AddBlockedRepositoryModalProps {
186+
blockedRepository: NewBlockedRepository;
187+
validate: (blockedRepository: NewBlockedRepository) => string | undefined;
188+
save: (br: NewBlockedRepository) => void;
189+
onClose: () => void;
190+
}
191+
192+
function AddBlockedRepositoryModal(p: AddBlockedRepositoryModalProps) {
193+
const [br, setBr] = useState({ ...p.blockedRepository });
194+
const [error, setError] = useState("");
195+
const ref = useRef(br);
196+
197+
const update = (previous: Partial<NewBlockedRepository>) => {
198+
const newEnv = { ...ref.current, ...previous };
199+
setBr(newEnv);
200+
ref.current = newEnv;
201+
};
202+
203+
useEffect(() => {
204+
setBr({ ...p.blockedRepository });
205+
setError("");
206+
}, [p.blockedRepository]);
207+
208+
let save = (): boolean => {
209+
const v = ref.current;
210+
const newError = p.validate(v);
211+
if (!!newError) {
212+
setError(newError);
213+
return false;
214+
}
215+
216+
p.save(v);
217+
p.onClose();
218+
return true;
219+
};
220+
221+
return (
222+
<Modal
223+
visible={true}
224+
title={"New Blocked Repository"}
225+
onClose={p.onClose}
226+
onEnter={save}
227+
buttons={[
228+
<button className="secondary" onClick={p.onClose}>
229+
Cancel
230+
</button>,
231+
<button className="ml-2" onClick={save}>
232+
Add Blocked Repository
233+
</button>,
234+
]}
235+
>
236+
<Details br={br} update={update} error={error} />
237+
</Modal>
238+
);
239+
}
240+
241+
function DeleteBlockedRepositoryModal(props: {
242+
blockedRepository: ExistingBlockedRepository;
243+
deleteBlockedRepository: () => void;
244+
onClose: () => void;
245+
}) {
246+
return (
247+
<ConfirmationModal
248+
title="Delete Blocked Repository"
249+
areYouSureText="Are you sure you want to delete this repository from the list?"
250+
buttonText="Delete Blocked Repository"
251+
onClose={props.onClose}
252+
onConfirm={() => {
253+
props.deleteBlockedRepository();
254+
props.onClose();
255+
}}
256+
>
257+
<Details br={props.blockedRepository} />
258+
</ConfirmationModal>
259+
);
260+
}
261+
262+
function Details(props: {
263+
br: NewBlockedRepository;
264+
error?: string;
265+
update?: (pev: Partial<NewBlockedRepository>) => void;
266+
}) {
267+
return (
268+
<div className="border-gray-200 dark:border-gray-800 -mx-6 px-6 py-4 flex flex-col">
269+
{props.error ? (
270+
<div className="bg-gitpod-kumquat-light rounded-md p-3 text-gitpod-red text-sm mb-2">{props.error}</div>
271+
) : null}
272+
<div>
273+
<h4>Repository URL RegEx</h4>
274+
<input
275+
autoFocus
276+
className="w-full"
277+
type="text"
278+
value={props.br.urlRegexp}
279+
disabled={!props.update}
280+
onChange={(v) => {
281+
if (!!props.update) {
282+
props.update({ urlRegexp: v.target.value });
283+
}
284+
}}
285+
/>
286+
</div>
287+
<CheckBox
288+
title={"Block User"}
289+
desc={"Block any user that tries to open a workspace for a repository URL that matches this RegEx."}
290+
checked={props.br.blockUser}
291+
disabled={!props.update}
292+
onChange={(v) => {
293+
if (!!props.update) {
294+
props.update({ blockUser: v.target.checked });
295+
}
296+
}}
297+
/>
298+
</div>
299+
);
300+
}

components/dashboard/src/admin/BlockedRepositorySettings.tsx

-15
This file was deleted.

0 commit comments

Comments
 (0)