First draft of it working
This commit is contained in:
@@ -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}
|
||||
|
||||
+7
-1
@@ -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">
|
||||
|
||||
+11
-97
@@ -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',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
+201
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user