[TSVB] [Rollup support] exclude unsupported metrics from the Metric Aggregations dropdown (#34165)

* [TSVB] [Rollup support] exclude unsupported metrics from the Metric Aggregations dropdown

Fix: #34047

* TSVB] [Rollup support] exclude unsupported metrics from the Metric Aggregations dropdown - fix broken tests

* [TSVB] [Rollup support] exclude unsupported metrics from the Metric Aggregations dropdown

* [TSVB] [Rollup support] exclude unsupported metrics from the Metric Aggregations dropdown - add filtering for group by

* fix tests

* Fix issue with fetching fields
This commit is contained in:
Alexey Antonov 2019-04-10 13:29:23 +03:00 committed by GitHub
parent aff8c69deb
commit 3099837678
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 509 additions and 154 deletions

View file

@ -22,17 +22,26 @@ import React from 'react';
import aggToComponent from '../lib/agg_to_component';
import { sortable } from 'react-anything-sortable';
import { UnsupportedAgg } from './unsupported_agg';
import { TemporaryUnsupportedAgg } from './temporary_unsupported_agg';
import { isMetricEnabled } from '../../lib/check_ui_restrictions';
function Agg(props) {
const { model } = props;
const { model, uiRestrictions } = props;
let Component = aggToComponent[model.type];
if (!Component) {
Component = UnsupportedAgg;
} else if (!isMetricEnabled(model.type, uiRestrictions)) {
Component = TemporaryUnsupportedAgg;
}
const style = {
cursor: 'default',
...props.style,
};
return (
<div
className={props.className}
@ -50,6 +59,7 @@ function Agg(props) {
panel={props.panel}
series={props.series}
siblings={props.siblings}
uiRestrictions={props.uiRestrictions}
/>
</div>
);
@ -70,6 +80,7 @@ Agg.propTypes = {
series: PropTypes.object,
siblings: PropTypes.array,
sortData: PropTypes.string,
uiRestrictions: PropTypes.object,
};
export default sortable(Agg);

View file

@ -22,6 +22,7 @@ import React from 'react';
import { EuiComboBox } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { injectI18n } from '@kbn/i18n/react';
import { isMetricEnabled } from '../../lib/check_ui_restrictions';
const metricAggs = [
{
@ -169,41 +170,45 @@ function filterByPanelType(panelType) {
}
function AggSelectUi(props) {
const { siblings, panelType, value, onChange, intl, ...rest } = props;
const selectedOption = allAggOptions.find(option => {
return value === option.value;
const { siblings, panelType, value, onChange, intl, uiRestrictions, ...rest } = props;
const selectedOptions = allAggOptions.filter(option => {
return value === option.value && isMetricEnabled(option.value, uiRestrictions);
});
const selectedOptions = selectedOption ? [selectedOption] : [];
let enablePipelines = siblings.some(
s => !!metricAggs.find(m => m.value === s.type)
);
if (siblings.length <= 1) enablePipelines = false;
let options;
if (panelType === 'metrics') {
options = metricAggs;
} else {
const disableSiblingAggs = agg => ({ ...agg, disabled: !enablePipelines || !isMetricEnabled(agg.value, uiRestrictions) });
options = [
{
label: intl.formatMessage({ id: 'tsvb.aggSelect.aggGroups.metricAggLabel', defaultMessage: 'Metric Aggregations' }),
options: metricAggs,
options: metricAggs
.map(agg => ({ ...agg, disabled: !isMetricEnabled(agg.value, uiRestrictions) })),
},
{
label: intl.formatMessage({
id: 'tsvb.aggSelect.aggGroups.parentPipelineAggLabel', defaultMessage: 'Parent Pipeline Aggregations' }),
options: pipelineAggs
.filter(filterByPanelType(panelType))
.map(agg => ({ ...agg, disabled: !enablePipelines })),
.map(disableSiblingAggs),
},
{
label: intl.formatMessage({
id: 'tsvb.aggSelect.aggGroups.siblingPipelineAggLabel', defaultMessage: 'Sibling Pipeline Aggregations' }),
options: siblingAggs.map(agg => ({ ...agg, disabled: !enablePipelines })),
options: siblingAggs.map(disableSiblingAggs),
},
{
label: intl.formatMessage({ id: 'tsvb.aggSelect.aggGroups.specialAggLabel', defaultMessage: 'Special Aggregations' }),
options: specialAggs.map(agg => ({ ...agg, disabled: !enablePipelines })),
options: specialAggs.map(disableSiblingAggs),
},
];
}
@ -233,6 +238,7 @@ AggSelectUi.propTypes = {
panelType: PropTypes.string,
siblings: PropTypes.array,
value: PropTypes.string,
uiRestrictions: PropTypes.object,
};
const AggSelect = injectI18n(AggSelectUi);

View file

@ -24,26 +24,39 @@ import {
} from '@elastic/eui';
import generateByTypeFilter from '../lib/generate_by_type_filter';
import { injectI18n } from '@kbn/i18n/react';
import { isFieldEnabled } from '../../lib/check_ui_restrictions';
function FieldSelectUi({
type,
fields,
indexPattern,
value,
onChange,
disabled,
restrict,
intl,
uiRestrictions,
...rest
}) {
function FieldSelectUi(props) {
const { type, fields, indexPattern, value, onChange, disabled, restrict, intl, ...rest } = props;
if (type === 'count') {
return null;
}
const options = (fields[indexPattern] || [])
.filter(generateByTypeFilter(restrict))
.map(field => {
return { label: field.name, value: field.name };
});
const selectedOption = options.find(option => {
return value === option.value;
});
const typeFilter = generateByTypeFilter(restrict);
const options = (fields[indexPattern] || [])
.filter(field => typeFilter(field) && isFieldEnabled(field.name, type, uiRestrictions))
.map(field => ({ label: field.name, value: field.name }));
const selectedOption = options.find(option => value === option.value);
const selectedOptions = selectedOption ? [selectedOption] : [];
return (
<EuiComboBox
placeholder={intl.formatMessage({ id: 'tsvb.fieldSelect.selectFieldPlaceholder', defaultMessage: 'Select field…' })}
placeholder={intl.formatMessage({
id: 'tsvb.fieldSelect.selectFieldPlaceholder',
defaultMessage: 'Select field...',
})}
isDisabled={disabled}
options={options}
selectedOptions={selectedOptions}
@ -57,7 +70,7 @@ function FieldSelectUi(props) {
FieldSelectUi.defaultProps = {
indexPattern: '*',
disabled: false,
restrict: 'none'
restrict: 'none',
};
FieldSelectUi.propTypes = {
@ -68,7 +81,8 @@ FieldSelectUi.propTypes = {
onChange: PropTypes.func,
restrict: PropTypes.string,
type: PropTypes.string,
value: PropTypes.string
value: PropTypes.string,
uiRestrictions: PropTypes.object,
};
const FieldSelect = injectI18n(FieldSelectUi);

View file

@ -28,8 +28,7 @@ import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFormLabel }
import { FormattedMessage } from '@kbn/i18n/react';
function StandardAgg(props) {
const { model, panel, series, fields } = props;
const { model, panel, series, fields, uiRestrictions } = props;
const handleChange = createChangeHandler(props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
let restrict = 'numeric';
@ -61,6 +60,7 @@ function StandardAgg(props) {
panelType={props.panel.type}
siblings={props.siblings}
value={model.type}
uiRestrictions={uiRestrictions}
onChange={handleSelectChange('type')}
fullWidth
/>
@ -85,6 +85,7 @@ function StandardAgg(props) {
indexPattern={indexPattern}
value={model.field}
onChange={handleSelectChange('field')}
uiRestrictions={uiRestrictions}
fullWidth
/>
</EuiFormRow>
@ -95,7 +96,6 @@ function StandardAgg(props) {
</EuiFlexGroup>
</AggRow>
);
}
StandardAgg.propTypes = {
@ -108,6 +108,7 @@ StandardAgg.propTypes = {
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
uiRestrictions: PropTypes.object,
};
export default StandardAgg;

View file

@ -128,6 +128,7 @@ const StandardSiblingAggUi = props => {
panelType={props.panel.type}
siblings={props.siblings}
value={model.type}
uiRestrictions={props.uiRestrictions}
onChange={handleSelectChange('type')}
/>
</EuiFlexItem>
@ -165,6 +166,7 @@ StandardSiblingAggUi.propTypes = {
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
uiRestrictions: PropTypes.object,
};
export const StandardSiblingAgg = injectI18n(StandardSiblingAggUi);

View file

@ -0,0 +1,45 @@
/*
* 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 AggRow from './agg_row';
import React from 'react';
import { EuiCode, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
export function TemporaryUnsupportedAgg(props) {
return (
<AggRow
disableDelete={props.disableDelete}
model={props.model}
onAdd={props.onAdd}
onDelete={props.onDelete}
siblings={props.siblings}
>
<EuiTitle className="tvbAggRow__unavailable" size="xxxs">
<span>
<FormattedMessage
id="tsvb.unsupportedAgg.aggIsTemporaryUnsupportedDescription"
defaultMessage="The {modelType} aggregation is currently unsupported."
values={{ modelType: (<EuiCode>{props.model.type}</EuiCode>) }}
/>
</span>
</EuiTitle>
</AggRow>
);
}

View file

@ -25,8 +25,9 @@ import Agg from '../aggs/agg';
export default function createAggRowRender(props) {
return (row, index, items) => {
const { panel, model, fields } = props;
const { panel, model, fields, uiRestrictions } = props;
const changeHandler = seriesChangeHandler(props, items);
return (
<Agg
key={row.id}
@ -39,6 +40,7 @@ export default function createAggRowRender(props) {
panel={panel}
series={model}
siblings={items}
uiRestrictions={uiRestrictions}
sortData={row.id}
/>
);

View file

@ -107,6 +107,7 @@ class GaugePanelConfigUi extends Component {
limit={1}
model={this.props.model}
name={this.props.name}
visData$={this.props.visData$}
onChange={this.props.onChange}
/>
);
@ -340,6 +341,7 @@ GaugePanelConfigUi.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
visData$: PropTypes.object,
};
const GaugePanelConfig = injectI18n(GaugePanelConfigUi);

View file

@ -110,6 +110,7 @@ class MarkdownPanelConfigUi extends Component {
fields={this.props.fields}
model={this.props.model}
name={this.props.name}
visData$={this.props.visData$}
onChange={this.props.onChange}
/>
);
@ -315,7 +316,8 @@ MarkdownPanelConfigUi.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
dateFormat: PropTypes.string
dateFormat: PropTypes.string,
visData$: PropTypes.object,
};
const MarkdownPanelConfig = injectI18n(MarkdownPanelConfigUi);

View file

@ -76,6 +76,7 @@ class MetricPanelConfig extends Component {
limit={2}
model={this.props.model}
name={this.props.name}
visData$={this.props.visData$}
onChange={this.props.onChange}
/>
);
@ -191,6 +192,7 @@ MetricPanelConfig.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
visData$: PropTypes.object,
};
export default MetricPanelConfig;

View file

@ -161,6 +161,7 @@ class TablePanelConfig extends Component {
fields={this.props.fields}
model={this.props.model}
name={this.props.name}
visData$={this.props.visData$}
onChange={this.props.onChange}
/>
</div>
@ -280,6 +281,7 @@ TablePanelConfig.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
visData$: PropTypes.object,
};
export default TablePanelConfig;

View file

@ -119,6 +119,7 @@ class TimeseriesPanelConfigUi extends Component {
fields={this.props.fields}
model={this.props.model}
name={this.props.name}
visData$={this.props.visData$}
onChange={this.props.onChange}
/>
);
@ -379,6 +380,7 @@ TimeseriesPanelConfigUi.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
visData$: PropTypes.object,
};
const TimeseriesPanelConfig = injectI18n(TimeseriesPanelConfigUi);

View file

@ -77,6 +77,7 @@ class TopNPanelConfig extends Component {
fields={this.props.fields}
model={this.props.model}
name={this.props.name}
visData$={this.props.visData$}
onChange={this.props.onChange}
/>
);
@ -246,6 +247,7 @@ TopNPanelConfig.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
visData$: PropTypes.object,
};
export default TopNPanelConfig;

View file

@ -19,7 +19,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import _ from 'lodash';
import { assign, get } from 'lodash';
import timeseries from './vis_types/timeseries/series';
import metric from './vis_types/metric/series';
@ -46,7 +46,10 @@ class Series extends Component {
this.state = {
visible: true,
selectedTab: 'metrics',
uiRestrictions: undefined,
};
this.visDataSubscription = null;
}
switchTab = (selectedTab) => {
@ -56,7 +59,7 @@ class Series extends Component {
handleChange = (part) => {
if (this.props.onChange) {
const { model } = this.props;
const doc = _.assign({}, model, part);
const doc = assign({}, model, part);
this.props.onChange(doc);
}
};
@ -71,12 +74,25 @@ class Series extends Component {
toggleVisible = (e) => {
e.preventDefault();
this.setState({ visible: !this.state.visible });
this.setState({
visible: !this.state.visible
});
};
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];
if (Component) {
const params = {
className: this.props.className,
@ -98,6 +114,7 @@ class Series extends Component {
selectedTab: this.state.selectedTab,
sortData: this.props.sortData,
style: this.props.style,
uiRestrictions: this.state.uiRestrictions,
switchTab: this.switchTab,
toggleVisible: this.toggleVisible,
togglePanelActivation: this.togglePanelActivation,
@ -116,6 +133,11 @@ class Series extends Component {
);
}
componentWillUnmount() {
if (this.visDataSubscription) {
this.visDataSubscription.unsubscribe();
}
}
}
Series.defaultProps = {
@ -139,6 +161,7 @@ Series.propTypes = {
onTouchStart: PropTypes.func,
model: PropTypes.object,
panel: PropTypes.object,
visData$: PropTypes.object,
sortData: PropTypes.string,
};

View file

@ -72,6 +72,7 @@ class SeriesEditor extends Component {
onClone={() => this.handleClone(row)}
onDelete={handleDelete.bind(null, props, row)}
onShouldSortItem={(direction) => this.sortSeries(index, direction, allSeries)}
visData$={this.props.visData$}
model={row}
panel={model}
sortData={row.id}
@ -115,7 +116,8 @@ SeriesEditor.propTypes = {
limit: PropTypes.number,
model: PropTypes.object,
name: PropTypes.string,
onChange: PropTypes.func
onChange: PropTypes.func,
visData$: PropTypes.object,
};
export default SeriesEditor;

View file

@ -20,71 +20,77 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import uuid from 'uuid';
import { get } from 'lodash';
import { SplitByTerms } from './splits/terms';
import { SplitByFilter } from './splits/filter';
import SplitByFilters from './splits/filters';
import SplitByEverything from './splits/everything';
import { SplitByFilters } from './splits/filters';
import { SplitByEverything } from './splits/everything';
import { SplitUnsupported } from './splits/unsupported_split';
import { isGroupByFieldsEnabled } from '../lib/check_ui_restrictions';
const SPLIT_MODES = {
FILTERS: 'filters',
FILTER: 'filter',
TERMS: 'terms',
EVERYTHING: 'everything',
};
class Split extends Component {
componentWillReceiveProps(nextProps) {
const { model } = nextProps;
if (model.split_mode === 'filters' && !model.split_filters) {
this.props.onChange({
split_filters: [
{ color: model.color, id: uuid.v1() }
]
{ color: model.color, id: uuid.v1() },
],
});
}
}
getComponent(splitMode, uiRestrictions) {
if (!isGroupByFieldsEnabled(splitMode, uiRestrictions)) {
return SplitUnsupported;
}
switch (splitMode) {
case SPLIT_MODES.TERMS:
return SplitByTerms;
case SPLIT_MODES.FILTER:
return SplitByFilter;
case SPLIT_MODES.FILTERS:
return SplitByFilters;
default:
return SplitByEverything;
}
}
render() {
const { model, panel } = this.props;
const { model, panel, uiRestrictions } = this.props;
const indexPattern = model.override_index_pattern &&
model.series_index_pattern ||
panel.index_pattern;
if (model.split_mode === 'filter') {
return (
<SplitByFilter
model={model}
onChange={this.props.onChange}
/>
);
}
if (model.split_mode === 'filters') {
return (
<SplitByFilters
model={model}
onChange={this.props.onChange}
/>
);
}
if (model.split_mode === 'terms') {
return (
<SplitByTerms
model={model}
indexPattern={indexPattern}
fields={this.props.fields}
onChange={this.props.onChange}
/>
);
}
return (
<SplitByEverything
model={model}
onChange={this.props.onChange}
/>
);
}
const splitMode = get(this.props, 'model.split_mode', SPLIT_MODES.EVERYTHING);
const Component = this.getComponent(splitMode, uiRestrictions);
return (
<Component
model={model}
indexPattern={indexPattern}
fields={this.props.fields}
onChange={this.props.onChange}
uiRestrictions={uiRestrictions}
/>);
}
}
Split.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
panel: PropTypes.object
panel: PropTypes.object,
};
export default Split;

View file

@ -55,6 +55,7 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js
}
indexPattern="kibana_sample_data_flights"
onChange={[Function]}
type="terms"
value="OriginCityName"
/>
</EuiFormRow>

View file

@ -18,14 +18,14 @@
*/
import createSelectHandler from '../lib/create_select_handler';
import GroupBySelect from './group_by_select';
import { GroupBySelect } from './group_by_select';
import PropTypes from 'prop-types';
import React from 'react';
import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
function SplitByEverything(props) {
const { onChange, model } = props;
export const SplitByEverything = (props) => {
const { onChange, model, uiRestrictions } = props;
const htmlId = htmlIdGenerator();
const handleSelectChange = createSelectHandler(onChange);
return (
@ -41,18 +41,18 @@ function SplitByEverything(props) {
<GroupBySelect
value={model.split_mode}
onChange={handleSelectChange('split_mode')}
uiRestrictions={uiRestrictions}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
);
}
};
SplitByEverything.propTypes = {
model: PropTypes.object,
onChange: PropTypes.func
onChange: PropTypes.func,
uiRestrictions: PropTypes.object,
};
export default SplitByEverything;

View file

@ -19,14 +19,14 @@
import createTextHandler from '../lib/create_text_handler';
import createSelectHandler from '../lib/create_select_handler';
import GroupBySelect from './group_by_select';
import { GroupBySelect } from './group_by_select';
import PropTypes from 'prop-types';
import React from 'react';
import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
export const SplitByFilter = props => {
const { onChange } = props;
const { onChange, uiRestrictions } = props;
const defaults = { filter: '' };
const model = { ...defaults, ...props.model };
const htmlId = htmlIdGenerator();
@ -45,6 +45,7 @@ export const SplitByFilter = props => {
<GroupBySelect
value={model.split_mode}
onChange={handleSelectChange('split_mode')}
uiRestrictions={uiRestrictions}
/>
</EuiFormRow>
</EuiFlexItem>
@ -68,5 +69,6 @@ export const SplitByFilter = props => {
SplitByFilter.propTypes = {
model: PropTypes.object,
onChange: PropTypes.func
onChange: PropTypes.func,
uiRestrictions: PropTypes.object,
};

View file

@ -18,18 +18,18 @@
*/
import createSelectHandler from '../lib/create_select_handler';
import GroupBySelect from './group_by_select';
import { GroupBySelect } from './group_by_select';
import FilterItems from './filter_items';
import PropTypes from 'prop-types';
import React from 'react';
import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
function SplitByFilters(props) {
const { onChange, model } = props;
export const SplitByFilters = (props) => {
const { onChange, model, uiRestrictions } = props;
const htmlId = htmlIdGenerator();
const handleSelectChange = createSelectHandler(onChange);
return(
return (
<div>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
@ -43,6 +43,7 @@ function SplitByFilters(props) {
<GroupBySelect
value={model.split_mode}
onChange={handleSelectChange('split_mode')}
uiRestrictions={uiRestrictions}
/>
</EuiFormRow>
</EuiFlexItem>
@ -55,11 +56,10 @@ function SplitByFilters(props) {
/>
</div>
);
}
};
SplitByFilters.propTypes = {
model: PropTypes.object,
onChange: PropTypes.func
onChange: PropTypes.func,
uiRestrictions: PropTypes.object,
};
export default SplitByFilters;

View file

@ -23,31 +23,43 @@ import {
EuiComboBox,
} from '@elastic/eui';
import { injectI18n } from '@kbn/i18n/react';
import { isGroupByFieldsEnabled } from '../../lib/check_ui_restrictions';
function GroupBySelectUi(props) {
const { intl } = props;
const modeOptions = [
const { intl, uiRestrictions } = props;
const modeOptions = ([
{
label: intl.formatMessage({ id: 'tsvb.splits.groupBySelect.modeOptions.everythingLabel', defaultMessage: 'Everything' }),
value: 'everything'
label: intl.formatMessage({
id: 'tsvb.splits.groupBySelect.modeOptions.everythingLabel',
defaultMessage: 'Everything',
}),
value: 'everything',
},
{
label: intl.formatMessage({ id: 'tsvb.splits.groupBySelect.modeOptions.filterLabel', defaultMessage: 'Filter' }),
value: 'filter'
value: 'filter',
},
{
label: intl.formatMessage({ id: 'tsvb.splits.groupBySelect.modeOptions.filtersLabel', defaultMessage: 'Filters' }),
value: 'filters'
label: intl.formatMessage({
id: 'tsvb.splits.groupBySelect.modeOptions.filtersLabel',
defaultMessage: 'Filters',
}),
value: 'filters',
},
{
label: intl.formatMessage({ id: 'tsvb.splits.groupBySelect.modeOptions.termsLabel', defaultMessage: 'Terms' }),
value: 'terms'
}
];
value: 'terms',
},
]).map(field => ({
...field,
disabled: !isGroupByFieldsEnabled(field.value, uiRestrictions),
}));
const selectedValue = props.value || 'everything';
const selectedOption = modeOptions.find(option => {
return selectedValue === option.value;
});
return (
<EuiComboBox
id={props.id}
@ -58,13 +70,12 @@ function GroupBySelectUi(props) {
singleSelection={{ asPlainText: true }}
/>
);
}
GroupBySelectUi.propTypes = {
onChange: PropTypes.func,
value: PropTypes.string
value: PropTypes.string,
uiRestrictions: PropTypes.object,
};
const GroupBySelect = injectI18n(GroupBySelectUi);
export default GroupBySelect;
export const GroupBySelect = injectI18n(GroupBySelectUi);

View file

@ -20,18 +20,26 @@
import PropTypes from 'prop-types';
import React from 'react';
import { get, find } from 'lodash';
import GroupBySelect from './group_by_select';
import { GroupBySelect } from './group_by_select';
import createTextHandler from '../lib/create_text_handler';
import createSelectHandler from '../lib/create_select_handler';
import FieldSelect from '../aggs/field_select';
import MetricSelect from '../aggs/metric_select';
import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiFieldNumber, EuiComboBox, EuiFieldText } from '@elastic/eui';
import {
htmlIdGenerator,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiFieldNumber,
EuiComboBox,
EuiFieldText
} from '@elastic/eui';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import { FIELD_TYPES } from '../../../common/field_types';
const DEFAULTS = { terms_direction: 'desc', terms_size: 10, terms_order_by: '_count' };
export const SplitByTermsUI = ({ onChange, indexPattern, intl, model: seriesModel, fields }) => {
export const SplitByTermsUI = ({ onChange, indexPattern, intl, model: seriesModel, fields, uiRestrictions }) => {
const htmlId = htmlIdGenerator();
const handleTextChange = createTextHandler(onChange);
const handleSelectChange = createSelectHandler(onChange);
@ -76,6 +84,7 @@ export const SplitByTermsUI = ({ onChange, indexPattern, intl, model: seriesMode
<GroupBySelect
value={model.split_mode}
onChange={handleSelectChange('split_mode')}
uiRestrictions={uiRestrictions}
/>
</EuiFormRow>
</EuiFlexItem>
@ -93,6 +102,8 @@ export const SplitByTermsUI = ({ onChange, indexPattern, intl, model: seriesMode
onChange={handleSelectChange('terms_field')}
value={model.terms_field}
fields={fields}
uiRestrictions={uiRestrictions}
type={'terms'}
/>
</EuiFormRow>
</EuiFlexItem>
@ -186,7 +197,8 @@ SplitByTermsUI.propTypes = {
model: PropTypes.object,
onChange: PropTypes.func,
indexPattern: PropTypes.string,
fields: PropTypes.object
fields: PropTypes.object,
uiRestrictions: PropTypes.object,
};
export const SplitByTerms = injectI18n(SplitByTermsUI);

View file

@ -0,0 +1,70 @@
/*
* 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 createSelectHandler from '../lib/create_select_handler';
import { GroupBySelect } from './group_by_select';
import PropTypes from 'prop-types';
import React from 'react';
import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiCode, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
export const SplitUnsupported = (props) => {
const { onChange, model, uiRestrictions } = props;
const htmlId = htmlIdGenerator();
const handleSelectChange = createSelectHandler(onChange);
return (
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiFormRow
id={htmlId('group')}
label={(<FormattedMessage
id="tsvb.splits.everything.groupByLabel"
defaultMessage="Group by"
/>)}
>
<GroupBySelect
value={model.split_mode}
onChange={handleSelectChange('split_mode')}
uiRestrictions={uiRestrictions}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle className="tvbAggRow__unavailable" size="xxxs">
<span>
<FormattedMessage
id="tsvb.unsupportedSplit.splitIsUnsupportedDescription"
defaultMessage="Split by {modelType} is unsupported."
values={{ modelType: (<EuiCode>{model.split_mode}</EuiCode>) }}
/>
</span>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
);
};
SplitUnsupported.propTypes = {
model: PropTypes.object,
onChange: PropTypes.func,
uiRestrictions: PropTypes.object,
};

View file

@ -43,7 +43,7 @@ class VisEditor extends Component {
extractedIndexPatterns: [''],
};
this.onBrush = brushHandler(props.vis.API.timeFilter);
this.visDataSubject = new Rx.Subject();
this.visDataSubject = new Rx.BehaviorSubject(this.props.visData);
this.visData$ = this.visDataSubject.asObservable().pipe(share());
}
@ -79,8 +79,8 @@ class VisEditor extends Component {
}
if (this.props.isEditorMode) {
const { params, fields } = this.props.vis;
const extractedIndexPatterns = extractIndexPatterns(params, fields);
const { params } = this.props.vis;
const extractedIndexPatterns = extractIndexPatterns(params);
if (!isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns)) {
fetchFields(extractedIndexPatterns)

View file

@ -41,7 +41,8 @@ function GaugeSeriesUi(props) {
disableAdd,
selectedTab,
visible,
intl
intl,
uiRestrictions
} = props;
const defaults = { label: '' };
@ -78,6 +79,7 @@ function GaugeSeriesUi(props) {
fields={fields}
panel={panel}
model={model}
uiRestrictions={uiRestrictions}
/>
</div>
</div>
@ -227,6 +229,7 @@ GaugeSeriesUi.propTypes = {
switchTab: PropTypes.func,
toggleVisible: PropTypes.func,
visible: PropTypes.bool,
uiRestrictions: PropTypes.object,
};
const GaugeSeries = injectI18n(GaugeSeriesUi);

View file

@ -39,7 +39,8 @@ function MarkdownSeriesUi(props) {
disableAdd,
selectedTab,
visible,
intl
intl,
uiRestrictions
} = props;
const defaults = { label: '', var_name: '' };
@ -76,6 +77,7 @@ function MarkdownSeriesUi(props) {
fields={fields}
panel={panel}
model={model}
uiRestrictions={uiRestrictions}
/>
</div>
</div>
@ -197,6 +199,7 @@ MarkdownSeriesUi.propTypes = {
switchTab: PropTypes.func,
toggleVisible: PropTypes.func,
visible: PropTypes.bool,
uiRestrictions: PropTypes.object,
};
const MarkdownSeries = injectI18n(MarkdownSeriesUi);

View file

@ -41,7 +41,8 @@ function MetricSeriesUi(props) {
disableAdd,
selectedTab,
visible,
intl
intl,
uiRestrictions
} = props;
const defaults = { label: '' };
@ -78,6 +79,7 @@ function MetricSeriesUi(props) {
fields={fields}
panel={panel}
model={model}
uiRestrictions={uiRestrictions}
/>
</div>
</div>
@ -232,6 +234,7 @@ MetricSeriesUi.propTypes = {
toggleVisible: PropTypes.func,
visible: PropTypes.bool,
togglePanelActivation: PropTypes.func,
uiRestrictions: PropTypes.object,
};
const MetricSeries = injectI18n(MetricSeriesUi);

View file

@ -202,6 +202,7 @@ TableSeries.propTypes = {
toggleVisible: PropTypes.func,
visible: PropTypes.bool,
togglePanelActivation: PropTypes.func,
uiRestrictions: PropTypes.object,
};
export default injectI18n(TableSeries);

View file

@ -41,7 +41,8 @@ const TimeseriesSeries = injectI18n(function (props) {
selectedTab,
onChange,
visible,
intl
intl,
uiRestrictions
} = props;
const defaults = { label: '' };
@ -78,6 +79,7 @@ const TimeseriesSeries = injectI18n(function (props) {
fields={fields}
panel={panel}
model={model}
uiRestrictions={uiRestrictions}
/>
</div>
</div>
@ -236,6 +238,7 @@ TimeseriesSeries.propTypes = {
toggleVisible: PropTypes.func,
visible: PropTypes.bool,
togglePanelActivation: PropTypes.func,
uiRestrictions: PropTypes.object,
};
export default injectI18n(TimeseriesSeries);

View file

@ -42,7 +42,8 @@ const TopNSeries = injectI18n(function (props) {
disableAdd,
selectedTab,
visible,
intl
intl,
uiRestrictions,
} = props;
const handleChange = createTextHandler(onChange);
@ -55,7 +56,7 @@ const TopNSeries = injectI18n(function (props) {
if (visible) {
let seriesBody;
if (selectedTab === 'metrics') {
const handleSort = (data) => {
const handleSort = data => {
const metrics = data.map(id => model.metrics.find(m => m.id === id));
props.onChange({ metrics });
};
@ -76,6 +77,7 @@ const TopNSeries = injectI18n(function (props) {
fields={fields}
panel={panel}
model={model}
uiRestrictions={uiRestrictions}
/>
</div>
</div>
@ -226,6 +228,7 @@ TopNSeries.propTypes = {
toggleVisible: PropTypes.func,
visible: PropTypes.bool,
togglePanelActivation: PropTypes.func,
uiRestrictions: PropTypes.object,
};
export default TopNSeries;

View file

@ -0,0 +1,50 @@
/*
* 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 { get } from 'lodash';
export const DEFAULT_UI_RESTRICTION = {
'*': true,
};
const RESTRICTION_TYPES = {
WHITE_LISTED_GROUP_BY_FIELDS: 'whiteListedGroupByFields',
WHITE_LISTER_METRICS: 'whiteListedMetrics',
};
const checkUIRestrictions = (key, restrictions = DEFAULT_UI_RESTRICTION, type) => {
const isAllEnabled = get(restrictions, `${type}.*`, true);
return isAllEnabled || get(restrictions, type, {})[key];
};
export const isMetricEnabled = (key, restrictions) => {
return checkUIRestrictions(key, restrictions, RESTRICTION_TYPES.WHITE_LISTER_METRICS);
};
export const isFieldEnabled = (field, metricType, restrictions = DEFAULT_UI_RESTRICTION) => {
if (isMetricEnabled(metricType, restrictions)) {
return checkUIRestrictions(field, restrictions[RESTRICTION_TYPES.WHITE_LISTER_METRICS], metricType);
}
return false;
};
export const isGroupByFieldsEnabled = (key, restrictions) => {
return checkUIRestrictions(key, restrictions, RESTRICTION_TYPES.WHITE_LISTED_GROUP_BY_FIELDS);
};

View file

@ -18,7 +18,7 @@
*/
import { uniq } from 'lodash';
export function extractIndexPatterns(params, fetchedFields) {
export function extractIndexPatterns(params, fetchedFields = {}) {
const patternsToFetch = [];
if (!fetchedFields[params.index_pattern]) {

View file

@ -33,10 +33,32 @@ export class DefaultSearchCapabilities {
return null;
}
get whiteListedMetrics() {
return this.createUiRestriction();
}
get whiteListedGroupByFields() {
return this.createUiRestriction();
}
get uiRestrictions() {
return {
whiteListedMetrics: this.whiteListedMetrics,
whiteListedGroupByFields: this.whiteListedGroupByFields,
};
}
get searchTimezone() {
return getTimezoneFromRequest(this.request);
}
createUiRestriction(restrictionsObject) {
return {
'*': !restrictionsObject,
...(restrictionsObject || {}),
};
}
parseInterval(interval) {
return parseInterval(interval);
}

View file

@ -36,27 +36,37 @@ export async function getSeriesData(req, panel) {
const body = (await Promise.all(bodiesPromises))
.reduce((acc, items) => acc.concat(items), []);
return searchRequest.search({ body })
.then(data => {
const series = data.map(handleResponseBody(panel));
return {
[panel.id]: {
id: panel.id,
series: series.reduce((acc, series) => acc.concat(series), []),
},
};
})
.then(resp => {
if (!panel.annotations || panel.annotations.length === 0) return resp;
return getAnnotations(req, panel, esQueryConfig, searchStrategy, capabilities).then(annotations => {
resp[panel.id].annotations = annotations;
return resp;
});
})
.then(resp => {
resp.type = panel.type;
return resp;
})
.catch(handleErrorResponse(panel));
}
const meta = {
type: panel.type,
uiRestrictions: capabilities.uiRestrictions,
};
try {
const data = await searchRequest.search({ body });
const series = data.map(handleResponseBody(panel));
let annotations = null;
if (panel.annotations && panel.annotations.length) {
annotations = await getAnnotations(req, panel, esQueryConfig, searchStrategy, capabilities);
}
return {
...meta,
[panel.id]: {
annotations,
id: panel.id,
series: series.reduce((acc, series) => acc.concat(series), []),
},
};
} catch (err) {
if (err.body) {
err.response = err.body;
return {
...meta,
...handleErrorResponse(panel)(err),
};
}
}
}

View file

@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import buildRequestBody from './table/build_request_body';
import handleErrorResponse from './handle_error_response';
import { get } from 'lodash';
@ -33,14 +32,27 @@ export async function getTableData(req, panel) {
const { indexPatternObject } = await getIndexPatternObject(req, panelIndexPattern);
const body = buildRequestBody(req, panel, esQueryConfig, indexPatternObject, capabilities);
const meta = {
type: panel.type,
uiRestrictions: capabilities.uiRestrictions,
};
try {
const [resp] = await searchRequest.search({ body });
const buckets = get(resp, 'aggregations.pivot.buckets', []);
return { type: 'table', series: buckets.map(processBucket(panel)) };
return {
...meta,
series: buckets.map(processBucket(panel)),
};
} catch (err) {
if (err.body) {
err.response = err.body;
return { type: 'table', ...handleErrorResponse(panel)(err) };
return {
...meta,
...handleErrorResponse(panel)(err)
};
}
}
}

View file

@ -64,7 +64,10 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter {
return Promise.all(requests)
.then(results => {
return results.map(result => {
const metricIds = Object.keys(result).filter(k => k !== 'type');
const metricIds = Object.keys(result).filter(
k => !['type', 'uiRestrictions'].includes(k)
);
return metricIds.map((id: string) => {
const infraMetricId: InfraMetric = (InfraMetric as any)[id];
if (!infraMetricId) {

View file

@ -3,18 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { get } from 'lodash';
import { get, has } from 'lodash';
import { unitsMap } from '@elastic/datemath';
const leastCommonInterval = (num = 0, base = 0) => Math.max(Math.ceil(num / base) * base, base);
const getDateHistogramAggregation = (fieldsCapabilities, rollupIndex) => {
const dateHistogramField = fieldsCapabilities[rollupIndex].aggs.date_histogram;
// there is also only one valid date_histogram field
return Object.values(dateHistogramField)[0];
};
const isCalendarInterval = ({ unit, value }) => value === 1 && ['calendar', 'mixed'].includes(unitsMap[unit].type);
export const getRollupSearchCapabilities = (DefaultSearchCapabilities) =>
@ -23,7 +15,13 @@ export const getRollupSearchCapabilities = (DefaultSearchCapabilities) =>
super(req, batchRequestsSupport, fieldsCapabilities);
this.rollupIndex = rollupIndex;
this.dateHistogram = getDateHistogramAggregation(fieldsCapabilities, rollupIndex);
this.availableMetrics = get(fieldsCapabilities, `${rollupIndex}.aggs`, {});
}
get dateHistogram() {
const [dateHistogram] = Object.values(this.availableMetrics.date_histogram);
return dateHistogram;
}
get defaultTimeInterval() {
@ -34,6 +32,30 @@ export const getRollupSearchCapabilities = (DefaultSearchCapabilities) =>
return get(this.dateHistogram, 'time_zone', null);
}
get whiteListedMetrics() {
const baseRestrictions = this.createUiRestriction({
count: this.createUiRestriction(),
});
const getFields = fields => Object.keys(fields)
.reduce((acc, item) => ({
...acc,
[item]: true,
}), this.createUiRestriction({}));
return Object.keys(this.availableMetrics).reduce((acc, item) => ({
...acc,
[item]: getFields(this.availableMetrics[item]),
}), baseRestrictions);
}
get whiteListedGroupByFields() {
return this.createUiRestriction({
everything: true,
terms: has(this.availableMetrics, 'terms'),
});
}
getValidTimeInterval(userIntervalString) {
const parsedRollupJobInterval = this.parseInterval(this.defaultTimeInterval);
const parsedUserInterval = this.parseInterval(userIntervalString);