Merge pull request #5123 from Infisical/ENG-4284-3

improvement(frontend): update org/sub-org group membership ui to v3 components
This commit is contained in:
Scott Wilson
2026-01-06 18:53:20 -08:00
committed by GitHub
13 changed files with 694 additions and 666 deletions

View File

@@ -14,6 +14,7 @@ export type TGroup = {
createdAt: string;
updatedAt: string;
role: string;
roleId: string;
};
export type TGroupMembership = {

View File

@@ -1,23 +1,18 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { ChevronLeftIcon, EllipsisIcon } from "lucide-react";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { DeleteActionModal, PageHeader, Spinner } from "@app/components/v2";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
PageHeader,
Spinner,
Tooltip
} from "@app/components/v2";
UnstableButton,
UnstableDropdownMenu,
UnstableDropdownMenuContent,
UnstableDropdownMenuItem,
UnstableDropdownMenuTrigger
} from "@app/components/v3";
import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteGroup } from "@app/hooks/api";
@@ -73,58 +68,70 @@ const Page = () => {
handlePopUpClose("deleteGroup");
};
if (isPending) return <Spinner size="sm" className="mt-2 ml-2" />;
if (isPending)
return (
<div className="flex w-full items-center justify-center p-24">
<Spinner />
</div>
);
return (
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex max-w-8xl flex-col">
{data && (
<div className="mx-auto w-full max-w-8xl">
<>
<Link
to="/organizations/$orgId/access-management"
params={{ orgId: currentOrg.id }}
search={{
selectedTab: TabSections.Groups
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
className="mb-4 flex w-fit items-center gap-x-1 text-sm text-mineshaft-400 transition duration-100 hover:text-mineshaft-400/80"
>
<FontAwesomeIcon icon={faChevronLeft} />
<ChevronLeftIcon size={16} />
{isSubOrganization ? "Sub-" : ""}Organization Groups
</Link>
<PageHeader
scope={isSubOrganization ? "namespace" : "org"}
description={`${isSubOrganization ? "Sub-" : ""}Organization Group`}
description={`Configure and manage ${isSubOrganization ? "sub-" : ""}organization group`}
title={data.group.name}
>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<Button variant="outline_bg">More</Button>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="p-1">
<UnstableDropdownMenu>
<UnstableDropdownMenuTrigger asChild>
<UnstableButton variant="outline">
Options
<EllipsisIcon />
</UnstableButton>
</UnstableDropdownMenuTrigger>
<UnstableDropdownMenuContent align="end">
<UnstableDropdownMenuItem
onClick={() => {
navigator.clipboard.writeText(groupId);
createNotification({
text: "Group ID copied to clipboard",
type: "info"
});
}}
>
Copy Group ID
</UnstableDropdownMenuItem>
<OrgPermissionCan
I={OrgPermissionGroupActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
<UnstableDropdownMenuItem
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("groupCreateUpdate", {
groupId,
name: data.group.name,
slug: data.group.slug,
role: data.group.role
role: data.group.roleId || data.group.role
});
}}
disabled={!isAllowed}
>
Edit Group
</DropdownMenuItem>
</UnstableDropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
@@ -132,37 +139,31 @@ const Page = () => {
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:bg-red-500! hover:text-white!"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
<UnstableDropdownMenuItem
variant="danger"
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("deleteGroup", {
id: groupId,
name: data.group.name
});
}}
disabled={!isAllowed}
>
Delete Group
</DropdownMenuItem>
</UnstableDropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</UnstableDropdownMenuContent>
</UnstableDropdownMenu>
</PageHeader>
<div className="flex flex-col gap-4 md:flex-row">
<div className="w-full md:w-96">
<GroupDetailsSection groupId={groupId} handlePopUpOpen={handlePopUpOpen} />
</div>
<div className="flex grow flex-col gap-4">
<div className="flex flex-col gap-5 lg:flex-row">
<GroupDetailsSection groupId={groupId} handlePopUpOpen={handlePopUpOpen} />
<div className="flex flex-1 flex-col gap-y-5">
<GroupMembersSection groupId={groupId} groupSlug={data.group.slug} />
<GroupProjectsSection groupId={groupId} groupSlug={data.group.slug} />
</div>
</div>
</div>
</>
)}
<GroupCreateUpdateModal
popUp={popUp}

View File

@@ -80,7 +80,7 @@ export const AddGroupIdentitiesTab = ({ groupId, groupSlug, search }: Props) =>
<Table>
<THead>
<Tr>
<Th>Machine Identity</Th>
<Th className="w-full">Machine Identity</Th>
<Th />
</Tr>
</THead>
@@ -90,9 +90,7 @@ export const AddGroupIdentitiesTab = ({ groupId, groupSlug, search }: Props) =>
data?.machineIdentities?.map((identity: TGroupMachineIdentity) => {
return (
<Tr className="items-center" key={`group-identity-${identity.id}`}>
<Td>
<p>{identity.name}</p>
</Td>
<Td className="max-w-0 truncate">{identity.name}</Td>
<Td className="flex justify-end">
<OrgPermissionCan
I={OrgPermissionGroupActions.Edit}

View File

@@ -77,7 +77,7 @@ export const AddGroupUsersTab = ({ groupId, groupSlug, search }: Props) => {
<Table>
<THead>
<Tr>
<Th>User</Th>
<Th className="w-full">User</Th>
<Th />
</Tr>
</THead>
@@ -87,9 +87,9 @@ export const AddGroupUsersTab = ({ groupId, groupSlug, search }: Props) => {
data?.users?.map(({ id, firstName, lastName, username }) => {
return (
<Tr className="items-center" key={`group-user-${id}`}>
<Td>
<p>{`${firstName ?? "-"} ${lastName ?? ""}`}</p>
<p>{username}</p>
<Td className="max-w-0">
<p className="truncate">{`${firstName ?? "-"} ${lastName ?? ""}`}</p>
<p className="truncate">{username}</p>
</Td>
<Td className="flex justify-end">
<OrgPermissionCan

View File

@@ -1,10 +1,23 @@
import { faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { CheckIcon, ClipboardListIcon, PencilIcon } from "lucide-react";
import { OrgPermissionCan } from "@app/components/permissions";
import { IconButton, Spinner, Tooltip } from "@app/components/v2";
import { CopyButton } from "@app/components/v2/CopyButton";
import { Tooltip } from "@app/components/v2";
import {
Detail,
DetailGroup,
DetailLabel,
DetailValue,
UnstableCard,
UnstableCardAction,
UnstableCardContent,
UnstableCardDescription,
UnstableCardHeader,
UnstableCardTitle,
UnstableIconButton
} from "@app/components/v3";
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetGroupById } from "@app/hooks/api/";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -14,75 +27,97 @@ type Props = {
};
export const GroupDetailsSection = ({ groupId, handlePopUpOpen }: Props) => {
const { data, isPending } = useGetGroupById(groupId);
const { data } = useGetGroupById(groupId);
if (isPending) return <Spinner size="sm" className="mt-2 ml-2" />;
const [, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const [, isCopyingSlug, setCopyTextSlug] = useTimedReset<string>({
initialState: "Copy slug to clipboard"
});
return data ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-medium text-mineshaft-100">Group Details</h3>
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => {
return (
<Tooltip content="Edit Group">
<IconButton
isDisabled={!isAllowed}
ariaLabel="edit group button"
variant="plain"
className="group relative"
<UnstableCard className="w-full lg:max-w-[24rem]">
<UnstableCardHeader className="border-b">
<UnstableCardTitle>Details</UnstableCardTitle>
<UnstableCardDescription>Group details</UnstableCardDescription>
<UnstableCardAction>
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<UnstableIconButton
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("groupCreateUpdate", {
groupId,
name: data.group.name,
slug: data.group.slug,
role: data.group.roleId || data.group.role
});
}}
size="xs"
variant="outline"
>
<PencilIcon />
</UnstableIconButton>
)}
</OrgPermissionCan>
</UnstableCardAction>
</UnstableCardHeader>
<UnstableCardContent>
<DetailGroup>
<Detail>
<DetailLabel>Name</DetailLabel>
<DetailValue>{data.group.name}</DetailValue>
</Detail>
<Detail>
<DetailLabel>ID</DetailLabel>
<DetailValue className="flex items-center gap-x-1">
{data.group.id}
<Tooltip content="Copy group ID to clipboard">
<UnstableIconButton
onClick={() => {
handlePopUpOpen("groupCreateUpdate", {
groupId,
name: data.group.name,
slug: data.group.slug,
role: data.group.role
});
navigator.clipboard.writeText(data.group.id);
setCopyTextId("Copied");
}}
variant="ghost"
size="xs"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
{isCopyingId ? <CheckIcon /> : <ClipboardListIcon className="text-label" />}
</UnstableIconButton>
</Tooltip>
);
}}
</OrgPermissionCan>
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Group ID</p>
<div className="group flex items-center gap-2">
<p className="text-sm text-mineshaft-300">{data.group.id}</p>
<CopyButton value={data.group.id} name="Group ID" size="xs" variant="plain" />
</div>
</div>
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">{data.group.name}</p>
</div>
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Slug</p>
<div className="group flex items-center gap-2">
<p className="text-sm text-mineshaft-300">{data.group.slug}</p>
<CopyButton value={data.group.slug} name="Slug" size="xs" variant="plain" />
</div>
</div>
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Organization Role</p>
<p className="text-sm text-mineshaft-300">{data.group.role}</p>
</div>
<div className="mb-4">
<p className="text-sm font-medium text-mineshaft-300">Created At</p>
<p className="text-sm text-mineshaft-300">
{new Date(data.group.createdAt).toLocaleString()}
</p>
</div>
</div>
</div>
</DetailValue>
</Detail>
<Detail>
<DetailLabel>Slug</DetailLabel>
<DetailValue className="flex items-center gap-x-1">
{data.group.slug}
<Tooltip content="Copy slug to clipboard">
<UnstableIconButton
onClick={() => {
navigator.clipboard.writeText(data.group.slug);
setCopyTextSlug("Copied");
}}
variant="ghost"
size="xs"
>
{isCopyingSlug ? <CheckIcon /> : <ClipboardListIcon className="text-label" />}
</UnstableIconButton>
</Tooltip>
</DetailValue>
</Detail>
<Detail>
<DetailLabel>Organization Role</DetailLabel>
<DetailValue>{data.group.role}</DetailValue>
</Detail>
<Detail>
<DetailLabel>Created</DetailLabel>
<DetailValue>{format(data.group.createdAt, "PPpp")}</DetailValue>
</Detail>
</DetailGroup>
</UnstableCardContent>
</UnstableCard>
) : (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<p className="text-mineshaft-300">Group data not found</p>
</div>
</div>
<div />
);
};

View File

@@ -1,9 +1,17 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PlusIcon } from "lucide-react";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { DeleteActionModal, IconButton } from "@app/components/v2";
import { DeleteActionModal } from "@app/components/v2";
import {
UnstableButton,
UnstableCard,
UnstableCardAction,
UnstableCardContent,
UnstableCardDescription,
UnstableCardHeader,
UnstableCardTitle
} from "@app/components/v3";
import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import {
useOidcManageGroupMembershipsEnabled,
@@ -76,37 +84,40 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {
};
return (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-medium text-mineshaft-100">Group Members</h3>
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<div className="mb-4 flex items-center justify-center">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
handlePopUpOpen("addGroupMembers", {
groupId,
slug: groupSlug
});
}}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</div>
)}
</OrgPermissionCan>
</div>
<div className="py-4">
<GroupMembersTable
groupId={groupId}
groupSlug={groupSlug}
handlePopUpOpen={handlePopUpOpen}
/>
</div>
<>
<UnstableCard>
<UnstableCardHeader>
<UnstableCardTitle>Group Members</UnstableCardTitle>
<UnstableCardDescription>Manage members of this group</UnstableCardDescription>
<UnstableCardAction>
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<UnstableButton
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("addGroupMembers", {
groupId,
slug: groupSlug
});
}}
size="xs"
variant="outline"
>
<PlusIcon />
Add Member
</UnstableButton>
)}
</OrgPermissionCan>
</UnstableCardAction>
</UnstableCardHeader>
<UnstableCardContent>
<GroupMembersTable
groupId={groupId}
groupSlug={groupSlug}
handlePopUpOpen={handlePopUpOpen}
/>
</UnstableCardContent>
</UnstableCard>
<AddGroupMembersModal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
@@ -122,6 +133,6 @@ export const GroupMembersSection = ({ groupId, groupSlug }: Props) => {
return handleRemoveMemberFromGroup(memberData);
}}
/>
</div>
</>
);
};

View File

@@ -1,38 +1,30 @@
import { useState } from "react";
import {
faArrowDown,
faArrowUp,
faCheckCircle,
faFilter,
faFolder,
faMagnifyingGlass,
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { HardDriveIcon, UserIcon } from "lucide-react";
import { ChevronDownIcon, FilterIcon, HardDriveIcon, PlusIcon, UserIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { OrgPermissionCan } from "@app/components/permissions";
import { Lottie } from "@app/components/v2";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TableSkeleton,
TBody,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
UnstableButton,
UnstableDropdownMenu,
UnstableDropdownMenuCheckboxItem,
UnstableDropdownMenuContent,
UnstableDropdownMenuLabel,
UnstableDropdownMenuTrigger,
UnstableEmpty,
UnstableEmptyContent,
UnstableEmptyDescription,
UnstableEmptyHeader,
UnstableEmptyTitle,
UnstableIconButton,
UnstableInput,
UnstablePagination,
UnstableTable,
UnstableTableBody,
UnstableTableHead,
UnstableTableHeader,
UnstableTableRow
} from "@app/components/v3";
import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import {
getUserTablePreference,
@@ -40,7 +32,6 @@ import {
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useOidcManageGroupMembershipsEnabled } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useListGroupMembers } from "@app/hooks/api/groups/queries";
import {
@@ -85,10 +76,7 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
setUserTablePreference("groupMembersTable", PreferenceKey.PerPage, newPerPage);
};
const { currentOrg } = useOrganization();
const { data: isOidcManageGroupMembershipsEnabled = false } =
useOidcManageGroupMembershipsEnabled(currentOrg.id);
const { isSubOrganization } = useOrganization();
const { data: groupMemberships, isPending } = useListGroupMembers({
id: groupId,
@@ -122,40 +110,42 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
}
];
if (isPending) {
return (
<div className="flex h-40 w-full items-center justify-center">
<Lottie icon="infisical_loading_white" isAutoPlay className="w-16" />
</div>
);
}
const isFiltered = search || memberTypeFilter.length;
return (
<div>
<div className="flex gap-2">
<Input
<>
<div className="mb-4 flex gap-2">
<UnstableInput
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
className="flex-1"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter Members"
variant="plain"
size="sm"
className={twMerge(
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
memberTypeFilter.length > 0 && "border-primary/50 text-primary"
)}
<UnstableDropdownMenu>
<UnstableDropdownMenuTrigger asChild>
<UnstableIconButton
variant={
// eslint-disable-next-line no-nested-ternary
memberTypeFilter.length ? (isSubOrganization ? "sub-org" : "org") : "outline"
}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent
sideOffset={2}
className="max-h-[70vh] thin-scrollbar overflow-y-auto"
align="end"
>
<DropdownMenuLabel>Filter by Member Type</DropdownMenuLabel>
<FilterIcon />
</UnstableIconButton>
</UnstableDropdownMenuTrigger>
<UnstableDropdownMenuContent align="end">
<UnstableDropdownMenuLabel>Filter by Member Type</UnstableDropdownMenuLabel>
{filterOptions.map((option) => (
<DropdownMenuItem
<UnstableDropdownMenuCheckboxItem
key={option.value}
className="flex items-center gap-2"
iconPos="right"
checked={memberTypeFilter.includes(option.value)}
onClick={(e) => {
e.preventDefault();
setMemberTypeFilter((prev) => {
@@ -166,115 +156,97 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
});
setPage(1);
}}
icon={
memberTypeFilter.includes(option.value) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
>
<div className="flex items-center gap-2">
{option.icon}
{option.label}
</div>
</DropdownMenuItem>
{option.label}
</UnstableDropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</UnstableDropdownMenuContent>
</UnstableDropdownMenu>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-5" />
<Th className="w-1/2 pl-2">
<div className="flex items-center">
Name
<IconButton
variant="plain"
className="ml-2"
ariaLabel="sort"
onClick={toggleOrderDirection}
>
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
/>
</IconButton>
</div>
</Th>
<Th>Added On</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="group-user-memberships" />}
{!isPending &&
groupMemberships?.members?.map((userGroupMembership) => {
return userGroupMembership.type === GroupMemberType.USER ? (
<GroupMembershipUserRow
key={`user-group-membership-${userGroupMembership.id}`}
user={userGroupMembership}
handlePopUpOpen={handlePopUpOpen}
/>
) : (
<GroupMembershipIdentityRow
key={`identity-group-membership-${userGroupMembership.id}`}
identity={userGroupMembership}
handlePopUpOpen={handlePopUpOpen}
/>
);
})}
</TBody>
</Table>
{Boolean(totalCount) && (
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
{!isPending && !members.length && (
<EmptyState
title={
groupMemberships?.members.length
? "No members match this search..."
: "This group does not have any members yet"
}
icon={groupMemberships?.members.length ? faSearch : faFolder}
/>
)}
{!groupMemberships?.members.length && (
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<Tooltip
className="text-center"
content={
isOidcManageGroupMembershipsEnabled
? "OIDC Group Membership Mapping Enabled. Assign users to this group in your OIDC provider."
: undefined
}
>
<div className="mb-4 flex items-center justify-center">
<Button
variant="solid"
colorSchema="secondary"
isDisabled={isOidcManageGroupMembershipsEnabled || !isAllowed}
onClick={() => {
handlePopUpOpen("addGroupMembers", {
groupId,
slug: groupSlug
});
}}
>
Add members
</Button>
</div>
</Tooltip>
{members.length ? (
<UnstableTable>
<UnstableTableHeader>
<UnstableTableRow>
<UnstableTableHead className="w-5" />
<UnstableTableHead onClick={toggleOrderDirection} className="w-2/3">
Name
<ChevronDownIcon
className={twMerge(
orderDirection === OrderByDirection.DESC && "rotate-180",
"transition-transform"
)}
/>
</UnstableTableHead>
<UnstableTableHead>Added On</UnstableTableHead>
<UnstableTableHead className="w-5" />
</UnstableTableRow>
</UnstableTableHeader>
<UnstableTableBody>
{members.map((userGroupMembership) =>
userGroupMembership.type === GroupMemberType.USER ? (
<GroupMembershipUserRow
key={`user-group-membership-${userGroupMembership.id}`}
user={userGroupMembership}
handlePopUpOpen={handlePopUpOpen}
/>
) : (
<GroupMembershipIdentityRow
key={`identity-group-membership-${userGroupMembership.id}`}
identity={userGroupMembership}
handlePopUpOpen={handlePopUpOpen}
/>
)
)}
</OrgPermissionCan>
)}
</TableContainer>
</div>
</UnstableTableBody>
</UnstableTable>
) : (
<UnstableEmpty className="border">
<UnstableEmptyHeader>
<UnstableEmptyTitle>
{isFiltered ? "No members match this search" : "This group does not have any members"}
</UnstableEmptyTitle>
<UnstableEmptyDescription>
{isFiltered
? "Adjust search filters to view members."
: "Add users or machine identities to this group."}
</UnstableEmptyDescription>
{!isFiltered && (
<UnstableEmptyContent>
<OrgPermissionCan
I={OrgPermissionGroupActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => (
<UnstableButton
variant={isSubOrganization ? "sub-org" : "org"}
size="xs"
isDisabled={!isAllowed}
onClick={() =>
handlePopUpOpen("addGroupMembers", {
groupId,
slug: groupSlug
})
}
>
<PlusIcon />
Add Member
</UnstableButton>
)}
</OrgPermissionCan>
</UnstableEmptyContent>
)}
</UnstableEmptyHeader>
</UnstableEmpty>
)}
{Boolean(members.length) && (
<UnstablePagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
</>
);
};

View File

@@ -1,18 +1,16 @@
import { faEllipsisV, faUserMinus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { HardDriveIcon } from "lucide-react";
import { format } from "date-fns";
import { HardDriveIcon, MoreHorizontalIcon } from "lucide-react";
import { OrgPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Td,
Tooltip,
Tr
} from "@app/components/v2";
UnstableDropdownMenu,
UnstableDropdownMenuContent,
UnstableDropdownMenuItem,
UnstableDropdownMenuTrigger,
UnstableIconButton,
UnstableTableCell,
UnstableTableRow
} from "@app/components/v3";
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context";
import { GroupMemberType, TGroupMemberMachineIdentity } from "@app/hooks/api/groups/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -34,57 +32,40 @@ export const GroupMembershipIdentityRow = ({
handlePopUpOpen
}: Props) => {
return (
<Tr className="items-center" key={`group-user-${id}`}>
<Td className="pr-0">
<HardDriveIcon size={20} />
</Td>
<Td className="pl-2">
<p>{name}</p>
</Td>
<Td>
<Tooltip content={new Date(joinedGroupAt).toLocaleString()}>
<p className="inline-block">{new Date(joinedGroupAt).toLocaleDateString()}</p>
</Tooltip>
</Td>
<Td>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => {
return (
<div>
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faUserMinus} />}
onClick={() =>
handlePopUpOpen("removeMemberFromGroup", {
memberType: GroupMemberType.MACHINE_IDENTITY,
identityId: id,
name
})
}
isDisabled={!isAllowed}
>
Remove Identity From Group
</DropdownMenuItem>
</div>
);
}}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
<UnstableTableRow key={`group-identity-${id}`}>
<UnstableTableCell>
<HardDriveIcon size={14} className="text-mineshaft-400" />
</UnstableTableCell>
<UnstableTableCell isTruncatable>{name}</UnstableTableCell>
<UnstableTableCell>{format(new Date(joinedGroupAt), "yyyy-MM-dd")}</UnstableTableCell>
<UnstableTableCell>
<UnstableDropdownMenu>
<UnstableDropdownMenuTrigger>
<UnstableIconButton variant="ghost" size="xs">
<MoreHorizontalIcon />
</UnstableIconButton>
</UnstableDropdownMenuTrigger>
<UnstableDropdownMenuContent align="end">
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<UnstableDropdownMenuItem
variant="danger"
onClick={() =>
handlePopUpOpen("removeMemberFromGroup", {
memberType: GroupMemberType.MACHINE_IDENTITY,
identityId: id,
name
})
}
isDisabled={!isAllowed}
>
Remove Identity From Group
</UnstableDropdownMenuItem>
)}
</OrgPermissionCan>
</UnstableDropdownMenuContent>
</UnstableDropdownMenu>
</UnstableTableCell>
</UnstableTableRow>
);
};

