feature: project group details page

This commit is contained in:
Scott Wilson
2025-06-05 19:17:46 -07:00
parent ce170a6a47
commit 4c22024d13
27 changed files with 1176 additions and 45 deletions
@@ -152,7 +152,7 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props
</Th>
<Th>Email</Th>
<Th>Added On</Th>
<Th />
<Th className="w-5" />
</Tr>
</THead>
<TBody>
@@ -1,8 +1,17 @@
import { faUserMinus } from "@fortawesome/free-solid-svg-icons";
import { faEllipsisV, faUserMinus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Td,
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useOidcManageGroupMembershipsEnabled } from "@app/hooks/api";
import { TGroupUser } from "@app/hooks/api/groups/types";
@@ -38,30 +47,47 @@ export const GroupMembershipRow = ({
<p>{new Date(joinedGroupAt).toLocaleDateString()}</p>
</Tooltip>
</Td>
<Td className="justify-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."
: "Remove user from group"
}
<Td>
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<IconButton
isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled}
ariaLabel="Remove user from group"
onClick={() => handlePopUpOpen("removeMemberFromGroup", { username })}
variant="plain"
colorSchema="danger"
>
<FontAwesomeIcon icon={faUserMinus} />
</IconButton>
</Tooltip>
);
}}
</OrgPermissionCan>
<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", { username })}
isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled}
>
Remove User From Group
</DropdownMenuItem>
</div>
</Tooltip>
);
}}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
);
@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
import { faCheck, faClock, faEdit, faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { PopperContentProps } from "@radix-ui/react-popper";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
@@ -196,12 +197,20 @@ type TForm = z.infer<typeof formSchema>;
export type TMemberRolesProp = {
disableEdit?: boolean;
groupId: string;
className?: string;
roles: TGroupMembership["roles"];
popperContentProps?: PopperContentProps;
};
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMemberRolesProp) => {
export const GroupRoles = ({
roles = [],
disableEdit = false,
groupId,
className,
popperContentProps
}: TMemberRolesProp) => {
const { currentWorkspace } = useWorkspace();
const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const);
const [searchRoles, setSearchRoles] = useState("");
@@ -209,7 +218,6 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
const {
handleSubmit,
control,
reset,
setValue,
formState: { isSubmitting, isDirty }
} = useForm<TForm>({
@@ -261,7 +269,7 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
};
return (
<div className="flex items-center space-x-2">
<div className={twMerge("flex items-center space-x-1", className)}>
{roles
.slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
.map(({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => {
@@ -325,17 +333,21 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
open={popUp.editRole.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("editRole", isOpen);
reset();
}}
>
{!disableEdit && (
<PopoverTrigger>
<PopoverTrigger onClick={(e) => e.stopPropagation()}>
<IconButton size="sm" variant="plain" ariaLabel="update">
<FontAwesomeIcon icon={faEdit} />
</IconButton>
</PopoverTrigger>
)}
<PopoverContent hideCloseBtn className="pt-4">
<PopoverContent
{...popperContentProps}
onClick={(e) => e.stopPropagation()}
hideCloseBtn
className="pt-4"
>
{isRolesLoading ? (
<div className="flex h-8 w-full items-center justify-center">
<Spinner />
@@ -8,6 +8,7 @@ import {
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { format } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions";
@@ -55,6 +56,7 @@ enum GroupsOrderBy {
export const GroupTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const navigate = useNavigate();
const {
search,
@@ -143,7 +145,32 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
.slice(offset, perPage * page)
.map(({ group: { id, name }, roles, createdAt }) => {
return (
<Tr className="group h-10" key={`st-v3-${id}`}>
<Tr
className="group h-10 w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`st-v3-${id}`}
role="button"
tabIndex={0}
onKeyDown={(evt) => {
if (evt.key === "Enter") {
navigate({
to: `/${currentWorkspace.type}/$projectId/groups/$groupId` as const,
params: {
projectId: currentWorkspace.id,
groupId: id
}
});
}
}}
onClick={() =>
navigate({
to: `/${currentWorkspace.type}/$projectId/groups/$groupId` as const,
params: {
projectId: currentWorkspace.id,
groupId: id
}
})
}
>
<Td>{name}</Td>
<Td>
<ProjectPermissionCan
@@ -165,7 +192,8 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content="Remove">
<IconButton
onClick={() => {
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteGroup", {
id,
name
@@ -0,0 +1,70 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useParams } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { EmptyState, PageHeader, Spinner } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useGetWorkspaceGroupMembershipDetails } from "@app/hooks/api/workspace/queries";
import { GroupDetailsSection } from "./components/GroupDetailsSection";
import { GroupMembersSection } from "./components/GroupMembersSection";
const Page = () => {
const groupId = useParams({
strict: false,
select: (el) => el.groupId as string
});
const { currentWorkspace } = useWorkspace();
const { data: groupMembership, isPending } = useGetWorkspaceGroupMembershipDetails(
currentWorkspace.id,
groupId
);
if (isPending)
return (
<div className="flex w-full items-center justify-center p-24">
<Spinner />
</div>
);
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{groupMembership ? (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={groupMembership.group.name} />
<div className="flex">
<div className="mr-4 w-96">
<GroupDetailsSection groupMembership={groupMembership} />
</div>
<GroupMembersSection groupMembership={groupMembership} />
</div>
</div>
) : (
<EmptyState title="Error: Unable to find the group." className="py-12" />
)}
</div>
);
};
export const GroupDetailsByIDPage = () => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.Groups}
passThrough={false}
renderGuardBanner
>
<Page />
</ProjectPermissionCan>
</>
);
};
@@ -0,0 +1,152 @@
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 { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton
} from "@app/components/v2";
import { CopyButton } from "@app/components/v2/CopyButton";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteGroupFromWorkspace } from "@app/hooks/api";
import { TGroupMembership } from "@app/hooks/api/groups/types";
import { GroupRoles } from "@app/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles";
type Props = {
groupMembership: TGroupMembership;
};
export const GroupDetailsSection = ({ groupMembership }: Props) => {
const { handlePopUpToggle, popUp, handlePopUpClose, handlePopUpOpen } = usePopUp([
"deleteGroup"
] as const);
const { mutateAsync: deleteMutateAsync } = useDeleteGroupFromWorkspace();
const { currentWorkspace } = useWorkspace();
const navigate = useNavigate();
const onRemoveGroupSubmit = async () => {
try {
await deleteMutateAsync({
groupId: groupMembership.group.id,
projectId: currentWorkspace.id
});
createNotification({
text: "Successfully removed identity from project",
type: "success"
});
navigate({
to: `/${currentWorkspace.type}/${currentWorkspace.id}/access-management?selectedTab=groups`
});
handlePopUpClose("deleteGroup");
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to remove group from project";
createNotification({
text,
type: "error"
});
}
};
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-semibold 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-semibold 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-semibold 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-semibold 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-semibold 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-semibold 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>
);
};
@@ -0,0 +1,20 @@
import { TGroupMembership } from "@app/hooks/api/groups/types";
import { GroupMembersTable } from "./GroupMembersTable";
type Props = {
groupMembership: TGroupMembership;
};
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-semibold text-mineshaft-100">Group Members</h3>
</div>
<div className="py-4">
<GroupMembersTable groupMembership={groupMembership} />
</div>
</div>
);
};
@@ -0,0 +1,235 @@
import { useMemo } from "react";
import {
faArrowDown,
faArrowUp,
faFolder,
faMagnifyingGlass,
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import {
ConfirmActionModal,
EmptyState,
IconButton,
Input,
Pagination,
Table,
TableContainer,
TableSkeleton,
TBody,
Th,
THead,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useAssumeProjectPrivileges } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useListProjectGroupUsers } from "@app/hooks/api/groups/queries";
import { EFilterReturnedUsers, TGroupMembership } from "@app/hooks/api/groups/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { GroupMembershipRow } from "./GroupMembershipRow";
type Props = {
groupMembership: TGroupMembership;
};
enum GroupMembersOrderBy {
Name = "name"
}
export const GroupMembersTable = ({ groupMembership }: Props) => {
const {
search,
setSearch,
setPage,
page,
perPage,
setPerPage,
offset,
orderDirection,
toggleOrderDirection
} = usePagination(GroupMembersOrderBy.Name, {
initPerPage: getUserTablePreference("projectGroupMembersTable", PreferenceKey.PerPage, 20)
});
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["assumePrivileges"] as const);
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
setUserTablePreference("projectGroupMembersTable", PreferenceKey.PerPage, newPerPage);
};
const { currentWorkspace } = useWorkspace();
const { data: groupMemberships, isPending } = useListProjectGroupUsers({
id: groupMembership.group.id,
groupSlug: groupMembership.group.slug,
projectId: currentWorkspace.id,
offset,
limit: perPage,
search,
filter: EFilterReturnedUsers.EXISTING_MEMBERS
});
const filteredGroupMemberships = useMemo(() => {
return groupMemberships && groupMemberships?.users
? groupMemberships?.users
?.filter((membership) => {
const userSearchString = `${membership.firstName && membership.firstName} ${
membership.lastName && membership.lastName
} ${membership.email && membership.email} ${
membership.username && membership.username
}`;
return userSearchString.toLowerCase().includes(search.trim().toLowerCase());
})
.sort((a, b) => {
const [membershipOne, membershipTwo] =
orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
const membershipOneComparisonString = membershipOne.firstName
? membershipOne.firstName
: membershipOne.email;
const membershipTwoComparisonString = membershipTwo.firstName
? membershipTwo.firstName
: membershipTwo.email;
const comparison = membershipOneComparisonString
.toLowerCase()
.localeCompare(membershipTwoComparisonString.toLowerCase());
return comparison;
})
: [];
}, [groupMemberships, orderDirection, search]);
useResetPageHelper({
totalCount: filteredGroupMemberships?.length,
offset,
setPage
});
const assumePrivileges = useAssumeProjectPrivileges();
const handleAssumePrivileges = async () => {
const { userId } = popUp?.assumePrivileges?.data as { userId: string };
assumePrivileges.mutate(
{
actorId: userId,
actorType: ActorType.USER,
projectId: currentWorkspace.id
},
{
onSuccess: () => {
createNotification({
type: "success",
text: "User privilege assumption has started"
});
let overviewPage: string;
switch (currentWorkspace.type) {
case ProjectType.SecretScanning:
overviewPage = "data-sources";
break;
case ProjectType.CertificateManager:
overviewPage = "subscribers";
break;
default:
overviewPage = "overview";
}
window.location.href = `/${currentWorkspace.type}/${currentWorkspace.id}/${overviewPage}`;
}
}
);
};
return (
<div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search users..."
/>
<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>Email</Th>
<Th>Added On</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="group-user-memberships" />}
{!isPending &&
filteredGroupMemberships.slice(offset, perPage * page).map((userGroupMembership) => {
return (
<GroupMembershipRow
key={`user-group-membership-${userGroupMembership.id}`}
user={userGroupMembership}
onAssumePrivileges={(userId) => handlePopUpOpen("assumePrivileges", { userId })}
/>
);
})}
</TBody>
</Table>
{Boolean(filteredGroupMemberships.length) && (
<Pagination
count={filteredGroupMemberships.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={handlePerPageChange}
/>
)}
{!isPending && !filteredGroupMemberships?.length && (
<EmptyState
title={
groupMemberships?.users.length
? "No users match this search..."
: "This group does not have any members yet"
}
icon={groupMemberships?.users.length ? faSearch : faFolder}
/>
)}
</TableContainer>
<ConfirmActionModal
isOpen={popUp.assumePrivileges.isOpen}
confirmKey="assume"
title="Do you want to assume privileges of this user?"
subTitle="This will set your privileges to those of the user for the next hour."
onChange={(isOpen) => handlePopUpToggle("assumePrivileges", isOpen)}
onConfirmed={handleAssumePrivileges}
buttonText="Confirm"
/>
</div>
);
};
@@ -0,0 +1,76 @@
import { faEllipsisV, faUser } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Td,
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/context";
import { TGroupUser } from "@app/hooks/api/groups/types";
type Props = {
user: TGroupUser;
onAssumePrivileges: (userId: string) => void;
};
export const GroupMembershipRow = ({
user: { firstName, lastName, joinedGroupAt, email, id },
onAssumePrivileges
}: Props) => {
return (
<Tr className="items-center" key={`group-user-${id}`}>
<Td>
<p>{`${firstName ?? "-"} ${lastName ?? ""}`}</p>
</Td>
<Td>
<p>{email}</p>
</Td>
<Td>
<Tooltip content={new Date(joinedGroupAt).toLocaleString()}>
<p>{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>
);
};
@@ -0,0 +1 @@
export { GroupMembersSection } from "./GroupMembersSection";
@@ -0,0 +1 @@
export { GroupDetailsSection } from "./GroupDetailsSection";
@@ -0,0 +1,28 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/groups/$groupId"
)({
component: GroupDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/cert-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Groups"
}
]
};
}
});
@@ -0,0 +1,28 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/groups/$groupId"
)({
component: GroupDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/kms/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Groups"
}
]
};
}
});
@@ -0,0 +1,28 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/groups/$groupId"
)({
component: GroupDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/secret-manager/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Groups"
}
]
};
}
});
@@ -0,0 +1,28 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId"
)({
component: GroupDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/secret-scanning/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Groups"
}
]
};
}
});
@@ -0,0 +1,28 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/groups/$groupId"
)({
component: GroupDetailsByIDPage,
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
{
label: "Access Control",
link: linkOptions({
to: "/ssh/$projectId/access-management",
params: {
projectId: params.projectId
}
})
},
{
label: "Groups"
}
]
};
}
});
@@ -0,0 +1,27 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId"
)({
component: GroupDetailsByIDPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Access Control",
link: linkOptions({ to: "/organization/access-management" })
},
{
label: "groups"
}
]
})
});