mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-29 12:30:42 +00:00
Compare commits
2 Commits
feat/fga-r
...
refactor/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d7f068085 | ||
|
|
44bbf847dd |
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import MEditor, { EditorProps } from '@monaco-editor/react';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { EditorProps } from '@monaco-editor/react';
|
||||
import MonacoEditor from 'lib/monaco/MonacoEditor';
|
||||
|
||||
function Editor({
|
||||
value,
|
||||
@@ -10,8 +10,6 @@ function Editor({
|
||||
height,
|
||||
options,
|
||||
}: MEditorProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const onChangeHandler = (newValue?: string): void => {
|
||||
if (readOnly) {
|
||||
return;
|
||||
@@ -28,8 +26,7 @@ function Editor({
|
||||
);
|
||||
|
||||
return (
|
||||
<MEditor
|
||||
theme={isDarkMode ? 'vs-dark' : 'vs-light'}
|
||||
<MonacoEditor
|
||||
language={language}
|
||||
value={value}
|
||||
options={editorOptions}
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import MEditor from '@monaco-editor/react';
|
||||
import { Play, RotateCcw } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import MonacoEditor from 'lib/monaco/MonacoEditor';
|
||||
|
||||
import styles from './TestTab.module.scss';
|
||||
import JsonEditorSkeleton from './JsonEditorSkeleton';
|
||||
import TestResult from './TestResult';
|
||||
import {
|
||||
defineSignozJsonTheme,
|
||||
SIGNOZ_JSON_THEME_DARK,
|
||||
SIGNOZ_JSON_THEME_LIGHT,
|
||||
} from './jsonEditorTheme';
|
||||
import { UseTestSpanMapper } from './useTestSpanMapper';
|
||||
|
||||
interface TestTabProps {
|
||||
@@ -32,7 +26,6 @@ function TestTab({ spanTest }: TestTabProps): JSX.Element {
|
||||
error,
|
||||
validationError,
|
||||
} = spanTest;
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
function renderResults(): JSX.Element {
|
||||
if (error) {
|
||||
@@ -122,14 +115,12 @@ function TestTab({ spanTest }: TestTabProps): JSX.Element {
|
||||
data-testid="test-span-input"
|
||||
aria-label="Sample span JSON"
|
||||
>
|
||||
<MEditor
|
||||
<MonacoEditor
|
||||
language="json"
|
||||
value={input}
|
||||
onChange={(value): void => setInput(value ?? '')}
|
||||
height="100%"
|
||||
loading={<JsonEditorSkeleton />}
|
||||
theme={isDarkMode ? SIGNOZ_JSON_THEME_DARK : SIGNOZ_JSON_THEME_LIGHT}
|
||||
beforeMount={defineSignozJsonTheme}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 12,
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
//TODO: This is a local theme for now, but ideally for the editor as well. Just like prettyViewer, we have a theme. We should have a theme for the editor as well.
|
||||
import { Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
|
||||
export const SIGNOZ_JSON_THEME_DARK = 'signoz-attr-mapping-json-dark';
|
||||
export const SIGNOZ_JSON_THEME_LIGHT = 'signoz-attr-mapping-json-light';
|
||||
|
||||
const SHARED_THEME = {
|
||||
inherit: true,
|
||||
colors: {
|
||||
'editor.background': '#00000000', // transparent — inherit the panel bg
|
||||
},
|
||||
fontFamily: 'SF Mono, Geist Mono, Fira Code, monospace',
|
||||
fontSize: 12,
|
||||
fontWeight: 'normal',
|
||||
lineHeight: 18,
|
||||
letterSpacing: -0.06,
|
||||
};
|
||||
|
||||
export function defineSignozJsonTheme(monaco: Monaco): void {
|
||||
monaco.editor.defineTheme(SIGNOZ_JSON_THEME_DARK, {
|
||||
...SHARED_THEME,
|
||||
base: 'vs-dark',
|
||||
rules: [
|
||||
{ token: 'string.key.json', foreground: Color.BG_ROBIN_400 },
|
||||
{ token: 'string.value.json', foreground: Color.BG_VANILLA_400 },
|
||||
],
|
||||
});
|
||||
|
||||
monaco.editor.defineTheme(SIGNOZ_JSON_THEME_LIGHT, {
|
||||
...SHARED_THEME,
|
||||
base: 'vs',
|
||||
rules: [
|
||||
{ token: 'string.key.json', foreground: Color.BG_ROBIN_500 },
|
||||
{ token: 'string.value.json', foreground: Color.BG_INK_400 },
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
import { ChangeEvent, useCallback } from 'react';
|
||||
import MEditor, { Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Input } from 'antd';
|
||||
import { LEGEND } from 'constants/global';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import MonacoEditor from 'lib/monaco/MonacoEditor';
|
||||
import { IClickHouseQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { getFormatedLegend } from 'utils/getFormatedLegend';
|
||||
@@ -71,22 +69,6 @@ function ClickHouseQueryBuilder({
|
||||
[handleUpdateQuery],
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
function setEditorTheme(monaco: Monaco): void {
|
||||
monaco.editor.defineTheme('my-theme', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'string.key.json', foreground: Color.BG_VANILLA_400 },
|
||||
{ token: 'string.value.json', foreground: Color.BG_ROBIN_400 },
|
||||
],
|
||||
colors: {
|
||||
'editor.background': Color.BG_INK_300,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryHeader
|
||||
name={queryData?.name}
|
||||
@@ -95,15 +77,13 @@ function ClickHouseQueryBuilder({
|
||||
onDelete={handleRemoveQuery}
|
||||
deletable={deletable}
|
||||
>
|
||||
<MEditor
|
||||
<MonacoEditor
|
||||
language="sql"
|
||||
height="200px"
|
||||
onChange={handleUpdateEditor}
|
||||
value={queryData?.query}
|
||||
onMount={(_, monaco): void => {
|
||||
document.fonts.ready.then(() => {
|
||||
monaco.editor.remeasureFonts();
|
||||
});
|
||||
void document.fonts.ready.then(() => monaco.editor.remeasureFonts());
|
||||
}}
|
||||
options={{
|
||||
scrollbar: {
|
||||
@@ -115,8 +95,6 @@ function ClickHouseQueryBuilder({
|
||||
fontSize: 14,
|
||||
fontFamily: 'Space Mono',
|
||||
}}
|
||||
theme={isDarkMode ? 'my-theme' : 'light'}
|
||||
beforeMount={setEditorTheme}
|
||||
/>
|
||||
<Input
|
||||
onChange={handleUpdateInput}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import MEditor, { Monaco, OnMount } from '@monaco-editor/react';
|
||||
import { Monaco, OnMount } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Check, Copy } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
@@ -15,12 +15,9 @@ import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import type { AuthtypesTransactionGroupDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { EDITABLE_EDITOR_OPTIONS } from 'lib/monaco/editorOptions';
|
||||
import MonacoEditor from 'lib/monaco/MonacoEditor';
|
||||
|
||||
import {
|
||||
defineJsonTheme,
|
||||
EDITABLE_EDITOR_OPTIONS,
|
||||
JSON_THEME_DARK,
|
||||
} from '../../monaco.config';
|
||||
import {
|
||||
transformResourcePermissionsToTransactionGroups,
|
||||
transformTransactionGroupsToResourcePermissions,
|
||||
@@ -34,7 +31,7 @@ import {
|
||||
import styles from './JsonEditor.module.scss';
|
||||
import { JsonEditorProps, JsonEditorRef } from './JsonEditor.types';
|
||||
|
||||
type MonacoEditor = Parameters<OnMount>[0];
|
||||
type MonacoEditorInstance = Parameters<OnMount>[0];
|
||||
|
||||
const JsonEditor = forwardRef<JsonEditorRef, JsonEditorProps>(
|
||||
function JsonEditor({ resources, mode, onChange, onValidityChange }, ref) {
|
||||
@@ -50,7 +47,7 @@ const JsonEditor = forwardRef<JsonEditorRef, JsonEditorProps>(
|
||||
});
|
||||
const prevModeRef = useRef(mode);
|
||||
const completionDisposableRef = useRef<{ dispose(): void } | null>(null);
|
||||
const editorRef = useRef<MonacoEditor | null>(null);
|
||||
const editorRef = useRef<MonacoEditorInstance | null>(null);
|
||||
const markersListenerRef = useRef<{ dispose(): void } | null>(null);
|
||||
|
||||
const hasError = parseError !== null || schemaErrors.length > 0;
|
||||
@@ -100,7 +97,6 @@ const JsonEditor = forwardRef<JsonEditorRef, JsonEditorProps>(
|
||||
);
|
||||
|
||||
const configureMonaco = useCallback((monaco: Monaco): void => {
|
||||
defineJsonTheme(monaco);
|
||||
registerJsonSchema(monaco);
|
||||
completionDisposableRef.current = registerCompletionProvider(monaco);
|
||||
}, []);
|
||||
@@ -184,7 +180,7 @@ const JsonEditor = forwardRef<JsonEditorRef, JsonEditorProps>(
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<MEditor
|
||||
<MonacoEditor
|
||||
value={jsonBuffer}
|
||||
language="json"
|
||||
path={ROLE_PERMISSIONS_MODEL_PATH}
|
||||
@@ -192,7 +188,6 @@ const JsonEditor = forwardRef<JsonEditorRef, JsonEditorProps>(
|
||||
onChange={handleEditorChange}
|
||||
onMount={handleEditorMount}
|
||||
height="100%"
|
||||
theme={isDarkMode ? JSON_THEME_DARK : 'light'}
|
||||
beforeMount={configureMonaco}
|
||||
/>
|
||||
</div>
|
||||
@@ -225,7 +220,11 @@ const JsonEditor = forwardRef<JsonEditorRef, JsonEditorProps>(
|
||||
className={styles.jsonEditorErrorMessage}
|
||||
>
|
||||
{schemaErrors[0]}
|
||||
{schemaErrors.length > 1 && ` (+${schemaErrors.length - 1} more)`}
|
||||
{schemaErrors.length > 1 && (
|
||||
<span className="translate-safe">
|
||||
{` (+${schemaErrors.length - 1} more)`}
|
||||
</span>
|
||||
)}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import MEditor from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Check, Copy } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { READONLY_EDITOR_OPTIONS } from 'lib/monaco/editorOptions';
|
||||
import MonacoEditor from 'lib/monaco/MonacoEditor';
|
||||
|
||||
import {
|
||||
defineJsonTheme,
|
||||
JSON_THEME_DARK,
|
||||
READONLY_EDITOR_OPTIONS,
|
||||
} from '../monaco.config';
|
||||
import { RolePermissionsData } from '../types';
|
||||
import { transformResourcePermissionsToTransactionGroups } from '../hooks/useRolePermissions';
|
||||
|
||||
@@ -79,13 +75,11 @@ function ReadOnlyJsonViewer({
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<MEditor
|
||||
<MonacoEditor
|
||||
value={jsonContent}
|
||||
language="json"
|
||||
options={READONLY_EDITOR_OPTIONS}
|
||||
height="100%"
|
||||
theme={isDarkMode ? JSON_THEME_DARK : 'light'}
|
||||
beforeMount={defineJsonTheme}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
25
frontend/src/lib/monaco/MonacoEditor.tsx
Normal file
25
frontend/src/lib/monaco/MonacoEditor.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useCallback } from 'react';
|
||||
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||
|
||||
import { EDITOR_THEME, registerEditorTheme } from './editorTheme';
|
||||
|
||||
export type MonacoEditorProps = Omit<EditorProps, 'theme'>;
|
||||
|
||||
function MonacoEditor({
|
||||
beforeMount,
|
||||
...props
|
||||
}: MonacoEditorProps): JSX.Element {
|
||||
const handleBeforeMount = useCallback(
|
||||
(monaco: Monaco): void => {
|
||||
registerEditorTheme(monaco);
|
||||
beforeMount?.(monaco);
|
||||
},
|
||||
[beforeMount],
|
||||
);
|
||||
|
||||
return (
|
||||
<MEditor {...props} theme={EDITOR_THEME} beforeMount={handleBeforeMount} />
|
||||
);
|
||||
}
|
||||
|
||||
export default MonacoEditor;
|
||||
58
frontend/src/lib/monaco/__tests__/MonacoEditor.test.tsx
Normal file
58
frontend/src/lib/monaco/__tests__/MonacoEditor.test.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { EditorProps, Monaco } from '@monaco-editor/react';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { EDITOR_THEME, registerEditorTheme } from '../editorTheme';
|
||||
import MonacoEditor from '../MonacoEditor';
|
||||
|
||||
const editorProps = jest.fn();
|
||||
|
||||
jest.mock('@monaco-editor/react', () => ({
|
||||
__esModule: true,
|
||||
default: (props: EditorProps): JSX.Element => {
|
||||
editorProps(props);
|
||||
return <div data-testid="monaco" />;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../editorTheme', () => ({
|
||||
EDITOR_THEME: 'signoz',
|
||||
registerEditorTheme: jest.fn(),
|
||||
}));
|
||||
|
||||
function lastProps(): EditorProps {
|
||||
return editorProps.mock.calls[editorProps.mock.calls.length - 1][0];
|
||||
}
|
||||
|
||||
describe('MonacoEditor', () => {
|
||||
beforeEach(() => {
|
||||
editorProps.mockClear();
|
||||
jest.mocked(registerEditorTheme).mockClear();
|
||||
});
|
||||
|
||||
it('renders with the shared theme and forwards the other props', () => {
|
||||
render(<MonacoEditor language="json" value="{}" height="100%" />);
|
||||
|
||||
expect(lastProps()).toMatchObject({
|
||||
theme: EDITOR_THEME,
|
||||
language: 'json',
|
||||
value: '{}',
|
||||
height: '100%',
|
||||
});
|
||||
});
|
||||
|
||||
it('registers the theme before the caller sets up monaco', () => {
|
||||
const calls: string[] = [];
|
||||
jest
|
||||
.mocked(registerEditorTheme)
|
||||
.mockImplementation(() => calls.push('theme'));
|
||||
const beforeMount = jest.fn(() => calls.push('caller'));
|
||||
const monaco = {} as Monaco;
|
||||
|
||||
render(<MonacoEditor beforeMount={beforeMount} />);
|
||||
lastProps().beforeMount?.(monaco);
|
||||
|
||||
expect(registerEditorTheme).toHaveBeenCalledWith(monaco);
|
||||
expect(beforeMount).toHaveBeenCalledWith(monaco);
|
||||
expect(calls).toStrictEqual(['theme', 'caller']);
|
||||
});
|
||||
});
|
||||
138
frontend/src/lib/monaco/__tests__/editorTheme.test.ts
Normal file
138
frontend/src/lib/monaco/__tests__/editorTheme.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { Monaco } from '@monaco-editor/react';
|
||||
|
||||
type EditorThemeModule = typeof import('../editorTheme');
|
||||
|
||||
const DARK_TOKENS = {
|
||||
'--l1-background': '#0b0c0e',
|
||||
'--l1-foreground': '#eceef2',
|
||||
'--bg-vanilla-400': '#c0c1c3',
|
||||
'--bg-robin-400': '#7190f9',
|
||||
};
|
||||
|
||||
const LIGHT_TOKENS = {
|
||||
'--l1-background': '#ffffff',
|
||||
'--l1-foreground': '#121317',
|
||||
'--bg-vanilla-400': '#c0c1c3',
|
||||
'--bg-robin-400': '#7190f9',
|
||||
};
|
||||
|
||||
function setTokens(tokens: Record<string, string>): void {
|
||||
Object.entries(tokens).forEach(([name, value]) => {
|
||||
document.body.style.setProperty(name, value);
|
||||
});
|
||||
}
|
||||
|
||||
function createMonaco(): { monaco: Monaco; defineTheme: jest.Mock } {
|
||||
const defineTheme = jest.fn();
|
||||
return {
|
||||
monaco: { editor: { defineTheme } } as unknown as Monaco,
|
||||
defineTheme,
|
||||
};
|
||||
}
|
||||
|
||||
function loadModule(): EditorThemeModule {
|
||||
let mod: EditorThemeModule | undefined;
|
||||
jest.isolateModules(() => {
|
||||
mod = jest.requireActual<EditorThemeModule>('../editorTheme');
|
||||
});
|
||||
return mod as EditorThemeModule;
|
||||
}
|
||||
|
||||
async function flushMutations(): Promise<void> {
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
describe('registerEditorTheme', () => {
|
||||
afterEach(() => {
|
||||
document.body.removeAttribute('style');
|
||||
document.body.className = '';
|
||||
});
|
||||
|
||||
it('defines a dark theme with the JSON token colours on a dark surface', () => {
|
||||
setTokens(DARK_TOKENS);
|
||||
const { registerEditorTheme, EDITOR_THEME } = loadModule();
|
||||
const { monaco, defineTheme } = createMonaco();
|
||||
|
||||
registerEditorTheme(monaco);
|
||||
|
||||
expect(defineTheme).toHaveBeenCalledTimes(1);
|
||||
expect(defineTheme).toHaveBeenCalledWith(EDITOR_THEME, {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'string.key.json', foreground: 'c0c1c3' },
|
||||
{ token: 'string.value.json', foreground: '7190f9' },
|
||||
],
|
||||
colors: {
|
||||
'editor.background': '#0b0c0e',
|
||||
'editor.foreground': '#eceef2',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the vs palette on a light surface', () => {
|
||||
setTokens(LIGHT_TOKENS);
|
||||
const { registerEditorTheme } = loadModule();
|
||||
const { monaco, defineTheme } = createMonaco();
|
||||
|
||||
registerEditorTheme(monaco);
|
||||
|
||||
expect(defineTheme).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
base: 'vs',
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#ffffff',
|
||||
'editor.foreground': '#121317',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('redefines the theme when the body class switches the tokens', async () => {
|
||||
setTokens(DARK_TOKENS);
|
||||
const { registerEditorTheme } = loadModule();
|
||||
const { monaco, defineTheme } = createMonaco();
|
||||
registerEditorTheme(monaco);
|
||||
|
||||
setTokens(LIGHT_TOKENS);
|
||||
document.body.classList.add('lightMode');
|
||||
await flushMutations();
|
||||
|
||||
expect(defineTheme).toHaveBeenCalledTimes(2);
|
||||
expect(defineTheme).toHaveBeenLastCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ base: 'vs' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores body class changes that leave the tokens alone', async () => {
|
||||
setTokens(DARK_TOKENS);
|
||||
const { registerEditorTheme } = loadModule();
|
||||
const { monaco, defineTheme } = createMonaco();
|
||||
registerEditorTheme(monaco);
|
||||
|
||||
document.body.classList.add('ant-scrolling-effect');
|
||||
await flushMutations();
|
||||
|
||||
expect(defineTheme).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('watches the body once however many editors mount', async () => {
|
||||
const observe = jest.spyOn(MutationObserver.prototype, 'observe');
|
||||
setTokens(DARK_TOKENS);
|
||||
const { registerEditorTheme } = loadModule();
|
||||
const { monaco, defineTheme } = createMonaco();
|
||||
registerEditorTheme(monaco);
|
||||
registerEditorTheme(monaco);
|
||||
|
||||
setTokens(LIGHT_TOKENS);
|
||||
document.body.classList.add('lightMode');
|
||||
await flushMutations();
|
||||
|
||||
expect(observe).toHaveBeenCalledTimes(1);
|
||||
expect(defineTheme).toHaveBeenCalledTimes(2);
|
||||
observe.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -1,24 +1,7 @@
|
||||
import { Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
|
||||
type EditorOptions = Parameters<Monaco['editor']['create']>[1];
|
||||
|
||||
export const JSON_THEME_DARK = 'json-theme-dark';
|
||||
|
||||
export function defineJsonTheme(monaco: Monaco): void {
|
||||
monaco.editor.defineTheme(JSON_THEME_DARK, {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'string.key.json', foreground: Color.BG_VANILLA_400 },
|
||||
{ token: 'string.value.json', foreground: Color.BG_ROBIN_400 },
|
||||
],
|
||||
colors: {
|
||||
'editor.background': Color.BG_INK_400,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const BASE_EDITOR_OPTIONS: EditorOptions = {
|
||||
automaticLayout: true,
|
||||
wordWrap: 'on',
|
||||
78
frontend/src/lib/monaco/editorTheme.ts
Normal file
78
frontend/src/lib/monaco/editorTheme.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Monaco } from '@monaco-editor/react';
|
||||
|
||||
export const EDITOR_THEME = 'signoz';
|
||||
|
||||
let definedBackground: string | undefined;
|
||||
let bodyClassObserver: MutationObserver | undefined;
|
||||
|
||||
function token(name: string): string {
|
||||
return getComputedStyle(document.body)
|
||||
.getPropertyValue(name)
|
||||
.trim()
|
||||
.replace('#', '');
|
||||
}
|
||||
|
||||
function isDark(hex: string): boolean {
|
||||
if (hex.length < 6) {
|
||||
return true;
|
||||
}
|
||||
const r = parseInt(hex.slice(0, 2), 16);
|
||||
const g = parseInt(hex.slice(2, 4), 16);
|
||||
const b = parseInt(hex.slice(4, 6), 16);
|
||||
return 0.299 * r + 0.587 * g + 0.114 * b < 128;
|
||||
}
|
||||
|
||||
function defineEditorTheme(monaco: Monaco): void {
|
||||
const background = token('--l1-background');
|
||||
if (background === definedBackground) {
|
||||
return;
|
||||
}
|
||||
definedBackground = background;
|
||||
|
||||
const foreground = token('--l1-foreground');
|
||||
const keyColor = token('--bg-vanilla-400');
|
||||
const valueColor = token('--bg-robin-400');
|
||||
|
||||
// Both token colours are picked to read on a dark surface, so on a light one
|
||||
// they wash out, the keys worst of all. Monaco's own `vs` palette is built
|
||||
// for that surface, so let it keep its colours there, as the role viewer does.
|
||||
const rules: { token: string; foreground: string }[] = [];
|
||||
if (isDark(background)) {
|
||||
if (keyColor) {
|
||||
rules.push({ token: 'string.key.json', foreground: keyColor });
|
||||
}
|
||||
if (valueColor) {
|
||||
rules.push({ token: 'string.value.json', foreground: valueColor });
|
||||
}
|
||||
}
|
||||
|
||||
const colors: Record<string, string> = {};
|
||||
if (background) {
|
||||
colors['editor.background'] = `#${background}`;
|
||||
}
|
||||
if (foreground) {
|
||||
colors['editor.foreground'] = `#${foreground}`;
|
||||
}
|
||||
|
||||
monaco.editor.defineTheme(EDITOR_THEME, {
|
||||
base: isDark(background) ? 'vs-dark' : 'vs',
|
||||
inherit: true,
|
||||
rules,
|
||||
colors,
|
||||
});
|
||||
}
|
||||
|
||||
// Monaco has a single theme for every editor on the page, and the design tokens
|
||||
// switch with the body's `dark` class, so the theme follows that class instead
|
||||
// of being defined per editor.
|
||||
export function registerEditorTheme(monaco: Monaco): void {
|
||||
defineEditorTheme(monaco);
|
||||
if (bodyClassObserver) {
|
||||
return;
|
||||
}
|
||||
bodyClassObserver = new MutationObserver(() => defineEditorTheme(monaco));
|
||||
bodyClassObserver.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
});
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import type { Monaco } from '@monaco-editor/react';
|
||||
|
||||
export const JSON_EDITOR_THEME = 'signoz-json';
|
||||
|
||||
function token(el: HTMLElement, name: string): string {
|
||||
return getComputedStyle(el).getPropertyValue(name).trim().replace('#', '');
|
||||
}
|
||||
|
||||
function isDark(hex: string): boolean {
|
||||
if (hex.length < 6) {
|
||||
return true;
|
||||
}
|
||||
const r = parseInt(hex.slice(0, 2), 16);
|
||||
const g = parseInt(hex.slice(2, 4), 16);
|
||||
const b = parseInt(hex.slice(4, 6), 16);
|
||||
return 0.299 * r + 0.587 * g + 0.114 * b < 128;
|
||||
}
|
||||
|
||||
export function defineJsonEditorTheme(monaco: Monaco, el: HTMLElement): void {
|
||||
const background = token(el, '--l1-background');
|
||||
const foreground = token(el, '--l1-foreground');
|
||||
const keyColor = token(el, '--bg-vanilla-400');
|
||||
const valueColor = token(el, '--bg-robin-400');
|
||||
|
||||
// Both token colours are picked to read on a dark surface, so on a light one
|
||||
// they wash out, the keys worst of all. Monaco's own `vs` palette is built
|
||||
// for that surface, so let it keep its colours there, as the role viewer does.
|
||||
const rules: { token: string; foreground: string }[] = [];
|
||||
if (isDark(background)) {
|
||||
if (keyColor) {
|
||||
rules.push({ token: 'string.key.json', foreground: keyColor });
|
||||
}
|
||||
if (valueColor) {
|
||||
rules.push({ token: 'string.value.json', foreground: valueColor });
|
||||
}
|
||||
}
|
||||
|
||||
const colors: Record<string, string> = {};
|
||||
if (background) {
|
||||
colors['editor.background'] = `#${background}`;
|
||||
}
|
||||
if (foreground) {
|
||||
colors['editor.foreground'] = `#${foreground}`;
|
||||
}
|
||||
|
||||
monaco.editor.defineTheme(JSON_EDITOR_THEME, {
|
||||
base: isDark(background) ? 'vs-dark' : 'vs',
|
||||
inherit: true,
|
||||
rules,
|
||||
colors,
|
||||
});
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import MEditor from '@monaco-editor/react';
|
||||
import { DrawerWrapper } from '@signozhq/ui/drawer';
|
||||
import { useGetNotificationChannel } from 'api/generated/services/channels';
|
||||
import ErrorEmptyState from 'components/Alerts/ErrorEmptyState';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { READONLY_EDITOR_OPTIONS } from 'container/RolesSettings/monaco.config';
|
||||
import {
|
||||
defineJsonEditorTheme,
|
||||
JSON_EDITOR_THEME,
|
||||
} from 'lib/monaco/jsonEditorTheme';
|
||||
import { READONLY_EDITOR_OPTIONS } from 'lib/monaco/editorOptions';
|
||||
import MonacoEditor from 'lib/monaco/MonacoEditor';
|
||||
|
||||
import { channelToJson, hasSecrets } from '../../utils/channelJson';
|
||||
import styles from './ChannelJsonDialog.module.scss';
|
||||
@@ -92,18 +88,11 @@ function ChannelJsonDialog({
|
||||
/>
|
||||
|
||||
<div className={styles.editor}>
|
||||
<MEditor
|
||||
<MonacoEditor
|
||||
value={json}
|
||||
language="json"
|
||||
options={READONLY_EDITOR_OPTIONS}
|
||||
height="100%"
|
||||
theme="vs-dark"
|
||||
// the same theme the dashboard JSON drawer uses: it reads the
|
||||
// page's own tokens, so it follows light and dark
|
||||
onMount={(editor, monaco): void => {
|
||||
defineJsonEditorTheme(monaco, editor.getContainerDomNode());
|
||||
monaco.editor.setTheme(JSON_EDITOR_THEME);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { KeyboardEvent, useCallback } from 'react';
|
||||
import MEditor from '@monaco-editor/react';
|
||||
import { TriangleAlert } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Tooltip } from '@signozhq/ui/tooltip';
|
||||
@@ -12,10 +11,7 @@ import { DashboardDetailEvents } from 'pages/DashboardPage/constants/events';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
|
||||
import {
|
||||
defineJsonEditorTheme,
|
||||
JSON_EDITOR_THEME,
|
||||
} from 'lib/monaco/jsonEditorTheme';
|
||||
import MonacoEditor from 'lib/monaco/MonacoEditor';
|
||||
import styles from './JsonEditorDrawer.module.scss';
|
||||
import JsonEditorToolbar from './JsonEditorToolbar';
|
||||
import { useJsonEditor } from './useJsonEditor';
|
||||
@@ -213,7 +209,7 @@ function JsonEditorDrawer({
|
||||
onReset={reset}
|
||||
/>
|
||||
<div className={styles.editor}>
|
||||
<MEditor
|
||||
<MonacoEditor
|
||||
language="json"
|
||||
height="100%"
|
||||
value={draft}
|
||||
@@ -225,10 +221,7 @@ function JsonEditorDrawer({
|
||||
fontSize: 13,
|
||||
fontFamily: 'Space Mono',
|
||||
}}
|
||||
theme="vs-dark"
|
||||
onMount={(editor, monaco): void => {
|
||||
defineJsonEditorTheme(monaco, editor.getContainerDomNode());
|
||||
monaco.editor.setTheme(JSON_EDITOR_THEME);
|
||||
onMount={(_editor, monaco): void => {
|
||||
void document.fonts.ready.then(() => monaco.editor.remeasureFonts());
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { Route } from 'react-router-dom';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { screen, userEvent, within } from 'storybook/test';
|
||||
import { screen, userEvent, waitFor, within } from 'storybook/test';
|
||||
|
||||
import { storyMocks } from '@/storybook/controls/defineStoryMocks';
|
||||
import type { PageStoryArgs } from '@/storybook/runtime/resolveStory';
|
||||
import { waitForMonacoText } from '@/storybook/visual/waitForMonacoText';
|
||||
|
||||
import { panelEditorMocks, savePanel } from './PanelEditorPage.stories.mocks';
|
||||
|
||||
@@ -249,3 +251,43 @@ export const TextPanelSavedToast: Story = {
|
||||
args: { panel: 'new', newPanelKind: 'text' },
|
||||
play: PanelSavedToast.play,
|
||||
};
|
||||
|
||||
const CLICKHOUSE_QUERY =
|
||||
'SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS ts, serviceName, count() AS calls FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp > now() - INTERVAL 1 HOUR GROUP BY ts, serviceName ORDER BY ts';
|
||||
|
||||
/**
|
||||
* The ClickHouse Query tab with a query written in its SQL editor, over the
|
||||
* panel the editor opened on.
|
||||
*/
|
||||
export const ClickHouseQuery: Story = {
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
const tab = await within(canvasElement).findByRole(
|
||||
'tab',
|
||||
{ name: /ClickHouse Query/ },
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
|
||||
await userEvent.click(tab);
|
||||
|
||||
// Typing into Monaco's hidden textarea never reaches its model in this runner.
|
||||
const model = await waitFor(
|
||||
() => {
|
||||
const found = monaco.editor
|
||||
.getModels()
|
||||
.find((candidate) => candidate.getLanguageId() === 'sql');
|
||||
|
||||
if (!found) {
|
||||
throw new Error('The SQL editor has not mounted');
|
||||
}
|
||||
|
||||
return found;
|
||||
},
|
||||
{ timeout: 15000 },
|
||||
);
|
||||
|
||||
model.setValue(CLICKHOUSE_QUERY);
|
||||
await waitForMonacoText(canvasElement);
|
||||
await userEvent.unhover(tab);
|
||||
(document.activeElement as HTMLElement | null)?.blur();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import MEditor, { Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DialogWrapper } from '@signozhq/ui/dialog';
|
||||
import { Maximize2 } from '@signozhq/icons';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import MonacoEditor from 'lib/monaco/MonacoEditor';
|
||||
|
||||
import styles from './NewDashboardModal.module.scss';
|
||||
|
||||
@@ -15,18 +13,6 @@ interface Props {
|
||||
height?: string;
|
||||
}
|
||||
|
||||
const defineTheme = (monaco: Monaco): void => {
|
||||
monaco.editor.defineTheme('my-theme', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'string.key.json', foreground: Color.BG_VANILLA_400 },
|
||||
{ token: 'string.value.json', foreground: Color.BG_ROBIN_400 },
|
||||
],
|
||||
colors: { 'editor.background': Color.BG_INK_300 },
|
||||
});
|
||||
};
|
||||
|
||||
// JSON editor with a one-click "expand" into an extra-wide modal for easier
|
||||
// editing/review. The expanded editor shares the same value, so edits persist.
|
||||
function JsonEditor({
|
||||
@@ -35,11 +21,10 @@ function JsonEditor({
|
||||
readOnly = false,
|
||||
height = '38vh',
|
||||
}: Props): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const renderEditor = (editorHeight: string): JSX.Element => (
|
||||
<MEditor
|
||||
<MonacoEditor
|
||||
language="json"
|
||||
height={editorHeight}
|
||||
value={value}
|
||||
@@ -51,8 +36,6 @@ function JsonEditor({
|
||||
fontSize: 14,
|
||||
fontFamily: 'Space Mono',
|
||||
}}
|
||||
theme={isDarkMode ? 'my-theme' : 'light'}
|
||||
beforeMount={defineTheme}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { expect, screen, userEvent, waitFor, within } from 'storybook/test';
|
||||
|
||||
import { storyMocks } from '@/storybook/controls/defineStoryMocks';
|
||||
import type { PageStoryArgs } from '@/storybook/runtime/resolveStory';
|
||||
import { waitForMonacoText } from '@/storybook/visual/waitForMonacoText';
|
||||
|
||||
import LLMObservabilityPage from '../../index';
|
||||
import {
|
||||
@@ -101,3 +102,21 @@ export const ChangesSavedToast: Story = {
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The Test tab: the JSON editor pre-filled with the sample span, before any run.
|
||||
*/
|
||||
export const TestTab: Story = {
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
const tab = await within(canvasElement).findByRole(
|
||||
'tab',
|
||||
{ name: 'Test' },
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
|
||||
await userEvent.click(tab);
|
||||
await waitForMonacoText(canvasElement);
|
||||
await userEvent.unhover(tab);
|
||||
(document.activeElement as HTMLElement | null)?.blur();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
|
||||
import { storyMocks } from '@/storybook/controls/defineStoryMocks';
|
||||
import type { PageStoryArgs } from '@/storybook/runtime/resolveStory';
|
||||
import { waitForMonacoText } from '@/storybook/visual/waitForMonacoText';
|
||||
|
||||
import {
|
||||
focusSpanIndex,
|
||||
@@ -325,3 +326,19 @@ export const TraceExportCancelledToast: Story = {
|
||||
await userEvent.click(await screen.findByTestId('trace-download-cancel'));
|
||||
},
|
||||
};
|
||||
|
||||
/** The span panel switched from the pretty view to the span's raw JSON. */
|
||||
export const SpanJsonView: Story = {
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
const option = await within(canvasElement).findByTestId(
|
||||
'data-viewer-view-mode-button-json',
|
||||
undefined,
|
||||
untilLoaded,
|
||||
);
|
||||
|
||||
await userEvent.click(option);
|
||||
await waitForMonacoText(canvasElement);
|
||||
await userEvent.unhover(option);
|
||||
(document.activeElement as HTMLElement | null)?.blur();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { EditorProps } from '@monaco-editor/react';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import MonacoEditor from 'lib/monaco/MonacoEditor';
|
||||
|
||||
import './JsonView.styles.scss';
|
||||
|
||||
@@ -38,36 +37,16 @@ const editorOptions: EditorProps['options'] = {
|
||||
folding: false,
|
||||
};
|
||||
|
||||
function setEditorTheme(monaco: Monaco): void {
|
||||
monaco.editor.defineTheme('signoz-dark', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'string.key.json', foreground: Color.BG_VANILLA_400 },
|
||||
{ token: 'string.value.json', foreground: Color.BG_ROBIN_400 },
|
||||
],
|
||||
colors: {
|
||||
'editor.background': '#00000000', // transparent
|
||||
},
|
||||
fontFamily: 'SF Mono, Geist Mono, Fira Code, monospace',
|
||||
fontSize: 12,
|
||||
fontWeight: 'normal',
|
||||
lineHeight: 18,
|
||||
letterSpacing: -0.06,
|
||||
});
|
||||
}
|
||||
|
||||
function JsonView({
|
||||
data,
|
||||
height = '575px',
|
||||
fontSize = 12,
|
||||
}: JsonViewProps): JSX.Element {
|
||||
const [isWrapWord, setIsWrapWord] = useState(true);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
return (
|
||||
<div className="json-view">
|
||||
<MEditor
|
||||
<MonacoEditor
|
||||
value={data}
|
||||
language="json"
|
||||
options={{
|
||||
@@ -77,8 +56,6 @@ function JsonView({
|
||||
}}
|
||||
onChange={(): void => {}}
|
||||
height={height}
|
||||
theme={isDarkMode ? 'signoz-dark' : 'light'}
|
||||
beforeMount={setEditorTheme}
|
||||
/>
|
||||
<div className="json-view__footer">
|
||||
<div className="json-view__wrap-toggle">
|
||||
|
||||
26
frontend/src/storybook/visual/waitForMonacoText.ts
Normal file
26
frontend/src/storybook/visual/waitForMonacoText.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { waitFor } from 'storybook/test';
|
||||
|
||||
/**
|
||||
* Monaco mounts its container before its language loads, so the editor is on
|
||||
* screen while still blank or uncoloured. Waits for text with at least two
|
||||
* token colours, which only exists once the tokenizer has run.
|
||||
*/
|
||||
export const waitForMonacoText = async (
|
||||
canvasElement: HTMLElement,
|
||||
): Promise<void> => {
|
||||
await waitFor(
|
||||
() => {
|
||||
const lines = canvasElement.querySelector('.monaco-editor .view-lines');
|
||||
const tokens = new Set(
|
||||
Array.from(lines?.querySelectorAll('span[class^="mtk"]') ?? []).map(
|
||||
(token) => token.className,
|
||||
),
|
||||
);
|
||||
|
||||
if (!lines?.textContent?.trim() || tokens.size < 2) {
|
||||
throw new Error('Monaco has not rendered its text');
|
||||
}
|
||||
},
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user