0
Fork 0
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:
Charles Zhao 2022-06-22 15:36:40 +08:00
parent f53aceff6f
commit 2312df059b
No known key found for this signature in database
GPG key ID: 4858774754C92DF2
12 changed files with 236 additions and 182 deletions

View file

@ -47,5 +47,10 @@
-webkit-text-fill-color: transparent;
outline: none;
}
textarea,
pre {
min-height: 20px;
}
}
}

View file

@ -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;
}

View file

@ -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>
);

View file

@ -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>

View file

@ -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' }}

View file

@ -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>

View file

@ -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);
}

View 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;

View file

@ -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;

View file

@ -134,6 +134,7 @@ const Connectors = () => {
type={createType}
onClose={() => {
setCreateType(undefined);
void mutate();
}}
/>
</>

View file

@ -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',

View file

@ -249,6 +249,7 @@ const translation = {
connector_status_not_in_use: '未使用',
social_connector_eg: '如: 微信登录,支付宝登录,微博登录',
next: '下一步',
save_and_done: '保存并完成',
type: {
email: '邮件服务商',
sms: '短信服务商',