diff --git a/web/src/components/modals/robotAccountWizard/AddToTeam.tsx b/web/src/components/modals/robotAccountWizard/AddToTeam.tsx
index 137794804..ba781fa9d 100644
--- a/web/src/components/modals/robotAccountWizard/AddToTeam.tsx
+++ b/web/src/components/modals/robotAccountWizard/AddToTeam.tsx
@@ -1,40 +1,24 @@
import {
- TableComposable,
- Tbody,
- Td,
- Th,
- Thead,
- Tr,
-} from '@patternfly/react-table';
-import {
- PageSection,
- PanelFooter,
- ToggleGroup,
- ToggleGroupItem,
- ToggleGroupItemProps,
- Toolbar,
- ToolbarContent,
- ToolbarItem,
DropdownItem,
Button,
Text,
TextVariants,
TextContent,
} from '@patternfly/react-core';
-import React, {useEffect, useState} from 'react';
+import {useState} from 'react';
import {DesktopIcon} from '@patternfly/react-icons';
import ToggleDrawer from 'src/components/ToggleDrawer';
import NameAndDescription from 'src/components/modals/robotAccountWizard/NameAndDescription';
-import {useTeams} from 'src/hooks/useTeams';
import {addDisplayError} from 'src/resources/ErrorHandling';
import TeamView from './TeamView';
+import {useCreateTeam} from 'src/hooks/UseTeams';
export default function AddToTeam(props: AddToTeamProps) {
const [newTeamName, setNewTeamName] = useState('');
const [newTeamDescription, setNewTeamDescription] = useState('');
const [err, setErr] = useState
();
- const {createNewTeamHook} = useTeams(props.namespace);
+ const {createNewTeamHook} = useCreateTeam(props.namespace);
const createNewTeam = () => {
props.setDrawerExpanded(true);
diff --git a/web/src/components/toolbar/Kebab.tsx b/web/src/components/toolbar/Kebab.tsx
index 2749c3d4c..9281519a6 100644
--- a/web/src/components/toolbar/Kebab.tsx
+++ b/web/src/components/toolbar/Kebab.tsx
@@ -8,7 +8,7 @@ export function Kebab(props: KebabProps) {
const fetchToggle = () => {
if (!props.useActions) {
- return ;
+ return ;
}
return (
void;
kebabItems: React.ReactElement[];
useActions?: boolean;
+ id?: string;
};
diff --git a/web/src/hooks/UseMembers.ts b/web/src/hooks/UseMembers.ts
new file mode 100644
index 000000000..b77cc28d8
--- /dev/null
+++ b/web/src/hooks/UseMembers.ts
@@ -0,0 +1,254 @@
+import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
+import {useState} from 'react';
+import {SearchState} from 'src/components/toolbar/SearchTypes';
+import {
+ IMemberTeams,
+ IMembers,
+ deleteCollaboratorForOrg,
+ deleteTeamMemberForOrg,
+ fetchCollaboratorsForOrg,
+ fetchMembersForOrg,
+ fetchTeamMembersForOrg,
+} from 'src/resources/MembersResource';
+import {IAvatar} from 'src/resources/OrganizationResource';
+import {collaboratorViewColumnNames} from 'src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/CollaboratorsView/CollaboratorsViewList';
+import {memberViewColumnNames} from 'src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/MembersView/MembersViewList';
+import {manageMemberColumnNames} from 'src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersList';
+
+export function useFetchMembers(orgName: string) {
+ const [page, setPage] = useState(1);
+ const [perPage, setPerPage] = useState(10);
+ const [search, setSearch] = useState({
+ query: '',
+ field: memberViewColumnNames.username,
+ });
+
+ const {
+ data: members,
+ isLoading,
+ isPlaceholderData,
+ isError: errorLoadingMembers,
+ } = useQuery(
+ ['members'],
+ ({signal}) => fetchMembersForOrg(orgName, signal),
+ {
+ placeholderData: [],
+ },
+ );
+
+ const filteredMembers =
+ search.query !== ''
+ ? members?.filter((member) => member.name.includes(search.query))
+ : members;
+
+ const paginatedMembers = filteredMembers?.slice(
+ page * perPage - perPage,
+ page * perPage - perPage + perPage,
+ );
+
+ return {
+ members,
+ filteredMembers,
+ paginatedMembers: paginatedMembers,
+ loading: isLoading || isPlaceholderData,
+ error: errorLoadingMembers,
+ page,
+ setPage,
+ perPage,
+ setPerPage,
+ search,
+ setSearch,
+ };
+}
+
+export interface ITeamMember {
+ name: string;
+ kind: string;
+ is_robot: false;
+ avatar: IAvatar;
+ invited: boolean;
+}
+
+export function useFetchTeamMembersForOrg(orgName: string, teamName: string) {
+ const [page, setPage] = useState(1);
+ const [perPage, setPerPage] = useState(10);
+ const [search, setSearch] = useState({
+ query: '',
+ field: manageMemberColumnNames.teamMember,
+ });
+
+ const {
+ data,
+ isLoading,
+ isPlaceholderData,
+ isError: errorLoadingTeamMembers,
+ } = useQuery(
+ ['teamMembers'],
+ ({signal}) => fetchTeamMembersForOrg(orgName, teamName, signal),
+ {
+ placeholderData: [],
+ },
+ );
+
+ const allMembers: ITeamMember[] = data;
+ const filteredAllMembers =
+ search.query !== ''
+ ? allMembers?.filter((member) => member.name.includes(search.query))
+ : allMembers;
+ const paginatedAllMembers = filteredAllMembers?.slice(
+ page * perPage - perPage,
+ page * perPage - perPage + perPage,
+ );
+
+ // Filter team members
+ const teamMembers = allMembers?.filter(
+ (team) => !team.is_robot && !team.invited,
+ );
+ const filteredTeamMembers =
+ search.query !== ''
+ ? teamMembers?.filter((member) => member.name.includes(search.query))
+ : teamMembers;
+ const paginatedTeamMembers = filteredTeamMembers?.slice(
+ page * perPage - perPage,
+ page * perPage - perPage + perPage,
+ );
+
+ // Filter robot account
+ const robotAccounts = allMembers?.filter((team) => team.is_robot);
+ const filteredRobotAccounts =
+ search.query !== ''
+ ? robotAccounts?.filter((member) => member.name.includes(search.query))
+ : robotAccounts;
+ const paginatedRobotAccounts = filteredRobotAccounts?.slice(
+ page * perPage - perPage,
+ page * perPage - perPage + perPage,
+ );
+
+ // Filter invited members
+ const invited = allMembers?.filter((team) => team.invited);
+ const filteredInvited =
+ search.query !== ''
+ ? invited?.filter((member) => member.name.includes(search.query))
+ : invited;
+ const paginatedInvited = filteredInvited?.slice(
+ page * perPage - perPage,
+ page * perPage - perPage + perPage,
+ );
+
+ return {
+ allMembers,
+ teamMembers,
+ robotAccounts,
+ invited,
+ paginatedAllMembers,
+ paginatedTeamMembers,
+ paginatedRobotAccounts,
+ paginatedInvited,
+ loading: isLoading || isPlaceholderData,
+ error: errorLoadingTeamMembers,
+ page,
+ setPage,
+ perPage,
+ setPerPage,
+ search,
+ setSearch,
+ };
+}
+
+export function useFetchCollaborators(orgName: string) {
+ const [page, setPage] = useState(1);
+ const [perPage, setPerPage] = useState(10);
+ const [search, setSearch] = useState({
+ query: '',
+ field: collaboratorViewColumnNames.username,
+ });
+
+ const {
+ data: collaborators,
+ isLoading,
+ isPlaceholderData,
+ isError: errorLoadingCollaborators,
+ } = useQuery(
+ ['collaborators'],
+ ({signal}) => fetchCollaboratorsForOrg(orgName, signal),
+ {
+ placeholderData: [],
+ },
+ );
+
+ const filteredCollaborators =
+ search.query !== ''
+ ? collaborators?.filter((collaborator) =>
+ collaborator.name.includes(search.query),
+ )
+ : collaborators;
+
+ const paginatedCollaborators = filteredCollaborators?.slice(
+ page * perPage - perPage,
+ page * perPage - perPage + perPage,
+ );
+
+ return {
+ collaborators,
+ filteredCollaborators,
+ paginatedCollaborators,
+ loading: isLoading || isPlaceholderData,
+ error: errorLoadingCollaborators,
+ page,
+ setPage,
+ perPage,
+ setPerPage,
+ search,
+ setSearch,
+ };
+}
+
+export function useDeleteTeamMember(orgName: string) {
+ const queryClient = useQueryClient();
+ const {
+ mutate: removeTeamMember,
+ isError: errorDeleteTeamMember,
+ isSuccess: successDeleteTeamMember,
+ reset: resetDeleteTeamMember,
+ } = useMutation(
+ async ({teamName, memberName}: {teamName: string; memberName: string}) => {
+ return deleteTeamMemberForOrg(orgName, teamName, memberName);
+ },
+ {
+ onSuccess: (_, variables) => {
+ queryClient.invalidateQueries(['teamMembers']);
+ },
+ },
+ );
+ return {
+ removeTeamMember,
+ errorDeleteTeamMember,
+ successDeleteTeamMember,
+ resetDeleteTeamMember,
+ };
+}
+
+export function useDeleteCollaborator(orgName: string) {
+ const queryClient = useQueryClient();
+ const {
+ mutate: removeCollaborator,
+ isError: errorDeleteCollaborator,
+ isSuccess: successDeleteCollaborator,
+ reset: resetDeleteCollaborator,
+ } = useMutation(
+ async ({collaborator}: {collaborator: string}) => {
+ return deleteCollaboratorForOrg(orgName, collaborator);
+ },
+ {
+ onSuccess: (_, variables) => {
+ queryClient.invalidateQueries(['collaborators']);
+ },
+ },
+ );
+ return {
+ removeCollaborator,
+ errorDeleteCollaborator,
+ successDeleteCollaborator,
+ resetDeleteCollaborator,
+ };
+}
diff --git a/web/src/hooks/UseRepositoryPermissions.ts b/web/src/hooks/UseRepositoryPermissions.ts
index fda2673a2..712325a1d 100644
--- a/web/src/hooks/UseRepositoryPermissions.ts
+++ b/web/src/hooks/UseRepositoryPermissions.ts
@@ -2,7 +2,7 @@ import {useQuery} from '@tanstack/react-query';
import {useState} from 'react';
import {SearchState} from 'src/components/toolbar/SearchTypes';
import {
- fetchTeamRepoPermissions,
+ fetchAllTeamPermissionsForRepository,
fetchUserRepoPermissions,
RepoMember,
} from 'src/resources/RepositoryResource';
@@ -42,7 +42,7 @@ export function useRepositoryPermissions(org: string, repo: string) {
isPlaceholderData: isTeamPlaceholderData,
} = useQuery(
['teamrepopermissions', org, repo],
- () => fetchTeamRepoPermissions(org, repo),
+ () => fetchAllTeamPermissionsForRepository(org, repo),
{
placeholderData: {},
},
diff --git a/web/src/hooks/UseTeams.ts b/web/src/hooks/UseTeams.ts
new file mode 100644
index 000000000..7424b2163
--- /dev/null
+++ b/web/src/hooks/UseTeams.ts
@@ -0,0 +1,267 @@
+import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
+import {
+ bulkDeleteTeams,
+ createNewTeamForNamespac,
+ fetchTeamRepoPermsForOrg,
+ fetchTeamsForNamespace,
+ updateTeamRepoPerm,
+ updateTeamRoleForNamespace,
+} from 'src/resources/TeamResources';
+import {useState} from 'react';
+import {IAvatar} from 'src/resources/OrganizationResource';
+import {teamViewColumnNames} from 'src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/TeamsViewList';
+import {SearchState} from 'src/components/toolbar/SearchTypes';
+import {setRepoPermForTeamColumnNames} from 'src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/SetRepoPermissionsModal/SetRepoPermissionForTeamModal';
+import {
+ IRepository,
+ fetchRepositoriesForNamespace,
+} from 'src/resources/RepositoryResource';
+import {BulkOperationError, ResourceError} from 'src/resources/ErrorHandling';
+import {useCurrentUser} from './UseCurrentUser';
+
+export function useCreateTeam(ns) {
+ const [namespace] = useState(ns);
+ const queryClient = useQueryClient();
+
+ const createTeamMutator = useMutation(
+ async ({namespace, name, description}: createNewTeamForNamespaceParams) => {
+ return createNewTeamForNamespac(namespace, name, description);
+ },
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries(['organization', namespace, 'teams']);
+ },
+ },
+ );
+
+ return {
+ createNewTeamHook: async (params: createNewTeamForNamespaceParams) =>
+ createTeamMutator.mutate(params),
+ };
+}
+
+interface createNewTeamForNamespaceParams {
+ namespace: string;
+ name: string;
+ description: string;
+}
+
+export interface ITeams {
+ name: string;
+ description: string;
+ role: string;
+ avatar: IAvatar;
+ can_view: boolean;
+ repo_count: number;
+ member_count: number;
+ is_synced: boolean;
+}
+
+export function useFetchTeams(orgName: string) {
+ const {user} = useCurrentUser();
+ const [page, setPage] = useState(1);
+ const [perPage, setPerPage] = useState(10);
+ const [search, setSearch] = useState({
+ query: '',
+ field: teamViewColumnNames.teamName,
+ });
+
+ const {
+ data,
+ isLoading,
+ isPlaceholderData,
+ isError: errorLoadingTeams,
+ } = useQuery(
+ ['teams'],
+ ({signal}) => fetchTeamsForNamespace(orgName, signal),
+ {
+ placeholderData: [],
+ enabled: !(user.username === orgName),
+ },
+ );
+
+ const teams: ITeams[] = Object.values(data);
+
+ const filteredTeams =
+ search.query !== ''
+ ? teams?.filter((team) => team.name.includes(search.query))
+ : teams;
+
+ const paginatedTeams = filteredTeams?.slice(
+ page * perPage - perPage,
+ page * perPage - perPage + perPage,
+ );
+
+ return {
+ teams: teams,
+ filteredTeams,
+ paginatedTeams: paginatedTeams,
+ loading: isLoading || isPlaceholderData,
+ error: errorLoadingTeams,
+ page,
+ setPage,
+ perPage,
+ setPerPage,
+ search,
+ setSearch,
+ };
+}
+
+export interface ITeamRepoPerms {
+ repoName: string;
+ role?: string;
+ lastModified: number;
+}
+
+export function useFetchRepoPermForTeam(orgName: string, teamName: string) {
+ const [page, setPage] = useState(1);
+ const [perPage, setPerPage] = useState(10);
+ const [search, setSearch] = useState({
+ query: '',
+ field: setRepoPermForTeamColumnNames.repoName,
+ });
+
+ const {
+ data: permissions,
+ isLoading: loadingPerms,
+ isPlaceholderData,
+ isError: errorLoadingTeamPerms,
+ } = useQuery(
+ ['teamrepopermissions'],
+ ({signal}) => fetchTeamRepoPermsForOrg(orgName, teamName, signal),
+ {
+ placeholderData: [],
+ },
+ );
+
+ const {
+ data: repos,
+ isLoading: loadingRepos,
+ isError: errorLoadingRepos,
+ } = useQuery(
+ ['repos'],
+ ({signal}) => fetchRepositoriesForNamespace(orgName, signal),
+ {
+ placeholderData: [],
+ },
+ );
+
+ const teamRepoPerms: ITeamRepoPerms[] = repos.map((repo) => ({
+ repoName: repo.name,
+ lastModified: repo.last_modified ? repo.last_modified : -1,
+ }));
+
+ // Add role from fetch permissions API
+ teamRepoPerms.forEach((repo) => {
+ const matchingPerm = permissions?.find(
+ (perm) => perm.repository.name === repo.repoName,
+ );
+ if (matchingPerm) {
+ repo['role'] = matchingPerm.role;
+ } else {
+ repo['role'] = 'none';
+ }
+ });
+
+ const filteredTeamRepoPerms =
+ search.query !== ''
+ ? teamRepoPerms?.filter((teamRepoPerm) =>
+ teamRepoPerm.repoName.includes(search.query),
+ )
+ : teamRepoPerms;
+
+ const paginatedTeamRepoPerms = filteredTeamRepoPerms?.slice(
+ page * perPage - perPage,
+ page * perPage - perPage + perPage,
+ );
+
+ return {
+ teamRepoPerms: teamRepoPerms,
+ filteredTeamRepoPerms: filteredTeamRepoPerms,
+ paginatedTeamRepoPerms: paginatedTeamRepoPerms,
+ loading: loadingPerms || loadingRepos || isPlaceholderData,
+ error: errorLoadingTeamPerms || errorLoadingRepos,
+ page,
+ setPage,
+ perPage,
+ setPerPage,
+ search,
+ setSearch,
+ };
+}
+
+export function useDeleteTeam({orgName, onSuccess, onError}) {
+ const queryClient = useQueryClient();
+ const deleteTeamsMutator = useMutation(
+ async (teams: ITeams[] | ITeams) => {
+ teams = Array.isArray(teams) ? teams : [teams];
+ return bulkDeleteTeams(orgName, teams);
+ },
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries(['teams']);
+ onSuccess();
+ },
+ onError: (err) => {
+ onError(err);
+ },
+ },
+ );
+ return {
+ removeTeam: async (teams: ITeams[] | ITeams) =>
+ deleteTeamsMutator.mutate(teams),
+ };
+}
+
+export function useUpdateTeamRole(orgName: string) {
+ const queryClient = useQueryClient();
+ const {
+ mutate: updateTeamRole,
+ isError: errorUpdateTeamRole,
+ isSuccess: successUpdateTeamRole,
+ reset: resetUpdateTeamRole,
+ } = useMutation(
+ async ({teamName, teamRole}: {teamName: string; teamRole: string}) => {
+ return updateTeamRoleForNamespace(orgName, teamName, teamRole);
+ },
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries(['teams']);
+ },
+ },
+ );
+ return {
+ updateTeamRole,
+ errorUpdateTeamRole,
+ successUpdateTeamRole,
+ resetUpdateTeamRole,
+ };
+}
+
+export function useUpdateTeamRepoPerm(orgName: string, teamName: string) {
+ const queryClient = useQueryClient();
+ const {
+ mutate: updateRepoPerm,
+ isError: errorUpdateRepoPerm,
+ error: detailedErrorUpdateRepoPerm,
+ isSuccess: successUpdateRepoPerm,
+ reset: resetUpdateRepoPerm,
+ } = useMutation(
+ async ({teamRepoPerms}: {teamRepoPerms: ITeamRepoPerms[]}) => {
+ return updateTeamRepoPerm(orgName, teamName, teamRepoPerms);
+ },
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries(['teams']);
+ },
+ },
+ );
+ return {
+ updateRepoPerm,
+ errorUpdateRepoPerm,
+ detailedErrorUpdateRepoPerm:
+ detailedErrorUpdateRepoPerm as BulkOperationError,
+ successUpdateRepoPerm,
+ resetUpdateRepoPerm,
+ };
+}
diff --git a/web/src/hooks/useTeams.ts b/web/src/hooks/useTeams.ts
deleted file mode 100644
index 862b4641a..000000000
--- a/web/src/hooks/useTeams.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
-import {createNewTeamForNamespac} from 'src/resources/TeamResources';
-import {useState} from 'react';
-
-export function useTeams(ns) {
- const [namespace, setNamespace] = useState(ns);
- const queryClient = useQueryClient();
-
- const createTeamMutator = useMutation(
- async ({namespace, name, description}: createNewTeamForNamespaceParams) => {
- return createNewTeamForNamespac(namespace, name, description);
- },
- {
- onSuccess: () => {
- queryClient.invalidateQueries(['organization', namespace, 'teams']);
- },
- },
- );
-
- return {
- createNewTeamHook: async (params: createNewTeamForNamespaceParams) =>
- createTeamMutator.mutate(params),
- };
-}
-
-interface createNewTeamForNamespaceParams {
- namespace: string;
- name: string;
- description: string;
-}
diff --git a/web/src/libs/utils.ts b/web/src/libs/utils.ts
index 104cbfa09..078f3c170 100644
--- a/web/src/libs/utils.ts
+++ b/web/src/libs/utils.ts
@@ -98,6 +98,16 @@ export function parseTagNameFromUrl(url: string): string {
return urlParts[tagKeywordIndex + 1];
}
+export function parseTeamNameFromUrl(url: string): string {
+ //url is in the format of /organization//teams/
+ const urlParts = url.split('/');
+ const teamKeywordIndex = urlParts.indexOf('teams');
+ if (teamKeywordIndex === -1) {
+ return '';
+ }
+ return urlParts[teamKeywordIndex + 1];
+}
+
export function humanizeTimeForExpiry(time_seconds: number): string {
return moment.duration(time_seconds || 0, 's').humanize();
}
@@ -107,7 +117,7 @@ export function getSeconds(duration_str: string): number {
return 0;
}
- let [number, suffix] = duration_str.split('');
+ const [number, suffix] = duration_str.split('');
return moment.duration(parseInt(number), suffix).asSeconds();
}
diff --git a/web/src/resources/MembersResource.ts b/web/src/resources/MembersResource.ts
index 0b4df3f1e..2fd7e5137 100644
--- a/web/src/resources/MembersResource.ts
+++ b/web/src/resources/MembersResource.ts
@@ -2,17 +2,16 @@ import {fetchOrg, IAvatar} from './OrganizationResource';
import axios from 'src/libs/axios';
import {assertHttpCode} from './ErrorHandling';
-export interface IMember {
- name: string;
- kind: string;
- teams: ITeam[];
- repositories: string[];
-}
-
-export interface ITeam {
+export interface IMemberTeams {
name: string;
avatar: IAvatar;
}
+export interface IMembers {
+ name: string;
+ kind: string;
+ teams?: IMemberTeams[];
+ repositories: string[];
+}
export async function fetchAllMembers(orgnames: string[], signal: AbortSignal) {
return await Promise.all(
@@ -23,9 +22,57 @@ export async function fetchAllMembers(orgnames: string[], signal: AbortSignal) {
export async function fetchMembersForOrg(
orgname: string,
signal: AbortSignal,
-): Promise {
+): Promise {
const getMembersUrl = `/api/v1/organization/${orgname}/members`;
const response = await axios.get(getMembersUrl, {signal});
assertHttpCode(response.status, 200);
return response.data?.members;
}
+
+export async function fetchCollaboratorsForOrg(
+ orgname: string,
+ signal: AbortSignal,
+): Promise {
+ const getCollaboratorsUrl = `/api/v1/organization/${orgname}/collaborators`;
+ const response = await axios.get(getCollaboratorsUrl, {signal});
+ assertHttpCode(response.status, 200);
+ return response.data?.collaborators;
+}
+
+export async function fetchTeamMembersForOrg(
+ org: string,
+ teamName: string,
+ signal?: AbortSignal,
+) {
+ const teamMemberUrl = `/api/v1/organization/${org}/team/${teamName}/members?includePending=true`;
+ const teamMembersResponse = await axios.get(teamMemberUrl, {signal});
+ assertHttpCode(teamMembersResponse.status, 200);
+ return teamMembersResponse.data?.members;
+}
+
+export async function deleteTeamMemberForOrg(
+ orgName: string,
+ teamName: string,
+ memberName: string,
+) {
+ try {
+ const response = await axios.delete(
+ `/api/v1/organization/${orgName}/team/${teamName}/members/${memberName}`,
+ );
+ } catch (err) {
+ console.error(`Unable to delete member for team: ${teamName}`, err);
+ }
+}
+
+export async function deleteCollaboratorForOrg(
+ orgName: string,
+ collaborator: string,
+) {
+ try {
+ await axios.delete(
+ `/api/v1/organization/${orgName}/members/${collaborator}`,
+ );
+ } catch (err) {
+ console.error(`Unable to delete collaborator for org: ${orgName}`, err);
+ }
+}
diff --git a/web/src/resources/OrganizationResource.ts b/web/src/resources/OrganizationResource.ts
index de0934b5d..9f1365da5 100644
--- a/web/src/resources/OrganizationResource.ts
+++ b/web/src/resources/OrganizationResource.ts
@@ -16,6 +16,7 @@ export interface IOrganization {
public?: boolean;
is_org_admin?: boolean;
is_admin?: boolean;
+ is_member?: boolean;
preferred_namespace?: boolean;
teams?: string[];
tag_expiration_s: number;
@@ -121,7 +122,7 @@ export async function updateOrgSettings(
const updateSettingsUrl = isUser
? `/api/v1/user/`
: `/api/v1/organization/${namespace}`;
- let payload = {};
+ const payload = {};
if (email) {
payload['email'] = email;
}
diff --git a/web/src/resources/RepositoryResource.ts b/web/src/resources/RepositoryResource.ts
index f6ce80bbb..1bed18c8d 100644
--- a/web/src/resources/RepositoryResource.ts
+++ b/web/src/resources/RepositoryResource.ts
@@ -189,7 +189,10 @@ export async function fetchUserRepoPermissions(org: string, repo: string) {
return response.data.permissions;
}
-export async function fetchTeamRepoPermissions(org: string, repo: string) {
+export async function fetchAllTeamPermissionsForRepository(
+ org: string,
+ repo: string,
+) {
const response: AxiosResponse = await axios.get(
`/api/v1/repository/${org}/${repo}/permissions/team/`,
);
diff --git a/web/src/resources/TeamResources.ts b/web/src/resources/TeamResources.ts
index 3674792b8..38746c2ac 100644
--- a/web/src/resources/TeamResources.ts
+++ b/web/src/resources/TeamResources.ts
@@ -1,6 +1,18 @@
-import {AxiosResponse} from 'axios';
+import {AxiosError, AxiosResponse} from 'axios';
import axios from 'src/libs/axios';
-import {assertHttpCode} from './ErrorHandling';
+import {ResourceError, assertHttpCode, throwIfError} from './ErrorHandling';
+import {ITeamRepoPerms, ITeams} from 'src/hooks/UseTeams';
+
+export class TeamDeleteError extends Error {
+ error: Error;
+ team: string;
+ constructor(message: string, team: string, error: AxiosError) {
+ super(message);
+ this.team = team;
+ this.error = error;
+ Object.setPrototypeOf(this, TeamDeleteError.prototype);
+ }
+}
export async function createNewTeamForNamespac(
namespace: string,
@@ -25,3 +37,102 @@ export async function updateTeamForRobot(
assertHttpCode(response.status, 200);
return response.data?.name;
}
+
+export async function updateTeamRoleForNamespace(
+ namespace: string,
+ teamName: string,
+ teamRole: string,
+) {
+ const updateTeamUrl = `/api/v1/organization/${namespace}/team/${teamName}`;
+ const payload = {name: teamName, role: teamRole};
+ const response: AxiosResponse = await axios.put(updateTeamUrl, payload);
+ assertHttpCode(response.status, 200);
+ return response.data?.name;
+}
+
+export async function updateTeamRepoPerm(
+ orgName: string,
+ teamName: string,
+ teamRepoPerms: ITeamRepoPerms[],
+) {
+ const responses = await Promise.allSettled(
+ teamRepoPerms?.map(async (repoPerm) => {
+ console.log(
+ '${%s}/${%s}: teamrole %s ',
+ orgName,
+ repoPerm.repoName,
+ repoPerm.role,
+ );
+ if (repoPerm.role === 'none') {
+ try {
+ const response: AxiosResponse = await axios.delete(
+ `/api/v1/repository/${orgName}/${repoPerm.repoName}/permissions/team/${teamName}`,
+ );
+ assertHttpCode(response.status, 204);
+ } catch (error) {
+ if (error.response.status !== 400) {
+ throw new ResourceError(
+ 'Unable to update repository permission for repo',
+ repoPerm.repoName,
+ error,
+ );
+ }
+ }
+ } else {
+ const updateTeamUrl = `/api/v1/repository/${orgName}/${repoPerm.repoName}/permissions/team/${teamName}`;
+ const payload = {role: repoPerm.role};
+ try {
+ const response: AxiosResponse = await axios.put(
+ updateTeamUrl,
+ payload,
+ );
+ assertHttpCode(response.status, 200);
+ } catch (error) {
+ throw new ResourceError(
+ 'Unable to update repository permission for repo',
+ repoPerm.repoName,
+ error,
+ );
+ }
+ }
+ }),
+ );
+ throwIfError(responses, 'Error updating team repo permissions');
+}
+
+export async function fetchTeamsForNamespace(
+ org: string,
+ signal?: AbortSignal,
+) {
+ const teamsForOrgUrl = `/api/v1/organization/${org}`;
+ const teamsResponse = await axios.get(teamsForOrgUrl, {signal});
+ assertHttpCode(teamsResponse.status, 200);
+ return teamsResponse.data?.teams;
+}
+
+export async function fetchTeamRepoPermsForOrg(
+ org: string,
+ teamName: string,
+ signal?: AbortSignal,
+) {
+ const response: AxiosResponse = await axios.get(
+ `/api/v1/organization/${org}/team/${teamName}/permissions`,
+ {signal},
+ );
+ return response.data.permissions;
+}
+
+export async function deleteTeamForOrg(orgName: string, teamName: string) {
+ try {
+ await axios.delete(`/api/v1/organization/${orgName}/team/${teamName}`);
+ } catch (error) {
+ throw new ResourceError('Unable to delete team', teamName, error);
+ }
+}
+
+export async function bulkDeleteTeams(orgName: string, teams: ITeams[]) {
+ const responses = await Promise.allSettled(
+ teams.map((team) => deleteTeamForOrg(orgName, team.name)),
+ );
+ throwIfError(responses, 'Error deleting teams');
+}
diff --git a/web/src/routes/Alerts.tsx b/web/src/routes/Alerts.tsx
index 8a6e60f14..4cb4c016e 100644
--- a/web/src/routes/Alerts.tsx
+++ b/web/src/routes/Alerts.tsx
@@ -1,26 +1,33 @@
-import { Alert, AlertActionCloseButton, AlertGroup } from "@patternfly/react-core";
-import { useRecoilState } from "recoil";
-import { AlertVariant, alertState } from "src/atoms/AlertState";
+import {
+ Alert,
+ AlertActionCloseButton,
+ AlertGroup,
+} from '@patternfly/react-core';
+import {useRecoilState} from 'recoil';
+import {AlertVariant, alertState} from 'src/atoms/AlertState';
-
-export default function Alerts(){
- const [alerts, setAlerts] = useRecoilState(alertState);
- return (
+export default function Alerts() {
+ const [alerts, setAlerts] = useRecoilState(alertState);
+ return (
- {alerts.map(alert=> (
+ {setAlerts(prev=>prev.filter(a=>a.key!==alert.key))}}
+ onClose={() => {
+ setAlerts((prev) => prev.filter((a) => a.key !== alert.key));
+ }}
/>
- }
- key={alert.key}
+ }
+ key={alert.key}
>
- {alert.message}
- )}
+ {alert.message}
+
+ ))}
- )
+ );
}
diff --git a/web/src/routes/NavigationPath.tsx b/web/src/routes/NavigationPath.tsx
index 831fc780c..8376c2bb1 100644
--- a/web/src/routes/NavigationPath.tsx
+++ b/web/src/routes/NavigationPath.tsx
@@ -16,12 +16,17 @@ const tagNameBreadcrumb = (match) => {
return {match.params.tagName};
};
+const teamMemberBreadcrumb = (match) => {
+ return {match.params.teamName};
+};
+
const Breadcrumb = {
organizationsListBreadcrumb: 'Organization',
repositoriesListBreadcrumb: 'Repository',
organizationDetailBreadcrumb: organizationNameBreadcrumb,
repositoryDetailBreadcrumb: repositoryNameBreadcrumb,
tagDetailBreadcrumb: tagNameBreadcrumb,
+ teamMemberBreadcrumb: teamMemberBreadcrumb,
};
export enum NavigationPath {
@@ -39,6 +44,9 @@ export enum NavigationPath {
// Tag Detail
tagDetail = '/repository/:organizationName/:repositoryName/tag/:tagName',
+
+ // Team Member
+ teamMember = '/organization/:organizationName/teams/:teamName',
}
export function getRepoDetailPath(
@@ -64,7 +72,6 @@ export function getTagDetailPath(
tagPath = tagPath.replace(':organizationName', org);
tagPath = tagPath.replace(':repositoryName', repo);
tagPath = tagPath.replace(':tagName', tagName);
-
if (queryParams) {
const params = [];
for (const entry of Array.from(queryParams.entries())) {
@@ -75,6 +82,21 @@ export function getTagDetailPath(
return domainRoute(currentRoute, tagPath);
}
+export function getTeamMemberPath(
+ currentRoute: string,
+ orgName: string,
+ teamName: string,
+ queryParams: string = null,
+): string {
+ let teamMemberPath = NavigationPath.teamMember.toString();
+ teamMemberPath = teamMemberPath.replace(':organizationName', orgName);
+ teamMemberPath = teamMemberPath.replace(':teamName', teamName);
+ if (queryParams) {
+ teamMemberPath = teamMemberPath + '?tab' + '=' + queryParams;
+ }
+ return domainRoute(currentRoute, teamMemberPath);
+}
+
export function getDomain() {
return process.env.REACT_APP_QUAY_DOMAIN || 'quay.io';
}
@@ -93,33 +115,35 @@ function domainRoute(currentRoute, definedRoute) {
);
}
-const currentRoute = window.location.pathname;
+export const getNavigationRoutes = () => {
+ const currentRoute = window.location.pathname;
-const NavigationRoutes = [
- {
- path: domainRoute(currentRoute, NavigationPath.organizationsList),
- Component: ,
- breadcrumb: Breadcrumb.organizationsListBreadcrumb,
- },
- {
- path: domainRoute(currentRoute, NavigationPath.organizationDetail),
- Component: ,
- breadcrumb: Breadcrumb.organizationDetailBreadcrumb,
- },
- {
- path: domainRoute(currentRoute, NavigationPath.repositoriesList),
- Component: ,
- breadcrumb: Breadcrumb.repositoriesListBreadcrumb,
- },
- {
- path: domainRoute(currentRoute, NavigationPath.repositoryDetail),
- Component: ,
- breadcrumb: Breadcrumb.repositoryDetailBreadcrumb,
- },
- {
- path: domainRoute(currentRoute, NavigationPath.tagDetail),
- Component: ,
- breadcrumb: Breadcrumb.tagDetailBreadcrumb,
- },
-];
-export {NavigationRoutes};
+ const NavigationRoutes = [
+ {
+ path: domainRoute(currentRoute, NavigationPath.organizationsList),
+ Component: ,
+ breadcrumb: Breadcrumb.organizationsListBreadcrumb,
+ },
+ {
+ path: domainRoute(currentRoute, NavigationPath.organizationDetail),
+ Component: ,
+ breadcrumb: Breadcrumb.organizationDetailBreadcrumb,
+ },
+ {
+ path: domainRoute(currentRoute, NavigationPath.repositoriesList),
+ Component: ,
+ breadcrumb: Breadcrumb.repositoriesListBreadcrumb,
+ },
+ {
+ path: domainRoute(currentRoute, NavigationPath.repositoryDetail),
+ Component: ,
+ breadcrumb: Breadcrumb.repositoryDetailBreadcrumb,
+ },
+ {
+ path: domainRoute(currentRoute, NavigationPath.tagDetail),
+ Component: ,
+ breadcrumb: Breadcrumb.tagDetailBreadcrumb,
+ },
+ ];
+ return NavigationRoutes;
+};
diff --git a/web/src/routes/OrganizationsList/Organization/Organization.tsx b/web/src/routes/OrganizationsList/Organization/Organization.tsx
index 985e02fec..0f13d35f4 100644
--- a/web/src/routes/OrganizationsList/Organization/Organization.tsx
+++ b/web/src/routes/OrganizationsList/Organization/Organization.tsx
@@ -7,20 +7,21 @@ import {
TabTitleText,
Title,
} from '@patternfly/react-core';
-import {useLocation, useParams, useSearchParams} from 'react-router-dom';
+import {useParams, useSearchParams} from 'react-router-dom';
import {useCallback, useState} from 'react';
import RepositoriesList from 'src/routes/RepositoriesList/RepositoriesList';
import Settings from './Tabs/Settings/Settings';
import {QuayBreadcrumb} from 'src/components/breadcrumb/Breadcrumb';
-import { useOrganization } from 'src/hooks/UseOrganization';
+import {useOrganization} from 'src/hooks/UseOrganization';
import {useOrganizations} from 'src/hooks/UseOrganizations';
import RobotAccountsList from 'src/routes/RepositoriesList/RobotAccountsList';
import {useQuayConfig} from 'src/hooks/UseQuayConfig';
+import TeamsAndMembershipList from './Tabs/TeamsAndMembership/TeamsAndMembershipList';
+import ManageMembersList from './Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersList';
export default function Organization() {
- const location = useLocation();
const quayConfig = useQuayConfig();
- const {organizationName} = useParams();
+ const {organizationName, teamName} = useParams();
const {usernames} = useOrganizations();
const isUserOrganization = usernames.includes(organizationName);
@@ -34,6 +35,7 @@ export default function Organization() {
const onTabSelect = useCallback(
(_event: React.MouseEvent, tabKey: string) => {
+ tabKey = tabKey.replace(/ /g, '');
setSearchParams({tab: tabKey});
setActiveTabKey(tabKey);
},
@@ -45,11 +47,15 @@ export default function Organization() {
return false;
}
- if (!isUserOrganization && organization && (tabname == 'Settings' || tabname == 'Robot accounts')) {
+ if (
+ !isUserOrganization &&
+ organization &&
+ (tabname == 'Settings' || tabname == 'Robot accounts')
+ ) {
return organization.is_org_admin || organization.is_admin;
}
return false;
- }
+ };
const repositoriesSubNav = [
{
@@ -57,11 +63,25 @@ export default function Organization() {
component: ,
visible: true,
},
+ {
+ name: 'Teams and membership',
+ component: !teamName ? (
+
+ ) : (
+
+ ),
+ visible:
+ !isUserOrganization &&
+ organization?.is_member &&
+ organization?.is_admin,
+ },
{
name: 'Robot accounts',
component: ,
visible: fetchTabVisibility('Robot accounts'),
-
},
{
name: 'Settings',
@@ -86,15 +106,17 @@ export default function Organization() {
padding={{default: 'noPadding'}}
>
- {repositoriesSubNav.filter((nav) => nav.visible).map((nav)=> (
- {nav.name}}
- >
- {nav.component}
-
- ))}
+ {repositoriesSubNav
+ .filter((nav) => nav.visible)
+ .map((nav) => (
+ {nav.name}}
+ >
+ {nav.component}
+
+ ))}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/Settings/Settings.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/Settings/Settings.tsx
index 499aa8cb4..f017042ff 100644
--- a/web/src/routes/OrganizationsList/Organization/Tabs/Settings/Settings.tsx
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/Settings/Settings.tsx
@@ -46,17 +46,31 @@ const GeneralSettings = (props: GeneralSettingsProps) => {
const {updateOrgSettings} = useOrganizationSettings({
name: props.organizationName,
onSuccess: (result) => {
- setAlerts(prevAlerts => {
- return [...prevAlerts,
-
- ]
+ setAlerts((prevAlerts) => {
+ return [
+ ...prevAlerts,
+ ,
+ ];
});
},
onError: (err) => {
- setAlerts(prevAlerts => {
- return [...prevAlerts,
-
- ]
+ setAlerts((prevAlerts) => {
+ return [
+ ...prevAlerts,
+ ,
+ ];
});
},
});
@@ -65,27 +79,30 @@ const GeneralSettings = (props: GeneralSettingsProps) => {
// Time Machine
const [timeMachineFormValue, setTimeMachineFormValue] = useState(
- timeMachineOptions[quayConfig.config.TAG_EXPIRATION_OPTIONS[0]],
+ timeMachineOptions[quayConfig?.config?.TAG_EXPIRATION_OPTIONS[0]],
);
- const namespaceTimeMachineExpiry = isUserOrganization ? user?.tag_expiration_s : (organization as IOrganization)?.tag_expiration_s;
+ const namespaceTimeMachineExpiry = isUserOrganization
+ ? user?.tag_expiration_s
+ : (organization as IOrganization)?.tag_expiration_s;
// Email
- const namespaceEmail = isUserOrganization ? user?.email || '' : (organization as any)?.email || '';
+ const namespaceEmail = isUserOrganization
+ ? user?.email || ''
+ : (organization as any)?.email || '';
const [emailFormValue, setEmailFormValue] = useState('');
const [validated, setValidated] = useState('default');
useEffect(() => {
setEmailFormValue(namespaceEmail);
const humanized_expiry = humanizeTimeForExpiry(namespaceTimeMachineExpiry);
- for (let key of Object.keys(timeMachineOptions)) {
- if (humanized_expiry == timeMachineOptions[key]){
+ for (const key of Object.keys(timeMachineOptions)) {
+ if (humanized_expiry == timeMachineOptions[key]) {
setTimeMachineFormValue(key);
break;
}
}
}, [loading, isUserLoading, isUserOrganization]);
-
const handleEmailChange = (emailFormValue: string) => {
setEmailFormValue(emailFormValue);
if (namespaceEmail == emailFormValue) {
@@ -104,13 +121,12 @@ const GeneralSettings = (props: GeneralSettingsProps) => {
if (isValidEmail(emailFormValue)) {
setValidated('success');
setError('');
- }
- else {
+ } else {
setValidated('error');
setError('Please enter a valid email address');
}
}
- }
+ };
const checkForChanges = () => {
if (namespaceEmail != emailFormValue) {
@@ -118,7 +134,7 @@ const GeneralSettings = (props: GeneralSettingsProps) => {
}
return getSeconds(timeMachineFormValue) != namespaceTimeMachineExpiry;
- }
+ };
const updateSettings = async () => {
try {
@@ -135,7 +151,7 @@ const GeneralSettings = (props: GeneralSettingsProps) => {
} catch (error) {
addDisplayError('Unable to update namespace settings', error);
}
- }
+ };
const onSubmit = (e) => {
e.preventDefault();
@@ -190,7 +206,7 @@ const GeneralSettings = (props: GeneralSettingsProps) => {
value={timeMachineFormValue}
onChange={(val) => setTimeMachineFormValue(val)}
>
- {quayConfig.config.TAG_EXPIRATION_OPTIONS.map((option, index) => (
+ {quayConfig?.config?.TAG_EXPIRATION_OPTIONS.map((option, index) => (
{
-
- {alerts}
-
+ {alerts}
);
};
-// const BillingInformation = () => {
-// return Hello
;
-// };
-
export default function Settings(props: SettingsProps) {
const [activeTabIndex, setActiveTabIndex] = useState(0);
@@ -239,11 +249,6 @@ export default function Settings(props: SettingsProps) {
id: 'generalsettings',
content: ,
},
- // {
- // name: 'Billing Information',
- // id: 'billinginformation',
- // content: ,
- // },
];
return (
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/CollaboratorsView/CollaboratorsDeleteModal.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/CollaboratorsView/CollaboratorsDeleteModal.tsx
new file mode 100644
index 000000000..b77eef0b7
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/CollaboratorsView/CollaboratorsDeleteModal.tsx
@@ -0,0 +1,79 @@
+import {Alert, Button, Modal, ModalVariant} from '@patternfly/react-core';
+import {useEffect} from 'react';
+import {AlertVariant} from 'src/atoms/AlertState';
+import {useAlerts} from 'src/hooks/UseAlerts';
+import {useDeleteCollaborator} from 'src/hooks/UseMembers';
+import {IMembers} from 'src/resources/MembersResource';
+
+export default function CollaboratorsDeleteModal(
+ props: CollaboratorsDeleteModalProps,
+) {
+ const {addAlert} = useAlerts();
+ const deleteMsg =
+ 'User will be removed from all teams and repositories under this organization in which they are a member or have permissions.';
+ const deleteAlert = (
+
+ );
+
+ const {
+ removeCollaborator,
+ errorDeleteCollaborator,
+ successDeleteCollaborator,
+ } = useDeleteCollaborator(props.organizationName);
+
+ useEffect(() => {
+ if (errorDeleteCollaborator) {
+ addAlert({
+ variant: AlertVariant.Failure,
+ title: `Error deleting collaborator`,
+ });
+ }
+ }, [errorDeleteCollaborator]);
+
+ useEffect(() => {
+ if (successDeleteCollaborator) {
+ addAlert({
+ variant: AlertVariant.Success,
+ title: `Successfully deleted collaborator`,
+ });
+ }
+ }, [successDeleteCollaborator]);
+
+ return (
+ {
+ removeCollaborator({
+ collaborator: props.collaborator.name,
+ });
+ props.toggleModal;
+ }}
+ data-testid={`${props.collaborator.name}-del-btn`}
+ >
+ Delete
+ ,
+ ,
+ ]}
+ >
+ Are you sure you want to delete {props.collaborator.name} ?
+
+ );
+}
+
+interface CollaboratorsDeleteModalProps {
+ isModalOpen: boolean;
+ toggleModal: () => void;
+ collaborator: IMembers;
+ organizationName: string;
+}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/CollaboratorsView/CollaboratorsViewList.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/CollaboratorsView/CollaboratorsViewList.tsx
new file mode 100644
index 000000000..83a7468ff
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/CollaboratorsView/CollaboratorsViewList.tsx
@@ -0,0 +1,177 @@
+import {
+ Button,
+ PageSection,
+ PageSectionVariants,
+ PanelFooter,
+ Spinner,
+} from '@patternfly/react-core';
+import CollaboratorsViewToolbar from './CollaboratorsViewToolbar';
+import {
+ TableComposable,
+ Tbody,
+ Td,
+ Th,
+ Thead,
+ Tr,
+} from '@patternfly/react-table';
+import {useFetchCollaborators} from 'src/hooks/UseMembers';
+import {useEffect, useState} from 'react';
+import {IMembers} from 'src/resources/MembersResource';
+import {TrashIcon} from '@patternfly/react-icons';
+import {useAlerts} from 'src/hooks/UseAlerts';
+import {AlertVariant} from 'src/atoms/AlertState';
+import {ToolbarPagination} from 'src/components/toolbar/ToolbarPagination';
+import CollaboratorsDeleteModal from './CollaboratorsDeleteModal';
+import Conditional from 'src/components/empty/Conditional';
+
+export const collaboratorViewColumnNames = {
+ username: 'User name',
+ directRepositoryPermissions: 'Direct repository permissions',
+};
+
+export default function CollaboratorsViewList(
+ props: CollaboratorsViewListProps,
+) {
+ const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+
+ const {
+ filteredCollaborators,
+ paginatedCollaborators,
+ loading,
+ error,
+ page,
+ setPage,
+ perPage,
+ setPerPage,
+ search,
+ setSearch,
+ } = useFetchCollaborators(props.organizationName);
+
+ const [selectedCollaborators, setSelectedCollaborators] = useState<
+ IMembers[]
+ >([]);
+ const {addAlert} = useAlerts();
+ const [collaboratorToBeDeleted, setCollaboratorToBeDeleted] =
+ useState();
+
+ useEffect(() => {
+ if (error) {
+ addAlert({
+ variant: AlertVariant.Failure,
+ title: `Could not load collaborators`,
+ });
+ }
+ }, [error]);
+
+ const onSelectCollaborator = (
+ collaborator: IMembers,
+ rowIndex: number,
+ isSelecting: boolean,
+ ) => {
+ setSelectedCollaborators((prevSelected) => {
+ const otherSelectedCollaborators = prevSelected.filter(
+ (m) => m.name !== collaborator.name,
+ );
+ return isSelecting
+ ? [...otherSelectedCollaborators, collaborator]
+ : otherSelectedCollaborators;
+ });
+ };
+
+ const deleteCollabModal = (
+ setIsDeleteModalOpen(!isDeleteModalOpen)}
+ collaborator={collaboratorToBeDeleted}
+ organizationName={props.organizationName}
+ />
+ );
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+ setSelectedCollaborators([])}
+ allItems={filteredCollaborators}
+ paginatedItems={paginatedCollaborators}
+ onItemSelect={onSelectCollaborator}
+ page={page}
+ setPage={setPage}
+ perPage={perPage}
+ setPerPage={setPerPage}
+ search={search}
+ setSearch={setSearch}
+ searchOptions={[collaboratorViewColumnNames.username]}
+ />
+ {props.children}
+ {deleteCollabModal}
+
+
+
+ |
+ {collaboratorViewColumnNames.username} |
+ {collaboratorViewColumnNames.directRepositoryPermissions} |
+ |
+
+
+
+ {paginatedCollaborators?.map((collaborator, rowIndex) => (
+
+ |
+ onSelectCollaborator(collaborator, rowIndex, isSelecting),
+ isSelected: selectedCollaborators.some(
+ (t) => t.name === collaborator.name,
+ ),
+ }}
+ />
+ |
+ {collaborator.name}
+ |
+
+ Direct permissions on {collaborator.repositories?.length}{' '}
+ repositories under this organization
+ |
+
+ }
+ variant="plain"
+ onClick={() => {
+ setCollaboratorToBeDeleted(collaborator);
+ setIsDeleteModalOpen(true);
+ }}
+ data-testid={`${collaborator.name}-del-icon`}
+ />
+ |
+
+ ))}
+
+
+
+
+
+
+ );
+}
+
+interface CollaboratorsViewListProps {
+ organizationName: string;
+ children?: React.ReactNode;
+}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/CollaboratorsView/CollaboratorsViewToolbar.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/CollaboratorsView/CollaboratorsViewToolbar.tsx
new file mode 100644
index 000000000..45b07a693
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/CollaboratorsView/CollaboratorsViewToolbar.tsx
@@ -0,0 +1,65 @@
+import {Flex, FlexItem, Toolbar, ToolbarContent} from '@patternfly/react-core';
+import {DropdownCheckbox} from 'src/components/toolbar/DropdownCheckbox';
+import {SearchDropdown} from 'src/components/toolbar/SearchDropdown';
+import {SearchInput} from 'src/components/toolbar/SearchInput';
+import {SearchState} from 'src/components/toolbar/SearchTypes';
+import {ToolbarPagination} from 'src/components/toolbar/ToolbarPagination';
+import {IMembers} from 'src/resources/MembersResource';
+
+export default function CollaboratorsViewToolbar(
+ props: CollaboratorsViewToolbarProps,
+) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+interface CollaboratorsViewToolbarProps {
+ selectedMembers: IMembers[];
+ deSelectAll: () => void;
+ allItems: IMembers[];
+ paginatedItems: IMembers[];
+ onItemSelect: (
+ item: IMembers,
+ rowIndex: number,
+ isSelecting: boolean,
+ ) => void;
+ page: number;
+ setPage: (page: number) => void;
+ perPage: number;
+ setPerPage: (perPage: number) => void;
+ searchOptions: string[];
+ search: SearchState;
+ setSearch: (search: SearchState) => void;
+}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/MembersView/MembersViewList.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/MembersView/MembersViewList.tsx
new file mode 100644
index 000000000..8358bdf1d
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/MembersView/MembersViewList.tsx
@@ -0,0 +1,213 @@
+import {
+ TableComposable,
+ Tbody,
+ Td,
+ Th,
+ Thead,
+ Tr,
+} from '@patternfly/react-table';
+import {Link, useSearchParams} from 'react-router-dom';
+import {
+ Label,
+ PageSection,
+ PageSectionVariants,
+ PanelFooter,
+ Popover,
+ Spinner,
+} from '@patternfly/react-core';
+import {useEffect, useState} from 'react';
+import MembersViewToolbar from './MembersViewToolbar';
+import {useFetchMembers} from 'src/hooks/UseMembers';
+import {IMemberTeams, IMembers} from 'src/resources/MembersResource';
+import {useAlerts} from 'src/hooks/UseAlerts';
+import {AlertVariant} from 'src/atoms/AlertState';
+import {getTeamMemberPath} from 'src/routes/NavigationPath';
+import {ToolbarPagination} from 'src/components/toolbar/ToolbarPagination';
+
+export const memberViewColumnNames = {
+ username: 'User name',
+ teams: 'Teams',
+ directRepositoryPermissions: 'Direct repository permissions',
+};
+
+export default function MembersViewList(props: MembersViewListProps) {
+ const {
+ filteredMembers,
+ paginatedMembers,
+ loading,
+ error,
+ page,
+ setPage,
+ perPage,
+ setPerPage,
+ search,
+ setSearch,
+ } = useFetchMembers(props.organizationName);
+
+ const [selectedMembers, setSelectedMembers] = useState([]);
+ const [isPopoverOpen, setPopoverOpen] = useState(false);
+ const [searchParams] = useSearchParams();
+ const {addAlert} = useAlerts();
+
+ useEffect(() => {
+ if (error) {
+ addAlert({
+ variant: AlertVariant.Failure,
+ title: `Could not load members`,
+ });
+ }
+ }, [error]);
+
+ const handleClick = () => {
+ setPopoverOpen(!isPopoverOpen);
+ };
+
+ const onSelectMember = (
+ member: IMembers,
+ rowIndex: number,
+ isSelecting: boolean,
+ ) => {
+ setSelectedMembers((prevSelected) => {
+ const otherSelectedMembers = prevSelected.filter(
+ (m) => m.name !== member.name,
+ );
+ return isSelecting
+ ? [...otherSelectedMembers, member]
+ : otherSelectedMembers;
+ });
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ const renderTeamLabels = (teamsArr: IMemberTeams[], rowIndex: number) => {
+ const labelsToRender = teamsArr.slice(0, 4);
+ const remainingLabels = teamsArr.slice(4);
+
+ const labelsToDisplay = labelsToRender.map((team, idx) => (
+
+ {' '}
+
+ ));
+
+ if (remainingLabels?.length) {
+ labelsToDisplay.push(
+ ,
+ );
+ }
+ return labelsToDisplay;
+ };
+
+ return (
+
+ setSelectedMembers([])}
+ allItems={filteredMembers}
+ paginatedItems={paginatedMembers}
+ onItemSelect={onSelectMember}
+ page={page}
+ setPage={setPage}
+ perPage={perPage}
+ setPerPage={setPerPage}
+ search={search}
+ setSearch={setSearch}
+ searchOptions={[memberViewColumnNames.username]}
+ />
+ {props.children}
+
+
+
+ |
+ {memberViewColumnNames.username} |
+ {memberViewColumnNames.teams} |
+ {memberViewColumnNames.directRepositoryPermissions} |
+
+
+
+ {paginatedMembers?.map((member, rowIndex) => (
+
+ |
+ onSelectMember(member, rowIndex, isSelecting),
+ isSelected: selectedMembers.some(
+ (t) => t.name === member.name,
+ ),
+ }}
+ />
+ | {member.name} |
+
+ {renderTeamLabels(member.teams, rowIndex)}
+ |
+
+ Direct permissions on {member.repositories?.length} repositories
+ under this organization
+ |
+
+ ))}
+
+
+
+
+
+
+ );
+}
+
+interface MembersViewListProps {
+ organizationName: string;
+ children?: React.ReactNode;
+}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/MembersView/MembersViewToolbar.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/MembersView/MembersViewToolbar.tsx
new file mode 100644
index 000000000..d2c64fd6b
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/MembersView/MembersViewToolbar.tsx
@@ -0,0 +1,63 @@
+import {Flex, FlexItem, Toolbar, ToolbarContent} from '@patternfly/react-core';
+import {DropdownCheckbox} from 'src/components/toolbar/DropdownCheckbox';
+import {SearchDropdown} from 'src/components/toolbar/SearchDropdown';
+import {SearchInput} from 'src/components/toolbar/SearchInput';
+import {SearchState} from 'src/components/toolbar/SearchTypes';
+import {ToolbarPagination} from 'src/components/toolbar/ToolbarPagination';
+import {IMembers} from 'src/resources/MembersResource';
+
+export default function MembersViewToolbar(props: MembersViewToolbarProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+interface MembersViewToolbarProps {
+ selectedMembers: IMembers[];
+ deSelectAll: () => void;
+ allItems: IMembers[];
+ paginatedItems: IMembers[];
+ onItemSelect: (
+ item: IMembers,
+ rowIndex: number,
+ isSelecting: boolean,
+ ) => void;
+ page: number;
+ setPage: (page: number) => void;
+ perPage: number;
+ setPerPage: (perPage: number) => void;
+ searchOptions: string[];
+ search: SearchState;
+ setSearch: (search: SearchState) => void;
+}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsAndMembershipList.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsAndMembershipList.tsx
new file mode 100644
index 000000000..71fd0d041
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsAndMembershipList.tsx
@@ -0,0 +1,92 @@
+import {
+ ToggleGroup,
+ ToggleGroupItem,
+ ToggleGroupItemProps,
+ Toolbar,
+ ToolbarContent,
+ ToolbarItem,
+} from '@patternfly/react-core';
+import {useState} from 'react';
+import TeamsViewList from './TeamsView/TeamsViewList';
+import CollaboratorsViewList from './CollaboratorsView/CollaboratorsViewList';
+import MembersViewList from './MembersView/MembersViewList';
+
+export enum TableModeType {
+ Teams = 'Teams',
+ Members = 'Members',
+ Collaborators = 'Collaborators',
+}
+
+export default function TeamsAndMembershipList(
+ props: TeamsAndMembershipListProps,
+) {
+ const [tableMode, setTableMode] = useState(
+ TableModeType.Teams,
+ );
+
+ const onTableModeChange: ToggleGroupItemProps['onChange'] = (
+ _isSelected,
+ event,
+ ) => {
+ const id = event.currentTarget.id;
+ setTableMode(id);
+ fetchTableItems();
+ };
+
+ const viewToggle = (
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ const fetchTableItems = () => {
+ if (tableMode == TableModeType.Teams) {
+ return (
+
+ {viewToggle}
+
+ );
+ } else if (tableMode == TableModeType.Members) {
+ return (
+
+ {viewToggle}
+
+ );
+ } else if (tableMode == TableModeType.Collaborators) {
+ return (
+
+ {viewToggle}
+
+ );
+ }
+ };
+
+ return fetchTableItems();
+}
+
+interface TeamsAndMembershipListProps {
+ organizationName: string;
+}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersList.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersList.tsx
new file mode 100644
index 000000000..e5f18a076
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersList.tsx
@@ -0,0 +1,272 @@
+import {
+ Button,
+ PageSection,
+ PageSectionVariants,
+ Spinner,
+ ToggleGroup,
+ ToggleGroupItem,
+ ToggleGroupItemProps,
+ Toolbar,
+ ToolbarContent,
+ ToolbarItem,
+} from '@patternfly/react-core';
+import {useEffect, useState} from 'react';
+import ManageMembersToolbar from './ManageMembersToolbar';
+import {
+ TableComposable,
+ Tbody,
+ Td,
+ Th,
+ Thead,
+ Tr,
+} from '@patternfly/react-table';
+import {
+ ITeamMember,
+ useDeleteTeamMember,
+ useFetchTeamMembersForOrg,
+} from 'src/hooks/UseMembers';
+import {CubesIcon, TrashIcon} from '@patternfly/react-icons';
+import {useParams} from 'react-router-dom';
+import Empty from 'src/components/empty/Empty';
+import {useAlerts} from 'src/hooks/UseAlerts';
+import {AlertVariant} from 'src/atoms/AlertState';
+
+export enum TableModeType {
+ AllMembers = 'All Members',
+ TeamMember = 'Team Member',
+ RobotAccounts = 'Robot Accounts',
+ Invited = 'Invited',
+}
+
+export const manageMemberColumnNames = {
+ teamMember: 'Team member',
+ account: 'Account',
+};
+
+export default function ManageMembersList() {
+ const {organizationName, teamName} = useParams();
+
+ const {
+ allMembers,
+ teamMembers,
+ robotAccounts,
+ invited,
+ paginatedAllMembers,
+ paginatedTeamMembers,
+ paginatedRobotAccounts,
+ paginatedInvited,
+ loading,
+ page,
+ setPage,
+ perPage,
+ setPerPage,
+ search,
+ setSearch,
+ } = useFetchTeamMembersForOrg(organizationName, teamName);
+
+ const [tableMembersList, setTableMembersList] = useState([]);
+ const [allMembersList, setAllMembersList] = useState([]);
+ const [selectedTeamMembers, setSelectedTeamMembers] = useState(
+ [],
+ );
+ const [tableMode, setTableMode] = useState(
+ TableModeType.AllMembers,
+ );
+ const {addAlert} = useAlerts();
+
+ useEffect(() => {
+ switch (tableMode) {
+ case TableModeType.AllMembers:
+ setTableMembersList(paginatedAllMembers);
+ setAllMembersList(allMembers);
+ break;
+
+ case TableModeType.TeamMember:
+ setTableMembersList(paginatedTeamMembers);
+ setAllMembersList(teamMembers);
+ break;
+
+ case TableModeType.RobotAccounts:
+ setTableMembersList(paginatedRobotAccounts);
+ setAllMembersList(robotAccounts);
+ break;
+
+ case TableModeType.Invited:
+ setTableMembersList(paginatedInvited);
+ setAllMembersList(invited);
+ break;
+
+ default:
+ break;
+ }
+ }, [tableMode, allMembers]);
+
+ const onTableModeChange: ToggleGroupItemProps['onChange'] = (
+ _isSelected,
+ event,
+ ) => {
+ const id = event.currentTarget.id;
+ setTableMode(id);
+ };
+
+ const onSelectTeamMember = (
+ teamMember: ITeamMember,
+ rowIndex: number,
+ isSelecting: boolean,
+ ) => {
+ setSelectedTeamMembers((prevSelected) => {
+ const otherSelectedTeamMembers = prevSelected.filter(
+ (t) => t.name !== teamMember.name,
+ );
+ return isSelecting
+ ? [...otherSelectedTeamMembers, teamMember]
+ : otherSelectedTeamMembers;
+ });
+ };
+
+ const {removeTeamMember, errorDeleteTeamMember, successDeleteTeamMember} =
+ useDeleteTeamMember(organizationName);
+
+ useEffect(() => {
+ if (successDeleteTeamMember) {
+ addAlert({
+ variant: AlertVariant.Success,
+ title: `Successfully deleted team member`,
+ });
+ }
+ }, [successDeleteTeamMember]);
+
+ useEffect(() => {
+ if (errorDeleteTeamMember) {
+ addAlert({
+ variant: AlertVariant.Failure,
+ title: `Error deleting team member`,
+ });
+ }
+ }, [errorDeleteTeamMember]);
+
+ const viewToggle = (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+
+ const getAccountTypeForMember = (member: ITeamMember): string => {
+ if (member.is_robot) {
+ return 'Robot account';
+ } else if (!member.is_robot && !member.invited) {
+ return 'Team member';
+ } else if (member.invited) {
+ return '(Invited)';
+ }
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ if (allMembers?.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+ setSelectedTeamMembers([])}
+ allItems={allMembersList}
+ paginatedItems={tableMembersList}
+ onItemSelect={onSelectTeamMember}
+ page={page}
+ setPage={setPage}
+ perPage={perPage}
+ setPerPage={setPerPage}
+ search={search}
+ setSearch={setSearch}
+ searchOptions={[manageMemberColumnNames.teamMember]}
+ >
+ {viewToggle}
+
+
+
+ |
+ {manageMemberColumnNames.teamMember} |
+ {manageMemberColumnNames.account} |
+ |
+
+
+
+ {tableMembersList.map((teamMember, rowIndex) => (
+
+ |
+ onSelectTeamMember(teamMember, rowIndex, isSelecting),
+ isSelected: selectedTeamMembers.some(
+ (t) => t.name === teamMember.name,
+ ),
+ }}
+ />
+ |
+ {teamMember.name}
+ |
+
+ {getAccountTypeForMember(teamMember)}
+ |
+
+ }
+ variant="plain"
+ onClick={() =>
+ removeTeamMember({
+ teamName,
+ memberName: teamMember.name,
+ })
+ }
+ data-testid={`${teamMember.name}-delete-icon`}
+ />
+ |
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersToolbar.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersToolbar.tsx
new file mode 100644
index 000000000..64e5dd737
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersToolbar.tsx
@@ -0,0 +1,82 @@
+import {
+ Flex,
+ FlexItem,
+ PanelFooter,
+ Toolbar,
+ ToolbarContent,
+} from '@patternfly/react-core';
+import {DropdownCheckbox} from 'src/components/toolbar/DropdownCheckbox';
+import {SearchDropdown} from 'src/components/toolbar/SearchDropdown';
+import {SearchInput} from 'src/components/toolbar/SearchInput';
+import {SearchState} from 'src/components/toolbar/SearchTypes';
+import {ToolbarPagination} from 'src/components/toolbar/ToolbarPagination';
+import {ITeamMember} from 'src/hooks/UseMembers';
+
+export default function ManageMembersToolbar(props: ManageMembersToolbarProps) {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ {props.children}
+
+
+
+ >
+ );
+}
+
+interface ManageMembersToolbarProps {
+ selectedTeams: ITeamMember[];
+ deSelectAll: () => void;
+ allItems: ITeamMember[];
+ paginatedItems: ITeamMember[];
+ onItemSelect: (
+ item: ITeamMember,
+ rowIndex: number,
+ isSelecting: boolean,
+ ) => void;
+ page: number;
+ setPage: (page: number) => void;
+ perPage: number;
+ setPerPage: (perPage: number) => void;
+ searchOptions: string[];
+ search: SearchState;
+ setSearch: (search: SearchState) => void;
+ children?: React.ReactNode;
+}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/SetRepoPermissionsModal/SetRepoPermForTeamRoleDropDown.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/SetRepoPermissionsModal/SetRepoPermForTeamRoleDropDown.tsx
new file mode 100644
index 000000000..b024d9f97
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/SetRepoPermissionsModal/SetRepoPermForTeamRoleDropDown.tsx
@@ -0,0 +1,62 @@
+import {Dropdown, DropdownItem, DropdownToggle} from '@patternfly/react-core';
+import {useEffect, useState} from 'react';
+import {ITeamRepoPerms} from 'src/hooks/UseTeams';
+import {RepoPermissionDropdownItems} from 'src/routes/RepositoriesList/RobotAccountsList';
+
+export function SetRepoPermForTeamRoleDropDown(
+ props: SetRepoPermForTeamRoleDropDownProps,
+) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [dropdownValue, setDropdownValue] = useState(
+ props.repoPerm?.role,
+ );
+
+ const dropdownOnSelect = (roleName) => {
+ setDropdownValue(roleName);
+ props.updateModifiedRepoPerms(roleName?.toLowerCase(), props.repoPerm);
+ };
+
+ useEffect(() => {
+ if (props?.isItemSelected) {
+ dropdownOnSelect(props.selectedVal);
+ }
+ }, [props.selectedVal]);
+
+ useEffect(() => {
+ setDropdownValue(props.repoPerm?.role);
+ }, [props.repoPerm?.role]);
+
+ return (
+ setIsOpen(false)}
+ toggle={
+ setIsOpen(!isOpen)}>
+ {dropdownValue
+ ? dropdownValue?.charAt(0).toUpperCase() + dropdownValue?.slice(1)
+ : 'None'}
+
+ }
+ isOpen={isOpen}
+ dropdownItems={RepoPermissionDropdownItems.map((item) => (
+ dropdownOnSelect(item.name)}
+ >
+ {item.name}
+
+ ))}
+ />
+ );
+}
+
+interface SetRepoPermForTeamRoleDropDownProps {
+ organizationName: string;
+ teamName: string;
+ repoPerm: ITeamRepoPerms;
+ updateModifiedRepoPerms: (role: string, repoPerm: ITeamRepoPerms) => void;
+ isItemSelected?: boolean;
+ selectedVal: string;
+}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/SetRepoPermissionsModal/SetRepoPermissionForTeamModal.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/SetRepoPermissionsModal/SetRepoPermissionForTeamModal.tsx
new file mode 100644
index 000000000..1f81ae13e
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/SetRepoPermissionsModal/SetRepoPermissionForTeamModal.tsx
@@ -0,0 +1,261 @@
+import {
+ TableComposable,
+ Tbody,
+ Td,
+ Th,
+ Thead,
+ Tr,
+} from '@patternfly/react-table';
+import {Button, Modal, ModalVariant, Spinner} from '@patternfly/react-core';
+import {useEffect, useState} from 'react';
+import Empty from 'src/components/empty/Empty';
+import {CubesIcon} from '@patternfly/react-icons';
+import {useAlerts} from 'src/hooks/UseAlerts';
+import SetRepoPermissionsToolbar from './SetRepoPermissionsForTeamToolbar';
+import {
+ ITeamRepoPerms,
+ useFetchRepoPermForTeam,
+ useUpdateTeamRepoPerm,
+} from 'src/hooks/UseTeams';
+import {SetRepoPermForTeamRoleDropDown} from './SetRepoPermForTeamRoleDropDown';
+import {formatDate} from 'src/libs/utils';
+import {AlertVariant} from 'src/atoms/AlertState';
+
+export const setRepoPermForTeamColumnNames = {
+ repoName: 'Repository',
+ permissions: 'Permissions',
+ lastUpdate: 'Last Updated',
+};
+
+export default function SetRepoPermissionForTeamModal(
+ props: SetRepoPermissionForTeamModalProps,
+) {
+ const {
+ teamRepoPerms,
+ paginatedTeamRepoPerms,
+ filteredTeamRepoPerms,
+ loading,
+ page,
+ setPage,
+ perPage,
+ setPerPage,
+ search,
+ setSearch,
+ } = useFetchRepoPermForTeam(props.organizationName, props.teamName);
+
+ const {
+ updateRepoPerm,
+ errorUpdateRepoPerm,
+ detailedErrorUpdateRepoPerm,
+ successUpdateRepoPerm,
+ } = useUpdateTeamRepoPerm(props.organizationName, props.teamName);
+
+ const [selectedRepoPerms, setSelectedRepoPerms] = useState(
+ [],
+ );
+ const [modifiedRepoPerms, setModifiedRepoPerms] = useState(
+ [],
+ );
+ const [isKebabOpen, setKebabOpen] = useState(false);
+ const {addAlert} = useAlerts();
+
+ useEffect(() => {
+ if (successUpdateRepoPerm) {
+ addAlert({
+ variant: AlertVariant.Success,
+ title: `Updated repo perm for team: ${props.teamName} successfully`,
+ });
+ props.handleModalToggle();
+ }
+ if (errorUpdateRepoPerm) {
+ const errorUpdatingRepoPermMessage = (
+ <>
+ {Array.from(detailedErrorUpdateRepoPerm.getErrors()).map(
+ ([repoPerm, error]) => (
+
+ Could not update repo permission for {repoPerm}:{' '}
+ {error.error.message}
+
+ ),
+ )}
+ >
+ );
+ addAlert({
+ variant: AlertVariant.Failure,
+ title: `Could not update repo permissions`,
+ message: errorUpdatingRepoPermMessage,
+ });
+ }
+ }, [successUpdateRepoPerm, errorUpdateRepoPerm]);
+
+ const onSelectRepoPerm = (
+ repoPerm: ITeamRepoPerms,
+ rowIndex: number,
+ isSelecting: boolean,
+ ) => {
+ setSelectedRepoPerms((prevSelected) => {
+ const otherSelectedRepoPerms = prevSelected.filter(
+ (t) => t.repoName !== repoPerm.repoName,
+ );
+ return isSelecting
+ ? [...otherSelectedRepoPerms, repoPerm]
+ : otherSelectedRepoPerms;
+ });
+ };
+
+ const setRepoPermForTeamHandler = () => {
+ // Filter to only update records which have modified role
+ const updatedRepoPerms = modifiedRepoPerms.filter((modifiedPerm) =>
+ teamRepoPerms.find(
+ (teamPerm) =>
+ teamPerm.repoName === modifiedPerm.repoName &&
+ teamPerm.role !== modifiedPerm.role,
+ ),
+ );
+ setModifiedRepoPerms(updatedRepoPerms);
+
+ if (updatedRepoPerms?.length > 0) {
+ updateRepoPerm({teamRepoPerms: updatedRepoPerms});
+ } else {
+ props.handleModalToggle();
+ }
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ const isItemSelected = (repoPerm) =>
+ selectedRepoPerms.some((t) => t.repoName === repoPerm.repoName);
+
+ const fetchRepoPermission = (repoPerm) => {
+ for (const item of modifiedRepoPerms) {
+ if (repoPerm.repoName == item.repoName) {
+ return item.role;
+ }
+ }
+ return 'None';
+ };
+
+ const updateModifiedRepoPerms = (
+ roleName: string,
+ repoPerm: ITeamRepoPerms,
+ ) => {
+ // Remove item if already present
+ setModifiedRepoPerms((prev) => [
+ ...prev.filter((item) => item.repoName !== repoPerm.repoName),
+ {
+ repoName: repoPerm.repoName,
+ role: roleName,
+ lastModified: repoPerm.lastModified,
+ },
+ ]);
+ };
+
+ const emptyPermComponent = (
+
+ );
+
+ return (
+
+ Update
+ ,
+ ,
+ ]}
+ >
+ {!teamRepoPerms?.length ? (
+ emptyPermComponent
+ ) : (
+ setSelectedRepoPerms([])}
+ allItems={filteredTeamRepoPerms}
+ paginatedItems={paginatedTeamRepoPerms}
+ onItemSelect={onSelectRepoPerm}
+ page={page}
+ setPage={setPage}
+ perPage={perPage}
+ setPerPage={setPerPage}
+ search={search}
+ setSearch={setSearch}
+ searchOptions={[setRepoPermForTeamColumnNames.repoName]}
+ isKebabOpen={isKebabOpen}
+ setKebabOpen={setKebabOpen}
+ updateModifiedRepoPerms={updateModifiedRepoPerms}
+ >
+
+
+
+ |
+ {setRepoPermForTeamColumnNames.repoName} |
+ {setRepoPermForTeamColumnNames.permissions} |
+ {setRepoPermForTeamColumnNames.lastUpdate} |
+
+
+
+ {paginatedTeamRepoPerms?.map((repoPerm, rowIndex) => (
+
+ |
+ onSelectRepoPerm(repoPerm, rowIndex, isSelecting),
+ isSelected: isItemSelected(repoPerm),
+ }}
+ />
+ |
+ {repoPerm.repoName}
+ |
+
+
+ |
+
+ {formatDate(repoPerm.lastModified)}
+ |
+
+ ))}
+
+
+
+ )}
+
+ );
+}
+
+interface SetRepoPermissionForTeamModalProps {
+ organizationName: string;
+ teamName: string;
+ isModalOpen: boolean;
+ handleModalToggle: () => void;
+}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/SetRepoPermissionsModal/SetRepoPermissionsForTeamToolbar.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/SetRepoPermissionsModal/SetRepoPermissionsForTeamToolbar.tsx
new file mode 100644
index 000000000..f5db3aab5
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/SetRepoPermissionsModal/SetRepoPermissionsForTeamToolbar.tsx
@@ -0,0 +1,119 @@
+import {
+ DropdownItem,
+ Flex,
+ FlexItem,
+ PanelFooter,
+ Toolbar,
+ ToolbarContent,
+ ToolbarItem,
+} from '@patternfly/react-core';
+import Conditional from 'src/components/empty/Conditional';
+import {DropdownCheckbox} from 'src/components/toolbar/DropdownCheckbox';
+import {Kebab} from 'src/components/toolbar/Kebab';
+import {SearchDropdown} from 'src/components/toolbar/SearchDropdown';
+import {SearchInput} from 'src/components/toolbar/SearchInput';
+import {SearchState} from 'src/components/toolbar/SearchTypes';
+import {ToolbarPagination} from 'src/components/toolbar/ToolbarPagination';
+import {ITeamRepoPerms} from 'src/hooks/UseTeams';
+import {RepoPermissionDropdownItems} from 'src/routes/RepositoriesList/RobotAccountsList';
+
+export default function SetRepoPermissionsForTeamModalToolbar(
+ props: SetRepoPermissionsForTeamModalToolbarProps,
+) {
+ const dropdownOnSelect = (selectedVal) => {
+ props.selectedRepoPerms.map((repoPerm) => {
+ props.updateModifiedRepoPerms(selectedVal?.toLowerCase(), repoPerm);
+ });
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ 0}>
+ (
+ dropdownOnSelect(item.name)}
+ >
+ {item.name}
+
+ ))}
+ useActions={false}
+ id="toggle-bulk-perms-kebab"
+ />
+
+
+
+
+
+ {props.children}
+
+
+
+ >
+ );
+}
+
+interface SetRepoPermissionsForTeamModalToolbarProps {
+ selectedRepoPerms: ITeamRepoPerms[];
+ deSelectAll: () => void;
+ allItems: ITeamRepoPerms[];
+ paginatedItems: ITeamRepoPerms[];
+ onItemSelect: (
+ item: ITeamRepoPerms,
+ rowIndex: number,
+ isSelecting: boolean,
+ ) => void;
+ page: number;
+ setPage: (page: number) => void;
+ perPage: number;
+ setPerPage: (perPage: number) => void;
+ searchOptions: string[];
+ search: SearchState;
+ setSearch: (search: SearchState) => void;
+ children?: React.ReactNode;
+ isKebabOpen: boolean;
+ setKebabOpen: (open: boolean) => void;
+ updateModifiedRepoPerms: (item, repoPerm) => void;
+}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/TeamViewKebab.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/TeamViewKebab.tsx
new file mode 100644
index 000000000..1128afa77
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/TeamViewKebab.tsx
@@ -0,0 +1,92 @@
+import {
+ Dropdown,
+ DropdownItem,
+ KebabToggle,
+ DropdownPosition,
+} from '@patternfly/react-core';
+import {useState} from 'react';
+import {Link, useSearchParams} from 'react-router-dom';
+import {AlertVariant} from 'src/atoms/AlertState';
+import {useAlerts} from 'src/hooks/UseAlerts';
+import {ITeams, useDeleteTeam} from 'src/hooks/UseTeams';
+import {getTeamMemberPath} from 'src/routes/NavigationPath';
+
+export default function TeamViewKebab(props: TeamViewKebabProps) {
+ const [isKebabOpen, setIsKebabOpen] = useState(false);
+ const [searchParams] = useSearchParams();
+ const {addAlert} = useAlerts();
+
+ const {removeTeam} = useDeleteTeam({
+ orgName: props.organizationName,
+ onSuccess: () => {
+ props.deSelectAll();
+ addAlert({
+ variant: AlertVariant.Success,
+ title: `Successfully deleted team`,
+ });
+ },
+ onError: (err) => {
+ addAlert({
+ variant: AlertVariant.Failure,
+ title: `Failed to delete team: ${err}`,
+ });
+ },
+ });
+
+ return (
+ setIsKebabOpen(!isKebabOpen)}
+ toggle={
+ {
+ setIsKebabOpen(!isKebabOpen);
+ }}
+ />
+ }
+ isOpen={isKebabOpen}
+ dropdownItems={[
+
+ Manage team members
+
+ }
+ >,
+
+ Set repository permissions
+ ,
+ removeTeam(props.team)}
+ className="red-color"
+ data-testid={`${props.team.name}-del-option`}
+ >
+ Delete
+ ,
+ ]}
+ isPlain
+ position={DropdownPosition.right}
+ />
+ );
+}
+
+interface TeamViewKebabProps {
+ organizationName: string;
+ team: ITeams;
+ deSelectAll: () => void;
+ onSelectRepo: () => void;
+}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/TeamsRoleDropDown.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/TeamsRoleDropDown.tsx
new file mode 100644
index 000000000..a4560970c
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/TeamsRoleDropDown.tsx
@@ -0,0 +1,73 @@
+import {Dropdown, DropdownItem, DropdownToggle} from '@patternfly/react-core';
+import {useEffect, useState} from 'react';
+import {AlertVariant} from 'src/atoms/AlertState';
+import {useAlerts} from 'src/hooks/UseAlerts';
+import {useUpdateTeamRole} from 'src/hooks/UseTeams';
+
+export enum teamPermissions {
+ Admin = 'admin',
+ Member = 'member',
+ Creator = 'creator',
+}
+
+export function TeamsRoleDropDown(props: TeamsRoleDropDownProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const {addAlert} = useAlerts();
+
+ const {
+ updateTeamRole,
+ errorUpdateTeamRole: error,
+ successUpdateTeamRole: success,
+ } = useUpdateTeamRole(props.organizationName);
+
+ useEffect(() => {
+ if (error) {
+ addAlert({
+ variant: AlertVariant.Failure,
+ title: `Unable to update role for team: ${props.teamName}`,
+ });
+ }
+ }, [error]);
+
+ useEffect(() => {
+ if (success) {
+ addAlert({
+ variant: AlertVariant.Success,
+ title: `Team role updated successfully for: ${props.teamName}`,
+ });
+ }
+ }, [success]);
+
+ return (
+ setIsOpen(false)}
+ toggle={
+ setIsOpen(!isOpen)}>
+ {props.teamRole.charAt(0).toUpperCase() + props.teamRole.slice(1)}
+
+ }
+ isOpen={isOpen}
+ dropdownItems={Object.keys(teamPermissions).map((key) => (
+
+ updateTeamRole({
+ teamName: props.teamName,
+ teamRole: teamPermissions[key],
+ })
+ }
+ >
+ {key}
+
+ ))}
+ />
+ );
+}
+
+interface TeamsRoleDropDownProps {
+ organizationName: string;
+ teamName: string;
+ teamRole: string;
+}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/TeamsViewList.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/TeamsViewList.tsx
new file mode 100644
index 000000000..ea0d055fb
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/TeamsViewList.tsx
@@ -0,0 +1,307 @@
+import {
+ TableComposable,
+ Tbody,
+ Td,
+ Th,
+ Thead,
+ Tr,
+} from '@patternfly/react-table';
+import TeamsViewToolbar from './TeamsViewToolbar';
+import {Link, useSearchParams} from 'react-router-dom';
+import {
+ Dropdown,
+ DropdownItem,
+ DropdownToggle,
+ PageSection,
+ PageSectionVariants,
+ PanelFooter,
+ Spinner,
+} from '@patternfly/react-core';
+import {useEffect, useState} from 'react';
+import TeamViewKebab from './TeamViewKebab';
+import {ITeams, useDeleteTeam, useFetchTeams} from 'src/hooks/UseTeams';
+import {TeamsRoleDropDown} from './TeamsRoleDropDown';
+import {BulkDeleteModalTemplate} from 'src/components/modals/BulkDeleteModalTemplate';
+import {BulkOperationError, addDisplayError} from 'src/resources/ErrorHandling';
+import ErrorModal from 'src/components/errors/ErrorModal';
+import {getTeamMemberPath} from 'src/routes/NavigationPath';
+import {useAlerts} from 'src/hooks/UseAlerts';
+import {AlertVariant} from 'src/atoms/AlertState';
+import SetRepoPermissionForTeamModal from 'src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/SetRepoPermissionsModal/SetRepoPermissionForTeamModal';
+import {ToolbarPagination} from 'src/components/toolbar/ToolbarPagination';
+
+export const teamViewColumnNames = {
+ teamName: 'Team name',
+ members: 'Members',
+ repositories: 'Repositories',
+ teamRole: 'Team role',
+};
+
+export default function TeamsViewList(props: TeamsViewListProps) {
+ const {
+ teams,
+ filteredTeams,
+ paginatedTeams,
+ loading,
+ error,
+ page,
+ setPage,
+ perPage,
+ setPerPage,
+ search,
+ setSearch,
+ } = useFetchTeams(props.organizationName);
+
+ const [selectedTeams, setSelectedTeams] = useState([]);
+ const [isKebabOpen, setKebabOpen] = useState(false);
+ const [deleteModalIsOpen, setDeleteModalIsOpen] = useState(false);
+ const [err, setIsError] = useState();
+ const [searchParams] = useSearchParams();
+ const {addAlert} = useAlerts();
+ const [isSetRepoPermModalOpen, setIsSetRepoPermModalOpen] = useState(false);
+ const [repoPermForTeam, setRepoPermForTeam] = useState('');
+
+ useEffect(() => {
+ if (error) {
+ addAlert({variant: AlertVariant.Failure, title: `Could not load teams`});
+ }
+ }, [error]);
+
+ const handleDeleteModalToggle = () => {
+ setKebabOpen(!isKebabOpen);
+ setDeleteModalIsOpen(!deleteModalIsOpen);
+ };
+
+ const kebabItems = [
+
+ Delete
+ ,
+ ];
+
+ /* Mapper object used to render bulk delete table
+ - keys are actual column names of the table
+ - value is an object type with a "label" which maps to the attributes of
+ and an optional "transformFunc" which can be used to modify the value being displayed */
+ const mapOfColNamesToTableData = {
+ 'Team Name': {
+ label: 'name',
+ transformFunc: (team: ITeams) => {
+ return `${team.name}`;
+ },
+ },
+ Members: {
+ label: 'members',
+ transformFunc: (team: ITeams) => team.member_count,
+ },
+ Repositories: {
+ label: 'repositories',
+ transformFunc: (team: ITeams) => {
+ return `${team.repo_count}`;
+ },
+ },
+ 'Team Role': {
+ label: 'team role',
+ transformFunc: (team: ITeams) => (
+
+ {team.role}
+
+ }
+ isOpen={false}
+ dropdownItems={[team.role]}
+ />
+ ),
+ },
+ };
+
+ const {removeTeam} = useDeleteTeam({
+ orgName: props.organizationName,
+ onSuccess: () => {
+ setDeleteModalIsOpen(!deleteModalIsOpen);
+ setSelectedTeams([]);
+ addAlert({
+ variant: AlertVariant.Success,
+ title: `Successfully deleted teams`,
+ });
+ },
+ onError: (err) => {
+ if (err instanceof BulkOperationError) {
+ const errMessages = [];
+ err.getErrors().forEach((error, team) => {
+ addAlert({
+ variant: AlertVariant.Failure,
+ title: `Could not delete team ${team}: ${error.error}`,
+ });
+ errMessages.push(
+ addDisplayError(`Failed to delete teams ${team}`, error.error),
+ );
+ });
+ setIsError(errMessages);
+ } else {
+ setIsError([addDisplayError('Failed to delete teams', err)]);
+ }
+ setDeleteModalIsOpen(!deleteModalIsOpen);
+ setSelectedTeams([]);
+ },
+ });
+
+ const deleteModal = (
+
+ selectedTeams.some((selected) => team.name === selected.name),
+ )}
+ resourceName={'teams'}
+ />
+ );
+
+ const onSelectTeam = (
+ team: ITeams,
+ rowIndex: number,
+ isSelecting: boolean,
+ ) => {
+ setSelectedTeams((prevSelected) => {
+ const otherSelectedTeams = prevSelected.filter(
+ (t) => t.name !== team.name,
+ );
+ return isSelecting ? [...otherSelectedTeams, team] : otherSelectedTeams;
+ });
+ };
+
+ const setRepoPermModal = (
+
+ setIsSetRepoPermModalOpen(!isSetRepoPermModalOpen)
+ }
+ organizationName={props.organizationName}
+ teamName={repoPermForTeam}
+ />
+ );
+
+ const openSetRepoPermModal = (teamName: string) => {
+ setRepoPermForTeam(teamName);
+ setIsSetRepoPermModalOpen(!isSetRepoPermModalOpen);
+ };
+
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+
+ setSelectedTeams([])}
+ allItems={filteredTeams}
+ paginatedItems={paginatedTeams}
+ onItemSelect={onSelectTeam}
+ page={page}
+ setPage={setPage}
+ perPage={perPage}
+ setPerPage={setPerPage}
+ search={search}
+ setSearch={setSearch}
+ searchOptions={[teamViewColumnNames.teamName]}
+ isKebabOpen={isKebabOpen}
+ setKebabOpen={setKebabOpen}
+ kebabItems={kebabItems}
+ deleteKebabIsOpen={deleteModalIsOpen}
+ deleteModal={deleteModal}
+ isSetRepoPermModalOpen={isSetRepoPermModalOpen}
+ setRepoPermModal={setRepoPermModal}
+ />
+ {props.children}
+
+
+
+ |
+ {teamViewColumnNames.teamName} |
+ {teamViewColumnNames.members} |
+ {teamViewColumnNames.repositories} |
+ {teamViewColumnNames.teamRole} |
+ |
+
+
+
+ {paginatedTeams?.map((team, rowIndex) => (
+
+ |
+ onSelectTeam(team, rowIndex, isSelecting),
+ isSelected: selectedTeams.some((t) => t.name === team.name),
+ }}
+ />
+ | {team.name} |
+
+
+ {team.member_count}
+
+ |
+
+ {
+ openSetRepoPermModal(team.name);
+ }}
+ >
+ {team.repo_count}
+
+ |
+
+
+ |
+
+ setSelectedTeams([])}
+ onSelectRepo={() => {
+ openSetRepoPermModal(team.name);
+ }}
+ />
+ |
+
+ ))}
+
+
+
+
+
+
+ );
+}
+
+interface TeamsViewListProps {
+ organizationName: string;
+ children?: React.ReactNode;
+}
diff --git a/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/TeamsViewToolbar.tsx b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/TeamsViewToolbar.tsx
new file mode 100644
index 000000000..4e9ba2267
--- /dev/null
+++ b/web/src/routes/OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/TeamsViewToolbar.tsx
@@ -0,0 +1,90 @@
+import {
+ Flex,
+ FlexItem,
+ Toolbar,
+ ToolbarContent,
+ ToolbarItem,
+} from '@patternfly/react-core';
+import Conditional from 'src/components/empty/Conditional';
+import {DropdownCheckbox} from 'src/components/toolbar/DropdownCheckbox';
+import {Kebab} from 'src/components/toolbar/Kebab';
+import {SearchDropdown} from 'src/components/toolbar/SearchDropdown';
+import {SearchInput} from 'src/components/toolbar/SearchInput';
+import {SearchState} from 'src/components/toolbar/SearchTypes';
+import {ToolbarPagination} from 'src/components/toolbar/ToolbarPagination';
+import {ITeams} from 'src/hooks/UseTeams';
+
+export default function TeamsViewToolbar(props: TeamsViewToolbarProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {props.deleteModal}
+
+
+ {props.setRepoPermModal}
+
+
+
+
+
+ );
+}
+
+interface TeamsViewToolbarProps {
+ selectedTeams: ITeams[];
+ deSelectAll: () => void;
+ allItems: ITeams[];
+ paginatedItems: ITeams[];
+ onItemSelect: (item: ITeams, rowIndex: number, isSelecting: boolean) => void;
+ page: number;
+ setPage: (page: number) => void;
+ perPage: number;
+ setPerPage: (perPage: number) => void;
+ searchOptions: string[];
+ search: SearchState;
+ setSearch: (search: SearchState) => void;
+ isKebabOpen: boolean;
+ setKebabOpen: (open: boolean) => void;
+ kebabItems: React.ReactElement[];
+ deleteKebabIsOpen: boolean;
+ deleteModal: object;
+ isSetRepoPermModalOpen: boolean;
+ setRepoPermModal: object;
+}
diff --git a/web/src/routes/PluginMain.tsx b/web/src/routes/PluginMain.tsx
index 1a366bec0..aaecc4fc2 100644
--- a/web/src/routes/PluginMain.tsx
+++ b/web/src/routes/PluginMain.tsx
@@ -1,6 +1,6 @@
import {Banner, Flex, FlexItem, Page} from '@patternfly/react-core';
-import {Navigate, Outlet, Route, Router, Routes} from 'react-router-dom';
+import {Navigate, Outlet, Route, Routes} from 'react-router-dom';
import {RecoilRoot, useSetRecoilState} from 'recoil';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
@@ -22,7 +22,7 @@ import {CreateNewUser} from 'src/components/modals/CreateNewUser';
import NewUserEmptyPage from 'src/components/NewUserEmptyPage';
import axios from 'axios';
import axiosIns from 'src/libs/axios';
-
+import ManageMembersList from './OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersList';
const NavigationRoutes = [
{
@@ -41,6 +41,10 @@ const NavigationRoutes = [
path: NavigationPath.repositoryDetail,
Component: ,
},
+ {
+ path: NavigationPath.teamMember,
+ Component: ,
+ },
];
function PluginMain() {
@@ -75,7 +79,7 @@ function PluginMain() {
useEffect(() => {
setIsPluginState(true);
- if (user?.prompts && user.prompts.includes("confirm_username")) {
+ if (user?.prompts && user.prompts.includes('confirm_username')) {
setConfirmUserModalOpen(true);
}
}, [user]);
@@ -86,10 +90,10 @@ function PluginMain() {
return (
-
{user?.prompts && user.prompts.includes('confirm_username') ? (
-
+
) : (
} />
diff --git a/web/src/routes/RepositoriesList/RobotAccountsList.tsx b/web/src/routes/RepositoriesList/RobotAccountsList.tsx
index d80e355f6..070a16082 100644
--- a/web/src/routes/RepositoriesList/RobotAccountsList.tsx
+++ b/web/src/routes/RepositoriesList/RobotAccountsList.tsx
@@ -23,13 +23,10 @@ import {RobotAccountColumnNames} from './ColumnNames';
import {RobotAccountsToolBar} from 'src/routes/RepositoriesList/RobotAccountsToolBar';
import CreateRobotAccountModal from 'src/components/modals/CreateRobotAccountModal';
import {IRobot} from 'src/resources/RobotsResource';
-import {useRecoilState, useRecoilValue} from 'recoil';
-import {
- searchRobotAccountState,
- selectedRobotAccountsState,
-} from 'src/atoms/RobotAccountState';
+import {useRecoilState} from 'recoil';
+import {selectedRobotAccountsState} from 'src/atoms/RobotAccountState';
import {useRobotAccounts} from 'src/hooks/useRobotAccounts';
-import {ReactElement, useState, useRef, useEffect} from 'react';
+import {ReactElement, useState, useRef} from 'react';
import {ToolbarPagination} from 'src/components/toolbar/ToolbarPagination';
import RobotAccountKebab from './RobotAccountKebab';
import {useDeleteRobotAccounts} from 'src/hooks/UseDeleteRobotAccount';
@@ -53,7 +50,7 @@ import {useRobotRepoPermissions} from 'src/hooks/UseRobotRepoPermissions';
import RobotTokensModal from 'src/components/modals/RobotTokensModal';
import {SearchState} from 'src/components/toolbar/SearchTypes';
-const RepoPermissionDropdownItems = [
+export const RepoPermissionDropdownItems = [
{
name: 'None',
description: 'No permissions on the repository',
@@ -72,7 +69,7 @@ const RepoPermissionDropdownItems = [
},
];
-const EmptyRobotAccount = {
+const emptyRobotAccount = {
name: '',
created: '',
last_accessed: '',
@@ -95,7 +92,7 @@ export default function RobotAccountsList(props: RobotAccountsListProps) {
const [robotRepos, setRobotRepos] = useState([]);
const [teams, setTeams] = useState([]);
const [robotForDeletion, setRobotForDeletion] = useState([]);
- const [robotForModalView, setRobotForModalView] = useState(EmptyRobotAccount);
+ const [robotForModalView, setRobotForModalView] = useState(emptyRobotAccount);
const [isTokenModalOpen, setTokenModalOpen] = useState(false);
// For repository modal view
const [selectedRepoPerms, setSelectedRepoPerms] = useRecoilState(
@@ -268,7 +265,7 @@ export default function RobotAccountsList(props: RobotAccountsListProps) {
setSelectedReposForModalView([]);
setSelectedRepoPerms([]);
robotPermissionsPlaceholder.current.resetRobotPermissions();
- setRobotForModalView(EmptyRobotAccount);
+ setRobotForModalView(emptyRobotAccount);
};
const onRepoModalSave = async () => {
@@ -360,7 +357,7 @@ export default function RobotAccountsList(props: RobotAccountsListProps) {
};
const onTokenModalClose = () => {
- setRobotForModalView(EmptyRobotAccount);
+ setRobotForModalView(emptyRobotAccount);
};
const mapOfColNamesToTableData = {
diff --git a/web/src/routes/StandaloneMain.tsx b/web/src/routes/StandaloneMain.tsx
index a6546035f..3646230b6 100644
--- a/web/src/routes/StandaloneMain.tsx
+++ b/web/src/routes/StandaloneMain.tsx
@@ -22,6 +22,10 @@ import axiosIns from 'src/libs/axios';
import Alerts from './Alerts';
const NavigationRoutes = [
+ {
+ path: NavigationPath.teamMember,
+ Component: ,
+ },
{
path: NavigationPath.organizationsList,
Component: ,
@@ -49,7 +53,6 @@ export function StandaloneMain() {
const quayConfig = useQuayConfig();
const {loading, error} = useCurrentUser();
-
useEffect(() => {
if (quayConfig?.config?.REGISTRY_TITLE) {
document.title = quayConfig.config.REGISTRY_TITLE;
@@ -89,7 +92,7 @@ export function StandaloneMain() {
-
+
} />
{NavigationRoutes.map(({path, Component}, key) => (