[TSVB] Fix the broken "aggregate function" in TSVB table (#119967)

* [TSVB] Fix the broken "aggregate function" in TSVB table

Closes: #91149

* [TSVB] Table series filter and aggregation function applied at the same time cause an error

# Conflicts:
#	src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/split_by_everything.ts
#	src/plugins/vis_types/timeseries/server/lib/vis_data/request_processors/table/split_by_terms.ts

* some work

* filter terms columns

* fix error message on no pivot_id

* fix CI

* enable aggregation function for entire timerange

* fix PR comments

* update check_aggs

* fix series aggs for table

* unify error messages

* fix pr comment: restrictions: UIRestrictions = DEFAULT_UI_RESTRICTION

* fix i18n translation error

* fixes translations

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Alexey Antonov 2022-01-04 15:27:46 +03:00 committed by GitHub
parent 348bfb8b33
commit 31b805a314
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 338 additions and 249 deletions

View file

@ -7,16 +7,24 @@
*/
import { get } from 'lodash';
import { RESTRICTIONS_KEYS, DEFAULT_UI_RESTRICTION } from '../../../common/ui_restrictions';
import {
RESTRICTIONS_KEYS,
DEFAULT_UI_RESTRICTION,
UIRestrictions,
TimeseriesUIRestrictions,
} from './ui_restrictions';
/**
* Generic method for checking all types of the UI Restrictions
* @private
*/
const checkUIRestrictions = (key, restrictions = DEFAULT_UI_RESTRICTION, type) => {
const checkUIRestrictions = (
key: string,
type: string,
restrictions: UIRestrictions = DEFAULT_UI_RESTRICTION
) => {
const isAllEnabled = get(restrictions, `${type}.*`, true);
return isAllEnabled || Boolean(get(restrictions, type, {})[key]);
return isAllEnabled || Boolean(get(restrictions, [type, key], false));
};
/**
@ -27,8 +35,11 @@ const checkUIRestrictions = (key, restrictions = DEFAULT_UI_RESTRICTION, type) =
* @param restrictions - uiRestrictions object. Comes from the /data request.
* @return {boolean}
*/
export const isMetricEnabled = (key, restrictions) => {
return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.WHITE_LISTED_METRICS);
export const isMetricEnabled = (
key: string,
restrictions: TimeseriesUIRestrictions | undefined
) => {
return checkUIRestrictions(key, RESTRICTIONS_KEYS.WHITE_LISTED_METRICS, restrictions);
};
/**
@ -40,12 +51,16 @@ export const isMetricEnabled = (key, restrictions) => {
* @param restrictions - uiRestrictions object. Comes from the /data request.
* @return {boolean}
*/
export const isFieldEnabled = (field, metricType, restrictions = DEFAULT_UI_RESTRICTION) => {
export const isFieldEnabled = (
field: string,
metricType: string,
restrictions?: TimeseriesUIRestrictions
) => {
if (isMetricEnabled(metricType, restrictions)) {
return checkUIRestrictions(
field,
restrictions[RESTRICTIONS_KEYS.WHITE_LISTED_METRICS],
metricType
metricType,
restrictions?.[RESTRICTIONS_KEYS.WHITE_LISTED_METRICS]
);
}
return false;
@ -60,8 +75,8 @@ export const isFieldEnabled = (field, metricType, restrictions = DEFAULT_UI_REST
* @param restrictions - uiRestrictions object. Comes from the /data request.
* @return {boolean}
*/
export const isGroupByFieldsEnabled = (key, restrictions) => {
return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS);
export const isGroupByFieldsEnabled = (key: string, restrictions: TimeseriesUIRestrictions) => {
return checkUIRestrictions(key, RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS, restrictions);
};
/**
@ -73,6 +88,26 @@ export const isGroupByFieldsEnabled = (key, restrictions) => {
* @param restrictions - uiRestrictions object. Comes from the /data request.
* @return {boolean}
*/
export const isTimerangeModeEnabled = (key, restrictions) => {
return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.WHITE_LISTED_TIMERANGE_MODES);
export const isTimerangeModeEnabled = (key: string, restrictions: TimeseriesUIRestrictions) => {
return checkUIRestrictions(key, RESTRICTIONS_KEYS.WHITE_LISTED_TIMERANGE_MODES, restrictions);
};
/**
* Using this method, you can check whether a specific configuration feature is allowed
* for current panel configuration or not.
* @public
* @param key - string value of the time range mode.
* All available mode you can find in the following object TIME_RANGE_DATA_MODES.
* @param restrictions - uiRestrictions object. Comes from the /data request.
* @return {boolean}
*/
export const isConfigurationFeatureEnabled = (
key: string,
restrictions: TimeseriesUIRestrictions
) => {
return checkUIRestrictions(
key,
RESTRICTIONS_KEYS.WHITE_LISTED_CONFIGURATION_FEATURES,
restrictions
);
};

View file

@ -46,12 +46,37 @@ export class ValidateIntervalError extends UIError {
}
}
export class AggNotSupportedInMode extends UIError {
constructor(metricType: string, timeRangeMode: string) {
export class AggNotSupportedError extends UIError {
constructor(metricType: string) {
super(
i18n.translate('visTypeTimeseries.wrongAggregationErrorMessage', {
defaultMessage: 'The aggregation {metricType} is not supported in {timeRangeMode} mode',
values: { metricType, timeRangeMode },
defaultMessage:
'The {metricType} aggregation is not supported for existing panel configuration.',
values: { metricType },
})
);
}
}
export const filterCannotBeAppliedErrorMessage = i18n.translate(
'visTypeTimeseries.filterCannotBeAppliedError',
{
defaultMessage: 'The "filter" cannot be applied with this configuration',
}
);
export class FilterCannotBeAppliedError extends UIError {
constructor() {
super(filterCannotBeAppliedErrorMessage);
}
}
export class PivotNotSelectedForTableError extends UIError {
constructor() {
super(
i18n.translate('visTypeTimeseries.table.noResultsAvailableWithDescriptionMessage', {
defaultMessage:
'No results available. You must choose a group by field for this visualization.',
})
);
}

View file

@ -25,19 +25,27 @@ export enum RESTRICTIONS_KEYS {
WHITE_LISTED_METRICS = 'whiteListedMetrics',
/**
* Key for getting the white listed Time Range modes from the UIRestrictions object.
* Key for getting the white listed Time Range modes from the UIRestrictions object.
*/
WHITE_LISTED_TIMERANGE_MODES = 'whiteListedTimerangeModes',
/**
* Key for getting the white listed Configuration Features from the UIRestrictions object.
*/
WHITE_LISTED_CONFIGURATION_FEATURES = 'whiteListedConfigurationFeatures',
}
export interface UIRestrictions {
'*': boolean;
[restriction: string]: boolean;
[key: string]: boolean | UIRestrictions;
}
export type TimeseriesUIRestrictions = {
[key in RESTRICTIONS_KEYS]: Record<string, UIRestrictions>;
};
export interface TimeseriesUIRestrictions extends UIRestrictions {
[RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS]: UIRestrictions;
[RESTRICTIONS_KEYS.WHITE_LISTED_METRICS]: UIRestrictions;
[RESTRICTIONS_KEYS.WHITE_LISTED_TIMERANGE_MODES]: UIRestrictions;
[RESTRICTIONS_KEYS.WHITE_LISTED_CONFIGURATION_FEATURES]: UIRestrictions;
}
/**
* Default value for the UIRestriction

View file

@ -11,8 +11,7 @@ import { EuiCode } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
// @ts-ignore
import { aggToComponent } from '../lib/agg_to_component';
// @ts-ignore
import { isMetricEnabled } from '../../lib/check_ui_restrictions';
import { isMetricEnabled } from '../../../../common/check_ui_restrictions';
import { getInvalidAggComponent } from './invalid_agg';
// @ts-expect-error not typed yet
import { seriesChangeHandler } from '../lib/series_change_handler';

View file

@ -9,8 +9,8 @@
import React, { useContext } from 'react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { isMetricEnabled } from '../../lib/check_ui_restrictions';
import { isMetricEnabled } from '../../../../common/check_ui_restrictions';
import { VisDataContext } from '../../contexts/vis_data_context';
import { getAggsByType, getAggsByPredicate } from '../../../../common/agg_utils';
import type { Agg } from '../../../../common/agg_utils';
@ -64,7 +64,7 @@ export function AggSelect(props: AggSelectUiProps) {
} else {
const disableSiblingAggs = (agg: AggSelectOption) => ({
...agg,
disabled: !enablePipelines || !isMetricEnabled(agg.value, uiRestrictions),
disabled: !enablePipelines || !isMetricEnabled(agg.value as string, uiRestrictions),
});
options = [

View file

@ -18,8 +18,7 @@ import { getIndexPatternKey } from '../../../../common/index_patterns_utils';
import type { SanitizedFieldType, IndexPatternValue } from '../../../../common/types';
import type { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions';
// @ts-ignore
import { isFieldEnabled } from '../../lib/check_ui_restrictions';
import { isFieldEnabled } from '../../../../common/check_ui_restrictions';
interface FieldSelectProps {
label: string | ReactNode;

View file

@ -32,7 +32,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { PANEL_TYPES, TIME_RANGE_DATA_MODES, TIME_RANGE_MODE_KEY } from '../../../common/enums';
import { AUTO_INTERVAL } from '../../../common/constants';
import { isTimerangeModeEnabled } from '../lib/check_ui_restrictions';
import { isTimerangeModeEnabled } from '../../../common/check_ui_restrictions';
import { VisDataContext } from '../contexts/vis_data_context';
import { PanelModelContext } from '../contexts/panel_model_context';
import { FormValidationContext } from '../contexts/form_validation_context';

View file

@ -15,7 +15,7 @@ import { QueryStringInput, QueryStringInputProps } from '../../../../../../plugi
import { getDataStart } from '../../services';
import { fetchIndexPattern, isStringTypeIndexPattern } from '../../../common/index_patterns_utils';
type QueryBarWrapperProps = Pick<QueryStringInputProps, 'query' | 'onChange'> & {
type QueryBarWrapperProps = Pick<QueryStringInputProps, 'query' | 'onChange' | 'isInvalid'> & {
indexPatterns: IndexPatternValue[];
'data-test-subj'?: string;
};
@ -23,6 +23,7 @@ type QueryBarWrapperProps = Pick<QueryStringInputProps, 'query' | 'onChange'> &
export function QueryBarWrapper({
query,
onChange,
isInvalid,
indexPatterns,
'data-test-subj': dataTestSubj,
}: QueryBarWrapperProps) {
@ -64,6 +65,7 @@ export function QueryBarWrapper({
<QueryStringInput
query={query}
onChange={onChange}
isInvalid={isInvalid}
indexPatterns={indexes}
{...coreStartContext}
dataTestSubj={dataTestSubj}

View file

@ -15,7 +15,7 @@ import { SplitByFilter } from './splits/filter';
import { SplitByFilters } from './splits/filters';
import { SplitByEverything } from './splits/everything';
import { SplitUnsupported } from './splits/unsupported_split';
import { isGroupByFieldsEnabled } from '../lib/check_ui_restrictions';
import { isGroupByFieldsEnabled } from '../../../common/check_ui_restrictions';
import { getDefaultQueryLanguage } from './lib/get_default_query_language';
const SPLIT_MODES = {

View file

@ -10,7 +10,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { EuiComboBox } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n-react';
import { isGroupByFieldsEnabled } from '../../lib/check_ui_restrictions';
import { isGroupByFieldsEnabled } from '../../../../common/check_ui_restrictions';
function GroupBySelectUi(props) {
const { intl, uiRestrictions } = props;

View file

@ -35,6 +35,9 @@ import { getDefaultQueryLanguage } from '../../lib/get_default_query_language';
import { checkIfNumericMetric } from '../../lib/check_if_numeric_metric';
import { QueryBarWrapper } from '../../query_bar_wrapper';
import { DATA_FORMATTERS } from '../../../../../common/enums';
import { isConfigurationFeatureEnabled } from '../../../../../common/check_ui_restrictions';
import { filterCannotBeAppliedErrorMessage } from '../../../../../common/errors';
import { KBN_FIELD_TYPES } from '../../../../../../../data/public';
export class TableSeriesConfig extends Component {
UNSAFE_componentWillMount() {
@ -123,6 +126,9 @@ export class TableSeriesConfig extends Component {
const isKibanaIndexPattern =
this.props.panel.use_kibana_indexes || this.props.indexPatternForQuery === '';
const isFilterCannotBeApplied =
model.filter?.query && !isConfigurationFeatureEnabled('filter', this.props.uiRestrictions);
return (
<div className="tvbAggRow">
<EuiFlexGroup gutterSize="s">
@ -174,6 +180,8 @@ export class TableSeriesConfig extends Component {
defaultMessage="Filter"
/>
}
isInvalid={isFilterCannotBeApplied}
error={filterCannotBeAppliedErrorMessage}
fullWidth
>
<QueryBarWrapper
@ -181,6 +189,7 @@ export class TableSeriesConfig extends Component {
language: model?.filter?.language || getDefaultQueryLanguage(),
query: model?.filter?.query || '',
}}
isInvalid={isFilterCannotBeApplied}
onChange={(filter) => this.props.onChange({ filter })}
indexPatterns={[this.props.indexPatternForQuery]}
/>
@ -214,6 +223,15 @@ export class TableSeriesConfig extends Component {
value={model.aggregate_by}
onChange={handleSelectChange('aggregate_by')}
fullWidth
restrict={[
KBN_FIELD_TYPES.NUMBER,
KBN_FIELD_TYPES.BOOLEAN,
KBN_FIELD_TYPES.DATE,
KBN_FIELD_TYPES.IP,
KBN_FIELD_TYPES.STRING,
]}
uiRestrictions={this.props.uiRestrictions}
type={'terms'}
/>
</EuiFlexItem>
<EuiFlexItem grow={true}>
@ -268,4 +286,5 @@ TableSeriesConfig.propTypes = {
model: PropTypes.object,
onChange: PropTypes.func,
indexPatternForQuery: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
uiRestrictions: PropTypes.object,
};

View file

@ -70,6 +70,7 @@ function TableSeriesUI(props) {
model={props.model}
onChange={props.onChange}
indexPatternForQuery={props.indexPatternForQuery}
uiRestrictions={props.uiRestrictions}
/>
);
}

View file

@ -240,31 +240,15 @@ class TableVis extends Component {
closeExternalUrlErrorModal = () => this.setState({ accessDeniedDrilldownUrl: null });
render() {
const { visData, model } = this.props;
const { visData } = this.props;
const { accessDeniedDrilldownUrl } = this.state;
const header = this.renderHeader();
let rows;
let rows = null;
if (isArray(visData.series) && visData.series.length) {
rows = visData.series.map(this.renderRow);
} else {
const message = model.pivot_id ? (
<FormattedMessage
id="visTypeTimeseries.table.noResultsAvailableMessage"
defaultMessage="No results available."
/>
) : (
<FormattedMessage
id="visTypeTimeseries.table.noResultsAvailableWithDescriptionMessage"
defaultMessage="No results available. You must choose a group by field for this visualization."
/>
);
rows = (
<tr>
<td colSpan={this.visibleSeries.length + 1}>{message}</td>
</tr>
);
}
return (
<>
<RedirectAppLinks

View file

@ -33,6 +33,7 @@ describe('DefaultSearchCapabilities', () => {
whiteListedMetrics: { '*': true },
whiteListedGroupByFields: { '*': true },
whiteListedTimerangeModes: { '*': true },
whiteListedConfigurationFeatures: { '*': true },
});
});

View file

@ -12,7 +12,7 @@ import {
parseInterval,
getSuitableUnit,
} from '../../vis_data/helpers/unit_to_seconds';
import { RESTRICTIONS_KEYS } from '../../../../common/ui_restrictions';
import { RESTRICTIONS_KEYS, TimeseriesUIRestrictions } from '../../../../common/ui_restrictions';
import {
TIME_RANGE_DATA_MODES,
PANEL_TYPES,
@ -28,6 +28,17 @@ export interface SearchCapabilitiesOptions {
panel?: Panel;
}
const convertAggsToRestriction = (allAvailableAggs: string[]) =>
allAvailableAggs.reduce(
(availableAggs, aggType) => ({
...availableAggs,
[aggType]: {
'*': true,
},
}),
{}
);
export class DefaultSearchCapabilities {
public timezone: SearchCapabilitiesOptions['timezone'];
public maxBucketsLimit: SearchCapabilitiesOptions['maxBucketsLimit'];
@ -44,30 +55,35 @@ export class DefaultSearchCapabilities {
}
public get whiteListedMetrics() {
if (
this.panel &&
this.panel.type !== PANEL_TYPES.TIMESERIES &&
this.panel.time_range_mode === TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE
) {
if (this.panel) {
const aggs = getAggsByType<string>((agg) => agg.id);
const allAvailableAggs = [
...aggs[AGG_TYPE.METRIC],
...aggs[AGG_TYPE.SIBLING_PIPELINE],
TSVB_METRIC_TYPES.MATH,
TSVB_METRIC_TYPES.CALCULATION,
BUCKET_TYPES.TERMS,
// SERIES_AGG should be blocked for table
...(this.panel.type === PANEL_TYPES.TABLE ? [] : [TSVB_METRIC_TYPES.SERIES_AGG]),
].reduce(
(availableAggs, aggType) => ({
...availableAggs,
[aggType]: {
'*': true,
},
}),
{}
);
return this.createUiRestriction(allAvailableAggs);
if (
this.panel.type !== PANEL_TYPES.TIMESERIES &&
this.panel.time_range_mode === TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE
) {
return this.createUiRestriction(
convertAggsToRestriction([
...aggs[AGG_TYPE.METRIC],
...aggs[AGG_TYPE.SIBLING_PIPELINE],
TSVB_METRIC_TYPES.MATH,
TSVB_METRIC_TYPES.CALCULATION,
BUCKET_TYPES.TERMS,
// SERIES_AGG should be blocked for table
...(this.panel.type === PANEL_TYPES.TABLE ? [] : [TSVB_METRIC_TYPES.SERIES_AGG]),
])
);
}
if (this.panel?.type === PANEL_TYPES.TABLE) {
return this.createUiRestriction(
convertAggsToRestriction(
[...Object.values(aggs).flat(), BUCKET_TYPES.TERMS].filter(
(item) => item !== TSVB_METRIC_TYPES.SERIES_AGG
)
)
);
}
}
return this.createUiRestriction();
}
@ -80,12 +96,18 @@ export class DefaultSearchCapabilities {
return this.createUiRestriction();
}
public get whiteListedConfigurationFeatures() {
return this.createUiRestriction();
}
public get uiRestrictions() {
return {
[RESTRICTIONS_KEYS.WHITE_LISTED_METRICS]: this.whiteListedMetrics,
[RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS]: this.whiteListedGroupByFields,
[RESTRICTIONS_KEYS.WHITE_LISTED_TIMERANGE_MODES]: this.whiteListedTimerangeModes,
};
[RESTRICTIONS_KEYS.WHITE_LISTED_CONFIGURATION_FEATURES]:
this.whiteListedConfigurationFeatures,
} as TimeseriesUIRestrictions;
}
createUiRestriction(restrictionsObject?: Record<string, any>) {

View file

@ -84,6 +84,12 @@ export class RollupSearchCapabilities extends DefaultSearchCapabilities {
});
}
public get whiteListedConfigurationFeatures() {
return this.createUiRestriction({
filter: false,
});
}
getValidTimeInterval(userIntervalString: string) {
const parsedRollupJobInterval = this.parseInterval(this.defaultTimeInterval);
const inRollupJobUnit = this.convertIntervalToUnit(

View file

@ -14,7 +14,7 @@ import { handleResponseBody } from './series/handle_response_body';
import { getSeriesRequestParams } from './series/get_request_params';
import { getActiveSeries } from './helpers/get_active_series';
import { isAggSupported } from './helpers/check_aggs';
import { isEntireTimeRangeMode } from './helpers/get_timerange_mode';
import type {
VisTypeTimeseriesRequestHandlerContext,
VisTypeTimeseriesVisDataRequest,
@ -61,10 +61,7 @@ export async function getSeriesData(
try {
const bodiesPromises = getActiveSeries(panel).map((series) => {
if (isEntireTimeRangeMode(panel, series)) {
isAggSupported(series.metrics, capabilities);
}
isAggSupported(series.metrics, capabilities);
return getSeriesRequestParams(req, panel, panelIndex, series, capabilities, services);
});

View file

@ -16,7 +16,8 @@ import { processBucket } from './table/process_bucket';
import { createFieldsFetcher } from '../search_strategies/lib/fields_fetcher';
import { extractFieldLabel } from '../../../common/fields_utils';
import { isAggSupported } from './helpers/check_aggs';
import { isEntireTimeRangeMode } from './helpers/get_timerange_mode';
import { isConfigurationFeatureEnabled } from '../../../common/check_ui_restrictions';
import { FilterCannotBeAppliedError, PivotNotSelectedForTableError } from '../../../common/errors';
import type {
VisTypeTimeseriesRequestHandlerContext,
@ -76,10 +77,15 @@ export async function getTableData(
const handleError = handleErrorResponse(panel);
try {
if (isEntireTimeRangeMode(panel)) {
panel.series.forEach((column) => {
isAggSupported(column.metrics, capabilities);
});
panel.series.forEach((series) => {
isAggSupported(series.metrics, capabilities);
if (series.filter?.query && !isConfigurationFeatureEnabled('filter', capabilities)) {
throw new FilterCannotBeAppliedError();
}
});
if (!panel.pivot_id) {
throw new PivotNotSelectedForTableError();
}
const body = await buildTableRequest({

View file

@ -6,37 +6,18 @@
* Side Public License, v 1.
*/
import { get } from 'lodash';
import { AggNotSupportedInMode } from '../../../../common/errors';
import { TIME_RANGE_DATA_MODES } from '../../../../common/enums';
import { DEFAULT_UI_RESTRICTION, RESTRICTIONS_KEYS } from '../../../../common/ui_restrictions';
import { AggNotSupportedError } from '../../../../common/errors';
import { isMetricEnabled } from '../../../../common/check_ui_restrictions';
import { Metric } from '../../../../common/types';
import { SearchCapabilities } from '../../search_strategies';
// @todo: will be removed in 8.1
// That logic was moved into common folder in that PR https://github.com/elastic/kibana/pull/119967
// isMetricEnabled method should be used instead. See check_ui_restrictions.ts file
const checkUIRestrictions = (key: string, restrictions: Record<string, unknown>, type: string) => {
const isAllEnabled = get(restrictions ?? DEFAULT_UI_RESTRICTION, `${type}.*`, true);
return isAllEnabled || Boolean(get(restrictions ?? DEFAULT_UI_RESTRICTION, [type, key], false));
};
import type { Metric } from '../../../../common/types';
import type { SearchCapabilities } from '../../search_strategies';
export function isAggSupported(metrics: Metric[], capabilities: SearchCapabilities) {
const metricTypes = metrics.filter(
(metric) =>
!checkUIRestrictions(
metric.type,
capabilities.uiRestrictions,
RESTRICTIONS_KEYS.WHITE_LISTED_METRICS
)
(metric) => !isMetricEnabled(metric.type, capabilities.uiRestrictions)
);
if (metricTypes.length) {
throw new AggNotSupportedInMode(
metricTypes.map((metric) => `"${metric.type}"`).join(', '),
TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE
);
throw new AggNotSupportedError(metricTypes.map((metric) => `"${metric.type}"`).join(', '));
}
}

View file

@ -7,6 +7,7 @@
*/
import { getSplits } from './get_splits';
import { Panel, Series } from '../../../../common/types';
describe('getSplits(resp, panel, series)', () => {
test('should return a splits for everything/filter group bys', async () => {
@ -19,7 +20,7 @@ describe('getSplits(resp, panel, series)', () => {
},
},
};
const panel = { type: 'timeseries' };
const panel = { type: 'timeseries' } as Panel;
const series = {
id: 'SERIES',
color: 'rgb(255, 0, 0)',
@ -28,8 +29,9 @@ describe('getSplits(resp, panel, series)', () => {
{ id: 'AVG', type: 'avg', field: 'cpu' },
{ id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' },
],
};
expect(await getSplits(resp, panel, series, undefined)).toEqual([
} as Series;
expect(await getSplits(resp, panel, series, undefined, () => {})).toEqual([
{
id: 'SERIES',
label: 'Overall Average of Average of cpu',
@ -72,9 +74,10 @@ describe('getSplits(resp, panel, series)', () => {
{ id: 'AVG', type: 'avg', field: 'cpu' },
{ id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' },
],
};
const panel = { type: 'top_n' };
expect(await getSplits(resp, panel, series)).toEqual([
} as unknown as Series;
const panel = { type: 'top_n' } as Panel;
expect(await getSplits(resp, panel, series, undefined, () => [])).toEqual([
{
id: 'SERIES:example-01',
key: 'example-01',
@ -131,9 +134,9 @@ describe('getSplits(resp, panel, series)', () => {
{ id: 'AVG', type: 'avg', field: 'cpu' },
{ id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' },
],
};
const panel = { type: 'top_n' };
expect(await getSplits(resp, panel, series)).toEqual([
} as unknown as Series;
const panel = { type: 'top_n' } as Panel;
expect(await getSplits(resp, panel, series, undefined, () => [])).toEqual([
{
id: 'SERIES:example-01',
key: 'example-01',
@ -192,10 +195,10 @@ describe('getSplits(resp, panel, series)', () => {
{ id: 'AVG', type: 'avg', field: 'cpu' },
{ id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' },
],
};
const panel = { type: 'top_n' };
} as unknown as Series;
const panel = { type: 'top_n' } as Panel;
expect(await getSplits(resp, panel, series)).toEqual([
expect(await getSplits(resp, panel, series, undefined, () => [])).toEqual([
{
id: 'SERIES:example-01',
key: 'example-01',
@ -248,10 +251,10 @@ describe('getSplits(resp, panel, series)', () => {
{ id: 'filter-2', color: '#0F0', filter: 'status_code:[300 TO *]', label: '300s' },
],
metrics: [{ id: 'COUNT', type: 'count' }],
};
const panel = { type: 'timeseries' };
} as unknown as Series;
const panel = { type: 'timeseries' } as Panel;
expect(await getSplits(resp, panel, series)).toEqual([
expect(await getSplits(resp, panel, series, undefined, () => [])).toEqual([
{
id: 'SERIES:filter-1',
key: 'filter-1',

View file

@ -42,7 +42,7 @@ export async function getSplits<TRawResponse = unknown, TMeta extends BaseMeta =
resp: TRawResponse,
panel: Panel,
series: Series,
meta: TMeta,
meta: TMeta | undefined,
extractFields: Function
): Promise<Array<SplittedData<TMeta>>> {
if (!meta) {
@ -52,12 +52,20 @@ export async function getSplits<TRawResponse = unknown, TMeta extends BaseMeta =
const color = new Color(series.color);
const metric = getLastMetric(series);
const buckets = get(resp, `aggregations.${series.id}.buckets`);
const fieldsForSeries = meta.index ? await extractFields({ id: meta.index }) : [];
const fieldsForSeries = meta?.index ? await extractFields({ id: meta.index }) : [];
const splitByLabel = calculateLabel(metric, series.metrics, fieldsForSeries);
if (buckets) {
if (Array.isArray(buckets)) {
return buckets.map((bucket) => {
if (bucket.column_filter) {
bucket = {
...bucket,
...bucket.column_filter,
};
}
bucket.id = `${series.id}:${bucket.key}`;
bucket.splitByLabel = splitByLabel;
bucket.label = formatKey(bucket.key, series);
@ -101,7 +109,7 @@ export async function getSplits<TRawResponse = unknown, TMeta extends BaseMeta =
label: series.label || splitByLabel,
color: color.string(),
...mergeObj,
meta,
meta: meta!,
},
];
}

View file

@ -25,8 +25,12 @@ function removeEmptyTopLevelAggregation(doc, series) {
if (isEmptyFilter(filter) && !hasSiblingPipelineAggregation(doc.aggs[series.id].aggs)) {
const meta = _.get(doc, `aggs.${series.id}.meta`);
overwrite(doc, `aggs`, doc.aggs[series.id].aggs);
overwrite(doc, `aggs.timeseries.meta`, meta);
overwrite(doc, `aggs.timeseries.meta`, {
...meta,
normalized: true,
});
}
return doc;

View file

@ -77,6 +77,7 @@ describe('normalizeQuery', () => {
expect(modifiedDoc.aggs.timeseries.meta).toEqual({
timeField: 'order_date',
normalized: true,
intervalString: '10s',
bucketSize: 10,
seriesId: [seriesId],

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { get } from 'lodash';
import { buildEsQuery } from '@kbn/es-query';
import { overwrite } from '../../helpers';
import type { TableRequestProcessorsFunction } from './types';
export const applyFilters: TableRequestProcessorsFunction =
({ panel, esQueryConfig, seriesIndex }) =>
(next) =>
(doc) => {
panel.series.forEach((column) => {
const hasAggregateByApplied = Boolean(column.aggregate_by && column.aggregate_function);
let filterSelector = `aggs.pivot.aggs.${column.id}.filter`;
if (hasAggregateByApplied && column.filter?.query) {
const originalAggsSelector = `aggs.pivot.aggs.${column.id}.aggs`;
const originalAggs = get(doc, originalAggsSelector);
overwrite(doc, originalAggsSelector, {
column_filter: {
aggs: originalAggs,
},
});
filterSelector = `${originalAggsSelector}.column_filter.filter`;
}
if (column.filter?.query) {
overwrite(
doc,
filterSelector,
buildEsQuery(seriesIndex.indexPattern || undefined, [column.filter], [], esQueryConfig)
);
} else {
if (!hasAggregateByApplied) {
overwrite(doc, `${filterSelector}.match_all`, {});
}
}
});
return next(doc);
};

View file

@ -6,16 +6,9 @@
* Side Public License, v 1.
*/
import { has } from 'lodash';
import type { TableSearchRequest } from '../table/types';
import type { Series } from '../../../../../common/types';
export function calculateAggRoot(doc: TableSearchRequest, column: Series) {
let aggRoot = `aggs.pivot.aggs.${column.id}.aggs`;
if (has(doc, `aggs.pivot.aggs.${column.id}.aggs.column_filter`)) {
aggRoot = `aggs.pivot.aggs.${column.id}.aggs.column_filter.aggs`;
}
return aggRoot;
return `aggs.pivot.aggs.${column.id}.aggs`;
}

View file

@ -8,7 +8,7 @@
export { pivot } from './pivot';
export { query } from './query';
export { splitByEverything } from './split_by_everything';
export { applyFilters } from './apply_filters';
export { splitByTerms } from './split_by_terms';
export { dateHistogram } from './date_histogram';
export { metricBuckets } from './metric_buckets';

View file

@ -85,6 +85,7 @@ describe('normalizeQuery', () => {
expect(modifiedDoc.aggs.pivot.aggs[seriesId].meta).toEqual({
seriesId,
normalized: true,
timeField: 'order_date',
intervalString: '10s',
bucketSize: 10,

View file

@ -41,14 +41,16 @@ export const normalizeQuery: TableRequestProcessorsFunction = () => {
};
overwrite(normalizedSeries, `${seriesId}`, agg);
overwrite(normalizedSeries, `${seriesId}.meta`, meta);
overwrite(normalizedSeries, `${seriesId}.meta`, {
...meta,
normalized: true,
});
} else {
overwrite(normalizedSeries, `${seriesId}`, value);
}
});
overwrite(doc, 'aggs.pivot.aggs', normalizedSeries);
return doc;
};
};

View file

@ -1,35 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { buildEsQuery } from '@kbn/es-query';
import { overwrite } from '../../helpers';
import type { TableRequestProcessorsFunction } from './types';
export const splitByEverything: TableRequestProcessorsFunction =
({ panel, esQueryConfig, seriesIndex }) =>
(next) =>
(doc) => {
const indexPattern = seriesIndex.indexPattern || undefined;
panel.series
.filter((c) => !(c.aggregate_by && c.aggregate_function))
.forEach((column) => {
if (column.filter) {
overwrite(
doc,
`aggs.pivot.aggs.${column.id}.filter`,
buildEsQuery(indexPattern, [column.filter], [], esQueryConfig)
);
} else {
overwrite(doc, `aggs.pivot.aggs.${column.id}.filter.match_all`, {});
}
});
return next(doc);
};

View file

@ -6,33 +6,19 @@
* Side Public License, v 1.
*/
import { buildEsQuery } from '@kbn/es-query';
import { overwrite } from '../../helpers';
import type { TableRequestProcessorsFunction } from './types';
export const splitByTerms: TableRequestProcessorsFunction = ({
panel,
esQueryConfig,
seriesIndex,
}) => {
const indexPattern = seriesIndex.indexPattern || undefined;
export const splitByTerms: TableRequestProcessorsFunction = ({ panel }) => {
return (next) => (doc) => {
panel.series
.filter((c) => c.aggregate_by && c.aggregate_function)
.forEach((column) => {
overwrite(doc, `aggs.pivot.aggs.${column.id}.terms.field`, column.aggregate_by);
overwrite(doc, `aggs.pivot.aggs.${column.id}.terms.size`, 100);
if (column.filter) {
overwrite(
doc,
`aggs.pivot.aggs.${column.id}.column_filter.filter`,
buildEsQuery(indexPattern, [column.filter], [], esQueryConfig)
);
}
});
return next(doc);
};
};

View file

@ -32,6 +32,7 @@ export interface TableRequestProcessorsParams {
export interface TableSearchRequestMeta extends BaseMeta {
panelId?: string;
timeField?: string;
normalized?: boolean;
}
export type TableSearchRequest = Record<string, any>;

View file

@ -14,13 +14,13 @@ import { dropLastBucket } from '../series/drop_last_bucket';
import type { TableResponseProcessorsFunction } from './types';
export const dropLastBucketFn: TableResponseProcessorsFunction =
({ bucket, panel, series }) =>
({ response, panel, series }) =>
(next) =>
(results) => {
const shouldDropLastBucket = isLastValueTimerangeMode(panel);
if (shouldDropLastBucket) {
const fn = dropLastBucket({ aggregations: bucket }, panel, series);
const fn = dropLastBucket(response, panel, series);
return fn(next)(results);
}

View file

@ -12,9 +12,9 @@ import { mathAgg } from '../series/math';
import type { TableResponseProcessorsFunction } from './types';
export const math: TableResponseProcessorsFunction =
({ bucket, panel, series, meta, extractFields }) =>
({ response, panel, series, meta, extractFields }) =>
(next) =>
(results) => {
const mathFn = mathAgg({ aggregations: bucket }, panel, series, meta, extractFields);
const mathFn = mathAgg(response, panel, series, meta, extractFields);
return mathFn(next)(results);
};

View file

@ -15,7 +15,7 @@ import type { TableResponseProcessorsFunction } from './types';
import type { PanelDataArray } from '../../../../../common/types/vis_data';
export const percentile: TableResponseProcessorsFunction =
({ bucket, panel, series, meta, extractFields }) =>
({ response, panel, series, meta, extractFields }) =>
(next) =>
async (results) => {
const metric = getLastMetric(series);
@ -24,11 +24,7 @@ export const percentile: TableResponseProcessorsFunction =
return next(results);
}
const fakeResp = {
aggregations: bucket,
};
(await getSplits(fakeResp, panel, series, meta, extractFields)).forEach((split) => {
(await getSplits(response, 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 lastPercentile = last(metric.percentiles)?.value ?? 0;
const percentileKey = toPercentileNumber(lastPercentile);

View file

@ -15,7 +15,7 @@ import type { TableResponseProcessorsFunction } from './types';
import type { PanelDataArray } from '../../../../../common/types/vis_data';
export const percentileRank: TableResponseProcessorsFunction =
({ bucket, panel, series, meta, extractFields }) =>
({ response, panel, series, meta, extractFields }) =>
(next) =>
async (results) => {
const metric = getLastMetric(series);
@ -24,11 +24,7 @@ export const percentileRank: TableResponseProcessorsFunction =
return next(results);
}
const fakeResp = {
aggregations: bucket,
};
(await getSplits(fakeResp, panel, series, meta, extractFields)).forEach((split) => {
(await getSplits(response, 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) ?? 0;
const lastPercentileNumber = toPercentileNumber(lastRankValue);

View file

@ -44,5 +44,6 @@ export const seriesAgg: TableResponseProcessorsFunction =
data: data[0],
});
}
return next(results);
};

View file

@ -12,7 +12,7 @@ import { TSVB_METRIC_TYPES } from '../../../../../common/enums';
import type { TableResponseProcessorsFunction } from './types';
export const stdMetric: TableResponseProcessorsFunction =
({ bucket, panel, series, meta, extractFields }) =>
({ response, panel, series, meta, extractFields }) =>
(next) =>
async (results) => {
const metric = getLastMetric(series);
@ -33,13 +33,8 @@ export const stdMetric: TableResponseProcessorsFunction =
return next(results);
}
const fakeResp = {
aggregations: bucket,
};
(await getSplits(fakeResp, panel, series, meta, extractFields)).forEach((split) => {
(await getSplits(response, panel, series, meta, extractFields)).forEach((split) => {
const data = mapEmptyToZero(metric, split.timeseries.buckets);
results.push({
id: split.id,
label: split.label,

View file

@ -12,7 +12,7 @@ import type { TableResponseProcessorsFunction } from './types';
import type { PanelDataArray } from '../../../../../common/types/vis_data';
export const stdSibling: TableResponseProcessorsFunction =
({ bucket, panel, series, meta, extractFields }) =>
({ response, panel, series, meta, extractFields }) =>
(next) =>
async (results) => {
const metric = getLastMetric(series);
@ -20,8 +20,7 @@ export const stdSibling: TableResponseProcessorsFunction =
if (!/_bucket$/.test(metric.type)) return next(results);
if (metric.type === 'std_deviation_bucket' && metric.mode === 'band') return next(results);
const fakeResp = { aggregations: bucket };
(await getSplits(fakeResp, panel, series, meta, extractFields)).forEach((split) => {
(await getSplits(response, panel, series, meta, extractFields)).forEach((split) => {
const data: PanelDataArray[] = split.timeseries.buckets.map((b) => {
return [b.key, getSiblingAggValue(split, metric)];
});

View file

@ -12,7 +12,9 @@ import type { TableSearchRequestMeta } from '../../request_processors/table/type
import type { Panel, Series, PanelData } from '../../../../../common/types';
export interface TableResponseProcessorsParams {
bucket: Record<string, unknown>;
response: {
aggregations: Record<string, any>;
};
panel: Panel;
series: Series;
meta: TableSearchRequestMeta;

View file

@ -159,6 +159,7 @@ describe('buildRequestBody(req)', () => {
},
meta: {
intervalString: '10s',
normalized: true,
seriesId: 'c9b5f9c0-e403-11e6-be91-6f7688e9fac7',
timeField: '@timestamp',
panelId: 'c9b5d2b0-e403-11e6-be91-6f7688e9fac7',

View file

@ -11,7 +11,7 @@ import {
query,
pivot,
splitByTerms,
splitByEverything,
applyFilters,
dateHistogram,
metricBuckets,
siblingBuckets,
@ -36,12 +36,12 @@ export function buildTableRequest(params: TableRequestProcessorsParams) {
query,
pivot,
splitByTerms,
splitByEverything,
dateHistogram,
metricBuckets,
siblingBuckets,
filterRatios,
positiveRate,
applyFilters,
normalizeQuery,
],
params

View file

@ -62,7 +62,9 @@ function createBuckets(series: string[]) {
timeField: 'timestamp',
seriesId,
},
buckets: createBucketsObjects(size, trend, seriesId),
timeseries: {
buckets: createBucketsObjects(size, trend, seriesId),
},
};
}
return baseObj;
@ -113,11 +115,13 @@ describe('processBucket(panel)', () => {
timeField: 'timestamp',
seriesId: SERIES_ID,
},
buckets: [
// this is a flat case, but 0/0 has not a valid number result
createValueObject(0, 0, SERIES_ID),
createValueObject(1, 0, SERIES_ID),
],
timeseries: {
buckets: [
// this is a flat case, but 0/0 has not a valid number result
createValueObject(0, 0, SERIES_ID),
createValueObject(1, 0, SERIES_ID),
],
},
},
};
const result = await bucketProcessor(bucketforNaNResult);

View file

@ -13,8 +13,9 @@ import { buildTableResponse } from './build_response_body';
import { createFieldsFetcher } from '../../search_strategies/lib/fields_fetcher';
import type { Panel } from '../../../../common/types';
import type { PanelDataArray } from '../../../../common/types/vis_data';
import type { TableSearchRequestMeta } from '../request_processors/table/types';
import { PanelDataArray } from '../../../../common/types/vis_data';
import type { TableResponseProcessorsParams } from '../response_processors/table/types';
function trendSinceLastBucket(data: PanelDataArray[]) {
if (data.length < 2) {
@ -36,23 +37,22 @@ export function processBucket({ panel, extractFields }: ProcessTableBucketParams
return async (bucket: Record<string, unknown>) => {
const resultSeries = await Promise.all(
getActiveSeries(panel).map(async (series) => {
const timeseries = get(bucket, `${series.id}.timeseries`);
const buckets = get(bucket, `${series.id}.buckets`);
let meta: TableSearchRequestMeta = {};
const response: TableResponseProcessorsParams['response'] = {
aggregations: {
[series.id]: get(bucket, `${series.id}`),
},
};
const meta = (response.aggregations[series.id]?.meta ?? {}) as TableSearchRequestMeta;
if (!timeseries && buckets) {
meta = get(bucket, `${series.id}.meta`) as TableSearchRequestMeta;
overwrite(bucket, series.id, {
meta,
timeseries: {
buckets: get(bucket, `${series.id}.buckets`),
},
if (meta.normalized && !get(response, `aggregations.${series.id}.timeseries`)) {
overwrite(response, `aggregations.${series.id}.timeseries`, {
buckets: get(bucket, `${series.id}.buckets`),
});
delete response.aggregations[series.id].buckets;
}
const [result] = await buildTableResponse({
bucket,
response,
panel,
series,
meta,

View file

@ -122,7 +122,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const error = await visualBuilder.getVisualizeError();
expect(error).to.eql(
'The aggregation "derivative" is not supported in entire_time_range mode'
'The "derivative" aggregation is not supported for existing panel configuration.'
);
});
@ -208,7 +208,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const error = await visualBuilder.getVisualizeError();
expect(error).to.eql(
'The aggregation "derivative" is not supported in entire_time_range mode'
'The "derivative" aggregation is not supported for existing panel configuration.'
);
});
@ -362,7 +362,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const error = await visualBuilder.getVisualizeError();
expect(error).to.eql(
'The aggregation "derivative" is not supported in entire_time_range mode'
'The "derivative" aggregation is not supported for existing panel configuration.'
);
});

View file

@ -731,7 +731,7 @@ export class VisualBuilderPageObject extends FtrService {
public async checkPreviewIsDisabled(): Promise<void> {
this.log.debug(`Check no data message is present`);
await this.testSubjects.existOrFail('timeseriesVis > visNoResult', { timeout: 5000 });
await this.testSubjects.existOrFail('visualization-error', { timeout: 5000 });
}
public async cloneSeries(nth: number = 0): Promise<void> {

View file

@ -4682,7 +4682,6 @@
"visTypeTimeseries.table.labelPlaceholder": "ラベル",
"visTypeTimeseries.table.maxLabel": "最高",
"visTypeTimeseries.table.minLabel": "最低",
"visTypeTimeseries.table.noResultsAvailableMessage": "結果がありません。",
"visTypeTimeseries.table.noResultsAvailableWithDescriptionMessage": "結果がありません。このビジュアライゼーションは、フィールドでグループを選択する必要があります。",
"visTypeTimeseries.table.optionsTab.dataLabel": "データ",
"visTypeTimeseries.table.optionsTab.ignoreGlobalFilterLabel": "グローバルフィルターを無視しますか?",
@ -4816,7 +4815,6 @@
"visTypeTimeseries.visPicker.tableLabel": "表",
"visTypeTimeseries.visPicker.timeSeriesLabel": "時系列",
"visTypeTimeseries.visPicker.topNLabel": "トップ N",
"visTypeTimeseries.wrongAggregationErrorMessage": "アグリゲーション{metricType}は{timeRangeMode}モードでサポートされていません",
"visTypeTimeseries.yesButtonLabel": "はい",
"visTypeVega.editor.formatError": "仕様のフォーマット中にエラーが発生",
"visTypeVega.editor.reformatAsHJSONButtonLabel": "HJSON に変換",

View file

@ -4715,7 +4715,6 @@
"visTypeTimeseries.table.labelPlaceholder": "标签",
"visTypeTimeseries.table.maxLabel": "最大值",
"visTypeTimeseries.table.minLabel": "最小值",
"visTypeTimeseries.table.noResultsAvailableMessage": "没有可用结果。",
"visTypeTimeseries.table.noResultsAvailableWithDescriptionMessage": "没有可用结果。必须为此可视化选择分组依据字段。",
"visTypeTimeseries.table.optionsTab.dataLabel": "数据",
"visTypeTimeseries.table.optionsTab.ignoreGlobalFilterLabel": "忽略全局筛选?",
@ -4849,7 +4848,6 @@
"visTypeTimeseries.visPicker.tableLabel": "表",
"visTypeTimeseries.visPicker.timeSeriesLabel": "时间序列",
"visTypeTimeseries.visPicker.topNLabel": "排名前 N",
"visTypeTimeseries.wrongAggregationErrorMessage": "{timeRangeMode} 模式下不支持聚合 {metricType}",
"visTypeTimeseries.yesButtonLabel": "是",
"visTypeVega.editor.formatError": "格式化规范时出错",
"visTypeVega.editor.reformatAsHJSONButtonLabel": "重新格式化为 HJSON",