Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] KQL in TSVB (#36784) #38333

Merged
merged 1 commit into from
Jun 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,6 @@ describe('QueryBarInput', () => {
intl={null as any}
/>
);

expect(mockFetchIndexPatterns).toHaveBeenCalledWith(['logstash-*']);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ interface Props {
intl: InjectedIntl;
query: Query;
appName: string;
id?: string;
disableAutoFocus?: boolean;
screenTitle: string;
screenTitle?: string;
prepend?: any;
store: Storage;
persistedLog?: PersistedLog;
Expand Down Expand Up @@ -434,6 +435,7 @@ export class QueryBarInputUI extends Component<Props, State> {
<div role="search">
<div className="kuiLocalSearchAssistedInput">
<EuiFieldText
id={this.props.id}
placeholder={this.props.intl.formatMessage({
id: 'data.query.queryBar.searchInputPlaceholder',
defaultMessage: 'Search',
Expand All @@ -452,17 +454,21 @@ export class QueryBarInputUI extends Component<Props, State> {
}}
autoComplete="off"
spellCheck={false}
aria-label={this.props.intl.formatMessage(
{
id: 'data.query.queryBar.searchInputAriaLabel',
defaultMessage:
'You are on search box of {previouslyTranslatedPageTitle} page. Start typing to search and filter the {pageType}',
},
{
previouslyTranslatedPageTitle: this.props.screenTitle,
pageType: this.props.appName,
}
)}
aria-label={
this.props.screenTitle
? this.props.intl.formatMessage(
{
id: 'data.query.queryBar.searchInputAriaLabel',
defaultMessage:
'You are on search box of {previouslyTranslatedPageTitle} page. Start typing to search and filter the {pageType}',
},
{
previouslyTranslatedPageTitle: this.props.screenTitle,
pageType: this.props.appName,
}
)
: undefined
}
type="text"
data-test-subj="queryInput"
aria-autocomplete="list"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function fetchIndexPatterns(indexPatternStrings: string[]) {
return [];
}

const searchString = indexPatternStrings.join(' | ');
const searchString = indexPatternStrings.map(string => `"${string}"`).join(' | ');
const indexPatternsFromSavedObjects = await chrome.getSavedObjectsClient().find({
type: 'index-pattern',
fields: ['title', 'fields'],
Expand Down
97 changes: 94 additions & 3 deletions src/legacy/core_plugins/kibana/migrations.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,6 @@ const migrateDateHistogramAggregation = doc => {
return doc;
};

const executeMigrations720 = flow(migratePercentileRankAggregation, migrateDateHistogramAggregation);

function removeDateHistogramTimeZones(doc) {
const visStateJSON = get(doc, 'attributes.visState');
if (visStateJSON) {
Expand Down Expand Up @@ -189,6 +187,99 @@ function migrateGaugeVerticalSplitToAlignment(doc) {
}
return doc;
}
// Migrate filters (string -> { query: string, language: lucene })
/*
Enabling KQL in TSVB causes problems with savedObject visualizations when these are saved with filters.
In a visualisation type of saved object, if the visState param is of type metric, the filter is saved as a string that is not interpretted correctly as a lucene query in the visualization itself.
We need to transform the filter string into an object containing the original string as a query and specify the query language as lucene.
For Metrics visualizations (param.type === "metric"), filters can be applied to each series object in the series array within the SavedObject.visState.params object.
Path to the series array is thus:
attributes.visState.
*/
function transformFilterStringToQueryObject(doc) {
// Migrate filters
// If any filters exist and they are a string, we assume it to be lucene and transform the filter into an object accordingly
const newDoc = cloneDeep(doc);
const visStateJSON = get(doc, 'attributes.visState');
if (visStateJSON) {
let visState;
try {
visState = JSON.parse(visStateJSON);
} catch (e) {
// let it go, the data is invalid and we'll leave it as is
}
if (visState) {
const visType = get(visState, 'params.type');
const tsvbTypes = ['metric', 'markdown', 'top_n', 'gauge', 'table', 'timeseries'];
if (tsvbTypes.indexOf(visType) === -1) {
// skip
return doc;
}
// migrate the params fitler
const params = get(visState, 'params');
if (params.filter && typeof params.filter === 'string') {
const paramsFilterObject = {
query: params.filter,
language: 'lucene',
};
params.filter = paramsFilterObject;
}

// migrate the annotations query string:
const annotations = get(visState, 'params.annotations') || [];
annotations.forEach((item) => {
if (!item.query_string) {
// we don't need to transform anything if there isn't a filter at all
return;
}
if (typeof item.query_string === 'string') {
const itemQueryStringObject = {
query: item.query_string,
language: 'lucene',
};
item.query_string = itemQueryStringObject;
}
});
// migrate the series filters
const series = get(visState, 'params.series') || [];
series.forEach((item) => {
if (!item.filter) {
// we don't need to transform anything if there isn't a filter at all
return;
}
// series item filter
if (typeof item.filter === 'string') {
const itemfilterObject = {
query: item.filter,
language: 'lucene',
};
item.filter = itemfilterObject;
}
// series item split filters filter
if (item.split_filters) {
const splitFilters = get(item, 'split_filters') || [];
splitFilters.forEach((filter) => {
if (!filter.filter) {
// we don't need to transform anything if there isn't a filter at all
return;
}
if (typeof filter.filter === 'string') {
const filterfilterObject = {
query: filter.filter,
language: 'lucene',
};
filter.filter = filterfilterObject;
}
});
}
});
newDoc.attributes.visState = JSON.stringify(visState);
}
}
return newDoc;
}
const executeMigrations720 = flow(migratePercentileRankAggregation, migrateDateHistogramAggregation);
const executeMigrations730 = flow(migrateGaugeVerticalSplitToAlignment, transformFilterStringToQueryObject);

export const migrations = {
'index-pattern': {
Expand Down Expand Up @@ -292,7 +383,7 @@ export const migrations = {
},
'7.0.1': removeDateHistogramTimeZones,
'7.2.0': doc => executeMigrations720(doc),
'7.3.0': migrateGaugeVerticalSplitToAlignment
'7.3.0': executeMigrations730
},
dashboard: {
'7.0.0': (doc) => {
Expand Down
73 changes: 73 additions & 0 deletions src/legacy/core_plugins/kibana/migrations.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,79 @@ Object {
`);
});
});
describe('7.3.0 tsvb', () => {
const migrate = doc => migrations.visualization['7.3.0'](doc);
const generateDoc = ({ params }) => ({
attributes: {
title: 'My Vis',
description: 'This is my super cool vis.',
visState: JSON.stringify({ params }),
uiStateJSON: '{}',
version: 1,
kibanaSavedObjectMeta: {
searchSourceJSON: '{}',
},
},
});
it('should change series item filters from a string into an object', () => {
const params = { type: 'metric', series: [{ filter: 'Filter Bytes Test:>1000' }] };
const testDoc1 = generateDoc({ params });
const migratedTestDoc1 = migrate(testDoc1);
const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series;
expect(series[0].filter).toHaveProperty('query');
expect(series[0].filter).toHaveProperty('language');
});
it('should not change a series item filter string in the object after migration', () => {
const markdownParams = {
type: 'markdown',
series: [
{
filter: 'Filter Bytes Test:>1000',
split_filters: [{ filter: 'bytes:>1000' }],
}
]
};
const markdownDoc = generateDoc({ params: markdownParams });
const migratedMarkdownDoc = migrate(markdownDoc);
const markdownSeries = JSON.parse(migratedMarkdownDoc.attributes.visState).params.series;
expect(markdownSeries[0].filter.query).toBe(JSON.parse(markdownDoc.attributes.visState).params.series[0].filter);
expect(markdownSeries[0].split_filters[0].filter.query)
.toBe(JSON.parse(markdownDoc.attributes.visState).params.series[0].split_filters[0].filter);
});
it('should change series item filters from a string into an object for all filters', () => {
const params = {
type: 'timeseries',
filter: 'bytes:>1000',
series: [
{
filter: 'Filter Bytes Test:>1000',
split_filters: [{ filter: 'bytes:>1000' }],
}
],
annotations: [{ query_string: 'bytes:>1000' }],
};
const timeSeriesDoc = generateDoc({ params: params });
const migratedtimeSeriesDoc = migrate(timeSeriesDoc);
const timeSeriesParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params;
expect(Object.keys(timeSeriesParams.series[0].filter)).toEqual(expect.arrayContaining(['query', 'language']));
expect(Object.keys(timeSeriesParams.series[0].split_filters[0].filter)).toEqual(expect.arrayContaining(['query', 'language']));
expect(Object.keys(timeSeriesParams.annotations[0].query_string)).toEqual(expect.arrayContaining(['query', 'language']));
});
it('should not fail on a metric visualization without a filter in a series item', () => {
const params = { type: 'metric', series: [{}, {}, {}] };
const testDoc1 = generateDoc({ params });
const migratedTestDoc1 = migrate(testDoc1);
const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series;
expect(series[2]).not.toHaveProperty('filter.query');
});
it('should not migrate a visualization of unknown type', () => {
const params = { type: 'unknown', series: [{ filter: 'foo:bar' }] };
const doc = generateDoc({ params });
const migratedDoc = migrate(doc);
const series = JSON.parse(migratedDoc.attributes.visState).params.series;
expect(series[0].filter).toEqual(params.series[0].filter);
});
});
});

describe('dashboard', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import { FieldSelect } from './aggs/field_select';
import uuid from 'uuid';
import { IconSelect } from './icon_select';
import { YesNo } from './yes_no';
import { Storage } from 'ui/storage';
import { data } from 'plugins/data';
const { QueryBarInput } = data.query.ui;
import { getDefaultQueryLanguage } from './lib/get_default_query_language';

import {
htmlIdGenerator,
Expand Down Expand Up @@ -58,8 +62,8 @@ function newAnnotation() {

const RESTRICT_FIELDS = [ES_TYPES.DATE];

const localStorage = new Storage(window.localStorage);
export class AnnotationsEditor extends Component {

constructor(props) {
super(props);
this.renderRow = this.renderRow.bind(this);
Expand All @@ -73,9 +77,20 @@ export class AnnotationsEditor extends Component {
handleChange(_.assign({}, item, part));
};
}

handleQueryChange = (model, filter) => {
const part = { query_string: filter };
collectionActions.handleChange(this.props, {
...model,
...part
});
};
renderRow(row) {
const defaults = { fields: '', template: '', index_pattern: '*', query_string: '' };
const defaults = {
fields: '',
template: '',
index_pattern: '*',
query_string: { query: '', language: getDefaultQueryLanguage() }
};
const model = { ...defaults, ...row };
const handleChange = (part) => {
const fn = collectionActions.handleChange.bind(null, this.props);
Expand Down Expand Up @@ -154,10 +169,16 @@ export class AnnotationsEditor extends Component {
/>)}
fullWidth
>
<EuiFieldText
onChange={this.handleChange(model, 'query_string')}
value={model.query_string}
fullWidth
<QueryBarInput
query={{
language: model.query_string.language || getDefaultQueryLanguage(),
query: model.query_string.query || '',
}}
onChange={query => this.handleQueryChange(model, query)}
appName={'VisEditor'}
indexPatterns={[model.index_pattern]}
store={localStorage}
showDatePicker={false}
/>
</EuiFormRow>
</EuiFlexItem>
Expand Down Expand Up @@ -250,7 +271,6 @@ export class AnnotationsEditor extends Component {
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>

</EuiFlexItem>

<EuiFlexItem grow={false}>
Expand Down Expand Up @@ -312,7 +332,6 @@ export class AnnotationsEditor extends Component {
</div>
);
}

}

AnnotationsEditor.defaultProps = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 chrome from 'ui/chrome';

export function getDefaultQueryLanguage() {
return chrome.getUiSettingsClient().get('search:queryLanguage');
}
Loading