diff --git a/src/legacy/core_plugins/metrics/common/timerange_data_modes.js b/src/legacy/core_plugins/metrics/common/timerange_data_modes.js new file mode 100644 index 00000000000000..7d69d36f213b7d --- /dev/null +++ b/src/legacy/core_plugins/metrics/common/timerange_data_modes.js @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Time Range data modes. + * @constant + * @public + */ +export const TIME_RANGE_DATA_MODES = { + /** + * Entire timerange mode will match all the documents selected in the + * timerange timepicker + */ + ENTIRE_TIME_RANGE: 'entire_time_range', + + /** + * Last value mode will match only the documents for the specified interval + * from the end of the timerange. + */ + LAST_VALUE: 'last_value', +}; + +/** + * Key for getting the Time Range mode from the Panel configuration object. + * @constant + * @public + */ +export const TIME_RANGE_MODE_KEY = 'time_range_mode'; diff --git a/src/legacy/core_plugins/metrics/common/ui_restrictions.js b/src/legacy/core_plugins/metrics/common/ui_restrictions.js new file mode 100644 index 00000000000000..96726d51e4a7c9 --- /dev/null +++ b/src/legacy/core_plugins/metrics/common/ui_restrictions.js @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * UI Restrictions keys + * @constant + * @public + */ +export const RESTRICTIONS_KEYS = { + /** + * Key for getting the white listed group by fields from the UIRestrictions object. + */ + WHITE_LISTED_GROUP_BY_FIELDS: 'whiteListedGroupByFields', + + /** + * Key for getting the white listed metrics from the UIRestrictions object. + */ + WHITE_LISTED_METRICS: 'whiteListedMetrics', + + /** + * Key for getting the white listed Time Range modes from the UIRestrictions object. + */ + WHITE_LISTED_TIMERANGE_MODES: 'whiteListedTimerangeModes', +}; + +/** + * Default value for the UIRestriction + * @constant + * @public + */ +export const DEFAULT_UI_RESTRICTION = { + '*': true, +}; diff --git a/src/legacy/core_plugins/metrics/public/components/index_pattern.js b/src/legacy/core_plugins/metrics/public/components/index_pattern.js index 79f41357d07edb..a7a7d14ea9940d 100644 --- a/src/legacy/core_plugins/metrics/public/components/index_pattern.js +++ b/src/legacy/core_plugins/metrics/public/components/index_pattern.js @@ -25,11 +25,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, - EuiFormLabel, - EuiSpacer, + EuiComboBox, + EuiText, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { FieldSelect } from './aggs/field_select'; import { createSelectHandler } from './lib/create_select_handler'; import { createTextHandler } from './lib/create_text_handler'; @@ -42,6 +40,11 @@ import { isAutoInterval, AUTO_INTERVAL, } from './lib/get_interval'; +import { i18n } from '@kbn/i18n'; +import { TIME_RANGE_DATA_MODES, TIME_RANGE_MODE_KEY } from '../../common/timerange_data_modes'; +import { PANEL_TYPES } from '../../common/panel_types'; +import { isTimerangeModeEnabled } from '../lib/check_ui_restrictions'; +import { UIRestrictionsContext } from '../contexts/ui_restriction_context'; const RESTRICT_FIELDS = [ES_TYPES.DATE]; @@ -56,75 +59,126 @@ const validateIntervalValue = intervalValue => { return validateReInterval(intervalValue); }; -export const IndexPattern = props => { - const { fields, prefix } = props; - const handleSelectChange = createSelectHandler(props.onChange); - const handleTextChange = createTextHandler(props.onChange); +const htmlId = htmlIdGenerator(); + +const isEntireTimeRangeActive = (model, isTimeSeries) => + !isTimeSeries && model[TIME_RANGE_MODE_KEY] === TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE; + +export const IndexPattern = ({ fields, prefix, onChange, disabled, model: _model }) => { + const handleSelectChange = createSelectHandler(onChange); + const handleTextChange = createTextHandler(onChange); const timeFieldName = `${prefix}time_field`; const indexPatternName = `${prefix}index_pattern`; const intervalName = `${prefix}interval`; const dropBucketName = `${prefix}drop_last_bucket`; const updateControlValidity = useContext(FormValidationContext); + const uiRestrictions = useContext(UIRestrictionsContext); + + const timeRangeOptions = [ + { + label: i18n.translate('tsvb.indexPattern.timeRange.lastValue', { + defaultMessage: 'Last value', + }), + value: TIME_RANGE_DATA_MODES.LAST_VALUE, + disabled: !isTimerangeModeEnabled(TIME_RANGE_DATA_MODES.LAST_VALUE, uiRestrictions), + }, + { + label: i18n.translate('tsvb.indexPattern.timeRange.entireTimeRange', { + defaultMessage: 'Entire time range', + }), + value: TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + disabled: !isTimerangeModeEnabled(TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, uiRestrictions), + }, + ]; const defaults = { default_index_pattern: '', [indexPatternName]: '*', [intervalName]: AUTO_INTERVAL, [dropBucketName]: 1, + [TIME_RANGE_MODE_KEY]: timeRangeOptions[0].value, }; - const htmlId = htmlIdGenerator(); - const model = { ...defaults, ...props.model }; + const model = { ...defaults, ..._model }; const isDefaultIndexPatternUsed = model.default_index_pattern && !model[indexPatternName]; const intervalValidation = validateIntervalValue(model[intervalName]); + const selectedTimeRangeOption = timeRangeOptions.find( + ({ value }) => model[TIME_RANGE_MODE_KEY] === value + ); + const isTimeSeries = model.type === PANEL_TYPES.TIMESERIES; updateControlValidity(intervalName, intervalValidation.isValid); return ( -
- +
+ {!isTimeSeries && ( + + + + + + + {i18n.translate('tsvb.indexPattern.timeRange.hint', { + defaultMessage: `This setting controls the timespan used for matching documents. + "Entire timerange" will match all the documents selected in the timepicker. + "Last value" will match only the documents for the specified interval from the end of the timerange.`, + })} + + + + )} + } + label={i18n.translate('tsvb.indexPatternLabel', { defaultMessage: 'Index pattern' })} helpText={ - isDefaultIndexPatternUsed && ( - - ) + isDefaultIndexPatternUsed && + i18n.translate('tsvb.indexPattern.searchByDefaultIndex', { + defaultMessage: 'Default index pattern is used. To query all indexes use *', + }) } - fullWidth > - } - fullWidth + label={i18n.translate('tsvb.indexPattern.timeFieldLabel', { + defaultMessage: 'Time field', + })} > @@ -133,20 +187,18 @@ export const IndexPattern = props => { isInvalid={!intervalValidation.isValid} error={intervalValidation.errorMessage} id={htmlId('interval')} - label={ - - } - helpText={ - - } + label={i18n.translate('tsvb.indexPattern.intervalLabel', { + defaultMessage: 'Interval', + })} + helpText={i18n.translate('tsvb.indexPattern.intervalHelpText', { + defaultMessage: 'Examples: auto, 1m, 1d, 7d, 1y, >=1m', + description: + 'auto, 1m, 1d, 7d, 1y, >=1m are required values and must not be translated.', + })} > { - - + - - - +
diff --git a/src/legacy/core_plugins/metrics/public/components/panel_config.js b/src/legacy/core_plugins/metrics/public/components/panel_config.js index bd94a175dd977f..45da23b0830c29 100644 --- a/src/legacy/core_plugins/metrics/public/components/panel_config.js +++ b/src/legacy/core_plugins/metrics/public/components/panel_config.js @@ -18,6 +18,7 @@ */ import PropTypes from 'prop-types'; import React, { useState, useEffect } from 'react'; +import { get } from 'lodash'; import { TimeseriesPanelConfig as timeseries } from './panel_config/timeseries'; import { MetricPanelConfig as metric } from './panel_config/metric'; import { TopNPanelConfig as topN } from './panel_config/top_n'; @@ -26,6 +27,7 @@ import { GaugePanelConfig as gauge } from './panel_config/gauge'; import { MarkdownPanelConfig as markdown } from './panel_config/markdown'; import { FormattedMessage } from '@kbn/i18n/react'; import { FormValidationContext } from '../contexts/form_validation_context'; +import { UIRestrictionsContext } from '../contexts/ui_restriction_context'; const types = { timeseries, @@ -43,11 +45,22 @@ export function PanelConfig(props) { const { model } = props; const Component = types[model.type]; const [formValidationResults] = useState({}); + const [uiRestrictions, setUIRestrictions] = useState(null); useEffect(() => { model.isModelInvalid = !checkModelValidity(formValidationResults); }); + useEffect(() => { + const visDataSubscription = props.visData$.subscribe(visData => + setUIRestrictions(get(visData, 'uiRestrictions', null)) + ); + + return function cleanup() { + visDataSubscription.unsubscribe(); + }; + }, [props.visData$]); + const updateControlValidity = (controlKey, isControlValid) => { formValidationResults[controlKey] = isControlValid; }; @@ -55,7 +68,9 @@ export function PanelConfig(props) { if (Component) { return ( - + + + ); } diff --git a/src/legacy/core_plugins/metrics/public/components/series.js b/src/legacy/core_plugins/metrics/public/components/series.js index 74f7e49f3fbbef..a4963db09e1153 100644 --- a/src/legacy/core_plugins/metrics/public/components/series.js +++ b/src/legacy/core_plugins/metrics/public/components/series.js @@ -19,7 +19,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { assign, get } from 'lodash'; +import { assign } from 'lodash'; import { TimeseriesSeries as timeseries } from './vis_types/timeseries/series'; import { MetricSeries as metric } from './vis_types/metric/series'; @@ -39,17 +39,10 @@ const lookup = { }; export class Series extends Component { - constructor(props) { - super(props); - - this.state = { - visible: true, - selectedTab: 'metrics', - uiRestrictions: undefined, - }; - - this.visDataSubscription = null; - } + state = { + visible: true, + selectedTab: 'metrics', + }; switchTab = selectedTab => { this.setState({ selectedTab }); @@ -79,16 +72,6 @@ export class Series extends Component { }); }; - componentDidMount() { - if (this.props.visData$) { - this.visDataSubscription = this.props.visData$.subscribe(visData => - this.setState({ - uiRestrictions: get(visData, 'uiRestrictions'), - }) - ); - } - } - render() { const { panel } = this.props; const Component = lookup[panel.type]; @@ -107,7 +90,6 @@ export class Series extends Component { panel: this.props.panel, selectedTab: this.state.selectedTab, style: this.props.style, - uiRestrictions: this.state.uiRestrictions, switchTab: this.switchTab, toggleVisible: this.toggleVisible, togglePanelActivation: this.togglePanelActivation, @@ -125,12 +107,6 @@ export class Series extends Component { /> ); } - - componentWillUnmount() { - if (this.visDataSubscription) { - this.visDataSubscription.unsubscribe(); - } - } } Series.defaultProps = { diff --git a/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/series.js b/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/series.js index e53750cc01b217..43e1ac9f0eda56 100644 --- a/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/series.js +++ b/src/legacy/core_plugins/metrics/public/components/vis_types/timeseries/series.js @@ -35,6 +35,7 @@ import { import { Split } from '../../split'; import { createTextHandler } from '../../lib/create_text_handler'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; +import { PANEL_TYPES } from '../../../../common/panel_types'; const TimeseriesSeriesUI = injectI18n(function(props) { const { @@ -51,7 +52,11 @@ const TimeseriesSeriesUI = injectI18n(function(props) { name, uiRestrictions, } = props; - const defaults = { label: '' }; + + const defaults = { + label: '', + type: PANEL_TYPES.TIMESERIES, + }; const model = { ...defaults, ...props.model }; const handleChange = createTextHandler(onChange); @@ -90,7 +95,7 @@ const TimeseriesSeriesUI = injectI18n(function(props) { seriesBody = ( diff --git a/src/legacy/core_plugins/metrics/public/components/yes_no.js b/src/legacy/core_plugins/metrics/public/components/yes_no.js index 3a3ca75212ddd5..63892ac80d85ea 100644 --- a/src/legacy/core_plugins/metrics/public/components/yes_no.js +++ b/src/legacy/core_plugins/metrics/public/components/yes_no.js @@ -24,7 +24,7 @@ import { EuiRadio, htmlIdGenerator } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; export function YesNo(props) { - const { name, value } = props; + const { name, value, disabled } = props; const handleChange = value => { const { name } = props; return () => { @@ -50,6 +50,7 @@ export function YesNo(props) { checked={Boolean(value)} value="yes" onChange={handleChange(1)} + disabled={disabled} />  
); @@ -75,3 +77,7 @@ YesNo.propTypes = { name: PropTypes.string, value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), }; + +YesNo.defaultProps = { + disabled: false, +}; diff --git a/src/legacy/core_plugins/metrics/public/contexts/ui_restriction_context.js b/src/legacy/core_plugins/metrics/public/contexts/ui_restriction_context.js new file mode 100644 index 00000000000000..24860b329e3043 --- /dev/null +++ b/src/legacy/core_plugins/metrics/public/contexts/ui_restriction_context.js @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +export const UIRestrictionsContext = React.createContext(); diff --git a/src/legacy/core_plugins/metrics/public/lib/check_ui_restrictions.js b/src/legacy/core_plugins/metrics/public/lib/check_ui_restrictions.js index b233d96988d253..1fade6ebe98495 100644 --- a/src/legacy/core_plugins/metrics/public/lib/check_ui_restrictions.js +++ b/src/legacy/core_plugins/metrics/public/lib/check_ui_restrictions.js @@ -18,37 +18,72 @@ */ import { get } from 'lodash'; +import { RESTRICTIONS_KEYS, DEFAULT_UI_RESTRICTION } from '../../common/ui_restrictions'; -export const DEFAULT_UI_RESTRICTION = { - '*': true, -}; - -const RESTRICTION_TYPES = { - WHITE_LISTED_GROUP_BY_FIELDS: 'whiteListedGroupByFields', - WHITE_LISTER_METRICS: 'whiteListedMetrics', -}; - +/** + * Generic method for checking all types of the UI Restrictions + * @private + */ const checkUIRestrictions = (key, restrictions = DEFAULT_UI_RESTRICTION, type) => { const isAllEnabled = get(restrictions, `${type}.*`, true); - return isAllEnabled || get(restrictions, type, {})[key]; + return isAllEnabled || Boolean(get(restrictions, type, {})[key]); }; +/** + * Using this method, you can check whether a specific Metric (Aggregation) is allowed + * for current panel configuration or not. + * @public + * @param key - string value of Metric (Aggregation). + * @param restrictions - uiRestrictions object. Comes from the /data request. + * @return {boolean} + */ export const isMetricEnabled = (key, restrictions) => { - return checkUIRestrictions(key, restrictions, RESTRICTION_TYPES.WHITE_LISTER_METRICS); + return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.WHITE_LISTED_METRICS); }; +/** + * Using this method, you can check whether a specific Field is allowed + * for Metric (aggregation) or not. + * @public + * @param field - string value of data Field. + * @param metricType - string value of Metric (Aggregation). + * @param restrictions - uiRestrictions object. Comes from the /data request. + * @return {boolean} + */ export const isFieldEnabled = (field, metricType, restrictions = DEFAULT_UI_RESTRICTION) => { if (isMetricEnabled(metricType, restrictions)) { return checkUIRestrictions( field, - restrictions[RESTRICTION_TYPES.WHITE_LISTER_METRICS], + restrictions[RESTRICTIONS_KEYS.WHITE_LISTED_METRICS], metricType ); } return false; }; +/** + * Using this method, you can check whether a specific Group By mode is allowed + * for current panel configuration or not. + * @public + * @param key - string value of Group by mode. + * All available mode you can find in the following object SPLIT_MODES. + * @param restrictions - uiRestrictions object. Comes from the /data request. + * @return {boolean} + */ export const isGroupByFieldsEnabled = (key, restrictions) => { - return checkUIRestrictions(key, restrictions, RESTRICTION_TYPES.WHITE_LISTED_GROUP_BY_FIELDS); + return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS); +}; + +/** + * Using this method, you can check whether a specific time range 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 isTimerangeModeEnabled = (key, restrictions) => { + return checkUIRestrictions(key, restrictions, RESTRICTIONS_KEYS.WHITE_LISTED_TIMERANGE_MODES); }; diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.js index 39c86f7901203a..1e693ae287b274 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.js @@ -21,6 +21,7 @@ import { parseInterval, getSuitableUnit, } from '../vis_data/helpers/unit_to_seconds'; +import { RESTRICTIONS_KEYS } from '../../../common/ui_restrictions'; const getTimezoneFromRequest = request => { return request.payload.timerange.timezone; @@ -44,10 +45,15 @@ export class DefaultSearchCapabilities { return this.createUiRestriction(); } + get whiteListedTimerangeModes() { + return this.createUiRestriction(); + } + get uiRestrictions() { return { - whiteListedMetrics: this.whiteListedMetrics, - whiteListedGroupByFields: this.whiteListedGroupByFields, + [RESTRICTIONS_KEYS.WHITE_LISTED_METRICS]: this.whiteListedMetrics, + [RESTRICTIONS_KEYS.WHITE_LISTED_GROUP_BY_FIELDS]: this.whiteListedGroupByFields, + [RESTRICTIONS_KEYS.WHITE_LISTED_TIMERANGE_MODES]: this.whiteListedTimerangeModes, }; } diff --git a/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.test.js b/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.test.js index 5be38b7cce9271..b9b7759711567c 100644 --- a/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.test.js +++ b/src/legacy/core_plugins/metrics/server/lib/search_strategies/default_search_capabilities.test.js @@ -36,6 +36,14 @@ describe('DefaultSearchCapabilities', () => { expect(defaultSearchCapabilities.defaultTimeInterval).toBe(null); }); + test('should return default uiRestrictions', () => { + expect(defaultSearchCapabilities.uiRestrictions).toEqual({ + whiteListedMetrics: { '*': true }, + whiteListedGroupByFields: { '*': true }, + whiteListedTimerangeModes: { '*': true }, + }); + }); + test('should return Search Timezone', () => { defaultSearchCapabilities.request = { payload: { diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_bucket_size.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_bucket_size.js index f2e1a15ce68a31..de23b90b21d6c0 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_bucket_size.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_bucket_size.js @@ -18,13 +18,13 @@ */ import { calculateAuto } from './calculate_auto'; -import moment from 'moment'; import { getUnitValue, parseInterval, convertIntervalToUnit, ASCENDING_UNIT_ORDER, } from './unit_to_seconds'; +import { getTimerangeDuration } from '../helpers/get_timerange'; import { INTERVAL_STRING_RE, GTE_INTERVAL_RE } from '../../../../common/interval_regexp'; const calculateBucketData = (timeInterval, capabilities) => { @@ -61,16 +61,14 @@ const calculateBucketData = (timeInterval, capabilities) => { }; }; -const getTimeRangeBucketSize = ({ min, max }) => { - const from = moment.utc(min); - const to = moment.utc(max); - const duration = moment.duration(to.valueOf() - from.valueOf(), 'ms'); +const calculateBucketSizeForAutoInterval = req => { + const duration = getTimerangeDuration(req); return calculateAuto.near(100, duration).asSeconds(); }; export const getBucketSize = (req, interval, capabilities) => { - const bucketSize = getTimeRangeBucketSize(req.payload.timerange); + const bucketSize = calculateBucketSizeForAutoInterval(req); let intervalString = `${bucketSize}s`; const gteAutoMatch = Boolean(interval) && interval.match(GTE_INTERVAL_RE); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_timerange.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_timerange.js index 2c6a41ab1b9682..4141c76f02464b 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_timerange.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_timerange.js @@ -19,8 +19,17 @@ import moment from 'moment'; -export function getTimerange(req) { - const from = moment.utc(req.payload.timerange.min); - const to = moment.utc(req.payload.timerange.max); - return { from, to }; -} +export const getTimerange = req => { + const { min, max } = req.payload.timerange; + + return { + from: moment.utc(min), + to: moment.utc(max), + }; +}; + +export const getTimerangeDuration = req => { + const { from, to } = getTimerange(req); + + return moment.duration(to.valueOf() - from.valueOf(), 'ms'); +}; diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_timerange_mode.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_timerange_mode.js new file mode 100644 index 00000000000000..8a83116b032b40 --- /dev/null +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/helpers/get_timerange_mode.js @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + TIME_RANGE_DATA_MODES, + TIME_RANGE_MODE_KEY, +} from '../../../../common/timerange_data_modes'; +import { PANEL_TYPES } from '../../../../common/panel_types'; + +const OVERRIDE_INDEX_PATTERN_KEY = 'override_index_pattern'; + +/** + * Check if passed 'series' has overridden index pattern or not. + * @private + * @param series - specific series + * @return {boolean} + */ +const hasOverriddenIndexPattern = series => Boolean(series[OVERRIDE_INDEX_PATTERN_KEY]); + +/** + * Get value of Time Range Mode for panel + * @private + * @param panel - panel configuration + * @return {string} - value of TIME_RANGE_DATA_MODES type + */ +const getPanelTimeRangeMode = panel => panel[TIME_RANGE_MODE_KEY]; + +/** + * Get value of Time Range Mode for series + * @private + * @param series - specific series + * @return {string} - value of TIME_RANGE_DATA_MODES type + */ +const getSeriesTimeRangeMode = series => series[TIME_RANGE_MODE_KEY]; + +/** + * Check if 'Entire Time Range' mode active or not. + * @public + * @param panel - panel configuration + * @param series - specific series + * @return {boolean} + */ +export const isEntireTimeRangeMode = (panel, series = {}) => { + if (panel.type === PANEL_TYPES.TIMESERIES) { + return false; + } + + const timeRangeMode = hasOverriddenIndexPattern(series) + ? getSeriesTimeRangeMode(series) + : getPanelTimeRangeMode(panel); + + return timeRangeMode === TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE; +}; + +/** + * Check if 'Last Value Time Range' mode active or not. + * @public + * @param panel - panel configuration + * @param series - specific series + * @return {boolean} + */ +export const isLastValueTimerangeMode = (panel, series) => !isEntireTimeRangeMode(panel, series); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js index f7d94fc57a5572..40eaba621aabb7 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/__tests__/date_histogram.js @@ -17,10 +17,10 @@ * under the License. */ -import { dateHistogram } from '../date_histogram'; import { expect } from 'chai'; import sinon from 'sinon'; import { DefaultSearchCapabilities } from '../../../../search_strategies/default_search_capabilities'; +import { dateHistogram } from '../date_histogram'; describe('dateHistogram(req, panel, series)', () => { let panel; @@ -163,4 +163,48 @@ describe('dateHistogram(req, panel, series)', () => { }, }); }); + + describe('dateHistogram for entire time range mode', () => { + it('should ignore entire range mode for timeseries', () => { + panel.time_range_mode = 'entire_time_range'; + panel.type = 'timeseries'; + + const next = doc => doc; + const doc = dateHistogram(req, panel, series, config, indexPatternObject, capabilities)(next)( + {} + ); + + expect(doc.aggs.test.aggs.timeseries.auto_date_histogram).to.eql(undefined); + expect(doc.aggs.test.aggs.timeseries.date_histogram).to.exist; + }); + + it('should returns valid date histogram for entire range mode', () => { + panel.time_range_mode = 'entire_time_range'; + + const next = doc => doc; + const doc = dateHistogram(req, panel, series, config, indexPatternObject, capabilities)(next)( + {} + ); + expect(doc).to.eql({ + aggs: { + test: { + aggs: { + timeseries: { + auto_date_histogram: { + field: '@timestamp', + buckets: 1, + }, + }, + }, + meta: { + timeField: '@timestamp', + seriesId: 'test', + bucketSize: 10, + intervalString: '10s', + }, + }, + }, + }); + }); + }); }); diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/date_histogram.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/date_histogram.js index c2231cf982ea4c..a15b3a44e2686c 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/date_histogram.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/series/date_histogram.js @@ -22,24 +22,41 @@ import { dateHistogramInterval } from '../../../../../../data/common'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { offsetTime } from '../../offset_time'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; +import { isLastValueTimerangeMode } from '../../helpers/get_timerange_mode'; export function dateHistogram(req, panel, series, esQueryConfig, indexPatternObject, capabilities) { return next => doc => { const { timeField, interval } = getIntervalAndTimefield(panel, series, indexPatternObject); const { bucketSize, intervalString } = getBucketSize(req, interval, capabilities); - const { from, to } = offsetTime(req, series.offset_time); - const timezone = capabilities.searchTimezone; - - set(doc, `aggs.${series.id}.aggs.timeseries.date_histogram`, { - field: timeField, - min_doc_count: 0, - time_zone: timezone, - extended_bounds: { - min: from.valueOf(), - max: to.valueOf(), - }, - ...dateHistogramInterval(intervalString), - }); + + const getDateHistogramForLastBucketMode = () => { + const { from, to } = offsetTime(req, series.offset_time); + const timezone = capabilities.searchTimezone; + + set(doc, `aggs.${series.id}.aggs.timeseries.date_histogram`, { + field: timeField, + min_doc_count: 0, + time_zone: timezone, + extended_bounds: { + min: from.valueOf(), + max: to.valueOf(), + }, + ...dateHistogramInterval(intervalString), + }); + }; + + const getDateHistogramForEntireTimerangeMode = () => + set(doc, `aggs.${series.id}.aggs.timeseries.auto_date_histogram`, { + field: timeField, + buckets: 1, + }); + + isLastValueTimerangeMode(panel, series) + ? getDateHistogramForLastBucketMode() + : getDateHistogramForEntireTimerangeMode(); + + // master + set(doc, `aggs.${series.id}.meta`, { timeField, intervalString, diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/date_histogram.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/date_histogram.js index c3d469d69a320f..e1b246a1f024c2 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/date_histogram.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/request_processors/table/date_histogram.js @@ -20,6 +20,7 @@ import { set } from 'lodash'; import { dateHistogramInterval } from '../../../../../../data/common'; import { getBucketSize } from '../../helpers/get_bucket_size'; +import { isLastValueTimerangeMode } from '../../helpers/get_timerange_mode'; import { getIntervalAndTimefield } from '../../get_interval_and_timefield'; import { getTimerange } from '../../helpers/get_timerange'; import { calculateAggRoot } from './calculate_agg_root'; @@ -27,29 +28,54 @@ import { calculateAggRoot } from './calculate_agg_root'; export function dateHistogram(req, panel, esQueryConfig, indexPatternObject, capabilities) { return next => doc => { const { timeField, interval } = getIntervalAndTimefield(panel, {}, indexPatternObject); - const { bucketSize, intervalString } = getBucketSize(req, interval, capabilities); - const { from, to } = getTimerange(req); - const timezone = capabilities.searchTimezone; - - panel.series.forEach(column => { - const aggRoot = calculateAggRoot(doc, column); - set(doc, `${aggRoot}.timeseries.date_histogram`, { - field: timeField, - min_doc_count: 0, - time_zone: timezone, - extended_bounds: { - min: from.valueOf(), - max: to.valueOf(), - }, - ...dateHistogramInterval(intervalString), + const meta = { + timeField, + }; + + const getDateHistogramForLastBucketMode = () => { + const { bucketSize, intervalString } = getBucketSize(req, interval, capabilities); + const { from, to } = getTimerange(req); + const timezone = capabilities.searchTimezone; + + panel.series.forEach(column => { + const aggRoot = calculateAggRoot(doc, column); + + set(doc, `${aggRoot}.timeseries.date_histogram`, { + field: timeField, + min_doc_count: 0, + time_zone: timezone, + extended_bounds: { + min: from.valueOf(), + max: to.valueOf(), + }, + ...dateHistogramInterval(intervalString), + }); + + set(doc, aggRoot.replace(/\.aggs$/, '.meta'), { + timeField, + intervalString, + bucketSize, + }); }); + }; - set(doc, aggRoot.replace(/\.aggs$/, '.meta'), { - timeField, - intervalString, - bucketSize, + const getDateHistogramForEntireTimerangeMode = () => { + panel.series.forEach(column => { + const aggRoot = calculateAggRoot(doc, column); + + set(doc, `${aggRoot}.timeseries.auto_date_histogram`, { + field: timeField, + buckets: 1, + }); + + set(doc, aggRoot.replace(/\.aggs$/, '.meta'), meta); }); - }); + }; + + isLastValueTimerangeMode(panel) + ? getDateHistogramForLastBucketMode() + : getDateHistogramForEntireTimerangeMode(); + return next(doc); }; } diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/drop_last_bucket.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/drop_last_bucket.js index 937e2ee8182da5..a4a7b01fa6f5eb 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/drop_last_bucket.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/series/drop_last_bucket.js @@ -18,15 +18,23 @@ */ import { get } from 'lodash'; +import { isLastValueTimerangeMode } from '../../helpers/get_timerange_mode'; + export function dropLastBucket(resp, panel, series) { return next => results => { - const seriesDropLastBucket = get(series, 'override_drop_last_bucket', 1); - const dropLastBucket = get(panel, 'drop_last_bucket', seriesDropLastBucket); - if (dropLastBucket) { - results.forEach(item => { - item.data = item.data.slice(0, item.data.length - 1); - }); + const shouldDropLastBucket = isLastValueTimerangeMode(panel, series); + + if (shouldDropLastBucket) { + const seriesDropLastBucket = get(series, 'override_drop_last_bucket', 1); + const dropLastBucket = get(panel, 'drop_last_bucket', seriesDropLastBucket); + + if (dropLastBucket) { + results.forEach(item => { + item.data = item.data.slice(0, item.data.length - 1); + }); + } } + return next(results); }; } diff --git a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/drop_last_bucket.js b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/drop_last_bucket.js index f594e7b559bfe6..05269335790b3f 100644 --- a/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/drop_last_bucket.js +++ b/src/legacy/core_plugins/metrics/server/lib/vis_data/response_processors/table/drop_last_bucket.js @@ -18,10 +18,18 @@ */ import { dropLastBucket } from '../series/drop_last_bucket'; +import { isLastValueTimerangeMode } from '../../helpers/get_timerange_mode'; export function dropLastBucketFn(bucket, panel, series) { return next => results => { - const fn = dropLastBucket({ aggregations: bucket }, panel, series); - return fn(next)(results); + const shouldDropLastBucket = isLastValueTimerangeMode(panel); + + if (shouldDropLastBucket) { + const fn = dropLastBucket({ aggregations: bucket }, panel, series); + + return fn(next)(results); + } + + return next(results); }; } diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.js b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.js index bb9b125901c055..b3a2526daccb30 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.js +++ b/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.js @@ -58,6 +58,12 @@ export const getRollupSearchCapabilities = (DefaultSearchCapabilities) => }); } + get whiteListedTimerangeModes() { + return this.createUiRestriction({ + last_value: true, + }); + } + getValidTimeInterval(userIntervalString) { const parsedRollupJobInterval = this.parseInterval(this.defaultTimeInterval); const inRollupJobUnit = this.convertIntervalToUnit(userIntervalString, parsedRollupJobInterval.unit);