View File

@@ -1,18 +1,17 @@
import { faEllipsisV, faUserMinus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { UserIcon } from "lucide-react";
import { format } from "date-fns";
import { MoreHorizontalIcon, UserIcon } from "lucide-react";
import { OrgPermissionCan } from "@app/components/permissions";
import { Tooltip } from "@app/components/v2";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Td,
Tooltip,
Tr
} from "@app/components/v2";
UnstableDropdownMenu,
UnstableDropdownMenuContent,
UnstableDropdownMenuItem,
UnstableDropdownMenuTrigger,
UnstableIconButton,
UnstableTableCell,
UnstableTableRow
} from "@app/components/v3";
import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useOidcManageGroupMembershipsEnabled } from "@app/hooks/api";
import { GroupMemberType, TGroupMemberUser } from "@app/hooks/api/groups/types";
@@ -40,68 +39,50 @@ export const GroupMembershipUserRow = ({
useOidcManageGroupMembershipsEnabled(currentOrg.id);
return (
<Tr className="items-center" key={`group-user-${id}`}>
<Td className="pr-0">
<UserIcon size={20} />
</Td>
<Td className="pl-2">
<p>
{`${firstName ?? "-"} ${lastName ?? ""}`}{" "}
<span className="text-mineshaft-400">({email})</span>
</p>
</Td>
<Td>
<Tooltip content={new Date(joinedGroupAt).toLocaleString()}>
<p className="inline-block">{new Date(joinedGroupAt).toLocaleDateString()}</p>
</Tooltip>
</Td>
<Td>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => {
return (
<Tooltip
content={
isOidcManageGroupMembershipsEnabled
? "OIDC Group Membership Mapping Enabled. Remove user from this group in your OIDC provider."
: undefined
}
position="left"
>
<div>
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faUserMinus} />}
onClick={() =>
handlePopUpOpen("removeMemberFromGroup", {
memberType: GroupMemberType.USER,
username
})
}
isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled}
>
Remove User From Group
</DropdownMenuItem>
</div>
</Tooltip>
);
}}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
<UnstableTableRow key={`group-user-${id}`}>
<UnstableTableCell>
<UserIcon size={14} className="text-mineshaft-400" />
</UnstableTableCell>
<UnstableTableCell isTruncatable>
{`${firstName ?? "-"} ${lastName ?? ""}`} <span className="text-muted">({email})</span>
</UnstableTableCell>
<UnstableTableCell>{format(new Date(joinedGroupAt), "yyyy-MM-dd")}</UnstableTableCell>
<UnstableTableCell>
<UnstableDropdownMenu>
<UnstableDropdownMenuTrigger>
<UnstableIconButton variant="ghost" size="xs">
<MoreHorizontalIcon />
</UnstableIconButton>
</UnstableDropdownMenuTrigger>
<UnstableDropdownMenuContent align="end">
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<Tooltip
content={
isOidcManageGroupMembershipsEnabled
? "OIDC Group Membership Mapping Enabled. Remove user from this group in your OIDC provider."
: undefined
}
position="left"
>
<UnstableDropdownMenuItem
variant="danger"
onClick={() =>
handlePopUpOpen("removeMemberFromGroup", {
memberType: GroupMemberType.USER,
username
})
}
isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled}
>
Remove User From Group
</UnstableDropdownMenuItem>
</Tooltip>
)}
</OrgPermissionCan>
</UnstableDropdownMenuContent>
</UnstableDropdownMenu>
</UnstableTableCell>
</UnstableTableRow>
);
};

