First draft of it working

This commit is contained in:
Kristina Durivage
2025-09-22 20:15:46 -05:00
parent 141885acae
commit 8f7ddd3c4c
11 changed files with 248 additions and 111 deletions
+7 -4
View File
@@ -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;
}
}
@@ -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;
@@ -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<JoinByFieldOptions> = {
@@ -42,7 +43,8 @@ export const joinByFieldTransformer: SynchronousDataTransformerInfo<JoinByFieldO
}
const joined = joinDataFrames({ frames: data, joinBy, mode: options.mode });
if (joined) {
joined.refId = `${DataTransformerID.joinByField}-${data.map((frame) => frame.refId).join('-')}`;
joined.refId =
options.refId ?? `${DataTransformerID.joinByField}-${data.map((frame) => frame.refId).join('-')}`;
return [joined];
}
}
@@ -12,7 +12,9 @@ interface ValuePointer {
index: number;
}
export interface MergeTransformerOptions {}
export interface MergeTransformerOptions {
refId?: string;
}
export const mergeTransformer: DataTransformerInfo<MergeTransformerOptions> = {
id: DataTransformerID.merge,
@@ -44,7 +46,7 @@ export const mergeTransformer: DataTransformerInfo<MergeTransformerOptions> = {
const fieldIndexByName: Record<string, Record<number, number>> = {};
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: [],
});
@@ -22,6 +22,7 @@ export interface ReduceTransformerOptions {
mode?: ReduceTransformerMode;
includeTimeField?: boolean;
labelsToFields?: boolean;
refId?: string;
}
export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> = {
@@ -57,7 +58,12 @@ export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> =
// 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('-')}`,
},
]
: [];
})
),
@@ -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<SeriesToRowsTransformerOptions> = {
id: DataTransformerID.seriesToRows,
@@ -38,7 +40,7 @@ export const seriesToRowsTransformer: DataTransformerInfo<SeriesToRowsTransforme
const timeFieldByIndex: Record<number, number> = {};
const targetFields = new Set<string>();
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 = {
@@ -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
<div className={styles.titleWrapper} onClick={onRowToggle}>
<div className={cx(styles.title, disabled && styles.disabled)}>{title} test</div>
<div className={cx(styles.title, disabled && styles.disabled)}>{title}</div>
</div>
)}
{headerElement}
@@ -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 (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="transformations-list" direction="vertical">
@@ -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 = () => {
///** <!--{validationError && <FieldValidationMessage horizontal>{validationError}</FieldValidationMessage>}-->
return (
<div className={styles.wrapper}>
{!isRefIdEditing && (
<button
className={styles.queryNameWrapper}
title={t('query.query-editor-row-header.query-name-div-title-edit-query-name', 'Edit transformation name')}
onClick={() => toggleIsRefIdEditing()}
data-testid="query-name-div"
type="button"
>
<span className={styles.queryName}>{configs[index].refId}</span>
<Icon name="pen" className={styles.queryEditIcon} size="sm" />
</button>
)}
{isRefIdEditing && (
<>
<Input
type="text"
defaultValue={configs[index].refId}
//onBlur={onEditQueryBlur}
autoFocus
//onKeyDown={onKeyDown}
//onFocus={onFocus}
//invalid={validationError !== null}
onChange={(input) => {
onChange(index, { ...configs[index].transformation, refId: input.currentTarget.value });
}}
className={styles.queryNameInput}
data-testid="query-name-input"
/>
</>
)}
</div>
<TransformationOperationRowHeader
index={index}
transformation={configs[index].transformation}
transformations={configs.map((config) => config.transformation)}
transformationTypeName={uiConfig.name}
disabled
onChange={onChange}
/>
);
};
@@ -271,8 +244,6 @@ export const TransformationOperationRow = ({
<QueryOperationRow
id={id}
index={index}
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
title={`${index + 1} - ${uiConfig.name}`}
draggable
actions={renderActions}
headerElement={renderHeader}
@@ -291,7 +262,6 @@ export const TransformationOperationRow = ({
onChange={onChange}
/>
)}
<TransformationEditor
input={input}
output={output}
@@ -307,59 +277,3 @@ 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',
}),
};
};
@@ -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<string | null>(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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
onEndEditRefId(event.currentTarget.value.trim());
};
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
onEndEditRefId(event.currentTarget.value);
}
};
const onFocus = (event: React.FocusEvent<HTMLInputElement>) => {
event.target.select();
};
return (
<div className={styles.wrapper}>
{!isRefIdEditing && (
<button
className={styles.refIdWrapper}
title={t('query.query-editor-row-header.query-name-div-title-edit-query-name', 'Edit transformation name')}
onClick={() => toggleIsRefIdEditing()}
data-testid="query-name-div"
type="button"
>
<span className={styles.refIdStyle}>{transformation.refId}</span>
<Icon name="pen" className={styles.refIdEditIcon} size="sm" />
</button>
)}
{isRefIdEditing && (
<>
<Input
type="text"
defaultValue={transformation.refId}
onBlur={onEditRefIdBlur}
autoFocus
onKeyDown={onKeyDown}
onFocus={onFocus}
//invalid={validationError !== null}
onChange={onInputChange}
className={styles.refIdInput}
data-testid="transformation-refid-input"
/>
</>
)}
<div>
<div className={cx(styles.title, disabled && styles.disabled)}>{transformationTypeName}</div>
</div>
</div>
);
};
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,
}),
};
};
@@ -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<JoinByLabelsTransformOptions> = () => ({
@@ -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({