mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Transformers: configure result transformations after query(alpha) (#18740)
This commit is contained in:
parent
205c0a58ac
commit
7d32caeac2
@ -682,3 +682,7 @@ app_tls_skip_verify_insecure = false
|
|||||||
|
|
||||||
[enterprise]
|
[enterprise]
|
||||||
license_path =
|
license_path =
|
||||||
|
|
||||||
|
[feature_toggles]
|
||||||
|
# enable features, separated by spaces
|
||||||
|
enable =
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { DataTransformerInfo, NoopDataTransformer } from './transformers';
|
import { DataTransformerInfo } from './transformers';
|
||||||
|
import { noopTransformer } from './noop';
|
||||||
import { DataFrame, Field } from '../../types/dataFrame';
|
import { DataFrame, Field } from '../../types/dataFrame';
|
||||||
import { FieldMatcherID } from '../matchers/ids';
|
import { FieldMatcherID } from '../matchers/ids';
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
@ -23,7 +24,7 @@ export const filterFieldsTransformer: DataTransformerInfo<FilterOptions> = {
|
|||||||
*/
|
*/
|
||||||
transformer: (options: FilterOptions) => {
|
transformer: (options: FilterOptions) => {
|
||||||
if (!options.include && !options.exclude) {
|
if (!options.include && !options.exclude) {
|
||||||
return NoopDataTransformer;
|
return noopTransformer.transformer({});
|
||||||
}
|
}
|
||||||
|
|
||||||
const include = options.include ? getFieldMatcher(options.include) : null;
|
const include = options.include ? getFieldMatcher(options.include) : null;
|
||||||
@ -75,7 +76,7 @@ export const filterFramesTransformer: DataTransformerInfo<FilterOptions> = {
|
|||||||
*/
|
*/
|
||||||
transformer: (options: FilterOptions) => {
|
transformer: (options: FilterOptions) => {
|
||||||
if (!options.include && !options.exclude) {
|
if (!options.include && !options.exclude) {
|
||||||
return NoopDataTransformer;
|
return noopTransformer.transformer({});
|
||||||
}
|
}
|
||||||
|
|
||||||
const include = options.include ? getFrameMatchers(options.include) : null;
|
const include = options.include ? getFrameMatchers(options.include) : null;
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
import { toDataFrame, transformDataFrame } from '../index';
|
||||||
|
import { FieldType } from '../../index';
|
||||||
|
import { DataTransformerID } from './ids';
|
||||||
|
|
||||||
|
export const seriesWithNamesToMatch = toDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'startsWithA', type: FieldType.time, values: [1000, 2000] },
|
||||||
|
{ name: 'B', type: FieldType.boolean, values: [true, false] },
|
||||||
|
{ name: 'startsWithC', type: FieldType.string, values: ['a', 'b'] },
|
||||||
|
{ name: 'D', type: FieldType.number, values: [1, 2] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filterByName transformer', () => {
|
||||||
|
it('returns original series if no options provided', () => {
|
||||||
|
const cfg = {
|
||||||
|
id: DataTransformerID.filterFields,
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0];
|
||||||
|
expect(filtered.fields.length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('respects', () => {
|
||||||
|
it('inclusion', () => {
|
||||||
|
const cfg = {
|
||||||
|
id: DataTransformerID.filterFieldsByName,
|
||||||
|
options: {
|
||||||
|
include: '/^(startsWith)/',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0];
|
||||||
|
expect(filtered.fields.length).toBe(2);
|
||||||
|
expect(filtered.fields[0].name).toBe('startsWithA');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exclusion', () => {
|
||||||
|
const cfg = {
|
||||||
|
id: DataTransformerID.filterFieldsByName,
|
||||||
|
options: {
|
||||||
|
exclude: '/^(startsWith)/',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0];
|
||||||
|
expect(filtered.fields.length).toBe(2);
|
||||||
|
expect(filtered.fields[0].name).toBe('B');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inclusion and exclusion', () => {
|
||||||
|
const cfg = {
|
||||||
|
id: DataTransformerID.filterFieldsByName,
|
||||||
|
options: {
|
||||||
|
exclude: '/^(startsWith)/',
|
||||||
|
include: `/^(B)$/`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0];
|
||||||
|
expect(filtered.fields.length).toBe(1);
|
||||||
|
expect(filtered.fields[0].name).toBe('B');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
38
packages/grafana-data/src/utils/transformers/filterByName.ts
Normal file
38
packages/grafana-data/src/utils/transformers/filterByName.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { DataTransformerInfo } from './transformers';
|
||||||
|
import { FieldMatcherID } from '../matchers/ids';
|
||||||
|
import { DataTransformerID } from './ids';
|
||||||
|
import { filterFieldsTransformer, FilterOptions } from './filter';
|
||||||
|
|
||||||
|
export interface FilterFieldsByNameTransformerOptions {
|
||||||
|
include?: string;
|
||||||
|
exclude?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const filterFieldsByNameTransformer: DataTransformerInfo<FilterFieldsByNameTransformerOptions> = {
|
||||||
|
id: DataTransformerID.filterFieldsByName,
|
||||||
|
name: 'Filter fields by name',
|
||||||
|
description: 'select a subset of fields',
|
||||||
|
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: FilterFieldsByNameTransformerOptions) => {
|
||||||
|
const filterOptions: FilterOptions = {};
|
||||||
|
if (options.include) {
|
||||||
|
filterOptions.include = {
|
||||||
|
id: FieldMatcherID.byName,
|
||||||
|
options: options.include,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (options.exclude) {
|
||||||
|
filterOptions.exclude = {
|
||||||
|
id: FieldMatcherID.byName,
|
||||||
|
options: options.exclude,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterFieldsTransformer.transformer(filterOptions);
|
||||||
|
},
|
||||||
|
};
|
@ -5,5 +5,7 @@ export enum DataTransformerID {
|
|||||||
reduce = 'reduce', // Run calculations on fields
|
reduce = 'reduce', // Run calculations on fields
|
||||||
|
|
||||||
filterFields = 'filterFields', // Pick some fields (keep all frames)
|
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)
|
filterFrames = 'filterFrames', // Pick some frames (keep all fields)
|
||||||
|
noop = 'noop', // Does nothing to the dataframe
|
||||||
}
|
}
|
||||||
|
23
packages/grafana-data/src/utils/transformers/noop.ts
Normal file
23
packages/grafana-data/src/utils/transformers/noop.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { DataTransformerInfo } from './transformers';
|
||||||
|
import { DataTransformerID } from './ids';
|
||||||
|
import { DataFrame } from '../../types/dataFrame';
|
||||||
|
|
||||||
|
export interface NoopTransformerOptions {
|
||||||
|
include?: string;
|
||||||
|
exclude?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const noopTransformer: DataTransformerInfo<NoopTransformerOptions> = {
|
||||||
|
id: DataTransformerID.noop,
|
||||||
|
name: 'noop',
|
||||||
|
description: 'No-operation transformer',
|
||||||
|
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: NoopTransformerOptions) => {
|
||||||
|
return (data: DataFrame[]) => data;
|
||||||
|
},
|
||||||
|
};
|
@ -8,26 +8,26 @@ import { KeyValue } from '../../types/data';
|
|||||||
import { ArrayVector } from '../vector';
|
import { ArrayVector } from '../vector';
|
||||||
import { guessFieldTypeForField } from '../processDataFrame';
|
import { guessFieldTypeForField } from '../processDataFrame';
|
||||||
|
|
||||||
export interface ReduceOptions {
|
export interface ReduceTransformerOptions {
|
||||||
reducers: string[];
|
reducers: ReducerID[];
|
||||||
fields?: MatcherConfig; // Assume all fields
|
fields?: MatcherConfig; // Assume all fields
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reduceTransformer: DataTransformerInfo<ReduceOptions> = {
|
export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> = {
|
||||||
id: DataTransformerID.reduce,
|
id: DataTransformerID.reduce,
|
||||||
name: 'Reducer',
|
name: 'Reducer',
|
||||||
description: 'Return a DataFrame with the reduction results',
|
description: 'Return a DataFrame with the reduction results',
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
calcs: [ReducerID.min, ReducerID.max, ReducerID.mean, ReducerID.last],
|
reducers: [ReducerID.min, ReducerID.max, ReducerID.mean, ReducerID.last],
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
* be applied, just return the input series
|
* be applied, just return the input series
|
||||||
*/
|
*/
|
||||||
transformer: (options: ReduceOptions) => {
|
transformer: (options: ReduceTransformerOptions) => {
|
||||||
const matcher = options.fields ? getFieldMatcher(options.fields) : alwaysFieldMatcher;
|
const matcher = options.fields ? getFieldMatcher(options.fields) : alwaysFieldMatcher;
|
||||||
const calculators = fieldReducers.list(options.reducers);
|
const calculators = options.reducers && options.reducers.length ? fieldReducers.list(options.reducers) : [];
|
||||||
const reducers = calculators.map(c => c.id);
|
const reducers = calculators.map(c => c.id);
|
||||||
|
|
||||||
return (data: DataFrame[]) => {
|
return (data: DataFrame[]) => {
|
||||||
|
@ -15,9 +15,6 @@ export interface DataTransformerConfig<TOptions = any> {
|
|||||||
options: TOptions;
|
options: TOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transformer that does nothing
|
|
||||||
export const NoopDataTransformer = (data: DataFrame[]) => data;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply configured transformations to the input data
|
* Apply configured transformations to the input data
|
||||||
*/
|
*/
|
||||||
@ -49,8 +46,10 @@ export function transformDataFrame(options: DataTransformerConfig[], data: DataF
|
|||||||
// Initalize the Registry
|
// Initalize the Registry
|
||||||
|
|
||||||
import { appendTransformer, AppendOptions } from './append';
|
import { appendTransformer, AppendOptions } from './append';
|
||||||
import { reduceTransformer, ReduceOptions } from './reduce';
|
import { reduceTransformer, ReduceTransformerOptions } from './reduce';
|
||||||
import { filterFieldsTransformer, filterFramesTransformer } from './filter';
|
import { filterFieldsTransformer, filterFramesTransformer } from './filter';
|
||||||
|
import { filterFieldsByNameTransformer, FilterFieldsByNameTransformerOptions } from './filterByName';
|
||||||
|
import { noopTransformer } from './noop';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registry of transformation options that can be driven by
|
* Registry of transformation options that can be driven by
|
||||||
@ -69,14 +68,18 @@ class TransformerRegistry extends Registry<DataTransformerInfo> {
|
|||||||
return appendTransformer.transformer(options || appendTransformer.defaultOptions)(data)[0];
|
return appendTransformer.transformer(options || appendTransformer.defaultOptions)(data)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
reduce(data: DataFrame[], options: ReduceOptions): DataFrame[] {
|
reduce(data: DataFrame[], options: ReduceTransformerOptions): DataFrame[] {
|
||||||
return reduceTransformer.transformer(options)(data);
|
return reduceTransformer.transformer(options)(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dataTransformers = new TransformerRegistry(() => [
|
export const dataTransformers = new TransformerRegistry(() => [
|
||||||
|
noopTransformer,
|
||||||
filterFieldsTransformer,
|
filterFieldsTransformer,
|
||||||
|
filterFieldsByNameTransformer,
|
||||||
filterFramesTransformer,
|
filterFramesTransformer,
|
||||||
appendTransformer,
|
appendTransformer,
|
||||||
reduceTransformer,
|
reduceTransformer,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export { ReduceTransformerOptions, FilterFieldsByNameTransformerOptions };
|
||||||
|
@ -10,6 +10,9 @@ export interface BuildInfo {
|
|||||||
hasUpdate: boolean;
|
hasUpdate: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FeatureToggles {
|
||||||
|
transformations: boolean;
|
||||||
|
}
|
||||||
export class GrafanaBootConfig {
|
export class GrafanaBootConfig {
|
||||||
datasources: { [str: string]: DataSourceInstanceSettings } = {};
|
datasources: { [str: string]: DataSourceInstanceSettings } = {};
|
||||||
panels: { [key: string]: PanelPluginMeta } = {};
|
panels: { [key: string]: PanelPluginMeta } = {};
|
||||||
@ -41,6 +44,9 @@ export class GrafanaBootConfig {
|
|||||||
disableSanitizeHtml = false;
|
disableSanitizeHtml = false;
|
||||||
theme: GrafanaTheme;
|
theme: GrafanaTheme;
|
||||||
pluginsToPreload: string[] = [];
|
pluginsToPreload: string[] = [];
|
||||||
|
featureToggles: FeatureToggles = {
|
||||||
|
transformations: false,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(options: GrafanaBootConfig) {
|
constructor(options: GrafanaBootConfig) {
|
||||||
this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark);
|
this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark);
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
import React, { FC, useContext } from 'react';
|
||||||
|
import { css, cx } from 'emotion';
|
||||||
|
import { PluginState, ThemeContext } from '../../index';
|
||||||
|
import { Tooltip } from '../index';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
state?: PluginState;
|
||||||
|
text?: JSX.Element;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AlphaNotice: FC<Props> = ({ state, text, className }) => {
|
||||||
|
const tooltipContent = text || (
|
||||||
|
<div>
|
||||||
|
<h5>Alpha Feature</h5>
|
||||||
|
<p>This feature is a work in progress and updates may include breaking changes.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const theme = useContext(ThemeContext);
|
||||||
|
|
||||||
|
const styles = cx(
|
||||||
|
className,
|
||||||
|
css`
|
||||||
|
background: linear-gradient(to bottom, ${theme.colors.blueBase}, ${theme.colors.blueShade});
|
||||||
|
color: ${theme.colors.gray7};
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: 3px;
|
||||||
|
text-shadow: none;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: help;
|
||||||
|
display: inline-block;
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content={tooltipContent} theme={'info'} placement={'top'}>
|
||||||
|
<div className={styles}>
|
||||||
|
<i className="fa fa-warning" /> {state}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
import React, { PureComponent, createRef } from 'react';
|
import React, { PureComponent, createRef } from 'react';
|
||||||
import { JsonExplorer } from 'app/core/core'; // We have made some monkey-patching of json-formatter-js so we can't switch right now
|
import { JsonExplorer } from './json_explorer/json_explorer'; // We have made some monkey-patching of json-formatter-js so we can't switch right now
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -31,10 +31,13 @@ export class JSONFormatter extends PureComponent<Props> {
|
|||||||
const { json, config, open, onDidRender } = this.props;
|
const { json, config, open, onDidRender } = this.props;
|
||||||
const wrapperEl = this.wrapperRef.current;
|
const wrapperEl = this.wrapperRef.current;
|
||||||
const formatter = new JsonExplorer(json, open, config);
|
const formatter = new JsonExplorer(json, open, config);
|
||||||
|
// @ts-ignore
|
||||||
const hasChildren: boolean = wrapperEl.hasChildNodes();
|
const hasChildren: boolean = wrapperEl.hasChildNodes();
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
|
// @ts-ignore
|
||||||
wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild);
|
wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild);
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
wrapperEl.appendChild(formatter.render());
|
wrapperEl.appendChild(formatter.render());
|
||||||
}
|
}
|
||||||
|
|
@ -28,7 +28,6 @@ export interface JsonExplorerConfig {
|
|||||||
const _defaultConfig: JsonExplorerConfig = {
|
const _defaultConfig: JsonExplorerConfig = {
|
||||||
animateOpen: true,
|
animateOpen: true,
|
||||||
animateClose: true,
|
animateClose: true,
|
||||||
theme: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -39,10 +38,10 @@ const _defaultConfig: JsonExplorerConfig = {
|
|||||||
*/
|
*/
|
||||||
export class JsonExplorer {
|
export class JsonExplorer {
|
||||||
// Hold the open state after the toggler is used
|
// Hold the open state after the toggler is used
|
||||||
private _isOpen: boolean = null;
|
private _isOpen: boolean | null = null;
|
||||||
|
|
||||||
// A reference to the element that we render to
|
// A reference to the element that we render to
|
||||||
private element: Element;
|
private element: Element | null = null;
|
||||||
|
|
||||||
private skipChildren = false;
|
private skipChildren = false;
|
||||||
|
|
||||||
@ -366,7 +365,7 @@ export class JsonExplorer {
|
|||||||
* Animated option is used when user triggers this via a click
|
* Animated option is used when user triggers this via a click
|
||||||
*/
|
*/
|
||||||
appendChildren(animated = false) {
|
appendChildren(animated = false) {
|
||||||
const children = this.element.querySelector(`div.${cssClass('children')}`);
|
const children = this.element && this.element.querySelector(`div.${cssClass('children')}`);
|
||||||
|
|
||||||
if (!children || this.isEmpty) {
|
if (!children || this.isEmpty) {
|
||||||
return;
|
return;
|
||||||
@ -404,7 +403,8 @@ export class JsonExplorer {
|
|||||||
* Animated option is used when user triggers this via a click
|
* Animated option is used when user triggers this via a click
|
||||||
*/
|
*/
|
||||||
removeChildren(animated = false) {
|
removeChildren(animated = false) {
|
||||||
const childrenElement = this.element.querySelector(`div.${cssClass('children')}`) as HTMLDivElement;
|
const childrenElement =
|
||||||
|
this.element && (this.element.querySelector(`div.${cssClass('children')}`) as HTMLDivElement);
|
||||||
|
|
||||||
if (animated) {
|
if (animated) {
|
||||||
let childrenRemoved = 0;
|
let childrenRemoved = 0;
|
@ -2,7 +2,7 @@
|
|||||||
import React, { FunctionComponent } from 'react';
|
import React, { FunctionComponent } from 'react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string | JSX.Element;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
children: JSX.Element | JSX.Element[] | boolean;
|
children: JSX.Element | JSX.Element[] | boolean;
|
||||||
onAdd?: () => void;
|
onAdd?: () => void;
|
||||||
|
@ -0,0 +1,163 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { FilterFieldsByNameTransformerOptions, DataTransformerID, dataTransformers, 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 FilterByNameTransformerEditorProps extends TransformerUIProps<FilterFieldsByNameTransformerOptions> {}
|
||||||
|
|
||||||
|
interface FilterByNameTransformerEditorState {
|
||||||
|
include: string;
|
||||||
|
options: FieldNameInfo[];
|
||||||
|
selected: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FieldNameInfo {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
export class FilterByNameTransformerEditor extends React.PureComponent<
|
||||||
|
FilterByNameTransformerEditorProps,
|
||||||
|
FilterByNameTransformerEditorState
|
||||||
|
> {
|
||||||
|
constructor(props: FilterByNameTransformerEditorProps) {
|
||||||
|
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: FieldNameInfo[] = [];
|
||||||
|
const byName: KeyValue<FieldNameInfo> = {};
|
||||||
|
for (const frame of input) {
|
||||||
|
for (const field of frame.fields) {
|
||||||
|
let v = byName[field.name];
|
||||||
|
if (!v) {
|
||||||
|
v = byName[field.name] = {
|
||||||
|
name: field.name,
|
||||||
|
count: 0,
|
||||||
|
};
|
||||||
|
allNames.push(v);
|
||||||
|
}
|
||||||
|
v.count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuredOptions.length) {
|
||||||
|
const options: FieldNameInfo[] = [];
|
||||||
|
const selected: FieldNameInfo[] = [];
|
||||||
|
for (const v of allNames) {
|
||||||
|
if (configuredOptions.includes(v.name)) {
|
||||||
|
selected.push(v);
|
||||||
|
}
|
||||||
|
options.push(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
options,
|
||||||
|
selected: selected.map(s => s.name),
|
||||||
|
});
|
||||||
|
} 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.name}${o.count > 1 ? ' (' + o.count + ')' : ''}`;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={css`
|
||||||
|
margin-right: ${i === options.length - 1 ? '0' : '10px'};
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<FilterPill
|
||||||
|
onClick={() => {
|
||||||
|
this.onFieldToggle(o.name);
|
||||||
|
}}
|
||||||
|
label={label}
|
||||||
|
selected={selected.indexOf(o.name) > -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 filterFieldsByNameTransformRegistryItem: TransformerUIRegistyItem<FilterFieldsByNameTransformerOptions> = {
|
||||||
|
id: DataTransformerID.filterFieldsByName,
|
||||||
|
component: FilterByNameTransformerEditor,
|
||||||
|
transformer: dataTransformers.get(DataTransformerID.filterFieldsByName),
|
||||||
|
name: 'Filter by name',
|
||||||
|
description: 'UI for filter by name transformation',
|
||||||
|
};
|
@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { StatsPicker } from '../StatsPicker/StatsPicker';
|
||||||
|
import { ReduceTransformerOptions, DataTransformerID, ReducerID } from '@grafana/data';
|
||||||
|
import { TransformerUIRegistyItem, TransformerUIProps } from './types';
|
||||||
|
import { dataTransformers } from '@grafana/data';
|
||||||
|
|
||||||
|
// TODO: Minimal implementation, needs some <3
|
||||||
|
export const ReduceTransformerEditor: React.FC<TransformerUIProps<ReduceTransformerOptions>> = ({
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
input,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StatsPicker
|
||||||
|
width={12}
|
||||||
|
placeholder="Choose Stat"
|
||||||
|
allowMultiple
|
||||||
|
stats={options.reducers || []}
|
||||||
|
onChange={stats => {
|
||||||
|
onChange({
|
||||||
|
...options,
|
||||||
|
reducers: stats as ReducerID[],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reduceTransformRegistryItem: TransformerUIRegistyItem<ReduceTransformerOptions> = {
|
||||||
|
id: DataTransformerID.reduce,
|
||||||
|
component: ReduceTransformerEditor,
|
||||||
|
transformer: dataTransformers.get(DataTransformerID.reduce),
|
||||||
|
name: 'Reduce',
|
||||||
|
description: 'UI for reduce transformation',
|
||||||
|
};
|
@ -0,0 +1,85 @@
|
|||||||
|
import React, { useContext, useState } from 'react';
|
||||||
|
import { ThemeContext } from '../../themes/ThemeContext';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
import { DataFrame } from '@grafana/data';
|
||||||
|
import { JSONFormatter } from '../JSONFormatter/JSONFormatter';
|
||||||
|
import { GrafanaTheme } from '../../types/theme';
|
||||||
|
|
||||||
|
interface TransformationRowProps {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
editor?: JSX.Element;
|
||||||
|
onRemove: () => void;
|
||||||
|
input: DataFrame[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme) => ({
|
||||||
|
title: css`
|
||||||
|
display: flex;
|
||||||
|
padding: 4px 8px 4px 8px;
|
||||||
|
position: relative;
|
||||||
|
height: 35px;
|
||||||
|
background: ${theme.colors.textFaint};
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
`,
|
||||||
|
name: css`
|
||||||
|
font-weight: ${theme.typography.weight.semibold};
|
||||||
|
color: ${theme.colors.blue};
|
||||||
|
`,
|
||||||
|
iconRow: css`
|
||||||
|
display: flex;
|
||||||
|
`,
|
||||||
|
icon: css`
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: ${theme.colors.textWeak};
|
||||||
|
margin-left: ${theme.spacing.sm};
|
||||||
|
&:hover {
|
||||||
|
color: ${theme.colors.text};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
editor: css`
|
||||||
|
border: 2px dashed ${theme.colors.textFaint};
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
padding: 8px;
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TransformationRow = ({ onRemove, editor, name, input }: TransformationRowProps) => {
|
||||||
|
const theme = useContext(ThemeContext);
|
||||||
|
const [viewDebug, setViewDebug] = useState(false);
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={css`
|
||||||
|
margin-bottom: 10px;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<div className={styles.name}>{name}</div>
|
||||||
|
<div className={styles.iconRow}>
|
||||||
|
<div onClick={() => setViewDebug(!viewDebug)} className={styles.icon}>
|
||||||
|
<i className="fa fa-fw fa-bug" />
|
||||||
|
</div>
|
||||||
|
<div onClick={onRemove} className={styles.icon}>
|
||||||
|
<i className="fa fa-fw fa-trash" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.editor}>
|
||||||
|
{editor}
|
||||||
|
{viewDebug && (
|
||||||
|
<div>
|
||||||
|
<JSONFormatter json={input} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,127 @@
|
|||||||
|
import { DataTransformerID, DataTransformerConfig, DataFrame, transformDataFrame } from '@grafana/data';
|
||||||
|
import { Select } from '../Select/Select';
|
||||||
|
import { transformersUIRegistry } from './transformers';
|
||||||
|
import React from 'react';
|
||||||
|
import { TransformationRow } from './TransformationRow';
|
||||||
|
import { Button } from '../Button/Button';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
|
||||||
|
interface TransformationsEditorState {
|
||||||
|
updateCounter: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransformationsEditorProps {
|
||||||
|
onChange: (transformations: DataTransformerConfig[]) => void;
|
||||||
|
transformations: DataTransformerConfig[];
|
||||||
|
getCurrentData: (applyTransformations?: boolean) => DataFrame[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TransformationsEditor extends React.PureComponent<TransformationsEditorProps, TransformationsEditorState> {
|
||||||
|
state = { updateCounter: 0 };
|
||||||
|
|
||||||
|
onTransformationAdd = () => {
|
||||||
|
const { transformations, onChange } = this.props;
|
||||||
|
onChange([
|
||||||
|
...transformations,
|
||||||
|
{
|
||||||
|
id: DataTransformerID.noop,
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
this.setState({ updateCounter: this.state.updateCounter + 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
onTransformationChange = (idx: number, config: DataTransformerConfig) => {
|
||||||
|
const { transformations, onChange } = this.props;
|
||||||
|
transformations[idx] = config;
|
||||||
|
onChange(transformations);
|
||||||
|
this.setState({ updateCounter: this.state.updateCounter + 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
onTransformationRemove = (idx: number) => {
|
||||||
|
const { transformations, onChange } = this.props;
|
||||||
|
transformations.splice(idx, 1);
|
||||||
|
onChange(transformations);
|
||||||
|
this.setState({ updateCounter: this.state.updateCounter + 1 });
|
||||||
|
};
|
||||||
|
|
||||||
|
renderTransformationEditors = () => {
|
||||||
|
const { transformations, getCurrentData } = this.props;
|
||||||
|
const hasTransformations = transformations.length > 0;
|
||||||
|
const preTransformData = getCurrentData(false);
|
||||||
|
|
||||||
|
if (!hasTransformations) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableTransformers = transformersUIRegistry.list().map(t => {
|
||||||
|
return {
|
||||||
|
value: t.transformer.id,
|
||||||
|
label: t.transformer.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{transformations.map((t, i) => {
|
||||||
|
let editor, input;
|
||||||
|
if (t.id === DataTransformerID.noop) {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className={css`
|
||||||
|
margin-bottom: 10px;
|
||||||
|
`}
|
||||||
|
key={`${t.id}-${i}`}
|
||||||
|
options={availableTransformers}
|
||||||
|
placeholder="Select transformation"
|
||||||
|
onChange={v => {
|
||||||
|
this.onTransformationChange(i, {
|
||||||
|
id: v.value as string,
|
||||||
|
options: {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const transformationUI = transformersUIRegistry.getIfExists(t.id);
|
||||||
|
input = transformDataFrame(transformations.slice(0, i), preTransformData);
|
||||||
|
|
||||||
|
if (transformationUI) {
|
||||||
|
editor = React.createElement(transformationUI.component, {
|
||||||
|
options: { ...transformationUI.transformer.defaultOptions, ...t.options },
|
||||||
|
input,
|
||||||
|
onChange: (options: any) => {
|
||||||
|
this.onTransformationChange(i, {
|
||||||
|
id: t.id,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TransformationRow
|
||||||
|
key={`${t.id}-${i}`}
|
||||||
|
input={input || []}
|
||||||
|
onRemove={() => this.onTransformationRemove(i)}
|
||||||
|
editor={editor}
|
||||||
|
name={transformationUI ? transformationUI.name : ''}
|
||||||
|
description={transformationUI ? transformationUI.description : ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.renderTransformationEditors()}
|
||||||
|
<Button variant="inverse" icon="fa fa-plus" onClick={this.onTransformationAdd}>
|
||||||
|
Add transformation
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
import { Registry } from '@grafana/data';
|
||||||
|
import { reduceTransformRegistryItem } from './ReduceTransformerEditor';
|
||||||
|
import { filterFieldsByNameTransformRegistryItem } from './FilterByNameTransformerEditor';
|
||||||
|
import { TransformerUIRegistyItem } from './types';
|
||||||
|
|
||||||
|
export const transformersUIRegistry = new Registry<TransformerUIRegistyItem<any>>(() => {
|
||||||
|
return [reduceTransformRegistryItem, filterFieldsByNameTransformRegistryItem];
|
||||||
|
});
|
15
packages/grafana-ui/src/components/TransformersUI/types.ts
Normal file
15
packages/grafana-ui/src/components/TransformersUI/types.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DataFrame, RegistryItem, DataTransformerInfo } from '@grafana/data';
|
||||||
|
|
||||||
|
export interface TransformerUIRegistyItem<TOptions> extends RegistryItem {
|
||||||
|
component: React.ComponentType<TransformerUIProps<TOptions>>;
|
||||||
|
transformer: DataTransformerInfo<TOptions>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransformerUIProps<T> {
|
||||||
|
// Transformer configuration, persisted on panel's model
|
||||||
|
options: T;
|
||||||
|
// Pre-transformation data frame
|
||||||
|
input: DataFrame[];
|
||||||
|
onChange: (options: T) => void;
|
||||||
|
}
|
@ -78,5 +78,10 @@ export { VariableSuggestion, VariableOrigin } from './DataLinks/DataLinkSuggesti
|
|||||||
export { DataLinksEditor } from './DataLinks/DataLinksEditor';
|
export { DataLinksEditor } from './DataLinks/DataLinksEditor';
|
||||||
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
|
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
|
||||||
export { SeriesIcon } from './Legend/SeriesIcon';
|
export { SeriesIcon } from './Legend/SeriesIcon';
|
||||||
|
export { transformersUIRegistry } from './TransformersUI/transformers';
|
||||||
|
export { TransformationRow } from './TransformersUI/TransformationRow';
|
||||||
|
export { TransformationsEditor } from './TransformersUI/TransformationsEditor';
|
||||||
|
export { JSONFormatter } from './JSONFormatter/JSONFormatter';
|
||||||
|
export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer';
|
||||||
export { ErrorBoundary, ErrorBoundaryAlert } from './ErrorBoundary/ErrorBoundary';
|
export { ErrorBoundary, ErrorBoundaryAlert } from './ErrorBoundary/ErrorBoundary';
|
||||||
|
export { AlphaNotice } from './AlphaNotice/AlphaNotice';
|
||||||
|
@ -195,6 +195,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
|
|||||||
"env": setting.Env,
|
"env": setting.Env,
|
||||||
"isEnterprise": setting.IsEnterprise,
|
"isEnterprise": setting.IsEnterprise,
|
||||||
},
|
},
|
||||||
|
"featureToggles": hs.Cfg.FeatureToggles,
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonObj, nil
|
return jsonObj, nil
|
||||||
|
@ -266,6 +266,8 @@ type Cfg struct {
|
|||||||
EditorsCanAdmin bool
|
EditorsCanAdmin bool
|
||||||
|
|
||||||
ApiKeyMaxSecondsToLive int64
|
ApiKeyMaxSecondsToLive int64
|
||||||
|
|
||||||
|
FeatureToggles map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandLineArgs struct {
|
type CommandLineArgs struct {
|
||||||
@ -941,6 +943,17 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
|||||||
cfg.PluginsEnableAlpha = pluginsSection.Key("enable_alpha").MustBool(false)
|
cfg.PluginsEnableAlpha = pluginsSection.Key("enable_alpha").MustBool(false)
|
||||||
cfg.PluginsAppsSkipVerifyTLS = pluginsSection.Key("app_tls_skip_verify_insecure").MustBool(false)
|
cfg.PluginsAppsSkipVerifyTLS = pluginsSection.Key("app_tls_skip_verify_insecure").MustBool(false)
|
||||||
|
|
||||||
|
// Read and populate feature toggles list
|
||||||
|
featureTogglesSection := iniFile.Section("feature_toggles")
|
||||||
|
cfg.FeatureToggles = make(map[string]bool)
|
||||||
|
featuresTogglesStr, err := valueAsString(featureTogglesSection, "enable", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, feature := range util.SplitString(featuresTogglesStr) {
|
||||||
|
cfg.FeatureToggles[feature] = true
|
||||||
|
}
|
||||||
|
|
||||||
// check old location for this option
|
// check old location for this option
|
||||||
if panelsSection.Key("enable_alpha").MustBool(false) {
|
if panelsSection.Key("enable_alpha").MustBool(false) {
|
||||||
cfg.PluginsEnableAlpha = true
|
cfg.PluginsEnableAlpha = true
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import { JsonExplorer } from '../json_explorer/json_explorer';
|
import { JsonExplorer } from '@grafana/ui';
|
||||||
|
|
||||||
coreModule.directive('jsonTree', [
|
coreModule.directive('jsonTree', [
|
||||||
function jsonTreeDirective() {
|
function jsonTreeDirective() {
|
||||||
|
@ -16,7 +16,7 @@ import './utils/outline';
|
|||||||
import './components/colorpicker/spectrum_picker';
|
import './components/colorpicker/spectrum_picker';
|
||||||
import './services/search_srv';
|
import './services/search_srv';
|
||||||
import './services/ng_react';
|
import './services/ng_react';
|
||||||
import { colors } from '@grafana/ui/';
|
import { colors, JsonExplorer } from '@grafana/ui/';
|
||||||
|
|
||||||
import { searchDirective } from './components/search/search';
|
import { searchDirective } from './components/search/search';
|
||||||
import { infoPopover } from './components/info_popover';
|
import { infoPopover } from './components/info_popover';
|
||||||
@ -38,7 +38,6 @@ import { assignModelProperties } from './utils/model_utils';
|
|||||||
import { contextSrv } from './services/context_srv';
|
import { contextSrv } from './services/context_srv';
|
||||||
import { KeybindingSrv } from './services/keybindingSrv';
|
import { KeybindingSrv } from './services/keybindingSrv';
|
||||||
import { helpModal } from './components/help/help';
|
import { helpModal } from './components/help/help';
|
||||||
import { JsonExplorer } from './components/json_explorer/json_explorer';
|
|
||||||
import { NavModelSrv } from './nav_model_srv';
|
import { NavModelSrv } from './nav_model_srv';
|
||||||
import { geminiScrollbar } from './components/scroll/scroll';
|
import { geminiScrollbar } from './components/scroll/scroll';
|
||||||
import { orgSwitcher } from './components/org_switcher';
|
import { orgSwitcher } from './components/org_switcher';
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter';
|
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
|
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { DashboardModel } from '../dashboard/state/DashboardModel';
|
import { DashboardModel } from '../dashboard/state/DashboardModel';
|
||||||
import { LoadingPlaceholder } from '@grafana/ui';
|
import { LoadingPlaceholder, JSONFormatter } from '@grafana/ui';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
panelId: number;
|
panelId: number;
|
||||||
|
@ -172,7 +172,6 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
if (!this.querySubscription) {
|
if (!this.querySubscription) {
|
||||||
this.querySubscription = queryRunner.subscribe(this.panelDataObserver);
|
this.querySubscription = queryRunner.subscribe(this.panelDataObserver);
|
||||||
}
|
}
|
||||||
|
|
||||||
queryRunner.run({
|
queryRunner.run({
|
||||||
datasource: panel.datasource,
|
datasource: panel.datasource,
|
||||||
queries: panel.targets,
|
queries: panel.targets,
|
||||||
@ -186,6 +185,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
|||||||
minInterval: panel.interval,
|
minInterval: panel.interval,
|
||||||
scopedVars: panel.scopedVars,
|
scopedVars: panel.scopedVars,
|
||||||
cacheTimeout: panel.cacheTimeout,
|
cacheTimeout: panel.cacheTimeout,
|
||||||
|
transformations: panel.transformations,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
|
import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
|
||||||
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
||||||
import { QueryInspector } from './QueryInspector';
|
import { QueryInspector } from './QueryInspector';
|
||||||
import { QueryOptions } from './QueryOptions';
|
import { QueryOptions } from './QueryOptions';
|
||||||
import { PanelOptionsGroup } from '@grafana/ui';
|
import { PanelOptionsGroup, TransformationsEditor } from '@grafana/ui';
|
||||||
import { QueryEditorRow } from './QueryEditorRow';
|
import { QueryEditorRow } from './QueryEditorRow';
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
@ -18,8 +19,8 @@ import config from 'app/core/config';
|
|||||||
// Types
|
// Types
|
||||||
import { PanelModel } from '../state/PanelModel';
|
import { PanelModel } from '../state/PanelModel';
|
||||||
import { DashboardModel } from '../state/DashboardModel';
|
import { DashboardModel } from '../state/DashboardModel';
|
||||||
import { DataQuery, DataSourceSelectItem, PanelData } from '@grafana/ui';
|
import { DataQuery, DataSourceSelectItem, PanelData, AlphaNotice, PluginState } from '@grafana/ui';
|
||||||
import { LoadingState } from '@grafana/data';
|
import { LoadingState, DataTransformerConfig } from '@grafana/data';
|
||||||
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||||
import { PanelQueryRunnerFormat } from '../state/PanelQueryRunner';
|
import { PanelQueryRunnerFormat } from '../state/PanelQueryRunner';
|
||||||
import { Unsubscribable } from 'rxjs';
|
import { Unsubscribable } from 'rxjs';
|
||||||
@ -215,15 +216,24 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onTransformersChange = (transformers: DataTransformerConfig[]) => {
|
||||||
|
this.props.panel.setTransformations(transformers);
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
|
setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
this.setState({ scrollTop: target.scrollTop });
|
this.setState({ scrollTop: target.scrollTop });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getCurrentData = (applyTransformations = true) => {
|
||||||
|
const queryRunner = this.props.panel.getQueryRunner();
|
||||||
|
return queryRunner.getCurrentData(applyTransformations).series;
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { panel, dashboard } = this.props;
|
const { panel, dashboard } = this.props;
|
||||||
const { currentDS, scrollTop, data } = this.state;
|
const { currentDS, scrollTop, data } = this.state;
|
||||||
|
|
||||||
const queryInspector: EditorToolbarView = {
|
const queryInspector: EditorToolbarView = {
|
||||||
title: 'Query Inspector',
|
title: 'Query Inspector',
|
||||||
render: this.renderQueryInspector,
|
render: this.renderQueryInspector,
|
||||||
@ -235,6 +245,8 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
render: this.renderHelp,
|
render: this.renderHelp,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const enableTransformations = config.featureToggles.transformations;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorTabBody
|
<EditorTabBody
|
||||||
heading="Query"
|
heading="Query"
|
||||||
@ -243,32 +255,58 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||||||
setScrollTop={this.setScrollTop}
|
setScrollTop={this.setScrollTop}
|
||||||
scrollTop={scrollTop}
|
scrollTop={scrollTop}
|
||||||
>
|
>
|
||||||
{isSharedDashboardQuery(currentDS.name) ? (
|
<>
|
||||||
<DashboardQueryEditor panel={panel} panelData={data} onChange={query => this.onQueryChange(query, 0)} />
|
{isSharedDashboardQuery(currentDS.name) ? (
|
||||||
) : (
|
<DashboardQueryEditor panel={panel} panelData={data} onChange={query => this.onQueryChange(query, 0)} />
|
||||||
<>
|
) : (
|
||||||
<div className="query-editor-rows">
|
<>
|
||||||
{panel.targets.map((query, index) => (
|
<div className="query-editor-rows">
|
||||||
<QueryEditorRow
|
{panel.targets.map((query, index) => (
|
||||||
dataSourceValue={query.datasource || panel.datasource}
|
<QueryEditorRow
|
||||||
key={query.refId}
|
dataSourceValue={query.datasource || panel.datasource}
|
||||||
panel={panel}
|
key={query.refId}
|
||||||
dashboard={dashboard}
|
panel={panel}
|
||||||
data={data}
|
dashboard={dashboard}
|
||||||
query={query}
|
data={data}
|
||||||
onChange={query => this.onQueryChange(query, index)}
|
query={query}
|
||||||
onRemoveQuery={this.onRemoveQuery}
|
onChange={query => this.onQueryChange(query, index)}
|
||||||
onAddQuery={this.onAddQuery}
|
onRemoveQuery={this.onRemoveQuery}
|
||||||
onMoveQuery={this.onMoveQuery}
|
onAddQuery={this.onAddQuery}
|
||||||
inMixedMode={currentDS.meta.mixed}
|
onMoveQuery={this.onMoveQuery}
|
||||||
|
inMixedMode={currentDS.meta.mixed}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<PanelOptionsGroup>
|
||||||
|
<QueryOptions panel={panel} datasource={currentDS} />
|
||||||
|
</PanelOptionsGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{enableTransformations && (
|
||||||
|
<PanelOptionsGroup
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Transform query results
|
||||||
|
<AlphaNotice
|
||||||
|
state={PluginState.alpha}
|
||||||
|
className={css`
|
||||||
|
margin-left: 16px;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{this.state.data.state !== LoadingState.NotStarted && (
|
||||||
|
<TransformationsEditor
|
||||||
|
transformations={this.props.panel.transformations || []}
|
||||||
|
onChange={this.onTransformersChange}
|
||||||
|
getCurrentData={this.getCurrentData}
|
||||||
/>
|
/>
|
||||||
))}
|
)}
|
||||||
</div>
|
|
||||||
<PanelOptionsGroup>
|
|
||||||
<QueryOptions panel={panel} datasource={currentDS} />
|
|
||||||
</PanelOptionsGroup>
|
</PanelOptionsGroup>
|
||||||
</>
|
)}
|
||||||
)}
|
</>
|
||||||
</EditorTabBody>
|
</EditorTabBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { JSONFormatter } from 'app/core/components/JSONFormatter/JSONFormatter';
|
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
|
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
|
||||||
import { LoadingPlaceholder } from '@grafana/ui';
|
import { LoadingPlaceholder, JSONFormatter } from '@grafana/ui';
|
||||||
|
|
||||||
interface DsQuery {
|
interface DsQuery {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
@ -7,7 +7,7 @@ import { getNextRefIdChar } from 'app/core/utils/query';
|
|||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { DataQuery, ScopedVars, DataQueryResponseData, PanelPlugin } from '@grafana/ui';
|
import { DataQuery, ScopedVars, DataQueryResponseData, PanelPlugin } from '@grafana/ui';
|
||||||
import { DataLink } from '@grafana/data';
|
import { DataLink, DataTransformerConfig } from '@grafana/data';
|
||||||
|
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
||||||
@ -66,6 +66,7 @@ const mustKeepProps: { [str: string]: boolean } = {
|
|||||||
transparent: true,
|
transparent: true,
|
||||||
pluginVersion: true,
|
pluginVersion: true,
|
||||||
queryRunner: true,
|
queryRunner: true,
|
||||||
|
transformations: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaults: any = {
|
const defaults: any = {
|
||||||
@ -93,6 +94,7 @@ export class PanelModel {
|
|||||||
panels?: any;
|
panels?: any;
|
||||||
soloMode?: boolean;
|
soloMode?: boolean;
|
||||||
targets: DataQuery[];
|
targets: DataQuery[];
|
||||||
|
transformations?: DataTransformerConfig[];
|
||||||
datasource: string;
|
datasource: string;
|
||||||
thresholds?: any;
|
thresholds?: any;
|
||||||
pluginVersion?: string;
|
pluginVersion?: string;
|
||||||
@ -290,7 +292,6 @@ export class PanelModel {
|
|||||||
} else if (oldOptions && oldOptions.options) {
|
} else if (oldOptions && oldOptions.options) {
|
||||||
old = oldOptions.options;
|
old = oldOptions.options;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.options = this.options || {};
|
this.options = this.options || {};
|
||||||
Object.assign(this.options, newPlugin.onPanelTypeChanged(this.options, oldPluginId, old));
|
Object.assign(this.options, newPlugin.onPanelTypeChanged(this.options, oldPluginId, old));
|
||||||
}
|
}
|
||||||
@ -344,6 +345,14 @@ export class PanelModel {
|
|||||||
this.queryRunner = null;
|
this.queryRunner = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTransformations(transformations: DataTransformerConfig[]) {
|
||||||
|
// save for persistence
|
||||||
|
this.transformations = transformations;
|
||||||
|
|
||||||
|
// update query runner transformers
|
||||||
|
this.getQueryRunner().setTransform(transformations);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginVersion(plugin: PanelPlugin): string {
|
function getPluginVersion(plugin: PanelPlugin): string {
|
||||||
|
@ -13,7 +13,8 @@ import { isSharedDashboardQuery, SharedQueryRunner } from 'app/plugins/datasourc
|
|||||||
// Types
|
// Types
|
||||||
import { PanelData, DataQuery, ScopedVars, DataQueryRequest, DataSourceApi, DataSourceJsonData } from '@grafana/ui';
|
import { PanelData, DataQuery, ScopedVars, DataQueryRequest, DataSourceApi, DataSourceJsonData } from '@grafana/ui';
|
||||||
|
|
||||||
import { TimeRange } from '@grafana/data';
|
import { TimeRange, DataTransformerConfig, transformDataFrame, toLegacyResponseData } from '@grafana/data';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
|
||||||
export interface QueryRunnerOptions<
|
export interface QueryRunnerOptions<
|
||||||
TQuery extends DataQuery = DataQuery,
|
TQuery extends DataQuery = DataQuery,
|
||||||
@ -32,6 +33,7 @@ export interface QueryRunnerOptions<
|
|||||||
scopedVars?: ScopedVars;
|
scopedVars?: ScopedVars;
|
||||||
cacheTimeout?: string;
|
cacheTimeout?: string;
|
||||||
delayStateNotification?: number; // default 100ms.
|
delayStateNotification?: number; // default 100ms.
|
||||||
|
transformations?: DataTransformerConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PanelQueryRunnerFormat {
|
export enum PanelQueryRunnerFormat {
|
||||||
@ -49,6 +51,7 @@ export class PanelQueryRunner {
|
|||||||
private subject?: Subject<PanelData>;
|
private subject?: Subject<PanelData>;
|
||||||
|
|
||||||
private state = new PanelQueryState();
|
private state = new PanelQueryState();
|
||||||
|
private transformations?: DataTransformerConfig[];
|
||||||
|
|
||||||
// Listen to another panel for changes
|
// Listen to another panel for changes
|
||||||
private sharedQueryRunner: SharedQueryRunner;
|
private sharedQueryRunner: SharedQueryRunner;
|
||||||
@ -62,6 +65,27 @@ export class PanelQueryRunner {
|
|||||||
return this.panelId;
|
return this.panelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last result -- optionally skip the transformation
|
||||||
|
*/
|
||||||
|
// TODO: add tests
|
||||||
|
getCurrentData(transform = true): PanelData {
|
||||||
|
const v = this.state.validateStreamsAndGetPanelData();
|
||||||
|
const transformData = config.featureToggles.transformations && transform;
|
||||||
|
const hasTransformations = this.transformations && this.transformations.length;
|
||||||
|
|
||||||
|
if (transformData && hasTransformations) {
|
||||||
|
const processed = transformDataFrame(this.transformations, v.series);
|
||||||
|
return {
|
||||||
|
...v,
|
||||||
|
series: processed,
|
||||||
|
legacy: processed.map(p => toLegacyResponseData(p)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen for updates to the PanelData. If a query has already run for this panel,
|
* Listen for updates to the PanelData. If a query has already run for this panel,
|
||||||
* the results will be immediatly passed to the observer
|
* the results will be immediatly passed to the observer
|
||||||
@ -78,7 +102,9 @@ export class PanelQueryRunner {
|
|||||||
|
|
||||||
// Send the last result
|
// Send the last result
|
||||||
if (this.state.isStarted()) {
|
if (this.state.isStarted()) {
|
||||||
observer.next(this.state.getDataAfterCheckingFormats());
|
// Force check formats again?
|
||||||
|
this.state.getDataAfterCheckingFormats();
|
||||||
|
observer.next(this.getCurrentData()); // transformed
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.subject.subscribe(observer);
|
return this.subject.subscribe(observer);
|
||||||
@ -98,9 +124,17 @@ export class PanelQueryRunner {
|
|||||||
return this.subscribe(runner.subject, format);
|
return this.subscribe(runner.subject, format);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentData(): PanelData {
|
/**
|
||||||
return this.state.validateStreamsAndGetPanelData();
|
* Change the current transformation and notify all listeners
|
||||||
}
|
* Should be used only by panel editor to update the transformers
|
||||||
|
*/
|
||||||
|
setTransform = (transformations?: DataTransformerConfig[]) => {
|
||||||
|
this.transformations = transformations;
|
||||||
|
|
||||||
|
if (this.state.isStarted()) {
|
||||||
|
this.onStreamingDataUpdated();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async run(options: QueryRunnerOptions): Promise<PanelData> {
|
async run(options: QueryRunnerOptions): Promise<PanelData> {
|
||||||
const { state } = this;
|
const { state } = this;
|
||||||
@ -200,13 +234,14 @@ export class PanelQueryRunner {
|
|||||||
}
|
}
|
||||||
}, delayStateNotification || 500);
|
}, delayStateNotification || 500);
|
||||||
|
|
||||||
const data = await state.execute(ds, request);
|
this.transformations = options.transformations;
|
||||||
|
|
||||||
|
const data = await state.execute(ds, request);
|
||||||
// Clear the delayed loading state timeout
|
// Clear the delayed loading state timeout
|
||||||
clearTimeout(loadingStateTimeoutId);
|
clearTimeout(loadingStateTimeoutId);
|
||||||
|
|
||||||
// Broadcast results
|
// Broadcast results
|
||||||
this.subject.next(data);
|
this.subject.next(this.getCurrentData());
|
||||||
return data;
|
return data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
clearTimeout(loadingStateTimeoutId);
|
clearTimeout(loadingStateTimeoutId);
|
||||||
@ -223,7 +258,7 @@ export class PanelQueryRunner {
|
|||||||
*/
|
*/
|
||||||
onStreamingDataUpdated = throttle(
|
onStreamingDataUpdated = throttle(
|
||||||
() => {
|
() => {
|
||||||
this.subject.next(this.state.validateStreamsAndGetPanelData());
|
this.subject.next(this.getCurrentData());
|
||||||
},
|
},
|
||||||
50,
|
50,
|
||||||
{ trailing: true, leading: true }
|
{ trailing: true, leading: true }
|
||||||
@ -241,6 +276,14 @@ export class PanelQueryRunner {
|
|||||||
// Will cancel and disconnect any open requets
|
// Will cancel and disconnect any open requets
|
||||||
this.state.cancel('destroy');
|
this.state.cancel('destroy');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setState = (state: PanelQueryState) => {
|
||||||
|
this.state = state;
|
||||||
|
};
|
||||||
|
|
||||||
|
getState = () => {
|
||||||
|
return this.state;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDataSource(
|
async function getDataSource(
|
||||||
|
@ -212,6 +212,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
|||||||
minInterval: panel.interval,
|
minInterval: panel.interval,
|
||||||
scopedVars: panel.scopedVars,
|
scopedVars: panel.scopedVars,
|
||||||
cacheTimeout: panel.cacheTimeout,
|
cacheTimeout: panel.cacheTimeout,
|
||||||
|
transformations: panel.transformations,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import React, { FC, useContext } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
import { PluginState, AlphaNotice } from '@grafana/ui';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { PluginState, Tooltip, ThemeContext } from '@grafana/ui';
|
|
||||||
import { PopoverContent } from '@grafana/ui/src/components/Tooltip/Tooltip';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
state?: PluginState;
|
state?: PluginState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginStateInfoText(state?: PluginState): PopoverContent | null {
|
function getPluginStateInfoText(state?: PluginState): JSX.Element | null {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case PluginState.alpha:
|
case PluginState.alpha:
|
||||||
return (
|
return (
|
||||||
@ -30,30 +29,15 @@ function getPluginStateInfoText(state?: PluginState): PopoverContent | null {
|
|||||||
|
|
||||||
const PluginStateinfo: FC<Props> = props => {
|
const PluginStateinfo: FC<Props> = props => {
|
||||||
const text = getPluginStateInfoText(props.state);
|
const text = getPluginStateInfoText(props.state);
|
||||||
if (!text) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const theme = useContext(ThemeContext);
|
|
||||||
|
|
||||||
const styles = css`
|
|
||||||
background: linear-gradient(to bottom, ${theme.colors.blueBase}, ${theme.colors.blueShade});
|
|
||||||
color: ${theme.colors.gray7};
|
|
||||||
white-space: nowrap;
|
|
||||||
border-radius: 3px;
|
|
||||||
text-shadow: none;
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
margin-left: 16px;
|
|
||||||
cursor: help;
|
|
||||||
`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content={text} theme={'info'} placement={'top'}>
|
<AlphaNotice
|
||||||
<div className={styles}>
|
state={props.state}
|
||||||
<i className="fa fa-warning" /> {props.state}
|
text={text}
|
||||||
</div>
|
className={css`
|
||||||
</Tooltip>
|
margin-left: 16px;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -57,6 +57,7 @@ export class SharedQueryRunner {
|
|||||||
this.listenToPanelId = panelId;
|
this.listenToPanelId = panelId;
|
||||||
this.listenToRunner = this.listenToPanel.getQueryRunner();
|
this.listenToRunner = this.listenToPanel.getQueryRunner();
|
||||||
this.subscription = this.listenToRunner.chain(this.runner);
|
this.subscription = this.listenToRunner.chain(this.runner);
|
||||||
|
this.runner.setState(this.listenToRunner.getState());
|
||||||
console.log('Connecting panel: ', this.containerPanel.id, 'to:', this.listenToPanelId);
|
console.log('Connecting panel: ', this.containerPanel.id, 'to:', this.listenToPanelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ class GraphCtrl extends MetricsPanelCtrl {
|
|||||||
|
|
||||||
this.events.on('render', this.onRender.bind(this));
|
this.events.on('render', this.onRender.bind(this));
|
||||||
this.events.on('data-received', this.onDataReceived.bind(this));
|
this.events.on('data-received', this.onDataReceived.bind(this));
|
||||||
this.events.on('data-frames-received', this.onDataReceived.bind(this));
|
this.events.on('data-frames-received', this.onDataFramesReceived.bind(this));
|
||||||
this.events.on('data-error', this.onDataError.bind(this));
|
this.events.on('data-error', this.onDataError.bind(this));
|
||||||
this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this));
|
this.events.on('data-snapshot-load', this.onDataSnapshotLoad.bind(this));
|
||||||
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
|
this.events.on('init-edit-mode', this.onInitEditMode.bind(this));
|
||||||
|
Loading…
Reference in New Issue
Block a user