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:
		| @@ -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%; | ||||
|     ` | ||||
|   ), | ||||
| })); | ||||
| }); | ||||
|   | ||||
| @@ -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); | ||||
|  | ||||
|   | ||||
| @@ -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, | ||||
|       }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user