View File

@@ -1,19 +1,22 @@
import { faEllipsisV, faFolderMinus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useMemo } from "react";
import { useNavigate } from "@tanstack/react-router";
import { format } from "date-fns";
import { MoreHorizontalIcon } from "lucide-react";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Td,
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context";
import { getProjectTitle } from "@app/helpers/project";
UnstableDropdownMenu,
UnstableDropdownMenuContent,
UnstableDropdownMenuItem,
UnstableDropdownMenuTrigger,
UnstableIconButton,
UnstableTableCell,
UnstableTableRow
} from "@app/components/v3";
import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { getProjectBaseURL, getProjectTitle } from "@app/helpers/project";
import { useGetUserProjects } from "@app/hooks/api";
import { TGroupProject } from "@app/hooks/api/groups/types";
import { ProjectType } from "@app/hooks/api/projects/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -27,55 +30,93 @@ type Props = {
};
export const GroupProjectRow = ({ project, handlePopUpOpen }: Props) => {
const { data: workspaces } = useGetUserProjects();
const navigate = useNavigate();
const { currentOrg } = useOrganization();
const isAccessible = useMemo(() => {
const workspaceIds = new Map();
workspaces?.forEach((workspace) => {
workspaceIds.set(workspace.id, true);
});
return workspaceIds.has(project.id);
}, [workspaces, project]);
return (
<Tr className="items-center" key={`group-project-${project.id}`}>
<Td>
<p>{project.name}</p>
</Td>
<Td>
<p>{getProjectTitle(project.type as ProjectType)}</p>
</Td>
<Td>
<Tooltip content={new Date(project.joinedGroupAt).toLocaleString()}>
<p>{new Date(project.joinedGroupAt).toLocaleDateString()}</p>
</Tooltip>
</Td>
<Td>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => {
return (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faFolderMinus} />}
onClick={() =>
handlePopUpOpen("removeProjectFromGroup", {
projectId: project.id,
projectName: project.name
})
}
isDisabled={!isAllowed}
>
Remove group from project
</DropdownMenuItem>
);
}}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
<UnstableTableRow
key={`group-project-${project.id}`}
onClick={() => {
if (isAccessible) {
navigate({
to: `${getProjectBaseURL(project.type as ProjectType)}/access-management` as const,
params: {
orgId: currentOrg?.id || "",
projectId: project.id
},
search: {
selectedTab: "groups"
}
});
return;
}
createNotification({
text: "Unable to access project",
type: "error"
});
}}
>
<UnstableTableCell className="max-w-0 truncate">{project.name}</UnstableTableCell>
<UnstableTableCell>{getProjectTitle(project.type as ProjectType)}</UnstableTableCell>
<UnstableTableCell>{format(new Date(project.joinedGroupAt), "yyyy-MM-dd")}</UnstableTableCell>
<UnstableTableCell>
<UnstableDropdownMenu>
<UnstableDropdownMenuTrigger>
<UnstableIconButton variant="ghost" size="xs">
<MoreHorizontalIcon />
</UnstableIconButton>
</UnstableDropdownMenuTrigger>
<UnstableDropdownMenuContent align="end">
<UnstableDropdownMenuItem
isDisabled={!isAccessible}
onClick={(e) => {
e.stopPropagation();
navigate({
to: `${getProjectBaseURL(project.type as ProjectType)}/access-management` as const,
params: {
orgId: currentOrg?.id || "",
projectId: project.id
},
search: {
selectedTab: "groups"
}
});
}}
>
Access Project
</UnstableDropdownMenuItem>
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<UnstableDropdownMenuItem
variant="danger"
isDisabled={!isAllowed}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeProjectFromGroup", {
projectId: project.id,
projectName: project.name
});
}}
>
Remove From Project
</UnstableDropdownMenuItem>
)}
</OrgPermissionCan>
</UnstableDropdownMenuContent>
</UnstableDropdownMenu>
</UnstableTableCell>
</UnstableTableRow>
);
};

View File

@@ -1,9 +1,17 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PlusIcon } from "lucide-react";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { DeleteActionModal, IconButton } from "@app/components/v2";
import { DeleteActionModal } from "@app/components/v2";
import {
UnstableButton,
UnstableCard,
UnstableCardAction,
UnstableCardContent,
UnstableCardDescription,
UnstableCardHeader,
UnstableCardTitle
} from "@app/components/v3";
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context";
import { useDeleteGroupFromWorkspace as useRemoveProjectFromGroup } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -39,35 +47,40 @@ export const GroupProjectsSection = ({ groupId, groupSlug }: Props) => {
};
return (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-medium text-mineshaft-100">Group Projects</h3>
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<IconButton
isDisabled={!isAllowed}
ariaLabel="add project"
variant="plain"
className="group relative"
onClick={() => {
handlePopUpOpen("addGroupProjects", {
groupId,
slug: groupSlug
});
}}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
)}
</OrgPermissionCan>
</div>
<div className="py-4">
<GroupProjectsTable
groupId={groupId}
groupSlug={groupSlug}
handlePopUpOpen={handlePopUpOpen}
/>
</div>
<>
<UnstableCard>
<UnstableCardHeader>
<UnstableCardTitle>Projects</UnstableCardTitle>
<UnstableCardDescription>Manage group project memberships</UnstableCardDescription>
<UnstableCardAction>
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<UnstableButton
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("addGroupProjects", {
groupId,
slug: groupSlug
});
}}
size="xs"
variant="outline"
>
<PlusIcon />
Add to Project
</UnstableButton>
)}
</OrgPermissionCan>
</UnstableCardAction>
</UnstableCardHeader>
<UnstableCardContent>
<GroupProjectsTable
groupId={groupId}
groupSlug={groupSlug}
handlePopUpOpen={handlePopUpOpen}
/>
</UnstableCardContent>
</UnstableCard>
<AddGroupProjectModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.removeProjectFromGroup.isOpen}
@@ -85,6 +98,6 @@ export const GroupProjectsSection = ({ groupId, groupSlug }: Props) => {
return handleRemoveProjectFromGroup(projectData.projectId, projectData.projectName);
}}
/>
</div>
</>
);
};

