Skip to content

Commit 4d9d49d

Browse files
Laurie T. Malauroboquat
Laurie T. Malau
authored andcommitted
Allow project search and show project detail
1 parent 803e93f commit 4d9d49d

File tree

19 files changed

+431
-55
lines changed

19 files changed

+431
-55
lines changed

components/dashboard/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const FromReferrer = React.lazy(() => import(/* webpackPrefetch: true */ './From
5454
const UserSearch = React.lazy(() => import(/* webpackPrefetch: true */ './admin/UserSearch'));
5555
const WorkspacesSearch = React.lazy(() => import(/* webpackPrefetch: true */ './admin/WorkspacesSearch'));
5656
const AdminSettings = React.lazy(() => import(/* webpackPrefetch: true */ './admin/Settings'));
57+
const ProjectsSearch = React.lazy(() => import(/* webpackPrefetch: true */ './admin/ProjectsSearch'));
5758
const OAuthClientApproval = React.lazy(() => import(/* webpackPrefetch: true */ './OauthClientApproval'));
5859

5960
function Loading() {
@@ -288,6 +289,7 @@ function App() {
288289
<Route path="/admin/users" component={UserSearch} />
289290
<Route path="/admin/workspaces" component={WorkspacesSearch} />
290291
<Route path="/admin/settings" component={AdminSettings} />
292+
<Route path="/admin/projects" component={ProjectsSearch} />
291293

292294
<Route path={["/", "/login"]} exact>
293295
<Redirect to={workspacesPathMain} />
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 moment from "moment";
8+
import { Link } from "react-router-dom";
9+
import { Project } from "@gitpod/gitpod-protocol";
10+
import Prebuilds from "../projects/Prebuilds"
11+
import Property from "./Property";
12+
13+
export default function ProjectDetail(props: { project: Project, owner: string | undefined }) {
14+
return <>
15+
<div className="flex">
16+
<div className="flex-1">
17+
<div className="flex"><h3>{props.project.name}</h3><span className="my-auto"></span></div>
18+
<p>{props.project.cloneUrl}</p>
19+
</div>
20+
</div>
21+
<div className="flex flex-col w-full -ml-3">
22+
<div className="flex w-full mt-6">
23+
<Property name="Created">{moment(props.project.creationTime).format('MMM D, YYYY')}</Property>
24+
<Property name="Repository"><a className="text-blue-400 dark:text-blue-600 hover:text-blue-600 dark:hover:text-blue-400 truncate" href={props.project.cloneUrl}>{props.project.name}</a></Property>
25+
{props.project.userId ?
26+
<Property name="Owner">
27+
<>
28+
<Link className="text-blue-400 dark:text-blue-600 hover:text-blue-600 dark:hover:text-blue-400 truncate" to={"/admin/users/" + props.project.userId}>{props.owner}</Link>
29+
<span className="text-gray-400 dark:text-gray-500"> (User)</span>
30+
</>
31+
</Property>
32+
:
33+
<Property name="Owner">{`${props.owner} (Team)`}</Property>}
34+
</div>
35+
<div className="flex w-full mt-6">
36+
<Property name="Incremental Prebuilds">{props.project.settings?.useIncrementalPrebuilds ? "Yes" : "No"}</Property>
37+
<Property name="Marked Deleted">{props.project.markedDeleted ? "Yes" : "No"}</Property>
38+
</div>
39+
</div>
40+
<div className="mt-6">
41+
<Prebuilds project={props.project} isAdminDashboard={true} />
42+
</div>
43+
</>;
44+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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 moment from "moment";
8+
import { useLocation } from "react-router";
9+
import { Link, Redirect } from "react-router-dom";
10+
import { useContext, useState, useEffect } from "react";
11+
12+
import { adminMenu } from "./admin-menu";
13+
import ProjectDetail from "./ProjectDetail";
14+
import { UserContext } from "../user-context";
15+
import { getGitpodService } from "../service/service";
16+
import { PageWithSubMenu } from "../components/PageWithSubMenu";
17+
import { AdminGetListResult, Project } from "@gitpod/gitpod-protocol";
18+
19+
export default function ProjectsSearchPage() {
20+
return (
21+
<PageWithSubMenu subMenu={adminMenu} title="Projects" subtitle="Search and manage all projects.">
22+
<ProjectsSearch />
23+
</PageWithSubMenu>
24+
)
25+
}
26+
27+
export function ProjectsSearch() {
28+
const location = useLocation();
29+
const { user } = useContext(UserContext);
30+
const [searchTerm, setSearchTerm] = useState('');
31+
const [searching, setSearching] = useState(false);
32+
const [searchResult, setSearchResult] = useState<AdminGetListResult<Project>>({ total: 0, rows: [] });
33+
const [currentProject, setCurrentProject] = useState<Project | undefined>(undefined);
34+
const [currentProjectOwner, setCurrentProjectOwner] = useState<string | undefined>("");
35+
36+
useEffect(() => {
37+
const projectId = location.pathname.split('/')[3];
38+
if (projectId && searchResult) {
39+
let currentProject = searchResult.rows.find(project => project.id === projectId);
40+
if (currentProject) {
41+
setCurrentProject(currentProject);
42+
} else {
43+
getGitpodService().server.adminGetProjectById(projectId)
44+
.then(project => setCurrentProject(project))
45+
.catch(e => console.error(e));
46+
}
47+
} else {
48+
setCurrentProject(undefined);
49+
}
50+
}, [location]);
51+
52+
useEffect(() => {
53+
(async () => {
54+
if (currentProject) {
55+
if (currentProject.userId) {
56+
const owner = await getGitpodService().server.adminGetUser(currentProject.userId);
57+
if (owner) { setCurrentProjectOwner(owner?.name) }
58+
}
59+
if (currentProject.teamId) {
60+
const owner = await getGitpodService().server.adminGetTeamById(currentProject.teamId);
61+
if (owner) { setCurrentProjectOwner(owner?.name) }
62+
}
63+
}
64+
})();
65+
}, [currentProject])
66+
67+
if (!user || !user?.rolesOrPermissions?.includes('admin')) {
68+
return <Redirect to="/" />
69+
}
70+
71+
if (currentProject) {
72+
return <ProjectDetail project={currentProject} owner={currentProjectOwner} />;
73+
}
74+
75+
const search = async () => {
76+
setSearching(true);
77+
try {
78+
const result = await getGitpodService().server.adminGetProjectsBySearchTerm({
79+
searchTerm,
80+
limit: 50,
81+
orderBy: 'creationTime',
82+
offset: 0,
83+
orderDir: "desc"
84+
})
85+
setSearchResult(result);
86+
} finally {
87+
setSearching(false);
88+
}
89+
}
90+
91+
return <>
92+
<div className="pt-8 flex">
93+
<div className="flex justify-between w-full">
94+
<div className="flex">
95+
<div className="py-4">
96+
<svg className={searching ? 'animate-spin' : ''} width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
97+
<path fillRule="evenodd" clipRule="evenodd" 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" fill="#A8A29E" />
98+
</svg>
99+
</div>
100+
<input type="search" placeholder="Search Projects" onKeyDown={(k) => k.key === 'Enter' && search()} onChange={(v) => { setSearchTerm(v.target.value) }} />
101+
</div>
102+
<button disabled={searching} onClick={search}>Search</button>
103+
</div>
104+
</div>
105+
<div className="flex flex-col space-y-2">
106+
<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">
107+
<div className="w-4/12">Name</div>
108+
<div className="w-6/12">Clone URL</div>
109+
<div className="w-2/12">Created</div>
110+
</div>
111+
{searchResult.rows.map(project => <ProjectResultItem project={project} />)}
112+
</div>
113+
</>
114+
115+
function ProjectResultItem(p: { project: Project }) {
116+
return (
117+
<Link key={'pr-' + p.project.name} to={'/admin/projects/' + p.project.id} data-analytics='{"button_type":"sidebar_menu"}'>
118+
<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">
119+
<div className="flex flex-col w-4/12 truncate">
120+
<div className="font-medium text-gray-800 dark:text-gray-100 truncate">{p.project.name}</div>
121+
</div>
122+
<div className="flex flex-col w-6/12 truncate">
123+
<div className="font-medium text-gray-800 dark:text-gray-100 truncate">{p.project.cloneUrl}</div>
124+
</div>
125+
<div className="flex w-2/12 self-center">
126+
<div className="text-sm w-full text-gray-400 truncate">{moment(p.project.creationTime).fromNow()}</div>
127+
</div>
128+
</div>
129+
</Link>
130+
)
131+
}
132+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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 { ReactChild } from 'react';
8+
9+
function Property(p: { name: string, children: string | ReactChild, actions?: { label: string, onClick: () => void }[] }) {
10+
return <div className="ml-3 flex flex-col w-4/12 truncate">
11+
<div className="text-base text-gray-500 truncate">
12+
{p.name}
13+
</div>
14+
<div className="text-lg text-gray-600 font-semibold truncate">
15+
{p.children}
16+
</div>
17+
{(p.actions || []).map(a =>
18+
<div className="cursor-pointer text-sm text-blue-400 dark:text-blue-600 hover:text-blue-600 dark:hover:text-blue-400 truncate" onClick={a.onClick}>
19+
{a.label || ''}
20+
</div>
21+
)}
22+
</div>;
23+
}
24+
25+
export default Property;

components/dashboard/src/admin/UserDetail.tsx

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import { NamedWorkspaceFeatureFlag, Permissions, RoleOrPermission, Roles, User,
88
import { AccountStatement, Subscription } from "@gitpod/gitpod-protocol/lib/accounting-protocol";
99
import { Plans } from "@gitpod/gitpod-protocol/lib/plans";
1010
import moment from "moment";
11-
import { ReactChild, useEffect, useRef, useState } from "react";
11+
import { useEffect, useRef, useState } from "react";
1212
import CheckBox from "../components/CheckBox";
1313
import Modal from "../components/Modal";
1414
import { PageWithSubMenu } from "../components/PageWithSubMenu"
1515
import { getGitpodService } from "../service/service";
1616
import { adminMenu } from "./admin-menu"
1717
import { WorkspaceSearch } from "./WorkspacesSearch";
18+
import Property from "./Property";
1819

1920

2021
export default function UserDetail(p: { user: User }) {
@@ -199,22 +200,6 @@ function Label(p: { text: string, color: string }) {
199200
return <div className={`ml-3 text-sm text-${p.color}-600 truncate bg-${p.color}-100 px-1.5 py-0.5 rounded-md my-auto`}>{p.text}</div>;
200201
}
201202

202-
export function Property(p: { name: string, children: string | ReactChild, actions?: { label: string, onClick: () => void }[] }) {
203-
return <div className="ml-3 flex flex-col w-4/12 truncate">
204-
<div className="text-base text-gray-500 truncate">
205-
{p.name}
206-
</div>
207-
<div className="text-lg text-gray-600 font-semibold truncate">
208-
{p.children}
209-
</div>
210-
{(p.actions || []).map(a =>
211-
<div className="cursor-pointer text-sm text-blue-400 dark:text-blue-600 hover:text-blue-600 dark:hover:text-blue-400 truncate" onClick={a.onClick}>
212-
{a.label || ''}
213-
</div>
214-
)}
215-
</div>;
216-
}
217-
218203
interface Entry {
219204
title: string,
220205
checked: boolean,

components/dashboard/src/admin/WorkspaceDetail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Link } from "react-router-dom";
1212
import { getGitpodService } from "../service/service";
1313
import { getProject, WorkspaceStatusIndicator } from "../workspaces/WorkspaceEntry";
1414
import { getAdminLinks } from "./gcp-info";
15-
import { Property } from "./UserDetail";
15+
import Property from "./Property";
1616

1717
export default function WorkspaceDetail(props: { workspace: WorkspaceAndInstance }) {
1818
const [workspace, setWorkspace] = useState(props.workspace);

components/dashboard/src/admin/admin-menu.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,21 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7-
export const adminMenu = [{
8-
title: 'Users',
9-
link: ['/admin/users', '/admin']
10-
}, {
11-
title: 'Workspaces',
12-
link: ['/admin/workspaces']
13-
}, {
14-
title: 'Settings',
15-
link: ['/admin/settings']
16-
},];
7+
export const adminMenu = [
8+
{
9+
title: 'Users',
10+
link: ['/admin/users', '/admin']
11+
},
12+
{
13+
title: 'Workspaces',
14+
link: ['/admin/workspaces']
15+
},
16+
{
17+
title: 'Projects',
18+
link: ['/admin/projects']
19+
},
20+
{
21+
title: 'Settings',
22+
link: ['/admin/settings']
23+
}
24+
];

0 commit comments

Comments
 (0)