Skip to content

Commit

Permalink
Fix Kibana filters loop in Firefox
Browse files Browse the repository at this point in the history
  • Loading branch information
juankaromo committed Aug 8, 2019
1 parent c1f4e51 commit 2644b91
Show file tree
Hide file tree
Showing 8 changed files with 596 additions and 4 deletions.
4 changes: 2 additions & 2 deletions public/kibana-integrations/kibana-discover.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import {
} from 'ui/courier';
import { toastNotifications } from 'ui/notify';
import { VisProvider } from 'ui/vis';
import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter';
import { FilterBarQueryFilterProvider } from './search-bar/query-filter';
import { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib';
import { docTitle } from 'ui/doc_title';
import { intervalOptions } from 'ui/agg_types/buckets/_interval_options';
Expand All @@ -78,7 +78,7 @@ import uiRoutes from 'ui/routes';
import { StateProvider } from 'ui/state_management/state';
import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query';
import { subscribeWithScope } from 'ui/utils/subscribe_with_scope';
import { getFilterGenerator } from 'ui/filter_manager';
import { getFilterGenerator } from './search-bar/filter-generator';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
import { VisualizeLoaderProvider } from 'ui/visualize/loader/visualize_loader';
import { recentlyAccessed } from 'ui/persisted_log';
Expand Down
4 changes: 2 additions & 2 deletions public/kibana-integrations/loader/visualize_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
// @ts-ignore
import chrome from 'ui/chrome';
// @ts-ignore
import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter';
import { FilterBarQueryFilterProvider } from '../search-bar/query-filter';
// @ts-ignore
import { IPrivate } from 'ui/private';
import { EmbeddedVisualizeHandler } from './embedded_visualize_handler';
Expand Down Expand Up @@ -150,7 +150,7 @@ export class VisualizeLoader {
// lets add Private to the params, we'll need to pass it to visualize later
Private: this.Private,
};

// @ts-ignore
return new EmbeddedVisualizeHandler(element, savedObj, handlerParams, this.injector, this.errorHandler);
}
}
Expand Down
98 changes: 98 additions & 0 deletions public/kibana-integrations/search-bar/filter-generator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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 _ from 'lodash';
import { getPhraseScript } from '@kbn/es-query';

// Adds a filter to a passed state
export function getFilterGenerator(queryFilter) {
const filterGen = {};

filterGen.generate = (field, values, operation, index) => {
values = Array.isArray(values) ? values : [values];
const fieldName = _.isObject(field) ? field.name : field;
const filters = _.flatten([queryFilter.getAppFilters()]);
const newFilters = [];

const negate = (operation === '-');

// TODO: On array fields, negating does not negate the combination, rather all terms
_.each(values, function (value) {
let filter;
const existing = _.find(filters, function (filter) {
if (!filter) return;

if (fieldName === '_exists_' && filter.exists) {
return filter.exists.field === value;
}

if (_.has(filter, 'query.match')) {
return filter.query.match[fieldName] && filter.query.match[fieldName].query === value;
}

if (filter.script) {
return filter.meta.field === fieldName && filter.script.script.params.value === value;
}
});

if (existing) {
existing.meta.disabled = false;
if (existing.meta.negate !== negate) {
existing.meta.negate = !existing.meta.negate;
}
newFilters.push(existing);
return;
}

switch (fieldName) {
case '_exists_':
filter = {
meta: { negate, index },
exists: {
field: value
}
};
break;
default:
if (field.scripted) {
filter = {
meta: { negate, index, field: fieldName },
script: getPhraseScript(field, value)
};
} else {
filter = { meta: { negate, index }, query: { match: {} } };
filter.query.match[fieldName] = { query: value, type: 'phrase' };
}

break;
}

newFilters.push(filter);
});

return newFilters;
};

filterGen.add = function (field, values, operation, index) {
const newFilters = this.generate(field, values, operation, index);
return queryFilter.addFilters(newFilters);
};

return filterGen;
}
206 changes: 206 additions & 0 deletions public/kibana-integrations/search-bar/filter-manager.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*
* 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 { Filter, isFilterPinned, FilterStateStore } from '@kbn/es-query';

import _ from 'lodash';
import { Subject } from 'rxjs';

import { npSetup } from 'ui/new_platform';

// @ts-ignore
import { compareFilters } from 'plugins/data/filter/filter_manager/lib/compare_filters';
// @ts-ignore
import { mapAndFlattenFilters } from 'plugins/data/filter/filter_manager/lib/map_and_flatten_filters';
// @ts-ignore
import { uniqFilters } from 'plugins/data/filter/filter_manager/lib/uniq_filters';
// @ts-ignore
import { extractTimeFilter } from 'plugins/data/filter/filter_manager/lib/extract_time_filter';
// @ts-ignore
import { changeTimeFilter } from 'plugins/data/filter/filter_manager/lib/change_time_filter';

import { onlyDisabledFiltersChanged } from './only_disabled';

