Commit 1816ab80 by Torkel Ödegaard Committed by GitHub

Inspect: Transformers (#23598)

* WIP: Inspect transformers

* Updated

* Transformations working in inspect drawer and series to columns working as normal transformation

* Minor name change

* Updated

* Updated

* Fix: fixes crash with dataFrameIndex out of bounds

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
parent 70546724
......@@ -5,6 +5,7 @@ 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 { OrganizeFieldsTransformerOptions } from './transformers/organize';
export { createOrderFieldsComparer } from './transformers/order';
......
......@@ -13,7 +13,9 @@ export function transformDataFrame(options: DataTransformerConfig[], data: DataF
return data;
}
const transformer = info.transformation.transformer(config.options);
const defaultOptions = info.transformation.defaultOptions ?? {};
const options = { ...defaultOptions, ...config.options };
const transformer = info.transformation.transformer(options);
const after = transformer(processed);
// Add a key to the metadata if the data changed
......
......@@ -2,7 +2,6 @@ import { noopTransformer } from './noop';
import { DataFrame, Field } from '../../types/dataFrame';
import { DataTransformerID } from './ids';
import { DataTransformerInfo, MatcherConfig } from '../../types/transformations';
import { FieldMatcherID } from '../matchers/ids';
import { getFieldMatcher, getFrameMatchers } from '../matchers';
export interface FilterOptions {
......@@ -14,9 +13,7 @@ export const filterFieldsTransformer: DataTransformerInfo<FilterOptions> = {
id: DataTransformerID.filterFields,
name: 'Filter Fields',
description: 'select a subset of fields',
defaultOptions: {
include: { id: FieldMatcherID.numeric },
},
defaultOptions: {},
/**
* Return a modified copy of the series. If the transform is not or should not
......
......@@ -5,14 +5,16 @@ import { filterFieldsByNameTransformer } from './filterByName';
import { ArrayVector } from '../../vector';
export interface SeriesToColumnsOptions {
byField: string;
byField?: string;
}
export const seriesToColumnsTransformer: DataTransformerInfo<SeriesToColumnsOptions> = {
id: DataTransformerID.seriesToColumns,
name: 'Series as Columns',
name: 'Series as columns',
description: 'Groups series by field and returns values as columns',
defaultOptions: {},
defaultOptions: {
byField: 'Time',
},
transformer: options => (data: DataFrame[]) => {
const regex = `/^(${options.byField})$/`;
// not sure if I should use filterFieldsByNameTransformer to get the key field
......
......@@ -21,7 +21,7 @@ const OrganizeFieldsTransformerEditor: React.FC<OrganizeFieldsTransformerEditorP
const { options, input, onChange } = props;
const { indexByName, excludeByName, renameByName } = options;
const fieldNames = useMemo(() => fieldNamesFromInput(input), [input]);
const fieldNames = useMemo(() => getAllFieldNamesFromDataFrames(input), [input]);
const orderedFieldNames = useMemo(() => orderFieldNamesByIndex(fieldNames, indexByName), [fieldNames, indexByName]);
const onToggleVisibility = useCallback(
......@@ -185,7 +185,7 @@ const orderFieldNamesByIndex = (fieldNames: string[], indexByName: Record<string
return fieldNames.sort(comparer);
};
const fieldNamesFromInput = (input: DataFrame[]): string[] => {
export const getAllFieldNamesFromDataFrames = (input: DataFrame[]): string[] => {
if (!Array.isArray(input)) {
return [] as string[];
}
......
import React, { useMemo, useCallback } from 'react';
import {
DataTransformerID,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
SeriesToColumnsOptions,
SelectableValue,
} 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">
<div className="gf-form-label">Field</div>
<Select options={fieldNameOptions} value={options.byField} onChange={onSelectField} />
</div>
</div>
);
};
export const seriesToFieldsTransformerRegistryItem: TransformerRegistyItem<SeriesToColumnsOptions> = {
id: DataTransformerID.seriesToColumns,
editor: SeriesToFieldsTransformerEditor,
transformation: standardTransformers.seriesToColumnsTransformer,
name: 'Join by field',
description: 'Joins many time series / data frames by a field',
};
......@@ -3,6 +3,7 @@ import { reduceTransformRegistryItem } from '../components/TransformersUI/Reduce
import { filterFieldsByNameTransformRegistryItem } from '../components/TransformersUI/FilterByNameTransformerEditor';
import { filterFramesByRefIdTransformRegistryItem } from '../components/TransformersUI/FilterByRefIdTransformerEditor';
import { organizeFieldsTransformRegistryItem } from '../components/TransformersUI/OrganizeFieldsTransformerEditor';
import { seriesToFieldsTransformerRegistryItem } from '../components/TransformersUI/SeriesToFieldsTransformerEditor';
export const getStandardTransformers = (): Array<TransformerRegistyItem<any>> => {
return [
......@@ -10,5 +11,6 @@ export const getStandardTransformers = (): Array<TransformerRegistyItem<any>> =>
filterFieldsByNameTransformRegistryItem,
filterFramesByRefIdTransformRegistryItem,
organizeFieldsTransformRegistryItem,
seriesToFieldsTransformerRegistryItem,
];
};
import React, { PureComponent } from 'react';
import { DataFrame, applyFieldOverrides, toCSV, SelectableValue } from '@grafana/data';
import { Button, Select, Icon, Table } from '@grafana/ui';
import {
applyFieldOverrides,
DataFrame,
DataTransformerID,
SelectableValue,
toCSV,
transformDataFrame,
} from '@grafana/data';
import { Button, Field, Icon, Select, Table } from '@grafana/ui';
import { getPanelInspectorStyles } from './styles';
import { config } from 'app/core/config';
import AutoSizer from 'react-virtualized-auto-sizer';
import { saveAs } from 'file-saver';
import { cx } from 'emotion';
interface Props {
data: DataFrame[];
dataFrameIndex: number;
isLoading: boolean;
onSelectedFrameChanged: (item: SelectableValue<number>) => void;
}
export class InspectDataTab extends PureComponent<Props> {
interface State {
transformId: DataTransformerID;
dataFrameIndex: number;
transformationOptions: Array<SelectableValue<string>>;
}
export class InspectDataTab extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
dataFrameIndex: 0,
transformId: DataTransformerID.noop,
transformationOptions: buildTransformationOptions(),
};
}
exportCsv = (dataFrame: DataFrame) => {
......@@ -28,8 +46,44 @@ export class InspectDataTab extends PureComponent<Props> {
saveAs(blob, dataFrame.name + '-' + new Date().getUTCDate() + '.csv');
};
onSelectedFrameChanged = (item: SelectableValue<number>) => {
this.setState({ dataFrameIndex: item.value || 0 });
};
onTransformationChange = (value: SelectableValue<DataTransformerID>) => {
this.setState({ transformId: value.value, dataFrameIndex: 0 });
};
getTransformedData(): DataFrame[] {
const { transformId, transformationOptions } = this.state;
const { data } = this.props;
if (!data) {
return [];
}
const currentTransform = transformationOptions.find(item => item.value === transformId);
if (currentTransform && currentTransform.transformer.id !== DataTransformerID.noop) {
return transformDataFrame([currentTransform.transformer], data);
}
return data;
}
getProcessedData(): DataFrame[] {
return applyFieldOverrides({
data: this.getTransformedData(),
theme: config.theme,
fieldConfig: { defaults: {}, overrides: [] },
replaceVariables: (value: string) => {
return value;
},
});
}
render() {
const { data, dataFrameIndex, isLoading, onSelectedFrameChanged } = this.props;
const { isLoading } = this.props;
const { dataFrameIndex, transformId, transformationOptions } = this.state;
const styles = getPanelInspectorStyles();
if (isLoading) {
......@@ -40,36 +94,32 @@ export class InspectDataTab extends PureComponent<Props> {
);
}
if (!data || !data.length) {
const dataFrames = this.getProcessedData();
if (!dataFrames || !dataFrames.length) {
return <div>No Data</div>;
}
const choices = data.map((frame, index) => {
const choices = dataFrames.map((frame, index) => {
return {
value: index,
label: `${frame.name} (${index})`,
};
});
const processed = applyFieldOverrides({
data,
theme: config.theme,
fieldConfig: { defaults: {}, overrides: [] },
replaceVariables: (value: string) => {
return value;
},
});
return (
<div className={styles.dataTabContent}>
<div className={styles.toolbar}>
<Field label="Transformer" className="flex-grow-1">
<Select options={transformationOptions} value={transformId} onChange={this.onTransformationChange} />
</Field>
{choices.length > 1 && (
<div className={styles.dataFrameSelect}>
<Select options={choices} value={dataFrameIndex} onChange={onSelectedFrameChanged} />
</div>
<Field label="Select result" className={cx(styles.toolbarItem, 'flex-grow-1')}>
<Select options={choices} value={dataFrameIndex} onChange={this.onSelectedFrameChanged} />
</Field>
)}
<div className={styles.downloadCsv}>
<Button variant="primary" onClick={() => this.exportCsv(processed[dataFrameIndex])}>
<Button variant="primary" onClick={() => this.exportCsv(dataFrames[dataFrameIndex])}>
Download CSV
</Button>
</div>
......@@ -83,7 +133,7 @@ export class InspectDataTab extends PureComponent<Props> {
return (
<div style={{ width, height }}>
<Table width={width} height={height} data={processed[dataFrameIndex]} />
<Table width={width} height={height} data={dataFrames[dataFrameIndex]} />
</div>
);
}}
......@@ -93,3 +143,25 @@ export class InspectDataTab extends PureComponent<Props> {
);
}
}
function buildTransformationOptions() {
const transformations: Array<SelectableValue<string>> = [
{
value: 'Do nothing',
label: 'None',
transformer: {
id: DataTransformerID.noop,
},
},
{
value: 'join by time',
label: 'Join by time',
transformer: {
id: DataTransformerID.seriesToColumns,
options: { byField: 'Time' },
},
},
];
return transformations;
}
......@@ -53,8 +53,6 @@ interface State {
last: PanelData;
// Data from the last response
data: DataFrame[];
// The selected data frame
selectedDataFrame: number;
// The Selected Tab
currentTab: InspectTab;
// If the datasource supports custom metadata
......@@ -73,7 +71,6 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
isLoading: true,
last: {} as PanelData,
data: [],
selectedDataFrame: 0,
currentTab: props.defaultTab ?? InspectTab.Data,
drawerWidth: '50%',
};
......@@ -165,10 +162,6 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
this.setState({ currentTab: item.value || InspectTab.Data });
};
onSelectedFrameChanged = (item: SelectableValue<number>) => {
this.setState({ selectedDataFrame: item.value || 0 });
};
renderMetadataInspector() {
const { metaDS, data } = this.state;
if (!metaDS || !metaDS.components?.MetadataInspector) {
......@@ -178,16 +171,8 @@ export class PanelInspectorUnconnected extends PureComponent<Props, State> {
}
renderDataTab() {
const { last, isLoading, selectedDataFrame } = this.state;
return (
<InspectDataTab
data={last.series}
isLoading={isLoading}
dataFrameIndex={selectedDataFrame}
onSelectedFrameChanged={this.onSelectedFrameChanged}
/>
);
const { last, isLoading } = this.state;
return <InspectDataTab data={last.series} isLoading={isLoading} />;
}
renderErrorTab(error?: DataQueryError) {
......
......@@ -223,6 +223,7 @@ export class QueryInspector extends PureComponent<Props, State> {
</Button>
</CopyToClipboard>
)}
<div className="flex-grow-1" />
</div>
<div className={styles.contentQueryInspector}>
{isLoading && <LoadingPlaceholder text="Loading query inspector..." />}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment