mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
refactor(console): polish ui (#6122)
* refactor(console): polish ui * refactor: fix code editor title color
This commit is contained in:
parent
07e3725740
commit
f8f84f5d75
12 changed files with 304 additions and 205 deletions
|
@ -1,5 +1,5 @@
|
|||
import { type OrganizationWithRoles } from '@logto/schemas';
|
||||
import { useState } from 'react';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
|
@ -23,9 +23,11 @@ import * as styles from './index.module.scss';
|
|||
type Props = {
|
||||
readonly type: 'user' | 'application';
|
||||
readonly data: { id: string };
|
||||
/** Placeholder to show when there is no data. */
|
||||
readonly placeholder?: ReactNode;
|
||||
};
|
||||
|
||||
function OrganizationList({ type, data: { id } }: Props) {
|
||||
function OrganizationList({ type, data: { id }, placeholder }: Props) {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { getPathname } = useTenantPathname();
|
||||
|
@ -44,7 +46,7 @@ function OrganizationList({ type, data: { id } }: Props) {
|
|||
isLoading={isLoading}
|
||||
rowIndexKey="id"
|
||||
rowGroups={[{ key: 'data', data }]}
|
||||
placeholder={<EmptyDataPlaceholder />}
|
||||
placeholder={placeholder ?? <EmptyDataPlaceholder />}
|
||||
columns={[
|
||||
{
|
||||
title: t('general.name'),
|
||||
|
|
|
@ -97,7 +97,7 @@ function OrganizationRolePermissionsAssignmentModal({
|
|||
subtitle="organization_role_details.permissions.assign_description"
|
||||
confirmButtonType="primary"
|
||||
confirmButtonText="general.save"
|
||||
cancelButtonText="general.discard"
|
||||
cancelButtonText="general.skip"
|
||||
size="large"
|
||||
onCancel={onCloseHandler}
|
||||
onConfirm={onSubmitHandler}
|
||||
|
|
|
@ -27,3 +27,8 @@ export const organizationRoleLink =
|
|||
export const organizationPermissionLink =
|
||||
'/docs/recipes/organizations/understand-how-it-works/#organization-permission';
|
||||
export const profilePropertyReferenceLink = '/docs/references/users/#profile-1';
|
||||
export const organizationJit = Object.freeze({
|
||||
enterpriseSso:
|
||||
'/docs/recipes/organizations/just-in-time-provisioning/#enterprise-sso-provisioning',
|
||||
emailDomain: '/docs/recipes/organizations/just-in-time-provisioning/#email-domain-provisioning',
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
padding-bottom: _.unit(2);
|
||||
margin-bottom: _.unit(3);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
color: #f7f8f8;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
|
|
|
@ -5,7 +5,6 @@ import { type RouteObject } from 'react-router-dom';
|
|||
import { isCloud } from '@/consts/env';
|
||||
import Dashboard from '@/pages/Dashboard';
|
||||
import GetStarted from '@/pages/GetStarted';
|
||||
import Mfa from '@/pages/Mfa';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
import SigningKeys from '@/pages/SigningKeys';
|
||||
|
||||
|
@ -15,6 +14,7 @@ import { auditLogs } from './routes/audit-logs';
|
|||
import { connectors } from './routes/connectors';
|
||||
import { customizeJwt } from './routes/customize-jwt';
|
||||
import { enterpriseSso } from './routes/enterprise-sso';
|
||||
import { mfa } from './routes/mfa';
|
||||
import { organizationTemplate } from './routes/organization-template';
|
||||
import { organizations } from './routes/organizations';
|
||||
import { roles } from './routes/roles';
|
||||
|
@ -35,7 +35,7 @@ export const useConsoleRoutes = () => {
|
|||
applications,
|
||||
apiResources,
|
||||
signInExperience,
|
||||
{ path: 'mfa', element: <Mfa /> },
|
||||
mfa,
|
||||
connectors,
|
||||
enterpriseSso,
|
||||
webhooks,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { type RouteObject } from 'react-router-dom';
|
||||
|
||||
import Mfa from '@/pages/Mfa';
|
||||
|
||||
export const mfa: RouteObject = { path: 'mfa', element: <Mfa /> };
|
|
@ -15,6 +15,7 @@ import ApplicationIcon from '@/components/ApplicationIcon';
|
|||
import DetailsForm from '@/components/DetailsForm';
|
||||
import DetailsPageHeader from '@/components/DetailsPage/DetailsPageHeader';
|
||||
import Drawer from '@/components/Drawer';
|
||||
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
|
||||
import OrganizationList from '@/components/OrganizationList';
|
||||
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||
import { ApplicationDetailsTabs, logtoThirdPartyGuideLink, protectedAppLink } from '@/consts';
|
||||
|
@ -22,7 +23,9 @@ import { isDevFeaturesEnabled } from '@/consts/env';
|
|||
import DeleteConfirmModal from '@/ds-components/DeleteConfirmModal';
|
||||
import TabNav, { TabNavItem } from '@/ds-components/TabNav';
|
||||
import TabWrapper from '@/ds-components/TabWrapper';
|
||||
import TextLink from '@/ds-components/TextLink';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { organizations } from '@/hooks/use-console-routes/routes/organizations';
|
||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import { applicationTypeI18nKey } from '@/types/applications';
|
||||
|
@ -237,7 +240,20 @@ function ApplicationDetailsContent({ data, oidcConfig, onApplicationUpdated }: P
|
|||
isActive={tab === ApplicationDetailsTabs.Organizations}
|
||||
className={styles.tabContainer}
|
||||
>
|
||||
<OrganizationList type="application" data={data} />
|
||||
<OrganizationList
|
||||
type="application"
|
||||
data={data}
|
||||
placeholder={
|
||||
<EmptyDataPlaceholder
|
||||
title={
|
||||
<Trans
|
||||
i18nKey="admin_console.application_details.no_organization_placeholder"
|
||||
components={{ a: <TextLink to={'/' + organizations.path} /> }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</TabWrapper>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
import { RoleType, type SsoConnectorWithProviderConfig } from '@logto/schemas';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Controller, type UseFormReturn } from 'react-hook-form';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
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 FormCard from '@/components/FormCard';
|
||||
import MultiOptionInput from '@/components/MultiOptionInput';
|
||||
import OrganizationRolesSelect from '@/components/OrganizationRolesSelect';
|
||||
import { organizationJit } from '@/consts';
|
||||
import ActionMenu from '@/ds-components/ActionMenu';
|
||||
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 TextLink from '@/ds-components/TextLink';
|
||||
import { enterpriseSso } from '@/hooks/use-console-routes/routes/enterprise-sso';
|
||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||
import SsoConnectorLogo from '@/pages/EnterpriseSso/SsoConnectorLogo';
|
||||
import { domainRegExp } from '@/pages/EnterpriseSsoDetails/Experience/DomainsInput/consts';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
import { type FormData } from './utils';
|
||||
|
||||
type Props = {
|
||||
readonly form: UseFormReturn<FormData>;
|
||||
};
|
||||
|
||||
function JitSettings({ form }: Props) {
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
setError,
|
||||
clearErrors,
|
||||
watch,
|
||||
} = form;
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const [emailDomains, ssoConnectorIds] = watch(['jitEmailDomains', 'jitSsoConnectorIds']);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
// Fetch all SSO connector to show if a domain is configured SSO
|
||||
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) => 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 { getDocumentationUrl } = useDocumentationUrl();
|
||||
|
||||
return (
|
||||
<FormCard
|
||||
title="organization_details.jit.title"
|
||||
description="organization_details.jit.description"
|
||||
>
|
||||
<FormField
|
||||
title="organization_details.jit.enterprise_sso"
|
||||
description={
|
||||
<Trans
|
||||
i18nKey="admin_console.organization_details.jit.enterprise_sso_description"
|
||||
components={{
|
||||
a: (
|
||||
<TextLink
|
||||
to={getDocumentationUrl(organizationJit.enterpriseSso)}
|
||||
targetBlank="noopener"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
descriptionPosition="top"
|
||||
>
|
||||
{ssoConnectorIds.length === 0 && (
|
||||
<InlineNotification>
|
||||
<Trans
|
||||
i18nKey="admin_console.organization_details.jit.no_enterprise_connector_set"
|
||||
components={{ a: <TextLink to={'/' + enterpriseSso.path} /> }}
|
||||
/>
|
||||
</InlineNotification>
|
||||
)}
|
||||
{ssoConnectorIds.length > 0 && (
|
||||
<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} - {connector.providerName}
|
||||
</span>
|
||||
</div>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onChange(value.filter((value) => value !== id));
|
||||
}}
|
||||
>
|
||||
<Minus />
|
||||
</IconButton>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
<ActionMenu
|
||||
buttonProps={{
|
||||
type: 'default',
|
||||
size: 'medium',
|
||||
title: 'organization_details.jit.add_enterprise_connector',
|
||||
icon: <Plus />,
|
||||
className: styles.addSsoConnectorButton,
|
||||
}}
|
||||
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={
|
||||
<Trans
|
||||
i18nKey="admin_console.organization_details.jit.email_domain_description"
|
||||
components={{
|
||||
a: (
|
||||
<TextLink
|
||||
to={getDocumentationUrl(organizationJit.emailDomain)}
|
||||
targetBlank="noopener"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
}
|
||||
descriptionPosition="top"
|
||||
className={styles.jitEmailDomains}
|
||||
>
|
||||
<Controller
|
||||
name="jitEmailDomains"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<MultiOptionInput
|
||||
values={value}
|
||||
valueClassName={(domain) => (hasSsoEnabled(domain) ? styles.ssoEnabled : undefined)}
|
||||
renderValue={(value) =>
|
||||
hasSsoEnabled(value) ? (
|
||||
<>
|
||||
<SsoIcon />
|
||||
{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');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{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"
|
||||
descriptionPosition="top"
|
||||
>
|
||||
<Controller
|
||||
name="jitRoles"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<OrganizationRolesSelect
|
||||
roleType={RoleType.User}
|
||||
keyword={keyword}
|
||||
setKeyword={setKeyword}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
</FormCard>
|
||||
);
|
||||
}
|
||||
|
||||
export default JitSettings;
|
|
@ -1,41 +1,27 @@
|
|||
import {
|
||||
type SignInExperience,
|
||||
type Organization,
|
||||
type SsoConnectorWithProviderConfig,
|
||||
RoleType,
|
||||
} from '@logto/schemas';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { type SignInExperience, type Organization } from '@logto/schemas';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Trans, 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';
|
||||
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 Switch from '@/ds-components/Switch';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
import TextLink from '@/ds-components/TextLink';
|
||||
import useApi, { type RequestError } from '@/hooks/use-api';
|
||||
import SsoConnectorLogo from '@/pages/EnterpriseSso/SsoConnectorLogo';
|
||||
import { domainRegExp } from '@/pages/EnterpriseSsoDetails/Experience/DomainsInput/consts';
|
||||
import { mfa } from '@/hooks/use-console-routes/routes/mfa';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import { type OrganizationDetailsOutletContext } from '../types';
|
||||
|
||||
import JitSettings from './JitSettings';
|
||||
import * as styles from './index.module.scss';
|
||||
import { assembleData, isJsonObject, normalizeData, type FormData } from './utils';
|
||||
|
||||
|
@ -43,42 +29,23 @@ function Settings() {
|
|||
const { isDeleting, data, jit, onUpdated } = useOutletContext<OrganizationDetailsOutletContext>();
|
||||
const { data: signInExperience } = useSWR<SignInExperience, RequestError>('api/sign-in-exp');
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
register,
|
||||
reset,
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isDirty, isSubmitting, errors },
|
||||
setError,
|
||||
clearErrors,
|
||||
watch,
|
||||
} = useForm<FormData>({
|
||||
const form = useForm<FormData>({
|
||||
defaultValues: normalizeData(data, {
|
||||
emailDomains: jit.emailDomains.map(({ emailDomain }) => emailDomain),
|
||||
roles: jit.roles.map(({ id, name }) => ({ value: id, title: name })),
|
||||
ssoConnectorIds: jit.ssoConnectorIds,
|
||||
}),
|
||||
});
|
||||
const [isMfaRequired, emailDomains] = watch(['isMfaRequired', 'jitEmailDomains']);
|
||||
const {
|
||||
register,
|
||||
reset,
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isDirty, isSubmitting, errors },
|
||||
watch,
|
||||
} = form;
|
||||
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: 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) => 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) => {
|
||||
|
@ -160,159 +127,20 @@ function Settings() {
|
|||
/>
|
||||
{isMfaRequired && signInExperience?.mfa.factors.length === 0 && (
|
||||
<InlineNotification severity="alert" className={styles.warning}>
|
||||
{t('organization_details.mfa.no_mfa_warning')}
|
||||
<Trans
|
||||
i18nKey="admin_console.organization_details.mfa.no_mfa_warning"
|
||||
components={{
|
||||
a: <TextLink to={'/' + mfa.path} />,
|
||||
}}
|
||||
/>
|
||||
</InlineNotification>
|
||||
)}
|
||||
</FormField>
|
||||
</FormCard>
|
||||
{isDevFeaturesEnabled && (
|
||||
<FormCard
|
||||
title="organization_details.jit.title"
|
||||
description="organization_details.jit.description"
|
||||
>
|
||||
<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} - {connector.providerName}
|
||||
</span>
|
||||
</div>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onChange(value.filter((value) => value !== id));
|
||||
}}
|
||||
>
|
||||
<Minus />
|
||||
</IconButton>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
<ActionMenu
|
||||
buttonProps={{
|
||||
type: 'default',
|
||||
size: 'medium',
|
||||
title: 'organization_details.jit.add_enterprise_connector',
|
||||
icon: <Plus />,
|
||||
className: styles.addSsoConnectorButton,
|
||||
}}
|
||||
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"
|
||||
className={styles.jitEmailDomains}
|
||||
>
|
||||
<Controller
|
||||
name="jitEmailDomains"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<MultiOptionInput
|
||||
values={value}
|
||||
valueClassName={(domain) =>
|
||||
hasSsoEnabled(domain) ? styles.ssoEnabled : undefined
|
||||
}
|
||||
renderValue={(value) =>
|
||||
hasSsoEnabled(value) ? (
|
||||
<>
|
||||
<SsoIcon />
|
||||
{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');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{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"
|
||||
descriptionPosition="top"
|
||||
>
|
||||
<Controller
|
||||
name="jitRoles"
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<OrganizationRolesSelect
|
||||
roleType={RoleType.User}
|
||||
keyword={keyword}
|
||||
setKeyword={setKeyword}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
</FormCard>
|
||||
)}
|
||||
{isDevFeaturesEnabled && <JitSettings form={form} />}
|
||||
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleting && isDirty} />
|
||||
</DetailsForm>
|
||||
);
|
||||
|
||||
// eslint-disable-next-line max-lines -- Should be ok once dev features flag is removed
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
|
|
|
@ -72,7 +72,7 @@ export default class ExpectOrganizations extends ExpectConsole {
|
|||
|
||||
// Skip permission assignment
|
||||
await this.toExpectModal('Assign permissions');
|
||||
await this.toClickButton('Discard');
|
||||
await this.toClickButton('Skip');
|
||||
|
||||
this.toMatchUrl(/\/organization-template\/organization-roles\/.+$/);
|
||||
}
|
||||
|
|
|
@ -92,6 +92,7 @@ const application_details = {
|
|||
'Ensure to protect your origin server from direct access. Refer to the guide for more <a>detailed instructions</a>.',
|
||||
session_duration: 'Session duration (days)',
|
||||
try_it: 'Try it',
|
||||
no_organization_placeholder: 'No organization found. <a>Go to organizations</a>',
|
||||
branding: {
|
||||
name: 'Branding',
|
||||
description: "Customize your application's display name and logo on the consent screen.",
|
||||
|
|
|
@ -44,16 +44,18 @@ const organization_details = {
|
|||
'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 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. <a>Learn more</a>',
|
||||
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',
|
||||
no_enterprise_connector_set:
|
||||
'You haven’t set up any enterprise SSO connector yet. Add connectors first to enable enterprise SSO provisioning. <a>Set up</a>',
|
||||
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.',
|
||||
'New users or existing users signing in through enterprise SSO for the first time will automatically join the organization. <a>Learn more</a>',
|
||||
organization_roles: 'Default organization roles',
|
||||
organization_roles_description:
|
||||
'Assign roles to users upon joining the organization through just-in-time provisioning.',
|
||||
|
@ -64,7 +66,7 @@ const organization_details = {
|
|||
description:
|
||||
'Require users to configure multi-factor authentication to access this organization.',
|
||||
no_mfa_warning:
|
||||
'No multi-factor authentication methods are enabled for your tenant. Users will not be able to access this organization until at least one multi-factor authentication method is enabled.',
|
||||
'No multi-factor authentication methods are enabled for your tenant. Users will not be able to access this organization until at least one <a>multi-factor authentication method</a> is enabled.',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue