[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 { 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 * Generic method for checking all types of the UI Restrictions
* @private * @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); const isAllEnabled = get(restrictions, `${type}.*`, true);
return isAllEnabled || Boolean(get(restrictions, [type, key], false));
return isAllEnabled || Boolean(get(restrictions, type, {})[key]);
}; };
/** /**
@ -27,8 +35,11 @@ const checkUIRestrictions = (key, restrictions = DEFAULT_UI_RESTRICTION, type) =
* @param restrictions - uiRestrictions object. Comes from the /data request. * @param restrictions - uiRestrictions object. Comes from the /data request.
* @return {boolean} * @return {boolean}
*/ */
export const isMetricEnabled = (key, restrictions) => { export const isMetricEnabled = (
return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.WHITE_LISTED_METRICS); 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. * @param restrictions - uiRestrictions object. Comes from the /data request.
* @return {boolean} * @return {boolean}
*/ */
export const isFieldEnabled = (field, metricType, restrictions = DEFAULT_UI_RESTRICTION) => { export const isFieldEnabled = (
field: string,
metricType: string,
restrictions?: TimeseriesUIRestrictions
) => {
if (isMetricEnabled(metricType, restrictions)) { if (isMetricEnabled(metricType, restrictions)) {
return checkUIRestrictions( return checkUIRestrictions(
field, field,
restrictions[RESTRICTIONS_KEYS.WHITE_LISTED_METRICS], metricType,
metricType restrictions?.[RESTRICTIONS_KEYS.WHITE_LISTED_METRICS]
); );
} }
return false; return false;
@ -60,8 +75,8 @@ export const isFieldEnabled = (field, metricType, restrictions = DEFAULT_UI_REST
* @param restrictions - uiRestrictions object. Comes from the /data request. * @param restrictions - uiRestrictions object. Comes from the /data request.
* @return {boolean} * @return {boolean}
*/ */
export const isGroupByFieldsEnabled = (key, restrictions) => { export const isGroupByFieldsEnabled = (key: string, restrictions: TimeseriesUIRestrictions) => {
return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS); 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. * @param restrictions - uiRestrictions object. Comes from the /data request.
* @return {boolean} * @return {boolean}
*/ */
export const isTimerangeModeEnabled = (key, restrictions) => { export const isTimerangeModeEnabled = (key: string, restrictions: TimeseriesUIRestrictions) => {
return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.WHITE_LISTED_TIMERANGE_MODES); 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 { export class AggNotSupportedError extends UIError {
constructor(metricType: string, timeRangeMode: string) { constructor(metricType: string) {
super( super(
i18n.translate('visTypeTimeseries.wrongAggregationErrorMessage', { i18n.translate('visTypeTimeseries.wrongAggregationErrorMessage', {
defaultMessage: 'The aggregation {metricType} is not supported in {timeRangeMode} mode', defaultMessage:
values: { metricType, timeRangeMode }, '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', 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', 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 { export interface UIRestrictions {
'*': boolean; '*': boolean;
[restriction: string]: boolean; [key: string]: boolean | UIRestrictions;
} }
export type TimeseriesUIRestrictions = { export interface TimeseriesUIRestrictions extends UIRestrictions {
[key in RESTRICTIONS_KEYS]: Record<string, 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 * Default value for the UIRestriction

View file

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

View file

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

View file

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

View file

@ -32,7 +32,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { PANEL_TYPES, TIME_RANGE_DATA_MODES, TIME_RANGE_MODE_KEY } from '../../../common/enums'; import { PANEL_TYPES, TIME_RANGE_DATA_MODES, TIME_RANGE_MODE_KEY } from '../../../common/enums';
import { AUTO_INTERVAL } from '../../../common/constants'; 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 { VisDataContext } from '../contexts/vis_data_context';
import { PanelModelContext } from '../contexts/panel_model_context'; import { PanelModelContext } from '../contexts/panel_model_context';
import { FormValidationContext } from '../contexts/form_validation_context'; import { FormValidationContext } from '../contexts/form_validation_context';

View file

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

View file

@ -15,7 +15,7 @@ import { SplitByFilter } from './splits/filter';
import { SplitByFilters } from './splits/filters'; import { SplitByFilters } from './splits/filters';
import { SplitByEverything } from './splits/everything'; import { SplitByEverything } from './splits/everything';
import { SplitUnsupported } from './splits/unsupported_split'; 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'; import { getDefaultQueryLanguage } from './lib/get_default_query_language';
const SPLIT_MODES = { const SPLIT_MODES = {

View file

@ -10,7 +10,7 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { EuiComboBox } from '@elastic/eui'; import { EuiComboBox } from '@elastic/eui';
import { injectI18n } from '@kbn/i18n-react'; import { injectI18n } from '@kbn/i18n-react';
import { isGroupByFieldsEnabled } from '../../lib/check_ui_restrictions'; import { isGroupByFieldsEnabled } from '../../../../common/check_ui_restrictions';
function GroupBySelectUi(props) { function GroupBySelectUi(props) {
const { intl, uiRestrictions } = 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 { checkIfNumericMetric } from '../../lib/check_if_numeric_metric';
import { QueryBarWrapper } from '../../query_bar_wrapper'; import { QueryBarWrapper } from '../../query_bar_wrapper';
import { DATA_FORMATTERS } from '../../../../../common/enums'; 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 { export class TableSeriesConfig extends Component {
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
@ -123,6 +126,9 @@ export class TableSeriesConfig extends Component {
const isKibanaIndexPattern = const isKibanaIndexPattern =
this.props.panel.use_kibana_indexes || this.props.indexPatternForQuery === ''; this.props.panel.use_kibana_indexes || this.props.indexPatternForQuery === '';
const isFilterCannotBeApplied =
model.filter?.query && !isConfigurationFeatureEnabled('filter', this.props.uiRestrictions);
return ( return (
<div className="tvbAggRow"> <div className="tvbAggRow">
<EuiFlexGroup gutterSize="s"> <EuiFlexGroup gutterSize="s">
@ -174,6 +180,8 @@ export class TableSeriesConfig extends Component {
defaultMessage="Filter" defaultMessage="Filter"
/> />
} }
isInvalid={isFilterCannotBeApplied}
error={filterCannotBeAppliedErrorMessage}
fullWidth fullWidth
> >
<QueryBarWrapper <QueryBarWrapper
@ -181,6 +189,7 @@ export class TableSeriesConfig extends Component {
language: model?.filter?.language || getDefaultQueryLanguage(), language: model?.filter?.language || getDefaultQueryLanguage(),
query: model?.filter?.query || '', query: model?.filter?.query || '',
}} }}
isInvalid={isFilterCannotBeApplied}
onChange={(filter) => this.props.onChange({ filter })} onChange={(filter) => this.props.onChange({ filter })}
indexPatterns={[this.props.indexPatternForQuery]} indexPatterns={[this.props.indexPatternForQuery]}
/> />
@ -214,6 +223,15 @@ export class TableSeriesConfig extends Component {
value={model.aggregate_by} value={model.aggregate_by}
onChange={handleSelectChange('aggregate_by')} onChange={handleSelectChange('aggregate_by')}
fullWidth 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>
<EuiFlexItem grow={true}> <EuiFlexItem grow={true}>
@ -268,4 +286,5 @@ TableSeriesConfig.propTypes = {
model: PropTypes.object, model: PropTypes.object,
onChange: PropTypes.func, onChange: PropTypes.func,
indexPatternForQuery: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), indexPatternForQuery: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
uiRestrictions: PropTypes.object,
}; };

View file

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

View file

@ -240,31 +240,15 @@ class TableVis extends Component {
closeExternalUrlErrorModal = () => this.setState({ accessDeniedDrilldownUrl: null }); closeExternalUrlErrorModal = () => this.setState({ accessDeniedDrilldownUrl: null });
render() { render() {
const { visData, model } = this.props; const { visData } = this.props;
const { accessDeniedDrilldownUrl } = this.state; const { accessDeniedDrilldownUrl } = this.state;
const header = this.renderHeader(); const header = this.renderHeader();
let rows; let rows = null;
if (isArray(visData.series) && visData.series.length) { if (isArray(visData.series) && visData.series.length) {
rows = visData.series.map(this.renderRow); 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 ( return (
<> <>
<RedirectAppLinks <RedirectAppLinks

View file

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

View file

@ -12,7 +12,7 @@ import {
parseInterval, parseInterval,
getSuitableUnit, getSuitableUnit,
} from '../../vis_data/helpers/unit_to_seconds'; } from '../../vis_data/helpers/unit_to_seconds';
import { RESTRICTIONS_KEYS } from '../../../../common/ui_restrictions'; import { RESTRICTIONS_KEYS, TimeseriesUIRestrictions } from '../../../../common/ui_restrictions';
import { import {
TIME_RANGE_DATA_MODES, TIME_RANGE_DATA_MODES,
PANEL_TYPES, PANEL_TYPES,
@ -28,6 +28,17 @@ export interface SearchCapabilitiesOptions {
panel?: Panel; panel?: Panel;
} }
const convertAggsToRestriction = (allAvailableAggs: string[]) =>
allAvailableAggs.reduce(
(availableAggs, aggType) => ({
...availableAggs,
[aggType]: {
'*': true,
},
}),
{}
);
export class DefaultSearchCapabilities { export class DefaultSearchCapabilities {
public timezone: SearchCapabilitiesOptions['timezone']; public timezone: SearchCapabilitiesOptions['timezone'];
public maxBucketsLimit: SearchCapabilitiesOptions['maxBucketsLimit']; public maxBucketsLimit: SearchCapabilitiesOptions['maxBucketsLimit'];
@ -44,30 +55,35 @@ export class DefaultSearchCapabilities {
} }
public get whiteListedMetrics() { public get whiteListedMetrics() {
if ( if (this.panel) {
this.panel &&
this.panel.type !== PANEL_TYPES.TIMESERIES &&
this.panel.time_range_mode === TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE
) {
const aggs = getAggsByType<string>((agg) => agg.id); const aggs = getAggsByType<string>((agg) => agg.id);
const allAvailableAggs = [
...aggs[AGG_TYPE.METRIC], if (
...aggs[AGG_TYPE.SIBLING_PIPELINE], this.panel.type !== PANEL_TYPES.TIMESERIES &&
TSVB_METRIC_TYPES.MATH, this.panel.time_range_mode === TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE
TSVB_METRIC_TYPES.CALCULATION, ) {
BUCKET_TYPES.TERMS, return this.createUiRestriction(
// SERIES_AGG should be blocked for table convertAggsToRestriction([
...(this.panel.type === PANEL_TYPES.TABLE ? [] : [TSVB_METRIC_TYPES.SERIES_AGG]), ...aggs[AGG_TYPE.METRIC],
].reduce( ...aggs[AGG_TYPE.SIBLING_PIPELINE],
(availableAggs, aggType) => ({ TSVB_METRIC_TYPES.MATH,
...availableAggs, TSVB_METRIC_TYPES.CALCULATION,
[aggType]: { BUCKET_TYPES.TERMS,
'*': true, // SERIES_AGG should be blocked for table
}, ...(this.panel.type === PANEL_TYPES.TABLE ? [] : [TSVB_METRIC_TYPES.SERIES_AGG]),
}), ])
{} );
); }
return this.createUiRestriction(allAvailableAggs);
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(); return this.createUiRestriction();
} }
@ -80,12 +96,18 @@ export class DefaultSearchCapabilities {
return this.createUiRestriction(); return this.createUiRestriction();
} }
public get whiteListedConfigurationFeatures() {
return this.createUiRestriction();
}
public get uiRestrictions() { public get uiRestrictions() {
return { return {
[RESTRICTIONS_KEYS.WHITE_LISTED_METRICS]: this.whiteListedMetrics, [RESTRICTIONS_KEYS.WHITE_LISTED_METRICS]: this.whiteListedMetrics,
[RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS]: this.whiteListedGroupByFields, [RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS]: this.whiteListedGroupByFields,
[RESTRICTIONS_KEYS.WHITE_LISTED_TIMERANGE_MODES]: this.whiteListedTimerangeModes, [RESTRICTIONS_KEYS.WHITE_LISTED_TIMERANGE_MODES]: this.whiteListedTimerangeModes,
}; [RESTRICTIONS_KEYS.WHITE_LISTED_CONFIGURATION_FEATURES]:
this.whiteListedConfigurationFeatures,
} as TimeseriesUIRestrictions;
} }
createUiRestriction(restrictionsObject?: Record<string, any>) { 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) { getValidTimeInterval(userIntervalString: string) {
const parsedRollupJobInterval = this.parseInterval(this.defaultTimeInterval); const parsedRollupJobInterval = this.parseInterval(this.defaultTimeInterval);
const inRollupJobUnit = this.convertIntervalToUnit( 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 { getSeriesRequestParams } from './series/get_request_params';
import { getActiveSeries } from './helpers/get_active_series'; import { getActiveSeries } from './helpers/get_active_series';
import { isAggSupported } from './helpers/check_aggs'; import { isAggSupported } from './helpers/check_aggs';
import { isEntireTimeRangeMode } from './helpers/get_timerange_mode';
import type { import type {
VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesRequestHandlerContext,
VisTypeTimeseriesVisDataRequest, VisTypeTimeseriesVisDataRequest,
@ -61,10 +61,7 @@ export async function getSeriesData(
try { try {
const bodiesPromises = getActiveSeries(panel).map((series) => { 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); 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 { createFieldsFetcher } from '../search_strategies/lib/fields_fetcher';
import { extractFieldLabel } from '../../../common/fields_utils'; import { extractFieldLabel } from '../../../common/fields_utils';
import { isAggSupported } from './helpers/check_aggs'; 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 { import type {
VisTypeTimeseriesRequestHandlerContext, VisTypeTimeseriesRequestHandlerContext,
@ -76,10 +77,15 @@ export async function getTableData(
const handleError = handleErrorResponse(panel); const handleError = handleErrorResponse(panel);
try { try {
if (isEntireTimeRangeMode(panel)) { panel.series.forEach((series) => {
panel.series.forEach((column) => { isAggSupported(series.metrics, capabilities);
isAggSupported(column.metrics, capabilities); if (series.filter?.query && !isConfigurationFeatureEnabled('filter', capabilities)) {
}); throw new FilterCannotBeAppliedError();
}
});
if (!panel.pivot_id) {
throw new PivotNotSelectedForTableError();
} }
const body = await buildTableRequest({ const body = await buildTableRequest({

View file

@ -6,37 +6,18 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import { get } from 'lodash'; import { AggNotSupportedError } from '../../../../common/errors';
import { AggNotSupportedInMode } from '../../../../common/errors'; import { isMetricEnabled } from '../../../../common/check_ui_restrictions';
import { TIME_RANGE_DATA_MODES } from '../../../../common/enums';
import { DEFAULT_UI_RESTRICTION, RESTRICTIONS_KEYS } from '../../../../common/ui_restrictions';
import { Metric } from '../../../../common/types'; import type { Metric } from '../../../../common/types';
import { SearchCapabilities } from '../../search_strategies'; import type { 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));
};
export function isAggSupported(metrics: Metric[], capabilities: SearchCapabilities) { export function isAggSupported(metrics: Metric[], capabilities: SearchCapabilities) {
const metricTypes = metrics.filter( const metricTypes = metrics.filter(
(metric) => (metric) => !isMetricEnabled(metric.type, capabilities.uiRestrictions)
!checkUIRestrictions(
metric.type,
capabilities.uiRestrictions,
RESTRICTIONS_KEYS.WHITE_LISTED_METRICS
)
); );
if (metricTypes.length) { if (metricTypes.length) {
throw new AggNotSupportedInMode( throw new AggNotSupportedError(metricTypes.map((metric) => `"${metric.type}"`).join(', '));
metricTypes.map((metric) => `"${metric.type}"`).join(', '),
TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE
);
} }
} }

View file

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

View file

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

View file

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

View file

@ -77,6 +77,7 @@ describe('normalizeQuery', () => {
expect(modifiedDoc.aggs.timeseries.meta).toEqual({ expect(modifiedDoc.aggs.timeseries.meta).toEqual({
timeField: 'order_date', timeField: 'order_date',
normalized: true,
intervalString: '10s', intervalString: '10s',
bucketSize: 10, bucketSize: 10,
seriesId: [seriesId], 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. * Side Public License, v 1.
*/ */
import { has } from 'lodash';
import type { TableSearchRequest } from '../table/types'; import type { TableSearchRequest } from '../table/types';
import type { Series } from '../../../../../common/types'; import type { Series } from '../../../../../common/types';
export function calculateAggRoot(doc: TableSearchRequest, column: Series) { export function calculateAggRoot(doc: TableSearchRequest, column: Series) {
let aggRoot = `aggs.pivot.aggs.${column.id}.aggs`; return `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;
} }

View file

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

View file

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

View file

@ -41,14 +41,16 @@ export const normalizeQuery: TableRequestProcessorsFunction = () => {
}; };
overwrite(normalizedSeries, `${seriesId}`, agg); overwrite(normalizedSeries, `${seriesId}`, agg);
overwrite(normalizedSeries, `${seriesId}.meta`, meta); overwrite(normalizedSeries, `${seriesId}.meta`, {
...meta,
normalized: true,
});
} else { } else {
overwrite(normalizedSeries, `${seriesId}`, value); overwrite(normalizedSeries, `${seriesId}`, value);
} }
}); });
overwrite(doc, 'aggs.pivot.aggs', normalizedSeries); overwrite(doc, 'aggs.pivot.aggs', normalizedSeries);
return doc; 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. * Side Public License, v 1.
*/ */
import { buildEsQuery } from '@kbn/es-query';
import { overwrite } from '../../helpers'; import { overwrite } from '../../helpers';
import type { TableRequestProcessorsFunction } from './types'; import type { TableRequestProcessorsFunction } from './types';
export const splitByTerms: TableRequestProcessorsFunction = ({ export const splitByTerms: TableRequestProcessorsFunction = ({ panel }) => {
panel,
esQueryConfig,
seriesIndex,
}) => {
const indexPattern = seriesIndex.indexPattern || undefined;
return (next) => (doc) => { return (next) => (doc) => {
panel.series panel.series
.filter((c) => c.aggregate_by && c.aggregate_function) .filter((c) => c.aggregate_by && c.aggregate_function)
.forEach((column) => { .forEach((column) => {
overwrite(doc, `aggs.pivot.aggs.${column.id}.terms.field`, column.aggregate_by); overwrite(doc, `aggs.pivot.aggs.${column.id}.terms.field`, column.aggregate_by);
overwrite(doc, `aggs.pivot.aggs.${column.id}.terms.size`, 100); 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); return next(doc);
}; };
}; };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -62,7 +62,9 @@ function createBuckets(series: string[]) {
timeField: 'timestamp', timeField: 'timestamp',
seriesId, seriesId,
}, },
buckets: createBucketsObjects(size, trend, seriesId), timeseries: {
buckets: createBucketsObjects(size, trend, seriesId),
},
}; };
} }
return baseObj; return baseObj;
@ -113,11 +115,13 @@ describe('processBucket(panel)', () => {
timeField: 'timestamp', timeField: 'timestamp',
seriesId: SERIES_ID, seriesId: SERIES_ID,
}, },
buckets: [ timeseries: {
// this is a flat case, but 0/0 has not a valid number result buckets: [
createValueObject(0, 0, SERIES_ID), // this is a flat case, but 0/0 has not a valid number result
createValueObject(1, 0, SERIES_ID), createValueObject(0, 0, SERIES_ID),
], createValueObject(1, 0, SERIES_ID),
],
},
}, },
}; };
const result = await bucketProcessor(bucketforNaNResult); 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 { createFieldsFetcher } from '../../search_strategies/lib/fields_fetcher';
import type { Panel } from '../../../../common/types'; import type { Panel } from '../../../../common/types';
import type { PanelDataArray } from '../../../../common/types/vis_data';
import type { TableSearchRequestMeta } from '../request_processors/table/types'; 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[]) { function trendSinceLastBucket(data: PanelDataArray[]) {
if (data.length < 2) { if (data.length < 2) {
@ -36,23 +37,22 @@ export function processBucket({ panel, extractFields }: ProcessTableBucketParams
return async (bucket: Record<string, unknown>) => { return async (bucket: Record<string, unknown>) => {
const resultSeries = await Promise.all( const resultSeries = await Promise.all(
getActiveSeries(panel).map(async (series) => { getActiveSeries(panel).map(async (series) => {
const timeseries = get(bucket, `${series.id}.timeseries`); const response: TableResponseProcessorsParams['response'] = {
const buckets = get(bucket, `${series.id}.buckets`); aggregations: {
let meta: TableSearchRequestMeta = {}; [series.id]: get(bucket, `${series.id}`),
},
};
const meta = (response.aggregations[series.id]?.meta ?? {}) as TableSearchRequestMeta;
if (!timeseries && buckets) { if (meta.normalized && !get(response, `aggregations.${series.id}.timeseries`)) {
meta = get(bucket, `${series.id}.meta`) as TableSearchRequestMeta; overwrite(response, `aggregations.${series.id}.timeseries`, {
buckets: get(bucket, `${series.id}.buckets`),
overwrite(bucket, series.id, {
meta,
timeseries: {
buckets: get(bucket, `${series.id}.buckets`),
},
}); });
delete response.aggregations[series.id].buckets;
} }
const [result] = await buildTableResponse({ const [result] = await buildTableResponse({
bucket, response,
panel, panel,
series, series,
meta, meta,

View file

@ -122,7 +122,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const error = await visualBuilder.getVisualizeError(); const error = await visualBuilder.getVisualizeError();
expect(error).to.eql( 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(); const error = await visualBuilder.getVisualizeError();
expect(error).to.eql( 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(); const error = await visualBuilder.getVisualizeError();
expect(error).to.eql( 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> { public async checkPreviewIsDisabled(): Promise<void> {
this.log.debug(`Check no data message is present`); 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> { public async cloneSeries(nth: number = 0): Promise<void> {

View file

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

View file

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