feat: switched to a wizard for create identity in project

This commit is contained in:
=
2025-11-13 12:52:39 +05:30
parent 4bb13d592f
commit d13a0dc5b6
5 changed files with 119 additions and 98 deletions
@@ -1,18 +1,19 @@
import { useState } from "react";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { import {
faArrowDown, faArrowDown,
faArrowUp, faArrowUp,
faChevronDown,
faCircleXmark, faCircleXmark,
faClock, faClock,
faEllipsisV, faEllipsisV,
faLink,
faMagnifyingGlass, faMagnifyingGlass,
faPlus, faPlus,
faServer faServer
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
import { LinkIcon, PlusIcon } from "lucide-react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -30,6 +31,8 @@ import {
HoverCardTrigger, HoverCardTrigger,
IconButton, IconButton,
Input, Input,
Modal,
ModalContent,
Pagination, Pagination,
Spinner, Spinner,
Table, Table,
@@ -79,12 +82,20 @@ import { ProjectLinkIdentityModal } from "./components/ProjectLinkIdentityModal"
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
enum WizardSteps {
SelectAction = "select-action",
LinkIdentity = "link-identity",
ProjectIdentity = "project-identity"
}
export const IdentityTab = withProjectPermission( export const IdentityTab = withProjectPermission(
() => { () => {
const { currentProject, projectId } = useProject(); const { currentProject, projectId } = useProject();
const navigate = useNavigate(); const navigate = useNavigate();
const { isSubOrganization, currentOrg } = useOrganization(); const { isSubOrganization, currentOrg } = useOrganization();
const [wizardStep, setWizardStep] = useState(WizardSteps.SelectAction);
const { const {
offset, offset,
limit, limit,
@@ -133,7 +144,6 @@ export const IdentityTab = withProjectPermission(
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"createIdentity", "createIdentity",
"linkIdentity",
"deleteIdentity", "deleteIdentity",
"upgradePlan", "upgradePlan",
"addOptions" "addOptions"
@@ -201,37 +211,6 @@ export const IdentityTab = withProjectPermission(
</Button> </Button>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
<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">
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Identity}
>
{(isAllowed) => (
<Button
variant="outline_bg"
className="w-full"
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faLink} />}
onClick={() => {
handlePopUpOpen("linkIdentity");
handlePopUpClose("addOptions");
}}
>
Assign Org Identity
</Button>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</div> </div>
<Input <Input
@@ -494,11 +473,94 @@ export const IdentityTab = withProjectPermission(
/> />
)} )}
</TableContainer> </TableContainer>
<ProjectIdentityModal <Modal
isOpen={popUp.createIdentity.isOpen} isOpen={popUp.createIdentity.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("createIdentity", isOpen)} onOpenChange={(open) => {
/> handlePopUpToggle("createIdentity", open);
<ProjectLinkIdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> if (!open) setWizardStep(WizardSteps.SelectAction);
}}
>
<ModalContent
bodyClassName="overflow-visible"
title="Add Project Identity"
subTitle="Create a new identity or assign an existing identity"
>
<AnimatePresence mode="wait">
{wizardStep === WizardSteps.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(WizardSteps.ProjectIdentity)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setWizardStep(WizardSteps.ProjectIdentity);
}
}}
>
<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 project. This identity
will be managed at the project-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(WizardSteps.LinkIdentity)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setWizardStep(WizardSteps.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 organization or namespace to this
project. The identity will continue to be managed at its original scope.
</div>
</div>
</motion.div>
)}
{wizardStep === WizardSteps.ProjectIdentity && (
<motion.div
key="identity-step"
transition={{ duration: 0.1 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<ProjectIdentityModal onClose={() => handlePopUpClose("createIdentity")} />
</motion.div>
)}
{wizardStep === WizardSteps.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 }}
>
<ProjectLinkIdentityModal handlePopUpToggle={handlePopUpToggle} />
</motion.div>
)}
</AnimatePresence>
</ModalContent>
</Modal>
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen} isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure you want to remove ${ title={`Are you sure you want to remove ${
@@ -13,8 +13,6 @@ import {
FormLabel, FormLabel,
IconButton, IconButton,
Input, Input,
Modal,
ModalContent,
Switch Switch
} from "@app/components/v2"; } from "@app/components/v2";
import { useProject } from "@app/context"; import { useProject } from "@app/context";
@@ -49,7 +47,7 @@ type ContentProps = {
identity?: TProjectIdentity; identity?: TProjectIdentity;
}; };
const Content = ({ onClose, identity }: ContentProps) => { export const ProjectIdentityModal = ({ onClose, identity }: ContentProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const { currentProject } = useProject(); const { currentProject } = useProject();
@@ -289,22 +287,3 @@ const Content = ({ onClose, identity }: ContentProps) => {
</form> </form>
); );
}; };
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
identity?: TProjectIdentity;
};
export const ProjectIdentityModal = ({ isOpen, onOpenChange, identity }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={(open) => onOpenChange(open)}>
<ModalContent
bodyClassName="overflow-visible"
title={`${identity ? "Update" : "Create"} Project Identity`}
>
<Content identity={identity} onClose={() => onOpenChange(false)} />
</ModalContent>
</Modal>
);
};
@@ -5,15 +5,7 @@ import { useQuery } from "@tanstack/react-query";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import { Button, FilterableSelect, FormControl, ModalClose, Spinner } from "@app/components/v2";
Button,
FilterableSelect,
FormControl,
Modal,
ModalClose,
ModalContent,
Spinner
} from "@app/components/v2";
import { useProject } from "@app/context"; import { useProject } from "@app/context";
import { import {
projectIdentityMembershipQuery, projectIdentityMembershipQuery,
@@ -34,11 +26,10 @@ const schema = z.object({
export type FormData = z.infer<typeof schema>; export type FormData = z.infer<typeof schema>;
type Props = { type Props = {
popUp: UsePopUpState<["linkIdentity"]>; handlePopUpToggle: (popUpName: keyof UsePopUpState<["createIdentity"]>, state?: boolean) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["linkIdentity"]>, state?: boolean) => void;
}; };
const Content = ({ popUp, handlePopUpToggle }: Props) => { export const ProjectLinkIdentityModal = ({ handlePopUpToggle }: Props) => {
const { projectId } = useProject(); const { projectId } = useProject();
// const [searchValue, setSearchValue] = useState(""); // const [searchValue, setSearchValue] = useState("");
@@ -107,7 +98,7 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => {
id: nextAvailableMembership?.id id: nextAvailableMembership?.id
} }
}); });
handlePopUpToggle("linkIdentity", false); handlePopUpToggle("createIdentity", false);
}; };
if (isMembershipsLoading || isRolesLoading) if (isMembershipsLoading || isRolesLoading)
@@ -168,7 +159,7 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => {
isLoading={isSubmitting} isLoading={isSubmitting}
isDisabled={isSubmitting} isDisabled={isSubmitting}
> >
{popUp?.linkIdentity?.data ? "Update" : "Link"} Link
</Button> </Button>
<ModalClose asChild> <ModalClose asChild>
<Button colorSchema="secondary" variant="plain"> <Button colorSchema="secondary" variant="plain">
@@ -179,22 +170,3 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => {
</form> </form>
); );
}; };
export const ProjectLinkIdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
return (
<Modal
isOpen={popUp?.linkIdentity?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("linkIdentity", isOpen);
}}
>
<ModalContent
title="Assign Existing Identity"
subTitle="Assign an existing identity from the parent organization to this project. The identity will continue to be managed at its original scope."
bodyClassName="overflow-visible"
>
<Content popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</ModalContent>
</Modal>
);
};
@@ -28,7 +28,6 @@ import { usePopUp } from "@app/hooks";
import { import {
useAssumeProjectPrivileges, useAssumeProjectPrivileges,
useDeleteProjectIdentityMembership, useDeleteProjectIdentityMembership,
useGetProjectIdentityMembership,
useGetProjectIdentityMembershipV2 useGetProjectIdentityMembershipV2
} from "@app/hooks/api"; } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums"; import { ActorType } from "@app/hooks/api/auditLogs/enums";
@@ -22,6 +22,8 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
IconButton, IconButton,
Modal,
ModalContent,
Tag, Tag,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
@@ -223,11 +225,18 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
)} )}
</div> </div>
</div> </div>
<ProjectIdentityModal <Modal
identity={identity}
isOpen={popUp.editIdentity.isOpen} isOpen={popUp.editIdentity.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("editIdentity", isOpen)} onOpenChange={(open) => handlePopUpToggle("editIdentity", open)}
/> >
<ModalContent bodyClassName="overflow-visible" title="Edit Project Identity">
<ProjectIdentityModal
identity={identity}
onClose={() => handlePopUpToggle("editIdentity", false)}
/>
</ModalContent>
</Modal>
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen} isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure you want to delete ${identity.name}?`} title={`Are you sure you want to delete ${identity.name}?`}