From 8b50c60342324d1fbbeebf2e780581eb1cd7e4fc Mon Sep 17 00:00:00 2001 From: Ryan McKinley <ryantxu@gmail.com> Date: Thu, 5 Jan 2023 02:08:00 -0800 Subject: [PATCH] TextPanel: Refactor to functional component (#60885) --- public/app/plugins/panel/text/TextPanel.tsx | 185 +++++++----------- .../plugins/panel/text/TextPanelEditor.tsx | 4 +- public/app/plugins/panel/text/module.tsx | 2 - 3 files changed, 78 insertions(+), 113 deletions(-) diff --git a/public/app/plugins/panel/text/TextPanel.tsx b/public/app/plugins/panel/text/TextPanel.tsx index cf7a265057f..b3bca99353f 100644 --- a/public/app/plugins/panel/text/TextPanel.tsx +++ b/public/app/plugins/panel/text/TextPanel.tsx @@ -1,127 +1,94 @@ -// Libraries import { css, cx } from '@emotion/css'; import DangerouslySetHtmlContent from 'dangerously-set-html-content'; -import { debounce } from 'lodash'; -import React, { PureComponent } from 'react'; +import React, { useState } from 'react'; +import { useDebounce } from 'react-use'; -import { GrafanaTheme2, PanelProps, renderTextPanelMarkdown, textUtil } from '@grafana/data'; -// Utils -import { CustomScrollbar, CodeEditor, stylesFactory, ThemeContext } from '@grafana/ui'; +import { GrafanaTheme2, PanelProps, renderTextPanelMarkdown, textUtil, InterpolateFunction } from '@grafana/data'; +import { CustomScrollbar, CodeEditor, useStyles2 } from '@grafana/ui'; import config from 'app/core/config'; -// Types import { defaultCodeOptions, PanelOptions, TextMode } from './models.gen'; export interface Props extends PanelProps<PanelOptions> {} -interface State { - html: string; -} +export function TextPanel(props: Props) { + const styles = useStyles2(getStyles); + const [processed, setProcessed] = useState<PanelOptions>({ + mode: props.options.mode, + content: processContent(props.options, props.replaceVariables, config.disableSanitizeHtml), + }); -export class TextPanel extends PureComponent<Props, State> { - declare context: React.ContextType<typeof ThemeContext>; - static contextType = ThemeContext; - - constructor(props: Props) { - super(props); - - this.state = { - html: this.processContent(props.options), - }; - } - - 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; - } - - result = renderTextPanelMarkdown(result); - return this.sanitizeString(result); - } - - 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 ( - <CodeEditor - key={`${code.showLineNumbers}/${code.showMiniMap}`} // will reinit-on change - value={html} - language={code.language ?? defaultCodeOptions.language!} - width={width} - height={height} - containerStyles={styles.codeEditorContainer} - showMiniMap={code.showMiniMap} - showLineNumbers={code.showLineNumbers} - readOnly={true} // future - /> - ); - } + useDebounce( + () => { + const { options, replaceVariables } = props; + const content = processContent(options, replaceVariables, config.disableSanitizeHtml); + if (content !== processed.content || options.mode !== processed.mode) { + setProcessed({ + mode: options.mode, + content, + }); + } + }, + 100, + [props] + ); + if (processed.mode === TextMode.Code) { + const code = props.options.code ?? defaultCodeOptions; return ( - <CustomScrollbar autoHeightMin="100%"> - <DangerouslySetHtmlContent html={html} className={styles.markdown} data-testid="TextPanel-converted-content" /> - </CustomScrollbar> + <CodeEditor + key={`${code.showLineNumbers}/${code.showMiniMap}`} // will reinit-on change + value={processed.content} + language={code.language ?? defaultCodeOptions.language!} + width={props.width} + height={props.height} + containerStyles={styles.codeEditorContainer} + showMiniMap={code.showMiniMap} + showLineNumbers={code.showLineNumbers} + readOnly={true} // future + /> ); } + + return ( + <CustomScrollbar autoHeightMin="100%"> + <DangerouslySetHtmlContent + html={processed.content} + className={styles.markdown} + data-testid="TextPanel-converted-content" + /> + </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` .monaco-editor .margin, .monaco-editor-background { @@ -134,4 +101,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => ({ height: 100%; ` ), -})); +}); diff --git a/public/app/plugins/panel/text/TextPanelEditor.tsx b/public/app/plugins/panel/text/TextPanelEditor.tsx index 8a899ac9b0c..5a3d2417022 100644 --- a/public/app/plugins/panel/text/TextPanelEditor.tsx +++ b/public/app/plugins/panel/text/TextPanelEditor.tsx @@ -1,5 +1,5 @@ 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 { GrafanaTheme2, StandardEditorProps } from '@grafana/data'; @@ -12,7 +12,7 @@ import { 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 styles = useStyles2(getStyles); diff --git a/public/app/plugins/panel/text/module.tsx b/public/app/plugins/panel/text/module.tsx index 05a14f7b45a..90535a49a3b 100644 --- a/public/app/plugins/panel/text/module.tsx +++ b/public/app/plugins/panel/text/module.tsx @@ -11,7 +11,6 @@ export const plugin = new PanelPlugin<PanelOptions>(TextPanel) .addRadio({ path: 'mode', name: 'Mode', - description: 'text mode of the panel', settings: { options: [ { value: TextMode.Markdown, label: 'Markdown' }, @@ -49,7 +48,6 @@ export const plugin = new PanelPlugin<PanelOptions>(TextPanel) id: 'content', path: 'content', name: 'Content', - description: 'Content of the panel', editor: TextPanelEditor, defaultValue: defaultPanelOptions.content, });