TransformersUI: move transformer ui to grafana app (#24360)
This commit is contained in:
committed by
Arve Knudsen
parent
fbd3b4b969
commit
10bf03b221
@@ -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;
|
||||
}
|
||||
|
||||
-371
@@ -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`,
|
||||
};
|
||||
-234
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
};
|
||||
Reference in New Issue
Block a user