mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 15:38:03 -05:00
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:
@@ -14,6 +14,7 @@ export type TGroup = {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
role: string;
|
||||
roleId: string;
|
||||
};
|
||||
|
||||
export type TGroupMembership = {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -201,7 +201,6 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
{option.icon}
|
||||
{option.label}
|
||||
</UnstableDropdownMenuCheckboxItem>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user