Visual builder: percentile ranks should allow multiple values (#33642)

* Visual builder: percentile ranks should allow multiple values

Fix: #33144

* Visual builder: percentile ranks should allow multiple values - fix translation

Fix: #33144

* Visual builder: percentile ranks should allow multiple values - fix translation

Fix: #33144

* Visual builder: percentile ranks should allow multiple values  - add migration script

* Visual builder: percentile ranks should allow multiple values  - fix tests

* Visual builder: percentile ranks should allow multiple values  - add executeMigrations function

* Visual builder: percentile ranks should allow multiple values  - fix table view label

* Visual builder: percentile ranks should allow multiple values  - fix comments

* Visual builder: percentile ranks should allow multiple values  -add multi value row

* Visual builder: percentile ranks should allow multiple values  -add multi value row

* Visual builder: percentile ranks should allow multiple values   fix review comments

* Visual builder: percentile ranks should allow multiple values   fix review comments

* [TSVB] Code cleanup - remove unused file (core_plugins/metrics/public/lib/create_new_panel.js)
This commit is contained in:
Alexey Antonov 2019-03-30 17:41:35 +03:00 committed by GitHub
parent 41e49cf93f
commit ed57d3b4d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 510 additions and 122 deletions

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { cloneDeep, get, omit } from 'lodash';
import { cloneDeep, get, omit, has } from 'lodash';
function migrateIndexPattern(doc) {
const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON');
@ -145,6 +145,44 @@ export const migrations = {
} catch (e) {
throw new Error(`Failure attempting to migrate saved object '${doc.attributes.title}' - ${e}`);
}
},
'7.1.0': doc => {
// [TSVB] Migrate percentile-rank aggregation (value -> values)
const migratePercentileRankAggregation = doc => {
const visStateJSON = get(doc, 'attributes.visState');
let visState;
if (visStateJSON) {
try {
visState = JSON.parse(visStateJSON);
} catch (e) {
// Let it go, the data is invalid and we'll leave it as is
}
if (visState && visState.type === 'metrics') {
const series = get(visState, 'params.series') || [];
series.forEach(part => {
(part.metrics || []).forEach(metric => {
if (metric.type === 'percentile_rank' && has(metric, 'value')) {
metric.values = [metric.value];
delete metric.value;
}
});
});
return {
...doc,
attributes: {
...doc.attributes,
visState: JSON.stringify(visState),
},
};
}
}
return doc;
};
return migratePercentileRankAggregation(doc);
}
},
dashboard: {

View file

@ -56,16 +56,10 @@ export default function calculateLabel(metric, metrics) {
);
}
if (metric.type === 'percentile_rank') {
return i18n.translate('tsvb.calculateLabel.percentileRankLabel', {
defaultMessage: '{lookupMetricType} ({metricValue}) of {metricField}',
values: { lookupMetricType: lookup[metric.type], metricValue: metric.value, metricField: metric.field }
});
}
if (includes(paths, metric.type)) {
const targetMetric = metrics.find(m => startsWith(metric.field, m.id));
const targetLabel = calculateLabel(targetMetric, metrics);
// For percentiles we need to parse the field id to extract the percentile
// the user configured in the percentile aggregation and specified in the
// submetric they selected. This applies only to pipeline aggs.

View file

@ -19,21 +19,23 @@
import PropTypes from 'prop-types';
import React from 'react';
import _ from 'lodash';
import { includes } from 'lodash';
import { injectI18n } from '@kbn/i18n/react';
import {
EuiComboBox,
} from '@elastic/eui';
import calculateSiblings from '../lib/calculate_siblings';
import calculateLabel from '../../../common/calculate_label';
import basicAggs from '../../../common/basic_aggs';
import { injectI18n } from '@kbn/i18n/react';
import { toPercentileNumber } from '../../../common/to_percentile_number';
import { METRIC_TYPES } from '../../../common/metric_types';
function createTypeFilter(restrict, exclude) {
return metric => {
if (_.includes(exclude, metric.type)) return false;
if (includes(exclude, metric.type)) return false;
switch (restrict) {
case 'basic':
return _.includes(basicAggs, metric.type);
return includes(basicAggs, metric.type);
default:
return true;
}
@ -73,15 +75,31 @@ function MetricSelectUi(props) {
.filter(row => /^percentile/.test(row.type))
.reduce((acc, row) => {
const label = calculateLabel(row, calculatedMetrics);
row.percentiles.forEach(p => {
if (p.value) {
const value = /\./.test(p.value) ? p.value : `${p.value}.0`;
acc.push({
value: `${row.id}[${value}]`,
label: `${label} (${value})`,
switch (row.type) {
case METRIC_TYPES.PERCENTILE_RANK:
(row.values || []).forEach(p => {
const value = toPercentileNumber(p);
acc.push({
value: `${row.id}[${value}]`,
label: `${label} (${value})`,
});
});
}
});
case METRIC_TYPES.PERCENTILE:
(row.percentiles || []).forEach(p => {
if (p.value) {
const value = toPercentileNumber(p.value);
acc.push({
value: `${row.id}[${value}]`,
label: `${label} (${value})`,
});
}
});
}
return acc;
}, []);

View file

@ -0,0 +1,19 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { PercentileRankAgg } from './percentile_rank';

View file

@ -0,0 +1,96 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import { get } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import {
htmlIdGenerator,
EuiFieldNumber,
EuiFlexGroup,
EuiFlexItem,
EuiFormLabel,
EuiSpacer,
} from '@elastic/eui';
import AddDeleteButtons from '../../add_delete_buttons';
export const MultiValueRow = ({
model,
onChange,
onDelete,
onAdd,
disableAdd,
disableDelete,
}) => {
const htmlId = htmlIdGenerator();
const onFieldNumberChange = event => onChange({
...model,
value: get(event, 'target.value')
});
return (
<div className="tvbAggRow__multiValueRow">
<EuiFlexGroup responsive={false} alignItems="center">
<EuiFlexItem grow={false}>
<EuiFormLabel htmlFor={htmlId('value')}>
<FormattedMessage
id="tsvb.multivalueRow.valueLabel"
defaultMessage="Value:"
/>
</EuiFormLabel>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFieldNumber
value={model.value === '' ? '' : Number(model.value)}
placeholder={0}
onChange={onFieldNumberChange}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<AddDeleteButtons
onAdd={onAdd}
onDelete={() => onDelete(model)}
disableDelete={disableDelete}
disableAdd={disableAdd}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer/>
</div>
);
};
MultiValueRow.defaultProps = {
model: {
id: null,
value: '',
},
};
MultiValueRow.propTypes = {
model: PropTypes.object,
onChange: PropTypes.func,
onDelete: PropTypes.func,
onAdd: PropTypes.func,
defaultAddValue: PropTypes.string,
disableDelete: PropTypes.bool,
};

View file

@ -19,33 +19,40 @@
import PropTypes from 'prop-types';
import React from 'react';
import AggSelect from './agg_select';
import FieldSelect from './field_select';
import AggRow from './agg_row';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import { assign } from 'lodash';
import AggSelect from '../agg_select';
import FieldSelect from '../field_select';
import AggRow from '../agg_row';
import createChangeHandler from '../../lib/create_change_handler';
import createSelectHandler from '../../lib/create_select_handler';
import { PercentileRankValues } from './percentile_rank_values';
import {
htmlIdGenerator,
EuiFlexGroup,
EuiFlexItem,
EuiFormLabel,
EuiFieldText,
EuiFormRow,
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
export const PercentileRankAgg = props => {
const { series, panel, fields } = props;
const defaults = { value: '' };
const defaults = { values: [''] };
const model = { ...defaults, ...props.model };
const handleChange = createChangeHandler(props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handleTextChange = createTextHandler(handleChange);
const indexPattern = series.override_index_pattern && series.series_index_pattern || panel.index_pattern;
const htmlId = htmlIdGenerator();
const isTablePanel = panel.type === 'table';
const handleChange = createChangeHandler(props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handlePercentileRankValuesChange = (values) => {
handleChange(assign({}, model, {
values,
}));
};
return (
<AggRow
@ -89,21 +96,15 @@ export const PercentileRankAgg = props => {
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
id={htmlId('value')}
label={(<FormattedMessage
id="tsvb.percentileRank.valueLabel"
defaultMessage="Value"
/>)}
>
<EuiFieldText
value={model.value}
onChange={handleTextChange('value')}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer/>
<PercentileRankValues
disableAdd={isTablePanel}
disableDelete={isTablePanel}
showOnlyLastRow={isTablePanel}
model={model.values}
onChange={handlePercentileRankValuesChange}
/>
</AggRow>
);
};

View file

@ -0,0 +1,83 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import { last } from 'lodash';
import {
EuiFlexGroup,
} from '@elastic/eui';
import { MultiValueRow } from './multi_value_row';
export const PercentileRankValues = props => {
const model = props.model || [];
const { onChange, disableAdd, disableDelete, showOnlyLastRow } = props;
const onChangeValue = ({ value, id }) => {
model[id] = value;
onChange(model);
};
const onDeleteValue = ({ id }) => onChange(
model.filter((item, currentIndex) => id !== currentIndex),
);
const onAddValue = () => onChange([...model, '']);
const renderRow = ({ rowModel, disableDelete, disableAdd }) => (
<MultiValueRow
key={`percentileRankValue__item${rowModel.id}`}
onAdd={onAddValue}
onChange={onChangeValue}
onDelete={onDeleteValue}
disableDelete={disableDelete}
disableAdd={disableAdd}
model={rowModel}
/>);
return (
<EuiFlexGroup direction="column" responsive={false} gutterSize="xs">
{showOnlyLastRow && renderRow({
rowModel: {
id: model.length - 1,
value: last(model),
},
disableAdd: true,
disableDelete: true
})}
{!showOnlyLastRow && model.map((value, id, array) => (
renderRow({
rowModel: {
id,
value,
},
disableAdd,
disableDelete: disableDelete || array.length < 2,
})))}
</EuiFlexGroup>
);
};
PercentileRankValues.propTypes = {
model: PropTypes.array,
onChange: PropTypes.func,
disableDelete: PropTypes.bool,
disableAdd: PropTypes.bool,
showOnlyLastRow: PropTypes.bool,
};

View file

@ -17,7 +17,7 @@
* under the License.
*/
import _ from 'lodash';
import _, { isArray, last, get } from 'lodash';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { fieldFormats } from 'ui/registry/field_formats';
@ -28,6 +28,8 @@ import { EuiToolTip, EuiIcon } from '@elastic/eui';
import replaceVars from '../../lib/replace_vars';
import { FormattedMessage } from '@kbn/i18n/react';
import { METRIC_TYPES } from '../../../../common/metric_types';
const DateFormat = fieldFormats.getType('date');
function getColor(rules, colorKey, value) {
@ -44,12 +46,6 @@ function getColor(rules, colorKey, value) {
return color;
}
const getPercentileLabel = (metric, item) => {
const { value } = _.last(metric.percentiles);
const label = calculateLabel(metric, item.metrics);
return `${label}, ${value || 0}`;
};
class TableVis extends Component {
constructor(props) {
@ -58,9 +54,7 @@ class TableVis extends Component {
}
get visibleSeries() {
return _
.get(this.props, 'model.series', [])
.filter(series => !series.hidden);
return get(this.props, 'model.series', []).filter(series => !series.hidden);
}
renderRow = row => {
@ -108,11 +102,23 @@ class TableVis extends Component {
order: 'asc',
});
const calculateHeaderLabel = (metric, item) => {
const defaultLabel = item.label || calculateLabel(metric, item.metrics);
switch (metric.type) {
case METRIC_TYPES.PERCENTILE:
return `${defaultLabel} (${last(metric.percentiles).value || 0})`;
case METRIC_TYPES.PERCENTILE_RANK:
return `${defaultLabel} (${last(metric.values) || 0})`;
default:
return defaultLabel;
}
};
const columns = this.visibleSeries.map(item => {
const metric = _.last(item.metrics);
const label = metric.type === 'percentile' ?
getPercentileLabel(metric, item) :
item.label || calculateLabel(metric, item.metrics);
const metric = last(item.metrics);
const label = calculateHeaderLabel(metric, item);
const handleClick = () => {
if (!isSortable(metric)) return;
let order;
@ -193,7 +199,7 @@ class TableVis extends Component {
const header = this.renderHeader();
let rows;
if (_.isArray(visData.series) && visData.series.length) {
if (isArray(visData.series) && visData.series.length) {
rows = visData.series.map(this.renderRow);
} else {
const message = model.pivot_id ?

View file

@ -21,17 +21,17 @@ import React from 'react';
import _ from 'lodash';
import { EuiIcon } from '@elastic/eui';
export default props => (row) => {
export default props => (row, index = 0) => {
function tickFormatter(value) {
if (_.isFunction(props.tickFormatter)) return props.tickFormatter(value);
return value;
}
const key = `tvbLegend__item${row.id}${index}`;
const formatter = row.tickFormatter || tickFormatter;
const value = formatter(props.seriesValues[row.id]);
const classes = ['tvbLegend__item'];
const key = row.id;
if (!_.includes(props.seriesFilter, row.id)) classes.push('disabled');
if (row.label == null || row.legend === false) return (<div key={key} style={{ display: 'none' }}/>);
return (

View file

@ -20,7 +20,7 @@
import parseSettings from './parse_settings';
import getBucketsPath from './get_buckets_path';
import { parseInterval } from './parse_interval';
import { set } from 'lodash';
import { set, isEmpty } from 'lodash';
import { i18n } from '@kbn/i18n';
function checkMetric(metric, fields) {
@ -122,17 +122,6 @@ export default {
return body;
},
percentile_rank: bucket => {
checkMetric(bucket, ['type', 'field', 'value']);
const body = {
percentile_ranks: {
field: bucket.field,
values: [bucket.value],
},
};
return body;
},
avg_bucket: extendStatsBucket,
max_bucket: extendStatsBucket,
min_bucket: extendStatsBucket,
@ -158,6 +147,19 @@ export default {
return agg;
},
percentile_rank: bucket => {
checkMetric(bucket, ['type', 'field', 'values']);
return {
percentile_ranks: {
field: bucket.field,
values: (bucket.values || [])
.map(value => isEmpty(value) ? 0 : value),
},
};
},
derivative: (bucket, metrics, bucketSize) => {
checkMetric(bucket, ['type', 'field']);
const body = {

View file

@ -18,8 +18,8 @@
*/
import { get, includes, max, min, sum } from 'lodash';
import { EXTENDED_STATS_TYPES, METRIC_TYPES } from './metric_types';
import { toPercentileNumber } from './to_percentile_number';
import { toPercentileNumber } from '../../../../common/to_percentile_number';
import { EXTENDED_STATS_TYPES, METRIC_TYPES } from '../../../../common/metric_types';
const aggFns = {
max,

View file

@ -18,8 +18,8 @@
*/
import { startsWith } from 'lodash';
import { METRIC_TYPES } from './metric_types';
import { toPercentileNumber } from './to_percentile_number';
import { toPercentileNumber } from '../../../../common/to_percentile_number';
import { METRIC_TYPES } from '../../../../common/metric_types';
const percentileTest = /\[[0-9\.]+\]$/;
@ -40,6 +40,7 @@ export default (id, metrics) => {
bucketsPath += `[${toPercentileNumber(percent.value)}]`;
break;
case METRIC_TYPES.PERCENTILE_RANK:
if (percentileTest.test(bucketsPath)) break;
bucketsPath += `[${toPercentileNumber(metric.value)}]`;
break;
case METRIC_TYPES.STD_DEVIATION:

View file

@ -18,6 +18,8 @@
*/
import percentile from './percentile';
import percentileRank from './percentile_rank';
import seriesAgg from './series_agg';
import stdDeviationBands from './std_deviation_bands';
import stdDeviationSibling from './std_deviation_sibling';
@ -29,6 +31,7 @@ import { mathAgg } from './math';
export default [
percentile,
percentileRank,
stdDeviationBands,
stdDeviationSibling,
stdMetric,

View file

@ -22,15 +22,20 @@ import getAggValue from '../../helpers/get_agg_value';
import getDefaultDecoration from '../../helpers/get_default_decoration';
import getSplits from '../../helpers/get_splits';
import getLastMetric from '../../helpers/get_last_metric';
import { METRIC_TYPES } from '../../../../../common/metric_types';
export default function percentile(resp, panel, series, meta) {
return next => results => {
const metric = getLastMetric(series);
if (metric.type !== 'percentile') return next(results);
if (metric.type !== METRIC_TYPES.PERCENTILE) {
return next(results);
}
getSplits(resp, panel, series, meta).forEach((split) => {
metric.percentiles.forEach(percentile => {
const percentileValue = percentile.value ? percentile.value : 0;
const label = (split.label) + ` (${percentileValue})`;
const label = `${split.label} (${percentileValue})`;
const data = split.timeseries.buckets.map(bucket => {
const m = _.assign({}, metric, { percent: percentileValue });
return [bucket.key, getAggValue(bucket, m)];

View file

@ -0,0 +1,54 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import getAggValue from '../../helpers/get_agg_value';
import getDefaultDecoration from '../../helpers/get_default_decoration';
import getSplits from '../../helpers/get_splits';
import getLastMetric from '../../helpers/get_last_metric';
import { toPercentileNumber } from '../../../../../common/to_percentile_number';
import { METRIC_TYPES } from '../../../../../common/metric_types';
export default function percentileRank(resp, panel, series, meta) {
return next => results => {
const metric = getLastMetric(series);
if (metric.type !== METRIC_TYPES.PERCENTILE_RANK) {
return next(results);
}
getSplits(resp, panel, series, meta).forEach(split => {
(metric.values || []).forEach(percentileRank => {
const data = split.timeseries.buckets.map(bucket => (
[bucket.key, getAggValue(bucket, {
...metric,
value: toPercentileNumber(percentileRank)
})]
));
results.push({
data,
id: `${split.id}:${percentileRank}`,
label: `${split.label} (${percentileRank || 0})`,
color: split.color,
...getDefaultDecoration(series),
});
});
});
return next(results);
};
}

View file

@ -21,13 +21,16 @@ import getDefaultDecoration from '../../helpers/get_default_decoration';
import getSplits from '../../helpers/get_splits';
import getLastMetric from '../../helpers/get_last_metric';
import mapBucket from '../../helpers/map_bucket';
import { METRIC_TYPES } from '../../../../../common/metric_types';
export default function stdMetric(resp, panel, series, meta) {
return next => results => {
const metric = getLastMetric(series);
if (metric.type === 'std_deviation' && metric.mode === 'band') {
if (metric.type === METRIC_TYPES.STD_DEVIATION && metric.mode === 'band') {
return next(results);
}
if (metric.type === 'percentile') {
if ([METRIC_TYPES.PERCENTILE_RANK, METRIC_TYPES.PERCENTILE].includes(metric.type)) {
return next(results);
}
if (/_bucket$/.test(metric.type)) return next(results);

View file

@ -22,11 +22,14 @@ import stdMetric from './std_metric';
import stdSibling from './std_sibling';
import seriesAgg from './series_agg';
import percentile from './percentile';
import percentileRank from './percentile_rank';
import { math } from './math';
import { dropLastBucketFn } from './drop_last_bucket';
export default [
percentile,
percentileRank,
stdMetric,
stdSibling,
math,

View file

@ -19,29 +19,35 @@
import { last } from 'lodash';
import getSplits from '../../helpers/get_splits';
import getLastMetric from '../../helpers/get_last_metric';
import { toPercentileNumber } from '../../../../../common/to_percentile_number';
import { METRIC_TYPES } from '../../../../../common/metric_types';
export default function percentile(bucket, panel, series) {
return next => results => {
const metric = getLastMetric(series);
if (metric.type !== 'percentile') return next(results);
const fakeResp = { aggregations: bucket };
if (metric.type !== METRIC_TYPES.PERCENTILE) {
return next(results);
}
const fakeResp = {
aggregations: bucket,
};
getSplits(fakeResp, panel, series).forEach(split => {
// table allows only one percentile in a series (the last one will be chosen in case of several)
const percentile = last(metric.percentiles);
let percentileKey = percentile.value ? percentile.value : 0;
if (!/\./.test(percentileKey)) {
percentileKey = `${percentileKey}.0`;
}
const data = split.timeseries.buckets.map(bucket => [bucket.key, bucket[metric.id].values[percentileKey]]);
const percentileKey = toPercentileNumber(percentile.value);
const data = split.timeseries.buckets
.map(bucket => [bucket.key, bucket[metric.id].values[percentileKey]]);
results.push({
id: split.id,
data
});
});
return next(results);
};
}

View file

@ -0,0 +1,59 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { last } from 'lodash';
import getSplits from '../../helpers/get_splits';
import getLastMetric from '../../helpers/get_last_metric';
import { toPercentileNumber } from '../../../../../common/to_percentile_number';
import getAggValue from '../../helpers/get_agg_value';
import { METRIC_TYPES } from '../../../../../common/metric_types';
export default function percentileRank(bucket, panel, series) {
return next => results => {
const metric = getLastMetric(series);
if (metric.type !== METRIC_TYPES.PERCENTILE_RANK) {
return next(results);
}
const fakeResp = {
aggregations: bucket,
};
getSplits(fakeResp, panel, series).forEach(split => {
// table allows only one percentile rank in a series (the last one will be chosen in case of several)
const lastRankValue = last(metric.values);
const percentileRank = toPercentileNumber(lastRankValue);
const data = split.timeseries.buckets.map(bucket => (
[bucket.key, getAggValue(bucket, {
...metric,
value: percentileRank
})]
));
results.push({
data,
id: split.id,
label: `${split.label} (${percentileRank || 0})`,
});
});
return next(results);
};
}

View file

@ -20,18 +20,28 @@
import getSplits from '../../helpers/get_splits';
import getLastMetric from '../../helpers/get_last_metric';
import mapBucket from '../../helpers/map_bucket';
import { METRIC_TYPES } from '../../../../../common/metric_types';
export default function stdMetric(bucket, panel, series) {
return next => results => {
const metric = getLastMetric(series);
if (metric.type === 'std_deviation' && metric.mode === 'band') {
return next(results);
}
if (metric.type === 'percentile') {
return next(results);
}
if (/_bucket$/.test(metric.type)) return next(results);
const fakeResp = { aggregations: bucket };
if (metric.type === METRIC_TYPES.STD_DEVIATION && metric.mode === 'band') {
return next(results);
}
if ([METRIC_TYPES.PERCENTILE_RANK, METRIC_TYPES.PERCENTILE].includes(metric.type)) {
return next(results);
}
if (/_bucket$/.test(metric.type)) {
return next(results);
}
const fakeResp = {
aggregations: bucket,
};
getSplits(fakeResp, panel, series).forEach(split => {
const data = split.timeseries.buckets.map(mapBucket(metric));
results.push({

View file

@ -54,9 +54,6 @@ export default function ({ getService }) {
saved_objects: [
{
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
migrationVersion: {
visualization: '7.0.0'
},
type: 'visualization',
updated_at: '2017-09-21T18:51:23.794Z',
version: resp.body.saved_objects[0].version,
@ -70,7 +67,7 @@ export default function ({ getService }) {
kibanaSavedObjectMeta: resp.body.saved_objects[0].attributes.kibanaSavedObjectMeta
},
migrationVersion: {
visualization: '7.0.0',
visualization: '7.1.0',
},
references: [{
name: 'kibanaSavedObjectMeta.searchSourceJSON.index',

View file

@ -48,16 +48,13 @@ export default function ({ getService }) {
id: resp.body.id,
type: 'visualization',
migrationVersion: {
visualization: '7.0.0'
visualization: '7.1.0'
},
updated_at: resp.body.updated_at,
version: 'WzgsMV0=',
attributes: {
title: 'My favorite vis'
},
migrationVersion: {
visualization: '7.0.0',
},
references: [],
});
});
@ -93,16 +90,13 @@ export default function ({ getService }) {
id: resp.body.id,
type: 'visualization',
migrationVersion: {
visualization: '7.0.0'
visualization: '7.1.0'
},
updated_at: resp.body.updated_at,
version: 'WzAsMV0=',
attributes: {
title: 'My favorite vis'
},
migrationVersion: {
visualization: '7.0.0',
},
references: [],
});
});

View file

@ -36,14 +36,11 @@ export default function ({ getService }) {
.then(resp => {
expect(resp.body).to.eql({
id: 'dd7caf20-9efd-11e7-acb3-3dab96693fab',
migrationVersion: {
visualization: '7.0.0'
},
type: 'visualization',
updated_at: '2017-09-21T18:51:23.794Z',
version: resp.body.version,
migrationVersion: {
visualization: '7.0.0',
visualization: '7.1.0',
},
attributes: {
title: 'Count of requests',

View file

@ -2793,7 +2793,6 @@
"tsvb.calculateLabel.lookupMetricTypeOfTargetLabel": "{targetLabel} 的 {lookupMetricType}",
"tsvb.calculateLabel.lookupMetricTypeOfTargetWithAdditionalLabel": "{targetLabel} ({additionalLabel}) 的 {lookupMetricType}",
"tsvb.calculateLabel.mathLabel": "数学",
"tsvb.calculateLabel.percentileRankLabel": "{metricField} 的 {lookupMetricType} ({metricValue})",
"tsvb.calculateLabel.seriesAggLabel": "序列聚合 ({metricFunction})",
"tsvb.calculateLabel.staticValueLabel": "{metricValue} 的静态值",
"tsvb.calculateLabel.unknownLabel": "未知",
@ -2974,7 +2973,7 @@
"tsvb.percentile.shadeLabel": "阴影0 到 1",
"tsvb.percentileRank.aggregationLabel": "聚合",
"tsvb.percentileRank.fieldLabel": "字段",
"tsvb.percentileRank.valueLabel": "值",
"tsvb.multivalueRow.valueLabel": "值",
"tsvb.positiveOnly.aggregationLabel": "聚合",
"tsvb.positiveOnly.metricLabel": "指标",
"tsvb.replaceVars.errors.markdownErrorDescription": "请确认您仅在使用 Markdown、已知变量和内置 Handlebar 表达式",

View file

@ -90,7 +90,7 @@ export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest<an
id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`,
type: 'visualization',
migrationVersion: {
visualization: '7.0.0',
visualization: '7.1.0',
},
updated_at: '2017-09-21T18:51:23.794Z',
version: resp.body.saved_objects[0].version,

View file

@ -59,7 +59,7 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe
expect(resp.body).to.eql({
id: resp.body.id,
migrationVersion: {
visualization: '7.0.0',
visualization: '7.1.0',
},
type: spaceAwareType,
updated_at: resp.body.updated_at,

View file

@ -71,7 +71,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
id: `${getIdPrefix(spaceId)}91200a00-9efd-11e7-acb3-3dab96693fab`,
},
],
migrationVersion: { visualization: '7.0.0' },
migrationVersion: { visualization: '7.1.0' },
updated_at: '2017-09-21T18:51:23.794Z',
});
};

View file

@ -96,7 +96,7 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperTest<any>)
id: `${getIdPrefix(spaceId)}dd7caf20-9efd-11e7-acb3-3dab96693fab`,
type: 'visualization',
migrationVersion: {
visualization: '7.0.0',
visualization: '7.1.0',
},
updated_at: '2017-09-21T18:51:23.794Z',
version: resp.body.version,