mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
improvement: add filter select to add identity to project modals
This commit is contained in:
@ -4,7 +4,14 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useAddIdentityToWorkspace,
|
||||
@ -16,8 +23,8 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
projectId: z.string(),
|
||||
role: z.string()
|
||||
project: z.object({ name: z.string(), id: z.string() }),
|
||||
role: z.object({ name: z.string(), slug: z.string() })
|
||||
})
|
||||
.required();
|
||||
|
||||
@ -32,7 +39,9 @@ type Props = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle }: Props) => {
|
||||
// TODO: eventually refactor to support adding to multiple projects at once? would lose role granularity unique to project
|
||||
|
||||
const Content = ({ identityId, handlePopUpToggle }: Omit<Props, "popUp">) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { workspaces } = useWorkspace();
|
||||
const { mutateAsync: addIdentityToWorkspace } = useAddIdentityToWorkspace();
|
||||
@ -47,10 +56,10 @@ export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
const projectId = watch("projectId");
|
||||
const projectId = watch("project")?.id;
|
||||
const { data: projectMemberships } = useGetIdentityProjectMemberships(identityId);
|
||||
const { data: project } = useGetWorkspaceById(projectId);
|
||||
const { data: roles } = useGetProjectRoles(project?.id ?? "");
|
||||
const { data: project, isLoading: isProjectLoading } = useGetWorkspaceById(projectId);
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(project?.id ?? "");
|
||||
|
||||
const filteredWorkspaces = useMemo(() => {
|
||||
const wsWorkspaceIds = new Map();
|
||||
@ -64,12 +73,12 @@ export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle
|
||||
);
|
||||
}, [workspaces, projectMemberships]);
|
||||
|
||||
const onFormSubmit = async ({ projectId: workspaceId, role }: FormData) => {
|
||||
const onFormSubmit = async ({ project: selectedProject, role }: FormData) => {
|
||||
try {
|
||||
await addIdentityToWorkspace({
|
||||
workspaceId,
|
||||
workspaceId: selectedProject.id,
|
||||
identityId,
|
||||
role: role || undefined
|
||||
role: role.slug || undefined
|
||||
});
|
||||
|
||||
createNotification({
|
||||
@ -91,87 +100,85 @@ export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle
|
||||
}
|
||||
};
|
||||
|
||||
const isProjectSelected = Boolean(projectId);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="project"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Projects"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={filteredWorkspaces}
|
||||
placeholder="Select project..."
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
isLoading={isProjectSelected && isProjectLoading}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="role"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Role"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<FilterableSelect
|
||||
isDisabled={!isProjectSelected}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={roles}
|
||||
isLoading={isProjectSelected && isRolesLoading}
|
||||
placeholder="Select role..."
|
||||
getOptionValue={(option) => option.slug}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const IdentityAddToProjectModal = ({ identityId, popUp, handlePopUpToggle }: Props) => {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.addIdentityToProject?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("addIdentityToProject", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Add Identity to Project">
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectId"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(filteredWorkspaces || []).map(({ id, name }) => (
|
||||
<SelectItem value={id} key={`project-${id}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="role"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Role"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{(roles || []).map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={`project-role-${slug}`}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("addIdentityToProject", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<ModalContent bodyClassName="overflow-visible" title="Add Identity to Projects">
|
||||
<Content identityId={identityId} handlePopUpToggle={handlePopUpToggle} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -42,7 +42,7 @@ export const IdentitySection = withPermission(
|
||||
? subscription.identitiesUsed < subscription.identityLimit
|
||||
: true;
|
||||
|
||||
const isEnterprise = subscription?.slug === "enterprise"
|
||||
const isEnterprise = subscription?.slug === "enterprise";
|
||||
|
||||
const onDeleteIdentitySubmit = async (identityId: string) => {
|
||||
try {
|
||||
@ -105,7 +105,7 @@ export const IdentitySection = withPermission(
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create identity
|
||||
Create Identity
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
|
@ -181,7 +181,7 @@ export const IdentityTab = withProjectPermission(
|
||||
onClick={() => handlePopUpOpen("identity")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add identity
|
||||
Add Identity
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
@ -1,12 +1,19 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import Link from "next/link";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Modal, ModalClose, ModalContent } from "@app/components/v2";
|
||||
import { ComboBox } from "@app/components/v2/ComboBox";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
Spinner
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useAddIdentityToWorkspace,
|
||||
@ -16,37 +23,30 @@ import {
|
||||
} from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = yup
|
||||
.object({
|
||||
identity: yup.object({
|
||||
id: yup.string().required("Identity id is required"),
|
||||
name: yup.string().required("Identity name is required")
|
||||
}),
|
||||
role: yup.object({
|
||||
slug: yup.string().required("role slug is required"),
|
||||
name: yup.string().required("role name is required")
|
||||
})
|
||||
})
|
||||
.required();
|
||||
const schema = z.object({
|
||||
identity: z.object({ name: z.string(), id: z.string() }),
|
||||
role: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
|
||||
export type FormData = yup.InferType<typeof schema>;
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["identity"]>;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const Content = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const organizationId = currentOrg?.id || "";
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
|
||||
const { data: identityMembershipOrgsData } = useGetIdentityMembershipOrgs({
|
||||
organizationId,
|
||||
limit: 20000 // TODO: this is temp to preserve functionality for larger projects, will replace with combobox in separate PR
|
||||
});
|
||||
const { data: identityMembershipOrgsData, isLoading: isMembershipsLoading } =
|
||||
useGetIdentityMembershipOrgs({
|
||||
organizationId,
|
||||
limit: 20000 // TODO: this is temp to preserve functionality for larger projects, will replace with combobox in separate PR
|
||||
});
|
||||
const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships;
|
||||
const { data: identityMembershipsData } = useGetWorkspaceIdentityMemberships({
|
||||
workspaceId,
|
||||
@ -54,11 +54,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
});
|
||||
const identityMemberships = identityMembershipsData?.identityMemberships;
|
||||
|
||||
const {
|
||||
data: roles,
|
||||
isLoading: isRolesLoading,
|
||||
isFetched: isRolesFetched
|
||||
} = useGetProjectRoles(workspaceId);
|
||||
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||
|
||||
const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();
|
||||
|
||||
@ -76,18 +72,11 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema)
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRolesFetched || !roles) return;
|
||||
|
||||
setValue("role", { name: roles[0]?.name, slug: roles[0]?.slug });
|
||||
}, [isRolesFetched, roles]);
|
||||
|
||||
const onFormSubmit = async ({ identity, role }: FormData) => {
|
||||
try {
|
||||
await addIdentityToWorkspaceMutateAsync({
|
||||
@ -125,104 +114,93 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (isMembershipsLoading || isRolesLoading)
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center py-10">
|
||||
<Spinner className="text-mineshaft-400" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return filteredIdentityMembershipOrgs.length ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="identity"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select identity..."
|
||||
options={filteredIdentityMembershipOrgs.map((membership) => membership.identity)}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="role"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Role"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<FilterableSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
options={roles}
|
||||
placeholder="Select role..."
|
||||
getOptionValue={(option) => option.slug}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{popUp?.identity?.data ? "Update" : "Create"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="text-sm">
|
||||
All identities in your organization have already been added to this project.
|
||||
</div>
|
||||
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
|
||||
<Button isDisabled={isRolesLoading} isLoading={isRolesLoading} variant="outline_bg">
|
||||
Create a new identity
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.identity?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("identity", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Add Identity to Project" bodyClassName="overflow-visible">
|
||||
{filteredIdentityMembershipOrgs.length ? (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="identity"
|
||||
defaultValue={{
|
||||
id: filteredIdentityMembershipOrgs?.[0]?.identity?.id,
|
||||
name: filteredIdentityMembershipOrgs?.[0]?.identity?.name
|
||||
}}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
|
||||
<ComboBox
|
||||
className="w-full"
|
||||
by="id"
|
||||
value={{ id: field.value.id, name: field.value.name }}
|
||||
defaultValue={{ id: field.value.id, name: field.value.name }}
|
||||
onSelectChange={(value) => onChange({ id: value.id, name: value.name })}
|
||||
displayValue={(el) => el.name}
|
||||
onFilter={({ value }, filterQuery) =>
|
||||
value.name.toLowerCase().includes(filterQuery.toLowerCase())
|
||||
}
|
||||
items={filteredIdentityMembershipOrgs.map(({ identity }) => ({
|
||||
key: identity.id,
|
||||
value: { id: identity.id, name: identity.name },
|
||||
label: identity.name
|
||||
}))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="role"
|
||||
defaultValue={{ name: "", slug: "" }}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Role"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<ComboBox
|
||||
className="w-full"
|
||||
by="slug"
|
||||
value={{ slug: field.value.slug, name: field.value.name }}
|
||||
defaultValue={{ slug: field.value.slug, name: field.value.name }}
|
||||
onSelectChange={(value) => onChange({ slug: value.slug, name: value.name })}
|
||||
displayValue={(el) => el.name}
|
||||
onFilter={({ value }, filterQuery) =>
|
||||
value.name.toLowerCase().includes(filterQuery.toLowerCase())
|
||||
}
|
||||
items={(roles || []).map(({ slug, name }) => ({
|
||||
key: slug,
|
||||
value: { slug, name },
|
||||
label: name
|
||||
}))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{popUp?.identity?.data ? "Update" : "Create"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="text-sm">
|
||||
All identities in your organization have already been added to this project.
|
||||
</div>
|
||||
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
|
||||
<Button isDisabled={isRolesLoading} isLoading={isRolesLoading} variant="outline_bg">
|
||||
Create a new identity
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<Content popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
|
Reference in New Issue
Block a user