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:
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';
|
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>
|
||||||
|
|
|
@ -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)}>
|
||||||
|
|
|
@ -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. */
|
/* 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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -44,3 +44,7 @@
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue