Transformations: filter results by refId (#20261)

This commit is contained in:
Ryan McKinley 2019-11-08 11:47:35 -08:00 committed by GitHub
parent 767c672a2f
commit 8430a182ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 276 additions and 5 deletions

View File

@ -4,4 +4,5 @@ export * from './matchers';
export * from './transformers';
export * from './fieldReducer';
export { FilterFieldsByNameTransformerOptions } from './transformers/filterByName';
export { FilterFramesByRefIdTransformerOptions } from './transformers/filterByRefId';
export { ReduceTransformerOptions } from './transformers/reduce';

View File

@ -1,6 +1,7 @@
import { DataFrame } from '../../types/dataFrame';
import { FrameMatcherID } from './ids';
import { FrameMatcherInfo } from '../../types/transformations';
import { stringToJsRegex } from '../../text';
// General Field matcher
const refIdMacher: FrameMatcherInfo<string> = {
@ -10,8 +11,9 @@ const refIdMacher: FrameMatcherInfo<string> = {
defaultOptions: 'A',
get: (pattern: string) => {
const regex = stringToJsRegex(pattern);
return (frame: DataFrame) => {
return pattern === frame.refId;
return regex.test(frame.refId || '');
};
},

View File

@ -8,6 +8,7 @@ import { filterFieldsTransformer, filterFramesTransformer } from './transformers
import { filterFieldsByNameTransformer, FilterFieldsByNameTransformerOptions } from './transformers/filterByName';
import { noopTransformer } from './transformers/noop';
import { DataTransformerInfo, DataTransformerConfig } from '../types/transformations';
import { filterFramesByRefIdTransformer } from './transformers/filterByRefId';
/**
* Apply configured transformations to the input data
@ -64,6 +65,7 @@ export const transformersRegistry = new TransformerRegistry(() => [
filterFieldsTransformer,
filterFieldsByNameTransformer,
filterFramesTransformer,
filterFramesByRefIdTransformer,
appendTransformer,
reduceTransformer,
]);

View File

@ -0,0 +1,44 @@
import { DataTransformerID } from './ids';
import { transformDataFrame } from '../transformers';
import { toDataFrame } from '../../dataframe/processDataFrame';
export const allSeries = [
toDataFrame({
refId: 'A',
fields: [],
}),
toDataFrame({
refId: 'B',
fields: [],
}),
toDataFrame({
refId: 'C',
fields: [],
}),
];
describe('filterByRefId transformer', () => {
it('returns all series if no options provided', () => {
const cfg = {
id: DataTransformerID.filterByRefId,
options: {},
};
const filtered = transformDataFrame([cfg], allSeries);
expect(filtered.length).toBe(3);
});
describe('respects', () => {
it('inclusion', () => {
const cfg = {
id: DataTransformerID.filterByRefId,
options: {
include: 'A|B',
},
};
const filtered = transformDataFrame([cfg], allSeries);
expect(filtered.map(f => f.refId)).toEqual(['A', 'B']);
});
});
});

View File

@ -0,0 +1,38 @@
import { DataTransformerID } from './ids';
import { FilterOptions, filterFramesTransformer } from './filter';
import { DataTransformerInfo } from '../../types/transformations';
import { FrameMatcherID } from '../matchers/ids';
export interface FilterFramesByRefIdTransformerOptions {
include?: string;
exclude?: string;
}
export const filterFramesByRefIdTransformer: DataTransformerInfo<FilterFramesByRefIdTransformerOptions> = {
id: DataTransformerID.filterByRefId,
name: 'Filter data by query refId',
description: 'select a subset of results',
defaultOptions: {},
/**
* Return a modified copy of the series. If the transform is not or should not
* be applied, just return the input series
*/
transformer: (options: FilterFramesByRefIdTransformerOptions) => {
const filterOptions: FilterOptions = {};
if (options.include) {
filterOptions.include = {
id: FrameMatcherID.byRefId,
options: options.include,
};
}
if (options.exclude) {
filterOptions.exclude = {
id: FrameMatcherID.byRefId,
options: options.exclude,
};
}
return filterFramesTransformer.transformer(filterOptions);
},
};

View File

@ -7,5 +7,6 @@ export enum DataTransformerID {
filterFields = 'filterFields', // Pick some fields (keep all frames)
filterFieldsByName = 'filterFieldsByName', // Pick fields with name matching regex (keep all frames)
filterFrames = 'filterFrames', // Pick some frames (keep all fields)
filterByRefId = 'filterByRefId', // Pick some frames by RefId
noop = 'noop', // Does nothing to the dataframe
}

View File

@ -0,0 +1,170 @@
import React, { useContext } from 'react';
import {
FilterFramesByRefIdTransformerOptions,
DataTransformerID,
transformersRegistry,
KeyValue,
} from '@grafana/data';
import { TransformerUIProps, TransformerUIRegistyItem } from './types';
import { ThemeContext } from '../../themes/ThemeContext';
import { css, cx } from 'emotion';
import { InlineList } from '../List/InlineList';
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();
}
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 (
<>
<InlineList
items={options}
renderItem={(o, i) => {
const label = `${o.refId}${o.count > 1 ? ' (' + o.count + ')' : ''}`;
return (
<span
className={css`
margin-right: ${i === options.length - 1 ? '0' : '10px'};
`}
>
<FilterPill
onClick={() => {
this.onFieldToggle(o.refId);
}}
label={label}
selected={selected.indexOf(o.refId) > -1}
/>
</span>
);
}}
/>
</>
);
}
}
interface FilterPillProps {
selected: boolean;
label: string;
onClick: React.MouseEventHandler<HTMLElement>;
}
const FilterPill: React.FC<FilterPillProps> = ({ label, selected, onClick }) => {
const theme = useContext(ThemeContext);
return (
<div
className={css`
padding: ${theme.spacing.xxs} ${theme.spacing.sm};
color: white;
background: ${selected ? theme.colors.blueLight : theme.colors.blueShade};
border-radius: 16px;
display: inline-block;
cursor: pointer;
`}
onClick={onClick}
>
{selected && (
<i
className={cx(
'fa fa-check',
css`
margin-right: 4px;
`
)}
/>
)}
{label}
</div>
);
};
export const filterFramesByRefIdTransformRegistryItem: TransformerUIRegistyItem<
FilterFramesByRefIdTransformerOptions
> = {
id: DataTransformerID.filterByRefId,
component: FilterByRefIdTransformerEditor,
transformer: transformersRegistry.get(DataTransformerID.filterByRefId),
name: 'Filter by refId',
description: 'Filter results by refId',
};

View File

@ -1,8 +1,13 @@
import { Registry } from '@grafana/data';
import { reduceTransformRegistryItem } from './ReduceTransformerEditor';
import { filterFieldsByNameTransformRegistryItem } from './FilterByNameTransformerEditor';
import { filterFramesByRefIdTransformRegistryItem } from './FilterByRefIdTransformerEditor';
import { TransformerUIRegistyItem } from './types';
export const transformersUIRegistry = new Registry<TransformerUIRegistyItem<any>>(() => {
return [reduceTransformRegistryItem, filterFieldsByNameTransformRegistryItem];
return [
reduceTransformRegistryItem,
filterFieldsByNameTransformRegistryItem,
filterFramesByRefIdTransformRegistryItem,
];
});

View File

@ -62,7 +62,7 @@ Array [
],
"meta": undefined,
"name": undefined,
"refId": undefined,
"refId": "GC",
},
Object {
"fields": Array [
@ -124,7 +124,7 @@ Array [
],
"meta": undefined,
"name": undefined,
"refId": undefined,
"refId": "GB",
},
]
`;

View File

@ -9,8 +9,13 @@ export function base64StringToArrowTable(text: string): Table {
return Table.from(arr);
}
function valueOrUndefined(val?: string) {
return val ? val : undefined;
}
export function arrowTableToDataFrame(table: Table): DataFrame {
const fields: Field[] = [];
for (let i = 0; i < table.numCols; i++) {
const col = table.getColumnAt(i);
if (col) {
@ -35,6 +40,7 @@ export function arrowTableToDataFrame(table: Table): DataFrame {
default:
console.log('UNKNOWN Type:', schema);
}
// console.log(' field>', schema.metadata);
fields.push({
name: col.name,
@ -44,9 +50,12 @@ export function arrowTableToDataFrame(table: Table): DataFrame {
});
}
}
const meta = table.schema.metadata;
return {
fields,
length: table.length,
refId: valueOrUndefined(meta.get('refId')),
name: valueOrUndefined(meta.get('name')),
};
}

View File

@ -37,7 +37,6 @@ export function runSharedRequest(options: QueryRunnerOptions): Observable<PanelD
const listenToRunner = listenToPanel.getQueryRunner();
const subscription = listenToRunner.getData(false).subscribe({
next: (data: PanelData) => {
console.log('got data from other panel', data);
subscriber.next(data);
},
});