Skip to content

Commit f328629

Browse files
committed
[dashboard] BlockedRepo UI
1 parent 058c31e commit f328629

File tree

3 files changed

+277
-17
lines changed

3 files changed

+277
-17
lines changed

components/dashboard/src/App.tsx

Lines changed: 2 additions & 2 deletions
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

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

components/dashboard/src/admin/BlockedRepositorySettings.tsx

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)