0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

Merge pull request #4845 from logto-io/yemq-log-7186-sso-connector-creation-modal

feat(console,phrases): add SSO connector creation modal
This commit is contained in:
Darcy Ye 2023-11-13 11:56:38 +08:00 committed by GitHub
commit ee62015f42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 515 additions and 3 deletions

View file

@ -5,10 +5,14 @@ import * as radioGroupStyles from '../ConnectorRadioGroup/index.module.scss';
import * as styles from './index.module.scss';
function Skeleton() {
type Props = {
numberOfLoadingConnectors?: number;
};
function Skeleton({ numberOfLoadingConnectors = 8 }: Props) {
return (
<div className={radioGroupStyles.connectorGroup}>
{Array.from({ length: 8 }).map((_, index) => (
{Array.from({ length: numberOfLoadingConnectors }).map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
<div key={index} className={classNames(radioStyles.connector, styles.connector)}>
<div className={styles.logo} />

View file

@ -116,7 +116,10 @@ function ConsoleContent() {
<Route path=":tab/:connectorId" element={<ConnectorDetails />} />
</Route>
{isDevFeaturesEnabled && (
<Route path="enterprise-sso" element={<EnterpriseSsoConnectors />} />
<Route path="enterprise-sso">
<Route index element={<EnterpriseSsoConnectors />} />
<Route path="create" element={<EnterpriseSsoConnectors />} />
</Route>
)}
<Route path="webhooks">
<Route index element={<Webhooks />} />

View file

@ -0,0 +1,46 @@
@use '@/scss/underscore' as _;
.ssoConnector {
font: var(--font-body-2);
display: flex;
.content {
flex: 1;
margin-left: _.unit(3);
.name {
font: var(--font-label-2);
@include _.multi-line-ellipsis(1);
padding-right: _.unit(3);
}
.description {
font: var(--font-body-3);
color: var(--color-text-secondary);
margin-top: _.unit(1);
@include _.multi-line-ellipsis(4);
}
}
}
.container {
width: 40px;
height: 40px;
border-radius: 8px;
background-color: var(--color-hover);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
> img {
width: 28px;
height: 28px;
}
}
.logo {
width: 40px;
height: 40px;
flex-shrink: 0;
}

View file

@ -0,0 +1,33 @@
import { type SsoConnectorFactoryDetail } from '@logto/schemas';
import classNames from 'classnames';
import ImageWithErrorFallback from '@/ds-components/ImageWithErrorFallback';
import * as styles from './index.module.scss';
type Props = {
data: SsoConnectorFactoryDetail;
};
function SsoConnectorRadio({ data: { providerName, logo, description } }: Props) {
return (
<div className={styles.ssoConnector}>
<ImageWithErrorFallback
containerClassName={styles.container}
className={styles.logo}
alt="logo"
src={logo}
/>
<div className={styles.content}>
<div className={classNames(styles.name)}>
<span>{providerName}</span>
</div>
<div className={styles.description}>
<span>{description}</span>
</div>
</div>
</div>
);
}
export default SsoConnectorRadio;

View file

@ -0,0 +1,40 @@
@use '@/scss/underscore' as _;
@use '@/scss/dimensions' as dim;
.ssoConnectorGroup {
gap: _.unit(4);
display: grid;
grid-template-columns: repeat(4, 1fr);
@media screen and (max-width: dim.$modal-layout-grid-large) {
grid-template-columns: repeat(3, 1fr);
}
@media screen and (max-width: dim.$modal-layout-grid-medium) {
grid-template-columns: repeat(2, 1fr);
}
@media screen and (max-width: dim.$modal-layout-grid-small) {
grid-template-columns: repeat(1, 1fr);
}
&.medium {
grid-template-columns: repeat(2, 1fr);
@media screen and (max-width: dim.$modal-layout-grid-small) {
grid-template-columns: repeat(1, 1fr);
}
}
&.large {
grid-template-columns: repeat(3, 1fr);
@media screen and (max-width: dim.$modal-layout-grid-medium) {
grid-template-columns: repeat(2, 1fr);
}
@media screen and (max-width: dim.$modal-layout-grid-small) {
grid-template-columns: repeat(1, 1fr);
}
}
}

View file

@ -0,0 +1,37 @@
import { type SsoConnectorFactoryDetail } from '@logto/schemas';
import classNames from 'classnames';
import { type ConnectorRadioGroupSize } from '@/components/CreateConnectorForm/ConnectorRadioGroup';
import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
import SsoConnectorRadio from './SsoConnectorRadio';
import * as styles from './index.module.scss';
type Props = {
name: string;
value?: string;
className?: string;
size: ConnectorRadioGroupSize;
connectors: SsoConnectorFactoryDetail[];
onChange: (providerName: string) => void;
};
function SsoConnectorRadioGroup({ name, value, className, size, connectors, onChange }: Props) {
return (
<RadioGroup
name={name}
value={value}
type="card"
className={classNames(className, styles.ssoConnectorGroup, styles[size])}
onChange={onChange}
>
{connectors.map((data) => (
<Radio key={data.providerName} value={data.providerName}>
<SsoConnectorRadio data={data} />
</Radio>
))}
</RadioGroup>
);
}
export default SsoConnectorRadioGroup;

View file

@ -0,0 +1,7 @@
@use '@/scss/underscore' as _;
.textDivider {
font: var(--font-body-2);
color: var(--color-text-secondary);
margin: _.unit(6) 0 _.unit(4);
}

View file

@ -0,0 +1,150 @@
import { type SsoConnectorFactoriesResponse, type SsoConnector } from '@logto/schemas';
import { useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import Modal from 'react-modal';
import useSWR from 'swr';
import Skeleton from '@/components/CreateConnectorForm/Skeleton';
import { getConnectorRadioGroupSize } from '@/components/CreateConnectorForm/utils';
import Button from '@/ds-components/Button';
import DynamicT from '@/ds-components/DynamicT';
import FormField from '@/ds-components/FormField';
import ModalLayout from '@/ds-components/ModalLayout';
import TextInput from '@/ds-components/TextInput';
import { type RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import * as modalStyles from '@/scss/modal.module.scss';
import { trySubmitSafe } from '@/utils/form';
import SsoConnectorRadioGroup from './SsoConnectorRadioGroup';
import * as styles from './index.module.scss';
type Props = {
isOpen: boolean;
onClose: (ssoConnectorId?: string) => void;
};
type FormType = {
connectorName: string;
};
function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [selectedProviderName, setSelectedProviderName] = useState<string>();
const { data, error } = useSWR<SsoConnectorFactoriesResponse, RequestError>(
'api/sso-connector-factories'
);
const {
reset,
register,
handleSubmit,
formState: { isSubmitting, errors },
} = useForm<FormType>();
const api = useApi();
const isLoading = !data && !error;
const { standardConnectors = [], providerConnectors = [] } = data ?? {};
const radioGroupSize = useMemo(
() => getConnectorRadioGroupSize(standardConnectors.length + providerConnectors.length),
[standardConnectors, providerConnectors]
);
const isCreateButtonDisabled = useMemo(
() =>
![...standardConnectors, ...providerConnectors].some(
({ providerName }) => selectedProviderName === providerName
),
[selectedProviderName, standardConnectors, providerConnectors]
);
// `rawOnClose` does not clean the state of the modal.
const onClose = (ssoConnectorId?: string) => {
setSelectedProviderName(undefined);
reset();
rawOnClose(ssoConnectorId);
};
const handleSsoSelection = (providerName: string) => {
setSelectedProviderName(providerName);
};
const onSubmit = handleSubmit(
trySubmitSafe(async (formData) => {
if (isSubmitting) {
return;
}
const createdSsoConnector = await api
.post(`api/sso-connectors`, { json: { ...formData, providerName: selectedProviderName } })
.json<SsoConnector>();
onClose(createdSsoConnector.id);
})
);
if (!isOpen) {
return null;
}
return (
<Modal
shouldCloseOnEsc
isOpen={isOpen}
className={modalStyles.content}
overlayClassName={modalStyles.overlay}
onRequestClose={() => {
onClose();
}}
>
<ModalLayout
title="enterprise_sso.create_modal.title"
footer={
<Button
title="enterprise_sso.create_modal.create_button_text"
type="primary"
disabled={isCreateButtonDisabled}
onClick={onSubmit}
/>
}
size={radioGroupSize}
onClose={onClose}
>
{isLoading && <Skeleton numberOfLoadingConnectors={2} />}
{error?.message}
{providerConnectors.length > 0 && (
<>
<SsoConnectorRadioGroup
name="providerConnectors"
value={selectedProviderName}
connectors={providerConnectors}
size={radioGroupSize}
onChange={handleSsoSelection}
/>
<div className={styles.textDivider}>
<DynamicT forKey="enterprise_sso.create_modal.text_divider" />
</div>
</>
)}
<SsoConnectorRadioGroup
name="standardConnectors"
value={selectedProviderName}
connectors={standardConnectors}
size={radioGroupSize}
onChange={handleSsoSelection}
/>
<FormField isRequired title="enterprise_sso.create_modal.connector_name_field_title">
<TextInput
{...register('connectorName', { required: true })}
placeholder={t('enterprise_sso.create_modal.connector_name_field_placeholder')}
error={Boolean(errors.connectorName)}
/>
</FormField>
</ModalLayout>
</Modal>
);
}
export default SsoCreationModal;

View file

@ -2,6 +2,7 @@ import { withAppInsights } from '@logto/app-insights/react';
import { type SsoConnectorWithProviderConfig, Theme } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import useSWR from 'swr';
import Plus from '@/assets/icons/plus.svg';
@ -20,16 +21,19 @@ import useTenantPathname from '@/hooks/use-tenant-pathname';
import useTheme from '@/hooks/use-theme';
import { buildUrl } from '@/utils/url';
import SsoCreationModal from './SsoCreationModal';
import * as styles from './index.module.scss';
const pageSize = defaultPageSize;
const enterpriseSsoPathname = '/enterprise-sso';
const createEnterpriseSsoPathname = `${enterpriseSsoPathname}/create`;
const buildGuidePathname = (id: string) => `${enterpriseSsoPathname}/${id}/guide`;
const buildDetailsPathname = (id: string) => `${enterpriseSsoPathname}/${id}`;
function EnterpriseSsoConnectors() {
const theme = useTheme();
const { pathname } = useLocation();
const { navigate } = useTenantPathname();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [{ page }, updateSearchParameters] = useSearchParametersWatcher({
@ -163,6 +167,19 @@ function EnterpriseSsoConnectors() {
),
onRetry: async () => mutate(undefined, true),
}}
widgets={
<SsoCreationModal
isOpen={pathname.endsWith(createEnterpriseSsoPathname)}
onClose={async (id) => {
if (id) {
navigate(buildGuidePathname(id), { replace: true });
return;
}
navigate(enterpriseSsoPathname);
}}
/>
}
/>
);
}

View file

@ -24,6 +24,18 @@ const enterprise_sso = {
/** UNTRANSLATED */
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
/** UNTRANSLATED */
title: 'Add enterprise connector',
/** UNTRANSLATED */
text_divider: 'Or you can customize your connector by a standard protocol.',
/** UNTRANSLATED */
connector_name_field_title: 'Connector name',
/** UNTRANSLATED */
connector_name_field_placeholder: 'Name for the enterprise identity provider',
/** UNTRANSLATED */
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);

View file

@ -12,6 +12,13 @@ const enterprise_sso = {
placeholder_title: 'Enterprise connector',
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
title: 'Add enterprise connector',
text_divider: 'Or you can customize your connector by a standard protocol.',
connector_name_field_title: 'Connector name',
connector_name_field_placeholder: 'Name for the enterprise identity provider',
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);

View file

@ -24,6 +24,18 @@ const enterprise_sso = {
/** UNTRANSLATED */
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
/** UNTRANSLATED */
title: 'Add enterprise connector',
/** UNTRANSLATED */
text_divider: 'Or you can customize your connector by a standard protocol.',
/** UNTRANSLATED */
connector_name_field_title: 'Connector name',
/** UNTRANSLATED */
connector_name_field_placeholder: 'Name for the enterprise identity provider',
/** UNTRANSLATED */
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);

View file

@ -24,6 +24,18 @@ const enterprise_sso = {
/** UNTRANSLATED */
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
/** UNTRANSLATED */
title: 'Add enterprise connector',
/** UNTRANSLATED */
text_divider: 'Or you can customize your connector by a standard protocol.',
/** UNTRANSLATED */
connector_name_field_title: 'Connector name',
/** UNTRANSLATED */
connector_name_field_placeholder: 'Name for the enterprise identity provider',
/** UNTRANSLATED */
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);

View file

@ -24,6 +24,18 @@ const enterprise_sso = {
/** UNTRANSLATED */
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
/** UNTRANSLATED */
title: 'Add enterprise connector',
/** UNTRANSLATED */
text_divider: 'Or you can customize your connector by a standard protocol.',
/** UNTRANSLATED */
connector_name_field_title: 'Connector name',
/** UNTRANSLATED */
connector_name_field_placeholder: 'Name for the enterprise identity provider',
/** UNTRANSLATED */
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);

View file

@ -24,6 +24,18 @@ const enterprise_sso = {
/** UNTRANSLATED */
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
/** UNTRANSLATED */
title: 'Add enterprise connector',
/** UNTRANSLATED */
text_divider: 'Or you can customize your connector by a standard protocol.',
/** UNTRANSLATED */
connector_name_field_title: 'Connector name',
/** UNTRANSLATED */
connector_name_field_placeholder: 'Name for the enterprise identity provider',
/** UNTRANSLATED */
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);

View file

@ -24,6 +24,18 @@ const enterprise_sso = {
/** UNTRANSLATED */
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
/** UNTRANSLATED */
title: 'Add enterprise connector',
/** UNTRANSLATED */
text_divider: 'Or you can customize your connector by a standard protocol.',
/** UNTRANSLATED */
connector_name_field_title: 'Connector name',
/** UNTRANSLATED */
connector_name_field_placeholder: 'Name for the enterprise identity provider',
/** UNTRANSLATED */
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);

View file

@ -24,6 +24,18 @@ const enterprise_sso = {
/** UNTRANSLATED */
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
/** UNTRANSLATED */
title: 'Add enterprise connector',
/** UNTRANSLATED */
text_divider: 'Or you can customize your connector by a standard protocol.',
/** UNTRANSLATED */
connector_name_field_title: 'Connector name',
/** UNTRANSLATED */
connector_name_field_placeholder: 'Name for the enterprise identity provider',
/** UNTRANSLATED */
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);

View file

@ -24,6 +24,18 @@ const enterprise_sso = {
/** UNTRANSLATED */
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
/** UNTRANSLATED */
title: 'Add enterprise connector',
/** UNTRANSLATED */
text_divider: 'Or you can customize your connector by a standard protocol.',
/** UNTRANSLATED */
connector_name_field_title: 'Connector name',
/** UNTRANSLATED */
connector_name_field_placeholder: 'Name for the enterprise identity provider',
/** UNTRANSLATED */
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);

View file

@ -24,6 +24,18 @@ const enterprise_sso = {
/** UNTRANSLATED */
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
/** UNTRANSLATED */
title: 'Add enterprise connector',
/** UNTRANSLATED */
text_divider: 'Or you can customize your connector by a standard protocol.',
/** UNTRANSLATED */
connector_name_field_title: 'Connector name',
/** UNTRANSLATED */
connector_name_field_placeholder: 'Name for the enterprise identity provider',
/** UNTRANSLATED */
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);

View file

@ -24,6 +24,18 @@ const enterprise_sso = {
/** UNTRANSLATED */
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
/** UNTRANSLATED */
title: 'Add enterprise connector',
/** UNTRANSLATED */
text_divider: 'Or you can customize your connector by a standard protocol.',
/** UNTRANSLATED */
connector_name_field_title: 'Connector name',
/** UNTRANSLATED */
connector_name_field_placeholder: 'Name for the enterprise identity provider',
/** UNTRANSLATED */
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);

View file

@ -24,6 +24,18 @@ const enterprise_sso = {
/** UNTRANSLATED */
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
/** UNTRANSLATED */
title: 'Add enterprise connector',
/** UNTRANSLATED */
text_divider: 'Or you can customize your connector by a standard protocol.',
/** UNTRANSLATED */
connector_name_field_title: 'Connector name',
/** UNTRANSLATED */
connector_name_field_placeholder: 'Name for the enterprise identity provider',
/** UNTRANSLATED */
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);

View file

@ -24,6 +24,18 @@ const enterprise_sso = {
/** UNTRANSLATED */
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
/** UNTRANSLATED */
title: 'Add enterprise connector',
/** UNTRANSLATED */
text_divider: 'Or you can customize your connector by a standard protocol.',
/** UNTRANSLATED */
connector_name_field_title: 'Connector name',
/** UNTRANSLATED */
connector_name_field_placeholder: 'Name for the enterprise identity provider',
/** UNTRANSLATED */
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);

View file

@ -24,6 +24,18 @@ const enterprise_sso = {
/** UNTRANSLATED */
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
/** UNTRANSLATED */
title: 'Add enterprise connector',
/** UNTRANSLATED */
text_divider: 'Or you can customize your connector by a standard protocol.',
/** UNTRANSLATED */
connector_name_field_title: 'Connector name',
/** UNTRANSLATED */
connector_name_field_placeholder: 'Name for the enterprise identity provider',
/** UNTRANSLATED */
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);

View file

@ -24,6 +24,18 @@ const enterprise_sso = {
/** UNTRANSLATED */
placeholder_description:
'Logto has provided many built-in enterprise identity providers to connect, meantime you can create your own with standard protocols.',
create_modal: {
/** UNTRANSLATED */
title: 'Add enterprise connector',
/** UNTRANSLATED */
text_divider: 'Or you can customize your connector by a standard protocol.',
/** UNTRANSLATED */
connector_name_field_title: 'Connector name',
/** UNTRANSLATED */
connector_name_field_placeholder: 'Name for the enterprise identity provider',
/** UNTRANSLATED */
create_button_text: 'Create connector',
},
};
export default Object.freeze(enterprise_sso);