0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

feat(console): add standard connector (#2560)

This commit is contained in:
wangsijie 2022-12-01 15:18:26 +08:00 committed by GitHub
parent e4b007da38
commit 0376fa9d30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 272 additions and 48 deletions

View file

@ -50,7 +50,9 @@ const CreateForm = ({ onClose, isOpen: isFormOpen, type }: Props) => {
...group,
connectors: group.connectors.map((connector) => ({
...connector,
added: existingConnectors.some(({ connectorId }) => connector.id === connectorId),
added: group.isStandard
? false
: existingConnectors.some(({ connectorId }) => connector.id === connectorId),
})),
}));
}, [factories, type, existingConnectors]);

View file

@ -0,0 +1,92 @@
import type { ConnectorFactoryResponse } from '@logto/schemas';
import { useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import Button from '@/components/Button';
import CodeEditor from '@/components/CodeEditor';
import FormField from '@/components/FormField';
import TextInput from '@/components/TextInput';
import * as styles from './index.module.scss';
import type { CreateConnectorForm } from './types';
type Props = {
connector: ConnectorFactoryResponse;
};
const Form = ({ connector }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { configTemplate, isStandard } = connector;
const { control, register } = useFormContext<CreateConnectorForm>();
const [darkVisible, setDarkVisible] = useState(false);
const toggleDarkVisible = () => {
setDarkVisible((previous) => !previous);
};
return (
<div>
{isStandard && (
<>
<FormField isRequired title="connectors.guide.name">
<TextInput
placeholder={t('connectors.guide.name')}
{...register('name', { required: true })}
/>
<div className={styles.tip}>{t('connectors.guide.name_tip')}</div>
</FormField>
<FormField title="connectors.guide.logo">
<TextInput
placeholder={t('connectors.guide.logo_placelholder')}
{...register('logo')}
/>
<div className={styles.tip}>{t('connectors.guide.logo_tip')}</div>
</FormField>
{!darkVisible && (
<Button
size="small"
type="text"
title="connectors.guide.logo_dark_show"
onClick={toggleDarkVisible}
/>
)}
{darkVisible && (
<FormField title="connectors.guide.logo_dark">
<TextInput
placeholder={t('connectors.guide.logo_dark_placelholder')}
{...register('logoDark')}
/>
<div className={styles.tip}>{t('connectors.guide.logo_dark_tip')}</div>
</FormField>
)}
{darkVisible && (
<Button
size="small"
type="text"
title="connectors.guide.logo_dark_collapse"
onClick={toggleDarkVisible}
/>
)}
<FormField isRequired title="connectors.guide.target">
<TextInput {...register('target', { required: true })} />
<div className={styles.tip}>{t('connectors.guide.target_tip')}</div>
</FormField>
</>
)}
<FormField isRequired title="connectors.guide.config">
<Controller
name="config"
control={control}
defaultValue={configTemplate}
rules={{ required: true }}
render={({ field: { onChange, value } }) => (
<CodeEditor language="json" value={value} onChange={onChange} />
)}
/>
</FormField>
</div>
);
};
export default Form;

View file

@ -42,11 +42,31 @@
background-color: var(--color-layer-1);
border-radius: 16px;
padding: 0 _.unit(6);
margin: _.unit(6) _.unit(10) _.unit(6) _.unit(18);
margin: _.unit(6) _.unit(3) _.unit(6) _.unit(18);
}
.setup {
padding: _.unit(6) _.unit(18) _.unit(6) 0;
background-color: var(--color-layer-1);
border-radius: 16px;
padding: 0 _.unit(6) _.unit(6);
margin: _.unit(6) _.unit(18) _.unit(6) _.unit(3);
.title {
font: var(--font-title-large);
margin: _.unit(6) 0;
}
.tip {
color: var(--color-text-secondary);
font: var(--font-body-medium);
margin-top: _.unit(0.5);
}
.footer {
margin-top: _.unit(6);
display: flex;
justify-content: right;
}
}
form + div {
@ -55,7 +75,6 @@
}
}
.editor,
.tester {
margin-top: _.unit(6);
}

View file

