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:
parent
e8ac64c23c
commit
abb2c9f649
8 changed files with 140 additions and 8 deletions
|
@ -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,
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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)}>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -44,3 +44,7 @@
|
|||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue