mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
refactor(console): combine steps in connector guide
This commit is contained in:
parent
f53aceff6f
commit
2312df059b
12 changed files with 236 additions and 182 deletions
|
@ -47,5 +47,10 @@
|
|||
-webkit-text-fill-color: transparent;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
textarea,
|
||||
pre {
|
||||
min-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,60 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.markdown {
|
||||
font: var(--font-body-medium);
|
||||
li {
|
||||
font: var(--font-body-medium);
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-inline-start: 1ch;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-inline-start: 4ch;
|
||||
|
||||
> li {
|
||||
margin-block-start: _.unit(2);
|
||||
margin-block-end: _.unit(2);
|
||||
padding-inline-start: _.unit(1);
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
padding-inline-start: 2ch;
|
||||
|
||||
> li {
|
||||
margin-block-start: _.unit(3);
|
||||
margin-block-end: _.unit(3);
|
||||
padding-inline-start: _.unit(1);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-text-link);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font: var(--font-title-large);
|
||||
margin: _.unit(6) 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font: var(--font-title-medium);
|
||||
}
|
||||
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
font: var(--font-title-small);
|
||||
color: var(--color-caption);
|
||||
margin: _.unit(6) 0 _.unit(3);
|
||||
}
|
||||
|
||||
p {
|
||||
font: var(--font-body-medium);
|
||||
margin: _.unit(3) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inlineCode {
|
||||
background: var(--color-layer-2);
|
||||
font: var(--font-body-medium);
|
||||
padding: _.unit(1) _.unit(1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,34 @@
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
children: string;
|
||||
};
|
||||
|
||||
const Markdown = ({ children }: Props) => (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} className={styles.markdown}>
|
||||
const Markdown = ({ className, children }: Props) => (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
className={classNames(styles.markdown, className)}
|
||||
components={{
|
||||
code: ({ node, inline, className, children, ...props }) => {
|
||||
const [, codeBlockType] = /language-(\w+)/.exec(className ?? '') ?? [];
|
||||
|
||||
return inline ? (
|
||||
<code className={styles.inlineCode} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
) : (
|
||||
<CodeEditor isReadonly language={codeBlockType} value={String(children)} />
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
|
|
|
@ -19,6 +19,8 @@ type Props = PropsWithChildren<{
|
|||
index: number;
|
||||
activeIndex: number;
|
||||
buttonText?: I18nKey;
|
||||
buttonHtmlType?: 'submit' | 'button';
|
||||
isLoading?: boolean;
|
||||
onButtonClick?: () => void;
|
||||
}>;
|
||||
|
||||
|
@ -29,6 +31,8 @@ const Step = ({
|
|||
index,
|
||||
activeIndex,
|
||||
buttonText = 'general.next',
|
||||
buttonHtmlType = 'button',
|
||||
isLoading,
|
||||
onButtonClick,
|
||||
}: Props) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
@ -73,7 +77,14 @@ const Step = ({
|
|||
<div className={classNames(styles.content, isExpanded && styles.expanded)}>
|
||||
{children}
|
||||
<div className={styles.buttonWrapper}>
|
||||
<Button type="outline" size="large" title={buttonText} onClick={onButtonClick} />
|
||||
<Button
|
||||
type="outline"
|
||||
size="large"
|
||||
isLoading={isLoading}
|
||||
htmlType={buttonHtmlType}
|
||||
title={buttonText}
|
||||
onClick={onButtonClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
@ -157,7 +157,7 @@ const ConnectorDetails = () => {
|
|||
setIsReadMeOpen(false);
|
||||
}}
|
||||
>
|
||||
<Markdown>{data.readme}</Markdown>
|
||||
<Markdown className={styles.readme}>{data.readme}</Markdown>
|
||||
</Drawer>
|
||||
<ActionMenu
|
||||
buttonProps={{ icon: <More className={styles.moreIcon} />, size: 'large' }}
|
||||
|
|
|
@ -9,7 +9,7 @@ import UnnamedTrans from '@/components/UnnamedTrans';
|
|||
import useConnectorGroups from '@/hooks/use-connector-groups';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import GuideModal from '../GuideModal';
|
||||
import Guide from '../Guide';
|
||||
import PlatformSelector from './PlatformSelector';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -132,11 +132,9 @@ const CreateForm = ({ onClose, isOpen: isFormOpen, type }: Props) => {
|
|||
/>
|
||||
)}
|
||||
{activeConnector && (
|
||||
<GuideModal
|
||||
connector={activeConnector}
|
||||
isOpen={isGetStartedModalOpen}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
<Modal isOpen={isGetStartedModalOpen} className={modalStyles.fullScreen}>
|
||||
<Guide connector={activeConnector} onClose={closeModal} />
|
||||
</Modal>
|
||||
)}
|
||||
</ModalLayout>
|
||||
</Modal>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-surface-1);
|
||||
background-color: var(--color-base);
|
||||
height: 100vh;
|
||||
|
||||
.header {
|
||||
|
@ -37,15 +37,13 @@
|
|||
> * {
|
||||
flex: 1;
|
||||
margin: 0 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.readme {
|
||||
background-color: var(--color-surface-variant);
|
||||
background-color: var(--color-layer-1);
|
||||
border-radius: 16px;
|
||||
padding: _.unit(6);
|
||||
padding: 0 _.unit(6);
|
||||
}
|
||||
|
||||
form + div {
|
||||
|
@ -53,3 +51,8 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor,
|
||||
.tester {
|
||||
margin-top: _.unit(6);
|
||||
}
|
136
packages/console/src/pages/Connectors/components/Guide/index.tsx
Normal file
136
packages/console/src/pages/Connectors/components/Guide/index.tsx
Normal file
|
@ -0,0 +1,136 @@
|
|||
import { ConnectorDTO, ConnectorType } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import i18next from 'i18next';
|
||||
import React from 'react';
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
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 Close from '@/icons/Close';
|
||||
import Step from '@/mdx-components/Step';
|
||||
import SenderTester from '@/pages/ConnectorDetails/components/SenderTester';
|
||||
import { GuideForm } from '@/types/guide';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
connector: ConnectorDTO;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
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 locale = i18next.language;
|
||||
// TODO: LOG-2393 should fix name[locale] syntax error
|
||||
const foundName = Object.entries(name).find(([lang]) => lang === locale);
|
||||
const connectorName = foundName ? foundName[1] : name.en;
|
||||
const isSocialConnector =
|
||||
connectorType !== ConnectorType.SMS && connectorType !== ConnectorType.Email;
|
||||
const methods = useForm<GuideForm>({ reValidateMode: 'onBlur' });
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
watch,
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async ({ connectorConfigJson }) => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const config = JSON.parse(connectorConfigJson) as JSON;
|
||||
await api
|
||||
.patch(`/api/connectors/${connectorId}`, {
|
||||
json: { config },
|
||||
})
|
||||
.json<ConnectorDTO>();
|
||||
await api
|
||||
.patch(`/api/connectors/${connectorId}/enabled`, {
|
||||
json: { enabled: true },
|
||||
})
|
||||
.json<ConnectorDTO>();
|
||||
|
||||
await updateSettings({
|
||||
...conditional(!isSocialConnector && { configurePasswordless: true }),
|
||||
...conditional(isSocialConnector && { configureSocialSignIn: true }),
|
||||
});
|
||||
|
||||
onClose();
|
||||
toast.success(t('connector_details.save_success'));
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SyntaxError) {
|
||||
toast.error(t('connector_details.save_error_json_parse_error'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<IconButton size="large" onClick={onClose}>
|
||||
<Close className={styles.closeIcon} />
|
||||
</IconButton>
|
||||
<div className={styles.separator} />
|
||||
<CardTitle
|
||||
size="small"
|
||||
title={<DangerousRaw>{connectorName}</DangerousRaw>}
|
||||
subtitle="connectors.guide.subtitle"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<Markdown className={styles.readme}>{readme}</Markdown>
|
||||
<div>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Step
|
||||
title="Enter your json here"
|
||||
subtitle="Lorem ipsum dolor sit amet, consectetuer adipiscing elit."
|
||||
index={0}
|
||||
activeIndex={0}
|
||||
buttonText="admin_console.connectors.save_and_done"
|
||||
buttonHtmlType="submit"
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
<Controller
|
||||
name="connectorConfigJson"
|
||||
control={control}
|
||||
defaultValue={configTemplate}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CodeEditor
|
||||
className={styles.editor}
|
||||
language="json"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{!isSocialConnector && (
|
||||
<SenderTester
|
||||
className={styles.tester}
|
||||
connectorType={connectorType}
|
||||
config={watch('connectorConfigJson')}
|
||||
/>
|
||||
)}
|
||||
</Step>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Guide;
|
|
@ -1,160 +0,0 @@
|
|||
import { ConnectorDTO, ConnectorType } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import i18next from 'i18next';
|
||||
import React, { useState } from 'react';
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
import CardTitle from '@/components/CardTitle';
|
||||
import CodeEditor from '@/components/CodeEditor';
|
||||
import DangerousRaw from '@/components/DangerousRaw';
|
||||
import IconButton from '@/components/IconButton';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useSettings from '@/hooks/use-settings';
|
||||
import Close from '@/icons/Close';
|
||||
import Step from '@/mdx-components/Step';
|
||||
import SenderTester from '@/pages/ConnectorDetails/components/SenderTester';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { GuideForm } from '@/types/guide';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
connector: ConnectorDTO;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onComplete?: (data: GuideForm) => Promise<void>;
|
||||
};
|
||||
|
||||
const GuideModal = ({ connector, isOpen, 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 locale = i18next.language;
|
||||
// TODO: LOG-2393 should fix name[locale] syntax error
|
||||
const foundName = Object.entries(name).find(([lang]) => lang === locale);
|
||||
const connectorName = foundName ? foundName[1] : name.en;
|
||||
const isSocialConnector =
|
||||
connectorType !== ConnectorType.SMS && connectorType !== ConnectorType.Email;
|
||||
const [activeStepIndex, setActiveStepIndex] = useState<number>(0);
|
||||
const steps = isSocialConnector ? 1 : 2;
|
||||
const methods = useForm<GuideForm>({ reValidateMode: 'onBlur' });
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async ({ connectorConfigJson }) => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const config = JSON.parse(connectorConfigJson) as JSON;
|
||||
await api
|
||||
.patch(`/api/connectors/${connectorId}`, {
|
||||
json: { config },
|
||||
})
|
||||
.json<ConnectorDTO>();
|
||||
await api
|
||||
.patch(`/api/connectors/${connectorId}/enabled`, {
|
||||
json: { enabled: true },
|
||||
})
|
||||
.json<ConnectorDTO>();
|
||||
|
||||
await updateSettings({
|
||||
...conditional(!isSocialConnector && { configurePasswordless: true }),
|
||||
...conditional(isSocialConnector && { configureSocialSignIn: true }),
|
||||
});
|
||||
|
||||
if (activeStepIndex === steps - 1) {
|
||||
onClose();
|
||||
} else {
|
||||
setActiveStepIndex(activeStepIndex + 1);
|
||||
}
|
||||
toast.success(t('connector_details.save_success'));
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SyntaxError) {
|
||||
toast.error(t('connector_details.save_error_json_parse_error'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} className={modalStyles.fullScreen}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<IconButton size="large" onClick={onClose}>
|
||||
<Close className={styles.closeIcon} />
|
||||
</IconButton>
|
||||
<div className={styles.separator} />
|
||||
<CardTitle
|
||||
size="small"
|
||||
title={<DangerousRaw>{connectorName}</DangerousRaw>}
|
||||
subtitle="connectors.guide.subtitle"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<ReactMarkdown
|
||||
className={styles.readme}
|
||||
components={{
|
||||
code: ({ node, inline, className, children, ...props }) => {
|
||||
const [, codeBlockType] = /language-(\w+)/.exec(className ?? '') ?? [];
|
||||
|
||||
return inline ? (
|
||||
<code {...props}>{children}</code>
|
||||
) : (
|
||||
<CodeEditor isReadonly language={codeBlockType} value={String(children)} />
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{readme}
|
||||
</ReactMarkdown>
|
||||
<div>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Step
|
||||
title="Enter your json here"
|
||||
subtitle="Lorem ipsum dolor sit amet, consectetuer adipiscing elit."
|
||||
index={0}
|
||||
activeIndex={activeStepIndex}
|
||||
buttonText={steps === 1 ? 'general.done' : undefined}
|
||||
>
|
||||
<Controller
|
||||
name="connectorConfigJson"
|
||||
control={control}
|
||||
defaultValue={configTemplate}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<CodeEditor language="json" value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</Step>
|
||||
</form>
|
||||
</FormProvider>
|
||||
{!isSocialConnector && (
|
||||
<Step
|
||||
title="Test your message"
|
||||
subtitle="Lorem ipsum dolor sit amet, consectetuer adipiscing elit."
|
||||
index={1}
|
||||
activeIndex={activeStepIndex}
|
||||
buttonText="general.done"
|
||||
onButtonClick={onClose}
|
||||
>
|
||||
<SenderTester connectorType={connectorType} />
|
||||
</Step>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default GuideModal;
|
|
@ -134,6 +134,7 @@ const Connectors = () => {
|
|||
type={createType}
|
||||
onClose={() => {
|
||||
setCreateType(undefined);
|
||||
void mutate();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -253,6 +253,7 @@ const translation = {
|
|||
connector_status_not_in_use: 'Not in use',
|
||||
social_connector_eg: 'e.g.: Google, Facebook, Twitter',
|
||||
next: 'Next',
|
||||
save_and_done: 'Save and done',
|
||||
type: {
|
||||
email: 'Email sender',
|
||||
sms: 'SMS sender',
|
||||
|
|
|
@ -249,6 +249,7 @@ const translation = {
|
|||
connector_status_not_in_use: '未使用',
|
||||
social_connector_eg: '如: 微信登录,支付宝登录,微博登录',
|
||||
next: '下一步',
|
||||
save_and_done: '保存并完成',
|
||||
type: {
|
||||
email: '邮件服务商',
|
||||
sms: '短信服务商',
|
||||
|
|
Loading…
Reference in a new issue