mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
parent
54d46696eb
commit
5341d11005
120 changed files with 1998 additions and 1811 deletions
|
@ -169,6 +169,7 @@
|
|||
"http-proxy-agent": "^2.1.0",
|
||||
"https-proxy-agent": "^2.2.1",
|
||||
"inert": "^5.1.0",
|
||||
"inline-style": "^2.0.0",
|
||||
"joi": "^13.5.2",
|
||||
"jquery": "^3.4.1",
|
||||
"js-yaml": "3.13.1",
|
||||
|
|
|
@ -38,7 +38,7 @@ export interface FilterMeta {
|
|||
}
|
||||
|
||||
export interface Filter {
|
||||
$state: FilterState;
|
||||
$state?: FilterState;
|
||||
meta: FilterMeta;
|
||||
query?: object;
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ export function buildEmptyFilter(isPinned: boolean, index?: string): Filter {
|
|||
}
|
||||
|
||||
export function isFilterPinned(filter: Filter) {
|
||||
return filter.$state.store === FilterStateStore.GLOBAL_STATE;
|
||||
return filter.$state && filter.$state.store === FilterStateStore.GLOBAL_STATE;
|
||||
}
|
||||
|
||||
export function toggleFilterDisabled(filter: Filter) {
|
||||
|
|
|
@ -21,7 +21,7 @@ import 'ngreact';
|
|||
import { uiModules } from 'ui/modules';
|
||||
import template from './directive.html';
|
||||
import { ApplyFiltersPopover } from './apply_filters_popover';
|
||||
import { mapAndFlattenFilters } from 'ui/filter_manager/lib/map_and_flatten_filters';
|
||||
import { mapAndFlattenFilters } from '../filter_manager/lib/map_and_flatten_filters';
|
||||
import { wrapInI18nContext } from 'ui/i18n';
|
||||
|
||||
const app = uiModules.get('app/data', ['react']);
|
||||
|
|
|
@ -441,17 +441,20 @@ class FilterEditorUI extends Component<Props, State> {
|
|||
queryDsl,
|
||||
} = this.state;
|
||||
|
||||
const { store } = this.props.filter.$state;
|
||||
const { $state } = this.props.filter;
|
||||
if (!$state || !$state.store) {
|
||||
return; // typescript validation
|
||||
}
|
||||
const alias = useCustomLabel ? customLabel : null;
|
||||
|
||||
if (isCustomEditorOpen) {
|
||||
const { index, disabled, negate } = this.props.filter.meta;
|
||||
const newIndex = index || this.props.indexPatterns[0].id;
|
||||
const body = JSON.parse(queryDsl);
|
||||
const filter = buildCustomFilter(newIndex, body, disabled, negate, alias, store);
|
||||
const filter = buildCustomFilter(newIndex, body, disabled, negate, alias, $state.store);
|
||||
this.props.onSubmit(filter);
|
||||
} else if (indexPattern && field && operator) {
|
||||
const filter = buildFilter(indexPattern, field, operator, params, alias, store);
|
||||
const filter = buildFilter(indexPattern, field, operator, params, alias, $state.store);
|
||||
this.props.onSubmit(filter);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -239,7 +239,11 @@ describe('Filter editor utils', () => {
|
|||
const filter = buildFilter(mockIndexPattern, mockFields[0], isOperator, params, alias, state);
|
||||
expect(filter.meta.negate).toBe(isOperator.negate);
|
||||
expect(filter.meta.alias).toBe(alias);
|
||||
expect(filter.$state.store).toBe(state);
|
||||
|
||||
expect(filter.$state).toBeDefined();
|
||||
if (filter.$state) {
|
||||
expect(filter.$state.store).toBe(state);
|
||||
}
|
||||
});
|
||||
|
||||
it('should build phrases filters', () => {
|
||||
|
@ -257,7 +261,10 @@ describe('Filter editor utils', () => {
|
|||
expect(filter.meta.type).toBe(isOneOfOperator.type);
|
||||
expect(filter.meta.negate).toBe(isOneOfOperator.negate);
|
||||
expect(filter.meta.alias).toBe(alias);
|
||||
expect(filter.$state.store).toBe(state);
|
||||
expect(filter.$state).toBeDefined();
|
||||
if (filter.$state) {
|
||||
expect(filter.$state.store).toBe(state);
|
||||
}
|
||||
});
|
||||
|
||||
it('should build range filters', () => {
|
||||
|
@ -274,7 +281,10 @@ describe('Filter editor utils', () => {
|
|||
);
|
||||
expect(filter.meta.negate).toBe(isBetweenOperator.negate);
|
||||
expect(filter.meta.alias).toBe(alias);
|
||||
expect(filter.$state.store).toBe(state);
|
||||
expect(filter.$state).toBeDefined();
|
||||
if (filter.$state) {
|
||||
expect(filter.$state.store).toBe(state);
|
||||
}
|
||||
});
|
||||
|
||||
it('should build exists filters', () => {
|
||||
|
@ -291,7 +301,10 @@ describe('Filter editor utils', () => {
|
|||
);
|
||||
expect(filter.meta.negate).toBe(existsOperator.negate);
|
||||
expect(filter.meta.alias).toBe(alias);
|
||||
expect(filter.$state.store).toBe(state);
|
||||
expect(filter.$state).toBeDefined();
|
||||
if (filter.$state) {
|
||||
expect(filter.$state.store).toBe(state);
|
||||
}
|
||||
});
|
||||
|
||||
it('should negate based on operator', () => {
|
||||
|
@ -308,7 +321,10 @@ describe('Filter editor utils', () => {
|
|||
);
|
||||
expect(filter.meta.negate).toBe(doesNotExistOperator.negate);
|
||||
expect(filter.meta.alias).toBe(alias);
|
||||
expect(filter.$state.store).toBe(state);
|
||||
expect(filter.$state).toBeDefined();
|
||||
if (filter.$state) {
|
||||
expect(filter.$state.store).toBe(state);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,683 @@
|
|||
/*
|
||||
* 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 sinon from 'sinon';
|
||||
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Filter, FilterStateStore } from '@kbn/es-query';
|
||||
|
||||
import { FilterStateManager } from './filter_state_manager';
|
||||
import { FilterManager } from './filter_manager';
|
||||
|
||||
import { getFilter } from './test_helpers/get_stub_filter';
|
||||
import { StubIndexPatterns } from './test_helpers/stub_index_pattern';
|
||||
import { StubState } from './test_helpers/stub_state';
|
||||
import { getFiltersArray } from './test_helpers/get_filters_array';
|
||||
|
||||
jest.mock(
|
||||
'ui/chrome',
|
||||
() => ({
|
||||
getBasePath: jest.fn(() => 'path'),
|
||||
getUiSettingsClient: jest.fn(() => {
|
||||
return {
|
||||
get: () => true,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
jest.mock('ui/new_platform', () => ({
|
||||
npStart: {
|
||||
core: {
|
||||
chrome: {
|
||||
recentlyAccessed: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
npSetup: {
|
||||
core: {
|
||||
uiSettings: {
|
||||
get: () => true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('filter_manager', () => {
|
||||
let appStateStub: StubState;
|
||||
let globalStateStub: StubState;
|
||||
|
||||
let updateSubscription: Subscription | undefined;
|
||||
let fetchSubscription: Subscription | undefined;
|
||||
let updateListener: sinon.SinonSpy<any[], any>;
|
||||
|
||||
let filterManager: FilterManager;
|
||||
let indexPatterns: any;
|
||||
let readyFilters: Filter[];
|
||||
|
||||
beforeEach(() => {
|
||||
updateListener = sinon.stub();
|
||||
appStateStub = new StubState();
|
||||
globalStateStub = new StubState();
|
||||
indexPatterns = new StubIndexPatterns();
|
||||
filterManager = new FilterManager(indexPatterns);
|
||||
readyFilters = getFiltersArray();
|
||||
|
||||
// FilterStateManager is tested indirectly.
|
||||
// Therefore, we don't need it's instance.
|
||||
new FilterStateManager(
|
||||
globalStateStub,
|
||||
() => {
|
||||
return appStateStub;
|
||||
},
|
||||
filterManager
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (updateSubscription) {
|
||||
updateSubscription.unsubscribe();
|
||||
}
|
||||
if (fetchSubscription) {
|
||||
fetchSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
await filterManager.removeAll();
|
||||
});
|
||||
|
||||
describe('observing', () => {
|
||||
test('should return observable', () => {
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
fetchSubscription = filterManager.getUpdates$().subscribe(() => {});
|
||||
expect(updateSubscription).toBeInstanceOf(Subscription);
|
||||
expect(fetchSubscription).toBeInstanceOf(Subscription);
|
||||
});
|
||||
|
||||
test('should observe global state', done => {
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(() => {
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(1);
|
||||
if (updateSubscription) {
|
||||
updateSubscription.unsubscribe();
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, true, true, 'age', 34);
|
||||
globalStateStub.filters.push(f1);
|
||||
});
|
||||
|
||||
test('should observe app state', done => {
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(() => {
|
||||
expect(filterManager.getAppFilters()).toHaveLength(1);
|
||||
if (updateSubscription) {
|
||||
updateSubscription.unsubscribe();
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
|
||||
appStateStub.filters.push(f1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get \\ set filters', () => {
|
||||
test('should be empty', () => {
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
expect(filterManager.getAppFilters()).toHaveLength(0);
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(0);
|
||||
expect(filterManager.getFilters()).toHaveLength(0);
|
||||
|
||||
const partitionedFiltres = filterManager.getPartitionedFilters();
|
||||
expect(partitionedFiltres.appFilters).toHaveLength(0);
|
||||
expect(partitionedFiltres.globalFilters).toHaveLength(0);
|
||||
expect(updateListener.called).toBeFalsy();
|
||||
});
|
||||
|
||||
test('app state should be set', async () => {
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
|
||||
await filterManager.setFilters([f1]);
|
||||
expect(filterManager.getAppFilters()).toHaveLength(1);
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(0);
|
||||
expect(filterManager.getFilters()).toHaveLength(1);
|
||||
|
||||
const partitionedFiltres = filterManager.getPartitionedFilters();
|
||||
expect(partitionedFiltres.appFilters).toHaveLength(1);
|
||||
expect(partitionedFiltres.globalFilters).toHaveLength(0);
|
||||
expect(updateListener.called).toBeTruthy();
|
||||
});
|
||||
|
||||
test('global state should be set', async () => {
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
await filterManager.setFilters([f1]);
|
||||
expect(filterManager.getAppFilters()).toHaveLength(0);
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(1);
|
||||
expect(filterManager.getFilters()).toHaveLength(1);
|
||||
|
||||
const partitionedFiltres = filterManager.getPartitionedFilters();
|
||||
expect(partitionedFiltres.appFilters).toHaveLength(0);
|
||||
expect(partitionedFiltres.globalFilters).toHaveLength(1);
|
||||
expect(updateListener.called).toBeTruthy();
|
||||
});
|
||||
|
||||
test('both states should be set', async () => {
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE');
|
||||
await filterManager.setFilters([f1, f2]);
|
||||
expect(filterManager.getAppFilters()).toHaveLength(1);
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(1);
|
||||
expect(filterManager.getFilters()).toHaveLength(2);
|
||||
|
||||
const partitionedFiltres = filterManager.getPartitionedFilters();
|
||||
expect(partitionedFiltres.appFilters).toHaveLength(1);
|
||||
expect(partitionedFiltres.globalFilters).toHaveLength(1);
|
||||
|
||||
// listener should be called just once
|
||||
expect(updateListener.called).toBeTruthy();
|
||||
expect(updateListener.callCount).toBe(1);
|
||||
});
|
||||
|
||||
test('set state should override previous state', async () => {
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE');
|
||||
|
||||
await filterManager.setFilters([f1]);
|
||||
await filterManager.setFilters([f2]);
|
||||
|
||||
expect(filterManager.getAppFilters()).toHaveLength(1);
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(0);
|
||||
expect(filterManager.getFilters()).toHaveLength(1);
|
||||
|
||||
const partitionedFiltres = filterManager.getPartitionedFilters();
|
||||
expect(partitionedFiltres.appFilters).toHaveLength(1);
|
||||
expect(partitionedFiltres.globalFilters).toHaveLength(0);
|
||||
|
||||
// listener should be called just once
|
||||
expect(updateListener.called).toBeTruthy();
|
||||
expect(updateListener.callCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add filters', async () => {
|
||||
test('app state should accept a single filter', async function() {
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
|
||||
await filterManager.addFilters(f1);
|
||||
expect(filterManager.getAppFilters()).toHaveLength(1);
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(0);
|
||||
expect(updateListener.callCount).toBe(1);
|
||||
expect(appStateStub.filters.length).toBe(1);
|
||||
});
|
||||
|
||||
test('app state should accept array', async () => {
|
||||
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
|
||||
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'female');
|
||||
await filterManager.addFilters([f1]);
|
||||
await filterManager.addFilters([f2]);
|
||||
expect(filterManager.getAppFilters()).toHaveLength(2);
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(0);
|
||||
expect(appStateStub.filters.length).toBe(2);
|
||||
});
|
||||
|
||||
test('global state should accept a single filer', async () => {
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
await filterManager.addFilters(f1);
|
||||
expect(filterManager.getAppFilters()).toHaveLength(0);
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(1);
|
||||
expect(updateListener.callCount).toBe(1);
|
||||
expect(globalStateStub.filters.length).toBe(1);
|
||||
});
|
||||
|
||||
test('global state should be accept array', async () => {
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
const f2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'gender', 'female');
|
||||
await filterManager.addFilters([f1, f2]);
|
||||
expect(filterManager.getAppFilters()).toHaveLength(0);
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(2);
|
||||
expect(globalStateStub.filters.length).toBe(2);
|
||||
});
|
||||
|
||||
test('add multiple filters at once', async () => {
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
const f2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'gender', 'female');
|
||||
await filterManager.addFilters([f1, f2]);
|
||||
expect(filterManager.getAppFilters()).toHaveLength(0);
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(2);
|
||||
expect(updateListener.callCount).toBe(1);
|
||||
});
|
||||
|
||||
test('add same filter to global and app', async () => {
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
|
||||
await filterManager.addFilters([f1, f2]);
|
||||
|
||||
// FILTER SHOULD BE ADDED ONLY ONCE, TO GLOBAL
|
||||
expect(filterManager.getAppFilters()).toHaveLength(0);
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(1);
|
||||
expect(updateListener.callCount).toBe(1);
|
||||
});
|
||||
|
||||
test('add same filter with different values to global and app', async () => {
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 38);
|
||||
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
|
||||
await filterManager.addFilters([f1, f2]);
|
||||
|
||||
// FILTER SHOULD BE ADDED TWICE
|
||||
expect(filterManager.getAppFilters()).toHaveLength(1);
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(1);
|
||||
expect(updateListener.callCount).toBe(1);
|
||||
});
|
||||
|
||||
test('add filter with no state, and force pin', async () => {
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 38);
|
||||
f1.$state = undefined;
|
||||
|
||||
await filterManager.addFilters([f1], true);
|
||||
|
||||
// FILTER SHOULD BE GLOBAL
|
||||
const f1Output = filterManager.getFilters()[0];
|
||||
expect(f1Output.$state).toBeDefined();
|
||||
if (f1Output.$state) {
|
||||
expect(f1Output.$state.store).toBe(FilterStateStore.GLOBAL_STATE);
|
||||
}
|
||||
});
|
||||
|
||||
test('add filter with no state, and dont force pin', async () => {
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 38);
|
||||
f1.$state = undefined;
|
||||
|
||||
await filterManager.addFilters([f1], false);
|
||||
|
||||
// FILTER SHOULD BE APP
|
||||
const f1Output = filterManager.getFilters()[0];
|
||||
expect(f1Output.$state).toBeDefined();
|
||||
if (f1Output.$state) {
|
||||
expect(f1Output.$state.store).toBe(FilterStateStore.APP_STATE);
|
||||
}
|
||||
});
|
||||
|
||||
test('should return app and global filters', async function() {
|
||||
const filters = getFiltersArray();
|
||||
await filterManager.addFilters(filters[0], false);
|
||||
await filterManager.addFilters(filters[1], true);
|
||||
|
||||
// global filters should be listed first
|
||||
let res = filterManager.getFilters();
|
||||
expect(res).toHaveLength(2);
|
||||
expect(res[0].$state && res[0].$state.store).toEqual(FilterStateStore.GLOBAL_STATE);
|
||||
expect(res[0].meta.disabled).toEqual(filters[1].meta.disabled);
|
||||
expect(res[0].query).toEqual(filters[1].query);
|
||||
|
||||
expect(res[1].$state && res[1].$state.store).toEqual(FilterStateStore.APP_STATE);
|
||||
expect(res[1].meta.disabled).toEqual(filters[0].meta.disabled);
|
||||
expect(res[1].query).toEqual(filters[0].query);
|
||||
|
||||
// should return updated version of filters
|
||||
await filterManager.addFilters(filters[2], false);
|
||||
|
||||
res = filterManager.getFilters();
|
||||
expect(res).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('should skip appStateStub filters that match globalStateStub filters', async function() {
|
||||
await filterManager.addFilters(readyFilters, true);
|
||||
const appFilter = _.cloneDeep(readyFilters[1]);
|
||||
await filterManager.addFilters(appFilter, false);
|
||||
|
||||
// global filters should be listed first
|
||||
const res = filterManager.getFilters();
|
||||
expect(res).toHaveLength(3);
|
||||
_.each(res, function(filter) {
|
||||
expect(filter.$state && filter.$state.store).toBe(FilterStateStore.GLOBAL_STATE);
|
||||
});
|
||||
});
|
||||
|
||||
test('should allow overwriting a positive filter by a negated one', async function() {
|
||||
// Add negate: false version of the filter
|
||||
const filter = _.cloneDeep(readyFilters[0]);
|
||||
filter.meta.negate = false;
|
||||
|
||||
await filterManager.addFilters(filter);
|
||||
expect(filterManager.getFilters()).toHaveLength(1);
|
||||
expect(filterManager.getFilters()[0]).toEqual(filter);
|
||||
|
||||
// Add negate: true version of the same filter
|
||||
const negatedFilter = _.cloneDeep(readyFilters[0]);
|
||||
negatedFilter.meta.negate = true;
|
||||
|
||||
await filterManager.addFilters(negatedFilter);
|
||||
// The negated filter should overwrite the positive one
|
||||
expect(globalStateStub.filters.length).toBe(1);
|
||||
expect(filterManager.getFilters()).toHaveLength(1);
|
||||
expect(filterManager.getFilters()[0]).toEqual(negatedFilter);
|
||||
});
|
||||
|
||||
test('should allow overwriting a negated filter by a positive one', async function() {
|
||||
// Add negate: true version of the same filter
|
||||
const negatedFilter = _.cloneDeep(readyFilters[0]);
|
||||
negatedFilter.meta.negate = true;
|
||||
|
||||
await filterManager.addFilters(negatedFilter);
|
||||
|
||||
// The negated filter should overwrite the positive one
|
||||
expect(globalStateStub.filters.length).toBe(1);
|
||||
expect(globalStateStub.filters[0]).toEqual(negatedFilter);
|
||||
|
||||
// Add negate: false version of the filter
|
||||
const filter = _.cloneDeep(readyFilters[0]);
|
||||
filter.meta.negate = false;
|
||||
|
||||
await filterManager.addFilters(filter);
|
||||
expect(globalStateStub.filters.length).toBe(1);
|
||||
expect(globalStateStub.filters[0]).toEqual(filter);
|
||||
});
|
||||
|
||||
test('should fire the update and fetch events', async function() {
|
||||
const updateStub = sinon.stub();
|
||||
const fetchStub = sinon.stub();
|
||||
|
||||
filterManager.getUpdates$().subscribe({
|
||||
next: updateStub,
|
||||
});
|
||||
|
||||
filterManager.getFetches$().subscribe({
|
||||
next: fetchStub,
|
||||
});
|
||||
|
||||
await filterManager.addFilters(readyFilters);
|
||||
|
||||
// updates should trigger state saves
|
||||
expect(appStateStub.save.callCount).toBe(1);
|
||||
expect(globalStateStub.save.callCount).toBe(1);
|
||||
|
||||
// this time, events should be emitted
|
||||
expect(fetchStub.called);
|
||||
expect(updateStub.called);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter reconciliation', function() {
|
||||
test('should de-dupe appStateStub filters being added', async function() {
|
||||
const newFilter = _.cloneDeep(readyFilters[1]);
|
||||
await filterManager.addFilters(readyFilters, false);
|
||||
expect(appStateStub.filters.length).toBe(3);
|
||||
|
||||
await filterManager.addFilters(newFilter, false);
|
||||
expect(appStateStub.filters.length).toBe(3);
|
||||
});
|
||||
|
||||
test('should de-dupe globalStateStub filters being added', async function() {
|
||||
const newFilter = _.cloneDeep(readyFilters[1]);
|
||||
await filterManager.addFilters(readyFilters, true);
|
||||
expect(globalStateStub.filters.length).toBe(3);
|
||||
|
||||
await filterManager.addFilters(newFilter, true);
|
||||
expect(globalStateStub.filters.length).toBe(3);
|
||||
});
|
||||
|
||||
test('should mutate global filters on appStateStub filter changes', async function() {
|
||||
const idx = 1;
|
||||
await filterManager.addFilters(readyFilters, true);
|
||||
|
||||
const appFilter = _.cloneDeep(readyFilters[idx]);
|
||||
appFilter.meta.negate = true;
|
||||
appFilter.$state = {
|
||||
store: FilterStateStore.APP_STATE,
|
||||
};
|
||||
await filterManager.addFilters(appFilter);
|
||||
const res = filterManager.getFilters();
|
||||
expect(res).toHaveLength(3);
|
||||
_.each(res, function(filter, i) {
|
||||
expect(filter.$state && filter.$state.store).toBe('globalState');
|
||||
// make sure global filter actually mutated
|
||||
expect(filter.meta.negate).toBe(i === idx);
|
||||
});
|
||||
});
|
||||
|
||||
test('should merge conflicting appStateStub filters', async function() {
|
||||
await filterManager.addFilters(readyFilters, true);
|
||||
const appFilter = _.cloneDeep(readyFilters[1]);
|
||||
appFilter.meta.negate = true;
|
||||
appFilter.$state = {
|
||||
store: FilterStateStore.APP_STATE,
|
||||
};
|
||||
await filterManager.addFilters(appFilter, false);
|
||||
|
||||
// global filters should be listed first
|
||||
const res = filterManager.getFilters();
|
||||
expect(res).toHaveLength(3);
|
||||
expect(
|
||||
res.filter(function(filter) {
|
||||
return filter.$state && filter.$state.store === FilterStateStore.GLOBAL_STATE;
|
||||
}).length
|
||||
).toBe(3);
|
||||
});
|
||||
|
||||
test('should enable disabled filters - global state', async function() {
|
||||
// test adding to globalStateStub
|
||||
const disabledFilters = _.map(readyFilters, function(filter) {
|
||||
const f = _.cloneDeep(filter);
|
||||
f.meta.disabled = true;
|
||||
return f;
|
||||
});
|
||||
await filterManager.addFilters(disabledFilters, true);
|
||||
await filterManager.addFilters(readyFilters, true);
|
||||
|
||||
const res = filterManager.getFilters();
|
||||
expect(res).toHaveLength(3);
|
||||
expect(
|
||||
res.filter(function(filter) {
|
||||
return filter.meta.disabled === false;
|
||||
}).length
|
||||
).toBe(3);
|
||||
});
|
||||
|
||||
test('should enable disabled filters - app state', async function() {
|
||||
// test adding to appStateStub
|
||||
const disabledFilters = _.map(readyFilters, function(filter) {
|
||||
const f = _.cloneDeep(filter);
|
||||
f.meta.disabled = true;
|
||||
return f;
|
||||
});
|
||||
await filterManager.addFilters(disabledFilters, true);
|
||||
await filterManager.addFilters(readyFilters, false);
|
||||
|
||||
const res = filterManager.getFilters();
|
||||
expect(res).toHaveLength(3);
|
||||
expect(
|
||||
res.filter(function(filter) {
|
||||
return filter.meta.disabled === false;
|
||||
}).length
|
||||
).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove filters', async () => {
|
||||
test('remove on empty should do nothing and not fire events', async () => {
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
await filterManager.removeAll();
|
||||
expect(updateListener.called).toBeFalsy();
|
||||
expect(filterManager.getFilters()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('remove on full should clean and fire events', async () => {
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE');
|
||||
await filterManager.setFilters([f1, f2]);
|
||||
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
await filterManager.removeAll();
|
||||
expect(updateListener.called).toBeTruthy();
|
||||
expect(filterManager.getFilters()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('remove non existing filter should do nothing and not fire events', async () => {
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE');
|
||||
const f3 = getFilter(FilterStateStore.APP_STATE, false, false, 'country', 'US');
|
||||
await filterManager.setFilters([f1, f2]);
|
||||
expect(filterManager.getFilters()).toHaveLength(2);
|
||||
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
await filterManager.removeFilter(f3);
|
||||
expect(updateListener.called).toBeFalsy();
|
||||
expect(filterManager.getFilters()).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('remove existing filter should remove and fire events', async () => {
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
const f2 = getFilter(FilterStateStore.APP_STATE, false, false, 'gender', 'FEMALE');
|
||||
const f3 = getFilter(FilterStateStore.APP_STATE, false, false, 'country', 'US');
|
||||
await filterManager.setFilters([f1, f2, f3]);
|
||||
expect(filterManager.getFilters()).toHaveLength(3);
|
||||
|
||||
updateSubscription = filterManager.getUpdates$().subscribe(updateListener);
|
||||
await filterManager.removeFilter(f3);
|
||||
expect(updateListener.called).toBeTruthy();
|
||||
expect(filterManager.getFilters()).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('should remove the filter from appStateStub', async function() {
|
||||
await filterManager.addFilters(readyFilters, false);
|
||||
expect(appStateStub.filters).toHaveLength(3);
|
||||
filterManager.removeFilter(readyFilters[0]);
|
||||
expect(appStateStub.filters).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('should remove the filter from globalStateStub', async function() {
|
||||
await filterManager.addFilters(readyFilters, true);
|
||||
expect(globalStateStub.filters).toHaveLength(3);
|
||||
filterManager.removeFilter(readyFilters[0]);
|
||||
expect(globalStateStub.filters).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('should fire the update and fetch events', async function() {
|
||||
const updateStub = sinon.stub();
|
||||
const fetchStub = sinon.stub();
|
||||
|
||||
await filterManager.addFilters(readyFilters, false);
|
||||
|
||||
filterManager.getUpdates$().subscribe({
|
||||
next: updateStub,
|
||||
});
|
||||
|
||||
filterManager.getFetches$().subscribe({
|
||||
next: fetchStub,
|
||||
});
|
||||
|
||||
filterManager.removeFilter(readyFilters[0]);
|
||||
|
||||
// this time, events should be emitted
|
||||
expect(fetchStub.called);
|
||||
expect(updateStub.called);
|
||||
});
|
||||
|
||||
test('should remove matching filters', async function() {
|
||||
await filterManager.addFilters([readyFilters[0], readyFilters[1]], true);
|
||||
await filterManager.addFilters([readyFilters[2]], false);
|
||||
|
||||
filterManager.removeFilter(readyFilters[0]);
|
||||
|
||||
expect(globalStateStub.filters).toHaveLength(1);
|
||||
expect(appStateStub.filters).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should remove matching filters by comparison', async function() {
|
||||
await filterManager.addFilters([readyFilters[0], readyFilters[1]], true);
|
||||
await filterManager.addFilters([readyFilters[2]], false);
|
||||
|
||||
filterManager.removeFilter(_.cloneDeep(readyFilters[0]));
|
||||
|
||||
expect(globalStateStub.filters).toHaveLength(1);
|
||||
expect(appStateStub.filters).toHaveLength(1);
|
||||
|
||||
filterManager.removeFilter(_.cloneDeep(readyFilters[2]));
|
||||
expect(globalStateStub.filters).toHaveLength(1);
|
||||
expect(appStateStub.filters).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should do nothing with a non-matching filter', async function() {
|
||||
await filterManager.addFilters([readyFilters[0], readyFilters[1]], true);
|
||||
await filterManager.addFilters([readyFilters[2]], false);
|
||||
|
||||
const missedFilter = _.cloneDeep(readyFilters[0]);
|
||||
missedFilter.meta.negate = !readyFilters[0].meta.negate;
|
||||
|
||||
filterManager.removeFilter(missedFilter);
|
||||
expect(globalStateStub.filters).toHaveLength(2);
|
||||
expect(appStateStub.filters).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should remove all the filters from both states', async function() {
|
||||
await filterManager.addFilters([readyFilters[0], readyFilters[1]], true);
|
||||
await filterManager.addFilters([readyFilters[2]], false);
|
||||
expect(globalStateStub.filters).toHaveLength(2);
|
||||
expect(appStateStub.filters).toHaveLength(1);
|
||||
|
||||
await filterManager.removeAll();
|
||||
expect(globalStateStub.filters).toHaveLength(0);
|
||||
expect(appStateStub.filters).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invert', () => {
|
||||
test('invert to disabled', async () => {
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
filterManager.invertFilter(f1);
|
||||
expect(f1.meta.negate).toBe(true);
|
||||
filterManager.invertFilter(f1);
|
||||
expect(f1.meta.negate).toBe(false);
|
||||
});
|
||||
|
||||
test('should fire the update and fetch events', function() {
|
||||
const updateStub = sinon.stub();
|
||||
const fetchStub = sinon.stub();
|
||||
|
||||
filterManager.addFilters(readyFilters);
|
||||
filterManager.getUpdates$().subscribe({
|
||||
next: updateStub,
|
||||
});
|
||||
|
||||
filterManager.getFetches$().subscribe({
|
||||
next: fetchStub,
|
||||
});
|
||||
|
||||
filterManager.invertFilter(readyFilters[1]);
|
||||
expect(fetchStub.called);
|
||||
expect(updateStub.called);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addFiltersAndChangeTimeFilter', () => {
|
||||
test('should just add filters if there is no time filter in array', async () => {
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
await filterManager.addFiltersAndChangeTimeFilter([f1]);
|
||||
expect(filterManager.getFilters()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* 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, Subscription } from 'rxjs';
|
||||
|
||||
import { npSetup } from 'ui/new_platform';
|
||||
|
||||
// @ts-ignore
|
||||
import { compareFilters } from './lib/compare_filters';
|
||||
// @ts-ignore
|
||||
import { mapAndFlattenFilters } from './lib/map_and_flatten_filters';
|
||||
// @ts-ignore
|
||||
import { uniqFilters } from './lib/uniq_filters';
|
||||
// @ts-ignore
|
||||
import { extractTimeFilter } from './lib/extract_time_filter';
|
||||
// @ts-ignore
|
||||
import { changeTimeFilter } from './lib/change_time_filter';
|
||||
|
||||
import { PartitionedFilters } from './partitioned_filters';
|
||||
|
||||
import { IndexPatterns } from '../../index_patterns';
|
||||
|
||||
export class FilterManager {
|
||||
private indexPatterns: IndexPatterns;
|
||||
private filters: Filter[] = [];
|
||||
private updated$: Subject<any> = new Subject();
|
||||
private fetch$: Subject<any> = new Subject();
|
||||
private updateSubscription$: Subscription | undefined;
|
||||
|
||||
constructor(indexPatterns: IndexPatterns) {
|
||||
this.indexPatterns = indexPatterns;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.updateSubscription$) {
|
||||
this.updateSubscription$.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
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 uniqFilters(appFilters.reverse().concat(globalFilters.reverse())).reverse();
|
||||
}
|
||||
|
||||
private filtersUpdated(newFilters: Filter[]): boolean {
|
||||
return !_.isEqual(this.filters, newFilters);
|
||||
}
|
||||
|
||||
private static partitionFilters(filters: Filter[]): PartitionedFilters {
|
||||
const [globalFilters, appFilters] = _.partition(filters, isFilterPinned);
|
||||
return {
|
||||
globalFilters,
|
||||
appFilters,
|
||||
};
|
||||
}
|
||||
|
||||
private handleStateUpdate(newFilters: Filter[]) {
|
||||
// This is where the angular update magic \ syncing diget happens
|
||||
const filtersUpdated = this.filtersUpdated(newFilters);
|
||||
|
||||
// global filters should always be first
|
||||
newFilters.sort(
|
||||
(a: Filter, b: Filter): number => {
|
||||
if (a.$state && a.$state.store === FilterStateStore.GLOBAL_STATE) {
|
||||
return -1;
|
||||
} else if (b.$state && b.$state.store === FilterStateStore.GLOBAL_STATE) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.filters = newFilters;
|
||||
if (filtersUpdated) {
|
||||
this.updated$.next();
|
||||
// Fired together with updated$, because historically (~4 years ago) there was a fetch optimization, that didn't call fetch for very specific cases.
|
||||
// This optimization seems irrelevant at the moment, but I didn't want to change the logic of all consumers.
|
||||
this.fetch$.next();
|
||||
}
|
||||
}
|
||||
|
||||
/* Getters */
|
||||
|
||||
public getFilters() {
|
||||
return 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.filters);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
const { uiSettings } = npSetup.core;
|
||||
if (pinFilterStatus === undefined) {
|
||||
pinFilterStatus = uiSettings.get('filters:pinnedByDefault');
|
||||
}
|
||||
|
||||
// set the store of all filters
|
||||
// TODO: is this necessary?
|
||||
const store = pinFilterStatus ? FilterStateStore.GLOBAL_STATE : FilterStateStore.APP_STATE;
|
||||
FilterManager.setFiltersStore(filters, store);
|
||||
|
||||
const mappedFilters = await mapAndFlattenFilters(this.indexPatterns, filters);
|
||||
const newPartitionedFilters = FilterManager.partitionFilters(mappedFilters);
|
||||
const partitionFilters = this.getPartitionedFilters();
|
||||
partitionFilters.appFilters.push(...newPartitionedFilters.appFilters);
|
||||
partitionFilters.globalFilters.push(...newPartitionedFilters.globalFilters);
|
||||
|
||||
const newFilters = this.mergeIncomingFilters(partitionFilters);
|
||||
this.handleStateUpdate(newFilters);
|
||||
}
|
||||
|
||||
public async setFilters(newFilters: Filter[]) {
|
||||
const mappedFilters = await mapAndFlattenFilters(this.indexPatterns, newFilters);
|
||||
this.handleStateUpdate(mappedFilters);
|
||||
}
|
||||
|
||||
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 invertFilter(filter: Filter) {
|
||||
filter.meta.negate = !filter.meta.negate;
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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 sinon from 'sinon';
|
||||
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
|
||||
import { Subscription } from 'rxjs';
|
||||
import { FilterStateManager } from './filter_state_manager';
|
||||
|
||||
import { StubState } from './test_helpers/stub_state';
|
||||
import { getFilter } from './test_helpers/get_stub_filter';
|
||||
import { FilterManager } from './filter_manager';
|
||||
import { StubIndexPatterns } from './test_helpers/stub_index_pattern';
|
||||
|
||||
jest.mock('ui/new_platform', () => ({
|
||||
npStart: {
|
||||
core: {
|
||||
chrome: {
|
||||
recentlyAccessed: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
npSetup: {
|
||||
core: {
|
||||
uiSettings: {
|
||||
get: () => true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('filter_state_manager', () => {
|
||||
let appStateStub: StubState;
|
||||
let globalStateStub: StubState;
|
||||
|
||||
let subscription: Subscription | undefined;
|
||||
let filterManager: FilterManager;
|
||||
|
||||
beforeEach(() => {
|
||||
appStateStub = new StubState();
|
||||
globalStateStub = new StubState();
|
||||
const indexPatterns = new StubIndexPatterns();
|
||||
filterManager = new FilterManager(indexPatterns);
|
||||
|
||||
// FilterStateManager is tested indirectly.
|
||||
// Therefore, we don't need it's instance.
|
||||
new FilterStateManager(
|
||||
globalStateStub,
|
||||
() => {
|
||||
return appStateStub;
|
||||
},
|
||||
filterManager
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (subscription) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
test('should update filter manager global filters', done => {
|
||||
const f1 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
globalStateStub.filters.push(f1);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(filterManager.getGlobalFilters()).toHaveLength(1);
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
test('should update filter manager app filters', done => {
|
||||
expect(filterManager.getAppFilters()).toHaveLength(0);
|
||||
|
||||
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
|
||||
appStateStub.filters.push(f1);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(filterManager.getAppFilters()).toHaveLength(1);
|
||||
done();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
test('should update URL when filter manager filters are set', async () => {
|
||||
appStateStub.save = sinon.stub();
|
||||
globalStateStub.save = sinon.stub();
|
||||
|
||||
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
|
||||
const f2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
|
||||
await filterManager.setFilters([f1, f2]);
|
||||
|
||||
sinon.assert.calledOnce(appStateStub.save);
|
||||
sinon.assert.calledOnce(globalStateStub.save);
|
||||
});
|
||||
|
||||
test('should update URL when filter manager filters are added', async () => {
|
||||
appStateStub.save = sinon.stub();
|
||||
globalStateStub.save = sinon.stub();
|
||||
|
||||
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
|
||||
const f2 = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
|
||||
|
||||
await filterManager.addFilters([f1, f2]);
|
||||
|
||||
sinon.assert.calledOnce(appStateStub.save);
|
||||
sinon.assert.calledOnce(globalStateStub.save);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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, FilterStateStore } from '@kbn/es-query';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { State } from 'ui/state_management/state';
|
||||
import { FilterManager } from './filter_manager';
|
||||
|
||||
/**
|
||||
* FilterStateManager is responsible for watching for filter changes
|
||||
* and synching with FilterManager, as well as syncing FilterManager changes
|
||||
* back to the URL.
|
||||
**/
|
||||
export class FilterStateManager {
|
||||
filterManager: FilterManager;
|
||||
globalState: State;
|
||||
getAppState: () => State;
|
||||
prevGlobalFilters: Filter[] | undefined;
|
||||
prevAppFilters: Filter[] | undefined;
|
||||
interval: NodeJS.Timeout | undefined;
|
||||
|
||||
constructor(globalState: State, getAppState: () => State, filterManager: FilterManager) {
|
||||
this.getAppState = getAppState;
|
||||
this.globalState = globalState;
|
||||
this.filterManager = filterManager;
|
||||
|
||||
this.watchFilterState();
|
||||
|
||||
this.filterManager.getUpdates$().subscribe(() => {
|
||||
this.updateAppState();
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
private watchFilterState() {
|
||||
// This is a temporary solution to remove rootscope.
|
||||
// Moving forward, state should provide observable subscriptions.
|
||||
this.interval = setInterval(() => {
|
||||
const appState = this.getAppState();
|
||||
const stateUndefined = !appState || !this.globalState;
|
||||
if (stateUndefined) return;
|
||||
|
||||
const globalFilters = this.globalState.filters || [];
|
||||
const appFilters = appState.filters || [];
|
||||
|
||||
const globalFilterChanged = !(
|
||||
this.prevGlobalFilters && _.isEqual(this.prevGlobalFilters, globalFilters)
|
||||
);
|
||||
const appFilterChanged = !(this.prevAppFilters && _.isEqual(this.prevAppFilters, appFilters));
|
||||
const filterStateChanged = globalFilterChanged || appFilterChanged;
|
||||
|
||||
if (!filterStateChanged) return;
|
||||
|
||||
const newGlobalFilters = _.cloneDeep(globalFilters);
|
||||
const newAppFilters = _.cloneDeep(appFilters);
|
||||
FilterManager.setFiltersStore(newAppFilters, FilterStateStore.APP_STATE);
|
||||
FilterManager.setFiltersStore(newGlobalFilters, FilterStateStore.GLOBAL_STATE);
|
||||
|
||||
this.filterManager.setFilters(newGlobalFilters.concat(newAppFilters));
|
||||
|
||||
// store new filter changes
|
||||
this.prevGlobalFilters = newGlobalFilters;
|
||||
this.prevAppFilters = newAppFilters;
|
||||
}, 10);
|
||||
}
|
||||
|
||||
private saveState() {
|
||||
const appState = this.getAppState();
|
||||
if (appState) appState.save();
|
||||
this.globalState.save();
|
||||
}
|
||||
|
||||
private updateAppState() {
|
||||
// Update Angular state before saving State objects (which save it to URL)
|
||||
const partitionedFilters = this.filterManager.getPartitionedFilters();
|
||||
const appState = this.getAppState();
|
||||
appState.filters = partitionedFilters.appFilters;
|
||||
this.globalState.filters = partitionedFilters.globalFilters;
|
||||
this.saveState();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { FilterManager } from './filter_manager';
|
||||
export { FilterStateManager } from './filter_state_manager';
|
||||
|
||||
// @ts-ignore
|
||||
export { uniqFilters } from './lib/uniq_filters';
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { chromeServiceMock } from '../../../../../../core/public/mocks';
|
||||
import { chromeServiceMock } from '../../../../../../../../core/public/mocks';
|
||||
|
||||
jest.doMock('ui/new_platform', () => ({
|
||||
npStart: {
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { SavedObjectNotFound } from '../../errors';
|
||||
import { SavedObjectNotFound } from 'ui/errors';
|
||||
|
||||
function getParams(filter, indexPattern) {
|
||||
const type = 'geo_bounding_box';
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { SavedObjectNotFound } from '../../errors';
|
||||
import { SavedObjectNotFound } from 'ui/errors';
|
||||
|
||||
function getParams(filter, indexPattern) {
|
||||
const type = 'geo_polygon';
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { SavedObjectNotFound } from '../../errors';
|
||||
import { SavedObjectNotFound } from 'ui/errors';
|
||||
|
||||
function isScriptedPhrase(filter) {
|
||||
const value = _.get(filter, ['script', 'script', 'params', 'value']);
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { has, get } from 'lodash';
|
||||
import { SavedObjectNotFound } from '../../errors';
|
||||
import { SavedObjectNotFound } from 'ui/errors';
|
||||
|
||||
|
||||
function isScriptedRange(filter) {
|
||||
|
@ -43,7 +43,7 @@ function getParams(filter, indexPattern) {
|
|||
// external factors e.g. a reindex. We only need the index in order to grab the field formatter, so we fallback
|
||||
// on displaying the raw value if the index is invalid.
|
||||
let value = `${left} to ${right}`;
|
||||
if (indexPattern) {
|
||||
if (indexPattern && indexPattern.fields.byName[key]) {
|
||||
const convert = indexPattern.fields.byName[key].format.getConverterFor('text');
|
||||
value = `${convert(left)} to ${convert(right)}`;
|
||||
}
|
|
@ -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 { Filter } from '@kbn/es-query';
|
||||
|
||||
export interface PartitionedFilters {
|
||||
globalFilters: Filter[];
|
||||
appFilters: Filter[];
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 } from '@kbn/es-query';
|
||||
|
||||
export function getFiltersArray(): Filter[] {
|
||||
return [
|
||||
{
|
||||
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
|
||||
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
|
||||
},
|
||||
{
|
||||
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
|
||||
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
|
||||
},
|
||||
{
|
||||
query: { match: { _type: { query: 'nginx', type: 'phrase' } } },
|
||||
meta: { index: 'logstash-*', negate: false, disabled: false, alias: null },
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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 { Filter, FilterStateStore } from '@kbn/es-query';
|
||||
|
||||
export function getFilter(
|
||||
store: FilterStateStore,
|
||||
disabled: boolean,
|
||||
negated: boolean,
|
||||
queryKey: string,
|
||||
queryValue: any
|
||||
): Filter {
|
||||
return {
|
||||
$state: {
|
||||
store,
|
||||
},
|
||||
meta: {
|
||||
index: 'logstash-*',
|
||||
disabled,
|
||||
negate: negated,
|
||||
alias: null,
|
||||
},
|
||||
query: {
|
||||
match: {
|
||||
[queryKey]: queryValue,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 class StubIndexPatterns {
|
||||
async get(index: any) {
|
||||
return {
|
||||
fields: {
|
||||
byName: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 sinon from 'sinon';
|
||||
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { State } from 'ui/state_management/state';
|
||||
|
||||
export class StubState implements State {
|
||||
filters: Filter[];
|
||||
save: sinon.SinonSpy<any[], any>;
|
||||
|
||||
constructor() {
|
||||
this.save = sinon.stub();
|
||||
this.filters = [];
|
||||
}
|
||||
|
||||
getQueryParamName() {
|
||||
return '_a';
|
||||
}
|
||||
|
||||
translateHashToRison(stateHashOrRison: string | string[]): string | string[] {
|
||||
return '';
|
||||
}
|
||||
}
|
|
@ -20,14 +20,23 @@
|
|||
import { once } from 'lodash';
|
||||
import { FilterBar, setupDirective as setupFilterBarDirective } from './filter_bar';
|
||||
import { ApplyFiltersPopover, setupDirective as setupApplyFiltersDirective } from './apply_filters';
|
||||
|
||||
import { IndexPatterns } from '../index_patterns';
|
||||
import { FilterManager } from './filter_manager';
|
||||
/**
|
||||
* FilterSearch Service
|
||||
* @internal
|
||||
*/
|
||||
|
||||
export interface FilterServiceDependencies {
|
||||
indexPatterns: IndexPatterns;
|
||||
}
|
||||
|
||||
export class FilterService {
|
||||
public setup() {
|
||||
public setup({ indexPatterns }: FilterServiceDependencies) {
|
||||
const filterManager = new FilterManager(indexPatterns);
|
||||
|
||||
return {
|
||||
filterManager,
|
||||
ui: {
|
||||
ApplyFiltersPopover,
|
||||
FilterBar,
|
||||
|
|
|
@ -49,6 +49,7 @@ export class DataPlugin {
|
|||
// TODO: this is imported here to avoid circular imports.
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { getInterpreter } = require('plugins/interpreter/interpreter');
|
||||
const indexPatternsService = this.indexPatterns.setup();
|
||||
return {
|
||||
expressions: this.expressions.setup({
|
||||
interpreter: {
|
||||
|
@ -56,8 +57,10 @@ export class DataPlugin {
|
|||
renderersRegistry,
|
||||
},
|
||||
}),
|
||||
indexPatterns: this.indexPatterns.setup(),
|
||||
filter: this.filter.setup(),
|
||||
indexPatterns: indexPatternsService,
|
||||
filter: this.filter.setup({
|
||||
indexPatterns: indexPatternsService.indexPatterns,
|
||||
}),
|
||||
search: this.search.setup(),
|
||||
query: this.query.setup(),
|
||||
};
|
||||
|
@ -87,6 +90,7 @@ export { ExpressionRenderer, ExpressionRendererProps, ExpressionRunner } from '.
|
|||
/** @public types */
|
||||
export { IndexPattern, StaticIndexPattern, StaticIndexPatternField, Field } from './index_patterns';
|
||||
export { Query } from './query';
|
||||
export { FilterManager, FilterStateManager, uniqFilters } from './filter/filter_manager';
|
||||
|
||||
/** @public static code */
|
||||
export { dateHistogramInterval } from '../common/date_histogram_interval';
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
export {
|
||||
IndexPatternsService,
|
||||
IndexPatterns,
|
||||
// types
|
||||
IndexPatternsSetup,
|
||||
IndexPattern,
|
||||
|
|
|
@ -75,7 +75,7 @@ const ui = {
|
|||
IndexPatternSelect,
|
||||
};
|
||||
|
||||
export { validateIndexPattern, constants, fixtures, ui };
|
||||
export { validateIndexPattern, constants, fixtures, ui, IndexPatterns };
|
||||
|
||||
/** @public */
|
||||
export type IndexPatternsSetup = ReturnType<IndexPatternsService['setup']>;
|
||||
|
|
|
@ -25,6 +25,7 @@ import { image } from './image';
|
|||
import { nullType } from './null';
|
||||
import { number } from './number';
|
||||
import { pointseries } from './pointseries';
|
||||
import { range } from './range';
|
||||
import { render } from './render';
|
||||
import { shape } from './shape';
|
||||
import { string } from './string';
|
||||
|
@ -41,6 +42,7 @@ export const typeSpecs = [
|
|||
number,
|
||||
nullType,
|
||||
pointseries,
|
||||
range,
|
||||
render,
|
||||
shape,
|
||||
string,
|
||||
|
@ -59,3 +61,4 @@ export * from './kibana_datatable';
|
|||
export * from './pointseries';
|
||||
export * from './render';
|
||||
export * from './style';
|
||||
export * from './range';
|
||||
|
|
51
src/legacy/core_plugins/interpreter/common/types/range.ts
Normal file
51
src/legacy/core_plugins/interpreter/common/types/range.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { ExpressionType, Render } from '../../types';
|
||||
|
||||
const name = 'range';
|
||||
|
||||
export interface Range {
|
||||
type: typeof name;
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
export const range = (): ExpressionType<typeof name, Range> => ({
|
||||
name,
|
||||
from: {
|
||||
null: (): Range => {
|
||||
return {
|
||||
type: 'range',
|
||||
from: 0,
|
||||
to: 0,
|
||||
};
|
||||
},
|
||||
},
|
||||
to: {
|
||||
render: (value: Range): Render<{ text: string }> => {
|
||||
const text = `from ${value.from} to ${value.to}`;
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'text',
|
||||
value: { text },
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
|
@ -22,9 +22,10 @@ import { esaggs } from './esaggs';
|
|||
import { font } from './font';
|
||||
import { kibana } from './kibana';
|
||||
import { kibanaContext } from './kibana_context';
|
||||
import { range } from './range';
|
||||
import { visualization } from './visualization';
|
||||
import { visDimension } from './vis_dimension';
|
||||
|
||||
export const functions = [
|
||||
clog, esaggs, font, kibana, kibanaContext, visualization, visDimension,
|
||||
clog, esaggs, font, kibana, kibanaContext, range, visualization, visDimension,
|
||||
];
|
||||
|
|
|
@ -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 { i18n } from '@kbn/i18n';
|
||||
import { KibanaDatatable, Range } from '../../common/types';
|
||||
import { ExpressionFunction } from '../../types';
|
||||
|
||||
const name = 'range';
|
||||
|
||||
type Context = KibanaDatatable | null;
|
||||
|
||||
interface Arguments {
|
||||
from: number;
|
||||
to: number;
|
||||
}
|
||||
|
||||
type Return = Range; // imported from type
|
||||
|
||||
export const range = (): ExpressionFunction<typeof name, Context, Arguments, Return> => ({
|
||||
name,
|
||||
help: i18n.translate('interpreter.function.range.help', {
|
||||
defaultMessage: 'Generates range object',
|
||||
}),
|
||||
type: 'range',
|
||||
args: {
|
||||
from: {
|
||||
types: ['number'],
|
||||
help: i18n.translate('interpreter.function.range.from.help', {
|
||||
defaultMessage: 'Start of range',
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
to: {
|
||||
types: ['number'],
|
||||
help: i18n.translate('interpreter.function.range.to.help', {
|
||||
defaultMessage: 'End of range',
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
fn: (context, args) => {
|
||||
return {
|
||||
type: 'range',
|
||||
from: args.from,
|
||||
to: args.to,
|
||||
};
|
||||
},
|
||||
});
|
|
@ -27,6 +27,7 @@ import './app';
|
|||
import contextAppRouteTemplate from './index.html';
|
||||
import { getRootBreadcrumbs } from '../discover/breadcrumbs';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
import { subscribeWithScope } from 'ui/utils/subscribe_with_scope';
|
||||
|
||||
uiRoutes
|
||||
.when('/context/:indexPatternId/:type/:id*', {
|
||||
|
@ -77,7 +78,7 @@ function ContextAppRouteController(
|
|||
'contextAppRoute.state.successorCount',
|
||||
], () => this.state.save(true));
|
||||
|
||||
const updateSubsciption = queryFilter.getUpdates$().subscribe({
|
||||
const updateSubsciption = subscribeWithScope($scope, queryFilter.getUpdates$(), {
|
||||
next: () => {
|
||||
this.filters = _.cloneDeep(queryFilter.getFilters());
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import expect from '@kbn/expect';
|
|||
import ngMock from 'ng_mock';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter';
|
||||
|
||||
import { createStateStub } from './_utils';
|
||||
import { QueryParameterActionsProvider } from '../actions';
|
||||
|
@ -35,8 +35,8 @@ describe('context app', function () {
|
|||
let addFilter;
|
||||
|
||||
beforeEach(ngMock.inject(function createPrivateStubs(Private) {
|
||||
filterManagerStub = createFilterManagerStub();
|
||||
Private.stub(FilterManagerProvider, filterManagerStub);
|
||||
filterManagerStub = createQueryFilterStub();
|
||||
Private.stub(FilterBarQueryFilterProvider, filterManagerStub);
|
||||
|
||||
addFilter = Private(QueryParameterActionsProvider).addFilter;
|
||||
}));
|
||||
|
@ -46,11 +46,13 @@ describe('context app', function () {
|
|||
|
||||
addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION');
|
||||
|
||||
const filterManagerAddStub = filterManagerStub.add;
|
||||
const filterManagerAddStub = filterManagerStub.addFilters;
|
||||
//get the generated filter
|
||||
const generatedFilter = filterManagerAddStub.firstCall.args[0][0];
|
||||
const queryKeys = Object.keys(generatedFilter.query.match);
|
||||
expect(filterManagerAddStub.calledOnce).to.be(true);
|
||||
expect(filterManagerAddStub.firstCall.args[0]).to.eql('FIELD_NAME');
|
||||
expect(filterManagerAddStub.firstCall.args[1]).to.eql('FIELD_VALUE');
|
||||
expect(filterManagerAddStub.firstCall.args[2]).to.eql('FILTER_OPERATION');
|
||||
expect(queryKeys[0]).to.eql('FIELD_NAME');
|
||||
expect(generatedFilter.query.match[queryKeys[0]].query).to.eql('FIELD_VALUE');
|
||||
});
|
||||
|
||||
it('should pass the index pattern id to the filterManager', function () {
|
||||
|
@ -58,15 +60,18 @@ describe('context app', function () {
|
|||
|
||||
addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION');
|
||||
|
||||
const filterManagerAddStub = filterManagerStub.add;
|
||||
const filterManagerAddStub = filterManagerStub.addFilters;
|
||||
const generatedFilter = filterManagerAddStub.firstCall.args[0][0];
|
||||
expect(filterManagerAddStub.calledOnce).to.be(true);
|
||||
expect(filterManagerAddStub.firstCall.args[3]).to.eql('INDEX_PATTERN_ID');
|
||||
expect(generatedFilter.meta.index).to.eql('INDEX_PATTERN_ID');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createFilterManagerStub() {
|
||||
function createQueryFilterStub() {
|
||||
return {
|
||||
add: sinon.stub(),
|
||||
addFilters: sinon.stub(),
|
||||
invertFilter: sinon.stub(),
|
||||
getAppFilters: sinon.stub(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
import expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
|
||||
import { createStateStub } from './_utils';
|
||||
import { QueryParameterActionsProvider } from '../actions';
|
||||
|
||||
|
@ -33,8 +31,6 @@ describe('context app', function () {
|
|||
let increasePredecessorCount;
|
||||
|
||||
beforeEach(ngMock.inject(function createPrivateStubs(Private) {
|
||||
Private.stub(FilterManagerProvider, {});
|
||||
|
||||
increasePredecessorCount = Private(QueryParameterActionsProvider).increasePredecessorCount;
|
||||
}));
|
||||
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
import expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
|
||||
import { createStateStub } from './_utils';
|
||||
import { QueryParameterActionsProvider } from '../actions';
|
||||
|
||||
|
@ -33,8 +31,6 @@ describe('context app', function () {
|
|||
let increaseSuccessorCount;
|
||||
|
||||
beforeEach(ngMock.inject(function createPrivateStubs(Private) {
|
||||
Private.stub(FilterManagerProvider, {});
|
||||
|
||||
increaseSuccessorCount = Private(QueryParameterActionsProvider).increaseSuccessorCount;
|
||||
}));
|
||||
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
import expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
|
||||
import { createStateStub } from './_utils';
|
||||
import { QueryParameterActionsProvider } from '../actions';
|
||||
|
||||
|
@ -33,8 +31,6 @@ describe('context app', function () {
|
|||
let setPredecessorCount;
|
||||
|
||||
beforeEach(ngMock.inject(function createPrivateStubs(Private) {
|
||||
Private.stub(FilterManagerProvider, {});
|
||||
|
||||
setPredecessorCount = Private(QueryParameterActionsProvider).setPredecessorCount;
|
||||
}));
|
||||
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
import expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
|
||||
import { createStateStub } from './_utils';
|
||||
import { QueryParameterActionsProvider } from '../actions';
|
||||
|
||||
|
@ -33,8 +31,6 @@ describe('context app', function () {
|
|||
let setQueryParameters;
|
||||
|
||||
beforeEach(ngMock.inject(function createPrivateStubs(Private) {
|
||||
Private.stub(FilterManagerProvider, {});
|
||||
|
||||
setQueryParameters = Private(QueryParameterActionsProvider).setQueryParameters;
|
||||
}));
|
||||
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
import expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
|
||||
import { createStateStub } from './_utils';
|
||||
import { QueryParameterActionsProvider } from '../actions';
|
||||
|
||||
|
@ -33,8 +31,6 @@ describe('context app', function () {
|
|||
let setSuccessorCount;
|
||||
|
||||
beforeEach(ngMock.inject(function createPrivateStubs(Private) {
|
||||
Private.stub(FilterManagerProvider, {});
|
||||
|
||||
setSuccessorCount = Private(QueryParameterActionsProvider).setSuccessorCount;
|
||||
}));
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter';
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
import { getFilterGenerator } from 'ui/filter_manager';
|
||||
import {
|
||||
MAX_CONTEXT_SIZE,
|
||||
MIN_CONTEXT_SIZE,
|
||||
|
@ -30,7 +30,7 @@ import {
|
|||
|
||||
export function QueryParameterActionsProvider(indexPatterns, Private) {
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
const filterManager = Private(FilterManagerProvider);
|
||||
const filterGen = getFilterGenerator(queryFilter);
|
||||
|
||||
const setPredecessorCount = (state) => (predecessorCount) => (
|
||||
state.queryParameters.predecessorCount = clamp(
|
||||
|
@ -73,7 +73,8 @@ export function QueryParameterActionsProvider(indexPatterns, Private) {
|
|||
|
||||
const addFilter = (state) => async (field, values, operation) => {
|
||||
const indexPatternId = state.queryParameters.indexPatternId;
|
||||
filterManager.add(field, values, operation, indexPatternId);
|
||||
const newFilters = filterGen.generate(field, values, operation, indexPatternId);
|
||||
queryFilter.addFilters(newFilters);
|
||||
const indexPattern = await indexPatterns.get(indexPatternId);
|
||||
indexPattern.popularizeField(field.name, 1);
|
||||
};
|
||||
|
|
|
@ -37,7 +37,8 @@ import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal';
|
|||
import * as filterActions from 'plugins/kibana/discover/doc_table/actions/filter';
|
||||
|
||||
// @ts-ignore
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
import { getFilterGenerator } from 'ui/filter_manager';
|
||||
import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter';
|
||||
import { EmbeddableFactory } from 'ui/embeddable';
|
||||
|
||||
import {
|
||||
|
@ -125,9 +126,10 @@ app.directive('dashboardApp', function($injector: IInjector) {
|
|||
|
||||
const Private = $injector.get<IPrivate>('Private');
|
||||
|
||||
const filterManager = Private(FilterManagerProvider);
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
const filterGen = getFilterGenerator(queryFilter);
|
||||
const addFilter: AddFilterFn = ({ field, value, operator, index }, appState: TAppState) => {
|
||||
filterActions.addFilter(field, value, operator, index, appState, filterManager);
|
||||
filterActions.addFilter(field, value, operator, index, appState, filterGen);
|
||||
};
|
||||
|
||||
const indexPatterns = $injector.get<{
|
||||
|
|
|
@ -40,9 +40,9 @@ import { timefilter } from 'ui/timefilter';
|
|||
import { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { VisProvider } from 'ui/vis';
|
||||
import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter';
|
||||
import { vislibSeriesResponseHandlerProvider } from 'ui/vis/response_handlers/vislib';
|
||||
import { docTitle } from 'ui/doc_title';
|
||||
import { FilterBarQueryFilterProvider } from 'ui/filter_manager/query_filter';
|
||||
import { intervalOptions } from 'ui/agg_types/buckets/_interval_options';
|
||||
import { stateMonitorFactory } from 'ui/state_management/state_monitor_factory';
|
||||
import uiRoutes from 'ui/routes';
|
||||
|
@ -50,7 +50,8 @@ import { uiModules } from 'ui/modules';
|
|||
import indexTemplate from '../index.html';
|
||||
import { StateProvider } from 'ui/state_management/state';
|
||||
import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query';
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
import { subscribeWithScope } from 'ui/utils/subscribe_with_scope';
|
||||
import { getFilterGenerator } from 'ui/filter_manager';
|
||||
import { SavedObjectsClientProvider } from 'ui/saved_objects';
|
||||
import { VisualizeLoaderProvider } from 'ui/visualize/loader/visualize_loader';
|
||||
import { recentlyAccessed } from 'ui/persisted_log';
|
||||
|
@ -196,11 +197,13 @@ function discoverController(
|
|||
const visualizeLoader = Private(VisualizeLoaderProvider);
|
||||
let visualizeHandler;
|
||||
const Vis = Private(VisProvider);
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
const responseHandler = vislibSeriesResponseHandlerProvider().handler;
|
||||
const filterManager = Private(FilterManagerProvider);
|
||||
const getUnhashableStates = Private(getUnhashableStatesProvider);
|
||||
const shareContextMenuExtensions = Private(ShareContextMenuExtensionsRegistryProvider);
|
||||
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
const filterGen = getFilterGenerator(queryFilter);
|
||||
|
||||
const inspectorAdapters = {
|
||||
requests: new RequestAdapter()
|
||||
};
|
||||
|
@ -561,17 +564,19 @@ function discoverController(
|
|||
});
|
||||
|
||||
// update data source when filters update
|
||||
filterUpdateSubscription = queryFilter.getUpdates$().subscribe(
|
||||
() => {
|
||||
filterUpdateSubscription = subscribeWithScope($scope, queryFilter.getUpdates$(), {
|
||||
next: () => {
|
||||
$scope.filters = queryFilter.getFilters();
|
||||
$scope.updateDataSource().then(function () {
|
||||
$state.save();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// fetch data when filters fire fetch event
|
||||
filterFetchSubscription = queryFilter.getFetches$().subscribe($scope.fetch);
|
||||
filterFetchSubscription = subscribeWithScope($scope, queryFilter.getUpdates$(), {
|
||||
next: $scope.fetch
|
||||
});
|
||||
|
||||
// update data source when hitting forward/back and the query changes
|
||||
$scope.$listen($state, 'fetch_with_changes', function (diff) {
|
||||
|
@ -860,7 +865,7 @@ function discoverController(
|
|||
// TODO: On array fields, negating does not negate the combination, rather all terms
|
||||
$scope.filterQuery = function (field, values, operation) {
|
||||
$scope.indexPattern.popularizeField(field, 1);
|
||||
filterActions.addFilter(field, values, operation, $scope.indexPattern.id, $scope.state, filterManager);
|
||||
filterActions.addFilter(field, values, operation, $scope.indexPattern.id, $scope.state, filterGen);
|
||||
};
|
||||
|
||||
$scope.addColumn = function addColumn(columnName) {
|
||||
|
|
|
@ -18,17 +18,22 @@
|
|||
*/
|
||||
|
||||
import { addFilter } from '../../actions/filter';
|
||||
import { FilterManagerProvider } from 'ui/filter_manager';
|
||||
import StubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import NoDigestPromises from 'test_utils/no_digest_promises';
|
||||
import expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
import sinon from 'sinon';
|
||||
|
||||
function getFilterGeneratorStub() {
|
||||
return {
|
||||
add: sinon.stub()
|
||||
};
|
||||
}
|
||||
|
||||
describe('doc table filter actions', function () {
|
||||
NoDigestPromises.activateForSuite();
|
||||
|
||||
let filterManager;
|
||||
let filterGen;
|
||||
let indexPattern;
|
||||
|
||||
beforeEach(ngMock.module(
|
||||
|
@ -41,8 +46,7 @@ describe('doc table filter actions', function () {
|
|||
|
||||
beforeEach(ngMock.inject(function (Private) {
|
||||
indexPattern = Private(StubbedLogstashIndexPatternProvider);
|
||||
filterManager = Private(FilterManagerProvider);
|
||||
sinon.stub(filterManager, 'add');
|
||||
filterGen = getFilterGeneratorStub();
|
||||
}));
|
||||
|
||||
describe('add', function () {
|
||||
|
@ -52,9 +56,9 @@ describe('doc table filter actions', function () {
|
|||
query: { query: 'foo', language: 'lucene' }
|
||||
};
|
||||
const args = ['foo', ['bar'], '+', indexPattern, ];
|
||||
addFilter('foo', ['bar'], '+', indexPattern, state, filterManager);
|
||||
expect(filterManager.add.calledOnce).to.be(true);
|
||||
expect(filterManager.add.calledWith(...args)).to.be(true);
|
||||
addFilter('foo', ['bar'], '+', indexPattern, state, filterGen);
|
||||
expect(filterGen.add.calledOnce).to.be(true);
|
||||
expect(filterGen.add.calledWith(...args)).to.be(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export function addFilter(field, values = [], operation, index, state, filterManager) {
|
||||
export function addFilter(field, values = [], operation, index, state, filterGen) {
|
||||
if (!Array.isArray(values)) {
|
||||
values = [values];
|
||||
}
|
||||
|
||||
filterManager.add(field, values, operation, index);
|
||||
filterGen.add(field, values, operation, index);
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ import { VisualizeConstants } from '../visualize_constants';
|
|||
import { KibanaParsedUrl } from 'ui/url/kibana_parsed_url';
|
||||
import { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url';
|
||||
import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query';
|
||||
import { subscribeWithScope } from 'ui/utils/subscribe_with_scope';
|
||||
import { recentlyAccessed } from 'ui/persisted_log';
|
||||
import { timefilter } from 'ui/timefilter';
|
||||
import { getVisualizeLoader } from '../../../../../ui/public/visualize/loader';
|
||||
|
@ -415,12 +416,12 @@ function VisEditor(
|
|||
$scope.$listenAndDigestAsync(timefilter, 'refreshIntervalUpdate', updateRefreshInterval);
|
||||
|
||||
// update the searchSource when filters update
|
||||
const filterUpdateSubscription = queryFilter.getUpdates$().subscribe(
|
||||
() => {
|
||||
const filterUpdateSubscription = subscribeWithScope($scope, queryFilter.getUpdates$(), {
|
||||
next: () => {
|
||||
$scope.filters = queryFilter.getFilters();
|
||||
$scope.fetch();
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// update the searchSource when query updates
|
||||
$scope.fetch = function () {
|
||||
|
|
|
@ -35,9 +35,12 @@ export const kibanaMarkdown = () => ({
|
|||
aliases: ['_'],
|
||||
required: true,
|
||||
},
|
||||
fontSize: {
|
||||
types: ['number'],
|
||||
default: 12,
|
||||
font: {
|
||||
types: ['style'],
|
||||
help: i18n.translate('markdownVis.function.font.help', {
|
||||
defaultMessage: 'Font settings.'
|
||||
}),
|
||||
default: `{font size=12}`,
|
||||
},
|
||||
openLinksInNewTab: {
|
||||
types: ['boolean'],
|
||||
|
@ -51,7 +54,9 @@ export const kibanaMarkdown = () => ({
|
|||
value: {
|
||||
visType: 'markdown',
|
||||
visConfig: {
|
||||
...args,
|
||||
markdown: args.markdown,
|
||||
openLinksInNewTab: args.openLinksInNewTab,
|
||||
fontSize: parseInt(args.font.spec.fontSize),
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -23,7 +23,7 @@ import { kibanaMarkdown } from './markdown_fn';
|
|||
describe('interpreter/functions#markdown', () => {
|
||||
const fn = functionWrapper(kibanaMarkdown);
|
||||
const args = {
|
||||
fontSize: 12,
|
||||
font: { spec: { fontSize: 12 } },
|
||||
openLinksInNewTab: true,
|
||||
markdown: '## hello _markdown_',
|
||||
};
|
||||
|
|
|
@ -9,42 +9,27 @@ Object {
|
|||
"listenOnChange": true,
|
||||
},
|
||||
"visConfig": Object {
|
||||
"addLegend": false,
|
||||
"addTooltip": true,
|
||||
"dimensions": Object {
|
||||
"metrics": undefined,
|
||||
},
|
||||
"metric": Object {
|
||||
"colorSchema": "Green to Red",
|
||||
"colorsRange": Array [
|
||||
Object {
|
||||
"from": 0,
|
||||
"to": 10000,
|
||||
},
|
||||
],
|
||||
"colorSchema": "\\"Green to Red\\"",
|
||||
"colorsRange": undefined,
|
||||
"invertColors": false,
|
||||
"labels": Object {
|
||||
"show": true,
|
||||
},
|
||||
"metricColorMode": "None",
|
||||
"metrics": Array [
|
||||
Object {
|
||||
"accessor": 0,
|
||||
"aggType": "count",
|
||||
"format": Object {
|
||||
"id": "number",
|
||||
},
|
||||
"params": Object {},
|
||||
},
|
||||
],
|
||||
"metricColorMode": "\\"None\\"",
|
||||
"percentageMode": false,
|
||||
"style": Object {
|
||||
"bgColor": false,
|
||||
"bgFill": "#000",
|
||||
"bgFill": "\\"#000\\"",
|
||||
"fontSize": 60,
|
||||
"labelColor": false,
|
||||
"subText": "",
|
||||
"subText": "\\"\\"",
|
||||
},
|
||||
"useRanges": false,
|
||||
},
|
||||
"type": "metric",
|
||||
},
|
||||
"visData": Object {
|
||||
"columns": Array [
|
||||
|
|
|
@ -19,9 +19,10 @@
|
|||
|
||||
import { functionsRegistry } from 'plugins/interpreter/registries';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { vislibColorMaps } from 'ui/vislib/components/color/colormaps';
|
||||
|
||||
export const metric = () => ({
|
||||
name: 'kibana_metric',
|
||||
name: 'metricVis',
|
||||
type: 'render',
|
||||
context: {
|
||||
types: [
|
||||
|
@ -32,13 +33,110 @@ export const metric = () => ({
|
|||
defaultMessage: 'Metric visualization'
|
||||
}),
|
||||
args: {
|
||||
visConfig: {
|
||||
types: ['string', 'null'],
|
||||
default: '"{}"',
|
||||
percentage: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: i18n.translate('metricVis.function.percentage.help', {
|
||||
defaultMessage: 'Shows metric in percentage mode. Requires colorRange to be set.'
|
||||
})
|
||||
},
|
||||
colorScheme: {
|
||||
types: ['string'],
|
||||
default: '"Green to Red"',
|
||||
options: Object.values(vislibColorMaps).map(value => value.id),
|
||||
help: i18n.translate('metricVis.function.colorScheme.help', {
|
||||
defaultMessage: 'Color scheme to use'
|
||||
})
|
||||
},
|
||||
colorMode: {
|
||||
types: ['string'],
|
||||
default: '"None"',
|
||||
options: ['None', 'Label', 'Background'],
|
||||
help: i18n.translate('metricVis.function.colorMode.help', {
|
||||
defaultMessage: 'Which part of metric to color'
|
||||
})
|
||||
},
|
||||
colorRange: {
|
||||
types: ['range'],
|
||||
multi: true,
|
||||
help: i18n.translate('metricVis.function.colorRange.help', {
|
||||
defaultMessage: 'A range object specifying groups of values to which different colors should be applied.'
|
||||
})
|
||||
},
|
||||
useRanges: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: i18n.translate('metricVis.function.useRanges.help', {
|
||||
defaultMessage: 'Enabled color ranges.'
|
||||
})
|
||||
},
|
||||
invertColors: {
|
||||
types: ['boolean'],
|
||||
default: false,
|
||||
help: i18n.translate('metricVis.function.invertColors.help', {
|
||||
defaultMessage: 'Inverts the color ranges'
|
||||
})
|
||||
},
|
||||
showLabels: {
|
||||
types: ['boolean'],
|
||||
default: true,
|
||||
help: i18n.translate('metricVis.function.showLabels.help', {
|
||||
defaultMessage: 'Shows labels under the metric values.'
|
||||
})
|
||||
},
|
||||
bgFill: {
|
||||
types: ['string'],
|
||||
default: '"#000"',
|
||||
aliases: ['backgroundFill', 'bgColor', 'backgroundColor'],
|
||||
help: i18n.translate('metricVis.function.bgFill.help', {
|
||||
defaultMessage: 'Color as html hex code (#123456), html color (red, blue) or rgba value (rgba(255,255,255,1)).'
|
||||
})
|
||||
},
|
||||
font: {
|
||||
types: ['style'],
|
||||
help: i18n.translate('metricVis.function.font.help', {
|
||||
defaultMessage: 'Font settings.'
|
||||
}),
|
||||
default: '{font size=60}',
|
||||
},
|
||||
subText: {
|
||||
types: ['string'],
|
||||
aliases: ['label', 'text', 'description'],
|
||||
default: '""',
|
||||
help: i18n.translate('metricVis.function.subText.help', {
|
||||
defaultMessage: 'Custom text to show under the metric'
|
||||
})
|
||||
},
|
||||
metric: {
|
||||
types: ['vis_dimension'],
|
||||
help: i18n.translate('metricVis.function.metric.help', {
|
||||
defaultMessage: 'metric dimension configuration'
|
||||
}),
|
||||
required: true,
|
||||
multi: true,
|
||||
},
|
||||
bucket: {
|
||||
types: ['vis_dimension'],
|
||||
help: i18n.translate('metricVis.function.bucket.help', {
|
||||
defaultMessage: 'bucket dimension configuration'
|
||||
}),
|
||||
},
|
||||
},
|
||||
fn(context, args) {
|
||||
const visConfig = JSON.parse(args.visConfig);
|
||||
|
||||
const dimensions = {
|
||||
metrics: args.metric,
|
||||
};
|
||||
|
||||
if (args.bucket) {
|
||||
dimensions.bucket = args.bucket;
|
||||
}
|
||||
|
||||
if (args.percentage && (!args.colorRange || args.colorRange.length === 0)) {
|
||||
throw new Error ('colorRange must be provided when using percentage');
|
||||
}
|
||||
|
||||
const fontSize = parseInt(args.font.spec.fontSize);
|
||||
|
||||
return {
|
||||
type: 'render',
|
||||
|
@ -46,7 +144,27 @@ export const metric = () => ({
|
|||
value: {
|
||||
visData: context,
|
||||
visType: 'metric',
|
||||
visConfig,
|
||||
visConfig: {
|
||||
metric: {
|
||||
percentageMode: args.percentage,
|
||||
useRanges: args.useRanges,
|
||||
colorSchema: args.colorScheme,
|
||||
metricColorMode: args.colorMode,
|
||||
colorsRange: args.colorRange,
|
||||
labels: {
|
||||
show: args.showLabels,
|
||||
},
|
||||
invertColors: args.invertColors,
|
||||
style: {
|
||||
bgFill: args.bgFill,
|
||||
bgColor: args.colorMode === 'Background',
|
||||
labelColor: args.colorMode === 'Labels',
|
||||
subText: args.subText,
|
||||
fontSize,
|
||||
}
|
||||
},
|
||||
dimensions,
|
||||
},
|
||||
params: {
|
||||
listenOnChange: true,
|
||||
}
|
||||
|
|
|
@ -27,47 +27,43 @@ describe('interpreter/functions#metric', () => {
|
|||
rows: [{ 'col-0-1': 0 }],
|
||||
columns: [{ id: 'col-0-1', name: 'Count' }],
|
||||
};
|
||||
const visConfig = {
|
||||
addTooltip: true,
|
||||
addLegend: false,
|
||||
type: 'metric',
|
||||
metric: {
|
||||
percentageMode: false,
|
||||
useRanges: false,
|
||||
colorSchema: 'Green to Red',
|
||||
metricColorMode: 'None',
|
||||
colorsRange: [
|
||||
{
|
||||
from: 0,
|
||||
to: 10000,
|
||||
}
|
||||
],
|
||||
labels: {
|
||||
show: true,
|
||||
},
|
||||
invertColors: false,
|
||||
style: {
|
||||
bgFill: '#000',
|
||||
bgColor: false,
|
||||
labelColor: false,
|
||||
subText: '',
|
||||
fontSize: 60,
|
||||
},
|
||||
metrics: [
|
||||
{
|
||||
accessor: 0,
|
||||
format: {
|
||||
id: 'number'
|
||||
},
|
||||
params: {},
|
||||
aggType: 'count',
|
||||
}
|
||||
]
|
||||
const args = {
|
||||
percentageMode: false,
|
||||
useRanges: false,
|
||||
colorSchema: 'Green to Red',
|
||||
metricColorMode: 'None',
|
||||
colorsRange: [
|
||||
{
|
||||
from: 0,
|
||||
to: 10000,
|
||||
}
|
||||
],
|
||||
labels: {
|
||||
show: true,
|
||||
},
|
||||
invertColors: false,
|
||||
style: {
|
||||
bgFill: '#000',
|
||||
bgColor: false,
|
||||
labelColor: false,
|
||||
subText: '',
|
||||
fontSize: 60,
|
||||
},
|
||||
font: { spec: { fontSize: 60 } },
|
||||
metrics: [
|
||||
{
|
||||
accessor: 0,
|
||||
format: {
|
||||
id: 'number'
|
||||
},
|
||||
params: {},
|
||||
aggType: 'count',
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
it('returns an object with the correct structure', () => {
|
||||
const actual = fn(context, { visConfig: JSON.stringify(visConfig) });
|
||||
const actual = fn(context, args);
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,6 +31,7 @@ const chrome = {
|
|||
addBasePath: path => path,
|
||||
getInjected: jest.fn(),
|
||||
getUiSettingsClient: () => uiSettingsClient,
|
||||
getSavedObjectsClient: () => '',
|
||||
getXsrfToken: () => 'kbn-xsrf-token',
|
||||
};
|
||||
|
||||
|
|
|
@ -49,7 +49,12 @@ export class KbnError {
|
|||
// http://stackoverflow.com/questions/33870684/why-doesnt-instanceof-work-on-instances-of-error-subclasses-under-babel-node
|
||||
// Hence we are inheriting from it this way, instead of using extends Error, and this will then preserve
|
||||
// instanceof checks.
|
||||
createLegacyClass(KbnError).inherits(Error);
|
||||
try {
|
||||
createLegacyClass(KbnError).inherits(Error);
|
||||
} catch (e) {
|
||||
// Avoid TypeError: Cannot redefine property: prototype
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Request Failure - When an entire multi request fails
|
||||
|
|
|
@ -1,211 +0,0 @@
|
|||
/*
|
||||
* 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 sinon from 'sinon';
|
||||
import expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
import MockState from 'fixtures/mock_state';
|
||||
import { FilterBarQueryFilterProvider } from '../query_filter';
|
||||
|
||||
describe('add filters', function () {
|
||||
require('test_utils/no_digest_promises').activateForSuite();
|
||||
|
||||
let filters;
|
||||
let queryFilter;
|
||||
let $rootScope;
|
||||
let appState;
|
||||
let globalState;
|
||||
|
||||
beforeEach(ngMock.module(
|
||||
'kibana',
|
||||
'kibana/courier',
|
||||
'kibana/global_state',
|
||||
function ($provide) {
|
||||
$provide.service('indexPatterns', require('fixtures/mock_index_patterns'));
|
||||
|
||||
appState = new MockState({ filters: [] });
|
||||
$provide.service('getAppState', function () {
|
||||
return function () { return appState; };
|
||||
});
|
||||
|
||||
globalState = new MockState({ filters: [] });
|
||||
$provide.service('globalState', function () {
|
||||
return globalState;
|
||||
});
|
||||
}
|
||||
));
|
||||
|
||||
beforeEach(ngMock.inject(function (_$rootScope_, Private) {
|
||||
$rootScope = _$rootScope_;
|
||||
queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
}));
|
||||
|
||||
beforeEach(function () {
|
||||
filters = [
|
||||
{
|
||||
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
|
||||
meta: { index: 'logstash-*', negate: false, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
|
||||
meta: { index: 'logstash-*', negate: false, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { '_type': { query: 'nginx', type: 'phrase' } } },
|
||||
meta: { index: 'logstash-*', negate: false, disabled: false }
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
describe('adding filters', function () {
|
||||
it('should add filters to appState', async function () {
|
||||
await queryFilter.addFilters(filters);
|
||||
expect(appState.filters.length).to.be(3);
|
||||
expect(globalState.filters.length).to.be(0);
|
||||
});
|
||||
|
||||
it('should add filters to globalState', async function () {
|
||||
await queryFilter.addFilters(filters, true);
|
||||
|
||||
expect(appState.filters.length).to.be(0);
|
||||
expect(globalState.filters.length).to.be(3);
|
||||
});
|
||||
|
||||
it('should accept a single filter', async function () {
|
||||
await queryFilter.addFilters(filters[0]);
|
||||
|
||||
expect(appState.filters.length).to.be(1);
|
||||
expect(globalState.filters.length).to.be(0);
|
||||
});
|
||||
|
||||
it('should allow overwriting a positive filter by a negated one', async function () {
|
||||
|
||||
// Add negate: false version of the filter
|
||||
const filter = _.cloneDeep(filters[0]);
|
||||
filter.meta.negate = false;
|
||||
|
||||
await queryFilter.addFilters(filter);
|
||||
$rootScope.$digest();
|
||||
expect(appState.filters.length).to.be(1);
|
||||
expect(appState.filters[0]).to.eql(filter);
|
||||
|
||||
// Add negate: true version of the same filter
|
||||
const negatedFilter = _.cloneDeep(filters[0]);
|
||||
negatedFilter.meta.negate = true;
|
||||
|
||||
await queryFilter.addFilters(negatedFilter);
|
||||
$rootScope.$digest();
|
||||
// The negated filter should overwrite the positive one
|
||||
expect(appState.filters.length).to.be(1);
|
||||
expect(appState.filters[0]).to.eql(negatedFilter);
|
||||
});
|
||||
|
||||
it('should allow overwriting a negated filter by a positive one', async function () {
|
||||
// Add negate: true version of the same filter
|
||||
const negatedFilter = _.cloneDeep(filters[0]);
|
||||
negatedFilter.meta.negate = true;
|
||||
|
||||
await queryFilter.addFilters(negatedFilter);
|
||||
$rootScope.$digest();
|
||||
|
||||
// The negated filter should overwrite the positive one
|
||||
expect(appState.filters.length).to.be(1);
|
||||
expect(appState.filters[0]).to.eql(negatedFilter);
|
||||
|
||||
// Add negate: false version of the filter
|
||||
const filter = _.cloneDeep(filters[0]);
|
||||
filter.meta.negate = false;
|
||||
|
||||
await queryFilter.addFilters(filter);
|
||||
$rootScope.$digest();
|
||||
expect(appState.filters.length).to.be(1);
|
||||
expect(appState.filters[0]).to.eql(filter);
|
||||
});
|
||||
|
||||
it('should fire the update and fetch events', async function () {
|
||||
const updateStub = sinon.stub();
|
||||
const fetchStub = sinon.stub();
|
||||
|
||||
queryFilter.getUpdates$().subscribe({
|
||||
next: updateStub,
|
||||
});
|
||||
|
||||
queryFilter.getFetches$().subscribe({
|
||||
next: fetchStub,
|
||||
});
|
||||
|
||||
// set up the watchers, add new filters, and crank the digest loop
|
||||
$rootScope.$digest();
|
||||
await queryFilter.addFilters(filters);
|
||||
$rootScope.$digest();
|
||||
|
||||
// updates should trigger state saves
|
||||
expect(appState.save.callCount).to.be(1);
|
||||
expect(globalState.save.callCount).to.be(1);
|
||||
|
||||
// this time, events should be emitted
|
||||
expect(fetchStub.called);
|
||||
expect(updateStub.called);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter reconciliation', function () {
|
||||
it('should de-dupe appState filters being added', async function () {
|
||||
const newFilter = _.cloneDeep(filters[1]);
|
||||
appState.filters = filters;
|
||||
$rootScope.$digest();
|
||||
expect(appState.filters.length).to.be(3);
|
||||
|
||||
await queryFilter.addFilters(newFilter);
|
||||
$rootScope.$digest();
|
||||
expect(appState.filters.length).to.be(3);
|
||||
});
|
||||
|
||||
it('should de-dupe globalState filters being added', async function () {
|
||||
const newFilter = _.cloneDeep(filters[1]);
|
||||
globalState.filters = filters;
|
||||
$rootScope.$digest();
|
||||
expect(globalState.filters.length).to.be(3);
|
||||
|
||||
await queryFilter.addFilters(newFilter, true);
|
||||
$rootScope.$digest();
|
||||
expect(globalState.filters.length).to.be(3);
|
||||
});
|
||||
|
||||
it('should mutate global filters on appState filter changes', async function () {
|
||||
const idx = 1;
|
||||
globalState.filters = filters;
|
||||
$rootScope.$digest();
|
||||
|
||||
const appFilter = _.cloneDeep(filters[idx]);
|
||||
appFilter.meta.negate = true;
|
||||
await queryFilter.addFilters(appFilter);
|
||||
$rootScope.$digest();
|
||||
|
||||
const res = queryFilter.getFilters();
|
||||
expect(res).to.have.length(3);
|
||||
_.each(res, function (filter, i) {
|
||||
expect(filter.$state.store).to.be('globalState');
|
||||
// make sure global filter actually mutated
|
||||
expect(filter.meta.negate).to.be(i === idx);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,202 +0,0 @@
|
|||
/*
|
||||
* 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 ngMock from 'ng_mock';
|
||||
import expect from '@kbn/expect';
|
||||
import MockState from 'fixtures/mock_state';
|
||||
import { FilterBarQueryFilterProvider } from '../query_filter';
|
||||
|
||||
describe('get filters', function () {
|
||||
const storeNames = {
|
||||
app: 'appState',
|
||||
global: 'globalState'
|
||||
};
|
||||
let queryFilter;
|
||||
let appState;
|
||||
let globalState;
|
||||
|
||||
beforeEach(ngMock.module(
|
||||
'kibana',
|
||||
'kibana/global_state',
|
||||
function ($provide) {
|
||||
appState = new MockState({ filters: [] });
|
||||
$provide.service('getAppState', function () {
|
||||
return function () { return appState; };
|
||||
});
|
||||
|
||||
globalState = new MockState({ filters: [] });
|
||||
$provide.service('globalState', function () {
|
||||
return globalState;
|
||||
});
|
||||
}
|
||||
));
|
||||
|
||||
beforeEach(ngMock.inject(function (_$rootScope_, Private) {
|
||||
queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
}));
|
||||
|
||||
describe('getFilters method', function () {
|
||||
let filters;
|
||||
|
||||
beforeEach(function () {
|
||||
filters = [
|
||||
{ query: { match: { extension: { query: 'jpg', type: 'phrase' } } } },
|
||||
{ query: { match: { '@tags': { query: 'info', type: 'phrase' } } } },
|
||||
null
|
||||
];
|
||||
});
|
||||
|
||||
it('should return app and global filters', function () {
|
||||
appState.filters = [filters[0]];
|
||||
globalState.filters = [filters[1]];
|
||||
|
||||
// global filters should be listed first
|
||||
let res = queryFilter.getFilters();
|
||||
expect(res[0]).to.eql(filters[1]);
|
||||
expect(res[1]).to.eql(filters[0]);
|
||||
|
||||
// should return updated version of filters
|
||||
const newFilter = { query: { match: { '_type': { query: 'nginx', type: 'phrase' } } } };
|
||||
appState.filters.push(newFilter);
|
||||
|
||||
res = queryFilter.getFilters();
|
||||
expect(res).to.contain(newFilter);
|
||||
});
|
||||
|
||||
it('should append the state store', function () {
|
||||
appState.filters = [filters[0]];
|
||||
globalState.filters = [filters[1]];
|
||||
|
||||
const res = queryFilter.getFilters();
|
||||
expect(res[0].$state.store).to.be(storeNames.global);
|
||||
expect(res[1].$state.store).to.be(storeNames.app);
|
||||
});
|
||||
|
||||
it('should return non-null filters from specific states', function () {
|
||||
const states = [
|
||||
[ globalState, queryFilter.getGlobalFilters ],
|
||||
[ appState, queryFilter.getAppFilters ],
|
||||
];
|
||||
|
||||
_.each(states, function (state) {
|
||||
state[0].filters = filters.slice(0);
|
||||
expect(state[0].filters).to.contain(null);
|
||||
|
||||
const res = state[1]();
|
||||
expect(res.length).to.be(state[0].filters.length);
|
||||
expect(state[0].filters).to.not.contain(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace the state, not save it', function () {
|
||||
const states = [
|
||||
[ globalState, queryFilter.getGlobalFilters ],
|
||||
[ appState, queryFilter.getAppFilters ],
|
||||
];
|
||||
|
||||
expect(appState.save.called).to.be(false);
|
||||
expect(appState.replace.called).to.be(false);
|
||||
|
||||
|
||||
_.each(states, function (state) {
|
||||
expect(state[0].save.called).to.be(false);
|
||||
expect(state[0].replace.called).to.be(false);
|
||||
|
||||
state[0].filters = filters.slice(0);
|
||||
state[1]();
|
||||
expect(state[0].save.called).to.be(false);
|
||||
expect(state[0].replace.called).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter reconciliation', function () {
|
||||
let filters;
|
||||
|
||||
beforeEach(function () {
|
||||
filters = [
|
||||
{
|
||||
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { '_type': { query: 'nginx', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
it('should skip appState filters that match globalState filters', function () {
|
||||
globalState.filters = filters;
|
||||
const appFilter = _.cloneDeep(filters[1]);
|
||||
appState.filters.push(appFilter);
|
||||
|
||||
// global filters should be listed first
|
||||
const res = queryFilter.getFilters();
|
||||
expect(res).to.have.length(3);
|
||||
_.each(res, function (filter) {
|
||||
expect(filter.$state.store).to.be('globalState');
|
||||
});
|
||||
});
|
||||
|
||||
it('should append conflicting appState filters', function () {
|
||||
globalState.filters = filters;
|
||||
const appFilter = _.cloneDeep(filters[1]);
|
||||
appFilter.meta.negate = true;
|
||||
appState.filters.push(appFilter);
|
||||
|
||||
// global filters should be listed first
|
||||
const res = queryFilter.getFilters();
|
||||
expect(res).to.have.length(4);
|
||||
expect(res.filter(function (filter) {
|
||||
return filter.$state.store === storeNames.global;
|
||||
}).length).to.be(3);
|
||||
expect(res.filter(function (filter) {
|
||||
return filter.$state.store === storeNames.app;
|
||||
}).length).to.be(1);
|
||||
});
|
||||
|
||||
it('should not affect disabled filters', function () {
|
||||
// test adding to globalState
|
||||
globalState.filters = _.map(filters, function (filter) {
|
||||
const f = _.cloneDeep(filter);
|
||||
f.meta.disabled = true;
|
||||
return f;
|
||||
});
|
||||
_.each(filters, function (filter) { globalState.filters.push(filter); });
|
||||
let res = queryFilter.getFilters();
|
||||
expect(res).to.have.length(6);
|
||||
|
||||
// test adding to appState
|
||||
globalState.filters = _.map(filters, function (filter) {
|
||||
const f = _.cloneDeep(filter);
|
||||
f.meta.disabled = true;
|
||||
return f;
|
||||
});
|
||||
_.each(filters, function (filter) { appState.filters.push(filter); });
|
||||
res = queryFilter.getFilters();
|
||||
expect(res).to.have.length(6);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,147 +0,0 @@
|
|||
/*
|
||||
* 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 sinon from 'sinon';
|
||||
import expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
import MockState from 'fixtures/mock_state';
|
||||
import { FilterBarQueryFilterProvider } from '../query_filter';
|
||||
|
||||
describe('invert filters', function () {
|
||||
let filters;
|
||||
let queryFilter;
|
||||
let $rootScope;
|
||||
let appState;
|
||||
let globalState;
|
||||
|
||||
beforeEach(ngMock.module(
|
||||
'kibana',
|
||||
'kibana/courier',
|
||||
'kibana/global_state',
|
||||
function ($provide) {
|
||||
$provide.service('indexPatterns', require('fixtures/mock_index_patterns'));
|
||||
|
||||
appState = new MockState({ filters: [] });
|
||||
$provide.service('getAppState', function () {
|
||||
return function () { return appState; };
|
||||
});
|
||||
|
||||
globalState = new MockState({ filters: [] });
|
||||
$provide.service('globalState', function () {
|
||||
return globalState;
|
||||
});
|
||||
}
|
||||
));
|
||||
|
||||
beforeEach(ngMock.inject(function (_$rootScope_, Private) {
|
||||
$rootScope = _$rootScope_;
|
||||
queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
|
||||
filters = [
|
||||
{
|
||||
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { '_type': { query: 'nginx', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
}
|
||||
];
|
||||
}));
|
||||
|
||||
describe('inverting a filter', function () {
|
||||
it('should swap the negate property in appState', function () {
|
||||
_.each(filters, function (filter) {
|
||||
expect(filter.meta.negate).to.be(false);
|
||||
appState.filters.push(filter);
|
||||
});
|
||||
|
||||
queryFilter.invertFilter(filters[1]);
|
||||
expect(appState.filters[1].meta.negate).to.be(true);
|
||||
});
|
||||
|
||||
it('should toggle the negate property in globalState', function () {
|
||||
_.each(filters, function (filter) {
|
||||
expect(filter.meta.negate).to.be(false);
|
||||
globalState.filters.push(filter);
|
||||
});
|
||||
|
||||
queryFilter.invertFilter(filters[1]);
|
||||
expect(globalState.filters[1].meta.negate).to.be(true);
|
||||
});
|
||||
|
||||
it('should fire the update and fetch events', function () {
|
||||
const updateStub = sinon.stub();
|
||||
const fetchStub = sinon.stub();
|
||||
|
||||
queryFilter.getUpdates$().subscribe({
|
||||
next: updateStub,
|
||||
});
|
||||
|
||||
queryFilter.getFetches$().subscribe({
|
||||
next: fetchStub,
|
||||
});
|
||||
appState.filters = filters;
|
||||
|
||||
// set up the watchers
|
||||
$rootScope.$digest();
|
||||
queryFilter.invertFilter(filters[1]);
|
||||
// trigger the digest loop to fire the watchers
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(fetchStub.called);
|
||||
expect(updateStub.called);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulk inverting', function () {
|
||||
beforeEach(function () {
|
||||
appState.filters = filters;
|
||||
globalState.filters = _.map(_.cloneDeep(filters), function (filter) {
|
||||
filter.meta.negate = true;
|
||||
return filter;
|
||||
});
|
||||
});
|
||||
|
||||
it('should swap the negate state for all filters', function () {
|
||||
queryFilter.invertAll();
|
||||
_.each(appState.filters, function (filter) {
|
||||
expect(filter.meta.negate).to.be(true);
|
||||
});
|
||||
_.each(globalState.filters, function (filter) {
|
||||
expect(filter.meta.negate).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work without global state filters', function () {
|
||||
// remove global filters
|
||||
delete globalState.filters;
|
||||
|
||||
queryFilter.invertAll();
|
||||
_.each(appState.filters, function (filter) {
|
||||
expect(filter.meta.negate).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,183 +0,0 @@
|
|||
/*
|
||||
* 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 sinon from 'sinon';
|
||||
import expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
import MockState from 'fixtures/mock_state';
|
||||
import { FilterBarQueryFilterProvider } from '../query_filter';
|
||||
|
||||
describe('pin filters', function () {
|
||||
let filters;
|
||||
let queryFilter;
|
||||
let $rootScope;
|
||||
let appState;
|
||||
let globalState;
|
||||
|
||||
beforeEach(ngMock.module(
|
||||
'kibana',
|
||||
'kibana/courier',
|
||||
'kibana/global_state',
|
||||
function ($provide) {
|
||||
$provide.service('indexPatterns', require('fixtures/mock_index_patterns'));
|
||||
|
||||
appState = new MockState({ filters: [] });
|
||||
$provide.service('getAppState', function () {
|
||||
return function () { return appState; };
|
||||
});
|
||||
|
||||
globalState = new MockState({ filters: [] });
|
||||
$provide.service('globalState', function () {
|
||||
return globalState;
|
||||
});
|
||||
}
|
||||
));
|
||||
|
||||
beforeEach(ngMock.inject(function (_$rootScope_, Private) {
|
||||
$rootScope = _$rootScope_;
|
||||
queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
|
||||
filters = [
|
||||
{
|
||||
query: { match: { extension: { query: 'gif', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
|
||||
meta: { negate: true, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { extension: { query: 'png', type: 'phrase' } } },
|
||||
meta: { negate: true, disabled: true }
|
||||
},
|
||||
{
|
||||
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { '@tags': { query: 'success', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { '@tags': { query: 'security', type: 'phrase' } } },
|
||||
meta: { negate: true, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { '_type': { query: 'nginx', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { '_type': { query: 'apache', type: 'phrase' } } },
|
||||
meta: { negate: true, disabled: true }
|
||||
}
|
||||
];
|
||||
}));
|
||||
|
||||
describe('pin a filter', function () {
|
||||
beforeEach(function () {
|
||||
globalState.filters = _.filter(filters, function (filter) {
|
||||
return !!filter.query.match._type;
|
||||
});
|
||||
appState.filters = _.filter(filters, function (filter) {
|
||||
return !filter.query.match._type;
|
||||
});
|
||||
expect(globalState.filters).to.have.length(2);
|
||||
expect(appState.filters).to.have.length(6);
|
||||
});
|
||||
|
||||
it('should move filter from appState to globalState', function () {
|
||||
const filter = appState.filters[1];
|
||||
|
||||
queryFilter.pinFilter(filter);
|
||||
expect(globalState.filters).to.contain(filter);
|
||||
expect(globalState.filters).to.have.length(3);
|
||||
expect(appState.filters).to.have.length(5);
|
||||
});
|
||||
|
||||
it('should move filter from globalState to appState', function () {
|
||||
const filter = globalState.filters[1];
|
||||
|
||||
queryFilter.pinFilter(filter);
|
||||
expect(appState.filters).to.contain(filter);
|
||||
expect(globalState.filters).to.have.length(1);
|
||||
expect(appState.filters).to.have.length(7);
|
||||
});
|
||||
|
||||
|
||||
it('should only fire the update event', function () {
|
||||
const updateStub = sinon.stub();
|
||||
const fetchStub = sinon.stub();
|
||||
|
||||
queryFilter.getUpdates$().subscribe({
|
||||
next: updateStub,
|
||||
});
|
||||
|
||||
queryFilter.getFetches$().subscribe({
|
||||
next: fetchStub,
|
||||
});
|
||||
|
||||
const filter = appState.filters[1];
|
||||
$rootScope.$digest();
|
||||
|
||||
queryFilter.pinFilter(filter);
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(!fetchStub.called);
|
||||
expect(updateStub.called);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulk pinning', function () {
|
||||
beforeEach(function () {
|
||||
globalState.filters = _.filter(filters, function (filter) {
|
||||
return !!filter.query.match.extension;
|
||||
});
|
||||
appState.filters = _.filter(filters, function (filter) {
|
||||
return !filter.query.match.extension;
|
||||
});
|
||||
expect(globalState.filters).to.have.length(3);
|
||||
expect(appState.filters).to.have.length(5);
|
||||
});
|
||||
|
||||
it('should swap the filters in both states', function () {
|
||||
const appSample = _.sample(appState.filters);
|
||||
const globalSample = _.sample(globalState.filters);
|
||||
|
||||
queryFilter.pinAll();
|
||||
expect(globalState.filters).to.have.length(5);
|
||||
expect(appState.filters).to.have.length(3);
|
||||
|
||||
expect(globalState.filters).to.contain(appSample);
|
||||
expect(appState.filters).to.contain(globalSample);
|
||||
});
|
||||
|
||||
it('should move all filters to globalState', function () {
|
||||
queryFilter.pinAll(true);
|
||||
expect(globalState.filters).to.have.length(8);
|
||||
expect(appState.filters).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should move all filters to appState', function () {
|
||||
queryFilter.pinAll(false);
|
||||
expect(globalState.filters).to.have.length(0);
|
||||
expect(appState.filters).to.have.length(8);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,170 +0,0 @@
|
|||
/*
|
||||
* 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 sinon from 'sinon';
|
||||
import expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
import MockState from 'fixtures/mock_state';
|
||||
import { FilterBarQueryFilterProvider } from '../query_filter';
|
||||
|
||||
describe('remove filters', function () {
|
||||
let filters;
|
||||
let queryFilter;
|
||||
let $rootScope;
|
||||
let appState;
|
||||
let globalState;
|
||||
|
||||
beforeEach(ngMock.module(
|
||||
'kibana',
|
||||
'kibana/courier',
|
||||
'kibana/global_state',
|
||||
function ($provide) {
|
||||
$provide.service('indexPatterns', require('fixtures/mock_index_patterns'));
|
||||
|
||||
appState = new MockState({ filters: [] });
|
||||
$provide.service('getAppState', function () {
|
||||
return function () { return appState; };
|
||||
});
|
||||
|
||||
globalState = new MockState({ filters: [] });
|
||||
$provide.service('globalState', function () {
|
||||
return globalState;
|
||||
});
|
||||
}
|
||||
));
|
||||
|
||||
beforeEach(ngMock.inject(function (_$rootScope_, Private) {
|
||||
$rootScope = _$rootScope_;
|
||||
queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
filters = [
|
||||
{
|
||||
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { '_type': { query: 'nginx', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
}
|
||||
];
|
||||
}));
|
||||
|
||||
describe('removing a filter', function () {
|
||||
it('should remove the filter from appState', function () {
|
||||
appState.filters = filters;
|
||||
expect(appState.filters).to.have.length(3);
|
||||
queryFilter.removeFilter(filters[0]);
|
||||
expect(appState.filters).to.have.length(2);
|
||||
});
|
||||
|
||||
it('should remove the filter from globalState', function () {
|
||||
globalState.filters = filters;
|
||||
expect(globalState.filters).to.have.length(3);
|
||||
queryFilter.removeFilter(filters[0]);
|
||||
expect(globalState.filters).to.have.length(2);
|
||||
});
|
||||
|
||||
it('should fire the update and fetch events', function () {
|
||||
const updateStub = sinon.stub();
|
||||
const fetchStub = sinon.stub();
|
||||
|
||||
queryFilter.getUpdates$().subscribe({
|
||||
next: updateStub,
|
||||
});
|
||||
|
||||
queryFilter.getFetches$().subscribe({
|
||||
next: fetchStub,
|
||||
});
|
||||
|
||||
appState.filters = filters;
|
||||
$rootScope.$digest();
|
||||
|
||||
queryFilter.removeFilter(filters[0]);
|
||||
$rootScope.$digest();
|
||||
|
||||
// this time, events should be emitted
|
||||
expect(fetchStub.called);
|
||||
expect(updateStub.called);
|
||||
});
|
||||
|
||||
it('should remove matching filters', function () {
|
||||
globalState.filters.push(filters[0]);
|
||||
globalState.filters.push(filters[1]);
|
||||
appState.filters.push(filters[2]);
|
||||
$rootScope.$digest();
|
||||
|
||||
queryFilter.removeFilter(filters[0]);
|
||||
$rootScope.$digest();
|
||||
expect(globalState.filters).to.have.length(1);
|
||||
expect(appState.filters).to.have.length(1);
|
||||
});
|
||||
|
||||
it('should remove matching filters by comparison', function () {
|
||||
globalState.filters.push(filters[0]);
|
||||
globalState.filters.push(filters[1]);
|
||||
appState.filters.push(filters[2]);
|
||||
$rootScope.$digest();
|
||||
|
||||
queryFilter.removeFilter(_.cloneDeep(filters[0]));
|
||||
$rootScope.$digest();
|
||||
expect(globalState.filters).to.have.length(1);
|
||||
expect(appState.filters).to.have.length(1);
|
||||
|
||||
queryFilter.removeFilter(_.cloneDeep(filters[2]));
|
||||
$rootScope.$digest();
|
||||
expect(globalState.filters).to.have.length(1);
|
||||
expect(appState.filters).to.have.length(0);
|
||||
});
|
||||
|
||||
it('should do nothing with a non-matching filter', function () {
|
||||
globalState.filters.push(filters[0]);
|
||||
globalState.filters.push(filters[1]);
|
||||
appState.filters.push(filters[2]);
|
||||
$rootScope.$digest();
|
||||
|
||||
const missedFilter = _.cloneDeep(filters[0]);
|
||||
missedFilter.meta = {
|
||||
negate: !filters[0].meta.negate
|
||||
};
|
||||
|
||||
queryFilter.removeFilter(missedFilter);
|
||||
$rootScope.$digest();
|
||||
expect(globalState.filters).to.have.length(2);
|
||||
expect(appState.filters).to.have.length(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulk removal', function () {
|
||||
it('should remove all the filters from both states', function () {
|
||||
globalState.filters.push(filters[0]);
|
||||
globalState.filters.push(filters[1]);
|
||||
appState.filters.push(filters[2]);
|
||||
expect(globalState.filters).to.have.length(2);
|
||||
expect(appState.filters).to.have.length(1);
|
||||
|
||||
queryFilter.removeAll();
|
||||
expect(globalState.filters).to.have.length(0);
|
||||
expect(appState.filters).to.have.length(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,204 +0,0 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
import sinon from 'sinon';
|
||||
import MockState from 'fixtures/mock_state';
|
||||
import { FilterBarQueryFilterProvider } from '../query_filter';
|
||||
|
||||
describe('toggle filters', function () {
|
||||
let filters;
|
||||
let queryFilter;
|
||||
let $rootScope;
|
||||
let appState;
|
||||
let globalState;
|
||||
|
||||
beforeEach(ngMock.module(
|
||||
'kibana',
|
||||
'kibana/courier',
|
||||
'kibana/global_state',
|
||||
function ($provide) {
|
||||
$provide.service('indexPatterns', require('fixtures/mock_index_patterns'));
|
||||
|
||||
appState = new MockState({ filters: [] });
|
||||
$provide.service('getAppState', function () {
|
||||
return function () { return appState; };
|
||||
});
|
||||
|
||||
globalState = new MockState({ filters: [] });
|
||||
$provide.service('globalState', function () {
|
||||
return globalState;
|
||||
});
|
||||
}
|
||||
));
|
||||
|
||||
beforeEach(ngMock.inject(function (_$rootScope_, Private) {
|
||||
$rootScope = _$rootScope_;
|
||||
queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
filters = [
|
||||
{
|
||||
query: { match: { extension: { query: 'jpg', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { '@tags': { query: 'info', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
},
|
||||
{
|
||||
query: { match: { '_type': { query: 'nginx', type: 'phrase' } } },
|
||||
meta: { negate: false, disabled: false }
|
||||
}
|
||||
];
|
||||
}));
|
||||
|
||||
describe('toggling a filter', function () {
|
||||
it('should toggle the disabled property in appState', function () {
|
||||
_.each(filters, function (filter) {
|
||||
expect(filter.meta.disabled).to.be(false);
|
||||
appState.filters.push(filter);
|
||||
});
|
||||
|
||||
queryFilter.toggleFilter(filters[1]);
|
||||
expect(appState.filters[1].meta.disabled).to.be(true);
|
||||
});
|
||||
|
||||
it('should toggle the disabled property in globalState', function () {
|
||||
_.each(filters, function (filter) {
|
||||
expect(filter.meta.disabled).to.be(false);
|
||||
globalState.filters.push(filter);
|
||||
});
|
||||
|
||||
queryFilter.toggleFilter(filters[1]);
|
||||
expect(globalState.filters[1].meta.disabled).to.be(true);
|
||||
});
|
||||
|
||||
it('should fire the update and fetch events', function () {
|
||||
const updateStub = sinon.stub();
|
||||
const fetchStub = sinon.stub();
|
||||
|
||||
queryFilter.getUpdates$().subscribe({
|
||||
next: updateStub,
|
||||
});
|
||||
|
||||
queryFilter.getFetches$().subscribe({
|
||||
next: fetchStub,
|
||||
});
|
||||
|
||||
appState.filters = filters;
|
||||
$rootScope.$digest();
|
||||
|
||||
queryFilter.toggleFilter(filters[1]);
|
||||
$rootScope.$digest();
|
||||
|
||||
// this time, events should be emitted
|
||||
expect(fetchStub.called);
|
||||
expect(updateStub.called);
|
||||
});
|
||||
|
||||
it('should always enable the filter', function () {
|
||||
appState.filters = filters.map(function (filter) {
|
||||
filter.meta.disabled = true;
|
||||
return filter;
|
||||
});
|
||||
|
||||
expect(appState.filters[1].meta.disabled).to.be(true);
|
||||
queryFilter.toggleFilter(filters[1], false);
|
||||
expect(appState.filters[1].meta.disabled).to.be(false);
|
||||
queryFilter.toggleFilter(filters[1], false);
|
||||
expect(appState.filters[1].meta.disabled).to.be(false);
|
||||
});
|
||||
|
||||
it('should always disable the filter', function () {
|
||||
globalState.filters = filters;
|
||||
|
||||
expect(globalState.filters[1].meta.disabled).to.be(false);
|
||||
queryFilter.toggleFilter(filters[1], true);
|
||||
expect(globalState.filters[1].meta.disabled).to.be(true);
|
||||
queryFilter.toggleFilter(filters[1], true);
|
||||
expect(globalState.filters[1].meta.disabled).to.be(true);
|
||||
});
|
||||
|
||||
it('should work without appState', function () {
|
||||
appState = undefined;
|
||||
globalState.filters = filters;
|
||||
|
||||
expect(globalState.filters[1].meta.disabled).to.be(false);
|
||||
expect(queryFilter.getFilters()).to.have.length(3);
|
||||
queryFilter.toggleFilter(filters[1]);
|
||||
expect(globalState.filters[1].meta.disabled).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulk toggling', function () {
|
||||
beforeEach(function () {
|
||||
appState.filters = filters;
|
||||
globalState.filters = _.map(_.cloneDeep(filters), function (filter) {
|
||||
filter.meta.disabled = true;
|
||||
return filter;
|
||||
});
|
||||
});
|
||||
|
||||
it('should swap the enabled state for all filters', function () {
|
||||
queryFilter.toggleAll();
|
||||
_.each(appState.filters, function (filter) {
|
||||
expect(filter.meta.disabled).to.be(true);
|
||||
});
|
||||
_.each(globalState.filters, function (filter) {
|
||||
expect(filter.meta.disabled).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should enable all filters', function () {
|
||||
queryFilter.toggleAll(true);
|
||||
_.each(appState.filters, function (filter) {
|
||||
expect(filter.meta.disabled).to.be(true);
|
||||
});
|
||||
_.each(globalState.filters, function (filter) {
|
||||
expect(filter.meta.disabled).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable all filters', function () {
|
||||
queryFilter.toggleAll(false);
|
||||
_.each(appState.filters, function (filter) {
|
||||
expect(filter.meta.disabled).to.be(false);
|
||||
});
|
||||
_.each(globalState.filters, function (filter) {
|
||||
expect(filter.meta.disabled).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work without appState', function () {
|
||||
appState = undefined;
|
||||
globalState.filters = filters;
|
||||
|
||||
_.each(globalState.filters, function (filter) {
|
||||
expect(filter.meta.disabled).to.be(false);
|
||||
});
|
||||
|
||||
queryFilter.toggleAll();
|
||||
|
||||
_.each(globalState.filters, function (filter) {
|
||||
expect(filter.meta.disabled).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -22,11 +22,11 @@ import sinon from 'sinon';
|
|||
import MockState from 'fixtures/mock_state';
|
||||
import expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
import { FilterManagerProvider } from '..';
|
||||
import { getFilterGenerator } from '..';
|
||||
import { FilterBarQueryFilterProvider } from '../../filter_manager/query_filter';
|
||||
import { getPhraseScript } from '@kbn/es-query';
|
||||
let queryFilter;
|
||||
let filterManager;
|
||||
let filterGen;
|
||||
let appState;
|
||||
|
||||
function checkAddFilters(length, comps, idx) {
|
||||
|
@ -56,10 +56,10 @@ describe('Filter Manager', function () {
|
|||
));
|
||||
|
||||
beforeEach(ngMock.inject(function (_$rootScope_, Private) {
|
||||
filterManager = Private(FilterManagerProvider);
|
||||
|
||||
// mock required queryFilter methods, used in the manager
|
||||
queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
filterGen = getFilterGenerator(queryFilter);
|
||||
sinon.stub(queryFilter, 'getAppFilters').callsFake(() => appState.filters);
|
||||
sinon.stub(queryFilter, 'addFilters').callsFake((filters) => {
|
||||
if (!Array.isArray(filters)) filters = [filters];
|
||||
|
@ -71,11 +71,11 @@ describe('Filter Manager', function () {
|
|||
}));
|
||||
|
||||
it('should have an `add` function', function () {
|
||||
expect(filterManager.add).to.be.a(Function);
|
||||
expect(filterGen.add).to.be.a(Function);
|
||||
});
|
||||
|
||||
it('should add a filter', function () {
|
||||
filterManager.add('myField', 1, '+', 'myIndex');
|
||||
filterGen.add('myField', 1, '+', 'myIndex');
|
||||
expect(queryFilter.addFilters.callCount).to.be(1);
|
||||
checkAddFilters(1, [{
|
||||
meta: { index: 'myIndex', negate: false },
|
||||
|
@ -84,7 +84,7 @@ describe('Filter Manager', function () {
|
|||
});
|
||||
|
||||
it('should add multiple filters if passed an array of values', function () {
|
||||
filterManager.add('myField', [1, 2, 3], '+', 'myIndex');
|
||||
filterGen.add('myField', [1, 2, 3], '+', 'myIndex');
|
||||
expect(queryFilter.addFilters.callCount).to.be(1);
|
||||
checkAddFilters(3, [{
|
||||
meta: { index: 'myIndex', negate: false },
|
||||
|
@ -99,7 +99,7 @@ describe('Filter Manager', function () {
|
|||
});
|
||||
|
||||
it('should add an exists filter if _exists_ is used as the field', function () {
|
||||
filterManager.add('_exists_', 'myField', '+', 'myIndex');
|
||||
filterGen.add('_exists_', 'myField', '+', 'myIndex');
|
||||
checkAddFilters(1, [{
|
||||
meta: { index: 'myIndex', negate: false },
|
||||
exists: { field: 'myField' }
|
||||
|
@ -107,7 +107,7 @@ describe('Filter Manager', function () {
|
|||
});
|
||||
|
||||
it('should negate existing filter instead of added a conflicting filter', function () {
|
||||
filterManager.add('myField', 1, '+', 'myIndex');
|
||||
filterGen.add('myField', 1, '+', 'myIndex');
|
||||
checkAddFilters(1, [{
|
||||
meta: { index: 'myIndex', negate: false },
|
||||
query: { match: { myField: { query: 1, type: 'phrase' } } }
|
||||
|
@ -115,30 +115,30 @@ describe('Filter Manager', function () {
|
|||
expect(appState.filters).to.have.length(1);
|
||||
|
||||
// NOTE: negating exists filters also forces disabled to false
|
||||
filterManager.add('myField', 1, '-', 'myIndex');
|
||||
filterGen.add('myField', 1, '-', 'myIndex');
|
||||
checkAddFilters(0, null, 1);
|
||||
expect(appState.filters).to.have.length(1);
|
||||
|
||||
filterManager.add('_exists_', 'myField', '+', 'myIndex');
|
||||
filterGen.add('_exists_', 'myField', '+', 'myIndex');
|
||||
checkAddFilters(1, [{
|
||||
meta: { index: 'myIndex', negate: false },
|
||||
exists: { field: 'myField' }
|
||||
}], 2);
|
||||
expect(appState.filters).to.have.length(2);
|
||||
|
||||
filterManager.add('_exists_', 'myField', '-', 'myIndex');
|
||||
filterGen.add('_exists_', 'myField', '-', 'myIndex');
|
||||
checkAddFilters(0, null, 3);
|
||||
expect(appState.filters).to.have.length(2);
|
||||
|
||||
const scriptedField = { name: 'scriptedField', scripted: true, script: 1, lang: 'painless' };
|
||||
filterManager.add(scriptedField, 1, '+', 'myIndex');
|
||||
filterGen.add(scriptedField, 1, '+', 'myIndex');
|
||||
checkAddFilters(1, [{
|
||||
meta: { index: 'myIndex', negate: false, field: 'scriptedField' },
|
||||
script: getPhraseScript(scriptedField, 1)
|
||||
}], 4);
|
||||
expect(appState.filters).to.have.length(3);
|
||||
|
||||
filterManager.add(scriptedField, 1, '-', 'myIndex');
|
||||
filterGen.add(scriptedField, 1, '-', 'myIndex');
|
||||
checkAddFilters(0, null, 5);
|
||||
expect(appState.filters).to.have.length(3);
|
||||
});
|
||||
|
@ -152,7 +152,7 @@ describe('Filter Manager', function () {
|
|||
expect(appState.filters.length).to.be(1);
|
||||
expect(appState.filters[0].meta.disabled).to.be(true);
|
||||
|
||||
filterManager.add('myField', 1, '+', 'myIndex');
|
||||
filterGen.add('myField', 1, '+', 'myIndex');
|
||||
expect(appState.filters.length).to.be(1);
|
||||
expect(appState.filters[0].meta.disabled).to.be(false);
|
||||
});
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import ngMock from 'ng_mock';
|
||||
import './_get_filters';
|
||||
import './_add_filters';
|
||||
import './_remove_filters';
|
||||
import './_toggle_filters';
|
||||
import './_invert_filters';
|
||||
import './_pin_filters';
|
||||
import { FilterBarQueryFilterProvider } from '../query_filter';
|
||||
let queryFilter;
|
||||
|
||||
describe('Query Filter', function () {
|
||||
describe('Module', function () {
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject(function (_$rootScope_, Private) {
|
||||
queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
}));
|
||||
|
||||
describe('module instance', function () {
|
||||
it('should use observables', function () {
|
||||
expect(queryFilter.getUpdates$).to.be.a('function');
|
||||
expect(queryFilter.getFetches$).to.be.a('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('module methods', function () {
|
||||
it('should have methods for getting filters', function () {
|
||||
expect(queryFilter.getFilters).to.be.a('function');
|
||||
expect(queryFilter.getAppFilters).to.be.a('function');
|
||||
expect(queryFilter.getGlobalFilters).to.be.a('function');
|
||||
});
|
||||
|
||||
it('should have methods for modifying filters', function () {
|
||||
expect(queryFilter.addFilters).to.be.a('function');
|
||||
expect(queryFilter.toggleFilter).to.be.a('function');
|
||||
expect(queryFilter.toggleAll).to.be.a('function');
|
||||
expect(queryFilter.removeFilter).to.be.a('function');
|
||||
expect(queryFilter.removeAll).to.be.a('function');
|
||||
expect(queryFilter.invertFilter).to.be.a('function');
|
||||
expect(queryFilter.invertAll).to.be.a('function');
|
||||
expect(queryFilter.pinFilter).to.be.a('function');
|
||||
expect(queryFilter.pinAll).to.be.a('function');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Actions', function () {
|
||||
});
|
||||
});
|
|
@ -18,15 +18,13 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { FilterBarQueryFilterProvider } from '../filter_manager/query_filter';
|
||||
import { getPhraseScript } from '@kbn/es-query';
|
||||
|
||||
// Adds a filter to a passed state
|
||||
export function FilterManagerProvider(Private) {
|
||||
const queryFilter = Private(FilterBarQueryFilterProvider);
|
||||
const filterManager = {};
|
||||
export function getFilterGenerator(queryFilter) {
|
||||
const filterGen = {};
|
||||
|
||||
filterManager.generate = (field, values, operation, index) => {
|
||||
filterGen.generate = (field, values, operation, index) => {
|
||||
values = Array.isArray(values) ? values : [values];
|
||||
const fieldName = _.isObject(field) ? field.name : field;
|
||||
const filters = _.flatten([queryFilter.getAppFilters()]);
|
||||
|
@ -90,10 +88,10 @@ export function FilterManagerProvider(Private) {
|
|||
return newFilters;
|
||||
};
|
||||
|
||||
filterManager.add = function (field, values, operation, index) {
|
||||
filterGen.add = function (field, values, operation, index) {
|
||||
const newFilters = this.generate(field, values, operation, index);
|
||||
return queryFilter.addFilters(newFilters);
|
||||
};
|
||||
|
||||
return filterManager;
|
||||
return filterGen;
|
||||
}
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { FilterManagerProvider } from './filter_manager';
|
||||
export { getFilterGenerator } from './filter_generator';
|
||||
|
|
|
@ -17,400 +17,32 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { Subject } from 'rxjs';
|
||||
import { FilterStateManager } from 'plugins/data';
|
||||
|
||||
import { onlyDisabled } from './lib/only_disabled';
|
||||
import { onlyStateChanged } from './lib/only_state_changed';
|
||||
import { uniqFilters } from './lib/uniq_filters';
|
||||
import { compareFilters } from './lib/compare_filters';
|
||||
import { mapAndFlattenFilters } from './lib/map_and_flatten_filters';
|
||||
import { extractTimeFilter } from './lib/extract_time_filter';
|
||||
import { changeTimeFilter } from './lib/change_time_filter';
|
||||
export function FilterBarQueryFilterProvider(getAppState, globalState) {
|
||||
// TODO: this is imported here to avoid circular imports.
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { data } = require('plugins/data/setup');
|
||||
const filterManager = data.filter.filterManager;
|
||||
const filterStateManager = new FilterStateManager(globalState, getAppState, filterManager);
|
||||
|
||||
import { npSetup } from 'ui/new_platform';
|
||||
|
||||
export function FilterBarQueryFilterProvider(Promise, indexPatterns, $rootScope, getAppState, globalState) {
|
||||
const queryFilter = {};
|
||||
const { uiSettings } = npSetup.core;
|
||||
queryFilter.getUpdates$ = filterManager.getUpdates$.bind(filterManager);
|
||||
queryFilter.getFetches$ = filterManager.getFetches$.bind(filterManager);
|
||||
queryFilter.getFilters = filterManager.getFilters.bind(filterManager);
|
||||
queryFilter.getAppFilters = filterManager.getAppFilters.bind(filterManager);
|
||||
queryFilter.getGlobalFilters = filterManager.getGlobalFilters.bind(filterManager);
|
||||
queryFilter.removeFilter = filterManager.removeFilter.bind(filterManager);
|
||||
queryFilter.invertFilter = filterManager.invertFilter.bind(filterManager);
|
||||
queryFilter.addFilters = filterManager.addFilters.bind(filterManager);
|
||||
queryFilter.setFilters = filterManager.setFilters.bind(filterManager);
|
||||
queryFilter.addFiltersAndChangeTimeFilter = filterManager.addFiltersAndChangeTimeFilter.bind(filterManager);
|
||||
queryFilter.removeAll = filterManager.removeAll.bind(filterManager);
|
||||
|
||||
const update$ = new Subject();
|
||||
const fetch$ = new Subject();
|
||||
|
||||
queryFilter.getUpdates$ = function () {
|
||||
return update$.asObservable();
|
||||
queryFilter.destroy = () => {
|
||||
filterManager.destroy();
|
||||
filterStateManager.destroy();
|
||||
};
|
||||
|
||||
queryFilter.getFetches$ = function () {
|
||||
return fetch$.asObservable();
|
||||
};
|
||||
|
||||
queryFilter.getFilters = function () {
|
||||
const compareOptions = { disabled: true, negate: true };
|
||||
const appFilters = queryFilter.getAppFilters();
|
||||
const globalFilters = queryFilter.getGlobalFilters();
|
||||
|
||||
return uniqFilters(globalFilters.concat(appFilters), compareOptions);
|
||||
};
|
||||
|
||||
queryFilter.getAppFilters = function () {
|
||||
const appState = getAppState();
|
||||
if (!appState || !appState.filters) return [];
|
||||
|
||||
// Work around for https://github.com/elastic/kibana/issues/5896
|
||||
appState.filters = validateStateFilters(appState);
|
||||
|
||||
return (appState.filters) ? _.map(appState.filters, appendStoreType('appState')) : [];
|
||||
};
|
||||
|
||||
queryFilter.getGlobalFilters = function () {
|
||||
if (!globalState.filters) return [];
|
||||
|
||||
// Work around for https://github.com/elastic/kibana/issues/5896
|
||||
globalState.filters = validateStateFilters(globalState);
|
||||
|
||||
return _.map(globalState.filters, appendStoreType('globalState'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds new filters to the scope and state
|
||||
* @param {object|array} filters Filter(s) to add
|
||||
* @param {bool} global Whether the filter should be added to global state
|
||||
* @returns {Promise} filter map promise
|
||||
*/
|
||||
queryFilter.addFilters = function (filters, addToGlobalState) {
|
||||
if (addToGlobalState === undefined) {
|
||||
addToGlobalState = uiSettings.get('filters:pinnedByDefault');
|
||||
}
|
||||
|
||||
// Determine the state for the new filter (whether to pass the filter through other apps or not)
|
||||
const appState = getAppState();
|
||||
const filterState = addToGlobalState ? globalState : appState;
|
||||
|
||||
if (!Array.isArray(filters)) {
|
||||
filters = [filters];
|
||||
}
|
||||
|
||||
return Promise.resolve(mapAndFlattenFilters(indexPatterns, filters))
|
||||
.then(function (filters) {
|
||||
if (!filterState.filters) {
|
||||
filterState.filters = [];
|
||||
}
|
||||
|
||||
filterState.filters = filterState.filters.concat(filters);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the filter from the proper state
|
||||
* @param {object} matchFilter The filter to remove
|
||||
*/
|
||||
queryFilter.removeFilter = function (matchFilter) {
|
||||
const appState = getAppState();
|
||||
const filter = _.omit(matchFilter, ['$$hashKey']);
|
||||
let state;
|
||||
let index;
|
||||
|
||||
// check for filter in appState
|
||||
if (appState) {
|
||||
index = _.findIndex(appState.filters, filter);
|
||||
if (index !== -1) state = appState;
|
||||
}
|
||||
|
||||
// if not found, check for filter in globalState
|
||||
if (!state) {
|
||||
index = _.findIndex(globalState.filters, filter);
|
||||
if (index !== -1) state = globalState;
|
||||
else return; // not found in either state, do nothing
|
||||
}
|
||||
|
||||
state.filters.splice(index, 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes all filters
|
||||
*/
|
||||
queryFilter.removeAll = function () {
|
||||
const appState = getAppState();
|
||||
appState.filters = [];
|
||||
globalState.filters = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the filter between enabled/disabled.
|
||||
* @param {object} filter The filter to toggle
|
||||
& @param {boolean} force Disabled true/false
|
||||
* @returns {object} updated filter
|
||||
*/
|
||||
queryFilter.toggleFilter = function (filter, force) {
|
||||
// Toggle the disabled flag
|
||||
const disabled = _.isUndefined(force) ? !filter.meta.disabled : !!force;
|
||||
filter.meta.disabled = disabled;
|
||||
return filter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Disables all filters
|
||||
* @params {boolean} force Disable/enable all filters
|
||||
*/
|
||||
queryFilter.toggleAll = function (force) {
|
||||
function doToggle(filter) {
|
||||
queryFilter.toggleFilter(filter, force);
|
||||
}
|
||||
|
||||
executeOnFilters(doToggle);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Inverts the negate value on the filter
|
||||
* @param {object} filter The filter to toggle
|
||||
* @returns {object} updated filter
|
||||
*/
|
||||
queryFilter.invertFilter = function (filter) {
|
||||
// Toggle the negate meta state
|
||||
filter.meta.negate = !filter.meta.negate;
|
||||
return filter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inverts all filters
|
||||
* @returns {object} Resulting updated filter list
|
||||
*/
|
||||
queryFilter.invertAll = function () {
|
||||
executeOnFilters(queryFilter.invertFilter);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Pins the filter to the global state
|
||||
* @param {object} filter The filter to pin
|
||||
* @param {boolean} force pinned state
|
||||
* @returns {object} updated filter
|
||||
*/
|
||||
queryFilter.pinFilter = function (filter, force) {
|
||||
const appState = getAppState();
|
||||
if (!appState) return filter;
|
||||
|
||||
// ensure that both states have a filters property
|
||||
if (!Array.isArray(globalState.filters)) globalState.filters = [];
|
||||
if (!Array.isArray(appState.filters)) appState.filters = [];
|
||||
|
||||
const appIndex = _.findIndex(appState.filters, appFilter => _.isEqual(appFilter, filter));
|
||||
|
||||
if (appIndex !== -1 && force !== false) {
|
||||
appState.filters.splice(appIndex, 1);
|
||||
globalState.filters.push(filter);
|
||||
} else {
|
||||
const globalIndex = _.findIndex(globalState.filters, globalFilter => _.isEqual(globalFilter, filter));
|
||||
|
||||
if (globalIndex === -1 || force === true) return filter;
|
||||
|
||||
globalState.filters.splice(globalIndex, 1);
|
||||
appState.filters.push(filter);
|
||||
}
|
||||
|
||||
return filter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pins all filters
|
||||
* @params {boolean} force Pin/Unpin all filters
|
||||
*/
|
||||
queryFilter.pinAll = function (force) {
|
||||
function pin(filter) {
|
||||
queryFilter.pinFilter(filter, force);
|
||||
}
|
||||
|
||||
executeOnFilters(pin);
|
||||
};
|
||||
|
||||
queryFilter.setFilters = filters => {
|
||||
return Promise.resolve(mapAndFlattenFilters(indexPatterns, filters))
|
||||
.then(mappedFilters => {
|
||||
const appState = getAppState();
|
||||
const [globalFilters, appFilters] = _.partition(mappedFilters, filter => {
|
||||
return filter.$state.store === 'globalState';
|
||||
});
|
||||
globalState.filters = globalFilters;
|
||||
if (appState) appState.filters = appFilters;
|
||||
});
|
||||
};
|
||||
|
||||
queryFilter.addFiltersAndChangeTimeFilter = async filters => {
|
||||
const timeFilter = await extractTimeFilter(indexPatterns, filters);
|
||||
if (timeFilter) changeTimeFilter(timeFilter);
|
||||
queryFilter.addFilters(filters.filter(filter => filter !== timeFilter));
|
||||
};
|
||||
|
||||
initWatchers();
|
||||
|
||||
return queryFilter;
|
||||
|
||||
/**
|
||||
* Rids filter list of null values and replaces state if any nulls are found
|
||||
*/
|
||||
function validateStateFilters(state) {
|
||||
const compacted = _.compact(state.filters);
|
||||
if (state.filters.length !== compacted.length) {
|
||||
state.filters = compacted;
|
||||
state.replace();
|
||||
}
|
||||
return state.filters;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Saves both app and global states, ensuring filters are persisted
|
||||
* @returns {object} Resulting filter list, app and global combined
|
||||
*/
|
||||
function saveState() {
|
||||
const appState = getAppState();
|
||||
if (appState) appState.save();
|
||||
globalState.save();
|
||||
}
|
||||
|
||||
function appendStoreType(type) {
|
||||
return function (filter) {
|
||||
filter.$state = {
|
||||
store: type
|
||||
};
|
||||
return filter;
|
||||
};
|
||||
}
|
||||
|
||||
// helper to run a function on all filters in all states
|
||||
function executeOnFilters(fn) {
|
||||
const appState = getAppState();
|
||||
let globalFilters = [];
|
||||
let appFilters = [];
|
||||
|
||||
if (globalState.filters) globalFilters = globalState.filters;
|
||||
if (appState && appState.filters) appFilters = appState.filters;
|
||||
|
||||
globalFilters.concat(appFilters).forEach(fn);
|
||||
}
|
||||
|
||||
function mergeStateFilters(gFilters, aFilters, compareOptions) {
|
||||
// ensure we don't mutate the filters passed in
|
||||
const globalFilters = gFilters ? _.cloneDeep(gFilters) : [];
|
||||
const appFilters = aFilters ? _.cloneDeep(aFilters) : [];
|
||||
|
||||
// existing globalFilters should be mutated by appFilters
|
||||
_.each(appFilters, function (filter, i) {
|
||||
const match = _.find(globalFilters, function (globalFilter) {
|
||||
return compareFilters(globalFilter, filter, compareOptions);
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// Reverse the order of globalFilters and appFilters, since uniqFilters
|
||||
// will throw out duplicates from the back of the array, but we want
|
||||
// newer filters to overwrite previously created filters.
|
||||
globalFilters.reverse();
|
||||
appFilters.reverse();
|
||||
|
||||
return [
|
||||
// Reverse filters after uniq again, so they are still in the order, they
|
||||
// were before updating them
|
||||
uniqFilters(globalFilters).reverse(),
|
||||
uniqFilters(appFilters).reverse()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes state watchers that use the event emitter
|
||||
* @returns {void}
|
||||
*/
|
||||
function initWatchers() {
|
||||
let removeAppStateWatchers;
|
||||
|
||||
$rootScope.$watch(getAppState, function () {
|
||||
removeAppStateWatchers && removeAppStateWatchers();
|
||||
removeAppStateWatchers = initAppStateWatchers();
|
||||
});
|
||||
|
||||
function initAppStateWatchers() {
|
||||
// multi watch on the app and global states
|
||||
const stateWatchers = [{
|
||||
fn: $rootScope.$watch,
|
||||
deep: true,
|
||||
get: queryFilter.getGlobalFilters
|
||||
}, {
|
||||
fn: $rootScope.$watch,
|
||||
deep: true,
|
||||
get: queryFilter.getAppFilters
|
||||
}];
|
||||
|
||||
// when states change, use event emitter to trigger updates and fetches
|
||||
return $rootScope.$watchMulti(stateWatchers, function (next, prev) {
|
||||
// prevent execution on watcher instantiation
|
||||
if (_.isEqual(next, prev)) return;
|
||||
|
||||
let doUpdate = false;
|
||||
let doFetch = false;
|
||||
|
||||
// reconcile filter in global and app states
|
||||
const filters = mergeStateFilters(next[0], next[1]);
|
||||
const [globalFilters, appFilters] = filters;
|
||||
const appState = getAppState();
|
||||
|
||||
// save the state, as it may have updated
|
||||
const globalChanged = !_.isEqual(next[0], globalFilters);
|
||||
const appChanged = !_.isEqual(next[1], appFilters);
|
||||
|
||||
// the filters were changed, apply to state (re-triggers this watcher)
|
||||
if (globalChanged || appChanged) {
|
||||
globalState.filters = globalFilters;
|
||||
if (appState) appState.filters = appFilters;
|
||||
return;
|
||||
}
|
||||
|
||||
// check for actions, bail if we're done
|
||||
getActions();
|
||||
if (doUpdate) {
|
||||
// save states and emit the required events
|
||||
saveState();
|
||||
update$.next();
|
||||
|
||||
if (doFetch) {
|
||||
fetch$.next();
|
||||
}
|
||||
}
|
||||
|
||||
// iterate over each state type, checking for changes
|
||||
function getActions() {
|
||||
let newFilters = [];
|
||||
let oldFilters = [];
|
||||
|
||||
stateWatchers.forEach(function (watcher, i) {
|
||||
const nextVal = next[i];
|
||||
const prevVal = prev[i];
|
||||
newFilters = newFilters.concat(nextVal);
|
||||
oldFilters = oldFilters.concat(prevVal);
|
||||
|
||||
// no update or fetch if there was no change
|
||||
if (nextVal === prevVal) return;
|
||||
|
||||
if (nextVal) doUpdate = true;
|
||||
|
||||
// don't trigger fetch when only disabled filters
|
||||
if (!onlyDisabled(nextVal, prevVal)) doFetch = true;
|
||||
});
|
||||
|
||||
// make sure change wasn't only a state move
|
||||
// checking length first is an optimization
|
||||
if (doFetch && newFilters.length === oldFilters.length) {
|
||||
if (onlyStateChanged(newFilters, oldFilters)) doFetch = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,11 +18,10 @@
|
|||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { pushFilterBarFilters } from '../../filter_manager/push_filters';
|
||||
import { pushFilterBarFilters } from '../push_filters';
|
||||
import { onBrushEvent } from './brush_event';
|
||||
import { uniqFilters } from '../../filter_manager/lib/uniq_filters';
|
||||
import { uniqFilters } from '../../../../core_plugins/data/public';
|
||||
import { toggleFilterNegated } from '@kbn/es-query';
|
||||
|
||||
/**
|
||||
* For terms aggregations on `__other__` buckets, this assembles a list of applicable filter
|
||||
* terms based on a specific cell in the tabified data.
|
||||
|
|
|
@ -26,6 +26,26 @@ import { VisResponseData } from './types';
|
|||
import { Inspector } from '../../inspector';
|
||||
import { EmbeddedVisualizeHandler } from './embedded_visualize_handler';
|
||||
|
||||
jest.mock('ui/new_platform', () => ({
|
||||
npStart: {
|
||||
core: {
|
||||
i18n: {
|
||||
Context: {},
|
||||
},
|
||||
chrome: {
|
||||
recentlyAccessed: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
npSetup: {
|
||||
core: {
|
||||
uiSettings: {
|
||||
get: () => true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('EmbeddedVisualizeHandler', () => {
|
||||
let handler: any;
|
||||
let div: HTMLElement;
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles input_control_vis function 1`] = `"input_control_vis visConfig='{\\"some\\":\\"nested\\",\\"data\\":{\\"here\\":true}}' "`;
|
||||
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles markdown function 1`] = `"markdownvis '## hello _markdown_' fontSize=12 openLinksInNewTab=true "`;
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles markdown function 1`] = `"markdownvis '## hello _markdown_' font={font size=12} openLinksInNewTab=true "`;
|
||||
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metric function with buckets 1`] = `"kibana_metric visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metrics\\":[0,1],\\"bucket\\":2}}' "`;
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metric function with buckets 1`] = `"metricvis metric={visdimension 0 } metric={visdimension 1 } "`;
|
||||
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metric function without buckets 1`] = `"kibana_metric visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metrics\\":[0,1]}}' "`;
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metric function without buckets 1`] = `"metricvis metric={visdimension 0 } metric={visdimension 1 } "`;
|
||||
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metrics/tsvb function 1`] = `"tsvb params='{\\"foo\\":\\"bar\\"}' uiState='{}' "`;
|
||||
|
||||
|
@ -28,7 +28,7 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct
|
|||
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tagcloud function with boolean param showLabel 1`] = `"tagcloud metric={visdimension 0} showLabel=false "`;
|
||||
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tagcloud function with buckets 1`] = `"tagcloud metric={visdimension 0} bucket={visdimension 1 } "`;
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tagcloud function with buckets 1`] = `"tagcloud metric={visdimension 0} bucket={visdimension 1 } "`;
|
||||
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tagcloud function without buckets 1`] = `"tagcloud metric={visdimension 0} "`;
|
||||
|
||||
|
@ -36,6 +36,6 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct
|
|||
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles timelion function 1`] = `"timelion_vis expression='foo' interval='bar' "`;
|
||||
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles undefined markdown function 1`] = `"markdownvis '' fontSize=12 openLinksInNewTab=true "`;
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles undefined markdown function 1`] = `"markdownvis '' font={font size=12} openLinksInNewTab=true "`;
|
||||
|
||||
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles vega function 1`] = `"vega spec='this is a test' "`;
|
||||
|
|
|
@ -157,15 +157,15 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
|
|||
describe('handles metric function', () => {
|
||||
const params = { metric: {} };
|
||||
it('without buckets', () => {
|
||||
const schemas = { metric: [0, 1] };
|
||||
const schemas = { metric: [{ accessor: 0 }, { accessor: 1 }] };
|
||||
const actual = buildPipelineVisFunction.metric({ params }, schemas);
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('with buckets', () => {
|
||||
const schemas = {
|
||||
metric: [0, 1],
|
||||
group: [2]
|
||||
metric: [{ accessor: 0 }, { accessor: 1 }],
|
||||
group: [{ accessor: 2 }]
|
||||
};
|
||||
const actual = buildPipelineVisFunction.metric({ params }, schemas);
|
||||
expect(actual).toMatchSnapshot();
|
||||
|
|
|
@ -193,17 +193,55 @@ export const getSchemas = (vis: Vis, timeRange?: any): Schemas => {
|
|||
};
|
||||
|
||||
export const prepareJson = (variable: string, data: object): string => {
|
||||
if (data === undefined) {
|
||||
return '';
|
||||
}
|
||||
return `${variable}='${JSON.stringify(data)
|
||||
.replace(/\\/g, `\\\\`)
|
||||
.replace(/'/g, `\\'`)}' `;
|
||||
};
|
||||
|
||||
export const prepareString = (variable: string, data: string): string => {
|
||||
export const escapeString = (data: string): string => {
|
||||
return data.replace(/\\/g, `\\\\`).replace(/'/g, `\\'`);
|
||||
};
|
||||
|
||||
export const prepareString = (variable: string, data?: string): string => {
|
||||
if (data === undefined) {
|
||||
return '';
|
||||
}
|
||||
return `${variable}='${escapeString(data)}' `;
|
||||
};
|
||||
|
||||
export const escapeString = (data: string): string => {
|
||||
return data.replace(/\\/g, `\\\\`).replace(/'/g, `\\'`);
|
||||
export const prepareValue = (variable: string, data: any, raw: boolean = false) => {
|
||||
if (data === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (raw) {
|
||||
return `${variable}=${data} `;
|
||||
}
|
||||
switch (typeof data) {
|
||||
case 'string':
|
||||
return prepareString(variable, data);
|
||||
case 'object':
|
||||
return prepareJson(variable, data);
|
||||
default:
|
||||
return `${variable}=${data} `;
|
||||
}
|
||||
};
|
||||
|
||||
export const prepareDimension = (variable: string, data: any) => {
|
||||
if (data === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let expr = `${variable}={visdimension ${data.accessor} `;
|
||||
if (data.format) {
|
||||
expr += prepareValue('format', data.format.id);
|
||||
expr += prepareJson('formatParams', data.format.params);
|
||||
}
|
||||
expr += '} ';
|
||||
|
||||
return expr;
|
||||
};
|
||||
|
||||
export const buildPipelineVisFunction: BuildPipelineVisFunction = {
|
||||
|
@ -231,12 +269,8 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = {
|
|||
escapedMarkdown = escapeString(markdown.toString());
|
||||
}
|
||||
let expr = `markdownvis '${escapedMarkdown}' `;
|
||||
if (fontSize) {
|
||||
expr += ` fontSize=${fontSize} `;
|
||||
}
|
||||
if (openLinksInNewTab) {
|
||||
expr += `openLinksInNewTab=${openLinksInNewTab} `;
|
||||
}
|
||||
expr += prepareValue('font', `{font size=${fontSize}}`, true);
|
||||
expr += prepareValue('openLinksInNewTab', openLinksInNewTab);
|
||||
return expr;
|
||||
},
|
||||
table: (visState, schemas) => {
|
||||
|
@ -247,41 +281,55 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = {
|
|||
return `kibana_table ${prepareJson('visConfig', visConfig)}`;
|
||||
},
|
||||
metric: (visState, schemas) => {
|
||||
const visConfig = {
|
||||
...visState.params,
|
||||
...buildVisConfig.metric(schemas),
|
||||
};
|
||||
return `kibana_metric ${prepareJson('visConfig', visConfig)}`;
|
||||
const {
|
||||
percentageMode,
|
||||
useRanges,
|
||||
colorSchema,
|
||||
metricColorMode,
|
||||
colorsRange,
|
||||
labels,
|
||||
invertColors,
|
||||
style,
|
||||
} = visState.params.metric;
|
||||
const { metrics, bucket } = buildVisConfig.metric(schemas).dimensions;
|
||||
|
||||
let expr = `metricvis `;
|
||||
expr += prepareValue('percentage', percentageMode);
|
||||
expr += prepareValue('colorScheme', colorSchema);
|
||||
expr += prepareValue('colorMode', metricColorMode);
|
||||
expr += prepareValue('useRanges', useRanges);
|
||||
expr += prepareValue('invertColors', invertColors);
|
||||
expr += prepareValue('showLabels', labels && labels.show);
|
||||
if (style) {
|
||||
expr += prepareValue('bgFill', style.bgFill);
|
||||
expr += prepareValue('font', `{font size=${style.fontSize}}`, true);
|
||||
expr += prepareValue('subText', style.subText);
|
||||
expr += prepareDimension('bucket', bucket);
|
||||
}
|
||||
|
||||
if (colorsRange) {
|
||||
colorsRange.forEach((range: any) => {
|
||||
expr += prepareValue('colorRange', `{range from=${range.from} to=${range.to}}`, true);
|
||||
});
|
||||
}
|
||||
|
||||
metrics.forEach((metric: SchemaConfig) => {
|
||||
expr += prepareDimension('metric', metric);
|
||||
});
|
||||
|
||||
return expr;
|
||||
},
|
||||
tagcloud: (visState, schemas) => {
|
||||
const { scale, orientation, minFontSize, maxFontSize, showLabel } = visState.params;
|
||||
const { metric, bucket } = buildVisConfig.tagcloud(schemas);
|
||||
let expr = `tagcloud metric={visdimension ${metric.accessor}} `;
|
||||
expr += prepareValue('scale', scale);
|
||||
expr += prepareValue('orientation', orientation);
|
||||
expr += prepareValue('minFontSize', minFontSize);
|
||||
expr += prepareValue('maxFontSize', maxFontSize);
|
||||
expr += prepareValue('showLabel', showLabel);
|
||||
expr += prepareDimension('bucket', bucket);
|
||||
|
||||
if (scale) {
|
||||
expr += `scale='${scale}' `;
|
||||
}
|
||||
if (orientation) {
|
||||
expr += `orientation='${orientation}' `;
|
||||
}
|
||||
if (minFontSize) {
|
||||
expr += `minFontSize=${minFontSize} `;
|
||||
}
|
||||
if (maxFontSize) {
|
||||
expr += `maxFontSize=${maxFontSize} `;
|
||||
}
|
||||
if (showLabel !== undefined) {
|
||||
expr += `showLabel=${showLabel} `;
|
||||
}
|
||||
|
||||
if (bucket) {
|
||||
expr += ` bucket={visdimension ${bucket.accessor} `;
|
||||
if (bucket.format) {
|
||||
expr += `format=${bucket.format.id} `;
|
||||
expr += prepareJson('formatParams', bucket.format.params);
|
||||
}
|
||||
expr += '} ';
|
||||
}
|
||||
return expr;
|
||||
},
|
||||
region_map: (visState, schemas) => {
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { TimeRange } from 'ui/timefilter/time_history';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { TimeRange } from 'ui/timefilter/time_history';
|
||||
import { Query } from 'src/legacy/core_plugins/data/public';
|
||||
import { SearchSource } from '../../courier';
|
||||
import { PersistedState } from '../../persisted_state';
|
||||
|
|
|
@ -572,7 +572,20 @@ export class WebElementWrapper {
|
|||
const screenshot = await this.driver.takeScreenshot();
|
||||
const buffer = Buffer.from(screenshot.toString(), 'base64');
|
||||
const { width, height, x, y } = await this.getPosition();
|
||||
const windowWidth = await this.driver.executeScript('return window.document.body.clientWidth');
|
||||
const src = PNG.sync.read(buffer);
|
||||
if (src.width > windowWidth) {
|
||||
// on linux size of screenshot is double size of screen, scale it down
|
||||
src.width = src.width / 2;
|
||||
src.height = src.height / 2;
|
||||
let h = false;
|
||||
let v = false;
|
||||
src.data = src.data.filter((d: any, i: number) => {
|
||||
h = i % 4 ? h : !h;
|
||||
v = i % (src.width * 2 * 4) ? v : !v;
|
||||
return h && v;
|
||||
});
|
||||
}
|
||||
const dst = new PNG({ width, height });
|
||||
PNG.bitblt(src, dst, x, y, width, height, 0, 0);
|
||||
return PNG.sync.write(dst);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue