diff --git a/public/app/features/canvas/elements/text.tsx b/public/app/features/canvas/elements/text.tsx new file mode 100644 index 00000000000..408e89120b7 --- /dev/null +++ b/public/app/features/canvas/elements/text.tsx @@ -0,0 +1,210 @@ +import { css } from '@emotion/css'; +import React, { useCallback } from 'react'; +import { useObservable } from 'react-use'; +import { of } from 'rxjs'; + +import { DataFrame, GrafanaTheme2 } from '@grafana/data'; +import { Input, usePanelContext, useStyles2 } from '@grafana/ui'; +import { DimensionContext } from 'app/features/dimensions/context'; +import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; +import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor'; + +import { CanvasElementItem, CanvasElementProps, defaultTextColor } from '../element'; +import { ElementState } from '../runtime/element'; +import { Align, TextConfig, TextData, VAlign } from '../types'; + +const TextDisplay = (props: CanvasElementProps) => { + const { data, isSelected } = props; + const styles = useStyles2(getStyles(data)); + + const context = usePanelContext(); + const scene = context.instanceState?.scene; + + const isEditMode = useObservable(scene?.editModeEnabled ?? of(false)); + + if (isEditMode && isSelected) { + return ; + } + return ( +
+ {data?.text ? data.text : 'Double click to set text'} +
+ ); +}; + +const TextEdit = (props: CanvasElementProps) => { + let { data, config } = props; + const context = usePanelContext(); + let panelData: DataFrame[]; + panelData = context.instanceState?.scene?.data.series; + + const onTextChange = (event: React.SyntheticEvent) => { + const { value: textValue } = event.currentTarget; + saveText(textValue); + }; + + const onKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + const scene = context.instanceState?.scene; + if (scene) { + scene.editModeEnabled.next(false); + } + } + }; + + const saveText = useCallback( + (textValue: string) => { + let selectedElement: ElementState; + selectedElement = context.instanceState?.selected[0]; + if (selectedElement) { + const options = selectedElement.options; + selectedElement.onChange({ + ...options, + config: { + ...options.config, + text: { ...selectedElement.options.config.text, fixed: textValue }, + }, + }); + + // Force a re-render (update scene data after config update) + const scene = context.instanceState?.scene; + if (scene) { + scene.updateData(scene.data); + } + } + }, + [context.instanceState?.scene, context.instanceState?.selected] + ); + + const styles = useStyles2(getStyles(data)); + return ( +
+ {panelData && } +
+ ); +}; + +const getStyles = (data: TextData | undefined) => (theme: GrafanaTheme2) => ({ + container: css` + position: absolute; + height: 100%; + width: 100%; + display: table; + `, + inlineEditorContainer: css` + height: 100%; + width: 100%; + display: flex; + align-items: center; + padding: 10px; + `, + span: css` + display: table-cell; + vertical-align: ${data?.valign}; + text-align: ${data?.align}; + font-size: ${data?.size}px; + color: ${data?.color}; + `, +}); + +export const textItem: CanvasElementItem = { + id: 'text', + name: 'Text', + description: 'Display text', + + display: TextDisplay, + + hasEditMode: true, + + defaultSize: { + width: 100, + height: 50, + }, + + getNewOptions: (options) => ({ + ...options, + config: { + align: Align.Center, + valign: VAlign.Middle, + color: { + fixed: defaultTextColor, + }, + size: 16, + }, + placement: { + top: 100, + left: 100, + }, + }), + + prepareData: (ctx: DimensionContext, cfg: TextConfig) => { + const data: TextData = { + text: cfg.text ? ctx.getText(cfg.text).value() : '', + align: cfg.align ?? Align.Center, + valign: cfg.valign ?? VAlign.Middle, + size: cfg.size, + }; + + if (cfg.color) { + data.color = ctx.getColor(cfg.color).value(); + } + + return data; + }, + + registerOptionsUI: (builder) => { + const category = ['Text']; + 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', + }, + }); + }, +}; diff --git a/public/app/features/canvas/registry.ts b/public/app/features/canvas/registry.ts index 28f6a71683f..b65c26bd4e0 100644 --- a/public/app/features/canvas/registry.ts +++ b/public/app/features/canvas/registry.ts @@ -8,6 +8,7 @@ import { droneTopItem } from './elements/droneTop'; import { iconItem } from './elements/icon'; import { metricValueItem } from './elements/metricValue'; import { rectangleItem } from './elements/rectangle'; +import { textItem } from './elements/text'; import { windTurbineItem } from './elements/windTurbine'; export const DEFAULT_CANVAS_ELEMENT_CONFIG: CanvasElementOptions = { @@ -19,6 +20,7 @@ export const DEFAULT_CANVAS_ELEMENT_CONFIG: CanvasElementOptions = { export const defaultElementItems = [ metricValueItem, // default for now + textItem, rectangleItem, iconItem, ]; diff --git a/public/app/features/dimensions/editors/TextDimensionEditor.tsx b/public/app/features/dimensions/editors/TextDimensionEditor.tsx index a74ecfa0477..bda46e88493 100644 --- a/public/app/features/dimensions/editors/TextDimensionEditor.tsx +++ b/public/app/features/dimensions/editors/TextDimensionEditor.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback, useState } from 'react'; +import React, { FC, useCallback } from 'react'; import { FieldNamePickerConfigSettings, @@ -30,9 +30,6 @@ export const TextDimensionEditor: FC { onChange({ @@ -65,11 +62,9 @@ export const TextDimensionEditor: FC { onFixedChange(''); - setRefresh(refresh + 1); }; const mode = value?.mode ?? TextDimensionMode.Fixed; - return ( <> @@ -90,7 +85,7 @@ export const TextDimensionEditor: FC )} {mode === TextDimensionMode.Fixed && ( - +