mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: New basic elements (#84205)
* feat(canvas): new canvas shapes added
This commit is contained in:
@@ -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.", "3"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
[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": [
|
"public/app/features/canvas/runtime/element.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
"editable": true,
|
"editable": true,
|
||||||
"fiscalYearStartMonth": 0,
|
"fiscalYearStartMonth": 0,
|
||||||
"graphTooltip": 0,
|
"graphTooltip": 0,
|
||||||
"id": 740,
|
|
||||||
"links": [],
|
"links": [],
|
||||||
"panels": [
|
"panels": [
|
||||||
{
|
{
|
||||||
@@ -1387,6 +1386,545 @@
|
|||||||
"width": 55
|
"width": 55
|
||||||
},
|
},
|
||||||
"type": "windTurbine"
|
"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",
|
"name": "Element 1708700648848",
|
||||||
@@ -2935,6 +3473,6 @@
|
|||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
"title": "Panel Tests - Canvas Datalinks",
|
"title": "Panel Tests - Canvas Datalinks",
|
||||||
"uid": "adf95uwu7w1s0e",
|
"uid": "adf95uwu7w1s0e",
|
||||||
"version": 26,
|
"version": 1,
|
||||||
"weekStart": ""
|
"weekStart": ""
|
||||||
}
|
}
|
||||||
@@ -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<CanvasElementConfig, CanvasElementData>) => {
|
||||||
|
const { data } = props;
|
||||||
|
const styles = getStyles(config.theme2, data);
|
||||||
|
|
||||||
|
// uuid needed to avoid id conflicts when multiple elements are rendered
|
||||||
|
const uniqueId = uuidv4();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 110 70"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern id={`image-${uniqueId}`} patternUnits="userSpaceOnUse" width="110" height="70">
|
||||||
|
<image xlinkHref={data?.backgroundImage} x="-50" y="-50" width="300" height="300"></image>
|
||||||
|
</pattern>
|
||||||
|
<clipPath id={`cloudClip-${uniqueId}`}>
|
||||||
|
<path d="M 23 13 C -1 13 -7 33 12.2 37 C -7 45.8 14.6 65 30.2 57 C 41 73 77 73 89 57 C 113 57 113 41 98 33 C 113 17 89 1 68 9 C 53 -3 29 -3 23 13 Z" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
{/* Apply background image within the clipping area */}
|
||||||
|
<rect x="0" y="0" width="100%" height="100%" clipPath={`url(#cloudClip-${uniqueId})`} />
|
||||||
|
<path
|
||||||
|
d="M 23 13 C -1 13 -7 33 12.2 37 C -7 45.8 14.6 65 30.2 57 C 41 73 77 73 89 57 C 113 57 113 41 98 33 C 113 17 89 1 68 9 C 53 -3 29 -3 23 13 Z"
|
||||||
|
className={styles.element}
|
||||||
|
style={{ fill: data?.backgroundImage ? `url(#image-${uniqueId})` : data?.backgroundColor }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className={styles.text}>{data?.text}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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<CanvasElementConfig>) => {
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
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 { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
@@ -15,48 +16,55 @@ import {
|
|||||||
defaultBgColor,
|
defaultBgColor,
|
||||||
defaultTextColor,
|
defaultTextColor,
|
||||||
} from '../element';
|
} from '../element';
|
||||||
import { Align, VAlign, EllipseConfig, EllipseData } from '../types';
|
import { Align, CanvasElementConfig, CanvasElementData, VAlign } from '../types';
|
||||||
|
|
||||||
class EllipseDisplay extends PureComponent<CanvasElementProps<EllipseConfig, EllipseData>> {
|
const Ellipse = (props: CanvasElementProps<CanvasElementConfig, CanvasElementData>) => {
|
||||||
render() {
|
const { data } = props;
|
||||||
const { data } = this.props;
|
const styles = getStyles(config.theme2, data);
|
||||||
const styles = getStyles(config.theme2, data);
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<span className={styles.span}>{data?.text}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2, data: any) => ({
|
// uuid needed to avoid id conflicts when multiple elements are rendered
|
||||||
container: css({
|
const uniqueId = uuidv4();
|
||||||
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,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ellipseItem: CanvasElementItem<EllipseConfig, EllipseData> = {
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern id={`image-${uniqueId}`} patternUnits="userSpaceOnUse" width="200" height="200">
|
||||||
|
<image xlinkHref={data?.backgroundImage} x="-50" y="-50" width="300" height="300"></image>
|
||||||
|
</pattern>
|
||||||
|
<clipPath id={`ellipseClip-${uniqueId}`}>
|
||||||
|
<ellipse cx="50%" cy="50%" rx="50%" ry="50%" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
{/* Apply background image within the clipping area */}
|
||||||
|
<rect x="0" y="0" width="100%" height="100%" clipPath={`url(#ellipseClip-${uniqueId})`} />
|
||||||
|
<ellipse
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
rx="50%"
|
||||||
|
ry="50%"
|
||||||
|
className={styles.element}
|
||||||
|
style={{ fill: data?.backgroundImage ? `url(#image-${uniqueId})` : data?.backgroundColor }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<span className={styles.text}>{data?.text}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementData> = {
|
||||||
id: 'ellipse',
|
id: 'ellipse',
|
||||||
name: 'Ellipse',
|
name: 'Ellipse',
|
||||||
description: 'Ellipse',
|
description: 'Ellipse',
|
||||||
|
|
||||||
display: EllipseDisplay,
|
display: Ellipse,
|
||||||
|
|
||||||
defaultSize: {
|
defaultSize: {
|
||||||
width: 160,
|
width: 160,
|
||||||
@@ -65,54 +73,52 @@ export const ellipseItem: CanvasElementItem<EllipseConfig, EllipseData> = {
|
|||||||
|
|
||||||
getNewOptions: (options) => ({
|
getNewOptions: (options) => ({
|
||||||
...options,
|
...options,
|
||||||
config: {
|
background: {
|
||||||
backgroundColor: {
|
color: {
|
||||||
fixed: defaultBgColor,
|
fixed: defaultBgColor,
|
||||||
},
|
},
|
||||||
borderColor: {
|
},
|
||||||
fixed: 'transparent',
|
config: {
|
||||||
},
|
|
||||||
width: 1,
|
|
||||||
align: Align.Center,
|
align: Align.Center,
|
||||||
valign: VAlign.Middle,
|
valign: VAlign.Middle,
|
||||||
color: {
|
color: {
|
||||||
fixed: defaultTextColor,
|
fixed: defaultTextColor,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
background: {
|
placement: {
|
||||||
color: {
|
width: options?.placement?.width ?? 160,
|
||||||
fixed: 'transparent',
|
height: options?.placement?.height ?? 138,
|
||||||
},
|
top: options?.placement?.top,
|
||||||
|
left: options?.placement?.left,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
prepareData: (dimensionContext: DimensionContext, elementOptions: CanvasElementOptions<EllipseConfig>) => {
|
prepareData: (dimensionContext: DimensionContext, elementOptions: CanvasElementOptions<CanvasElementConfig>) => {
|
||||||
const ellipseConfig = elementOptions.config;
|
const textConfig = elementOptions.config;
|
||||||
|
|
||||||
const data: EllipseData = {
|
const data: CanvasElementData = {
|
||||||
width: ellipseConfig?.width,
|
text: textConfig?.text ? dimensionContext.getText(textConfig.text).value() : '',
|
||||||
text: ellipseConfig?.text ? dimensionContext.getText(ellipseConfig.text).value() : '',
|
align: textConfig?.align ?? Align.Center,
|
||||||
align: ellipseConfig?.align ?? Align.Center,
|
valign: textConfig?.valign ?? VAlign.Middle,
|
||||||
valign: ellipseConfig?.valign ?? VAlign.Middle,
|
size: textConfig?.size,
|
||||||
size: ellipseConfig?.size,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ellipseConfig?.backgroundColor) {
|
if (textConfig?.color) {
|
||||||
data.backgroundColor = dimensionContext.getColor(ellipseConfig.backgroundColor).value();
|
data.color = dimensionContext.getColor(textConfig.color).value();
|
||||||
}
|
|
||||||
if (ellipseConfig?.borderColor) {
|
|
||||||
data.borderColor = dimensionContext.getColor(ellipseConfig.borderColor).value();
|
|
||||||
}
|
|
||||||
if (ellipseConfig?.color) {
|
|
||||||
data.color = dimensionContext.getColor(ellipseConfig.color).value();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.links = getDataLinks(dimensionContext, elementOptions, data.text);
|
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;
|
return data;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Heatmap overlay options
|
|
||||||
registerOptionsUI: (builder) => {
|
registerOptionsUI: (builder) => {
|
||||||
const category = ['Ellipse'];
|
const category = ['Ellipse'];
|
||||||
builder
|
builder
|
||||||
@@ -145,32 +151,6 @@ export const ellipseItem: CanvasElementItem<EllipseConfig, EllipseData> = {
|
|||||||
},
|
},
|
||||||
defaultValue: Align.Left,
|
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({
|
.addRadio({
|
||||||
category,
|
category,
|
||||||
path: 'config.valign',
|
path: 'config.valign',
|
||||||
@@ -194,3 +174,30 @@ export const ellipseItem: CanvasElementItem<EllipseConfig, EllipseData> = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -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<CanvasElementConfig, CanvasElementData>) => {
|
||||||
|
const { data } = props;
|
||||||
|
const styles = getStyles(config.theme2, data);
|
||||||
|
|
||||||
|
// uuid needed to avoid id conflicts when multiple elements are rendered
|
||||||
|
const uniqueId = uuidv4();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 250 150"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern id={`image-${uniqueId}`} patternUnits="userSpaceOnUse" width="250" height="150">
|
||||||
|
<image xlinkHref={data?.backgroundImage} x="-50" y="-50" width="350" height="200"></image>
|
||||||
|
</pattern>
|
||||||
|
<clipPath id={`parallelogramClip-${uniqueId}`}>
|
||||||
|
<polygon points="0,150 50,0 250,0 200,150" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
{/* Apply background image within the clipping area */}
|
||||||
|
<rect x="0" y="0" width="100%" height="100%" clipPath={`url(#parallelogramClip-${uniqueId})`} />
|
||||||
|
<polygon
|
||||||
|
points="0,150 50,0 250,0 200,150"
|
||||||
|
className={styles.element}
|
||||||
|
style={{ fill: data?.backgroundImage ? `url(#image-${uniqueId})` : data?.backgroundColor }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className={styles.text}>{data?.text}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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<CanvasElementConfig>) => {
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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<CanvasElementConfig, CanvasElementData>) => {
|
||||||
|
const { data } = props;
|
||||||
|
const styles = getStyles(config.theme2, data);
|
||||||
|
|
||||||
|
// uuid needed to avoid id conflicts when multiple elements are rendered
|
||||||
|
const uniqueId = uuidv4();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<pattern id={`image-${uniqueId}`} patternUnits="userSpaceOnUse" width="200" height="200">
|
||||||
|
<image xlinkHref={data?.backgroundImage} x="-50" y="-50" width="300" height="300"></image>
|
||||||
|
</pattern>
|
||||||
|
<clipPath id={`triangleClip-${uniqueId}`}>
|
||||||
|
<polygon points="100,0 200,200 0,200" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
{/* Apply background image within the clipping area */}
|
||||||
|
<rect x="0" y="0" width="100%" height="100%" clipPath={`url(#triangleClip-${uniqueId})`} />
|
||||||
|
<polygon
|
||||||
|
points="100,0 200,200 0,200"
|
||||||
|
className={styles.element}
|
||||||
|
style={{ fill: data?.backgroundImage ? `url(#image-${uniqueId})` : data?.backgroundColor }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<span className={styles.text}>{data?.text}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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<CanvasElementConfig>) => {
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -2,15 +2,18 @@ import { Registry } from '@grafana/data';
|
|||||||
|
|
||||||
import { CanvasElementItem, CanvasElementOptions } from './element';
|
import { CanvasElementItem, CanvasElementOptions } from './element';
|
||||||
import { buttonItem } from './elements/button';
|
import { buttonItem } from './elements/button';
|
||||||
|
import { cloudItem } from './elements/cloud';
|
||||||
import { droneFrontItem } from './elements/droneFront';
|
import { droneFrontItem } from './elements/droneFront';
|
||||||
import { droneSideItem } from './elements/droneSide';
|
import { droneSideItem } from './elements/droneSide';
|
||||||
import { droneTopItem } from './elements/droneTop';
|
import { droneTopItem } from './elements/droneTop';
|
||||||
import { ellipseItem } from './elements/ellipse';
|
import { ellipseItem } from './elements/ellipse';
|
||||||
import { iconItem } from './elements/icon';
|
import { iconItem } from './elements/icon';
|
||||||
import { metricValueItem } from './elements/metricValue';
|
import { metricValueItem } from './elements/metricValue';
|
||||||
|
import { parallelogramItem } from './elements/parallelogram';
|
||||||
import { rectangleItem } from './elements/rectangle';
|
import { rectangleItem } from './elements/rectangle';
|
||||||
import { serverItem } from './elements/server/server';
|
import { serverItem } from './elements/server/server';
|
||||||
import { textItem } from './elements/text';
|
import { textItem } from './elements/text';
|
||||||
|
import { triangleItem } from './elements/triangle';
|
||||||
import { windTurbineItem } from './elements/windTurbine';
|
import { windTurbineItem } from './elements/windTurbine';
|
||||||
|
|
||||||
export const DEFAULT_CANVAS_ELEMENT_CONFIG: CanvasElementOptions = {
|
export const DEFAULT_CANVAS_ELEMENT_CONFIG: CanvasElementOptions = {
|
||||||
@@ -27,6 +30,9 @@ export const defaultElementItems = [
|
|||||||
rectangleItem,
|
rectangleItem,
|
||||||
iconItem,
|
iconItem,
|
||||||
serverItem,
|
serverItem,
|
||||||
|
triangleItem,
|
||||||
|
cloudItem,
|
||||||
|
parallelogramItem,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const advancedElementItems = [buttonItem, windTurbineItem, droneTopItem, droneFrontItem, droneSideItem];
|
export const advancedElementItems = [buttonItem, windTurbineItem, droneTopItem, droneFrontItem, droneSideItem];
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import { Scene } from './scene';
|
|||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
|
const SVGElements = new Set<string>(['parallelogram', 'triangle', 'cloud', 'ellipse']);
|
||||||
|
|
||||||
export class ElementState implements LayerElement {
|
export class ElementState implements LayerElement {
|
||||||
// UID necessary for moveable to work (for now)
|
// UID necessary for moveable to work (for now)
|
||||||
readonly UID = counter++;
|
readonly UID = counter++;
|
||||||
@@ -191,8 +193,13 @@ export class ElementState implements LayerElement {
|
|||||||
this.div.style[key as any] = (this.sizeStyle as any)[key];
|
this.div.style[key as any] = (this.sizeStyle as any)[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in this.dataStyle) {
|
const elementType = this.options.type;
|
||||||
this.div.style[key as any] = (this.dataStyle as any)[key];
|
// 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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LinkModel } from '@grafana/data/src';
|
import { LinkModel } from '@grafana/data/src';
|
||||||
import { ColorDimensionConfig, TextDimensionConfig } from '@grafana/schema';
|
import { ColorDimensionConfig, ResourceDimensionConfig, TextDimensionConfig } from '@grafana/schema';
|
||||||
import {
|
import {
|
||||||
BackgroundImageSize,
|
BackgroundImageSize,
|
||||||
Constraint,
|
Constraint,
|
||||||
@@ -48,16 +48,20 @@ export interface TextConfig {
|
|||||||
valign: VAlign;
|
valign: VAlign;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EllipseConfig extends TextConfig {
|
export interface CanvasElementConfig extends TextConfig {
|
||||||
backgroundColor?: ColorDimensionConfig;
|
backgroundColor?: ColorDimensionConfig;
|
||||||
|
backgroundImage?: ResourceDimensionConfig;
|
||||||
|
backgroundSize?: BackgroundImageSize;
|
||||||
borderColor?: ColorDimensionConfig;
|
borderColor?: ColorDimensionConfig;
|
||||||
width?: number;
|
borderWidth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EllipseData extends TextData {
|
export interface CanvasElementData extends TextData {
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
backgroundImage?: string;
|
||||||
|
backgroundSize?: string;
|
||||||
borderColor?: string;
|
borderColor?: string;
|
||||||
width?: number;
|
borderWidth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StandardEditorConfig {
|
export interface StandardEditorConfig {
|
||||||
|
|||||||
@@ -18,5 +18,25 @@ export const canvasMigrationHandler = (panel: PanelModel): Partial<Options> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return panel.options;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user