TextPanel: Refactor to functional component (#60885)

This commit is contained in:
Ryan McKinley 2023-01-05 02:08:00 -08:00 committed by GitHub
parent 3f1908464d
commit 8b50c60342
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 78 additions and 113 deletions

View File

@ -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%;
` `
), ),
})); });

View File

@ -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);

View File

@ -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,
}); });