+ );
+}
+
+export default Dashboard;
diff --git a/packages/console/src/pages/CustomizeJwtDetails/MonacoCodeEditor/config.ts b/packages/console/src/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor/config.ts
similarity index 82%
rename from packages/console/src/pages/CustomizeJwtDetails/MonacoCodeEditor/config.ts
rename to packages/console/src/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor/config.ts
index fbf03cecb..075776f7b 100644
--- a/packages/console/src/pages/CustomizeJwtDetails/MonacoCodeEditor/config.ts
+++ b/packages/console/src/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor/config.ts
@@ -7,6 +7,13 @@ export const logtoDarkTheme: IStandaloneThemeData = {
base: 'vs-dark',
inherit: true,
rules: [],
+ colors: {
+ 'editor.background': '#090613', // :token/code/code-bg
+ },
+};
+
+export const logtoLightTheme: IStandaloneThemeData = {
+ ...logtoDarkTheme,
colors: {
'editor.background': '#181133', // :token/code/code-bg
},
diff --git a/packages/console/src/pages/CustomizeJwtDetails/MonacoCodeEditor/index.module.scss b/packages/console/src/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor/index.module.scss
similarity index 86%
rename from packages/console/src/pages/CustomizeJwtDetails/MonacoCodeEditor/index.module.scss
rename to packages/console/src/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor/index.module.scss
index 54c622df8..b53083ef8 100644
--- a/packages/console/src/pages/CustomizeJwtDetails/MonacoCodeEditor/index.module.scss
+++ b/packages/console/src/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor/index.module.scss
@@ -1,9 +1,6 @@
@use '@/scss/underscore' as _;
-@use '@/scss/code-editor' as codeEditor;
.codeEditor {
- @include codeEditor.color;
- @include codeEditor.font;
position: relative;
display: flex;
flex-direction: column;
@@ -37,7 +34,7 @@
&.active,
&:hover {
color: var(--color-code-white);
- background-color: var(--color-code-tab-active-bg);
+ background-color: var(--color-code-dark-bg-focused);
border-radius: 8px;
}
}
@@ -54,5 +51,10 @@
.editorContainer {
position: relative;
flex-grow: 1;
+
+ &.dashboardOpen {
+ flex-grow: 0;
+ height: 50%;
+ }
}
}
diff --git a/packages/console/src/pages/CustomizeJwtDetails/MonacoCodeEditor/index.tsx b/packages/console/src/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor/index.tsx
similarity index 74%
rename from packages/console/src/pages/CustomizeJwtDetails/MonacoCodeEditor/index.tsx
rename to packages/console/src/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor/index.tsx
index 377810106..2f0bd1793 100644
--- a/packages/console/src/pages/CustomizeJwtDetails/MonacoCodeEditor/index.tsx
+++ b/packages/console/src/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor/index.tsx
@@ -1,17 +1,21 @@
-import { Editor, useMonaco, type BeforeMount, type OnMount } from '@monaco-editor/react';
+import { Theme } from '@logto/schemas';
+import { Editor, useMonaco, type OnMount } from '@monaco-editor/react';
import { type Nullable } from '@silverhand/essentials';
import classNames from 'classnames';
-import { useCallback, useEffect, useMemo, useRef } from 'react';
+import { useCallback, useEffect, useMemo, useRef, type ReactNode } from 'react';
import CopyToClipboard from '@/ds-components/CopyToClipboard';
+import useTheme from '@/hooks/use-theme';
import { onKeyDownHandler } from '@/utils/a11y';
import CodeRestoreButton from './ActionButton/CodeRestoreButton.js';
-import { defaultOptions, logtoDarkTheme } from './config.js';
+import DashBoard, { type Props as DashboardProps } from './Dashboard';
+import { defaultOptions, logtoDarkTheme, logtoLightTheme } from './config.js';
import * as styles from './index.module.scss';
import type { IStandaloneCodeEditor, ModelSettings } from './type.js';
import useEditorHeight from './use-editor-height.js';
+export type { Props as DashboardProps } from './Dashboard';
export type { ModelControl, ModelSettings } from './type.js';
type ActionButtonType = 'restore' | 'copy';
@@ -26,6 +30,8 @@ type Props = {
environmentVariablesDefinition?: string;
onChange?: (value: string | undefined) => void;
onMountHandler?: (editor: IStandaloneCodeEditor) => void;
+ actionButtons?: ReactNode;
+ dashboard?: DashboardProps;
};
/**
* Monaco code editor component.
@@ -38,6 +44,8 @@ type Props = {
* @param {string} prop.value - The value of the code editor for the current active model.
* @param {(value: string | undefined) => void} prop.onChange - The callback function to handle the value change of the code editor.
* @param {string} [prop.environmentVariablesDefinition] - The environment variables type definition for the script section.
+ * @param {ReactNode} [actionButtons] - Additional action buttons shown on the header
+ * @param {DashboardProps} [dashboard] - The dashboard component shown at the bottom of the editor.
*
* @returns
*/
@@ -51,9 +59,12 @@ function MonacoCodeEditor({
setActiveModel,
onChange,
onMountHandler,
+ actionButtons,
+ dashboard,
}: Props) {
const monaco = useMonaco();
const editorRef = useRef>(null);
+ const theme = useTheme();
const activeModel = useMemo(
() => activeModelName && models.find((model) => model.name === activeModelName),
@@ -63,8 +74,9 @@ function MonacoCodeEditor({
const isMultiModals = useMemo(() => models.length > 1, [models]);
// Get the container ref and the editor height
- const { containerRef, headerRef, editorHeight } = useEditorHeight();
+ const { containerRef, editorHeight } = useEditorHeight();
+ // Handle editor extraLibs and language compile settings
useEffect(() => {
// Monaco will be ready after the editor is mounted, useEffect will be called after the monaco is ready
if (!monaco || !activeModel) {
@@ -84,18 +96,27 @@ function MonacoCodeEditor({
'environmentVariables.d.ts'
);
}
+
+ if (activeModel.language === 'typescript') {
+ // Set the typescript compiler options
+ monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
+ allowNonTsExtensions: true,
+ strictNullChecks: true,
+ });
+ }
}, [activeModel, monaco, environmentVariablesDefinition]);
- const handleEditorWillMount = useCallback((monaco) => {
- // Register the new logto theme
- monaco.editor.defineTheme('logto-dark', logtoDarkTheme);
+ // Handle the editor theme settings
+ useEffect(() => {
+ // Monaco will be ready after the editor is mounted, useEffect will be called after the monaco is ready
+ if (!monaco) {
+ return;
+ }
- // Set the typescript compiler options
- monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
- allowNonTsExtensions: true,
- strictNullChecks: true,
- });
- }, []);
+ const editorTheme = theme === Theme.Light ? logtoLightTheme : logtoDarkTheme;
+
+ monaco.editor.defineTheme('logto-dark', editorTheme);
+ }, [monaco, theme]);
const handleEditorDidMount = useCallback(
(editor) => {
@@ -107,8 +128,8 @@ function MonacoCodeEditor({
);
return (
-
-
+
+
{models.map(({ name, title, icon }) => (
{
- if (activeModel) {
+ if (activeModel && value !== activeModel.defaultValue) {
onChange?.(activeModel.defaultValue);
}
}}
@@ -147,9 +168,13 @@ function MonacoCodeEditor({
{enabledActions.includes('copy') && (
)}
+ {actionButtons}
-
+
{activeModel && (
)}
+ {dashboard && }
);
}
diff --git a/packages/console/src/pages/CustomizeJwtDetails/MonacoCodeEditor/type.ts b/packages/console/src/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor/type.ts
similarity index 100%
rename from packages/console/src/pages/CustomizeJwtDetails/MonacoCodeEditor/type.ts
rename to packages/console/src/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor/type.ts
diff --git a/packages/console/src/pages/CustomizeJwtDetails/MonacoCodeEditor/use-editor-height.ts b/packages/console/src/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor/use-editor-height.ts
similarity index 74%
rename from packages/console/src/pages/CustomizeJwtDetails/MonacoCodeEditor/use-editor-height.ts
rename to packages/console/src/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor/use-editor-height.ts
index 1c75d1694..b3a1045fc 100644
--- a/packages/console/src/pages/CustomizeJwtDetails/MonacoCodeEditor/use-editor-height.ts
+++ b/packages/console/src/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor/use-editor-height.ts
@@ -1,23 +1,18 @@
-import { useRef, useState, useLayoutEffect } from 'react';
+import { useLayoutEffect, useRef, useState } from 'react';
// Recalculate the height of the editor when the container size changes
// This is to avoid the code editor's height shaking when the content is updated.
// @see {@link https://github.com/react-monaco-editor/react-monaco-editor/issues/391}
const useEditorHeight = () => {
const containerRef = useRef(null);
- const headerRef = useRef(null);
const [editorHeight, setEditorHeight] = useState('100%');
const safeArea = 16;
useLayoutEffect(() => {
const handleResize = () => {
- const safeAreaHeight = headerRef.current?.clientHeight
- ? headerRef.current.clientHeight + safeArea
- : safeArea;
-
if (containerRef.current) {
- setEditorHeight(containerRef.current.clientHeight - safeAreaHeight);
+ setEditorHeight(containerRef.current.clientHeight - safeArea);
}
};
@@ -34,7 +29,7 @@ const useEditorHeight = () => {
};
}, []);
- return { containerRef, headerRef, editorHeight };
+ return { containerRef, editorHeight };
};
export default useEditorHeight;
diff --git a/packages/console/src/pages/CustomizeJwtDetails/MainContent/ScriptSection/ErrorContent/index.module.scss b/packages/console/src/pages/CustomizeJwtDetails/MainContent/ScriptSection/ErrorContent/index.module.scss
new file mode 100644
index 000000000..d9f27a639
--- /dev/null
+++ b/packages/console/src/pages/CustomizeJwtDetails/MainContent/ScriptSection/ErrorContent/index.module.scss
@@ -0,0 +1,6 @@
+@use '@/scss/underscore' as _;
+
+.error {
+ margin: _.unit(4) 0;
+ color: var(--color-error-60);
+}
diff --git a/packages/console/src/pages/CustomizeJwtDetails/MainContent/ScriptSection/ErrorContent/index.tsx b/packages/console/src/pages/CustomizeJwtDetails/MainContent/ScriptSection/ErrorContent/index.tsx
new file mode 100644
index 000000000..5fdb26658
--- /dev/null
+++ b/packages/console/src/pages/CustomizeJwtDetails/MainContent/ScriptSection/ErrorContent/index.tsx
@@ -0,0 +1,28 @@
+import { type TestResultData } from '@/pages/CustomizeJwtDetails/MainContent/ScriptSection/use-test-handler';
+
+import * as styles from './index.module.scss';
+
+type Props = {
+ testResult: TestResultData;
+};
+
+function ErrorContent({ testResult }: Props) {
+ return (
+
+ {testResult.error && (
+
+ {'Error: \n'}
+ {testResult.error}
+
+ )}
+ {testResult.payload && (
+
+ {'JWT Payload: \n'}
+ {testResult.payload}
+
+ )}
+
+ );
+}
+
+export default ErrorContent;
diff --git a/packages/console/src/pages/CustomizeJwtDetails/MainContent/ScriptSection/index.module.scss b/packages/console/src/pages/CustomizeJwtDetails/MainContent/ScriptSection/index.module.scss
index fd959ef67..0e6e77891 100644
--- a/packages/console/src/pages/CustomizeJwtDetails/MainContent/ScriptSection/index.module.scss
+++ b/packages/console/src/pages/CustomizeJwtDetails/MainContent/ScriptSection/index.module.scss
@@ -1,17 +1,18 @@
@use '@/scss/underscore' as _;
-
-.codePanel {
+.scripeSection {
position: relative;
- display: flex;
- flex-direction: column;
+ min-width: 50%;
- .cardTitle {
- font: var(--font-label-2);
- margin-bottom: _.unit(3);
+ .fixHeightWrapper {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
}
- .flexGrow {
+ .codeEditor {
+ min-height: 200px;
flex-grow: 1;
}
}
diff --git a/packages/console/src/pages/CustomizeJwtDetails/MainContent/ScriptSection/index.tsx b/packages/console/src/pages/CustomizeJwtDetails/MainContent/ScriptSection/index.tsx
index d1d488f8e..421fdc552 100644
--- a/packages/console/src/pages/CustomizeJwtDetails/MainContent/ScriptSection/index.tsx
+++ b/packages/console/src/pages/CustomizeJwtDetails/MainContent/ScriptSection/index.tsx
@@ -1,10 +1,17 @@
/* Code Editor for the custom JWT claims script. */
import { LogtoJwtTokenPath } from '@logto/schemas';
+import classNames from 'classnames';
import { useCallback, useContext, useMemo } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import RunIcon from '@/assets/icons/start.svg';
+import Button from '@/ds-components/Button';
import { CodeEditorLoadingContext } from '@/pages/CustomizeJwtDetails/CodeEditorLoadingContext';
-import MonacoCodeEditor, { type ModelSettings } from '@/pages/CustomizeJwtDetails/MonacoCodeEditor';
+import MonacoCodeEditor, {
+ type DashboardProps,
+ type ModelSettings,
+} from '@/pages/CustomizeJwtDetails/MainContent/MonacoCodeEditor';
import { type JwtCustomizerForm } from '@/pages/CustomizeJwtDetails/type';
import {
accessTokenJwtCustomizerModel,
@@ -12,9 +19,12 @@ import {
} from '@/pages/CustomizeJwtDetails/utils/config';
import { buildEnvironmentVariablesTypeDefinition } from '@/pages/CustomizeJwtDetails/utils/type-definitions';
+import ErrorContent from './ErrorContent';
import * as styles from './index.module.scss';
+import useTestHandler from './use-test-handler';
function ScriptSection() {
+ const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { watch, control } = useFormContext();
const tokenType = watch('tokenType');
@@ -46,35 +56,64 @@ function ScriptSection() {
setIsMonacoLoaded(true);
}, [setIsMonacoLoaded]);
- return (
- (
- {
- // If the value is the same as the default code and the original form script value is undefined, reset the value to undefined as well
- if (newValue === activeModel.defaultValue && !defaultValues?.script) {
- onChange('');
- return;
- }
+ // Test handler
+ const { onTestHandler, setTestResult, isLoading, testResult } = useTestHandler();
- // Input value should not be undefined for react-hook-form @see https://react-hook-form.com/docs/usecontroller/controller
- onChange(newValue ?? '');
- }}
- onMountHandler={onMountHandler}
+ const dashBoardProps = useMemo(() => {
+ if (!testResult) {
+ return;
+ }
+
+ return {
+ title: t('jwt_claims.tester.result_title'),
+ content: ,
+ onClose: () => {
+ setTestResult(undefined);
+ },
+ };
+ }, [setTestResult, t, testResult]);
+
+ return (
+
+
+ (
+ }
+ size="small"
+ title="jwt_claims.tester.run_button"
+ type="primary"
+ isLoading={isLoading}
+ onClick={onTestHandler}
+ />
+ }
+ dashboard={dashBoardProps}
+ onChange={(newValue) => {
+ // If the value is the same as the default code and the original form script value is undefined, reset the value to undefined as well
+ if (newValue === activeModel.defaultValue && !defaultValues?.script) {
+ onChange('');
+ return;
+ }
+
+ // Input value should not be undefined for react-hook-form @see https://react-hook-form.com/docs/usecontroller/controller
+ onChange(newValue ?? '');
+ }}
+ onMountHandler={onMountHandler}
+ />
+ )}
/>
- )}
- />
+
-
- {/**
- * We use useFieldArray hook to manage the list of environment variables in the EnvironmentVariablesField component.
- * useFieldArray will read the form context and return the necessary methods and values to manage the list.
- * The form context will mutate when the tokenType changes. It will provide different form state and methods based on the tokenType. (@see JwtClaims component.)
- * However, the form context/controller updates did not trigger a re-render of the useFieldArray hook. (@see {@link https://github.com/react-hook-form/react-hook-form/blob/master/src/useFieldArray.ts#L95})
- *
- * This cause issues when the tokenType changes and the environment variables list is not rerendered. The form state will be stale.
- * In order to fix this, we need to re-render the EnvironmentVariablesField component when the tokenType changes.
- * Achieve this by adding a key to the EnvironmentVariablesField component. Force a re-render when the tokenType changes.
- */}
-
+ {
+ setExpendCard(expand ? CardType.EnvironmentVariables : undefined);
+ }}
+ >
+