mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-10 07:58:15 -05:00
Merge pull request #5072 from Infisical/ENG-4283-2
improvement(frontend): update project group membership page and v3 component additions
This commit is contained in:
@@ -30,10 +30,10 @@ const SCOPE_BADGE: Record<NonNullable<Props["scope"]>, { icon: LucideIcon; class
|
||||
export const PageHeader = ({ title, description, children, className, scope }: Props) => (
|
||||
<div className={twMerge("mb-10 w-full", className)}>
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="mr-4 flex w-full items-center">
|
||||
<div className="mr-4 flex min-w-0 flex-1 items-center">
|
||||
<h1
|
||||
className={twMerge(
|
||||
"text-2xl font-medium text-white underline decoration-2 underline-offset-4",
|
||||
"truncate text-2xl font-medium text-white underline decoration-2 underline-offset-4",
|
||||
scope === "org" && "decoration-org/90",
|
||||
scope === "instance" && "decoration-neutral/90",
|
||||
scope === "namespace" && "decoration-sub-org/90",
|
||||
|
||||
@@ -10,7 +10,7 @@ function UnstableAccordion({ ...props }: React.ComponentProps<typeof AccordionPr
|
||||
return (
|
||||
<AccordionPrimitive.Root
|
||||
data-slot="accordion"
|
||||
className="border border-border bg-container"
|
||||
className="overflow-clip rounded-md border border-border bg-container"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { cn } from "@app/components/v3/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
cn(
|
||||
"inline-flex items-center active:scale-[0.95] justify-center border cursor-pointer whitespace-nowrap",
|
||||
"inline-flex items-center rounded-md active:scale-[0.95] justify-center border cursor-pointer whitespace-nowrap",
|
||||
" text-sm transition-all disabled:pointer-events-none disabled:opacity-75 shrink-0",
|
||||
"[&>svg]:pointer-events-none [&>svg]:shrink-0",
|
||||
"focus-visible:ring-ring outline-0 focus-visible:ring-2 select-none"
|
||||
@@ -36,10 +36,10 @@ const buttonVariants = cva(
|
||||
"border-danger/25 bg-danger/15 text-foreground hover:bg-danger/30 hover:border-danger/30"
|
||||
},
|
||||
size: {
|
||||
xs: "h-7 px-2 rounded-[3px] text-xs [&>svg]:size-3 gap-1.5",
|
||||
sm: "h-8 px-2.5 rounded-[4px] text-sm [&>svg]:size-3 gap-1.5",
|
||||
md: "h-9 px-3 rounded-[5px] text-sm [&>svg]:size-3.5 gap-1.5",
|
||||
lg: "h-10 px-3 rounded-[6px] text-sm [&>svg]:size-3.5 gap-1.5"
|
||||
xs: "h-7 px-2 text-xs rounded-sm [&>svg]:size-3 gap-1.5",
|
||||
sm: "h-8 px-2.5 text-sm [&>svg]:size-3 gap-1.5",
|
||||
md: "h-9 px-3 text-sm [&>svg]:size-3.5 gap-1.5",
|
||||
lg: "h-10 px-3 text-sm [&>svg]:size-3.5 gap-1.5"
|
||||
},
|
||||
isPending: {
|
||||
true: "text-transparent"
|
||||
|
||||
@@ -9,7 +9,7 @@ function UnstableCard({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"flex h-fit flex-col gap-6 rounded-md border border-border bg-card p-5 text-foreground shadow-sm",
|
||||
"flex h-fit flex-col gap-5 rounded-md border border-border bg-card p-5 text-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -11,7 +11,9 @@ function DetailLabel({ className, ...props }: React.ComponentProps<"div">) {
|
||||
}
|
||||
|
||||
function DetailValue({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return <div data-slot="detail-value" className={cn("text-sm", className)} {...props} />;
|
||||
return (
|
||||
<div data-slot="detail-value" className={cn("text-sm break-words", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DetailGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
|
||||
@@ -104,7 +104,7 @@ function UnstableDropdownMenuCheckboxItem({
|
||||
>
|
||||
<span className="pointer-events-none absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
<CheckIcon className="size-3.5" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
|
||||
@@ -5,7 +5,7 @@ function UnstableEmpty({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="empty"
|
||||
className={cn(
|
||||
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 border-dashed border-border bg-container p-6 text-center text-balance md:p-12",
|
||||
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-md border-dashed border-border bg-container p-6 text-center text-balance md:p-12",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { cn } from "@app/components/v3/utils";
|
||||
|
||||
const iconButtonVariants = cva(
|
||||
cn(
|
||||
"inline-flex items-center active:scale-[0.99] justify-center border cursor-pointer whitespace-nowrap rounded-[4px] text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-75 [&_svg]:pointer-events-none shrink-0 [&>svg]:shrink-0",
|
||||
"inline-flex items-center active:scale-[0.99] justify-center border cursor-pointer whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&>svg]:shrink-0",
|
||||
"focus-visible:ring-ring outline-0 focus-visible:ring-2"
|
||||
),
|
||||
{
|
||||
@@ -22,22 +22,22 @@ const iconButtonVariants = cva(
|
||||
outline: "text-foreground hover:bg-foreground/20 border-border hover:border-foreground/50",
|
||||
ghost: "text-foreground hover:bg-foreground/40 border-transparent",
|
||||
project:
|
||||
"border-project/75 bg-project/40 text-foreground hover:bg-project/50 hover:border-kms",
|
||||
org: "border-org/75 bg-org/40 text-foreground hover:bg-org/50 hover:border-org",
|
||||
"border-project/65 bg-project/20 text-foreground hover:bg-project/30 hover:border-kms",
|
||||
org: "border-org/65 bg-org/20 text-foreground hover:bg-org/30 hover:border-org",
|
||||
"sub-org":
|
||||
"border-sub-org/75 bg-sub-org/40 text-foreground hover:bg-sub-org/50 hover:border-namespace",
|
||||
"border-sub-org/65 bg-sub-org/20 text-foreground hover:bg-sub-org/30 hover:border-namespace",
|
||||
success:
|
||||
"border-success/75 bg-success/40 text-foreground hover:bg-success/50 hover:border-success",
|
||||
info: "border-info/75 bg-info/40 text-foreground hover:bg-info/50 hover:border-info",
|
||||
"border-success/65 bg-success/20 text-foreground hover:bg-success/30 hover:border-success",
|
||||
info: "border-info/65 bg-info/20 text-foreground hover:bg-info/30 hover:border-info",
|
||||
warning:
|
||||
"border-warning/75 bg-warning/40 text-foreground hover:bg-warning/50 hover:border-warning",
|
||||
"border-warning/65 bg-warning/20 text-foreground hover:bg-warning/30 hover:border-warning",
|
||||
danger:
|
||||
"border-danger/75 bg-danger/40 text-foreground hover:bg-danger/50 hover:border-danger"
|
||||
"border-danger/65 bg-danger/20 text-foreground hover:bg-danger/30 hover:border-danger"
|
||||
},
|
||||
size: {
|
||||
xs: "h-7 w-7 [&>svg]:size-3.5 [&>svg]:stroke-[1.75]",
|
||||
xs: "h-7 w-7 [&>svg]:size-4 rounded-sm [&>svg]:stroke-[1.75]",
|
||||
sm: "h-8 w-8 [&>svg]:size-4 [&>svg]:stroke-[1.5]",
|
||||
md: "h-9 w-9 [&>svg]:size-6 [&>svg]:stroke-[1.5]",
|
||||
md: "h-9 w-9 [&>svg]:size-4 [&>svg]:stroke-[1.5]",
|
||||
lg: "h-10 w-10 [&>svg]:size-7 [&>svg]:stroke-[1.5]"
|
||||
},
|
||||
isPending: {
|
||||
|
||||
20
frontend/src/components/v3/generic/Input/Input.tsx
Normal file
20
frontend/src/components/v3/generic/Input/Input.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { cn } from "../../utils";
|
||||
|
||||
function UnstableInput({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-9 w-full min-w-0 rounded-md border border-border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"selection:bg-foreground selection:text-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { UnstableInput };
|
||||
1
frontend/src/components/v3/generic/Input/index.ts
Normal file
1
frontend/src/components/v3/generic/Input/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Input";
|
||||
121
frontend/src/components/v3/generic/Pagination/Pagination.tsx
Normal file
121
frontend/src/components/v3/generic/Pagination/Pagination.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { ReactElement } from "react";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronFirstIcon,
|
||||
ChevronLastIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon
|
||||
} from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
UnstableDropdownMenu,
|
||||
UnstableDropdownMenuCheckboxItem,
|
||||
UnstableDropdownMenuContent,
|
||||
UnstableDropdownMenuTrigger
|
||||
} from "../Dropdown";
|
||||
import { UnstableIconButton } from "../IconButton";
|
||||
|
||||
type UnstablePaginationProps = {
|
||||
count: number;
|
||||
page: number;
|
||||
perPage?: number;
|
||||
onChangePage: (pageNumber: number) => void;
|
||||
onChangePerPage: (newRows: number) => void;
|
||||
className?: string;
|
||||
perPageList?: number[];
|
||||
startAdornment?: ReactElement;
|
||||
};
|
||||
|
||||
const UnstablePagination = ({
|
||||
count,
|
||||
page = 1,
|
||||
perPage = 20,
|
||||
onChangePage,
|
||||
onChangePerPage,
|
||||
perPageList = [10, 20, 50, 100],
|
||||
className,
|
||||
startAdornment
|
||||
}: UnstablePaginationProps) => {
|
||||
const prevPageNumber = Math.max(1, page - 1);
|
||||
const canGoPrev = page > 1;
|
||||
|
||||
const upperLimit = Math.ceil(count / perPage);
|
||||
const nextPageNumber = Math.min(upperLimit, page + 1);
|
||||
const canGoNext = page + 1 <= upperLimit;
|
||||
const canGoFirst = page > 1;
|
||||
const canGoLast = page < upperLimit;
|
||||
|
||||
return (
|
||||
<div className={twMerge("flex w-full items-center justify-end px-2 pt-2", className)}>
|
||||
{startAdornment}
|
||||
<div className={twMerge("mr-4 flex items-center space-x-2", startAdornment && "ml-auto")}>
|
||||
<div className="text-xs">
|
||||
{(page - 1) * perPage + 1} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
|
||||
</div>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableIconButton variant="ghost" size="xs">
|
||||
<ChevronDownIcon />
|
||||
</UnstableIconButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
{perPageList.map((perPageOption) => (
|
||||
<UnstableDropdownMenuCheckboxItem
|
||||
checked={perPageOption === perPage}
|
||||
key={`pagination-per-page-options-${perPageOption}`}
|
||||
onClick={() => {
|
||||
const totalPages = Math.ceil(count / perPageOption);
|
||||
|
||||
if (page > totalPages) {
|
||||
onChangePage(totalPages);
|
||||
}
|
||||
|
||||
onChangePerPage(perPageOption);
|
||||
}}
|
||||
>
|
||||
{perPageOption} rows per page
|
||||
</UnstableDropdownMenuCheckboxItem>
|
||||
))}
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<UnstableIconButton
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => onChangePage(1)}
|
||||
isDisabled={!canGoFirst}
|
||||
>
|
||||
<ChevronFirstIcon />
|
||||
</UnstableIconButton>
|
||||
<UnstableIconButton
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => onChangePage(prevPageNumber)}
|
||||
isDisabled={!canGoPrev}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
</UnstableIconButton>
|
||||
<UnstableIconButton
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => onChangePage(nextPageNumber)}
|
||||
isDisabled={!canGoNext}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
</UnstableIconButton>
|
||||
<UnstableIconButton
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={() => onChangePage(upperLimit)}
|
||||
isDisabled={!canGoLast}
|
||||
>
|
||||
<ChevronLastIcon />
|
||||
</UnstableIconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { UnstablePagination, type UnstablePaginationProps };
|
||||
1
frontend/src/components/v3/generic/Pagination/index.ts
Normal file
1
frontend/src/components/v3/generic/Pagination/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Pagination";
|
||||
@@ -8,7 +8,7 @@ function UnstableTable({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto border border-border bg-container"
|
||||
className="relative w-full overflow-x-auto rounded-md border border-border bg-container"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
@@ -64,6 +64,7 @@ function UnstableTableHead({ className, ...props }: React.ComponentProps<"th">)
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"h-[30px] border-x-0 border-t-0 border-b border-border px-3 text-left align-middle text-xs whitespace-nowrap text-accent [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
"has-[>svg]:cursor-pointer [&>svg]:ml-1 [&>svg]:inline-block [&>svg]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -71,12 +72,17 @@ function UnstableTableHead({ className, ...props }: React.ComponentProps<"th">)
|
||||
);
|
||||
}
|
||||
|
||||
function UnstableTableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
function UnstableTableCell({
|
||||
className,
|
||||
isTruncatable,
|
||||
...props
|
||||
}: React.ComponentProps<"td"> & { isTruncatable?: boolean }) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"h-[40px] px-3 align-middle whitespace-nowrap text-mineshaft-200 [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
isTruncatable && "max-w-0 truncate",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -8,6 +8,8 @@ export * from "./Detail";
|
||||
export * from "./Dropdown";
|
||||
export * from "./Empty";
|
||||
export * from "./IconButton";
|
||||
export * from "./Input";
|
||||
export * from "./PageLoader";
|
||||
export * from "./Pagination";
|
||||
export * from "./Separator";
|
||||
export * from "./Table";
|
||||
|
||||
@@ -263,21 +263,23 @@ const GroupRolesForm = ({ projectRoles, roles, groupId, onClose }: FormProps) =>
|
||||
setSearchRoles("");
|
||||
};
|
||||
|
||||
const filteredRoles =
|
||||
projectRoles?.filter(
|
||||
({ name, slug }) =>
|
||||
name.toLowerCase().includes(searchRoles.toLowerCase()) ||
|
||||
slug.toLowerCase().includes(searchRoles.toLowerCase())
|
||||
) ?? [];
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleRoleUpdate)} id="role-update-form">
|
||||
<div className="max-h-80 thin-scrollbar space-y-4 overflow-y-auto">
|
||||
{projectRoles
|
||||
?.filter(
|
||||
({ name, slug }) =>
|
||||
name.toLowerCase().includes(searchRoles.toLowerCase()) ||
|
||||
slug.toLowerCase().includes(searchRoles.toLowerCase())
|
||||
)
|
||||
?.map(({ id, name, slug }) => {
|
||||
{filteredRoles.length > 0 ? (
|
||||
filteredRoles.map(({ id, name, slug }) => {
|
||||
const userProjectRoleDetails = userRolesGroupBySlug?.[slug]?.[0];
|
||||
|
||||
return (
|
||||
<div key={id} className="flex items-center space-x-4">
|
||||
<div className="grow">
|
||||
<div className="flex-1 truncate">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={Boolean(userProjectRoleDetails?.id)}
|
||||
@@ -332,7 +334,10 @@ const GroupRolesForm = ({ projectRoles, roles, groupId, onClose }: FormProps) =>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
})
|
||||
) : (
|
||||
<span className="text-sm text-mineshaft-400">No roles match search...</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 flex items-center space-x-2 border-t border-t-gray-700 pt-3">
|
||||
<div>
|
||||
@@ -385,7 +390,9 @@ export const GroupRoles = ({
|
||||
return (
|
||||
<Tag key={id} className="capitalize">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>{formatProjectRoleName(role, customRoleName)}</div>
|
||||
<div className="max-w-32 truncate">
|
||||
{formatProjectRoleName(role, customRoleName)}
|
||||
</div>
|
||||
{isTemporary && (
|
||||
<div>
|
||||
<Tooltip content={isExpired ? "Expired Temporary Access" : "Temporary Access"}>
|
||||
|
||||
@@ -57,7 +57,7 @@ export const GroupsSection = () => {
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Project Groups</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups#user-groups" />
|
||||
|
||||
@@ -195,7 +195,7 @@ export const IdentityTab = withProjectPermission(
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Project Machine Identities</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
|
||||
|
||||
@@ -47,7 +47,7 @@ export const MembersSection = () => {
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Project Users</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" />
|
||||
|
||||
@@ -162,7 +162,7 @@ export const ProjectRoleList = () => {
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Project Roles</p>
|
||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/access-controls/role-based-access-controls#project-level-access-controls" />
|
||||
@@ -193,7 +193,7 @@ export const ProjectRoleList = () => {
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>
|
||||
<Th className="w-1/3">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
@@ -206,7 +206,7 @@ export const ProjectRoleList = () => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<Th className="w-1/3">
|
||||
<div className="flex items-center">
|
||||
Slug
|
||||
<IconButton
|
||||
@@ -257,8 +257,8 @@ export const ProjectRoleList = () => {
|
||||
})
|
||||
}
|
||||
>
|
||||
<Td>{name}</Td>
|
||||
<Td>{slug}</Td>
|
||||
<Td className="max-w-0 truncate">{name}</Td>
|
||||
<Td className="max-w-0 truncate">{slug}</Td>
|
||||
<Td>
|
||||
<Badge variant="ghost">
|
||||
{isCustomProjectRole(slug) ? (
|
||||
|
||||
@@ -41,7 +41,7 @@ export const ServiceTokenSection = withProjectPermission(
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="mb-2 flex flex-col items-start gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Service Tokens</p>
|
||||
|
||||
@@ -1,12 +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, useParams } from "@tanstack/react-router";
|
||||
import { formatRelative } from "date-fns";
|
||||
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { ChevronLeftIcon, EllipsisIcon } from "lucide-react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { EmptyState, PageHeader, Spinner } from "@app/components/v2";
|
||||
import { DeleteActionModal, EmptyState, PageHeader, Spinner } from "@app/components/v2";
|
||||
import {
|
||||
UnstableButton,
|
||||
UnstableDropdownMenu,
|
||||
UnstableDropdownMenuContent,
|
||||
UnstableDropdownMenuItem,
|
||||
UnstableDropdownMenuTrigger
|
||||
} from "@app/components/v3";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
@@ -14,6 +20,8 @@ import {
|
||||
useProject
|
||||
} from "@app/context";
|
||||
import { getProjectBaseURL } from "@app/helpers/project";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteGroupFromWorkspace } from "@app/hooks/api";
|
||||
import { useGetWorkspaceGroupMembershipDetails } from "@app/hooks/api/projects/queries";
|
||||
import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
|
||||
@@ -34,6 +42,37 @@ const Page = () => {
|
||||
groupId
|
||||
);
|
||||
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteGroupFromWorkspace();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { handlePopUpToggle, popUp, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
"deleteGroup"
|
||||
] as const);
|
||||
|
||||
const onRemoveGroupSubmit = async () => {
|
||||
await deleteMutateAsync({
|
||||
groupId: groupMembership!.group.id,
|
||||
projectId: currentProject.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully removed group from project",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: `${getProjectBaseURL(currentProject.type)}/access-management`,
|
||||
params: {
|
||||
projectId: currentProject.id
|
||||
},
|
||||
search: {
|
||||
selectedTab: "groups"
|
||||
}
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteGroup");
|
||||
};
|
||||
|
||||
if (isPending)
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center p-24">
|
||||
@@ -42,9 +81,9 @@ const Page = () => {
|
||||
);
|
||||
|
||||
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">
|
||||
{groupMembership ? (
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<>
|
||||
<Link
|
||||
to={`${getProjectBaseURL(currentProject.type)}/access-management`}
|
||||
params={{
|
||||
@@ -54,26 +93,71 @@ const Page = () => {
|
||||
search={{
|
||||
selectedTab: ProjectAccessControlTabs.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} />
|
||||
Project Groups
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope={currentProject.type}
|
||||
title={groupMembership.group.name}
|
||||
description={`Group joined on ${formatRelative(new Date(groupMembership.createdAt || ""), new Date())}`}
|
||||
/>
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<GroupDetailsSection groupMembership={groupMembership} />
|
||||
</div>
|
||||
description="Configure and manage project access control"
|
||||
>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableButton variant="outline">
|
||||
Options
|
||||
<EllipsisIcon />
|
||||
</UnstableButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<UnstableDropdownMenuItem
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(groupMembership.group.id);
|
||||
createNotification({
|
||||
text: "Group ID copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
>
|
||||
Copy Group ID
|
||||
</UnstableDropdownMenuItem>
|
||||
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Groups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
variant="danger"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handlePopUpOpen("deleteGroup")}
|
||||
>
|
||||
Remove From Project
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
</PageHeader>
|
||||
<div className="flex flex-col gap-5 lg:flex-row">
|
||||
<GroupDetailsSection groupMembership={groupMembership} />
|
||||
<GroupMembersSection groupMembership={groupMembership} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<EmptyState title="Error: Unable to find the group." className="py-12" />
|
||||
)}
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteGroup.isOpen}
|
||||
title={`Are you sure you want to remove the group ${
|
||||
groupMembership?.group.name
|
||||
} from the project?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Remove"
|
||||
onDeleteApproved={onRemoveGroupSubmit}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { format } from "date-fns";
|
||||
import { CheckIcon, ClipboardListIcon } from "lucide-react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconButton
|
||||
} from "@app/components/v2";
|
||||
import { CopyButton } from "@app/components/v2/CopyButton";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
|
||||
import { getProjectBaseURL } from "@app/helpers/project";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteGroupFromWorkspace } from "@app/hooks/api";
|
||||
Detail,
|
||||
DetailGroup,
|
||||
DetailLabel,
|
||||
DetailValue,
|
||||
UnstableCard,
|
||||
UnstableCardContent,
|
||||
UnstableCardDescription,
|
||||
UnstableCardHeader,
|
||||
UnstableCardTitle,
|
||||
UnstableIconButton
|
||||
} from "@app/components/v3";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { TGroupMembership } from "@app/hooks/api/groups/types";
|
||||
import { GroupRoles } from "@app/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles";
|
||||
|
||||
@@ -26,123 +25,68 @@ type Props = {
|
||||
};
|
||||
|
||||
export const GroupDetailsSection = ({ groupMembership }: Props) => {
|
||||
const { handlePopUpToggle, popUp, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||
"deleteGroup"
|
||||
] as const);
|
||||
const { group } = groupMembership;
|
||||
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteGroupFromWorkspace();
|
||||
const { currentProject } = useProject();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onRemoveGroupSubmit = async () => {
|
||||
await deleteMutateAsync({
|
||||
groupId: groupMembership.group.id,
|
||||
projectId: currentProject.id
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully removed group from project",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: `${getProjectBaseURL(currentProject.type)}/access-management`,
|
||||
params: {
|
||||
projectId: currentProject.id
|
||||
},
|
||||
search: {
|
||||
selectedTab: "groups"
|
||||
}
|
||||
});
|
||||
|
||||
handlePopUpClose("deleteGroup");
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-unused-vars
|
||||
const [_, isCopyingId, setCopyTextId] = useTimedReset<string>({
|
||||
initialState: "Copy ID to clipboard"
|
||||
});
|
||||
|
||||
return (
|
||||
<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>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton ariaLabel="Options" colorSchema="secondary" className="w-6" variant="plain">
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent sideOffset={2} align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Groups}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
onClick={() => handlePopUpOpen("deleteGroup")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Remove Group From Project
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</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">{groupMembership.group.id}</p>
|
||||
<CopyButton
|
||||
value={groupMembership.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">{groupMembership.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">{groupMembership.group.slug}</p>
|
||||
<CopyButton value={groupMembership.group.slug} name="Slug" size="xs" variant="plain" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Project Role</p>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Groups}>
|
||||
{(isAllowed) => (
|
||||
<GroupRoles
|
||||
className="mt-1"
|
||||
popperContentProps={{ side: "right" }}
|
||||
roles={groupMembership.roles}
|
||||
groupId={groupMembership.group.id}
|
||||
disableEdit={!isAllowed}
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Assigned to Project</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{format(groupMembership.createdAt, "M/d/yyyy")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteGroup.isOpen}
|
||||
title={`Are you sure you want to remove the group ${
|
||||
groupMembership.group.name
|
||||
} from the project?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
|
||||
deleteKey="confirm"
|
||||
buttonText="Remove"
|
||||
onDeleteApproved={onRemoveGroupSubmit}
|
||||
/>
|
||||
</div>
|
||||
<UnstableCard className="w-full lg:max-w-[24rem]">
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Details</UnstableCardTitle>
|
||||
<UnstableCardDescription>Group details</UnstableCardDescription>
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
<DetailGroup>
|
||||
<Detail>
|
||||
<DetailLabel>Name</DetailLabel>
|
||||
<DetailValue>{group.name}</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>ID</DetailLabel>
|
||||
<DetailValue className="flex items-center gap-x-1">
|
||||
{group.id}
|
||||
<Tooltip content="Copy group ID to clipboard">
|
||||
<UnstableIconButton
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(group.id);
|
||||
setCopyTextId("Copied");
|
||||
}}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
>
|
||||
{/* TODO(scott): color this should be a button variant and create re-usable copy button */}
|
||||
{isCopyingId ? <CheckIcon /> : <ClipboardListIcon className="text-label" />}
|
||||
</UnstableIconButton>
|
||||
</Tooltip>
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>Project Role</DetailLabel>
|
||||
<DetailValue>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Groups}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<GroupRoles
|
||||
popperContentProps={{ side: "right" }}
|
||||
roles={groupMembership.roles}
|
||||
groupId={groupMembership.group.id}
|
||||
disableEdit={!isAllowed}
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>Joined project</DetailLabel>
|
||||
<DetailValue>{format(groupMembership.createdAt, "PPpp")}</DetailValue>
|
||||
</Detail>
|
||||
</DetailGroup>
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import {
|
||||
UnstableCard,
|
||||
UnstableCardContent,
|
||||
UnstableCardDescription,
|
||||
UnstableCardHeader,
|
||||
UnstableCardTitle
|
||||
} from "@app/components/v3";
|
||||
import { TGroupMembership } from "@app/hooks/api/groups/types";
|
||||
|
||||
import { GroupMembersTable } from "./GroupMembersTable";
|
||||
@@ -8,13 +15,14 @@ type Props = {
|
||||
|
||||
export const GroupMembersSection = ({ groupMembership }: 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>
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<UnstableCard className="flex-1">
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Group Members</UnstableCardTitle>
|
||||
<UnstableCardDescription>View members of this group</UnstableCardDescription>
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
<GroupMembersTable groupMembership={groupMembership} />
|
||||
</div>
|
||||
</div>
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,38 +1,29 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCheckCircle,
|
||||
faFilter,
|
||||
faFolder,
|
||||
faMagnifyingGlass,
|
||||
faSearch
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { HardDriveIcon, UserIcon } from "lucide-react";
|
||||
import { ChevronDownIcon, FilterIcon, HardDriveIcon, UserIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ConfirmActionModal, Lottie } from "@app/components/v2";
|
||||
import {
|
||||
ConfirmActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
UnstableDropdownMenu,
|
||||
UnstableDropdownMenuCheckboxItem,
|
||||
UnstableDropdownMenuContent,
|
||||
UnstableDropdownMenuLabel,
|
||||
UnstableDropdownMenuTrigger,
|
||||
UnstableEmpty,
|
||||
UnstableEmptyDescription,
|
||||
UnstableEmptyHeader,
|
||||
UnstableEmptyTitle,
|
||||
UnstableIconButton,
|
||||
UnstableInput,
|
||||
UnstablePagination,
|
||||
UnstableTable,
|
||||
UnstableTableBody,
|
||||
UnstableTableHead,
|
||||
UnstableTableHeader,
|
||||
UnstableTableRow
|
||||
} from "@app/components/v3";
|
||||
import { useOrganization, useProject } from "@app/context";
|
||||
import { getProjectHomePage } from "@app/helpers/project";
|
||||
import {
|
||||
@@ -114,6 +105,8 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
|
||||
memberTypeFilter: memberTypeFilter.length > 0 ? memberTypeFilter : undefined
|
||||
});
|
||||
|
||||
const isFiltered = Boolean(search) || memberTypeFilter.length > 0;
|
||||
|
||||
const { members = [], totalCount = 0 } = groupMemberships ?? {};
|
||||
|
||||
useResetPageHelper({
|
||||
@@ -167,40 +160,36 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
|
||||
}
|
||||
];
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
// scott: todo proper loader
|
||||
<div className="flex h-40 w-full items-center justify-center">
|
||||
<Lottie icon="infisical_loading_white" isAutoPlay className="w-16" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
<>
|
||||
<div className="mb-5 flex gap-2.5">
|
||||
{/* TODO(scott): add input group with icon once component added */}
|
||||
<UnstableInput
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search members..."
|
||||
placeholder="Search group members..."
|
||||
/>
|
||||
<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"
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableIconButton variant={memberTypeFilter.length ? "project" : "outline"}>
|
||||
<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) => {
|
||||
@@ -211,95 +200,85 @@ export const GroupMembersTable = ({ groupMembership }: 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.icon}
|
||||
{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}
|
||||
onAssumePrivileges={(userId) =>
|
||||
handlePopUpOpen("assumePrivileges", {
|
||||
actorId: userId,
|
||||
actorType: ActorType.USER
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<GroupMembershipIdentityRow
|
||||
key={`identity-group-membership-${userGroupMembership.id}`}
|
||||
identity={userGroupMembership}
|
||||
onAssumePrivileges={(identityId) =>
|
||||
handlePopUpOpen("assumePrivileges", {
|
||||
actorId: identityId,
|
||||
actorType: ActorType.IDENTITY
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</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}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
{members.length > 0 ? (
|
||||
<UnstableTable>
|
||||
<UnstableTableHeader>
|
||||
<UnstableTableRow>
|
||||
<UnstableTableHead className="w-5" />
|
||||
<UnstableTableHead className="w-1/2" onClick={toggleOrderDirection}>
|
||||
Name
|
||||
<ChevronDownIcon
|
||||
className={twMerge(
|
||||
orderDirection === OrderByDirection.DESC && "rotate-180",
|
||||
"transition-transform"
|
||||
)}
|
||||
/>
|
||||
</UnstableTableHead>
|
||||
<UnstableTableHead>Joined Group</UnstableTableHead>
|
||||
<UnstableTableHead className="w-5" />
|
||||
</UnstableTableRow>
|
||||
</UnstableTableHeader>
|
||||
<UnstableTableBody>
|
||||
{groupMemberships?.members?.map((userGroupMembership) => {
|
||||
return userGroupMembership.type === GroupMemberType.USER ? (
|
||||
<GroupMembershipUserRow
|
||||
key={`user-group-membership-${userGroupMembership.id}`}
|
||||
user={userGroupMembership}
|
||||
onAssumePrivileges={(userId) =>
|
||||
handlePopUpOpen("assumePrivileges", {
|
||||
actorId: userId,
|
||||
actorType: ActorType.USER
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<GroupMembershipIdentityRow
|
||||
key={`identity-group-membership-${userGroupMembership.id}`}
|
||||
identity={userGroupMembership}
|
||||
onAssumePrivileges={(identityId) =>
|
||||
handlePopUpOpen("assumePrivileges", {
|
||||
actorId: identityId,
|
||||
actorType: ActorType.IDENTITY
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</UnstableTableBody>
|
||||
</UnstableTable>
|
||||
) : (
|
||||
<UnstableEmpty className="border">
|
||||
<UnstableEmptyHeader>
|
||||
<UnstableEmptyTitle>
|
||||
{isFiltered
|
||||
? "No group members match this search"
|
||||
: "This group doesn't have any members"}
|
||||
</UnstableEmptyTitle>
|
||||
<UnstableEmptyDescription>
|
||||
{isFiltered
|
||||
? "Adjust search filters to view group members."
|
||||
: "Assign members from organization access control or contact an organization admin."}
|
||||
</UnstableEmptyDescription>
|
||||
</UnstableEmptyHeader>
|
||||
</UnstableEmpty>
|
||||
)}
|
||||
{Boolean(totalCount) && (
|
||||
<UnstablePagination
|
||||
count={totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
<ConfirmActionModal
|
||||
isOpen={popUp.assumePrivileges.isOpen}
|
||||
confirmKey="assume"
|
||||
@@ -309,6 +288,6 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
|
||||
onConfirmed={handleAssumePrivileges}
|
||||
buttonText="Confirm"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { faEllipsisV, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { HardDriveIcon } from "lucide-react";
|
||||
import { EllipsisIcon, HardDriveIcon } from "lucide-react";
|
||||
|
||||
import { ProjectPermissionCan } 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 { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/context";
|
||||
import { TGroupMemberMachineIdentity } from "@app/hooks/api/groups/types";
|
||||
|
||||
@@ -30,52 +27,38 @@ export const GroupMembershipIdentityRow = ({
|
||||
onAssumePrivileges
|
||||
}: Props) => {
|
||||
return (
|
||||
<Tr className="items-center" key={`group-identity-${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">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionIdentityActions.AssumePrivileges}
|
||||
a={ProjectPermissionSub.Identity}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faUser} />}
|
||||
onClick={() => onAssumePrivileges(id)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Assume Privileges
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
<UnstableTableRow>
|
||||
<UnstableTableCell>
|
||||
<HardDriveIcon size={14} className="text-mineshaft-400" />
|
||||
</UnstableTableCell>
|
||||
<UnstableTableCell isTruncatable>{name}</UnstableTableCell>
|
||||
<UnstableTableCell>{new Date(joinedGroupAt).toLocaleDateString()}</UnstableTableCell>
|
||||
<UnstableTableCell>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableIconButton variant="ghost" size="xs">
|
||||
<EllipsisIcon />
|
||||
</UnstableIconButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionIdentityActions.AssumePrivileges}
|
||||
a={ProjectPermissionSub.Identity}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<UnstableDropdownMenuItem
|
||||
onClick={() => onAssumePrivileges(id)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Assume Privileges
|
||||
</UnstableDropdownMenuItem>
|
||||
);
|
||||
}}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
</UnstableTableCell>
|
||||
</UnstableTableRow>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { faEllipsisV, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { UserIcon } from "lucide-react";
|
||||
import { EllipsisIcon, UserIcon } from "lucide-react";
|
||||
|
||||
import { ProjectPermissionCan } 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 { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/context";
|
||||
import { TGroupMemberUser } from "@app/hooks/api/groups/types";
|
||||
|
||||
@@ -30,55 +27,41 @@ export const GroupMembershipUserRow = ({
|
||||
onAssumePrivileges
|
||||
}: Props) => {
|
||||
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">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionMemberActions.AssumePrivileges}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon icon={faUser} />}
|
||||
onClick={() => onAssumePrivileges(id)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Assume Privileges
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
<UnstableTableRow>
|
||||
<UnstableTableCell>
|
||||
<UserIcon size={14} className="text-mineshaft-400" />
|
||||
</UnstableTableCell>
|
||||
<UnstableTableCell isTruncatable>
|
||||
{`${firstName ?? "-"} ${lastName ?? ""}`}{" "}
|
||||
<span className="text-mineshaft-400">({email})</span>
|
||||
</UnstableTableCell>
|
||||
<UnstableTableCell>{new Date(joinedGroupAt).toLocaleDateString()}</UnstableTableCell>
|
||||
<UnstableTableCell>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableIconButton size="xs" variant="ghost">
|
||||
<EllipsisIcon />
|
||||
</UnstableIconButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionMemberActions.AssumePrivileges}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<UnstableDropdownMenuItem
|
||||
onClick={() => onAssumePrivileges(id)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Assume Privileges
|
||||
</UnstableDropdownMenuItem>
|
||||
);
|
||||
}}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
</UnstableTableCell>
|
||||
</UnstableTableRow>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -169,10 +169,7 @@ export const IdentityRoleDetailsSection = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<UnstableTableRow
|
||||
className="group h-10"
|
||||
key={`user-project-identity-${roleDetails?.id}`}
|
||||
>
|
||||
<UnstableTableRow key={`user-project-identity-${roleDetails?.id}`}>
|
||||
<UnstableTableCell className="max-w-0 truncate">
|
||||
{roleDetails.role === "custom"
|
||||
? roleDetails.customRoleName
|
||||
@@ -235,7 +232,7 @@ export const IdentityRoleDetailsSection = ({
|
||||
<UnstableEmpty className="border">
|
||||
<UnstableEmptyHeader>
|
||||
<UnstableEmptyTitle>
|
||||
This machine identity doesn t have any roles
|
||||
This machine identity doesn't have any roles
|
||||
</UnstableEmptyTitle>
|
||||
<UnstableEmptyDescription>
|
||||
Give this machine identity one or more roles
|
||||
|
||||
@@ -134,7 +134,7 @@ export const ProjectIdentityDetailsSection = ({
|
||||
</UnstableButtonGroup>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted">No metadata</span>
|
||||
<span className="text-muted">—</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
@@ -150,7 +150,7 @@ export const ProjectIdentityDetailsSection = ({
|
||||
{membership.lastLoginAuthMethod ? (
|
||||
identityAuthToNameMap[membership.lastLoginAuthMethod]
|
||||
) : (
|
||||
<span className="text-muted">N/A</span>
|
||||
<span className="text-muted">—</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
@@ -160,7 +160,7 @@ export const ProjectIdentityDetailsSection = ({
|
||||
{membership.lastLoginTime ? (
|
||||
format(membership.lastLoginTime, "PPpp")
|
||||
) : (
|
||||
<span className="text-muted">N/A</span>
|
||||
<span className="text-muted">—</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
|
||||
@@ -235,7 +235,7 @@ export const MemberRoleDetailsSection = ({
|
||||
) : (
|
||||
<UnstableEmpty className="border">
|
||||
<UnstableEmptyHeader>
|
||||
<UnstableEmptyTitle>This user doesn t have any roles</UnstableEmptyTitle>
|
||||
<UnstableEmptyTitle>This user doesn't have any roles</UnstableEmptyTitle>
|
||||
<UnstableEmptyDescription>
|
||||
Give this user one or more roles
|
||||
</UnstableEmptyDescription>
|
||||
|
||||
@@ -48,7 +48,7 @@ export const ProjectMemberDetailsSection = ({ membership }: Props) => {
|
||||
<DetailGroup>
|
||||
<Detail>
|
||||
<DetailLabel>Name</DetailLabel>
|
||||
<DetailValue>{name || <span className="text-muted">Not set</span>}</DetailValue>
|
||||
<DetailValue>{name || <span className="text-muted">—</span>}</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>ID</DetailLabel>
|
||||
@@ -91,7 +91,7 @@ export const ProjectMemberDetailsSection = ({ membership }: Props) => {
|
||||
{username !== email && (
|
||||
<Detail>
|
||||
<DetailLabel>Username</DetailLabel>
|
||||
<DetailValue>{username || <span className="text-muted">Not set</span>}</DetailValue>
|
||||
<DetailValue>{username || <span className="text-muted">—</span>}</DetailValue>
|
||||
</Detail>
|
||||
)}
|
||||
<Detail>
|
||||
|
||||
Reference in New Issue
Block a user