0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-03 21:48:55 -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';
type Props = {
className?: string;
tab: LogtoJwtTokenPath;
accessTokenJwtCustomizer: AccessTokenJwtCustomizer | undefined;
clientCredentialsJwtCustomizer: ClientCredentialsJwtCustomizer | undefined;
@ -28,6 +29,7 @@ type Props = {
};
function Main({
className,
tab,
accessTokenJwtCustomizer,
clientCredentialsJwtCustomizer,
@ -87,7 +89,7 @@ function Main({
return (
<>
<FormProvider {...activeForm}>
<form className={classNames(styles.tabContent)}>
<form className={classNames(styles.tabContent, className)}>
<ScriptSection />
<SettingsSection />
</form>

View file

@ -25,6 +25,7 @@ type Props = {
setActiveModel?: (name: string) => void;
value?: string;
onChange?: (value: string | undefined) => void;
onMountHandler?: (editor: IStandaloneCodeEditor) => void;
};
/**
* Monaco code editor component.
@ -47,6 +48,7 @@ function MonacoCodeEditor({
value,
setActiveModel,
onChange,
onMountHandler,
}: Props) {
const monaco = useMonaco();
const editorRef = useRef<Nullable<IStandaloneCodeEditor>>(null);
@ -84,10 +86,14 @@ function MonacoCodeEditor({
monaco.editor.defineTheme('logto-dark', logtoDarkTheme);
}, []);
const handleEditorDidMount = useCallback<OnMount>((editor) => {
// eslint-disable-next-line @silverhand/fp/no-mutation
editorRef.current = editor;
}, []);
const handleEditorDidMount = useCallback<OnMount>(
(editor) => {
// eslint-disable-next-line @silverhand/fp/no-mutation
editorRef.current = editor;
onMountHandler?.(editor);
},
[onMountHandler]
);
return (
<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. */
import { LogtoJwtTokenPath } from '@logto/schemas';
import { useMemo } from 'react';
import { useMemo, useContext, useCallback } from 'react';
import { useFormContext, Controller } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import Card from '@/ds-components/Card';
import { CodeEditorLoadingContext } from './CodeEditorLoadingContext';
import MonacoCodeEditor, { type ModelSettings } from './MonacoCodeEditor';
import { userJwtFile, machineToMachineJwtFile } from './config';
import * as styles from './index.module.scss';
@ -22,11 +23,17 @@ function ScriptSection() {
const { watch, control } = useFormContext<JwtClaimsFormType>();
const tokenType = watch('tokenType');
const { setIsMonacoLoaded } = useContext(CodeEditorLoadingContext);
const activeModel = useMemo<ModelSettings>(
() => (tokenType === LogtoJwtTokenPath.AccessToken ? userJwtFile : machineToMachineJwtFile),
[tokenType]
);
const onMountHandler = useCallback(() => {
setIsMonacoLoaded(true);
}, [setIsMonacoLoaded]);
return (
<Card className={styles.codePanel}>
<div className={styles.cardTitle}>
@ -56,6 +63,7 @@ function ScriptSection() {
onChange(newValue);
}}
onMountHandler={onMountHandler}
/>
)}
/>

View file

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

View file

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