Skip to content

Commit 0dcdfc2

Browse files
authored
Monaco Editor: Load via ESM (grafana#78261)
* chore(monaco): bump monaco-editor to latest version * feat(codeeditor): use esm to load monaco editor * revert(monaco): put back previous version * feat(monaco): setup MonacoEnvironment when bootstrapping app * feat(monaco): load monaco languages from registry as workers * feat(webpack): clean up warnings, remove need to copy monaco into lib * fix(plugins): wip - remove amd loader workaround in systemjs hooks * chore(azure): clean up so QueryField passes typecheck * test(jest): update config to fix failing tests due to missing monaco-editor * test(jest): update config to work with monaco-editor and kusto * test(jest): prevent message eventlistener in nodeGraph/layout.worker tripping up monaco tests * test(plugins): wip - remove amd related tests from systemjs hooks * test(alerting): prefer clearAllMocks to prevent monaco editor failing due to missing matchMedia * test(parca): fix failing test due to undefined backendSrv * chore: move monacoEnv to app/core * test: increase testing-lib timeout to 2secs, fix parca test to assert dom element * feat(plugins): share kusto via systemjs * test(e2e): increase timeout for checking monaco editor in exemplars spec * test(e2e): assert monaco has loaded by checking the spinner is gone and window.monaco exists * test(e2e): check for monaco editor textarea * test(e2e): check monaco editor is loaded before assertions * test(e2e): add waitForMonacoToLoad util to reduce duplication * test(e2e): fix failing mysql spec * chore(jest): add comment to setupTests explaining need to incresae default timeout * chore(nodegraph): improve comment in layout.worker.utils to better explain the need for file
1 parent 0dbf2da commit 0dcdfc2

File tree

29 files changed

+702
-573
lines changed

29 files changed

+702
-573
lines changed

.betterer.results

-4
Original file line numberDiff line numberDiff line change
@@ -4581,10 +4581,6 @@ exports[`better eslint`] = {
45814581
"public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts:5381": [
45824582
[0, 0, 0, "Do not use any type assertions.", "0"]
45834583
],
4584-
"public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx:5381": [
4585-
[0, 0, 0, "Do not use any type assertions.", "0"],
4586-
[0, 0, 0, "Do not use any type assertions.", "1"]
4587-
],
45884584
"public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx:5381": [
45894585
[0, 0, 0, "Do not use any type assertions.", "0"]
45904586
],

e2e/utils/support/monaco.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { e2e } from '../index';
2+
3+
export function waitForMonacoToLoad() {
4+
e2e.components.QueryField.container().children('[data-testid="Spinner"]').should('not.exist');
5+
cy.window().its('monaco').should('exist');
6+
cy.get('.monaco-editor textarea:first').should('exist');
7+
}

e2e/various-suite/exemplars.spec.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { e2e } from '../utils';
2+
import { waitForMonacoToLoad } from '../utils/support/monaco';
23

34
const dataSourceName = 'PromExemplar';
45
const addDataSource = () => {
@@ -57,12 +58,8 @@ describe('Exemplars', () => {
5758
// Switch to code editor
5859
e2e.components.RadioButton.container().filter(':contains("Code")').click();
5960

60-
// we need to wait for the query-field being lazy-loaded, in two steps:
61-
// 1. first we wait for the text 'Loading...' to appear
62-
// 1. then we wait for the text 'Loading...' to disappear
63-
const monacoLoadingText = 'Loading...';
64-
e2e.components.QueryField.container().should('be.visible').should('have.text', monacoLoadingText);
65-
e2e.components.QueryField.container().should('be.visible').should('not.have.text', monacoLoadingText);
61+
// Wait for lazy loading Monaco
62+
waitForMonacoToLoad();
6663

6764
e2e.components.TimePicker.openButton().click();
6865
e2e.components.TimePicker.fromField().clear().type('2021-07-10 17:10:00');

e2e/various-suite/loki-editor.spec.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { e2e } from '../utils';
2+
import { waitForMonacoToLoad } from '../utils/support/monaco';
23

34
const dataSourceName = 'LokiEditor';
45
const addDataSource = () => {
@@ -39,11 +40,7 @@ describe('Loki Query Editor', () => {
3940

4041
e2e.components.RadioButton.container().filter(':contains("Code")').click();
4142

42-
// Wait for lazy loading
43-
const monacoLoadingText = 'Loading...';
44-
45-
e2e.components.QueryField.container().should('be.visible').should('have.text', monacoLoadingText);
46-
e2e.components.QueryField.container().should('be.visible').should('not.have.text', monacoLoadingText);
43+
waitForMonacoToLoad();
4744

4845
// adds closing braces around empty value
4946
e2e.components.QueryField.container().type('time(');

e2e/various-suite/mysql.spec.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ describe('MySQL datasource', () => {
3737
it.skip('code editor autocomplete should handle table name escaping/quoting', () => {
3838
e2e.components.RadioButton.container().filter(':contains("Code")').click();
3939

40+
e2e.components.CodeEditor.container().children('[data-testid="Spinner"]').should('not.exist');
41+
cy.window().its('monaco').should('exist');
42+
4043
cy.get('textarea').type('S{downArrow}{enter}');
4144
cy.wait('@tables');
4245
cy.get('.suggest-widget').contains(tableNameWithSpecialCharacter).should('be.visible');
@@ -88,8 +91,10 @@ describe('MySQL datasource', () => {
8891
cy.get("[aria-label='Macros value selector']").should('be.visible').click();
8992
selectOption('timeFilter');
9093

91-
// Validate that the timeFilter macro was added
94+
e2e.components.CodeEditor.container().children('[data-testid="Spinner"]').should('not.exist');
95+
cy.window().its('monaco').should('exist');
9296

97+
// Validate that the timeFilter macro was added
9398
e2e.components.CodeEditor.container()
9499
.get('textarea')
95100
.should(

e2e/various-suite/query-editor.spec.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { e2e } from '../utils';
2+
import { waitForMonacoToLoad } from '../utils/support/monaco';
23

34
describe('Query editor', () => {
45
beforeEach(() => {
@@ -14,13 +15,8 @@ describe('Query editor', () => {
1415

1516
e2e.components.RadioButton.container().filter(':contains("Code")').click();
1617

17-
// we need to wait for the query-field being lazy-loaded, in two steps:
18-
// it is a two-step process:
19-
// 1. first we wait for the text 'Loading...' to appear
20-
// 1. then we wait for the text 'Loading...' to disappear
21-
const monacoLoadingText = 'Loading...';
22-
e2e.components.QueryField.container().should('be.visible').should('have.text', monacoLoadingText);
23-
e2e.components.QueryField.container().should('be.visible').should('not.have.text', monacoLoadingText);
18+
waitForMonacoToLoad();
19+
2420
e2e.components.QueryField.container().type(queryText, { parseSpecialCharSequences: false }).type('{backspace}');
2521

2622
cy.contains(queryText.slice(0, -1)).should('be.visible');

jest.config.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ const esModules = [
1515
'leven',
1616
'nanoid',
1717
'monaco-promql',
18+
'@kusto/monaco-kusto',
19+
'monaco-editor',
20+
'lodash-es',
1821
].join('|');
1922

2023
module.exports = {
@@ -41,7 +44,9 @@ module.exports = {
4144
'\\.svg': '<rootDir>/public/test/mocks/svg.ts',
4245
'\\.css': '<rootDir>/public/test/mocks/style.ts',
4346
'react-inlinesvg': '<rootDir>/public/test/mocks/react-inlinesvg.tsx',
44-
'monaco-editor/esm/vs/editor/editor.api': '<rootDir>/public/test/mocks/monaco.ts',
47+
// resolve directly as monaco and kusto don't have main property in package.json which jest needs
48+
'^monaco-editor$': 'monaco-editor/esm/vs/editor/editor.api.js',
49+
'@kusto/monaco-kusto': '@kusto/monaco-kusto/release/esm/monaco.contribution.js',
4550
// near-membrane-dom won't work in a nodejs environment.
4651
'@locker/near-membrane-dom': '<rootDir>/public/test/mocks/nearMembraneDom.ts',
4752
'^@grafana/schema/dist/esm/(.*)$': '<rootDir>/packages/grafana-schema/src/$1',

packages/grafana-data/src/monaco/languageRegistry.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Registry, RegistryItem } from '../utils/Registry';
44
* @alpha
55
*/
66
export interface MonacoLanguageRegistryItem extends RegistryItem {
7-
init: () => Promise<void>;
7+
init: () => Worker;
88
}
99

1010
/**

packages/grafana-runtime/src/utils/plugin.ts

-8
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,3 @@ export function getPluginImportUtils(): PluginImportUtils {
6262

6363
return pluginImportUtils;
6464
}
65-
66-
// Grafana relies on RequireJS for Monaco Editor to load.
67-
// The SystemJS AMD extra creates a global define which causes RequireJS to silently bail.
68-
// Here we move and reset global define so Monaco Editor loader script continues to work.
69-
// @ts-ignore
70-
window.__grafana_amd_define = window.define;
71-
// @ts-ignore
72-
window.define = undefined;

packages/grafana-ui/src/components/Monaco/CodeEditor.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,12 @@ class UnthemedCodeEditor extends PureComponent<Props> {
119119
}
120120
});
121121

122-
const languagePromise = this.loadCustomLanguage();
123-
124122
if (onChange) {
125123
editor.getModel()?.onDidChangeContent(() => onChange(editor.getValue()));
126124
}
127125

128126
if (onEditorDidMount) {
129-
languagePromise.then(() => onEditorDidMount(editor, monaco));
127+
onEditorDidMount(editor, monaco);
130128
}
131129
};
132130

Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
import MonacoEditor, { loader as monacoEditorLoader, Monaco } from '@monaco-editor/react';
1+
import Editor, { loader as monacoEditorLoader, Monaco } from '@monaco-editor/react';
2+
import * as monaco from 'monaco-editor';
23
import React, { useCallback } from 'react';
34

45
import { useTheme2 } from '../../themes';
56

67
import defineThemes from './theme';
78
import type { ReactMonacoEditorProps } from './types';
89

9-
monacoEditorLoader.config({
10-
paths: {
11-
vs: (window.__grafana_public_path__ ?? 'public/') + 'lib/monaco/min/vs',
12-
},
13-
});
10+
// pass the monaco editor to the loader to bypass requirejs
11+
monacoEditorLoader.config({ monaco });
1412

1513
export const ReactMonacoEditor = (props: ReactMonacoEditorProps) => {
1614
const { beforeMount } = props;
@@ -25,10 +23,6 @@ export const ReactMonacoEditor = (props: ReactMonacoEditorProps) => {
2523
);
2624

2725
return (
28-
<MonacoEditor
29-
{...props}
30-
theme={theme.isDark ? 'grafana-dark' : 'grafana-light'}
31-
beforeMount={onMonacoBeforeMount}
32-
/>
26+
<Editor {...props} theme={theme.isDark ? 'grafana-dark' : 'grafana-light'} beforeMount={onMonacoBeforeMount} />
3327
);
3428
};

public/app/app.ts

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { PluginPage } from './core/components/Page/PluginPage';
5656
import { GrafanaContextType, useReturnToPreviousInternal } from './core/context/GrafanaContext';
5757
import { initIconCache } from './core/icons/iconBundle';
5858
import { initializeI18n } from './core/internationalization';
59+
import { setMonacoEnv } from './core/monacoEnv';
5960
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
6061
import { ModalManager } from './core/services/ModalManager';
6162
import { NewFrontendAssetsChecker } from './core/services/NewFrontendAssetsChecker';
@@ -170,7 +171,9 @@ export class GrafanaApp {
170171
createAdHocVariableAdapter(),
171172
createSystemVariableAdapter(),
172173
]);
174+
173175
monacoLanguageRegistry.setInit(getDefaultMonacoLanguages);
176+
setMonacoEnv();
174177

175178
setQueryRunnerFactory(() => new QueryRunner());
176179
setVariableQueryRunner(new VariableQueryRunner());

public/app/core/monacoEnv.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { monacoLanguageRegistry } from '@grafana/data';
2+
import { CorsWorker as Worker } from 'app/core/utils/CorsWorker';
3+
4+
export function setMonacoEnv() {
5+
self.MonacoEnvironment = {
6+
getWorker(_moduleId, label) {
7+
const language = monacoLanguageRegistry.getIfExists(label);
8+
9+
if (language) {
10+
return language.init();
11+
}
12+
13+
if (label === 'json') {
14+
return new Worker(new URL('monaco-editor/esm/vs/language/json/json.worker', import.meta.url));
15+
}
16+
17+
if (label === 'css' || label === 'scss' || label === 'less') {
18+
return new Worker(new URL('monaco-editor/esm/vs/language/css/css.worker', import.meta.url));
19+
}
20+
21+
if (label === 'html' || label === 'handlebars' || label === 'razor') {
22+
return new Worker(new URL('monaco-editor/esm/vs/language/html/html.worker', import.meta.url));
23+
}
24+
25+
if (label === 'typescript' || label === 'javascript') {
26+
return new Worker(new URL('monaco-editor/esm/vs/language/typescript/ts.worker', import.meta.url));
27+
}
28+
29+
return new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker', import.meta.url));
30+
},
31+
};
32+
}

public/app/features/alerting/unified/components/admin/AlertmanagerConfig.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ const ui = {
8989

9090
describe('Admin config', () => {
9191
beforeEach(() => {
92-
jest.resetAllMocks();
92+
jest.clearAllMocks();
9393
// FIXME: scope down
9494
grantUserPermissions(Object.values(AccessControlAction));
9595
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
export const SHARED_DEPENDENCY_PREFIX = 'package';
22
export const LOAD_PLUGIN_CSS_REGEX = /^plugins.+\.css$/i;
33
export const JS_CONTENT_TYPE_REGEX = /^(text|application)\/(x-)?javascript(;|$)/;
4-
export const AMD_MODULE_REGEX =
5-
/(?:^\uFEFF?|[^$_a-zA-Z\xA0-\uFFFF.])define\s*\(\s*("[^"]+"\s*,\s*|'[^']+'\s*,\s*)?\s*(\[(\s*(("[^"]+"|'[^']+')\s*,|\/\/.*\r?\n))*(\s*("[^"]+"|'[^']+')\s*,?)?(\s*(\/\/.*\r?\n|\/\*\/))*\s*\]|function\s*|{|[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*\))/;

0 commit comments

Comments
 (0)