feat: added wizard and fixed assume privilege issue

This commit is contained in:
=
2025-11-08 16:53:23 +05:30
parent 4850889854
commit 43c23bfa12
7 changed files with 266 additions and 209 deletions

View File

@@ -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);
}
}

View File

@@ -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(
</Button>
)}
</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>
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
@@ -202,7 +181,6 @@ export const IdentitySection = withPermission(
</div>
<IdentityAuthTemplatesTable handlePopUpOpen={handlePopUpOpen} />
</div>
<OrgIdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<IdentityAuthTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<MachineAuthTemplateUsagesModal
isOpen={popUp.viewUsages.isOpen}
@@ -216,19 +194,99 @@ export const IdentitySection = withPermission(
?.name || ""
}
/>
<IdentityTokenAuthTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<Modal
isOpen={popUp.linkIdentity.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("linkIdentity", isOpen)}
isOpen={popUp.identity.isOpen}
onOpenChange={(open) => {
handlePopUpToggle("identity", open);
if (!open) {
setWizardStep(IdentityWizardSteps.SelectAction);
}
}}
>
<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"
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>
</Modal>
<IdentityTokenAuthTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure you want to delete ${

View File

@@ -173,164 +173,147 @@ export const OrgIdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
};
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)}>
{isOrgIdentity && (
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-4"
label="Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Machine 1" />
</FormControl>
)}
/>
<form onSubmit={handleSubmit(onFormSubmit)}>
{isOrgIdentity && (
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-4"
label="Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Machine 1" />
</FormControl>
)}
<Controller
control={control}
name="role"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.identity?.data ? "Update" : ""} Role`}
errorText={error?.message}
isError={Boolean(error)}
/>
)}
<Controller
control={control}
name="role"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
label={`${popUp?.identity?.data ? "Update" : ""} Role`}
errorText={error?.message}
isError={Boolean(error)}
>
<FilterableSelect
placeholder="Select role..."
options={roles}
onChange={onChange}
value={value}
getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
{isOrgIdentity && (
<Controller
control={control}
name="hasDeleteProtection"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)}>
<Switch
className="mr-2 ml-0 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
containerClassName="flex-row-reverse w-fit"
id="delete-protection-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<FilterableSelect
placeholder="Select role..."
options={roles}
onChange={onChange}
value={value}
getOptionValue={(option) => option.slug}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
{isOrgIdentity && (
<Controller
control={control}
name="hasDeleteProtection"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl errorText={error?.message} isError={Boolean(error)}>
<Switch
className="mr-2 ml-0 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
containerClassName="flex-row-reverse w-fit"
id="delete-protection-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
<p>Delete Protection {value ? "Enabled" : "Disabled"}</p>
</Switch>
</FormControl>
)}
/>
<p>Delete Protection {value ? "Enabled" : "Disabled"}</p>
</Switch>
</FormControl>
)}
{isOrgIdentity && (
<>
<div>
<FormLabel label="Metadata" />
</div>
<div className="mb-3 flex flex-col space-y-2">
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
<div key={metadataFieldId} className="flex items-end space-x-2">
<div className="grow">
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`metadata.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div className="grow">
{i === 0 && (
<FormLabel
label="Value"
className="text-xs text-mineshaft-400"
isOptional
/>
)}
<Controller
control={control}
name={`metadata.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="delete key"
className="bottom-0.5 h-9"
variant="outline_bg"
onClick={() => metadataFormFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div className="mt-2 flex justify-end">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => metadataFormFields.append({ key: "", value: "" })}
>
Add Key
</Button>
</div>
</div>
</>
)}
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.identity?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identity", false)}
>
Cancel
</Button>
/>
)}
{isOrgIdentity && (
<>
<div>
<FormLabel label="Metadata" />
</div>
</form>
</ModalContent>
</Modal>
<div className="mb-3 flex flex-col space-y-2">
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
<div key={metadataFieldId} className="flex items-end space-x-2">
<div className="grow">
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`metadata.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div className="grow">
{i === 0 && (
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
)}
<Controller
control={control}
name={`metadata.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="delete key"
className="bottom-0.5 h-9"
variant="outline_bg"
onClick={() => metadataFormFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div className="mt-2 flex justify-end">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => metadataFormFields.append({ key: "", value: "" })}
>
Add Key
</Button>
</div>
</div>
</>
)}
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.identity?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identity", false)}
>
Cancel
</Button>
</div>
</form>
);
};

View File

@@ -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 = () => {
</div>
</div>
)}
<OrgIdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<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} />
</ModalContent>
</Modal>
<IdentityAuthMethodModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}

View File

@@ -24,7 +24,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { useProject } from "@app/context";
import { useOrganization, useProject } from "@app/context";
import { getProjectHomePage } from "@app/helpers/project";
import {
getUserTablePreference,
@@ -86,6 +86,7 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}