From 43c23bfa12b45041115cdb33a70fb941b2835cf9 Mon Sep 17 00:00:00 2001 From: = Date: Sat, 8 Nov 2025 16:53:23 +0530 Subject: [PATCH] feat: added wizard and fixed assume privilege issue --- .../AssumePrivilegeModeBanner.tsx | 5 +- .../IdentitySection/IdentitySection.tsx | 148 ++++++--- .../IdentitySection/OrgIdentityModal.tsx | 293 +++++++++--------- .../IdentityDetailsByIDPage.tsx | 14 +- .../GroupMembersSection/GroupMembersTable.tsx | 5 +- .../IdentityDetailsByIDPage.tsx | 6 +- .../MemberDetailsByIDPage.tsx | 4 +- 7 files changed, 266 insertions(+), 209 deletions(-) diff --git a/frontend/src/layouts/ProjectLayout/components/AssumePrivilegeModeBanner/AssumePrivilegeModeBanner.tsx b/frontend/src/layouts/ProjectLayout/components/AssumePrivilegeModeBanner/AssumePrivilegeModeBanner.tsx index 45978e3d7..6afdc2c98 100644 --- a/frontend/src/layouts/ProjectLayout/components/AssumePrivilegeModeBanner/AssumePrivilegeModeBanner.tsx +++ b/frontend/src/layouts/ProjectLayout/components/AssumePrivilegeModeBanner/AssumePrivilegeModeBanner.tsx @@ -2,12 +2,13 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button } from "@app/components/v2"; -import { useProject, useProjectPermission } from "@app/context"; +import { useOrganization, useProject, useProjectPermission } from "@app/context"; import { getProjectHomePage } from "@app/helpers/project"; import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api"; import { ActorType } from "@app/hooks/api/auditLogs/enums"; export const AssumePrivilegeModeBanner = () => { + const { isSubOrganization, currentOrg } = useOrganization(); const { currentProject } = useProject(); const exitAssumePrivilegeMode = useRemoveAssumeProjectPrivilege(); const { assumedPrivilegeDetails } = useProjectPermission(); @@ -36,7 +37,7 @@ export const AssumePrivilegeModeBanner = () => { }, { onSuccess: () => { - const url = getProjectHomePage(currentProject.type, currentProject.environments); + const url = `${getProjectHomePage(currentProject.type, currentProject.environments)}${isSubOrganization ? `?subOrganization=${currentOrg.slug}` : ""}`; window.location.href = url.replace("$projectId", currentProject.id); } } diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index 674302459..a8c00b05a 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -22,7 +22,7 @@ import { } from "@app/context"; import { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types"; import { withPermission } from "@app/hoc"; -import { useDeleteOrgIdentity } from "@app/hooks/api"; +import { subOrganizationsQuery, useDeleteOrgIdentity } from "@app/hooks/api"; import { useDeleteIdentityAuthTemplate } from "@app/hooks/api/identityAuthTemplates"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -33,6 +33,15 @@ import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal"; import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal"; import { OrgIdentityLinkForm } from "./OrgIdentityLinkForm"; import { OrgIdentityModal } from "./OrgIdentityModal"; +import { useState } from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { LinkIcon, PlusIcon } from "lucide-react"; + +enum IdentityWizardSteps { + SelectAction = "select-action", + LinkIdentity = "link-identity", + OrganizationIdentity = "project-identity" +} export const IdentitySection = withPermission( () => { @@ -40,6 +49,8 @@ export const IdentitySection = withPermission( const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id || ""; + const [wizardStep, setWizardStep] = useState(IdentityWizardSteps.SelectAction); + const { mutateAsync: deleteMutateAsync } = useDeleteOrgIdentity(); const { mutateAsync: deleteTemplateMutateAsync } = useDeleteIdentityAuthTemplate(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -54,7 +65,6 @@ export const IdentitySection = withPermission( "editTemplate", "deleteTemplate", "viewUsages", - "linkIdentity", "addOptions" ] as const); @@ -119,6 +129,11 @@ export const IdentitySection = withPermission( }); return; } + + if (!isSubOrganization) { + setWizardStep(IdentityWizardSteps.OrganizationIdentity); + } + handlePopUpOpen("identity"); }} isDisabled={!isAllowed} @@ -127,42 +142,6 @@ export const IdentitySection = withPermission( )} - {isSubOrganization && ( - handlePopUpToggle("addOptions", isOpen)} - > - - - - - - {(isAllowed) => ( - - )} - - - - )} @@ -202,7 +181,6 @@ export const IdentitySection = withPermission( - + handlePopUpToggle("linkIdentity", isOpen)} + isOpen={popUp.identity.isOpen} + onOpenChange={(open) => { + handlePopUpToggle("identity", open); + if (!open) { + setWizardStep(IdentityWizardSteps.SelectAction); + } + }} > - handlePopUpClose("linkIdentity")} /> + + {wizardStep === IdentityWizardSteps.SelectAction && ( + +
setWizardStep(IdentityWizardSteps.OrganizationIdentity)} + onKeyDown={(e) => { + if (e.key === "Enter") { + setWizardStep(IdentityWizardSteps.OrganizationIdentity); + } + }} + > +
+ +
Create New Identity
+
+
+ Create a new machine identity specifically for this sub-organization. This + identity will be managed at the sub-organization level. +
+
+
setWizardStep(IdentityWizardSteps.LinkIdentity)} + onKeyDown={(e) => { + if (e.key === "Enter") { + setWizardStep(IdentityWizardSteps.LinkIdentity); + } + }} + > +
+ +
Assign Existing Identity
+
+
+ Assign an existing identity from your parent organization. The identity will + continue to be managed at its original scope. +
+
+
+ )} + {wizardStep === IdentityWizardSteps.OrganizationIdentity && ( + + + + )} + {wizardStep === IdentityWizardSteps.LinkIdentity && ( + + handlePopUpClose("identity")} /> + + )} +
- { }; return ( - { - handlePopUpToggle("identity", isOpen); - reset(); - }} - > - -
- {isOrgIdentity && ( - ( - - - - )} - /> + + {isOrgIdentity && ( + ( + + + )} - ( - + )} + ( + + option.slug} + getOptionLabel={(option) => option.name} + /> + + )} + /> + {isOrgIdentity && ( + ( + + - option.slug} - getOptionLabel={(option) => option.name} - /> - - )} - /> - {isOrgIdentity && ( - ( - - -

Delete Protection {value ? "Enabled" : "Disabled"}

-
-
- )} - /> +

Delete Protection {value ? "Enabled" : "Disabled"}

+ +
)} - {isOrgIdentity && ( - <> -
- -
-
- {metadataFormFields.fields.map(({ id: metadataFieldId }, i) => ( -
-
- {i === 0 && Key} - ( - - - - )} - /> -
-
- {i === 0 && ( - - )} - ( - - - - )} - /> -
- metadataFormFields.remove(i)} - > - - -
- ))} -
- -
-
- - )} -
- - + /> + )} + {isOrgIdentity && ( + <> +
+
- - - +
+ {metadataFormFields.fields.map(({ id: metadataFieldId }, i) => ( +
+
+ {i === 0 && Key} + ( + + + + )} + /> +
+
+ {i === 0 && ( + + )} + ( + + + + )} + /> +
+ metadataFormFields.remove(i)} + > + + +
+ ))} +
+ +
+
+ + )} +
+ + +
+ ); }; diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index db682e5ab..ad5f1839c 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -7,7 +7,7 @@ import { Link, useNavigate, useParams } from "@tanstack/react-router"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; -import { DeleteActionModal, PageHeader } from "@app/components/v2"; +import { DeleteActionModal, Modal, ModalContent, PageHeader } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { useDeleteOrgIdentity, useGetOrgIdentityMembershipById } from "@app/hooks/api"; @@ -100,7 +100,17 @@ const Page = () => {
)} - + handlePopUpToggle("identity", isOpen)} + > + + + + { setUserTablePreference("projectGroupMembersTable", PreferenceKey.PerPage, newPerPage); }; + const { isSubOrganization, currentOrg } = useOrganization(); const { currentProject } = useProject(); const { data: groupMemberships, isPending } = useListProjectGroupUsers({ @@ -153,7 +154,7 @@ export const GroupMembersTable = ({ groupMembership }: Props) => { text: "User privilege assumption has started" }); - const url = getProjectHomePage(currentProject.type, currentProject.environments); + const url = `${getProjectHomePage(currentProject.type, currentProject.environments)}${isSubOrganization ? `?subOrganization=${currentOrg.slug}` : ""}`; window.location.href = url.replace("$projectId", currentProject.id); } } diff --git a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx index 8b77712e1..9137021ff 100644 --- a/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx +++ b/frontend/src/pages/project/IdentityDetailsByIDPage/IdentityDetailsByIDPage.tsx @@ -21,6 +21,7 @@ import { ProjectPermissionActions, ProjectPermissionIdentityActions, ProjectPermissionSub, + useOrganization, useProject } from "@app/context"; import { getProjectBaseURL, getProjectHomePage } from "@app/helpers/project"; @@ -46,6 +47,7 @@ const Page = () => { select: (el) => el.identityId as string }); const { currentProject, projectId } = useProject(); + const { currentOrg, isSubOrganization } = useOrganization(); const { data: identityMembershipDetails, isPending: isMembershipDetailsLoading } = useGetProjectIdentityMembershipV2(projectId, identityId); @@ -54,6 +56,8 @@ const Page = () => { useDeleteProjectIdentityMembership(); const isProjectIdentity = Boolean(identityMembershipDetails?.identity.projectId); + const isNonScopedIdentity = + !isProjectIdentity && currentOrg.id !== identityMembershipDetails?.identity?.orgId; const { data: identity, @@ -86,7 +90,7 @@ const Page = () => { type: "success", text: "Identity privilege assumption has started" }); - const url = getProjectHomePage(currentProject.type, currentProject.environments); + const url = `${getProjectHomePage(currentProject.type, currentProject.environments)}${isSubOrganization && isNonScopedIdentity ? `?subOrganization=${currentOrg.slug}` : ""}`; window.location.href = url.replace("$projectId", currentProject.id); } } diff --git a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx index 5d8ae812c..2c578ea4f 100644 --- a/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx +++ b/frontend/src/pages/project/MemberDetailsByIDPage/MemberDetailsByIDPage.tsx @@ -42,7 +42,7 @@ export const Page = () => { strict: false, select: (el) => el.membershipId as string }); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const { currentProject, projectId } = useProject(); const { data: membershipDetails, isPending: isMembershipDetailsLoading } = @@ -73,7 +73,7 @@ export const Page = () => { text: "User privilege assumption has started" }); - const url = getProjectHomePage(currentProject.type, currentProject.environments); + const url = `${getProjectHomePage(currentProject.type, currentProject.environments)}${isSubOrganization ? `?subOrganization=${currentOrg.slug}` : ""}`; window.location.href = url.replace("$projectId", currentProject.id); } }