View File

@@ -1,28 +1,23 @@
import {
faArrowDown,
faArrowUp,
faFolder,
faMagnifyingGlass,
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ChevronDownIcon, PlusIcon } from "lucide-react";
import { twMerge } from "tailwind-merge";
import { OrgPermissionCan } from "@app/components/permissions";
import { Lottie } from "@app/components/v2";
import {
Button,
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TableSkeleton,
TBody,
Th,
THead,
Tr
} from "@app/components/v2";
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context";
UnstableButton,
UnstableEmpty,
UnstableEmptyContent,
UnstableEmptyDescription,
UnstableEmptyHeader,
UnstableEmptyTitle,
UnstableInput,
UnstablePagination,
UnstableTable,
UnstableTableBody,
UnstableTableHead,
UnstableTableHeader,
UnstableTableRow
} from "@app/components/v3";
import { useOrganization } from "@app/context";
import {
getUserTablePreference,
PreferenceKey,
@@ -50,6 +45,8 @@ enum GroupProjectsOrderBy {
}
export const GroupProjectsTable = ({ groupId, groupSlug, handlePopUpOpen }: Props) => {
const { isSubOrganization } = useOrganization();
const {
search,
debouncedSearch,
@@ -82,7 +79,6 @@ export const GroupProjectsTable = ({ groupId, groupSlug, handlePopUpOpen }: Prop
});
const totalCount = groupMemberships?.totalCount ?? 0;
const isEmpty = !isPending && totalCount === 0;
const projects = groupMemberships?.projects ?? [];
useResetPageHelper({
@@ -91,93 +87,92 @@ export const GroupProjectsTable = ({ groupId, groupSlug, handlePopUpOpen }: Prop
setPage
});
if (isPending) {
return (
<div className="flex h-40 w-full items-center justify-center">
<Lottie icon="infisical_loading_white" isAutoPlay className="w-16" />
</div>
);
}
return (
<div>
<Input
<>
<UnstableInput
className="mb-4"
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search projects..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-1/3">
<div className="flex items-center">
Name
<IconButton
variant="plain"
className="ml-2"
ariaLabel="sort"
onClick={toggleOrderDirection}
>
<FontAwesomeIcon
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
/>
</IconButton>
</div>
</Th>
<Th>Type</Th>
<Th>Added On</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="group-project-memberships" />}
{!isPending &&
projects.map((project) => {
return (
<GroupProjectRow
key={`group-project-${project.id}`}
project={project}
handlePopUpOpen={handlePopUpOpen}
/>
);
})}
</TBody>
</Table>
{!isEmpty && (
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
{isEmpty && (
<EmptyState
title={
debouncedSearch
? "No projects match this search..."
: "This group is not a part of any projects yet"
}
icon={debouncedSearch ? faSearch : faFolder}
/>
)}
{isEmpty && (
<OrgPermissionCan I={OrgPermissionGroupActions.Edit} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => (
<div className="mb-4 flex items-center justify-center">
<Button
variant="solid"
colorSchema="secondary"
isDisabled={!isAllowed}
onClick={() => {
{projects.length ? (
<UnstableTable>
<UnstableTableHeader>
<UnstableTableRow>
<UnstableTableHead onClick={toggleOrderDirection} className="w-1/3">
Name
<ChevronDownIcon
className={twMerge(
orderDirection === OrderByDirection.DESC && "rotate-180",
"transition-transform"
)}
/>
</UnstableTableHead>
<UnstableTableHead>Type</UnstableTableHead>
<UnstableTableHead>Added On</UnstableTableHead>
<UnstableTableHead className="w-5" />
</UnstableTableRow>
</UnstableTableHeader>
<UnstableTableBody>
{projects.map((project) => (
<GroupProjectRow
key={`group-project-${project.id}`}
project={project}
handlePopUpOpen={handlePopUpOpen}
/>
))}
</UnstableTableBody>
</UnstableTable>
) : (
<UnstableEmpty className="border">
<UnstableEmptyHeader>
<UnstableEmptyTitle>
{debouncedSearch
? "No projects match this search"
: "This group is not a member of any projects"}
</UnstableEmptyTitle>
<UnstableEmptyDescription>
{debouncedSearch
? "Adjust search filters to view project memberships."
: "Add this group to a project."}
</UnstableEmptyDescription>
{!debouncedSearch && (
<UnstableEmptyContent>
<UnstableButton
variant={isSubOrganization ? "sub-org" : "org"}
size="xs"
onClick={() =>
handlePopUpOpen("addGroupProjects", {
groupId,
slug: groupSlug
});
}}
})
}
>
Add projects
</Button>
</div>
<PlusIcon />
Add to Project
</UnstableButton>
</UnstableEmptyContent>
)}
</OrgPermissionCan>
)}
</TableContainer>
</div>
</UnstableEmptyHeader>
</UnstableEmpty>
)}
{Boolean(projects.length) && (
<UnstablePagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
</>
);
};

View File

@@ -201,7 +201,6 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
setPage(1);
}}
>
{option.icon}
{option.label}
</UnstableDropdownMenuCheckboxItem>
))}