From c04bf6599842fb6719cb2e609fde475d2a9dee12 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 23 Jan 2023 12:44:56 -0800 Subject: [PATCH] TextPanel: Refactor to functional component (#60885) (#61940) backport --- public/app/plugins/panel/text/TextPanel.tsx | 184 ++++++++---------- .../plugins/panel/text/TextPanelEditor.tsx | 4 +- public/app/plugins/panel/text/module.tsx | 2 - 3 files changed, 78 insertions(+), 112 deletions(-) diff --git a/public/app/plugins/panel/text/TextPanel.tsx b/public/app/plugins/panel/text/TextPanel.tsx index 57096477e0e..b3bca99353f 100644 --- a/public/app/plugins/panel/text/TextPanel.tsx +++ b/public/app/plugins/panel/text/TextPanel.tsx @@ -1,126 +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 {} -interface State { - html: string; -} +export function TextPanel(props: Props) { + const styles = useStyles2(getStyles); + const [processed, setProcessed] = useState({ + mode: props.options.mode, + content: processContent(props.options, props.replaceVariables, config.disableSanitizeHtml), + }); -export class TextPanel extends PureComponent { - 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 ( - - ); - } + 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 ( - - - + ); } + + return ( + + + + ); } -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 { @@ -133,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> = ({ value, onChange, context }) => { +export const TextPanelEditor = ({ value, onChange, context }: StandardEditorProps) => { 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(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(TextPanel) id: 'content', path: 'content', name: 'Content', - description: 'Content of the panel', editor: TextPanelEditor, defaultValue: defaultPanelOptions.content, });