Commit 1a711e7d by Ryan McKinley Committed by GitHub

Panel Inspect: use monaco for json display (#25251)

parent dcd57520
const esModule = '@iconscout/react-unicons'; const esModule = '@iconscout/react-unicons|monaco-editor/esm/vs';
module.exports = { module.exports = {
verbose: false, verbose: false,
...@@ -16,5 +16,6 @@ module.exports = { ...@@ -16,5 +16,6 @@ module.exports = {
globals: { 'ts-jest': { isolatedModules: true } }, globals: { 'ts-jest': { isolatedModules: true } },
moduleNameMapper: { moduleNameMapper: {
'\\.svg': '<rootDir>/public/test/mocks/svg.ts', '\\.svg': '<rootDir>/public/test/mocks/svg.ts',
'\\.css': '<rootDir>/public/test/mocks/style.ts',
}, },
}; };
...@@ -163,7 +163,8 @@ ...@@ -163,7 +163,8 @@
"mini-css-extract-plugin": "0.9.0", "mini-css-extract-plugin": "0.9.0",
"mocha": "7.0.1", "mocha": "7.0.1",
"module-alias": "2.2.2", "module-alias": "2.2.2",
"monaco-editor": "0.15.6", "monaco-editor": "0.20.0",
"monaco-editor-webpack-plugin": "1.9.0",
"mutationobserver-shim": "0.3.3", "mutationobserver-shim": "0.3.3",
"ngtemplate-loader": "2.0.1", "ngtemplate-loader": "2.0.1",
"node-sass": "4.13.1", "node-sass": "4.13.1",
......
...@@ -180,6 +180,8 @@ const getBaseWebpackConfig: WebpackConfigurationGetter = async options => { ...@@ -180,6 +180,8 @@ const getBaseWebpackConfig: WebpackConfigurationGetter = async options => {
'@grafana/ui', '@grafana/ui',
'@grafana/runtime', '@grafana/runtime',
'@grafana/data', '@grafana/data',
'monaco-editor',
'react-monaco-editor',
// @ts-ignore // @ts-ignore
(context, request, callback) => { (context, request, callback) => {
const prefix = 'grafana/'; const prefix = 'grafana/';
......
...@@ -78,12 +78,36 @@ module.exports = ({ config, mode }) => { ...@@ -78,12 +78,36 @@ module.exports = ({ config, mode }) => {
config.optimization = { config.optimization = {
nodeEnv: 'production', nodeEnv: 'production',
moduleIds: 'hashed',
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
minChunks: 1,
cacheGroups: {
monaco: {
test: /[\\/]node_modules[\\/](monaco-editor)[\\/].*[jt]sx?$/,
chunks: 'initial',
priority: 20,
enforce: true,
},
vendors: {
test: /[\\/]node_modules[\\/].*[jt]sx?$/,
chunks: 'initial',
priority: -10,
reuseExistingChunk: true,
enforce: true,
},
default: {
priority: -20,
chunks: 'all',
test: /.*[jt]sx?$/,
reuseExistingChunk: true,
},
},
},
minimize: true,
minimizer: [ minimizer: [
new TerserPlugin({ new TerserPlugin({ cache: false, parallel: false, sourceMap: false, exclude: /monaco/ }),
cache: false,
parallel: false,
sourceMap: false,
}),
new OptimizeCSSAssetsPlugin({}), new OptimizeCSSAssetsPlugin({}),
], ],
}; };
......
...@@ -47,6 +47,8 @@ ...@@ -47,6 +47,8 @@
"immutable": "3.8.2", "immutable": "3.8.2",
"jquery": "3.5.1", "jquery": "3.5.1",
"lodash": "4.17.15", "lodash": "4.17.15",
"monaco-editor": "0.20.0",
"react-monaco-editor": "0.36.0",
"moment": "2.24.0", "moment": "2.24.0",
"papaparse": "4.6.3", "papaparse": "4.6.3",
"rc-cascader": "1.0.1", "rc-cascader": "1.0.1",
......
...@@ -25,7 +25,16 @@ const buildCjsPackage = ({ env }) => { ...@@ -25,7 +25,16 @@ const buildCjsPackage = ({ env }) => {
}, },
}, },
], ],
external: ['react', 'react-dom', '@grafana/data', 'moment', '@grafana/e2e-selectors'], external: [
'react',
'react-dom',
'@grafana/data',
'@grafana/e2e-selectors',
'moment',
'monaco-editor', // Monaco should not be used directly
'monaco-editor/esm/vs/editor/editor.api', // Monaco should not be used directly
'react-monaco-editor',
],
plugins: [ plugins: [
commonjs({ commonjs({
include: /node_modules/, include: /node_modules/,
......
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
import { CodeEditor } from './CodeEditor';
<Meta title="MDX|CodeEditor" component={CodeEditor} />
# CodeEditor
Monaco Code editor
\ No newline at end of file
import React from 'react';
import { text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import mdx from './CodeEditor.mdx';
import CodeEditor from './CodeEditor';
const getKnobs = () => {
return {
text: text('Body', 'SELECT * FROM table LIMIT 10'),
language: text('Language', 'sql'),
};
};
export default {
title: 'CodeEditor',
component: CodeEditor,
decorators: [withCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
export const basic = () => {
const { text, language } = getKnobs();
return (
<CodeEditor
value={text}
language={language}
onBlur={(text: string) => {
console.log('Blur: ', text);
action('code blur')(text);
}}
onSave={(text: string) => {
console.log('Save: ', text);
action('code saved')(text);
}}
/>
);
};
import React from 'react';
import { withTheme } from '../../themes';
import { Themeable } from '../../types';
import { KeyCode, editor, KeyMod } from 'monaco-editor/esm/vs/editor/editor.api';
import ReactMonaco from 'react-monaco-editor';
export interface CodeEditorProps {
value: string;
language: string;
width?: number | string;
height?: number | string;
readOnly?: boolean;
showMiniMap?: boolean;
/**
* Callback after the editor has mounted that gives you raw access to monaco
*
* @experimental
*/
onEditorDidMount?: (editor: editor.IStandaloneCodeEditor) => void;
/** Handler to be performed when editor is blurred */
onBlur?: CodeEditorChangeHandler;
/** Handler to be performed when Cmd/Ctrl+S is pressed */
onSave?: CodeEditorChangeHandler;
}
type Props = CodeEditorProps & Themeable;
class UnthemedCodeEditor extends React.PureComponent<Props> {
getEditorValue = () => '';
onBlur = () => {
const { onBlur } = this.props;
if (onBlur) {
onBlur(this.getEditorValue());
}
};
editorDidMount = (editor: editor.IStandaloneCodeEditor) => {
const { onSave, onEditorDidMount } = this.props;
this.getEditorValue = () => editor.getValue();
if (onSave) {
editor.addCommand(KeyMod.CtrlCmd | KeyCode.KEY_S, () => {
onSave(this.getEditorValue());
});
}
if (onEditorDidMount) {
onEditorDidMount(editor);
}
};
render() {
const { theme, language, width, height, showMiniMap, readOnly } = this.props;
const value = this.props.value ?? '';
const longText = value.length > 100;
return (
<div onBlur={this.onBlur}>
<ReactMonaco
width={width}
height={height}
language={language}
theme={theme.isDark ? 'vs-dark' : 'vs-light'}
value={value}
options={{
wordWrap: 'off',
codeLens: false, // not included in the bundle
minimap: {
enabled: longText && showMiniMap,
renderCharacters: false,
},
readOnly,
lineNumbersMinChars: 4,
lineDecorationsWidth: 0,
overviewRulerBorder: false,
automaticLayout: true,
}}
editorDidMount={this.editorDidMount}
/>
</div>
);
}
}
export type CodeEditorChangeHandler = (value: string) => void;
export default withTheme(UnthemedCodeEditor);
import React from 'react';
import { useAsyncDependency } from '../../utils/useAsyncDependency';
import { ErrorWithStack, LoadingPlaceholder } from '..';
import { CodeEditorProps } from './CodeEditor';
export type CodeEditorChangeHandler = (value: string) => void;
export const CodeEditor: React.FC<CodeEditorProps> = props => {
const { loading, error, dependency } = useAsyncDependency(
import(/* webpackChunkName: "code-editor" */ './CodeEditor')
);
if (loading) {
return <LoadingPlaceholder text={'Loading...'} />;
}
if (error) {
return (
<ErrorWithStack
title="Code editor failed to load"
error={error}
errorInfo={{ componentStack: error?.stack || '' }}
/>
);
}
const CodeEditor = dependency.default;
return <CodeEditor {...props} />;
};
...@@ -34,6 +34,7 @@ export { FilterPill } from './FilterPill/FilterPill'; ...@@ -34,6 +34,7 @@ export { FilterPill } from './FilterPill/FilterPill';
export { ConfirmModal } from './ConfirmModal/ConfirmModal'; export { ConfirmModal } from './ConfirmModal/ConfirmModal';
export { QueryField } from './QueryField/QueryField'; export { QueryField } from './QueryField/QueryField';
export { CodeEditor } from './Monaco/CodeEditorLazy';
// TODO: namespace // TODO: namespace
export { Modal } from './Modal/Modal'; export { Modal } from './Modal/Modal';
......
import { useAsync } from 'react-use';
// Allows simple dynamic imports in the components
export const useAsyncDependency = (importStatement: Promise<any>) => {
const state = useAsync(async () => {
return await importStatement;
});
return {
...state,
dependency: state.value,
};
};
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { chain } from 'lodash'; import { chain } from 'lodash';
import { AppEvents, PanelData, SelectableValue } from '@grafana/data'; import { AppEvents, PanelData, SelectableValue } from '@grafana/data';
import { Button, ClipboardButton, Field, JSONFormatter, Select, TextArea } from '@grafana/ui'; import { Button, CodeEditor, Field, Select } from '@grafana/ui';
import AutoSizer from 'react-virtualized-auto-sizer';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { appEvents } from 'app/core/core'; import { appEvents } from 'app/core/core';
import { DashboardModel, PanelModel } from '../../state'; import { DashboardModel, PanelModel } from '../../state';
import { getPanelInspectorStyles } from './styles'; import { getPanelInspectorStyles } from './styles';
...@@ -49,20 +49,18 @@ export class InspectJSONTab extends PureComponent<Props, State> { ...@@ -49,20 +49,18 @@ export class InspectJSONTab extends PureComponent<Props, State> {
super(props); super(props);
this.state = { this.state = {
show: ShowContent.PanelJSON, show: ShowContent.PanelJSON,
text: getSaveModelJSON(props.panel), text: getPrettyJSON(props.panel.getSaveModel()),
}; };
} }
onSelectChanged = (item: SelectableValue<ShowContent>) => { onSelectChanged = (item: SelectableValue<ShowContent>) => {
let text = ''; const show = this.getJSONObject(item.value);
if (item.value === ShowContent.PanelJSON) { const text = getPrettyJSON(show);
text = getSaveModelJSON(this.props.panel);
}
this.setState({ text, show: item.value }); this.setState({ text, show: item.value });
}; };
onTextChanged = (e: React.FormEvent<HTMLTextAreaElement>) => { // Called onBlur
const text = e.currentTarget.value; onTextChanged = (text: string) => {
this.setState({ text }); this.setState({ text });
}; };
...@@ -93,17 +91,7 @@ export class InspectJSONTab extends PureComponent<Props, State> { ...@@ -93,17 +91,7 @@ export class InspectJSONTab extends PureComponent<Props, State> {
return this.props.panel.getSaveModel(); return this.props.panel.getSaveModel();
} }
return { note: 'Unknown Object', show }; return { note: `Unknown Object: ${show}` };
};
getClipboardText = () => {
const { show } = this.state;
const obj = this.getJSONObject(show);
return JSON.stringify(obj, null, 2);
};
onClipboardCopied = () => {
appEvents.emit(AppEvents.alertSuccess, ['Content copied to clipboard']);
}; };
onApplyPanelModel = () => { onApplyPanelModel = () => {
...@@ -126,12 +114,6 @@ export class InspectJSONTab extends PureComponent<Props, State> { ...@@ -126,12 +114,6 @@ export class InspectJSONTab extends PureComponent<Props, State> {
onClose(); onClose();
}; };
renderPanelJSON(styles: any) {
return (
<TextArea spellCheck={false} value={this.state.text} onChange={this.onTextChanged} className={styles.editor} />
);
}
render() { render() {
const { dashboard } = this.props; const { dashboard } = this.props;
const { show } = this.state; const { show } = this.state;
...@@ -146,14 +128,6 @@ export class InspectJSONTab extends PureComponent<Props, State> { ...@@ -146,14 +128,6 @@ export class InspectJSONTab extends PureComponent<Props, State> {
<Field label="Select source" className="flex-grow-1"> <Field label="Select source" className="flex-grow-1">
<Select options={options} value={selected} onChange={this.onSelectChanged} /> <Select options={options} value={selected} onChange={this.onSelectChanged} />
</Field> </Field>
<ClipboardButton
variant="secondary"
className={styles.toolbarItem}
getText={this.getClipboardText}
onClipboardCopy={this.onClipboardCopied}
>
Copy to clipboard
</ClipboardButton>
{isPanelJSON && canEdit && ( {isPanelJSON && canEdit && (
<Button className={styles.toolbarItem} onClick={this.onApplyPanelModel}> <Button className={styles.toolbarItem} onClick={this.onApplyPanelModel}>
Apply Apply
...@@ -161,19 +135,24 @@ export class InspectJSONTab extends PureComponent<Props, State> { ...@@ -161,19 +135,24 @@ export class InspectJSONTab extends PureComponent<Props, State> {
)} )}
</div> </div>
<div className={styles.content}> <div className={styles.content}>
{isPanelJSON ? ( <AutoSizer disableWidth>
this.renderPanelJSON(styles) {({ height }) => (
) : ( <CodeEditor
<div className={styles.viewer}> width="100%"
<JSONFormatter json={this.getJSONObject(show)} /> height={height}
</div> language="json"
value={this.state.text}
readOnly={!isPanelJSON}
onBlur={this.onTextChanged}
/>
)} )}
</AutoSizer>
</div> </div>
</> </>
); );
} }
} }
function getSaveModelJSON(panel: PanelModel): string { function getPrettyJSON(obj: any): string {
return JSON.stringify(panel.getSaveModel(), null, 2); return JSON.stringify(obj, null, 2);
} }
export const style = 'style';
const path = require('path'); const path = require('path');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
// https://github.com/visionmedia/debug/issues/701#issuecomment-505487361 // https://github.com/visionmedia/debug/issues/701#issuecomment-505487361
function shouldExclude(filename) { function shouldExclude(filename) {
// There is external js code inside this which needs to be processed by babel. // There is external js code inside this which needs to be processed by babel.
...@@ -15,6 +17,7 @@ function shouldExclude(filename) { ...@@ -15,6 +17,7 @@ function shouldExclude(filename) {
'react-hook-form', 'react-hook-form',
'rc-trigger', 'rc-trigger',
'@iconscout/react-unicons', '@iconscout/react-unicons',
'monaco-editor',
]; ];
for (const package of packagesToProcessbyBabel) { for (const package of packagesToProcessbyBabel) {
if (filename.indexOf(`node_modules/${package}`) > 0) { if (filename.indexOf(`node_modules/${package}`) > 0) {
...@@ -58,6 +61,55 @@ module.exports = { ...@@ -58,6 +61,55 @@ module.exports = {
node: { node: {
fs: 'empty', fs: 'empty',
}, },
plugins: [
new MonacoWebpackPlugin({
// available options are documented at https://github.com/Microsoft/monaco-editor-webpack-plugin#options
filename: 'monaco-[name].worker.js',
languages: ['json', 'markdown', 'html', 'sql', 'mysql', 'pgsql'],
features: [
'!accessibilityHelp',
'bracketMatching',
'caretOperations',
'!clipboard',
'!codeAction',
'!codelens',
'!colorDetector',
'!comment',
'!contextmenu',
'!coreCommands',
'!cursorUndo',
'!dnd',
'!find',
'!folding',
'!fontZoom',
'!format',
'!gotoError',
'!gotoLine',
'!gotoSymbol',
'!hover',
'!iPadShowKeyboard',
'!inPlaceReplace',
'!inspectTokens',
'!linesOperations',
'!links',
'!multicursor',
'!parameterHints',
'!quickCommand',
'!quickOutline',
'!referenceSearch',
'!rename',
'!smartSelect',
'!snippets',
'!suggest',
'!toggleHighContrast',
'!toggleTabFocusMode',
'!transpose',
'!wordHighlighter',
'!wordOperations',
'!wordPartOperations',
],
}),
],
module: { module: {
rules: [ rules: [
/** /**
...@@ -109,6 +161,11 @@ module.exports = { ...@@ -109,6 +161,11 @@ module.exports = {
], ],
}, },
{ {
test: /\.css$/,
// include: MONACO_DIR, // https://github.com/react-monaco-editor/react-monaco-editor
use: ['style-loader', 'css-loader'],
},
{
test: /\.(svg|ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/, test: /\.(svg|ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/,
loader: 'file-loader', loader: 'file-loader',
options: { name: 'static/img/[name].[hash:8].[ext]' }, options: { name: 'static/img/[name].[hash:8].[ext]' },
......
...@@ -6456,6 +6456,14 @@ ...@@ -6456,6 +6456,14 @@
"@types/prop-types" "*" "@types/prop-types" "*"
csstype "^2.2.0" csstype "^2.2.0"
"@types/react@^16.x":
version "16.9.35"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ==
dependencies:
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/recompose@^0.30.7": "@types/recompose@^0.30.7":
version "0.30.7" version "0.30.7"
resolved "https://registry.yarnpkg.com/@types/recompose/-/recompose-0.30.7.tgz#0d47f3da3bdf889a4f36d4ca7531fac1eee1c6bd" resolved "https://registry.yarnpkg.com/@types/recompose/-/recompose-0.30.7.tgz#0d47f3da3bdf889a4f36d4ca7531fac1eee1c6bd"
...@@ -10595,7 +10603,7 @@ cypress-file-upload@^4.0.7: ...@@ -10595,7 +10603,7 @@ cypress-file-upload@^4.0.7:
dependencies: dependencies:
mime "^2.4.4" mime "^2.4.4"
cypress@4.9.0: cypress@^4.9.0:
version "4.9.0" version "4.9.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.9.0.tgz#c188a3864ddf841c0fdc81a9e4eff5cf539cd1c1" resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.9.0.tgz#c188a3864ddf841c0fdc81a9e4eff5cf539cd1c1"
integrity sha512-qGxT5E0j21FPryzhb0OBjCdhoR/n1jXtumpFFSBPYWsaZZhNaBvc3XlBUDEZKkkXPsqUFYiyhWdHN/zo0t5FcA== integrity sha512-qGxT5E0j21FPryzhb0OBjCdhoR/n1jXtumpFFSBPYWsaZZhNaBvc3XlBUDEZKkkXPsqUFYiyhWdHN/zo0t5FcA==
...@@ -18292,10 +18300,17 @@ moment@2.26.0: ...@@ -18292,10 +18300,17 @@ moment@2.26.0:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
monaco-editor@0.15.6: monaco-editor-webpack-plugin@1.9.0:
version "0.15.6" version "1.9.0"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.15.6.tgz#d63b3b06f86f803464f003b252627c3eb4a09483" resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.9.0.tgz#5b547281b9f404057dc5d8c5722390df9ac90be6"
integrity sha512-JoU9V9k6KqT9R9Tiw1RTU8ohZ+Xnf9DMg6Ktqqw5hILumwmq7xqa/KLXw513uTUsWbhtnHoSJYYR++u3pkyxJg== integrity sha512-tOiiToc94E1sb50BgZ8q8WK/bxus77SRrwCqIpAB5er3cpX78SULbEBY4YPOB8kDolOzKRt30WIHG/D6gz69Ww==
dependencies:
loader-utils "^1.2.3"
monaco-editor@*, monaco-editor@0.20.0:
version "0.20.0"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.20.0.tgz#5d5009343a550124426cb4d965a4d27a348b4dea"
integrity sha512-hkvf4EtPJRMQlPC3UbMoRs0vTAFAYdzFQ+gpMb8A+9znae1c43q8Mab9iVsgTcg/4PNiLGGn3SlDIa8uvK1FIQ==
moo@^0.4.3: moo@^0.4.3:
version "0.4.3" version "0.4.3"
...@@ -21914,6 +21929,15 @@ react-loadable@5.5.0: ...@@ -21914,6 +21929,15 @@ react-loadable@5.5.0:
dependencies: dependencies:
prop-types "^15.5.0" prop-types "^15.5.0"
react-monaco-editor@0.36.0:
version "0.36.0"
resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.36.0.tgz#ac085c14f25fb072514c925596f6a06a711ee078"
integrity sha512-JVA5SZhOoYZ0DCdTwYgagtRb3jHo4KN7TVFiJauG+ZBAJWfDSTzavPIrwzWbgu8ahhDqDk4jUcYlOJL2BC/0UA==
dependencies:
"@types/react" "^16.x"
monaco-editor "*"
prop-types "^15.7.2"
react-popper-tooltip@^2.8.3: react-popper-tooltip@^2.8.3:
version "2.9.1" version "2.9.1"
resolved "https://registry.yarnpkg.com/react-popper-tooltip/-/react-popper-tooltip-2.9.1.tgz#cc602c89a937aea378d9e2675b1ce62805beb4f6" resolved "https://registry.yarnpkg.com/react-popper-tooltip/-/react-popper-tooltip-2.9.1.tgz#cc602c89a937aea378d9e2675b1ce62805beb4f6"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment