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:
parent
41e49cf93f
commit
ed57d3b4d3
|
@ -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: {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -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';
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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 ?
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)];
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: [],
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 表达式",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue