-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #973 from nextstrain/list-group-members
Add page for listing Group members
- Loading branch information
Showing
19 changed files
with
242 additions
and
60 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
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 |
---|---|---|
@@ -1,34 +1,36 @@ | ||
import dynamic from 'next/dynamic' | ||
import { useRouter } from 'next/router' | ||
const IndividualGroupPage = dynamic(() => import("../../src/sections/individual-group-page"), {ssr: false}) | ||
const GroupMembersPage = dynamic(() => import("../../src/sections/group-members-page.tsx"), {ssr: false}) | ||
const GroupSettingsPage = dynamic(() => import("../../src/sections/group-settings-page"), {ssr: false}) | ||
|
||
|
||
/** | ||
* NextJS dynamic routing doesn't allow us to set up routes where we have (e.g.) | ||
* groups/:groupName/settings -> <GroupSettingsPage> | ||
* groups/:groupName/settings/members -> <GroupMembersPage> | ||
* groups/:groupName[/*] -> <IndividualGroupPage> | ||
* so we use some JS routing here to serve the right component | ||
*/ | ||
const Index = () => { | ||
const {query} = useRouter(); | ||
if (!query.groupName) return null; // wait until query ready | ||
|
||
|
||
|
||
|
||
/* nextJs dynamic routing will set the part parts _beyond_ "groups/" as query.groupName */ | ||
if (query.groupName.length===2 && query.groupName[1]==='settings') { | ||
// param location TODO!!! | ||
return <GroupSettingsPage groupName={query.groupName[0]}/> | ||
const groupName = query.groupName[0] | ||
const resourcePath = query.groupName.slice(1).join("/") | ||
|
||
switch(resourcePath) { | ||
case 'settings': | ||
return <GroupSettingsPage groupName={groupName}/> | ||
case 'settings/members': | ||
return <GroupMembersPage groupName={groupName}/> | ||
default: | ||
return <IndividualGroupPage | ||
groupName={groupName} | ||
resourcePath={resourcePath.length ? resourcePath : undefined} | ||
/> | ||
} | ||
|
||
const resourcePath = query.groupName.slice(1); | ||
|
||
return ( | ||
<IndividualGroupPage groupName={query.groupName[0]} resourcePath={resourcePath.length ? resourcePath : undefined}/> | ||
); | ||
|
||
} | ||
|
||
export default Index; | ||
export default Index; |
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
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,159 @@ | ||
import React, { useEffect, useState } from "react"; | ||
import styled from "styled-components"; | ||
import { startCase } from "lodash" | ||
import { uri } from "../../../src/templateLiterals.js"; | ||
import GenericPage from "../layouts/generic-page.jsx"; | ||
import { BigSpacer, CenteredContainer, FlexGridRight, MediumSpacer } from "../layouts/generalComponents.jsx"; | ||
import * as splashStyles from "../components/splash/styles"; | ||
import { ErrorBanner } from "../components/errorMessages.jsx"; | ||
|
||
interface GroupMember { | ||
username: string, | ||
roles: string[] | ||
} | ||
|
||
interface ErrorMessage { | ||
title: string, | ||
contents: string | ||
} | ||
|
||
const GroupMembersPage = ({ groupName }: {groupName: string}) => { | ||
const [ errorMessage, setErrorMessage ] = useState<ErrorMessage>({title: "", contents: ""}); | ||
const [ roles, setRoles ] = useState<string[]>([]); | ||
const [ members, setMembers ] = useState<GroupMember[]>([]); | ||
|
||
useEffect(() => { | ||
async function getGroupMembership(groupName: string) { | ||
const headers = { headers: {"Accept": "application/json"}}; | ||
let roles, members = []; | ||
try { | ||
const [ rolesResponse, membersResponse ] = await Promise.all([ | ||
fetch(uri`/groups/${groupName}/settings/roles`, headers), | ||
fetch(uri`/groups/${groupName}/settings/members`, headers) | ||
]); | ||
if (!rolesResponse.ok) { | ||
throw new Error(`Fetching group roles failed: ${rolesResponse.status} ${rolesResponse.statusText}`) | ||
} | ||
if (!membersResponse.ok) { | ||
throw new Error(`Fetching group members failed: ${membersResponse.status} ${membersResponse.statusText}`) | ||
} | ||
roles = await rolesResponse.json(); | ||
members = await membersResponse.json(); | ||
} catch (err) { | ||
const errorMessage = (err as Error).message | ||
if(!ignore) { | ||
setErrorMessage({ | ||
title: "An error occurred when trying to fetch group membership data", | ||
contents: errorMessage}) | ||
} | ||
} | ||
return {roles, members}; | ||
} | ||
|
||
let ignore = false; | ||
getGroupMembership(groupName).then(result => { | ||
if (!ignore) { | ||
setRoles(result.roles); | ||
setMembers(result.members); | ||
} | ||
}) | ||
return () => { | ||
ignore = true; | ||
}; | ||
}, [groupName]); | ||
|
||
return ( | ||
<GenericPage banner={errorMessage.title ? <ErrorBanner {...errorMessage} /> : undefined}> | ||
<FlexGridRight> | ||
<splashStyles.Button to={uri`/groups/${groupName}`}> | ||
Return to {`"${groupName}"`} Page | ||
</splashStyles.Button> | ||
</FlexGridRight> | ||
<MediumSpacer/> | ||
|
||
<splashStyles.H2> | ||
{`"${groupName}"`} Group Members | ||
</splashStyles.H2> | ||
<BigSpacer/> | ||
|
||
{roles && members | ||
? <MembersTable members={members as GroupMember[]} /> | ||
: <splashStyles.H4>Fetching group members...</splashStyles.H4>} | ||
</GenericPage> | ||
) | ||
}; | ||
|
||
const MembersTableContainer = styled.div` | ||
border: 1px solid #CCC; | ||
.row { | ||
border-bottom: 1px solid #CCC; | ||
} | ||
.row:last-child { | ||
border-bottom: 0; | ||
} | ||
.row:nth-child(even) { | ||
background-color: #F1F1F1; | ||
} | ||
p { | ||
margin: 10px; | ||
} | ||
`; | ||
|
||
const MembersTable = ({ members }: { members: GroupMember[]}) => { | ||
const sortedMembers = members.toSorted((a, b) => a.username.localeCompare(b.username)); | ||
function prettifyRoles(memberRoles: string[]) { | ||
// Prettify the role names by making them singular and capitalized | ||
return memberRoles.map((roleName) => startCase(roleName.replace(/s$/, ''))).join(", "); | ||
} | ||
|
||
return ( | ||
<CenteredContainer> | ||
<MembersTableContainer> | ||
<div className="row no-gutters"> | ||
<div className="col"> | ||
<splashStyles.CenteredFocusParagraph> | ||
<strong>Username</strong> | ||
</splashStyles.CenteredFocusParagraph> | ||
</div> | ||
<div className="col"> | ||
<splashStyles.CenteredFocusParagraph> | ||
<strong>Roles</strong> | ||
</splashStyles.CenteredFocusParagraph> | ||
</div> | ||
</div> | ||
|
||
{sortedMembers.map((member) => | ||
<div className="row no-gutters" key={member.username}> | ||
<div className="col"> | ||
<splashStyles.CenteredFocusParagraph> | ||
{member.username} | ||
</splashStyles.CenteredFocusParagraph> | ||
</div> | ||
<div className="col"> | ||
<splashStyles.CenteredFocusParagraph> | ||
{prettifyRoles(member.roles)} | ||
</splashStyles.CenteredFocusParagraph> | ||
</div> | ||
</div> | ||
)} | ||
</MembersTableContainer> | ||
</CenteredContainer> | ||
) | ||
}; | ||
|
||
export async function canViewGroupMembers(groupName: string) { | ||
try { | ||
const groupMemberOptions = await fetch(uri`/groups/${groupName}/settings/members`, { method: "OPTIONS"}); | ||
if ([401, 403].includes(groupMemberOptions.status)) { | ||
console.log("You can ignore the console error above; it is used to determine whether the members can be shown."); | ||
} | ||
const allowedMethods = new Set(groupMemberOptions.headers.get("Allow")?.split(/\s*,\s*/)); | ||
return allowedMethods.has("GET"); | ||
} catch (err) { | ||
const errorMessage = (err as Error).message | ||
console.error("Cannot check user permissions to view group members", errorMessage); | ||
} | ||
return false | ||
} | ||
|
||
export default GroupMembersPage; |
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.