import { css } from '@emotion/css'; import { useCallback, useEffect, useRef } from 'react'; import * as React from 'react'; import { useObservable } from 'react-use'; import { of } from 'rxjs'; import { DataFrame, GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; 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, CanvasElementOptions, CanvasElementProps, defaultThemeTextColor } 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 : t('canvas.text-display.double-click-to-set', '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 textRef = useRef(config.text?.fixed ?? ''); // Save text on TextEdit unmount useEffect(() => { return () => { saveText(textRef.current); }; }); const onKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { event.preventDefault(); const scene = context.instanceState?.scene; if (scene) { scene.editModeEnabled.next(false); } } }; const onKeyUp = (event: React.KeyboardEvent) => { textRef.current = event.currentTarget.value; }; 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', alignItems: 'center', padding: theme.spacing(1), }), span: css({ display: 'table-cell', verticalAlign: data?.valign, textAlign: data?.align, fontSize: `${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: defaultThemeTextColor, }, size: 16, }, placement: { width: options?.placement?.width ?? 100, height: options?.placement?.height ?? 100, top: options?.placement?.top, left: options?.placement?.left, rotation: options?.placement?.rotation ?? 0, }, links: options?.links ?? [], }), prepareData: (dimensionContext: DimensionContext, elementOptions: CanvasElementOptions) => { const textConfig = elementOptions.config; const data: TextData = { text: textConfig?.text ? dimensionContext.getText(textConfig.text).value() : '', field: textConfig?.text?.field, align: textConfig?.align ?? Align.Center, valign: textConfig?.valign ?? VAlign.Middle, size: textConfig?.size, }; if (textConfig?.color) { data.color = dimensionContext.getColor(textConfig.color).value(); } return data; }, registerOptionsUI: (builder) => { const category = [t('canvas.text-item.category-text', 'Text')]; builder .addCustomEditor({ category, id: 'textSelector', path: 'config.text', name: t('canvas.text-item.name-text', 'Text'), editor: TextDimensionEditor, }) .addCustomEditor({ category, id: 'config.color', path: 'config.color', name: t('canvas.text-item.name-text-color', 'Text color'), editor: ColorDimensionEditor, settings: {}, defaultValue: {}, }) .addRadio({ category, path: 'config.align', name: t('canvas.text-item.name-align-text', 'Align text'), settings: { options: [ { value: Align.Left, label: t('canvas.text-item.label.left', 'Left') }, { value: Align.Center, label: t('canvas.text-item.label.center', 'Center') }, { value: Align.Right, label: t('canvas.text-item.label.right', 'Right') }, ], }, defaultValue: Align.Left, }) .addRadio({ category, path: 'config.valign', name: t('canvas.text-item.name-vertical-align', 'Vertical align'), settings: { options: [ { value: VAlign.Top, label: t('canvas.text-item.label.top', 'Top') }, { value: VAlign.Middle, label: t('canvas.text-item.label.middle', 'Middle') }, { value: VAlign.Bottom, label: t('canvas.text-item.label.bottom', 'Bottom') }, ], }, defaultValue: VAlign.Middle, }) .addNumberInput({ category, path: 'config.size', name: t('canvas.text-item.name-text-size', 'Text size'), settings: { placeholder: t('canvas.text-item.placeholder.auto', 'Auto'), }, }); }, };