import { PartitionedFilters } from 'plugins/data/filter/filter_manager/partitioned_filters';

import { IndexPatterns } from 'plugins/data/index_patterns';

export class FilterManager {
private indexPatterns: IndexPatterns;
private filters: Filter[] = [];
private updated$: Subject<void> = new Subject();
private fetch$: Subject<void> = new Subject();

constructor(indexPatterns: IndexPatterns) {
this.indexPatterns = indexPatterns;
}

private mergeIncomingFilters(partitionedFilters: PartitionedFilters): Filter[] {
const globalFilters = partitionedFilters.globalFilters;
const appFilters = partitionedFilters.appFilters;

// existing globalFilters should be mutated by appFilters
_.each(appFilters, function (filter, i) {
const match = _.find(globalFilters, function (globalFilter) {
return compareFilters(globalFilter, filter);
});

// no match, do nothing
if (!match) return;

// matching filter in globalState, update global and remove from appState
_.assign(match.meta, filter.meta);
appFilters.splice(i, 1);
});

return FilterManager.mergeFilters(appFilters, globalFilters);
}

private static mergeFilters(appFilters: Filter[], globalFilters: Filter[]): Filter[] {
return uniqFilters(appFilters.reverse().concat(globalFilters.reverse())).reverse();
}

private static partitionFilters(filters: Filter[]): PartitionedFilters {
const [globalFilters, appFilters] = _.partition(filters, isFilterPinned);
return {
globalFilters,
appFilters,
};
}

private handleStateUpdate(newFilters: Filter[]) {
// global filters should always be first
newFilters.sort(({ $state: a }: Filter, { $state: b }: Filter): number => {
return a!.store === FilterStateStore.GLOBAL_STATE &&
b!.store !== FilterStateStore.GLOBAL_STATE
? -1
: 1;
});

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

this.filters = newFilters;
if (filtersUpdated) {
this.updated$.next();
if (!updatedOnlyDisabledFilters) {
this.fetch$.next();
}
}
}

/* Getters */

public getFilters() {
return _.cloneDeep(this.filters);
}

public getAppFilters() {
const { appFilters } = this.getPartitionedFilters();
return appFilters;
}

public getGlobalFilters() {
const { globalFilters } = this.getPartitionedFilters();
return globalFilters;
}

public getPartitionedFilters(): PartitionedFilters {
return FilterManager.partitionFilters(this.getFilters());
}

public getUpdates$() {
return this.updated$.asObservable();
}

public getFetches$() {
return this.fetch$.asObservable();
}

/* Setters */

public async addFilters(filters: Filter[] | Filter, pinFilterStatus?: boolean) {
if (!Array.isArray(filters)) {
filters = [filters];
}

if (filters.length === 0) {
return;
}

const { uiSettings } = npSetup.core;
if (pinFilterStatus === undefined) {
pinFilterStatus = uiSettings.get('filters:pinnedByDefault');
}

// Set the store of all filters. For now.
// In the future, all filters should come in with filter state store already set.
const store = pinFilterStatus ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE;
FilterManager.setFiltersStore(filters, store);

const mappedFilters = await mapAndFlattenFilters(this.indexPatterns, filters);

// This is where we add new filters to the correct place (app \ global)
const newPartitionedFilters = FilterManager.partitionFilters(mappedFilters);
const currentFilters = this.getPartitionedFilters();
currentFilters.appFilters.push(...newPartitionedFilters.appFilters);
currentFilters.globalFilters.push(...newPartitionedFilters.globalFilters);

const newFilters = this.mergeIncomingFilters(currentFilters);
this.handleStateUpdate(newFilters);
}

public async setFilters(newFilters: Filter[]) {
const mappedFilters = await mapAndFlattenFilters(this.indexPatterns, newFilters);
const newPartitionedFilters = FilterManager.partitionFilters(mappedFilters);
const mergedFilters = this.mergeIncomingFilters(newPartitionedFilters);
this.handleStateUpdate(mergedFilters);
}

public removeFilter(filter: Filter) {
const filterIndex = _.findIndex(this.filters, item => {
return _.isEqual(item.meta, filter.meta) && _.isEqual(item.query, filter.query);
});

if (filterIndex >= 0) {
const newFilters = _.cloneDeep(this.filters);
newFilters.splice(filterIndex, 1);
this.handleStateUpdate(newFilters);
}
}

public async removeAll() {
await this.setFilters([]);
}

public async addFiltersAndChangeTimeFilter(filters: Filter[]) {
const timeFilter = await extractTimeFilter(this.indexPatterns, filters);
if (timeFilter) changeTimeFilter(timeFilter);
return this.addFilters(filters.filter(filter => filter !== timeFilter));
}

public static setFiltersStore(filters: Filter[], store: FilterStateStore) {
_.map(filters, (filter: Filter) => {
// Override status only for filters that didn't have state in the first place.
if (filter.$state === undefined) {
filter.$state = { store };
}
});
}
}
Loading

0 comments on commit 2644b91

Please sign in to comment.