updating metricvis interpreter func arguments (#34532) (#39872)

This commit is contained in:
Peter Pisljar 2019-06-28 08:16:09 +02:00 committed by GitHub
parent 54d46696eb
commit 5341d11005
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
120 changed files with 1998 additions and 1811 deletions

View file

@ -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",

View file

@ -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) {

View file

@ -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']);

View file

@ -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);
}
};

View file

@ -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);
}
});
});
});

View file

@ -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);
});
});
});

View file

@ -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 };
}
});
}
}

View file

@ -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);
});
});

View file

@ -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();
}
}

View file

@ -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';

View file

@ -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: {

View file

@ -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';

View file

@ -18,7 +18,7 @@
*/
import _ from 'lodash';
import { SavedObjectNotFound } from '../../errors';
import { SavedObjectNotFound } from 'ui/errors';
function getParams(filter, indexPattern) {
const type = 'geo_polygon';

View file

@ -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']);

View file

@ -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)}`;
}

View file

@ -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[];
}

View file

@ -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 },
},
];
}

View file

@ -0,0 +1,45 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { 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,
},
},
};
}

View file

@ -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: {},
},
};
}
}

View file

@ -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 '';
}
}

View file

@ -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,

View file

@ -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';

View file

@ -19,6 +19,7 @@
export {
IndexPatternsService,
IndexPatterns,
// types
IndexPatternsSetup,
IndexPattern,

View file

@ -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']>;

View file

@ -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';

View 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 },
};
},
},
});

View file

@ -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,
];

View file

@ -0,0 +1,64 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { 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,
};
},
});

View file

@ -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());
}

View file

@ -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(),
};
}

View file

@ -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;
}));

View file

@ -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;
}));

View file

@ -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;
}));

View file

@ -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;
}));

View file

@ -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;
}));

View file

@ -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);
};

View file

@ -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<{

View file

@ -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) {

View file

@ -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);
});
});

View file

@ -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);
}

View file

@ -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 () {

View file

@ -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),
},
}
};

View file

@ -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_',
};

View file

@ -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 [

View file

@ -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,
}

View file

@ -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();
});
});

View file

@ -31,6 +31,7 @@ const chrome = {
addBasePath: path => path,
getInjected: jest.fn(),
getUiSettingsClient: () => uiSettingsClient,
getSavedObjectsClient: () => '',
getXsrfToken: () => 'kbn-xsrf-token',
};

View file

@ -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

View file

@ -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);
});
});
});
});

View file

@ -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);
});
});
});

View file

@ -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);
});
});
});
});

View file

@ -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);
});
});
});

View file

@ -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);
});
});
});

View file

@ -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);
});
});
});
});

View file

@ -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);
});

View file

@ -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 () {
});
});

View file

@ -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;
}

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { FilterManagerProvider } from './filter_manager';
export { getFilterGenerator } from './filter_generator';

View file

@ -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;
}
}
});
}
}
}

View file

@ -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.

View file

@ -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;

View file

@ -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' "`;

View file

@ -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();

View file

@ -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) => {

View file

@ -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';

View file

@ -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