diff --git a/packages/grafana-data/src/query/refId.ts b/packages/grafana-data/src/query/refId.ts index bdfa87ef68a..134a668b31f 100644 --- a/packages/grafana-data/src/query/refId.ts +++ b/packages/grafana-data/src/query/refId.ts @@ -1,12 +1,15 @@ -import { DataQuery } from '@grafana/schema'; +import { DataQuery, DataTransformerConfig } from '@grafana/schema'; /** * Finds the next available refId for a query */ -export const getNextRefId = (queries: DataQuery[]): string => { +export const getNextRefId = (item: DataQuery[] | DataTransformerConfig[], prefix?: string): string => { for (let num = 0; ; num++) { - const refId = getRefId(num); - if (!queries.some((query) => query.refId === refId)) { + let refId = getRefId(num); + if (prefix !== undefined) { + refId = `${prefix}${refId}`; + } + if (!item.some((i) => i.refId === refId)) { return refId; } } diff --git a/packages/grafana-data/src/transformations/transformDataFrame.ts b/packages/grafana-data/src/transformations/transformDataFrame.ts index 35951f9f5f6..4acf1b7df82 100644 --- a/packages/grafana-data/src/transformations/transformDataFrame.ts +++ b/packages/grafana-data/src/transformations/transformDataFrame.ts @@ -23,7 +23,7 @@ const getOperator = } const defaultOptions = info.transformation.defaultOptions ?? {}; - const options = { ...defaultOptions, ...config.options }; + const options = { ...defaultOptions, ...config.options, refId: config.refId }; // when running within Scenes, we can skip var interpolation, since it's already handled upstream const isScenes = window.__grafanaSceneContext != null; diff --git a/packages/grafana-data/src/transformations/transformers/joinByField.ts b/packages/grafana-data/src/transformations/transformers/joinByField.ts index e70a0355820..734c113b8fa 100644 --- a/packages/grafana-data/src/transformations/transformers/joinByField.ts +++ b/packages/grafana-data/src/transformations/transformers/joinByField.ts @@ -17,6 +17,7 @@ export enum JoinMode { export interface JoinByFieldOptions { byField?: string; // empty will pick the field automatically mode?: JoinMode; + refId?: string; } export const joinByFieldTransformer: SynchronousDataTransformerInfo = { @@ -42,7 +43,8 @@ export const joinByFieldTransformer: SynchronousDataTransformerInfo frame.refId).join('-')}`; + joined.refId = + options.refId ?? `${DataTransformerID.joinByField}-${data.map((frame) => frame.refId).join('-')}`; return [joined]; } } diff --git a/packages/grafana-data/src/transformations/transformers/merge.ts b/packages/grafana-data/src/transformations/transformers/merge.ts index 275bfa65840..0089d41fa96 100644 --- a/packages/grafana-data/src/transformations/transformers/merge.ts +++ b/packages/grafana-data/src/transformations/transformers/merge.ts @@ -12,7 +12,9 @@ interface ValuePointer { index: number; } -export interface MergeTransformerOptions {} +export interface MergeTransformerOptions { + refId?: string; +} export const mergeTransformer: DataTransformerInfo = { id: DataTransformerID.merge, @@ -44,7 +46,7 @@ export const mergeTransformer: DataTransformerInfo = { const fieldIndexByName: Record> = {}; const fieldNamesForKey: string[] = []; const dataFrame = new MutableDataFrame({ - refId: `${DataTransformerID.merge}-${data.map((frame) => frame.refId).join('-')}`, + refId: options.refId ?? `${DataTransformerID.merge}-${data.map((frame) => frame.refId).join('-')}`, fields: [], }); diff --git a/packages/grafana-data/src/transformations/transformers/reduce.ts b/packages/grafana-data/src/transformations/transformers/reduce.ts index f9d2cd01d4c..eb73e19f5f5 100644 --- a/packages/grafana-data/src/transformations/transformers/reduce.ts +++ b/packages/grafana-data/src/transformations/transformers/reduce.ts @@ -22,6 +22,7 @@ export interface ReduceTransformerOptions { mode?: ReduceTransformerMode; includeTimeField?: boolean; labelsToFields?: boolean; + refId?: string; } export const reduceTransformer: DataTransformerInfo = { @@ -57,7 +58,12 @@ export const reduceTransformer: DataTransformerInfo = // Add a row for each series const res = reduceSeriesToRows(data, matcher, options.reducers, options.labelsToFields); return res - ? [{ ...res, refId: `${DataTransformerID.reduce}-${data.map((frame) => frame.refId).join('-')}` }] + ? [ + { + ...res, + refId: options.refId ?? `${DataTransformerID.reduce}-${data.map((frame) => frame.refId).join('-')}`, + }, + ] : []; }) ), diff --git a/packages/grafana-data/src/transformations/transformers/seriesToRows.ts b/packages/grafana-data/src/transformations/transformers/seriesToRows.ts index 6c7dd055f05..2fb675511f0 100644 --- a/packages/grafana-data/src/transformations/transformers/seriesToRows.ts +++ b/packages/grafana-data/src/transformations/transformers/seriesToRows.ts @@ -16,7 +16,9 @@ import { DataTransformerInfo } from '../../types/transformations'; import { DataTransformerID } from './ids'; -export interface SeriesToRowsTransformerOptions {} +export interface SeriesToRowsTransformerOptions { + refId?: string; +} export const seriesToRowsTransformer: DataTransformerInfo = { id: DataTransformerID.seriesToRows, @@ -38,7 +40,7 @@ export const seriesToRowsTransformer: DataTransformerInfo = {}; const targetFields = new Set(); const dataFrame = new MutableDataFrame({ - refId: `${DataTransformerID.seriesToRows}-${data.map((frame) => frame.refId).join('-')}`, + refId: options.refId ?? `${DataTransformerID.seriesToRows}-${data.map((frame) => frame.refId).join('-')}`, fields: [], }); const metricField: Field = { diff --git a/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx b/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx index 7e712870ae2..18abd7e0505 100644 --- a/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx +++ b/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx @@ -72,7 +72,7 @@ export const QueryOperationRowHeader = ({ // this is just to provide a better experience for mouse users // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
-
{title} test
+
{title}
)} {headerElement} diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx index 27f434da0a9..46ffcfe4741 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { DragDropContext, DropResult, Droppable } from '@hello-pangea/dnd'; import { useState } from 'react'; -import { DataTransformerConfig, GrafanaTheme2, PanelData } from '@grafana/data'; +import { DataTransformerConfig, getNextRefId, GrafanaTheme2, PanelData } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; import { @@ -176,6 +176,12 @@ function TransformationsEditor({ transformations, model, data }: TransformationE model.onChangeTransformations(update.map((t) => t.transformation)); }; + // populate refIds for any transformations that don't have them + const refLessTransformations = transformations.filter((transformation) => transformation.refId === undefined); + refLessTransformations.forEach((transformation) => { + transformation.refId = getNextRefId(transformations, 'T-'); + }); + return ( diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx index 690d086f886..fdcabb034ec 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRow.tsx @@ -14,9 +14,9 @@ import { GrafanaTheme2, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { t, Trans } from '@grafana/i18n'; +import { t } from '@grafana/i18n'; import { getTemplateSrv, reportInteraction } from '@grafana/runtime'; -import { ConfirmModal, FieldValidationMessage, Icon, Input, useStyles2 } from '@grafana/ui'; +import { ConfirmModal, useStyles2 } from '@grafana/ui'; import { QueryOperationAction, QueryOperationToggleAction, @@ -28,6 +28,7 @@ import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo import { TransformationEditor } from './TransformationEditor'; import { TransformationEditorHelpDisplay } from './TransformationEditorHelpDisplay'; import { TransformationFilter } from './TransformationFilter'; +import { TransformationOperationRowHeader } from './TransformationOperationRowHeader'; import { TransformationData } from './TransformationsEditor'; import { TransformationsEditorTransformation } from './types'; @@ -50,11 +51,9 @@ export const TransformationOperationRow = ({ uiConfig, onChange, }: TransformationOperationRowProps) => { - const styles = useStyles2(getStyles); const [showDeleteModal, setShowDeleteModal] = useToggle(false); const [showDebug, toggleShowDebug] = useToggle(false); const [showHelp, toggleShowHelp] = useToggle(false); - const [isRefIdEditing, toggleIsRefIdEditing] = useToggle(false); const disabled = !!configs[index].transformation.disabled; const topic = configs[index].transformation.topic; const showFilterEditor = configs[index].transformation.filter != null || topic != null; @@ -162,41 +161,15 @@ export const TransformationOperationRow = ({ }, [index, data, configs]); const renderHeader = () => { - ///** return ( -
- {!isRefIdEditing && ( - - )} - - {isRefIdEditing && ( - <> - { - onChange(index, { ...configs[index].transformation, refId: input.currentTarget.value }); - }} - className={styles.queryNameInput} - data-testid="query-name-input" - /> - - )} -
+ config.transformation)} + transformationTypeName={uiConfig.name} + disabled + onChange={onChange} + /> ); }; @@ -271,8 +244,6 @@ export const TransformationOperationRow = ({ )} - ); }; - -const getStyles = (theme: GrafanaTheme2) => { - return { - wrapper: css({ - label: 'Wrapper', - display: 'flex', - alignItems: 'center', - marginLeft: theme.spacing(0.5), - overflow: 'hidden', - }), - queryNameWrapper: css({ - display: 'flex', - cursor: 'pointer', - border: '1px solid transparent', - borderRadius: theme.shape.radius.default, - alignItems: 'center', - padding: theme.spacing(0, 0, 0, 0.5), - margin: 0, - background: 'transparent', - overflow: 'hidden', - - '&:hover': { - background: theme.colors.action.hover, - border: `1px dashed ${theme.colors.border.strong}`, - }, - - '&:focus': { - border: `2px solid ${theme.colors.primary.border}`, - }, - - '&:hover, &:focus': { - '.query-name-edit-icon': { - visibility: 'visible', - }, - }, - }), - queryName: css({ - fontWeight: theme.typography.fontWeightMedium, - color: theme.colors.primary.text, - cursor: 'pointer', - overflow: 'hidden', - marginLeft: theme.spacing(0.5), - }), - queryEditIcon: cx( - css({ - marginLeft: theme.spacing(2), - visibility: 'hidden', - }), - 'query-name-edit-icon' - ), - queryNameInput: css({ - maxWidth: '300px', - margin: '-4px 0', - }), - }; -}; diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRowHeader.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRowHeader.tsx new file mode 100644 index 00000000000..dbefc450d95 --- /dev/null +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRowHeader.tsx @@ -0,0 +1,201 @@ +import { css, cx } from '@emotion/css'; +import * as React from 'react'; +import { useState } from 'react'; +import { useToggle } from 'react-use'; + +import { GrafanaTheme2, DataTransformerConfig } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { Icon, Input, useStyles2 } from '@grafana/ui'; + +export interface Props { + index: number; + transformation: DataTransformerConfig; + transformations: DataTransformerConfig[]; + transformationTypeName: string; + disabled?: boolean; + onChange: (index: number, config: DataTransformerConfig) => void; +} + +export const TransformationOperationRowHeader = (props: Props) => { + const { index, transformation, transformations, onChange, disabled, transformationTypeName } = props; + + const styles = useStyles2(getStyles); + const [isRefIdEditing, toggleIsRefIdEditing] = useToggle(false); + const [validationError, setValidationError] = useState(null); + + const onEndEditRefId = (newRefId: string) => { + toggleIsRefIdEditing(false); + + // Ignore change if invalid + if (validationError) { + setValidationError(null); + return; + } + + if (transformation.refId !== newRefId) { + onChange(index, { + ...transformation, + refId: newRefId, + }); + } + }; + + const onInputChange = (event: React.SyntheticEvent) => { + const newRefId = event.currentTarget.value.trim(); + + if (newRefId.length === 0) { + setValidationError('An empty refId is not allowed'); + return; + } + + for (const otherTransformation of transformations) { + if (otherTransformation !== transformation && newRefId === otherTransformation.refId) { + setValidationError('Transformation name already exists'); + return; + } + } + + if (validationError) { + setValidationError(null); + } + }; + + const onEditRefIdBlur = (event: React.SyntheticEvent) => { + onEndEditRefId(event.currentTarget.value.trim()); + }; + + const onKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + onEndEditRefId(event.currentTarget.value); + } + }; + + const onFocus = (event: React.FocusEvent) => { + event.target.select(); + }; + + return ( +
+ {!isRefIdEditing && ( + + )} + + {isRefIdEditing && ( + <> + + + )} +
+
{transformationTypeName}
+
+
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + return { + wrapper: css({ + label: 'Wrapper', + display: 'flex', + alignItems: 'center', + marginLeft: theme.spacing(0.5), + overflow: 'hidden', + }), + refIdWrapper: css({ + display: 'flex', + cursor: 'pointer', + border: '1px solid transparent', + borderRadius: theme.shape.radius.default, + alignItems: 'center', + padding: theme.spacing(0, 0, 0, 0.5), + margin: 0, + background: 'transparent', + overflow: 'hidden', + + '&:hover': { + background: theme.colors.action.hover, + border: `1px dashed ${theme.colors.border.strong}`, + }, + + '&:focus': { + border: `2px solid ${theme.colors.primary.border}`, + }, + + '&:hover, &:focus': { + '.query-name-edit-icon': { + visibility: 'visible', + }, + }, + }), + refIdStyle: css({ + fontWeight: theme.typography.fontWeightMedium, + color: theme.colors.primary.text, + cursor: 'pointer', + overflow: 'hidden', + marginLeft: theme.spacing(0.5), + }), + refIdEditIcon: cx( + css({ + marginLeft: theme.spacing(2), + visibility: 'hidden', + }), + 'query-name-edit-icon' + ), + refIdInput: css({ + maxWidth: '300px', + margin: '-4px 0', + }), + collapsedText: css({ + fontWeight: theme.typography.fontWeightRegular, + fontSize: theme.typography.bodySmall.fontSize, + color: theme.colors.text.secondary, + paddingLeft: theme.spacing(1), + alignItems: 'center', + overflow: 'hidden', + fontStyle: 'italic', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }), + contextInfo: css({ + fontSize: theme.typography.bodySmall.fontSize, + fontStyle: 'italic', + color: theme.colors.text.secondary, + paddingLeft: '10px', + paddingRight: '10px', + }), + itemWrapper: css({ + display: 'flex', + marginLeft: '4px', + }), + title: css({ + fontWeight: theme.typography.fontWeightBold, + color: theme.colors.text.link, + marginLeft: theme.spacing(0.5), + overflow: 'hidden', + textOverflow: 'ellipsis', + }), + disabled: css({ + color: theme.colors.text.disabled, + }), + }; +}; diff --git a/public/app/features/transformers/joinByLabels/joinByLabels.ts b/public/app/features/transformers/joinByLabels/joinByLabels.ts index 9d6d8c99772..338dc039d90 100644 --- a/public/app/features/transformers/joinByLabels/joinByLabels.ts +++ b/public/app/features/transformers/joinByLabels/joinByLabels.ts @@ -8,6 +8,7 @@ import { getDistinctLabels } from '../utils'; export interface JoinByLabelsTransformOptions { value: string; // something must be defined join?: string[]; + refId?: string; } export const getJoinByLabelsTransformer: () => SynchronousDataTransformerInfo = () => ({ @@ -111,7 +112,7 @@ export function joinByLabels(options: JoinByLabelsTransformOptions, data: DataFr const frame: DataFrame = { fields: [], length: nameValues[0].length, - refId: `${DataTransformerID.joinByLabels}-${data.map((frame) => frame.refId).join('-')}`, + refId: options.refId ?? `${DataTransformerID.joinByLabels}-${data.map((frame) => frame.refId).join('-')}`, }; for (let i = 0; i < join.length; i++) { frame.fields.push({