-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
306 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/** | ||
* Copyright (c) 2022 Gitpod GmbH. All rights reserved. | ||
* Licensed under the GNU Affero General Public License (AGPL). | ||
* See License-AGPL.txt in the project root for license information. | ||
*/ | ||
|
||
import moment from "moment"; | ||
import { useEffect, useState } from "react"; | ||
import { Team, TeamMemberInfo, TeamMemberRole } from "@gitpod/gitpod-protocol"; | ||
import { getGitpodService } from "../service/service"; | ||
import { Item, ItemField, ItemsList } from "../components/ItemsList"; | ||
import PillLabel from "../components/PillLabel"; | ||
import DropDown from "../components/DropDown"; | ||
import { Link } from "react-router-dom"; | ||
|
||
export default function TeamDetail(props: { team: Team }) { | ||
const { team } = props; | ||
const [teamMembers, setTeamMembers] = useState<TeamMemberInfo[] | undefined>(undefined); | ||
const [searchText, setSearchText] = useState<string>(''); | ||
|
||
useEffect(() => { | ||
(async () => { | ||
const members = await getGitpodService().server.adminGetTeamMembers(team.id); | ||
if (members.length > 0) { | ||
setTeamMembers(members) | ||
} | ||
})(); | ||
}, [team]); | ||
|
||
const filteredMembers = teamMembers?.filter(m => { | ||
const memberSearchText = `${m.fullName || ''}${m.primaryEmail || ''}`.toLocaleLowerCase(); | ||
if (!memberSearchText.includes(searchText.toLocaleLowerCase())) { | ||
return false; | ||
} | ||
return true; | ||
}); | ||
|
||
const setTeamMemberRole = async (userId: string, role: TeamMemberRole) => { | ||
await getGitpodService().server.adminSetTeamMemberRole(team!.id, userId, role); | ||
setTeamMembers(await getGitpodService().server.adminGetTeamMembers(team!.id)); | ||
} | ||
return <> | ||
<div className="flex"> | ||
<div className="flex-1"> | ||
<div className="flex"><h3>{team.name}</h3> | ||
{team.markedDeleted && <span className="mt-2"><PillLabel type="warn" className="font-semibold mt-2 py-0.5 px-2 self-center">Deleted</PillLabel></span>} | ||
</div> | ||
<p className="mb-6">/t/{team.slug}</p> | ||
{!team.markedDeleted && <> | ||
<span className="text-gray-400">{teamMembers && teamMembers.length} member(s)</span> | ||
<span className="text-gray-400"> · </span> | ||
</>} | ||
<span className="text-gray-400">Created on {moment(team.creationTime).format('MMM D, YYYY')}</span> | ||
</div> | ||
</div> | ||
<div className="flex mt-4"> | ||
<div className="flex"> | ||
<div className="py-4"> | ||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16" width="16" height="16"><path fill="#A8A29E" 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" /></svg> | ||
</div> | ||
<input type="search" placeholder="Search Members" onChange={e => setSearchText(e.target.value)} /> | ||
</div> | ||
</div> | ||
|
||
<ItemsList className="mt-2"> | ||
{!team.markedDeleted && <Item header={true} className="grid grid-cols-3"> | ||
<ItemField className="my-auto"> | ||
<span className="pl-14">Name</span> | ||
</ItemField> | ||
<ItemField className="flex items-center space-x-1 my-auto"> | ||
<span>Joined</span> | ||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" className="h-4 w-4" viewBox="0 0 16 16"><path fill="#A8A29E" fill-rule="evenodd" d="M13.366 8.234a.8.8 0 010 1.132l-4.8 4.8a.8.8 0 01-1.132 0l-4.8-4.8a.8.8 0 111.132-1.132L7.2 11.67V2.4a.8.8 0 111.6 0v9.269l3.434-3.435a.8.8 0 011.132 0z" clip-rule="evenodd" /></svg> | ||
</ItemField> | ||
<ItemField className="flex items-center my-auto"> | ||
<span className="flex-grow">Role</span> | ||
</ItemField> | ||
</Item>} | ||
{!team.markedDeleted && (!filteredMembers || filteredMembers.length === 0) | ||
? <p className="pt-16 text-center">No members found</p> | ||
: filteredMembers && filteredMembers.map(m => <Item className="grid grid-cols-3" key={m.userId}> | ||
<ItemField className="flex items-center my-auto"> | ||
<div className="w-14">{m.avatarUrl && <img className="rounded-full w-8 h-8" src={m.avatarUrl || ''} alt={m.fullName} />}</div> | ||
<Link to={"/admin/users/" + m.userId}><div> | ||
<div className="text-base text-gray-900 dark:text-gray-50 font-medium">{m.fullName}</div> | ||
<p>{m.primaryEmail}</p> | ||
</div></Link> | ||
</ItemField> | ||
<ItemField className="my-auto"> | ||
<span className="text-gray-400">{moment(m.memberSince).fromNow()}</span> | ||
</ItemField> | ||
<ItemField className="flex items-center my-auto"> | ||
<span className="text-gray-400 capitalize"> | ||
<DropDown contextMenuWidth="w-32" activeEntry={m.role} entries={[{ | ||
title: 'owner', | ||
onClick: () => setTeamMemberRole(m.userId, 'owner') | ||
}, { | ||
title: 'member', | ||
onClick: () => setTeamMemberRole(m.userId, 'member') | ||
}]} /> | ||
</span> | ||
</ItemField> | ||
</Item>)} | ||
</ItemsList> | ||
</> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
/** | ||
* Copyright (c) 2022 Gitpod GmbH. All rights reserved. | ||
* Licensed under the GNU Affero General Public License (AGPL). | ||
* See License-AGPL.txt in the project root for license information. | ||
*/ | ||
|
||
import moment from "moment"; | ||
import { useState, useContext, useEffect } from "react"; | ||
|
||
import TeamDetail from "./TeamDetail"; | ||
import { adminMenu } from "./admin-menu"; | ||
import { useLocation } from "react-router"; | ||
import { Link, Redirect } from "react-router-dom"; | ||
import { UserContext } from "../user-context"; | ||
import { getGitpodService } from "../service/service"; | ||
import { PageWithSubMenu } from "../components/PageWithSubMenu"; | ||
import { AdminGetListResult, Team } from "@gitpod/gitpod-protocol"; | ||
import PillLabel from "../components/PillLabel"; | ||
|
||
export default function TeamsSearchPage() { | ||
return ( | ||
<PageWithSubMenu subMenu={adminMenu} title="Teams" subtitle="Search and manage teams."> | ||
<TeamsSearch /> | ||
</PageWithSubMenu> | ||
) | ||
} | ||
|
||
export function TeamsSearch() { | ||
const location = useLocation(); | ||
const { user } = useContext(UserContext); | ||
const [searching, setSearching] = useState(false); | ||
const [searchTerm, setSearchTerm] = useState(''); | ||
const [currentTeam, setCurrentTeam] = useState<Team | undefined>(undefined); | ||
const [searchResult, setSearchResult] = useState<AdminGetListResult<Team>>({ total: 0, rows: [] }); | ||
|
||
useEffect(() => { | ||
const teamId = location.pathname.split('/')[3]; | ||
if (teamId && searchResult) { | ||
let foundTeam = searchResult.rows.find(team => team.id === teamId); | ||
if (foundTeam) { | ||
setCurrentTeam(foundTeam); | ||
} else { | ||
getGitpodService().server.adminGetTeamById(teamId) | ||
.then(team => setCurrentTeam(team)) | ||
.catch(e => console.error(e)); | ||
} | ||
} else { | ||
setCurrentTeam(undefined); | ||
} | ||
}, [location]); | ||
|
||
if (!user || !user?.rolesOrPermissions?.includes('admin')) { | ||
return <Redirect to="/"/> | ||
} | ||
|
||
if (currentTeam) { | ||
return <TeamDetail team={currentTeam} />; | ||
} | ||
|
||
const search = async () => { | ||
setSearching(true); | ||
try { | ||
const result = await getGitpodService().server.adminGetTeams({ | ||
searchTerm, | ||
limit: 100, | ||
orderBy: 'creationTime', | ||
offset: 0, | ||
orderDir: "desc" | ||
}) | ||
setSearchResult(result); | ||
} finally { | ||
setSearching(false); | ||
} | ||
} | ||
return <> | ||
<div className="pt-8 flex"> | ||
<div className="flex justify-between w-full"> | ||
<div className="flex"> | ||
<div className="py-4"> | ||
<svg className={searching ? 'animate-spin' : ''} width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
<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" /> | ||
</svg> | ||
</div> | ||
<input type="search" placeholder="Search Teams" onKeyDown={(k) => k.key === 'Enter' && search()} onChange={(v) => { setSearchTerm(v.target.value) }} /> | ||
</div> | ||
<button disabled={searching} onClick={search}>Search</button> | ||
</div> | ||
</div> | ||
<div className="flex flex-col space-y-2"> | ||
<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"> | ||
<div className="w-7/12">Name</div> | ||
<div className="w-5/12 flex items-center"><span>Created</span> | ||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" className="h-4 w-4" viewBox="0 0 16 16"><path fill="#A8A29E" fill-rule="evenodd" d="M13.366 8.234a.8.8 0 010 1.132l-4.8 4.8a.8.8 0 01-1.132 0l-4.8-4.8a.8.8 0 111.132-1.132L7.2 11.67V2.4a.8.8 0 111.6 0v9.269l3.434-3.435a.8.8 0 011.132 0z" clip-rule="evenodd" /></svg> | ||
</div> | ||
|
||
|
||
</div> | ||
{searchResult.rows.map(team => <TeamResultItem team={team} />)} | ||
</div> | ||
</> | ||
|
||
function TeamResultItem(props: { team: Team }) { | ||
return ( | ||
<Link key={'pr-' + props.team.name} to={'/admin/teams/' + props.team.id} data-analytics='{"button_type":"sidebar_menu"}'> | ||
<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"> | ||
<div className="flex flex-col w-7/12 truncate"> | ||
<div className="font-medium text-gray-800 dark:text-gray-100 truncate">{props.team.name} | ||
{props.team.markedDeleted && <span><PillLabel type="warn" className="font-semibold mt-2 py-0.5 px-2 self-center">Deleted</PillLabel></span>} | ||
</div> | ||
</div> | ||
<div className="flex w-5/12 self-center"> | ||
<div className="text-sm w-full text-gray-400 truncate">{moment(props.team.creationTime).format('MMM D, YYYY')}</div> | ||
</div> | ||
</div> | ||
</Link> | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.