feat(rbac): implemented granular blocking of actions based on permissions on org level ui

This commit is contained in:
Akhil Mohan
2023-09-08 21:21:43 +05:30
parent 34fb7be1c4
commit aac3168c80
41 changed files with 1992 additions and 1652 deletions
@@ -292,7 +292,7 @@ export const createOrganizationPortalSession = async (req: Request, res: Respons
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
GeneralPermissionActions.Create, GeneralPermissionActions.Edit,
OrgPermissionSubjects.Billing OrgPermissionSubjects.Billing
); );
@@ -0,0 +1,50 @@
import { FunctionComponent, ReactNode } from "react";
import { BoundCanProps, Can } from "@casl/react";
import {
OrgPermissionSubjects,
OrgWorkspacePermissionActions,
TOrgPermission,
useOrgPermission
} from "@app/context/OrgPermissionContext";
import { Tooltip } from "../v2";
type Props = {
label?: ReactNode;
} & BoundCanProps<TOrgPermission>;
export const OrgPermissionCan: FunctionComponent<Props> = ({
label = "Permission Denied. Kindly contact your org admin",
children,
passThrough = true,
...props
}) => {
const permission = useOrgPermission();
return (
<Can
{...props}
passThrough={passThrough}
ability={props?.ability || permission}
I={OrgWorkspacePermissionActions.Read}
a={OrgPermissionSubjects.Sso}
>
{(isAllowed, ability) => {
// akhilmhdh: This is set as type due to error in casl react type.
const finalChild =
typeof children === "function"
? children(isAllowed, ability as TOrgPermission)
: children;
if (!isAllowed && passThrough) {
return <Tooltip content={label}>{finalChild}</Tooltip>;
}
if (!isAllowed) return null;
return finalChild;
}}
</Can>
);
};
@@ -0,0 +1 @@
export { OrgPermissionCan } from "./OrgPermissionCan";
@@ -3,13 +3,13 @@ import { createContext, ReactNode, useContext } from "react";
import { useGetUserOrgPermissions } from "@app/hooks/api"; import { useGetUserOrgPermissions } from "@app/hooks/api";
import { useOrganization } from "../OrganizationContext"; import { useOrganization } from "../OrganizationContext";
import { TPermission } from "./types"; import { TOrgPermission } from "./types";
type Props = { type Props = {
children: ReactNode; children: ReactNode;
}; };
const PermissionContext = createContext<null | TPermission>(null); const OrgPermissionContext = createContext<null | TOrgPermission>(null);
export const OrgPermissionProvider = ({ children }: Props): JSX.Element => { export const OrgPermissionProvider = ({ children }: Props): JSX.Element => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
@@ -37,11 +37,13 @@ export const OrgPermissionProvider = ({ children }: Props): JSX.Element => {
); );
} }
return <PermissionContext.Provider value={permission}>{children}</PermissionContext.Provider>; return (
<OrgPermissionContext.Provider value={permission}>{children}</OrgPermissionContext.Provider>
);
}; };
export const useOrgPermission = () => { export const useOrgPermission = () => {
const ctx = useContext(PermissionContext); const ctx = useContext(OrgPermissionContext);
if (!ctx) { if (!ctx) {
throw new Error("useOrgPermission to be used within <OrgPermissionProvider>"); throw new Error("useOrgPermission to be used within <OrgPermissionProvider>");
} }
@@ -1 +1,7 @@
export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext"; export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext";
export type { TOrgPermission } from "./types";
export {
OrgGeneralPermissionActions,
OrgPermissionSubjects,
OrgWorkspacePermissionActions
} from "./types";
@@ -1,13 +1,13 @@
import { MongoAbility } from "@casl/ability"; import { MongoAbility } from "@casl/ability";
export enum GeneralPermissionActions { export enum OrgGeneralPermissionActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
Edit = "edit", Edit = "edit",
Delete = "delete" Delete = "delete"
} }
export enum WorkspacePermissionActions { export enum OrgWorkspacePermissionActions {
Read = "read", Read = "read",
Create = "create" Create = "create"
} }
@@ -24,13 +24,13 @@ export enum OrgPermissionSubjects {
} }
export type OrgPermissionSet = export type OrgPermissionSet =
| [WorkspacePermissionActions, OrgPermissionSubjects.Workspace] | [OrgWorkspacePermissionActions, OrgPermissionSubjects.Workspace]
| [GeneralPermissionActions, OrgPermissionSubjects.Role] | [OrgGeneralPermissionActions, OrgPermissionSubjects.Role]
| [GeneralPermissionActions, OrgPermissionSubjects.Member] | [OrgGeneralPermissionActions, OrgPermissionSubjects.Member]
| [GeneralPermissionActions, OrgPermissionSubjects.Settings] | [OrgGeneralPermissionActions, OrgPermissionSubjects.Settings]
| [GeneralPermissionActions, OrgPermissionSubjects.IncidentAccount] | [OrgGeneralPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [GeneralPermissionActions, OrgPermissionSubjects.Sso] | [OrgGeneralPermissionActions, OrgPermissionSubjects.Sso]
| [GeneralPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgGeneralPermissionActions, OrgPermissionSubjects.SecretScanning]
| [GeneralPermissionActions, OrgPermissionSubjects.Billing]; | [OrgGeneralPermissionActions, OrgPermissionSubjects.Billing];
export type TPermission = MongoAbility<OrgPermissionSet>; export type TOrgPermission = MongoAbility<OrgPermissionSet>;
+7 -1
View File
@@ -1,6 +1,12 @@
export { AuthProvider } from "./AuthContext"; export { AuthProvider } from "./AuthContext";
export { OrgProvider, useOrganization } from "./OrganizationContext"; export { OrgProvider, useOrganization } from "./OrganizationContext";
export { OrgPermissionProvider,useOrgPermission } from "./OrgPermissionContext"; export type { TOrgPermission } from "./OrgPermissionContext";
export {
OrgGeneralPermissionActions,
OrgPermissionSubjects,
OrgWorkspacePermissionActions
} from "./OrgPermissionContext";
export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext";
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext"; export { SubscriptionProvider, useSubscription } from "./SubscriptionContext";
export { UserProvider, useUser } from "./UserContext"; export { UserProvider, useUser } from "./UserContext";
export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext"; export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext";
+1
View File
@@ -0,0 +1 @@
export { withPermission } from "./withPermission";
@@ -0,0 +1 @@
export { withPermission } from "./withPermission";
@@ -0,0 +1,62 @@
import { ComponentType } from "react";
import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability";
import { faLock } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { TOrgPermission, useOrgPermission } from "@app/context";
type Props<T extends Abilities> = (T extends AbilityTuple
? {
action: T[0];
subject: Extract<T[1], SubjectType>;
}
: {
action: string;
subject: string;
}) & { className?: string; containerClassName?: string };
export const withPermission = <T extends {}, J extends TOrgPermission>(
Component: ComponentType<T>,
{ action, subject, className, containerClassName }: Props<Generics<J>["abilities"]>
) => {
const HOC = (hocProps: T) => {
const permission = useOrgPermission();
// akhilmhdh: Set as any due to casl/react ts type bug
// REASON: casl due to its type checking can't seem to union even if union intersection is applied
if (permission.cannot(action as any, subject)) {
return (
<div
className={twMerge(
"container h-full mx-auto flex justify-center items-center",
containerClassName
)}
>
<div
className={twMerge(
"rounded-md bg-mineshaft-800 text-bunker-300 p-16 flex space-x-12 items-end",
className
)}
>
<div>
<FontAwesomeIcon icon={faLock} size="6x" />
</div>
<div>
<div className="text-4xl font-medium mb-2">Permission Denied</div>
<div className="text-sm">
You do not have permission to this page. <br /> Kindly contact your organization
administrator
</div>
</div>
</div>
</div>
);
}
return <Component {...hocProps} />;
};
HOC.displayName = "WithPermission";
return HOC;
};
+21 -14
View File
@@ -1,21 +1,28 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Head from "next/head"; import Head from "next/head";
import { OrgGeneralPermissionActions, OrgPermissionSubjects, TOrgPermission } from "@app/context";
import { withPermission } from "@app/hoc";
import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage"; import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage";
export default function SettingsBilling() { const SettingsBilling = withPermission<{}, TOrgPermission>(
const { t } = useTranslation(); () => {
const { t } = useTranslation();
return ( return (
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
<Head> <Head>
<title>{t("common.head-title", { title: t("billing.title") })}</title> <title>{t("common.head-title", { title: t("billing.title") })}</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" /> <meta property="og:image" content="/images/message.png" />
</Head> </Head>
<BillingSettingsPage /> <BillingSettingsPage />
</div> </div>
); );
} },
{ action: OrgGeneralPermissionActions.Delete, subject: OrgPermissionSubjects.Billing }
);
SettingsBilling.requireAuth = true; Object.assign(SettingsBilling, { requireAuth: true });
export default SettingsBilling;
+333 -301
View File
@@ -31,6 +31,7 @@ import * as Tabs from "@radix-ui/react-tabs";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck"; import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
import { import {
Button, Button,
@@ -42,9 +43,22 @@ import {
Skeleton, Skeleton,
UpgradePlanModal UpgradePlanModal
} from "@app/components/v2"; } from "@app/components/v2";
import { useSubscription, useUser, useWorkspace } from "@app/context";
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useRegisterUserAction,useUploadWsKey } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import {
OrgPermissionSubjects,
OrgWorkspacePermissionActions,
useSubscription,
useUser,
useWorkspace
} from "@app/context";
import { withPermission } from "@app/hoc";
import {
fetchOrgUsers,
useAddUserToWs,
useCreateWorkspace,
useRegisterUserAction,
useUploadWsKey
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto"; import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto";
@@ -301,9 +315,7 @@ const LearningItem = ({
tabIndex={0} tabIndex={0}
onClick={async () => { onClick={async () => {
if (userAction && userAction !== "first_time_secrets_pushed") { if (userAction && userAction !== "first_time_secrets_pushed") {
await registerUserAction.mutateAsync( await registerUserAction.mutateAsync(userAction);
userAction
);
} }
}} }}
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${ className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${
@@ -446,19 +458,20 @@ type TAddProjectFormData = yup.InferType<typeof formSchema>;
// #TODO: Update all the workspaceIds // #TODO: Update all the workspaceIds
export default function Organization() { const OrganizationPage = withPermission(
const { t } = useTranslation(); () => {
const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const { workspaces, isLoading: isWorkspaceLoading } = useWorkspace(); const { workspaces, isLoading: isWorkspaceLoading } = useWorkspace();
const orgWorkspaces = const orgWorkspaces =
workspaces?.filter( workspaces?.filter(
(workspace) => workspace.organization === localStorage.getItem("orgData.id") (workspace) => workspace.organization === localStorage.getItem("orgData.id")
) || []; ) || [];
const currentOrg = String(router.query.id); const currentOrg = String(router.query.id);
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const addWsUser = useAddUserToWs(); const addWsUser = useAddUserToWs();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addNewWs", "addNewWs",
@@ -537,24 +550,24 @@ export default function Organization() {
? subscription.workspacesUsed < subscription.workspaceLimit ? subscription.workspacesUsed < subscription.workspaceLimit
: true; : true;
useEffect(() => { useEffect(() => {
onboardingCheck({ onboardingCheck({
setHasUserClickedIntro, setHasUserClickedIntro,
setHasUserClickedSlack, setHasUserClickedSlack,
setHasUserPushedSecrets, setHasUserPushedSecrets,
setUsersInOrg setUsersInOrg
}); });
}, []); }, []);
const isWorkspaceEmpty = !isWorkspaceLoading && orgWorkspaces?.length === 0; const isWorkspaceEmpty = !isWorkspaceLoading && orgWorkspaces?.length === 0;
return ( return (
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen"> <div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen">
<Head> <Head>
<title>{t("common.head-title", { title: t("settings.members.title") })}</title> <title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Head> </Head>
{!serverDetails?.redisConfigured && <div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl"> {!serverDetails?.redisConfigured && <div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
<p className="mr-4 mb-4 font-semibold text-white">Announcements</p> <p className="mr-4 mb-4 font-semibold text-white">Announcements</p>
<div className="w-full border border-blue-400/70 rounded-md bg-blue-900/70 p-2 text-base text-mineshaft-100 flex items-center"> <div className="w-full border border-blue-400/70 rounded-md bg-blue-900/70 p-2 text-base text-mineshaft-100 flex items-center">
<FontAwesomeIcon icon={faExclamationCircle} className="text-2xl mr-4 p-4 text-mineshaft-50"/> <FontAwesomeIcon icon={faExclamationCircle} className="text-2xl mr-4 p-4 text-mineshaft-50"/>
@@ -566,290 +579,309 @@ export default function Organization() {
</Link>. </Link>.
</div> </div>
</div>} </div>}
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl"> <div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
<p className="mr-4 font-semibold text-white">Projects</p> <p className="mr-4 font-semibold text-white">Projects</p>
<div className="mt-6 flex w-full flex-row"> <div className="mt-6 flex w-full flex-row">
<Input <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 duration-200 focus:bg-mineshaft-700/80"
placeholder="Search by project name..." placeholder="Search by project name..."
value={searchFilter} value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)} onChange={(e) => setSearchFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
/> />
<Button <OrgPermissionCan
colorSchema="primary" I={OrgWorkspacePermissionActions.Create}
leftIcon={<FontAwesomeIcon icon={faPlus} />} an={OrgPermissionSubjects.Workspace}
onClick={() => { >
if (isAddingProjectsAllowed) { {(isAllowed) => (
handlePopUpOpen("addNewWs"); <Button
} else { isDisabled={!isAllowed}
handlePopUpOpen("upgradePlan"); colorSchema="primary"
} leftIcon={<FontAwesomeIcon icon={faPlus} />}
}}
className="ml-2"
>
Add New Project
</Button>
</div>
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{isWorkspaceLoading &&
Array.apply(0, Array(3)).map((_x, i) => (
<div
key={`workspace-cards-loading-${i + 1}`}
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="mt-0 text-lg text-mineshaft-100">
<Skeleton className="w-3/4 bg-mineshaft-600" />
</div>
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
<div className="flex justify-end">
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
</div>
))}
{orgWorkspaces
.filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
.map((workspace) => (
<div
key={workspace._id}
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="mt-0 text-lg text-mineshaft-100">{workspace.name}</div>
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
{workspace.environments?.length || 0} environments
</div>
<button
type="button"
onClick={() => { onClick={() => {
router.push(`/project/${workspace._id}/secrets/overview`); if (isAddingProjectsAllowed) {
localStorage.setItem("projectData.id", workspace._id); handlePopUpOpen("addNewWs");
} else {
handlePopUpOpen("upgradePlan");
}
}} }}
className="ml-2"
> >
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200"> Add New Project
Explore{" "} </Button>
)}
</OrgPermissionCan>
</div>
<div className="mt-4 grid w-full grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{isWorkspaceLoading &&
Array.apply(0, Array(3)).map((_x, i) => (
<div
key={`workspace-cards-loading-${i + 1}`}
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="mt-0 text-lg text-mineshaft-100">
<Skeleton className="w-3/4 bg-mineshaft-600" />
</div>
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
<div className="flex justify-end">
<Skeleton className="w-1/2 bg-mineshaft-600" />
</div>
</div>
))}
{orgWorkspaces
.filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
.map((workspace) => (
<div
key={workspace._id}
className="min-w-72 flex h-40 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="mt-0 text-lg text-mineshaft-100">{workspace.name}</div>
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
{workspace.environments?.length || 0} environments
</div>
<button
type="button"
onClick={() => {
router.push(`/project/${workspace._id}/secrets/overview`);
localStorage.setItem("projectData.id", workspace._id);
}}
>
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200">
Explore{" "}
<FontAwesomeIcon
icon={faArrowRight}
className="pl-1.5 pr-0.5 duration-200 group-hover:pl-2 group-hover:pr-0"
/>
</div>
</button>
</div>
))}
</div>
{isWorkspaceEmpty && (
<div className="w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
<FontAwesomeIcon
icon={faFolderOpen}
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
/>
<div className="text-center font-light">
You are not part of any projects in this organization yet. When you are, they will
appear here.
</div>
<div className="mt-0.5 text-center font-light">
Create a new project, or ask other organization members to give you necessary
permissions.
</div>
</div>
)}
</div>
{new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000 && (
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<LearningItemSquare
text="Watch Infisical demo"
subText="Set up Infisical in 3 min."
complete={hasUserClickedIntro}
icon={faHandPeace}
time="3 min"
userAction="intro_cta_clicked"
link="https://www.youtube.com/watch?v=PK23097-25I"
/>
{orgWorkspaces.length !== 0 && (
<>
<LearningItemSquare
text="Add your secrets"
subText="Drop a .env file or type your secrets."
complete={hasUserPushedSecrets}
icon={faPlus}
time="1 min"
userAction="first_time_secrets_pushed"
link={`/project/${orgWorkspaces[0]?._id}/secrets/overview`}
/>
<LearningItemSquare
text="Invite your teammates"
subText="Infisical is better used as a team."
complete={usersInOrg}
icon={faUserPlus}
time="2 min"
link={`/org/${router.query.id}/members?action=invite`}
/>
</>
)}
<div className="block xl:hidden 2xl:block">
<LearningItemSquare
text="Join Infisical Slack"
subText="Have any questions? Ask us!"
complete={hasUserClickedSlack}
icon={faSlack}
time="1 min"
userAction="slack_cta_clicked"
link="https://infisical.com/slack"
/>
</div>
</div>
{orgWorkspaces.length !== 0 && (
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 text-mineshaft-100 shadow-xl duration-200">
<div className="mb-4 flex w-full flex-row items-center pr-4">
<div className="mr-4 flex w-full flex-row items-center">
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
{false && (
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-5 w-5 text-4xl text-green"
/>
</div>
)}
<div className="flex flex-col items-start pl-0.5">
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
<div className="text-sm font-normal">
Replace .env files with a more secure and efficient alternative.
</div>
</div>
</div>
<div
className={`w-28 pr-4 text-right text-sm font-semibold ${
false && "text-green"
}`}
>
About 2 min
</div>
</div>
<TabsObject />
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
</div>
)}
{orgWorkspaces.length !== 0 && (
<LearningItem
text="Integrate Infisical with your infrastructure"
subText="Connect Infisical to various 3rd party services and platforms."
complete={false}
icon={faPlug}
time="15 min"
link="https://infisical.com/docs/integrations/overview"
/>
)}
</div>
)}
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-6 text-3xl">
<p className="mr-4 font-semibold text-white">Explore More</p>
<div
className="mt-4 grid w-full grid-flow-dense gap-4"
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(256px, 4fr))" }}
>
{features.map((feature) => (
<div
key={feature._id}
className="flex h-44 w-96 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
>
<div className="mt-0 text-lg text-mineshaft-100">{feature.name}</div>
<div className="mb-4 mt-2 text-[15px] font-light text-mineshaft-300">
{feature.description}
</div>
<div className="flex w-full items-center">
<div className="text-[15px] font-light text-mineshaft-300">
Setup time: 20 min
</div>
<a
target="_blank"
rel="noopener noreferrer"
className="group ml-auto w-max cursor-default rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200"
href="https://infisical.com/docs/documentation/getting-started/kubernetes"
>
Learn more{" "}
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowRight} icon={faArrowRight}
className="pl-1.5 pr-0.5 duration-200 group-hover:pl-2 group-hover:pr-0" className="pl-1.5 pr-0.5 duration-200 group-hover:pl-2 group-hover:pr-0"
/> />
</div> </a>
</button> </div>
</div> </div>
))} ))}
</div>
{isWorkspaceEmpty && (
<div className="w-full rounded-md border border-mineshaft-700 bg-mineshaft-800 px-4 py-6 text-base text-mineshaft-300">
<FontAwesomeIcon
icon={faFolderOpen}
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
/>
<div className="text-center font-light">
You are not part of any projects in this organization yet. When you are, they will
appear here.
</div>
<div className="mt-0.5 text-center font-light">
Create a new project, or ask other organization members to give you necessary
permissions.
</div>
</div> </div>
)}
</div>
{new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000 && (
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<LearningItemSquare
text="Watch Infisical demo"
subText="Set up Infisical in 3 min."
complete={hasUserClickedIntro}
icon={faHandPeace}
time="3 min"
userAction="intro_cta_clicked"
link="https://www.youtube.com/watch?v=PK23097-25I"
/>
{orgWorkspaces.length !== 0 && (
<>
<LearningItemSquare
text="Add your secrets"
subText="Drop a .env file or type your secrets."
complete={hasUserPushedSecrets}
icon={faPlus}
time="1 min"
userAction="first_time_secrets_pushed"
link={`/project/${orgWorkspaces[0]?._id}/secrets/overview`}
/>
<LearningItemSquare
text="Invite your teammates"
subText="Infisical is better used as a team."
complete={usersInOrg}
icon={faUserPlus}
time="2 min"
link={`/org/${router.query.id}/members?action=invite`}
/>
</>
)}
<div className="block xl:hidden 2xl:block">
<LearningItemSquare
text="Join Infisical Slack"
subText="Have any questions? Ask us!"
complete={hasUserClickedSlack}
icon={faSlack}
time="1 min"
userAction="slack_cta_clicked"
link="https://infisical.com/slack"
/>
</div>
</div>
{orgWorkspaces.length !== 0 && (
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 text-mineshaft-100 shadow-xl duration-200">
<div className="mb-4 flex w-full flex-row items-center pr-4">
<div className="mr-4 flex w-full flex-row items-center">
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
{false && (
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-5 w-5 text-4xl text-green"
/>
</div>
)}
<div className="flex flex-col items-start pl-0.5">
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
<div className="text-sm font-normal">
Replace .env files with a more secure and efficient alternative.
</div>
</div>
</div>
<div
className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`}
>
About 2 min
</div>
</div>
<TabsObject />
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
</div>
)}
{orgWorkspaces.length !== 0 && (
<LearningItem
text="Integrate Infisical with your infrastructure"
subText="Connect Infisical to various 3rd party services and platforms."
complete={false}
icon={faPlug}
time="15 min"
link="https://infisical.com/docs/integrations/overview"
/>
)}
</div> </div>
)} <Modal
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-6 text-3xl"> isOpen={popUp.addNewWs.isOpen}
<p className="mr-4 font-semibold text-white">Explore More</p> onOpenChange={(isModalOpen) => {
<div handlePopUpToggle("addNewWs", isModalOpen);
className="mt-4 grid w-full grid-flow-dense gap-4" reset();
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(256px, 4fr))" }} }}
> >
{features.map((feature) => ( <ModalContent
<div title="Create a new project"
key={feature._id} subTitle="This project will contain your secrets and configurations."
className="flex h-44 w-96 flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4" >
> <form onSubmit={handleSubmit(onCreateProject)}>
<div className="mt-0 text-lg text-mineshaft-100">{feature.name}</div>
<div className="mb-4 mt-2 text-[15px] font-light text-mineshaft-300">
{feature.description}
</div>
<div className="flex w-full items-center">
<div className="text-[15px] font-light text-mineshaft-300">Setup time: 20 min</div>
<a
target="_blank"
rel="noopener noreferrer"
className="group ml-auto w-max cursor-default rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200"
href="https://infisical.com/docs/documentation/getting-started/kubernetes"
>
Learn more{" "}
<FontAwesomeIcon
icon={faArrowRight}
className="pl-1.5 pr-0.5 duration-200 group-hover:pl-2 group-hover:pr-0"
/>
</a>
</div>
</div>
))}
</div>
</div>
<Modal
isOpen={popUp.addNewWs.isOpen}
onOpenChange={(isModalOpen) => {
handlePopUpToggle("addNewWs", isModalOpen);
reset();
}}
>
<ModalContent
title="Create a new project"
subTitle="This project will contain your secrets and configurations."
>
<form onSubmit={handleSubmit(onCreateProject)}>
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Type your project name" />
</FormControl>
)}
/>
<div className="mt-4 pl-1">
<Controller <Controller
control={control} control={control}
name="addMembers" name="name"
defaultValue defaultValue=""
render={({ field: { onBlur, value, onChange } }) => ( render={({ field, fieldState: { error } }) => (
<Checkbox <FormControl
id="add-project-layout" label="Project Name"
isChecked={value} isError={Boolean(error)}
onCheckedChange={onChange} errorText={error?.message}
onBlur={onBlur}
> >
Add all members of my organization to this project <Input {...field} placeholder="Type your project name" />
</Checkbox> </FormControl>
)} )}
/> />
</div> <div className="mt-4 pl-1">
<div className="mt-7 flex items-center"> <Controller
<Button control={control}
isDisabled={isSubmitting} name="addMembers"
isLoading={isSubmitting} defaultValue
key="layout-create-project-submit" render={({ field: { onBlur, value, onChange } }) => (
className="mr-4" <Checkbox
type="submit" id="add-project-layout"
> isChecked={value}
Create Project onCheckedChange={onChange}
</Button> onBlur={onBlur}
<Button >
key="layout-cancel-create-project" Add all members of my organization to this project
onClick={() => handlePopUpClose("addNewWs")} </Checkbox>
variant="plain" )}
colorSchema="secondary" />
> </div>
Cancel <div className="mt-7 flex items-center">
</Button> <Button
</div> isDisabled={isSubmitting}
</form> isLoading={isSubmitting}
</ModalContent> key="layout-create-project-submit"
</Modal> className="mr-4"
<UpgradePlanModal type="submit"
isOpen={popUp.upgradePlan.isOpen} >
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} Create Project
text="You have exceeded the number of projects allowed on the free plan." </Button>
/> <Button
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */} key="layout-cancel-create-project"
</div> onClick={() => handlePopUpClose("addNewWs")}
); variant="plain"
} colorSchema="secondary"
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You have exceeded the number of projects allowed on the free plan."
/>
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
</div>
);
},
{
action: OrgWorkspacePermissionActions.Read,
subject: OrgPermissionSubjects.Workspace
}
);
Organization.requireAuth = true; Object.assign(OrganizationPage, { requireAuth: true });
export default OrganizationPage;
@@ -1,89 +1,133 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router" import { useRouter } from "next/router";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2"; import { Button } from "@app/components/v2";
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components"; import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
import createNewIntegrationSession from "../../../api/secret-scanning/createSecretScanningSession"; import createNewIntegrationSession from "../../../api/secret-scanning/createSecretScanningSession";
import getInstallationStatus from "../../../api/secret-scanning/getInstallationStatus"; import getInstallationStatus from "../../../api/secret-scanning/getInstallationStatus";
import linkGitAppInstallationWithOrganization from "../../../api/secret-scanning/linkGitAppInstallationWithOrganization"; import linkGitAppInstallationWithOrganization from "../../../api/secret-scanning/linkGitAppInstallationWithOrganization";
export default function SecretScanning() { const SecretScanning = withPermission(
const router = useRouter() () => {
const queryParams = router.query const router = useRouter();
const [integrationEnabled, setIntegrationStatus] = useState(false) const queryParams = router.query;
const [integrationEnabled, setIntegrationStatus] = useState(false);
useEffect(()=>{ useEffect(() => {
const linkInstallation = async () => { const linkInstallation = async () => {
if (typeof queryParams.state === "string" && typeof queryParams.installation_id === "string"){ if (
try { typeof queryParams.state === "string" &&
const isLinked = await linkGitAppInstallationWithOrganization(queryParams.installation_id as string, queryParams.state as string) typeof queryParams.installation_id === "string"
if (isLinked){ ) {
router.reload() try {
const isLinked = await linkGitAppInstallationWithOrganization(
queryParams.installation_id as string,
queryParams.state as string
);
if (isLinked) {
router.reload();
}
console.log("installation verification complete");
} catch (e) {
console.log("app installation is stale, start new session", e);
} }
console.log("installation verification complete")
}catch (e){
console.log("app installation is stale, start new session", e)
} }
} };
}
const fetchInstallationStatus = async () => { const fetchInstallationStatus = async () => {
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id"))) const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")));
setIntegrationStatus(status) setIntegrationStatus(status);
} };
fetchInstallationStatus() fetchInstallationStatus();
linkInstallation() linkInstallation();
},[queryParams.state, queryParams.installation_id]) }, [queryParams.state, queryParams.installation_id]);
const generateNewIntegrationSession = async () => { const generateNewIntegrationSession = async () => {
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id"))) const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")));
router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`) router.push(
} `https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`
);
};
return ( return (
<div> <div>
<Head> <Head>
<title>Secret scanning</title> <title>Secret scanning</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" /> <meta property="og:image" content="/images/message.png" />
</Head> </Head>
<div className="flex justify-center bg-bunker-800 text-white w-full h-full"> <div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<div className="max-w-7xl px-6 w-full"> <div className="max-w-7xl px-6 w-full">
<div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div> <div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div>
<div className="mb-6 text-lg text-mineshaft-300">Automatically monitor your GitHub activity and prevent secret leaks</div> <div className="mb-6 text-lg text-mineshaft-300">
<div className="relative flex justify-between bg-mineshaft-800 border border-mineshaft-600 rounded-md p-6 mb-6"> Automatically monitor your GitHub activity and prevent secret leaks
<div className="flex flex-col items-start">
<div className="flex flex-row mb-1">Secret Scanning Status: {integrationEnabled ? <p className="text-green ml-1.5 font-semibold">Enabled</p> : <p className="text-red ml-1.5 font-semibold">Not enabled</p>}</div>
<div>{integrationEnabled ? <p className="text-mineshaft-300">Your GitHub organization is connected to Infisical, and is being continuously monitored for secret leaks.</p> : <p className="text-mineshaft-300">Connect your GitHub organization to Infisical.</p>}</div>
</div> </div>
{integrationEnabled ? ( <div className="relative flex justify-between bg-mineshaft-800 border border-mineshaft-600 rounded-md p-6 mb-6">
<div> <div className="flex flex-col items-start">
<div className="absolute right-[2.5rem] top-[2.5rem] animate-ping rounded-full h-6 w-6 bg-green flex items-center justify-center"/> <div className="flex flex-row mb-1">
<div className="absolute right-[2.63rem] top-[2.63rem] animate-ping rounded-full h-5 w-5 bg-green flex items-center justify-center"/> Secret Scanning Status:{" "}
<div className="absolute right-[2.82rem] top-[2.82rem] animate-ping rounded-full h-3.5 w-3.5 bg-green flex items-center justify-center"/> {integrationEnabled ? (
<p className="text-green ml-1.5 font-semibold">Enabled</p>
) : (
<p className="text-red ml-1.5 font-semibold">Not enabled</p>
)}
</div>
<div>
{integrationEnabled ? (
<p className="text-mineshaft-300">
Your GitHub organization is connected to Infisical, and is being continuously
monitored for secret leaks.
</p>
) : (
<p className="text-mineshaft-300">
Connect your GitHub organization to Infisical.
</p>
)}
</div>
</div> </div>
) : ( {integrationEnabled ? (
<div className="flex items-center h-[3.25rem]"> <div>
<Button <div className="absolute right-[2.5rem] top-[2.5rem] animate-ping rounded-full h-6 w-6 bg-green flex items-center justify-center" />
variant="solid" <div className="absolute right-[2.63rem] top-[2.63rem] animate-ping rounded-full h-5 w-5 bg-green flex items-center justify-center" />
colorSchema="primary" <div className="absolute right-[2.82rem] top-[2.82rem] animate-ping rounded-full h-3.5 w-3.5 bg-green flex items-center justify-center" />
onClick={generateNewIntegrationSession} </div>
className="py-2 h-min" ) : (
> <div className="flex items-center h-[3.25rem]">
Integrate with GitHub <OrgPermissionCan
</Button> I={OrgGeneralPermissionActions.Create}
</div> a={OrgPermissionSubjects.SecretScanning}
)} >
{(isAllowed) => (
<Button
variant="solid"
colorSchema="primary"
onClick={generateNewIntegrationSession}
className="py-2 h-min"
isDisabled={!isAllowed}
>
Integrate with GitHub
</Button>
)}
</OrgPermissionCan>
</div>
)}
</div>
<SecretScanningLogsTable />
</div> </div>
<SecretScanningLogsTable />
</div> </div>
</div> </div>
</div> );
); },
} { action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.SecretScanning }
);
SecretScanning.requireAuth = true; Object.assign(SecretScanning, { requireAuth: true });
export default SecretScanning;
@@ -3,7 +3,8 @@ import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { withPermission } from "@app/hoc";
import { useGetRoles } from "@app/hooks/api"; import { useGetRoles } from "@app/hooks/api";
import { OrgMembersTable } from "./components/OrgMembersTable"; import { OrgMembersTable } from "./components/OrgMembersTable";
@@ -14,45 +15,48 @@ enum TabSections {
Roles = "roles" Roles = "roles"
} }
export const MembersPage = () => { export const MembersPage = withPermission(
const { t } = useTranslation(); () => {
const { currentOrg } = useOrganization(); const { t } = useTranslation();
const { currentOrg } = useOrganization();
const orgId = currentOrg?._id || ""; const orgId = currentOrg?._id || "";
const { data: roles } = useGetRoles({ const { data: roles } = useGetRoles({
orgId orgId
}); });
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mb-6 w-full py-6 px-6 max-w-7xl mx-auto"> <div className="mb-6 w-full py-6 px-6 max-w-7xl mx-auto">
<p className="mr-4 mb-4 text-3xl font-semibold text-white"> <p className="mr-4 mb-4 text-3xl font-semibold text-white">
{t("section.members.org-members")} {t("section.members.org-members")}
</p> </p>
<Tabs defaultValue={TabSections.Member}> <Tabs defaultValue={TabSections.Member}>
<TabList> <TabList>
<Tab value={TabSections.Member}>Members</Tab> <Tab value={TabSections.Member}>Members</Tab>
{process.env.NEXT_PUBLIC_NEW_PERMISSION_FLAG === "true" && ( {process.env.NEXT_PUBLIC_NEW_PERMISSION_FLAG === "true" && (
<Tab value={TabSections.Roles}>Roles</Tab> <Tab value={TabSections.Roles}>Roles</Tab>
)} )}
</TabList> </TabList>
<TabPanel value={TabSections.Member}> <TabPanel value={TabSections.Member}>
<motion.div <motion.div
key="panel-1" key="panel-1"
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }} initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }} animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }} exit={{ opacity: 0, translateX: 30 }}
> >
<OrgMembersTable roles={roles} /> <OrgMembersTable roles={roles} />
</motion.div> </motion.div>
</TabPanel> </TabPanel>
<TabPanel value={TabSections.Roles}> <TabPanel value={TabSections.Roles}>
<OrgRoleTabSection roles={roles} /> <OrgRoleTabSection roles={roles} />
</TabPanel> </TabPanel>
</Tabs> </Tabs>
</div>
</div> </div>
</div> );
); },
}; { action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.Member }
);
@@ -14,6 +14,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
decryptAssymmetric, decryptAssymmetric,
encryptAssymmetric encryptAssymmetric
@@ -41,7 +42,14 @@ import {
Tr, Tr,
UpgradePlanModal UpgradePlanModal
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context"; import {
OrgGeneralPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription,
useUser,
useWorkspace
} from "@app/context";
import { usePopUp, useToggle } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { import {
useAddUserToOrg, useAddUserToOrg,
@@ -297,27 +305,32 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
placeholder="Search members..." placeholder="Search members..."
/> />
</div> </div>
<Button <OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Member}>
leftIcon={<FontAwesomeIcon icon={faPlus} />} {(isAllowed) => (
onClick={() => { <Button
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) { isDisabled={!isAllowed}
createNotification({ leftIcon={<FontAwesomeIcon icon={faPlus} />}
text: "You cannot invite users when SAML SSO is configured for your organization", onClick={() => {
type: "error" if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
}); createNotification({
text: "You cannot invite users when SAML SSO is configured for your organization",
type: "error"
});
return; return;
} }
if (isMoreUsersNotAllowed) { if (isMoreUsersNotAllowed) {
handlePopUpOpen("upgradePlan"); handlePopUpOpen("upgradePlan");
} else { } else {
handlePopUpOpen("addMember"); handlePopUpOpen("addMember");
} }
}} }}
> >
Add Member Add Member
</Button> </Button>
)}
</OrgPermissionCan>
</div> </div>
<div> <div>
<TableContainer> <TableContainer>
@@ -345,48 +358,59 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
<Td>{name}</Td> <Td>{name}</Td>
<Td>{email}</Td> <Td>{email}</Td>
<Td> <Td>
{status === "accepted" && ( <OrgPermissionCan
<Select I={OrgGeneralPermissionActions.Edit}
defaultValue={ a={OrgPermissionSubjects.Member}
role === "custom" ? findRoleFromId(customRole)?.slug : role >
} {(isAllowed) => (
isDisabled={userId === u?._id} <>
className="w-40 bg-mineshaft-600" {status === "accepted" && (
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800" <Select
onValueChange={(selectedRole) => defaultValue={
onRoleChange(orgMembershipId, selectedRole) role === "custom" ? findRoleFromId(customRole)?.slug : role
} }
> isDisabled={userId === u?._id || !isAllowed}
{roles className="w-40 bg-mineshaft-600"
.filter(({ slug }) => dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
slug === "owner" ? isIamOwner || role === "owner" : true onValueChange={(selectedRole) =>
) onRoleChange(orgMembershipId, selectedRole)
.map(({ slug, name: roleName }) => ( }
<SelectItem value={slug} key={`owner-option-${slug}`}> >
{roleName} {roles
</SelectItem> .filter(({ slug }) =>
))} slug === "owner" ? isIamOwner || role === "owner" : true
</Select> )
)} .map(({ slug, name: roleName }) => (
{(status === "invited" || status === "verified") && <SelectItem value={slug} key={`owner-option-${slug}`}>
serverDetails?.emailConfigured && ( {roleName}
<Button </SelectItem>
className="w-40" ))}
colorSchema="primary" </Select>
variant="outline_bg" )}
onClick={() => onAddUserToOrg(email)} {(status === "invited" || status === "verified") &&
> serverDetails?.emailConfigured && (
Resend Invite <Button
</Button> isDisabled={!isAllowed}
className="w-40"
colorSchema="primary"
variant="outline_bg"
onClick={() => onAddUserToOrg(email)}
>
Resend Invite
</Button>
)}
{status === "completed" && (
<Button
colorSchema="secondary"
isDisabled={!isAllowed}
onClick={() => onGrantAccess(u?._id, u?.publicKey)}
>
Grant Access
</Button>
)}
</>
)} )}
{status === "completed" && ( </OrgPermissionCan>
<Button
colorSchema="secondary"
onClick={() => onGrantAccess(u?._id, u?.publicKey)}
>
Grant Access
</Button>
)}
</Td> </Td>
<Td> <Td>
{userWs ? ( {userWs ? (
@@ -428,16 +452,23 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
</Td> </Td>
<Td> <Td>
{userId !== u?._id && ( {userId !== u?._id && (
<IconButton <OrgPermissionCan
ariaLabel="delete" I={OrgGeneralPermissionActions.Delete}
colorSchema="danger" a={OrgPermissionSubjects.Member}
isDisabled={userId === u?._id}
onClick={() =>
handlePopUpOpen("removeMember", { id: orgMembershipId })
}
> >
<FontAwesomeIcon icon={faTrash} /> {(isAllowed) => (
</IconButton> <IconButton
ariaLabel="delete"
colorSchema="danger"
isDisabled={userId === u?._id || !isAllowed}
onClick={() =>
handlePopUpOpen("removeMember", { id: orgMembershipId })
}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</OrgPermissionCan>
)} )}
</Td> </Td>
</Tr> </Tr>
@@ -38,10 +38,9 @@ export const BillingPermission = ({ isNonEditable, setValue, control }: Props) =
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
@@ -52,11 +51,14 @@ export const BillingPermission = ({ isNonEditable, setValue, control }: Props) =
}, [rule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setValue( setValue(
@@ -38,10 +38,9 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
@@ -52,14 +51,16 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
}, [rule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setIsCustom.off();
setValue( setValue(
"permissions.incident-contact", "permissions.incident-contact",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -67,7 +68,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
); );
break; break;
case Permission.FullAccess: case Permission.FullAccess:
setIsCustom.off();
setValue( setValue(
"permissions.incident-contact", "permissions.incident-contact",
{ read: true, edit: true, create: true, delete: true }, { read: true, edit: true, create: true, delete: true },
@@ -75,7 +75,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
); );
break; break;
case Permission.ReadOnly: case Permission.ReadOnly:
setIsCustom.off();
setValue( setValue(
"permissions.incident-contact", "permissions.incident-contact",
{ read: true, edit: false, create: false, delete: false }, { read: true, edit: false, create: false, delete: false },
@@ -83,7 +82,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
); );
break; break;
default: default:
setIsCustom.on();
setValue( setValue(
"permissions.incident-contact", "permissions.incident-contact",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -31,31 +31,34 @@ const PERMISSIONS = [
] as const; ] as const;
export const MemberPermission = ({ isNonEditable, setValue, control }: Props) => { export const MemberPermission = ({ isNonEditable, setValue, control }: Props) => {
const memberRule = useWatch({ const rule = useWatch({
control, control,
name: "permissions.member" name: "permissions.member"
}); });
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const actions = Object.keys(memberRule || {}) as Array<keyof typeof memberRule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += memberRule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess; if (score === totalActions) return Permission.FullAccess;
if (score === 1 && memberRule.read) return Permission.ReadOnly; if (score === 1 && rule.read) return Permission.ReadOnly;
return Permission.Custom; return Permission.Custom;
}, [memberRule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setValue( setValue(
@@ -31,34 +31,36 @@ const PERMISSIONS = [
] as const; ] as const;
export const RolePermission = ({ isNonEditable, setValue, control }: Props) => { export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
const roleRule = useWatch({ const rule = useWatch({
control, control,
name: "permissions.role" name: "permissions.role"
}); });
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const actions = Object.keys(roleRule || {}) as Array<keyof typeof roleRule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += roleRule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess; if (score === totalActions) return Permission.FullAccess;
if (score === 1 && roleRule.read) return Permission.ReadOnly; if (score === 1 && rule.read) return Permission.ReadOnly;
return Permission.Custom; return Permission.Custom;
}, [roleRule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setIsCustom.off();
setValue( setValue(
"permissions.role", "permissions.role",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
); );
break; break;
case Permission.FullAccess: case Permission.FullAccess:
setIsCustom.off();
setValue( setValue(
"permissions.role", "permissions.role",
{ read: true, edit: true, create: true, delete: true }, { read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
); );
break; break;
case Permission.ReadOnly: case Permission.ReadOnly:
setIsCustom.off();
setValue( setValue(
"permissions.role", "permissions.role",
{ read: true, edit: false, create: false, delete: false }, { read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
); );
break; break;
default: default:
setIsCustom.on();
setValue( setValue(
"permissions.role", "permissions.role",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -38,10 +38,9 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
@@ -52,13 +51,16 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
}, [rule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setIsCustom.off();
setValue( setValue(
"permissions.secret-scanning", "permissions.secret-scanning",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
); );
break; break;
case Permission.FullAccess: case Permission.FullAccess:
setIsCustom.off();
setValue( setValue(
"permissions.secret-scanning", "permissions.secret-scanning",
{ read: true, edit: true, create: true, delete: true }, { read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
); );
break; break;
case Permission.ReadOnly: case Permission.ReadOnly:
setIsCustom.off();
setValue( setValue(
"permissions.secret-scanning", "permissions.secret-scanning",
{ read: true, edit: false, create: false, delete: false }, { read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
); );
break; break;
default: default:
setIsCustom.on();
setValue( setValue(
"permissions.secret-scanning", "permissions.secret-scanning",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -38,10 +38,9 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
@@ -52,13 +51,16 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
}, [rule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setIsCustom.off();
setValue( setValue(
"permissions.settings", "permissions.settings",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
); );
break; break;
case Permission.FullAccess: case Permission.FullAccess:
setIsCustom.off();
setValue( setValue(
"permissions.settings", "permissions.settings",
{ read: true, edit: true, create: true, delete: true }, { read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
); );
break; break;
case Permission.ReadOnly: case Permission.ReadOnly:
setIsCustom.off();
setValue( setValue(
"permissions.settings", "permissions.settings",
{ read: true, edit: false, create: false, delete: false }, { read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
); );
break; break;
default: default:
setIsCustom.on();
setValue( setValue(
"permissions.settings", "permissions.settings",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -38,10 +38,9 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
@@ -52,13 +51,16 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
}, [rule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setIsCustom.off();
setValue( setValue(
"permissions.sso", "permissions.sso",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -66,7 +68,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
); );
break; break;
case Permission.FullAccess: case Permission.FullAccess:
setIsCustom.off();
setValue( setValue(
"permissions.sso", "permissions.sso",
{ read: true, edit: true, create: true, delete: true }, { read: true, edit: true, create: true, delete: true },
@@ -74,7 +75,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
); );
break; break;
case Permission.ReadOnly: case Permission.ReadOnly:
setIsCustom.off();
setValue( setValue(
"permissions.sso", "permissions.sso",
{ read: true, edit: false, create: false, delete: false }, { read: true, edit: false, create: false, delete: false },
@@ -82,7 +82,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
); );
break; break;
default: default:
setIsCustom.on();
setValue( setValue(
"permissions.sso", "permissions.sso",
{ read: false, edit: false, create: false, delete: false }, { read: false, edit: false, create: false, delete: false },
@@ -36,10 +36,9 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
const [isCustom, setIsCustom] = useToggle(); const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => { const selectedPermissionCategory = useMemo(() => {
let score = 0;
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>; const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length; const totalActions = PERMISSIONS.length;
actions.forEach((key) => (score += rule[key] ? 1 : 0)); const score = actions.map((key) => (rule[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom; if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess; if (score === 0) return Permission.NoAccess;
@@ -50,11 +49,14 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
}, [rule, isCustom]); }, [rule, isCustom]);
useEffect(() => { useEffect(() => {
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]); }, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => { const handlePermissionChange = (val: Permission) => {
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off(); if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) { switch (val) {
case Permission.NoAccess: case Permission.NoAccess:
setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true }); setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });
@@ -1,28 +1,47 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { OrgPermissionCan } from "@app/components/permissions";
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import updateRiskStatus, { RiskStatus } from "@app/pages/api/secret-scanning/updateRiskStatus"; import updateRiskStatus, { RiskStatus } from "@app/pages/api/secret-scanning/updateRiskStatus";
export const RiskStatusSelection = ({riskId, currentSelection}: {riskId: any, currentSelection: any }) => { export const RiskStatusSelection = ({
const [selectedRiskStatus, setSelectedRiskStatus] = useState(currentSelection); riskId,
useEffect(()=>{ currentSelection
if (currentSelection !== selectedRiskStatus){ }: {
const updateSelection = async () =>{ riskId: any;
await updateRiskStatus(String(localStorage.getItem("orgData.id")), riskId, selectedRiskStatus) currentSelection: any;
} }) => {
updateSelection() const [selectedRiskStatus, setSelectedRiskStatus] = useState(currentSelection);
} useEffect(() => {
},[selectedRiskStatus]) if (currentSelection !== selectedRiskStatus) {
const updateSelection = async () => {
await updateRiskStatus(
String(localStorage.getItem("orgData.id")),
riskId,
selectedRiskStatus
);
};
updateSelection();
}
}, [selectedRiskStatus]);
return ( return (
<select <OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.SecretScanning}>
value={selectedRiskStatus} {(isAllowed) => (
onChange={(e) => setSelectedRiskStatus(e.target.value)} <select
className="block w-full py-2 px-3 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" disabled={!isAllowed}
> value={selectedRiskStatus}
<option>Unresolved</option> onChange={(e) => setSelectedRiskStatus(e.target.value)}
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>This is a false positive, resolved</option> className="block w-full py-2 px-3 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolved</option> >
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolved</option> <option>Unresolved</option>
</select> <option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>
); This is a false positive, resolved
} </option>
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolved</option>
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolved</option>
</select>
)}
</OrgPermissionCan>
);
};
@@ -1,125 +1,148 @@
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2"; import { Button } from "@app/components/v2";
import { useOrganization,useSubscription } from "@app/context"; import {
import { OrgGeneralPermissionActions,
useCreateCustomerPortalSession, OrgPermissionSubjects,
useGetOrgPlanBillingInfo, useOrganization,
useGetOrgTrialUrl useSubscription
} from "@app/context";
import {
useCreateCustomerPortalSession,
useGetOrgPlanBillingInfo,
useGetOrgTrialUrl
} from "@app/hooks/api"; } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { ManagePlansModal } from "./ManagePlansModal"; import { ManagePlansModal } from "./ManagePlansModal";
export const PreviewSection = () => { export const PreviewSection = () => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?._id ?? ""); const { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?._id ?? "");
const getOrgTrialUrl = useGetOrgTrialUrl(); const getOrgTrialUrl = useGetOrgTrialUrl();
const createCustomerPortalSession = useCreateCustomerPortalSession(); const createCustomerPortalSession = useCreateCustomerPortalSession();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"managePlan"
] as const);
const formatAmount = (amount: number) => {
const formattedTotal = (Math.floor(amount) / 100).toLocaleString("en-US", {
style: "currency",
currency: "USD",
});
return formattedTotal;
}
const formatDate = (date: number) => {
const createdDate = new Date(date * 1000);
const day: number = createdDate.getDate();
const month: number = createdDate.getMonth() + 1;
const year: number = createdDate.getFullYear();
const formattedDate: string = `${day}/${month}/${year}`;
return formattedDate;
}
function formatPlanSlug(slug: string) { const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["managePlan"] as const);
return slug
.replace(/(\b[a-z])/g, match => match.toUpperCase()) const formatAmount = (amount: number) => {
.replace(/-/g, " "); const formattedTotal = (Math.floor(amount) / 100).toLocaleString("en-US", {
style: "currency",
currency: "USD"
});
return formattedTotal;
};
const formatDate = (date: number) => {
const createdDate = new Date(date * 1000);
const day: number = createdDate.getDate();
const month: number = createdDate.getMonth() + 1;
const year: number = createdDate.getFullYear();
const formattedDate: string = `${day}/${month}/${year}`;
return formattedDate;
};
function formatPlanSlug(slug: string) {
return slug.replace(/(\b[a-z])/g, (match) => match.toUpperCase()).replace(/-/g, " ");
}
const handleUpgradeBtnClick = async () => {
try {
if (!subscription || !currentOrg) return;
if (!subscription.has_used_trial) {
// direct user to start pro trial
const url = await getOrgTrialUrl.mutateAsync({
orgId: currentOrg._id,
success_url: window.location.href
});
window.location.href = url;
} else {
// open compare plans modal
handlePopUpOpen("managePlan");
}
} catch (err) {
console.error(err);
} }
};
const handleUpgradeBtnClick = async () => {
try { return (
if (!subscription || !currentOrg) return; <div>
{subscription &&
if (!subscription.has_used_trial) { subscription?.slug !== "enterprise" &&
// direct user to start pro trial subscription?.slug !== "pro" &&
const url = await getOrgTrialUrl.mutateAsync({ subscription?.slug !== "pro-annual" && (
orgId: currentOrg._id, <div className="p-4 rounded-lg flex-1 border border-mineshaft-600 mb-6 flex items-center bg-mineshaft-600">
success_url: window.location.href <div className="flex-1">
}); <h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2>
<p className="text-gray-400 mt-4">
window.location.href = url; Unlimited members, projects, RBAC, smart alerts, and so much more
} else { </p>
// open compare plans modal </div>
handlePopUpOpen("managePlan"); <OrgPermissionCan
} I={OrgGeneralPermissionActions.Create}
} catch (err) { a={OrgPermissionSubjects.Billing}
console.error(err); >
} {(isAllowed) => (
} <Button
onClick={() => handleUpgradeBtnClick()}
return ( color="mineshaft"
<div> isDisabled={!isAllowed}
{subscription && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && ( >
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mb-6 flex items-center bg-mineshaft-600"> {!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
<div className="flex-1"> </Button>
<h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2> )}
<p className="text-gray-400 mt-4">Unlimited members, projects, RBAC, smart alerts, and so much more</p> </OrgPermissionCan>
</div> </div>
<Button )}
// onClick={() => handlePopUpOpen("managePlan")} {!isLoading && subscription && data && (
onClick={() => handleUpgradeBtnClick()} <div className="flex mb-6">
color="mineshaft" <div className="p-4 bg-mineshaft-900 rounded-lg flex-1 mr-4 border border-mineshaft-600">
> <p className="mb-2 text-gray-400">Current plan</p>
{!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"} <p className="text-2xl text-mineshaft-50 font-semibold mb-8">
</Button> {`${formatPlanSlug(subscription.slug)} ${
</div> subscription.status === "trialing" ? "(Trial)" : ""
)} }`}
{!isLoading && subscription && data && ( </p>
<div className="flex mb-6"> <OrgPermissionCan
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 mr-4 border border-mineshaft-600"> I={OrgGeneralPermissionActions.Edit}
<p className="mb-2 text-gray-400">Current plan</p> a={OrgPermissionSubjects.Billing}
<p className="text-2xl text-mineshaft-50 font-semibold mb-8"> >
{`${formatPlanSlug(subscription.slug)} ${subscription.status === "trialing" ? "(Trial)" : ""}`} {(isAllowed) => (
</p> <button
<button type="button"
type="button" onClick={async () => {
onClick={async () => { if (!currentOrg?._id) return;
if (!currentOrg?._id) return; const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id);
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id); window.location.href = url;
window.location.href = url; }}
}} disabled={!isAllowed}
className="text-primary" className="text-primary"
> >
Manage plan &rarr; Manage plan &rarr;
</button> </button>
</div> )}
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mr-4"> </OrgPermissionCan>
<p className="mb-2 text-gray-400">Price</p> </div>
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold"> <div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mr-4">
{subscription.status === "trialing" ? "$0.00 / month" : `${formatAmount(data.amount)} / ${data.interval}`} <p className="mb-2 text-gray-400">Price</p>
</p> <p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
</div> {subscription.status === "trialing"
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600"> ? "$0.00 / month"
<p className="mb-2 text-gray-400">Subscription renews on</p> : `${formatAmount(data.amount)} / ${data.interval}`}
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold"> </p>
{formatDate(data.currentPeriodEnd)} </div>
</p> <div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600">
</div> <p className="mb-2 text-gray-400">Subscription renews on</p>
</div> <p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
)} {formatDate(data.currentPeriodEnd)}
<ManagePlansModal </p>
popUp={popUp} </div>
handlePopUpToggle={handlePopUpToggle}
/>
</div> </div>
); )}
} <ManagePlansModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</div>
);
};
@@ -1,98 +1,92 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import { OrgPermissionCan } from "@app/components/permissions";
Button, import { Button, FormControl, Input } from "@app/components/v2";
FormControl, import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
Input import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useGetOrgBillingDetails,
useUpdateOrgBillingDetails
} from "@app/hooks/api";
const schema = yup.object({ const schema = yup
.object({
name: yup.string().required("Company name is required") name: yup.string().required("Company name is required")
}).required(); })
.required();
export const CompanyNameSection = () => { export const CompanyNameSection = () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { reset, control, handleSubmit } = useForm({ const { reset, control, handleSubmit } = useForm({
defaultValues: { defaultValues: {
name: "" name: ""
}, },
resolver: yupResolver(schema) resolver: yupResolver(schema)
}); });
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? ""); const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
const { mutateAsync, isLoading } = useUpdateOrgBillingDetails(); const { mutateAsync, isLoading } = useUpdateOrgBillingDetails();
useEffect(() => {
if (data) {
reset({
name: data?.name ?? ""
});
}
}, [data]);
const onFormSubmit = async ({ name }: { name: string }) => {
try {
if (!currentOrg?._id) return;
if (name === "") return;
await mutateAsync({
name,
organizationId: currentOrg._id
});
createNotification({
text: "Successfully updated business name",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update business name",
type: "error"
});
}
}
return ( useEffect(() => {
<form if (data) {
onSubmit={handleSubmit(onFormSubmit)} reset({
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600" name: data?.name ?? ""
> });
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8"> }
Business name }, [data]);
</h2>
<div className="max-w-md"> const onFormSubmit = async ({ name }: { name: string }) => {
<Controller try {
defaultValue="" if (!currentOrg?._id) return;
render={({ field, fieldState: { error } }) => ( if (name === "") return;
<FormControl isError={Boolean(error)} errorText={error?.message}> await mutateAsync({
<Input name,
placeholder="Acme Corp" organizationId: currentOrg._id
{...field} });
className="bg-mineshaft-800"
/> createNotification({
</FormControl> text: "Successfully updated business name",
)} type: "success"
control={control} });
name="name" } catch (err) {
/> console.error(err);
</div> createNotification({
<Button text: "Failed to update business name",
type="submit" type: "error"
colorSchema="secondary" });
isLoading={isLoading} }
isDisabled={isLoading} };
>
Save return (
</Button> <form
</form> onSubmit={handleSubmit(onFormSubmit)}
); className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
} >
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">Business name</h2>
<div className="max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input placeholder="Acme Corp" {...field} className="bg-mineshaft-800" />
</FormControl>
)}
control={control}
name="name"
/>
</div>
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<Button
type="submit"
colorSchema="secondary"
isLoading={isLoading}
isDisabled={isLoading || !isAllowed}
>
Save
</Button>
)}
</OrgPermissionCan>
</form>
);
};
@@ -1,97 +1,93 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import { OrgPermissionCan } from "@app/components/permissions";
Button, import { Button, FormControl, Input } from "@app/components/v2";
FormControl, import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
Input} from "@app/components/v2"; import { useGetOrgBillingDetails, useUpdateOrgBillingDetails } from "@app/hooks/api";
import { useOrganization } from "@app/context";
import {
useGetOrgBillingDetails,
useUpdateOrgBillingDetails
} from "@app/hooks/api";
const schema = yup.object({ const schema = yup
.object({
email: yup.string().required("Email is required") email: yup.string().required("Email is required")
}).required(); })
.required();
export const InvoiceEmailSection = () => { export const InvoiceEmailSection = () => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { reset, control, handleSubmit } = useForm({ const { reset, control, handleSubmit } = useForm({
defaultValues: { defaultValues: {
email: "" email: ""
}, },
resolver: yupResolver(schema) resolver: yupResolver(schema)
}); });
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? ""); const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
const { mutateAsync, isLoading } = useUpdateOrgBillingDetails(); const { mutateAsync, isLoading } = useUpdateOrgBillingDetails();
useEffect(() => { useEffect(() => {
if (data) { if (data) {
reset({ reset({
email: data?.email ?? "" email: data?.email ?? ""
}); });
}
}, [data]);
const onFormSubmit = async ({ email }: { email: string }) => {
try {
if (!currentOrg?._id) return;
if (email === "") return;
await mutateAsync({
email,
organizationId: currentOrg._id
});
createNotification({
text: "Successfully updated invoice email recipient",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to update invoice email recipient",
type: "error"
});
}
} }
}, [data]);
return ( const onFormSubmit = async ({ email }: { email: string }) => {
<form try {
onSubmit={handleSubmit(onFormSubmit)} if (!currentOrg?._id) return;
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600" if (email === "") return;
>
<h2 className="text-xl font-semibold flex-1 text-white mb-8"> await mutateAsync({
Invoice email recipient email,
</h2> organizationId: currentOrg._id
<div className="max-w-md"> });
<Controller
defaultValue="" createNotification({
render={({ field, fieldState: { error } }) => ( text: "Successfully updated invoice email recipient",
<FormControl isError={Boolean(error)} errorText={error?.message}> type: "success"
<Input });
placeholder="[email protected]" } catch (err) {
{...field} console.error(err);
className="bg-mineshaft-800" createNotification({
/> text: "Failed to update invoice email recipient",
</FormControl> type: "error"
)} });
control={control} }
name="email" };
/>
</div> return (
<Button <form
type="submit" onSubmit={handleSubmit(onFormSubmit)}
colorSchema="secondary" className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
isLoading={isLoading} >
> <h2 className="text-xl font-semibold flex-1 text-white mb-8">Invoice email recipient</h2>
Save <div className="max-w-md">
</Button> <Controller
</form> defaultValue=""
); render={({ field, fieldState: { error } }) => (
} <FormControl isError={Boolean(error)} errorText={error?.message}>
<Input placeholder="[email protected]" {...field} className="bg-mineshaft-800" />
</FormControl>
)}
control={control}
name="email"
/>
</div>
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
{(isAllowed) => (
<Button
type="submit"
colorSchema="secondary"
isLoading={isLoading}
isDisabled={isLoading || !isAllowed}
>
Save
</Button>
)}
</OrgPermissionCan>
</form>
);
};
@@ -1,46 +1,47 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { OrgPermissionCan } from "@app/components/permissions";
Button import { Button } from "@app/components/v2";
} from "@app/components/v2"; import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useOrganization } from "@app/context";
import { useAddOrgPmtMethod } from "@app/hooks/api"; import { useAddOrgPmtMethod } from "@app/hooks/api";
import { PmtMethodsTable } from "./PmtMethodsTable"; import { PmtMethodsTable } from "./PmtMethodsTable";
export const PmtMethodsSection = () => { export const PmtMethodsSection = () => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { mutateAsync, isLoading } = useAddOrgPmtMethod(); const { mutateAsync, isLoading } = useAddOrgPmtMethod();
const handleAddPmtMethodBtnClick = async () => { const handleAddPmtMethodBtnClick = async () => {
if (!currentOrg?._id) return; if (!currentOrg?._id) return;
const url = await mutateAsync({ const url = await mutateAsync({
organizationId: currentOrg._id, organizationId: currentOrg._id,
success_url: window.location.href, success_url: window.location.href,
cancel_url: window.location.href cancel_url: window.location.href
}); });
window.location.href = url; window.location.href = url;
} };
return ( return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> <div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8"> <div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white"> <h2 className="text-xl font-semibold flex-1 text-white">Payment methods</h2>
Payment methods <OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
</h2> {(isAllowed) => (
<Button <Button
onClick={handleAddPmtMethodBtnClick} onClick={handleAddPmtMethodBtnClick}
colorSchema="secondary" colorSchema="secondary"
isLoading={isLoading} isLoading={isLoading}
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
> isDisabled={!isAllowed}
Add method >
</Button> Add method
</div> </Button>
<PmtMethodsTable /> )}
</div> </OrgPermissionCan>
); </div>
} <PmtMethodsTable />
</div>
);
};
@@ -1,6 +1,7 @@
import { faCreditCard, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faCreditCard, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
EmptyState, EmptyState,
IconButton, IconButton,
@@ -13,7 +14,7 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api"; import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
export const PmtMethodsTable = () => { export const PmtMethodsTable = () => {
@@ -52,17 +53,25 @@ export const PmtMethodsTable = () => {
<Td>{last4}</Td> <Td>{last4}</Td>
<Td>{`${exp_month}/${exp_year}`}</Td> <Td>{`${exp_month}/${exp_year}`}</Td>
<Td> <Td>
<IconButton <OrgPermissionCan
onClick={async () => { I={OrgGeneralPermissionActions.Delete}
await handleDeletePmtMethodBtnClick(_id); a={OrgPermissionSubjects.Billing}
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
> >
<FontAwesomeIcon icon={faXmark} /> {(isAllowed) => (
</IconButton> <IconButton
onClick={async () => {
await handleDeletePmtMethodBtnClick(_id);
}}
size="lg"
isDisabled={!isAllowed}
colorSchema="danger"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</OrgPermissionCan>
</Td> </Td>
</Tr> </Tr>
))} ))}
@@ -1,38 +1,42 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2"; import { Button } from "@app/components/v2";
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { TaxIDModal } from "./TaxIDModal"; import { TaxIDModal } from "./TaxIDModal";
import { TaxIDTable } from "./TaxIDTable"; import { TaxIDTable } from "./TaxIDTable";
export const TaxIDSection = () => { export const TaxIDSection = () => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addTaxID" "addTaxID"
] as const); ] as const);
return ( return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> <div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8"> <div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white"> <h2 className="text-xl font-semibold flex-1 text-white">Tax ID</h2>
Tax ID <OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
</h2> {(isAllowed) => (
<Button <Button
onClick={() => handlePopUpOpen("addTaxID")} onClick={() => handlePopUpOpen("addTaxID")}
colorSchema="secondary" colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />} isDisabled={!isAllowed}
> leftIcon={<FontAwesomeIcon icon={faPlus} />}
Add method >
</Button> Add method
</div> </Button>
<TaxIDTable /> )}
<TaxIDModal </OrgPermissionCan>
popUp={popUp} </div>
handlePopUpClose={handlePopUpClose} <TaxIDTable />
handlePopUpToggle={handlePopUpToggle} <TaxIDModal
/> popUp={popUp}
</div> handlePopUpClose={handlePopUpClose}
); handlePopUpToggle={handlePopUpToggle}
} />
</div>
);
};
@@ -1,6 +1,7 @@
import { faFileInvoice, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faFileInvoice, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
EmptyState, EmptyState,
IconButton, IconButton,
@@ -13,7 +14,7 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api"; import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api";
const taxIDTypeLabelMap: { [key: string]: string } = { const taxIDTypeLabelMap: { [key: string]: string } = {
@@ -101,17 +102,25 @@ export const TaxIDTable = () => {
<Td>{taxIDTypeLabelMap[type]}</Td> <Td>{taxIDTypeLabelMap[type]}</Td>
<Td>{value}</Td> <Td>{value}</Td>
<Td> <Td>
<IconButton <OrgPermissionCan
onClick={async () => { I={OrgGeneralPermissionActions.Delete}
await handleDeleteTaxIdBtnClick(_id); a={OrgPermissionSubjects.Billing}
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
> >
<FontAwesomeIcon icon={faXmark} /> {(isAllowed) => (
</IconButton> <IconButton
onClick={async () => {
await handleDeleteTaxIdBtnClick(_id);
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</OrgPermissionCan>
</Td> </Td>
</Tr> </Tr>
))} ))}
@@ -1,5 +1,8 @@
import { Fragment } from "react" import { Fragment } from "react";
import { Tab } from "@headlessui/react" import { Tab } from "@headlessui/react";
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { BillingCloudTab } from "../BillingCloudTab"; import { BillingCloudTab } from "../BillingCloudTab";
import { BillingDetailsTab } from "../BillingDetailsTab"; import { BillingDetailsTab } from "../BillingDetailsTab";
@@ -7,43 +10,48 @@ import { BillingReceiptsTab } from "../BillingReceiptsTab";
import { BillingSelfHostedTab } from "../BillingSelfHostedTab"; import { BillingSelfHostedTab } from "../BillingSelfHostedTab";
const tabs = [ const tabs = [
{ name: "Infisical Cloud", key: "tab-infisical-cloud" }, { name: "Infisical Cloud", key: "tab-infisical-cloud" },
{ name: "Infisical Self-Hosted", key: "tab-infisical-self-hosted" }, { name: "Infisical Self-Hosted", key: "tab-infisical-self-hosted" },
{ name: "Receipts", key: "tab-receipts" }, { name: "Receipts", key: "tab-receipts" },
{ name: "Billing details", key: "tab-billing-details" } { name: "Billing details", key: "tab-billing-details" }
]; ];
export const BillingTabGroup = () => { export const BillingTabGroup = withPermission(
() => {
return ( return (
<Tab.Group> <Tab.Group>
<Tab.List className="mt-8 mb-6 border-b-2 border-mineshaft-800"> <Tab.List className="mt-8 mb-6 border-b-2 border-mineshaft-800">
{tabs.map((tab) => ( {tabs.map((tab) => (
<Tab as={Fragment} key={tab.key}> <Tab as={Fragment} key={tab.key}>
{({ selected }) => ( {({ selected }) => (
<button <button
type="button" type="button"
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`} className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${
> selected ? "border-b border-white text-white" : "text-mineshaft-400"
{tab.name} }`}
</button> >
)} {tab.name}
</Tab> </button>
))} )}
</Tab.List> </Tab>
<Tab.Panels> ))}
<Tab.Panel> </Tab.List>
<BillingCloudTab /> <Tab.Panels>
</Tab.Panel> <Tab.Panel>
<Tab.Panel> <BillingCloudTab />
<BillingSelfHostedTab /> </Tab.Panel>
</Tab.Panel> <Tab.Panel>
<Tab.Panel> <BillingSelfHostedTab />
<BillingReceiptsTab /> </Tab.Panel>
</Tab.Panel> <Tab.Panel>
<Tab.Panel> <BillingReceiptsTab />
<BillingDetailsTab /> </Tab.Panel>
</Tab.Panel> <Tab.Panel>
</Tab.Panels> <BillingDetailsTab />
</Tab.Group> </Tab.Panel>
</Tab.Panels>
</Tab.Group>
); );
} },
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.Billing }
);
@@ -1,9 +1,15 @@
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { OrgSSOSection } from "./OrgSSOSection"; import { OrgSSOSection } from "./OrgSSOSection";
export const OrgAuthTab = () => { export const OrgAuthTab = withPermission(
() => {
return ( return (
<div> <div>
<OrgSSOSection /> <OrgSSOSection />
</div> </div>
); );
} },
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.Sso }
);
@@ -2,136 +2,150 @@ import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, Switch, UpgradePlanModal } from "@app/components/v2"; import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import {
import { OrgGeneralPermissionActions,
useCreateSSOConfig, OrgPermissionSubjects,
useGetSSOConfig, useOrganization,
useUpdateSSOConfig useSubscription
} from "@app/hooks/api"; } from "@app/context";
import { useCreateSSOConfig, useGetSSOConfig, useUpdateSSOConfig } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { SSOModal } from "./SSOModal"; import { SSOModal } from "./SSOModal";
const ssoAuthProviderMap: { [key: string]: string } = { const ssoAuthProviderMap: { [key: string]: string } = {
"okta-saml": "Okta SAML", "okta-saml": "Okta SAML",
"azure-saml": "Azure SAML", "azure-saml": "Azure SAML",
"jumpcloud-saml": "JumpCloud SAML" "jumpcloud-saml": "JumpCloud SAML"
} };
export const OrgSSOSection = (): JSX.Element => { export const OrgSSOSection = (): JSX.Element => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { data, isLoading } = useGetSSOConfig(currentOrg?._id ?? ""); const { data, isLoading } = useGetSSOConfig(currentOrg?._id ?? "");
const { mutateAsync } = useUpdateSSOConfig(); const { mutateAsync } = useUpdateSSOConfig();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"upgradePlan", "upgradePlan",
"addSSO" "addSSO"
] as const); ] as const);
const { mutateAsync: createMutateAsync } = useCreateSSOConfig();
const handleSamlSSOToggle = async (value: boolean) => {
try {
if (!currentOrg?._id) return;
await mutateAsync({ const { mutateAsync: createMutateAsync } = useCreateSSOConfig();
organizationId: currentOrg?._id,
isActive: value
});
createNotification({ const handleSamlSSOToggle = async (value: boolean) => {
text: `Successfully ${value ? "enabled" : "disabled"} SAML SSO`, try {
type: "success" if (!currentOrg?._id) return;
});
} catch (err) { await mutateAsync({
console.error(err); organizationId: currentOrg?._id,
createNotification({ isActive: value
text: `Failed to ${value ? "enable" : "disable"} SAML SSO`, });
type: "error"
}); createNotification({
} text: `Successfully ${value ? "enabled" : "disabled"} SAML SSO`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${value ? "enable" : "disable"} SAML SSO`,
type: "error"
});
} }
};
const addSSOBtnClick = async () => {
try {
if (subscription?.samlSSO && currentOrg) {
if (!data) {
// case: SAML SSO is not configured
// -> initialize empty SAML SSO configuration
await createMutateAsync({
organizationId: currentOrg._id,
authProvider: "okta-saml",
isActive: false,
entryPoint: "",
issuer: "",
cert: ""
});
}
handlePopUpOpen("addSSO"); const addSSOBtnClick = async () => {
} else { try {
handlePopUpOpen("upgradePlan"); if (subscription?.samlSSO && currentOrg) {
} if (!data) {
} catch (err) { // case: SAML SSO is not configured
console.error(err); // -> initialize empty SAML SSO configuration
await createMutateAsync({
organizationId: currentOrg._id,
authProvider: "okta-saml",
isActive: false,
entryPoint: "",
issuer: "",
cert: ""
});
} }
handlePopUpOpen("addSSO");
} else {
handlePopUpOpen("upgradePlan");
}
} catch (err) {
console.error(err);
} }
};
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> return (
<div className="flex items-center mb-8"> <div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<h2 className="text-xl font-semibold flex-1 text-white"> <div className="flex items-center mb-8">
SAML SSO Configuration <h2 className="text-xl font-semibold flex-1 text-white">SAML SSO Configuration</h2>
</h2> {!isLoading && (
{!isLoading && ( <OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
<Button {(isAllowed) => (
onClick={addSSOBtnClick} <Button
colorSchema="secondary" onClick={addSSOBtnClick}
leftIcon={<FontAwesomeIcon icon={faPlus} />} colorSchema="secondary"
> isDisabled={!isAllowed}
{data ? "Update SAML SSO" : "Set up SAML SSO"} leftIcon={<FontAwesomeIcon icon={faPlus} />}
</Button> >
)} {data ? "Update SAML SSO" : "Set up SAML SSO"}
</div> </Button>
{data && (
<div className="mb-4">
<Switch
id="enable-saml-sso"
onCheckedChange={(value) => handleSamlSSOToggle(value)}
isChecked={data ? data.isActive : false}
>
Enable SAML SSO
</Switch>
</div>
)} )}
<div className="mb-4"> </OrgPermissionCan>
<h3 className="text-mineshaft-400 text-sm">SSO identifier</h3> )}
<p className="text-gray-400 text-md">{(data && data._id !== "") ? data._id : "-"}</p> </div>
</div> {data && (
<div className="mb-4"> <div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Type</h3> <OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
<p className="text-gray-400 text-md">{(data && data.authProvider !== "") ? ssoAuthProviderMap[data.authProvider] : "-"}</p> {(isAllowed) => (
</div> <Switch
<div className="mb-4"> id="enable-saml-sso"
<h3 className="text-mineshaft-400 text-sm">Entrypoint</h3> onCheckedChange={(value) => handleSamlSSOToggle(value)}
<p className="text-gray-400 text-md">{(data && data.entryPoint !== "") ? data.entryPoint : "-"}</p> isChecked={data ? data.isActive : false}
</div> isDisabled={!isAllowed}
<div className="mb-4"> >
<h3 className="text-mineshaft-400 text-sm">Issuer</h3> Enable SAML SSO
<p className="text-gray-400 text-md">{(data && data.issuer !== "") ? data.issuer : "-"}</p> </Switch>
</div> )}
<SSOModal </OrgPermissionCan>
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use SAML SSO if you switch to Infisical's Pro plan."
/>
</div> </div>
); )}
}; <div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">SSO identifier</h3>
<p className="text-gray-400 text-md">{data && data._id !== "" ? data._id : "-"}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Type</h3>
<p className="text-gray-400 text-md">
{data && data.authProvider !== "" ? ssoAuthProviderMap[data.authProvider] : "-"}
</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Entrypoint</h3>
<p className="text-gray-400 text-md">
{data && data.entryPoint !== "" ? data.entryPoint : "-"}
</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Issuer</h3>
<p className="text-gray-400 text-md">{data && data.issuer !== "" ? data.issuer : "-"}</p>
</div>
<SSOModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use SAML SSO if you switch to Infisical's Pro plan."
/>
</div>
);
};
@@ -3,13 +3,11 @@ import { OrgNameChangeSection } from "../OrgNameChangeSection";
import { OrgServiceAccountsTable } from "../OrgServiceAccountsTable"; import { OrgServiceAccountsTable } from "../OrgServiceAccountsTable";
export const OrgGeneralTab = () => { export const OrgGeneralTab = () => {
return ( return (
<div> <div>
<OrgNameChangeSection /> <OrgNameChangeSection />
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6"> <OrgServiceAccountsTable />
<OrgServiceAccountsTable /> <OrgIncidentContactsSection />
</div> </div>
<OrgIncidentContactsSection /> );
</div> };
);
}
@@ -3,17 +3,9 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
Button,
FormControl,
Input,
Modal,
ModalContent
} from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { import { useAddIncidentContact } from "@app/hooks/api";
useAddIncidentContact
} from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -24,97 +16,90 @@ const addContactFormSchema = yup.object({
type TAddContactForm = yup.InferType<typeof addContactFormSchema>; type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
type Props = { type Props = {
popUp: UsePopUpState<["addContact"]>; popUp: UsePopUpState<["addContact"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["addContact"]>) => void; handlePopUpClose: (popUpName: keyof UsePopUpState<["addContact"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addContact"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["addContact"]>, state?: boolean) => void;
}; };
export const AddOrgIncidentContactModal = ({ export const AddOrgIncidentContactModal = ({
popUp, popUp,
handlePopUpClose, handlePopUpClose,
handlePopUpToggle handlePopUpToggle
}: Props) => { }: Props) => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { data: serverDetails } = useFetchServerStatus() const { data: serverDetails } = useFetchServerStatus();
const { const { control, handleSubmit, reset } = useForm<TAddContactForm>({
control, resolver: yupResolver(addContactFormSchema)
handleSubmit, });
reset
} = useForm<TAddContactForm>({ resolver: yupResolver(addContactFormSchema) });
const { mutateAsync, isLoading } = useAddIncidentContact(); const { mutateAsync, isLoading } = useAddIncidentContact();
const onFormSubmit = async ({ email }: TAddContactForm) => {
try {
if (!currentOrg?._id) return;
await mutateAsync({
orgId: currentOrg._id,
email
});
createNotification({ const onFormSubmit = async ({ email }: TAddContactForm) => {
text: "Successfully added incident contact", try {
type: "success" if (!currentOrg?._id) return;
});
if (serverDetails?.emailConfigured){
handlePopUpClose("addContact");
}
reset(); await mutateAsync({
} catch (err) { orgId: currentOrg._id,
console.error(err); email
createNotification({ });
text: "Failed to add incident contact",
type: "error" createNotification({
}); text: "Successfully added incident contact",
} type: "success"
});
if (serverDetails?.emailConfigured) {
handlePopUpClose("addContact");
}
reset();
} catch (err) {
console.error(err);
createNotification({
text: "Failed to add incident contact",
type: "error"
});
} }
};
return ( return (
<Modal <Modal
isOpen={popUp?.addContact?.isOpen} isOpen={popUp?.addContact?.isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle("addContact", isOpen); handlePopUpToggle("addContact", isOpen);
reset(); reset();
}} }}
> >
<ModalContent <ModalContent
title="Add an Incident Contact" title="Add an Incident Contact"
subTitle="This contact will be notified in the unlikely event of a severe incident." subTitle="This contact will be notified in the unlikely event of a severe incident."
>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="email"
render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center space-x-4">
<Button size="sm" type="submit" isLoading={isLoading} isDisabled={isLoading}>
Add Incident Contact
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addContact")}
> >
<form onSubmit={handleSubmit(onFormSubmit)}> Cancel
<Controller </Button>
control={control} </div>
defaultValue="" </form>
name="email" </ModalContent>
render={({ field, fieldState: { error } }) => ( </Modal>
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}> );
<Input {...field} /> };
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
size="sm"
type="submit"
isLoading={isLoading}
isDisabled={isLoading}
>
Add Incident Contact
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addContact")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}
@@ -2,42 +2,53 @@ import { useTranslation } from "react-i18next";
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { OrgPermissionCan } from "@app/components/permissions";
Button import { Button } from "@app/components/v2";
} from "@app/components/v2"; import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal"; import { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal";
import { OrgIncidentContactsTable } from "./OrgIncidentContactsTable"; import { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
export const OrgIncidentContactsSection = () => { export const OrgIncidentContactsSection = withPermission(
() => {
const { t } = useTranslation(); const { t } = useTranslation();
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addContact" "addContact"
] as const); ] as const);
return ( return (
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600"> <div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-4"> <div className="flex justify-between mb-4">
<p className="min-w-max text-xl font-semibold"> <p className="min-w-max text-xl font-semibold">
{t("section.incident.incident-contacts")} {t("section.incident.incident-contacts")}
</p> </p>
<Button <OrgPermissionCan
colorSchema="secondary" I={OrgGeneralPermissionActions.Create}
type="submit" a={OrgPermissionSubjects.IncidentAccount}
leftIcon={<FontAwesomeIcon icon={faPlus} />} >
onClick={() => handlePopUpOpen("addContact")} {(isAllowed) => (
> <Button
Add contact colorSchema="secondary"
</Button> type="submit"
</div> isDisabled={!isAllowed}
<OrgIncidentContactsTable /> leftIcon={<FontAwesomeIcon icon={faPlus} />}
<AddOrgIncidentContactModal onClick={() => handlePopUpOpen("addContact")}
popUp={popUp} >
handlePopUpClose={handlePopUpClose} Add contact
handlePopUpToggle={handlePopUpToggle} </Button>
/> )}
</OrgPermissionCan>
</div> </div>
<OrgIncidentContactsTable />
<AddOrgIncidentContactModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
</div>
); );
} },
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.IncidentAccount }
);
@@ -3,6 +3,7 @@ import { faContactBook, faMagnifyingGlass, faTrash } from "@fortawesome/free-sol
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
DeleteActionModal, DeleteActionModal,
EmptyState, EmptyState,
@@ -17,7 +18,7 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteIncidentContact, useGetOrgIncidentContact } from "@app/hooks/api"; import { useDeleteIncidentContact, useGetOrgIncidentContact } from "@app/hooks/api";
@@ -83,13 +84,21 @@ export const OrgIncidentContactsTable = () => {
<Tr key={email}> <Tr key={email}>
<Td className="w-full">{email}</Td> <Td className="w-full">{email}</Td>
<Td className="mr-4"> <Td className="mr-4">
<IconButton <OrgPermissionCan
ariaLabel="delete" I={OrgGeneralPermissionActions.Delete}
colorSchema="danger" an={OrgPermissionSubjects.IncidentAccount}
onClick={() => handlePopUpOpen("removeContact", { email })}
> >
<FontAwesomeIcon icon={faTrash} /> {(isAllowed) => (
</IconButton> <IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen("removeContact", { email })}
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</OrgPermissionCan>
</Td> </Td>
</Tr> </Tr>
))} ))}
@@ -4,8 +4,10 @@ import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup"; import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { withPermission } from "@app/hoc";
import { useRenameOrg } from "@app/hooks/api"; import { useRenameOrg } from "@app/hooks/api";
const formSchema = yup.object({ const formSchema = yup.object({
@@ -14,49 +16,46 @@ const formSchema = yup.object({
type FormData = yup.InferType<typeof formSchema>; type FormData = yup.InferType<typeof formSchema>;
export const OrgNameChangeSection = (): JSX.Element => { export const OrgNameChangeSection = withPermission(
const { currentOrg } = useOrganization(); (): JSX.Element => {
const { createNotification } = useNotificationContext(); const { currentOrg } = useOrganization();
const { const { createNotification } = useNotificationContext();
handleSubmit, const { handleSubmit, control, reset } = useForm<FormData>({
control, resolver: yupResolver(formSchema)
reset });
} = useForm<FormData>({ resolver: yupResolver(formSchema) }); const { mutateAsync, isLoading } = useRenameOrg();
const { mutateAsync, isLoading } = useRenameOrg();
useEffect(() => { useEffect(() => {
if (currentOrg) { if (currentOrg) {
reset({ name: currentOrg.name }); reset({ name: currentOrg.name });
} }
}, [currentOrg]); }, [currentOrg]);
const onFormSubmit = async ({ name }: FormData) => { const onFormSubmit = async ({ name }: FormData) => {
try { try {
if (!currentOrg?._id) return; if (!currentOrg?._id) return;
if (name === "") return; if (name === "") return;
await mutateAsync({ orgId: currentOrg?._id, newOrgName: name }); await mutateAsync({ orgId: currentOrg?._id, newOrgName: name });
createNotification({ createNotification({
text: "Successfully renamed organization", text: "Successfully renamed organization",
type: "success" type: "success"
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
createNotification({ createNotification({
text: "Failed to rename organization", text: "Failed to rename organization",
type: "error" type: "error"
}); });
} }
}; };
return ( return (
<form <form
onSubmit={handleSubmit(onFormSubmit)} onSubmit={handleSubmit(onFormSubmit)}
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600" className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
> >
<p className="text-xl font-semibold text-mineshaft-100 mb-4"> <p className="text-xl font-semibold text-mineshaft-100 mb-4">Organization name</p>
Organization name
</p>
<div className="mb-2 max-w-md"> <div className="mb-2 max-w-md">
<Controller <Controller
defaultValue="" defaultValue=""
@@ -69,14 +68,25 @@ export const OrgNameChangeSection = (): JSX.Element => {
name="name" name="name"
/> />
</div> </div>
<Button <OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
isLoading={isLoading} {(isAllowed) => (
colorSchema="primary" <Button
variant="outline_bg" isLoading={isLoading}
type="submit" isDisabled={!isAllowed}
> colorSchema="primary"
Save variant="outline_bg"
</Button> type="submit"
</form> >
); Save
}; </Button>
)}
</OrgPermissionCan>
</form>
);
},
{
action: OrgGeneralPermissionActions.Read,
subject: OrgPermissionSubjects.Settings,
containerClassName: "mb-4"
}
);
@@ -34,7 +34,13 @@ import {
THead, THead,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context"; import {
OrgGeneralPermissionActions,
OrgPermissionSubjects,
useOrganization,
useWorkspace
} from "@app/context";
import { withPermission } from "@app/hoc";
import { usePopUp, useToggle } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { import {
// useCreateServiceAccount, // useCreateServiceAccount,
@@ -62,313 +68,322 @@ import // Controller,
// type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>; // type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
export const OrgServiceAccountsTable = () => { export const OrgServiceAccountsTable = withPermission(
const router = useRouter(); () => {
const { currentOrg } = useOrganization(); const router = useRouter();
const { currentWorkspace } = useWorkspace(); const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?._id || ""; const orgId = currentOrg?._id || "";
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false); const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false); const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false); const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
const [accessKey] = useState(""); const [accessKey] = useState("");
const [publicKey] = useState(""); const [publicKey] = useState("");
const [privateKey] = useState(""); const [privateKey] = useState("");
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState(""); const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"addServiceAccount", "addServiceAccount",
"removeServiceAccount" "removeServiceAccount"
] as const); ] as const);
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } =
useGetServiceAccounts(orgId); useGetServiceAccounts(orgId);
// const createServiceAccount = useCreateServiceAccount(); // const createServiceAccount = useCreateServiceAccount();
const removeServiceAccount = useDeleteServiceAccount(); const removeServiceAccount = useDeleteServiceAccount();
useEffect(() => { useEffect(() => {
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
if (isAccessKeyCopied) { if (isAccessKeyCopied) {
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000); timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
} }
if (isPublicKeyCopied) { if (isPublicKeyCopied) {
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000); timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
} }
if (isPrivateKeyCopied) { if (isPrivateKeyCopied) {
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000); timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
} }
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]); }, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
// const { // const {
// control, // control,
// handleSubmit, // handleSubmit,
// reset, // reset,
// formState: { isSubmitting } // formState: { isSubmitting }
// } = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) }); // } = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
// const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => { // const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
// if (!currentOrg?._id) return; // if (!currentOrg?._id) return;
// const keyPair = generateKeyPair(); // const keyPair = generateKeyPair();
// setPublicKey(keyPair.publicKey); // setPublicKey(keyPair.publicKey);
// setPrivateKey(keyPair.privateKey); // setPrivateKey(keyPair.privateKey);
// const serviceAccountDetails = await createServiceAccount.mutateAsync({ // const serviceAccountDetails = await createServiceAccount.mutateAsync({
// name, // name,
// organizationId: currentOrg?._id, // organizationId: currentOrg?._id,
// publicKey: keyPair.publicKey, // publicKey: keyPair.publicKey,
// expiresIn: Number(expiresIn) // expiresIn: Number(expiresIn)
// }); // });
// setAccessKey(serviceAccountDetails.serviceAccountAccessKey); // setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
// setStep(1); // setStep(1);
// reset(); // reset();
// } // }
const onRemoveServiceAccount = async () => { const onRemoveServiceAccount = async () => {
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id; const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
await removeServiceAccount.mutateAsync(serviceAccountId); await removeServiceAccount.mutateAsync(serviceAccountId);
handlePopUpClose("removeServiceAccount"); handlePopUpClose("removeServiceAccount");
}; };
const filteredServiceAccounts = useMemo( const filteredServiceAccounts = useMemo(
() => () =>
serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)), serviceAccounts.filter(({ name }) =>
[serviceAccounts, searchServiceAccountFilter] name.toLowerCase().includes(searchServiceAccountFilter)
); ),
[serviceAccounts, searchServiceAccountFilter]
);
const renderStep = (stepToRender: number) => { const renderStep = (stepToRender: number) => {
switch (stepToRender) { switch (stepToRender) {
case 0: case 0:
return ( return (
<div> <div>
We are currently revising the service account mechanism. In the meantime, please use We are currently revising the service account mechanism. In the meantime, please use
service tokens or API key to fetch secrets via API request. service tokens or API key to fetch secrets via API request.
</div>
// <form onSubmit={handleSubmit(onAddServiceAccount)}>
// <Controller
// control={control}
// defaultValue=""
// name="name"
// render={({ field, fieldState: { error } }) => (
// <FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
// <Input {...field} />
// </FormControl>
// )}
// />
// <Controller
// control={control}
// name="expiresIn"
// defaultValue={String(serviceAccountExpiration?.[0]?.value)}
// render={({ field: { onChange, ...field }, fieldState: { error } }) => {
// return (
// <FormControl
// label="Expiration"
// errorText={error?.message}
// isError={Boolean(error)}
// >
// <Select
// defaultValue={field.value}
// {...field}
// onValueChange={(e) => onChange(e)}
// className="w-full"
// >
// {serviceAccountExpiration.map(({ label, value }) => (
// <SelectItem value={String(value)} key={label}>
// {label}
// </SelectItem>
// ))}
// </Select>
// </FormControl>
// );
// }}
// />
// <div className="mt-8 flex items-center">
// <Button
// className="mr-4"
// size="sm"
// type="submit"
// isLoading={isSubmitting}
// isDisabled={isSubmitting}
// >
// Create Service Account
// </Button>
// <Button
// colorSchema="secondary"
// variant="plain"
// onClick={() => handlePopUpClose("addServiceAccount")}
// >
// Cancel
// </Button>
// </div>
// </form>
);
case 1:
return (
<>
<p>Access Key</p>
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{accessKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(accessKey);
setIsAccessKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isAccessKeyCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Copy
</span>
</IconButton>
</div> </div>
<p className="mt-4">Public Key</p> // <form onSubmit={handleSubmit(onAddServiceAccount)}>
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400"> // <Controller
<p className="mr-4 break-all">{publicKey}</p> // control={control}
<IconButton // defaultValue=""
ariaLabel="copy icon" // name="name"
colorSchema="secondary" // render={({ field, fieldState: { error } }) => (
className="group relative" // <FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
onClick={() => { // <Input {...field} />
navigator.clipboard.writeText(publicKey); // </FormControl>
setIsPublicKeyCopied.on(); // )}
}} // />
> // <Controller
<FontAwesomeIcon icon={isPublicKeyCopied ? faCheck : faCopy} /> // control={control}
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn"> // name="expiresIn"
Copy // defaultValue={String(serviceAccountExpiration?.[0]?.value)}
</span> // render={({ field: { onChange, ...field }, fieldState: { error } }) => {
</IconButton> // return (
</div> // <FormControl
<p className="mt-4">Private Key</p> // label="Expiration"
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400"> // errorText={error?.message}
<p className="mr-4 break-all">{privateKey}</p> // isError={Boolean(error)}
<IconButton // >
ariaLabel="copy icon" // <Select
colorSchema="secondary" // defaultValue={field.value}
className="group relative" // {...field}
onClick={() => { // onValueChange={(e) => onChange(e)}
navigator.clipboard.writeText(privateKey); // className="w-full"
setIsPrivateKeyCopied.on(); // >
}} // {serviceAccountExpiration.map(({ label, value }) => (
> // <SelectItem value={String(value)} key={label}>
<FontAwesomeIcon icon={isPrivateKeyCopied ? faCheck : faCopy} /> // {label}
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn"> // </SelectItem>
Copy // ))}
</span> // </Select>
</IconButton> // </FormControl>
</div> // );
</> // }}
); // />
default: // <div className="mt-8 flex items-center">
return <div />; // <Button
} // className="mr-4"
}; // size="sm"
// type="submit"
// isLoading={isSubmitting}
// isDisabled={isSubmitting}
// >
// Create Service Account
// </Button>
// <Button
// colorSchema="secondary"
// variant="plain"
// onClick={() => handlePopUpClose("addServiceAccount")}
// >
// Cancel
// </Button>
// </div>
// </form>
);
case 1:
return (
<>
<p>Access Key</p>
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{accessKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(accessKey);
setIsAccessKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isAccessKeyCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Copy
</span>
</IconButton>
</div>
<p className="mt-4">Public Key</p>
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{publicKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(publicKey);
setIsPublicKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isPublicKeyCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Copy
</span>
</IconButton>
</div>
<p className="mt-4">Private Key</p>
<div className="flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{privateKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(privateKey);
setIsPrivateKeyCopied.on();
}}
>
<FontAwesomeIcon icon={isPrivateKeyCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Copy
</span>
</IconButton>
</div>
</>
);
default:
return <div />;
}
};
return ( return (
<div className="w-full"> <div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
<div className="mb-4 flex justify-between"> <div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p> <p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
<Button <Button
colorSchema="secondary" colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
setStep(0); setStep(0);
// reset();
handlePopUpOpen("addServiceAccount");
}}
>
Add Service Account
</Button>
</div>
<Input
value={searchServiceAccountFilter}
onChange={(e) => setSearchServiceAccountFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search service accounts..."
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Th>Name</Th>
<Th className="w-full">Valid Until</Th>
<Th aria-label="actions" />
</THead>
<TBody>
{isServiceAccountsLoading && (
<TableSkeleton columns={5} innerKey="org-service-accounts" />
)}
{!isServiceAccountsLoading &&
filteredServiceAccounts.map(({ name, expiresAt, _id: serviceAccountId }) => {
return (
<Tr key={`org-service-account-${serviceAccountId}`}>
<Td>{name}</Td>
<Td>{new Date(expiresAt).toUTCString()}</Td>
<Td>
<div className="flex">
<IconButton
ariaLabel="edit"
colorSchema="secondary"
onClick={() => {
if (currentWorkspace?._id) {
router.push(
`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`
);
}
}}
className="mr-2"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() =>
handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })
}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
<EmptyState title="No service accounts found" icon={faServer} />
)}
</TableContainer>
<Modal
isOpen={popUp?.addServiceAccount?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addServiceAccount", isOpen);
// reset(); // reset();
handlePopUpOpen("addServiceAccount");
}} }}
> >
Add Service Account <ModalContent
</Button> title="Add Service Account"
subTitle="A service account represents a machine identity such as a VM or application client."
>
{renderStep(step)}
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeServiceAccount.isOpen}
deleteKey="remove"
title="Do you want to remove this service account from the org?"
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
onDeleteApproved={onRemoveServiceAccount}
/>
</div> </div>
<Input );
value={searchServiceAccountFilter} },
onChange={(e) => setSearchServiceAccountFilter(e.target.value)} {
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} action: OrgGeneralPermissionActions.Read,
placeholder="Search service accounts..." subject: OrgPermissionSubjects.Settings,
/> containerClassName: "mb-4"
<TableContainer className="mt-4"> }
<Table> );
<THead>
<Th>Name</Th>
<Th className="w-full">Valid Until</Th>
<Th aria-label="actions" />
</THead>
<TBody>
{isServiceAccountsLoading && (
<TableSkeleton columns={5} innerKey="org-service-accounts" />
)}
{!isServiceAccountsLoading &&
filteredServiceAccounts.map(({ name, expiresAt, _id: serviceAccountId }) => {
return (
<Tr key={`org-service-account-${serviceAccountId}`}>
<Td>{name}</Td>
<Td>{new Date(expiresAt).toUTCString()}</Td>
<Td>
<div className="flex">
<IconButton
ariaLabel="edit"
colorSchema="secondary"
onClick={() => {
if (currentWorkspace?._id) {
router.push(
`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`
);
}
}}
className="mr-2"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() =>
handlePopUpOpen("removeServiceAccount", { _id: serviceAccountId })
}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
<EmptyState title="No service accounts found" icon={faServer} />
)}
</TableContainer>
<Modal
isOpen={popUp?.addServiceAccount?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addServiceAccount", isOpen);
// reset();
}}
>
<ModalContent
title="Add Service Account"
subTitle="A service account represents a machine identity such as a VM or application client."
>
{renderStep(step)}
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeServiceAccount.isOpen}
deleteKey="remove"
title="Do you want to remove this service account from the org?"
onChange={(isOpen) => handlePopUpToggle("removeServiceAccount", isOpen)}
onDeleteApproved={onRemoveServiceAccount}
/>
</div>
);
};
@@ -1,59 +1,40 @@
import { Fragment } from "react" import { Fragment } from "react";
import { Tab } from "@headlessui/react" import { Tab } from "@headlessui/react";
import { useOrganization,useUser } from "@app/context";
import {
useGetOrgUsers
} from "@app/hooks/api";
import { OrgAuthTab } from "../OrgAuthTab"; import { OrgAuthTab } from "../OrgAuthTab";
import { OrgGeneralTab } from "../OrgGeneralTab"; import { OrgGeneralTab } from "../OrgGeneralTab";
const tabs = [
{ name: "General", key: "tab-org-general" },
{ name: "Authentication", key: "tab-org-auth" }
];
export const OrgTabGroup = () => { export const OrgTabGroup = () => {
const { currentOrg } = useOrganization(); return (
const { user } = useUser(); <Tab.Group>
const { data } = useGetOrgUsers(currentOrg?._id ?? ""); <Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
{tabs.map((tab) => (
const isRoleSufficient = data?.some((orgUser) => { <Tab as={Fragment} key={tab.key}>
return orgUser.role !== "member" && orgUser.user._id === user._id; {({ selected }) => (
}); <button
type="button"
const tabs = [ className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${
{ name: "General", key: "tab-org-general" }, selected ? "border-b border-white text-white" : "text-mineshaft-400"
]; }`}
>
if (isRoleSufficient) { {tab.name}
tabs.push( </button>
{ name: "Authentication", key: "tab-org-auth" } )}
); </Tab>
} ))}
</Tab.List>
return ( <Tab.Panels>
<Tab.Group> <Tab.Panel>
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full"> <OrgGeneralTab />
{tabs.map((tab) => ( </Tab.Panel>
<Tab as={Fragment} key={tab.key}> <Tab.Panel>
{({ selected }) => ( <OrgAuthTab />
<button </Tab.Panel>
type="button" </Tab.Panels>
className={`w-30 py-2 mx-2 mr-4 font-medium text-sm outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"}`} </Tab.Group>
> );
{tab.name} };
</button>
)}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<OrgGeneralTab />
</Tab.Panel>
{isRoleSufficient && (
<Tab.Panel>
<OrgAuthTab />
</Tab.Panel>
)}
</Tab.Panels>
</Tab.Group>
);
}