@ -3,24 +3,24 @@ import type { ConnectorFactoryResponse, ConnectorResponse } from '@logto/schemas
import { ConnectorType } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import i18next from 'i18next';
import { Controller, useForm } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import Close from '@/assets/images/close.svg';
import Button from '@/components/Button';
import CardTitle from '@/components/CardTitle';
import CodeEditor from '@/components/CodeEditor';
import DangerousRaw from '@/components/DangerousRaw';
import IconButton from '@/components/IconButton';
import Markdown from '@/components/Markdown';
import useApi from '@/hooks/use-api';
import useSettings from '@/hooks/use-settings';
import Step from '@/mdx-components/Step';
import SenderTester from '@/pages/ConnectorDetails/components/SenderTester';
import type { GuideForm } from '@/types/guide';
import { safeParseJson } from '@/utilities/json';
import Form from './Form';
import * as styles from './index.module.scss';
import type { CreateConnectorForm } from './types';
type Props = {
connector: ConnectorFactoryResponse;
@ -31,25 +31,25 @@ const Guide = ({ connector, onClose }: Props) => {
const api = useApi();
const { updateSettings } = useSettings();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { id: connectorId, type: connectorType, name, configTemplate, readme } = connector;
const { id: connectorId, type: connectorType, name, readme, isStandard } = connector;
const { language } = i18next;
const connectorName = conditional(isLanguageTag(language) && name[language]) ?? name.en;
const isSocialConnector =
connectorType !== ConnectorType.Sms && connectorType !== ConnectorType.Email;
const methods = useForm<GuideForm>({ reValidateMode: 'onBlur' });
const methods = useForm<CreateConnectorForm>({ reValidateMode: 'onBlur' });
const {
control,
formState: { isSubmitting },
handleSubmit,
watch,
} = methods;
const onSubmit = handleSubmit(async ({ connectorConfigJson }) => {
const onSubmit = handleSubmit(async (data) => {
if (isSubmitting) {
return;
}
const result = safeParseJson(connectorConfigJson);
const { config, name, ...otherData } = data;
const result = safeParseJson(config);
if (!result.success) {
toast.error(result.error);
@ -60,7 +60,18 @@ const Guide = ({ connector, onClose }: Props) => {
const { id: connectorId } = connector;
await api
.post('/api/connectors', { json: { config: result.data, connectorId } })
.post('/api/connectors', {
json: {
config: result.data,
connectorId,
metadata: conditional(
isStandard && {
...otherData,
name: { en: name },
}
),
},
})
.json<ConnectorResponse>();
await updateSettings({
@ -88,39 +99,28 @@ const Guide = ({ connector, onClose }: Props) => {
<div className={styles.content}>
<Markdown className={styles.readme}>{readme}</Markdown>
<div className={styles.setup}>
<Step
title={t('connector_details.edit_config_label')}
index={0}
activeIndex={0}
buttonText="connectors.save_and_done"
buttonType="primary"
isLoading={isSubmitting}
onButtonClick={onSubmit}
>
<form {...methods}>
<Controller
name="connectorConfigJson"
control={control}
defaultValue={configTemplate}
render={({ field: { onChange, value } }) => (
<CodeEditor
className={styles.editor}
language="json"
value={value}
onChange={onChange}
/>
)}
/>
<div className={styles.title}>{t('connectors.guide.connector_setting')}</div>
<FormProvider {...methods}>
<form onSubmit={onSubmit}>
<Form connector={connector} />
{!isSocialConnector && (
<SenderTester
className={styles.tester}
connectorId={connectorId}
connectorType={connectorType}
config={watch('config')}
/>
)}
<div className={styles.footer}>
<Button
title="connectors.save_and_done"
type="primary"
htmlType="submit"
isLoading={isSubmitting}
/>
</div>
</form>
{!isSocialConnector && (
<SenderTester
className={styles.tester}
connectorId={connectorId}
connectorType={connectorType}
config={watch('connectorConfigJson')}
/>
)}
</Step>
</FormProvider>
</div>
</div>
</div>

View file

@ -0,0 +1,7 @@
export type CreateConnectorForm = {
config: string;
name: string;
logo: string;
logoDark: string;
target: string;
};

View file

@ -25,6 +25,7 @@ export const getConnectorGroups = <
description: item.description,
target: item.target,
type: item.type,
isStandard: item.isStandard,
connectors: [item],
},
];

View file

@ -2,7 +2,7 @@ import type { ConnectorResponse } from '@logto/schemas';
export type ConnectorGroup<T = ConnectorResponse> = Pick<
ConnectorResponse,
'name' | 'logo' | 'logoDark' | 'target' | 'type' | 'description'
'name' | 'logo' | 'logoDark' | 'target' | 'type' | 'description' | 'isStandard'
> & {
id: string;
connectors: T[];

View file

@ -1,5 +1,4 @@
export type GuideForm = {
redirectUris: string[];
postLogoutRedirectUris: string[];
connectorConfigJson: string;
};

View file

@ -30,6 +30,21 @@ const connectors = {
},
guide: {
subtitle: 'Eine Schritt-für-Schritt-Anleitung zur Konfiguration deines Connectors',
connector_setting: 'Connector setting', // UNTRANSLATED
name: 'Connector name', // UNTRANSLATED
name_tip: 'Connector buttons name will display as "Continue with {{Connector Name}}".', // UNTRANSLATED
logo: 'Connector logo URL', // UNTRANSLATED
logo_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED
logo_tip: 'The logo image will also display on the connector button.', // UNTRANSLATED
logo_dark: 'Connector logo URL (Dark mode)', // UNTRANSLATED
logo_dark_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED
logo_dark_tip:
'This will be used when opening “Enable dark mode” in the setting of sign in experience.', // UNTRANSLATED
logo_dark_collapse: 'Collapse', // UNTRANSLATED
logo_dark_show: 'Show "Logo for dark mode"', // UNTRANSLATED
target: 'Connector identity target', // UNTRANSLATED
target_tip: 'A unique identifier for the connector.', // UNTRANSLATED
config: 'Enter your JSON here', // UNTRANSLATED
},
platform: {
universal: 'Universal',

View file

@ -30,6 +30,21 @@ const connectors = {
},
guide: {
subtitle: 'A step by step guide to configure your connector',
connector_setting: 'Connector setting',
name: 'Connector name',
name_tip: 'Connector buttons name will display as "Continue with {{Connector Name}}".',
logo: 'Connector logo URL',
logo_placelholder: 'https://your.cdn.domain/logo.png',
logo_tip: 'The logo image will also display on the connector button.',
logo_dark: 'Connector logo URL (Dark mode)',
logo_dark_placelholder: 'https://your.cdn.domain/logo.png',
logo_dark_tip:
'This will be used when opening “Enable dark mode” in the setting of sign in experience.',
logo_dark_collapse: 'Collapse',
logo_dark_show: 'Show "Logo for dark mode"',
target: 'Connector identity target',
target_tip: 'A unique identifier for the connector.',
config: 'Enter your JSON here',
},
platform: {
universal: 'Universal',

View file

@ -31,6 +31,21 @@ const connectors = {
},
guide: {
subtitle: 'Un guide étape par étape pour configurer votre connecteur',
connector_setting: 'Connector setting', // UNTRANSLATED
name: 'Connector name', // UNTRANSLATED
name_tip: 'Connector buttons name will display as "Continue with {{Connector Name}}".', // UNTRANSLATED
logo: 'Connector logo URL', // UNTRANSLATED
logo_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED
logo_tip: 'The logo image will also display on the connector button.', // UNTRANSLATED
logo_dark: 'Connector logo URL (Dark mode)', // UNTRANSLATED
logo_dark_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED
logo_dark_tip:
'This will be used when opening “Enable dark mode” in the setting of sign in experience.', // UNTRANSLATED
logo_dark_collapse: 'Collapse', // UNTRANSLATED
logo_dark_show: 'Show "Logo for dark mode"', // UNTRANSLATED
target: 'Connector identity target', // UNTRANSLATED
target_tip: 'A unique identifier for the connector.', // UNTRANSLATED
config: 'Enter your JSON here', // UNTRANSLATED
},
platform: {
universal: 'Universel',

View file

@ -30,6 +30,21 @@ const connectors = {
},
guide: {
subtitle: '단계별 가이드를 따라, 연동해주세요.',
connector_setting: 'Connector setting', // UNTRANSLATED
name: 'Connector name', // UNTRANSLATED
name_tip: 'Connector buttons name will display as "Continue with {{Connector Name}}".', // UNTRANSLATED
logo: 'Connector logo URL', // UNTRANSLATED
logo_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED
logo_tip: 'The logo image will also display on the connector button.', // UNTRANSLATED
logo_dark: 'Connector logo URL (Dark mode)', // UNTRANSLATED
logo_dark_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED
logo_dark_tip:
'This will be used when opening “Enable dark mode” in the setting of sign in experience.', // UNTRANSLATED
logo_dark_collapse: 'Collapse', // UNTRANSLATED
logo_dark_show: 'Show "Logo for dark mode"', // UNTRANSLATED
target: 'Connector identity target', // UNTRANSLATED
target_tip: 'A unique identifier for the connector.', // UNTRANSLATED
config: 'Enter your JSON here', // UNTRANSLATED
},
platform: {
universal: 'Universal',

View file

@ -30,6 +30,21 @@ const connectors = {
},
guide: {
subtitle: 'Um guia passo a passo para configurar o conector',
connector_setting: 'Connector setting', // UNTRANSLATED
name: 'Connector name', // UNTRANSLATED
name_tip: 'Connector buttons name will display as "Continue with {{Connector Name}}".', // UNTRANSLATED
logo: 'Connector logo URL', // UNTRANSLATED
logo_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED
logo_tip: 'The logo image will also display on the connector button.', // UNTRANSLATED
logo_dark: 'Connector logo URL (Dark mode)', // UNTRANSLATED
logo_dark_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED
logo_dark_tip:
'This will be used when opening “Enable dark mode” in the setting of sign in experience.', // UNTRANSLATED
logo_dark_collapse: 'Collapse', // UNTRANSLATED
logo_dark_show: 'Show "Logo for dark mode"', // UNTRANSLATED
target: 'Connector identity target', // UNTRANSLATED
target_tip: 'A unique identifier for the connector.', // UNTRANSLATED
config: 'Enter your JSON here', // UNTRANSLATED
},
platform: {
universal: 'Universal',

View file

@ -31,6 +31,21 @@ const connectors = {
},
guide: {
subtitle: 'Connectorı yapılandırmak için adım adım kılavuz',
connector_setting: 'Connector setting', // UNTRANSLATED
name: 'Connector name', // UNTRANSLATED
name_tip: 'Connector buttons name will display as "Continue with {{Connector Name}}".', // UNTRANSLATED
logo: 'Connector logo URL', // UNTRANSLATED
logo_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED
logo_tip: 'The logo image will also display on the connector button.', // UNTRANSLATED
logo_dark: 'Connector logo URL (Dark mode)', // UNTRANSLATED
logo_dark_placelholder: 'https://your.cdn.domain/logo.png', // UNTRANSLATED
logo_dark_tip:
'This will be used when opening “Enable dark mode” in the setting of sign in experience.', // UNTRANSLATED
logo_dark_collapse: 'Collapse', // UNTRANSLATED
logo_dark_show: 'Show "Logo for dark mode"', // UNTRANSLATED
target: 'Connector identity target', // UNTRANSLATED
target_tip: 'A unique identifier for the connector.', // UNTRANSLATED
config: 'Enter your JSON here', // UNTRANSLATED
},
platform: {
universal: 'Evrensel',

View file

@ -29,6 +29,20 @@ const connectors = {
},
guide: {
subtitle: '参考以下步骤完成你的连接器设置',
connector_setting: '连接器设置',
name: '连接器名称',
name_tip: '连接器按钮名将会是「通过 {{连接器名称}} 登录」。',
logo: '连接器图标地址',
logo_placelholder: 'https://your.cdn.domain/logo.png',
logo_tip: '图标将会在连接器按钮中展示',
logo_dark: '连接器图标地址(深色模式)',
logo_dark_placelholder: 'https://your.cdn.domain/logo.png',
logo_dark_tip: '在登录体验设置中打开「启用深色模式」后生效',
logo_dark_collapse: '折叠',
logo_dark_show: '显示「深色模式图标」',
target: '连接器 target',
target_tip: '连接器标识符',
config: '请在此输入你的 JSON 配置',
},
platform: {
universal: '通用',