diff --git a/devenv/dev-dashboards/panel-text/text-options.json b/devenv/dev-dashboards/panel-text/text-options.json new file mode 100644 index 00000000000..c7afb466570 --- /dev/null +++ b/devenv/dev-dashboards/panel-text/text-options.json @@ -0,0 +1,322 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1348, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 4, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod\n\n---\ntext = $Text", + "mode": "markdown" + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "title": "Markdown (with variables)", + "type": "text" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 5, + "options": { + "code": { + "language": "json", + "showLineNumbers": true, + "showMiniMap": false + }, + "content": "{\n \"datacenter\": $datacenter,\n \"server\": $server,\n \"pod\": $pod\n \"text\": $Text\n}\n", + "mode": "code" + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "title": "JSON (with variables)", + "type": "text" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 6, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "

Data center

\n

$datacenter

\n\n

server

\n

$server

\n\n

pod

\n

$pod

\n\n

Text

\n

$Text

", + "mode": "html" + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "title": "HTML (with variables)", + "type": "text" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 7, + "options": { + "code": { + "language": "markdown", + "showLineNumbers": true, + "showMiniMap": true + }, + "content": "## Data center\n$datacenter\n\n### server\n$server\n\n#### pod = \n$pod\n", + "mode": "code" + }, + "pluginVersion": "9.2.0-pre", + "targets": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "random_walk" + } + ], + "title": "Markdown (code w/ with variables)", + "type": "text" + } + ], + "refresh": false, + "schemaVersion": 37, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "definition": "*", + "hide": 0, + "includeAll": true, + "multi": true, + "name": "datacenter", + "options": [], + "query": { + "query": "*", + "refId": "gdev-testdata-datacenter-Variable-Query" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "definition": "$datacenter.*", + "hide": 0, + "includeAll": true, + "multi": true, + "name": "server", + "options": [], + "query": { + "query": "$datacenter.*", + "refId": "gdev-testdata-server-Variable-Query" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": [ + "AAA", + "ACB" + ], + "value": [ + "AAA", + "ACB" + ] + }, + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "definition": "$datacenter.$server.*", + "hide": 0, + "includeAll": true, + "multi": true, + "name": "pod", + "options": [], + "query": { + "query": "$datacenter.$server.*", + "refId": "gdev-testdata-pod-Variable-Query" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": "temp", + "value": "temp" + }, + "hide": 0, + "name": "Text", + "options": [ + { + "selected": true, + "text": "temp", + "value": "temp" + } + ], + "query": "temp", + "skipUrlSync": false, + "type": "textbox" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Text options", + "uid": "WZ7AhQiVz", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/docs/sources/visualizations/text-panel.md b/docs/sources/visualizations/text-panel.md index 07a98c680d3..15847e33817 100644 --- a/docs/sources/visualizations/text-panel.md +++ b/docs/sources/visualizations/text-panel.md @@ -15,6 +15,26 @@ weight: 1100 # Text -The text panel visualization lets you make information and description panels for your dashboards. +The text panel enables you to directly include text or HTML in your dashboards. This can be used to add contextual information and descriptions or embed complex HTML. -In **Mode**, select whether you want to use markdown or HTML to style your text, then enter content in the box below. Grafana includes a title and paragraph to help you get started, or you can paste content in from another editor. +## Mode + +**Mode** determines how embedded content appears. + +### Markdown + +This option formats the content as [markdown](https://en.wikipedia.org/wiki/Markdown). + +### HTML + +This setting renders the content as [sanitized](https://github.com/grafana/grafana/blob/code-in-text-panel/packages/grafana-data/src/text/sanitize.ts) HTML. If you require more direct control over the output, you can set the +[disable_sanitize_html]({{< relref "../setup-grafana/configure-grafana/#disable_sanitize_html" >}}) flag which enables you to directly enter HTML. + +### Code + +This setting renders content inside a read-only code editor. Select an appropriate language to apply syntax highlighting +to the embedded text. + +## Variables + +[Variables]({{< relref "../variables/syntax/" >}}) in the content will be expanded for display. diff --git a/public/app/plugins/panel/text/TextPanel.tsx b/public/app/plugins/panel/text/TextPanel.tsx index 8494f3a76c8..57096477e0e 100644 --- a/public/app/plugins/panel/text/TextPanel.tsx +++ b/public/app/plugins/panel/text/TextPanel.tsx @@ -4,13 +4,13 @@ import DangerouslySetHtmlContent from 'dangerously-set-html-content'; import { debounce } from 'lodash'; import React, { PureComponent } from 'react'; -import { PanelProps, renderTextPanelMarkdown, textUtil } from '@grafana/data'; +import { GrafanaTheme2, PanelProps, renderTextPanelMarkdown, textUtil } from '@grafana/data'; // Utils -import { CustomScrollbar, stylesFactory } from '@grafana/ui'; +import { CustomScrollbar, CodeEditor, stylesFactory, ThemeContext } from '@grafana/ui'; import config from 'app/core/config'; // Types -import { PanelOptions, TextMode } from './models.gen'; +import { defaultCodeOptions, PanelOptions, TextMode } from './models.gen'; export interface Props extends PanelProps {} @@ -19,6 +19,8 @@ interface State { } export class TextPanel extends PureComponent { + static contextType = ThemeContext; + constructor(props: Props) { super(props); @@ -63,8 +65,8 @@ export class TextPanel extends PureComponent { } interpolateString(content: string): string { - const { replaceVariables } = this.props; - return replaceVariables(content, {}, 'html'); + const { replaceVariables, options } = this.props; + return replaceVariables(content, {}, options.code?.language === 'json' ? 'json' : 'html'); } sanitizeString(content: string): string { @@ -80,6 +82,8 @@ export class TextPanel extends PureComponent { if (mode === TextMode.HTML) { return this.prepareHTML(content); + } else if (mode === TextMode.Code) { + return this.interpolateString(content); } return this.prepareMarkdown(content); @@ -87,23 +91,46 @@ export class TextPanel extends PureComponent { render() { const { html } = this.state; - const styles = getStyles(); + 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 ( - + ); } } -const getStyles = stylesFactory(() => { - return { - content: css` +const getStyles = stylesFactory((theme: GrafanaTheme2) => ({ + codeEditorContainer: css` + .monaco-editor .margin, + .monaco-editor-background { + background-color: ${theme.colors.background.primary}; + } + `, + markdown: cx( + 'markdown-html', + css` height: 100%; - `, - }; -}); + ` + ), +})); diff --git a/public/app/plugins/panel/text/models.cue b/public/app/plugins/panel/text/models.cue index 2c48081d409..d046b646452 100644 --- a/public/app/plugins/panel/text/models.cue +++ b/public/app/plugins/panel/text/models.cue @@ -22,9 +22,20 @@ Panel: thema.#Lineage & { { schemas: [ { - TextMode: "html" | "markdown" @cuetsy(kind="enum",memberNames="HTML|Markdown") + TextMode: "html" | "markdown" | "code" @cuetsy(kind="enum",memberNames="HTML|Markdown|Code") + + CodeLanguage: "json" | "yaml" | "xml" | "typescript" | "sql" | "go" | "markdown" | "html" | *"plaintext" @cuetsy(kind="enum") + + CodeOptions: { + // The language passed to monaco code editor + language: CodeLanguage + showLineNumbers: bool | *false + showMiniMap: bool | *false + } @cuetsy(kind="interface") + PanelOptions: { mode: TextMode | *"markdown" + code?: CodeOptions content: string | *""" # Title diff --git a/public/app/plugins/panel/text/models.gen.ts b/public/app/plugins/panel/text/models.gen.ts index 3af3b8b5420..23ee8a95cfc 100644 --- a/public/app/plugins/panel/text/models.gen.ts +++ b/public/app/plugins/panel/text/models.gen.ts @@ -9,11 +9,39 @@ export const PanelModelVersion = Object.freeze([0, 0]); export enum TextMode { + Code = 'code', HTML = 'html', Markdown = 'markdown', } +export enum CodeLanguage { + Go = 'go', + Html = 'html', + Json = 'json', + Markdown = 'markdown', + Plaintext = 'plaintext', + Sql = 'sql', + Typescript = 'typescript', + Xml = 'xml', + Yaml = 'yaml', +} + +export const defaultCodeLanguage: CodeLanguage = CodeLanguage.Plaintext; + +export interface CodeOptions { + language: CodeLanguage; + showLineNumbers: boolean; + showMiniMap: boolean; +} + +export const defaultCodeOptions: Partial = { + language: CodeLanguage.Plaintext, + showLineNumbers: false, + showMiniMap: false, +}; + export interface PanelOptions { + code?: CodeOptions; content: string; mode: TextMode; } diff --git a/public/app/plugins/panel/text/module.tsx b/public/app/plugins/panel/text/module.tsx index 66f6b127a1e..05a14f7b45a 100644 --- a/public/app/plugins/panel/text/module.tsx +++ b/public/app/plugins/panel/text/module.tsx @@ -2,7 +2,7 @@ import { PanelPlugin } from '@grafana/data'; import { TextPanel } from './TextPanel'; import { TextPanelEditor } from './TextPanelEditor'; -import { defaultPanelOptions, PanelOptions, TextMode } from './models.gen'; +import { CodeLanguage, defaultCodeOptions, defaultPanelOptions, PanelOptions, TextMode } from './models.gen'; import { textPanelMigrationHandler } from './textPanelMigrationHandler'; export const plugin = new PanelPlugin(TextPanel) @@ -16,10 +16,35 @@ export const plugin = new PanelPlugin(TextPanel) options: [ { value: TextMode.Markdown, label: 'Markdown' }, { value: TextMode.HTML, label: 'HTML' }, + { value: TextMode.Code, label: 'Code' }, ], }, defaultValue: defaultPanelOptions.mode, }) + .addSelect({ + path: 'code.language', + name: 'Language', + settings: { + options: Object.values(CodeLanguage).map((v) => ({ + value: v, + label: v, + })), + }, + defaultValue: defaultCodeOptions.language, + showIf: (v) => v.mode === TextMode.Code, + }) + .addBooleanSwitch({ + path: 'code.showLineNumbers', + name: 'Show line numbers', + defaultValue: defaultCodeOptions.showLineNumbers, + showIf: (v) => v.mode === TextMode.Code, + }) + .addBooleanSwitch({ + path: 'code.showMiniMap', + name: 'Show mini map', + defaultValue: defaultCodeOptions.showMiniMap, + showIf: (v) => v.mode === TextMode.Code, + }) .addCustomEditor({ id: 'content', path: 'content',