mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 20:26:44 +00:00
feat(rbac): implemented granular blocking of actions based on permissions on org level ui
This commit is contained in:
@@ -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>;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
|||||||
+101
-70
@@ -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>
|
||||||
|
|||||||
+6
-4
@@ -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(
|
||||||
|
|||||||
+6
-8
@@ -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 },
|
||||||
|
|||||||
+10
-7
@@ -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(
|
||||||
|
|||||||
+10
-11
@@ -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 },
|
||||||
|
|||||||
+6
-7
@@ -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 },
|
||||||
|
|||||||
+6
-7
@@ -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 },
|
||||||
|
|||||||
+6
-7
@@ -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 },
|
||||||
|
|||||||
+6
-4
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+138
-115
@@ -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 →
|
Manage plan →
|
||||||
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+82
-88
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+81
-85
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+39
-38
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+20
-11
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+31
-27
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+20
-11
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+46
-38
@@ -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 }
|
||||||
|
);
|
||||||
|
|||||||
+131
-117
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+8
-10
@@ -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>
|
};
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+79
-94
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+38
-27
@@ -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 }
|
||||||
|
);
|
||||||
|
|||||||
+16
-7
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+61
-51
@@ -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"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
+304
-289
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
+35
-54
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user