[TSVB] Top_hit supports runtime fields (#103401)

* [TSVB] Refactor top-hit aggregation to work with fields instead of _source

* Allow select date strings for top_hit aggregation in table, metric, and markdown

* Fix agg_with handling for top_hit and add some tests

* Refactor get_agg_value and fix type check for _tsvb_chart

* Refactor top_hit.js

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Diana Derevyankina 2021-07-12 17:25:52 +03:00 committed by GitHub
parent 832d349930
commit 87066e06b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 168 additions and 36 deletions

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React from 'react';
import React, { useMemo, useEffect } from 'react';
import { AggRow } from './agg_row';
import { AggSelect } from './agg_select';
import { FieldSelect } from './field_select';
@ -62,6 +62,7 @@ const getAggWithOptions = (field = {}, fieldTypesRestriction) => {
},
];
case KBN_FIELD_TYPES.STRING:
case KBN_FIELD_TYPES.DATE:
return [
{
label: i18n.translate('visTypeTimeseries.topHit.aggWithOptions.concatenate', {
@ -91,16 +92,18 @@ const getOrderOptions = () => [
},
];
const AGG_WITH_KEY = 'agg_with';
const ORDER_DATE_RESTRICT_FIELDS = [KBN_FIELD_TYPES.DATE];
const getModelDefaults = () => ({
size: 1,
order: 'desc',
[AGG_WITH_KEY]: 'noop',
});
const TopHitAggUi = (props) => {
const { fields, series, panel } = props;
const defaults = {
size: 1,
agg_with: 'noop',
order: 'desc',
};
const model = { ...defaults, ...props.model };
const model = useMemo(() => ({ ...getModelDefaults(), ...props.model }), [props.model]);
const indexPattern = series.override_index_pattern
? series.series_index_pattern
: panel.index_pattern;
@ -110,7 +113,7 @@ const TopHitAggUi = (props) => {
PANEL_TYPES.METRIC,
PANEL_TYPES.MARKDOWN,
].includes(panel.type)
? [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.STRING]
? [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.STRING, KBN_FIELD_TYPES.DATE]
: [KBN_FIELD_TYPES.NUMBER];
const handleChange = createChangeHandler(props.onChange, model);
@ -124,13 +127,23 @@ const TopHitAggUi = (props) => {
const htmlId = htmlIdGenerator();
const selectedAggWithOption = aggWithOptions.find((option) => {
return model.agg_with === option.value;
return model[AGG_WITH_KEY] === option.value;
});
const selectedOrderOption = orderOptions.find((option) => {
return model.order === option.value;
});
useEffect(() => {
const defaultFn = aggWithOptions?.[0]?.value;
const aggWith = model[AGG_WITH_KEY];
if (aggWith && defaultFn && aggWith !== defaultFn && !selectedAggWithOption) {
handleChange({
[AGG_WITH_KEY]: defaultFn,
});
}
}, [model, selectedAggWithOption, aggWithOptions, handleChange]);
return (
<AggRow
disableDelete={props.disableDelete}
@ -195,7 +208,7 @@ const TopHitAggUi = (props) => {
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
id={htmlId('agg_with')}
id={htmlId(AGG_WITH_KEY)}
label={
<FormattedMessage
id="visTypeTimeseries.topHit.aggregateWithLabel"
@ -213,8 +226,9 @@ const TopHitAggUi = (props) => {
)}
options={aggWithOptions}
selectedOptions={selectedAggWithOption ? [selectedAggWithOption] : []}
onChange={handleSelectChange('agg_with')}
onChange={handleSelectChange(AGG_WITH_KEY)}
singleSelection={{ asPlainText: true }}
data-test-subj="topHitAggregateWithComboBox"
/>
</EuiFormRow>
</EuiFlexItem>
@ -231,6 +245,7 @@ const TopHitAggUi = (props) => {
onChange={handleSelectChange('order_by')}
indexPattern={indexPattern}
fields={fields}
data-test-subj="topHitOrderByFieldSelect"
/>
</EuiFlexItem>
<EuiFlexItem>

View file

@ -16,7 +16,7 @@ export const createTickFormatter = (format = '0,0.[00]', template, getConfig = n
const fieldFormats = getFieldFormats();
if (!template) template = '{{value}}';
const render = handlebars.compile(template, { knownHelpersOnly: true });
const render = handlebars.compile(template, { noEscape: true, knownHelpersOnly: true });
let formatter;
if (isDuration(format)) {

View file

@ -111,7 +111,7 @@ export const bucketTransform = {
docs: {
top_hits: {
size: bucket.size,
_source: { includes: [bucket.field] },
fields: [bucket.field],
},
},
},

View file

@ -45,10 +45,10 @@ export const getAggValue = (row, metric) => {
}
const hits = get(row, [metric.id, 'docs', 'hits', 'hits'], []);
const values = hits.map((doc) => get(doc, `_source.${metric.field}`));
const values = hits.map((doc) => doc.fields[metric.field]);
const aggWith = (metric.agg_with && aggFns[metric.agg_with]) || aggFns.noop;
return aggWith(values);
return aggWith(values.flat());
case METRIC_TYPES.COUNT:
return get(row, 'doc_count', null);
default:

View file

@ -67,11 +67,7 @@ describe('getAggValue', () => {
doc_count: 1,
docs: {
hits: {
hits: [
{ _source: { example: { value: 25 } } },
{ _source: { example: { value: 25 } } },
{ _source: { example: { value: 25 } } },
],
hits: [{ fields: { 'example.value': [25, 25, 25] } }],
},
},
},

View file

@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'timePicker',
'visChart',
'common',
'settings',
]);
describe('visual builder', function describeIndexTests() {
@ -44,14 +45,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
describe('metric', () => {
const { visualBuilder } = PageObjects;
beforeEach(async () => {
await PageObjects.visualBuilder.resetPage();
await PageObjects.visualBuilder.clickMetric();
await PageObjects.visualBuilder.checkMetricTabIsPresent();
await PageObjects.visualBuilder.clickPanelOptions('metric');
await PageObjects.visualBuilder.setMetricsDataTimerangeMode('Last value');
await PageObjects.visualBuilder.setDropLastBucket(true);
await PageObjects.visualBuilder.clickDataTab('metric');
await visualBuilder.resetPage();
await visualBuilder.clickMetric();
await visualBuilder.checkMetricTabIsPresent();
await visualBuilder.clickPanelOptions('metric');
await visualBuilder.setMetricsDataTimerangeMode('Last value');
await visualBuilder.setDropLastBucket(true);
await visualBuilder.clickDataTab('metric');
});
it('should not have inspector enabled', async () => {
@ -59,28 +62,98 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should show correct data', async () => {
const value = await PageObjects.visualBuilder.getMetricValue();
const value = await visualBuilder.getMetricValue();
expect(value).to.eql('156');
});
it('should show correct data with Math Aggregation', async () => {
await PageObjects.visualBuilder.createNewAgg();
await PageObjects.visualBuilder.selectAggType('math', 1);
await PageObjects.visualBuilder.fillInVariable();
await PageObjects.visualBuilder.fillInExpression('params.test + 1');
const value = await PageObjects.visualBuilder.getMetricValue();
await visualBuilder.createNewAgg();
await visualBuilder.selectAggType('math', 1);
await visualBuilder.fillInVariable();
await visualBuilder.fillInExpression('params.test + 1');
const value = await visualBuilder.getMetricValue();
expect(value).to.eql('157');
});
it('should populate fields for basic functions', async () => {
const { visualBuilder } = PageObjects;
await visualBuilder.selectAggType('Average');
await visualBuilder.setFieldForAggregation('machine.ram');
const isFieldForAggregationValid = await visualBuilder.checkFieldForAggregationValidity();
expect(isFieldForAggregationValid).to.be(true);
});
it('should show correct data for Value Count with Entire time range mode', async () => {
await visualBuilder.selectAggType('Value Count');
await visualBuilder.setFieldForAggregation('machine.ram');
await visualBuilder.clickPanelOptions('metric');
await visualBuilder.setMetricsDataTimerangeMode('Entire time range');
const value = await visualBuilder.getMetricValue();
expect(value).to.eql('13,492');
});
it('should show same data for kibana and string index pattern modes', async () => {
await visualBuilder.selectAggType('Max');
await visualBuilder.setFieldForAggregation('machine.ram');
const kibanaIndexPatternModeValue = await visualBuilder.getMetricValue();
await visualBuilder.clickPanelOptions('metric');
await visualBuilder.switchIndexPatternSelectionMode(false);
const stringIndexPatternModeValue = await visualBuilder.getMetricValue();
expect(kibanaIndexPatternModeValue).to.eql(stringIndexPatternModeValue);
expect(kibanaIndexPatternModeValue).to.eql('32,212,254,720');
});
describe('Color rules', () => {
beforeEach(async () => {
await visualBuilder.selectAggType('Min');
await visualBuilder.setFieldForAggregation('machine.ram');
await visualBuilder.clickPanelOptions('metric');
await visualBuilder.setColorRuleOperator('>= greater than or equal');
await visualBuilder.setColorRuleValue(0);
});
it('should apply color rules to visualization background', async () => {
await visualBuilder.setColorPickerValue('#FFCFDF');
const backGroundStyle = await visualBuilder.getBackgroundStyle();
expect(backGroundStyle).to.eql('background-color: rgb(255, 207, 223);');
});
it('should apply color rules to metric value', async () => {
await visualBuilder.setColorPickerValue('#AD7DE6', 1);
const backGroundStyle = await visualBuilder.getMetricValueStyle();
expect(backGroundStyle).to.eql('color: rgb(173, 125, 230);');
});
});
describe('Top Hit aggregation', () => {
beforeEach(async () => {
await visualBuilder.selectAggType('Top Hit');
await visualBuilder.setTopHitOrderByField('@timestamp');
});
it('should show correct data for string type field', async () => {
await visualBuilder.setFieldForAggregation('machine.os.raw');
await visualBuilder.setTopHitAggregateWithOption('Concatenate');
const value = await visualBuilder.getMetricValue();
expect(value).to.eql('win 7');
});
it('should show correct data for runtime field', async () => {
await visualBuilder.setFieldForAggregation('hello_world_runtime_field');
await visualBuilder.setTopHitAggregateWithOption('Concatenate');
const value = await visualBuilder.getMetricValue();
expect(value).to.eql('hello world');
});
});
});
describe('gauge', () => {

File diff suppressed because one or more lines are too long

View file

@ -575,6 +575,42 @@ export class VisualBuilderPageObject extends FtrService {
await this.testSubjects.existOrFail('euiColorPickerPopover', { timeout: 5000 });
}
public async setColorPickerValue(colorHex: string, nth: number = 0): Promise<void> {
const picker = await this.find.allByCssSelector('.tvbColorPicker button');
await picker[nth].clickMouseButton();
await this.checkColorPickerPopUpIsPresent();
await this.find.setValue('.euiColorPicker input', colorHex);
await this.visChart.waitForVisualizationRenderingStabilized();
}
public async setColorRuleOperator(condition: string): Promise<void> {
await this.retry.try(async () => {
await this.comboBox.clearInputField('colorRuleOperator');
await this.comboBox.set('colorRuleOperator', condition);
});
}
public async setColorRuleValue(value: number): Promise<void> {
await this.retry.try(async () => {
const colorRuleValueInput = await this.find.byCssSelector(
'[data-test-subj="colorRuleValue"]'
);
await colorRuleValueInput.type(value.toString());
});
}
public async getBackgroundStyle(): Promise<string> {
await this.visChart.waitForVisualizationRenderingStabilized();
const visualization = await this.find.byClassName('tvbVis');
return await visualization.getAttribute('style');
}
public async getMetricValueStyle(): Promise<string> {
await this.visChart.waitForVisualizationRenderingStabilized();
const metricValue = await this.find.byCssSelector('[data-test-subj="tsvbMetricValue"]');
return await metricValue.getAttribute('style');
}
public async changePanelPreview(nth: number = 0): Promise<void> {
const prevRenderingCount = await this.visChart.getVisualizationRenderingCount();
const changePreviewBtnArray = await this.testSubjects.findAll('AddActivatePanelBtn');
@ -680,4 +716,15 @@ export class VisualBuilderPageObject extends FtrService {
const dataTimeRangeMode = await this.testSubjects.find('dataTimeRangeMode');
return await this.comboBox.isOptionSelected(dataTimeRangeMode, value);
}
public async setTopHitAggregateWithOption(option: string): Promise<void> {
await this.comboBox.set('topHitAggregateWithComboBox', option);
}
public async setTopHitOrderByField(timeField: string) {
await this.retry.try(async () => {
await this.comboBox.clearInputField('topHitOrderByFieldSelect');
await this.comboBox.set('topHitOrderByFieldSelect', timeField);
});
}
}