Skip to content

Commit

Permalink
[TSVB] Introducing Timerange Data Mode for Metric Style Visualizations (
Browse files Browse the repository at this point in the history
#37185)

* Add Time Range input to Panel on TSVB

* Disable YesNo Component

* Remove redundant translations

* add timerange_data_modes

* Fix reviews

* BE part

* add new ui restriction

* add new ui restriction - ui part

* override index pattern issue

* Take TIME_RANGE_MODE_KEY out

* Make TimeRange more stable

* Small improvements

* Small improvements - BE part

* Small improvements - BE part

* Small improvements - BE part

* Bind uiRestrictions to UI of the Time Range

* Get rid of context consumers

* Remove console.log

* Add translation and refactor a little

* Pretty Panel config
  • Loading branch information
gospodarsky committed Jul 18, 2019
1 parent ade8a2f commit eeb92b4
Show file tree
Hide file tree
Showing 20 changed files with 564 additions and 147 deletions.
44 changes: 44 additions & 0 deletions src/legacy/core_plugins/metrics/common/timerange_data_modes.js
Original file line number Diff line number Diff line change
@@ -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';
49 changes: 49 additions & 0 deletions src/legacy/core_plugins/metrics/common/ui_restrictions.js
Original file line number Diff line number Diff line change
@@ -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,
};
149 changes: 103 additions & 46 deletions src/legacy/core_plugins/metrics/public/components/index_pattern.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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];

Expand All @@ -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 (
<div className={props.className}>
<EuiFlexGroup responsive={false} wrap={true}>
<div className="index-pattern">
{!isTimeSeries && (
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
id={htmlId('timeRange')}
label={i18n.translate('tsvb.indexPattern.timeRange.label', {
defaultMessage: 'Data timerange mode',
})}
>
<EuiComboBox
isClearable={false}
placeholder={i18n.translate('tsvb.indexPattern.timeRange.selectTimeRange', {
defaultMessage: 'Select',
})}
options={timeRangeOptions}
selectedOptions={selectedTimeRangeOption ? [selectedTimeRangeOption] : []}
onChange={handleSelectChange(TIME_RANGE_MODE_KEY)}
singleSelection={{ asPlainText: true }}
isDisabled={disabled}
/>
</EuiFormRow>
<EuiText size="xs" style={{ margin: 0 }}>
{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.`,
})}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
id={htmlId('indexPattern')}
label={<FormattedMessage id="tsvb.indexPatternLabel" defaultMessage="Index pattern" />}
label={i18n.translate('tsvb.indexPatternLabel', { defaultMessage: 'Index pattern' })}
helpText={
isDefaultIndexPatternUsed && (
<FormattedMessage
id="tsvb.indexPattern.searchByDefaultIndex"
defaultMessage="Default index pattern is used. To query all indexes use *"
/>
)
isDefaultIndexPatternUsed &&
i18n.translate('tsvb.indexPattern.searchByDefaultIndex', {
defaultMessage: 'Default index pattern is used. To query all indexes use *',
})
}
fullWidth
>
<EuiFieldText
data-test-subj="metricsIndexPatternInput"
disabled={props.disabled}
disabled={disabled}
placeholder={model.default_index_pattern}
onChange={handleTextChange(indexPatternName, '*')}
value={model[indexPatternName]}
fullWidth
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
id={htmlId('timeField')}
label={
<FormattedMessage id="tsvb.indexPattern.timeFieldLabel" defaultMessage="Time field" />
}
fullWidth
label={i18n.translate('tsvb.indexPattern.timeFieldLabel', {
defaultMessage: 'Time field',
})}
>
<FieldSelect
data-test-subj="metricsIndexPatternFieldsSelect"
restrict={RESTRICT_FIELDS}
value={model[timeFieldName]}
disabled={props.disabled}
disabled={disabled}
onChange={handleSelectChange(timeFieldName)}
indexPattern={model[indexPatternName]}
fields={fields}
placeholder={isDefaultIndexPatternUsed ? model.default_timefield : undefined}
fullWidth
/>
</EuiFormRow>
</EuiFlexItem>
Expand All @@ -133,35 +187,38 @@ export const IndexPattern = props => {
isInvalid={!intervalValidation.isValid}
error={intervalValidation.errorMessage}
id={htmlId('interval')}
label={
<FormattedMessage id="tsvb.indexPattern.intervalLabel" defaultMessage="Interval" />
}
helpText={
<FormattedMessage
id="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."
/>
}
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.',
})}
>
<EuiFieldText
isInvalid={!intervalValidation.isValid}
disabled={props.disabled}
disabled={disabled || isEntireTimeRangeActive(model, isTimeSeries)}
onChange={handleTextChange(intervalName, AUTO_INTERVAL)}
value={model[intervalName]}
placeholder={AUTO_INTERVAL}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormLabel>
<FormattedMessage
id="tsvb.indexPattern.dropLastBucketLabel"
defaultMessage="Drop last bucket?"
<EuiFormRow
id={htmlId('dropLastBucket')}
label={i18n.translate('tsvb.indexPattern.dropLastBucketLabel', {
defaultMessage: 'Drop last bucket?',
})}
>
<YesNo
value={model[dropBucketName]}
name={dropBucketName}
onChange={onChange}
disabled={disabled || isEntireTimeRangeActive(model, isTimeSeries)}
/>
</EuiFormLabel>
<EuiSpacer size="s" />
<YesNo value={model[dropBucketName]} name={dropBucketName} onChange={props.onChange} />
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -43,19 +45,32 @@ 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;
};

if (Component) {
return (
<FormValidationContext.Provider value={updateControlValidity}>
<Component {...props} />
<UIRestrictionsContext.Provider value={uiRestrictions}>
<Component {...props} />
</UIRestrictionsContext.Provider>
</FormValidationContext.Provider>
);
}
Expand Down
Loading

0 comments on commit eeb92b4

Please sign in to comment.