pull/860/head
parent
34fb7be1c4
commit
aac3168c80
@ -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";
|
@ -1 +1,7 @@
|
||||
export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext";
|
||||
export type { TOrgPermission } from "./types";
|
||||
export {
|
||||
OrgGeneralPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
OrgWorkspacePermissionActions
|
||||
} from "./types";
|
||||
|
@ -1,6 +1,12 @@
|
||||
export { AuthProvider } from "./AuthContext";
|
||||
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 { UserProvider, useUser } from "./UserContext";
|
||||
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 Head from "next/head";
|
||||
|
||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, TOrgPermission } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage";
|
||||
|
||||
export default function SettingsBilling() {
|
||||
const { t } = useTranslation();
|
||||
const SettingsBilling = withPermission<{}, TOrgPermission>(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="h-full bg-bunker-800">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("billing.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<BillingSettingsPage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="h-full bg-bunker-800">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("billing.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<BillingSettingsPage />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: OrgGeneralPermissionActions.Delete, subject: OrgPermissionSubjects.Billing }
|
||||
);
|
||||
|
||||
SettingsBilling.requireAuth = true;
|
||||
Object.assign(SettingsBilling, { requireAuth: true });
|
||||
|
||||
export default SettingsBilling;
|
||||
|
@ -1,89 +1,133 @@
|
||||
import { useEffect, useState } from "react";
|
||||
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 { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
|
||||
|
||||
import createNewIntegrationSession from "../../../api/secret-scanning/createSecretScanningSession";
|
||||
import getInstallationStatus from "../../../api/secret-scanning/getInstallationStatus";
|
||||
import linkGitAppInstallationWithOrganization from "../../../api/secret-scanning/linkGitAppInstallationWithOrganization";
|
||||
|
||||
export default function SecretScanning() {
|
||||
const router = useRouter()
|
||||
const queryParams = router.query
|
||||
const [integrationEnabled, setIntegrationStatus] = useState(false)
|
||||
const SecretScanning = withPermission(
|
||||
() => {
|
||||
const router = useRouter();
|
||||
const queryParams = router.query;
|
||||
const [integrationEnabled, setIntegrationStatus] = useState(false);
|
||||
|
||||
useEffect(()=>{
|
||||
const linkInstallation = async () => {
|
||||
if (typeof queryParams.state === "string" && typeof queryParams.installation_id === "string"){
|
||||
try {
|
||||
const isLinked = await linkGitAppInstallationWithOrganization(queryParams.installation_id as string, queryParams.state as string)
|
||||
if (isLinked){
|
||||
router.reload()
|
||||
useEffect(() => {
|
||||
const linkInstallation = async () => {
|
||||
if (
|
||||
typeof queryParams.state === "string" &&
|
||||
typeof queryParams.installation_id === "string"
|
||||
) {
|
||||
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 status = await getInstallationStatus(String(localStorage.getItem("orgData.id")))
|
||||
setIntegrationStatus(status)
|
||||
}
|
||||
const fetchInstallationStatus = async () => {
|
||||
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")));
|
||||
setIntegrationStatus(status);
|
||||
};
|
||||
|
||||
fetchInstallationStatus()
|
||||
linkInstallation()
|
||||
},[queryParams.state, queryParams.installation_id])
|
||||
fetchInstallationStatus();
|
||||
linkInstallation();
|
||||
}, [queryParams.state, queryParams.installation_id]);
|
||||
|
||||
const generateNewIntegrationSession = async () => {
|
||||
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")))
|
||||
router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`)
|
||||
}
|
||||
const generateNewIntegrationSession = async () => {
|
||||
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")));
|
||||
router.push(
|
||||
`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Secret scanning</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<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="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="relative flex justify-between bg-mineshaft-800 border border-mineshaft-600 rounded-md p-6 mb-6">
|
||||
<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>
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Secret scanning</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<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="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>
|
||||
{integrationEnabled ? (
|
||||
<div>
|
||||
<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="absolute right-[2.63rem] top-[2.63rem] animate-ping rounded-full h-5 w-5 bg-green flex items-center justify-center"/>
|
||||
<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"/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center h-[3.25rem]">
|
||||
<Button
|
||||
variant="solid"
|
||||
colorSchema="primary"
|
||||
onClick={generateNewIntegrationSession}
|
||||
className="py-2 h-min"
|
||||
>
|
||||
Integrate with GitHub
|
||||
</Button>
|
||||
<div className="relative flex justify-between bg-mineshaft-800 border border-mineshaft-600 rounded-md p-6 mb-6">
|
||||
<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>
|
||||
)}
|
||||
{integrationEnabled ? (
|
||||
<div>
|
||||
<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="absolute right-[2.63rem] top-[2.63rem] animate-ping rounded-full h-5 w-5 bg-green flex items-center justify-center" />
|
||||
<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" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center h-[3.25rem]">
|
||||
<OrgPermissionCan
|
||||
I={OrgGeneralPermissionActions.Create}
|
||||
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>
|
||||
<SecretScanningLogsTable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.SecretScanning }
|
||||
);
|
||||
|
||||
Object.assign(SecretScanning, { requireAuth: true });
|
||||
|
||||
SecretScanning.requireAuth = true;
|
||||
export default SecretScanning;
|
||||
|
@ -1,28 +1,47 @@
|
||||
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";
|
||||
|
||||
export const RiskStatusSelection = ({riskId, currentSelection}: {riskId: any, currentSelection: any }) => {
|
||||
const [selectedRiskStatus, setSelectedRiskStatus] = useState(currentSelection);
|
||||
useEffect(()=>{
|
||||
if (currentSelection !== selectedRiskStatus){
|
||||
const updateSelection = async () =>{
|
||||
await updateRiskStatus(String(localStorage.getItem("orgData.id")), riskId, selectedRiskStatus)
|
||||
}
|
||||
updateSelection()
|
||||
}
|
||||
},[selectedRiskStatus])
|
||||
export const RiskStatusSelection = ({
|
||||
riskId,
|
||||
currentSelection
|
||||
}: {
|
||||
riskId: any;
|
||||
currentSelection: any;
|
||||
}) => {
|
||||
const [selectedRiskStatus, setSelectedRiskStatus] = useState(currentSelection);
|
||||
useEffect(() => {
|
||||
if (currentSelection !== selectedRiskStatus) {
|
||||
const updateSelection = async () => {
|
||||
await updateRiskStatus(
|
||||
String(localStorage.getItem("orgData.id")),
|
||||
riskId,
|
||||
selectedRiskStatus
|
||||
);
|
||||
};
|
||||
updateSelection();
|
||||
}
|
||||
}, [selectedRiskStatus]);
|
||||
|
||||
return (
|
||||
<select
|
||||
value={selectedRiskStatus}
|
||||
onChange={(e) => setSelectedRiskStatus(e.target.value)}
|
||||
className="block w-full py-2 px-3 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option>Unresolved</option>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.SecretScanning}>
|
||||
{(isAllowed) => (
|
||||
<select
|
||||
disabled={!isAllowed}
|
||||
value={selectedRiskStatus}
|
||||
onChange={(e) => setSelectedRiskStatus(e.target.value)}
|
||||
className="block w-full py-2 px-3 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option>Unresolved</option>
|
||||
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>
|
||||
This is a false positive, resolved
|
||||
</option>
|
||||
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolved</option>
|
||||
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolved</option>
|
||||
</select>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
);
|
||||
};
|
||||
|
@ -1,125 +1,148 @@
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useOrganization,useSubscription } from "@app/context";
|
||||
import {
|
||||
useCreateCustomerPortalSession,
|
||||
useGetOrgPlanBillingInfo,
|
||||
useGetOrgTrialUrl
|
||||
import {
|
||||
OrgGeneralPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import {
|
||||
useCreateCustomerPortalSession,
|
||||
useGetOrgPlanBillingInfo,
|
||||
useGetOrgTrialUrl
|
||||
} from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { ManagePlansModal } from "./ManagePlansModal";
|
||||
|
||||
export const PreviewSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?._id ?? "");
|
||||
const getOrgTrialUrl = useGetOrgTrialUrl();
|
||||
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",
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?._id ?? "");
|
||||
const getOrgTrialUrl = useGetOrgTrialUrl();
|
||||
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) {
|
||||
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
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
window.location.href = url;
|
||||
} else {
|
||||
// open compare plans modal
|
||||
handlePopUpOpen("managePlan");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{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">
|
||||
<div className="flex-1">
|
||||
<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>
|
||||
</div>
|
||||
<Button
|
||||
// onClick={() => handlePopUpOpen("managePlan")}
|
||||
onClick={() => handleUpgradeBtnClick()}
|
||||
color="mineshaft"
|
||||
>
|
||||
{!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && subscription && data && (
|
||||
<div className="flex mb-6">
|
||||
<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>
|
||||
<p className="text-2xl text-mineshaft-50 font-semibold mb-8">
|
||||
{`${formatPlanSlug(subscription.slug)} ${subscription.status === "trialing" ? "(Trial)" : ""}`}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!currentOrg?._id) return;
|
||||
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id);
|
||||
window.location.href = url;
|
||||
}}
|
||||
className="text-primary"
|
||||
>
|
||||
Manage plan →
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mr-4">
|
||||
<p className="mb-2 text-gray-400">Price</p>
|
||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
|
||||
{subscription.status === "trialing" ? "$0.00 / month" : `${formatAmount(data.amount)} / ${data.interval}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600">
|
||||
<p className="mb-2 text-gray-400">Subscription renews on</p>
|
||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
|
||||
{formatDate(data.currentPeriodEnd)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ManagePlansModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{subscription &&
|
||||
subscription?.slug !== "enterprise" &&
|
||||
subscription?.slug !== "pro" &&
|
||||
subscription?.slug !== "pro-annual" && (
|
||||
<div className="p-4 rounded-lg flex-1 border border-mineshaft-600 mb-6 flex items-center bg-mineshaft-600">
|
||||
<div className="flex-1">
|
||||
<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>
|
||||
</div>
|
||||
<OrgPermissionCan
|
||||
I={OrgGeneralPermissionActions.Create}
|
||||
a={OrgPermissionSubjects.Billing}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => handleUpgradeBtnClick()}
|
||||
color="mineshaft"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && subscription && data && (
|
||||
<div className="flex mb-6">
|
||||
<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>
|
||||
<p className="text-2xl text-mineshaft-50 font-semibold mb-8">
|
||||
{`${formatPlanSlug(subscription.slug)} ${
|
||||
subscription.status === "trialing" ? "(Trial)" : ""
|
||||
}`}
|
||||
</p>
|
||||
<OrgPermissionCan
|
||||
I={OrgGeneralPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Billing}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!currentOrg?._id) return;
|
||||
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id);
|
||||
window.location.href = url;
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
className="text-primary"
|
||||
>
|
||||
Manage plan →
|
||||
</button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mr-4">
|
||||
<p className="mb-2 text-gray-400">Price</p>
|
||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
|
||||
{subscription.status === "trialing"
|
||||
? "$0.00 / month"
|
||||
: `${formatAmount(data.amount)} / ${data.interval}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600">
|
||||
<p className="mb-2 text-gray-400">Subscription renews on</p>
|
||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
|
||||
{formatDate(data.currentPeriodEnd)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
<ManagePlansModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,98 +1,92 @@
|
||||
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 * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
useGetOrgBillingDetails,
|
||||
useUpdateOrgBillingDetails
|
||||
} from "@app/hooks/api";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, 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")
|
||||
}).required();
|
||||
})
|
||||
.required();
|
||||
|
||||
export const CompanyNameSection = () => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { reset, control, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
name: ""
|
||||
},
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
|
||||
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"
|
||||
});
|
||||
}
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { reset, control, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
name: ""
|
||||
},
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
|
||||
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 (
|
||||
<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>
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<h2 className="text-xl font-semibold flex-1 text-mineshaft-100 mb-8">Business name</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder="Acme Corp" {...field} className="bg-mineshaft-800" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@ -1,97 +1,93 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
useGetOrgBillingDetails,
|
||||
useUpdateOrgBillingDetails
|
||||
} from "@app/hooks/api";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, 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")
|
||||
}).required();
|
||||
})
|
||||
.required();
|
||||
|
||||
export const InvoiceEmailSection = () => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { reset, control, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
email: ""
|
||||
},
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
|
||||
const { mutateAsync, isLoading } = useUpdateOrgBillingDetails();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { reset, control, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
email: ""
|
||||
},
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
|
||||
const { mutateAsync, isLoading } = useUpdateOrgBillingDetails();
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
email: data?.email ?? ""
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
email: data?.email ?? ""
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormSubmit = async ({ email }: { email: string }) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
if (email === "") return;
|
||||
|
||||
const onFormSubmit = async ({ email }: { email: string }) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
if (email === "") return;
|
||||
await mutateAsync({
|
||||
email,
|
||||
organizationId: currentOrg._id
|
||||
});
|
||||
|
||||
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"
|
||||
});
|
||||
}
|
||||
createNotification({
|
||||
text: "Successfully updated invoice email recipient",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update invoice email recipient",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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-white mb-8">
|
||||
Invoice email recipient
|
||||
</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
placeholder="jane@acme.com"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="email"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<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-white mb-8">Invoice email recipient</h2>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder="jane@acme.com" {...field} className="bg-mineshaft-800" />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="email"
|
||||
/>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
@ -1,46 +1,47 @@
|
||||
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useAddOrgPmtMethod } from "@app/hooks/api";
|
||||
|
||||
import { PmtMethodsTable } from "./PmtMethodsTable";
|
||||
|
||||
export const PmtMethodsSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync, isLoading } = useAddOrgPmtMethod();
|
||||
|
||||
const handleAddPmtMethodBtnClick = async () => {
|
||||
if (!currentOrg?._id) return;
|
||||
const url = await mutateAsync({
|
||||
organizationId: currentOrg._id,
|
||||
success_url: window.location.href,
|
||||
cancel_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||
Payment methods
|
||||
</h2>
|
||||
<Button
|
||||
onClick={handleAddPmtMethodBtnClick}
|
||||
colorSchema="secondary"
|
||||
isLoading={isLoading}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add method
|
||||
</Button>
|
||||
</div>
|
||||
<PmtMethodsTable />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync, isLoading } = useAddOrgPmtMethod();
|
||||
|
||||
const handleAddPmtMethodBtnClick = async () => {
|
||||
if (!currentOrg?._id) return;
|
||||
const url = await mutateAsync({
|
||||
organizationId: currentOrg._id,
|
||||
success_url: window.location.href,
|
||||
cancel_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">Payment methods</h2>
|
||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={handleAddPmtMethodBtnClick}
|
||||
colorSchema="secondary"
|
||||
isLoading={isLoading}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add method
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<PmtMethodsTable />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,38 +1,42 @@
|
||||
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { TaxIDModal } from "./TaxIDModal";
|
||||
import { TaxIDTable } from "./TaxIDTable";
|
||||
|
||||
export const TaxIDSection = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"addTaxID"
|
||||
] as const);
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"addTaxID"
|
||||
] as const);
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||
Tax ID
|
||||
</h2>
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("addTaxID")}
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add method
|
||||
</Button>
|
||||
</div>
|
||||
<TaxIDTable />
|
||||
<TaxIDModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">Tax ID</h2>
|
||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("addTaxID")}
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add method
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<TaxIDTable />
|
||||
<TaxIDModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,9 +1,15 @@
|
||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
|
||||
import { OrgSSOSection } from "./OrgSSOSection";
|
||||
|
||||
export const OrgAuthTab = () => {
|
||||
export const OrgAuthTab = withPermission(
|
||||
() => {
|
||||
return (
|
||||
<div>
|
||||
<OrgSSOSection />
|
||||
</div>
|
||||
<div>
|
||||
<OrgSSOSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{ action: OrgGeneralPermissionActions.Read, subject: OrgPermissionSubjects.Sso }
|
||||
);
|
||||
|
@ -1,59 +1,40 @@
|
||||
import { Fragment } from "react"
|
||||
import { Tab } from "@headlessui/react"
|
||||
|
||||
import { useOrganization,useUser } from "@app/context";
|
||||
import {
|
||||
useGetOrgUsers
|
||||
} from "@app/hooks/api";
|
||||
import { Fragment } from "react";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
import { OrgAuthTab } from "../OrgAuthTab";
|
||||
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-org-general" },
|
||||
{ name: "Authentication", key: "tab-org-auth" }
|
||||
];
|
||||
export const OrgTabGroup = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { user } = useUser();
|
||||
const { data } = useGetOrgUsers(currentOrg?._id ?? "");
|
||||
|
||||
const isRoleSufficient = data?.some((orgUser) => {
|
||||
return orgUser.role !== "member" && orgUser.user._id === user._id;
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{ name: "General", key: "tab-org-general" },
|
||||
];
|
||||
|
||||
if (isRoleSufficient) {
|
||||
tabs.push(
|
||||
{ name: "Authentication", key: "tab-org-auth" }
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tab.Group>
|
||||
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
|
||||
{tabs.map((tab) => (
|
||||
<Tab as={Fragment} key={tab.key}>
|
||||
{({ selected }) => (
|
||||
<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"}`}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<OrgGeneralTab />
|
||||
</Tab.Panel>
|
||||
{isRoleSufficient && (
|
||||
<Tab.Panel>
|
||||
<OrgAuthTab />
|
||||
</Tab.Panel>
|
||||
)}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tab.Group>
|
||||
<Tab.List className="mb-6 border-b-2 border-mineshaft-800 w-full">
|
||||
{tabs.map((tab) => (
|
||||
<Tab as={Fragment} key={tab.key}>
|
||||
{({ selected }) => (
|
||||
<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"
|
||||
}`}
|
||||
>
|
||||
{tab.name}
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<OrgGeneralTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<OrgAuthTab />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in new issue