From 4c22024d1380c5c95b0af73e181ee9c218d9a146 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Thu, 5 Jun 2025 19:17:46 -0700 Subject: [PATCH 1/2] feature: project group details page --- backend/src/ee/services/group/group-types.ts | 4 + backend/src/lib/api-docs/constants.ts | 1 + .../server/routes/v2/group-project-router.ts | 60 ++++- .../group-project/group-project-service.ts | 48 +++- frontend/src/hooks/api/groups/queries.tsx | 70 +++++- .../src/hooks/api/workspace/mutations.tsx | 5 +- frontend/src/hooks/api/workspace/queries.tsx | 15 ++ .../src/hooks/api/workspace/query-keys.tsx | 2 + .../GroupMembersSection/GroupMembersTable.tsx | 2 +- .../GroupMembershipRow.tsx | 76 ++++-- .../components/GroupsSection/GroupRoles.tsx | 24 +- .../components/GroupsSection/GroupsTable.tsx | 32 ++- .../GroupDetailsByIDPage.tsx | 70 ++++++ .../components/GroupDetailsSection.tsx | 152 +++++++++++ .../GroupMembersSection.tsx | 20 ++ .../GroupMembersSection/GroupMembersTable.tsx | 235 ++++++++++++++++++ .../GroupMembershipRow.tsx | 76 ++++++ .../components/GroupMembersSection/index.tsx | 1 + .../GroupDetailsByIDPage/components/index.tsx | 1 + .../route-cert-manager.tsx | 28 +++ .../GroupDetailsByIDPage/route-kms.tsx | 28 +++ .../route-secret-manager.tsx | 28 +++ .../route-secret-scanning.tsx | 28 +++ .../GroupDetailsByIDPage/route-ssh.tsx | 28 +++ .../project/GroupDetailsByIDPage/route.tsx | 27 ++ frontend/src/routeTree.gen.ts | 145 +++++++++++ frontend/src/routes.ts | 15 +- 27 files changed, 1176 insertions(+), 45 deletions(-) create mode 100644 frontend/src/pages/project/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx create mode 100644 frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx create mode 100644 frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx create mode 100644 frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx create mode 100644 frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx create mode 100644 frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/index.tsx create mode 100644 frontend/src/pages/project/GroupDetailsByIDPage/components/index.tsx create mode 100644 frontend/src/pages/project/GroupDetailsByIDPage/route-cert-manager.tsx create mode 100644 frontend/src/pages/project/GroupDetailsByIDPage/route-kms.tsx create mode 100644 frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx create mode 100644 frontend/src/pages/project/GroupDetailsByIDPage/route-secret-scanning.tsx create mode 100644 frontend/src/pages/project/GroupDetailsByIDPage/route-ssh.tsx create mode 100644 frontend/src/pages/project/GroupDetailsByIDPage/route.tsx diff --git a/backend/src/ee/services/group/group-types.ts b/backend/src/ee/services/group/group-types.ts index 9424075ca..1d7c5fc71 100644 --- a/backend/src/ee/services/group/group-types.ts +++ b/backend/src/ee/services/group/group-types.ts @@ -42,6 +42,10 @@ export type TListGroupUsersDTO = { filter?: EFilterReturnedUsers; } & TGenericPermission; +export type TListProjectGroupUsersDTO = TListGroupUsersDTO & { + projectId: string; +}; + export type TAddUserToGroupDTO = { id: string; username: string; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 3734cbf21..0f0a7e2ec 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -89,6 +89,7 @@ export const GROUPS = { limit: "The number of users to return.", username: "The username to search for.", search: "The text string that user email or name will be filtered by.", + projectId: "The ID of the project the group belongs to.", filterUsers: "Whether to filter the list of returned users. 'existingMembers' will only return existing users in the group, 'nonMembers' will only return users not in the group, undefined will return all users in the organization." }, diff --git a/backend/src/server/routes/v2/group-project-router.ts b/backend/src/server/routes/v2/group-project-router.ts index 5a081a3d9..8ed03ca08 100644 --- a/backend/src/server/routes/v2/group-project-router.ts +++ b/backend/src/server/routes/v2/group-project-router.ts @@ -4,9 +4,11 @@ import { GroupProjectMembershipsSchema, GroupsSchema, ProjectMembershipRole, - ProjectUserMembershipRolesSchema + ProjectUserMembershipRolesSchema, + UsersSchema } from "@app/db/schemas"; -import { ApiDocsTags, PROJECTS } from "@app/lib/api-docs"; +import { EFilterReturnedUsers } from "@app/ee/services/group/group-types"; +import { ApiDocsTags, GROUPS, PROJECTS } from "@app/lib/api-docs"; import { ms } from "@app/lib/ms"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -301,4 +303,58 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => return { groupMembership }; } }); + + server.route({ + method: "GET", + url: "/:projectId/groups/:groupId/users", + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + hide: false, + tags: [ApiDocsTags.ProjectGroups], + description: "Return project group users", + params: z.object({ + projectId: z.string().trim().describe(GROUPS.LIST_USERS.projectId), + groupId: z.string().trim().describe(GROUPS.LIST_USERS.id) + }), + querystring: z.object({ + offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset), + limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit), + username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username), + search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search), + filter: z.nativeEnum(EFilterReturnedUsers).optional().describe(GROUPS.LIST_USERS.filterUsers) + }), + response: { + 200: z.object({ + users: UsersSchema.pick({ + email: true, + username: true, + firstName: true, + lastName: true, + id: true + }) + .merge( + z.object({ + isPartOfGroup: z.boolean(), + joinedGroupAt: z.date().nullable() + }) + ) + .array(), + totalCount: z.number() + }) + } + }, + handler: async (req) => { + const { users, totalCount } = await server.services.groupProject.listProjectGroupUsers({ + id: req.params.groupId, + projectId: req.params.projectId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query + }); + + return { users, totalCount }; + } + }); }; diff --git a/backend/src/services/group-project/group-project-service.ts b/backend/src/services/group-project/group-project-service.ts index 1ff2c78d2..a793ecfab 100644 --- a/backend/src/services/group-project/group-project-service.ts +++ b/backend/src/services/group-project/group-project-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas"; +import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types"; import { constructPermissionErrorMessage, validatePrivilegeChangeOperation @@ -42,7 +43,7 @@ type TGroupProjectServiceFactoryDep = { projectKeyDAL: Pick; projectRoleDAL: Pick; projectBotDAL: TProjectBotDALFactory; - groupDAL: Pick; + groupDAL: Pick; permissionService: Pick; }; @@ -471,11 +472,54 @@ export const groupProjectServiceFactory = ({ return groupMembership; }; + const listProjectGroupUsers = async ({ + id, + projectId, + offset, + limit, + username, + actor, + actorId, + actorAuthMethod, + actorOrgId, + search, + filter + }: TListProjectGroupUsersDTO) => { + const project = await projectDAL.findById(projectId); + + if (!project) { + throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId, + actionProjectType: ActionProjectType.Any + }); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups); + + const { members, totalCount } = await groupDAL.findAllGroupPossibleMembers({ + orgId: project.orgId, + groupId: id, + offset, + limit, + username, + search, + filter + }); + + return { users: members, totalCount }; + }; + return { addGroupToProject, updateGroupInProject, removeGroupFromProject, listGroupsInProject, - getGroupInProject + getGroupInProject, + listProjectGroupUsers }; }; diff --git a/frontend/src/hooks/api/groups/queries.tsx b/frontend/src/hooks/api/groups/queries.tsx index eb10085bc..6fbe0ec13 100644 --- a/frontend/src/hooks/api/groups/queries.tsx +++ b/frontend/src/hooks/api/groups/queries.tsx @@ -21,7 +21,27 @@ export const groupKeys = { limit: number; search: string; filter?: EFilterReturnedUsers; - }) => [...groupKeys.forGroupUserMemberships(slug), { offset, limit, search, filter }] as const + }) => [...groupKeys.forGroupUserMemberships(slug), { offset, limit, search, filter }] as const, + specificProjectGroupUserMemberships: ({ + projectId, + slug, + offset, + limit, + search, + filter + }: { + slug: string; + projectId: string; + offset: number; + limit: number; + search: string; + filter?: EFilterReturnedUsers; + }) => + [ + ...groupKeys.forGroupUserMemberships(slug), + projectId, + { offset, limit, search, filter } + ] as const }; export const useGetGroupById = (groupId: string) => { @@ -80,3 +100,51 @@ export const useListGroupUsers = ({ } }); }; + +export const useListProjectGroupUsers = ({ + id, + projectId, + groupSlug, + offset = 0, + limit = 10, + search, + filter +}: { + id: string; + groupSlug: string; + projectId: string; + offset: number; + limit: number; + search: string; + filter?: EFilterReturnedUsers; +}) => { + return useQuery({ + queryKey: groupKeys.specificProjectGroupUserMemberships({ + slug: groupSlug, + projectId, + offset, + limit, + search, + filter + }), + enabled: Boolean(groupSlug), + placeholderData: (previousData) => previousData, + queryFn: async () => { + const params = new URLSearchParams({ + offset: String(offset), + limit: String(limit), + search, + ...(filter && { filter }) + }); + + const { data } = await apiRequest.get<{ users: TGroupUser[]; totalCount: number }>( + `/api/v2/workspace/${projectId}/groups/${id}/users`, + { + params + } + ); + + return data; + } + }); +}; diff --git a/frontend/src/hooks/api/workspace/mutations.tsx b/frontend/src/hooks/api/workspace/mutations.tsx index ea7376d3d..5cd7e3bbc 100644 --- a/frontend/src/hooks/api/workspace/mutations.tsx +++ b/frontend/src/hooks/api/workspace/mutations.tsx @@ -50,10 +50,13 @@ export const useUpdateGroupWorkspaceRole = () => { return groupMembership; }, - onSuccess: (_, { projectId }) => { + onSuccess: (_, { projectId, groupId }) => { queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId) }); + queryClient.invalidateQueries({ + queryKey: workspaceKeys.getWorkspaceGroupMembershipDetails(projectId, groupId) + }); } }); }; diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index c040a1267..2ae2bc463 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -691,6 +691,21 @@ export const useGetWorkspaceIdentityMembershipDetails = (projectId: string, iden }); }; +export const useGetWorkspaceGroupMembershipDetails = (projectId: string, groupId: string) => { + return useQuery({ + enabled: Boolean(projectId && groupId), + queryKey: workspaceKeys.getWorkspaceGroupMembershipDetails(projectId, groupId), + queryFn: async () => { + const { + data: { groupMembership } + } = await apiRequest.get<{ groupMembership: TGroupMembership }>( + `/api/v2/workspace/${projectId}/groups/${groupId}` + ); + return groupMembership; + } + }); +}; + export const useListWorkspaceGroups = (projectId: string) => { return useQuery({ queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId), diff --git a/frontend/src/hooks/api/workspace/query-keys.tsx b/frontend/src/hooks/api/workspace/query-keys.tsx index c10616b63..15317d2fd 100644 --- a/frontend/src/hooks/api/workspace/query-keys.tsx +++ b/frontend/src/hooks/api/workspace/query-keys.tsx @@ -36,6 +36,8 @@ export const workspaceKeys = { searchWorkspace: (dto: TSearchProjectsDTO) => ["search-projects", dto] as const, getWorkspaceGroupMemberships: (workspaceId: string) => [{ workspaceId }, "workspace-groups"] as const, + getWorkspaceGroupMembershipDetails: (workspaceId: string, groupId: string) => + [{ workspaceId, groupId }, "workspace-group-membership-details"] as const, getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) => [{ projectSlug }, "workspace-cas"] as const, specificWorkspaceCas: ({ projectSlug, status }: { projectSlug: string; status?: CaStatus }) => diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx index 6472c34c1..f56f852d4 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx @@ -152,7 +152,7 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props Email Added On - + diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx index 829ad2908..8ec279a9d 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx @@ -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 = ({

{new Date(joinedGroupAt).toLocaleDateString()}

- - - {(isAllowed) => { - return ( - + + + + - handlePopUpOpen("removeMemberFromGroup", { username })} - variant="plain" - colorSchema="danger" - > - - - - ); - }} - + + + + + + {(isAllowed) => { + return ( + +
+ } + onClick={() => handlePopUpOpen("removeMemberFromGroup", { username })} + isDisabled={!isAllowed || isOidcManageGroupMembershipsEnabled} + > + Remove User From Group + +
+
+ ); + }} +
+
+ + ); diff --git a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx index 08a17148e..c3f255a9b 100644 --- a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx @@ -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; 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({ @@ -261,7 +269,7 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember }; return ( -
+
{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 && ( - + e.stopPropagation()}> )} - + e.stopPropagation()} + hideCloseBtn + className="pt-4" + > {isRolesLoading ? (
diff --git a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx index 8f06576e2..ff2c53aca 100644 --- a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx @@ -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 ( - + { + 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 + } + }) + } + > {name} {
{ + onClick={(e) => { + e.stopPropagation(); handlePopUpOpen("deleteGroup", { id, name diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx new file mode 100644 index 000000000..393419e64 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx @@ -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 ( +
+ +
+ ); + + return ( +
+ {groupMembership ? ( +
+ +
+
+ +
+ +
+
+ ) : ( + + )} +
+ ); +}; + +export const GroupDetailsByIDPage = () => { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: t("settings.members.title") })} + + + + + + + ); +}; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx new file mode 100644 index 000000000..f69806d00 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupDetailsSection.tsx @@ -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 ( +
+
+

Group Details

+ + + + + + + + + {(isAllowed) => { + return ( + } + onClick={() => handlePopUpOpen("deleteGroup")} + isDisabled={!isAllowed} + > + Remove Group From Project + + ); + }} + + + +
+
+
+

Group ID

+
+

{groupMembership.group.id}

+ +
+
+
+

Name

+

{groupMembership.group.name}

+
+
+

Slug

+
+

{groupMembership.group.slug}

+ +
+
+
+

Project Role

+ + {(isAllowed) => ( + + )} + +
+
+

Assigned to Project

+

+ {format(groupMembership.createdAt, "M/d/yyyy")} +

+
+
+ handlePopUpToggle("deleteGroup", isOpen)} + deleteKey="confirm" + buttonText="Remove" + onDeleteApproved={onRemoveGroupSubmit} + /> +
+ ); +}; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx new file mode 100644 index 000000000..0be3a9b01 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersSection.tsx @@ -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 ( +
+
+

Group Members

+
+
+ +
+
+ ); +}; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx new file mode 100644 index 000000000..c0639260a --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx @@ -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 ( +
+ setSearch(e.target.value)} + leftIcon={} + placeholder="Search users..." + /> + + + + + + + + + + + {isPending && } + {!isPending && + filteredGroupMemberships.slice(offset, perPage * page).map((userGroupMembership) => { + return ( + handlePopUpOpen("assumePrivileges", { userId })} + /> + ); + })} + +
+
+ Name + + + +
+
EmailAdded On +
+ {Boolean(filteredGroupMemberships.length) && ( + + )} + {!isPending && !filteredGroupMemberships?.length && ( + + )} +
+ handlePopUpToggle("assumePrivileges", isOpen)} + onConfirmed={handleAssumePrivileges} + buttonText="Confirm" + /> +
+ ); +}; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx new file mode 100644 index 000000000..55f566477 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembershipRow.tsx @@ -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 ( + + +

{`${firstName ?? "-"} ${lastName ?? ""}`}

+ + +

{email}

+ + + +

{new Date(joinedGroupAt).toLocaleDateString()}

+
+ + + + + + + + + + + + {(isAllowed) => { + return ( + } + onClick={() => onAssumePrivileges(id)} + isDisabled={!isAllowed} + > + Assume Privileges + + ); + }} + + + + + + + ); +}; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/index.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/index.tsx new file mode 100644 index 000000000..70c696609 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/index.tsx @@ -0,0 +1 @@ +export { GroupMembersSection } from "./GroupMembersSection"; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/index.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/index.tsx new file mode 100644 index 000000000..003c47910 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/index.tsx @@ -0,0 +1 @@ +export { GroupDetailsSection } from "./GroupDetailsSection"; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-cert-manager.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-cert-manager.tsx new file mode 100644 index 000000000..2663d75b1 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-cert-manager.tsx @@ -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" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-kms.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-kms.tsx new file mode 100644 index 000000000..6f4b5a8a0 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-kms.tsx @@ -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" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx new file mode 100644 index 000000000..30fb82048 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx @@ -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" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-scanning.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-scanning.tsx new file mode 100644 index 000000000..0c3d7c998 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-scanning.tsx @@ -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" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-ssh.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-ssh.tsx new file mode 100644 index 000000000..3aa9338fd --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-ssh.tsx @@ -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" + } + ] + }; + } +}); diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route.tsx new file mode 100644 index 000000000..83ead0221 --- /dev/null +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route.tsx @@ -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: () => , + link: linkOptions({ to: "/organization/secret-manager/overview" }) + }, + { + label: "Access Control", + link: linkOptions({ to: "/organization/access-management" }) + }, + { + label: "groups" + } + ] + }) +}); diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts index 449ea28cb..9496230af 100644 --- a/frontend/src/routeTree.gen.ts +++ b/frontend/src/routeTree.gen.ts @@ -115,19 +115,24 @@ import { Route as certManagerAlertingPageRouteImport } from './pages/cert-manage import { Route as projectRoleDetailsBySlugPageRouteSshImport } from './pages/project/RoleDetailsBySlugPage/route-ssh' import { Route as projectMemberDetailsByIDPageRouteSshImport } from './pages/project/MemberDetailsByIDPage/route-ssh' import { Route as projectIdentityDetailsByIDPageRouteSshImport } from './pages/project/IdentityDetailsByIDPage/route-ssh' +import { Route as projectGroupDetailsByIDPageRouteSshImport } from './pages/project/GroupDetailsByIDPage/route-ssh' import { Route as projectRoleDetailsBySlugPageRouteSecretScanningImport } from './pages/project/RoleDetailsBySlugPage/route-secret-scanning' import { Route as projectMemberDetailsByIDPageRouteSecretScanningImport } from './pages/project/MemberDetailsByIDPage/route-secret-scanning' import { Route as projectIdentityDetailsByIDPageRouteSecretScanningImport } from './pages/project/IdentityDetailsByIDPage/route-secret-scanning' +import { Route as projectGroupDetailsByIDPageRouteSecretScanningImport } from './pages/project/GroupDetailsByIDPage/route-secret-scanning' import { Route as projectRoleDetailsBySlugPageRouteSecretManagerImport } from './pages/project/RoleDetailsBySlugPage/route-secret-manager' import { Route as projectMemberDetailsByIDPageRouteSecretManagerImport } from './pages/project/MemberDetailsByIDPage/route-secret-manager' import { Route as projectIdentityDetailsByIDPageRouteSecretManagerImport } from './pages/project/IdentityDetailsByIDPage/route-secret-manager' +import { Route as projectGroupDetailsByIDPageRouteSecretManagerImport } from './pages/project/GroupDetailsByIDPage/route-secret-manager' import { Route as projectRoleDetailsBySlugPageRouteKmsImport } from './pages/project/RoleDetailsBySlugPage/route-kms' import { Route as projectMemberDetailsByIDPageRouteKmsImport } from './pages/project/MemberDetailsByIDPage/route-kms' import { Route as projectIdentityDetailsByIDPageRouteKmsImport } from './pages/project/IdentityDetailsByIDPage/route-kms' +import { Route as projectGroupDetailsByIDPageRouteKmsImport } from './pages/project/GroupDetailsByIDPage/route-kms' import { Route as projectRoleDetailsBySlugPageRouteCertManagerImport } from './pages/project/RoleDetailsBySlugPage/route-cert-manager' import { Route as certManagerPkiCollectionDetailsByIDPageRoutesImport } from './pages/cert-manager/PkiCollectionDetailsByIDPage/routes' import { Route as projectMemberDetailsByIDPageRouteCertManagerImport } from './pages/project/MemberDetailsByIDPage/route-cert-manager' import { Route as projectIdentityDetailsByIDPageRouteCertManagerImport } from './pages/project/IdentityDetailsByIDPage/route-cert-manager' +import { Route as projectGroupDetailsByIDPageRouteCertManagerImport } from './pages/project/GroupDetailsByIDPage/route-cert-manager' import { Route as sshSshHostGroupDetailsByIDPageRouteImport } from './pages/ssh/SshHostGroupDetailsByIDPage/route' import { Route as sshSshCaByIDPageRouteImport } from './pages/ssh/SshCaByIDPage/route' import { Route as secretManagerSecretDashboardPageRouteImport } from './pages/secret-manager/SecretDashboardPage/route' @@ -1149,6 +1154,13 @@ const projectIdentityDetailsByIDPageRouteSshRoute = getParentRoute: () => sshLayoutRoute, } as any) +const projectGroupDetailsByIDPageRouteSshRoute = + projectGroupDetailsByIDPageRouteSshImport.update({ + id: '/groups/$groupId', + path: '/groups/$groupId', + getParentRoute: () => sshLayoutRoute, + } as any) + const projectRoleDetailsBySlugPageRouteSecretScanningRoute = projectRoleDetailsBySlugPageRouteSecretScanningImport.update({ id: '/roles/$roleSlug', @@ -1170,6 +1182,13 @@ const projectIdentityDetailsByIDPageRouteSecretScanningRoute = getParentRoute: () => secretScanningLayoutRoute, } as any) +const projectGroupDetailsByIDPageRouteSecretScanningRoute = + projectGroupDetailsByIDPageRouteSecretScanningImport.update({ + id: '/groups/$groupId', + path: '/groups/$groupId', + getParentRoute: () => secretScanningLayoutRoute, + } as any) + const projectRoleDetailsBySlugPageRouteSecretManagerRoute = projectRoleDetailsBySlugPageRouteSecretManagerImport.update({ id: '/roles/$roleSlug', @@ -1191,6 +1210,13 @@ const projectIdentityDetailsByIDPageRouteSecretManagerRoute = getParentRoute: () => secretManagerLayoutRoute, } as any) +const projectGroupDetailsByIDPageRouteSecretManagerRoute = + projectGroupDetailsByIDPageRouteSecretManagerImport.update({ + id: '/groups/$groupId', + path: '/groups/$groupId', + getParentRoute: () => secretManagerLayoutRoute, + } as any) + const projectRoleDetailsBySlugPageRouteKmsRoute = projectRoleDetailsBySlugPageRouteKmsImport.update({ id: '/roles/$roleSlug', @@ -1212,6 +1238,13 @@ const projectIdentityDetailsByIDPageRouteKmsRoute = getParentRoute: () => kmsLayoutRoute, } as any) +const projectGroupDetailsByIDPageRouteKmsRoute = + projectGroupDetailsByIDPageRouteKmsImport.update({ + id: '/groups/$groupId', + path: '/groups/$groupId', + getParentRoute: () => kmsLayoutRoute, + } as any) + const projectRoleDetailsBySlugPageRouteCertManagerRoute = projectRoleDetailsBySlugPageRouteCertManagerImport.update({ id: '/roles/$roleSlug', @@ -1240,6 +1273,13 @@ const projectIdentityDetailsByIDPageRouteCertManagerRoute = getParentRoute: () => certManagerLayoutRoute, } as any) +const projectGroupDetailsByIDPageRouteCertManagerRoute = + projectGroupDetailsByIDPageRouteCertManagerImport.update({ + id: '/groups/$groupId', + path: '/groups/$groupId', + getParentRoute: () => certManagerLayoutRoute, + } as any) + const sshSshHostGroupDetailsByIDPageRouteRoute = sshSshHostGroupDetailsByIDPageRouteImport.update({ id: '/ssh-host-groups/$sshHostGroupId', @@ -2861,6 +2901,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof sshSshHostGroupDetailsByIDPageRouteImport parentRoute: typeof sshLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/groups/$groupId': { + id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/groups/$groupId' + path: '/groups/$groupId' + fullPath: '/cert-manager/$projectId/groups/$groupId' + preLoaderRoute: typeof projectGroupDetailsByIDPageRouteCertManagerImport + parentRoute: typeof certManagerLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId': { id: '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId' path: '/identities/$identityId' @@ -2889,6 +2936,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectRoleDetailsBySlugPageRouteCertManagerImport parentRoute: typeof certManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/groups/$groupId': { + id: '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/groups/$groupId' + path: '/groups/$groupId' + fullPath: '/kms/$projectId/groups/$groupId' + preLoaderRoute: typeof projectGroupDetailsByIDPageRouteKmsImport + parentRoute: typeof kmsLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/identities/$identityId': { id: '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/identities/$identityId' path: '/identities/$identityId' @@ -2910,6 +2964,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectRoleDetailsBySlugPageRouteKmsImport parentRoute: typeof kmsLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/groups/$groupId': { + id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/groups/$groupId' + path: '/groups/$groupId' + fullPath: '/secret-manager/$projectId/groups/$groupId' + preLoaderRoute: typeof projectGroupDetailsByIDPageRouteSecretManagerImport + parentRoute: typeof secretManagerLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId': { id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId' path: '/identities/$identityId' @@ -2931,6 +2992,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectRoleDetailsBySlugPageRouteSecretManagerImport parentRoute: typeof secretManagerLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId': { + id: '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId' + path: '/groups/$groupId' + fullPath: '/secret-scanning/$projectId/groups/$groupId' + preLoaderRoute: typeof projectGroupDetailsByIDPageRouteSecretScanningImport + parentRoute: typeof secretScanningLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId': { id: '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId' path: '/identities/$identityId' @@ -2952,6 +3020,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof projectRoleDetailsBySlugPageRouteSecretScanningImport parentRoute: typeof secretScanningLayoutImport } + '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/groups/$groupId': { + id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/groups/$groupId' + path: '/groups/$groupId' + fullPath: '/ssh/$projectId/groups/$groupId' + preLoaderRoute: typeof projectGroupDetailsByIDPageRouteSshImport + parentRoute: typeof sshLayoutImport + } '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId': { id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId' path: '/identities/$identityId' @@ -3735,6 +3810,7 @@ interface certManagerLayoutRouteChildren { AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutCertificateTemplatesRouteWithChildren AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren certManagerCertAuthDetailsByIDPageRouteRoute: typeof certManagerCertAuthDetailsByIDPageRouteRoute + projectGroupDetailsByIDPageRouteCertManagerRoute: typeof projectGroupDetailsByIDPageRouteCertManagerRoute projectIdentityDetailsByIDPageRouteCertManagerRoute: typeof projectIdentityDetailsByIDPageRouteCertManagerRoute projectMemberDetailsByIDPageRouteCertManagerRoute: typeof projectMemberDetailsByIDPageRouteCertManagerRoute certManagerPkiCollectionDetailsByIDPageRoutesRoute: typeof certManagerPkiCollectionDetailsByIDPageRoutesRoute @@ -3755,6 +3831,8 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = { AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdCertManagerLayoutSubscribersRouteWithChildren, certManagerCertAuthDetailsByIDPageRouteRoute: certManagerCertAuthDetailsByIDPageRouteRoute, + projectGroupDetailsByIDPageRouteCertManagerRoute: + projectGroupDetailsByIDPageRouteCertManagerRoute, projectIdentityDetailsByIDPageRouteCertManagerRoute: projectIdentityDetailsByIDPageRouteCertManagerRoute, projectMemberDetailsByIDPageRouteCertManagerRoute: @@ -3787,6 +3865,7 @@ interface kmsLayoutRouteChildren { kmsOverviewPageRouteRoute: typeof kmsOverviewPageRouteRoute kmsSettingsPageRouteRoute: typeof kmsSettingsPageRouteRoute projectAccessControlPageRouteKmsRoute: typeof projectAccessControlPageRouteKmsRoute + projectGroupDetailsByIDPageRouteKmsRoute: typeof projectGroupDetailsByIDPageRouteKmsRoute projectIdentityDetailsByIDPageRouteKmsRoute: typeof projectIdentityDetailsByIDPageRouteKmsRoute projectMemberDetailsByIDPageRouteKmsRoute: typeof projectMemberDetailsByIDPageRouteKmsRoute projectRoleDetailsBySlugPageRouteKmsRoute: typeof projectRoleDetailsBySlugPageRouteKmsRoute @@ -3797,6 +3876,8 @@ const kmsLayoutRouteChildren: kmsLayoutRouteChildren = { kmsOverviewPageRouteRoute: kmsOverviewPageRouteRoute, kmsSettingsPageRouteRoute: kmsSettingsPageRouteRoute, projectAccessControlPageRouteKmsRoute: projectAccessControlPageRouteKmsRoute, + projectGroupDetailsByIDPageRouteKmsRoute: + projectGroupDetailsByIDPageRouteKmsRoute, projectIdentityDetailsByIDPageRouteKmsRoute: projectIdentityDetailsByIDPageRouteKmsRoute, projectMemberDetailsByIDPageRouteKmsRoute: @@ -4078,6 +4159,7 @@ interface secretManagerLayoutRouteChildren { projectAccessControlPageRouteSecretManagerRoute: typeof projectAccessControlPageRouteSecretManagerRoute AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren secretManagerSecretDashboardPageRouteRoute: typeof secretManagerSecretDashboardPageRouteRoute + projectGroupDetailsByIDPageRouteSecretManagerRoute: typeof projectGroupDetailsByIDPageRouteSecretManagerRoute projectIdentityDetailsByIDPageRouteSecretManagerRoute: typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute projectMemberDetailsByIDPageRouteSecretManagerRoute: typeof projectMemberDetailsByIDPageRouteSecretManagerRoute projectRoleDetailsBySlugPageRouteSecretManagerRoute: typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute @@ -4098,6 +4180,8 @@ const secretManagerLayoutRouteChildren: secretManagerLayoutRouteChildren = { AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdSecretManagerLayoutIntegrationsRouteWithChildren, secretManagerSecretDashboardPageRouteRoute: secretManagerSecretDashboardPageRouteRoute, + projectGroupDetailsByIDPageRouteSecretManagerRoute: + projectGroupDetailsByIDPageRouteSecretManagerRoute, projectIdentityDetailsByIDPageRouteSecretManagerRoute: projectIdentityDetailsByIDPageRouteSecretManagerRoute, projectMemberDetailsByIDPageRouteSecretManagerRoute: @@ -4146,6 +4230,7 @@ interface secretScanningLayoutRouteChildren { secretScanningSettingsPageRouteRoute: typeof secretScanningSettingsPageRouteRoute projectAccessControlPageRouteSecretScanningRoute: typeof projectAccessControlPageRouteSecretScanningRoute AuthenticateInjectOrgDetailsOrgLayoutSecretScanningProjectIdSecretScanningLayoutDataSourcesRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren + projectGroupDetailsByIDPageRouteSecretScanningRoute: typeof projectGroupDetailsByIDPageRouteSecretScanningRoute projectIdentityDetailsByIDPageRouteSecretScanningRoute: typeof projectIdentityDetailsByIDPageRouteSecretScanningRoute projectMemberDetailsByIDPageRouteSecretScanningRoute: typeof projectMemberDetailsByIDPageRouteSecretScanningRoute projectRoleDetailsBySlugPageRouteSecretScanningRoute: typeof projectRoleDetailsBySlugPageRouteSecretScanningRoute @@ -4159,6 +4244,8 @@ const secretScanningLayoutRouteChildren: secretScanningLayoutRouteChildren = { projectAccessControlPageRouteSecretScanningRoute, AuthenticateInjectOrgDetailsOrgLayoutSecretScanningProjectIdSecretScanningLayoutDataSourcesRoute: AuthenticateInjectOrgDetailsOrgLayoutSecretScanningProjectIdSecretScanningLayoutDataSourcesRouteWithChildren, + projectGroupDetailsByIDPageRouteSecretScanningRoute: + projectGroupDetailsByIDPageRouteSecretScanningRoute, projectIdentityDetailsByIDPageRouteSecretScanningRoute: projectIdentityDetailsByIDPageRouteSecretScanningRoute, projectMemberDetailsByIDPageRouteSecretScanningRoute: @@ -4192,6 +4279,7 @@ interface sshLayoutRouteChildren { projectAccessControlPageRouteSshRoute: typeof projectAccessControlPageRouteSshRoute sshSshCaByIDPageRouteRoute: typeof sshSshCaByIDPageRouteRoute sshSshHostGroupDetailsByIDPageRouteRoute: typeof sshSshHostGroupDetailsByIDPageRouteRoute + projectGroupDetailsByIDPageRouteSshRoute: typeof projectGroupDetailsByIDPageRouteSshRoute projectIdentityDetailsByIDPageRouteSshRoute: typeof projectIdentityDetailsByIDPageRouteSshRoute projectMemberDetailsByIDPageRouteSshRoute: typeof projectMemberDetailsByIDPageRouteSshRoute projectRoleDetailsBySlugPageRouteSshRoute: typeof projectRoleDetailsBySlugPageRouteSshRoute @@ -4206,6 +4294,8 @@ const sshLayoutRouteChildren: sshLayoutRouteChildren = { sshSshCaByIDPageRouteRoute: sshSshCaByIDPageRouteRoute, sshSshHostGroupDetailsByIDPageRouteRoute: sshSshHostGroupDetailsByIDPageRouteRoute, + projectGroupDetailsByIDPageRouteSshRoute: + projectGroupDetailsByIDPageRouteSshRoute, projectIdentityDetailsByIDPageRouteSshRoute: projectIdentityDetailsByIDPageRouteSshRoute, projectMemberDetailsByIDPageRouteSshRoute: @@ -4561,19 +4651,24 @@ export interface FileRoutesByFullPath { '/secret-manager/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute '/ssh/$projectId/ca/$caId': typeof sshSshCaByIDPageRouteRoute '/ssh/$projectId/ssh-host-groups/$sshHostGroupId': typeof sshSshHostGroupDetailsByIDPageRouteRoute + '/cert-manager/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteCertManagerRoute '/cert-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute '/cert-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteCertManagerRoute '/cert-manager/$projectId/pki-collections/$collectionId': typeof certManagerPkiCollectionDetailsByIDPageRoutesRoute '/cert-manager/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteCertManagerRoute + '/kms/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteKmsRoute '/kms/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteKmsRoute '/kms/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteKmsRoute '/kms/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteKmsRoute + '/secret-manager/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretManagerRoute '/secret-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute '/secret-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute '/secret-manager/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute + '/secret-scanning/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretScanningRoute '/secret-scanning/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretScanningRoute '/secret-scanning/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretScanningRoute '/secret-scanning/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretScanningRoute + '/ssh/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSshRoute '/ssh/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSshRoute '/ssh/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSshRoute '/ssh/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSshRoute @@ -4764,19 +4859,24 @@ export interface FileRoutesByTo { '/secret-manager/$projectId/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute '/ssh/$projectId/ca/$caId': typeof sshSshCaByIDPageRouteRoute '/ssh/$projectId/ssh-host-groups/$sshHostGroupId': typeof sshSshHostGroupDetailsByIDPageRouteRoute + '/cert-manager/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteCertManagerRoute '/cert-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute '/cert-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteCertManagerRoute '/cert-manager/$projectId/pki-collections/$collectionId': typeof certManagerPkiCollectionDetailsByIDPageRoutesRoute '/cert-manager/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteCertManagerRoute + '/kms/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteKmsRoute '/kms/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteKmsRoute '/kms/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteKmsRoute '/kms/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteKmsRoute + '/secret-manager/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretManagerRoute '/secret-manager/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute '/secret-manager/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute '/secret-manager/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute + '/secret-scanning/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretScanningRoute '/secret-scanning/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretScanningRoute '/secret-scanning/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretScanningRoute '/secret-scanning/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretScanningRoute + '/ssh/$projectId/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSshRoute '/ssh/$projectId/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSshRoute '/ssh/$projectId/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSshRoute '/ssh/$projectId/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSshRoute @@ -4990,19 +5090,24 @@ export interface FileRoutesById { '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug': typeof secretManagerSecretDashboardPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId': typeof sshSshCaByIDPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId': typeof sshSshHostGroupDetailsByIDPageRouteRoute + '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/groups/$groupId': typeof projectGroupDetailsByIDPageRouteCertManagerRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteCertManagerRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteCertManagerRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId': typeof certManagerPkiCollectionDetailsByIDPageRoutesRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteCertManagerRoute + '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/groups/$groupId': typeof projectGroupDetailsByIDPageRouteKmsRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteKmsRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteKmsRoute '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteKmsRoute + '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretManagerRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretManagerRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretManagerRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretManagerRoute + '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSecretScanningRoute '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSecretScanningRoute '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSecretScanningRoute '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSecretScanningRoute + '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/groups/$groupId': typeof projectGroupDetailsByIDPageRouteSshRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId': typeof projectIdentityDetailsByIDPageRouteSshRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/members/$membershipId': typeof projectMemberDetailsByIDPageRouteSshRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/roles/$roleSlug': typeof projectRoleDetailsBySlugPageRouteSshRoute @@ -5207,19 +5312,24 @@ export interface FileRouteTypes { | '/secret-manager/$projectId/secrets/$envSlug' | '/ssh/$projectId/ca/$caId' | '/ssh/$projectId/ssh-host-groups/$sshHostGroupId' + | '/cert-manager/$projectId/groups/$groupId' | '/cert-manager/$projectId/identities/$identityId' | '/cert-manager/$projectId/members/$membershipId' | '/cert-manager/$projectId/pki-collections/$collectionId' | '/cert-manager/$projectId/roles/$roleSlug' + | '/kms/$projectId/groups/$groupId' | '/kms/$projectId/identities/$identityId' | '/kms/$projectId/members/$membershipId' | '/kms/$projectId/roles/$roleSlug' + | '/secret-manager/$projectId/groups/$groupId' | '/secret-manager/$projectId/identities/$identityId' | '/secret-manager/$projectId/members/$membershipId' | '/secret-manager/$projectId/roles/$roleSlug' + | '/secret-scanning/$projectId/groups/$groupId' | '/secret-scanning/$projectId/identities/$identityId' | '/secret-scanning/$projectId/members/$membershipId' | '/secret-scanning/$projectId/roles/$roleSlug' + | '/ssh/$projectId/groups/$groupId' | '/ssh/$projectId/identities/$identityId' | '/ssh/$projectId/members/$membershipId' | '/ssh/$projectId/roles/$roleSlug' @@ -5409,19 +5519,24 @@ export interface FileRouteTypes { | '/secret-manager/$projectId/secrets/$envSlug' | '/ssh/$projectId/ca/$caId' | '/ssh/$projectId/ssh-host-groups/$sshHostGroupId' + | '/cert-manager/$projectId/groups/$groupId' | '/cert-manager/$projectId/identities/$identityId' | '/cert-manager/$projectId/members/$membershipId' | '/cert-manager/$projectId/pki-collections/$collectionId' | '/cert-manager/$projectId/roles/$roleSlug' + | '/kms/$projectId/groups/$groupId' | '/kms/$projectId/identities/$identityId' | '/kms/$projectId/members/$membershipId' | '/kms/$projectId/roles/$roleSlug' + | '/secret-manager/$projectId/groups/$groupId' | '/secret-manager/$projectId/identities/$identityId' | '/secret-manager/$projectId/members/$membershipId' | '/secret-manager/$projectId/roles/$roleSlug' + | '/secret-scanning/$projectId/groups/$groupId' | '/secret-scanning/$projectId/identities/$identityId' | '/secret-scanning/$projectId/members/$membershipId' | '/secret-scanning/$projectId/roles/$roleSlug' + | '/ssh/$projectId/groups/$groupId' | '/ssh/$projectId/identities/$identityId' | '/ssh/$projectId/members/$membershipId' | '/ssh/$projectId/roles/$roleSlug' @@ -5633,19 +5748,24 @@ export interface FileRouteTypes { | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId' + | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/groups/$groupId' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/roles/$roleSlug' + | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/groups/$groupId' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/identities/$identityId' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/roles/$roleSlug' + | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/groups/$groupId' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug' + | '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId' | '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId' | '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/roles/$roleSlug' + | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/groups/$groupId' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/members/$membershipId' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/roles/$roleSlug' @@ -6206,6 +6326,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/certificate-templates", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/subscribers", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/ca/$caName", + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/members/$membershipId", "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId", @@ -6220,6 +6341,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/overview", "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/settings", "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/access-management", + "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/members/$membershipId", "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/roles/$roleSlug" @@ -6237,6 +6359,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/access-management", "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/integrations", "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug", + "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/members/$membershipId", "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/roles/$roleSlug" @@ -6250,6 +6373,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/settings", "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/access-management", "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/data-sources", + "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/members/$membershipId", "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/roles/$roleSlug" @@ -6266,6 +6390,7 @@ export const routeTree = rootRoute "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management", "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ca/$caId", "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/ssh-host-groups/$sshHostGroupId", + "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/groups/$groupId", "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId", "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/members/$membershipId", "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/roles/$roleSlug" @@ -6558,6 +6683,10 @@ export const routeTree = rootRoute "filePath": "ssh/SshHostGroupDetailsByIDPage/route.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout" }, + "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/groups/$groupId": { + "filePath": "project/GroupDetailsByIDPage/route-cert-manager.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" + }, "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/identities/$identityId": { "filePath": "project/IdentityDetailsByIDPage/route-cert-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" @@ -6574,6 +6703,10 @@ export const routeTree = rootRoute "filePath": "project/RoleDetailsBySlugPage/route-cert-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/groups/$groupId": { + "filePath": "project/GroupDetailsByIDPage/route-kms.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout" + }, "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout/identities/$identityId": { "filePath": "project/IdentityDetailsByIDPage/route-kms.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout" @@ -6586,6 +6719,10 @@ export const routeTree = rootRoute "filePath": "project/RoleDetailsBySlugPage/route-kms.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/kms/$projectId/_kms-layout" }, + "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/groups/$groupId": { + "filePath": "project/GroupDetailsByIDPage/route-secret-manager.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout" + }, "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/identities/$identityId": { "filePath": "project/IdentityDetailsByIDPage/route-secret-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout" @@ -6598,6 +6735,10 @@ export const routeTree = rootRoute "filePath": "project/RoleDetailsBySlugPage/route-secret-manager.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout" }, + "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/groups/$groupId": { + "filePath": "project/GroupDetailsByIDPage/route-secret-scanning.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout" + }, "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout/identities/$identityId": { "filePath": "project/IdentityDetailsByIDPage/route-secret-scanning.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout" @@ -6610,6 +6751,10 @@ export const routeTree = rootRoute "filePath": "project/RoleDetailsBySlugPage/route-secret-scanning.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/secret-scanning/$projectId/_secret-scanning-layout" }, + "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/groups/$groupId": { + "filePath": "project/GroupDetailsByIDPage/route-ssh.tsx", + "parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout" + }, "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/identities/$identityId": { "filePath": "project/IdentityDetailsByIDPage/route-ssh.tsx", "parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout" diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index 0c14e27c9..2572f9ce2 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -270,7 +270,8 @@ const secretManagerRoutes = route("/secret-manager/$projectId", [ route("/access-management", "project/AccessControlPage/route-secret-manager.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-manager.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-manager.tsx"), - route("/members/$membershipId", "project/MemberDetailsByIDPage/route-secret-manager.tsx") + route("/members/$membershipId", "project/MemberDetailsByIDPage/route-secret-manager.tsx"), + route("/groups/$groupId", "project/GroupDetailsByIDPage/route-secret-manager.tsx") ]) ]); @@ -314,7 +315,8 @@ const certManagerRoutes = route("/cert-manager/$projectId", [ route("/access-management", "project/AccessControlPage/route-cert-manager.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-cert-manager.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-cert-manager.tsx"), - route("/members/$membershipId", "project/MemberDetailsByIDPage/route-cert-manager.tsx") + route("/members/$membershipId", "project/MemberDetailsByIDPage/route-cert-manager.tsx"), + route("/groups/$groupId", "project/GroupDetailsByIDPage/route-cert-manager.tsx") ]) ]); @@ -326,7 +328,8 @@ const kmsRoutes = route("/kms/$projectId", [ route("/access-management", "project/AccessControlPage/route-kms.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-kms.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-kms.tsx"), - route("/members/$membershipId", "project/MemberDetailsByIDPage/route-kms.tsx") + route("/members/$membershipId", "project/MemberDetailsByIDPage/route-kms.tsx"), + route("/groups/$groupId", "project/GroupDetailsByIDPage/route-kms.tsx") ]) ]); @@ -341,7 +344,8 @@ const sshRoutes = route("/ssh/$projectId", [ route("/access-management", "project/AccessControlPage/route-ssh.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-ssh.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-ssh.tsx"), - route("/members/$membershipId", "project/MemberDetailsByIDPage/route-ssh.tsx") + route("/members/$membershipId", "project/MemberDetailsByIDPage/route-ssh.tsx"), + route("/groups/$groupId", "project/GroupDetailsByIDPage/route-ssh.tsx") ]) ]); @@ -356,7 +360,8 @@ const secretScanningRoutes = route("/secret-scanning/$projectId", [ route("/access-management", "project/AccessControlPage/route-secret-scanning.tsx"), route("/roles/$roleSlug", "project/RoleDetailsBySlugPage/route-secret-scanning.tsx"), route("/identities/$identityId", "project/IdentityDetailsByIDPage/route-secret-scanning.tsx"), - route("/members/$membershipId", "project/MemberDetailsByIDPage/route-secret-scanning.tsx") + route("/members/$membershipId", "project/MemberDetailsByIDPage/route-secret-scanning.tsx"), + route("/groups/$groupId", "project/GroupDetailsByIDPage/route-secret-scanning.tsx") ]) ]); From ea5a5e0aa769d02bb0e5f92294d65cbbb7068c0a Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Fri, 6 Jun 2025 14:13:18 -0700 Subject: [PATCH 2/2] improvements: address feedback --- .../server/routes/v2/group-project-router.ts | 3 + .../components/GroupsSection/GroupRoles.tsx | 247 ++++++++++-------- .../GroupDetailsByIDPage.tsx | 2 +- .../components/GroupDetailsSection.tsx | 2 +- 4 files changed, 139 insertions(+), 115 deletions(-) diff --git a/backend/src/server/routes/v2/group-project-router.ts b/backend/src/server/routes/v2/group-project-router.ts index 8ed03ca08..d07e3bd8b 100644 --- a/backend/src/server/routes/v2/group-project-router.ts +++ b/backend/src/server/routes/v2/group-project-router.ts @@ -308,6 +308,9 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) => method: "GET", url: "/:projectId/groups/:groupId/users", onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + config: { + rateLimit: readLimit + }, schema: { hide: false, tags: [ApiDocsTags.ProjectGroups], diff --git a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx index c3f255a9b..91b6d3186 100644 --- a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupRoles.tsx @@ -29,6 +29,7 @@ import { formatProjectRoleName } from "@app/helpers/roles"; import { usePopUp } from "@app/hooks"; import { useGetProjectRoles, useUpdateGroupWorkspaceRole } from "@app/hooks/api"; import { TGroupMembership } from "@app/hooks/api/groups/types"; +import { TProjectRole } from "@app/hooks/api/roles/types"; import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types"; import { groupBy } from "@app/lib/fn/array"; @@ -204,17 +205,22 @@ export type TMemberRolesProp = { const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; -export const GroupRoles = ({ - roles = [], - disableEdit = false, - groupId, - className, - popperContentProps -}: TMemberRolesProp) => { +type FormProps = { + projectRoles: Omit[] | undefined; + roles: TGroupMembership["roles"]; + groupId: string; + onClose: VoidFunction; +}; + +const GroupRolesForm = ({ projectRoles, roles, groupId, onClose }: FormProps) => { const { currentWorkspace } = useWorkspace(); - const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const); + const [searchRoles, setSearchRoles] = useState(""); + const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role); + + const updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole(); + const { handleSubmit, control, @@ -224,13 +230,6 @@ export const GroupRoles = ({ resolver: zodResolver(formSchema) }); - const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles( - currentWorkspace?.id ?? "" - ); - const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role); - - const updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole(); - const handleRoleUpdate = async (data: TForm) => { const selectedRoles = Object.keys(data) .filter((el) => Boolean(data[el].isChecked)) @@ -261,13 +260,126 @@ export const GroupRoles = ({ roles: selectedRoles }); createNotification({ text: "Successfully updated group role", type: "success" }); - handlePopUpToggle("editRole"); + onClose(); setSearchRoles(""); } catch { createNotification({ text: "Failed to update group role", type: "error" }); } }; + return ( +
+
+ {projectRoles + ?.filter( + ({ name, slug }) => + name.toLowerCase().includes(searchRoles.toLowerCase()) || + slug.toLowerCase().includes(searchRoles.toLowerCase()) + ) + ?.map(({ id, name, slug }) => { + const userProjectRoleDetails = userRolesGroupBySlug?.[slug]?.[0]; + + return ( +
+
+ ( + { + field.onChange(isChecked); + setValue(`${slug}.temporaryAccess`, false); + }} + > + {name} + + )} + /> +
+
+ ( + { + setValue(`${slug}.isChecked`, true, { shouldDirty: true }); + field.onChange({ isTemporary: true, ...data }); + }} + onRemoveTemporary={() => { + setValue(`${slug}.isChecked`, false, { shouldDirty: true }); + field.onChange(false); + }} + /> + )} + /> +
+
+ ); + })} +
+
+
+ setSearchRoles(el.target.value)} + leftIcon={} + placeholder="Search roles.." + /> +
+
+ +
+
+
+ ); +}; + +export const GroupRoles = ({ + roles = [], + disableEdit = false, + groupId, + className, + popperContentProps +}: TMemberRolesProp) => { + const { currentWorkspace } = useWorkspace(); + const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const); + + const { data: projectRoles, isPending: isRolesLoading } = useGetProjectRoles( + currentWorkspace?.id ?? "" + ); + return (
{roles @@ -353,103 +465,12 @@ export const GroupRoles = ({
) : ( -
-
- {projectRoles - ?.filter( - ({ name, slug }) => - name.toLowerCase().includes(searchRoles.toLowerCase()) || - slug.toLowerCase().includes(searchRoles.toLowerCase()) - ) - ?.map(({ id, name, slug }) => { - const userProjectRoleDetails = userRolesGroupBySlug?.[slug]?.[0]; - - return ( -
-
- ( - { - field.onChange(isChecked); - setValue(`${slug}.temporaryAccess`, false); - }} - > - {name} - - )} - /> -
-
- ( - { - setValue(`${slug}.isChecked`, true, { shouldDirty: true }); - field.onChange({ isTemporary: true, ...data }); - }} - onRemoveTemporary={() => { - setValue(`${slug}.isChecked`, false, { shouldDirty: true }); - field.onChange(false); - }} - /> - )} - /> -
-
- ); - })} -
-
-
- setSearchRoles(el.target.value)} - leftIcon={} - placeholder="Search roles.." - /> -
-
- -
-
-
+ handlePopUpToggle("editRole")} + /> )} diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx index 393419e64..a3dfabcd0 100644 --- a/frontend/src/pages/project/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx +++ b/frontend/src/pages/project/GroupDetailsByIDPage/GroupDetailsByIDPage.tsx @@ -54,7 +54,7 @@ export const GroupDetailsByIDPage = () => { return ( <> - {t("common.head-title", { title: t("settings.members.title") })} + {t("common.head-title", { title: "Project Group" })} { }); createNotification({ - text: "Successfully removed identity from project", + text: "Successfully removed group from project", type: "success" });