mirror of
https://github.com/logto-io/logto.git
synced 2025-02-03 21:48:55 -05:00
feat(console): update jit config
This commit is contained in:
parent
2cf30d2f03
commit
a471d6a58c
7 changed files with 300 additions and 185 deletions
|
@ -57,7 +57,7 @@ function ActionMenu(props: Props) {
|
|||
<Button
|
||||
{...props.buttonProps}
|
||||
ref={anchorReference}
|
||||
className={styles.actionMenuButton}
|
||||
className={classNames(styles.actionMenuButton, props.buttonProps.className)}
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.container {
|
||||
background-color: var(--color-hover);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
|
@ -12,3 +12,49 @@
|
|||
.warning {
|
||||
margin-top: _.unit(3);
|
||||
}
|
||||
|
||||
.addSsoConnectorButton {
|
||||
margin-top: _.unit(3);
|
||||
}
|
||||
|
||||
.dropdownItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: _.unit(2);
|
||||
font: var(--font-body-2);
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.ssoConnectorList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: _.unit(1);
|
||||
}
|
||||
|
||||
.ssoConnector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: _.unit(2);
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: _.unit(3);
|
||||
gap: _.unit(3);
|
||||
background-color: var(--color-layer-2);
|
||||
font: var(--font-label-2);
|
||||
border-radius: 8px;
|
||||
color: var(--color-text);
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import { type SignInExperience, type Organization, type SsoConnector } from '@logto/schemas';
|
||||
import {
|
||||
type SignInExperience,
|
||||
type Organization,
|
||||
type SsoConnectorWithProviderConfig,
|
||||
} from '@logto/schemas';
|
||||
import { trySafe } from '@silverhand/essentials';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
import useSWRInfinite from 'swr/infinite';
|
||||
|
||||
import Minus from '@/assets/icons/minus.svg';
|
||||
import Plus from '@/assets/icons/plus.svg';
|
||||
import SsoIcon from '@/assets/icons/single-sign-on.svg';
|
||||
import DetailsForm from '@/components/DetailsForm';
|
||||
import FormCard from '@/components/FormCard';
|
||||
|
@ -15,13 +21,17 @@ import MultiOptionInput from '@/components/MultiOptionInput';
|
|||
import OrganizationRolesSelect from '@/components/OrganizationRolesSelect';
|
||||
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import ActionMenu from '@/ds-components/ActionMenu';
|
||||
import CodeEditor from '@/ds-components/CodeEditor';
|
||||
import { DropdownItem } from '@/ds-components/Dropdown';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import IconButton from '@/ds-components/IconButton';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
import { type Option } from '@/ds-components/Select/MultiSelect';
|
||||
import Switch from '@/ds-components/Switch';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
import useApi, { type RequestError } from '@/hooks/use-api';
|
||||
import SsoConnectorLogo from '@/pages/EnterpriseSso/SsoConnectorLogo';
|
||||
import { domainRegExp } from '@/pages/EnterpriseSsoDetails/Experience/DomainsInput/consts';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
|
@ -30,9 +40,9 @@ import { type OrganizationDetailsOutletContext } from '../types';
|
|||
import * as styles from './index.module.scss';
|
||||
|
||||
type FormData = Partial<Omit<Organization, 'customData'> & { customData: string }> & {
|
||||
isJitEnabled: boolean;
|
||||
jitEmailDomains: string[];
|
||||
jitRoles: Array<Option<string>>;
|
||||
jitSsoConnectorIds: string[];
|
||||
};
|
||||
|
||||
const isJsonObject = (value: string) => {
|
||||
|
@ -42,18 +52,19 @@ const isJsonObject = (value: string) => {
|
|||
|
||||
const normalizeData = (
|
||||
data: Organization,
|
||||
jit: { emailDomains: string[]; roles: Array<Option<string>> }
|
||||
jit: { emailDomains: string[]; roles: Array<Option<string>>; ssoConnectorIds: string[] }
|
||||
): FormData => ({
|
||||
...data,
|
||||
isJitEnabled: jit.emailDomains.length > 0 || jit.roles.length > 0,
|
||||
jitEmailDomains: jit.emailDomains,
|
||||
jitRoles: jit.roles,
|
||||
jitSsoConnectorIds: jit.ssoConnectorIds,
|
||||
customData: JSON.stringify(data.customData, undefined, 2),
|
||||
});
|
||||
|
||||
const assembleData = ({
|
||||
isJitEnabled,
|
||||
jitEmailDomains,
|
||||
jitRoles,
|
||||
jitSsoConnectorIds,
|
||||
customData,
|
||||
...data
|
||||
}: FormData): Partial<Organization> => ({
|
||||
|
@ -79,25 +90,23 @@ function Settings() {
|
|||
defaultValues: normalizeData(data, {
|
||||
emailDomains: jit.emailDomains.map(({ emailDomain }) => emailDomain),
|
||||
roles: jit.roles.map(({ id, name }) => ({ value: id, title: name })),
|
||||
ssoConnectorIds: jit.ssoConnectorIds,
|
||||
}),
|
||||
});
|
||||
const [isJitEnabled, isMfaRequired] = watch(['isJitEnabled', 'isMfaRequired']);
|
||||
const isMfaRequired = watch('isMfaRequired');
|
||||
const api = useApi();
|
||||
const [keyword, setKeyword] = useState('');
|
||||
// Fetch all SSO connector to show if a domain is configured SSO
|
||||
const { data: ssoConnectors } = useSWRInfinite<SsoConnector[]>(
|
||||
const { data: ssoConnectorMatrix } = useSWRInfinite<SsoConnectorWithProviderConfig[]>(
|
||||
(index, previous) => {
|
||||
return previous && previous.length === 0 ? null : `api/sso-connectors?page=${index + 1}`;
|
||||
},
|
||||
{ initialSize: Number.POSITIVE_INFINITY }
|
||||
);
|
||||
|
||||
const allSsoConnectors = useMemo(() => ssoConnectorMatrix?.flat(), [ssoConnectorMatrix]);
|
||||
const hasSsoEnabled = useCallback(
|
||||
(domain: string) =>
|
||||
ssoConnectors?.some((connectors) =>
|
||||
connectors.some(({ domains }) => domains.includes(domain))
|
||||
),
|
||||
[ssoConnectors]
|
||||
(domain: string) => allSsoConnectors?.some(({ domains }) => domains.includes(domain)),
|
||||
[allSsoConnectors]
|
||||
);
|
||||
|
||||
const onSubmit = handleSubmit(
|
||||
|
@ -106,8 +115,9 @@ function Settings() {
|
|||
return;
|
||||
}
|
||||
|
||||
const emailDomains = data.isJitEnabled ? data.jitEmailDomains : [];
|
||||
const roles = data.isJitEnabled ? data.jitRoles : [];
|
||||
const emailDomains = data.jitEmailDomains;
|
||||
const roles = data.jitRoles;
|
||||
const ssoConnectorIds = data.jitSsoConnectorIds;
|
||||
const updatedData = await api
|
||||
.patch(`api/organizations/${data.id}`, {
|
||||
json: assembleData(data),
|
||||
|
@ -121,9 +131,12 @@ function Settings() {
|
|||
api.put(`api/organizations/${data.id}/jit/roles`, {
|
||||
json: { organizationRoleIds: roles.map(({ value }) => value) },
|
||||
}),
|
||||
api.put(`api/organizations/${data.id}/jit/sso-connectors`, {
|
||||
json: { ssoConnectorIds },
|
||||
}),
|
||||
]);
|
||||
|
||||
reset(normalizeData(updatedData, { emailDomains, roles }));
|
||||
reset(normalizeData(updatedData, { emailDomains, roles, ssoConnectorIds }));
|
||||
toast.success(t('general.saved'));
|
||||
onUpdated(updatedData);
|
||||
})
|
||||
|
@ -169,104 +182,147 @@ function Settings() {
|
|||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField title="organization_details.mfa.title" tip={t('organization_details.mfa.tip')}>
|
||||
<Switch
|
||||
label={t('organization_details.mfa.description')}
|
||||
{...register('isMfaRequired')}
|
||||
/>
|
||||
{isMfaRequired && signInExperience?.mfa.factors.length === 0 && (
|
||||
<InlineNotification severity="alert" className={styles.warning}>
|
||||
{t('organization_details.mfa.no_mfa_warning')}
|
||||
</InlineNotification>
|
||||
)}
|
||||
</FormField>
|
||||
</FormCard>
|
||||
{isDevFeaturesEnabled && (
|
||||
<FormCard
|
||||
title="organization_details.membership_policies"
|
||||
description="organization_details.membership_policies_description"
|
||||
title="organization_details.jit.title"
|
||||
description="organization_details.jit.description"
|
||||
>
|
||||
<FormField title="organization_details.jit.title">
|
||||
<div className={styles.jitContent}>
|
||||
<Switch
|
||||
label={t('organization_details.jit.description')}
|
||||
{...register('isJitEnabled')}
|
||||
/>
|
||||
</div>
|
||||
</FormField>
|
||||
{isJitEnabled && (
|
||||
<FormField
|
||||
title="organization_details.jit.email_domains"
|
||||
description={
|
||||
<Trans
|
||||
components={{
|
||||
Icon: <SsoIcon className={styles.ssoEnabled} />,
|
||||
}}
|
||||
>
|
||||
{t('organization_details.jit.sso_email_domain_description')}
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="jitEmailDomains"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<MultiOptionInput
|
||||
values={value}
|
||||
renderValue={(value) =>
|
||||
hasSsoEnabled(value) ? (
|
||||
<>
|
||||
<SsoIcon className={styles.ssoEnabled} />
|
||||
{value}
|
||||
</>
|
||||
) : (
|
||||
value
|
||||
<FormField
|
||||
title="organization_details.jit.enterprise_sso"
|
||||
description="organization_details.jit.enterprise_sso_description"
|
||||
descriptionPosition="top"
|
||||
>
|
||||
<Controller
|
||||
name="jitSsoConnectorIds"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<div className={styles.ssoConnectorList}>
|
||||
{value.map((id) => {
|
||||
const connector = allSsoConnectors?.find(
|
||||
({ id: connectorId }) => id === connectorId
|
||||
);
|
||||
return (
|
||||
connector && (
|
||||
<div key={connector.id} className={styles.ssoConnector}>
|
||||
<div className={styles.info}>
|
||||
<SsoConnectorLogo className={styles.icon} data={connector} />
|
||||
<span>{connector.connectorName}</span>
|
||||
</div>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onChange(value.filter((value) => value !== id));
|
||||
}}
|
||||
>
|
||||
<Minus />
|
||||
</IconButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
validateInput={(input) => {
|
||||
if (!domainRegExp.test(input)) {
|
||||
return t('organization_details.jit.invalid_domain');
|
||||
}
|
||||
|
||||
if (value.includes(input)) {
|
||||
return t('organization_details.jit.domain_already_added');
|
||||
}
|
||||
|
||||
return { value: input };
|
||||
);
|
||||
})}
|
||||
<ActionMenu
|
||||
buttonProps={{
|
||||
type: 'default',
|
||||
size: 'medium',
|
||||
title: 'organization_details.jit.add_enterprise_connector',
|
||||
icon: <Plus />,
|
||||
className: styles.addSsoConnectorButton,
|
||||
}}
|
||||
placeholder={t('organization_details.jit.email_domains_placeholder')}
|
||||
error={errors.jitEmailDomains?.message}
|
||||
onChange={onChange}
|
||||
onError={(error) => {
|
||||
setError('jitEmailDomains', { type: 'custom', message: error });
|
||||
}}
|
||||
onClearError={() => {
|
||||
clearErrors('jitEmailDomains');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
{isJitEnabled && (
|
||||
<FormField
|
||||
title="organization_details.jit.organization_roles"
|
||||
description="organization_details.jit.organization_roles_description"
|
||||
descriptionPosition="top"
|
||||
>
|
||||
<Controller
|
||||
name="jitRoles"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<OrganizationRolesSelect
|
||||
keyword={keyword}
|
||||
setKeyword={setKeyword}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
<FormField title="organization_details.mfa.title" tip={t('organization_details.mfa.tip')}>
|
||||
<Switch
|
||||
label={t('organization_details.mfa.description')}
|
||||
{...register('isMfaRequired')}
|
||||
dropdownHorizontalAlign="start"
|
||||
>
|
||||
{allSsoConnectors
|
||||
?.filter(({ id }) => !value.includes(id))
|
||||
.map((connector) => (
|
||||
<DropdownItem
|
||||
key={connector.id}
|
||||
className={styles.dropdownItem}
|
||||
onClick={() => {
|
||||
onChange([...value, connector.id]);
|
||||
}}
|
||||
>
|
||||
<SsoConnectorLogo className={styles.icon} data={connector} />
|
||||
<span>{connector.connectorName}</span>
|
||||
</DropdownItem>
|
||||
))}
|
||||
</ActionMenu>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
title="organization_details.jit.email_domain"
|
||||
description="organization_details.jit.email_domain_description"
|
||||
descriptionPosition="top"
|
||||
>
|
||||
<Controller
|
||||
name="jitEmailDomains"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<MultiOptionInput
|
||||
values={value}
|
||||
renderValue={(value) =>
|
||||
hasSsoEnabled(value) ? (
|
||||
<>
|
||||
<SsoIcon className={styles.ssoEnabled} />
|
||||
{value}
|
||||
</>
|
||||
) : (
|
||||
value
|
||||
)
|
||||
}
|
||||
validateInput={(input) => {
|
||||
if (!domainRegExp.test(input)) {
|
||||
return t('organization_details.jit.invalid_domain');
|
||||
}
|
||||
|
||||
if (value.includes(input)) {
|
||||
return t('organization_details.jit.domain_already_added');
|
||||
}
|
||||
|
||||
return { value: input };
|
||||
}}
|
||||
placeholder={t('organization_details.jit.email_domain_placeholder')}
|
||||
error={errors.jitEmailDomains?.message}
|
||||
onChange={onChange}
|
||||
onError={(error) => {
|
||||
setError('jitEmailDomains', { type: 'custom', message: error });
|
||||
}}
|
||||
onClearError={() => {
|
||||
clearErrors('jitEmailDomains');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
title="organization_details.jit.organization_roles"
|
||||
description="organization_details.jit.organization_roles_description"
|
||||
descriptionPosition="top"
|
||||
>
|
||||
<Controller
|
||||
name="jitRoles"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<OrganizationRolesSelect
|
||||
keyword={keyword}
|
||||
setKeyword={setKeyword}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{isMfaRequired && signInExperience?.mfa.factors.length === 0 && (
|
||||
<InlineNotification severity="alert" className={styles.warning}>
|
||||
{t('organization_details.mfa.no_mfa_warning')}
|
||||
</InlineNotification>
|
||||
)}
|
||||
</FormField>
|
||||
</FormCard>
|
||||
)}
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
type OrganizationJitEmailDomain,
|
||||
type Organization,
|
||||
type OrganizationRole,
|
||||
type SsoConnector,
|
||||
} from '@logto/schemas';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -30,6 +31,7 @@ import { OrganizationDetailsTabs, type OrganizationDetailsOutletContext } from '
|
|||
|
||||
const pathname = '/organizations';
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
function OrganizationDetails() {
|
||||
const { id } = useParams();
|
||||
const { navigate } = useTenantPathname();
|
||||
|
@ -41,6 +43,9 @@ function OrganizationDetails() {
|
|||
const jitRoles = useSWR<OrganizationRole[], RequestError>(
|
||||
id && `api/organizations/${id}/jit/roles`
|
||||
);
|
||||
const jitSsoConnectors = useSWR<SsoConnector[], RequestError>(
|
||||
id && `api/organizations/${id}/jit/sso-connectors`
|
||||
);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isGuideDrawerOpen, setIsGuideDrawerOpen] = useState(false);
|
||||
const [isDeleteFormOpen, setIsDeleteFormOpen] = useState(false);
|
||||
|
@ -63,81 +68,88 @@ function OrganizationDetails() {
|
|||
const isLoading =
|
||||
(!organization.data && !organization.error) ||
|
||||
(!jitEmailDomains.data && !jitEmailDomains.error) ||
|
||||
(!jitRoles.data && !jitRoles.error);
|
||||
const error = organization.error ?? jitEmailDomains.error ?? jitRoles.error;
|
||||
(!jitRoles.data && !jitRoles.error) ||
|
||||
(!jitSsoConnectors.data && !jitSsoConnectors.error);
|
||||
const error =
|
||||
organization.error ?? jitEmailDomains.error ?? jitRoles.error ?? jitSsoConnectors.error;
|
||||
|
||||
return (
|
||||
<DetailsPage backLink={pathname} backLinkTitle="organizations.title" className={styles.page}>
|
||||
<PageMeta titleKey="organization_details.page_title" />
|
||||
{isLoading && <Skeleton />}
|
||||
{error && <AppError errorCode={error.body?.code} errorMessage={error.body?.message} />}
|
||||
{id && organization.data && jitEmailDomains.data && jitRoles.data && (
|
||||
<>
|
||||
<DetailsPageHeader
|
||||
icon={<ThemedIcon for={OrganizationIcon} size={60} />}
|
||||
title={organization.data.name}
|
||||
identifier={{ name: t('organization_details.organization_id'), value: id }}
|
||||
additionalActionButton={{
|
||||
icon: <File />,
|
||||
title: 'application_details.check_guide',
|
||||
onClick: () => {
|
||||
setIsGuideDrawerOpen(true);
|
||||
},
|
||||
}}
|
||||
actionMenuItems={[
|
||||
{
|
||||
icon: <Delete />,
|
||||
title: 'general.delete',
|
||||
type: 'danger',
|
||||
{id &&
|
||||
organization.data &&
|
||||
jitEmailDomains.data &&
|
||||
jitRoles.data &&
|
||||
jitSsoConnectors.data && (
|
||||
<>
|
||||
<DetailsPageHeader
|
||||
icon={<ThemedIcon for={OrganizationIcon} size={60} />}
|
||||
title={organization.data.name}
|
||||
identifier={{ name: t('organization_details.organization_id'), value: id }}
|
||||
additionalActionButton={{
|
||||
icon: <File />,
|
||||
title: 'application_details.check_guide',
|
||||
onClick: () => {
|
||||
setIsDeleteFormOpen(true);
|
||||
setIsGuideDrawerOpen(true);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Drawer
|
||||
title="organizations.guide.title"
|
||||
subtitle="organizations.guide.subtitle"
|
||||
isOpen={isGuideDrawerOpen}
|
||||
onClose={() => {
|
||||
setIsGuideDrawerOpen(false);
|
||||
}}
|
||||
>
|
||||
<Introduction />
|
||||
</Drawer>
|
||||
<DeleteConfirmModal
|
||||
isOpen={isDeleteFormOpen}
|
||||
isLoading={isDeleting}
|
||||
onCancel={() => {
|
||||
setIsDeleteFormOpen(false);
|
||||
}}
|
||||
onConfirm={deleteOrganization}
|
||||
>
|
||||
{t('organization_details.delete_confirmation')}
|
||||
</DeleteConfirmModal>
|
||||
<TabNav>
|
||||
<TabNavItem href={`${pathname}/${id}/${OrganizationDetailsTabs.Settings}`}>
|
||||
{t('general.settings_nav')}
|
||||
</TabNavItem>
|
||||
<TabNavItem href={`${pathname}/${id}/${OrganizationDetailsTabs.Members}`}>
|
||||
{t('organizations.members')}
|
||||
</TabNavItem>
|
||||
</TabNav>
|
||||
<Outlet
|
||||
context={
|
||||
{
|
||||
data: organization.data,
|
||||
jit: {
|
||||
emailDomains: jitEmailDomains.data,
|
||||
roles: jitRoles.data,
|
||||
}}
|
||||
actionMenuItems={[
|
||||
{
|
||||
icon: <Delete />,
|
||||
title: 'general.delete',
|
||||
type: 'danger',
|
||||
onClick: () => {
|
||||
setIsDeleteFormOpen(true);
|
||||
},
|
||||
},
|
||||
isDeleting,
|
||||
onUpdated: async (data) => organization.mutate(data),
|
||||
} satisfies OrganizationDetailsOutletContext
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
]}
|
||||
/>
|
||||
<Drawer
|
||||
title="organizations.guide.title"
|
||||
subtitle="organizations.guide.subtitle"
|
||||
isOpen={isGuideDrawerOpen}
|
||||
onClose={() => {
|
||||
setIsGuideDrawerOpen(false);
|
||||
}}
|
||||
>
|
||||
<Introduction />
|
||||
</Drawer>
|
||||
<DeleteConfirmModal
|
||||
isOpen={isDeleteFormOpen}
|
||||
isLoading={isDeleting}
|
||||
onCancel={() => {
|
||||
setIsDeleteFormOpen(false);
|
||||
}}
|
||||
onConfirm={deleteOrganization}
|
||||
>
|
||||
{t('organization_details.delete_confirmation')}
|
||||
</DeleteConfirmModal>
|
||||
<TabNav>
|
||||
<TabNavItem href={`${pathname}/${id}/${OrganizationDetailsTabs.Settings}`}>
|
||||
{t('general.settings_nav')}
|
||||
</TabNavItem>
|
||||
<TabNavItem href={`${pathname}/${id}/${OrganizationDetailsTabs.Members}`}>
|
||||
{t('organizations.members')}
|
||||
</TabNavItem>
|
||||
</TabNav>
|
||||
<Outlet
|
||||
context={
|
||||
{
|
||||
data: organization.data,
|
||||
jit: {
|
||||
emailDomains: jitEmailDomains.data,
|
||||
roles: jitRoles.data,
|
||||
ssoConnectorIds: jitSsoConnectors.data.map(({ id }) => id),
|
||||
},
|
||||
isDeleting,
|
||||
onUpdated: async (data) => organization.mutate(data),
|
||||
} satisfies OrganizationDetailsOutletContext
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DetailsPage>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ export type OrganizationDetailsOutletContext = {
|
|||
data: Organization;
|
||||
jit: {
|
||||
emailDomains: OrganizationJitEmailDomain[];
|
||||
ssoConnectorIds: string[];
|
||||
roles: OrganizationRole[];
|
||||
};
|
||||
/**
|
||||
|
|
|
@ -26,19 +26,20 @@ const organization_details = {
|
|||
custom_data_tip:
|
||||
'Custom data is a JSON object that can be used to store additional data associated with the organization.',
|
||||
invalid_json_object: 'Invalid JSON object.',
|
||||
membership_policies: 'Membership policies',
|
||||
membership_policies_description:
|
||||
'Define how users can join this organization and what requirements they must meet for access.',
|
||||
jit: {
|
||||
title: 'Enable just-in-time provisioning',
|
||||
title: 'Just-in-time provisioning',
|
||||
description:
|
||||
'Users can automatically join the organization and receive role assignments if their verified email matches specific domains, either during sign-up or when added via the Management API.',
|
||||
email_domains: 'JIT provisioning email domains',
|
||||
email_domains_placeholder: 'Enter email domains for just-in-time provisioning',
|
||||
email_domain: 'Email domain provisioning',
|
||||
email_domain_description:
|
||||
'New users signing up with their verified email addresses or through social sign-in with synced verified email addresses will automatically join the organization.',
|
||||
email_domain_placeholder: 'Enter email domains for just-in-time provisioning',
|
||||
invalid_domain: 'Invalid domain',
|
||||
sso_email_domain_description:
|
||||
'<Icon /> means the domain is enabled for enterprise SSO. Users who signed in through the configured IdP can automatically join the organization.',
|
||||
domain_already_added: 'Domain already added',
|
||||
enterprise_sso: 'Enterprise SSO provisioning',
|
||||
add_enterprise_connector: 'Add enterprise connector',
|
||||
enterprise_sso_description:
|
||||
'New or existing users signing in through enterprise SSO for the first time will automatically join the organization.',
|
||||
organization_roles: 'Default organization roles',
|
||||
organization_roles_description:
|
||||
'Assign roles to users upon joining the organization through just-in-time provisioning.',
|
||||
|
|
Loading…
Add table
Reference in a new issue