[TSVB] Allow custom label for fields via index pattern field management (#84612)

* [TSVB] Allow custom label for fields via index pattern field management

Closes: #84336

* replace saveObject, elasticsearch client to new one

* fix CI

* update schema

* fix Top Hit

* some changes

* partially move getting fields into client side

* fix PR comments

* fix issue with getting fields

* move SanitizedFieldType to common types

* fix issue on changing index pattern

* fix issue

* fix regression

* some work

* remove extractFieldName, createCustomLabelSelectHandler

* request/response processors should be async

* some work

* remove tests for createCustomLabelSelectHandler

* fix table

* fix placeholder

* some work

* fix jest

* fix CI

* fix label for table view

* test: visualize app visual builder switch index patterns should be able to switch between index patterns

* fix functional tests

* fix sorting

* fix labels for entire timerange mode

* add createFieldsFetcher method

* table view - fix pivot label

* fix PR comments

* fix issue with selecting buckets scripts

* fix types

* Update create_select_handler.test.ts

* fix PR comments

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Alexey Antonov 2021-01-12 19:28:45 +03:00 committed by GitHub
parent ef441cca24
commit 0b7e83f736
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
108 changed files with 980 additions and 699 deletions

View file

@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`src/legacy/core_plugins/metrics/common/model_options.js MODEL_TYPES should match a snapshot of constants 1`] = `
Object {
"UNWEIGHTED": "simple",
"WEIGHTED_EXPONENTIAL": "ewma",
"WEIGHTED_EXPONENTIAL_DOUBLE": "holt",
"WEIGHTED_EXPONENTIAL_TRIPLE": "holt_winters",
"WEIGHTED_LINEAR": "linear",
}
`;

View file

@ -18,14 +18,15 @@
*/
import { isBasicAgg } from './agg_lookup';
import { MetricsItemsSchema } from './types';
describe('aggLookup', () => {
describe('isBasicAgg(metric)', () => {
test('returns true for a basic metric (count)', () => {
expect(isBasicAgg({ type: 'count' })).toEqual(true);
expect(isBasicAgg({ type: 'count' } as MetricsItemsSchema)).toEqual(true);
});
test('returns false for a pipeline metric (derivative)', () => {
expect(isBasicAgg({ type: 'derivative' })).toEqual(false);
expect(isBasicAgg({ type: 'derivative' } as MetricsItemsSchema)).toEqual(false);
});
});
});

View file

