mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
refactor: improve code, content, and styles
This commit is contained in:
parent
a471d6a58c
commit
71f5fa308f
5 changed files with 80 additions and 46 deletions
|
@ -13,6 +13,7 @@ type CanBePromise<T> = T | Promise<T>;
|
|||
|
||||
type Props<T> = {
|
||||
readonly className?: string;
|
||||
readonly valueClassName?: string | ((value: T) => string | undefined);
|
||||
readonly values: T[];
|
||||
readonly getId?: (value: T) => string;
|
||||
readonly onError?: (error: string) => void;
|
||||
|
@ -27,6 +28,7 @@ type Props<T> = {
|
|||
|
||||
function MultiOptionInput<T>({
|
||||
className,
|
||||
valueClassName,
|
||||
values,
|
||||
getId: getIdInput,
|
||||
onError,
|
||||
|
@ -98,7 +100,11 @@ function MultiOptionInput<T>({
|
|||
<Tag
|
||||
key={getId(option)}
|
||||
variant="cell"
|
||||
className={classNames(styles.tag, getId(option) === focusedValueId && styles.focused)}
|
||||
className={classNames(
|
||||
styles.tag,
|
||||
getId(option) === focusedValueId && styles.focused,
|
||||
typeof valueClassName === 'function' ? valueClassName(option) : valueClassName
|
||||
)}
|
||||
onClick={() => {
|
||||
ref.current?.focus();
|
||||
}}
|
||||
|
|
|
@ -4,11 +4,19 @@
|
|||
margin-top: _.unit(3);
|
||||
}
|
||||
|
||||
.ssoEnabled {
|
||||
vertical-align: middle;
|
||||
color: var(--color-text-link);
|
||||
// Additional layer of nesting to override the styles from the imported components
|
||||
.jitEmailDomains .ssoEnabled {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: _.unit(1);
|
||||
background-color: var(--color-alert-container);
|
||||
|
||||
> svg {
|
||||
color: var(--color-on-alert-container);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.warning {
|
||||
margin-top: _.unit(3);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import {
|
|||
type Organization,
|
||||
type SsoConnectorWithProviderConfig,
|
||||
} from '@logto/schemas';
|
||||
import { trySafe } from '@silverhand/essentials';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
@ -27,7 +26,6 @@ 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';
|
||||
|
@ -38,40 +36,7 @@ import { trySubmitSafe } from '@/utils/form';
|
|||
import { type OrganizationDetailsOutletContext } from '../types';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type FormData = Partial<Omit<Organization, 'customData'> & { customData: string }> & {
|
||||
jitEmailDomains: string[];
|
||||
jitRoles: Array<Option<string>>;
|
||||
jitSsoConnectorIds: string[];
|
||||
};
|
||||
|
||||
const isJsonObject = (value: string) => {
|
||||
const parsed = trySafe<unknown>(() => JSON.parse(value));
|
||||
return Boolean(parsed && typeof parsed === 'object');
|
||||
};
|
||||
|
||||
const normalizeData = (
|
||||
data: Organization,
|
||||
jit: { emailDomains: string[]; roles: Array<Option<string>>; ssoConnectorIds: string[] }
|
||||
): FormData => ({
|
||||
...data,
|
||||
jitEmailDomains: jit.emailDomains,
|
||||
jitRoles: jit.roles,
|
||||
jitSsoConnectorIds: jit.ssoConnectorIds,
|
||||
customData: JSON.stringify(data.customData, undefined, 2),
|
||||
});
|
||||
|
||||
const assembleData = ({
|
||||
jitEmailDomains,
|
||||
jitRoles,
|
||||
jitSsoConnectorIds,
|
||||
customData,
|
||||
...data
|
||||
}: FormData): Partial<Organization> => ({
|
||||
...data,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
customData: JSON.parse(customData ?? '{}'),
|
||||
});
|
||||
import { assembleData, isJsonObject, normalizeData, type FormData } from './utils';
|
||||
|
||||
function Settings() {
|
||||
const { isDeleting, data, jit, onUpdated } = useOutletContext<OrganizationDetailsOutletContext>();
|
||||
|
@ -93,7 +58,7 @@ function Settings() {
|
|||
ssoConnectorIds: jit.ssoConnectorIds,
|
||||
}),
|
||||
});
|
||||
const isMfaRequired = watch('isMfaRequired');
|
||||
const [isMfaRequired, emailDomains] = watch(['isMfaRequired', 'jitEmailDomains']);
|
||||
const api = useApi();
|
||||
const [keyword, setKeyword] = useState('');
|
||||
// Fetch all SSO connector to show if a domain is configured SSO
|
||||
|
@ -108,6 +73,11 @@ function Settings() {
|
|||
(domain: string) => allSsoConnectors?.some(({ domains }) => domains.includes(domain)),
|
||||
[allSsoConnectors]
|
||||
);
|
||||
/** If any of the email domains has SSO enabled. */
|
||||
const hasSsoEnabledEmailDomain = useMemo(
|
||||
() => emailDomains.some((domain) => hasSsoEnabled(domain)),
|
||||
[emailDomains, hasSsoEnabled]
|
||||
);
|
||||
|
||||
const onSubmit = handleSubmit(
|
||||
trySubmitSafe(async (data) => {
|
||||
|
@ -218,7 +188,9 @@ function Settings() {
|
|||
<div key={connector.id} className={styles.ssoConnector}>
|
||||
<div className={styles.info}>
|
||||
<SsoConnectorLogo className={styles.icon} data={connector} />
|
||||
<span>{connector.connectorName}</span>
|
||||
<span>
|
||||
{connector.connectorName} - {connector.providerName}
|
||||
</span>
|
||||
</div>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
|
@ -264,6 +236,7 @@ function Settings() {
|
|||
title="organization_details.jit.email_domain"
|
||||
description="organization_details.jit.email_domain_description"
|
||||
descriptionPosition="top"
|
||||
className={styles.jitEmailDomains}
|
||||
>
|
||||
<Controller
|
||||
name="jitEmailDomains"
|
||||
|
@ -271,10 +244,13 @@ function Settings() {
|
|||
render={({ field: { onChange, value } }) => (
|
||||
<MultiOptionInput
|
||||
values={value}
|
||||
valueClassName={(domain) =>
|
||||
hasSsoEnabled(domain) ? styles.ssoEnabled : undefined
|
||||
}
|
||||
renderValue={(value) =>
|
||||
hasSsoEnabled(value) ? (
|
||||
<>
|
||||
<SsoIcon className={styles.ssoEnabled} />
|
||||
<SsoIcon />
|
||||
{value}
|
||||
</>
|
||||
) : (
|
||||
|
@ -304,8 +280,12 @@ function Settings() {
|
|||
/>
|
||||
)}
|
||||
/>
|
||||
{hasSsoEnabledEmailDomain && (
|
||||
<InlineNotification severity="alert" className={styles.warning}>
|
||||
{t('organization_details.jit.sso_enabled_domain_warning')}
|
||||
</InlineNotification>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
title="organization_details.jit.organization_roles"
|
||||
description="organization_details.jit.organization_roles_description"
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { type Organization } from '@logto/schemas';
|
||||
import { trySafe } from '@silverhand/essentials';
|
||||
|
||||
import { type Option } from '@/ds-components/Select/MultiSelect';
|
||||
|
||||
export type FormData = Partial<Omit<Organization, 'customData'> & { customData: string }> & {
|
||||
jitEmailDomains: string[];
|
||||
jitRoles: Array<Option<string>>;
|
||||
jitSsoConnectorIds: string[];
|
||||
};
|
||||
|
||||
export const isJsonObject = (value: string) => {
|
||||
const parsed = trySafe<unknown>(() => JSON.parse(value));
|
||||
return Boolean(parsed && typeof parsed === 'object');
|
||||
};
|
||||
|
||||
export const normalizeData = (
|
||||
data: Organization,
|
||||
jit: { emailDomains: string[]; roles: Array<Option<string>>; ssoConnectorIds: string[] }
|
||||
): FormData => ({
|
||||
...data,
|
||||
jitEmailDomains: jit.emailDomains,
|
||||
jitRoles: jit.roles,
|
||||
jitSsoConnectorIds: jit.ssoConnectorIds,
|
||||
customData: JSON.stringify(data.customData, undefined, 2),
|
||||
});
|
||||
|
||||
export const assembleData = ({
|
||||
jitEmailDomains,
|
||||
jitRoles,
|
||||
jitSsoConnectorIds,
|
||||
customData,
|
||||
...data
|
||||
}: FormData): Partial<Organization> => ({
|
||||
...data,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
customData: JSON.parse(customData ?? '{}'),
|
||||
});
|
|
@ -29,13 +29,15 @@ const organization_details = {
|
|||
jit: {
|
||||
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.',
|
||||
'Users can automatically join the organization and be assigned roles upon their first sign-in through some authentication methods. You can set requirements to meet 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.',
|
||||
'New users signing up with their verified email addresses or through social sign-in with verified email addresses will automatically join the organization.',
|
||||
email_domain_placeholder: 'Enter email domains for just-in-time provisioning',
|
||||
invalid_domain: 'Invalid domain',
|
||||
domain_already_added: 'Domain already added',
|
||||
sso_enabled_domain_warning:
|
||||
'You have entered one or more email domains associated to enterprise SSO. Users with these emails will follow the standard SSO flow and won’t be provisioned to this organization unless enterprise SSO provisioning is configured.',
|
||||
enterprise_sso: 'Enterprise SSO provisioning',
|
||||
add_enterprise_connector: 'Add enterprise connector',
|
||||
enterprise_sso_description:
|
||||
|
|
Loading…
Reference in a new issue