mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 06:45:28 +00:00
feat(rbac): implemented granular blocking of actions based on permissions on org level ui
This commit is contained in:
@@ -3,7 +3,8 @@ import { useTranslation } from "react-i18next";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
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 { OrgMembersTable } from "./components/OrgMembersTable";
|
||||
@@ -14,45 +15,48 @@ enum TabSections {
|
||||
Roles = "roles"
|
||||
}
|
||||
|
||||
export const MembersPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentOrg } = useOrganization();
|
||||
export const MembersPage = withPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const orgId = currentOrg?._id || "";
|
||||
const orgId = currentOrg?._id || "";
|
||||
|
||||
const { data: roles } = useGetRoles({
|
||||
orgId
|
||||
});
|
||||
const { data: roles } = useGetRoles({
|
||||
orgId
|
||||
});
|
||||
|
||||
return (
|
||||
<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">
|
||||
<p className="mr-4 mb-4 text-3xl font-semibold text-white">
|
||||
{t("section.members.org-members")}
|
||||
</p>
|
||||
<Tabs defaultValue={TabSections.Member}>
|
||||
<TabList>
|
||||
<Tab value={TabSections.Member}>Members</Tab>
|
||||
{process.env.NEXT_PUBLIC_NEW_PERMISSION_FLAG === "true" && (
|
||||
<Tab value={TabSections.Roles}>Roles</Tab>
|
||||
)}
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Member}>
|
||||
<motion.div
|
||||
key="panel-1"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<OrgMembersTable roles={roles} />
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Roles}>
|
||||
<OrgRoleTabSection roles={roles} />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
return (
|
||||
<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">
|
||||
<p className="mr-4 mb-4 text-3xl font-semibold text-white">
|
||||
{t("section.members.org-members")}
|
||||
</p>
|
||||
<Tabs defaultValue={TabSections.Member}>
|
||||
<TabList>
|
||||
<Tab value={TabSections.Member}>Members</Tab>
|
||||
{process.env.NEXT_PUBLIC_NEW_PERMISSION_FLAG === "true" && (
|
||||
<Tab value={TabSections.Roles}>Roles</Tab>
|
||||
)}
|
||||
</TabList>
|
||||
<TabPanel value={TabSections.Member}>
|
||||
<motion.div
|
||||
key="panel-1"
|
||||
transition={{ duration: 0.15 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
>
|
||||
<OrgMembersTable roles={roles} />
|
||||
</motion.div>
|
||||
</TabPanel>
|
||||
<TabPanel value={TabSections.Roles}>
|
||||
<OrgRoleTabSection roles={roles} />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</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 { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
@@ -41,7 +42,14 @@ import {
|
||||
Tr,
|
||||
UpgradePlanModal
|
||||
} 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 {
|
||||
useAddUserToOrg,
|
||||
@@ -297,27 +305,32 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
|
||||
placeholder="Search members..."
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
|
||||
createNotification({
|
||||
text: "You cannot invite users when SAML SSO is configured for your organization",
|
||||
type: "error"
|
||||
});
|
||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
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) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
} else {
|
||||
handlePopUpOpen("addMember");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add Member
|
||||
</Button>
|
||||
if (isMoreUsersNotAllowed) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
} else {
|
||||
handlePopUpOpen("addMember");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add Member
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<div>
|
||||
<TableContainer>
|
||||
@@ -345,48 +358,59 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
|
||||
<Td>{name}</Td>
|
||||
<Td>{email}</Td>
|
||||
<Td>
|
||||
{status === "accepted" && (
|
||||
<Select
|
||||
defaultValue={
|
||||
role === "custom" ? findRoleFromId(customRole)?.slug : role
|
||||
}
|
||||
isDisabled={userId === u?._id}
|
||||
className="w-40 bg-mineshaft-600"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={(selectedRole) =>
|
||||
onRoleChange(orgMembershipId, selectedRole)
|
||||
}
|
||||
>
|
||||
{roles
|
||||
.filter(({ slug }) =>
|
||||
slug === "owner" ? isIamOwner || role === "owner" : true
|
||||
)
|
||||
.map(({ slug, name: roleName }) => (
|
||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||
{roleName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
{(status === "invited" || status === "verified") &&
|
||||
serverDetails?.emailConfigured && (
|
||||
<Button
|
||||
className="w-40"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => onAddUserToOrg(email)}
|
||||
>
|
||||
Resend Invite
|
||||
</Button>
|
||||
<OrgPermissionCan
|
||||
I={OrgGeneralPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<>
|
||||
{status === "accepted" && (
|
||||
<Select
|
||||
defaultValue={
|
||||
role === "custom" ? findRoleFromId(customRole)?.slug : role
|
||||
}
|
||||
isDisabled={userId === u?._id || !isAllowed}
|
||||
className="w-40 bg-mineshaft-600"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={(selectedRole) =>
|
||||
onRoleChange(orgMembershipId, selectedRole)
|
||||
}
|
||||
>
|
||||
{roles
|
||||
.filter(({ slug }) =>
|
||||
slug === "owner" ? isIamOwner || role === "owner" : true
|
||||
)
|
||||
.map(({ slug, name: roleName }) => (
|
||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||
{roleName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
{(status === "invited" || status === "verified") &&
|
||||
serverDetails?.emailConfigured && (
|
||||
<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" && (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
onClick={() => onGrantAccess(u?._id, u?.publicKey)}
|
||||
>
|
||||
Grant Access
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
<Td>
|
||||
{userWs ? (
|
||||
@@ -428,16 +452,23 @@ export const OrgMembersTable = ({ roles = [] }: Props) => {
|
||||
</Td>
|
||||
<Td>
|
||||
{userId !== u?._id && (
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
isDisabled={userId === u?._id}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("removeMember", { id: orgMembershipId })
|
||||
}
|
||||
<OrgPermissionCan
|
||||
I={OrgGeneralPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
isDisabled={userId === u?._id || !isAllowed}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("removeMember", { id: orgMembershipId })
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
+6
-4
@@ -38,10 +38,9 @@ export const BillingPermission = ({ isNonEditable, setValue, control }: Props) =
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
let score = 0;
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
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 (score === 0) return Permission.NoAccess;
|
||||
@@ -52,11 +51,14 @@ export const BillingPermission = ({ isNonEditable, setValue, control }: Props) =
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
|
||||
if (val === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
setValue(
|
||||
|
||||
+6
-8
@@ -38,10 +38,9 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
let score = 0;
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
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 (score === 0) return Permission.NoAccess;
|
||||
@@ -52,14 +51,16 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
|
||||
if (val === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.incident-contact",
|
||||
{ read: false, edit: false, create: false, delete: false },
|
||||
@@ -67,7 +68,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
|
||||
);
|
||||
break;
|
||||
case Permission.FullAccess:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.incident-contact",
|
||||
{ read: true, edit: true, create: true, delete: true },
|
||||
@@ -75,7 +75,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
|
||||
);
|
||||
break;
|
||||
case Permission.ReadOnly:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.incident-contact",
|
||||
{ read: true, edit: false, create: false, delete: false },
|
||||
@@ -83,7 +82,6 @@ export const IncidentContactPermission = ({ isNonEditable, setValue, control }:
|
||||
);
|
||||
break;
|
||||
default:
|
||||
setIsCustom.on();
|
||||
setValue(
|
||||
"permissions.incident-contact",
|
||||
{ read: false, edit: false, create: false, delete: false },
|
||||
|
||||
+10
-7
@@ -31,31 +31,34 @@ const PERMISSIONS = [
|
||||
] as const;
|
||||
|
||||
export const MemberPermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||
const memberRule = useWatch({
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.member"
|
||||
});
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
let score = 0;
|
||||
const actions = Object.keys(memberRule || {}) as Array<keyof typeof memberRule>;
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
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 (score === 0) return Permission.NoAccess;
|
||||
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;
|
||||
}, [memberRule, isCustom]);
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (val === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
setValue(
|
||||
|
||||
+10
-11
@@ -31,34 +31,36 @@ const PERMISSIONS = [
|
||||
] as const;
|
||||
|
||||
export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||
const roleRule = useWatch({
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.role"
|
||||
});
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
let score = 0;
|
||||
const actions = Object.keys(roleRule || {}) as Array<keyof typeof roleRule>;
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
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 (score === 0) return Permission.NoAccess;
|
||||
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;
|
||||
}, [roleRule, isCustom]);
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (val === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.role",
|
||||
{ read: false, edit: false, create: false, delete: false },
|
||||
@@ -66,7 +68,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||
);
|
||||
break;
|
||||
case Permission.FullAccess:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.role",
|
||||
{ read: true, edit: true, create: true, delete: true },
|
||||
@@ -74,7 +75,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||
);
|
||||
break;
|
||||
case Permission.ReadOnly:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.role",
|
||||
{ read: true, edit: false, create: false, delete: false },
|
||||
@@ -82,7 +82,6 @@ export const RolePermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||
);
|
||||
break;
|
||||
default:
|
||||
setIsCustom.on();
|
||||
setValue(
|
||||
"permissions.role",
|
||||
{ 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 selectedPermissionCategory = useMemo(() => {
|
||||
let score = 0;
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
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 (score === 0) return Permission.NoAccess;
|
||||
@@ -52,13 +51,16 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (val === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.secret-scanning",
|
||||
{ read: false, edit: false, create: false, delete: false },
|
||||
@@ -66,7 +68,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
|
||||
);
|
||||
break;
|
||||
case Permission.FullAccess:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.secret-scanning",
|
||||
{ read: true, edit: true, create: true, delete: true },
|
||||
@@ -74,7 +75,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
|
||||
);
|
||||
break;
|
||||
case Permission.ReadOnly:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.secret-scanning",
|
||||
{ read: true, edit: false, create: false, delete: false },
|
||||
@@ -82,7 +82,6 @@ export const SecretScannigPermission = ({ isNonEditable, setValue, control }: Pr
|
||||
);
|
||||
break;
|
||||
default:
|
||||
setIsCustom.on();
|
||||
setValue(
|
||||
"permissions.secret-scanning",
|
||||
{ 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 selectedPermissionCategory = useMemo(() => {
|
||||
let score = 0;
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
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 (score === 0) return Permission.NoAccess;
|
||||
@@ -52,13 +51,16 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (val === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.settings",
|
||||
{ read: false, edit: false, create: false, delete: false },
|
||||
@@ -66,7 +68,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
|
||||
);
|
||||
break;
|
||||
case Permission.FullAccess:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.settings",
|
||||
{ read: true, edit: true, create: true, delete: true },
|
||||
@@ -74,7 +75,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
|
||||
);
|
||||
break;
|
||||
case Permission.ReadOnly:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.settings",
|
||||
{ read: true, edit: false, create: false, delete: false },
|
||||
@@ -82,7 +82,6 @@ export const SettingsPermission = ({ isNonEditable, setValue, control }: Props)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
setIsCustom.on();
|
||||
setValue(
|
||||
"permissions.settings",
|
||||
{ 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 selectedPermissionCategory = useMemo(() => {
|
||||
let score = 0;
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
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 (score === 0) return Permission.NoAccess;
|
||||
@@ -52,13 +51,16 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (val === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.sso",
|
||||
{ read: false, edit: false, create: false, delete: false },
|
||||
@@ -66,7 +68,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||
);
|
||||
break;
|
||||
case Permission.FullAccess:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.sso",
|
||||
{ read: true, edit: true, create: true, delete: true },
|
||||
@@ -74,7 +75,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||
);
|
||||
break;
|
||||
case Permission.ReadOnly:
|
||||
setIsCustom.off();
|
||||
setValue(
|
||||
"permissions.sso",
|
||||
{ read: true, edit: false, create: false, delete: false },
|
||||
@@ -82,7 +82,6 @@ export const SsoPermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||
);
|
||||
break;
|
||||
default:
|
||||
setIsCustom.on();
|
||||
setValue(
|
||||
"permissions.sso",
|
||||
{ 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 selectedPermissionCategory = useMemo(() => {
|
||||
let score = 0;
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
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 (score === 0) return Permission.NoAccess;
|
||||
@@ -50,11 +49,14 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedPermissionCategory === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
val === Permission.Custom ? setIsCustom.on() : setIsCustom.off();
|
||||
if (val === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.NoAccess:
|
||||
setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+138
-115
@@ -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",
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?._id ?? "");
|
||||
const getOrgTrialUrl = useGetOrgTrialUrl();
|
||||
const createCustomerPortalSession = useCreateCustomerPortalSession();
|
||||
|
||||
function formatPlanSlug(slug: string) {
|
||||
return slug
|
||||
.replace(/(\b[a-z])/g, match => match.toUpperCase())
|
||||
.replace(/-/g, " ");
|
||||
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
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
} else {
|
||||
// open compare plans modal
|
||||
handlePopUpOpen("managePlan");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
+82
-88
@@ -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();
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
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>
|
||||
<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 { 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]);
|
||||
|
||||
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"
|
||||
});
|
||||
}
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
email: data?.email ?? ""
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
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="[email protected]"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="email"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
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"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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="[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 { 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>
|
||||
);
|
||||
};
|
||||
|
||||
+20
-11
@@ -1,6 +1,7 @@
|
||||
import { faCreditCard, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useDeleteOrgPmtMethod, useGetOrgPmtMethods } from "@app/hooks/api";
|
||||
|
||||
export const PmtMethodsTable = () => {
|
||||
@@ -52,17 +53,25 @@ export const PmtMethodsTable = () => {
|
||||
<Td>{last4}</Td>
|
||||
<Td>{`${exp_month}/${exp_year}`}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await handleDeletePmtMethodBtnClick(_id);
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
<OrgPermissionCan
|
||||
I={OrgGeneralPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Billing}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await handleDeletePmtMethodBtnClick(_id);
|
||||
}}
|
||||
size="lg"
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
+31
-27
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+20
-11
@@ -1,6 +1,7 @@
|
||||
import { faFileInvoice, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useDeleteOrgTaxId, useGetOrgTaxIds } from "@app/hooks/api";
|
||||
|
||||
const taxIDTypeLabelMap: { [key: string]: string } = {
|
||||
@@ -101,17 +102,25 @@ export const TaxIDTable = () => {
|
||||
<Td>{taxIDTypeLabelMap[type]}</Td>
|
||||
<Td>{value}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await handleDeleteTaxIdBtnClick(_id);
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
<OrgPermissionCan
|
||||
I={OrgGeneralPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Billing}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
await handleDeleteTaxIdBtnClick(_id);
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
+46
-38
@@ -1,5 +1,8 @@
|
||||
import { Fragment } from "react"
|
||||
import { Tab } from "@headlessui/react"
|
||||
import { Fragment } from "react";
|
||||
import { Tab } from "@headlessui/react";
|
||||
|
||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
|
||||
import { BillingCloudTab } from "../BillingCloudTab";
|
||||
import { BillingDetailsTab } from "../BillingDetailsTab";
|
||||
@@ -7,43 +10,48 @@ import { BillingReceiptsTab } from "../BillingReceiptsTab";
|
||||
import { BillingSelfHostedTab } from "../BillingSelfHostedTab";
|
||||
|
||||
const tabs = [
|
||||
{ name: "Infisical Cloud", key: "tab-infisical-cloud" },
|
||||
{ name: "Infisical Self-Hosted", key: "tab-infisical-self-hosted" },
|
||||
{ name: "Receipts", key: "tab-receipts" },
|
||||
{ name: "Billing details", key: "tab-billing-details" }
|
||||
{ name: "Infisical Cloud", key: "tab-infisical-cloud" },
|
||||
{ name: "Infisical Self-Hosted", key: "tab-infisical-self-hosted" },
|
||||
{ name: "Receipts", key: "tab-receipts" },
|
||||
{ name: "Billing details", key: "tab-billing-details" }
|
||||
];
|
||||
|
||||
export const BillingTabGroup = () => {
|
||||
export const BillingTabGroup = withPermission(
|
||||
() => {
|
||||
return (
|
||||
<Tab.Group>
|
||||
<Tab.List className="mt-8 mb-6 border-b-2 border-mineshaft-800">
|
||||
{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>
|
||||
<BillingCloudTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingSelfHostedTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingReceiptsTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingDetailsTab />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
<Tab.Group>
|
||||
<Tab.List className="mt-8 mb-6 border-b-2 border-mineshaft-800">
|
||||
{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>
|
||||
<BillingCloudTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingSelfHostedTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingReceiptsTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingDetailsTab />
|
||||
</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";
|
||||
|
||||
export const OrgAuthTab = () => {
|
||||
export const OrgAuthTab = withPermission(
|
||||
() => {
|
||||
return (
|
||||
<div>
|
||||
<OrgSSOSection />
|
||||
</div>
|
||||
<div>
|
||||
<OrgSSOSection />
|
||||
</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 { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useCreateSSOConfig,
|
||||
useGetSSOConfig,
|
||||
useUpdateSSOConfig
|
||||
} from "@app/hooks/api";
|
||||
import {
|
||||
OrgGeneralPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { useCreateSSOConfig, useGetSSOConfig, useUpdateSSOConfig } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { SSOModal } from "./SSOModal";
|
||||
|
||||
const ssoAuthProviderMap: { [key: string]: string } = {
|
||||
"okta-saml": "Okta SAML",
|
||||
"azure-saml": "Azure SAML",
|
||||
"jumpcloud-saml": "JumpCloud SAML"
|
||||
}
|
||||
"okta-saml": "Okta SAML",
|
||||
"azure-saml": "Azure SAML",
|
||||
"jumpcloud-saml": "JumpCloud SAML"
|
||||
};
|
||||
|
||||
export const OrgSSOSection = (): JSX.Element => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { data, isLoading } = useGetSSOConfig(currentOrg?._id ?? "");
|
||||
const { mutateAsync } = useUpdateSSOConfig();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"upgradePlan",
|
||||
"addSSO"
|
||||
] as const);
|
||||
|
||||
const { mutateAsync: createMutateAsync } = useCreateSSOConfig();
|
||||
|
||||
const handleSamlSSOToggle = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
const { currentOrg } = useOrganization();
|
||||
const { subscription } = useSubscription();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { data, isLoading } = useGetSSOConfig(currentOrg?._id ?? "");
|
||||
const { mutateAsync } = useUpdateSSOConfig();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"upgradePlan",
|
||||
"addSSO"
|
||||
] as const);
|
||||
|
||||
await mutateAsync({
|
||||
organizationId: currentOrg?._id,
|
||||
isActive: value
|
||||
});
|
||||
const { mutateAsync: createMutateAsync } = useCreateSSOConfig();
|
||||
|
||||
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 handleSamlSSOToggle = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
|
||||
await mutateAsync({
|
||||
organizationId: currentOrg?._id,
|
||||
isActive: value
|
||||
});
|
||||
|
||||
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");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
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");
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
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">
|
||||
SAML SSO Configuration
|
||||
</h2>
|
||||
{!isLoading && (
|
||||
<Button
|
||||
onClick={addSSOBtnClick}
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
{data ? "Update SAML SSO" : "Set up SAML SSO"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{data && (
|
||||
<div className="mb-4">
|
||||
<Switch
|
||||
id="enable-saml-sso"
|
||||
onCheckedChange={(value) => handleSamlSSOToggle(value)}
|
||||
isChecked={data ? data.isActive : false}
|
||||
>
|
||||
Enable SAML SSO
|
||||
</Switch>
|
||||
</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">SAML SSO Configuration</h2>
|
||||
{!isLoading && (
|
||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
onClick={addSSOBtnClick}
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
{data ? "Update SAML SSO" : "Set up SAML SSO"}
|
||||
</Button>
|
||||
)}
|
||||
<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."
|
||||
/>
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
{data && (
|
||||
<div className="mb-4">
|
||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enable-saml-sso"
|
||||
onCheckedChange={(value) => handleSamlSSOToggle(value)}
|
||||
isChecked={data ? data.isActive : false}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Enable SAML SSO
|
||||
</Switch>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</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";
|
||||
|
||||
export const OrgGeneralTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<OrgNameChangeSection />
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
|
||||
<OrgServiceAccountsTable />
|
||||
</div>
|
||||
<OrgIncidentContactsSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<OrgNameChangeSection />
|
||||
<OrgServiceAccountsTable />
|
||||
<OrgIncidentContactsSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+79
-94
@@ -3,17 +3,9 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent
|
||||
} from "@app/components/v2";
|
||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import {
|
||||
useAddIncidentContact
|
||||
} from "@app/hooks/api";
|
||||
import { useAddIncidentContact } from "@app/hooks/api";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
@@ -24,97 +16,90 @@ const addContactFormSchema = yup.object({
|
||||
type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
|
||||
|
||||
type Props = {
|
||||
popUp: UsePopUpState<["addContact"]>;
|
||||
handlePopUpClose: (popUpName: keyof UsePopUpState<["addContact"]>) => void;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addContact"]>, state?: boolean) => void;
|
||||
popUp: UsePopUpState<["addContact"]>;
|
||||
handlePopUpClose: (popUpName: keyof UsePopUpState<["addContact"]>) => void;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addContact"]>, state?: boolean) => void;
|
||||
};
|
||||
|
||||
export const AddOrgIncidentContactModal = ({
|
||||
popUp,
|
||||
handlePopUpClose,
|
||||
handlePopUpToggle
|
||||
popUp,
|
||||
handlePopUpClose,
|
||||
handlePopUpToggle
|
||||
}: Props) => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data: serverDetails } = useFetchServerStatus()
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset
|
||||
} = useForm<TAddContactForm>({ resolver: yupResolver(addContactFormSchema) });
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
const { control, handleSubmit, reset } = useForm<TAddContactForm>({
|
||||
resolver: yupResolver(addContactFormSchema)
|
||||
});
|
||||
|
||||
const { mutateAsync, isLoading } = useAddIncidentContact();
|
||||
|
||||
const onFormSubmit = async ({ email }: TAddContactForm) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
|
||||
await mutateAsync({
|
||||
orgId: currentOrg._id,
|
||||
email
|
||||
});
|
||||
const { mutateAsync, isLoading } = useAddIncidentContact();
|
||||
|
||||
createNotification({
|
||||
text: "Successfully added incident contact",
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (serverDetails?.emailConfigured){
|
||||
handlePopUpClose("addContact");
|
||||
}
|
||||
const onFormSubmit = async ({ email }: TAddContactForm) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
|
||||
reset();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to add incident contact",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
await mutateAsync({
|
||||
orgId: currentOrg._id,
|
||||
email
|
||||
});
|
||||
|
||||
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 (
|
||||
<Modal
|
||||
isOpen={popUp?.addContact?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("addContact", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Add an Incident Contact"
|
||||
subTitle="This contact will be notified in the unlikely event of a severe incident."
|
||||
return (
|
||||
<Modal
|
||||
isOpen={popUp?.addContact?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("addContact", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Add an Incident Contact"
|
||||
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)}>
|
||||
<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">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
|
||||
import { AddOrgIncidentContactModal } from "./AddOrgIncidentContactModal";
|
||||
import { OrgIncidentContactsTable } from "./OrgIncidentContactsTable";
|
||||
|
||||
export const OrgIncidentContactsSection = () => {
|
||||
export const OrgIncidentContactsSection = withPermission(
|
||||
() => {
|
||||
const { t } = useTranslation();
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"addContact"
|
||||
"addContact"
|
||||
] as const);
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex justify-between mb-4">
|
||||
<p className="min-w-max text-xl font-semibold">
|
||||
{t("section.incident.incident-contacts")}
|
||||
</p>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addContact")}
|
||||
>
|
||||
Add contact
|
||||
</Button>
|
||||
</div>
|
||||
<OrgIncidentContactsTable />
|
||||
<AddOrgIncidentContactModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<div className="flex justify-between mb-4">
|
||||
<p className="min-w-max text-xl font-semibold">
|
||||
{t("section.incident.incident-contacts")}
|
||||
</p>
|
||||
<OrgPermissionCan
|
||||
I={OrgGeneralPermissionActions.Create}
|
||||
a={OrgPermissionSubjects.IncidentAccount}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addContact")}
|
||||
>
|
||||
Add contact
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</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 { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { OrgGeneralPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useDeleteIncidentContact, useGetOrgIncidentContact } from "@app/hooks/api";
|
||||
|
||||
@@ -83,13 +84,21 @@ export const OrgIncidentContactsTable = () => {
|
||||
<Tr key={email}>
|
||||
<Td className="w-full">{email}</Td>
|
||||
<Td className="mr-4">
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
onClick={() => handlePopUpOpen("removeContact", { email })}
|
||||
<OrgPermissionCan
|
||||
I={OrgGeneralPermissionActions.Delete}
|
||||
an={OrgPermissionSubjects.IncidentAccount}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
onClick={() => handlePopUpOpen("removeContact", { email })}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
|
||||
+61
-51
@@ -4,8 +4,10 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
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";
|
||||
|
||||
const formSchema = yup.object({
|
||||
@@ -14,49 +16,46 @@ const formSchema = yup.object({
|
||||
|
||||
type FormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
export const OrgNameChangeSection = (): JSX.Element => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
reset
|
||||
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
const { mutateAsync, isLoading } = useRenameOrg();
|
||||
export const OrgNameChangeSection = withPermission(
|
||||
(): JSX.Element => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({
|
||||
resolver: yupResolver(formSchema)
|
||||
});
|
||||
const { mutateAsync, isLoading } = useRenameOrg();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentOrg) {
|
||||
reset({ name: currentOrg.name });
|
||||
}
|
||||
}, [currentOrg]);
|
||||
useEffect(() => {
|
||||
if (currentOrg) {
|
||||
reset({ name: currentOrg.name });
|
||||
}
|
||||
}, [currentOrg]);
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
if (name === "") return;
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
if (name === "") return;
|
||||
|
||||
await mutateAsync({ orgId: currentOrg?._id, newOrgName: name });
|
||||
createNotification({
|
||||
text: "Successfully renamed organization",
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to rename organization",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
await mutateAsync({ orgId: currentOrg?._id, newOrgName: name });
|
||||
createNotification({
|
||||
text: "Successfully renamed organization",
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to rename organization",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
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">
|
||||
Organization name
|
||||
</p>
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
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">Organization name</p>
|
||||
<div className="mb-2 max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
@@ -69,14 +68,25 @@ export const OrgNameChangeSection = (): JSX.Element => {
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
<OrgPermissionCan I={OrgGeneralPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
{
|
||||
action: OrgGeneralPermissionActions.Read,
|
||||
subject: OrgPermissionSubjects.Settings,
|
||||
containerClassName: "mb-4"
|
||||
}
|
||||
);
|
||||
|
||||
+304
-289
@@ -34,7 +34,13 @@ import {
|
||||
THead,
|
||||
Tr
|
||||
} 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 {
|
||||
// useCreateServiceAccount,
|
||||
@@ -62,313 +68,322 @@ import // Controller,
|
||||
|
||||
// type TAddServiceAccountForm = yup.InferType<typeof addServiceAccountFormSchema>;
|
||||
|
||||
export const OrgServiceAccountsTable = () => {
|
||||
const router = useRouter();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
export const OrgServiceAccountsTable = withPermission(
|
||||
() => {
|
||||
const router = useRouter();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const orgId = currentOrg?._id || "";
|
||||
const [step, setStep] = useState(0);
|
||||
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
||||
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
|
||||
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
||||
const [accessKey] = useState("");
|
||||
const [publicKey] = useState("");
|
||||
const [privateKey] = useState("");
|
||||
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"addServiceAccount",
|
||||
"removeServiceAccount"
|
||||
] as const);
|
||||
const orgId = currentOrg?._id || "";
|
||||
const [step, setStep] = useState(0);
|
||||
const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
|
||||
const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
|
||||
const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
|
||||
const [accessKey] = useState("");
|
||||
const [publicKey] = useState("");
|
||||
const [privateKey] = useState("");
|
||||
const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState("");
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"addServiceAccount",
|
||||
"removeServiceAccount"
|
||||
] as const);
|
||||
|
||||
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } =
|
||||
useGetServiceAccounts(orgId);
|
||||
const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } =
|
||||
useGetServiceAccounts(orgId);
|
||||
|
||||
// const createServiceAccount = useCreateServiceAccount();
|
||||
const removeServiceAccount = useDeleteServiceAccount();
|
||||
// const createServiceAccount = useCreateServiceAccount();
|
||||
const removeServiceAccount = useDeleteServiceAccount();
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isAccessKeyCopied) {
|
||||
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
|
||||
}
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isAccessKeyCopied) {
|
||||
timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isPublicKeyCopied) {
|
||||
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
|
||||
}
|
||||
if (isPublicKeyCopied) {
|
||||
timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
|
||||
}
|
||||
|
||||
if (isPrivateKeyCopied) {
|
||||
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
|
||||
}
|
||||
if (isPrivateKeyCopied) {
|
||||
timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
|
||||
return () => clearTimeout(timer);
|
||||
}, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
|
||||
|
||||
// const {
|
||||
// control,
|
||||
// handleSubmit,
|
||||
// reset,
|
||||
// formState: { isSubmitting }
|
||||
// } = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
||||
// const {
|
||||
// control,
|
||||
// handleSubmit,
|
||||
// reset,
|
||||
// formState: { isSubmitting }
|
||||
// } = useForm<TAddServiceAccountForm>({ resolver: yupResolver(addServiceAccountFormSchema) });
|
||||
|
||||
// const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
||||
// if (!currentOrg?._id) return;
|
||||
// const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
|
||||
// if (!currentOrg?._id) return;
|
||||
|
||||
// const keyPair = generateKeyPair();
|
||||
// setPublicKey(keyPair.publicKey);
|
||||
// setPrivateKey(keyPair.privateKey);
|
||||
// const keyPair = generateKeyPair();
|
||||
// setPublicKey(keyPair.publicKey);
|
||||
// setPrivateKey(keyPair.privateKey);
|
||||
|
||||
// const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
||||
// name,
|
||||
// organizationId: currentOrg?._id,
|
||||
// publicKey: keyPair.publicKey,
|
||||
// expiresIn: Number(expiresIn)
|
||||
// });
|
||||
// const serviceAccountDetails = await createServiceAccount.mutateAsync({
|
||||
// name,
|
||||
// organizationId: currentOrg?._id,
|
||||
// publicKey: keyPair.publicKey,
|
||||
// expiresIn: Number(expiresIn)
|
||||
// });
|
||||
|
||||
// setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
||||
// setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
|
||||
|
||||
// setStep(1);
|
||||
// reset();
|
||||
// }
|
||||
// setStep(1);
|
||||
// reset();
|
||||
// }
|
||||
|
||||
const onRemoveServiceAccount = async () => {
|
||||
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
||||
await removeServiceAccount.mutateAsync(serviceAccountId);
|
||||
handlePopUpClose("removeServiceAccount");
|
||||
};
|
||||
const onRemoveServiceAccount = async () => {
|
||||
const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
|
||||
await removeServiceAccount.mutateAsync(serviceAccountId);
|
||||
handlePopUpClose("removeServiceAccount");
|
||||
};
|
||||
|
||||
const filteredServiceAccounts = useMemo(
|
||||
() =>
|
||||
serviceAccounts.filter(({ name }) => name.toLowerCase().includes(searchServiceAccountFilter)),
|
||||
[serviceAccounts, searchServiceAccountFilter]
|
||||
);
|
||||
const filteredServiceAccounts = useMemo(
|
||||
() =>
|
||||
serviceAccounts.filter(({ name }) =>
|
||||
name.toLowerCase().includes(searchServiceAccountFilter)
|
||||
),
|
||||
[serviceAccounts, searchServiceAccountFilter]
|
||||
);
|
||||
|
||||
const renderStep = (stepToRender: number) => {
|
||||
switch (stepToRender) {
|
||||
case 0:
|
||||
return (
|
||||
<div>
|
||||
We are currently revising the service account mechanism. In the meantime, please use
|
||||
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>
|
||||
const renderStep = (stepToRender: number) => {
|
||||
switch (stepToRender) {
|
||||
case 0:
|
||||
return (
|
||||
<div>
|
||||
We are currently revising the service account mechanism. In the meantime, please use
|
||||
service tokens or API key to fetch secrets via API request.
|
||||
</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 />;
|
||||
}
|
||||
};
|
||||
// <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>
|
||||
<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 (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
setStep(0);
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600 mb-6">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Service Accounts</p>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
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();
|
||||
handlePopUpOpen("addServiceAccount");
|
||||
}}
|
||||
>
|
||||
Add Service Account
|
||||
</Button>
|
||||
<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>
|
||||
<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();
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
);
|
||||
},
|
||||
{
|
||||
action: OrgGeneralPermissionActions.Read,
|
||||
subject: OrgPermissionSubjects.Settings,
|
||||
containerClassName: "mb-4"
|
||||
}
|
||||
);
|
||||
|
||||
+35
-54
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user