@ -17,10 +17,11 @@
* under the License.
*/
import _ from 'lodash';
import { omit, pick, includes } from 'lodash';
import { i18n } from '@kbn/i18n';
import { MetricsItemsSchema } from './types';
export const lookup = {
export const lookup: Record<string, string> = {
count: i18n.translate('visTypeTimeseries.aggLookup.countLabel', { defaultMessage: 'Count' }),
calculation: i18n.translate('visTypeTimeseries.aggLookup.calculationLabel', {
defaultMessage: 'Calculation',
@ -122,11 +123,11 @@ const pipeline = [
const byType = {
_all: lookup,
pipeline: pipeline,
basic: _.omit(lookup, pipeline),
metrics: _.pick(lookup, ['count', 'avg', 'min', 'max', 'sum', 'cardinality', 'value_count']),
pipeline,
basic: omit(lookup, pipeline),
metrics: pick(lookup, ['count', 'avg', 'min', 'max', 'sum', 'cardinality', 'value_count']),
};
export function isBasicAgg(item) {
return _.includes(Object.keys(byType.basic), item.type);
export function isBasicAgg(item: MetricsItemsSchema) {
return includes(Object.keys(byType.basic), item.type);
}

View file

@ -18,66 +18,79 @@
*/
import { calculateLabel } from './calculate_label';
import type { MetricsItemsSchema } from './types';
describe('calculateLabel(metric, metrics)', () => {
test('returns "Unknown" for empty metric', () => {
expect(calculateLabel()).toEqual('Unknown');
});
test('returns the metric.alias if set', () => {
expect(calculateLabel({ alias: 'Example' })).toEqual('Example');
expect(calculateLabel({ alias: 'Example' } as MetricsItemsSchema)).toEqual('Example');
});
test('returns "Count" for a count metric', () => {
expect(calculateLabel({ type: 'count' })).toEqual('Count');
expect(calculateLabel({ type: 'count' } as MetricsItemsSchema)).toEqual('Count');
});
test('returns "Calculation" for a bucket script metric', () => {
expect(calculateLabel({ type: 'calculation' })).toEqual('Bucket Script');
expect(calculateLabel({ type: 'calculation' } as MetricsItemsSchema)).toEqual('Bucket Script');
});
test('returns formated label for series_agg', () => {
const label = calculateLabel({ type: 'series_agg', function: 'max' });
test('returns formatted label for series_agg', () => {
const label = calculateLabel({ type: 'series_agg', function: 'max' } as MetricsItemsSchema);
expect(label).toEqual('Series Agg (max)');
});
test('returns formated label for basic aggs', () => {
const label = calculateLabel({ type: 'avg', field: 'memory' });
test('returns formatted label for basic aggs', () => {
const label = calculateLabel({ type: 'avg', field: 'memory' } as MetricsItemsSchema);
expect(label).toEqual('Average of memory');
});
test('returns formated label for pipeline aggs', () => {
const metric = { id: 2, type: 'derivative', field: 1 };
const metrics = [{ id: 1, type: 'max', field: 'network.out.bytes' }, metric];
test('returns formatted label for pipeline aggs', () => {
const metric = ({ id: 2, type: 'derivative', field: 1 } as unknown) as MetricsItemsSchema;
const metrics = ([
{ id: 1, type: 'max', field: 'network.out.bytes' },
metric,
] as unknown) as MetricsItemsSchema[];
const label = calculateLabel(metric, metrics);
expect(label).toEqual('Derivative of Max of network.out.bytes');
});
test('returns formated label for derivative of percentile', () => {
const metric = { id: 2, type: 'derivative', field: '1[50.0]' };
const metrics = [{ id: 1, type: 'percentile', field: 'network.out.bytes' }, metric];
test('returns formatted label for derivative of percentile', () => {
const metric = ({
id: 2,
type: 'derivative',
field: '1[50.0]',
} as unknown) as MetricsItemsSchema;
const metrics = ([
{ id: 1, type: 'percentile', field: 'network.out.bytes' },
metric,
] as unknown) as MetricsItemsSchema[];
const label = calculateLabel(metric, metrics);
expect(label).toEqual('Derivative of Percentile of network.out.bytes (50.0)');
});
test('returns formated label for pipeline aggs (deep)', () => {
const metric = { id: 3, type: 'derivative', field: 2 };
const metrics = [
test('returns formatted label for pipeline aggs (deep)', () => {
const metric = ({ id: 3, type: 'derivative', field: 2 } as unknown) as MetricsItemsSchema;
const metrics = ([
{ id: 1, type: 'max', field: 'network.out.bytes' },
{ id: 2, type: 'moving_average', field: 1 },
metric,
];
] as unknown) as MetricsItemsSchema[];
const label = calculateLabel(metric, metrics);
expect(label).toEqual('Derivative of Moving Average of Max of network.out.bytes');
});
test('returns formated label for pipeline aggs uses alias for field metric', () => {
const metric = { id: 2, type: 'derivative', field: 1 };
const metrics = [
test('returns formatted label for pipeline aggs uses alias for field metric', () => {
const metric = ({ id: 2, type: 'derivative', field: 1 } as unknown) as MetricsItemsSchema;
const metrics = ([
{ id: 1, type: 'max', field: 'network.out.bytes', alias: 'Outbound Traffic' },
metric,
];
] as unknown) as MetricsItemsSchema[];
const label = calculateLabel(metric, metrics);
expect(label).toEqual('Derivative of Outbound Traffic');
});
});

View file

@ -18,8 +18,9 @@
*/
import { includes, startsWith } from 'lodash';
import { lookup } from './agg_lookup';
import { i18n } from '@kbn/i18n';
import { lookup } from './agg_lookup';
import { MetricsItemsSchema, SanitizedFieldType } from './types';
const paths = [
'cumulative_sum',
@ -36,7 +37,15 @@ const paths = [
'positive_only',
];
export function calculateLabel(metric, metrics) {
export const extractFieldLabel = (fields: SanitizedFieldType[], name: string) => {
return fields.find((f) => f.name === name)?.label ?? name;
};
export const calculateLabel = (
metric: MetricsItemsSchema,
metrics: MetricsItemsSchema[] = [],
fields: SanitizedFieldType[] = []
): string => {
if (!metric) {
return i18n.translate('visTypeTimeseries.calculateLabel.unknownLabel', {
defaultMessage: 'Unknown',
@ -73,7 +82,7 @@ export function calculateLabel(metric, metrics) {
if (metric.type === 'positive_rate') {
return i18n.translate('visTypeTimeseries.calculateLabel.positiveRateLabel', {
defaultMessage: 'Counter Rate of {field}',
values: { field: metric.field },
values: { field: extractFieldLabel(fields, metric.field!) },
});
}
if (metric.type === 'static') {
@ -84,15 +93,15 @@ export function calculateLabel(metric, metrics) {
}
if (includes(paths, metric.type)) {
const targetMetric = metrics.find((m) => startsWith(metric.field, m.id));
const targetLabel = calculateLabel(targetMetric, metrics);
const targetMetric = metrics.find((m) => startsWith(metric.field!, m.id));
const targetLabel = calculateLabel(targetMetric!, metrics, fields);
// 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.
if (targetMetric && targetMetric.type === 'percentile') {
const percentileValueMatch = /\[([0-9\.]+)\]$/;
const matches = metric.field.match(percentileValueMatch);
const matches = metric.field!.match(percentileValueMatch);
if (matches) {
return i18n.translate(
'visTypeTimeseries.calculateLabel.lookupMetricTypeOfTargetWithAdditionalLabel',
@ -115,6 +124,9 @@ export function calculateLabel(metric, metrics) {
return i18n.translate('visTypeTimeseries.calculateLabel.lookupMetricTypeOfMetricFieldRankLabel', {
defaultMessage: '{lookupMetricType} of {metricField}',
values: { lookupMetricType: lookup[metric.type], metricField: metric.field },
values: {
lookupMetricType: lookup[metric.type],
metricField: extractFieldLabel(fields, metric.field!),
},
});
}
};

View file

@ -18,16 +18,13 @@
*/
import { extractIndexPatterns } from './extract_index_patterns';
import { PanelSchema } from './types';
describe('extractIndexPatterns(vis)', () => {
let visParams;
let visFields;
let panel: PanelSchema;
beforeEach(() => {
visFields = {
'*': [],
};
visParams = {
panel = {
index_pattern: '*',
series: [
{
@ -40,25 +37,10 @@ describe('extractIndexPatterns(vis)', () => {
},
],
annotations: [{ index_pattern: 'notes-*' }, { index_pattern: 'example-1-*' }],
};
} as PanelSchema;
});
test('should return index patterns', () => {
visFields = {};
expect(extractIndexPatterns(visParams, visFields)).toEqual([
'*',
'example-1-*',
'example-2-*',
'notes-*',
]);
});
test('should return index patterns that do not exist in visFields', () => {
expect(extractIndexPatterns(visParams, visFields)).toEqual([
'example-1-*',
'example-2-*',
'notes-*',
]);
expect(extractIndexPatterns(panel, '')).toEqual(['*', 'example-1-*', 'example-2-*', 'notes-*']);
});
});

View file

@ -17,17 +17,21 @@
* under the License.
*/
import { uniq } from 'lodash';
import { PanelSchema } from '../common/types';
export function extractIndexPatterns(panel, excludedFields = {}) {
const patterns = [];
export function extractIndexPatterns(
panel: PanelSchema,
defaultIndex?: PanelSchema['default_index_pattern']
) {
const patterns: string[] = [];
if (!excludedFields[panel.index_pattern]) {
if (panel.index_pattern) {
patterns.push(panel.index_pattern);
}
panel.series.forEach((series) => {
const indexPattern = series.series_index_pattern;
if (indexPattern && series.override_index_pattern && !excludedFields[indexPattern]) {
if (indexPattern && series.override_index_pattern) {
patterns.push(indexPattern);
}
});
@ -35,15 +39,15 @@ export function extractIndexPatterns(panel, excludedFields = {}) {
if (panel.annotations) {
panel.annotations.forEach((item) => {
const indexPattern = item.index_pattern;
if (indexPattern && !excludedFields[indexPattern]) {
if (indexPattern) {
patterns.push(indexPattern);
}
});
}
if (patterns.length === 0) {
patterns.push('');
if (patterns.length === 0 && defaultIndex) {
patterns.push(defaultIndex);
}
return uniq(patterns).sort();
return uniq<string>(patterns).sort();
}

View file

@ -17,10 +17,10 @@
* under the License.
*/
export const FIELD_TYPES = {
BOOLEAN: 'boolean',
DATE: 'date',
GEO: 'geo_point',
NUMBER: 'number',
STRING: 'string',
};
export enum FIELD_TYPES {
BOOLEAN = 'boolean',
DATE = 'date',
GEO = 'geo_point',
NUMBER = 'number',
STRING = 'string',
}

View file

@ -17,10 +17,10 @@
* under the License.
*/
export const MODEL_TYPES = {
UNWEIGHTED: 'simple',
WEIGHTED_EXPONENTIAL: 'ewma',
WEIGHTED_EXPONENTIAL_DOUBLE: 'holt',
WEIGHTED_EXPONENTIAL_TRIPLE: 'holt_winters',
WEIGHTED_LINEAR: 'linear',
};
export enum MODEL_TYPES {
UNWEIGHTED = 'simple',
WEIGHTED_EXPONENTIAL = 'ewma',
WEIGHTED_EXPONENTIAL_DOUBLE = 'holt',
WEIGHTED_EXPONENTIAL_TRIPLE = 'holt_winters',
WEIGHTED_LINEAR = 'linear',
}

View file

@ -22,19 +22,19 @@
* @constant
* @public
*/
export const TIME_RANGE_DATA_MODES = {
export enum TIME_RANGE_DATA_MODES {
/**
* Entire timerange mode will match all the documents selected in the
* timerange timepicker
*/
ENTIRE_TIME_RANGE: 'entire_time_range',
ENTIRE_TIME_RANGE = 'entire_time_range',
/**
* Last value mode will match only the documents for the specified interval
* from the end of the timerange.
*/
LAST_VALUE: 'last_value',
};
LAST_VALUE = 'last_value',
}
/**
* Key for getting the Time Range mode from the Panel configuration object.

View file

@ -18,5 +18,5 @@
*/
const percentileNumberTest = /\d+\.\d+/;
export const toPercentileNumber = (value) =>
export const toPercentileNumber = (value: string) =>
percentileNumberTest.test(`${value}`) ? value : `${value}.0`;

View file

@ -18,7 +18,7 @@
*/
import { TypeOf } from '@kbn/config-schema';
import { metricsItems, panel, seriesItems, visPayloadSchema } from './vis_schema';
import { metricsItems, panel, seriesItems, visPayloadSchema, fieldObject } from './vis_schema';
import { PANEL_TYPES } from './panel_types';
import { TimeseriesUIRestrictions } from './ui_restrictions';
@ -26,6 +26,7 @@ export type SeriesItemsSchema = TypeOf<typeof seriesItems>;
export type MetricsItemsSchema = TypeOf<typeof metricsItems>;
export type PanelSchema = TypeOf<typeof panel>;
export type VisPayload = TypeOf<typeof visPayloadSchema>;
export type FieldObject = TypeOf<typeof fieldObject>;
interface PanelData {
id: string;
@ -53,3 +54,9 @@ export type TimeseriesVisData = SeriesData & {
*/
series?: unknown[];
};
export interface SanitizedFieldType {
name: string;
type: string;
label?: string;
}

View file

@ -47,6 +47,8 @@ const numberOptionalOrEmptyString = schema.maybe(
schema.oneOf([numberOptional, schema.literal('')])
);
export const fieldObject = stringOptionalNullable;
const annotationsItems = schema.object({
color: stringOptionalNullable,
fields: stringOptionalNullable,
@ -58,7 +60,7 @@ const annotationsItems = schema.object({
index_pattern: stringOptionalNullable,
query_string: schema.maybe(queryObject),
template: stringOptionalNullable,
time_field: stringOptionalNullable,
time_field: fieldObject,
});
const backgroundColorRulesItems = schema.object({
@ -77,8 +79,9 @@ const gaugeColorRulesItems = schema.object({
value: schema.maybe(schema.nullable(schema.number())),
});
export const metricsItems = schema.object({
field: stringOptionalNullable,
field: fieldObject,
id: stringRequired,
alias: stringOptionalNullable,
metric_agg: stringOptionalNullable,
numerator: schema.maybe(queryObject),
denominator: schema.maybe(queryObject),
@ -98,7 +101,7 @@ export const metricsItems = schema.object({
variables: schema.maybe(
schema.arrayOf(
schema.object({
field: stringOptionalNullable,
field: fieldObject,
id: stringRequired,
name: stringOptionalNullable,
})
@ -109,7 +112,7 @@ export const metricsItems = schema.object({
schema.arrayOf(
schema.object({
id: stringRequired,
field: stringOptionalNullable,
field: fieldObject,
mode: schema.oneOf([schema.literal('line'), schema.literal('band')]),
shade: schema.oneOf([numberOptional, stringOptionalNullable]),
value: schema.maybe(schema.oneOf([numberOptional, stringOptionalNullable])),
@ -123,7 +126,7 @@ export const metricsItems = schema.object({
size: stringOrNumberOptionalNullable,
agg_with: stringOptionalNullable,
order: stringOptionalNullable,
order_by: stringOptionalNullable,
order_by: fieldObject,
});
const splitFiltersItems = schema.object({
@ -134,7 +137,7 @@ const splitFiltersItems = schema.object({
});
export const seriesItems = schema.object({
aggregate_by: stringOptionalNullable,
aggregate_by: fieldObject,
aggregate_function: stringOptionalNullable,
axis_position: stringRequired,
axis_max: stringOrNumberOptionalNullable,
@ -176,7 +179,7 @@ export const seriesItems = schema.object({
seperate_axis: numberIntegerOptional,
series_index_pattern: stringOptionalNullable,
series_max_bars: numberIntegerOptional,
series_time_field: stringOptionalNullable,
series_time_field: fieldObject,
series_interval: stringOptionalNullable,
series_drop_last_bucket: numberIntegerOptional,
split_color_mode: stringOptionalNullable,
@ -184,7 +187,7 @@ export const seriesItems = schema.object({
split_mode: stringRequired,
stacked: stringRequired,
steps: numberIntegerOptional,
terms_field: stringOptionalNullable,
terms_field: fieldObject,
terms_order_by: stringOptionalNullable,
terms_size: stringOptionalNullable,
terms_direction: stringOptionalNullable,
@ -241,7 +244,7 @@ export const panel = schema.object({
markdown_vertical_align: stringOptionalNullable,
markdown_less: stringOptionalNullable,
markdown_css: stringOptionalNullable,
pivot_id: stringOptionalNullable,
pivot_id: fieldObject,
pivot_label: stringOptionalNullable,
pivot_type: stringOptionalNullable,
pivot_rows: stringOptionalNullable,
@ -251,7 +254,7 @@ export const panel = schema.object({
tooltip_mode: schema.maybe(
schema.oneOf([schema.literal('show_all'), schema.literal('show_focused')])
),
time_field: stringOptionalNullable,
time_field: fieldObject,
time_range_mode: stringOptionalNullable,
type: schema.oneOf([
schema.literal('table'),

View file

@ -59,6 +59,10 @@ export function Agg(props: AggProps) {
...props.style,
};
const indexPattern =
(props.series.override_index_pattern && props.series.series_index_pattern) ||
props.panel.index_pattern;
return (
<div className={props.className} style={style}>
<Component
@ -71,6 +75,7 @@ export function Agg(props: AggProps) {
panel={props.panel}
series={props.series}
siblings={props.siblings}
indexPattern={indexPattern}
uiRestrictions={props.uiRestrictions}
dragHandleProps={props.dragHandleProps}
/>

View file

@ -44,7 +44,7 @@ const checkModel = (model) => Array.isArray(model.variables) && model.script !==
export function CalculationAgg(props) {
const htmlId = htmlIdGenerator();
const { siblings, model } = props;
const { siblings, model, indexPattern, fields } = props;
const handleChange = createChangeHandler(props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
@ -97,6 +97,8 @@ export function CalculationAgg(props) {
<CalculationVars
id={htmlId('variables')}
metrics={siblings}
indexPattern={indexPattern}
fields={fields}
onChange={handleChange}
name="variables"
model={model}
@ -140,6 +142,7 @@ export function CalculationAgg(props) {
CalculationAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
indexPattern: PropTypes.string,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,

View file

@ -36,10 +36,11 @@ import {
} from '@elastic/eui';
export function CumulativeSumAgg(props) {
const { model, siblings } = props;
const { model, siblings, fields, indexPattern } = props;
const htmlId = htmlIdGenerator();
const handleChange = createChangeHandler(props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
return (
<AggRow
disableDelete={props.disableDelete}
@ -80,6 +81,7 @@ export function CumulativeSumAgg(props) {
onChange={handleSelectChange('field')}
metrics={siblings}
metric={model}
fields={fields[indexPattern]}
value={model.field}
exclude={[METRIC_TYPES.TOP_HIT]}
/>
@ -93,6 +95,7 @@ export function CumulativeSumAgg(props) {
CumulativeSumAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
indexPattern: PropTypes.string,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,

View file

@ -38,7 +38,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
export const DerivativeAgg = (props) => {
const { siblings } = props;
const { siblings, fields, indexPattern } = props;
const defaults = { unit: '' };
const model = { ...defaults, ...props.model };
@ -91,6 +91,7 @@ export const DerivativeAgg = (props) => {
onChange={handleSelectChange('field')}
metrics={siblings}
metric={model}
fields={fields[indexPattern]}
value={model.field}
exclude={[METRIC_TYPES.TOP_HIT]}
fullWidth
@ -120,6 +121,7 @@ export const DerivativeAgg = (props) => {
DerivativeAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
indexPattern: PropTypes.string,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,

View file

@ -1,115 +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 PropTypes from 'prop-types';
import React from 'react';
import { EuiComboBox } from '@elastic/eui';
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,
fields,
indexPattern,
value,
onChange,
disabled,
restrict,
placeholder,
uiRestrictions,
...rest
}) {
if (type === 'count') {
return null;
}
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,
};
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={placeholder}
isDisabled={disabled}
options={options}
selectedOptions={selectedOptions}
onChange={onChange}
singleSelection={{ asPlainText: true }}
{...rest}
/>
);
}
FieldSelectUi.defaultProps = {
indexPattern: '',
disabled: false,
restrict: [],
placeholder: i18n.translate('visTypeTimeseries.fieldSelect.selectFieldPlaceholder', {
defaultMessage: 'Select field...',
}),
};
FieldSelectUi.propTypes = {
disabled: PropTypes.bool,
fields: PropTypes.object,
id: PropTypes.string,
indexPattern: PropTypes.string,
onChange: PropTypes.func,
restrict: PropTypes.array,
type: PropTypes.string,
value: PropTypes.string,
uiRestrictions: PropTypes.object,
placeholder: PropTypes.string,
};
export const FieldSelect = injectI18n(FieldSelectUi);

View file

@ -0,0 +1,139 @@
/*
* 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 React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiComboBox, EuiComboBoxProps, EuiComboBoxOptionOption } from '@elastic/eui';
import { METRIC_TYPES } from '../../../../common/metric_types';
import type { SanitizedFieldType } from '../../../../common/types';
import type { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions';
// @ts-ignore
import { isFieldEnabled } from '../../lib/check_ui_restrictions';
interface FieldSelectProps {
type: string;
fields: Record<string, SanitizedFieldType[]>;
indexPattern: string;
value: string;
onChange: (options: Array<EuiComboBoxOptionOption<string>>) => void;
disabled?: boolean;
restrict?: string[];
placeholder?: string;
uiRestrictions?: TimeseriesUIRestrictions;
'data-test-subj'?: string;
}
const defaultPlaceholder = i18n.translate('visTypeTimeseries.fieldSelect.selectFieldPlaceholder', {
defaultMessage: 'Select field...',
});
const isFieldTypeEnabled = (fieldRestrictions: string[], fieldType: string) =>
fieldRestrictions.length ? fieldRestrictions.includes(fieldType) : true;
const sortByLabel = (a: EuiComboBoxOptionOption<string>, b: EuiComboBoxOptionOption<string>) => {
const getNormalizedString = (option: EuiComboBoxOptionOption<string>) =>
(option.label || '').toLowerCase();
return getNormalizedString(a).localeCompare(getNormalizedString(b));
};
export function FieldSelect({
type,
fields,
indexPattern = '',
value = '',
onChange,
disabled = false,
restrict = [],
placeholder = defaultPlaceholder,
uiRestrictions,
'data-test-subj': dataTestSubj = 'metricsIndexPatternFieldsSelect',
}: FieldSelectProps) {
if (type === METRIC_TYPES.COUNT) {
return null;
}
const selectedOptions: Array<EuiComboBoxOptionOption<string>> = [];
let newPlaceholder = placeholder;
const groupedOptions: EuiComboBoxProps<string>['options'] = Object.values(
(fields[indexPattern] || []).reduce<Record<string, EuiComboBoxOptionOption<string>>>(
(acc, field) => {
if (placeholder === field?.name) {
newPlaceholder = field.label ?? field.name;
}
if (
isFieldTypeEnabled(restrict, field.type) &&
isFieldEnabled(field.name, type, uiRestrictions)
) {
const item: EuiComboBoxOptionOption<string> = {
value: field.name,
label: field.label ?? field.name,
};
const fieldTypeOptions = acc[field.type]?.options;
if (fieldTypeOptions) {
fieldTypeOptions.push(item);
} else {
acc[field.type] = {
options: [item],
label: field.type,
};
}
if (value === item.value) {
selectedOptions.push(item);
}
}
return acc;
},
{}
)
);
// sort groups
groupedOptions.sort(sortByLabel);
// sort items
groupedOptions.forEach((group) => {
if (Array.isArray(group.options)) {
group.options.sort(sortByLabel);
}
});
if (value && !selectedOptions.length) {
onChange([]);
}
return (
<EuiComboBox
data-test-subj={dataTestSubj}
placeholder={newPlaceholder}
isDisabled={disabled}
options={groupedOptions}
selectedOptions={selectedOptions}
onChange={onChange}
singleSelection={{ asPlainText: true }}
/>
);
}

View file

@ -24,6 +24,7 @@ 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 {
htmlIdGenerator,
EuiFlexGroup,

View file

@ -42,7 +42,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
const checkModel = (model) => Array.isArray(model.variables) && model.script !== undefined;
export function MathAgg(props) {
const { siblings, model } = props;
const { siblings, model, fields, indexPattern } = props;
const htmlId = htmlIdGenerator();
const handleChange = createChangeHandler(props.onChange, model);
@ -95,6 +95,8 @@ export function MathAgg(props) {
<CalculationVars
id={htmlId('variables')}
metrics={siblings}
fields={fields}
indexPattern={indexPattern}
onChange={handleChange}
name="variables"
model={model}
@ -159,6 +161,7 @@ export function MathAgg(props) {
MathAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
indexPattern: PropTypes.string,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,

View file

@ -19,21 +19,23 @@
import PropTypes from 'prop-types';
import React from 'react';
import { includes } from 'lodash';
import { injectI18n } from '@kbn/i18n/react';
import { EuiComboBox } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { calculateSiblings } from '../lib/calculate_siblings';
import { calculateLabel } from '../../../../common/calculate_label';
import { basicAggs } from '../../../../common/basic_aggs';
import { toPercentileNumber } from '../../../../common/to_percentile_number';
import { METRIC_TYPES } from '../../../../common/metric_types';
function createTypeFilter(restrict, exclude) {
function createTypeFilter(restrict, exclude = []) {
return (metric) => {
if (includes(exclude, metric.type)) return false;
if (exclude.includes(metric.type)) {
return false;
}
switch (restrict) {
case 'basic':
return includes(basicAggs, metric.type);
return basicAggs.includes(metric.type);
default:
return true;
}
@ -55,21 +57,20 @@ export function filterRows(includeSiblings) {
};
}
function MetricSelectUi(props) {
export function MetricSelect(props) {
const {
additionalOptions,
restrict,
metric,
fields,
metrics,
onChange,
value,
exclude,
includeSiblings,
clearable,
intl,
...rest
} = props;
const calculatedMetrics = metrics.filter(createTypeFilter(restrict, exclude));
const siblings = calculateSiblings(calculatedMetrics, metric);
@ -80,7 +81,7 @@ function MetricSelectUi(props) {
const percentileOptions = siblings
.filter((row) => /^percentile/.test(row.type))
.reduce((acc, row) => {
const label = calculateLabel(row, calculatedMetrics);
const label = calculateLabel(row, calculatedMetrics, fields);
switch (row.type) {
case METRIC_TYPES.PERCENTILE_RANK:
@ -110,7 +111,7 @@ function MetricSelectUi(props) {
}, []);
const options = siblings.filter(filterRows(includeSiblings)).map((row) => {
const label = calculateLabel(row, calculatedMetrics);
const label = calculateLabel(row, calculatedMetrics, fields);
return { value: row.id, label };
});
const allOptions = [...options, ...additionalOptions, ...percentileOptions];
@ -122,8 +123,7 @@ function MetricSelectUi(props) {
return (
<EuiComboBox
placeholder={intl.formatMessage({
id: 'visTypeTimeseries.metricSelect.selectMetricPlaceholder',
placeholder={i18n.translate('visTypeTimeseries.metricSelect.selectMetricPlaceholder', {
defaultMessage: 'Select metric…',
})}
options={allOptions}
@ -136,7 +136,7 @@ function MetricSelectUi(props) {
);
}
MetricSelectUi.defaultProps = {
MetricSelect.defaultProps = {
additionalOptions: [],
exclude: [],
metric: {},
@ -144,7 +144,7 @@ MetricSelectUi.defaultProps = {
includeSiblings: false,
};
MetricSelectUi.propTypes = {
MetricSelect.propTypes = {
additionalOptions: PropTypes.array,
exclude: PropTypes.array,
metric: PropTypes.object,
@ -153,5 +153,3 @@ MetricSelectUi.propTypes = {
value: PropTypes.string,
includeSiblings: PropTypes.bool,
};
export const MetricSelect = injectI18n(MetricSelectUi);

View file

@ -53,7 +53,7 @@ const shouldShowHint = ({ model_type: type, window, period }) =>
type === MODEL_TYPES.WEIGHTED_EXPONENTIAL_TRIPLE && period * 2 > window;
export const MovingAverageAgg = (props) => {
const { siblings } = props;
const { siblings, fields, indexPattern } = props;
const model = { ...DEFAULTS, ...props.model };
const modelOptions = [
@ -153,6 +153,7 @@ export const MovingAverageAgg = (props) => {
onChange={handleSelectChange('field')}
metrics={siblings}
metric={model}
fields={fields[indexPattern]}
value={model.field}
exclude={[METRIC_TYPES.TOP_HIT]}
/>
@ -315,6 +316,7 @@ export const MovingAverageAgg = (props) => {
MovingAverageAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
indexPattern: PropTypes.string,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,

View file

@ -40,8 +40,8 @@ import { createNumberHandler } from '../../lib/create_number_handler';
import { AggRow } from '../agg_row';
import { PercentileRankValues } from './percentile_rank_values';
import { IFieldType, KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public';
import { MetricsItemsSchema, PanelSchema, SeriesItemsSchema } from '../../../../../common/types';
import { KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public';
import { MetricsItemsSchema, PanelSchema, SanitizedFieldType } from '../../../../../common/types';
import { DragHandleProps } from '../../../../types';
import { PercentileHdr } from '../percentile_hdr';
@ -49,10 +49,10 @@ const RESTRICT_FIELDS = [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM];
interface PercentileRankAggProps {
disableDelete: boolean;
fields: IFieldType[];
fields: Record<string, SanitizedFieldType[]>;
indexPattern: string;
model: MetricsItemsSchema;
panel: PanelSchema;
series: SeriesItemsSchema;
siblings: MetricsItemsSchema[];
dragHandleProps: DragHandleProps;
onAdd(): void;
@ -61,12 +61,10 @@ interface PercentileRankAggProps {
}
export const PercentileRankAgg = (props: PercentileRankAggProps) => {
const { series, panel, fields } = props;
const { panel, fields, indexPattern } = props;
const defaults = { values: [''] };
const model = { ...defaults, ...props.model };
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);
@ -79,7 +77,6 @@ export const PercentileRankAgg = (props: PercentileRankAggProps) => {
values,
});
};
return (
<AggRow
disableDelete={props.disableDelete}
@ -121,7 +118,7 @@ export const PercentileRankAgg = (props: PercentileRankAggProps) => {
type={model.type}
restrict={RESTRICT_FIELDS}
indexPattern={indexPattern}
value={model.field}
value={model.field ?? ''}
onChange={handleSelectChange('field')}
/>
</EuiFormRow>

View file

@ -36,7 +36,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
export const PositiveOnlyAgg = (props) => {
const { siblings } = props;
const { siblings, fields, indexPattern } = props;
const defaults = { unit: '' };
const model = { ...defaults, ...props.model };
@ -85,6 +85,7 @@ export const PositiveOnlyAgg = (props) => {
onChange={handleSelectChange('field')}
metrics={siblings}
metric={model}
fields={fields[indexPattern]}
value={model.field}
exclude={[METRIC_TYPES.TOP_HIT]}
/>
@ -98,6 +99,7 @@ export const PositiveOnlyAgg = (props) => {
PositiveOnlyAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
indexPattern: PropTypes.string,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,

View file

@ -37,7 +37,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
export const SerialDiffAgg = (props) => {
const { siblings } = props;
const { siblings, fields, indexPattern } = props;
const defaults = { lag: '' };
const model = { ...defaults, ...props.model };
@ -87,6 +87,7 @@ export const SerialDiffAgg = (props) => {
onChange={handleSelectChange('field')}
metrics={siblings}
metric={model}
fields={fields[indexPattern]}
value={model.field}
exclude={[METRIC_TYPES.TOP_HIT]}
/>
@ -125,6 +126,7 @@ export const SerialDiffAgg = (props) => {
SerialDiffAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
indexPattern: PropTypes.string,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,

View file

@ -37,8 +37,10 @@ import { getSupportedFieldsByMetricType } from '../lib/get_supported_fields_by_m
export function StandardAgg(props) {
const { model, panel, series, fields, uiRestrictions } = props;
const handleChange = createChangeHandler(props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const restrictFields = getSupportedFieldsByMetricType(model.type);
const indexPattern =
(series.override_index_pattern && series.series_index_pattern) || panel.index_pattern;

View file

@ -40,7 +40,7 @@ import {
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
const StandardSiblingAggUi = (props) => {
const { siblings, intl } = props;
const { siblings, intl, fields, indexPattern } = props;
const defaults = { sigma: '' };
const model = { ...defaults, ...props.model };
const htmlId = htmlIdGenerator();
@ -158,6 +158,7 @@ const StandardSiblingAggUi = (props) => {
onChange={handleSelectChange('field')}
exclude={[METRIC_TYPES.PERCENTILE, METRIC_TYPES.TOP_HIT]}
metrics={siblings}
fields={fields[indexPattern]}
metric={model}
value={model.field}
/>
@ -173,6 +174,7 @@ const StandardSiblingAggUi = (props) => {
StandardSiblingAggUi.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
indexPattern: PropTypes.string,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,

View file

@ -70,6 +70,7 @@ export class CalculationVars extends Component {
metrics={this.props.metrics}
metric={this.props.model}
value={row.field}
fields={this.props.fields[this.props.indexPattern]}
includeSiblings={this.props.includeSiblings}
exclude={this.props.exclude}
/>
@ -105,6 +106,8 @@ CalculationVars.defaultProps = {
};
CalculationVars.propTypes = {
fields: PropTypes.object,
indexPattern: PropTypes.string,
metrics: PropTypes.array,
model: PropTypes.object,
name: PropTypes.string,

View file

@ -74,6 +74,7 @@ export class AnnotationsEditor extends Component {
handleChange(_.assign({}, item, part));
};
}
handleQueryChange = (model, filter) => {
const part = { query_string: filter };
collectionActions.handleChange(this.props, {

View file

@ -78,10 +78,6 @@ export const IndexPattern = ({
allowLevelofDetail,
}) => {
const config = getUISettings();
const handleSelectChange = createSelectHandler(onChange);
const handleTextChange = createTextHandler(onChange);
const timeFieldName = `${prefix}time_field`;
const indexPatternName = `${prefix}index_pattern`;
const intervalName = `${prefix}interval`;
@ -100,6 +96,9 @@ export const IndexPattern = ({
[onChange, maxBarsName]
);
const handleSelectChange = createSelectHandler(onChange);
const handleTextChange = createTextHandler(onChange);
const timeRangeOptions = [
{
label: i18n.translate('visTypeTimeseries.indexPattern.timeRange.lastValue', {
@ -119,7 +118,7 @@ export const IndexPattern = ({
const defaults = {
default_index_pattern: '',
[indexPatternName]: '*',
[indexPatternName]: '',
[intervalName]: AUTO_INTERVAL,
[dropBucketName]: 1,
[maxBarsName]: config.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
@ -191,7 +190,7 @@ export const IndexPattern = ({
data-test-subj="metricsIndexPatternInput"
disabled={disabled}
placeholder={model.default_index_pattern}
onChange={handleTextChange(indexPatternName, '*')}
onChange={handleTextChange(indexPatternName)}
value={model[indexPatternName]}
/>
</EuiFormRow>
@ -204,7 +203,6 @@ export const IndexPattern = ({
})}
>
<FieldSelect
data-test-subj="metricsIndexPatternFieldsSelect"
restrict={RESTRICT_FIELDS}
value={model[timeFieldName]}
disabled={disabled}

View file

@ -17,23 +17,26 @@
* under the License.
*/
import { createSelectHandler } from './create_select_handler';
import { createSelectHandler, HandleChange } from './create_select_handler';
describe('createSelectHandler()', () => {
let handleChange;
let changeHandler;
describe('createSelectHandler', () => {
describe('createSelectHandler()', () => {
let handleChange: HandleChange;
let changeHandler: ReturnType<typeof createSelectHandler>;
beforeEach(() => {
handleChange = jest.fn();
changeHandler = createSelectHandler(handleChange);
const fn = changeHandler('test');
fn([{ value: 'foo' }]);
});
beforeEach(() => {
handleChange = jest.fn();
changeHandler = createSelectHandler(handleChange);
});
test('calls handleChange() function with partial', () => {
expect(handleChange.mock.calls.length).toEqual(1);
expect(handleChange.mock.calls[0][0]).toEqual({
test: 'foo',
test('should calls handleChange() function with the correct data', () => {
const fn = changeHandler('test');
fn([{ value: 'foo', label: 'foo' }]);
expect(handleChange).toHaveBeenCalledWith({
test: 'foo',
});
});
});
});

View file

@ -16,13 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { EuiComboBoxOptionOption } from '@elastic/eui';
import { MODEL_TYPES } from './model_options';
export type HandleChange = (partialModel: Record<string, any>) => void;
describe('src/legacy/core_plugins/metrics/common/model_options.js', () => {
describe('MODEL_TYPES', () => {
test('should match a snapshot of constants', () => {
expect(MODEL_TYPES).toMatchSnapshot();
});
export const createSelectHandler = (handleChange: HandleChange) => (name: string) => (
selected: EuiComboBoxOptionOption[] = []
) =>
handleChange?.({
[name]: selected[0]?.value ?? null,
});
});

View file

@ -17,7 +17,6 @@
* under the License.
*/
import _ from 'lodash';
import { newMetricAggFn } from './new_metric_agg_fn';
import { isBasicAgg } from '../../../../common/agg_lookup';
import { handleAdd, handleChange } from './collection_actions';
@ -30,8 +29,10 @@ export const seriesChangeHandler = (props, items) => (doc) => {
handleAdd.call(null, props, () => {
const metric = newMetricAggFn();
metric.type = doc.type;
const incompatPipelines = ['calculation', 'series_agg'];
if (!_.includes(incompatPipelines, doc.type)) metric.field = doc.id;
if (!['calculation', 'series_agg'].includes(doc.type)) {
metric.field = doc.id;
}
return metric;
});
} else {

View file

@ -46,7 +46,7 @@ const lessC = less(window, { env: 'production' });
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { QueryBarWrapper } from '../query_bar_wrapper';
import { getDefaultQueryLanguage } from '../lib/get_default_query_language';
import { VisDataContext } from './../../contexts/vis_data_context';
import { VisDataContext } from '../../contexts/vis_data_context';
class MarkdownPanelConfigUi extends Component {
constructor(props) {

View file

@ -45,7 +45,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { QueryBarWrapper } from '../query_bar_wrapper';
import { getDefaultQueryLanguage } from '../lib/get_default_query_language';
import { VisDataContext } from './../../contexts/vis_data_context';
import { VisDataContext } from '../../contexts/vis_data_context';
import { BUCKET_TYPES } from '../../../../common/metric_types';
export class TablePanelConfig extends Component {
static contextType = VisDataContext;

View file

@ -43,7 +43,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js
}
labelType="label"
>
<InjectIntl(FieldSelectUi)
<FieldSelect
data-test-subj="groupByField"
fields={
Object {
@ -156,7 +156,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js
}
labelType="label"
>
<InjectIntl(MetricSelectUi)
<MetricSelect
additionalOptions={
Array [
Object {
@ -170,6 +170,23 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js
]
}
clearable={false}
exclude={Array []}
fields={
Array [
Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "OriginCityName",
"readFromDocValues": true,
"searchable": true,
"type": "string",
},
]
}
includeSiblings={false}
metric={Object {}}
onChange={[Function]}
restrict="basic"
value="_count"

View file

@ -213,6 +213,7 @@ export const SplitByTermsUI = ({
metrics={metrics}
clearable={false}
additionalOptions={[defaultCount, terms]}
fields={fields[indexPattern]}
onChange={handleSelectChange('terms_order_by')}
restrict="basic"
value={model.terms_order_by}

View file

@ -40,13 +40,8 @@ export class VisEditor extends Component {
constructor(props) {
super(props);
this.localStorage = new Storage(window.localStorage);
this.state = {
model: props.visParams,
dirty: false,
autoApply: true,
visFields: props.visFields,
extractedIndexPatterns: [''],
};
this.state = {};
this.visDataSubject = new Rx.BehaviorSubject(this.props.visData);
this.visData$ = this.visDataSubject.asObservable().pipe(share());
@ -75,7 +70,10 @@ export class VisEditor extends Component {
isDirty: false,
});
const extractedIndexPatterns = extractIndexPatterns(this.state.model);
const extractedIndexPatterns = extractIndexPatterns(
this.state.model,
this.state.model.default_index_pattern
);
if (!isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns)) {
this.abortableFetchFields(extractedIndexPatterns).then((visFields) => {
this.setState({
@ -191,6 +189,31 @@ export class VisEditor extends Component {
}
componentDidMount() {
const dataStart = getDataStart();
dataStart.indexPatterns.getDefault().then(async (index) => {
const defaultIndexTitle = index?.title ?? '';
const indexPatterns = extractIndexPatterns(this.props.visParams, defaultIndexTitle);
this.setState({
model: {
...this.props.visParams,
/** @legacy
* please use IndexPatterns service instead
* **/
default_index_pattern: defaultIndexTitle,
/** @legacy
* please use IndexPatterns service instead
* **/
default_timefield: index?.timeFieldName ?? '',
},
dirty: false,
autoApply: true,
visFields: await fetchFields(indexPatterns),
extractedIndexPatterns: [''],
});
});
this.props.eventEmitter.on('updateEditor', this.updateModel);
}
@ -207,10 +230,8 @@ VisEditor.defaultProps = {
VisEditor.propTypes = {
vis: PropTypes.object,
visData: PropTypes.object,
visFields: PropTypes.object,
renderComplete: PropTypes.func,
config: PropTypes.object,
savedObj: PropTypes.object,
timeRange: PropTypes.object,
appState: PropTypes.object,
};

View file

@ -22,7 +22,6 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { RedirectAppLinks } from '../../../../../../kibana_react/public';
import { createTickFormatter } from '../../lib/tick_formatter';
import { calculateLabel } from '../../../../../common/calculate_label';
import { isSortable } from './is_sortable';
import { EuiToolTip, EuiIcon } from '@elastic/eui';
import { replaceVars } from '../../lib/replace_vars';
@ -30,8 +29,6 @@ import { fieldFormats } from '../../../../../../../plugins/data/public';
import { FormattedMessage } from '@kbn/i18n/react';
import { getFieldFormats, getCoreStart } from '../../../../services';
import { METRIC_TYPES } from '../../../../../common/metric_types';
function getColor(rules, colorKey, value) {
let color;
if (rules) {
@ -109,30 +106,19 @@ class TableVis extends Component {
};
renderHeader() {
const { model, uiState, onUiState } = this.props;
const { model, uiState, onUiState, visData } = this.props;
const stateKey = `${model.type}.sort`;
const sort = uiState.get(stateKey, {
column: '_default_',
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 calculateHeaderLabel = (metric, item) =>
item.label || visData.series[0]?.series?.find((s) => item.id === s.id)?.label;
const columns = this.visibleSeries.map((item) => {
const metric = last(item.metrics);
const label = calculateHeaderLabel(metric, item);
const handleClick = () => {
if (!isSortable(metric)) return;
let order;
@ -179,7 +165,7 @@ class TableVis extends Component {
</th>
);
});
const label = model.pivot_label || model.pivot_field || model.pivot_id;
const label = visData.pivot_label || model.pivot_label || model.pivot_id;
let sortIcon;
if (sort.column === '_default_') {
sortIcon = sort.order === 'asc' ? 'sortUp' : 'sortDown';

View file

@ -20,8 +20,7 @@
import React from 'react';
import { getDisplayName } from './lib/get_display_name';
import { labelDateFormatter } from './lib/label_date_formatter';
import { last, findIndex, first } from 'lodash';
import { calculateLabel } from '../../../common/calculate_label';
import { findIndex, first } from 'lodash';
export function visWithSplits(WrappedComponent) {
function SplitVisComponent(props) {
@ -35,8 +34,8 @@ export function visWithSplits(WrappedComponent) {
const [seriesId, splitId] = series.id.split(':');
const seriesModel = model.series.find((s) => s.id === seriesId);
if (!seriesModel || !splitId) return acc;
const metric = last(seriesModel.metrics);
const label = calculateLabel(metric, seriesModel.metrics);
const label = series.splitByLabel;
if (!acc[splitId]) {
acc[splitId] = {
@ -102,6 +101,7 @@ export function visWithSplits(WrappedComponent) {
return <div className="tvbSplitVis">{rows}</div>;
}
SplitVisComponent.displayName = `SplitVisComponent(${getDisplayName(WrappedComponent)})`;
return SplitVisComponent;
}

View file

@ -19,8 +19,7 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { fetchIndexPatternFields } from './lib/fetch_fields';
import { getSavedObjectsClient, getUISettings, getI18n } from '../services';
import { getUISettings, getI18n } from '../services';
import { VisEditor } from './components/vis_editor_lazy';
export class EditorController {
@ -31,42 +30,18 @@ export class EditorController {
this.eventEmitter = eventEmitter;
this.state = {
fields: [],
vis: vis,
isLoaded: false,
};
}
fetchDefaultIndexPattern = async () => {
const indexPattern = await getSavedObjectsClient().client.get(
'index-pattern',
getUISettings().get('defaultIndex')
);
return indexPattern.attributes;
};
fetchDefaultParams = async () => {
const { title, timeFieldName } = await this.fetchDefaultIndexPattern();
this.state.vis.params.default_index_pattern = title;
this.state.vis.params.default_timefield = timeFieldName;
this.state.fields = await fetchIndexPatternFields(this.state.vis);
this.state.isLoaded = true;
};
async render(params) {
const I18nContext = getI18n().Context;
!this.state.isLoaded && (await this.fetchDefaultParams());
render(
<I18nContext>
<VisEditor
config={getUISettings()}
vis={this.state.vis}
visFields={this.state.fields}
visParams={this.state.vis.params}
timeRange={params.timeRange}
renderComplete={() => {}}

View file

@ -17,31 +17,43 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { extractIndexPatterns } from '../../../common/extract_index_patterns';
import { getCoreStart } from '../../services';
import { getCoreStart, getDataStart } from '../../services';
import { ROUTES } from '../../../common/constants';
import { SanitizedFieldType } from '../../../common/types';
export async function fetchFields(
indexes: string[] = [],
signal?: AbortSignal
): Promise<Record<string, SanitizedFieldType[]>> {
const patterns = Array.isArray(indexes) ? indexes : [indexes];
const coreStart = getCoreStart();
const dataStart = getDataStart();
export async function fetchFields(indexPatterns = [], signal) {
const patterns = Array.isArray(indexPatterns) ? indexPatterns : [indexPatterns];
try {
const defaultIndexPattern = await dataStart.indexPatterns.getDefault();
const indexFields = await Promise.all(
patterns.map((pattern) =>
getCoreStart().http.get(ROUTES.FIELDS, {
patterns.map(async (pattern) => {
return coreStart.http.get(ROUTES.FIELDS, {
query: {
index: pattern,
},
signal,
})
)
});
})
);
return patterns.reduce(
const fields: Record<string, SanitizedFieldType[]> = patterns.reduce(
(cumulatedFields, currentPattern, index) => ({
...cumulatedFields,
[currentPattern]: indexFields[index],
}),
{}
);
if (defaultIndexPattern?.title && patterns.includes(defaultIndexPattern.title)) {
fields[''] = fields[defaultIndexPattern.title];
}
return fields;
} catch (error) {
if (error.name !== 'AbortError') {
getCoreStart().notifications.toasts.addDanger({
@ -52,11 +64,5 @@ export async function fetchFields(indexPatterns = [], signal) {
});
}
}
return [];
}
export async function fetchIndexPatternFields({ params, fields = {} }) {
const indexPatterns = extractIndexPatterns(params, fields);
return await fetchFields(indexPatterns);
return {};
}

View file

@ -16,24 +16,31 @@
* specific language governing permissions and limitations
* under the License.
*/
import { uniqBy, get } from 'lodash';
import { uniqBy } from 'lodash';
import { first, map } from 'rxjs/operators';
import { KibanaRequest, RequestHandlerContext } from 'kibana/server';
import { Framework } from '../plugin';
import {
indexPatterns,
IndexPatternFieldDescriptor,
IndexPatternsFetcher,
} from '../../../data/server';
import { IndexPatternsFetcher } from '../../../data/server';
import { ReqFacade } from './search_strategies/strategies/abstract_search_strategy';
export async function getFields(
requestContext: RequestHandlerContext,
request: KibanaRequest,
framework: Framework,
indexPattern: string
indexPatternString: string
) {
const getIndexPatternsService = async () => {
const [, { data }] = await framework.core.getStartServices();
return await data.indexPatterns.indexPatternsServiceFactory(
requestContext.core.savedObjects.client,
requestContext.core.elasticsearch.client.asCurrentUser
);
};
const indexPatternsService = await getIndexPatternsService();
// NOTE / TODO: This facade has been put in place to make migrating to the New Platform easier. It
// removes the need to refactor many layers of dependencies on "req", and instead just augments the top
// level object passed from here. The layers should be refactored fully at some point, but for now
@ -44,7 +51,7 @@ export async function getFields(
framework,
payload: {},
pre: {
indexPatternsService: new IndexPatternsFetcher(
indexPatternsFetcher: new IndexPatternsFetcher(
requestContext.core.elasticsearch.client.asCurrentUser
),
},
@ -58,19 +65,13 @@ export async function getFields(
)
.toPromise();
},
getIndexPatternsService: async () => indexPatternsService,
};
let indexPatternString = indexPattern;
if (!indexPatternString) {
const [{ savedObjects, elasticsearch }, { data }] = await framework.core.getStartServices();
const savedObjectsClient = savedObjects.getScopedClient(request);
const clusterClient = elasticsearch.client.asScoped(request).asCurrentUser;
const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory(
savedObjectsClient,
clusterClient
);
const defaultIndexPattern = await indexPatternsService.getDefault();
indexPatternString = get(defaultIndexPattern, 'title', '');
indexPatternString = defaultIndexPattern?.title ?? '';
}
const {
@ -78,12 +79,10 @@ export async function getFields(
capabilities,
} = (await framework.searchStrategyRegistry.getViableStrategy(reqFacade, indexPatternString))!;
const fields = ((await searchStrategy.getFieldsForWildcard(
const fields = await searchStrategy.getFieldsForWildcard(
reqFacade,
indexPatternString,
capabilities
)) as IndexPatternFieldDescriptor[]).filter(
(field) => field.aggregatable && !indexPatterns.isNestedField(field)
);
return uniqBy(fields, (field) => field.name);

View file

@ -71,6 +71,14 @@ export function getVisData(
)
.toPromise();
},
getIndexPatternsService: async () => {
const [, { data }] = await framework.core.getStartServices();
return await data.indexPatterns.indexPatternsServiceFactory(
requestContext.core.savedObjects.client,
requestContext.core.elasticsearch.client.asCurrentUser
);
},
};
const promises = reqFacade.payload.panels.map(getPanelData(reqFacade));
return Promise.all(promises).then((res) => {

View file

@ -24,7 +24,8 @@ import { DefaultSearchStrategy } from './strategies/default_search_strategy';
import { extractIndexPatterns } from '../../../common/extract_index_patterns';
export type RequestFacade = any;
export type Panel = any;
import { PanelSchema } from '../../../common/types';
export class SearchStrategyRegistry {
private strategies: AbstractSearchStrategy[] = [];
@ -53,8 +54,8 @@ export class SearchStrategyRegistry {
}
}
async getViableStrategyForPanel(req: RequestFacade, panel: Panel) {
const indexPattern = extractIndexPatterns(panel).join(',');
async getViableStrategyForPanel(req: RequestFacade, panel: PanelSchema) {
const indexPattern = extractIndexPatterns(panel, panel.default_index_pattern).join(',');
return this.getViableStrategy(req, indexPattern);
}

View file

@ -26,14 +26,17 @@ describe('AbstractSearchStrategy', () => {
let indexPattern;
beforeEach(() => {
mockedFields = {};
mockedFields = [];
req = {
payload: {},
pre: {
indexPatternsService: {
indexPatternsFetcher: {
getFieldsForWildcard: jest.fn().mockReturnValue(mockedFields),
},
},
getIndexPatternsService: jest.fn(() => ({
find: jest.fn(() => []),
})),
};
abstractSearchStrategy = new AbstractSearchStrategy();
@ -48,9 +51,10 @@ describe('AbstractSearchStrategy', () => {
test('should return fields for wildcard', async () => {
const fields = await abstractSearchStrategy.getFieldsForWildcard(req, indexPattern);
expect(fields).toBe(mockedFields);
expect(req.pre.indexPatternsService.getFieldsForWildcard).toHaveBeenCalledWith({
expect(fields).toEqual(mockedFields);
expect(req.pre.indexPatternsFetcher.getFieldsForWildcard).toHaveBeenCalledWith({
pattern: indexPattern,
metaFields: [],
fieldCapsOptions: { allow_no_indices: true },
});
});

View file

@ -17,16 +17,19 @@
* under the License.
*/
import {
import type {
RequestHandlerContext,
FakeRequest,
IUiSettingsClient,
SavedObjectsClientContract,
} from 'kibana/server';
import { Framework } from '../../../plugin';
import { IndexPatternsFetcher } from '../../../../../data/server';
import { VisPayload } from '../../../../common/types';
import type { Framework } from '../../../plugin';
import type { IndexPatternsFetcher, IFieldType } from '../../../../../data/server';
import type { VisPayload } from '../../../../common/types';
import type { IndexPatternsService } from '../../../../../data/common';
import { indexPatterns } from '../../../../../data/server';
import { SanitizedFieldType } from '../../../../common/types';
/**
* ReqFacade is a regular KibanaRequest object extended with additional service
@ -39,13 +42,27 @@ export interface ReqFacade<T = unknown> extends FakeRequest {
framework: Framework;
payload: T;
pre: {
indexPatternsService?: IndexPatternsFetcher;
indexPatternsFetcher?: IndexPatternsFetcher;
};
getUiSettingsService: () => IUiSettingsClient;
getSavedObjectsClient: () => SavedObjectsClientContract;
getEsShardTimeout: () => Promise<number>;
getIndexPatternsService: () => Promise<IndexPatternsService>;
}
const toSanitizedFieldType = (fields: IFieldType[]) => {
return fields
.filter((field) => field.aggregatable && !indexPatterns.isNestedField(field))
.map(
(field: IFieldType) =>
({
name: field.name,
label: field.customLabel ?? field.name,
type: field.type,
} as SanitizedFieldType)
);
};
export abstract class AbstractSearchStrategy {
async search(req: ReqFacade<VisPayload>, bodies: any[], indexType?: string) {
const requests: any[] = [];
@ -81,13 +98,27 @@ export abstract class AbstractSearchStrategy {
async getFieldsForWildcard<TPayload = unknown>(
req: ReqFacade<TPayload>,
indexPattern: string,
capabilities?: unknown
capabilities?: unknown,
options?: Partial<{
type: string;
rollupIndex: string;
}>
) {
const { indexPatternsService } = req.pre;
const { indexPatternsFetcher } = req.pre;
const indexPatternsService = await req.getIndexPatternsService();
const kibanaIndexPattern = (await indexPatternsService.find(indexPattern)).find(
(index) => index.title === indexPattern
);
return await indexPatternsService!.getFieldsForWildcard({
pattern: indexPattern,
fieldCapsOptions: { allow_no_indices: true },
});
return toSanitizedFieldType(
kibanaIndexPattern
? kibanaIndexPattern.fields.getAll()
: await indexPatternsFetcher!.getFieldsForWildcard({
pattern: indexPattern,
fieldCapsOptions: { allow_no_indices: true },
metaFields: [],
...options,
})
);
}
}

View file

@ -30,4 +30,12 @@ export class DefaultSearchStrategy extends AbstractSearchStrategy {
capabilities: new DefaultSearchCapabilities(req),
});
}
async getFieldsForWildcard<TPayload = unknown>(
req: ReqFacade<TPayload>,
indexPattern: string,
capabilities?: unknown
) {
return super.getFieldsForWildcard(req, indexPattern, capabilities);
}
}

View file

@ -32,8 +32,8 @@ import { processors } from '../request_processors/annotations';
* ]
* @returns {Object} doc - processed body
*/
export function buildAnnotationRequest(...args) {
export async function buildAnnotationRequest(...args) {
const processor = buildProcessorFunction(processors, ...args);
const doc = processor({});
const doc = await processor({});
return doc;
}

View file

@ -19,7 +19,6 @@
import { buildAnnotationRequest } from './build_request_body';
import { getEsShardTimeout } from '../helpers/get_es_shard_timeout';
import { getIndexPatternObject } from '../helpers/get_index_pattern';
import { UI_SETTINGS } from '../../../../../data/common';
export async function getAnnotationRequestParams(
req,
@ -32,17 +31,14 @@ export async function getAnnotationRequestParams(
const esShardTimeout = await getEsShardTimeout(req);
const indexPattern = annotation.index_pattern;
const { indexPatternObject, indexPatternString } = await getIndexPatternObject(req, indexPattern);
const request = buildAnnotationRequest(
const request = await buildAnnotationRequest(
req,
panel,
annotation,
esQueryConfig,
indexPatternObject,
capabilities,
{
maxBarsUiSettings: await uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
barTargetUiSettings: await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
}
uiSettings
);
return {

View file

@ -45,11 +45,14 @@ export async function getSeriesData(req, panel) {
);
const data = await searchStrategy.search(req, searches);
const handleResponseBodyFn = handleResponseBody(panel);
const handleResponseBodyFn = handleResponseBody(panel, req, searchStrategy, capabilities);
const series = data.map((resp) =>
handleResponseBodyFn(resp.rawResponse ? resp.rawResponse : resp)
const series = await Promise.all(
data.map(
async (resp) => await handleResponseBodyFn(resp.rawResponse ? resp.rawResponse : resp)
)
);
let annotations = null;
if (panel.annotations && panel.annotations.length) {

View file

@ -16,13 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import { buildRequestBody } from './table/build_request_body';
import { handleErrorResponse } from './handle_error_response';
import { get } from 'lodash';
import { processBucket } from './table/process_bucket';
import { getEsQueryConfig } from './helpers/get_es_query_uisettings';
import { getIndexPatternObject } from './helpers/get_index_pattern';
import { UI_SETTINGS } from '../../../../data/common';
import { createFieldsFetcher } from './helpers/fields_fetcher';
import { extractFieldLabel } from '../../../common/calculate_label';
export async function getTableData(req, panel) {
const panelIndexPattern = panel.index_pattern;
@ -33,18 +35,33 @@ export async function getTableData(req, panel) {
} = await req.framework.searchStrategyRegistry.getViableStrategy(req, panelIndexPattern);
const esQueryConfig = await getEsQueryConfig(req);
const { indexPatternObject } = await getIndexPatternObject(req, panelIndexPattern);
const extractFields = createFieldsFetcher(req, searchStrategy, capabilities);
const calculatePivotLabel = async () => {
if (panel.pivot_id && indexPatternObject?.title) {
const fields = await extractFields(indexPatternObject.title);
return extractFieldLabel(fields, panel.pivot_id);
}
return panel.pivot_id;
};
const meta = {
type: panel.type,
pivot_label: panel.pivot_label || (await calculatePivotLabel()),
uiRestrictions: capabilities.uiRestrictions,
};
try {
const uiSettings = req.getUiSettingsService();
const body = buildRequestBody(req, panel, esQueryConfig, indexPatternObject, capabilities, {
maxBarsUiSettings: await uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
barTargetUiSettings: await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
});
const body = await buildRequestBody(
req,
panel,
esQueryConfig,
indexPatternObject,
capabilities,
uiSettings
);
const [resp] = await searchStrategy.search(req, [
{
@ -59,9 +76,13 @@ export async function getTableData(req, panel) {
[]
);
const series = await Promise.all(
buckets.map(processBucket(panel, req, searchStrategy, capabilities, extractFields))
);
return {
...meta,
series: buckets.map(processBucket(panel)),
series,
};
} catch (err) {
if (err.body || err.name === 'KQLSyntaxError') {

View file

@ -128,7 +128,8 @@ export const bucketTransform = {
},
};
if (bucket.order_by) {
set(body, 'aggs.docs.top_hits.sort', [{ [bucket.order_by]: { order: bucket.order } }]);
const orderField = bucket.order_by;
set(body, 'aggs.docs.top_hits.sort', [{ [orderField]: { order: bucket.order } }]);
}
return body;
},

View file

@ -17,12 +17,24 @@
* under the License.
*/
import _ from 'lodash';
import { AbstractSearchStrategy, DefaultSearchCapabilities, ReqFacade } from '../../..';
export const createSelectHandler = (handleChange) => {
return (name) => (selectedOptions) => {
return handleChange?.({
[name]: _.get(selectedOptions, '[0].value', null),
});
export const createFieldsFetcher = (
req: ReqFacade,
searchStrategy: AbstractSearchStrategy,
capabilities: DefaultSearchCapabilities
) => {
const fieldsCacheMap = new Map();
return async (index: string) => {
if (fieldsCacheMap.has(index)) {
return fieldsCacheMap.get(index);
}
const fields = await searchStrategy.getFieldsForWildcard(req, index, capabilities);
fieldsCacheMap.set(index, fields);
return fields;
};
};

View file

@ -27,7 +27,7 @@ import { formatKey } from './format_key';
const getTimeSeries = (resp, series) =>
_.get(resp, `aggregations.timeseries`) || _.get(resp, `aggregations.${series.id}.timeseries`);
export function getSplits(resp, panel, series, meta) {
export async function getSplits(resp, panel, series, meta, extractFields) {
if (!meta) {
meta = _.get(resp, `aggregations.${series.id}.meta`);
}
@ -35,12 +35,17 @@ export function getSplits(resp, panel, series, meta) {
const color = new Color(series.color);
const metric = getLastMetric(series);
const buckets = _.get(resp, `aggregations.${series.id}.buckets`);
const fieldsForMetaIndex = meta.index ? await extractFields(meta.index) : [];
const splitByLabel = calculateLabel(metric, series.metrics, fieldsForMetaIndex);
if (buckets) {
if (Array.isArray(buckets)) {
const size = buckets.length;
const colors = getSplitColors(series.color, size, series.split_color_mode);
return buckets.map((bucket) => {
bucket.id = `${series.id}:${bucket.key}`;
bucket.splitByLabel = splitByLabel;
bucket.label = formatKey(bucket.key, series);
bucket.labelFormatted = bucket.key_as_string ? formatKey(bucket.key_as_string, series) : '';
bucket.color = panel.type === 'top_n' ? color.string() : colors.shift();
@ -72,10 +77,12 @@ export function getSplits(resp, panel, series, meta) {
.forEach((m) => {
mergeObj[m.id] = _.get(resp, `aggregations.${series.id}.${m.id}`);
});
return [
{
id: series.id,
label: series.label || calculateLabel(metric, series.metrics),
splitByLabel,
label: series.label || splitByLabel,
color: color.string(),
...mergeObj,
meta,

View file

@ -20,7 +20,7 @@
import { getSplits } from './get_splits';
describe('getSplits(resp, panel, series)', () => {
test('should return a splits for everything/filter group bys', () => {
test('should return a splits for everything/filter group bys', async () => {
const resp = {
aggregations: {
SERIES: {
@ -40,19 +40,20 @@ describe('getSplits(resp, panel, series)', () => {
{ id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' },
],
};
expect(getSplits(resp, panel, series)).toEqual([
expect(await getSplits(resp, panel, series, undefined)).toEqual([
{
id: 'SERIES',
label: 'Overall Average of Average of cpu',
meta: { bucketSize: 10 },
color: 'rgb(255, 0, 0)',
splitByLabel: 'Overall Average of Average of cpu',
timeseries: { buckets: [] },
SIBAGG: { value: 1 },
},
]);
});
test('should return a splits for terms group bys for top_n', () => {
test('should return a splits for terms group bys for top_n', async () => {
const resp = {
aggregations: {
SERIES: {
@ -84,7 +85,7 @@ describe('getSplits(resp, panel, series)', () => {
],
};
const panel = { type: 'top_n' };
expect(getSplits(resp, panel, series)).toEqual([
expect(await getSplits(resp, panel, series)).toEqual([
{
id: 'SERIES:example-01',
key: 'example-01',
@ -92,6 +93,7 @@ describe('getSplits(resp, panel, series)', () => {
labelFormatted: '',
meta: { bucketSize: 10 },
color: 'rgb(255, 0, 0)',
splitByLabel: 'Overall Average of Average of cpu',
timeseries: { buckets: [] },
SIBAGG: { value: 1 },
},
@ -102,13 +104,14 @@ describe('getSplits(resp, panel, series)', () => {
labelFormatted: '',
meta: { bucketSize: 10 },
color: 'rgb(255, 0, 0)',
splitByLabel: 'Overall Average of Average of cpu',
timeseries: { buckets: [] },
SIBAGG: { value: 2 },
},
]);
});
test('should return a splits for terms group with label formatted by {{key}} placeholder', () => {
test('should return a splits for terms group with label formatted by {{key}} placeholder', async () => {
const resp = {
aggregations: {
SERIES: {
@ -141,7 +144,7 @@ describe('getSplits(resp, panel, series)', () => {
],
};
const panel = { type: 'top_n' };
expect(getSplits(resp, panel, series)).toEqual([
expect(await getSplits(resp, panel, series)).toEqual([
{
id: 'SERIES:example-01',
key: 'example-01',
@ -149,6 +152,7 @@ describe('getSplits(resp, panel, series)', () => {
labelFormatted: '',
meta: { bucketSize: 10 },
color: 'rgb(255, 0, 0)',
splitByLabel: 'Overall Average of Average of cpu',
timeseries: { buckets: [] },
SIBAGG: { value: 1 },
},
@ -159,13 +163,14 @@ describe('getSplits(resp, panel, series)', () => {
labelFormatted: '',
meta: { bucketSize: 10 },
color: 'rgb(255, 0, 0)',
splitByLabel: 'Overall Average of Average of cpu',
timeseries: { buckets: [] },
SIBAGG: { value: 2 },
},
]);
});
test('should return a splits for terms group with labelFormatted if {{key}} placeholder is applied and key_as_string exists', () => {
test('should return a splits for terms group with labelFormatted if {{key}} placeholder is applied and key_as_string exists', async () => {
const resp = {
aggregations: {
SERIES: {
@ -200,7 +205,8 @@ describe('getSplits(resp, panel, series)', () => {
],
};
const panel = { type: 'top_n' };
expect(getSplits(resp, panel, series)).toEqual([
expect(await getSplits(resp, panel, series)).toEqual([
{
id: 'SERIES:example-01',
key: 'example-01',
@ -209,6 +215,7 @@ describe('getSplits(resp, panel, series)', () => {
labelFormatted: '--false--',
meta: { bucketSize: 10 },
color: 'rgb(255, 0, 0)',
splitByLabel: 'Overall Average of Average of cpu',
timeseries: { buckets: [] },
SIBAGG: { value: 1 },
},
@ -220,6 +227,7 @@ describe('getSplits(resp, panel, series)', () => {
labelFormatted: '--true--',
meta: { bucketSize: 10 },
color: 'rgb(255, 0, 0)',
splitByLabel: 'Overall Average of Average of cpu',
timeseries: { buckets: [] },
SIBAGG: { value: 2 },
},
@ -247,7 +255,7 @@ describe('getSplits(resp, panel, series)', () => {
},
};
test('should return a splits with no color', () => {
test('should return a splits with no color', async () => {
const series = {
id: 'SERIES',
color: '#F00',
@ -260,7 +268,8 @@ describe('getSplits(resp, panel, series)', () => {
],
};
const panel = { type: 'timeseries' };
expect(getSplits(resp, panel, series)).toEqual([
expect(await getSplits(resp, panel, series)).toEqual([
{
id: 'SERIES:example-01',
key: 'example-01',
@ -268,6 +277,7 @@ describe('getSplits(resp, panel, series)', () => {
labelFormatted: '',
meta: { bucketSize: 10 },
color: undefined,
splitByLabel: 'Overall Average of Average of cpu',
timeseries: { buckets: [] },
SIBAGG: { value: 1 },
},
@ -278,13 +288,14 @@ describe('getSplits(resp, panel, series)', () => {
labelFormatted: '',
meta: { bucketSize: 10 },
color: undefined,
splitByLabel: 'Overall Average of Average of cpu',
timeseries: { buckets: [] },
SIBAGG: { value: 2 },
},
]);
});
test('should return gradient color', () => {
test('should return gradient color', async () => {
const series = {
id: 'SERIES',
color: '#F00',
@ -298,7 +309,8 @@ describe('getSplits(resp, panel, series)', () => {
],
};
const panel = { type: 'timeseries' };
expect(getSplits(resp, panel, series)).toEqual([
expect(await getSplits(resp, panel, series)).toEqual([
expect.objectContaining({
color: 'rgb(255, 0, 0)',
}),
@ -308,7 +320,7 @@ describe('getSplits(resp, panel, series)', () => {
]);
});
test('should return rainbow color', () => {
test('should return rainbow color', async () => {
const series = {
id: 'SERIES',
color: '#F00',
@ -322,7 +334,8 @@ describe('getSplits(resp, panel, series)', () => {
],
};
const panel = { type: 'timeseries' };
expect(getSplits(resp, panel, series)).toEqual([
expect(await getSplits(resp, panel, series)).toEqual([
expect.objectContaining({
color: '#68BC00',
}),
@ -333,7 +346,7 @@ describe('getSplits(resp, panel, series)', () => {
});
});
test('should return a splits for filters group bys', () => {
test('should return a splits for filters group bys', async () => {
const resp = {
aggregations: {
SERIES: {
@ -360,7 +373,8 @@ describe('getSplits(resp, panel, series)', () => {
metrics: [{ id: 'COUNT', type: 'count' }],
};
const panel = { type: 'timeseries' };
expect(getSplits(resp, panel, series)).toEqual([
expect(await getSplits(resp, panel, series)).toEqual([
{
id: 'SERIES:filter-1',
key: 'filter-1',

View file

@ -20,7 +20,8 @@
import { overwrite } from '../../helpers';
import { getBucketSize } from '../../helpers/get_bucket_size';
import { getTimerange } from '../../helpers/get_timerange';
import { search } from '../../../../../../../plugins/data/server';
import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server';
const { dateHistogramInterval } = search.aggs;
export function dateHistogram(
@ -30,9 +31,10 @@ export function dateHistogram(
esQueryConfig,
indexPatternObject,
capabilities,
{ barTargetUiSettings }
uiSettings
) {
return (next) => (doc) => {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
const timeField = annotation.time_field;
const { bucketSize, intervalString } = getBucketSize(
req,

View file

@ -19,7 +19,7 @@
import { getBucketSize } from '../../helpers/get_bucket_size';
import { getTimerange } from '../../helpers/get_timerange';
import { esQuery } from '../../../../../../data/server';
import { esQuery, UI_SETTINGS } from '../../../../../../data/server';
export function query(
req,
@ -28,9 +28,10 @@ export function query(
esQueryConfig,
indexPattern,
capabilities,
{ barTargetUiSettings }
uiSettings
) {
return (next) => (doc) => {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
const timeField = annotation.time_field;
const { bucketSize } = getBucketSize(req, 'auto', capabilities, barTargetUiSettings);
const { from, to } = getTimerange(req);

View file

@ -23,6 +23,7 @@ export function topHits(req, panel, annotation) {
return (next) => (doc) => {
const fields = (annotation.fields && annotation.fields.split(/[,\s]+/)) || [];
const timeField = annotation.time_field;
overwrite(doc, `aggs.${annotation.id}.aggs.hits.top_hits`, {
sort: [
{

View file

@ -22,7 +22,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size';
import { offsetTime } from '../../offset_time';
import { getIntervalAndTimefield } from '../../get_interval_and_timefield';
import { isLastValueTimerangeMode } from '../../helpers/get_timerange_mode';
import { search } from '../../../../../../../plugins/data/server';
import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server';
const { dateHistogramInterval } = search.aggs;
export function dateHistogram(
@ -32,9 +32,12 @@ export function dateHistogram(
esQueryConfig,
indexPatternObject,
capabilities,
{ maxBarsUiSettings, barTargetUiSettings }
uiSettings
) {
return (next) => (doc) => {
return (next) => async (doc) => {
const maxBarsUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS);
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
const { timeField, interval, maxBars } = getIntervalAndTimefield(
panel,
series,
@ -73,11 +76,10 @@ export function dateHistogram(
? getDateHistogramForLastBucketMode()
: getDateHistogramForEntireTimerangeMode();
// master
overwrite(doc, `aggs.${series.id}.meta`, {
timeField,
intervalString,
index: indexPatternObject?.title,
bucketSize,
seriesId: series.id,
});

View file

@ -19,6 +19,7 @@
import { DefaultSearchCapabilities } from '../../../search_strategies/default_search_capabilities';
import { dateHistogram } from './date_histogram';
import { UI_SETTINGS } from '../../../../../../data/common';
describe('dateHistogram(req, panel, series)', () => {
let panel;
@ -51,20 +52,30 @@ describe('dateHistogram(req, panel, series)', () => {
};
indexPatternObject = {};
capabilities = new DefaultSearchCapabilities(req);
uiSettings = { maxBarsUiSettings: 100, barTargetUiSettings: 50 };
uiSettings = {
get: async (key) => (key === UI_SETTINGS.HISTOGRAM_MAX_BARS ? 100 : 50),
};
});
test('calls next when finished', () => {
test('calls next when finished', async () => {
const next = jest.fn();
dateHistogram(req, panel, series, config, indexPatternObject, capabilities, uiSettings)(next)(
{}
);
await dateHistogram(
req,
panel,
series,
config,
indexPatternObject,
capabilities,
uiSettings
)(next)({});
expect(next.mock.calls.length).toEqual(1);
});
test('returns valid date histogram', () => {
test('returns valid date histogram', async () => {
const next = (doc) => doc;
const doc = dateHistogram(
const doc = await dateHistogram(
req,
panel,
series,
@ -102,10 +113,10 @@ describe('dateHistogram(req, panel, series)', () => {
});
});
test('returns valid date histogram (offset by 1h)', () => {
test('returns valid date histogram (offset by 1h)', async () => {
series.offset_time = '1h';
const next = (doc) => doc;
const doc = dateHistogram(
const doc = await dateHistogram(
req,
panel,
series,
@ -143,13 +154,13 @@ describe('dateHistogram(req, panel, series)', () => {
});
});
test('returns valid date histogram with overridden index pattern', () => {
test('returns valid date histogram with overridden index pattern', async () => {
series.override_index_pattern = 1;
series.series_index_pattern = '*';
series.series_time_field = 'timestamp';
series.series_interval = '20s';
const next = (doc) => doc;
const doc = dateHistogram(
const doc = await dateHistogram(
req,
panel,
series,
@ -188,12 +199,12 @@ describe('dateHistogram(req, panel, series)', () => {
});
describe('dateHistogram for entire time range mode', () => {
test('should ignore entire range mode for timeseries', () => {
test('should ignore entire range mode for timeseries', async () => {
panel.time_range_mode = 'entire_time_range';
panel.type = 'timeseries';
const next = (doc) => doc;
const doc = dateHistogram(
const doc = await dateHistogram(
req,
panel,
series,
@ -207,11 +218,11 @@ describe('dateHistogram(req, panel, series)', () => {
expect(doc.aggs.test.aggs.timeseries.date_histogram).toBeDefined();
});
test('should returns valid date histogram for entire range mode', () => {
test('should returns valid date histogram for entire range mode', async () => {
panel.time_range_mode = 'entire_time_range';
const next = (doc) => doc;
const doc = dateHistogram(
const doc = await dateHistogram(
req,
panel,
series,

View file

@ -17,11 +17,12 @@
* under the License.
*/
const filter = (metric) => metric.type === 'filter_ratio';
import { bucketTransform } from '../../helpers/bucket_transform';
import { overwrite } from '../../helpers';
import { esQuery } from '../../../../../../data/server';
const filter = (metric) => metric.type === 'filter_ratio';
export function ratios(req, panel, series, esQueryConfig, indexPatternObject) {
return (next) => (doc) => {
if (series.metrics.some(filter)) {

View file

@ -20,6 +20,7 @@ import { overwrite } from '../../helpers';
import { getBucketSize } from '../../helpers/get_bucket_size';
import { bucketTransform } from '../../helpers/bucket_transform';
import { getIntervalAndTimefield } from '../../get_interval_and_timefield';
import { UI_SETTINGS } from '../../../../../../data/common';
export function metricBuckets(
req,
@ -28,9 +29,11 @@ export function metricBuckets(
esQueryConfig,
indexPatternObject,
capabilities,
{ barTargetUiSettings }
uiSettings
) {
return (next) => (doc) => {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject);
const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings);

View file

@ -63,20 +63,20 @@ describe('metricBuckets(req, panel, series)', () => {
{},
undefined,
{
barTargetUiSettings: 50,
get: async () => 50,
}
);
});
test('calls next when finished', () => {
test('calls next when finished', async () => {
const next = jest.fn();
metricBucketsProcessor(next)({});
await metricBucketsProcessor(next)({});
expect(next.mock.calls.length).toEqual(1);
});
test('returns metric aggs', () => {
test('returns metric aggs', async () => {
const next = (doc) => doc;
const doc = metricBucketsProcessor(next)({});
const doc = await metricBucketsProcessor(next)({});
expect(doc).toEqual({
aggs: {

View file

@ -21,6 +21,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size';
import { getIntervalAndTimefield } from '../../get_interval_and_timefield';
import { bucketTransform } from '../../helpers/bucket_transform';
import { overwrite } from '../../helpers';
import { UI_SETTINGS } from '../../../../../../data/common';
export const filter = (metric) => metric.type === 'positive_rate';
@ -29,7 +30,11 @@ export const createPositiveRate = (doc, intervalString, aggRoot) => (metric) =>
const derivativeFn = bucketTransform.derivative;
const positiveOnlyFn = bucketTransform.positive_only;
const maxMetric = { id: `${metric.id}-positive-rate-max`, type: 'max', field: metric.field };
const maxMetric = {
id: `${metric.id}-positive-rate-max`,
type: 'max',
field: metric.field,
};
const derivativeMetric = {
id: `${metric.id}-positive-rate-derivative`,
type: 'derivative',
@ -64,9 +69,11 @@ export function positiveRate(
esQueryConfig,
indexPatternObject,
capabilities,
{ barTargetUiSettings }
uiSettings
) {
return (next) => (doc) => {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject);
const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings);

View file

@ -51,19 +51,21 @@ describe('positiveRate(req, panel, series)', () => {
},
};
uiSettings = {
barTargetUiSettings: 50,
get: async () => 50,
};
});
test('calls next when finished', () => {
test('calls next when finished', async () => {
const next = jest.fn();
positiveRate(req, panel, series, {}, {}, undefined, uiSettings)(next)({});
await positiveRate(req, panel, series, {}, {}, undefined, uiSettings)(next)({});
expect(next.mock.calls.length).toEqual(1);
});
test('returns positive rate aggs', () => {
test('returns positive rate aggs', async () => {
const next = (doc) => doc;
const doc = positiveRate(req, panel, series, {}, {}, undefined, uiSettings)(next)({});
const doc = await positiveRate(req, panel, series, {}, {}, undefined, uiSettings)(next)({});
expect(doc).toEqual({
aggs: {
test: {

View file

@ -21,6 +21,7 @@ import { overwrite } from '../../helpers';
import { getBucketSize } from '../../helpers/get_bucket_size';
import { bucketTransform } from '../../helpers/bucket_transform';
import { getIntervalAndTimefield } from '../../get_interval_and_timefield';
import { UI_SETTINGS } from '../../../../../../data/common';
export function siblingBuckets(
req,
@ -29,9 +30,10 @@ export function siblingBuckets(
esQueryConfig,
indexPatternObject,
capabilities,
{ barTargetUiSettings }
uiSettings
) {
return (next) => (doc) => {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
const { interval } = getIntervalAndTimefield(panel, series, indexPatternObject);
const { bucketSize } = getBucketSize(req, interval, capabilities, barTargetUiSettings);

View file

@ -56,19 +56,19 @@ describe('siblingBuckets(req, panel, series)', () => {
},
};
uiSettings = {
barTargetUiSettings: 50,
get: async () => 50,
};
});
test('calls next when finished', () => {
test('calls next when finished', async () => {
const next = jest.fn();
siblingBuckets(req, panel, series, {}, {}, undefined, uiSettings)(next)({});
await siblingBuckets(req, panel, series, {}, {}, undefined, uiSettings)(next)({});
expect(next.mock.calls.length).toEqual(1);
});
test('returns sibling aggs', () => {
test('returns sibling aggs', async () => {
const next = (doc) => doc;
const doc = siblingBuckets(req, panel, series, {}, {}, undefined, uiSettings)(next)({});
const doc = await siblingBuckets(req, panel, series, {}, {}, undefined, uiSettings)(next)({});
expect(doc).toEqual({
aggs: {

View file

@ -25,9 +25,12 @@ import { bucketTransform } from '../../helpers/bucket_transform';
export function splitByTerms(req, panel, series) {
return (next) => (doc) => {
if (series.split_mode === 'terms' && series.terms_field) {
const termsField = series.terms_field;
const orderByTerms = series.terms_order_by;
const direction = series.terms_direction || 'desc';
const metric = series.metrics.find((item) => item.id === series.terms_order_by);
overwrite(doc, `aggs.${series.id}.terms.field`, series.terms_field);
const metric = series.metrics.find((item) => item.id === orderByTerms);
overwrite(doc, `aggs.${series.id}.terms.field`, termsField);
overwrite(doc, `aggs.${series.id}.terms.size`, series.terms_size);
if (series.terms_include) {
overwrite(doc, `aggs.${series.id}.terms.include`, series.terms_include);
@ -36,16 +39,16 @@ export function splitByTerms(req, panel, series) {
overwrite(doc, `aggs.${series.id}.terms.exclude`, series.terms_exclude);
}
if (metric && metric.type !== 'count' && ~basicAggs.indexOf(metric.type)) {
const sortAggKey = `${series.terms_order_by}-SORT`;
const sortAggKey = `${orderByTerms}-SORT`;
const fn = bucketTransform[metric.type];
const bucketPath = getBucketsPath(series.terms_order_by, series.metrics).replace(
series.terms_order_by,
const bucketPath = getBucketsPath(orderByTerms, series.metrics).replace(
orderByTerms,
sortAggKey
);
overwrite(doc, `aggs.${series.id}.terms.order`, { [bucketPath]: direction });
overwrite(doc, `aggs.${series.id}.aggs`, { [sortAggKey]: fn(metric) });
} else if (['_key', '_count'].includes(series.terms_order_by)) {
overwrite(doc, `aggs.${series.id}.terms.order`, { [series.terms_order_by]: direction });
} else if (['_key', '_count'].includes(orderByTerms)) {
overwrite(doc, `aggs.${series.id}.terms.order`, { [orderByTerms]: direction });
} else {
overwrite(doc, `aggs.${series.id}.terms.order`, { _count: direction });
}

View file

@ -23,7 +23,7 @@ import { isLastValueTimerangeMode } from '../../helpers/get_timerange_mode';
import { getIntervalAndTimefield } from '../../get_interval_and_timefield';
import { getTimerange } from '../../helpers/get_timerange';
import { calculateAggRoot } from './calculate_agg_root';
import { search } from '../../../../../../../plugins/data/server';
import { search, UI_SETTINGS } from '../../../../../../../plugins/data/server';
const { dateHistogramInterval } = search.aggs;
export function dateHistogram(
@ -32,12 +32,14 @@ export function dateHistogram(
esQueryConfig,
indexPatternObject,
capabilities,
{ barTargetUiSettings }
uiSettings
) {
return (next) => (doc) => {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
const { timeField, interval } = getIntervalAndTimefield(panel, {}, indexPatternObject);
const meta = {
timeField,
index: indexPatternObject?.title,
};
const getDateHistogramForLastBucketMode = () => {
@ -65,7 +67,7 @@ export function dateHistogram(
});
overwrite(doc, aggRoot.replace(/\.aggs$/, '.meta'), {
timeField,
...meta,
intervalString,
bucketSize,
});

View file

@ -17,12 +17,13 @@
* under the License.
*/
const filter = (metric) => metric.type === 'filter_ratio';
import { esQuery } from '../../../../../../data/server';
import { bucketTransform } from '../../helpers/bucket_transform';
import { overwrite } from '../../helpers';
import { calculateAggRoot } from './calculate_agg_root';
const filter = (metric) => metric.type === 'filter_ratio';
export function ratios(req, panel, esQueryConfig, indexPatternObject) {
return (next) => (doc) => {
panel.series.forEach((column) => {

View file

@ -22,6 +22,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size';
import { bucketTransform } from '../../helpers/bucket_transform';
import { getIntervalAndTimefield } from '../../get_interval_and_timefield';
import { calculateAggRoot } from './calculate_agg_root';
import { UI_SETTINGS } from '../../../../../../data/common';
export function metricBuckets(
req,
@ -29,9 +30,10 @@ export function metricBuckets(
esQueryConfig,
indexPatternObject,
capabilities,
{ barTargetUiSettings }
uiSettings
) {
return (next) => (doc) => {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject);
const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings);

View file

@ -27,6 +27,7 @@ import { bucketTransform } from '../../helpers/bucket_transform';
export function pivot(req, panel) {
return (next) => (doc) => {
const { sort } = req.payload.state;
if (panel.pivot_id) {
overwrite(doc, 'aggs.pivot.terms.field', panel.pivot_id);
overwrite(doc, 'aggs.pivot.terms.size', panel.pivot_rows);

View file

@ -21,6 +21,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size';
import { getIntervalAndTimefield } from '../../get_interval_and_timefield';
import { calculateAggRoot } from './calculate_agg_root';
import { createPositiveRate, filter } from '../series/positive_rate';
import { UI_SETTINGS } from '../../../../../../data/common';
export function positiveRate(
req,
@ -28,9 +29,10 @@ export function positiveRate(
esQueryConfig,
indexPatternObject,
capabilities,
{ barTargetUiSettings }
uiSettings
) {
return (next) => (doc) => {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject);
const { intervalString } = getBucketSize(req, interval, capabilities, barTargetUiSettings);

View file

@ -22,6 +22,7 @@ import { getBucketSize } from '../../helpers/get_bucket_size';
import { bucketTransform } from '../../helpers/bucket_transform';
import { getIntervalAndTimefield } from '../../get_interval_and_timefield';
import { calculateAggRoot } from './calculate_agg_root';
import { UI_SETTINGS } from '../../../../../../data/common';
export function siblingBuckets(
req,
@ -29,9 +30,10 @@ export function siblingBuckets(
esQueryConfig,
indexPatternObject,
capabilities,
{ barTargetUiSettings }
uiSettings
) {
return (next) => (doc) => {
return (next) => async (doc) => {
const barTargetUiSettings = await uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET);
const { interval } = getIntervalAndTimefield(panel, {}, indexPatternObject);
const { bucketSize } = getBucketSize(req, interval, capabilities, barTargetUiSettings);

View file

@ -25,8 +25,8 @@ import { getSplits } from '../../helpers/get_splits';
import { mapBucket } from '../../helpers/map_bucket';
import { evaluate } from 'tinymath';
export function mathAgg(resp, panel, series, meta) {
return (next) => (results) => {
export function mathAgg(resp, panel, series, meta, extractFields) {
return (next) => async (results) => {
const mathMetric = last(series.metrics);
if (mathMetric.type !== 'math') return next(results);
// Filter the results down to only the ones that match the series.id. Sometimes
@ -38,7 +38,7 @@ export function mathAgg(resp, panel, series, meta) {
return true;
});
const decoration = getDefaultDecoration(series);
const splits = getSplits(resp, panel, series, meta);
const splits = await getSplits(resp, panel, series, meta, extractFields);
const mathSeries = splits.map((split) => {
if (mathMetric.variables.length) {
// Gather the data for the splits. The data will either be a sibling agg or

View file

@ -91,15 +91,16 @@ describe('math(resp, panel, series)', () => {
};
});
test('calls next when finished', () => {
test('calls next when finished', async () => {
const next = jest.fn();
mathAgg(resp, panel, series)(next)([]);
await mathAgg(resp, panel, series)(next)([]);
expect(next.mock.calls.length).toEqual(1);
});
test('creates a series', () => {
const next = mathAgg(resp, panel, series)((results) => results);
const results = stdMetric(resp, panel, series)(next)([]);
test('creates a series', async () => {
const next = await mathAgg(resp, panel, series)((results) => results);
const results = await stdMetric(resp, panel, series)(next)([]);
expect(results).toHaveLength(1);
expect(results[0]).toEqual({
@ -118,12 +119,12 @@ describe('math(resp, panel, series)', () => {
});
});
test('turns division by zero into null values', () => {
test('turns division by zero into null values', async () => {
resp.aggregations.test.buckets[0].timeseries.buckets[0].mincpu = 0;
const next = mathAgg(resp, panel, series)((results) => results);
const results = stdMetric(resp, panel, series)(next)([]);
expect(results).toHaveLength(1);
const next = await mathAgg(resp, panel, series)((results) => results);
const results = await stdMetric(resp, panel, series)(next)([]);
expect(results).toHaveLength(1);
expect(results[0]).toEqual(
expect.objectContaining({
data: [
@ -134,15 +135,35 @@ describe('math(resp, panel, series)', () => {
);
});
test('throws on actual tinymath expression errors', () => {
test('throws on actual tinymath expression errors #1', async () => {
series.metrics[2].script = 'notExistingFn(params.a)';
expect(() =>
stdMetric(resp, panel, series)(mathAgg(resp, panel, series)((results) => results))([])
).toThrow();
try {
await stdMetric(
resp,
panel,
series
)(await mathAgg(resp, panel, series)((results) => results))([]);
} catch (e) {
expect(e.message).toEqual(
'Failed to parse expression. Expected "*", "+", "-", "/", or end of input but "(" found.'
);
}
});
test('throws on actual tinymath expression errors #2', async () => {
series.metrics[2].script = 'divide(params.a, params.b';
expect(() =>
stdMetric(resp, panel, series)(mathAgg(resp, panel, series)((results) => results))([])
).toThrow();
try {
await stdMetric(
resp,
panel,
series
)(await mathAgg(resp, panel, series)((results) => results))([]);
} catch (e) {
expect(e.message).toEqual(
'Failed to parse expression. Expected "*", "+", "-", "/", or end of input but "(" found.'
);
}
});
});

View file

@ -23,15 +23,15 @@ import { getSplits } from '../../helpers/get_splits';
import { getLastMetric } from '../../helpers/get_last_metric';
import { METRIC_TYPES } from '../../../../../common/metric_types';
export function percentile(resp, panel, series, meta) {
return (next) => (results) => {
export function percentile(resp, panel, series, meta, extractFields) {
return (next) => async (results) => {
const metric = getLastMetric(series);
if (metric.type !== METRIC_TYPES.PERCENTILE) {
return next(results);
}
getSplits(resp, panel, series, meta).forEach((split) => {
(await getSplits(resp, panel, series, meta, extractFields)).forEach((split) => {
metric.percentiles.forEach((percentile) => {
const percentileValue = percentile.value ? percentile.value : 0;
const id = `${split.id}:${percentile.id}`;

View file

@ -80,15 +80,18 @@ describe('percentile(resp, panel, series)', () => {
};
});
test('calls next when finished', () => {
test('calls next when finished', async () => {
const next = jest.fn();
percentile(resp, panel, series)(next)([]);
await percentile(resp, panel, series, {})(next)([]);
expect(next.mock.calls.length).toEqual(1);
});
test('creates a series', () => {
test('creates a series', async () => {
const next = (results) => results;
const results = percentile(resp, panel, series)(next)([]);
const results = await percentile(resp, panel, series, {})(next)([]);
expect(results).toHaveLength(2);
expect(results[0]).toHaveProperty('id', 'test:10-90');

View file

@ -23,15 +23,15 @@ import { getLastMetric } from '../../helpers/get_last_metric';
import { toPercentileNumber } from '../../../../../common/to_percentile_number';
import { METRIC_TYPES } from '../../../../../common/metric_types';
export function percentileRank(resp, panel, series, meta) {
return (next) => (results) => {
export function percentileRank(resp, panel, series, meta, extractFields) {
return (next) => async (results) => {
const metric = getLastMetric(series);
if (metric.type !== METRIC_TYPES.PERCENTILE_RANK) {
return next(results);
}
getSplits(resp, panel, series, meta).forEach((split) => {
(await getSplits(resp, panel, series, meta, extractFields)).forEach((split) => {
(metric.values || []).forEach((percentileRank, index) => {
const data = split.timeseries.buckets.map((bucket) => [
bucket.key,

View file

@ -22,8 +22,8 @@ import _ from 'lodash';
import { getDefaultDecoration } from '../../helpers/get_default_decoration';
import { calculateLabel } from '../../../../../common/calculate_label';
export function seriesAgg(resp, panel, series) {
return (next) => (results) => {
export function seriesAgg(resp, panel, series, meta, extractFields) {
return (next) => async (results) => {
if (series.metrics.some((m) => m.type === 'series_agg')) {
const decoration = getDefaultDecoration(series);
@ -43,9 +43,14 @@ export function seriesAgg(resp, panel, series) {
const fn = SeriesAgg[m.function];
return (fn && fn(acc)) || acc;
}, targetSeries);
const fieldsForMetaIndex = meta.index ? await extractFields(meta.index) : [];
results.push({
id: `${series.id}`,
label: series.label || calculateLabel(_.last(series.metrics), series.metrics),
label:
series.label ||
calculateLabel(_.last(series.metrics), series.metrics, fieldsForMetaIndex),
color: series.color,
data: _.first(data),
...decoration,

View file

@ -91,15 +91,16 @@ describe('seriesAgg(resp, panel, series)', () => {
};
});
test('calls next when finished', () => {
test('calls next when finished', async () => {
const next = jest.fn();
seriesAgg(resp, panel, series)(next)([]);
await seriesAgg(resp, panel, series, {})(next)([]);
expect(next.mock.calls.length).toEqual(1);
});
test('creates a series', () => {
const next = seriesAgg(resp, panel, series)((results) => results);
const results = stdMetric(resp, panel, series)(next)([]);
test('creates a series', async () => {
const next = await seriesAgg(resp, panel, series, {})((results) => results);
const results = await stdMetric(resp, panel, series, {})(next)([]);
expect(results).toHaveLength(1);
expect(results[0]).toEqual({

View file

@ -20,36 +20,38 @@
import { getAggValue, getLastMetric, getSplits } from '../../helpers';
import { METRIC_TYPES } from '../../../../../common/metric_types';
export function stdDeviationBands(resp, panel, series, meta) {
return (next) => (results) => {
export function stdDeviationBands(resp, panel, series, meta, extractFields) {
return (next) => async (results) => {
const metric = getLastMetric(series);
if (metric.type === METRIC_TYPES.STD_DEVIATION && metric.mode === 'band') {
getSplits(resp, panel, series, meta).forEach(({ id, color, label, timeseries }) => {
const data = timeseries.buckets.map((bucket) => [
bucket.key,
getAggValue(bucket, { ...metric, mode: 'upper' }),
getAggValue(bucket, { ...metric, mode: 'lower' }),
]);
(await getSplits(resp, panel, series, meta, extractFields)).forEach(
({ id, color, label, timeseries }) => {
const data = timeseries.buckets.map((bucket) => [
bucket.key,
getAggValue(bucket, { ...metric, mode: 'upper' }),
getAggValue(bucket, { ...metric, mode: 'lower' }),
]);
results.push({
id,
label,
color,
data,
lines: {
show: series.chart_type === 'line',
fill: 0.5,
lineWidth: 0,
mode: 'band',
},
bars: {
show: series.chart_type === 'bar',
fill: 0.5,
mode: 'band',
},
points: { show: false },
});
});
results.push({
id,
label,
color,
data,
lines: {
show: series.chart_type === 'line',
fill: 0.5,
lineWidth: 0,
mode: 'band',
},
bars: {
show: series.chart_type === 'bar',
fill: 0.5,
mode: 'band',
},
points: { show: false },
});
}
);
}
return next(results);
};

View file

@ -77,15 +77,15 @@ describe('stdDeviationBands(resp, panel, series)', () => {
};
});
test('calls next when finished', () => {
test('calls next when finished', async () => {
const next = jest.fn();
stdDeviationBands(resp, panel, series)(next)([]);
await stdDeviationBands(resp, panel, series, {})(next)([]);
expect(next.mock.calls.length).toEqual(1);
});
test('creates a series', () => {
test('creates a series', async () => {
const next = (results) => results;
const results = stdDeviationBands(resp, panel, series)(next)([]);
const results = await stdDeviationBands(resp, panel, series, {})(next)([]);
expect(results).toHaveLength(1);
expect(results[0]).toEqual({

View file

@ -19,11 +19,11 @@
import { getSplits, getLastMetric, getSiblingAggValue } from '../../helpers';
export function stdDeviationSibling(resp, panel, series, meta) {
return (next) => (results) => {
export function stdDeviationSibling(resp, panel, series, meta, extractFields) {
return (next) => async (results) => {
const metric = getLastMetric(series);
if (metric.mode === 'band' && metric.type === 'std_deviation_bucket') {
getSplits(resp, panel, series, meta).forEach((split) => {
(await getSplits(resp, panel, series, meta, extractFields)).forEach((split) => {
const data = split.timeseries.buckets.map((bucket) => [
bucket.key,
getSiblingAggValue(split, { ...metric, mode: 'upper' }),

View file

@ -77,15 +77,15 @@ describe('stdDeviationSibling(resp, panel, series)', () => {
};
});
test('calls next when finished', () => {
test('calls next when finished', async () => {
const next = jest.fn();
stdDeviationSibling(resp, panel, series)(next)([]);
await stdDeviationSibling(resp, panel, series, {})(next)([]);
expect(next.mock.calls.length).toEqual(1);
});
test('creates a series', () => {
test('creates a series', async () => {
const next = (results) => results;
const results = stdDeviationSibling(resp, panel, series)(next)([]);
const results = await stdDeviationSibling(resp, panel, series, {})(next)([]);
expect(results).toHaveLength(1);
expect(results[0]).toEqual({

View file

@ -23,8 +23,8 @@ import { getLastMetric } from '../../helpers/get_last_metric';
import { mapBucket } from '../../helpers/map_bucket';
import { METRIC_TYPES } from '../../../../../common/metric_types';
export function stdMetric(resp, panel, series, meta) {
return (next) => (results) => {
export function stdMetric(resp, panel, series, meta, extractFields) {
return (next) => async (results) => {
const metric = getLastMetric(series);
if (metric.type === METRIC_TYPES.STD_DEVIATION && metric.mode === 'band') {
return next(results);
@ -35,17 +35,20 @@ export function stdMetric(resp, panel, series, meta) {
}
if (/_bucket$/.test(metric.type)) return next(results);
const decoration = getDefaultDecoration(series);
getSplits(resp, panel, series, meta).forEach((split) => {
(await getSplits(resp, panel, series, meta, extractFields)).forEach((split) => {
const data = split.timeseries.buckets.map(mapBucket(metric));
results.push({
id: `${split.id}`,
label: split.label,
splitByLabel: split.splitByLabel,
labelFormatted: split.labelFormatted,
color: split.color,
data,
...decoration,
});
});
return next(results);
};
}

View file

@ -58,32 +58,38 @@ describe('stdMetric(resp, panel, series)', () => {
};
});
test('calls next when finished', () => {
test('calls next when finished', async () => {
const next = jest.fn();
stdMetric(resp, panel, series)(next)([]);
await stdMetric(resp, panel, series, {})(next)([]);
expect(next.mock.calls.length).toEqual(1);
});
test('calls next when finished (percentile)', () => {
test('calls next when finished (percentile)', async () => {
series.metrics[0].type = 'percentile';
const next = jest.fn((d) => d);
const results = stdMetric(resp, panel, series)(next)([]);
const results = await stdMetric(resp, panel, series, {})(next)([]);
expect(next.mock.calls.length).toEqual(1);
expect(results).toHaveLength(0);
});
test('calls next when finished (std_deviation band)', () => {
test('calls next when finished (std_deviation band)', async () => {
series.metrics[0].type = 'std_deviation';
series.metrics[0].mode = 'band';
const next = jest.fn((d) => d);
const results = stdMetric(resp, panel, series)(next)([]);
const results = await stdMetric(resp, panel, series, {})(next)([]);
expect(next.mock.calls.length).toEqual(1);
expect(results).toHaveLength(0);
});
test('creates a series', () => {
test('creates a series', async () => {
const next = (results) => results;
const results = stdMetric(resp, panel, series)(next)([]);
const results = await stdMetric(resp, panel, series, {})(next)([]);
expect(results).toHaveLength(1);
expect(results[0]).toHaveProperty('color', 'rgb(255, 0, 0)');
expect(results[0]).toHaveProperty('id', 'test');

View file

@ -22,15 +22,15 @@ import { getSplits } from '../../helpers/get_splits';
import { getLastMetric } from '../../helpers/get_last_metric';
import { getSiblingAggValue } from '../../helpers/get_sibling_agg_value';
export function stdSibling(resp, panel, series, meta) {
return (next) => (results) => {
export function stdSibling(resp, panel, series, meta, extractFields) {
return (next) => async (results) => {
const metric = getLastMetric(series);
if (!/_bucket$/.test(metric.type)) return next(results);
if (metric.type === 'std_deviation_bucket' && metric.mode === 'band') return next(results);
const decoration = getDefaultDecoration(series);
getSplits(resp, panel, series, meta).forEach((split) => {
(await getSplits(resp, panel, series, meta, extractFields)).forEach((split) => {
const data = split.timeseries.buckets.map((bucket) => {
return [bucket.key, getSiblingAggValue(split, metric)];
});
@ -42,6 +42,7 @@ export function stdSibling(resp, panel, series, meta) {
...decoration,
});
});
return next(results);
};
}

View file

@ -72,23 +72,23 @@ describe('stdSibling(resp, panel, series)', () => {
};
});
test('calls next when finished', () => {
test('calls next when finished', async () => {
const next = jest.fn();
stdSibling(resp, panel, series)(next)([]);
await stdSibling(resp, panel, series, {})(next)([]);
expect(next.mock.calls.length).toEqual(1);
});
test('calls next when std. deviation bands set', () => {
test('calls next when std. deviation bands set', async () => {
series.metrics[1].mode = 'band';
const next = jest.fn((results) => results);
const results = stdSibling(resp, panel, series)(next)([]);
const results = await stdSibling(resp, panel, series, {})(next)([]);
expect(next.mock.calls.length).toEqual(1);
expect(results).toHaveLength(0);
});
test('creates a series', () => {
test('creates a series', async () => {
const next = (results) => results;
const results = stdSibling(resp, panel, series)(next)([]);
const results = await stdSibling(resp, panel, series, {})(next)([]);
expect(results).toHaveLength(1);
expect(results[0]).toEqual({

View file

@ -60,15 +60,17 @@ describe('timeShift(resp, panel, series)', () => {
};
});
test('calls next when finished', () => {
test('calls next when finished', async () => {
const next = jest.fn();
timeShift(resp, panel, series)(next)([]);
await timeShift(resp, panel, series, {})(next)([]);
expect(next.mock.calls.length).toEqual(1);
});
test('creates a series', () => {
const next = timeShift(resp, panel, series)((results) => results);
const results = stdMetric(resp, panel, series)(next)([]);
test('creates a series', async () => {
const next = await timeShift(resp, panel, series, {})((results) => results);
const results = await stdMetric(resp, panel, series, {})(next)([]);
expect(results).toHaveLength(1);
expect(results[0]).toHaveProperty('color', 'rgb(255, 0, 0)');
expect(results[0]).toHaveProperty('id', 'test');

View file

@ -17,7 +17,6 @@
* under the License.
*/
// import percentile from './percentile';
import { stdMetric } from './std_metric';
import { stdSibling } from './std_sibling';
import { seriesAgg } from './series_agg';

View file

@ -22,8 +22,8 @@ import { getLastMetric } from '../../helpers/get_last_metric';
import { toPercentileNumber } from '../../../../../common/to_percentile_number';
import { METRIC_TYPES } from '../../../../../common/metric_types';
export function percentile(bucket, panel, series) {
return (next) => (results) => {
export function percentile(bucket, panel, series, meta, extractFields) {
return (next) => async (results) => {
const metric = getLastMetric(series);
if (metric.type !== METRIC_TYPES.PERCENTILE) {
@ -34,7 +34,7 @@ export function percentile(bucket, panel, series) {
aggregations: bucket,
};
getSplits(fakeResp, panel, series).forEach((split) => {
(await getSplits(fakeResp, panel, series, meta, extractFields)).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);
const percentileKey = toPercentileNumber(percentile.value);
@ -45,6 +45,7 @@ export function percentile(bucket, panel, series) {
results.push({
id: split.id,
label: `${split.label} (${percentile.value ?? 0})`,
data,
});
});

View file

@ -23,8 +23,8 @@ import { toPercentileNumber } from '../../../../../common/to_percentile_number';
import { getAggValue } from '../../helpers/get_agg_value';
import { METRIC_TYPES } from '../../../../../common/metric_types';
export function percentileRank(bucket, panel, series) {
return (next) => (results) => {
export function percentileRank(bucket, panel, series, meta, extractFields) {
return (next) => async (results) => {
const metric = getLastMetric(series);
if (metric.type !== METRIC_TYPES.PERCENTILE_RANK) {
@ -35,7 +35,7 @@ export function percentileRank(bucket, panel, series) {
aggregations: bucket,
};
getSplits(fakeResp, panel, series).forEach((split) => {
(await getSplits(fakeResp, panel, series, meta, extractFields)).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);
@ -51,7 +51,7 @@ export function percentileRank(bucket, panel, series) {
results.push({
data,
id: split.id,
label: `${split.label} (${percentileRank || 0})`,
label: `${split.label} (${lastRankValue ?? 0})`,
});
});

View file

@ -21,8 +21,8 @@ import { SeriesAgg } from './_series_agg';
import _ from 'lodash';
import { calculateLabel } from '../../../../../common/calculate_label';
export function seriesAgg(resp, panel, series) {
return (next) => (results) => {
export function seriesAgg(resp, panel, series, meta, extractFields) {
return (next) => async (results) => {
if (series.aggregate_by && series.aggregate_function) {
const targetSeries = [];
// Filter out the seires with the matching metric and store them
@ -36,9 +36,14 @@ export function seriesAgg(resp, panel, series) {
});
const fn = SeriesAgg[series.aggregate_function];
const data = fn(targetSeries);
const fieldsForMetaIndex = meta.index ? await extractFields(meta.index) : [];
results.push({
id: `${series.id}`,
label: series.label || calculateLabel(_.last(series.metrics), series.metrics),
label:
series.label ||
calculateLabel(_.last(series.metrics), series.metrics, fieldsForMetaIndex),
data: _.first(data),
});
}

View file

@ -22,8 +22,8 @@ import { getLastMetric } from '../../helpers/get_last_metric';
import { mapBucket } from '../../helpers/map_bucket';
import { METRIC_TYPES } from '../../../../../common/metric_types';
export function stdMetric(bucket, panel, series) {
return (next) => (results) => {
export function stdMetric(bucket, panel, series, meta, extractFields) {
return (next) => async (results) => {
const metric = getLastMetric(series);
if (metric.type === METRIC_TYPES.STD_DEVIATION && metric.mode === 'band') {
@ -42,7 +42,7 @@ export function stdMetric(bucket, panel, series) {
aggregations: bucket,
};
getSplits(fakeResp, panel, series).forEach((split) => {
(await getSplits(fakeResp, panel, series, meta, extractFields)).forEach((split) => {
const data = split.timeseries.buckets.map(mapBucket(metric));
results.push({
id: split.id,

View file

@ -21,15 +21,15 @@ import { getSplits } from '../../helpers/get_splits';
import { getLastMetric } from '../../helpers/get_last_metric';
import { getSiblingAggValue } from '../../helpers/get_sibling_agg_value';
export function stdSibling(bucket, panel, series) {
return (next) => (results) => {
export function stdSibling(bucket, panel, series, meta, extractFields) {
return (next) => async (results) => {
const metric = getLastMetric(series);
if (!/_bucket$/.test(metric.type)) return next(results);
if (metric.type === 'std_deviation_bucket' && metric.mode === 'band') return next(results);
const fakeResp = { aggregations: bucket };
getSplits(fakeResp, panel, series).forEach((split) => {
(await getSplits(fakeResp, panel, series, meta, extractFields)).forEach((split) => {
const data = split.timeseries.buckets.map((b) => {
return [b.key, getSiblingAggValue(split, metric)];
});

View file

@ -78,7 +78,7 @@ const body = JSON.parse(`
`);
describe('buildRequestBody(req)', () => {
test('returns a valid body', () => {
test('returns a valid body', async () => {
const panel = body.panels[0];
const series = panel.series[0];
const getValidTimeInterval = jest.fn(() => '10s');
@ -91,14 +91,16 @@ describe('buildRequestBody(req)', () => {
queryStringOptions: {},
};
const indexPatternObject = {};
const doc = buildRequestBody(
const doc = await buildRequestBody(
{ payload: body },
panel,
series,
config,
indexPatternObject,
capabilities,
{ barTargetUiSettings: 50 }
{
get: async () => 50,
}
);
expect(doc).toEqual({

View file

@ -34,8 +34,8 @@ import { processors } from '../request_processors/series/index';
* ]
* @returns {Object} doc - processed body
*/
export function buildRequestBody(...args: any[]) {
export async function buildRequestBody(...args: any[]) {
const processor = buildProcessorFunction(processors, ...args);
const doc = processor({});
const doc = await processor({});
return doc;
}

Some files were not shown because too many files have changed in this diff Show more