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