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

refactor(console): refactor connector creation modal (#4183)

This commit is contained in:
Xiao Yijun 2023-07-19 15:59:12 +08:00 committed by GitHub
parent 3c51ecc29d
commit 1b0f9be88b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 257 additions and 145 deletions

View file

@ -0,0 +1,30 @@
@use '@/scss/underscore' as _;
.connector {
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);
}
.connectorId {
margin-top: _.unit(1);
font: var(--font-body-3);
color: var(--color-text-secondary);
}
.description {
font: var(--font-body-3);
color: var(--color-text-secondary);
margin-top: _.unit(1);
@include _.multi-line-ellipsis(4);
}
}
}

View file

@ -0,0 +1,30 @@
import { type ConnectorFactoryResponse } from '@logto/schemas';
import classNames from 'classnames';
import ConnectorLogo from '@/components/ConnectorLogo';
import UnnamedTrans from '@/components/UnnamedTrans';
import { type ConnectorGroup } from '@/types/connector';
import * as styles from './index.module.scss';
type Props = {
data: ConnectorGroup<ConnectorFactoryResponse>;
};
function ConnectorRadio({ data: { name, logo, logoDark, description } }: Props) {
return (
<div className={styles.connector}>
<ConnectorLogo data={{ logo, logoDark }} />
<div className={styles.content}>
<div className={classNames(styles.name)}>
<UnnamedTrans resource={name} />
</div>
<div className={styles.description}>
<UnnamedTrans resource={description} />
</div>
</div>
</div>
);
}
export default ConnectorRadio;

View file

@ -0,0 +1,40 @@
@use '@/scss/underscore' as _;
@use '@/scss/dimensions' as dim;
.connectorGroup {
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,38 @@
import { type ConnectorFactoryResponse } from '@logto/schemas';
import classNames from 'classnames';
import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
import { type ConnectorGroup } from '@/types/connector';
import ConnectorRadio from './ConnectorRadio';
import * as styles from './index.module.scss';
export type ConnectorRadioGroupSize = 'medium' | 'large' | 'xlarge';
type Props = {
name: string;
value?: string;
groups: Array<ConnectorGroup<ConnectorFactoryResponse>>;
size: ConnectorRadioGroupSize;
onChange: (groupId: string) => void;
};
function ConnectorRadioGroup({ name, groups, value, size, onChange }: Props) {
return (
<RadioGroup
name={name}
value={value}
type="card"
className={classNames(styles.connectorGroup, styles[size])}
onChange={onChange}
>
{groups.map((data) => (
<Radio key={data.id} value={data.id}>
<ConnectorRadio data={data} />
</Radio>
))}
</RadioGroup>
);
}
export default ConnectorRadioGroup;

View file

@ -1,17 +1,18 @@
import classNames from 'classnames';
import * as layout from '../index.module.scss';
import * as radioStyles from '../ConnectorRadioGroup/ConnectorRadio/index.module.scss';
import * as radioGroupStyles from '../ConnectorRadioGroup/index.module.scss';
import * as styles from './index.module.scss';
function Skeleton() {
return (
<div className={layout.connectorGroup}>
<div className={radioGroupStyles.connectorGroup}>
{Array.from({ length: 8 }).map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
<div key={index} className={classNames(layout.connector, styles.connector)}>
<div key={index} className={classNames(radioStyles.connector, styles.connector)}>
<div className={styles.logo} />
<div className={layout.content}>
<div className={radioStyles.content}>
<div className={styles.name} />
<div>
<div className={styles.description} />

View file

@ -1,71 +1,6 @@
@use '@/scss/underscore' as _;
@use '@/scss/dimensions' as dim;
.body {
.connectorGroup {
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);
}
}
.connector {
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);
}
.connectorId {
margin-top: _.unit(1);
font: var(--font-body-3);
color: var(--color-text-secondary);
}
.description {
font: var(--font-body-3);
color: var(--color-text-secondary);
margin-top: _.unit(1);
@include _.multi-line-ellipsis(4);
}
}
}
}
.standardLabel {
font: var(--font-label-2);
margin: _.unit(6) 0 _.unit(4);
}

View file

@ -1,24 +1,25 @@
import { ConnectorType } from '@logto/schemas';
import type { ConnectorFactoryResponse, ConnectorResponse } from '@logto/schemas';
import classNames from 'classnames';
import {
ConnectorType,
type ConnectorFactoryResponse,
type ConnectorResponse,
} from '@logto/schemas';
import { useMemo, useState } from 'react';
import Modal from 'react-modal';
import useSWR from 'swr';
import ConnectorLogo from '@/components/ConnectorLogo';
import UnnamedTrans from '@/components/UnnamedTrans';
import Button from '@/ds-components/Button';
import DynamicT from '@/ds-components/DynamicT';
import ModalLayout from '@/ds-components/ModalLayout';
import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
import type { RequestError } from '@/hooks/use-api';
import * as modalStyles from '@/scss/modal.module.scss';
import { getConnectorGroups } from '../../pages/Connectors/utils';
import ConnectorRadioGroup from './ConnectorRadioGroup';
import PlatformSelector from './PlatformSelector';
import Skeleton from './Skeleton';
import * as styles from './index.module.scss';
import { getConnectorOrder } from './utils';
import { compareConnectors, getConnectorRadioGroupSize, getModalTitle } from './utils';
type Props = {
isOpen: boolean;
@ -60,12 +61,7 @@ function CreateConnectorForm({ onClose, isOpen: isFormOpen, type }: Props) {
}))
.filter(({ connectors }) => !connectors.every(({ added }) => added))
.slice()
.sort((connectorA, connectorB) => {
const orderA = getConnectorOrder(connectorA.target, connectorA.isStandard);
const orderB = getConnectorOrder(connectorB.target, connectorB.isStandard);
return orderA - orderB;
});
.sort(compareConnectors);
}, [factories, type, existingConnectors]);
const activeGroup = useMemo(
@ -73,41 +69,11 @@ function CreateConnectorForm({ onClose, isOpen: isFormOpen, type }: Props) {
[activeGroupId, groups]
);
const cardTitle = useMemo(() => {
if (type === ConnectorType.Email) {
return 'connectors.setup_title.email';
}
if (type === ConnectorType.Sms) {
return 'connectors.setup_title.sms';
}
return 'connectors.setup_title.social';
}, [type]);
const modalSize = useMemo(() => {
/**
* Note:
* Fix the size to large, since now we have little passwordless connectors.
*/
if (type !== ConnectorType.Social) {
return 'large';
}
if (groups.length <= 2) {
return 'medium';
}
if (groups.length === 3) {
return 'large';
}
return 'xlarge';
}, [groups.length, type]);
if (!isFormOpen) {
return null;
}
const cardTitle = useMemo(() => getModalTitle(type), [type]);
const radioGroupSize = useMemo(
() => getConnectorRadioGroupSize(groups.length, type),
[groups.length, type]
);
const handleGroupChange = (groupId: string) => {
setActiveGroupId(groupId);
@ -123,6 +89,17 @@ function CreateConnectorForm({ onClose, isOpen: isFormOpen, type }: Props) {
setActiveFactoryId(firstAvailableConnector?.id);
};
const defaultGroups = useMemo(
() => (type === ConnectorType.Social ? groups.filter((group) => !group.isStandard) : groups),
[groups, type]
);
const standardGroups = useMemo(() => groups.filter((group) => group.isStandard), [groups]);
if (!isFormOpen) {
return null;
}
return (
<Modal
shouldCloseOnEsc
@ -145,35 +122,32 @@ function CreateConnectorForm({ onClose, isOpen: isFormOpen, type }: Props) {
}}
/>
}
className={styles.body}
size={modalSize}
size={radioGroupSize}
onClose={onClose}
>
{isLoading && <Skeleton />}
{factoriesError?.message ?? connectorsError?.message}
<RadioGroup
<ConnectorRadioGroup
name="group"
groups={defaultGroups}
value={activeGroupId}
type="card"
className={classNames(styles.connectorGroup, styles[modalSize])}
size={radioGroupSize}
onChange={handleGroupChange}
>
{groups.map(({ id, name, logo, logoDark, description }) => (
<Radio key={id} value={id}>
<div className={styles.connector}>
<ConnectorLogo data={{ logo, logoDark }} />
<div className={styles.content}>
<div className={classNames(styles.name)}>
<UnnamedTrans resource={name} />
</div>
<div className={styles.description}>
<UnnamedTrans resource={description} />
</div>
</div>
</div>
</Radio>
))}
</RadioGroup>
/>
{standardGroups.length > 0 && (
<>
<div className={styles.standardLabel}>
<DynamicT forKey="connectors.standard_connectors" />
</div>
<ConnectorRadioGroup
name="group"
groups={standardGroups}
value={activeGroupId}
size={radioGroupSize}
onChange={handleGroupChange}
/>
</>
)}
{activeGroup && (
<PlatformSelector
connectorGroup={activeGroup}

View file

@ -1,6 +1,10 @@
import { type AdminConsoleKey } from '@logto/phrases';
import { ConnectorType, type ConnectorResponse } from '@logto/schemas';
import { type ConnectorRadioGroupSize } from './ConnectorRadioGroup';
import { featuredConnectorTargets } from './constants';
export const getConnectorOrder = (target: string, isStandard?: boolean): number => {
const getConnectorOrder = (target: string, isStandard?: boolean): number => {
const order = featuredConnectorTargets.indexOf(target);
if (order === -1) {
@ -10,3 +14,48 @@ export const getConnectorOrder = (target: string, isStandard?: boolean): number
return order;
};
export const compareConnectors = <T extends Pick<ConnectorResponse, 'target' | 'isStandard'>>(
connectorA: T,
connectorB: T
) => {
const orderA = getConnectorOrder(connectorA.target, connectorA.isStandard);
const orderB = getConnectorOrder(connectorB.target, connectorB.isStandard);
return orderA - orderB;
};
export const getConnectorRadioGroupSize = (
connectorCount: number,
connectorType?: ConnectorType
): ConnectorRadioGroupSize => {
/**
* Note:
* Fix the size to large, since now we have little passwordless connectors.
*/
if (connectorType !== ConnectorType.Social) {
return 'large';
}
if (connectorCount <= 2) {
return 'medium';
}
if (connectorCount === 3) {
return 'large';
}
return 'xlarge';
};
export const getModalTitle = (connectorType?: ConnectorType): AdminConsoleKey => {
if (connectorType === ConnectorType.Email) {
return 'connectors.setup_title.email';
}
if (connectorType === ConnectorType.Sms) {
return 'connectors.setup_title.sms';
}
return 'connectors.setup_title.social';
};

View file

@ -88,6 +88,7 @@ const connectors = {
drawer_title: 'Connector Anleitung',
drawer_subtitle: 'Folge den Anweisungen, um deinen Connector zu integrieren',
unknown: 'Unbekannter Connector',
standard_connectors: 'Standard-Connectoren',
};
export default connectors;

View file

@ -87,6 +87,7 @@ const connectors = {
drawer_title: 'Connector Guide',
drawer_subtitle: 'Follow the instructions to integrate your connector',
unknown: 'Unknown Connector',
standard_connectors: 'Standard connectors',
};
export default connectors;

View file

@ -88,6 +88,7 @@ const connectors = {
drawer_title: 'Guía del conector',
drawer_subtitle: 'Siga las instrucciones para integrar su conector',
unknown: 'Conector desconocido',
standard_connectors: 'Conectores estándar',
};
export default connectors;

View file

@ -90,6 +90,7 @@ const connectors = {
drawer_title: 'Guide des connecteurs',
drawer_subtitle: 'Suivez les instructions pour intégrer votre connecteur',
unknown: 'Connecteur inconnu',
standard_connectors: 'Connecteurs standard',
};
export default connectors;

View file

@ -88,6 +88,7 @@ const connectors = {
drawer_title: 'Guida per il connettore',
drawer_subtitle: 'Segui le istruzioni per integrare il tuo connettore',
unknown: 'Connettore sconosciuto',
standard_connectors: 'Connettori standard',
};
export default connectors;

View file

@ -86,6 +86,7 @@ const connectors = {
drawer_title: 'コネクターガイド',
drawer_subtitle: 'インテグレーションの手順に従ってください',
unknown: '不明なコネクタ',
standard_connectors: '標準コネクタ',
};
export default connectors;

View file

@ -83,6 +83,7 @@ const connectors = {
drawer_title: '연동 가이드',
drawer_subtitle: '연동하기 위해 가이드를 따라 주세요.',
unknown: '알 수 없는 연동',
standard_connectors: '기본 커넥터',
};
export default connectors;

View file

@ -87,6 +87,7 @@ const connectors = {
drawer_title: 'Poradnik łącznika',
drawer_subtitle: 'Postępuj zgodnie z instrukcjami, aby zintegrować swój łącznik',
unknown: 'Nieznany Łącznik',
standard_connectors: 'Standardowe łączniki',
};
export default connectors;

View file

@ -22,7 +22,7 @@ const connectors = {
'Fora de uso significa que sua experiência de login não usou esse método de login. <a>{{link}}</a> para adicionar este método de login. ',
go_to_sie: 'Vá para a experiência de login',
},
placeholder_title: 'Social connector',
placeholder_title: 'Conector social',
placeholder_description:
'Logto tem fornecido muitos conectores de login social amplamente utilizados enquanto você pode criar o seu próprio com padrões padrão.',
save_and_done: 'Salvar e completar',
@ -43,7 +43,7 @@ const connectors = {
test_connection: 'Teste de conexão',
name: 'Nome do botão de login social',
name_placeholder: 'Insira o nome do botão de login social',
name_tip: 'O nome do botão do conector será exibido como "Continue com {{Nome do Conector}}".',
name_tip: 'O nome do botão do conector será exibido como "Continuar com {{Nome do Conector}}".',
logo: 'URL do logo para o botão de login social',
logo_placeholder: 'https://your.cdn.domain/logo.png',
logo_tip: 'A imagem do logotipo também será exibida no botão do conector.',
@ -86,6 +86,7 @@ const connectors = {
drawer_title: 'Guia do Conector',
drawer_subtitle: 'Siga as instruções para integrar seu conector',
unknown: 'Conector desconhecido',
standard_connectors: 'Conectores padrão',
};
export default connectors;

View file

@ -88,6 +88,7 @@ const connectors = {
drawer_title: 'Guia do conector',
drawer_subtitle: 'Siga as instruções para integrar o conector',
unknown: 'Conector desconhecido',
standard_connectors: 'Conectores padrão',
};
export default connectors;

View file

@ -87,6 +87,7 @@ const connectors = {
drawer_title: 'Руководство Коннектора',
drawer_subtitle: 'Следуйте инструкциям, чтобы интегрировать свой коннектор',
unknown: 'Неизвестный коннектор',
standard_connectors: 'стандартные разъемы',
};
export default connectors;

View file

@ -87,6 +87,7 @@ const connectors = {
drawer_title: 'Connector Kılavuzu',
drawer_subtitle: 'Connectorı entegre etmek için yönergeleri izleyin',
unknown: 'Bilinmeyen connector',
standard_connectors: 'Standart connectorlar',
};
export default connectors;

View file

@ -78,6 +78,7 @@ const connectors = {
drawer_title: '连接器配置指南',
drawer_subtitle: '参考以下步骤完善或修改你的连接器设置',
unknown: '未知连接器',
standard_connectors: '标准连接器',
};
export default connectors;

View file

@ -78,6 +78,7 @@ const connectors = {
drawer_title: '連接器配置指南',
drawer_subtitle: '參考以下步驟完善或修改你的連接器設置',
unknown: '未知連接器',
standard_connectors: '標準連接器',
};
export default connectors;

View file

@ -78,6 +78,7 @@ const connectors = {
drawer_title: '連接器配置指南',
drawer_subtitle: '參考以下步驟完善或修改你的連接器設置',
unknown: '未知連接器',
standard_connectors: '標準連接器',
};
export default connectors;