TransformersUI: move transformer ui to grafana app (#24360)

This commit is contained in:
Ryan McKinley
2020-05-08 09:13:40 +02:00
committed by Arve Knudsen
parent fbd3b4b969
commit 10bf03b221
14 changed files with 38 additions and 68 deletions
@@ -3,18 +3,6 @@ export * from './transformers/ids';
export * from './matchers';
export { standardTransformers } from './transformers';
export * from './fieldReducer';
export { FilterFieldsByNameTransformerOptions } from './transformers/filterByName';
export { FilterFramesByRefIdTransformerOptions } from './transformers/filterByRefId';
export { SeriesToColumnsOptions } from './transformers/seriesToColumns';
export { ReduceTransformerOptions } from './transformers/reduce';
export { LabelsToFieldsOptions } from './transformers/labelsToFields';
export {
CalculateFieldTransformerOptions,
CalculateFieldMode,
getResultFieldNameForCalculateFieldTransformerOptions,
} from './transformers/calculateField';
export { OrganizeFieldsTransformerOptions } from './transformers/organize';
export { createOrderFieldsComparer } from './transformers/order';
export { transformDataFrame } from './transformDataFrame';
export {
TransformerRegistyItem,
@@ -16,13 +16,13 @@ export enum CalculateFieldMode {
BinaryOperation = 'binary',
}
interface ReduceOptions {
export interface ReduceOptions {
include?: string; // Assume all fields
reducer: ReducerID;
nullValueMode?: NullValueMode;
}
interface BinaryOptions {
export interface BinaryOptions {
left: string;
operator: BinaryOperationID;
right: string;
@@ -99,7 +99,7 @@ export const calculateFieldTransformer: DataTransformerInfo<CalculateFieldTransf
}
const field = {
name: getResultFieldNameForCalculateFieldTransformerOptions(options),
name: getNameFromOptions(options),
type: FieldType.number,
config: {},
values,
@@ -229,7 +229,7 @@ function findConsistentTimeFieldName(data: DataFrame[]): string | undefined {
return name;
}
export function getResultFieldNameForCalculateFieldTransformerOptions(options: CalculateFieldTransformerOptions) {
export function getNameFromOptions(options: CalculateFieldTransformerOptions) {
if (options.alias?.length) {
return options.alias;
}
@@ -1,371 +0,0 @@
import React, { ChangeEvent } from 'react';
import {
CalculateFieldTransformerOptions,
DataTransformerID,
FieldType,
KeyValue,
ReducerID,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
NullValueMode,
BinaryOperationID,
SelectableValue,
binaryOperators,
CalculateFieldMode,
getResultFieldNameForCalculateFieldTransformerOptions,
getFieldTitle,
} from '@grafana/data';
import { StatsPicker } from '../StatsPicker/StatsPicker';
import { Switch } from '../Forms/Legacy/Switch/Switch';
import { Input } from '../Input/Input';
import { FilterPill } from '../FilterPill/FilterPill';
import { HorizontalGroup } from '../Layout/Layout';
import { Select } from '../Select/Select';
import defaults from 'lodash/defaults';
// Copied from @grafana/data ;( not sure how to best support his
interface ReduceOptions {
include?: string; // Assume all fields
reducer: ReducerID;
nullValueMode?: NullValueMode;
}
interface BinaryOptions {
left: string;
operator: BinaryOperationID;
right: string;
}
interface CalculateFieldTransformerEditorProps extends TransformerUIProps<CalculateFieldTransformerOptions> {}
interface CalculateFieldTransformerEditorState {
include: string;
names: string[];
selected: string[];
}
const calculationModes = [
{ value: CalculateFieldMode.BinaryOperation, label: 'Binary operation' },
{ value: CalculateFieldMode.ReduceRow, label: 'Reduce row' },
];
export class CalculateFieldTransformerEditor extends React.PureComponent<
CalculateFieldTransformerEditorProps,
CalculateFieldTransformerEditorState
> {
constructor(props: CalculateFieldTransformerEditorProps) {
super(props);
this.state = {
include: props.options?.reduce?.include || '',
names: [],
selected: [],
};
}
componentDidMount() {
this.initOptions();
}
componentDidUpdate(oldProps: CalculateFieldTransformerEditorProps) {
if (this.props.input !== oldProps.input) {
this.initOptions();
}
}
private initOptions() {
const { input, options } = this.props;
const include = options?.reduce?.include || '';
const configuredOptions = include.split('|');
const allNames: string[] = [];
const byName: KeyValue<boolean> = {};
for (const frame of input) {
for (const field of frame.fields) {
if (field.type !== FieldType.number) {
continue;
}
const title = getFieldTitle(field, frame, input);
if (!byName[title]) {
byName[title] = true;
allNames.push(title);
}
}
}
if (configuredOptions.length) {
const options: string[] = [];
const selected: string[] = [];
for (const v of allNames) {
if (configuredOptions.includes(v)) {
selected.push(v);
}
options.push(v);
}
this.setState({
names: options,
selected: selected,
});
} else {
this.setState({ names: allNames, selected: [] });
}
}
onToggleReplaceFields = () => {
const { options } = this.props;
this.props.onChange({
...options,
replaceFields: !options.replaceFields,
});
};
onModeChanged = (value: SelectableValue<CalculateFieldMode>) => {
const { options, onChange } = this.props;
const mode = value.value ?? CalculateFieldMode.BinaryOperation;
onChange({
...options,
mode,
});
};
onAliasChanged = (evt: ChangeEvent<HTMLInputElement>) => {
const { options } = this.props;
this.props.onChange({
...options,
alias: evt.target.value,
});
};
//---------------------------------------------------------
// Reduce by Row
//---------------------------------------------------------
updateReduceOptions = (v: ReduceOptions) => {
const { options, onChange } = this.props;
onChange({
...options,
mode: CalculateFieldMode.ReduceRow,
reduce: v,
});
};
onFieldToggle = (fieldName: string) => {
const { selected } = this.state;
if (selected.indexOf(fieldName) > -1) {
this.onChange(selected.filter(s => s !== fieldName));
} else {
this.onChange([...selected, fieldName]);
}
};
onChange = (selected: string[]) => {
this.setState({ selected });
const { reduce } = this.props.options;
this.updateReduceOptions({
...reduce!,
include: selected.join('|'),
});
};
onStatsChange = (stats: string[]) => {
const reducer = stats.length ? (stats[0] as ReducerID) : ReducerID.sum;
const { reduce } = this.props.options;
this.updateReduceOptions({ ...reduce, reducer });
};
renderReduceRow(options?: ReduceOptions) {
const { names, selected } = this.state;
options = defaults(options, { reducer: ReducerID.sum });
return (
<>
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Field name</div>
<HorizontalGroup spacing="xs" align="flex-start" wrap>
{names.map((o, i) => {
return (
<FilterPill
key={`${o}/${i}`}
onClick={() => {
this.onFieldToggle(o);
}}
label={o}
selected={selected.indexOf(o) > -1}
/>
);
})}
</HorizontalGroup>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<div className="gf-form-label width-8">Calculation</div>
<StatsPicker
allowMultiple={false}
className="width-18"
stats={[options.reducer]}
onChange={this.onStatsChange}
defaultStat={ReducerID.sum}
/>
</div>
</div>
</>
);
}
//---------------------------------------------------------
// Binary Operator
//---------------------------------------------------------
updateBinaryOptions = (v: BinaryOptions) => {
const { options, onChange } = this.props;
onChange({
...options,
mode: CalculateFieldMode.BinaryOperation,
binary: v,
});
};
onBinaryLeftChanged = (v: SelectableValue<string>) => {
const { binary } = this.props.options;
this.updateBinaryOptions({
...binary!,
left: v.value!,
});
};
onBinaryRightChanged = (v: SelectableValue<string>) => {
const { binary } = this.props.options;
this.updateBinaryOptions({
...binary!,
right: v.value!,
});
};
onBinaryOperationChanged = (v: SelectableValue<string>) => {
const { binary } = this.props.options;
this.updateBinaryOptions({
...binary!,
operator: v.value! as BinaryOperationID,
});
};
renderBinaryOperation(options?: BinaryOptions) {
options = defaults(options, { reducer: ReducerID.sum });
let foundLeft = !options?.left;
let foundRight = !options?.right;
const names = this.state.names.map(v => {
if (v === options?.left) {
foundLeft = true;
}
if (v === options?.right) {
foundRight = true;
}
return { label: v, value: v };
});
const leftNames = foundLeft ? names : [...names, { label: options?.left, value: options?.left }];
const rightNames = foundRight ? names : [...names, { label: options?.right, value: options?.right }];
const ops = binaryOperators.list().map(v => {
return { label: v.id, value: v.id };
});
return (
<div className="gf-form-inline">
<div className="gf-form">
<div className="gf-form-label width-8">Operation</div>
</div>
<div className="gf-form">
<Select
allowCustomValue
placeholder="Field or number"
options={leftNames}
className="min-width-18 gf-form-spacing"
value={options?.left}
onChange={this.onBinaryLeftChanged}
/>
<Select
className="width-8 gf-form-spacing"
options={ops}
value={options.operator ?? ops[0].value}
onChange={this.onBinaryOperationChanged}
/>
<Select
allowCustomValue
placeholder="Field or number"
className="min-width-10"
options={rightNames}
value={options?.right}
onChange={this.onBinaryRightChanged}
/>
</div>
</div>
);
}
//---------------------------------------------------------
// Render
//---------------------------------------------------------
render() {
const { options } = this.props;
const mode = options.mode ?? CalculateFieldMode.BinaryOperation;
return (
<div>
<div className="gf-form-inline">
<div className="gf-form">
<div className="gf-form-label width-8">Mode</div>
<Select
className="width-18"
options={calculationModes}
value={calculationModes.find(v => v.value === mode)}
onChange={this.onModeChanged}
/>
</div>
</div>
{mode === CalculateFieldMode.BinaryOperation && this.renderBinaryOperation(options.binary)}
{mode === CalculateFieldMode.ReduceRow && this.renderReduceRow(options.reduce)}
<div className="gf-form-inline">
<div className="gf-form">
<div className="gf-form-label width-8">Alias</div>
<Input
className="width-18"
value={options.alias ?? ''}
placeholder={getResultFieldNameForCalculateFieldTransformerOptions(options)}
onChange={this.onAliasChanged}
/>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<Switch
label="Replace all fields"
labelClass="width-8"
checked={!!options.replaceFields}
onChange={this.onToggleReplaceFields}
/>
</div>
</div>
</div>
);
}
}
export const calculateFieldTransformRegistryItem: TransformerRegistyItem<CalculateFieldTransformerOptions> = {
id: DataTransformerID.calculateField,
editor: CalculateFieldTransformerEditor,
transformation: standardTransformers.calculateFieldTransformer,
name: 'Add field from calculation',
description: 'Use the row values to calculate a new field',
};
@@ -1,202 +0,0 @@
import React from 'react';
import {
DataTransformerID,
FilterFieldsByNameTransformerOptions,
KeyValue,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
getFieldTitle,
} from '@grafana/data';
import { HorizontalGroup } from '../Layout/Layout';
import { Input } from '../Input/Input';
import { FilterPill } from '../FilterPill/FilterPill';
import { Field } from '../Forms/Field';
import { css } from 'emotion';
interface FilterByNameTransformerEditorProps extends TransformerUIProps<FilterFieldsByNameTransformerOptions> {}
interface FilterByNameTransformerEditorState {
include: string[];
options: FieldNameInfo[];
selected: string[];
regex?: string;
isRegexValid?: boolean;
}
interface FieldNameInfo {
name: string;
count: number;
}
export class FilterByNameTransformerEditor extends React.PureComponent<
FilterByNameTransformerEditorProps,
FilterByNameTransformerEditorState
> {
constructor(props: FilterByNameTransformerEditorProps) {
super(props);
this.state = {
include: props.options.include || [],
options: [],
selected: [],
isRegexValid: true,
};
}
componentDidMount() {
this.initOptions();
}
componentDidUpdate(oldProps: FilterByNameTransformerEditorProps) {
if (this.props.input !== oldProps.input) {
this.initOptions();
}
}
private initOptions() {
const { input, options } = this.props;
const configuredOptions = options.include ? options.include : [];
const allNames: FieldNameInfo[] = [];
const byName: KeyValue<FieldNameInfo> = {};
for (const frame of input) {
for (const field of frame.fields) {
const id = getFieldTitle(field, frame, input);
let v = byName[id];
if (!v) {
v = byName[id] = {
name: id,
count: 0,
};
allNames.push(v);
}
v.count++;
}
}
let regexOption;
if (configuredOptions.length) {
let selected: FieldNameInfo[] = [];
for (const o of configuredOptions) {
const selectedFields = allNames.filter(n => n.name === o);
if (selectedFields.length > 0) {
selected = selected.concat(selectedFields);
} else {
// there can be only one regex in the options
regexOption = o;
}
}
this.setState({
options: allNames,
selected: selected.map(s => s.name),
regex: regexOption,
});
} else {
this.setState({ options: allNames, selected: allNames.map(n => n.name) });
}
}
onFieldToggle = (fieldName: string) => {
const { selected } = this.state;
if (selected.indexOf(fieldName) > -1) {
this.onChange(selected.filter(s => s !== fieldName));
} else {
this.onChange([...selected, fieldName]);
}
};
onChange = (selected: string[]) => {
const { regex, isRegexValid } = this.state;
let include = selected;
if (regex && isRegexValid) {
include = include.concat([regex]);
}
this.setState({ selected }, () => {
this.props.onChange({
...this.props.options,
include,
});
});
};
onInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const { selected, regex } = this.state;
let isRegexValid = true;
try {
if (regex) {
new RegExp(regex);
}
} catch (e) {
isRegexValid = false;
}
if (isRegexValid) {
this.props.onChange({
...this.props.options,
include: regex ? [...selected, regex] : selected,
});
} else {
this.props.onChange({
...this.props.options,
include: selected,
});
}
this.setState({
isRegexValid,
});
};
render() {
const { options, selected, isRegexValid } = this.state;
return (
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Identifier</div>
<HorizontalGroup spacing="xs" align="flex-start" wrap>
<Field
invalid={!isRegexValid}
error={!isRegexValid ? 'Invalid pattern' : undefined}
className={css`
margin-bottom: 0;
`}
>
<Input
placeholder="Regular expression pattern"
value={this.state.regex || ''}
onChange={e => this.setState({ regex: e.currentTarget.value })}
onBlur={this.onInputBlur}
width={25}
/>
</Field>
{options.map((o, i) => {
const label = `${o.name}${o.count > 1 ? ' (' + o.count + ')' : ''}`;
const isSelected = selected.indexOf(o.name) > -1;
return (
<FilterPill
key={`${o.name}/${i}`}
onClick={() => {
this.onFieldToggle(o.name);
}}
label={label}
selected={isSelected}
/>
);
})}
</HorizontalGroup>
</div>
</div>
);
}
}
export const filterFieldsByNameTransformRegistryItem: TransformerRegistyItem<FilterFieldsByNameTransformerOptions> = {
id: DataTransformerID.filterFieldsByName,
editor: FilterByNameTransformerEditor,
transformation: standardTransformers.filterFieldsByNameTransformer,
name: 'Filter by name',
description: 'Removes part of the query results using a regex pattern. The pattern can be inclusive or exclusive.',
};
@@ -1,139 +0,0 @@
import React from 'react';
import {
DataTransformerID,
FilterFramesByRefIdTransformerOptions,
KeyValue,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
} from '@grafana/data';
import { HorizontalGroup } from '../Layout/Layout';
import { FilterPill } from '../FilterPill/FilterPill';
interface FilterByRefIdTransformerEditorProps extends TransformerUIProps<FilterFramesByRefIdTransformerOptions> {}
interface FilterByRefIdTransformerEditorState {
include: string;
options: RefIdInfo[];
selected: string[];
}
interface RefIdInfo {
refId: string;
count: number;
}
export class FilterByRefIdTransformerEditor extends React.PureComponent<
FilterByRefIdTransformerEditorProps,
FilterByRefIdTransformerEditorState
> {
constructor(props: FilterByRefIdTransformerEditorProps) {
super(props);
this.state = {
include: props.options.include || '',
options: [],
selected: [],
};
}
componentDidMount() {
this.initOptions();
}
componentDidUpdate(oldProps: FilterByRefIdTransformerEditorProps) {
if (this.props.input !== oldProps.input) {
this.initOptions();
}
}
private initOptions() {
const { input, options } = this.props;
const configuredOptions = options.include ? options.include.split('|') : [];
const allNames: RefIdInfo[] = [];
const byName: KeyValue<RefIdInfo> = {};
for (const frame of input) {
if (frame.refId) {
let v = byName[frame.refId];
if (!v) {
v = byName[frame.refId] = {
refId: frame.refId,
count: 0,
};
allNames.push(v);
}
v.count++;
}
}
if (configuredOptions.length) {
const options: RefIdInfo[] = [];
const selected: RefIdInfo[] = [];
for (const v of allNames) {
if (configuredOptions.includes(v.refId)) {
selected.push(v);
}
options.push(v);
}
this.setState({
options,
selected: selected.map(s => s.refId),
});
} else {
this.setState({ options: allNames, selected: [] });
}
}
onFieldToggle = (fieldName: string) => {
const { selected } = this.state;
if (selected.indexOf(fieldName) > -1) {
this.onChange(selected.filter(s => s !== fieldName));
} else {
this.onChange([...selected, fieldName]);
}
};
onChange = (selected: string[]) => {
this.setState({ selected });
this.props.onChange({
...this.props.options,
include: selected.join('|'),
});
};
render() {
const { options, selected } = this.state;
return (
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Series refId</div>
<HorizontalGroup spacing="xs" align="flex-start" wrap>
{options.map((o, i) => {
const label = `${o.refId}${o.count > 1 ? ' (' + o.count + ')' : ''}`;
const isSelected = selected.indexOf(o.refId) > -1;
return (
<FilterPill
key={`${o.refId}/${i}`}
onClick={() => {
this.onFieldToggle(o.refId);
}}
label={label}
selected={isSelected}
/>
);
})}
</HorizontalGroup>
</div>
</div>
);
}
}
export const filterFramesByRefIdTransformRegistryItem: TransformerRegistyItem<FilterFramesByRefIdTransformerOptions> = {
id: DataTransformerID.filterByRefId,
editor: FilterByRefIdTransformerEditor,
transformation: standardTransformers.filterFramesByRefIdTransformer,
name: 'Filter data by query',
description:
'Filter data by query. This is useful if you are sharing the results from a different panel that has many queries and you want to only visualize a subset of that in this panel.',
};
@@ -1,25 +0,0 @@
import React from 'react';
import {
DataTransformerID,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
LabelsToFieldsOptions,
} from '@grafana/data';
export const LabelsAsFieldsTransformerEditor: React.FC<TransformerUIProps<LabelsToFieldsOptions>> = ({
input,
options,
onChange,
}) => {
return null;
};
export const labelsToFieldsTransformerRegistryItem: TransformerRegistyItem<LabelsToFieldsOptions> = {
id: DataTransformerID.labelsToFields,
editor: LabelsAsFieldsTransformerEditor,
transformation: standardTransformers.labelsToFieldsTransformer,
name: 'Labels to fields',
description: `Groups series by time and return labels or tags as fields.
Useful for showing time series with labels in a table where each label key becomes a seperate column`,
};
@@ -1,234 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import { css, cx } from 'emotion';
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
import {
createOrderFieldsComparer,
DataFrame,
DataTransformerID,
GrafanaTheme,
OrganizeFieldsTransformerOptions,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
getFieldTitle,
} from '@grafana/data';
import { stylesFactory, useTheme } from '../../themes';
import { Input } from '../Input/Input';
import { IconButton } from '../IconButton/IconButton';
interface OrganizeFieldsTransformerEditorProps extends TransformerUIProps<OrganizeFieldsTransformerOptions> {}
const OrganizeFieldsTransformerEditor: React.FC<OrganizeFieldsTransformerEditorProps> = props => {
const { options, input, onChange } = props;
const { indexByName, excludeByName, renameByName } = options;
const fieldNames = useMemo(() => getAllFieldNamesFromDataFrames(input), [input]);
const orderedFieldNames = useMemo(() => orderFieldNamesByIndex(fieldNames, indexByName), [fieldNames, indexByName]);
const onToggleVisibility = useCallback(
(field: string, shouldExclude: boolean) => {
onChange({
...options,
excludeByName: {
...excludeByName,
[field]: shouldExclude,
},
});
},
[onChange, excludeByName, indexByName]
);
const onDragEnd = useCallback(
(result: DropResult) => {
if (!result || !result.destination) {
return;
}
const startIndex = result.source.index;
const endIndex = result.destination.index;
if (startIndex === endIndex) {
return;
}
onChange({
...options,
indexByName: reorderToIndex(fieldNames, startIndex, endIndex),
});
},
[onChange, indexByName, excludeByName, fieldNames]
);
const onRenameField = useCallback(
(from: string, to: string) => {
onChange({
...options,
renameByName: {
...options.renameByName,
[from]: to,
},
});
},
[onChange, fieldNames, renameByName]
);
// Show warning that we only apply the first frame
if (input.length > 1) {
return <div>Organize fields only works with a single frame. Consider applying a join transformation first.</div>;
}
return (
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="sortable-fields-transformer" direction="vertical">
{provided => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{orderedFieldNames.map((fieldName, index) => {
return (
<DraggableFieldName
fieldName={fieldName}
renamedFieldName={renameByName[fieldName]}
index={index}
onToggleVisibility={onToggleVisibility}
onRenameField={onRenameField}
visible={!excludeByName[fieldName]}
key={fieldName}
/>
);
})}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
};
OrganizeFieldsTransformerEditor.displayName = 'OrganizeFieldsTransformerEditor';
interface DraggableFieldProps {
fieldName: string;
renamedFieldName?: string;
index: number;
visible: boolean;
onToggleVisibility: (fieldName: string, isVisible: boolean) => void;
onRenameField: (from: string, to: string) => void;
}
const DraggableFieldName: React.FC<DraggableFieldProps> = ({
fieldName,
renamedFieldName,
index,
visible,
onToggleVisibility,
onRenameField,
}) => {
const theme = useTheme();
const styles = getFieldNameStyles(theme);
return (
<Draggable draggableId={fieldName} index={index}>
{provided => (
<div
className="gf-form-inline"
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--justify-left width-30">
<i className={cx('fa fa-ellipsis-v', styles.draggable)} />
<IconButton
className={styles.toggle}
size="md"
name={visible ? 'eye' : 'eye-slash'}
surface="header"
onClick={() => onToggleVisibility(fieldName, visible)}
/>
<span className={styles.name} title={fieldName}>
{fieldName}
</span>
</div>
<Input
className="flex-grow-1"
defaultValue={renamedFieldName || ''}
placeholder={`Rename ${fieldName}`}
onBlur={event => onRenameField(fieldName, event.currentTarget.value)}
/>
</div>
</div>
)}
</Draggable>
);
};
DraggableFieldName.displayName = 'DraggableFieldName';
const getFieldNameStyles = stylesFactory((theme: GrafanaTheme) => ({
toggle: css`
margin: 0 8px;
color: ${theme.colors.textWeak};
`,
draggable: css`
padding: 0 ${theme.spacing.xs};
font-size: ${theme.typography.size.md};
opacity: 0.4;
&:hover {
color: ${theme.colors.textStrong};
}
`,
name: css`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: ${theme.typography.size.sm};
font-weight: ${theme.typography.weight.semibold};
`,
}));
const reorderToIndex = (fieldNames: string[], startIndex: number, endIndex: number) => {
const result = Array.from(fieldNames);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result.reduce((nameByIndex, fieldName, index) => {
nameByIndex[fieldName] = index;
return nameByIndex;
}, {} as Record<string, number>);
};
const orderFieldNamesByIndex = (fieldNames: string[], indexByName: Record<string, number> = {}): string[] => {
if (!indexByName || Object.keys(indexByName).length === 0) {
return fieldNames;
}
const comparer = createOrderFieldsComparer(indexByName);
return fieldNames.sort(comparer);
};
export const getAllFieldNamesFromDataFrames = (input: DataFrame[]): string[] => {
if (!Array.isArray(input)) {
return [] as string[];
}
return Object.keys(
input.reduce((names, frame) => {
if (!frame || !Array.isArray(frame.fields)) {
return names;
}
return frame.fields.reduce((names, field) => {
const t = getFieldTitle(field, frame, input);
names[t] = true;
return names;
}, names);
}, {} as Record<string, boolean>)
);
};
export const organizeFieldsTransformRegistryItem: TransformerRegistyItem<OrganizeFieldsTransformerOptions> = {
id: DataTransformerID.organize,
editor: OrganizeFieldsTransformerEditor,
transformation: standardTransformers.organizeFieldsTransformer,
name: 'Organize fields',
description:
"Allows the user to re-order, hide, or rename fields / columns. Useful when data source doesn't allow overrides for visualizing data.",
};
@@ -1,44 +0,0 @@
import React from 'react';
import { StatsPicker } from '../StatsPicker/StatsPicker';
import {
ReduceTransformerOptions,
DataTransformerID,
ReducerID,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
} from '@grafana/data';
// TODO: Minimal implementation, needs some <3
export const ReduceTransformerEditor: React.FC<TransformerUIProps<ReduceTransformerOptions>> = ({
options,
onChange,
}) => {
return (
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Calculations</div>
<StatsPicker
className="flex-grow-1"
placeholder="Choose Stat"
allowMultiple
stats={options.reducers || []}
onChange={stats => {
onChange({
...options,
reducers: stats as ReducerID[],
});
}}
/>
</div>
</div>
);
};
export const reduceTransformRegistryItem: TransformerRegistyItem<ReduceTransformerOptions> = {
id: DataTransformerID.reduce,
editor: ReduceTransformerEditor,
transformation: standardTransformers.reduceTransformer,
name: standardTransformers.reduceTransformer.name,
description: standardTransformers.reduceTransformer.description,
};
@@ -1,48 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import {
DataTransformerID,
SelectableValue,
SeriesToColumnsOptions,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
} from '@grafana/data';
import { getAllFieldNamesFromDataFrames } from './OrganizeFieldsTransformerEditor';
import { Select } from '../Select/Select';
export const SeriesToFieldsTransformerEditor: React.FC<TransformerUIProps<SeriesToColumnsOptions>> = ({
input,
options,
onChange,
}) => {
const fieldNames = useMemo(() => getAllFieldNamesFromDataFrames(input), [input]);
const fieldNameOptions = fieldNames.map((item: string) => ({ label: item, value: item }));
const onSelectField = useCallback(
(value: SelectableValue<string>) => {
onChange({
...options,
byField: value.value,
});
},
[onChange, options]
);
return (
<div className="gf-form-inline">
<div className="gf-form gf-form--grow">
<div className="gf-form-label width-8">Field name</div>
<Select options={fieldNameOptions} value={options.byField} onChange={onSelectField} isClearable />
</div>
</div>
);
};
export const seriesToFieldsTransformerRegistryItem: TransformerRegistyItem<SeriesToColumnsOptions> = {
id: DataTransformerID.seriesToColumns,
editor: SeriesToFieldsTransformerEditor,
transformation: standardTransformers.seriesToColumnsTransformer,
name: 'Outer join',
description:
'Joins many time series/tables by a field. This can be used to outer join multiple time series on the _time_ field to show many time series in one table.',
};
@@ -29,6 +29,7 @@ export { TagsInput } from './TagsInput/TagsInput';
export { Pagination } from './Pagination/Pagination';
export { Tag } from './Tags/Tag';
export { TagList } from './Tags/TagList';
export { FilterPill } from './FilterPill/FilterPill';
export { ConfirmModal } from './ConfirmModal/ConfirmModal';
export { QueryField } from './QueryField/QueryField';
-2
View File
@@ -11,5 +11,3 @@ export * from './slate-plugins';
// Exposes standard editors for registries of optionsUi config and panel options UI
export { getStandardFieldConfigs, getStandardOptionEditors } from './utils//standardEditors';
// Exposes standard transformers for registry of Transformations
export { getStandardTransformers } from './utils/standardTransformers';
@@ -1,20 +0,0 @@
import { TransformerRegistyItem } from '@grafana/data';
import { reduceTransformRegistryItem } from '../components/TransformersUI/ReduceTransformerEditor';
import { filterFieldsByNameTransformRegistryItem } from '../components/TransformersUI/FilterByNameTransformerEditor';
import { filterFramesByRefIdTransformRegistryItem } from '../components/TransformersUI/FilterByRefIdTransformerEditor';
import { organizeFieldsTransformRegistryItem } from '../components/TransformersUI/OrganizeFieldsTransformerEditor';
import { seriesToFieldsTransformerRegistryItem } from '../components/TransformersUI/SeriesToFieldsTransformerEditor';
import { calculateFieldTransformRegistryItem } from '../components/TransformersUI/CalculateFieldTransformerEditor';
import { labelsToFieldsTransformerRegistryItem } from '../components/TransformersUI/LabelsToFieldsTransformerEditor';
export const getStandardTransformers = (): Array<TransformerRegistyItem<any>> => {
return [
reduceTransformRegistryItem,
filterFieldsByNameTransformRegistryItem,
filterFramesByRefIdTransformRegistryItem,
organizeFieldsTransformRegistryItem,
seriesToFieldsTransformerRegistryItem,
calculateFieldTransformRegistryItem,
labelsToFieldsTransformerRegistryItem,
];
};