Skip to content

Commit

Permalink
[BUG] Data fetching twice on discover timefilter change (elastic#55279)
Browse files Browse the repository at this point in the history
* Fix bug elastic#54887
 - Filters are not only fetch once on timefilter change
 - Make sure that discover doesn't fetch data when a disabled filter is changed
 - Support compareFilters on an array of filters.
 - Added tests to compare filters
 - Exctracted sortFilters and added tests to it.

* code review + FilterCompareOptions

* Remove sort by

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
Liza Katz and elasticmachine committed Jan 21, 2020
1 parent 1f1a0da commit 69edee7
Show file tree
Hide file tree
Showing 9 changed files with 351 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ import _ from 'lodash';
import { State } from 'ui/state_management/state';
import { FilterManager, esFilters } from '../../../../../../plugins/data/public';

import {
compareFilters,
COMPARE_ALL_OPTIONS,
// this whole file will soon be deprecated by new state management.
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../../../../plugins/data/public/query/filter_manager/lib/compare_filters';

type GetAppStateFunc = () => State | undefined | null;

/**
Expand Down Expand Up @@ -63,8 +70,16 @@ export class FilterStateManager {
const globalFilters = this.globalState.filters || [];
const appFilters = (appState && appState.filters) || [];

const globalFilterChanged = !_.isEqual(this.filterManager.getGlobalFilters(), globalFilters);
const appFilterChanged = !_.isEqual(this.filterManager.getAppFilters(), appFilters);
const globalFilterChanged = !compareFilters(
this.filterManager.getGlobalFilters(),
globalFilters,
COMPARE_ALL_OPTIONS
);
const appFilterChanged = !compareFilters(
this.filterManager.getAppFilters(),
appFilters,
COMPARE_ALL_OPTIONS
);
const filterStateChanged = globalFilterChanged || appFilterChanged;

if (!filterStateChanged) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ function discoverController(

// fetch data when filters fire fetch event
subscriptions.add(
subscribeWithScope($scope, filterManager.getUpdates$(), {
subscribeWithScope($scope, filterManager.getFetches$(), {
next: $scope.fetch,
})
);
Expand Down
18 changes: 4 additions & 14 deletions src/plugins/data/public/query/filter_manager/filter_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import { Subject } from 'rxjs';

import { IUiSettingsClient } from 'src/core/public';

import { compareFilters } from './lib/compare_filters';
import { compareFilters, COMPARE_ALL_OPTIONS } from './lib/compare_filters';
import { sortFilters } from './lib/sort_filters';
import { mapAndFlattenFilters } from './lib/map_and_flatten_filters';
import { uniqFilters } from './lib/uniq_filters';
import { onlyDisabledFiltersChanged } from './lib/only_disabled';
Expand Down Expand Up @@ -76,20 +77,9 @@ export class FilterManager {
}

private handleStateUpdate(newFilters: esFilters.Filter[]) {
// global filters should always be first

newFilters.sort(({ $state: a }: esFilters.Filter, { $state: b }: esFilters.Filter): number => {
if (a!.store === b!.store) {
return 0;
} else {
return a!.store === esFilters.FilterStateStore.GLOBAL_STATE &&
b!.store !== esFilters.FilterStateStore.GLOBAL_STATE
? -1
: 1;
}
});
newFilters.sort(sortFilters);

const filtersUpdated = !_.isEqual(this.filters, newFilters);
const filtersUpdated = !compareFilters(this.filters, newFilters, COMPARE_ALL_OPTIONS);
const updatedOnlyDisabledFilters = onlyDisabledFiltersChanged(newFilters, this.filters);

this.filters = newFilters;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/

import { compareFilters } from './compare_filters';
import { compareFilters, COMPARE_ALL_OPTIONS } from './compare_filters';
import { esFilters } from '../../../../common';

describe('filter manager utilities', () => {
Expand Down Expand Up @@ -83,5 +83,134 @@ describe('filter manager utilities', () => {

expect(compareFilters(f1, f2)).toBeTruthy();
});

test('should compare filters array to non array', () => {
const f1 = esFilters.buildQueryFilter(
{ _type: { match: { query: 'apache', type: 'phrase' } } },
'index',
''
);

const f2 = esFilters.buildQueryFilter(
{ _type: { match: { query: 'mochi', type: 'phrase' } } },
'index',
''
);

expect(compareFilters([f1, f2], f1)).toBeFalsy();
});

test('should compare filters array to partial array', () => {
const f1 = esFilters.buildQueryFilter(
{ _type: { match: { query: 'apache', type: 'phrase' } } },
'index',
''
);

const f2 = esFilters.buildQueryFilter(
{ _type: { match: { query: 'mochi', type: 'phrase' } } },
'index',
''
);

expect(compareFilters([f1, f2], [f1])).toBeFalsy();
});

test('should compare filters array to exact array', () => {
const f1 = esFilters.buildQueryFilter(
{ _type: { match: { query: 'apache', type: 'phrase' } } },
'index',
''
);

const f2 = esFilters.buildQueryFilter(
{ _type: { match: { query: 'mochi', type: 'phrase' } } },
'index',
''
);

expect(compareFilters([f1, f2], [f1, f2])).toBeTruthy();
});

test('should compare array of duplicates, ignoring meta attributes', () => {
const f1 = esFilters.buildQueryFilter(
{ _type: { match: { query: 'apache', type: 'phrase' } } },
'index1',
''
);
const f2 = esFilters.buildQueryFilter(
{ _type: { match: { query: 'apache', type: 'phrase' } } },
'index2',
''
);

expect(compareFilters([f1], [f2])).toBeTruthy();
});

test('should compare array of duplicates, ignoring $state attributes', () => {
const f1 = {
$state: { store: esFilters.FilterStateStore.APP_STATE },
...esFilters.buildQueryFilter(
{ _type: { match: { query: 'apache', type: 'phrase' } } },
'index',
''
),
};
const f2 = {
$state: { store: esFilters.FilterStateStore.GLOBAL_STATE },
...esFilters.buildQueryFilter(
{ _type: { match: { query: 'apache', type: 'phrase' } } },
'index',
''
),
};

expect(compareFilters([f1], [f2])).toBeTruthy();
});

test('should compare duplicates with COMPARE_ALL_OPTIONS should check store', () => {
const f1 = {
$state: { store: esFilters.FilterStateStore.APP_STATE },
...esFilters.buildQueryFilter(
{ _type: { match: { query: 'apache', type: 'phrase' } } },
'index',
''
),
};
const f2 = {
$state: { store: esFilters.FilterStateStore.GLOBAL_STATE },
...esFilters.buildQueryFilter(
{ _type: { match: { query: 'apache', type: 'phrase' } } },
'index',
''
),
};

expect(compareFilters([f1], [f2], COMPARE_ALL_OPTIONS)).toBeFalsy();
});

test('should compare duplicates with COMPARE_ALL_OPTIONS should not check key and value ', () => {
const f1 = {
$state: { store: esFilters.FilterStateStore.GLOBAL_STATE },
...esFilters.buildQueryFilter(
{ _type: { match: { query: 'apache', type: 'phrase' } } },
'index',
''
),
};
const f2 = {
$state: { store: esFilters.FilterStateStore.GLOBAL_STATE },
...esFilters.buildQueryFilter(
{ _type: { match: { query: 'apache', type: 'phrase' } } },
'index',
''
),
};

f2.meta.key = 'wassup';
f2.meta.value = 'dog';

expect(compareFilters([f1], [f2], COMPARE_ALL_OPTIONS)).toBeTruthy();
});
});
});
78 changes: 61 additions & 17 deletions src/plugins/data/public/query/filter_manager/lib/compare_filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,64 @@
* under the License.
*/

import { defaults, isEqual, omit } from 'lodash';
import { defaults, isEqual, omit, map } from 'lodash';
import { esFilters } from '../../../../common';

export interface FilterCompareOptions {
disabled?: boolean;
negate?: boolean;
state?: boolean;
}

/**
* Include disabled, negate and store when comparing filters
*/
export const COMPARE_ALL_OPTIONS: FilterCompareOptions = {
disabled: true,
negate: true,
state: true,
};

const mapFilter = (
filter: esFilters.Filter,
comparators: FilterCompareOptions,
excludedAttributes: string[]
) => {
const cleaned: esFilters.FilterMeta = omit(filter, excludedAttributes);

if (comparators.negate) cleaned.negate = filter.meta && Boolean(filter.meta.negate);
if (comparators.disabled) cleaned.disabled = filter.meta && Boolean(filter.meta.disabled);

return cleaned;
};

const mapFilterArray = (
filters: esFilters.Filter[],
comparators: FilterCompareOptions,
excludedAttributes: string[]
) => {
return map(filters, (filter: esFilters.Filter) =>
mapFilter(filter, comparators, excludedAttributes)
);
};

/**
* Compare two filters to see if they match
* Compare two filters or filter arrays to see if they match.
* For filter arrays, the assumption is they are sorted.
*
* @param {object} first The first filter to compare
* @param {object} second The second filter to compare
* @param {object} comparatorOptions Parameters to use for comparison
* @param {esFilters.Filter | esFilters.Filter[]} first The first filter or filter array to compare
* @param {esFilters.Filter | esFilters.Filter[]} second The second filter or filter array to compare
* @param {FilterCompareOptions} comparatorOptions Parameters to use for comparison
*
* @returns {bool} Filters are the same
*/
export const compareFilters = (
first: esFilters.Filter,
second: esFilters.Filter,
comparatorOptions: any = {}
first: esFilters.Filter | esFilters.Filter[],
second: esFilters.Filter | esFilters.Filter[],
comparatorOptions: FilterCompareOptions = {}
) => {
let comparators: any = {};
const mapFilter = (filter: esFilters.Filter) => {
const cleaned: esFilters.FilterMeta = omit(filter, excludedAttributes);

if (comparators.negate) cleaned.negate = filter.meta && Boolean(filter.meta.negate);
if (comparators.disabled) cleaned.disabled = filter.meta && Boolean(filter.meta.disabled);
let comparators: FilterCompareOptions = {};

return cleaned;
};
const excludedAttributes: string[] = ['$$hashKey', 'meta'];

comparators = defaults(comparatorOptions || {}, {
Expand All @@ -53,5 +85,17 @@ export const compareFilters = (

if (!comparators.state) excludedAttributes.push('$state');

return isEqual(mapFilter(first), mapFilter(second));
if (Array.isArray(first) && Array.isArray(second)) {
return isEqual(
mapFilterArray(first, comparators, excludedAttributes),
mapFilterArray(second, comparators, excludedAttributes)
);
} else if (!Array.isArray(first) && !Array.isArray(second)) {
return isEqual(
mapFilter(first, comparators, excludedAttributes),
mapFilter(second, comparators, excludedAttributes)
);
} else {
return false;
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { filter, find } from 'lodash';
import { compareFilters } from './compare_filters';
import { compareFilters, FilterCompareOptions } from './compare_filters';
import { esFilters } from '../../../../common';

/**
Expand All @@ -33,7 +33,7 @@ import { esFilters } from '../../../../common';
export const dedupFilters = (
existingFilters: esFilters.Filter[],
filters: esFilters.Filter[],
comparatorOptions: any = {}
comparatorOptions: FilterCompareOptions = {}
) => {
if (!Array.isArray(filters)) {
filters = [filters];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
* under the License.
*/

import { filter, isEqual } from 'lodash';
import { filter } from 'lodash';
import { esFilters } from '../../../../common';
import { compareFilters, COMPARE_ALL_OPTIONS } from './compare_filters';

const isEnabled = (f: esFilters.Filter) => f && f.meta && !f.meta.disabled;

Expand All @@ -35,5 +36,5 @@ export const onlyDisabledFiltersChanged = (
const newEnabledFilters = filter(newFilters || [], isEnabled);
const oldEnabledFilters = filter(oldFilters || [], isEnabled);

return isEqual(oldEnabledFilters, newEnabledFilters);
return compareFilters(oldEnabledFilters, newEnabledFilters, COMPARE_ALL_OPTIONS);
};
Loading

0 comments on commit 69edee7

Please sign in to comment.