Migrate old style queries stored in filters array (#38945) (#39293)

* Migrate old query filters

* Null check instead of undefined for more completeness

* remove unnecessary undefined check

* Use good defaults, not undefined, for brand new dashboards.

* fix: typescript errors

* be explicit instead of matchinline snapshot.

* default to Kuery when there is no query given
This commit is contained in:
Stacey Gammon 2019-06-19 15:49:58 -04:00 committed by GitHub
parent c08e13c64f
commit 4422fb1f57
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 479 additions and 64 deletions

View file

@ -0,0 +1,21 @@
/*
* 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.
*/
// @ts-ignore
export { migrations } from './migrations';

View file

@ -0,0 +1,31 @@
/*
* 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 { Doc } from './types';
export function isDoc(doc: { [key: string]: unknown } | Doc): doc is Doc {
return (
typeof doc.id === 'string' &&
typeof doc.type === 'string' &&
doc.attributes !== null &&
typeof doc.attributes === 'object' &&
doc.references !== null &&
typeof doc.references === 'object'
);
}

View file

@ -18,6 +18,7 @@
*/
import { cloneDeep, get, omit, has, flow } from 'lodash';
import { migrations730 as dashboardMigrations730 } from '../public/dashboard/migrations';
function migrateIndexPattern(doc) {
const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON');
@ -422,6 +423,7 @@ export const migrations = {
'7.0.0': (doc) => {
// Set new "references" attribute
doc.references = doc.references || [];
// Migrate index pattern
migrateIndexPattern(doc);
// Migrate panels
@ -455,6 +457,7 @@ export const migrations = {
doc.attributes.panelsJSON = JSON.stringify(panels);
return doc;
},
'7.3.0': dashboardMigrations730
},
search: {
'7.0.0': (doc) => {

View file

@ -0,0 +1,39 @@
/*
* 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 { SavedObjectReference } from '../../../../legacy/server/saved_objects/routes/types';
export interface SavedObjectAttributes {
kibanaSavedObjectMeta: {
searchSourceJSON: string;
};
}
export interface Doc<Attributes extends SavedObjectAttributes = SavedObjectAttributes> {
references: SavedObjectReference[];
attributes: Attributes;
id: string;
type: string;
}
export interface DocPre700<Attributes extends SavedObjectAttributes = SavedObjectAttributes> {
attributes: Attributes;
id: string;
type: string;
}

View file

@ -40,6 +40,8 @@ export function getSavedDashboardMock(
save: () => {
return Promise.resolve('123');
},
getQuery: () => ({ query: '', language: 'kuery' }),
getFilters: () => [],
...config,
};
}

View file

@ -24,9 +24,9 @@ import { stateMonitorFactory, StateMonitor } from 'ui/state_management/state_mon
import { StaticIndexPattern } from 'ui/index_patterns';
import { AppStateClass as TAppStateClass } from 'ui/state_management/app_state';
import { Timefilter } from 'ui/timefilter';
import { RefreshInterval } from 'ui/timefilter/timefilter';
import { Filter } from '@kbn/es-query';
import moment from 'moment';
import { RefreshInterval } from 'ui/timefilter/timefilter';
import { Query } from 'src/legacy/core_plugins/data/public';
import { TimeRange } from 'ui/timefilter/time_history';
import { DashboardViewMode } from './dashboard_view_mode';
@ -278,7 +278,7 @@ export class DashboardStateManager {
_pushFiltersToStore() {
const state = store.getState();
const dashboardFilters = this.getDashboardFilterBars();
const dashboardFilters = this.savedDashboard.getFilters();
if (
!_.isEqual(
FilterUtils.cleanFiltersForComparison(dashboardFilters),
@ -386,8 +386,8 @@ export class DashboardStateManager {
return {
timeTo: this.savedDashboard.timeTo,
timeFrom: this.savedDashboard.timeFrom,
filterBars: this.getDashboardFilterBars(),
query: this.getDashboardQuery(),
filterBars: this.savedDashboard.getFilters(),
query: this.savedDashboard.getQuery(),
};
}
@ -455,14 +455,6 @@ export class DashboardStateManager {
return this.savedDashboard.timeRestore;
}
public getDashboardFilterBars() {
return FilterUtils.getFilterBarsForDashboard(this.savedDashboard);
}
public getDashboardQuery() {
return FilterUtils.getQueryFilterForDashboard(this.savedDashboard);
}
public getLastSavedFilterBars(): Filter[] {
return this.lastSavedDashboardFilters.filterBars;
}

View file

@ -19,9 +19,7 @@
import _ from 'lodash';
import moment, { Moment } from 'moment';
import { QueryFilter } from 'ui/filter_manager/query_filter';
import { Filter } from '@kbn/es-query';
import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard';
/**
* @typedef {Object} QueryFilter
@ -30,51 +28,6 @@ import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard';
*/
export class FilterUtils {
/**
*
* @param filter
* @returns {Boolean} True if the filter is of the special query type
* (e.g. goes in the query input bar), false otherwise (e.g. is in the filter bar).
*/
public static isQueryFilter(filter: Filter) {
return filter.query && !filter.meta;
}
/**
*
* @param {SavedDashboard} dashboard
* @returns {Array.<Object>} An array of filters stored with the dashboard. Includes
* both query filters and filter bar filters.
*/
public static getDashboardFilters(dashboard: SavedObjectDashboard): Filter[] {
return dashboard.searchSource.getOwnField('filter');
}
/**
* Grabs a saved query to use from the dashboard, or if none exists, creates a default one.
* @param {SavedDashboard} dashboard
* @returns {QueryFilter}
*/
public static getQueryFilterForDashboard(dashboard: SavedObjectDashboard): QueryFilter | string {
if (dashboard.searchSource.getOwnField('query')) {
return dashboard.searchSource.getOwnField('query');
}
const dashboardFilters = this.getDashboardFilters(dashboard);
const dashboardQueryFilter = _.find(dashboardFilters, this.isQueryFilter);
return dashboardQueryFilter ? dashboardQueryFilter.query : '';
}
/**
* Returns the filters for the dashboard that should appear in the filter bar area.
* @param {SavedDashboard} dashboard
* @return {Array.<Object>} Array of filters that should appear in the filter bar for the
* given dashboard
*/
public static getFilterBarsForDashboard(dashboard: SavedObjectDashboard) {
return _.reject(this.getDashboardFilters(dashboard), this.isQueryFilter);
}
/**
* Converts the time to a utc formatted string. If the time is not valid (e.g. it might be in a relative format like
* 'now-15m', then it just returns what it was passed).

View file

@ -18,7 +18,6 @@
*/
import { DashboardViewMode } from '../dashboard_view_mode';
import { FilterUtils } from './filter_utils';
import { SavedObjectDashboard } from '../saved_dashboard/saved_dashboard';
import {
Pre61SavedDashboardPanel,
@ -37,8 +36,8 @@ export function getAppStateDefaults(
timeRestore: savedDashboard.timeRestore,
panels: savedDashboard.panelsJSON ? JSON.parse(savedDashboard.panelsJSON) : [],
options: savedDashboard.optionsJSON ? JSON.parse(savedDashboard.optionsJSON) : {},
query: FilterUtils.getQueryFilterForDashboard(savedDashboard),
filters: FilterUtils.getFilterBarsForDashboard(savedDashboard),
query: savedDashboard.getQuery(),
filters: savedDashboard.getFilters(),
viewMode:
savedDashboard.id || hideWriteControls ? DashboardViewMode.VIEW : DashboardViewMode.EDIT,
};

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export { migrations730 } from './migrations_730';

View file

@ -0,0 +1,35 @@
/*
* 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 { DashboardDoc } from './types';
import { isDoc } from '../../../migrations/is_doc';
export function isDashboardDoc(
doc: { [key: string]: unknown } | DashboardDoc
): doc is DashboardDoc {
if (!isDoc(doc)) {
return false;
}
if (typeof (doc as DashboardDoc).attributes.panelsJSON !== 'string') {
return false;
}
return true;
}

View file

@ -0,0 +1,60 @@
/*
* 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 { migrations730 } from './migrations_730';
import { DashboardDoc } from './types';
test('dashboard migration 7.3.0 migrates filters to query on search source', () => {
const doc: DashboardDoc = {
id: '1',
type: 'dashboard',
references: [],
attributes: {
description: '',
uiStateJSON: '{}',
version: 1,
timeRestore: false,
kibanaSavedObjectMeta: {
searchSourceJSON:
'{"filter":[{"query":{"query_string":{"query":"n: 6","analyze_wildcard":true}}}],"highlightAll":true,"version":true}',
},
panelsJSON:
'[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]',
},
};
const newDoc = migrations730(doc);
expect(newDoc).toMatchInlineSnapshot(`
Object {
"attributes": Object {
"description": "",
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "{\\"filter\\":[],\\"highlightAll\\":true,\\"version\\":true,\\"query\\":{\\"query\\":\\"n: 6\\",\\"language\\":\\"lucene\\"}}",
},
"panelsJSON": "[{\\"id\\":\\"1\\",\\"type\\":\\"visualization\\",\\"foo\\":true},{\\"id\\":\\"2\\",\\"type\\":\\"visualization\\",\\"bar\\":true}]",
"timeRestore": false,
"uiStateJSON": "{}",
"version": 1,
},
"id": "1",
"references": Array [],
"type": "dashboard",
}
`);
});

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 { DashboardDoc } from './types';
import { isDashboardDoc } from './is_dashboard_doc';
import { moveFiltersToQuery } from './move_filters_to_query';
export function migrations730(
doc:
| {
[key: string]: unknown;
}
| DashboardDoc
): DashboardDoc | { [key: string]: unknown } {
if (!isDashboardDoc(doc)) {
// NOTE: we should probably throw an error here... but for now following suit and in the
// case of errors, just returning the same document.
return doc;
}
try {
const searchSource = JSON.parse(doc.attributes.kibanaSavedObjectMeta.searchSourceJSON);
doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(
moveFiltersToQuery(searchSource)
);
return doc;
} catch (e) {
return doc;
}
}

View file

@ -0,0 +1,64 @@
/*
* 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 { moveFiltersToQuery, Pre600FilterQuery } from './move_filters_to_query';
import { Filter, FilterStateStore } from '@kbn/es-query';
const filter: Filter = {
meta: { disabled: false, negate: false, alias: '' },
query: {},
$state: { store: FilterStateStore.APP_STATE },
};
const queryFilter: Pre600FilterQuery = {
query: { query_string: { query: 'hi!', analyze_wildcard: true } },
};
test('Migrates an old filter query into the query field', () => {
const newSearchSource = moveFiltersToQuery({
filter: [filter, queryFilter],
});
expect(newSearchSource).toEqual({
filter: [
{
$state: { store: FilterStateStore.APP_STATE },
meta: {
alias: '',
disabled: false,
negate: false,
},
query: {},
},
],
query: {
language: 'lucene',
query: 'hi!',
},
});
});
test('Preserves query if search source is new', () => {
const newSearchSource = moveFiltersToQuery({
filter: [filter],
query: { query: 'bye', language: 'kuery' },
});
expect(newSearchSource.query).toEqual({ query: 'bye', language: 'kuery' });
});

View file

@ -0,0 +1,69 @@
/*
* 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 { Query } from 'src/legacy/core_plugins/data/public';
import { Filter } from '@kbn/es-query';
export interface Pre600FilterQuery {
// pre 6.0.0 global query:queryString:options were stored per dashboard and would
// be applied even if the setting was subsequently removed from the advanced
// settings. This is considered a bug, and this migration will fix that behavior.
query: { query_string: { query: string } & { [key: string]: unknown } };
}
export interface SearchSourcePre600 {
filter: Array<Filter | Pre600FilterQuery>;
}
export interface SearchSource730 {
filter: Filter[];
query: Query;
highlightAll?: boolean;
version?: boolean;
}
function isQueryFilter(filter: Filter | { query: unknown }): filter is Pre600FilterQuery {
return filter.query && !(filter as Filter).meta;
}
export function moveFiltersToQuery(
searchSource: SearchSourcePre600 | SearchSource730
): SearchSource730 {
const searchSource730: SearchSource730 = {
...searchSource,
filter: [],
query: (searchSource as SearchSource730).query || {
query: '',
language: 'kuery',
},
};
searchSource.filter.forEach(filter => {
if (isQueryFilter(filter)) {
searchSource730.query = {
query: filter.query.query_string.query,
language: 'lucene',
};
} else {
searchSource730.filter.push(filter);
}
});
return searchSource730;
}

View file

@ -0,0 +1,38 @@
/*
* 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 { Doc, DocPre700 } from '../../../migrations/types';
export interface SavedObjectAttributes {
kibanaSavedObjectMeta: {
searchSourceJSON: string;
};
}
interface DashboardAttributes extends SavedObjectAttributes {
panelsJSON: string;
description: string;
uiStateJSON: string;
version: number;
timeRestore: boolean;
}
export type DashboardDoc = Doc<DashboardAttributes>;
export type DashboardDocPre700 = DocPre700<DashboardAttributes>;

View file

@ -21,6 +21,8 @@ import { SearchSource } from 'ui/courier';
import { SavedObject } from 'ui/saved_objects/saved_object';
import moment from 'moment';
import { RefreshInterval } from 'ui/timefilter/timefilter';
import { Query } from 'src/legacy/core_plugins/data/public';
import { Filter } from '@kbn/es-query';
export interface SavedObjectDashboard extends SavedObject {
id?: string;
@ -38,4 +40,6 @@ export interface SavedObjectDashboard extends SavedObject {
searchSource: SearchSource;
destroy: () => void;
refreshInterval?: RefreshInterval;
getQuery(): Query;
getFilters(): Filter[];
}

View file

@ -71,7 +71,6 @@ module.factory('SavedDashboard', function (Private) {
clearSavedIndexPattern: true
});
this.showInRecentlyAccessed = true;
}
@ -113,5 +112,15 @@ module.factory('SavedDashboard', function (Private) {
return `/app/kibana#${createDashboardEditUrl(this.id)}`;
};
SavedDashboard.prototype.getQuery = function () {
return this.searchSource.getOwnField('query') ||
{ query: '', language: 'kuery' };
};
SavedDashboard.prototype.getFilters = function () {
return this.searchSource.getOwnField('filter') || [];
};
return SavedDashboard;
});

View file

@ -57,8 +57,39 @@ export default function ({ getService, getPageObjects }) {
kibanaBaseUrl = currentUrl.substring(0, currentUrl.indexOf('#'));
});
describe('6.0 urls', () => {
describe('5.6 urls', () => {
it('url with filters and query', async () => {
const url56 = `` +
`_g=(refreshInterval:(display:Off,pause:!f,value:0),` +
`time:(from:'2012-11-17T00:00:00.000Z',mode:absolute,to:'2015-11-17T18:01:36.621Z'))&` +
`_a=(` +
`description:'',` +
`filters:!(('$state':(store:appState),` +
`meta:(alias:!n,disabled:!f,index:'logstash-*',key:bytes,negate:!f,type:phrase,value:'12345'),` +
`query:(match:(bytes:(query:12345,type:phrase))))),` +
`fullScreenMode:!f,` +
`options:(),` +
`panels:!((col:1,id:Visualization-MetricChart,panelIndex:1,row:1,size_x:6,size_y:3,type:visualization),` +
`(col:7,id:Visualization-PieChart,panelIndex:2,row:1,size_x:6,size_y:3,type:visualization)),` +
`query:(query_string:(analyze_wildcard:!t,query:'memory:>220000')),` +
`timeRestore:!f,` +
`title:'New+Dashboard',` +
`uiState:(),` +
`viewMode:edit)`;
const url = `${kibanaBaseUrl}#/dashboard?${url56}`;
log.debug(`Navigating to ${url}`);
await browser.get(url, true);
await PageObjects.header.waitUntilLoadingHasFinished();
const query = await queryBar.getQueryString();
expect(query).to.equal('memory:>220000');
await pieChart.expectPieSliceCount(0);
await dashboardExpect.panelCount(2);
});
});
describe('6.0 urls', () => {
it('loads an unsaved dashboard', async function () {
const url = `${kibanaBaseUrl}#/dashboard?${urlQuery}`;
log.debug(`Navigating to ${url}`);