[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:
parent
3bf90dc9f1
commit
84ad08e70d
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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',
|
||||
];
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
113
src/core_plugins/metrics/public/components/aggs/math.js
Normal file
113
src/core_plugins/metrics/public/components/aggs/math.js
Normal 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.<name></code> To access all the data use
|
||||
<code>params._all.<name>.values</code> for an array of the
|
||||
values and <code>params._all.<name>.timestamps</code>
|
||||
for an array of the timestamps. <code>params._timestamp</code>
|
||||
is available for the current bucket's timestamp,
|
||||
<code>params._index</code> is available for the current
|
||||
bucket'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;
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: [] },
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
}
|
|
@ -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
|
||||
];
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue