0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-20 21:32:31 -05:00

refactor(console): fix onboarding issues

This commit is contained in:
Gao Sun 2024-05-30 12:40:19 +08:00
parent d5f67ecc77
commit b755e95a75
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
4 changed files with 158 additions and 119 deletions

View file

@ -23,13 +23,25 @@ type Props = {
readonly mode: Theme;
readonly language?: LanguageTag;
readonly signInExperience?: SignInExperience;
/**
* The Logto endpoint to use for the preview. If not provided, the current tenant endpoint from
* the `AppDataContext` will be used.
*/
readonly endpoint?: URL;
};
function SignInExperiencePreview({ platform, mode, language = 'en', signInExperience }: Props) {
function SignInExperiencePreview({
platform,
mode,
language = 'en',
signInExperience,
endpoint: endpointInput,
}: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { customPhrases } = useUiLanguages();
const { tenantEndpoint } = useContext(AppDataContext);
const endpoint = endpointInput ?? tenantEndpoint;
const previewRef = useRef<HTMLIFrameElement>(null);
const { data: allConnectors } = useSWR<ConnectorResponse[], RequestError>('api/connectors');
const [iframeLoaded, setIframeLoaded] = useState(false);
@ -76,9 +88,9 @@ function SignInExperiencePreview({ platform, mode, language = 'en', signInExperi
previewRef.current?.contentWindow?.postMessage(
{ sender: 'ac_preview', config: configForUiPage },
tenantEndpoint?.origin ?? ''
endpoint?.origin ?? ''
);
}, [tenantEndpoint?.origin, configForUiPage, customPhrases]);
}, [endpoint?.origin, configForUiPage, customPhrases]);
const iframeOnLoadEventHandler = useCallback(() => {
setIframeLoaded(true);
@ -102,7 +114,7 @@ function SignInExperiencePreview({ platform, mode, language = 'en', signInExperi
postPreviewMessage();
}, [iframeLoaded, postPreviewMessage]);
if (!tenantEndpoint) {
if (!endpoint) {
return null;
}
@ -131,7 +143,7 @@ function SignInExperiencePreview({ platform, mode, language = 'en', signInExperi
ref={previewRef}
// Allow all sandbox rules
sandbox={undefined}
src={new URL('/sign-in?preview=true', tenantEndpoint).toString()}
src={new URL('/sign-in?preview=true', endpoint).toString()}
tabIndex={-1}
title={t('sign_in_exp.preview.title')}
/>

View file

@ -22,20 +22,17 @@ type AppData = {
export const AppDataContext = createContext<AppData>({});
export const useTenantEndpoint = (tenantId: string) => {
return useSWRImmutable(`api/.well-known/endpoints/${tenantId}`, async (pathname) => {
const { user } = await ky.get(new URL(pathname, adminTenantEndpoint)).json<{ user: string }>();
return new URL(user);
});
};
/** The context provider for the global app data. */
function AppDataProvider({ children }: Props) {
const { currentTenantId } = useContext(TenantsContext);
const { data: tenantEndpoint } = useSWRImmutable(
`api/.well-known/endpoints/${currentTenantId}`,
async (pathname) => {
const { user } = await ky
.get(new URL(pathname, adminTenantEndpoint))
.json<{ user: string }>();
return new URL(user);
}
);
const { data: tenantEndpoint } = useTenantEndpoint(currentTenantId);
const memorizedContext = useMemo(
() =>
({

View file

@ -12,9 +12,14 @@ import * as styles from './index.module.scss';
type Props = {
readonly signInExperience?: SignInExperience;
readonly className?: string;
/**
* The Logto endpoint to use for the preview. If not provided, the current tenant endpoint from
* the `AppDataContext` will be used.
*/
readonly endpoint?: URL;
};
function Preview({ signInExperience, className }: Props) {
function Preview({ signInExperience, className, endpoint }: Props) {
const [currentTab, setCurrentTab] = useState(PreviewPlatform.DesktopWeb);
return (
@ -24,6 +29,7 @@ function Preview({ signInExperience, className }: Props) {
platform={currentTab}
mode={Theme.Light}
signInExperience={signInExperience}
endpoint={endpoint}
/>
</div>
);

View file

@ -4,12 +4,14 @@ import type { SignInExperience as SignInExperienceType, ConnectorResponse } from
import { useCallback, useEffect, useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import { useParams } from 'react-router-dom';
import useSWR, { SWRConfig } from 'swr';
import Tools from '@/assets/icons/tools.svg';
import ActionBar from '@/components/ActionBar';
import { GtagConversionId, reportConversion } from '@/components/Conversion/utils';
import PageMeta from '@/components/PageMeta';
import { useTenantEndpoint } from '@/contexts/AppDataProvider';
import Button from '@/ds-components/Button';
import ColorPicker from '@/ds-components/ColorPicker';
import FormField from '@/ds-components/FormField';
@ -39,6 +41,19 @@ import { authenticationOptions, identifierOptions } from './options';
import { defaultOnboardingSieFormData } from './sie-config-templates';
import { Authentication, type OnboardingSieFormData } from './types';
const useCurrentTenantEndpoint = () => {
const { tenantId: currentTenantId } = useParams();
if (!currentTenantId) {
throw new Error(
'No tenant ID param found in the current route. This hook should be used in a route with a tenant ID param.'
);
}
const { data } = useTenantEndpoint(currentTenantId);
return data;
};
function SignInExperience() {
const swrOptions = useTenantSwrOptions();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
@ -54,6 +69,7 @@ function SignInExperience() {
const { isReady: isUserAssetsServiceReady } = useUserAssetsService();
const { update } = useUserOnboardingData();
const { user } = useCurrentUser();
const endpoint = useCurrentTenantEndpoint();
const enterAdminConsole = async () => {
await update({ isOnboardingDone: true });
@ -136,114 +152,122 @@ function SignInExperience() {
}
return (
<div className={pageLayout.page}>
<PageMeta titleKey={['cloud.sie.page_title', 'cloud.general.onboarding']} />
<OverlayScrollbar className={pageLayout.contentContainer}>
<div className={styles.content}>
<div className={styles.config}>
<Tools />
<div className={pageLayout.title}>{t('cloud.sie.title')}</div>
<InspireMe
onInspired={(template) => {
for (const [key, value] of Object.entries(template)) {
// eslint-disable-next-line no-restricted-syntax
setValue(key as keyof OnboardingSieFormData, value, { shouldDirty: true });
}
updateAuthenticationConfig();
}}
/>
<FormField title="cloud.sie.logo_field">
{isUserAssetsServiceReady ? (
<SWRConfig value={swrOptions}>
<div className={pageLayout.page}>
<PageMeta titleKey={['cloud.sie.page_title', 'cloud.general.onboarding']} />
<OverlayScrollbar className={pageLayout.contentContainer}>
<div className={styles.content}>
<div className={styles.config}>
<Tools />
<div className={pageLayout.title}>{t('cloud.sie.title')}</div>
<InspireMe
onInspired={(template) => {
for (const [key, value] of Object.entries(template)) {
// eslint-disable-next-line no-restricted-syntax
setValue(key as keyof OnboardingSieFormData, value, { shouldDirty: true });
}
updateAuthenticationConfig();
}}
/>
<FormField title="cloud.sie.logo_field">
{isUserAssetsServiceReady ? (
<Controller
name="logo"
control={control}
render={({ field: { onChange, value, name } }) => (
<ImageUploaderField name={name} value={value ?? ''} onChange={onChange} />
)}
/>
) : (
<TextInput
{...register('logo', {
validate: (value) =>
!value || uriValidator(value) || t('errors.invalid_uri_format'),
})}
error={errors.logo?.message}
/>
)}
</FormField>
<FormField title="cloud.sie.color_field">
<Controller
name="logo"
name="color"
control={control}
render={({ field: { onChange, value, name } }) => (
<ImageUploaderField name={name} value={value ?? ''} onChange={onChange} />
render={({ field: { onChange, value } }) => (
<ColorPicker value={value} onChange={onChange} />
)}
/>
) : (
<TextInput
{...register('logo', {
validate: (value) =>
!value || uriValidator(value) || t('errors.invalid_uri_format'),
})}
error={errors.logo?.message}
</FormField>
<FormField title="cloud.sie.identifier_field" headlineSpacing="large">
<Controller
name="identifier"
control={control}
render={({ field: { name, value, onChange } }) => (
<CardSelector
name={name}
value={value}
options={identifierOptions}
onChange={(value) => {
onChange(value);
updateAuthenticationConfig();
}}
/>
)}
/>
)}
</FormField>
<FormField title="cloud.sie.color_field">
<Controller
name="color"
control={control}
render={({ field: { onChange, value } }) => (
<ColorPicker value={value} onChange={onChange} />
)}
</FormField>
<FormField isMultiple title="cloud.sie.authn_field" headlineSpacing="large">
<Controller
name="authentications"
control={control}
defaultValue={defaultOnboardingSieFormData.authentications}
render={({ field: { value, onChange } }) => (
<MultiCardSelector
isNotAllowEmpty
className={styles.authnSelector}
value={value}
options={authenticationOptions.filter(
({ value }) =>
onboardingSieFormData.identifier !== SignInIdentifier.Username ||
value === Authentication.Password
)}
onChange={onChange}
/>
)}
/>
</FormField>
<FormField isMultiple title="cloud.sie.social_field" headlineSpacing="large">
<Controller
name="socialTargets"
control={control}
defaultValue={defaultOnboardingSieFormData.socialTargets}
render={({ field: { value, onChange } }) => (
<SocialSelector value={value ?? []} onChange={onChange} />
)}
/>
</FormField>
</div>
{endpoint && (
<Preview
className={styles.preview}
signInExperience={previewSieConfig}
endpoint={endpoint}
/>
</FormField>
<FormField title="cloud.sie.identifier_field" headlineSpacing="large">
<Controller
name="identifier"
control={control}
render={({ field: { name, value, onChange } }) => (
<CardSelector
name={name}
value={value}
options={identifierOptions}
onChange={(value) => {
onChange(value);
updateAuthenticationConfig();
}}
/>
)}
/>
</FormField>
<FormField isMultiple title="cloud.sie.authn_field" headlineSpacing="large">
<Controller
name="authentications"
control={control}
defaultValue={defaultOnboardingSieFormData.authentications}
render={({ field: { value, onChange } }) => (
<MultiCardSelector
isNotAllowEmpty
className={styles.authnSelector}
value={value}
options={authenticationOptions.filter(
({ value }) =>
onboardingSieFormData.identifier !== SignInIdentifier.Username ||
value === Authentication.Password
)}
onChange={onChange}
/>
)}
/>
</FormField>
<FormField isMultiple title="cloud.sie.social_field" headlineSpacing="large">
<Controller
name="socialTargets"
control={control}
defaultValue={defaultOnboardingSieFormData.socialTargets}
render={({ field: { value, onChange } }) => (
<SocialSelector value={value ?? []} onChange={onChange} />
)}
/>
</FormField>
)}
</div>
<Preview className={styles.preview} signInExperience={previewSieConfig} />
</div>
</OverlayScrollbar>
<ActionBar step={3} totalSteps={3}>
<div className={styles.continueActions}>
<Button
type="primary"
title="cloud.sie.finish_and_done"
disabled={isSubmitting}
onClick={async () => {
await handleSubmit(submit(enterAdminConsole))();
}}
/>
</div>
</ActionBar>
</div>
</OverlayScrollbar>
<ActionBar step={3} totalSteps={3}>
<div className={styles.continueActions}>
<Button
type="primary"
title="cloud.sie.finish_and_done"
disabled={isSubmitting}
onClick={async () => {
await handleSubmit(submit(enterAdminConsole))();
}}
/>
</div>
</ActionBar>
</div>
</SWRConfig>
);
}