feat: added wizard and fixed assume privilege issue

This commit is contained in:
=
2025-11-13 12:52:40 +05:30
parent 4850889854
commit 43c23bfa12
7 changed files with 266 additions and 209 deletions
@@ -2,12 +2,13 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "@app/components/v2"; 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 { getProjectHomePage } from "@app/helpers/project";
import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api"; import { useRemoveAssumeProjectPrivilege } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums"; import { ActorType } from "@app/hooks/api/auditLogs/enums";
export const AssumePrivilegeModeBanner = () => { export const AssumePrivilegeModeBanner = () => {
const { isSubOrganization, currentOrg } = useOrganization();
const { currentProject } = useProject(); const { currentProject } = useProject();
const exitAssumePrivilegeMode = useRemoveAssumeProjectPrivilege(); const exitAssumePrivilegeMode = useRemoveAssumeProjectPrivilege();
const { assumedPrivilegeDetails } = useProjectPermission(); const { assumedPrivilegeDetails } = useProjectPermission();
@@ -36,7 +37,7 @@ export const AssumePrivilegeModeBanner = () => {
}, },
{ {
onSuccess: () => { 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); window.location.href = url.replace("$projectId", currentProject.id);
} }
} }
@@ -22,7 +22,7 @@ import {
} from "@app/context"; } from "@app/context";
import { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types"; import { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc"; 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 { useDeleteIdentityAuthTemplate } from "@app/hooks/api/identityAuthTemplates";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
@@ -33,6 +33,15 @@ import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal";
import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal"; import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal";
import { OrgIdentityLinkForm } from "./OrgIdentityLinkForm"; import { OrgIdentityLinkForm } from "./OrgIdentityLinkForm";
import { OrgIdentityModal } from "./OrgIdentityModal"; 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( export const IdentitySection = withPermission(
() => { () => {
@@ -40,6 +49,8 @@ export const IdentitySection = withPermission(
const { currentOrg, isSubOrganization } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const [wizardStep, setWizardStep] = useState(IdentityWizardSteps.SelectAction);
const { mutateAsync: deleteMutateAsync } = useDeleteOrgIdentity(); const { mutateAsync: deleteMutateAsync } = useDeleteOrgIdentity();
const { mutateAsync: deleteTemplateMutateAsync } = useDeleteIdentityAuthTemplate(); const { mutateAsync: deleteTemplateMutateAsync } = useDeleteIdentityAuthTemplate();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -54,7 +65,6 @@ export const IdentitySection = withPermission(
"editTemplate", "editTemplate",
"deleteTemplate", "deleteTemplate",
"viewUsages", "viewUsages",
"linkIdentity",
"addOptions" "addOptions"
] as const); ] as const);
@@ -119,6 +129,11 @@ export const IdentitySection = withPermission(
}); });
return; return;
} }
if (!isSubOrganization) {
setWizardStep(IdentityWizardSteps.OrganizationIdentity);
}
handlePopUpOpen("identity"); handlePopUpOpen("identity");
}} }}
isDisabled={!isAllowed} isDisabled={!isAllowed}
@@ -127,42 +142,6 @@ export const IdentitySection = withPermission(
</Button> </Button>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
{isSubOrganization && (
<DropdownMenu
open={popUp.addOptions.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addOptions", isOpen)}
>
<DropdownMenuTrigger>
<Button
variant="outline_bg"
className="rounded-l-none border-l-mineshaft-800 px-3"
>
<FontAwesomeIcon icon={faChevronDown} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={6} className="p-1">
<OrgPermissionCan
I={OrgPermissionIdentityActions.Create}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Button
variant="outline_bg"
className="w-full"
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faLink} />}
onClick={() => {
handlePopUpClose("addOptions");
handlePopUpOpen("linkIdentity");
}}
>
Assign Org Identity
</Button>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
)}
</div> </div>
</div> </div>
<IdentityTable handlePopUpOpen={handlePopUpOpen} /> <IdentityTable handlePopUpOpen={handlePopUpOpen} />
@@ -202,7 +181,6 @@ export const IdentitySection = withPermission(
</div> </div>
<IdentityAuthTemplatesTable handlePopUpOpen={handlePopUpOpen} /> <IdentityAuthTemplatesTable handlePopUpOpen={handlePopUpOpen} />
</div> </div>
<OrgIdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<IdentityAuthTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <IdentityAuthTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<MachineAuthTemplateUsagesModal <MachineAuthTemplateUsagesModal
isOpen={popUp.viewUsages.isOpen} isOpen={popUp.viewUsages.isOpen}
@@ -216,19 +194,99 @@ export const IdentitySection = withPermission(
?.name || "" ?.name || ""
} }
/> />
<IdentityTokenAuthTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<Modal <Modal
isOpen={popUp.linkIdentity.isOpen} isOpen={popUp.identity.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("linkIdentity", isOpen)} onOpenChange={(open) => {
handlePopUpToggle("identity", open);
if (!open) {
setWizardStep(IdentityWizardSteps.SelectAction);
}
}}
> >
<ModalContent <ModalContent
title="Assign Existing Identity"
subTitle="Assign an existing identity from your root organization to this sub organization. The identity will continue to be managed at its original scope."
bodyClassName="overflow-visible" bodyClassName="overflow-visible"
title="Add Identity"
subTitle={
isSubOrganization ? "Create a new identity or assign an existing identity" : undefined
}
> >
<OrgIdentityLinkForm onClose={() => handlePopUpClose("linkIdentity")} /> <AnimatePresence mode="wait">
{wizardStep === IdentityWizardSteps.SelectAction && (
<motion.div
key="select-type-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<div
className="cursor-pointer rounded-md border border-mineshaft-600 p-4 transition-all hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onClick={() => setWizardStep(IdentityWizardSteps.OrganizationIdentity)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setWizardStep(IdentityWizardSteps.OrganizationIdentity);
}
}}
>
<div className="flex items-center gap-2">
<PlusIcon size="1rem" />
<div>Create New Identity</div>
</div>
<div className="mt-2 text-xs text-mineshaft-300">
Create a new machine identity specifically for this sub-organization. This
identity will be managed at the sub-organization level.
</div>
</div>
<div
className="mt-4 cursor-pointer rounded-md border border-mineshaft-600 p-4 transition-all hover:bg-mineshaft-700"
role="button"
tabIndex={0}
onClick={() => setWizardStep(IdentityWizardSteps.LinkIdentity)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setWizardStep(IdentityWizardSteps.LinkIdentity);
}
}}
>
<div className="flex items-center gap-2">
<LinkIcon size="1rem" />
<div>Assign Existing Identity</div>
</div>
<div className="mt-2 text-xs text-mineshaft-300">
Assign an existing identity from your parent organization. The identity will
continue to be managed at its original scope.
</div>
</div>
</motion.div>
)}
{wizardStep === IdentityWizardSteps.OrganizationIdentity && (
<motion.div
key="identity-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<OrgIdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</motion.div>
)}
{wizardStep === IdentityWizardSteps.LinkIdentity && (
<motion.div
key="link-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<OrgIdentityLinkForm onClose={() => handlePopUpClose("identity")} />
</motion.div>
)}
</AnimatePresence>
</ModalContent> </ModalContent>
</Modal> </Modal>
<IdentityTokenAuthTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen} isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure you want to delete ${ title={`Are you sure you want to delete ${
@@ -173,17 +173,6 @@ export const OrgIdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
}; };
return ( return (
<Modal
isOpen={popUp?.identity?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("identity", isOpen);
reset();
}}
>
<ModalContent
bodyClassName="overflow-visible"
title={`${popUp?.identity?.data ? "Update" : "Create"} Identity`}
>
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
{isOrgIdentity && ( {isOrgIdentity && (
<Controller <Controller
@@ -268,11 +257,7 @@ export const OrgIdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
</div> </div>
<div className="grow"> <div className="grow">
{i === 0 && ( {i === 0 && (
<FormLabel <FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
label="Value"
className="text-xs text-mineshaft-400"
isOptional
/>
)} )}
<Controller <Controller
control={control} control={control}
@@ -330,7 +315,5 @@ export const OrgIdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
</Button> </Button>
</div> </div>
</form> </form>
</ModalContent>
</Modal>
); );
}; };
@@ -7,7 +7,7 @@ import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; 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 { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteOrgIdentity, useGetOrgIdentityMembershipById } from "@app/hooks/api"; import { useDeleteOrgIdentity, useGetOrgIdentityMembershipById } from "@app/hooks/api";
@@ -100,7 +100,17 @@ const Page = () => {
</div> </div>
</div> </div>
)} )}
<Modal
isOpen={popUp?.identity?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("identity", isOpen)}
>
<ModalContent
bodyClassName="overflow-visible"
title={`${popUp?.identity?.data ? "Update" : "Create"} Identity`}
>
<OrgIdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <OrgIdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</ModalContent>
</Modal>
<IdentityAuthMethodModal <IdentityAuthMethodModal
popUp={popUp} popUp={popUp}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
@@ -24,7 +24,7 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useProject } from "@app/context"; import { useOrganization, useProject } from "@app/context";
import { getProjectHomePage } from "@app/helpers/project"; import { getProjectHomePage } from "@app/helpers/project";
import { import {
getUserTablePreference, getUserTablePreference,
@@ -86,6 +86,7 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
setUserTablePreference("projectGroupMembersTable", PreferenceKey.PerPage, newPerPage); setUserTablePreference("projectGroupMembersTable", PreferenceKey.PerPage, newPerPage);
}; };
const { isSubOrganization, currentOrg } = useOrganization();
const { currentProject } = useProject(); const { currentProject } = useProject();
const { data: groupMemberships, isPending } = useListProjectGroupUsers({ const { data: groupMemberships, isPending } = useListProjectGroupUsers({
@@ -153,7 +154,7 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
text: "User privilege assumption has started" 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); window.location.href = url.replace("$projectId", currentProject.id);
} }
} }
@@ -21,6 +21,7 @@ import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionIdentityActions, ProjectPermissionIdentityActions,
ProjectPermissionSub, ProjectPermissionSub,
useOrganization,
useProject useProject
} from "@app/context"; } from "@app/context";
import { getProjectBaseURL, getProjectHomePage } from "@app/helpers/project"; import { getProjectBaseURL, getProjectHomePage } from "@app/helpers/project";
@@ -46,6 +47,7 @@ const Page = () => {
select: (el) => el.identityId as string select: (el) => el.identityId as string
}); });
const { currentProject, projectId } = useProject(); const { currentProject, projectId } = useProject();
const { currentOrg, isSubOrganization } = useOrganization();
const { data: identityMembershipDetails, isPending: isMembershipDetailsLoading } = const { data: identityMembershipDetails, isPending: isMembershipDetailsLoading } =
useGetProjectIdentityMembershipV2(projectId, identityId); useGetProjectIdentityMembershipV2(projectId, identityId);
@@ -54,6 +56,8 @@ const Page = () => {
useDeleteProjectIdentityMembership(); useDeleteProjectIdentityMembership();
const isProjectIdentity = Boolean(identityMembershipDetails?.identity.projectId); const isProjectIdentity = Boolean(identityMembershipDetails?.identity.projectId);
const isNonScopedIdentity =
!isProjectIdentity && currentOrg.id !== identityMembershipDetails?.identity?.orgId;
const { const {
data: identity, data: identity,
@@ -86,7 +90,7 @@ const Page = () => {
type: "success", type: "success",
text: "Identity privilege assumption has started" 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); window.location.href = url.replace("$projectId", currentProject.id);
} }
} }
@@ -42,7 +42,7 @@ export const Page = () => {
strict: false, strict: false,
select: (el) => el.membershipId as string select: (el) => el.membershipId as string
}); });
const { currentOrg } = useOrganization(); const { currentOrg, isSubOrganization } = useOrganization();
const { currentProject, projectId } = useProject(); const { currentProject, projectId } = useProject();
const { data: membershipDetails, isPending: isMembershipDetailsLoading } = const { data: membershipDetails, isPending: isMembershipDetailsLoading } =
@@ -73,7 +73,7 @@ export const Page = () => {
text: "User privilege assumption has started" 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); window.location.href = url.replace("$projectId", currentProject.id);
} }
} }