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 * 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
|
|||||||
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';
|
|||||||
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> = {
|
|||||||
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';
|
|||||||
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
|
|||||||
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
|
|||||||
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[];
|
||||||
}
|
}
|
||||||
|
@ -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 { 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>> =>
|
|||||||
filterFieldsByNameTransformRegistryItem,
|
filterFieldsByNameTransformRegistryItem,
|
||||||
filterFramesByRefIdTransformRegistryItem,
|
filterFramesByRefIdTransformRegistryItem,
|
||||||
organizeFieldsTransformRegistryItem,
|
organizeFieldsTransformRegistryItem,
|
||||||
|
seriesToFieldsTransformerRegistryItem,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
@ -1,21 +1,39 @@
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
|||||||
|
|
||||||
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> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
|||||||
</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..." />}
|
||||||
|
Loading…
Reference in New Issue
Block a user