mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
parent
705467242d
commit
1816ab8013
@ -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[];
|
||||
}
|
||||
|
@ -0,0 +1,47 @@
|
||||
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,
|
||||
];
|
||||
};
|
||||
|
@ -1,21 +1,39 @@
|
||||
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..." />}
|
||||
|
Loading…
Reference in New Issue
Block a user