Commit e0ecbc4c by Torkel Ödegaard

Merge branch 'table-reducer' of https://github.com/ryantxu/grafana into ryantxu-table-reducer

parents a0d2e586 78a92437
import React, { PureComponent } from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { StatsPicker } from './StatsPicker';
import { text, boolean } from '@storybook/addon-knobs';
const getKnobs = () => {
return {
placeholder: text('Placeholder Text', ''),
defaultStat: text('Default Stat', ''),
allowMultiple: boolean('Allow Multiple', false),
initialStats: text('Initial Stats', ''),
};
};
interface State {
stats: string[];
}
export class WrapperWithState extends PureComponent<any, State> {
constructor(props: any) {
super(props);
this.state = {
stats: this.toStatsArray(props.initialReducers),
};
}
toStatsArray = (txt: string): string[] => {
if (!txt) {
return [];
}
return txt.split(',').map(v => v.trim());
};
componentDidUpdate(prevProps: any) {
const { initialReducers } = this.props;
if (initialReducers !== prevProps.initialReducers) {
console.log('Changing initial reducers');
this.setState({ stats: this.toStatsArray(initialReducers) });
}
}
render() {
const { placeholder, defaultStat, allowMultiple } = this.props;
const { stats } = this.state;
return (
<StatsPicker
placeholder={placeholder}
defaultStat={defaultStat}
allowMultiple={allowMultiple}
stats={stats}
onChange={(stats: string[]) => {
action('Picked:')(stats);
this.setState({ stats });
}}
/>
);
}
}
const story = storiesOf('UI/StatsPicker', module);
story.addDecorator(withCenteredStory);
story.add('picker', () => {
const { placeholder, defaultStat, allowMultiple, initialStats } = getKnobs();
return (
<div>
<WrapperWithState
placeholder={placeholder}
defaultStat={defaultStat}
allowMultiple={allowMultiple}
initialStats={initialStats}
/>
</div>
);
});
import React, { PureComponent } from 'react';
import isArray from 'lodash/isArray';
import difference from 'lodash/difference';
import { Select } from '../index';
import { getStatsCalculators } from '../../utils/statsCalculator';
import { SelectOptionItem } from '../Select/Select';
interface Props {
placeholder?: string;
onChange: (stats: string[]) => void;
stats: string[];
width?: number;
allowMultiple?: boolean;
defaultStat?: string;
}
export class StatsPicker extends PureComponent<Props> {
static defaultProps = {
width: 12,
allowMultiple: false,
};
componentDidMount() {
this.checkInput();
}
componentDidUpdate(prevProps: Props) {
this.checkInput();
}
checkInput = () => {
const { stats, allowMultiple, defaultStat, onChange } = this.props;
const current = getStatsCalculators(stats);
if (current.length !== stats.length) {
const found = current.map(v => v.id);
const notFound = difference(stats, found);
console.warn('Unknown stats', notFound, stats);
onChange(current.map(stat => stat.id));
}
// Make sure there is only one
if (!allowMultiple && stats.length > 1) {
console.warn('Removing extra stat', stats);
onChange([stats[0]]);
}
// Set the reducer from callback
if (defaultStat && stats.length < 1) {
onChange([defaultStat]);
}
};
onSelectionChange = (item: SelectOptionItem) => {
const { onChange } = this.props;
if (isArray(item)) {
onChange(item.map(v => v.value));
} else {
onChange([item.value]);
}
};
render() {
const { width, stats, allowMultiple, defaultStat, placeholder } = this.props;
const options = getStatsCalculators().map(s => {
return {
value: s.id,
label: s.name,
description: s.description,
};
});
const value: SelectOptionItem[] = [];
stats.forEach(s => {
const o = options.find(v => v.value === s);
if (o) {
value.push(o);
}
});
//getStatsCalculators(stats);
return (
<Select
width={width}
value={value}
isClearable={!defaultStat}
isMulti={allowMultiple}
isSearchable={true}
options={options}
placeholder={placeholder}
onChange={this.onSelectionChange}
/>
);
}
}
...@@ -27,6 +27,7 @@ export { Switch } from './Switch/Switch'; ...@@ -27,6 +27,7 @@ export { Switch } from './Switch/Switch';
export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult'; export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
export { PieChart, PieChartDataPoint, PieChartType } from './PieChart/PieChart'; export { PieChart, PieChartDataPoint, PieChartType } from './PieChart/PieChart';
export { UnitPicker } from './UnitPicker/UnitPicker'; export { UnitPicker } from './UnitPicker/UnitPicker';
export { StatsPicker } from './StatsPicker/StatsPicker';
export { Input, InputStatus } from './Input/Input'; export { Input, InputStatus } from './Input/Input';
// Visualizations // Visualizations
......
...@@ -5,6 +5,7 @@ export * from './colors'; ...@@ -5,6 +5,7 @@ export * from './colors';
export * from './namedColorsPalette'; export * from './namedColorsPalette';
export * from './thresholds'; export * from './thresholds';
export * from './string'; export * from './string';
export * from './statsCalculator';
export * from './displayValue'; export * from './displayValue';
export * from './deprecationWarning'; export * from './deprecationWarning';
export { getMappedValue } from './valueMappings'; export { getMappedValue } from './valueMappings';
......
import { parseCSV } from './processTableData';
import { getStatsCalculators, StatID, calculateStats } from './statsCalculator';
import _ from 'lodash';
describe('Stats Calculators', () => {
const basicTable = parseCSV('a,b,c\n10,20,30\n20,30,40');
it('should load all standard stats', () => {
const names = [
StatID.sum,
StatID.max,
StatID.min,
StatID.logmin,
StatID.mean,
StatID.last,
StatID.first,
StatID.count,
StatID.range,
StatID.diff,
StatID.step,
StatID.delta,
// StatID.allIsZero,
// StatID.allIsNull,
];
const stats = getStatsCalculators(names);
expect(stats.length).toBe(names.length);
});
it('should fail to load unknown stats', () => {
const names = ['not a stat', StatID.max, StatID.min, 'also not a stat'];
const stats = getStatsCalculators(names);
expect(stats.length).toBe(2);
const found = stats.map(v => v.id);
const notFound = _.difference(names, found);
expect(notFound.length).toBe(2);
expect(notFound[0]).toBe('not a stat');
});
it('should calculate basic stats', () => {
const stats = calculateStats({
table: basicTable,
columnIndex: 0,
stats: ['first', 'last', 'mean'],
});
// First
expect(stats.first).toEqual(10);
// Last
expect(stats.last).toEqual(20);
// Mean
expect(stats.mean).toEqual(15);
});
it('should support a single stat also', () => {
const stats = calculateStats({
table: basicTable,
columnIndex: 0,
stats: ['first'],
});
// Should do the simple version that just looks up value
expect(Object.keys(stats).length).toEqual(1);
expect(stats.first).toEqual(10);
});
it('should get non standard stats', () => {
const stats = calculateStats({
table: basicTable,
columnIndex: 0,
stats: [StatID.distinctCount, StatID.changeCount],
});
expect(stats.distinctCount).toEqual(2);
expect(stats.changeCount).toEqual(1);
});
it('should calculate step', () => {
const stats = calculateStats({
table: { columns: [{ text: 'A' }], rows: [[100], [200], [300], [400]] },
columnIndex: 0,
stats: [StatID.step, StatID.delta],
});
expect(stats.step).toEqual(100);
expect(stats.delta).toEqual(300);
});
});
...@@ -98,6 +98,11 @@ export class DashboardPanel extends PureComponent<Props, State> { ...@@ -98,6 +98,11 @@ export class DashboardPanel extends PureComponent<Props, State> {
} }
panel.changeType(pluginId, hook); panel.changeType(pluginId, hook);
} }
} else if (plugin.exports && plugin.exports.reactPanel) {
const hook = plugin.exports.reactPanel.panelTypeChangedHook;
if (hook) {
panel.options = hook(panel.options || {}, null, null);
}
} }
this.setState({ plugin, angularPanel: null }); this.setState({ plugin, angularPanel: null });
......
import { VizOrientation, SelectOptionItem } from '@grafana/ui'; import { VizOrientation, SelectOptionItem, StatID } from '@grafana/ui';
import { SingleStatBaseOptions } from '../singlestat2/types'; import { SingleStatBaseOptions } from '../singlestat2/types';
export interface BarGaugeOptions extends SingleStatBaseOptions { export interface BarGaugeOptions extends SingleStatBaseOptions {
...@@ -25,7 +25,7 @@ export const defaults: BarGaugeOptions = { ...@@ -25,7 +25,7 @@ export const defaults: BarGaugeOptions = {
orientation: VizOrientation.Horizontal, orientation: VizOrientation.Horizontal,
valueOptions: { valueOptions: {
unit: 'none', unit: 'none',
stat: 'avg', stat: StatID.mean,
prefix: '', prefix: '',
suffix: '', suffix: '',
decimals: null, decimals: null,
......
import { SingleStatBaseOptions } from '../singlestat2/types'; import { SingleStatBaseOptions } from '../singlestat2/types';
import { VizOrientation } from '@grafana/ui'; import { VizOrientation, StatID } from '@grafana/ui';
export interface GaugeOptions extends SingleStatBaseOptions { export interface GaugeOptions extends SingleStatBaseOptions {
maxValue: number; maxValue: number;
...@@ -17,7 +17,7 @@ export const defaults: GaugeOptions = { ...@@ -17,7 +17,7 @@ export const defaults: GaugeOptions = {
prefix: '', prefix: '',
suffix: '', suffix: '',
decimals: null, decimals: null,
stat: 'avg', stat: StatID.mean,
unit: 'none', unit: 'none',
}, },
valueMappings: [], valueMappings: [],
......
...@@ -4,7 +4,7 @@ import React, { PureComponent, CSSProperties } from 'react'; ...@@ -4,7 +4,7 @@ import React, { PureComponent, CSSProperties } from 'react';
// Types // Types
import { SingleStatOptions, SingleStatBaseOptions } from './types'; import { SingleStatOptions, SingleStatBaseOptions } from './types';
import { DisplayValue, PanelProps, processTimeSeries, NullValueMode, ColumnType } from '@grafana/ui'; import { DisplayValue, PanelProps, NullValueMode, ColumnType, calculateStats } from '@grafana/ui';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { getDisplayProcessor } from '@grafana/ui'; import { getDisplayProcessor } from '@grafana/ui';
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater'; import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
...@@ -14,7 +14,7 @@ export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): D ...@@ -14,7 +14,7 @@ export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): D
const { valueOptions, valueMappings } = options; const { valueOptions, valueMappings } = options;
const { unit, decimals, stat } = valueOptions; const { unit, decimals, stat } = valueOptions;
const processor = getDisplayProcessor({ const display = getDisplayProcessor({
unit, unit,
decimals, decimals,
mappings: valueMappings, mappings: valueMappings,
...@@ -25,21 +25,25 @@ export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): D ...@@ -25,21 +25,25 @@ export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): D
}); });
const values: DisplayValue[] = []; const values: DisplayValue[] = [];
for (const table of data) { for (const table of data) {
if (stat === 'name') {
values.push(display(table.name));
}
for (let i = 0; i < table.columns.length; i++) { for (let i = 0; i < table.columns.length; i++) {
const column = table.columns[i]; const column = table.columns[i];
// Show all columns that are not 'time' // Show all columns that are not 'time'
if (column.type === ColumnType.number) { if (column.type === ColumnType.number) {
const series = processTimeSeries({ const stats = calculateStats({
data: [table], table,
xColumn: i, columnIndex: i, // Hardcoded for now!
yColumn: i, stats: [stat], // The stats to calculate
nullValueMode: NullValueMode.Null, nullValueMode: NullValueMode.Null,
})[0]; });
const displayValue = display(stats[stat]);
const value = stat !== 'name' ? series.stats[stat] : series.label; values.push(displayValue);
values.push(processor(value));
} }
} }
} }
...@@ -47,6 +51,7 @@ export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): D ...@@ -47,6 +51,7 @@ export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): D
if (values.length === 0) { if (values.length === 0) {
throw { message: 'Could not find numeric data' }; throw { message: 'Could not find numeric data' };
} }
return values; return values;
}; };
......
...@@ -2,25 +2,11 @@ ...@@ -2,25 +2,11 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
// Components // Components
import { FormField, FormLabel, PanelOptionsGroup, Select, UnitPicker } from '@grafana/ui'; import { FormField, FormLabel, PanelOptionsGroup, StatsPicker, UnitPicker, StatID } from '@grafana/ui';
// Types // Types
import { SingleStatValueOptions } from './types'; import { SingleStatValueOptions } from './types';
const statOptions = [
{ value: 'min', label: 'Min' },
{ value: 'max', label: 'Max' },
{ value: 'avg', label: 'Average' },
{ value: 'current', label: 'Current' },
{ value: 'total', label: 'Total' },
{ value: 'name', label: 'Name' },
{ value: 'first', label: 'First' },
{ value: 'delta', label: 'Delta' },
{ value: 'diff', label: 'Difference' },
{ value: 'range', label: 'Range' },
{ value: 'last_time', label: 'Time of last point' },
];
const labelWidth = 6; const labelWidth = 6;
export interface Props { export interface Props {
...@@ -30,7 +16,11 @@ export interface Props { ...@@ -30,7 +16,11 @@ export interface Props {
export class SingleStatValueEditor extends PureComponent<Props> { export class SingleStatValueEditor extends PureComponent<Props> {
onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value });
onStatsChange = stats => {
const stat = stats[0] || StatID.mean;
this.props.onChange({ ...this.props.options, stat });
};
onDecimalChange = event => { onDecimalChange = event => {
if (!isNaN(event.target.value)) { if (!isNaN(event.target.value)) {
...@@ -61,11 +51,13 @@ export class SingleStatValueEditor extends PureComponent<Props> { ...@@ -61,11 +51,13 @@ export class SingleStatValueEditor extends PureComponent<Props> {
<PanelOptionsGroup title="Value"> <PanelOptionsGroup title="Value">
<div className="gf-form"> <div className="gf-form">
<FormLabel width={labelWidth}>Stat</FormLabel> <FormLabel width={labelWidth}>Stat</FormLabel>
<Select <StatsPicker
width={12} width={12}
options={statOptions} placeholder="Choose Stat"
onChange={this.onStatChange} defaultStat={StatID.mean}
value={statOptions.find(option => option.value === stat)} allowMultiple={false}
stats={[stat]}
onChange={this.onStatsChange}
/> />
</div> </div>
<div className="gf-form"> <div className="gf-form">
......
import { ReactPanelPlugin } from '@grafana/ui'; import { ReactPanelPlugin, getStatsCalculators } from '@grafana/ui';
import { SingleStatOptions, defaults, SingleStatBaseOptions } from './types'; import { SingleStatOptions, defaults, SingleStatBaseOptions } from './types';
import { SingleStatPanel } from './SingleStatPanel'; import { SingleStatPanel } from './SingleStatPanel';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
...@@ -21,6 +21,13 @@ export const singleStatBaseOptionsCheck = ( ...@@ -21,6 +21,13 @@ export const singleStatBaseOptionsCheck = (
}); });
} }
// 6.1 renamed some stats, This makes sure they are up to date
// avg -> mean, current -> last, total -> sum
const { valueOptions } = options;
if (valueOptions && valueOptions.stat) {
valueOptions.stat = getStatsCalculators([valueOptions.stat]).map(s => s.id)[0];
console.log('CHANGED', valueOptions);
}
return options; return options;
}; };
......
import { VizOrientation, ValueMapping, Threshold } from '@grafana/ui'; import { VizOrientation, ValueMapping, Threshold, StatID } from '@grafana/ui';
export interface SingleStatBaseOptions { export interface SingleStatBaseOptions {
valueMappings: ValueMapping[]; valueMappings: ValueMapping[];
...@@ -24,7 +24,7 @@ export const defaults: SingleStatOptions = { ...@@ -24,7 +24,7 @@ export const defaults: SingleStatOptions = {
prefix: '', prefix: '',
suffix: '', suffix: '',
decimals: null, decimals: null,
stat: 'avg', stat: StatID.mean,
unit: 'none', unit: 'none',
}, },
valueMappings: [], valueMappings: [],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment