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

feat(console): add global loading skeleton (#5498)

add global loading skeleton
This commit is contained in:
simeng-li 2024-03-14 10:15:07 +08:00 committed by GitHub
parent e8ac64c23c
commit abb2c9f649
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 140 additions and 8 deletions

View file

@ -0,0 +1,20 @@
/**
* This context is used to share the loading state of the code editor with the root page.
*
* 1. First code editor won't be rendered until the fetch API is returned.
* 2. After the code editor is mounted, multiple async operations will be triggered to load the monaco editor.
* 3. We need to mount the code editor component will keep the loading state until the monaco editor is ready.
*/
import { noop } from '@silverhand/essentials';
import { createContext } from 'react';
type CodeEditorLoadingContextType = {
isMonacoLoaded: boolean;
setIsMonacoLoaded: (isLoading: boolean) => void;
};
export const CodeEditorLoadingContext = createContext<CodeEditorLoadingContextType>({
isMonacoLoaded: false,
setIsMonacoLoaded: noop,
});

View file

@ -20,6 +20,7 @@ import { type JwtClaimsFormType } from './type';
import { formatResponseDataToFormData, formatFormDataToRequestData, getApiPath } from './utils'; import { formatResponseDataToFormData, formatFormDataToRequestData, getApiPath } from './utils';
type Props = { type Props = {
className?: string;
tab: LogtoJwtTokenPath; tab: LogtoJwtTokenPath;
accessTokenJwtCustomizer: AccessTokenJwtCustomizer | undefined; accessTokenJwtCustomizer: AccessTokenJwtCustomizer | undefined;
clientCredentialsJwtCustomizer: ClientCredentialsJwtCustomizer | undefined; clientCredentialsJwtCustomizer: ClientCredentialsJwtCustomizer | undefined;
@ -28,6 +29,7 @@ type Props = {
}; };
function Main({ function Main({
className,
tab, tab,
accessTokenJwtCustomizer, accessTokenJwtCustomizer,
clientCredentialsJwtCustomizer, clientCredentialsJwtCustomizer,
@ -87,7 +89,7 @@ function Main({
return ( return (
<> <>
<FormProvider {...activeForm}> <FormProvider {...activeForm}>
<form className={classNames(styles.tabContent)}> <form className={classNames(styles.tabContent, className)}>
<ScriptSection /> <ScriptSection />
<SettingsSection /> <SettingsSection />
</form> </form>

View file

@ -25,6 +25,7 @@ type Props = {
setActiveModel?: (name: string) => void; setActiveModel?: (name: string) => void;
value?: string; value?: string;
onChange?: (value: string | undefined) => void; onChange?: (value: string | undefined) => void;
onMountHandler?: (editor: IStandaloneCodeEditor) => void;
}; };
/** /**
* Monaco code editor component. * Monaco code editor component.
@ -47,6 +48,7 @@ function MonacoCodeEditor({
value, value,
setActiveModel, setActiveModel,
onChange, onChange,
onMountHandler,
}: Props) { }: Props) {
const monaco = useMonaco(); const monaco = useMonaco();
const editorRef = useRef<Nullable<IStandaloneCodeEditor>>(null); const editorRef = useRef<Nullable<IStandaloneCodeEditor>>(null);
@ -84,10 +86,14 @@ function MonacoCodeEditor({
monaco.editor.defineTheme('logto-dark', logtoDarkTheme); monaco.editor.defineTheme('logto-dark', logtoDarkTheme);
}, []); }, []);
const handleEditorDidMount = useCallback<OnMount>((editor) => { const handleEditorDidMount = useCallback<OnMount>(
// eslint-disable-next-line @silverhand/fp/no-mutation (editor) => {
editorRef.current = editor; // eslint-disable-next-line @silverhand/fp/no-mutation
}, []); editorRef.current = editor;
onMountHandler?.(editor);
},
[onMountHandler]
);
return ( return (
<div className={classNames(className, styles.codeEditor)}> <div className={classNames(className, styles.codeEditor)}>

View file

@ -0,0 +1,32 @@
@use '@/scss/underscore' as _;
.blockShimmer {
@include _.shimmering-animation;
border-radius: 8px;
flex: 1;
}
.card:not(:last-child) {
margin-bottom: _.unit(4);
}
.textShimmer {
@include _.shimmering-animation;
width: 100%;
height: _.unit(6);
border-radius: 8px;
&:not(:last-child) {
margin-bottom: _.unit(4);
}
&.title {
width: _.unit(30);
}
&.large {
height: _.unit(8);
}
}

View file

@ -0,0 +1,46 @@
import { LogtoJwtTokenPath } from '@logto/schemas';
import classNames from 'classnames';
import Card from '@/ds-components/Card';
import * as pageLayoutStyles from '../index.module.scss';
import * as styles from './index.module.scss';
type Props = {
tokenType: LogtoJwtTokenPath;
};
function PageLoadingSkeleton({ tokenType }: Props) {
return (
<div className={pageLayoutStyles.tabContent}>
<Card className={pageLayoutStyles.codePanel}>
<div className={classNames(styles.textShimmer, styles.title)} />
<div className={styles.blockShimmer} />
</Card>
<div>
<div className={classNames(styles.textShimmer, styles.large)} />
<Card className={styles.card}>
<div className={classNames(styles.textShimmer, styles.title)} />
<div className={styles.textShimmer} />
</Card>
<Card className={styles.card}>
<div className={classNames(styles.textShimmer, styles.title)} />
<div className={styles.textShimmer} />
</Card>
<Card className={styles.card}>
<div className={classNames(styles.textShimmer, styles.title)} />
<div className={styles.textShimmer} />
</Card>
{tokenType === LogtoJwtTokenPath.AccessToken && (
<Card className={styles.card}>
<div className={styles.textShimmer} />
<div className={styles.textShimmer} />
</Card>
)}
</div>
</div>
);
}
export default PageLoadingSkeleton;

View file

@ -1,11 +1,12 @@
/* Code Editor for the custom JWT claims script. */ /* Code Editor for the custom JWT claims script. */
import { LogtoJwtTokenPath } from '@logto/schemas'; import { LogtoJwtTokenPath } from '@logto/schemas';
import { useMemo } from 'react'; import { useMemo, useContext, useCallback } from 'react';
import { useFormContext, Controller } from 'react-hook-form'; import { useFormContext, Controller } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Card from '@/ds-components/Card'; import Card from '@/ds-components/Card';
import { CodeEditorLoadingContext } from './CodeEditorLoadingContext';
import MonacoCodeEditor, { type ModelSettings } from './MonacoCodeEditor'; import MonacoCodeEditor, { type ModelSettings } from './MonacoCodeEditor';
import { userJwtFile, machineToMachineJwtFile } from './config'; import { userJwtFile, machineToMachineJwtFile } from './config';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
@ -22,11 +23,17 @@ function ScriptSection() {
const { watch, control } = useFormContext<JwtClaimsFormType>(); const { watch, control } = useFormContext<JwtClaimsFormType>();
const tokenType = watch('tokenType'); const tokenType = watch('tokenType');
const { setIsMonacoLoaded } = useContext(CodeEditorLoadingContext);
const activeModel = useMemo<ModelSettings>( const activeModel = useMemo<ModelSettings>(
() => (tokenType === LogtoJwtTokenPath.AccessToken ? userJwtFile : machineToMachineJwtFile), () => (tokenType === LogtoJwtTokenPath.AccessToken ? userJwtFile : machineToMachineJwtFile),
[tokenType] [tokenType]
); );
const onMountHandler = useCallback(() => {
setIsMonacoLoaded(true);
}, [setIsMonacoLoaded]);
return ( return (
<Card className={styles.codePanel}> <Card className={styles.codePanel}>
<div className={styles.cardTitle}> <div className={styles.cardTitle}>
@ -56,6 +63,7 @@ function ScriptSection() {
onChange(newValue); onChange(newValue);
}} }}
onMountHandler={onMountHandler}
/> />
)} )}
/> />

View file

@ -44,3 +44,7 @@
flex-grow: 1; flex-grow: 1;
} }
} }
.hidden {
display: none;
}

View file

@ -1,11 +1,14 @@
import { withAppInsights } from '@logto/app-insights/react/AppInsightsReact'; import { withAppInsights } from '@logto/app-insights/react/AppInsightsReact';
import { LogtoJwtTokenPath } from '@logto/schemas'; import { LogtoJwtTokenPath } from '@logto/schemas';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import CardTitle from '@/ds-components/CardTitle'; import CardTitle from '@/ds-components/CardTitle';
import TabNav, { TabNavItem } from '@/ds-components/TabNav'; import TabNav, { TabNavItem } from '@/ds-components/TabNav';
import { CodeEditorLoadingContext } from './CodeEditorLoadingContext';
import Main from './Main'; import Main from './Main';
import PageLoadingSkeleton from './PageLoadingSkeleton';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
import useJwtCustomizer from './use-jwt-customizer'; import useJwtCustomizer from './use-jwt-customizer';
@ -24,6 +27,12 @@ function JwtClaims({ tab }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { isLoading, ...rest } = useJwtCustomizer(); const { isLoading, ...rest } = useJwtCustomizer();
const [isMonacoLoaded, setIsMonacoLoaded] = useState(false);
const codeEditorContextValue = useMemo(
() => ({ isMonacoLoaded, setIsMonacoLoaded }),
[isMonacoLoaded]
);
return ( return (
<div className={styles.container}> <div className={styles.container}>
@ -39,8 +48,13 @@ function JwtClaims({ tab }: Props) {
</TabNavItem> </TabNavItem>
))} ))}
</TabNav> </TabNav>
{/* TODO: Loading skelton */} {(isLoading || !isMonacoLoaded) && <PageLoadingSkeleton tokenType={tab} />}
{!isLoading && <Main tab={tab} {...rest} />}
{!isLoading && (
<CodeEditorLoadingContext.Provider value={codeEditorContextValue}>
<Main tab={tab} {...rest} className={isMonacoLoaded ? undefined : styles.hidden} />
</CodeEditorLoadingContext.Provider>
)}
</div> </div>
); );
} }