mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 19:02:03 +00:00
Merge pull request #4701 from Infisical/feat/sub-org
feat: sub organization
This commit is contained in:
@@ -24,7 +24,7 @@ import { OrgGroupsTab, OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "
|
||||
export const AccessManagementPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { permission } = useOrgPermission();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
|
||||
const navigate = useNavigate({
|
||||
from: ROUTE_PATHS.Organization.AccessControlPage.path
|
||||
@@ -82,7 +82,7 @@ export const AccessManagementPage = () => {
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="org"
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
title="Access Control"
|
||||
description="Manage fine-grained access for users, groups, roles, and identities within your organization resources."
|
||||
/>
|
||||
@@ -116,7 +116,11 @@ export const AccessManagementPage = () => {
|
||||
{tabSections
|
||||
.filter((el) => !el.isHidden)
|
||||
.map((el) => (
|
||||
<Tab variant="org" value={el.key} key={`org-access-tab-${el.key}`}>
|
||||
<Tab
|
||||
variant={isSubOrganization ? "namespace" : "org"}
|
||||
value={el.key}
|
||||
key={`org-access-tab-${el.key}`}
|
||||
>
|
||||
{el.label}
|
||||
</Tab>
|
||||
))}
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FilterableSelect, FormControl } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useGetAvailableOrgIdentities, useGetOrgRoles } from "@app/hooks/api";
|
||||
import { useCreateOrgIdentityMembership } from "@app/hooks/api/orgIdentityMembership";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
identity: z.object({ name: z.string(), id: z.string() }),
|
||||
role: z.object({ name: z.string(), slug: z.string() })
|
||||
})
|
||||
.required();
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const IdentityLinkForm = ({ onClose }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { data: roles } = useGetOrgRoles(orgId);
|
||||
|
||||
const { mutateAsync: createMutateAsync } = useCreateOrgIdentityMembership();
|
||||
const { data: rootOrgIdentities, isPending: isRootOrgLoading } = useGetAvailableOrgIdentities();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {}
|
||||
});
|
||||
|
||||
const onFormSubmit = async ({ identity, role }: FormData) => {
|
||||
try {
|
||||
await createMutateAsync({
|
||||
identityId: identity.id,
|
||||
roles: [{ role: role.slug, isTemporary: false }]
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully linked identity",
|
||||
type: "success"
|
||||
});
|
||||
navigate({
|
||||
to: "/organization/identities/$identityId",
|
||||
params: {
|
||||
identityId: identity.id
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = err as any;
|
||||
const text = error?.response?.data?.message ?? "Failed to link identity";
|
||||
|
||||
createNotification({
|
||||
text,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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={rootOrgIdentities}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
isLoading={isRootOrgLoading}
|
||||
/>
|
||||
</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}
|
||||
// menuPortalTarget={document.body}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Link
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain" onClick={() => onClose()}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+39
-29
@@ -53,6 +53,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { data: roles } = useGetOrgRoles(orgId);
|
||||
const isOrgIdentity = popUp?.identity?.data ? orgId === popUp?.identity?.data?.orgId : true;
|
||||
|
||||
const { mutateAsync: createMutateAsync } = useCreateIdentity();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
|
||||
@@ -113,6 +114,7 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
name: string;
|
||||
role: string;
|
||||
hasDeleteProtection: boolean;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
if (identity) {
|
||||
@@ -196,16 +198,23 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
title={`${popUp?.identity?.data ? "Update" : "Create"} Identity`}
|
||||
>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} placeholder="Machine 1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{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"
|
||||
@@ -214,7 +223,6 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
label={`${popUp?.identity?.data ? "Update" : ""} Role`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
>
|
||||
<FilterableSelect
|
||||
placeholder="Select role..."
|
||||
@@ -227,24 +235,26 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
{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>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<FormLabel label="Metadata" />
|
||||
</div>
|
||||
|
||||
+45
-16
@@ -1,10 +1,15 @@
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faLink,
|
||||
faPlus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||
import { Button, DeleteActionModal, Modal, ModalContent } from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionIdentityActions,
|
||||
OrgPermissionSubjects,
|
||||
@@ -19,6 +24,7 @@ import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IdentityAuthTemplateModal } from "./IdentityAuthTemplateModal";
|
||||
import { IdentityAuthTemplatesTable } from "./IdentityAuthTemplatesTable";
|
||||
import { IdentityLinkForm } from "./IdentityLinkForm";
|
||||
import { IdentityModal } from "./IdentityModal";
|
||||
import { IdentityTable } from "./IdentityTable";
|
||||
import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal";
|
||||
@@ -27,7 +33,7 @@ import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal
|
||||
export const IdentitySection = withPermission(
|
||||
() => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteIdentity();
|
||||
@@ -43,7 +49,8 @@ export const IdentitySection = withPermission(
|
||||
"createTemplate",
|
||||
"editTemplate",
|
||||
"deleteTemplate",
|
||||
"viewUsages"
|
||||
"viewUsages",
|
||||
"linkIdentity"
|
||||
] as const);
|
||||
|
||||
const isMoreIdentitiesAllowed = subscription?.identityLimit
|
||||
@@ -105,8 +112,8 @@ export const IdentitySection = withPermission(
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="mb-4 flex w-full items-center gap-4">
|
||||
<div className="flex flex-1 items-center gap-1">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Identities</p>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/platform/identities/overview"
|
||||
@@ -123,6 +130,26 @@ export const IdentitySection = withPermission(
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{isSubOrganization && (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Create}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faLink} />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("linkIdentity");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Link Identity
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Create}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
@@ -210,16 +237,18 @@ export const IdentitySection = withPermission(
|
||||
?.name || ""
|
||||
}
|
||||
/>
|
||||
{/* <IdentityAuthMethodModal
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/> */}
|
||||
{/* <IdentityUniversalAuthClientSecretModal
|
||||
popUp={popUp}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/> */}
|
||||
<Modal
|
||||
isOpen={popUp.linkIdentity.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("linkIdentity", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Assign Existing Identity"
|
||||
subTitle="Assign an existing identity from your root organization to the sub organization. The identity will continue to be managed at its original scope."
|
||||
bodyClassName="overflow-visible"
|
||||
>
|
||||
<IdentityLinkForm onClose={() => handlePopUpClose("linkIdentity")} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<IdentityTokenAuthTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteIdentity.isOpen}
|
||||
|
||||
+15
-3
@@ -2,6 +2,7 @@ import { useCallback, useState } from "react";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faBuilding,
|
||||
faCheckCircle,
|
||||
faChevronRight,
|
||||
faEdit,
|
||||
@@ -78,7 +79,7 @@ type Filter = {
|
||||
|
||||
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
|
||||
const {
|
||||
offset,
|
||||
@@ -286,15 +287,18 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
{isSubOrganization && <Th>Managed By</Th>}
|
||||
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={3} innerKey="org-identities" />}
|
||||
{isPending && (
|
||||
<TableSkeleton columns={isSubOrganization ? 4 : 3} innerKey="org-identities" />
|
||||
)}
|
||||
{!isPending &&
|
||||
data?.identities?.map(
|
||||
({
|
||||
identity: { id, name },
|
||||
identity: { id, name, orgId },
|
||||
role,
|
||||
customRole,
|
||||
lastLoginAuthMethod,
|
||||
@@ -362,6 +366,14 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
}}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
{isSubOrganization && (
|
||||
<Td>
|
||||
<p className="truncate">
|
||||
<FontAwesomeIcon size="sm" className="mr-1.5" icon={faBuilding} />
|
||||
{currentOrg.id === orgId ? "Sub Organization" : "Root Organization"}
|
||||
</p>
|
||||
</Td>
|
||||
)}
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
||||
+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,
|
||||
DeleteActionModal,
|
||||
EmailServiceSetupModal,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
@@ -26,11 +28,12 @@ import { OrgUser } from "@app/hooks/api/users/types";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddOrgMemberModal } from "./AddOrgMemberModal";
|
||||
import { AddSubOrgMemberModal } from "./AddSubOrgMemberModal";
|
||||
import { OrgMembersTable } from "./OrgMembersTable";
|
||||
|
||||
export const OrgMembersSection = () => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
const orgId = currentOrg?.id ?? "";
|
||||
const { user } = useUser();
|
||||
const userId = user?.id || "";
|
||||
@@ -41,6 +44,7 @@ export const OrgMembersSection = () => {
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"addMember",
|
||||
"addMemberToSubOrg",
|
||||
"removeMember",
|
||||
"deactivateMember",
|
||||
"upgradePlan",
|
||||
@@ -210,7 +214,9 @@ export const OrgMembersSection = () => {
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handleAddMemberModal()}
|
||||
onClick={() =>
|
||||
isSubOrganization ? handlePopUpOpen("addMemberToSubOrg") : handleAddMemberModal()
|
||||
}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Member
|
||||
@@ -230,6 +236,14 @@ export const OrgMembersSection = () => {
|
||||
completeInviteLinks={completeInviteLinks}
|
||||
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
|
||||
isOpen={popUp.removeMember.isOpen}
|
||||
title={`Are you sure you want to remove member with username ${
|
||||
|
||||
+2
-1
@@ -109,7 +109,7 @@ export const OrgMembersTable = ({
|
||||
}: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
const { user } = useUser();
|
||||
const userId = user?.id || "";
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -586,6 +586,7 @@ export const OrgMembersTable = ({
|
||||
{isActive &&
|
||||
(status === "invited" || status === "verified") &&
|
||||
email &&
|
||||
!isSubOrganization &&
|
||||
serverDetails?.emailConfigured && (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
|
||||
+4
-2
@@ -68,7 +68,7 @@ enum RolesOrderBy {
|
||||
|
||||
export const OrgRoleTable = () => {
|
||||
const navigate = useNavigate();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
@@ -200,7 +200,9 @@ export const OrgRoleTable = () => {
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Organization Roles</p>
|
||||
<p className="text-xl font-medium text-mineshaft-100">
|
||||
{isSubOrganization ? "Sub-" : ""}Organization Roles
|
||||
</p>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
|
||||
+4
-1
@@ -3,6 +3,7 @@ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
OrgPermissionAppConnectionActions,
|
||||
OrgPermissionSubjects
|
||||
@@ -12,6 +13,8 @@ import { AppConnectionsTable } from "@app/pages/organization/AppConnections/AppC
|
||||
|
||||
export const AppConnectionsPage = withPermission(
|
||||
() => {
|
||||
const { isSubOrganization } = useOrganization();
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800">
|
||||
<Helmet>
|
||||
@@ -22,7 +25,7 @@ export const AppConnectionsPage = withPermission(
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="org"
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
className="w-full"
|
||||
title="App Connections"
|
||||
description="Manage organization App Connections"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnectionsPage } from "./AppConnectionsPage";
|
||||
|
||||
@@ -6,6 +7,11 @@ export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/"
|
||||
)({
|
||||
component: AppConnectionsPage,
|
||||
validateSearch: z.object({
|
||||
error: z.string().optional(),
|
||||
success: z.string().optional(),
|
||||
connectionId: z.string().optional()
|
||||
}),
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
|
||||
import { LogsSection } from "./components";
|
||||
|
||||
export const AuditLogsPage = () => {
|
||||
const { isSubOrganization } = useOrganization();
|
||||
|
||||
return (
|
||||
<div className="h-full bg-bunker-800">
|
||||
<Helmet>
|
||||
@@ -15,7 +18,7 @@ export const AuditLogsPage = () => {
|
||||
<div className="flex w-full justify-center bg-bunker-800 pb-6 text-white">
|
||||
<div className="w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="org"
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
title="Audit Logs"
|
||||
description="Audit logs for security and compliance teams to monitor information access."
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
import { BillingPage } from "./BillingPage";
|
||||
|
||||
@@ -6,7 +6,14 @@ export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/billing"
|
||||
)({
|
||||
component: BillingPage,
|
||||
beforeLoad: () => {
|
||||
beforeLoad: ({ search }) => {
|
||||
if (search.subOrganization) {
|
||||
throw redirect({
|
||||
to: "/organization/projects",
|
||||
search
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
breadcrumbs: [
|
||||
{
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useDeleteGroup } from "@app/hooks/api";
|
||||
import { useGetGroupById } from "@app/hooks/api/groups/queries";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
@@ -45,6 +45,8 @@ const Page = () => {
|
||||
|
||||
const { data, isPending } = useGetGroupById(groupId);
|
||||
|
||||
const { isSubOrganization } = useOrganization();
|
||||
|
||||
const { mutateAsync: deleteMutateAsync } = useDeleteGroup();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
@@ -95,7 +97,11 @@ const Page = () => {
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Groups
|
||||
</Link>
|
||||
<PageHeader scope="org" description="Organization Group" title={data.group.name}>
|
||||
<PageHeader
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
description={`${isSubOrganization ? "Sub-" : ""}Organization Group`}
|
||||
title={data.group.name}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
|
||||
+15
-4
@@ -29,10 +29,11 @@ const Page = () => {
|
||||
from: ROUTE_PATHS.Organization.IdentityDetailsByIDPage.id
|
||||
});
|
||||
const identityId = params.identityId as string;
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
const { data } = useGetIdentityById(identityId);
|
||||
const { mutateAsync: deleteIdentity } = useDeleteIdentity();
|
||||
const isAuthHidden = orgId !== data?.identity?.orgId;
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"identity",
|
||||
@@ -87,14 +88,24 @@ const Page = () => {
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Identities
|
||||
</Link>
|
||||
<PageHeader scope="org" description="Organization Identity" title={data.identity.name} />
|
||||
<PageHeader
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
description={`${isSubOrganization ? "Sub-" : ""}Organization Identity`}
|
||||
title={data.identity.name}
|
||||
/>
|
||||
<div className="flex">
|
||||
<div className="mr-4 w-96">
|
||||
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
|
||||
<IdentityAuthenticationSection
|
||||
<IdentityDetailsSection
|
||||
isOrgIdentity={data.identity.orgId === currentOrg.id}
|
||||
identityId={identityId}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
{!isAuthHidden && (
|
||||
<IdentityAuthenticationSection
|
||||
identityId={identityId}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<IdentityProjectsSection identityId={identityId} />
|
||||
</div>
|
||||
|
||||
+37
-20
@@ -21,23 +21,25 @@ import {
|
||||
Tag,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
identityId: string;
|
||||
isOrgIdentity?: boolean;
|
||||
handlePopUpOpen: (
|
||||
popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "deleteIdentity"]>,
|
||||
data?: object
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) => {
|
||||
export const IdentityDetailsSection = ({ identityId, handlePopUpOpen, isOrgIdentity }: Props) => {
|
||||
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
|
||||
initialState: "Copy ID to clipboard"
|
||||
});
|
||||
const { isSubOrganization } = useOrganization();
|
||||
|
||||
const { data } = useGetIdentityById(identityId);
|
||||
return data ? (
|
||||
@@ -75,6 +77,7 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
|
||||
handlePopUpOpen("identity", {
|
||||
identityId,
|
||||
name: data.identity.name,
|
||||
orgId: data.identity.orgId,
|
||||
hasDeleteProtection: data.identity.hasDeleteProtection,
|
||||
role: data.role,
|
||||
customRole: data.customRole,
|
||||
@@ -140,24 +143,38 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
|
||||
<p className="text-sm font-medium text-mineshaft-300">Name</p>
|
||||
<p className="text-sm text-mineshaft-300">{data.identity.name}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Last Login Auth Method</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{data.lastLoginAuthMethod ? identityAuthToNameMap[data.lastLoginAuthMethod] : "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Last Login Time</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{data.lastLoginTime ? format(data.lastLoginTime, "PPpp") : "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Delete Protection</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{data.identity.hasDeleteProtection ? "On" : "Off"}
|
||||
</p>
|
||||
</div>
|
||||
{isSubOrganization && (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Managed By</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{isOrgIdentity ? "Organization" : "Root Organization"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{isOrgIdentity && (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Last Login Auth Method</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{data.lastLoginAuthMethod ? identityAuthToNameMap[data.lastLoginAuthMethod] : "-"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{isOrgIdentity && (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Last Login Time</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{data.lastLoginTime ? format(data.lastLoginTime, "PPpp") : "-"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{isOrgIdentity && (
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Delete Protection</p>
|
||||
<p className="text-sm text-mineshaft-300">
|
||||
{data.identity.hasDeleteProtection ? "On" : "Off"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<p className="text-sm font-medium text-mineshaft-300">Organization Role</p>
|
||||
<p className="text-sm text-mineshaft-300">{data.role}</p>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
|
||||
import { NetworkingTabGroup } from "./components/NetworkingTabGroup/NetworkingTabGroup";
|
||||
|
||||
export const NetworkingPage = () => {
|
||||
const { isSubOrganization } = useOrganization();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -14,7 +17,7 @@ export const NetworkingPage = () => {
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="org"
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
title="Networking"
|
||||
description="Manage gateways and relays to securely access private network resources from Infisical"
|
||||
/>
|
||||
|
||||
+4
-1
@@ -2,6 +2,7 @@ import { useState } from "react";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
|
||||
import { GatewayTab } from "../GatewayTab/GatewayTab";
|
||||
import { RelayTab } from "../RelayTab/RelayTab";
|
||||
@@ -18,11 +19,13 @@ export const NetworkingTabGroup = () => {
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
|
||||
|
||||
const { isSubOrganization } = useOrganization();
|
||||
|
||||
return (
|
||||
<Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
<Tab variant="org" value={tab.key} key={tab.key}>
|
||||
<Tab variant={isSubOrganization ? "namespace" : "org"} value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { NewProjectModal } from "@app/components/projects";
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AllProjectView } from "./components/AllProjectView";
|
||||
@@ -52,7 +52,7 @@ export const ProjectsPage = () => {
|
||||
] as const);
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { isSubOrganization } = useOrganization();
|
||||
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||
: true;
|
||||
@@ -64,7 +64,7 @@ export const ProjectsPage = () => {
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Helmet>
|
||||
<PageHeader
|
||||
scope="org"
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
title="Overview"
|
||||
description="Your team's complete security toolkit - organized and ready when you need them."
|
||||
/>
|
||||
|
||||
@@ -378,7 +378,7 @@ export const MyProjectView = ({
|
||||
<div className="flex w-full flex-row">
|
||||
<ProjectListToggle value={projectListView} onChange={onProjectListViewChange} />
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
className="h-[2.3rem] bg-mineshaft-800 text-sm placeholder-mineshaft-50/60 duration-200 focus:bg-mineshaft-700/80"
|
||||
containerClassName="w-full ml-2"
|
||||
placeholder="Search by project name..."
|
||||
value={searchFilter}
|
||||
|
||||
@@ -30,7 +30,7 @@ export const Page = () => {
|
||||
from: ROUTE_PATHS.Organization.RoleByIDPage.id
|
||||
});
|
||||
const roleId = params.roleId as string;
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
const { data } = useGetOrgRole(orgId, roleId);
|
||||
const { mutateAsync: deleteOrgRole } = useDeleteOrgRole();
|
||||
@@ -91,7 +91,7 @@ export const Page = () => {
|
||||
Roles
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope="org"
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
title={data.name}
|
||||
description={
|
||||
<>
|
||||
|
||||
+10
-1
@@ -12,6 +12,7 @@ import {
|
||||
OrgPermissionKmipActions,
|
||||
OrgPermissionMachineIdentityAuthTemplateActions,
|
||||
OrgPermissionSecretShareAction,
|
||||
OrgPermissionSubOrgActions,
|
||||
OrgRelayPermissionActions
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { TPermission } from "@app/hooks/api/roles/types";
|
||||
@@ -123,6 +124,13 @@ const secretSharingPermissionSchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
const subOrganizationPermissionSchema = z
|
||||
.object({
|
||||
[OrgPermissionSubOrgActions.Create]: z.boolean().optional(),
|
||||
[OrgPermissionSubOrgActions.DirectAccess]: z.boolean().optional()
|
||||
})
|
||||
.optional();
|
||||
|
||||
export const formSchema = z.object({
|
||||
name: z.string().trim(),
|
||||
description: z.string().trim().optional(),
|
||||
@@ -159,7 +167,8 @@ export const formSchema = z.object({
|
||||
gateway: orgGatewayPermissionSchema,
|
||||
relay: orgRelayPermissionSchema,
|
||||
"machine-identity-auth-template": machineIdentityAuthTemplatePermissionSchema,
|
||||
"secret-share": secretSharingPermissionSchema
|
||||
"secret-share": secretSharingPermissionSchema,
|
||||
"sub-organization": subOrganizationPermissionSchema
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||
import { OrgPermissionSubOrgActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { TFormSchema } from "../OrgRoleModifySection.utils";
|
||||
|
||||
const PERMISSION_ACTIONS = [
|
||||
{ action: OrgPermissionSubOrgActions.Create, label: "Create" },
|
||||
{ action: OrgPermissionSubOrgActions.DirectAccess, label: "Direct Access" }
|
||||
] as const;
|
||||
|
||||
type Props = {
|
||||
isEditable: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
FullAccess = "full-access",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
export const OrgPermissionSubOrgRow = ({ isEditable, control, setValue }: Props) => {
|
||||
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.sub-organization"
|
||||
});
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
const totalActions = PERMISSION_ACTIONS.length;
|
||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||
|
||||
if (isCustom) return Permission.Custom;
|
||||
if (score === 0) return Permission.NoAccess;
|
||||
if (score === totalActions) return Permission.FullAccess;
|
||||
return Permission.Custom;
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (val === Permission.Custom) {
|
||||
setIsRowExpanded.on();
|
||||
setIsCustom.on();
|
||||
return;
|
||||
}
|
||||
setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
setValue(
|
||||
"permissions.sub-organization",
|
||||
{
|
||||
[OrgPermissionSubOrgActions.Create]: false,
|
||||
[OrgPermissionSubOrgActions.DirectAccess]: false
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
case Permission.FullAccess:
|
||||
setValue(
|
||||
"permissions.sub-organization",
|
||||
{
|
||||
[OrgPermissionSubOrgActions.Create]: true,
|
||||
[OrgPermissionSubOrgActions.DirectAccess]: true
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
default:
|
||||
setValue(
|
||||
"permissions.sub-organization",
|
||||
{
|
||||
[OrgPermissionSubOrgActions.Create]: true,
|
||||
[OrgPermissionSubOrgActions.DirectAccess]: true
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() => setIsRowExpanded.toggle()}
|
||||
>
|
||||
<Td className="w-4">
|
||||
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
||||
</Td>
|
||||
<Td className="w-full select-none">Sub-Organizations</Td>
|
||||
<Td>
|
||||
<Select
|
||||
value={selectedPermissionCategory}
|
||||
className="h-8 w-40 bg-mineshaft-700"
|
||||
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={handlePermissionChange}
|
||||
isDisabled={!isEditable}
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||
</Select>
|
||||
</Td>
|
||||
</Tr>
|
||||
{isRowExpanded && (
|
||||
<Tr>
|
||||
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
|
||||
<div className="flex grow flex-wrap justify-start gap-x-8 gap-y-4">
|
||||
{PERMISSION_ACTIONS.map(({ action, label }) => {
|
||||
return (
|
||||
<Controller
|
||||
name={`permissions.sub-organization.${action}`}
|
||||
key={`permissions.sub-organization.${action}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={field.value}
|
||||
onCheckedChange={(e) => {
|
||||
if (!isEditable) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update default role"
|
||||
});
|
||||
return;
|
||||
}
|
||||
field.onChange(e);
|
||||
}}
|
||||
id={`permissions.sub-organization.${action}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
@@ -74,6 +74,7 @@ type Props = {
|
||||
| "billing"
|
||||
| "audit-logs"
|
||||
| "machine-identity-auth-template"
|
||||
| "sub-organization"
|
||||
>;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
|
||||
+31
-7
@@ -25,6 +25,7 @@ import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
|
||||
import { OrgPermissionMachineIdentityAuthTemplateRow } from "./OrgPermissionMachineIdentityAuthTemplateRow";
|
||||
import { OrgRelayPermissionRow } from "./OrgPermissionRelayRow";
|
||||
import { OrgPermissionSecretShareRow } from "./OrgPermissionSecretShareRow";
|
||||
import { OrgPermissionSubOrgRow } from "./OrgPermissionSubOrgRow";
|
||||
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
|
||||
import { RolePermissionRow } from "./RolePermissionRow";
|
||||
|
||||
@@ -76,8 +77,18 @@ type Props = {
|
||||
roleId: string;
|
||||
};
|
||||
|
||||
const INVALID_SUBORG_PERMISSIONS = [
|
||||
OrgPermissionSubjects.Sso,
|
||||
OrgPermissionSubjects.Ldap,
|
||||
OrgPermissionSubjects.Scim,
|
||||
OrgPermissionSubjects.GithubOrgSync,
|
||||
OrgPermissionSubjects.GithubOrgSyncManual,
|
||||
OrgPermissionSubjects.Billing,
|
||||
OrgPermissionSubjects.SubOrganization
|
||||
];
|
||||
|
||||
export const RolePermissionsSection = ({ roleId }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isRootOrganization } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
const { data: role } = useGetOrgRole(orgId, roleId);
|
||||
@@ -152,7 +163,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TBody>
|
||||
{SIMPLE_PERMISSION_OPTIONS.map((permission) => {
|
||||
{SIMPLE_PERMISSION_OPTIONS.filter((el) =>
|
||||
isRootOrganization
|
||||
? true
|
||||
: !INVALID_SUBORG_PERMISSIONS.includes(el.formName as OrgPermissionSubjects)
|
||||
).map((permission) => {
|
||||
return (
|
||||
<RolePermissionRow
|
||||
title={permission.title}
|
||||
@@ -194,11 +209,13 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
<OrgPermissionBillingRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
{isRootOrganization && (
|
||||
<OrgPermissionBillingRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
)}
|
||||
<OrgPermissionSecretShareRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
@@ -224,6 +241,13 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
{isRootOrganization && (
|
||||
<OrgPermissionSubOrgRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
@@ -4,12 +4,13 @@ import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
|
||||
import { ShareSecretSection } from "./ShareSecretSection";
|
||||
|
||||
export const SecretSharingPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { isSubOrganization } = useOrganization();
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -22,7 +23,7 @@ export const SecretSharingPage = () => {
|
||||
<div className="h-full">
|
||||
<div className="mx-auto h-full w-full max-w-8xl bg-bunker-800 text-white">
|
||||
<PageHeader
|
||||
scope="org"
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
title="Secret Sharing"
|
||||
description="Share secrets securely using a shareable link"
|
||||
>
|
||||
|
||||
@@ -3,17 +3,21 @@ import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useOrganization } from "@app/context";
|
||||
|
||||
import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab";
|
||||
import { SecretSharingSettingsTab } from "./components/SecretSharingSettings/SecretSharingSettingsTab";
|
||||
import { ShareSecretTab } from "./components/ShareSecret/ShareSecretTab";
|
||||
|
||||
enum SecretSharingPageTabs {
|
||||
ShareSecret = "share-secret",
|
||||
RequestSecret = "request-secret"
|
||||
RequestSecret = "request-secret",
|
||||
Settings = "settings"
|
||||
}
|
||||
|
||||
export const ShareSecretSection = () => {
|
||||
const navigate = useNavigate();
|
||||
const { isSubOrganization } = useOrganization();
|
||||
|
||||
const { selectedTab } = useSearch({
|
||||
from: ROUTE_PATHS.Organization.SecretSharing.id
|
||||
@@ -26,6 +30,8 @@ export const ShareSecretSection = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const tabVariant = isSubOrganization ? "namespace" : "org";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
@@ -36,12 +42,17 @@ export const ShareSecretSection = () => {
|
||||
|
||||
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
|
||||
<TabList>
|
||||
<Tab variant="org" value={SecretSharingPageTabs.ShareSecret}>
|
||||
<Tab variant={tabVariant} value={SecretSharingPageTabs.ShareSecret}>
|
||||
Share Secrets
|
||||
</Tab>
|
||||
<Tab variant="org" value={SecretSharingPageTabs.RequestSecret}>
|
||||
<Tab variant={tabVariant} value={SecretSharingPageTabs.RequestSecret}>
|
||||
Request Secrets
|
||||
</Tab>
|
||||
{!isSubOrganization && (
|
||||
<Tab variant={tabVariant} value={SecretSharingPageTabs.Settings}>
|
||||
Settings
|
||||
</Tab>
|
||||
)}
|
||||
</TabList>
|
||||
<TabPanel value={SecretSharingPageTabs.ShareSecret}>
|
||||
<ShareSecretTab />
|
||||
@@ -49,6 +60,9 @@ export const ShareSecretSection = () => {
|
||||
<TabPanel value={SecretSharingPageTabs.RequestSecret}>
|
||||
<RequestSecretTab />
|
||||
</TabPanel>
|
||||
<TabPanel value={SecretSharingPageTabs.Settings}>
|
||||
<SecretSharingSettingsTab />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
||||
+11
-3
@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheck, faCopy, faRedo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -36,6 +37,10 @@ export const RequestSecretForm = () => {
|
||||
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
|
||||
initialState: "Copy to clipboard"
|
||||
});
|
||||
const subOrganization = useSearch({
|
||||
strict: false,
|
||||
select: (el) => el?.subOrganization
|
||||
});
|
||||
|
||||
const { mutateAsync: createSecretRequest } = useCreateSecretRequest();
|
||||
|
||||
@@ -58,12 +63,15 @@ export const RequestSecretForm = () => {
|
||||
expiresAt
|
||||
});
|
||||
|
||||
const link = `${window.location.origin}/secret-request/secret/${id}`;
|
||||
const link = new URL(`${window.location.origin}/secret-request/secret/${id}`);
|
||||
if (subOrganization) {
|
||||
link.searchParams.set("subOrganization", subOrganization);
|
||||
}
|
||||
|
||||
setSecretLink(link);
|
||||
setSecretLink(link.toString());
|
||||
reset();
|
||||
|
||||
navigator.clipboard.writeText(link);
|
||||
navigator.clipboard.writeText(link.toString());
|
||||
setCopyTextSecret("secret");
|
||||
|
||||
createNotification({
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { OrgPermissionSubjects } from "@app/context";
|
||||
import { OrgPermissionSecretShareAction } from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
|
||||
import { OrgSecretShareLimitSection } from "./OrgSecretShareLimitSection";
|
||||
import { SecretSharingAllowShareToAnyone } from "./SecretSharingAllowShareToAnyone";
|
||||
|
||||
export const SecretSharingSettingsTab = withPermission(
|
||||
() => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<SecretSharingAllowShareToAnyone />
|
||||
<OrgSecretShareLimitSection />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{
|
||||
action: OrgPermissionSecretShareAction.ManageSettings,
|
||||
subject: OrgPermissionSubjects.SecretShare
|
||||
}
|
||||
);
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionSecretShareAction,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
|
||||
import { SecretSharingSettingsTabGroup } from "./components";
|
||||
|
||||
export const SecretSharingSettingsPage = withPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Secret Share Settings" })}</title>
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-8xl">
|
||||
<PageHeader scope="org" title="Secret Share Settings" />
|
||||
<SecretSharingSettingsTabGroup />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
{
|
||||
action: OrgPermissionSecretShareAction.ManageSettings,
|
||||
subject: OrgPermissionSubjects.SecretShare
|
||||
}
|
||||
);
|
||||
-1
@@ -1 +0,0 @@
|
||||
export { OrgSecretShareLimitSection } from "./OrgSecretShareLimitSection";
|
||||
-1
@@ -1 +0,0 @@
|
||||
export { SecretSharingAllowShareToAnyone } from "./SecretSharingAllowShareToAnyone";
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { OrgSecretShareLimitSection } from "../OrgSecretShareLimitSection";
|
||||
import { SecretSharingAllowShareToAnyone } from "../SecretSharingAllowShareToAnyone";
|
||||
|
||||
export const SecretSharingSettingsGeneralTab = () => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<SecretSharingAllowShareToAnyone />
|
||||
<OrgSecretShareLimitSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-1
@@ -1 +0,0 @@
|
||||
export { SecretSharingSettingsGeneralTab } from "./SecretSharingSettingsGeneralTab";
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
|
||||
import { SecretSharingSettingsGeneralTab } from "../SecretSharingSettingsGeneralTab";
|
||||
|
||||
export const SecretSharingSettingsTabGroup = () => {
|
||||
const search = useSearch({
|
||||
from: ROUTE_PATHS.Organization.SecretSharingSettings.id
|
||||
});
|
||||
const tabs = [
|
||||
{
|
||||
name: "General",
|
||||
key: "tab-secret-sharing-general",
|
||||
component: SecretSharingSettingsGeneralTab
|
||||
}
|
||||
];
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
|
||||
|
||||
return (
|
||||
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
<Tab value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
{tabs.map(({ key, component: Component }) => (
|
||||
<TabPanel value={key} key={`tab-panel-${key}`}>
|
||||
<Component />
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
-1
@@ -1 +0,0 @@
|
||||
export { SecretSharingSettingsTabGroup } from "./SecretSharingSettingsTabGroup";
|
||||
@@ -1 +0,0 @@
|
||||
export { SecretSharingSettingsTabGroup } from "./SecretSharingSettingsTabGroup";
|
||||
@@ -1,30 +0,0 @@
|
||||
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSharingSettingsPage } from "./SecretSharingSettingsPage";
|
||||
|
||||
const SettingsPageQueryParams = z.object({
|
||||
selectedTab: z.string().catch("")
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/settings"
|
||||
)({
|
||||
component: SecretSharingSettingsPage,
|
||||
validateSearch: zodValidator(SettingsPageQueryParams),
|
||||
search: {
|
||||
middlewares: [stripSearchParams({ selectedTab: "" })]
|
||||
},
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Secret Sharing",
|
||||
link: linkOptions({ to: "/organization/secret-sharing" })
|
||||
},
|
||||
{
|
||||
label: "Settings"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
@@ -2,11 +2,13 @@ import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
|
||||
import { OrgTabGroup } from "./components";
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isSubOrganization } = useOrganization();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -16,7 +18,7 @@ export const SettingsPage = () => {
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="org"
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
description="Configure organization-wide settings"
|
||||
title={t("settings.org.title")}
|
||||
/>
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ export const OrgDeleteSection = () => {
|
||||
onClick={() => handlePopUpOpen("deleteOrg")}
|
||||
isDisabled={Boolean(!hasOrgRole(OrgMembershipRole.Admin))}
|
||||
>
|
||||
{`Delete ${currentOrg?.name}`}
|
||||
{`Delete ${currentOrg.subOrganization?.name ?? currentOrg?.name}`}
|
||||
</Button>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
|
||||
+5
-4
@@ -1,16 +1,17 @@
|
||||
import { useOrgPermission } from "@app/context";
|
||||
import { useOrganization, useOrgPermission } from "@app/context";
|
||||
import { OrgMembershipRole } from "@app/helpers/roles";
|
||||
|
||||
import { OrgDeleteSection } from "../OrgDeleteSection";
|
||||
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
|
||||
import { OrgNameChangeSection } from "../OrgNameChangeSection";
|
||||
import { OrgNameChangeSection, SubOrgNameChangeSection } from "../OrgNameChangeSection";
|
||||
|
||||
export const OrgGeneralTab = () => {
|
||||
const { hasOrgRole } = useOrgPermission();
|
||||
const { isSubOrganization } = useOrganization();
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
|
||||
<OrgNameChangeSection />
|
||||
<OrgIncidentContactsSection />
|
||||
{isSubOrganization ? <SubOrgNameChangeSection /> : <OrgNameChangeSection />}
|
||||
{!isSubOrganization && <OrgIncidentContactsSection />}
|
||||
{hasOrgRole(OrgMembershipRole.Admin) && <OrgDeleteSection />}
|
||||
</div>
|
||||
);
|
||||
|
||||
+13
-15
@@ -41,21 +41,19 @@ export const OrgNameChangeSection = (): JSX.Element => {
|
||||
const [isFormInitialized, setIsFormInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentOrg) {
|
||||
reset({
|
||||
name: currentOrg.name,
|
||||
slug: currentOrg.slug,
|
||||
...(canReadOrgRoles &&
|
||||
roles?.length && {
|
||||
// will always be present, can't remove role if default
|
||||
defaultMembershipRole: isCustomOrgRole(currentOrg.defaultMembershipRole)
|
||||
? roles?.find((role) => currentOrg.defaultMembershipRole === role.id)?.slug || ""
|
||||
: currentOrg.defaultMembershipRole
|
||||
})
|
||||
});
|
||||
setIsFormInitialized(true);
|
||||
}
|
||||
}, [currentOrg, roles]);
|
||||
reset({
|
||||
name: currentOrg.name,
|
||||
slug: currentOrg.slug,
|
||||
...(canReadOrgRoles &&
|
||||
roles?.length && {
|
||||
// will always be present, can't remove role if default
|
||||
defaultMembershipRole: isCustomOrgRole(currentOrg.defaultMembershipRole)
|
||||
? roles?.find((role) => currentOrg.defaultMembershipRole === role.id)?.slug || ""
|
||||
: currentOrg.defaultMembershipRole
|
||||
})
|
||||
});
|
||||
setIsFormInitialized(true);
|
||||
}, [roles]);
|
||||
|
||||
const onFormSubmit = async ({ name, slug, defaultMembershipRole }: FormData) => {
|
||||
try {
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useOrgPermission
|
||||
} from "@app/context";
|
||||
import { useUpdateSubOrganization } from "@app/hooks/api";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const SubOrgNameChangeSection = (): JSX.Element => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { permission } = useOrgPermission();
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { handleSubmit, control } = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: currentOrg?.subOrganization?.name || ""
|
||||
}
|
||||
});
|
||||
const { mutateAsync, isPending } = useUpdateSubOrganization();
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
try {
|
||||
await mutateAsync({
|
||||
name,
|
||||
subOrgId: currentOrg.id
|
||||
});
|
||||
|
||||
navigate({ to: "/organization/settings", search: { subOrganization: name } });
|
||||
queryClient.invalidateQueries();
|
||||
await router.invalidate({ sync: true });
|
||||
createNotification({
|
||||
text: "Successfully updated sub-organization details",
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to update sub-organization details",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="py-4">
|
||||
<div>
|
||||
<h2 className="text-md mb-2 text-mineshaft-100">Organization Name</h2>
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message} className="max-w-md">
|
||||
<Input
|
||||
isDisabled={permission.cannot(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Settings
|
||||
)}
|
||||
placeholder="Acme Corp"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isLoading={isPending}
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export { OrgNameChangeSection } from "./OrgNameChangeSection";
|
||||
export { SubOrgNameChangeSection } from "./SubOrgNameChangeSection";
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ export const OrgProductSelectSection = () => {
|
||||
}));
|
||||
}
|
||||
});
|
||||
}, [currentOrg]);
|
||||
}, [currentOrg?.id]);
|
||||
|
||||
const onProductToggle = async (value: boolean, key: string) => {
|
||||
setIsLoading(true);
|
||||
|
||||
+32
-14
@@ -3,6 +3,7 @@ import { useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { ROUTE_PATHS } from "@app/const/routes";
|
||||
import { useOrganization } from "@app/context";
|
||||
|
||||
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
||||
import { ExternalMigrationsTab } from "../ExternalMigrationsTab";
|
||||
@@ -19,20 +20,33 @@ export const OrgTabGroup = () => {
|
||||
const search = useSearch({
|
||||
from: ROUTE_PATHS.Organization.SettingsPage.id
|
||||
});
|
||||
const { isSubOrganization } = useOrganization();
|
||||
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-org-general", component: OrgGeneralTab },
|
||||
{
|
||||
name: "SSO",
|
||||
key: "sso-settings",
|
||||
component: OrgSsoTab
|
||||
component: OrgSsoTab,
|
||||
isHidden: isSubOrganization
|
||||
},
|
||||
{
|
||||
name: "Provisioning",
|
||||
key: "provisioning-settings",
|
||||
component: OrgProvisioningTab
|
||||
component: OrgProvisioningTab,
|
||||
isHidden: isSubOrganization
|
||||
},
|
||||
{
|
||||
name: "Security",
|
||||
key: "tab-org-security",
|
||||
component: OrgSecurityTab,
|
||||
isHidden: isSubOrganization
|
||||
},
|
||||
{
|
||||
name: "Encryption",
|
||||
key: "tab-org-encryption",
|
||||
component: OrgEncryptionTab
|
||||
},
|
||||
{ name: "Security", key: "tab-org-security", component: OrgSecurityTab },
|
||||
{ name: "Encryption", key: "tab-org-encryption", component: OrgEncryptionTab },
|
||||
{
|
||||
name: "Workflow Integrations",
|
||||
key: "workflow-integrations",
|
||||
@@ -57,17 +71,21 @@ export const OrgTabGroup = () => {
|
||||
return (
|
||||
<Tabs orientation="vertical" value={selectedTab} onValueChange={setSelectedTab}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
<Tab variant="org" value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
{tabs
|
||||
.filter((el) => !el.isHidden)
|
||||
.map((tab) => (
|
||||
<Tab variant={isSubOrganization ? "namespace" : "org"} value={tab.key} key={tab.key}>
|
||||
{tab.name}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
{tabs.map(({ key, component: Component }) => (
|
||||
<TabPanel value={key} key={`tab-panel-${key}`}>
|
||||
<Component />
|
||||
</TabPanel>
|
||||
))}
|
||||
{tabs
|
||||
.filter((el) => !el.isHidden)
|
||||
.map(({ key, component: Component }) => (
|
||||
<TabPanel value={key} key={`tab-panel-${key}`}>
|
||||
<Component />
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
+118
-144
@@ -1,6 +1,5 @@
|
||||
import { faPlus, faTrash, faUnlock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
@@ -70,154 +69,129 @@ export const ProjectTemplateRolesSection = ({ projectTemplate, isInfisicalTempla
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<AnimatePresence>
|
||||
{popUp?.editRole.isOpen ? (
|
||||
<motion.div
|
||||
key="edit-role"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="absolute min-h-40 w-full"
|
||||
>
|
||||
<ProjectTemplateEditRoleForm
|
||||
onGoBack={() => handlePopUpClose("editRole")}
|
||||
projectTemplate={projectTemplate}
|
||||
role={editRole}
|
||||
isDisabled={
|
||||
permission.cannot(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.ProjectTemplates
|
||||
) ||
|
||||
(editRole && !isCustomProjectRole(editRole.slug))
|
||||
}
|
||||
/>
|
||||
<div className="h-4 w-full" />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="role-list"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: 0 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
className="absolute w-full"
|
||||
>
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">Project Roles</h2>
|
||||
<p className="text-sm text-mineshaft-400">
|
||||
{isInfisicalTemplate
|
||||
? "Click a role to view the associated permissions"
|
||||
: "Add, edit and remove roles for this project template"}
|
||||
</p>
|
||||
</div>
|
||||
{!isInfisicalTemplate && (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.ProjectTemplates}
|
||||
{popUp?.editRole.isOpen ? (
|
||||
<ProjectTemplateEditRoleForm
|
||||
onGoBack={() => handlePopUpClose("editRole")}
|
||||
projectTemplate={projectTemplate}
|
||||
role={editRole}
|
||||
isDisabled={
|
||||
permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates) ||
|
||||
(editRole && !isCustomProjectRole(editRole.slug))
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">Project Roles</h2>
|
||||
<p className="text-sm text-mineshaft-400">
|
||||
{isInfisicalTemplate
|
||||
? "Click a role to view the associated permissions"
|
||||
: "Add, edit and remove roles for this project template"}
|
||||
</p>
|
||||
</div>
|
||||
{!isInfisicalTemplate && (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.ProjectTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
handlePopUpOpen("editRole");
|
||||
}}
|
||||
colorSchema="primary"
|
||||
className="ml-auto"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
handlePopUpOpen("editRole");
|
||||
}}
|
||||
colorSchema="primary"
|
||||
className="ml-auto"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Role
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
Add Role
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Slug</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{roles.length ? (
|
||||
roles.map((role) => {
|
||||
return (
|
||||
<Tr
|
||||
key={role.slug}
|
||||
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
handlePopUpOpen("editRole", role);
|
||||
}
|
||||
}}
|
||||
onClick={() => handlePopUpOpen("editRole", role)}
|
||||
>
|
||||
<Td>{role.name}</Td>
|
||||
<Td>{role.slug}</Td>
|
||||
<Td>
|
||||
{isCustomProjectRole(role.slug) && (
|
||||
<div className="flex space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.ProjectTemplates}
|
||||
renderTooltip
|
||||
allowedLabel="Remove Role"
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Slug</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{roles.length ? (
|
||||
roles.map((role) => {
|
||||
return (
|
||||
<Tr
|
||||
key={role.slug}
|
||||
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
handlePopUpOpen("editRole", role);
|
||||
}
|
||||
}}
|
||||
onClick={() => handlePopUpOpen("editRole", role)}
|
||||
>
|
||||
<Td>{role.name}</Td>
|
||||
<Td>{role.slug}</Td>
|
||||
<Td>
|
||||
{isCustomProjectRole(role.slug) && (
|
||||
<div className="flex space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.ProjectTemplates}
|
||||
renderTooltip
|
||||
allowedLabel="Remove Role"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete-icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handlePopUpOpen("removeRole", role);
|
||||
}}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete-icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handlePopUpOpen("removeRole", role);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Tr>
|
||||
<Td colSpan={2}>
|
||||
<EmptyState title="No roles assigned to template" icon={faUnlock} />
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeRole.isOpen}
|
||||
deleteKey="remove"
|
||||
title={`Are you sure you want to remove the role ${roleToDelete?.slug}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("removeRole", isOpen)}
|
||||
onDeleteApproved={() => handleRemoveRole(roleToDelete?.slug)}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-4 w-full" />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Tr>
|
||||
<Td colSpan={2}>
|
||||
<EmptyState title="No roles assigned to template" icon={faUnlock} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeRole.isOpen}
|
||||
deleteKey="remove"
|
||||
title={`Are you sure you want to remove the role ${roleToDelete?.slug}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("removeRole", isOpen)}
|
||||
onDeleteApproved={() => handleRemoveRole(roleToDelete?.slug)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+62
-86
@@ -1,7 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
@@ -25,93 +24,70 @@ export const ProjectTemplatesSection = () => {
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<AnimatePresence>
|
||||
{editTemplate ? (
|
||||
<motion.div
|
||||
key="edit-project-template"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="absolute min-h-40 w-full"
|
||||
>
|
||||
<EditProjectTemplateSection
|
||||
template={editTemplate}
|
||||
onBack={() => setEditTemplate(null)}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="project-templates-list"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="absolute min-h-40 w-full"
|
||||
>
|
||||
<div>
|
||||
<p className="mb-6 font-inter text-bunker-300">
|
||||
Create and configure templates with predefined roles and environments to streamline
|
||||
project setup
|
||||
</p>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-start">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Project Templates</p>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/documentation/platform/project-templates"
|
||||
>
|
||||
<div className="mt-[0.32rem] ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Create}
|
||||
a={OrgPermissionSubjects.ProjectTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (!subscription?.projectTemplates) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("addTemplate");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
className="ml-auto"
|
||||
>
|
||||
Add Template
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
{editTemplate ? (
|
||||
<EditProjectTemplateSection template={editTemplate} onBack={() => setEditTemplate(null)} />
|
||||
) : (
|
||||
<div>
|
||||
<p className="mb-6 font-inter text-bunker-300">
|
||||
Create and configure templates with predefined roles and environments to streamline
|
||||
project setup
|
||||
</p>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex items-start">
|
||||
<p className="text-xl font-medium text-mineshaft-100">Project Templates</p>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://infisical.com/docs/documentation/platform/project-templates"
|
||||
>
|
||||
<div className="mt-[0.32rem] ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
<ProjectTemplatesTable onEdit={setEditTemplate} />
|
||||
<ProjectTemplateDetailsModal
|
||||
onComplete={(template) => setEditTemplate(template)}
|
||||
isOpen={popUp.addTemplate.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addTemplate", isOpen)}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can create project templates if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Create}
|
||||
a={OrgPermissionSubjects.ProjectTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (!subscription?.projectTemplates) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("addTemplate");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
className="ml-auto"
|
||||
>
|
||||
Add Template
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<ProjectTemplatesTable onEdit={setEditTemplate} />
|
||||
<ProjectTemplateDetailsModal
|
||||
onComplete={(template) => setEditTemplate(template)}
|
||||
isOpen={popUp.addTemplate.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addTemplate", isOpen)}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can create project templates if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ const Page = withPermission(
|
||||
});
|
||||
const membershipId = search.membershipId as string;
|
||||
const { user } = useUser();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
|
||||
const userId = user?.id || "";
|
||||
const orgId = currentOrg?.id || "";
|
||||
@@ -131,7 +131,7 @@ const Page = withPermission(
|
||||
Users
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope="org"
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
title={
|
||||
membership.user.firstName || membership.user.lastName
|
||||
? `${membership.user.firstName} ${membership.user.lastName ?? ""}`.trim()
|
||||
@@ -140,7 +140,7 @@ const Page = withPermission(
|
||||
membership.inviteEmail ??
|
||||
"Unknown User")
|
||||
}
|
||||
description="Organization User Membership"
|
||||
description={`${isSubOrganization ? "Sub-" : ""}Organization User Membership`}
|
||||
>
|
||||
<div>
|
||||
{userId !== membership.user.id && (
|
||||
|
||||
+3
-2
@@ -35,7 +35,7 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
|
||||
});
|
||||
|
||||
const { user } = useUser();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
const userId = user?.id || "";
|
||||
const orgId = currentOrg?.id || "";
|
||||
|
||||
@@ -214,7 +214,8 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
|
||||
<p className="text-sm text-mineshaft-300">-</p>
|
||||
)}
|
||||
</div>
|
||||
{membership.isActive &&
|
||||
{!isSubOrganization &&
|
||||
membership.isActive &&
|
||||
(membership.status === "invited" || membership.status === "verified") &&
|
||||
membership.user.email &&
|
||||
serverDetails?.emailConfigured && (
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { createFileRoute, retainSearchParams } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrganizationLayout } from "@app/layouts/OrganizationLayout";
|
||||
|
||||
export const Route = createFileRoute("/_authenticate/_inject-org-details/_org-layout")({
|
||||
component: OrganizationLayout
|
||||
component: OrganizationLayout,
|
||||
validateSearch: z.object({
|
||||
subOrganization: z.string().optional()
|
||||
}),
|
||||
search: {
|
||||
middlewares: [retainSearchParams(["subOrganization"])]
|
||||
}
|
||||
});
|
||||
|
||||
+36
-2
@@ -1,11 +1,16 @@
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { components, OptionProps } from "react-select";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faGlobe } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
@@ -24,7 +29,7 @@ import {
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const schema = z.object({
|
||||
identity: z.object({ name: z.string(), id: z.string() }),
|
||||
identity: z.object({ name: z.string(), id: z.string(), isManagedByRootOrg: z.boolean() }),
|
||||
role: z.object({ name: z.string(), slug: z.string() })
|
||||
});
|
||||
|
||||
@@ -35,6 +40,29 @@ type Props = {
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
const Option = ({
|
||||
isSelected,
|
||||
children,
|
||||
...props
|
||||
}: OptionProps<{ name: string; id: string; isManagedByRootOrg: boolean }>) => {
|
||||
return (
|
||||
<components.Option isSelected={isSelected} {...props}>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<p className="truncate">{children}</p>
|
||||
{props.data.isManagedByRootOrg && (
|
||||
<Badge variant="org" className="ml-auto cursor-pointer">
|
||||
<FontAwesomeIcon icon={faGlobe} />
|
||||
Managed by Root Org
|
||||
</Badge>
|
||||
)}
|
||||
{isSelected && (
|
||||
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
|
||||
)}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
const Content = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { projectId } = useProject();
|
||||
@@ -131,9 +159,15 @@ const Content = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="Select identity..."
|
||||
options={filteredIdentityMembershipOrgs.map((membership) => membership.identity)}
|
||||
options={filteredIdentityMembershipOrgs.map((membership) => ({
|
||||
...membership.identity,
|
||||
isManagedByRootOrg: membership.identity.orgId !== currentOrg.id
|
||||
}))}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
components={{
|
||||
Option
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ErrorPage = ({ error }: ErrorComponentProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="flex h-screen items-center justify-center bg-bunker-800">
|
||||
<div className="flex max-w-3xl flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-8 text-center text-mineshaft-200">
|
||||
<FontAwesomeIcon icon={faBugs} className="my-2 inline text-6xl" />
|
||||
<p>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheck, faCopy, faRedo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useSearch } from "@tanstack/react-router";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -87,6 +88,10 @@ export const ShareSecretForm = ({
|
||||
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
|
||||
initialState: "Copy to clipboard"
|
||||
});
|
||||
const subOrganization = useSearch({
|
||||
strict: false,
|
||||
select: (el) => el?.subOrganization
|
||||
});
|
||||
|
||||
const publicSharedSecretCreator = useCreatePublicSharedSecret();
|
||||
const privateSharedSecretCreator = useCreateSharedSecret();
|
||||
@@ -148,11 +153,14 @@ export const ShareSecretForm = ({
|
||||
type: "success"
|
||||
});
|
||||
} else {
|
||||
const link = `${window.location.origin}/shared/secret/${id}`;
|
||||
const link = new URL(`${window.location.origin}/shared/secret/${id}`);
|
||||
if (subOrganization) {
|
||||
link.searchParams.set("subOrganization", subOrganization);
|
||||
}
|
||||
|
||||
setSecretLink(link);
|
||||
setSecretLink(link.toString());
|
||||
|
||||
navigator.clipboard.writeText(link);
|
||||
navigator.clipboard.writeText(link.toString());
|
||||
setCopyTextSecret("secret");
|
||||
|
||||
createNotification({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
|
||||
import { createRootRouteWithContext, Outlet, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { NotificationContainer } from "@app/components/notifications";
|
||||
import { TooltipProvider } from "@app/components/v2";
|
||||
@@ -13,6 +14,15 @@ type TRouterContext = {
|
||||
};
|
||||
|
||||
const RootPage = () => {
|
||||
const subOrganization = useSearch({
|
||||
strict: false,
|
||||
select: (el) => el?.subOrganization
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
queryClient.invalidateQueries();
|
||||
}, [subOrganization]);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
|
||||
Reference in New Issue
Block a user