Merge pull request #1560 from Infisical/toggle-invite-org

Update UI for Org Settings Page
This commit is contained in:
BlackMagiq
2024-03-12 16:26:39 -07:00
committed by GitHub
19 changed files with 254 additions and 299 deletions
+11 -9
View File
@@ -164,15 +164,17 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
method: "PATCH", method: "PATCH",
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
schema: { schema: {
body: z.object({ body: z
organizationId: z.string().trim(), .object({
isActive: z.boolean().optional(), isActive: z.boolean(),
url: z.string().trim().optional(), url: z.string().trim(),
bindDN: z.string().trim().optional(), bindDN: z.string().trim(),
bindPass: z.string().trim().optional(), bindPass: z.string().trim(),
searchBase: z.string().trim().optional(), searchBase: z.string().trim(),
caCert: z.string().trim().optional() caCert: z.string().trim()
}), })
.partial()
.merge(z.object({ organizationId: z.string() })),
response: { response: {
200: LdapConfigsSchema 200: LdapConfigsSchema
} }
@@ -180,21 +180,21 @@ export const ldapConfigServiceFactory = ({
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
}); });
if (bindDN) { if (bindDN !== undefined) {
const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key); const { ciphertext: encryptedBindDN, iv: bindDNIV, tag: bindDNTag } = encryptSymmetric(bindDN, key);
updateQuery.encryptedBindDN = encryptedBindDN; updateQuery.encryptedBindDN = encryptedBindDN;
updateQuery.bindDNIV = bindDNIV; updateQuery.bindDNIV = bindDNIV;
updateQuery.bindDNTag = bindDNTag; updateQuery.bindDNTag = bindDNTag;
} }
if (bindPass) { if (bindPass !== undefined) {
const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key); const { ciphertext: encryptedBindPass, iv: bindPassIV, tag: bindPassTag } = encryptSymmetric(bindPass, key);
updateQuery.encryptedBindPass = encryptedBindPass; updateQuery.encryptedBindPass = encryptedBindPass;
updateQuery.bindPassIV = bindPassIV; updateQuery.bindPassIV = bindPassIV;
updateQuery.bindPassTag = bindPassTag; updateQuery.bindPassTag = bindPassTag;
} }
if (caCert) { if (caCert !== undefined) {
const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key); const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
updateQuery.encryptedCACert = encryptedCACert; updateQuery.encryptedCACert = encryptedCACert;
updateQuery.caCertIV = caCertIV; updateQuery.caCertIV = caCertIV;
@@ -172,7 +172,7 @@ export const samlConfigServiceFactory = ({
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
}); });
if (entryPoint) { if (entryPoint !== undefined) {
const { const {
ciphertext: encryptedEntryPoint, ciphertext: encryptedEntryPoint,
iv: entryPointIV, iv: entryPointIV,
@@ -182,18 +182,19 @@ export const samlConfigServiceFactory = ({
updateQuery.entryPointIV = entryPointIV; updateQuery.entryPointIV = entryPointIV;
updateQuery.entryPointTag = entryPointTag; updateQuery.entryPointTag = entryPointTag;
} }
if (issuer) { if (issuer !== undefined) {
const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key); const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key);
updateQuery.encryptedIssuer = encryptedIssuer; updateQuery.encryptedIssuer = encryptedIssuer;
updateQuery.issuerIV = issuerIV; updateQuery.issuerIV = issuerIV;
updateQuery.issuerTag = issuerTag; updateQuery.issuerTag = issuerTag;
} }
if (cert) { if (cert !== undefined) {
const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key); const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key);
updateQuery.encryptedCert = encryptedCert; updateQuery.encryptedCert = encryptedCert;
updateQuery.certIV = certIV; updateQuery.certIV = certIV;
updateQuery.certTag = certTag; updateQuery.certTag = certTag;
} }
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery); const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);
await orgDAL.updateById(orgId, { authEnforced: false, scimEnabled: false }); await orgDAL.updateById(orgId, { authEnforced: false, scimEnabled: false });
-8
View File
@@ -159,14 +159,6 @@
"documentation/platform/ldap/general" "documentation/platform/ldap/general"
] ]
}, },
{
"group": "LDAP",
"pages": [
"documentation/platform/ldap/overview",
"documentation/platform/ldap/jumpcloud",
"documentation/platform/ldap/general"
]
},
{ {
"group": "SCIM", "group": "SCIM",
"pages": [ "pages": [
@@ -7,7 +7,7 @@ export const OrgSettingsPage = () => {
return ( return (
<div className="flex justify-center bg-bunker-800 text-white w-full py-6"> <div className="flex justify-center bg-bunker-800 text-white w-full py-6">
<div className="max-w-7xl w-full px-6"> <div className="max-w-4xl w-full px-6">
<div className="mb-4"> <div className="mb-4">
<p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p> <p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
</div> </div>
@@ -21,10 +21,10 @@ import {
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const LDAPFormSchema = z.object({ const LDAPFormSchema = z.object({
url: z.string().min(1, "URL is requiredx"), url: z.string().default(""),
bindDN: z.string().min(1, "Bind DN is requiredx"), bindDN: z.string().default(""),
bindPass: z.string().min(1, "Bind Pass is required"), bindPass: z.string().default(""),
searchBase: z.string().min(1, "Search Base is required"), searchBase: z.string().default(""),
caCert: z.string().optional() caCert: z.string().optional()
}); });
@@ -122,7 +122,7 @@ export const LDAPModal = ({
reset(); reset();
}} }}
> >
<ModalContent title="Add LDAP"> <ModalContent title="Manage LDAP configuration">
<form onSubmit={handleSubmit(onSSOModalSubmit)}> <form onSubmit={handleSubmit(onSSOModalSubmit)}>
<Controller <Controller
control={control} control={control}
@@ -9,7 +9,7 @@ import { OrgSSOSection } from "./OrgSSOSection";
export const OrgAuthTab = withPermission( export const OrgAuthTab = withPermission(
() => { () => {
return ( return (
<div> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<OrgGeneralAuthSection /> <OrgGeneralAuthSection />
<OrgSSOSection /> <OrgSSOSection />
<OrgLDAPSection /> <OrgLDAPSection />
@@ -59,25 +59,44 @@ export const OrgGeneralAuthSection = () => {
} }
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <>
<h2 className="flex-1 text-xl font-semibold text-white mb-8">Settings</h2> {/* <div className="py-4">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}> <div className="mb-2 flex justify-between">
{(isAllowed) => ( <h3 className="text-md text-mineshaft-100">Allow users to send invites</h3>
<Switch <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
id="enforce-org-auth" {(isAllowed) => (
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)} <Switch
isChecked={currentOrg?.authEnforced ?? false} id="allow-org-invites"
isDisabled={!isAllowed} onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
> isChecked={currentOrg?.authEnforced ?? false}
Enforce SAML SSO isDisabled={!isAllowed}
</Switch> />
)} )}
</OrgPermissionCan> </OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">Allow members to invite new users to this organization</p>
</div> */}
<div className="py-4">
<div className="mb-2 flex justify-between">
<h3 className="text-md text-mineshaft-100">Enforce SAML SSO</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enforce-org-auth"
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
isChecked={currentOrg?.authEnforced ?? false}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">Enforce members to authenticate via SAML to access this organization</p>
</div>
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can enforce SAML SSO if you switch to Infisical's Pro plan." text="You can enforce SAML SSO if you switch to Infisical's Pro plan."
/> />
</div> </>
); );
} }
@@ -1,6 +1,3 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { import {
@@ -27,7 +24,7 @@ export const OrgLDAPSection = (): JSX.Element => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { data, isLoading } = useGetLDAPConfig(currentOrg?.id ?? ""); const { data } = useGetLDAPConfig(currentOrg?.id ?? "");
const { mutateAsync } = useUpdateLDAPConfig(); const { mutateAsync } = useUpdateLDAPConfig();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addLDAP", "addLDAP",
@@ -88,50 +85,55 @@ export const OrgLDAPSection = (): JSX.Element => {
}; };
return ( return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> <>
<div className="flex items-center mb-8"> <hr className="border-mineshaft-600" />
<h2 className="text-xl font-semibold flex-1 text-white">LDAP</h2> <div className="py-4">
{!isLoading && ( <div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">LDAP</h2>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}> <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Ldap}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
onClick={addLDAPBtnClick} onClick={addLDAPBtnClick}
colorSchema="secondary" colorSchema="secondary"
isDisabled={!isAllowed} isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
> >
Configure Manage
</Button> </Button>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
)} </div>
<p className="text-sm text-mineshaft-300">Manage LDAP authentication configuration</p>
</div> </div>
{data && ( {data && (
<div className="mb-4"> <div className="py-4">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Ldap}> <div className="mb-2 flex items-center justify-between">
{(isAllowed) => ( <h2 className="text-md text-mineshaft-100">Enable LDAP</h2>
<Switch <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Ldap}>
id="enable-saml-sso" {(isAllowed) => (
onCheckedChange={(value) => handleLDAPToggle(value)} <Switch
isChecked={data ? data.isActive : false} id="enable-saml-sso"
isDisabled={!isAllowed} onCheckedChange={(value) => handleLDAPToggle(value)}
> isChecked={data ? data.isActive : false}
Enable isDisabled={!isAllowed}
</Switch> >
)} Enable
</OrgPermissionCan> </Switch>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">Allow members to authenticate into Infisical with LDAP</p>
</div> </div>
)} )}
<LDAPModal <LDAPModal
popUp={popUp} popUp={popUp}
handlePopUpClose={handlePopUpClose} handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use LDAP authentication if you switch to Infisical's Enterprise plan." text="You can use LDAP authentication if you switch to Infisical's Enterprise plan."
/> />
</div> </>
); );
}; };
@@ -1,6 +1,3 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { import {
@@ -64,40 +61,47 @@ export const OrgScimSection = () => {
} }
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <>
<div className="mb-8 flex items-center"> <hr className="border-mineshaft-600" />
<h2 className="flex-1 text-xl font-semibold text-white">SCIM</h2> <div className="py-4">
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.Scim}> <div className="mb-2 flex justify-between items-center">
<h2 className="text-md text-mineshaft-100">SCIM</h2>
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.Scim}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
onClick={addScimTokenBtnClick} onClick={addScimTokenBtnClick}
colorSchema="secondary" colorSchema="secondary"
isDisabled={!isAllowed} isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
> >
Configure Manage
</Button> </Button>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">Manage SCIM configuration</p>
</div>
<div className="py-4">
<div className="mb-2 flex justify-between items-center">
<h2 className="text-md text-mineshaft-100">Enable SCIM</h2>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Scim}>
{(isAllowed) => (
<Switch
id="enable-scim"
onCheckedChange={(value) => {
if (subscription?.scim) {
handleEnableSCIMToggle(value)
} else {
handlePopUpOpen("upgradePlan");
}
}}
isChecked={currentOrg?.scimEnabled ?? false}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">Allow member provisioning/deprovisioning with SCIM</p>
</div> </div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Scim}>
{(isAllowed) => (
<Switch
id="enable-scim"
onCheckedChange={(value) => {
if (subscription?.scim) {
handleEnableSCIMToggle(value)
} else {
handlePopUpOpen("upgradePlan");
}
}}
isChecked={currentOrg?.scimEnabled ?? false}
isDisabled={!isAllowed}
>
Enable
</Switch>
)}
</OrgPermissionCan>
<ScimTokenModal <ScimTokenModal
popUp={popUp} popUp={popUp}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
@@ -108,6 +112,6 @@ export const OrgScimSection = () => {
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use SCIM Provisioning if you switch to Infisical's Enterprise plan." text="You can use SCIM Provisioning if you switch to Infisical's Enterprise plan."
/> />
</div> </>
); );
} }
@@ -1,6 +1,3 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import { Button, Switch, UpgradePlanModal } from "@app/components/v2"; import { Button, Switch, UpgradePlanModal } from "@app/components/v2";
@@ -81,49 +78,55 @@ export const OrgSSOSection = (): JSX.Element => {
}; };
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <>
<div className="mb-8 flex items-center"> <hr className="border-mineshaft-600" />
<h2 className="flex-1 text-xl font-semibold text-white">SAML</h2> <div className="py-4">
{!isLoading && ( <div className="mb-2 flex items-center justify-between">
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}> <h2 className="text-md text-mineshaft-100">SAML</h2>
{(isAllowed) => ( {!isLoading && (
<Button <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
onClick={addSSOBtnClick} {(isAllowed) => (
colorSchema="secondary" <Button
isDisabled={!isAllowed} onClick={addSSOBtnClick}
leftIcon={<FontAwesomeIcon icon={faPlus} />} colorSchema="secondary"
> isDisabled={!isAllowed}
Configure >
</Button> Manage
)} </Button>
</OrgPermissionCan> )}
)} </OrgPermissionCan>
</div> )}
{/* {data && ( */}
<div className="mb-4">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enable-saml-sso"
onCheckedChange={(value) => handleSamlSSOToggle(value)}
isChecked={data ? data.isActive : false}
isDisabled={!isAllowed}
>
Enable
</Switch>
)}
</OrgPermissionCan>
</div> </div>
<SSOModal <p className="text-sm text-mineshaft-300">Manage SAML authentication configuration</p>
popUp={popUp} </div>
handlePopUpClose={handlePopUpClose} <div className="py-4">
handlePopUpToggle={handlePopUpToggle} <div className="mb-2 flex items-center justify-between">
/> <h2 className="text-md text-mineshaft-100">Enable SAML</h2>
<UpgradePlanModal {!isLoading && (
isOpen={popUp.upgradePlan.isOpen} <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} {(isAllowed) => (
text="You can use SAML SSO if you switch to Infisical's Pro plan." <Switch
/> id="enable-saml-sso"
</div> onCheckedChange={(value) => handleSamlSSOToggle(value)}
isChecked={data ? data.isActive : false}
isDisabled={!isAllowed}
/>
)}
</OrgPermissionCan>
)}
</div>
<p className="text-sm text-mineshaft-300">Allow members to authenticate into Infisical with SAML</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."
/>
</>
); );
}; };
@@ -1,7 +1,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { zodResolver } from "@hookform/resolvers/zod";
import * as yup from "yup"; import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { import {
@@ -32,16 +32,16 @@ const ssoAuthProviders = [
{ label: "Google SAML", value: AuthProvider.GOOGLE_SAML } { label: "Google SAML", value: AuthProvider.GOOGLE_SAML }
]; ];
const schema = yup const schema = z
.object({ .object({
authProvider: yup.string().required("SSO Type is required"), authProvider: z.string().min(1, "SSO Type is required"),
entryPoint: yup.string().required("IdP entrypoint is required"), entryPoint: z.string().default(""),
issuer: yup.string().required("Issuer string is required"), issuer: z.string().default(""),
cert: yup.string().required("IdP's public signing certificate is required") cert: z.string().default("")
}) })
.required(); .required();
export type AddSSOFormData = yup.InferType<typeof schema>; export type AddSSOFormData = z.infer<typeof schema>;
type Props = { type Props = {
popUp: UsePopUpState<["addSSO"]>; popUp: UsePopUpState<["addSSO"]>;
@@ -60,7 +60,7 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
defaultValues: { defaultValues: {
authProvider: AuthProvider.OKTA_SAML authProvider: AuthProvider.OKTA_SAML
}, },
resolver: yupResolver(schema) resolver: zodResolver(schema)
}); });
useEffect(() => { useEffect(() => {
@@ -173,7 +173,7 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
reset(); reset();
}} }}
> >
<ModalContent title="Add SSO"> <ModalContent title="Manage SAML configuration">
<form onSubmit={handleSubmit(onSSOModalSubmit)}> <form onSubmit={handleSubmit(onSSOModalSubmit)}>
<Controller <Controller
control={control} control={control}
@@ -45,18 +45,21 @@ export const OrgDeleteSection = () => {
}; };
return ( return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <>
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Danger Zone</p> <hr className="border-mineshaft-600" />
<Button <div className="py-4">
isLoading={isLoading} <p className="mb-4 text-md text-mineshaft-100">Danger Zone</p>
colorSchema="danger" <Button
variant="outline_bg" isLoading={isLoading}
type="submit" colorSchema="danger"
onClick={() => handlePopUpOpen("deleteOrg")} variant="outline_bg"
isDisabled={Boolean(membership && membership.role !== "admin")} type="submit"
> onClick={() => handlePopUpOpen("deleteOrg")}
{`Delete ${currentOrg?.name}`} isDisabled={Boolean(membership && membership.role !== "admin")}
</Button> >
{`Delete ${currentOrg?.name}`}
</Button>
</div>
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteOrg.isOpen} isOpen={popUp.deleteOrg.isOpen}
title="Are you sure want to delete this organization?" title="Are you sure want to delete this organization?"
@@ -65,6 +68,6 @@ export const OrgDeleteSection = () => {
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={handleDeleteOrgSubmit} onDeleteApproved={handleDeleteOrgSubmit}
/> />
</div> </>
); );
}; };
@@ -3,15 +3,12 @@ import { useOrgPermission } from "@app/context";
import { OrgDeleteSection } from "../OrgDeleteSection"; import { OrgDeleteSection } from "../OrgDeleteSection";
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection"; import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
import { OrgNameChangeSection } from "../OrgNameChangeSection"; import { OrgNameChangeSection } from "../OrgNameChangeSection";
import { OrgSlugChangeSection } from "../OrgSlugChangeSection";
export const OrgGeneralTab = () => { export const OrgGeneralTab = () => {
const { membership } = useOrgPermission(); const { membership } = useOrgPermission();
return ( return (
<div> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<OrgNameChangeSection /> <OrgNameChangeSection />
<OrgSlugChangeSection />
<OrgIncidentContactsSection /> <OrgIncidentContactsSection />
{membership && membership.role === "admin" && <OrgDeleteSection />} {membership && membership.role === "admin" && <OrgDeleteSection />}
</div> </div>
@@ -18,9 +18,10 @@ export const OrgIncidentContactsSection = () => {
const { permission } = useOrgPermission(); const { permission } = useOrgPermission();
return ( return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600"> <>
<div className="flex justify-between mb-4"> <hr className="border-mineshaft-600" />
<p className="min-w-max text-xl font-semibold">{t("section.incident.incident-contacts")}</p> <div className="flex items-center justify-between pt-4">
<p className="text-md text-mineshaft-100">{t("section.incident.incident-contacts")}</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.IncidentAccount}> <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.IncidentAccount}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -35,16 +36,18 @@ export const OrgIncidentContactsSection = () => {
)} )}
</OrgPermissionCan> </OrgPermissionCan>
</div> </div>
{permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount) ? ( <div className="py-4">
<OrgIncidentContactsTable /> {permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount) ? (
) : ( <OrgIncidentContactsTable />
<PermissionDeniedBanner /> ) : (
)} <PermissionDeniedBanner />
)}
</div>
<AddOrgIncidentContactModal <AddOrgIncidentContactModal
popUp={popUp} popUp={popUp}
handlePopUpClose={handlePopUpClose} handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
</div> </>
); );
}; };
@@ -10,11 +10,12 @@ import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@a
import { useUpdateOrg } from "@app/hooks/api"; import { useUpdateOrg } from "@app/hooks/api";
const formSchema = yup.object({ const formSchema = yup.object({
name: yup name: yup.string().required().label("Organization Name").max(64, "Too long, maximum length is 64 characters"),
.string() slug: yup
.required() .string()
.label("Organization Name") .matches(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
.max(64, "Too long, maximum length is 64 characters") .required()
.label("Organization Slug")
}); });
type FormData = yup.InferType<typeof formSchema>; type FormData = yup.InferType<typeof formSchema>;
@@ -29,40 +30,44 @@ export const OrgNameChangeSection = (): JSX.Element => {
useEffect(() => { useEffect(() => {
if (currentOrg) { if (currentOrg) {
reset({ name: currentOrg.name }); reset({
name: currentOrg.name,
slug: currentOrg.slug
});
} }
}, [currentOrg]); }, [currentOrg]);
const onFormSubmit = async ({ name }: FormData) => { const onFormSubmit = async ({ name, slug }: FormData) => {
try { try {
if (!currentOrg?.id) return; if (!currentOrg?.id) return;
if (name === "") return;
await mutateAsync({ orgId: currentOrg?.id, name }); await mutateAsync({
orgId: currentOrg?.id,
name,
slug
});
createNotification({ createNotification({
text: "Successfully renamed organization", text: "Successfully updated organization details",
type: "success" type: "success"
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
createNotification({ createNotification({
text: "Failed to rename organization", text: "Failed to update organization details",
type: "error" type: "error"
}); });
} }
}; };
return ( return (
<form <form onSubmit={handleSubmit(onFormSubmit)} className="py-4">
onSubmit={handleSubmit(onFormSubmit)} <div className="">
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4" <h2 className="mb-2 text-md text-mineshaft-100">Organization Name</h2>
>
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Organization Name</p>
<div className="mb-2 max-w-md">
<Controller <Controller
defaultValue="" defaultValue=""
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}> <FormControl isError={Boolean(error)} errorText={error?.message} className="max-w-md">
<Input placeholder="Acme Corp" {...field} /> <Input placeholder="Acme Corp" {...field} />
</FormControl> </FormControl>
)} )}
@@ -70,6 +75,19 @@ export const OrgNameChangeSection = (): JSX.Element => {
name="name" name="name"
/> />
</div> </div>
<div className="py-4">
<h2 className="mb-2 text-md text-mineshaft-100">Organization Slug</h2>
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} className="max-w-md">
<Input placeholder="acme" {...field} />
</FormControl>
)}
control={control}
name="slug"
/>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}> <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
@@ -85,4 +103,4 @@ export const OrgNameChangeSection = (): JSX.Element => {
</OrgPermissionCan> </OrgPermissionCan>
</form> </form>
); );
}; };
@@ -1,88 +0,0 @@
import { useEffect } from "react";
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 { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useUpdateOrg } from "@app/hooks/api";
const formSchema = yup.object({
slug: yup
.string()
.matches(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
.required()
.label("Project Slug")
});
type FormData = yup.InferType<typeof formSchema>;
export const OrgSlugChangeSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const { handleSubmit, control, reset } = useForm<FormData>({
resolver: yupResolver(formSchema)
});
const { mutateAsync, isLoading } = useUpdateOrg();
useEffect(() => {
if (currentOrg) {
reset({ slug: currentOrg.slug });
}
}, [currentOrg]);
const onFormSubmit = async ({ slug }: FormData) => {
try {
if (!currentOrg?.id) return;
if (slug === "") return;
await mutateAsync({ orgId: currentOrg?.id, slug });
createNotification({
text: "Successfully updated organization slug",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to update organization slug",
type: "error"
});
}
};
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Organization Slug</p>
<div className="mb-2 max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input placeholder="acme" {...field} />
</FormControl>
)}
control={control}
name="slug"
/>
</div>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Button
isLoading={isLoading}
isDisabled={!isAllowed}
colorSchema="primary"
variant="outline_bg"
type="submit"
>
Save
</Button>
)}
</OrgPermissionCan>
</form>
);
}
@@ -1 +0,0 @@
export { OrgSlugChangeSection } from "./OrgSlugChangeSection";
@@ -6,7 +6,7 @@ import { OrgGeneralTab } from "../OrgGeneralTab";
const tabs = [ const tabs = [
{ name: "General", key: "tab-org-general" }, { name: "General", key: "tab-org-general" },
{ name: "Authentication", key: "tab-org-auth" } { name: "Security", key: "tab-org-security" }
]; ];
export const OrgTabGroup = () => { export const OrgTabGroup = () => {
return ( return (