mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TextPanel: Refactor to functional component (#60885)
This commit is contained in:
parent
3f1908464d
commit
8b50c60342
@ -1,110 +1,47 @@
|
|||||||
// Libraries
|
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import DangerouslySetHtmlContent from 'dangerously-set-html-content';
|
import DangerouslySetHtmlContent from 'dangerously-set-html-content';
|
||||||
import { debounce } from 'lodash';
|
import React, { useState } from 'react';
|
||||||
import React, { PureComponent } from 'react';
|
import { useDebounce } from 'react-use';
|
||||||
|
|
||||||
import { GrafanaTheme2, PanelProps, renderTextPanelMarkdown, textUtil } from '@grafana/data';
|
import { GrafanaTheme2, PanelProps, renderTextPanelMarkdown, textUtil, InterpolateFunction } from '@grafana/data';
|
||||||
// Utils
|
import { CustomScrollbar, CodeEditor, useStyles2 } from '@grafana/ui';
|
||||||
import { CustomScrollbar, CodeEditor, stylesFactory, ThemeContext } from '@grafana/ui';
|
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
||||||
// Types
|
|
||||||
import { defaultCodeOptions, PanelOptions, TextMode } from './models.gen';
|
import { defaultCodeOptions, PanelOptions, TextMode } from './models.gen';
|
||||||
|
|
||||||
export interface Props extends PanelProps<PanelOptions> {}
|
export interface Props extends PanelProps<PanelOptions> {}
|
||||||
|
|
||||||
interface State {
|
export function TextPanel(props: Props) {
|
||||||
html: string;
|
const styles = useStyles2(getStyles);
|
||||||
}
|
const [processed, setProcessed] = useState<PanelOptions>({
|
||||||
|
mode: props.options.mode,
|
||||||
export class TextPanel extends PureComponent<Props, State> {
|
content: processContent(props.options, props.replaceVariables, config.disableSanitizeHtml),
|
||||||
declare context: React.ContextType<typeof ThemeContext>;
|
});
|
||||||
static contextType = ThemeContext;
|
|
||||||
|
useDebounce(
|
||||||
constructor(props: Props) {
|
() => {
|
||||||
super(props);
|
const { options, replaceVariables } = props;
|
||||||
|
const content = processContent(options, replaceVariables, config.disableSanitizeHtml);
|
||||||
this.state = {
|
if (content !== processed.content || options.mode !== processed.mode) {
|
||||||
html: this.processContent(props.options),
|
setProcessed({
|
||||||
};
|
mode: options.mode,
|
||||||
}
|
content,
|
||||||
|
|
||||||
updateHTML = debounce(() => {
|
|
||||||
const html = this.processContent(this.props.options);
|
|
||||||
if (html !== this.state.html) {
|
|
||||||
this.setState({ html });
|
|
||||||
}
|
|
||||||
}, 150);
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
|
||||||
// Since any change could be referenced in a template variable,
|
|
||||||
// This needs to process every time (with debounce)
|
|
||||||
this.updateHTML();
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareHTML(html: string): string {
|
|
||||||
const result = this.interpolateString(html);
|
|
||||||
return config.disableSanitizeHtml ? result : this.sanitizeString(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareMarkdown(content: string): string {
|
|
||||||
// Always interpolate variables before converting to markdown
|
|
||||||
// because `marked` replaces '{' and '}' in URLs with '%7B' and '%7D'
|
|
||||||
// See https://marked.js.org/demo
|
|
||||||
let result = this.interpolateString(content);
|
|
||||||
|
|
||||||
if (config.disableSanitizeHtml) {
|
|
||||||
result = renderTextPanelMarkdown(result, {
|
|
||||||
noSanitize: true,
|
|
||||||
});
|
});
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
[props]
|
||||||
|
);
|
||||||
|
|
||||||
result = renderTextPanelMarkdown(result);
|
if (processed.mode === TextMode.Code) {
|
||||||
return this.sanitizeString(result);
|
const code = props.options.code ?? defaultCodeOptions;
|
||||||
}
|
|
||||||
|
|
||||||
interpolateString(content: string): string {
|
|
||||||
const { replaceVariables, options } = this.props;
|
|
||||||
return replaceVariables(content, {}, options.code?.language === 'json' ? 'json' : 'html');
|
|
||||||
}
|
|
||||||
|
|
||||||
sanitizeString(content: string): string {
|
|
||||||
return textUtil.sanitizeTextPanelContent(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
processContent(options: PanelOptions): string {
|
|
||||||
const { mode, content } = options;
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === TextMode.HTML) {
|
|
||||||
return this.prepareHTML(content);
|
|
||||||
} else if (mode === TextMode.Code) {
|
|
||||||
return this.interpolateString(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.prepareMarkdown(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { html } = this.state;
|
|
||||||
const { options } = this.props;
|
|
||||||
const styles = getStyles(this.context);
|
|
||||||
|
|
||||||
if (options.mode === TextMode.Code) {
|
|
||||||
const { width, height } = this.props;
|
|
||||||
const code = options.code ?? defaultCodeOptions;
|
|
||||||
return (
|
return (
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
key={`${code.showLineNumbers}/${code.showMiniMap}`} // will reinit-on change
|
key={`${code.showLineNumbers}/${code.showMiniMap}`} // will reinit-on change
|
||||||
value={html}
|
value={processed.content}
|
||||||
language={code.language ?? defaultCodeOptions.language!}
|
language={code.language ?? defaultCodeOptions.language!}
|
||||||
width={width}
|
width={props.width}
|
||||||
height={height}
|
height={props.height}
|
||||||
containerStyles={styles.codeEditorContainer}
|
containerStyles={styles.codeEditorContainer}
|
||||||
showMiniMap={code.showMiniMap}
|
showMiniMap={code.showMiniMap}
|
||||||
showLineNumbers={code.showLineNumbers}
|
showLineNumbers={code.showLineNumbers}
|
||||||
@ -115,13 +52,43 @@ export class TextPanel extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomScrollbar autoHeightMin="100%">
|
<CustomScrollbar autoHeightMin="100%">
|
||||||
<DangerouslySetHtmlContent html={html} className={styles.markdown} data-testid="TextPanel-converted-content" />
|
<DangerouslySetHtmlContent
|
||||||
|
html={processed.content}
|
||||||
|
className={styles.markdown}
|
||||||
|
data-testid="TextPanel-converted-content"
|
||||||
|
/>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
function processContent(options: PanelOptions, interpolate: InterpolateFunction, disableSanitizeHtml: boolean): string {
|
||||||
|
let { mode, content } = options;
|
||||||
|
if (!content) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
content = interpolate(content, {}, options.code?.language === 'json' ? 'json' : 'html');
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case TextMode.Code:
|
||||||
|
break; // nothing
|
||||||
|
case TextMode.HTML:
|
||||||
|
if (!disableSanitizeHtml) {
|
||||||
|
content = textUtil.sanitizeTextPanelContent(content);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TextMode.Markdown:
|
||||||
|
default:
|
||||||
|
// default to markdown
|
||||||
|
content = renderTextPanelMarkdown(content, {
|
||||||
|
noSanitize: disableSanitizeHtml,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
codeEditorContainer: css`
|
codeEditorContainer: css`
|
||||||
.monaco-editor .margin,
|
.monaco-editor .margin,
|
||||||
.monaco-editor-background {
|
.monaco-editor-background {
|
||||||
@ -134,4 +101,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
`
|
`
|
||||||
),
|
),
|
||||||
}));
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React, { FC, useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
|
||||||
import { GrafanaTheme2, StandardEditorProps } from '@grafana/data';
|
import { GrafanaTheme2, StandardEditorProps } from '@grafana/data';
|
||||||
@ -12,7 +12,7 @@ import {
|
|||||||
|
|
||||||
import { PanelOptions, TextMode } from './models.gen';
|
import { PanelOptions, TextMode } from './models.gen';
|
||||||
|
|
||||||
export const TextPanelEditor: FC<StandardEditorProps<string, any, PanelOptions>> = ({ value, onChange, context }) => {
|
export const TextPanelEditor = ({ value, onChange, context }: StandardEditorProps<string, any, PanelOptions>) => {
|
||||||
const language = useMemo(() => context.options?.mode ?? TextMode.Markdown, [context]);
|
const language = useMemo(() => context.options?.mode ?? TextMode.Markdown, [context]);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
@ -11,7 +11,6 @@ export const plugin = new PanelPlugin<PanelOptions>(TextPanel)
|
|||||||
.addRadio({
|
.addRadio({
|
||||||
path: 'mode',
|
path: 'mode',
|
||||||
name: 'Mode',
|
name: 'Mode',
|
||||||
description: 'text mode of the panel',
|
|
||||||
settings: {
|
settings: {
|
||||||
options: [
|
options: [
|
||||||
{ value: TextMode.Markdown, label: 'Markdown' },
|
{ value: TextMode.Markdown, label: 'Markdown' },
|
||||||
@ -49,7 +48,6 @@ export const plugin = new PanelPlugin<PanelOptions>(TextPanel)
|
|||||||
id: 'content',
|
id: 'content',
|
||||||
path: 'content',
|
path: 'content',
|
||||||
name: 'Content',
|
name: 'Content',
|
||||||
description: 'Content of the panel',
|
|
||||||
editor: TextPanelEditor,
|
editor: TextPanelEditor,
|
||||||
defaultValue: defaultPanelOptions.content,
|
defaultValue: defaultPanelOptions.content,
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user