Allow non numeric fields in Top Hits in TSVB (#35661) (#35943)

* Allow non numeric fields in Top Hits in TSVB

* fix build

* fix pr comment
This commit is contained in:
Alexey Antonov 2019-05-02 14:49:18 +03:00 committed by GitHub
parent fcc2520822
commit 8a98a89cbf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 233 additions and 193 deletions

View file

@ -22,8 +22,8 @@ import getLastValue from '../get_last_value';
describe('getLastValue(data)', () => {
it('returns zero if data is not array', () => {
expect(getLastValue('foo')).to.equal(0);
it('returns data if data is not array', () => {
expect(getLastValue('foo')).to.equal('foo');
});
it('returns the last value', () => {
@ -33,10 +33,10 @@ describe('getLastValue(data)', () => {
it('returns the second to last value if the last value is null (default)', () => {
const data = [[1, 4], [2, null]];
expect(getLastValue(data)).to.equal(0);
expect(getLastValue(data)).to.equal(4);
});
it('returns the zero if second to last is null (default)', () => {
it('returns 0 if second to last is not defined (default)', () => {
const data = [[1, null], [2, null]];
expect(getLastValue(data)).to.equal(0);
});
@ -45,7 +45,5 @@ describe('getLastValue(data)', () => {
const data = [[1, 4], [2, null], [3, null]];
expect(getLastValue(data, 3)).to.equal(4);
});
});

View file

@ -17,25 +17,10 @@
* under the License.
*/
import _ from 'lodash';
export default function byType(type) {
return (field) => {
switch (type) {
case 'numeric':
return _.includes([
'number'
], field.type);
case 'string':
return _.includes([
'string', 'keyword', 'text'
], field.type);
case 'date':
return _.includes([
'date'
], field.type);
default:
return true;
}
};
}
export const ES_TYPES = {
NUMBER: 'number',
STRING: 'string',
KEYWORD: 'keyword',
TEXT: 'text',
DATE: 'date',
};

View file

@ -17,24 +17,20 @@
* under the License.
*/
import _ from 'lodash';
export default (data, lookback = 1) => {
if (_.isNumber(data)) return data;
if (!Array.isArray(data)) return 0;
// First try the last value
const last = data[data.length - 1];
const lastValue = Array.isArray(last) && last[1];
if (lastValue) return lastValue;
import { isArray, findLast } from 'lodash';
const DEFAULT_VALUE = 0;
export default (data, defaultValue = DEFAULT_VALUE) => {
if (!isArray(data)) {
return data;
}
const extractValue = data => data && data[1] || null;
// If the last value is zero or null because of a partial bucket or
// some kind of timeshift weirdness we will show the second to last.
let lookbackCounter = 1;
let value;
while (lookback > lookbackCounter && !value) {
const next = data[data.length - ++lookbackCounter];
value = _.isArray(next) && next[1] || 0;
}
return value || 0;
const lastValid = findLast(data, item => extractValue(item));
return extractValue(lastValid) || defaultValue;
};

View file

@ -26,6 +26,7 @@ export const METRIC_TYPES = {
STD_DEVIATION: 'std_deviation',
VARIANCE: 'variance',
SUM_OF_SQUARES: 'sum_of_squares',
CARDINALITY: 'cardinality',
};
export const EXTENDED_STATS_TYPES = [

View file

@ -0,0 +1,27 @@
/*
* 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 const PANEL_TYPES = {
TABLE: 'table',
GAUGE: 'gauge',
MARKDOWN: 'markdown',
TOP_N: 'top_n',
TIMESERIES: 'timeseries',
METRIC: 'metric',
};

View file

@ -22,9 +22,12 @@ import React from 'react';
import {
EuiComboBox,
} from '@elastic/eui';
import generateByTypeFilter from '../lib/generate_by_type_filter';
import { injectI18n } from '@kbn/i18n/react';
import { isFieldEnabled } from '../../lib/check_ui_restrictions';
import { i18n } from '@kbn/i18n';
const isFieldTypeEnabled = (fieldRestrictions, fieldType) =>
fieldRestrictions.length ? fieldRestrictions.includes(fieldType) : true;
function FieldSelectUi({
type,
@ -34,7 +37,6 @@ function FieldSelectUi({
onChange,
disabled,
restrict,
intl,
uiRestrictions,
...rest
}) {
@ -43,18 +45,38 @@ function FieldSelectUi({
return null;
}
const typeFilter = generateByTypeFilter(restrict);
const options = (fields[indexPattern] || [])
.filter(field => typeFilter(field) && isFieldEnabled(field.name, type, uiRestrictions))
.map(field => ({ label: field.name, value: field.name }));
const selectedOptions = [];
const options = Object.values((fields[indexPattern] || []).reduce((acc, field) => {
if (isFieldTypeEnabled(restrict, field.type) && isFieldEnabled(field.name, type, uiRestrictions)) {
const item = {
label: field.name,
value: field.name
};
const selectedOption = options.find(option => value === option.value);
const selectedOptions = selectedOption ? [selectedOption] : [];
if (acc[field.type]) {
acc[field.type].options.push(item);
} else {
acc[field.type] = {
options: [item],
label: field.type,
};
}
if (value === item.value) {
selectedOptions.push(item);
}
}
return acc;
}, {}));
if (onChange && value && !selectedOptions.length) {
onChange();
}
return (
<EuiComboBox
placeholder={intl.formatMessage({
id: 'tsvb.fieldSelect.selectFieldPlaceholder',
placeholder={i18n.translate('tsvb.fieldSelect.selectFieldPlaceholder', {
defaultMessage: 'Select field...',
})}
isDisabled={disabled}
@ -70,7 +92,7 @@ function FieldSelectUi({
FieldSelectUi.defaultProps = {
indexPattern: '*',
disabled: false,
restrict: 'none',
restrict: [],
};
FieldSelectUi.propTypes = {
@ -79,7 +101,7 @@ FieldSelectUi.propTypes = {
id: PropTypes.string,
indexPattern: PropTypes.string,
onChange: PropTypes.func,
restrict: PropTypes.string,
restrict: PropTypes.array,
type: PropTypes.string,
value: PropTypes.string,
uiRestrictions: PropTypes.object,

View file

@ -35,6 +35,8 @@ import {
EuiFormRow,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ES_TYPES } from '../../../common/es_types';
import { METRIC_TYPES } from '../../../common/metric_types';
export const FilterRatioAgg = props => {
const {
@ -57,7 +59,7 @@ export const FilterRatioAgg = props => {
const model = { ...defaults, ...props.model };
const htmlId = htmlIdGenerator();
const restrictMode = model.metric_agg === 'cardinality' ? 'none' : 'numeric';
const restrictFields = model.metric_agg === METRIC_TYPES.CARDINALITY ? [] : [ES_TYPES.NUMBER];
return (
<AggRow
@ -149,7 +151,7 @@ export const FilterRatioAgg = props => {
<FieldSelect
fields={fields}
type={model.metric_agg}
restrict={restrictMode}
restrict={restrictFields}
indexPattern={indexPattern}
value={model.field}
onChange={handleSelectChange('field')}

View file

@ -39,11 +39,14 @@ import {
EuiFormRow,
} from '@elastic/eui';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { ES_TYPES } from '../../../common/es_types';
const newPercentile = (opts) => {
return _.assign({ id: uuid.v1(), mode: 'line', shade: 0.2 }, opts);
};
const RESTRICT_FIELDS = [ES_TYPES.NUMBER];
class PercentilesUi extends Component {
handleTextChange(item, name) {
@ -257,7 +260,7 @@ class PercentileAgg extends Component { // eslint-disable-line react/no-multi-co
<FieldSelect
fields={fields}
type={model.type}
restrict="numeric"
restrict={RESTRICT_FIELDS}
indexPattern={indexPattern}
value={model.field}
onChange={handleSelectChange('field')}

View file

@ -36,6 +36,9 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ES_TYPES } from '../../../../common/es_types';
const RESTRICT_FIELDS = [ES_TYPES.NUMBER];
export const PercentileRankAgg = props => {
const { series, panel, fields } = props;
@ -89,7 +92,7 @@ export const PercentileRankAgg = props => {
<FieldSelect
fields={fields}
type={model.type}
restrict="numeric"
restrict={RESTRICT_FIELDS}
indexPattern={indexPattern}
value={model.field}
onChange={handleSelectChange('field')}

View file

@ -26,15 +26,14 @@ import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFormLabel } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ES_TYPES } from '../../../common/es_types';
import { METRIC_TYPES } from '../../../common/metric_types';
function StandardAgg(props) {
const { model, panel, series, fields, uiRestrictions } = props;
const handleChange = createChangeHandler(props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
let restrict = 'numeric';
if (model.type === 'cardinality') {
restrict = 'none';
}
const restrictFields = model.type === METRIC_TYPES.CARDINALITY ? [] : [ES_TYPES.NUMBER];
const indexPattern = series.override_index_pattern && series.series_index_pattern || panel.index_pattern;
const htmlId = htmlIdGenerator();
@ -81,7 +80,7 @@ function StandardAgg(props) {
<FieldSelect
fields={fields}
type={model.type}
restrict={restrict}
restrict={restrictFields}
indexPattern={indexPattern}
value={model.field}
onChange={handleSelectChange('field')}

View file

@ -35,6 +35,9 @@ import {
EuiFormRow,
} from '@elastic/eui';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { ES_TYPES } from '../../../common/es_types';
const RESTRICT_FIELDS = [ES_TYPES.NUMBER];
const StandardDeviationAggUi = props => {
const { series, panel, fields, intl } = props;
@ -108,7 +111,7 @@ const StandardDeviationAggUi = props => {
<FieldSelect
fields={fields}
type={model.type}
restrict="numeric"
restrict={RESTRICT_FIELDS}
indexPattern={indexPattern}
value={model.field}
onChange={handleSelectChange('field')}

View file

@ -21,6 +21,7 @@ import React from 'react';
import AggRow from './agg_row';
import AggSelect from './agg_select';
import FieldSelect from './field_select';
import { i18n } from '@kbn/i18n';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
@ -34,12 +35,80 @@ import {
EuiFormRow,
} from '@elastic/eui';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { ES_TYPES } from '../../../common/es_types';
import { PANEL_TYPES } from '../../../common/panel_types';
const isFieldTypeEnabled = (fieldRestrictions, fieldType) =>
fieldRestrictions.length ? fieldRestrictions.includes(fieldType) : true;
const getAggWithOptions = (field = {}, fieldTypesRestriction) => {
if (isFieldTypeEnabled(fieldTypesRestriction, field.type)) {
switch (field.type) {
case ES_TYPES.NUMBER:
return [
{
label: i18n.translate('tsvb.topHit.aggWithOptions.averageLabel', {
defaultMessage: 'Avg',
}),
value: 'avg',
},
{
label: i18n.translate('tsvb.topHit.aggWithOptions.maxLabel', {
defaultMessage: 'Max',
}),
value: 'max',
},
{
label: i18n.translate('tsvb.topHit.aggWithOptions.minLabel', {
defaultMessage: 'Min',
}),
value: 'min',
},
{
label: i18n.translate('tsvb.topHit.aggWithOptions.sumLabel', {
defaultMessage: 'Sum',
}),
value: 'sum',
},
];
case ES_TYPES.KEYWORD:
case ES_TYPES.STRING:
return [
{
label: i18n.translate('tsvb.topHit.aggWithOptions.concatenate', {
defaultMessage: 'Concatenate',
}),
value: 'concat',
},
];
}
}
return [];
};
const getOrderOptions = () => [
{
label: i18n.translate('tsvb.topHit.orderOptions.ascLabel', {
defaultMessage: 'Asc',
}),
value: 'asc',
},
{
label: i18n.translate('tsvb.topHit.orderOptions.descLabel', {
defaultMessage: 'Desc',
}),
value: 'desc',
},
];
const ORDER_DATE_RESTRICT_FIELDS = [ES_TYPES.DATE];
const TopHitAggUi = props => {
const { fields, series, panel, intl } = props;
const { fields, series, panel } = props;
const defaults = {
agg_with: 'avg',
size: 1,
agg_with: 'noop',
order: 'desc',
};
const model = { ...defaults, ...props.model };
@ -47,47 +116,30 @@ const TopHitAggUi = props => {
(series.override_index_pattern && series.series_index_pattern) ||
panel.index_pattern;
const aggWithOptionsRestrictFields = [
PANEL_TYPES.TABLE,
PANEL_TYPES.METRIC,
PANEL_TYPES.MARKDOWN
].includes(panel.type) ? [ES_TYPES.NUMBER, ES_TYPES.KEYWORD, ES_TYPES.STRING] : [ES_TYPES.NUMBER];
const handleChange = createChangeHandler(props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handleTextChange = createTextHandler(handleChange);
const aggWithOptions = [
{
label: intl.formatMessage({ id: 'tsvb.topHit.aggWithOptions.averageLabel', defaultMessage: 'Avg' }),
value: 'avg',
},
{
label: intl.formatMessage({ id: 'tsvb.topHit.aggWithOptions.maxLabel', defaultMessage: 'Max' }),
value: 'max'
},
{
label: intl.formatMessage({ id: 'tsvb.topHit.aggWithOptions.minLabel', defaultMessage: 'Min' }),
value: 'min'
},
{
label: intl.formatMessage({ id: 'tsvb.topHit.aggWithOptions.sumLabel', defaultMessage: 'Sum' }),
value: 'sum'
},
];
const orderOptions = [
{
label: intl.formatMessage({ id: 'tsvb.topHit.orderOptions.ascLabel', defaultMessage: 'Asc' }),
value: 'asc'
},
{
label: intl.formatMessage({ id: 'tsvb.topHit.orderOptions.descLabel', defaultMessage: 'Desc' }),
value: 'desc'
},
];
const field = fields[indexPattern].find(f => f.name === model.field);
const aggWithOptions = getAggWithOptions(field, aggWithOptionsRestrictFields);
const orderOptions = getOrderOptions();
const htmlId = htmlIdGenerator();
const selectedAggWithOption = aggWithOptions.find(option => {
return model.agg_with === option.value;
});
const selectedOrderOption = orderOptions.find(option => {
return model.order === option.value;
});
return (
<AggRow
disableDelete={props.disableDelete}
@ -123,7 +175,7 @@ const TopHitAggUi = props => {
<FieldSelect
fields={fields}
type={model.type}
restrict="numeric"
restrict={aggWithOptionsRestrictFields}
indexPattern={indexPattern}
value={model.field}
onChange={handleSelectChange('field')}
@ -132,7 +184,7 @@ const TopHitAggUi = props => {
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiSpacer size="m"/>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
@ -164,7 +216,9 @@ const TopHitAggUi = props => {
>
<EuiComboBox
isClearable={false}
placeholder={intl.formatMessage({ id: 'tsvb.topHit.aggregateWith.selectPlaceholder', defaultMessage: 'Select…' })}
placeholder={i18n.translate('tsvb.topHit.aggregateWith.selectPlaceholder', {
defaultMessage: 'Select...',
})}
options={aggWithOptions}
selectedOptions={selectedAggWithOption ? [selectedAggWithOption] : []}
onChange={handleSelectChange('agg_with')}
@ -181,7 +235,7 @@ const TopHitAggUi = props => {
/>)}
>
<FieldSelect
restrict="date"
restrict={ORDER_DATE_RESTRICT_FIELDS}
value={model.order_by}
onChange={handleSelectChange('order_by')}
indexPattern={indexPattern}
@ -199,7 +253,9 @@ const TopHitAggUi = props => {
>
<EuiComboBox
isClearable={false}
placeholder={intl.formatMessage({ id: 'tsvb.topHit.order.selectPlaceholder', defaultMessage: 'Select…' })}
placeholder={i18n.translate('tsvb.topHit.order.selectPlaceholder', {
defaultMessage: 'Select...',
})}
options={orderOptions}
selectedOptions={selectedOrderOption ? [selectedOrderOption] : []}
onChange={handleSelectChange('order')}

View file

@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import _ from 'lodash';
import * as collectionActions from './lib/collection_actions';
import { ES_TYPES } from '../../common/es_types';
import AddDeleteButtons from './add_delete_buttons';
import ColorPicker from './color_picker';
import FieldSelect from './aggs/field_select';
@ -55,6 +56,8 @@ function newAnnotation() {
};
}
const RESTRICT_FIELDS = [ES_TYPES.DATE];
class AnnotationsEditor extends Component {
constructor(props) {
@ -128,7 +131,7 @@ class AnnotationsEditor extends Component {
fullWidth
>
<FieldSelect
restrict="date"
restrict={RESTRICT_FIELDS}
value={model.time_field}
onChange={this.handleChange(model, 'time_field')}
indexPattern={model.index_pattern}

View file

@ -33,6 +33,9 @@ import {
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { ES_TYPES } from '../../common/es_types';
const RESTRICT_FIELDS = [ES_TYPES.DATE];
export const IndexPattern = props => {
const { fields, prefix } = props;
@ -90,7 +93,7 @@ export const IndexPattern = props => {
>
<FieldSelect
data-test-subj="metricsIndexPatternFieldsSelect"
restrict="date"
restrict={RESTRICT_FIELDS}
value={model[timeFieldName]}
disabled={props.disabled}
onChange={handleSelectChange(timeFieldName)}

View file

@ -1,62 +0,0 @@
/*
* 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 generateByTypeFilter from '../generate_by_type_filter';
import { expect } from 'chai';
describe('generateByTypeFilter()', () => {
describe('numeric', () => {
const fn = generateByTypeFilter('numeric');
[
'number'
].forEach((type) => {
it(`should return true for ${type}`, () => expect(fn({ type })).to.equal(true));
});
});
describe('string', () => {
const fn = generateByTypeFilter('string');
['string', 'keyword', 'text'].forEach((type) => {
it(`should return true for ${type}`, () => expect(fn({ type })).to.equal(true));
});
});
describe('date', () => {
const fn = generateByTypeFilter('date');
['date'].forEach((type) => {
it(`should return true for ${type}`, () => expect(fn({ type })).to.equal(true));
});
});
describe('all', () => {
const fn = generateByTypeFilter('all');
[
'number',
'string',
'text',
'keyword',
'date',
'whatever'
].forEach((type) => {
it(`should return true for ${type}`, () => expect(fn({ type })).to.equal(true));
});
});
});

View file

@ -61,11 +61,6 @@ describe('tickFormatter(format, template)', () => {
expect(fn(1500)).to.equal('1.5k/s');
});
it('returns zero if passed a string', () => {
const fn = tickFormatter();
expect(fn('100')).to.equal('0');
});
it('returns value if passed a bad formatter', () => {
const fn = tickFormatter('102');
expect(fn(100)).to.equal('100');
@ -78,6 +73,4 @@ describe('tickFormatter(format, template)', () => {
const fn = tickFormatter('number', '{{value', (key) => config[key]);
expect(fn(1.5556)).to.equal('1.56');
});
});

View file

@ -55,7 +55,7 @@ export default (format = '0,0.[00]', template, getConfig = null) => {
return (val) => {
let value;
if (!isNumber(val)) {
value = 0;
value = val;
} else {
try {
value = formatter.convert(val, 'text');

View file

@ -23,6 +23,7 @@ import { keyCodes, EuiFlexGroup, EuiFlexItem, EuiButton, EuiText, EuiSwitch } fr
import { getVisualizeLoader } from 'ui/visualize/loader/visualize_loader';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import { getInterval, convertIntervalIntoUnit, isIntervalValid, isGteInterval } from './lib/get_interval';
import { PANEL_TYPES } from '../../common/panel_types';
const MIN_CHART_HEIGHT = 250;
@ -117,11 +118,11 @@ class VisEditorVisualization extends Component {
const type = get(this.props, 'model.type', '');
return [
'metric',
'top_n',
'gauge',
'markdown',
'table',
PANEL_TYPES.METRIC,
PANEL_TYPES.TOP_N,
PANEL_TYPES.GAUGE,
PANEL_TYPES.MARKDOWN,
PANEL_TYPES.TABLE,
].includes(type);
}

View file

@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { EuiTabs, EuiTab } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
import { PANEL_TYPES } from '../../common/panel_types';
function VisPickerItem(props) {
const { label, type, selected } = props;
@ -52,12 +53,12 @@ const VisPicker = injectI18n(function (props) {
const { model, intl } = props;
const tabs = [
{ type: 'timeseries', label: intl.formatMessage({ id: 'tsvb.visPicker.timeSeriesLabel', defaultMessage: 'Time Series' }) },
{ type: 'metric', label: intl.formatMessage({ id: 'tsvb.visPicker.metricLabel', defaultMessage: 'Metric' }) },
{ type: 'top_n', label: intl.formatMessage({ id: 'tsvb.visPicker.topNLabel', defaultMessage: 'Top N' }) },
{ type: 'gauge', label: intl.formatMessage({ id: 'tsvb.visPicker.gaugeLabel', defaultMessage: 'Gauge' }) },
{ type: 'markdown', label: 'Markdown' },
{ type: 'table', label: intl.formatMessage({ id: 'tsvb.visPicker.tableLabel', defaultMessage: 'Table' }) }
{ type: PANEL_TYPES.TIMESERIES, label: intl.formatMessage({ id: 'tsvb.visPicker.timeSeriesLabel', defaultMessage: 'Time Series' }) },
{ type: PANEL_TYPES.METRIC, label: intl.formatMessage({ id: 'tsvb.visPicker.metricLabel', defaultMessage: 'Metric' }) },
{ type: PANEL_TYPES.TOP_N, label: intl.formatMessage({ id: 'tsvb.visPicker.topNLabel', defaultMessage: 'Top N' }) },
{ type: PANEL_TYPES.GAUGE, label: intl.formatMessage({ id: 'tsvb.visPicker.gaugeLabel', defaultMessage: 'Gauge' }) },
{ type: PANEL_TYPES.MARKDOWN, label: 'Markdown' },
{ type: PANEL_TYPES.TABLE, label: intl.formatMessage({ id: 'tsvb.visPicker.tableLabel', defaultMessage: 'Table' }) }
].map(item => {
return (
<VisPickerItem

View file

@ -22,6 +22,8 @@ import { ReactEditorControllerProvider } from './editor_controller';
import { VisFactoryProvider } from 'ui/vis/vis_factory';
import { defaultFeedbackMessage } from 'ui/vis/default_feedback_message';
import { PANEL_TYPES } from '../../common/panel_types';
// register the provider with the visTypes registry so that other know it exists
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
VisTypesRegistryProvider.register(MetricsVisProvider);
@ -41,7 +43,7 @@ export default function MetricsVisProvider(Private, i18n) {
visConfig: {
defaults: {
id: '61ca57f0-469d-11e7-af02-69e470af7417',
type: 'timeseries',
type: PANEL_TYPES.TIMESERIES,
series: [
{
id: '61ca57f1-469d-11e7-af02-69e470af7417',

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { get, includes, max, min, sum } from 'lodash';
import { get, includes, max, min, sum, noop } from 'lodash';
import { toPercentileNumber } from '../../../../common/to_percentile_number';
import { EXTENDED_STATS_TYPES, METRIC_TYPES } from '../../../../common/metric_types';
@ -25,6 +25,8 @@ const aggFns = {
max,
min,
sum,
noop,
concat: values => values.join(', '),
avg: values => sum(values) / values.length,
};
@ -53,12 +55,14 @@ export default (row, metric) => {
row[metric.id].values[percentileRankKey]
);
case METRIC_TYPES.TOP_HIT:
if (row[metric.id].doc_count === 0) return null;
if (row[metric.id].doc_count === 0) {
return null;
}
const hits = get(row, [metric.id, 'docs', 'hits', 'hits'], []);
const values = hits.map(doc => {
return get(doc, `_source.${metric.field}`, 0);
});
const aggWith = (metric.agg_with && aggFns[metric.agg_with]) || aggFns.avg;
const values = hits.map(doc => get(doc, `_source.${metric.field}`));
const aggWith = (metric.agg_with && aggFns[metric.agg_with]) || aggFns.noop;
return aggWith(values);
case METRIC_TYPES.COUNT:
return get(row, 'doc_count', null);