diff --git a/.betterer.results b/.betterer.results index 64fe3978eff..e83b056e92c 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2330,9 +2330,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], - "public/app/features/canvas/elements/ellipse.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "public/app/features/canvas/runtime/element.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], diff --git a/devenv/dev-dashboards/panel-canvas/canvas-datalinks.json b/devenv/dev-dashboards/panel-canvas/canvas-datalinks.json index 8cbe54eb83e..0b95594b58c 100644 --- a/devenv/dev-dashboards/panel-canvas/canvas-datalinks.json +++ b/devenv/dev-dashboards/panel-canvas/canvas-datalinks.json @@ -19,7 +19,6 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 740, "links": [], "panels": [ { @@ -1387,6 +1386,545 @@ "width": 55 }, "type": "windTurbine" + }, + { + "background": { + "color": { + "fixed": "transparent" + } + }, + "border": { + "color": { + "fixed": "dark-green" + } + }, + "config": { + "align": "center", + "color": { + "fixed": "rgb(204, 204, 220)" + }, + "size": 24, + "text": { + "fixed": "Cloud" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 39", + "placement": { + "height": 50, + "left": 530, + "top": 438, + "width": 172 + }, + "type": "text" + }, + { + "background": { + "color": { + "fixed": "#D9D9D9" + } + }, + "border": { + "color": { + "fixed": "dark-green" + } + }, + "config": { + "align": "center", + "color": { + "fixed": "#000000" + }, + "text": { + "field": "A-hehwzd", + "mode": "field" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 40", + "placement": { + "height": 50, + "left": 530, + "top": 517, + "width": 188 + }, + "type": "cloud" + }, + { + "background": { + "color": { + "fixed": "#D9D9D9" + } + }, + "border": { + "color": { + "fixed": "dark-green" + } + }, + "config": { + "align": "center", + "color": { + "field": "A-hehwzd", + "fixed": "#000000" + }, + "text": { + "fixed": "Text color" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 41", + "placement": { + "height": 50, + "left": 530, + "top": 596, + "width": 188 + }, + "type": "cloud" + }, + { + "background": { + "color": { + "fixed": "#D9D9D9" + }, + "image": { + "field": "Count (transformation)", + "mode": "field" + } + }, + "border": { + "color": { + "fixed": "dark-green" + } + }, + "config": { + "align": "left", + "color": { + "fixed": "#000000" + }, + "text": { + "fixed": "Background image" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 42", + "placement": { + "height": 50, + "left": 530, + "top": 675, + "width": 188 + }, + "type": "cloud" + }, + { + "background": { + "color": { + "fixed": "#D9D9D9" + } + }, + "border": { + "color": { + "field": "A-hehwzd", + "fixed": "dark-green" + }, + "width": 5 + }, + "config": { + "align": "center", + "color": { + "fixed": "#000000" + }, + "text": { + "fixed": "Border" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 43", + "placement": { + "height": 50, + "left": 530, + "top": 754, + "width": 188 + }, + "type": "cloud" + }, + { + "background": { + "color": { + "fixed": "transparent" + } + }, + "border": { + "color": { + "fixed": "dark-green" + } + }, + "config": { + "align": "center", + "color": { + "fixed": "rgb(204, 204, 220)" + }, + "size": 24, + "text": { + "fixed": "Parallelogram" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 44", + "placement": { + "height": 50, + "left": 806, + "top": 438, + "width": 172 + }, + "type": "text" + }, + { + "background": { + "color": { + "fixed": "#D9D9D9" + } + }, + "border": { + "color": { + "fixed": "dark-green" + } + }, + "config": { + "align": "center", + "color": { + "fixed": "#000000" + }, + "text": { + "field": "A-hehwzd", + "fixed": "", + "mode": "field" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 45", + "placement": { + "height": 50, + "left": 798, + "top": 516, + "width": 188 + }, + "type": "parallelogram" + }, + { + "background": { + "color": { + "fixed": "#D9D9D9" + } + }, + "border": { + "color": { + "fixed": "dark-green" + } + }, + "config": { + "align": "center", + "color": { + "field": "A-hehwzd", + "fixed": "#000000" + }, + "text": { + "fixed": "Text color" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 46", + "placement": { + "height": 50, + "left": 798, + "top": 595, + "width": 188 + }, + "type": "parallelogram" + }, + { + "background": { + "color": { + "fixed": "#D9D9D9" + }, + "image": { + "field": "Count (transformation)", + "mode": "field" + } + }, + "border": { + "color": { + "fixed": "dark-green" + } + }, + "config": { + "align": "left", + "color": { + "fixed": "#000000" + }, + "text": { + "fixed": "Background image" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 47", + "placement": { + "height": 50, + "left": 798, + "top": 674, + "width": 188 + }, + "type": "parallelogram" + }, + { + "background": { + "color": { + "fixed": "#D9D9D9" + } + }, + "border": { + "color": { + "field": "A-hehwzd", + "fixed": "dark-green" + }, + "width": 5 + }, + "config": { + "align": "center", + "color": { + "fixed": "#000000" + }, + "text": { + "fixed": "Border" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 48", + "placement": { + "height": 50, + "left": 798, + "top": 753, + "width": 188 + }, + "type": "parallelogram" + }, + { + "background": { + "color": { + "fixed": "transparent" + } + }, + "border": { + "color": { + "fixed": "dark-green" + } + }, + "config": { + "align": "center", + "color": { + "fixed": "rgb(204, 204, 220)" + }, + "size": 24, + "text": { + "fixed": "Triangle" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 49", + "placement": { + "height": 50, + "left": 1074, + "top": 438, + "width": 172 + }, + "type": "text" + }, + { + "background": { + "color": { + "fixed": "#D9D9D9" + } + }, + "border": { + "color": { + "fixed": "dark-green" + } + }, + "config": { + "align": "center", + "color": { + "fixed": "#000000" + }, + "text": { + "field": "A-hehwzd", + "fixed": "", + "mode": "field" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 50", + "placement": { + "height": 50, + "left": 1066, + "top": 516, + "width": 188 + }, + "type": "triangle" + }, + { + "background": { + "color": { + "fixed": "#D9D9D9" + } + }, + "border": { + "color": { + "fixed": "dark-green" + } + }, + "config": { + "align": "center", + "color": { + "field": "A-hehwzd", + "fixed": "#000000" + }, + "text": { + "fixed": "Text color" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 51", + "placement": { + "height": 50, + "left": 1066, + "top": 595, + "width": 188 + }, + "type": "triangle" + }, + { + "background": { + "color": { + "fixed": "#D9D9D9" + }, + "image": { + "field": "Count (transformation)", + "mode": "field" + } + }, + "border": { + "color": { + "fixed": "dark-green" + } + }, + "config": { + "align": "left", + "color": { + "fixed": "#000000" + }, + "text": { + "fixed": "Background image" + }, + "valign": "bottom" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 52", + "placement": { + "height": 50, + "left": 1066, + "top": 674, + "width": 188 + }, + "type": "triangle" + }, + { + "background": { + "color": { + "fixed": "#D9D9D9" + } + }, + "border": { + "color": { + "field": "A-hehwzd", + "fixed": "dark-green" + }, + "width": 5 + }, + "config": { + "align": "center", + "color": { + "fixed": "#000000" + }, + "text": { + "fixed": "Border" + }, + "valign": "middle" + }, + "constraint": { + "horizontal": "left", + "vertical": "top" + }, + "name": "Element 53", + "placement": { + "height": 50, + "left": 1066, + "top": 753, + "width": 188 + }, + "type": "triangle" } ], "name": "Element 1708700648848", @@ -2935,6 +3473,6 @@ "timezone": "browser", "title": "Panel Tests - Canvas Datalinks", "uid": "adf95uwu7w1s0e", - "version": 26, + "version": 1, "weekStart": "" } \ No newline at end of file diff --git a/public/app/features/canvas/elements/cloud.tsx b/public/app/features/canvas/elements/cloud.tsx new file mode 100644 index 00000000000..735bcfc94ff --- /dev/null +++ b/public/app/features/canvas/elements/cloud.tsx @@ -0,0 +1,200 @@ +import { css } from '@emotion/css'; +import React from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { config } from 'app/core/config'; +import { DimensionContext } from 'app/features/dimensions'; +import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; +import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; +import { getDataLinks } from 'app/plugins/panel/canvas/utils'; + +import { + CanvasElementItem, + CanvasElementProps, + CanvasElementOptions, + defaultBgColor, + defaultTextColor, +} from '../element'; +import { Align, CanvasElementConfig, CanvasElementData, VAlign } from '../types'; + +const Cloud = (props: CanvasElementProps) => { + const { data } = props; + const styles = getStyles(config.theme2, data); + + // uuid needed to avoid id conflicts when multiple elements are rendered + const uniqueId = uuidv4(); + + return ( +
+ + + + + + + + + + {/* Apply background image within the clipping area */} + + + + {data?.text} +
+ ); +}; + +export const cloudItem: CanvasElementItem = { + id: 'cloud', + name: 'Cloud', + description: 'Cloud', + + display: Cloud, + + defaultSize: { + width: 110, + height: 70, + }, + + getNewOptions: (options) => ({ + ...options, + background: { + color: { + fixed: defaultBgColor, + }, + }, + config: { + align: Align.Center, + valign: VAlign.Middle, + color: { + fixed: defaultTextColor, + }, + }, + placement: { + width: options?.placement?.width ?? 110, + height: options?.placement?.height ?? 70, + top: options?.placement?.top, + left: options?.placement?.left, + }, + }), + + // Called when data changes + prepareData: (dimensionContext: DimensionContext, elementOptions: CanvasElementOptions) => { + const textConfig = elementOptions.config; + + const data: CanvasElementData = { + text: textConfig?.text ? dimensionContext.getText(textConfig.text).value() : '', + align: textConfig?.align ?? Align.Center, + valign: textConfig?.valign ?? VAlign.Middle, + size: textConfig?.size, + }; + + if (textConfig?.color) { + data.color = dimensionContext.getColor(textConfig.color).value(); + } + + data.links = getDataLinks(dimensionContext, elementOptions, data.text); + + const { background, border } = elementOptions; + data.backgroundColor = background?.color ? dimensionContext.getColor(background.color).value() : defaultBgColor; + data.borderColor = border?.color ? dimensionContext.getColor(border.color).value() : defaultBgColor; + data.borderWidth = border?.width ?? 0; + + data.backgroundImage = background?.image ? dimensionContext.getResource(background.image).value() : undefined; + + return data; + }, + + registerOptionsUI: (builder) => { + const category = ['Cloud']; + builder + .addCustomEditor({ + category, + id: 'textSelector', + path: 'config.text', + name: 'Text', + editor: TextDimensionEditor, + }) + .addCustomEditor({ + category, + id: 'config.color', + path: 'config.color', + name: 'Text color', + editor: ColorDimensionEditor, + settings: {}, + defaultValue: {}, + }) + .addRadio({ + category, + path: 'config.align', + name: 'Align text', + settings: { + options: [ + { value: Align.Left, label: 'Left' }, + { value: Align.Center, label: 'Center' }, + { value: Align.Right, label: 'Right' }, + ], + }, + defaultValue: Align.Left, + }) + .addRadio({ + category, + path: 'config.valign', + name: 'Vertical align', + settings: { + options: [ + { value: VAlign.Top, label: 'Top' }, + { value: VAlign.Middle, label: 'Middle' }, + { value: VAlign.Bottom, label: 'Bottom' }, + ], + }, + defaultValue: VAlign.Middle, + }) + .addNumberInput({ + category, + path: 'config.size', + name: 'Text size', + settings: { + placeholder: 'Auto', + }, + }); + }, +}; + +const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => { + const textTop = data?.valign === VAlign.Middle ? '50%' : data?.valign === VAlign.Top ? '10%' : '90%'; + const textLeft = data?.align === Align.Center ? '50%' : data?.align === Align.Left ? '10%' : '90%'; + const textTransform = `translate(${data?.align === Align.Center ? '-50%' : data?.align === Align.Left ? '10%' : '-90%'}, ${ + data?.valign === VAlign.Middle ? '-50%' : data?.valign === VAlign.Top ? '10%' : '-90%' + })`; + + return { + container: css({ + height: '100%', + width: '100%', + }), + text: css({ + position: 'absolute', + top: textTop, + left: textLeft, + transform: textTransform, + fontSize: `${data?.size}px`, + color: data?.color, + }), + element: css({ + stroke: data?.borderColor, + strokeWidth: data?.borderWidth, + }), + }; +}; diff --git a/public/app/features/canvas/elements/ellipse.tsx b/public/app/features/canvas/elements/ellipse.tsx index 02663bb344e..60cd9513833 100644 --- a/public/app/features/canvas/elements/ellipse.tsx +++ b/public/app/features/canvas/elements/ellipse.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; -import React, { PureComponent } from 'react'; +import React from 'react'; +import { v4 as uuidv4 } from 'uuid'; import { GrafanaTheme2 } from '@grafana/data'; import { config } from 'app/core/config'; @@ -15,48 +16,55 @@ import { defaultBgColor, defaultTextColor, } from '../element'; -import { Align, VAlign, EllipseConfig, EllipseData } from '../types'; +import { Align, CanvasElementConfig, CanvasElementData, VAlign } from '../types'; -class EllipseDisplay extends PureComponent> { - render() { - const { data } = this.props; - const styles = getStyles(config.theme2, data); - return ( -
- {data?.text} -
- ); - } -} +const Ellipse = (props: CanvasElementProps) => { + const { data } = props; + const styles = getStyles(config.theme2, data); -const getStyles = (theme: GrafanaTheme2, data: any) => ({ - container: css({ - display: 'table', - position: 'absolute', - top: '50%', - transform: 'translateY(-50%)', - width: '100%', - height: '100%', - backgroundColor: data?.backgroundColor, - border: `${data?.width}px solid ${data?.borderColor}`, - // eslint-disable-next-line @grafana/no-border-radius-literal - borderRadius: '50%', - }), - span: css({ - display: 'table-cell', - verticalAlign: data?.valign, - textAlign: data?.align, - fontSize: `${data?.size}px`, - color: data?.color, - }), -}); + // uuid needed to avoid id conflicts when multiple elements are rendered + const uniqueId = uuidv4(); -export const ellipseItem: CanvasElementItem = { + return ( +
+ + + + + + + + + + {/* Apply background image within the clipping area */} + + + + + {data?.text} +
+ ); +}; + +export const ellipseItem: CanvasElementItem = { id: 'ellipse', name: 'Ellipse', description: 'Ellipse', - display: EllipseDisplay, + display: Ellipse, defaultSize: { width: 160, @@ -65,54 +73,52 @@ export const ellipseItem: CanvasElementItem = { getNewOptions: (options) => ({ ...options, - config: { - backgroundColor: { + background: { + color: { fixed: defaultBgColor, }, - borderColor: { - fixed: 'transparent', - }, - width: 1, + }, + config: { align: Align.Center, valign: VAlign.Middle, color: { fixed: defaultTextColor, }, }, - background: { - color: { - fixed: 'transparent', - }, + placement: { + width: options?.placement?.width ?? 160, + height: options?.placement?.height ?? 138, + top: options?.placement?.top, + left: options?.placement?.left, }, }), - prepareData: (dimensionContext: DimensionContext, elementOptions: CanvasElementOptions) => { - const ellipseConfig = elementOptions.config; + prepareData: (dimensionContext: DimensionContext, elementOptions: CanvasElementOptions) => { + const textConfig = elementOptions.config; - const data: EllipseData = { - width: ellipseConfig?.width, - text: ellipseConfig?.text ? dimensionContext.getText(ellipseConfig.text).value() : '', - align: ellipseConfig?.align ?? Align.Center, - valign: ellipseConfig?.valign ?? VAlign.Middle, - size: ellipseConfig?.size, + const data: CanvasElementData = { + text: textConfig?.text ? dimensionContext.getText(textConfig.text).value() : '', + align: textConfig?.align ?? Align.Center, + valign: textConfig?.valign ?? VAlign.Middle, + size: textConfig?.size, }; - if (ellipseConfig?.backgroundColor) { - data.backgroundColor = dimensionContext.getColor(ellipseConfig.backgroundColor).value(); - } - if (ellipseConfig?.borderColor) { - data.borderColor = dimensionContext.getColor(ellipseConfig.borderColor).value(); - } - if (ellipseConfig?.color) { - data.color = dimensionContext.getColor(ellipseConfig.color).value(); + if (textConfig?.color) { + data.color = dimensionContext.getColor(textConfig.color).value(); } data.links = getDataLinks(dimensionContext, elementOptions, data.text); + const { background, border } = elementOptions; + data.backgroundColor = background?.color ? dimensionContext.getColor(background.color).value() : defaultBgColor; + data.borderColor = border?.color ? dimensionContext.getColor(border.color).value() : defaultBgColor; + data.borderWidth = border?.width ?? 0; + + data.backgroundImage = background?.image ? dimensionContext.getResource(background.image).value() : undefined; + return data; }, - // Heatmap overlay options registerOptionsUI: (builder) => { const category = ['Ellipse']; builder @@ -145,32 +151,6 @@ export const ellipseItem: CanvasElementItem = { }, defaultValue: Align.Left, }) - .addCustomEditor({ - category, - id: 'config.borderColor', - path: 'config.borderColor', - name: 'Ellipse border color', - editor: ColorDimensionEditor, - settings: {}, - defaultValue: {}, - }) - .addNumberInput({ - category, - path: 'config.width', - name: 'Ellipse border width', - settings: { - placeholder: 'Auto', - }, - }) - .addCustomEditor({ - category, - id: 'config.backgroundColor', - path: 'config.backgroundColor', - name: 'Ellipse background color', - editor: ColorDimensionEditor, - settings: {}, - defaultValue: {}, - }) .addRadio({ category, path: 'config.valign', @@ -194,3 +174,30 @@ export const ellipseItem: CanvasElementItem = { }); }, }; + +const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => { + const textTop = data?.valign === VAlign.Middle ? '50%' : data?.valign === VAlign.Top ? '10%' : '90%'; + const textLeft = data?.align === Align.Center ? '50%' : data?.align === Align.Left ? '10%' : '90%'; + const textTransform = `translate(${data?.align === Align.Center ? '-50%' : data?.align === Align.Left ? '10%' : '-90%'}, ${ + data?.valign === VAlign.Middle ? '-50%' : data?.valign === VAlign.Top ? '10%' : '-90%' + })`; + + return { + container: css({ + height: '100%', + width: '100%', + }), + text: css({ + position: 'absolute', + top: textTop, + left: textLeft, + transform: textTransform, + fontSize: `${data?.size}px`, + color: data?.color, + }), + element: css({ + stroke: data?.borderColor, + strokeWidth: data?.borderWidth, + }), + }; +}; diff --git a/public/app/features/canvas/elements/parallelogram.tsx b/public/app/features/canvas/elements/parallelogram.tsx new file mode 100644 index 00000000000..3c52ec5b3d6 --- /dev/null +++ b/public/app/features/canvas/elements/parallelogram.tsx @@ -0,0 +1,200 @@ +import { css } from '@emotion/css'; +import React from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { config } from 'app/core/config'; +import { DimensionContext } from 'app/features/dimensions'; +import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; +import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; +import { getDataLinks } from 'app/plugins/panel/canvas/utils'; + +import { + CanvasElementItem, + CanvasElementProps, + CanvasElementOptions, + defaultBgColor, + defaultTextColor, +} from '../element'; +import { Align, CanvasElementConfig, CanvasElementData, VAlign } from '../types'; + +const Parallelogram = (props: CanvasElementProps) => { + const { data } = props; + const styles = getStyles(config.theme2, data); + + // uuid needed to avoid id conflicts when multiple elements are rendered + const uniqueId = uuidv4(); + + return ( +
+ + + + + + + + + + {/* Apply background image within the clipping area */} + + + + {data?.text} +
+ ); +}; + +export const parallelogramItem: CanvasElementItem = { + id: 'parallelogram', + name: 'Parallelogram', + description: 'Parallelogram', + + display: Parallelogram, + + defaultSize: { + width: 250, + height: 150, + }, + + getNewOptions: (options) => ({ + ...options, + background: { + color: { + fixed: defaultBgColor, + }, + }, + config: { + align: Align.Center, + valign: VAlign.Middle, + color: { + fixed: defaultTextColor, + }, + }, + placement: { + width: options?.placement?.width ?? 250, + height: options?.placement?.height ?? 150, + top: options?.placement?.top, + left: options?.placement?.left, + }, + }), + + // Called when data changes + prepareData: (dimensionContext: DimensionContext, elementOptions: CanvasElementOptions) => { + const textConfig = elementOptions.config; + + const data: CanvasElementData = { + text: textConfig?.text ? dimensionContext.getText(textConfig.text).value() : '', + align: textConfig?.align ?? Align.Center, + valign: textConfig?.valign ?? VAlign.Middle, + size: textConfig?.size, + }; + + if (textConfig?.color) { + data.color = dimensionContext.getColor(textConfig.color).value(); + } + + data.links = getDataLinks(dimensionContext, elementOptions, data.text); + + const { background, border } = elementOptions; + data.backgroundColor = background?.color ? dimensionContext.getColor(background.color).value() : defaultBgColor; + data.borderColor = border?.color ? dimensionContext.getColor(border.color).value() : defaultBgColor; + data.borderWidth = border?.width ?? 0; + + data.backgroundImage = background?.image ? dimensionContext.getResource(background.image).value() : undefined; + + return data; + }, + + registerOptionsUI: (builder) => { + const category = ['Parallelogram']; + builder + .addCustomEditor({ + category, + id: 'textSelector', + path: 'config.text', + name: 'Text', + editor: TextDimensionEditor, + }) + .addCustomEditor({ + category, + id: 'config.color', + path: 'config.color', + name: 'Text color', + editor: ColorDimensionEditor, + settings: {}, + defaultValue: {}, + }) + .addRadio({ + category, + path: 'config.align', + name: 'Align text', + settings: { + options: [ + { value: Align.Left, label: 'Left' }, + { value: Align.Center, label: 'Center' }, + { value: Align.Right, label: 'Right' }, + ], + }, + defaultValue: Align.Left, + }) + .addRadio({ + category, + path: 'config.valign', + name: 'Vertical align', + settings: { + options: [ + { value: VAlign.Top, label: 'Top' }, + { value: VAlign.Middle, label: 'Middle' }, + { value: VAlign.Bottom, label: 'Bottom' }, + ], + }, + defaultValue: VAlign.Middle, + }) + .addNumberInput({ + category, + path: 'config.size', + name: 'Text size', + settings: { + placeholder: 'Auto', + }, + }); + }, +}; + +const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => { + const textTop = data?.valign === VAlign.Middle ? '50%' : data?.valign === VAlign.Top ? '10%' : '90%'; + const textLeft = data?.align === Align.Center ? '50%' : data?.align === Align.Left ? '10%' : '90%'; + const textTransform = `translate(${data?.align === Align.Center ? '-50%' : data?.align === Align.Left ? '10%' : '-90%'}, ${ + data?.valign === VAlign.Middle ? '-50%' : data?.valign === VAlign.Top ? '10%' : '-90%' + })`; + + return { + container: css({ + height: '100%', + width: '100%', + }), + text: css({ + position: 'absolute', + top: textTop, + left: textLeft, + transform: textTransform, + fontSize: `${data?.size}px`, + color: data?.color, + }), + element: css({ + stroke: data?.borderColor, + strokeWidth: data?.borderWidth, + }), + }; +}; diff --git a/public/app/features/canvas/elements/triangle.tsx b/public/app/features/canvas/elements/triangle.tsx new file mode 100644 index 00000000000..ef3dc94da2f --- /dev/null +++ b/public/app/features/canvas/elements/triangle.tsx @@ -0,0 +1,201 @@ +import { css } from '@emotion/css'; +import React from 'react'; +import { v4 as uuidv4 } from 'uuid'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { config } from 'app/core/config'; +import { DimensionContext } from 'app/features/dimensions'; +import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; +import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; +import { getDataLinks } from 'app/plugins/panel/canvas/utils'; + +import { + CanvasElementItem, + CanvasElementProps, + CanvasElementOptions, + defaultBgColor, + defaultTextColor, +} from '../element'; +import { Align, CanvasElementConfig, CanvasElementData, VAlign } from '../types'; + +const Triangle = (props: CanvasElementProps) => { + const { data } = props; + const styles = getStyles(config.theme2, data); + + // uuid needed to avoid id conflicts when multiple elements are rendered + const uniqueId = uuidv4(); + + return ( +
+ + + + + + + + + + {/* Apply background image within the clipping area */} + + + + + {data?.text} +
+ ); +}; + +export const triangleItem: CanvasElementItem = { + id: 'triangle', + name: 'Triangle', + description: 'Triangle', + + display: Triangle, + + defaultSize: { + width: 160, + height: 138, + }, + + getNewOptions: (options) => ({ + ...options, + background: { + color: { + fixed: defaultBgColor, + }, + }, + config: { + align: Align.Center, + valign: VAlign.Middle, + color: { + fixed: defaultTextColor, + }, + }, + placement: { + width: options?.placement?.width ?? 160, + height: options?.placement?.height ?? 138, + top: options?.placement?.top, + left: options?.placement?.left, + }, + }), + + // Called when data changes + prepareData: (dimensionContext: DimensionContext, elementOptions: CanvasElementOptions) => { + const textConfig = elementOptions.config; + + const data: CanvasElementData = { + text: textConfig?.text ? dimensionContext.getText(textConfig.text).value() : '', + align: textConfig?.align ?? Align.Center, + valign: textConfig?.valign ?? VAlign.Middle, + size: textConfig?.size, + }; + + if (textConfig?.color) { + data.color = dimensionContext.getColor(textConfig.color).value(); + } + + data.links = getDataLinks(dimensionContext, elementOptions, data.text); + + const { background, border } = elementOptions; + data.backgroundColor = background?.color ? dimensionContext.getColor(background.color).value() : defaultBgColor; + data.borderColor = border?.color ? dimensionContext.getColor(border.color).value() : defaultBgColor; + data.borderWidth = border?.width ?? 0; + + data.backgroundImage = background?.image ? dimensionContext.getResource(background.image).value() : undefined; + + return data; + }, + + registerOptionsUI: (builder) => { + const category = ['Triangle']; + builder + .addCustomEditor({ + category, + id: 'textSelector', + path: 'config.text', + name: 'Text', + editor: TextDimensionEditor, + }) + .addCustomEditor({ + category, + id: 'config.color', + path: 'config.color', + name: 'Text color', + editor: ColorDimensionEditor, + settings: {}, + defaultValue: {}, + }) + .addRadio({ + category, + path: 'config.align', + name: 'Align text', + settings: { + options: [ + { value: Align.Left, label: 'Left' }, + { value: Align.Center, label: 'Center' }, + { value: Align.Right, label: 'Right' }, + ], + }, + defaultValue: Align.Left, + }) + .addRadio({ + category, + path: 'config.valign', + name: 'Vertical align', + settings: { + options: [ + { value: VAlign.Top, label: 'Top' }, + { value: VAlign.Middle, label: 'Middle' }, + { value: VAlign.Bottom, label: 'Bottom' }, + ], + }, + defaultValue: VAlign.Middle, + }) + .addNumberInput({ + category, + path: 'config.size', + name: 'Text size', + settings: { + placeholder: 'Auto', + }, + }); + }, +}; + +const getStyles = (theme: GrafanaTheme2, data: CanvasElementData | undefined) => { + const textTop = data?.valign === VAlign.Middle ? '50%' : data?.valign === VAlign.Top ? '10%' : '90%'; + const textLeft = data?.align === Align.Center ? '50%' : data?.align === Align.Left ? '10%' : '90%'; + const textTransform = `translate(${data?.align === Align.Center ? '-50%' : data?.align === Align.Left ? '10%' : '-90%'}, ${ + data?.valign === VAlign.Middle ? '-50%' : data?.valign === VAlign.Top ? '10%' : '-90%' + })`; + + return { + container: css({ + height: '100%', + width: '100%', + }), + text: css({ + position: 'absolute', + top: textTop, + left: textLeft, + transform: textTransform, + fontSize: `${data?.size}px`, + color: data?.color, + }), + element: css({ + stroke: data?.borderColor, + strokeWidth: data?.borderWidth, + }), + }; +}; diff --git a/public/app/features/canvas/registry.ts b/public/app/features/canvas/registry.ts index a99d142d617..61c2be7409e 100644 --- a/public/app/features/canvas/registry.ts +++ b/public/app/features/canvas/registry.ts @@ -2,15 +2,18 @@ import { Registry } from '@grafana/data'; import { CanvasElementItem, CanvasElementOptions } from './element'; import { buttonItem } from './elements/button'; +import { cloudItem } from './elements/cloud'; import { droneFrontItem } from './elements/droneFront'; import { droneSideItem } from './elements/droneSide'; import { droneTopItem } from './elements/droneTop'; import { ellipseItem } from './elements/ellipse'; import { iconItem } from './elements/icon'; import { metricValueItem } from './elements/metricValue'; +import { parallelogramItem } from './elements/parallelogram'; import { rectangleItem } from './elements/rectangle'; import { serverItem } from './elements/server/server'; import { textItem } from './elements/text'; +import { triangleItem } from './elements/triangle'; import { windTurbineItem } from './elements/windTurbine'; export const DEFAULT_CANVAS_ELEMENT_CONFIG: CanvasElementOptions = { @@ -27,6 +30,9 @@ export const defaultElementItems = [ rectangleItem, iconItem, serverItem, + triangleItem, + cloudItem, + parallelogramItem, ]; export const advancedElementItems = [buttonItem, windTurbineItem, droneTopItem, droneFrontItem, droneSideItem]; diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index 77d6158a47c..01f043582b1 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -20,6 +20,8 @@ import { Scene } from './scene'; let counter = 0; +const SVGElements = new Set(['parallelogram', 'triangle', 'cloud', 'ellipse']); + export class ElementState implements LayerElement { // UID necessary for moveable to work (for now) readonly UID = counter++; @@ -191,8 +193,13 @@ export class ElementState implements LayerElement { this.div.style[key as any] = (this.sizeStyle as any)[key]; } - for (const key in this.dataStyle) { - this.div.style[key as any] = (this.dataStyle as any)[key]; + const elementType = this.options.type; + // SVG elements have their own styles + // TODO: This is a hack, we should have a better way to handle this + if (!SVGElements.has(elementType)) { + for (const key in this.dataStyle) { + this.div.style[key as any] = (this.dataStyle as any)[key]; + } } } } diff --git a/public/app/features/canvas/types.ts b/public/app/features/canvas/types.ts index fbed9984600..ff378e6d35e 100644 --- a/public/app/features/canvas/types.ts +++ b/public/app/features/canvas/types.ts @@ -1,5 +1,5 @@ import { LinkModel } from '@grafana/data/src'; -import { ColorDimensionConfig, TextDimensionConfig } from '@grafana/schema'; +import { ColorDimensionConfig, ResourceDimensionConfig, TextDimensionConfig } from '@grafana/schema'; import { BackgroundImageSize, Constraint, @@ -48,16 +48,20 @@ export interface TextConfig { valign: VAlign; } -export interface EllipseConfig extends TextConfig { +export interface CanvasElementConfig extends TextConfig { backgroundColor?: ColorDimensionConfig; + backgroundImage?: ResourceDimensionConfig; + backgroundSize?: BackgroundImageSize; borderColor?: ColorDimensionConfig; - width?: number; + borderWidth?: number; } -export interface EllipseData extends TextData { +export interface CanvasElementData extends TextData { backgroundColor?: string; + backgroundImage?: string; + backgroundSize?: string; borderColor?: string; - width?: number; + borderWidth?: number; } export interface StandardEditorConfig { diff --git a/public/app/plugins/panel/canvas/migrations.ts b/public/app/plugins/panel/canvas/migrations.ts index 11e43b81d38..5d0d92271e6 100644 --- a/public/app/plugins/panel/canvas/migrations.ts +++ b/public/app/plugins/panel/canvas/migrations.ts @@ -18,5 +18,25 @@ export const canvasMigrationHandler = (panel: PanelModel): Partial => { } } + if (pluginVersion.startsWith('11.0')) { + // Migration for v11.0 for ellipse element refactor: https://github.com/grafana/grafana/pull/84205 + const root = panel.options?.root; + if (root?.elements) { + for (const element of root.elements) { + if (element.type === 'ellipse') { + // Take existing ellipse specific background and border config and apply it to the element's general background and border config + element.background = element.config.backgroundColor; + element.border.color = element.config.borderColor; + element.border.width = element.config.width; + + // Remove the ellipse specific background and border config + delete element.config.backgroundColor; + delete element.config.borderColor; + delete element.config.width; + } + } + } + } + return panel.options; };