[TSVB] Math Aggregation (two point oh) (#16965)

* Revert "Remove MathJS Feature (#15652)"

This reverts commit 43bf1dbf4a.

* replacing math with tinymath

* pining version

* updating yarn.lock

* Fixing Prettier mis formatting

* fixing tests

* Adding unsupported agg back

* Adding functional test for math aggregation

- Fixing bad tests
- Resetting page for every test suite (why donesn't beforeEach work?)
- Adding test for Math agg

* Trying to fix values (due to inconsistencies in env)
This commit is contained in:
Chris Cowan 2018-04-11 14:06:28 -07:00
parent 3bf90dc9f1
commit 84ad08e70d
28 changed files with 461 additions and 66 deletions

View file

@ -197,6 +197,7 @@
"style-loader": "0.19.0",
"tar": "2.2.0",
"tinygradient": "0.3.0",
"tinymath": "0.2.1",
"topojson-client": "3.0.0",
"trunc-html": "1.0.2",
"trunc-text": "1.0.2",

View file

@ -14,7 +14,7 @@ describe('aggLookup', () => {
describe('createOptions(type, siblings)', () => {
it('returns options for all aggs', () => {
const options = createOptions();
expect(options).to.have.length(29);
expect(options).to.have.length(30);
options.forEach(option => {
expect(option).to.have.property('label');
expect(option).to.have.property('value');
@ -32,7 +32,7 @@ describe('aggLookup', () => {
it('returns options for pipeline', () => {
const options = createOptions('pipeline');
expect(options).to.have.length(14);
expect(options).to.have.length(15);
expect(options.every(opt => !isBasicAgg({ type: opt.value }))).to.equal(
true
);
@ -40,7 +40,7 @@ describe('aggLookup', () => {
it('returns options for all if given unknown key', () => {
const options = createOptions('foo');
expect(options).to.have.length(29);
expect(options).to.have.length(30);
});
});
});

View file

@ -15,7 +15,7 @@ describe('calculateLabel(metric, metrics)', () => {
});
it('returns "Calcuation" for a bucket script metric', () => {
expect(calculateLabel({ type: 'calculation' })).to.equal('Calculation');
expect(calculateLabel({ type: 'calculation' })).to.equal('Bucket Script');
});
it('returns formated label for series_agg', () => {

View file

@ -24,6 +24,7 @@ const lookup = {
sum_of_squares_bucket: 'Overall Sum of Sq.',
std_deviation_bucket: 'Overall Std. Deviation',
series_agg: 'Series Agg',
math: 'Math',
serial_diff: 'Serial Difference',
filter_ratio: 'Filter Ratio',
positive_only: 'Positive Only',
@ -44,6 +45,7 @@ const pipeline = [
'sum_of_squares_bucket',
'std_deviation_bucket',
'series_agg',
'math',
'serial_diff',
'positive_only',
];

View file

@ -19,7 +19,8 @@ export default function calculateLabel(metric, metrics) {
if (metric.alias) return metric.alias;
if (metric.type === 'count') return 'Count';
if (metric.type === 'calculation') return 'Calculation';
if (metric.type === 'calculation') return 'Bucket Script';
if (metric.type === 'math') return 'Math';
if (metric.type === 'series_agg') return `Series Agg (${metric.function})`;
if (metric.type === 'filter_ratio') return 'Filter Ratio';
if (metric.type === 'static') return `Static Value of ${metric.value}`;

View file

@ -3,6 +3,7 @@ import React from 'react';
import { EuiToolTip } from '@elastic/eui';
function AddDeleteButtons(props) {
const { testSubj } = props;
const createDelete = () => {
if (props.disableDelete) {
return null;
@ -10,8 +11,9 @@ function AddDeleteButtons(props) {
return (
<EuiToolTip content={props.deleteTooltip}>
<button
data-test-subj={`${testSubj}DeleteBtn`}
aria-label={props.deleteTooltip}
className="thor__button-outlined-danger sm"
className="thor__button-outlined-danger thor__button-delete sm"
onClick={props.onDelete}
>
<i className="fa fa-trash-o" />
@ -26,8 +28,9 @@ function AddDeleteButtons(props) {
return (
<EuiToolTip content={props.addTooltip}>
<button
data-test-subj={`${testSubj}AddBtn`}
aria-label={props.addTooltip}
className="thor__button-outlined-default sm"
className="thor__button-outlined-default sm thor__button-add"
onClick={props.onAdd}
>
<i className="fa fa-plus" />
@ -42,8 +45,9 @@ function AddDeleteButtons(props) {
clone = (
<EuiToolTip content={props.cloneTooltip}>
<button
data-test-subj={`${testSubj}CloneBtn`}
aria-label={props.cloneTooltip}
className="thor__button-outlined-default sm"
className="thor__button-outlined-default thor__button-clone sm"
onClick={props.onClone}
>
<i className="fa fa-files-o" />
@ -61,6 +65,7 @@ function AddDeleteButtons(props) {
}
AddDeleteButtons.defaultProps = {
testSubj: 'Add',
addTooltip: 'Add',
deleteTooltip: 'Delete',
cloneTooltip: 'Clone'

View file

@ -12,7 +12,7 @@ function Agg(props) {
}
const style = {
cursor: 'default',
...props.style
...props.style,
};
return (
<div
@ -34,7 +34,6 @@ function Agg(props) {
/>
</div>
);
}
Agg.propTypes = {

View file

@ -28,13 +28,14 @@ function AggRow(props) {
return (
<div className="vis_editor__agg_row">
<div className="vis_editor__agg_row-item">
<div className="vis_editor__agg_row-item" data-test-subj="aggRow">
<div className={iconRowClassName}>
<i className={iconClassName} />
</div>
{props.children}
{ dragHandle }
<AddDeleteButtons
testSubj="addMetric"
addTooltip="Add Metric"
deleteTooltip="Delete Metric"
onAdd={props.onAdd}

View file

@ -21,13 +21,12 @@ const metricAggs = [
];
const pipelineAggs = [
{ label: 'Calculation', value: 'calculation' },
{ label: 'Bucket Script', value: 'calculation' },
{ label: 'Cumulative Sum', value: 'cumulative_sum' },
{ label: 'Derivative', value: 'derivative' },
{ label: 'Moving Average', value: 'moving_average' },
{ label: 'Positive Only', value: 'positive_only' },
{ label: 'Serial Difference', value: 'serial_diff' },
{ label: 'Series Agg', value: 'series_agg' },
];
const siblingAggs = [
@ -40,6 +39,11 @@ const siblingAggs = [
{ label: 'Overall Variance', value: 'variance_bucket' },
];
const specialAggs = [
{ label: 'Series Agg', value: 'series_agg' },
{ label: 'Math', value: 'math' },
];
class AggSelectOption extends Component {
constructor(props) {
super(props);
@ -169,6 +173,14 @@ function AggSelect(props) {
disabled: true,
},
...siblingAggs.map(agg => ({ ...agg, disabled: !enablePipelines })),
{
label: 'Special Aggregations',
value: null,
pipeline: true,
heading: true,
disabled: true,
},
...specialAggs.map(agg => ({ ...agg, disabled: !enablePipelines })),
];
}
@ -179,7 +191,7 @@ function AggSelect(props) {
};
return (
<div className="vis_editor__row_item">
<div data-test-subj="aggSelector" className="vis_editor__row_item">
<Select
aria-label="Select aggregation"
clearable={false}

View file

@ -0,0 +1,113 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import uuid from 'uuid';
import AggRow from './agg_row';
import AggSelect from './agg_select';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import Vars from './vars';
class MathAgg extends Component {
componentWillMount() {
if (!this.props.model.variables) {
this.props.onChange(
_.assign({}, this.props.model, {
variables: [{ id: uuid.v1() }],
})
);
}
}
render() {
const { siblings } = this.props;
const defaults = { script: '' };
const model = { ...defaults, ...this.props.model };
const handleChange = createChangeHandler(this.props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handleTextChange = createTextHandler(handleChange);
return (
<AggRow
disableDelete={this.props.disableDelete}
model={this.props.model}
onAdd={this.props.onAdd}
onDelete={this.props.onDelete}
siblings={this.props.siblings}
>
<div className="vis_editor__row_item">
<div>
<div className="vis_editor__label">Aggregation</div>
<AggSelect
siblings={this.props.siblings}
value={model.type}
onChange={handleSelectChange('type')}
/>
<div className="vis_editor__variables">
<div className="vis_editor__label">Variables</div>
<Vars
metrics={siblings}
onChange={handleChange}
name="variables"
model={model}
includeSiblings={true}
/>
</div>
<div className="vis_editor__row_item">
<label
className="vis_editor__label"
htmlFor="mathExpressionInput"
>
Expression
</label>
<textarea
data-test-subj="mathExpression"
id="mathExpressionInput"
aria-describedby="mathExpressionDescription"
className="vis_editor__input-grows-100"
onChange={handleTextChange('script')}
>
{model.script}
</textarea>
<div className="vis_editor__note" id="mathExpressionDescription">
This field uses basic math expresions (see{' '}
<a
href="https://github.com/elastic/tinymath/blob/master/docs/functions.md"
target="_blank"
>
TinyMath
</a>) - Variables are keys on the <code>params</code> object,
i.e. <code>params.&lt;name&gt;</code> To access all the data use
<code>params._all.&lt;name&gt;.values</code> for an array of the
values and <code>params._all.&lt;name&gt;.timestamps</code>
for an array of the timestamps. <code>params._timestamp</code>
is available for the current bucket&apos;s timestamp,
<code>params._index</code> is available for the current
bucket&apos;s index, and <code>params._interval</code>s
available for the interval in milliseconds.
</div>
</div>
</div>
</div>
</AggRow>
);
}
}
MathAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
};
export default MathAgg;

View file

@ -19,17 +19,26 @@ function createTypeFilter(restrict, exclude) {
}
// This filters out sibling aggs, percentiles, and special aggs (like Series Agg)
export function filterRows(row) {
return (
!/_bucket$/.test(row.type) &&
!/^series/.test(row.type) &&
!/^percentile/.test(row.type) &&
!/^top_hit/.test(row.type)
);
export function filterRows(includeSiblings) {
return row => {
if (includeSiblings) {
return (
!/^series/.test(row.type) &&
!/^percentile/.test(row.type) &&
row.type !== 'math'
);
}
return (
!/_bucket$/.test(row.type) &&
!/^series/.test(row.type) &&
!/^percentile/.test(row.type) &&
row.type !== 'math'
);
};
}
function MetricSelect(props) {
const { restrict, metric, onChange, value, exclude } = props;
const { restrict, metric, onChange, value, exclude, includeSiblings } = props;
const metrics = props.metrics.filter(createTypeFilter(restrict, exclude));
@ -54,7 +63,7 @@ function MetricSelect(props) {
return acc;
}, []);
const options = siblings.filter(filterRows).map(row => {
const options = siblings.filter(filterRows(includeSiblings)).map(row => {
const label = calculateLabel(row, metrics);
return { value: row.id, label };
});
@ -75,6 +84,7 @@ MetricSelect.defaultProps = {
exclude: [],
metric: {},
restrict: 'none',
includeSiblings: false,
};
MetricSelect.propTypes = {
@ -84,6 +94,7 @@ MetricSelect.propTypes = {
onChange: PropTypes.func,
restrict: PropTypes.string,
value: PropTypes.string,
includeSiblings: PropTypes.bool,
};
export default MetricSelect;

View file

@ -10,9 +10,11 @@ export function UnsupportedAgg(props) {
siblings={props.siblings}
>
<div className="vis_editor__row_item">
<p>The <code>{props.model.type}</code> aggregation is no longer supported.</p>
<p>
The <code>{props.model.type}</code> aggregation is no longer
supported.
</p>
</div>
</AggRow>
);
}

View file

@ -25,7 +25,7 @@ class CalculationVars extends Component {
const handleAdd = collectionActions.handleAdd.bind(null, this.props);
const handleDelete = collectionActions.handleDelete.bind(null, this.props, row);
return (
<div className="vis_editor__calc_vars-row" key={row.id}>
<div className="vis_editor__calc_vars-row" key={row.id} data-test-subj="varRow">
<div className="vis_editor__calc_vars-name">
<input
aria-label="Variable name"
@ -42,6 +42,7 @@ class CalculationVars extends Component {
metrics={this.props.metrics}
metric={this.props.model}
value={row.field}
includeSiblings={this.props.includeSiblings}
/>
</div>
<div className="vis_editor__calc_vars-control">
@ -69,14 +70,16 @@ class CalculationVars extends Component {
}
CalculationVars.defaultProps = {
name: 'variables'
name: 'variables',
includeSiblings: false
};
CalculationVars.propTypes = {
metrics: PropTypes.array,
model: PropTypes.object,
name: PropTypes.string,
onChange: PropTypes.func
onChange: PropTypes.func,
includeSiblings: PropTypes.bool
};
export default CalculationVars;

View file

@ -12,6 +12,7 @@ import { PositiveOnlyAgg } from '../aggs/positive_only';
import { FilterRatioAgg } from '../aggs/filter_ratio';
import { PercentileRankAgg } from '../aggs/percentile_rank';
import { Static } from '../aggs/static';
import MathAgg from '../aggs/math';
import { TopHitAgg } from '../aggs/top_hit';
export default {
count: StdAgg,
@ -42,5 +43,6 @@ export default {
filter_ratio: FilterRatioAgg,
positive_only: PositiveOnlyAgg,
static: Static,
math: MathAgg,
top_hit: TopHitAgg,
};

View file

@ -58,6 +58,11 @@
margin: 0 10px 0 0;
}
}
.vis_editor__note {
.vis_editor__label;
font-style: italic;
}
.vis_editor__input {
padding: 8px 10px;
border-radius: @borderRadius;
@ -332,6 +337,7 @@
margin-bottom: 2px;
padding: 10px;
align-items: center;
.vis_editor__note,
.vis_editor__label {
margin-bottom: 5px;
font-size: 12px;

View file

@ -8,7 +8,8 @@ describe('getSplits(resp, panel, series)', () => {
aggregations: {
SERIES: {
timeseries: { buckets: [] },
SIBAGG: { value: 1 }
SIBAGG: { value: 1 },
meta: { bucketSize: 10 }
}
}
};
@ -26,6 +27,7 @@ describe('getSplits(resp, panel, series)', () => {
{
id: 'SERIES',
label: 'Overall Average of Average of cpu',
meta: { bucketSize: 10 },
color: '#FF0000',
timeseries: { buckets: [] },
SIBAGG: { value: 1 }
@ -48,7 +50,8 @@ describe('getSplits(resp, panel, series)', () => {
timeseries: { buckets: [] },
SIBAGG: { value: 2 }
}
]
],
meta: { bucketSize: 10 }
}
}
};
@ -69,6 +72,7 @@ describe('getSplits(resp, panel, series)', () => {
id: 'SERIES:example-01',
key: 'example-01',
label: 'example-01',
meta: { bucketSize: 10 },
color: '#FF0000',
timeseries: { buckets: [] },
SIBAGG: { value: 1 }
@ -77,6 +81,7 @@ describe('getSplits(resp, panel, series)', () => {
id: 'SERIES:example-02',
key: 'example-02',
label: 'example-02',
meta: { bucketSize: 10 },
color: '#FF0000',
timeseries: { buckets: [] },
SIBAGG: { value: 2 }
@ -99,7 +104,8 @@ describe('getSplits(resp, panel, series)', () => {
timeseries: { buckets: [] },
SIBAGG: { value: 2 }
}
]
],
meta: { bucketSize: 10 }
}
}
};
@ -120,6 +126,7 @@ describe('getSplits(resp, panel, series)', () => {
id: 'SERIES:example-01',
key: 'example-01',
label: 'example-01',
meta: { bucketSize: 10 },
color: '#FF0000',
timeseries: { buckets: [] },
SIBAGG: { value: 1 }
@ -128,6 +135,7 @@ describe('getSplits(resp, panel, series)', () => {
id: 'SERIES:example-02',
key: 'example-02',
label: 'example-02',
meta: { bucketSize: 10 },
color: '#930000',
timeseries: { buckets: [] },
SIBAGG: { value: 2 }
@ -146,7 +154,8 @@ describe('getSplits(resp, panel, series)', () => {
'filter-2': {
timeseries: { buckets: [] },
}
}
},
meta: { bucketSize: 10 }
}
}
};
@ -168,6 +177,7 @@ describe('getSplits(resp, panel, series)', () => {
id: 'SERIES:filter-1',
key: 'filter-1',
label: '200s',
meta: { bucketSize: 10 },
color: '#F00',
timeseries: { buckets: [] },
},
@ -175,6 +185,7 @@ describe('getSplits(resp, panel, series)', () => {
id: 'SERIES:filter-2',
key: 'filter-2',
label: '300s',
meta: { bucketSize: 10 },
color: '#0F0',
timeseries: { buckets: [] },
}

View file

@ -5,6 +5,7 @@ import getLastMetric from './get_last_metric';
import getSplitColors from './get_split_colors';
import { formatKey } from './format_key';
export default function getSplits(resp, panel, series) {
const meta = _.get(resp, `aggregations.${series.id}.meta`);
const color = new Color(series.color);
const metric = getLastMetric(series);
if (_.has(resp, `aggregations.${series.id}.buckets`)) {
@ -16,6 +17,7 @@ export default function getSplits(resp, panel, series) {
bucket.id = `${series.id}:${bucket.key}`;
bucket.label = formatKey(bucket.key, series);
bucket.color = panel.type === 'top_n' ? color.hex() : colors.shift();
bucket.meta = meta;
return bucket;
});
}
@ -27,6 +29,7 @@ export default function getSplits(resp, panel, series) {
bucket.key = filter.id;
bucket.color = filter.color;
bucket.label = filter.label || filter.filter || '*';
bucket.meta = meta;
return bucket;
});
}
@ -46,7 +49,8 @@ export default function getSplits(resp, panel, series) {
id: series.id,
label: series.label || calculateLabel(metric, series.metrics),
color: color.hex(),
...mergeObj
...mergeObj,
meta
}
];
}

View file

@ -50,6 +50,11 @@ describe('dateHistogram(req, panel, series)', () => {
}
}
}
},
meta: {
bucketSize: 10,
intervalString: '10s',
timeField: '@timestamp'
}
}
}
@ -76,6 +81,11 @@ describe('dateHistogram(req, panel, series)', () => {
}
}
}
},
meta: {
bucketSize: 10,
intervalString: '10s',
timeField: '@timestamp'
}
}
}
@ -105,6 +115,11 @@ describe('dateHistogram(req, panel, series)', () => {
}
}
}
},
meta: {
bucketSize: 20,
intervalString: '20s',
timeField: 'timestamp'
}
}
}

View file

@ -5,7 +5,7 @@ import { set } from 'lodash';
export default function dateHistogram(req, panel, series) {
return next => doc => {
const { timeField, interval } = getIntervalAndTimefield(panel, series);
const { intervalString } = getBucketSize(req, interval);
const { bucketSize, intervalString } = getBucketSize(req, interval);
const { from, to } = offsetTime(req, series.offset_time);
const { timezone } = req.payload.timerange;
@ -19,6 +19,11 @@ export default function dateHistogram(req, panel, series) {
max: to.valueOf()
}
});
set(doc, `aggs.${series.id}.meta`, {
timeField,
intervalString,
bucketSize
});
return next(doc);
};
}

View file

@ -1,4 +1,4 @@
import _ from 'lodash';
import { set } from 'lodash';
import getBucketSize from '../../helpers/get_bucket_size';
import getIntervalAndTimefield from '../../get_interval_and_timefield';
import getTimerange from '../../helpers/get_timerange';
@ -7,11 +7,11 @@ import { calculateAggRoot } from './calculate_agg_root';
export default function dateHistogram(req, panel) {
return next => doc => {
const { timeField, interval } = getIntervalAndTimefield(panel);
const { intervalString } = getBucketSize(req, interval);
const { bucketSize, intervalString } = getBucketSize(req, interval);
const { from, to } = getTimerange(req);
panel.series.forEach(column => {
const aggRoot = calculateAggRoot(doc, column);
_.set(doc, `${aggRoot}.timeseries.date_histogram`, {
set(doc, `${aggRoot}.timeseries.date_histogram`, {
field: timeField,
interval: intervalString,
min_doc_count: 0,
@ -20,6 +20,11 @@ export default function dateHistogram(req, panel) {
max: to.valueOf()
}
});
set(doc, aggRoot.replace(/\.aggs$/, '.meta'), {
timeField,
intervalString,
bucketSize
});
});
return next(doc);
};

View file

@ -6,6 +6,7 @@ import stdMetric from './std_metric';
import stdSibling from './std_sibling';
import timeShift from './time_shift';
import { dropLastBucket } from './drop_last_bucket';
import { mathAgg } from './math';
export default [
percentile,
@ -13,6 +14,7 @@ export default [
stdDeviationSibling,
stdMetric,
stdSibling,
mathAgg,
seriesAgg,
timeShift,
dropLastBucket

View file

@ -0,0 +1,104 @@
const percentileValueMatch = /\[([0-9\.]+)\]$/;
import { startsWith, flatten, values, first, last } from 'lodash';
import getDefaultDecoration from '../../helpers/get_default_decoration';
import getSiblingAggValue from '../../helpers/get_sibling_agg_value';
import getSplits from '../../helpers/get_splits';
import mapBucket from '../../helpers/map_bucket';
import { evaluate } from 'tinymath';
export function mathAgg(resp, panel, series) {
return next => results => {
const mathMetric = last(series.metrics);
if (mathMetric.type !== 'math') return next(results);
// Filter the results down to only the ones that match the series.id. Sometimes
// there will be data from other series mixed in.
results = results.filter(s => {
if (s.id.split(/:/)[0] === series.id) {
return false;
}
return true;
});
const decoration = getDefaultDecoration(series);
const splits = getSplits(resp, panel, series);
const mathSeries = splits.map(split => {
if (mathMetric.variables.length) {
// Gather the data for the splits. The data will either be a sibling agg or
// a standard metric/pipeline agg
const splitData = mathMetric.variables.reduce((acc, v) => {
const metric = series.metrics.find(m => startsWith(v.field, m.id));
if (!metric) return acc;
if (/_bucket$/.test(metric.type)) {
acc[v.name] = split.timeseries.buckets.map(bucket => {
return [bucket.key, getSiblingAggValue(split, metric)];
});
} else {
const percentileMatch = v.field.match(percentileValueMatch);
const m = percentileMatch
? { ...metric, percent: percentileMatch[1] }
: { ...metric };
acc[v.name] = split.timeseries.buckets.map(mapBucket(m));
}
return acc;
}, {});
// Create an params._all so the users can access the entire series of data
// in the Math.js equation
const all = Object.keys(splitData).reduce((acc, key) => {
acc[key] = {
values: splitData[key].map(x => x[1]),
timestamps: splitData[key].map(x => x[0]),
};
return acc;
}, {});
// Get the first var and check that it shows up in the split data otherwise
// we need to return an empty array for the data since we can't opperate
// without the first varaible
const firstVar = first(mathMetric.variables);
if (!splitData[firstVar.name]) {
return {
id: split.id,
label: split.label,
color: split.color,
data: [],
...decoration,
};
}
// Use the first var to collect all the timestamps
const timestamps = splitData[firstVar.name].map(r => first(r));
// Map the timestamps to actual data
const data = timestamps.map((ts, index) => {
const params = mathMetric.variables.reduce((acc, v) => {
acc[v.name] = last(splitData[v.name].find(row => row[0] === ts));
return acc;
}, {});
// If some of the values are null, return the timestamp and null, this is
// a safety check for the user
const someNull = values(params).some(v => v == null);
if (someNull) return [ts, null];
// calculate the result based on the user's script and return the value
const result = evaluate(mathMetric.script, {
params: {
...params,
_index: index,
_timestamp: ts,
_all: all,
_interval: split.meta.bucketSize * 1000,
},
});
// if the result is an object (usually when the user is working with maps and functions) flatten the results and return the last value.
if (typeof result === 'object') {
return [ts, last(flatten(result.valueOf()))];
}
return [ts, result];
});
return {
id: split.id,
label: split.label,
color: split.color,
data,
...decoration,
};
}
});
return next(results.concat(mathSeries));
};
}

View file

@ -2,12 +2,14 @@
import stdMetric from './std_metric';
import stdSibling from './std_sibling';
import seriesAgg from './series_agg';
import { math } from './math';
import { dropLastBucketFn } from './drop_last_bucket';
export default [
// percentile,
stdMetric,
stdSibling,
math,
seriesAgg,
dropLastBucketFn
];

View file

@ -0,0 +1,8 @@
import { mathAgg } from '../series/math';
export function math(bucket, panel, series) {
return next => results => {
const mathFn = mathAgg({ aggregations: bucket }, panel, series);
return mathFn(next)(results);
};
}

View file

@ -99,16 +99,21 @@ describe('buildRequestBody(req)', () => {
filter: {
match_all: {}
},
'aggs': {
'timeseries': {
'date_histogram': {
'field': '@timestamp',
'interval': '10s',
'min_doc_count': 0,
'time_zone': 'UTC',
'extended_bounds': {
'min': 1485463055881,
'max': 1485463955881
meta: {
timeField: '@timestamp',
bucketSize: 10,
intervalString: '10s'
},
aggs: {
timeseries: {
date_histogram: {
field: '@timestamp',
interval: '10s',
min_doc_count: 0,
time_zone: 'UTC',
extended_bounds: {
min: 1485463055881,
max: 1485463955881
}
},
aggs: {

View file

@ -5,26 +5,11 @@ export default function ({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['common', 'visualize', 'header', 'settings', 'visualBuilder']);
describe('visual builder', function describeIndexTests() {
before(function () {
const fromTime = '2015-09-19 06:31:44.000';
const toTime = '2015-09-22 18:31:44.000';
log.debug('navigateToApp visualize');
return PageObjects.common.navigateToUrl('visualize', 'new')
.then(function () {
log.debug('clickVisualBuilderChart');
return PageObjects.visualize.clickVisualBuilder();
})
.then(function setAbsoluteRange() {
log.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"');
return PageObjects.header.setAbsoluteRange(fromTime, toTime);
})
.then(function () {
return PageObjects.header.waitUntilLoadingHasFinished();
});
});
describe('Time Series', function () {
before(async () => {
await PageObjects.visualBuilder.resetPage();
});
it('should show the correct count in the legend', async function () {
const actualCount = await PageObjects.visualBuilder.getRhythmChartLegendValue();
@ -51,8 +36,35 @@ export default function ({ getService, getPageObjects }) {
});
describe('Math Aggregation', () => {
before(async () => {
await PageObjects.visualBuilder.resetPage();
await PageObjects.visualBuilder.clickMetric();
await PageObjects.visualBuilder.createNewAgg();
await PageObjects.visualBuilder.selectAggType('math', 1);
await PageObjects.visualBuilder.fillInVariable();
await PageObjects.visualBuilder.fillInExpression('params.test + 1');
});
it('should not display spy panel toggle button', async function () {
const spyToggleExists = await PageObjects.visualize.getSpyToggleExists();
expect(spyToggleExists).to.be(false);
});
it('should show correct data', async function () {
const expectedMetricValue = '157';
return PageObjects.visualBuilder.getMetricValue()
.then(function (value) {
log.debug(`metric value: ${value}`);
expect(value).to.eql(expectedMetricValue);
});
});
});
describe('metric', () => {
before(async () => {
await PageObjects.visualBuilder.resetPage();
await PageObjects.visualBuilder.clickMetric();
});
@ -63,18 +75,19 @@ export default function ({ getService, getPageObjects }) {
it('should show correct data', async function () {
const expectedMetricValue = '156';
return PageObjects.visualBuilder.getMetricValue()
.then(function (value) {
log.debug(`metric value: ${value}`);
expect(value).to.eql(expectedMetricValue);
});
});
});
// add a gauge test
describe('gauge', () => {
before(async () => {
await PageObjects.visualBuilder.resetPage();
await PageObjects.visualBuilder.clickGauge();
log.debug('clicked on Gauge');
});
@ -90,6 +103,7 @@ export default function ({ getService, getPageObjects }) {
// add a top N test
describe('topN', () => {
before(async () => {
await PageObjects.visualBuilder.resetPage();
await PageObjects.visualBuilder.clickTopN();
log.debug('clicked on TopN');
});
@ -107,6 +121,7 @@ export default function ({ getService, getPageObjects }) {
describe('markdown', () => {
before(async () => {
await PageObjects.visualBuilder.resetPage();
await PageObjects.visualBuilder.clickMarkdown();
await PageObjects.header.setAbsoluteRange('2015-09-22 06:00:00.000', '2015-09-22 11:00:00.000');
});
@ -151,6 +166,7 @@ export default function ({ getService, getPageObjects }) {
// add a table sanity timestamp
describe('table', () => {
before(async () => {
await PageObjects.visualBuilder.resetPage();
await PageObjects.visualBuilder.clickTable();
await PageObjects.header.setAbsoluteRange('2015-09-22 06:00:00.000', '2015-09-22 11:00:00.000');
log.debug('clicked on Table');

View file

@ -2,10 +2,26 @@ import Keys from 'leadfoot/keys';
export function VisualBuilderPageProvider({ getService, getPageObjects }) {
const find = getService('find');
const retry = getService('retry');
const log = getService('log');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common', 'header']);
class VisualBuilderPage {
async resetPage() {
const fromTime = '2015-09-19 06:31:44.000';
const toTime = '2015-09-22 18:31:44.000';
log.debug('navigateToApp visualize');
await PageObjects.common.navigateToUrl('visualize', 'new');
await PageObjects.header.waitUntilLoadingHasFinished();
log.debug('clickVisualBuilderChart');
await find.clickByPartialLinkText('Visual Builder');
log.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"');
await PageObjects.header.setAbsoluteRange(fromTime, toTime);
await PageObjects.header.waitUntilLoadingHasFinished();
}
async clickMetric() {
const button = await testSubjects.find('metricTsvbTypeBtn');
await button.click();
@ -105,6 +121,44 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }) {
await PageObjects.header.waitUntilLoadingHasFinished();
}
async createNewAgg(nth = 0) {
return await retry.try(async () => {
const elements = await testSubjects.findAll('addMetricAddBtn');
await elements[nth].click();
await PageObjects.header.waitUntilLoadingHasFinished();
const aggs = await testSubjects.findAll('aggSelector');
if (aggs.length < 2) {
throw new Error('there should be atleast 2 aggSelectors');
}
});
}
async selectAggType(type, nth = 0) {
const elements = await testSubjects.findAll('aggSelector');
const input = await elements[nth].findByCssSelector('.Select-input input');
await input.type(type);
const option = await elements[nth].findByCssSelector('.Select-option');
await option.click();
return await PageObjects.header.waitUntilLoadingHasFinished();
}
async fillInExpression(expression, nth = 0) {
const expressions = await testSubjects.findAll('mathExpression');
await expressions[nth].type(expression);
return await PageObjects.header.waitUntilLoadingHasFinished();
}
async fillInVariable(name = 'test', metric = 'count', nth = 0) {
const elements = await testSubjects.findAll('varRow');
const input = await elements[nth].findByCssSelector('.vis_editor__calc_vars-name input');
await input.type(name);
const select = await elements[nth].findByCssSelector('.Select-input input');
await select.type(metric);
const option = await elements[nth].findByCssSelector('.Select-option');
await option.click();
return await PageObjects.header.waitUntilLoadingHasFinished();
}
async selectGroupByField(fieldName) {
const element = await testSubjects.find('groupByField');

View file

@ -11235,6 +11235,12 @@ tinygradient@0.3.0:
dependencies:
tinycolor2 "~1.0.0"
tinymath@0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tinymath/-/tinymath-0.2.1.tgz#3a7f2ee03956bd86c6aca172a6b2ab857cf53717"
dependencies:
lodash.get "^4.4.2"
tmp@0.0.23:
version "0.0.23"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.23.tgz#de874aa5e974a85f0a32cdfdbd74663cb3bd9c74"