mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 22:26:41 +00:00
feat: switched to new org invitation modal for sub organizations
This commit is contained in:
@@ -657,7 +657,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const query = (tx || db.replicaNode())(TableName.Identity)
|
const query = (tx || db.replicaNode())(TableName.Membership)
|
||||||
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
|
.where(`${TableName.Membership}.scope`, AccessScope.Organization)
|
||||||
.whereNotNull(`${TableName.Membership}.actorIdentityId`)
|
.whereNotNull(`${TableName.Membership}.actorIdentityId`)
|
||||||
.where(filter)
|
.where(filter)
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ export const membershipUserDALFactory = (db: TDbClient) => {
|
|||||||
.where(`${TableName.Users}.isGhost`, false)
|
.where(`${TableName.Users}.isGhost`, false)
|
||||||
.whereNotNull(`${TableName.Membership}.actorUserId`)
|
.whereNotNull(`${TableName.Membership}.actorUserId`)
|
||||||
.where(`${TableName.Membership}.scopeOrgId`, rootOrgId)
|
.where(`${TableName.Membership}.scopeOrgId`, rootOrgId)
|
||||||
.whereNot(`${TableName.Membership}.actorUserId`, usersConnectedToOrg)
|
.whereNotIn(`${TableName.Membership}.actorUserId`, usersConnectedToOrg)
|
||||||
.select(
|
.select(
|
||||||
db.ref("id").withSchema(TableName.Users),
|
db.ref("id").withSchema(TableName.Users),
|
||||||
db.ref("email").withSchema(TableName.Users),
|
db.ref("email").withSchema(TableName.Users),
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => {
|
|||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
label="Name"
|
label="Name"
|
||||||
>
|
>
|
||||||
<Input autoFocus value={value} onChange={onChange} placeholder="My Organization" />
|
<Input autoFocus value={value} onChange={onChange} placeholder="example-team" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
+280
@@ -0,0 +1,280 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { RoleOption } from "@app/components/roles";
|
||||||
|
import { Button, FilterableSelect, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import { findOrgMembershipRole } from "@app/helpers/roles";
|
||||||
|
import {
|
||||||
|
useAddUsersToOrg,
|
||||||
|
useAddUserToWsNonE2EE,
|
||||||
|
useGetOrgRoles,
|
||||||
|
useGetUserProjects
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { useGetAvailableOrgUsers } from "@app/hooks/api/organization/queries";
|
||||||
|
import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types";
|
||||||
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
|
const DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG = "member";
|
||||||
|
|
||||||
|
const addMemberFormSchema = z.object({
|
||||||
|
users: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
username: z.string().trim(),
|
||||||
|
email: z.string().trim()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.min(1),
|
||||||
|
projects: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
id: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
version: z.nativeEnum(ProjectVersion)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.default([]),
|
||||||
|
projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG),
|
||||||
|
organizationRole: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
description: z.string().optional()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddSubOrgMemberModal = ({ onClose }: Props) => {
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
|
const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? "");
|
||||||
|
const { data: members = [], isPending: isMembersPending } = useGetAvailableOrgUsers();
|
||||||
|
|
||||||
|
const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg();
|
||||||
|
const { mutateAsync: addUserToProject } = useAddUserToWsNonE2EE();
|
||||||
|
|
||||||
|
const { data: projects, isPending: isProjectsLoading } = useGetUserProjects({
|
||||||
|
includeRoles: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<TAddMemberForm>({
|
||||||
|
resolver: zodResolver(addMemberFormSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
// set initial form role based off org default role
|
||||||
|
useEffect(() => {
|
||||||
|
if (organizationRoles) {
|
||||||
|
reset({
|
||||||
|
organizationRole: findOrgMembershipRole(organizationRoles, currentOrg.defaultMembershipRole)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [organizationRoles]);
|
||||||
|
|
||||||
|
const onAddMembers = async ({
|
||||||
|
users,
|
||||||
|
organizationRole,
|
||||||
|
projects: selectedProjects,
|
||||||
|
projectRoleSlug
|
||||||
|
}: TAddMemberForm) => {
|
||||||
|
if (!currentOrg?.id) return;
|
||||||
|
|
||||||
|
if (selectedProjects?.length) {
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const project of selectedProjects) {
|
||||||
|
if (project.version !== ProjectVersion.V3) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: `Cannot add users to project "${project.name}" because it's incompatible. Please upgrade the project.`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const usernames = users.map((el) => el.username);
|
||||||
|
await addUsersMutateAsync({
|
||||||
|
organizationId: currentOrg?.id,
|
||||||
|
inviteeEmails: usernames,
|
||||||
|
organizationRoleSlug: organizationRole.slug
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.allSettled(
|
||||||
|
selectedProjects.map((el) =>
|
||||||
|
addUserToProject({
|
||||||
|
orgId: currentOrg.id,
|
||||||
|
projectId: el.id,
|
||||||
|
roleSlugs: [projectRoleSlug],
|
||||||
|
usernames
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to add user to suborganization",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGroupHeaderLabel = (type: ProjectType) => {
|
||||||
|
switch (type) {
|
||||||
|
case ProjectType.SecretManager:
|
||||||
|
return "Secrets";
|
||||||
|
case ProjectType.CertificateManager:
|
||||||
|
return "PKI";
|
||||||
|
case ProjectType.KMS:
|
||||||
|
return "KMS";
|
||||||
|
case ProjectType.SSH:
|
||||||
|
return "SSH";
|
||||||
|
default:
|
||||||
|
return "Other";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onAddMembers)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="users"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Emails" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<FilterableSelect
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Add one or more users..."
|
||||||
|
isMulti
|
||||||
|
name="members"
|
||||||
|
isLoading={isMembersPending}
|
||||||
|
options={members}
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
getOptionValue={(option) => option.username}
|
||||||
|
getOptionLabel={(option) => option.username}
|
||||||
|
/* eslint-disable-next-line react/no-unstable-nested-components */
|
||||||
|
noOptionsMessage={() => (
|
||||||
|
<p>All root organization users are already assigned to this project</p>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="organizationRole"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="Select which organization role you want to assign to the user."
|
||||||
|
label="Assign organization role"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
placeholder="Select role..."
|
||||||
|
options={organizationRoles}
|
||||||
|
getOptionValue={(option) => option.slug}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
components={{ Option: RoleOption }}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div className="w-full">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="projects"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Assign users to projects"
|
||||||
|
isOptional
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<FilterableSelect
|
||||||
|
isMulti
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
isLoading={isProjectsLoading}
|
||||||
|
getOptionLabel={(project) => project.name}
|
||||||
|
getOptionValue={(project) => project.id}
|
||||||
|
options={projects}
|
||||||
|
groupBy="type"
|
||||||
|
getGroupHeaderLabel={getGroupHeaderLabel}
|
||||||
|
placeholder="Select projects..."
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-[0.15rem] flex min-w-fit justify-end">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="projectRoleSlug"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="Select which role to assign to the users in the selected projects."
|
||||||
|
label="Role"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
isDisabled={watch("projects", []).length === 0}
|
||||||
|
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(val) => field.onChange(val)}
|
||||||
|
>
|
||||||
|
{Object.entries(ProjectMembershipRole).map(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
([_, slug]) =>
|
||||||
|
slug !== "custom" && (
|
||||||
|
<SelectItem key={slug} value={slug}>
|
||||||
|
<span className="capitalize">{slug.replace("-", " ")}</span>
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Add Member
|
||||||
|
</Button>
|
||||||
|
<Button colorSchema="secondary" variant="plain" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
+16
-2
@@ -11,6 +11,8 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmailServiceSetupModal,
|
EmailServiceSetupModal,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
@@ -27,10 +29,11 @@ import { usePopUp } from "@app/hooks/usePopUp";
|
|||||||
|
|
||||||
import { AddOrgMemberModal } from "./AddOrgMemberModal";
|
import { AddOrgMemberModal } from "./AddOrgMemberModal";
|
||||||
import { OrgMembersTable } from "./OrgMembersTable";
|
import { OrgMembersTable } from "./OrgMembersTable";
|
||||||
|
import { AddSubOrgMemberModal } from "./AddSubOrgMemberModal";
|
||||||
|
|
||||||
export const OrgMembersSection = () => {
|
export const OrgMembersSection = () => {
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg, isSubOrganization } = useOrganization();
|
||||||
const orgId = currentOrg?.id ?? "";
|
const orgId = currentOrg?.id ?? "";
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const userId = user?.id || "";
|
const userId = user?.id || "";
|
||||||
@@ -41,6 +44,7 @@ export const OrgMembersSection = () => {
|
|||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"addMember",
|
"addMember",
|
||||||
|
"addMemberToSubOrg",
|
||||||
"removeMember",
|
"removeMember",
|
||||||
"deactivateMember",
|
"deactivateMember",
|
||||||
"upgradePlan",
|
"upgradePlan",
|
||||||
@@ -210,7 +214,9 @@ export const OrgMembersSection = () => {
|
|||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => handleAddMemberModal()}
|
onClick={() =>
|
||||||
|
isSubOrganization ? handlePopUpOpen("addMemberToSubOrg") : handleAddMemberModal()
|
||||||
|
}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Add Member
|
Add Member
|
||||||
@@ -230,6 +236,14 @@ export const OrgMembersSection = () => {
|
|||||||
completeInviteLinks={completeInviteLinks}
|
completeInviteLinks={completeInviteLinks}
|
||||||
setCompleteInviteLinks={setCompleteInviteLinks}
|
setCompleteInviteLinks={setCompleteInviteLinks}
|
||||||
/>
|
/>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.addMemberToSubOrg.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("addMemberToSubOrg", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent title="Add member from your organization">
|
||||||
|
<AddSubOrgMemberModal onClose={() => handlePopUpClose("addMemberToSubOrg")} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.removeMember.isOpen}
|
isOpen={popUp.removeMember.isOpen}
|
||||||
title={`Are you sure you want to remove member with username ${
|
title={`Are you sure you want to remove member with username ${
|
||||||
|
|||||||
+2
-1
@@ -109,7 +109,7 @@ export const OrgMembersTable = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg, isSubOrganization } = useOrganization();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const userId = user?.id || "";
|
const userId = user?.id || "";
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@@ -586,6 +586,7 @@ export const OrgMembersTable = ({
|
|||||||
{isActive &&
|
{isActive &&
|
||||||
(status === "invited" || status === "verified") &&
|
(status === "invited" || status === "verified") &&
|
||||||
email &&
|
email &&
|
||||||
|
!isSubOrganization &&
|
||||||
serverDetails?.emailConfigured && (
|
serverDetails?.emailConfigured && (
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Edit}
|
||||||
|
|||||||
Reference in New Issue
Block a user