mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* [lens] Use top nav in Lens app * Add tests for saved query, pass filters around more places * Fix filter passing * Add unit test for field popover making correct queries * Respond to review feedback * Fix type errors * Respond to all review comments * Remove commented code * Top nav should be compatible as angular directive * Fix rendering issue with filter updates * Respond to review comments and add onChange test * Add specific test for the index pattern bug from Tina
This commit is contained in:
parent
100865a57e
commit
f6c0ea623c
37 changed files with 1097 additions and 394 deletions
39
packages/kbn-es-query/src/es_query/index.d.ts
vendored
Normal file
39
packages/kbn-es-query/src/es_query/index.d.ts
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export function buildQueryFromFilters(filters: unknown[], indexPattern: unknown): unknown;
|
||||
export function buildEsQuery(
|
||||
indexPattern: unknown,
|
||||
queries: unknown,
|
||||
filters: unknown,
|
||||
config?: {
|
||||
allowLeadingWildcards: boolean;
|
||||
queryStringOptions: unknown;
|
||||
ignoreFilterIfFieldNotInIndex: boolean;
|
||||
dateFormatTZ?: string | null;
|
||||
}
|
||||
): unknown;
|
||||
export function getEsQueryConfig(config: {
|
||||
get: (name: string) => unknown;
|
||||
}): {
|
||||
allowLeadingWildcards: boolean;
|
||||
queryStringOptions: unknown;
|
||||
ignoreFilterIfFieldNotInIndex: boolean;
|
||||
dateFormatTZ?: string | null;
|
||||
};
|
1
packages/kbn-es-query/src/index.d.ts
vendored
1
packages/kbn-es-query/src/index.d.ts
vendored
|
@ -17,5 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './es_query';
|
||||
export * from './kuery';
|
||||
export * from './filters';
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
// /// Define plugin function
|
||||
import { DataPlugin as Plugin, DataSetup } from './plugin';
|
||||
import { DataPlugin as Plugin, DataSetup, DataStart } from './plugin';
|
||||
|
||||
export function plugin() {
|
||||
return new Plugin();
|
||||
|
@ -28,6 +28,7 @@ export function plugin() {
|
|||
|
||||
/** @public types */
|
||||
export type DataSetup = DataSetup;
|
||||
export type DataStart = DataStart;
|
||||
|
||||
export { FilterBar, ApplyFiltersPopover } from './filter';
|
||||
export {
|
||||
|
|
|
@ -66,14 +66,18 @@ export interface SearchBarOwnProps {
|
|||
showFilterBar?: boolean;
|
||||
showDatePicker?: boolean;
|
||||
showAutoRefreshOnly?: boolean;
|
||||
showSaveQuery?: boolean;
|
||||
|
||||
onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void;
|
||||
// Query bar - should be in SearchBarInjectedDeps
|
||||
query?: Query;
|
||||
// Show when user has privileges to save
|
||||
showSaveQuery?: boolean;
|
||||
savedQuery?: SavedQuery;
|
||||
onQuerySubmit?: (payload: { dateRange: TimeRange; query?: Query }) => void;
|
||||
// User has saved the current state as a saved query
|
||||
onSaved?: (savedQuery: SavedQuery) => void;
|
||||
// User has modified the saved query, your app should persist the update
|
||||
onSavedQueryUpdated?: (savedQuery: SavedQuery) => void;
|
||||
// User has cleared the active query, your app should clear the entire query bar
|
||||
onClearSavedQuery?: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -65,17 +65,17 @@ export const lens: LegacyPluginInitializer = kibana => {
|
|||
api: [PLUGIN_ID],
|
||||
catalogue: [PLUGIN_ID],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
all: ['search'],
|
||||
read: ['index-pattern'],
|
||||
},
|
||||
ui: ['save', 'show'],
|
||||
ui: ['save', 'show', 'saveQuery'],
|
||||
},
|
||||
read: {
|
||||
api: [PLUGIN_ID],
|
||||
catalogue: [PLUGIN_ID],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
read: ['index-pattern'],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
}
|
||||
|
||||
.lnsApp__header {
|
||||
padding: $euiSize;
|
||||
border-bottom: $euiBorderThin;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,19 +5,23 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { buildExistsFilter } from '@kbn/es-query';
|
||||
import { App } from './app';
|
||||
import { EditorFrameInstance } from '../types';
|
||||
import { Storage } from 'ui/storage';
|
||||
import { Document, SavedObjectStore } from '../persistence';
|
||||
import { mount } from 'enzyme';
|
||||
import { QueryBarTopRow } from '../../../../../../src/legacy/core_plugins/data/public/query/query_bar';
|
||||
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
|
||||
import {
|
||||
TopNavMenu,
|
||||
TopNavMenuData,
|
||||
} from '../../../../../../src/legacy/core_plugins/kibana_react/public';
|
||||
import { DataStart } from '../../../../../../src/legacy/core_plugins/data/public';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
|
||||
const dataStartMock = dataPluginMock.createStartContract();
|
||||
|
||||
jest.mock('../../../../../../src/legacy/core_plugins/data/public/query/query_bar', () => ({
|
||||
QueryBarTopRow: jest.fn(() => null),
|
||||
jest.mock('../../../../../../src/legacy/core_plugins/kibana_react/public', () => ({
|
||||
TopNavMenu: jest.fn(() => null),
|
||||
}));
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
|
@ -33,14 +37,39 @@ function createMockFrame(): jest.Mocked<EditorFrameInstance> {
|
|||
};
|
||||
}
|
||||
|
||||
function createMockFilterManager() {
|
||||
const unsubscribe = jest.fn();
|
||||
|
||||
let subscriber: () => void;
|
||||
let filters: unknown = [];
|
||||
|
||||
return {
|
||||
getUpdates$: () => ({
|
||||
subscribe: ({ next }: { next: () => void }) => {
|
||||
subscriber = next;
|
||||
return unsubscribe;
|
||||
},
|
||||
}),
|
||||
setFilters: (newFilters: unknown[]) => {
|
||||
filters = newFilters;
|
||||
subscriber();
|
||||
},
|
||||
getFilters: () => filters,
|
||||
removeAll: () => {
|
||||
filters = [];
|
||||
subscriber();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('Lens App', () => {
|
||||
let frame: jest.Mocked<EditorFrameInstance>;
|
||||
let core: ReturnType<typeof coreMock['createStart']>;
|
||||
|
||||
function makeDefaultArgs(): jest.Mocked<{
|
||||
editorFrame: EditorFrameInstance;
|
||||
data: typeof dataStartMock;
|
||||
core: typeof core;
|
||||
data: DataStart;
|
||||
store: Storage;
|
||||
docId?: string;
|
||||
docStorage: SavedObjectStore;
|
||||
|
@ -48,8 +77,29 @@ describe('Lens App', () => {
|
|||
}> {
|
||||
return ({
|
||||
editorFrame: createMockFrame(),
|
||||
core,
|
||||
data: dataStartMock,
|
||||
core: {
|
||||
...core,
|
||||
application: {
|
||||
...core.application,
|
||||
capabilities: {
|
||||
...core.application.capabilities,
|
||||
lens: { save: true, saveQuery: true, show: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
indexPatterns: {
|
||||
indexPatterns: {
|
||||
get: jest.fn(id => {
|
||||
return new Promise(resolve => resolve({ id }));
|
||||
}),
|
||||
},
|
||||
},
|
||||
timefilter: { history: {} },
|
||||
filter: {
|
||||
filterManager: createMockFilterManager(),
|
||||
},
|
||||
},
|
||||
store: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
|
@ -57,13 +107,11 @@ describe('Lens App', () => {
|
|||
load: jest.fn(),
|
||||
save: jest.fn(),
|
||||
},
|
||||
QueryBarTopRow: jest.fn(() => <div />),
|
||||
redirectTo: jest.fn(id => {}),
|
||||
savedObjectsClient: jest.fn(),
|
||||
} as unknown) as jest.Mocked<{
|
||||
editorFrame: EditorFrameInstance;
|
||||
data: typeof dataStartMock;
|
||||
core: typeof core;
|
||||
data: DataStart;
|
||||
store: Storage;
|
||||
docId?: string;
|
||||
docStorage: SavedObjectStore;
|
||||
|
@ -109,12 +157,14 @@ describe('Lens App', () => {
|
|||
"toDate": "now",
|
||||
},
|
||||
"doc": undefined,
|
||||
"filters": Array [],
|
||||
"onChange": [Function],
|
||||
"onError": [Function],
|
||||
"query": Object {
|
||||
"language": "kuery",
|
||||
"query": "",
|
||||
},
|
||||
"savedQuery": undefined,
|
||||
},
|
||||
],
|
||||
]
|
||||
|
@ -174,12 +224,11 @@ describe('Lens App', () => {
|
|||
await waitForPromises();
|
||||
|
||||
expect(args.docStorage.load).toHaveBeenCalledWith('1234');
|
||||
expect(QueryBarTopRow).toHaveBeenCalledWith(
|
||||
expect(args.data.indexPatterns.indexPatterns.get).toHaveBeenCalledWith('1');
|
||||
expect(TopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dateRangeFrom: 'now-7d',
|
||||
dateRangeTo: 'now',
|
||||
query: 'fake query',
|
||||
indexPatterns: ['saved'],
|
||||
indexPatterns: [{ id: '1' }],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
@ -233,30 +282,51 @@ describe('Lens App', () => {
|
|||
});
|
||||
|
||||
describe('save button', () => {
|
||||
it('shows a save button that is enabled when the frame has provided its state', () => {
|
||||
function getButton(instance: ReactWrapper): TopNavMenuData {
|
||||
return (instance
|
||||
.find('[data-test-subj="lnsApp_topNav"]')
|
||||
.prop('config') as TopNavMenuData[]).find(
|
||||
button => button.testId === 'lnsApp_saveButton'
|
||||
)!;
|
||||
}
|
||||
|
||||
it('shows a disabled save button when the user does not have permissions', async () => {
|
||||
const args = makeDefaultArgs();
|
||||
args.core.application = {
|
||||
...args.core.application,
|
||||
capabilities: {
|
||||
...args.core.application.capabilities,
|
||||
lens: { save: false, saveQuery: false, show: true },
|
||||
},
|
||||
};
|
||||
args.editorFrame = frame;
|
||||
|
||||
const instance = mount(<App {...args} />);
|
||||
|
||||
expect(getButton(instance).disableButton).toEqual(true);
|
||||
|
||||
const onChange = frame.mount.mock.calls[0][1].onChange;
|
||||
onChange({ filterableIndexPatterns: [], doc: ('will save this' as unknown) as Document });
|
||||
|
||||
instance.update();
|
||||
|
||||
expect(getButton(instance).disableButton).toEqual(true);
|
||||
});
|
||||
|
||||
it('shows a save button that is enabled when the frame has provided its state', async () => {
|
||||
const args = makeDefaultArgs();
|
||||
args.editorFrame = frame;
|
||||
|
||||
const instance = mount(<App {...args} />);
|
||||
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsApp_saveButton"]')
|
||||
.first()
|
||||
.prop('disabled')
|
||||
).toEqual(true);
|
||||
expect(getButton(instance).disableButton).toEqual(true);
|
||||
|
||||
const onChange = frame.mount.mock.calls[0][1].onChange;
|
||||
onChange({ indexPatternTitles: [], doc: ('will save this' as unknown) as Document });
|
||||
onChange({ filterableIndexPatterns: [], doc: ('will save this' as unknown) as Document });
|
||||
|
||||
instance.update();
|
||||
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsApp_saveButton"]')
|
||||
.first()
|
||||
.prop('disabled')
|
||||
).toEqual(false);
|
||||
expect(getButton(instance).disableButton).toEqual(false);
|
||||
});
|
||||
|
||||
it('saves the latest doc and then prevents more saving', async () => {
|
||||
|
@ -269,21 +339,15 @@ describe('Lens App', () => {
|
|||
expect(frame.mount).toHaveBeenCalledTimes(1);
|
||||
|
||||
const onChange = frame.mount.mock.calls[0][1].onChange;
|
||||
onChange({ indexPatternTitles: [], doc: ({ id: undefined } as unknown) as Document });
|
||||
onChange({ filterableIndexPatterns: [], doc: ({ id: undefined } as unknown) as Document });
|
||||
|
||||
instance.update();
|
||||
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsApp_saveButton"]')
|
||||
.first()
|
||||
.prop('disabled')
|
||||
).toEqual(false);
|
||||
expect(getButton(instance).disableButton).toEqual(false);
|
||||
|
||||
instance
|
||||
.find('[data-test-subj="lnsApp_saveButton"]')
|
||||
.first()
|
||||
.prop('onClick')!({} as React.MouseEvent);
|
||||
act(() => {
|
||||
getButton(instance).run(instance.getDOMNode());
|
||||
});
|
||||
|
||||
expect(args.docStorage.save).toHaveBeenCalledWith({ id: undefined });
|
||||
|
||||
|
@ -295,12 +359,7 @@ describe('Lens App', () => {
|
|||
|
||||
expect(args.docStorage.load).not.toHaveBeenCalled();
|
||||
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsApp_saveButton"]')
|
||||
.first()
|
||||
.prop('disabled')
|
||||
).toEqual(true);
|
||||
expect(getButton(instance).disableButton).toEqual(true);
|
||||
});
|
||||
|
||||
it('handles save failure by showing a warning, but still allows another save', async () => {
|
||||
|
@ -311,27 +370,22 @@ describe('Lens App', () => {
|
|||
const instance = mount(<App {...args} />);
|
||||
|
||||
const onChange = frame.mount.mock.calls[0][1].onChange;
|
||||
onChange({ indexPatternTitles: [], doc: ({ id: undefined } as unknown) as Document });
|
||||
onChange({ filterableIndexPatterns: [], doc: ({ id: undefined } as unknown) as Document });
|
||||
|
||||
instance.update();
|
||||
|
||||
instance
|
||||
.find('[data-test-subj="lnsApp_saveButton"]')
|
||||
.first()
|
||||
.prop('onClick')!({} as React.MouseEvent);
|
||||
act(() => {
|
||||
getButton(instance).run(instance.getDOMNode());
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
await waitForPromises();
|
||||
|
||||
expect(args.core.notifications.toasts.addDanger).toHaveBeenCalled();
|
||||
expect(args.redirectTo).not.toHaveBeenCalled();
|
||||
await waitForPromises();
|
||||
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsApp_saveButton"]')
|
||||
.first()
|
||||
.prop('disabled')
|
||||
).toEqual(false);
|
||||
expect(getButton(instance).disableButton).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -343,10 +397,8 @@ describe('Lens App', () => {
|
|||
|
||||
mount(<App {...args} />);
|
||||
|
||||
expect(QueryBarTopRow).toHaveBeenCalledWith(
|
||||
expect(TopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dateRangeFrom: 'now-7d',
|
||||
dateRangeTo: 'now',
|
||||
query: { query: '', language: 'kuery' },
|
||||
}),
|
||||
{}
|
||||
|
@ -360,13 +412,13 @@ describe('Lens App', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('updates the index patterns when the editor frame is changed', () => {
|
||||
it('updates the index patterns when the editor frame is changed', async () => {
|
||||
const args = makeDefaultArgs();
|
||||
args.editorFrame = frame;
|
||||
|
||||
const instance = mount(<App {...args} />);
|
||||
|
||||
expect(QueryBarTopRow).toHaveBeenCalledWith(
|
||||
expect(TopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
indexPatterns: [],
|
||||
}),
|
||||
|
@ -375,40 +427,52 @@ describe('Lens App', () => {
|
|||
|
||||
const onChange = frame.mount.mock.calls[0][1].onChange;
|
||||
onChange({
|
||||
indexPatternTitles: ['newIndex'],
|
||||
filterableIndexPatterns: [{ id: '1', title: 'newIndex' }],
|
||||
doc: ({ id: undefined } as unknown) as Document,
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
instance.update();
|
||||
|
||||
expect(QueryBarTopRow).toHaveBeenCalledWith(
|
||||
expect(TopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
indexPatterns: ['newIndex'],
|
||||
indexPatterns: [{ id: '1' }],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
// Do it again to verify that the dirty checking is done right
|
||||
onChange({
|
||||
filterableIndexPatterns: [{ id: '2', title: 'second index' }],
|
||||
doc: ({ id: undefined } as unknown) as Document,
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
instance.update();
|
||||
|
||||
expect(TopNavMenu).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
indexPatterns: [{ id: '2' }],
|
||||
}),
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('updates the editor frame when the user changes query or time', () => {
|
||||
it('updates the editor frame when the user changes query or time in the search bar', () => {
|
||||
const args = makeDefaultArgs();
|
||||
args.editorFrame = frame;
|
||||
|
||||
const instance = mount(<App {...args} />);
|
||||
|
||||
instance
|
||||
.find('[data-test-subj="lnsApp_queryBar"]')
|
||||
.first()
|
||||
.prop('onSubmit')!(({
|
||||
instance.find(TopNavMenu).prop('onQuerySubmit')!({
|
||||
dateRange: { from: 'now-14d', to: 'now-7d' },
|
||||
query: { query: 'new', language: 'lucene' },
|
||||
} as unknown) as React.FormEvent);
|
||||
});
|
||||
|
||||
instance.update();
|
||||
|
||||
expect(QueryBarTopRow).toHaveBeenCalledWith(
|
||||
expect(TopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dateRangeFrom: 'now-14d',
|
||||
dateRangeTo: 'now-7d',
|
||||
query: { query: 'new', language: 'lucene' },
|
||||
}),
|
||||
{}
|
||||
|
@ -421,6 +485,159 @@ describe('Lens App', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('updates the filters when the user changes them', () => {
|
||||
const args = makeDefaultArgs();
|
||||
args.editorFrame = frame;
|
||||
|
||||
const instance = mount(<App {...args} />);
|
||||
|
||||
args.data.filter.filterManager.setFilters([
|
||||
buildExistsFilter({ name: 'myfield' }, { id: 'index1' }),
|
||||
]);
|
||||
|
||||
instance.update();
|
||||
|
||||
expect(frame.mount).toHaveBeenCalledWith(
|
||||
expect.any(Element),
|
||||
expect.objectContaining({
|
||||
filters: [buildExistsFilter({ name: 'myfield' }, { id: 'index1' })],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saved query handling', () => {
|
||||
it('does not allow saving when the user is missing the saveQuery permission', () => {
|
||||
const args = makeDefaultArgs();
|
||||
args.core.application = {
|
||||
...args.core.application,
|
||||
capabilities: {
|
||||
...args.core.application.capabilities,
|
||||
lens: { save: false, saveQuery: false, show: true },
|
||||
},
|
||||
};
|
||||
|
||||
mount(<App {...args} />);
|
||||
|
||||
expect(TopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ showSaveQuery: false }),
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('persists the saved query ID when the query is saved', () => {
|
||||
const args = makeDefaultArgs();
|
||||
args.editorFrame = frame;
|
||||
|
||||
const instance = mount(<App {...args} />);
|
||||
|
||||
expect(TopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
showSaveQuery: true,
|
||||
savedQuery: undefined,
|
||||
onSaved: expect.any(Function),
|
||||
onSavedQueryUpdated: expect.any(Function),
|
||||
onClearSavedQuery: expect.any(Function),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
act(() => {
|
||||
instance.find(TopNavMenu).prop('onSaved')!({
|
||||
id: '1',
|
||||
attributes: {
|
||||
title: '',
|
||||
description: '',
|
||||
query: { query: '', language: 'lucene' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(TopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
savedQuery: {
|
||||
id: '1',
|
||||
attributes: {
|
||||
title: '',
|
||||
description: '',
|
||||
query: { query: '', language: 'lucene' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('changes the saved query ID when the query is updated', () => {
|
||||
const args = makeDefaultArgs();
|
||||
args.editorFrame = frame;
|
||||
|
||||
const instance = mount(<App {...args} />);
|
||||
|
||||
act(() => {
|
||||
instance.find(TopNavMenu).prop('onSaved')!({
|
||||
id: '1',
|
||||
attributes: {
|
||||
title: '',
|
||||
description: '',
|
||||
query: { query: '', language: 'lucene' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
instance.find(TopNavMenu).prop('onSavedQueryUpdated')!({
|
||||
id: '2',
|
||||
attributes: {
|
||||
title: 'new title',
|
||||
description: '',
|
||||
query: { query: '', language: 'lucene' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(TopNavMenu).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
savedQuery: {
|
||||
id: '2',
|
||||
attributes: {
|
||||
title: 'new title',
|
||||
description: '',
|
||||
query: { query: '', language: 'lucene' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
{}
|
||||
);
|
||||
});
|
||||
|
||||
it('clears all existing filters when the active saved query is cleared', () => {
|
||||
const args = makeDefaultArgs();
|
||||
args.editorFrame = frame;
|
||||
|
||||
const instance = mount(<App {...args} />);
|
||||
|
||||
instance.find(TopNavMenu).prop('onQuerySubmit')!({
|
||||
dateRange: { from: 'now-14d', to: 'now-7d' },
|
||||
query: { query: 'new', language: 'lucene' },
|
||||
});
|
||||
|
||||
args.data.filter.filterManager.setFilters([
|
||||
buildExistsFilter({ name: 'myfield' }, { id: 'index1' }),
|
||||
]);
|
||||
instance.update();
|
||||
|
||||
instance.find(TopNavMenu).prop('onClearSavedQuery')!();
|
||||
instance.update();
|
||||
|
||||
expect(frame.mount).toHaveBeenLastCalledWith(
|
||||
expect.any(Element),
|
||||
expect.objectContaining({
|
||||
filters: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays errors from the frame in a toast', () => {
|
||||
|
|
|
@ -8,12 +8,17 @@ import _ from 'lodash';
|
|||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { Storage } from 'ui/storage';
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import { DataPublicPluginStart } from 'src/plugins/data/public';
|
||||
import { Query } from '../../../../../../src/legacy/core_plugins/data/public';
|
||||
import { QueryBarTopRow } from '../../../../../../src/legacy/core_plugins/data/public/query/query_bar';
|
||||
import { CoreStart, NotificationsStart } from 'src/core/public';
|
||||
import {
|
||||
DataStart,
|
||||
IndexPattern as IndexPatternInstance,
|
||||
IndexPatterns as IndexPatternsService,
|
||||
SavedQuery,
|
||||
Query,
|
||||
} from 'src/legacy/core_plugins/data/public';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { TopNavMenu } from '../../../../../../src/legacy/core_plugins/kibana_react/public';
|
||||
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
|
||||
import { Document, SavedObjectStore } from '../persistence';
|
||||
import { EditorFrameInstance } from '../types';
|
||||
|
@ -22,32 +27,17 @@ import { NativeRenderer } from '../native_renderer';
|
|||
interface State {
|
||||
isLoading: boolean;
|
||||
isDirty: boolean;
|
||||
indexPatternsForTopNav: IndexPatternInstance[];
|
||||
persistedDoc?: Document;
|
||||
|
||||
// Properties needed to interface with TopNav
|
||||
dateRange: {
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
};
|
||||
query: Query;
|
||||
indexPatternTitles: string[];
|
||||
persistedDoc?: Document;
|
||||
localQueryBarState: {
|
||||
query?: Query;
|
||||
dateRange?: {
|
||||
from: string;
|
||||
to: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function isLocalStateDirty(
|
||||
localState: State['localQueryBarState'],
|
||||
query: Query,
|
||||
dateRange: State['dateRange']
|
||||
) {
|
||||
return Boolean(
|
||||
(localState.query && query && localState.query.query !== query.query) ||
|
||||
(localState.dateRange && dateRange.fromDate !== localState.dateRange.from) ||
|
||||
(localState.dateRange && dateRange.toDate !== localState.dateRange.to)
|
||||
);
|
||||
filters: Filter[];
|
||||
savedQuery?: SavedQuery;
|
||||
}
|
||||
|
||||
export function App({
|
||||
|
@ -60,8 +50,8 @@ export function App({
|
|||
redirectTo,
|
||||
}: {
|
||||
editorFrame: EditorFrameInstance;
|
||||
data: DataPublicPluginStart;
|
||||
core: CoreStart;
|
||||
data: DataStart;
|
||||
store: Storage;
|
||||
docId?: string;
|
||||
docStorage: SavedObjectStore;
|
||||
|
@ -74,23 +64,29 @@ export function App({
|
|||
const [state, setState] = useState<State>({
|
||||
isLoading: !!docId,
|
||||
isDirty: false,
|
||||
indexPatternsForTopNav: [],
|
||||
|
||||
query: { query: '', language },
|
||||
dateRange: {
|
||||
fromDate: timeDefaults.from,
|
||||
toDate: timeDefaults.to,
|
||||
},
|
||||
indexPatternTitles: [],
|
||||
localQueryBarState: {
|
||||
query: { query: '', language },
|
||||
dateRange: {
|
||||
from: timeDefaults.from,
|
||||
to: timeDefaults.to,
|
||||
},
|
||||
},
|
||||
filters: [],
|
||||
});
|
||||
|
||||
const lastKnownDocRef = useRef<Document | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = data.filter.filterManager.getUpdates$().subscribe({
|
||||
next: () => {
|
||||
setState(s => ({ ...s, filters: data.filter.filterManager.getFilters() }));
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Sync Kibana breadcrumbs any time the saved document's title changes
|
||||
useEffect(() => {
|
||||
core.chrome.setBreadcrumbs([
|
||||
|
@ -110,26 +106,34 @@ export function App({
|
|||
|
||||
useEffect(() => {
|
||||
if (docId && (!state.persistedDoc || state.persistedDoc.id !== docId)) {
|
||||
setState({ ...state, isLoading: true });
|
||||
setState(s => ({ ...s, isLoading: true }));
|
||||
docStorage
|
||||
.load(docId)
|
||||
.then(doc => {
|
||||
setState({
|
||||
...state,
|
||||
isLoading: false,
|
||||
persistedDoc: doc,
|
||||
query: doc.state.query,
|
||||
localQueryBarState: {
|
||||
...state.localQueryBarState,
|
||||
query: doc.state.query,
|
||||
},
|
||||
indexPatternTitles: doc.state.datasourceMetaData.filterableIndexPatterns.map(
|
||||
({ title }) => title
|
||||
),
|
||||
});
|
||||
getAllIndexPatterns(
|
||||
doc.state.datasourceMetaData.filterableIndexPatterns,
|
||||
data.indexPatterns.indexPatterns,
|
||||
core.notifications
|
||||
)
|
||||
.then(indexPatterns => {
|
||||
setState(s => ({
|
||||
...s,
|
||||
isLoading: false,
|
||||
persistedDoc: doc,
|
||||
query: doc.state.query,
|
||||
filters: doc.state.filters,
|
||||
dateRange: doc.state.dateRange || s.dateRange,
|
||||
indexPatternsForTopNav: indexPatterns,
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
setState(s => ({ ...s, isLoading: false }));
|
||||
|
||||
redirectTo();
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setState({ ...state, isLoading: false });
|
||||
setState(s => ({ ...s, isLoading: false }));
|
||||
|
||||
core.notifications.toasts.addDanger(
|
||||
i18n.translate('xpack.lens.editorFrame.docLoadingError', {
|
||||
|
@ -145,7 +149,7 @@ export function App({
|
|||
// Can save if the frame has told us what it has, and there is either:
|
||||
// a) No saved doc
|
||||
// b) A saved doc that differs from the frame state
|
||||
const isSaveable = state.isDirty;
|
||||
const isSaveable = state.isDirty && (core.application.capabilities.lens.save as boolean);
|
||||
|
||||
const onError = useCallback(
|
||||
(e: { message: string }) =>
|
||||
|
@ -160,83 +164,101 @@ export function App({
|
|||
<KibanaContextProvider
|
||||
services={{
|
||||
appName: 'lens',
|
||||
autocomplete: data.autocomplete,
|
||||
store,
|
||||
...core,
|
||||
}}
|
||||
>
|
||||
<div className="lnsApp">
|
||||
<div className="lnsApp__header">
|
||||
<nav>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink
|
||||
data-test-subj="lnsApp_saveButton"
|
||||
onClick={() => {
|
||||
if (isSaveable && lastKnownDocRef.current) {
|
||||
docStorage
|
||||
.save(lastKnownDocRef.current)
|
||||
.then(({ id }) => {
|
||||
// Prevents unnecessary network request and disables save button
|
||||
const newDoc = { ...lastKnownDocRef.current!, id };
|
||||
setState({
|
||||
...state,
|
||||
isDirty: false,
|
||||
persistedDoc: newDoc,
|
||||
});
|
||||
if (docId !== id) {
|
||||
redirectTo(id);
|
||||
}
|
||||
})
|
||||
.catch(reason => {
|
||||
core.notifications.toasts.addDanger(
|
||||
i18n.translate('xpack.lens.editorFrame.docSavingError', {
|
||||
defaultMessage: 'Error saving document {reason}',
|
||||
values: { reason },
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}}
|
||||
color={isSaveable ? 'primary' : 'subdued'}
|
||||
disabled={!isSaveable}
|
||||
>
|
||||
{i18n.translate('xpack.lens.editorFrame.save', {
|
||||
defaultMessage: 'Save',
|
||||
})}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</nav>
|
||||
<QueryBarTopRow
|
||||
data-test-subj="lnsApp_queryBar"
|
||||
<TopNavMenu
|
||||
config={[
|
||||
{
|
||||
label: i18n.translate('xpack.lens.editorFrame.save', {
|
||||
defaultMessage: 'Save',
|
||||
}),
|
||||
run: () => {
|
||||
if (isSaveable && lastKnownDocRef.current) {
|
||||
docStorage
|
||||
.save(lastKnownDocRef.current)
|
||||
.then(({ id }) => {
|
||||
// Prevents unnecessary network request and disables save button
|
||||
const newDoc = { ...lastKnownDocRef.current!, id };
|
||||
setState(s => ({
|
||||
...s,
|
||||
isDirty: false,
|
||||
persistedDoc: newDoc,
|
||||
}));
|
||||
if (docId !== id) {
|
||||
redirectTo(id);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
core.notifications.toasts.addDanger(
|
||||
i18n.translate('xpack.lens.editorFrame.docSavingError', {
|
||||
defaultMessage: 'Error saving document',
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
testId: 'lnsApp_saveButton',
|
||||
disableButton: !isSaveable,
|
||||
},
|
||||
]}
|
||||
data-test-subj="lnsApp_topNav"
|
||||
screenTitle={'lens'}
|
||||
onSubmit={payload => {
|
||||
onQuerySubmit={payload => {
|
||||
const { dateRange, query } = payload;
|
||||
setState({
|
||||
...state,
|
||||
setState(s => ({
|
||||
...s,
|
||||
dateRange: {
|
||||
fromDate: dateRange.from,
|
||||
toDate: dateRange.to,
|
||||
},
|
||||
query: query || state.query,
|
||||
localQueryBarState: payload,
|
||||
});
|
||||
query: query || s.query,
|
||||
}));
|
||||
}}
|
||||
onChange={localQueryBarState => {
|
||||
setState({ ...state, localQueryBarState });
|
||||
}}
|
||||
isDirty={isLocalStateDirty(state.localQueryBarState, state.query, state.dateRange)}
|
||||
indexPatterns={state.indexPatternTitles}
|
||||
appName={'lens'}
|
||||
indexPatterns={state.indexPatternsForTopNav}
|
||||
showSearchBar={true}
|
||||
showDatePicker={true}
|
||||
showQueryInput={true}
|
||||
query={state.localQueryBarState.query}
|
||||
dateRangeFrom={
|
||||
state.localQueryBarState.dateRange && state.localQueryBarState.dateRange.from
|
||||
}
|
||||
dateRangeTo={
|
||||
state.localQueryBarState.dateRange && state.localQueryBarState.dateRange.to
|
||||
}
|
||||
showQueryBar={true}
|
||||
showFilterBar={true}
|
||||
showSaveQuery={core.application.capabilities.lens.saveQuery as boolean}
|
||||
savedQuery={state.savedQuery}
|
||||
onSaved={savedQuery => {
|
||||
setState(s => ({ ...s, savedQuery }));
|
||||
}}
|
||||
onSavedQueryUpdated={savedQuery => {
|
||||
data.filter.filterManager.setFilters(
|
||||
savedQuery.attributes.filters || state.filters
|
||||
);
|
||||
setState(s => ({
|
||||
...s,
|
||||
savedQuery: { ...savedQuery }, // Shallow query for reference issues
|
||||
dateRange: savedQuery.attributes.timefilter
|
||||
? {
|
||||
fromDate: savedQuery.attributes.timefilter.from,
|
||||
toDate: savedQuery.attributes.timefilter.to,
|
||||
}
|
||||
: s.dateRange,
|
||||
}));
|
||||
}}
|
||||
onClearSavedQuery={() => {
|
||||
data.filter.filterManager.removeAll();
|
||||
setState(s => ({
|
||||
...s,
|
||||
savedQuery: undefined,
|
||||
filters: [],
|
||||
query: {
|
||||
query: '',
|
||||
language:
|
||||
store.get('kibana.userQueryLanguage') ||
|
||||
core.uiSettings.get('search:queryLanguage'),
|
||||
},
|
||||
}));
|
||||
}}
|
||||
query={state.query}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -247,22 +269,35 @@ export function App({
|
|||
nativeProps={{
|
||||
dateRange: state.dateRange,
|
||||
query: state.query,
|
||||
filters: state.filters,
|
||||
savedQuery: state.savedQuery,
|
||||
doc: state.persistedDoc,
|
||||
onError,
|
||||
onChange: ({ indexPatternTitles, doc }) => {
|
||||
const indexPatternChange = !_.isEqual(
|
||||
state.indexPatternTitles,
|
||||
indexPatternTitles
|
||||
);
|
||||
const docChange = !_.isEqual(state.persistedDoc, doc);
|
||||
if (indexPatternChange || docChange) {
|
||||
setState({
|
||||
...state,
|
||||
indexPatternTitles,
|
||||
isDirty: docChange,
|
||||
onChange: ({ filterableIndexPatterns, doc }) => {
|
||||
lastKnownDocRef.current = doc;
|
||||
|
||||
if (!_.isEqual(state.persistedDoc, doc)) {
|
||||
setState(s => ({ ...s, isDirty: true }));
|
||||
}
|
||||
|
||||
// Update the cached index patterns if the user made a change to any of them
|
||||
if (
|
||||
state.indexPatternsForTopNav.length !== filterableIndexPatterns.length ||
|
||||
filterableIndexPatterns.find(
|
||||
({ id }) =>
|
||||
!state.indexPatternsForTopNav.find(indexPattern => indexPattern.id === id)
|
||||
)
|
||||
) {
|
||||
getAllIndexPatterns(
|
||||
filterableIndexPatterns,
|
||||
data.indexPatterns.indexPatterns,
|
||||
core.notifications
|
||||
).then(indexPatterns => {
|
||||
if (indexPatterns) {
|
||||
setState(s => ({ ...s, indexPatternsForTopNav: indexPatterns }));
|
||||
}
|
||||
});
|
||||
}
|
||||
lastKnownDocRef.current = doc;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -272,3 +307,21 @@ export function App({
|
|||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAllIndexPatterns(
|
||||
ids: Array<{ id: string }>,
|
||||
indexPatternsService: IndexPatternsService,
|
||||
notifications: NotificationsStart
|
||||
): Promise<IndexPatternInstance[]> {
|
||||
try {
|
||||
return await Promise.all(ids.map(({ id }) => indexPatternsService.get(id)));
|
||||
} catch (e) {
|
||||
notifications.toasts.addDanger(
|
||||
i18n.translate('xpack.lens.editorFrame.indexPatternLoadingError', {
|
||||
defaultMessage: 'Error loading index patterns',
|
||||
})
|
||||
);
|
||||
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@ import chrome from 'ui/chrome';
|
|||
import { Storage } from 'ui/storage';
|
||||
import { CoreSetup, CoreStart } from 'src/core/public';
|
||||
import { npSetup, npStart } from 'ui/new_platform';
|
||||
import { DataPublicPluginStart } from 'src/plugins/data/public';
|
||||
import { DataStart } from '../../../../../../src/legacy/core_plugins/data/public';
|
||||
import { start as dataStart } from '../../../../../../src/legacy/core_plugins/data/public/legacy';
|
||||
import { editorFrameSetup, editorFrameStart, editorFrameStop } from '../editor_frame_plugin';
|
||||
import { indexPatternDatasourceSetup, indexPatternDatasourceStop } from '../indexpattern_plugin';
|
||||
import { SavedObjectIndexStore } from '../persistence';
|
||||
|
@ -25,7 +26,7 @@ import { App } from './app';
|
|||
import { EditorFrameInstance } from '../types';
|
||||
|
||||
export interface LensPluginStartDependencies {
|
||||
data: DataPublicPluginStart;
|
||||
data: DataStart;
|
||||
}
|
||||
export class AppPlugin {
|
||||
private instance: EditorFrameInstance | null = null;
|
||||
|
@ -33,7 +34,7 @@ export class AppPlugin {
|
|||
|
||||
constructor() {}
|
||||
|
||||
setup(core: CoreSetup) {
|
||||
setup(core: CoreSetup, plugins: {}) {
|
||||
// TODO: These plugins should not be called from the top level, but since this is the
|
||||
// entry point to the app we have no choice until the new platform is ready
|
||||
const indexPattern = indexPatternDatasourceSetup();
|
||||
|
@ -43,10 +44,10 @@ export class AppPlugin {
|
|||
const editorFrameSetupInterface = editorFrameSetup();
|
||||
this.store = new SavedObjectIndexStore(chrome!.getSavedObjectsClient());
|
||||
|
||||
editorFrameSetupInterface.registerDatasource('indexpattern', indexPattern);
|
||||
editorFrameSetupInterface.registerVisualization(xyVisualization);
|
||||
editorFrameSetupInterface.registerVisualization(datatableVisualization);
|
||||
editorFrameSetupInterface.registerVisualization(metricVisualization);
|
||||
editorFrameSetupInterface.registerDatasource('indexpattern', indexPattern);
|
||||
}
|
||||
|
||||
start(core: CoreStart, { data }: LensPluginStartDependencies) {
|
||||
|
@ -113,6 +114,6 @@ export class AppPlugin {
|
|||
|
||||
const app = new AppPlugin();
|
||||
|
||||
export const appSetup = () => app.setup(npSetup.core);
|
||||
export const appStart = () => app.start(npStart.core, { data: npStart.plugins.data });
|
||||
export const appSetup = () => app.setup(npSetup.core, {});
|
||||
export const appStart = () => app.start(npStart.core, { data: dataStart });
|
||||
export const appStop = () => app.stop();
|
||||
|
|
|
@ -27,6 +27,7 @@ function mockFrame(): FramePublicAPI {
|
|||
fromDate: 'now-7d',
|
||||
toDate: 'now',
|
||||
},
|
||||
filters: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
import React, { useMemo, memo, useContext, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { EuiPopover, EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui';
|
||||
import { Query } from 'src/plugins/data/common';
|
||||
import { DatasourceDataPanelProps, Datasource } from '../../../public';
|
||||
|
@ -23,6 +24,7 @@ interface DataPanelWrapperProps {
|
|||
core: DatasourceDataPanelProps['core'];
|
||||
query: Query;
|
||||
dateRange: FramePublicAPI['dateRange'];
|
||||
filters: Filter[];
|
||||
}
|
||||
|
||||
export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
||||
|
@ -45,6 +47,7 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => {
|
|||
core: props.core,
|
||||
query: props.query,
|
||||
dateRange: props.dateRange,
|
||||
filters: props.filters,
|
||||
};
|
||||
|
||||
const [showDatasourceSwitcher, setDatasourceSwitcher] = useState(false);
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { EuiPanel, EuiToolTip } from '@elastic/eui';
|
||||
import { mountWithIntl as mount } from 'test_utils/enzyme_helpers';
|
||||
import { EditorFrame } from './editor_frame';
|
||||
import { Visualization, DatasourcePublicAPI, DatasourceSuggestion } from '../../types';
|
||||
|
@ -19,7 +20,7 @@ import {
|
|||
} from '../mocks';
|
||||
import { ExpressionRenderer } from 'src/legacy/core_plugins/expressions/public';
|
||||
import { DragDrop } from '../../drag_drop';
|
||||
import { EuiPanel, EuiToolTip } from '@elastic/eui';
|
||||
import { FrameLayout } from './frame_layout';
|
||||
|
||||
// calling this function will wait for all pending Promises from mock
|
||||
// datasources to be processed by its callers.
|
||||
|
@ -48,6 +49,7 @@ function getDefaultProps() {
|
|||
onChange: jest.fn(),
|
||||
dateRange: { fromDate: '', toDate: '' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
core: coreMock.createSetup(),
|
||||
};
|
||||
}
|
||||
|
@ -256,6 +258,7 @@ describe('editor_frame', () => {
|
|||
addNewLayer: expect.any(Function),
|
||||
removeLayers: expect.any(Function),
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
dateRange: { fromDate: 'now-7d', toDate: 'now' },
|
||||
});
|
||||
});
|
||||
|
@ -409,56 +412,58 @@ describe('editor_frame', () => {
|
|||
instance.update();
|
||||
|
||||
expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {},
|
||||
"function": "kibana",
|
||||
"type": "function",
|
||||
},
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"filters": Array [],
|
||||
"query": Array [
|
||||
"{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}",
|
||||
],
|
||||
"timeRange": Array [
|
||||
"{\\"from\\":\\"\\",\\"to\\":\\"\\"}",
|
||||
],
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {},
|
||||
"function": "kibana",
|
||||
"type": "function",
|
||||
},
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"filters": Array [
|
||||
"[]",
|
||||
],
|
||||
"query": Array [
|
||||
"{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}",
|
||||
],
|
||||
"timeRange": Array [
|
||||
"{\\"from\\":\\"\\",\\"to\\":\\"\\"}",
|
||||
],
|
||||
},
|
||||
"function": "kibana_context",
|
||||
"type": "function",
|
||||
},
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"layerIds": Array [
|
||||
"first",
|
||||
],
|
||||
"tables": Array [
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {},
|
||||
"function": "datasource",
|
||||
"type": "function",
|
||||
},
|
||||
"function": "kibana_context",
|
||||
"type": "function",
|
||||
},
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"layerIds": Array [
|
||||
"first",
|
||||
],
|
||||
"tables": Array [
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {},
|
||||
"function": "datasource",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"type": "expression",
|
||||
},
|
||||
],
|
||||
},
|
||||
"function": "lens_merge_tables",
|
||||
"type": "function",
|
||||
},
|
||||
Object {
|
||||
"arguments": Object {},
|
||||
"function": "vis",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"type": "expression",
|
||||
}
|
||||
`);
|
||||
],
|
||||
"type": "expression",
|
||||
},
|
||||
],
|
||||
},
|
||||
"function": "lens_merge_tables",
|
||||
"type": "function",
|
||||
},
|
||||
Object {
|
||||
"arguments": Object {},
|
||||
"function": "vis",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"type": "expression",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should render individual expression for each given layer', async () => {
|
||||
|
@ -525,7 +530,9 @@ describe('editor_frame', () => {
|
|||
},
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"filters": Array [],
|
||||
"filters": Array [
|
||||
"[]",
|
||||
],
|
||||
"query": Array [
|
||||
"{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}",
|
||||
],
|
||||
|
@ -1491,7 +1498,7 @@ describe('editor_frame', () => {
|
|||
|
||||
expect(onChange).toHaveBeenCalledTimes(2);
|
||||
expect(onChange).toHaveBeenNthCalledWith(1, {
|
||||
indexPatternTitles: ['resolved'],
|
||||
filterableIndexPatterns: [{ id: '1', title: 'resolved' }],
|
||||
doc: {
|
||||
expression: '',
|
||||
id: undefined,
|
||||
|
@ -1501,6 +1508,7 @@ describe('editor_frame', () => {
|
|||
datasourceStates: { testDatasource: undefined },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
dateRange: { fromDate: '', toDate: '' },
|
||||
},
|
||||
title: 'New visualization',
|
||||
type: 'lens',
|
||||
|
@ -1508,7 +1516,7 @@ describe('editor_frame', () => {
|
|||
},
|
||||
});
|
||||
expect(onChange).toHaveBeenLastCalledWith({
|
||||
indexPatternTitles: ['resolved'],
|
||||
filterableIndexPatterns: [{ id: '1', title: 'resolved' }],
|
||||
doc: {
|
||||
expression: '',
|
||||
id: undefined,
|
||||
|
@ -1520,6 +1528,7 @@ describe('editor_frame', () => {
|
|||
datasourceStates: { testDatasource: undefined },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
dateRange: { fromDate: '', toDate: '' },
|
||||
},
|
||||
title: 'New visualization',
|
||||
type: 'lens',
|
||||
|
@ -1567,7 +1576,7 @@ describe('editor_frame', () => {
|
|||
await waitForPromises();
|
||||
expect(onChange).toHaveBeenCalledTimes(3);
|
||||
expect(onChange).toHaveBeenNthCalledWith(3, {
|
||||
indexPatternTitles: [],
|
||||
filterableIndexPatterns: [],
|
||||
doc: {
|
||||
expression: expect.stringContaining('vis "expression"'),
|
||||
id: undefined,
|
||||
|
@ -1577,6 +1586,7 @@ describe('editor_frame', () => {
|
|||
visualization: { initialState: true },
|
||||
query: { query: 'new query', language: 'lucene' },
|
||||
filters: [],
|
||||
dateRange: { fromDate: '', toDate: '' },
|
||||
},
|
||||
title: 'New visualization',
|
||||
type: 'lens',
|
||||
|
@ -1584,5 +1594,44 @@ describe('editor_frame', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onChange when the datasource makes an internal state change', async () => {
|
||||
const onChange = jest.fn();
|
||||
|
||||
mockDatasource.initialize.mockResolvedValue({});
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
mockDatasource.getMetaData.mockReturnValue({
|
||||
filterableIndexPatterns: [{ id: '1', title: 'resolved' }],
|
||||
});
|
||||
mockVisualization.initialize.mockReturnValue({ initialState: true });
|
||||
|
||||
act(() => {
|
||||
instance = mount(
|
||||
<EditorFrame
|
||||
{...getDefaultProps()}
|
||||
visualizationMap={{ testVis: mockVisualization }}
|
||||
datasourceMap={{ testDatasource: mockDatasource }}
|
||||
initialDatasourceId="testDatasource"
|
||||
initialVisualizationId="testVis"
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
expect(onChange).toHaveBeenCalledTimes(2);
|
||||
|
||||
(instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({
|
||||
type: 'UPDATE_DATASOURCE_STATE',
|
||||
updater: () => ({
|
||||
newState: true,
|
||||
}),
|
||||
datasourceId: 'testDatasource',
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,9 +6,16 @@
|
|||
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
import { CoreSetup, CoreStart } from 'src/core/public';
|
||||
import { Query } from '../../../../../../../src/legacy/core_plugins/data/public';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { Query, SavedQuery } from '../../../../../../../src/legacy/core_plugins/data/public';
|
||||
import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/expressions/public';
|
||||
import { Datasource, DatasourcePublicAPI, FramePublicAPI, Visualization } from '../../types';
|
||||
import {
|
||||
Datasource,
|
||||
DatasourcePublicAPI,
|
||||
FramePublicAPI,
|
||||
Visualization,
|
||||
DatasourceMetaData,
|
||||
} from '../../types';
|
||||
import { reducer, getInitialState } from './state_management';
|
||||
import { DataPanelWrapper } from './data_panel_wrapper';
|
||||
import { ConfigPanelWrapper } from './config_panel_wrapper';
|
||||
|
@ -34,7 +41,12 @@ export interface EditorFrameProps {
|
|||
toDate: string;
|
||||
};
|
||||
query: Query;
|
||||
onChange: (arg: { indexPatternTitles: string[]; doc: Document }) => void;
|
||||
filters: Filter[];
|
||||
savedQuery?: SavedQuery;
|
||||
onChange: (arg: {
|
||||
filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns'];
|
||||
doc: Document;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export function EditorFrame(props: EditorFrameProps) {
|
||||
|
@ -98,6 +110,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
datasourceLayers,
|
||||
dateRange: props.dateRange,
|
||||
query: props.query,
|
||||
filters: props.filters,
|
||||
|
||||
addNewLayer() {
|
||||
const newLayerId = generateId();
|
||||
|
@ -170,7 +183,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
return;
|
||||
}
|
||||
|
||||
const indexPatternTitles: string[] = [];
|
||||
const indexPatterns: DatasourceMetaData['filterableIndexPatterns'] = [];
|
||||
Object.entries(props.datasourceMap)
|
||||
.filter(([id, datasource]) => {
|
||||
const stateWrapper = state.datasourceStates[id];
|
||||
|
@ -181,10 +194,8 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
);
|
||||
})
|
||||
.forEach(([id, datasource]) => {
|
||||
indexPatternTitles.push(
|
||||
...datasource
|
||||
.getMetaData(state.datasourceStates[id].state)
|
||||
.filterableIndexPatterns.map(pattern => pattern.title)
|
||||
indexPatterns.push(
|
||||
...datasource.getMetaData(state.datasourceStates[id].state).filterableIndexPatterns
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -201,8 +212,16 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
framePublicAPI,
|
||||
});
|
||||
|
||||
props.onChange({ indexPatternTitles, doc });
|
||||
}, [state.datasourceStates, state.visualization, props.query, props.dateRange, state.title]);
|
||||
props.onChange({ filterableIndexPatterns: indexPatterns, doc });
|
||||
}, [
|
||||
state.datasourceStates,
|
||||
state.visualization,
|
||||
props.query,
|
||||
props.dateRange,
|
||||
props.filters,
|
||||
props.savedQuery,
|
||||
state.title,
|
||||
]);
|
||||
|
||||
return (
|
||||
<FrameLayout
|
||||
|
@ -222,6 +241,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
core={props.core}
|
||||
query={props.query}
|
||||
dateRange={props.dateRange}
|
||||
filters={props.filters}
|
||||
/>
|
||||
}
|
||||
configPanel={
|
||||
|
|
|
@ -86,7 +86,7 @@ export function prependKibanaContext(
|
|||
arguments: {
|
||||
timeRange: timeRange ? [JSON.stringify(timeRange)] : [],
|
||||
query: query ? [JSON.stringify(query)] : [],
|
||||
filters: filters ? [JSON.stringify(filters)] : [],
|
||||
filters: [JSON.stringify(filters || [])],
|
||||
},
|
||||
},
|
||||
...parsedExpression.chain,
|
||||
|
@ -121,13 +121,14 @@ export function buildExpression({
|
|||
const visualizationExpression = visualization.toExpression(visualizationState, framePublicAPI);
|
||||
|
||||
const expressionContext = removeDateRange
|
||||
? { query: framePublicAPI.query }
|
||||
? { query: framePublicAPI.query, filters: framePublicAPI.filters }
|
||||
: {
|
||||
query: framePublicAPI.query,
|
||||
timeRange: {
|
||||
from: framePublicAPI.dateRange.fromDate,
|
||||
to: framePublicAPI.dateRange.toDate,
|
||||
},
|
||||
filters: framePublicAPI.filters,
|
||||
};
|
||||
|
||||
const completeExpression = prependDatasourceExpression(
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { buildExistsFilter } from '@kbn/es-query';
|
||||
import { getSavedObjectFormat, Props } from './save';
|
||||
import { createMockDatasource, createMockVisualization } from '../mocks';
|
||||
|
||||
|
@ -36,6 +37,7 @@ describe('save editor frame state', () => {
|
|||
},
|
||||
query: { query: '', language: 'lucene' },
|
||||
dateRange: { fromDate: 'now-7d', toDate: 'now' },
|
||||
filters: [buildExistsFilter({ name: '@timestamp' }, { id: 'indexpattern' })],
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -83,7 +85,13 @@ describe('save editor frame state', () => {
|
|||
},
|
||||
visualization: { things: '4_vis_persisted' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
filters: [
|
||||
{
|
||||
meta: { index: 'indexpattern' },
|
||||
exists: { field: '@timestamp' },
|
||||
},
|
||||
],
|
||||
dateRange: { fromDate: 'now-7d', toDate: 'now' },
|
||||
},
|
||||
title: 'bbb',
|
||||
type: 'lens',
|
||||
|
|
|
@ -58,7 +58,8 @@ export function getSavedObjectFormat({
|
|||
},
|
||||
visualization: visualization.getPersistableState(state.visualization.state),
|
||||
query: framePublicAPI.query,
|
||||
filters: [], // TODO: Support filters
|
||||
filters: framePublicAPI.filters,
|
||||
dateRange: framePublicAPI.dateRange,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ describe('editor_frame state management', () => {
|
|||
core: coreMock.createSetup(),
|
||||
dateRange: { fromDate: 'now-7d', toDate: 'now' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -279,7 +279,7 @@ describe('suggestion_panel', () => {
|
|||
|
||||
expect(passedExpression).toMatchInlineSnapshot(`
|
||||
"kibana
|
||||
| kibana_context timeRange=\\"{\\\\\\"from\\\\\\":\\\\\\"now-7d\\\\\\",\\\\\\"to\\\\\\":\\\\\\"now\\\\\\"}\\" query=\\"{\\\\\\"query\\\\\\":\\\\\\"\\\\\\",\\\\\\"language\\\\\\":\\\\\\"lucene\\\\\\"}\\"
|
||||
| kibana_context timeRange=\\"{\\\\\\"from\\\\\\":\\\\\\"now-7d\\\\\\",\\\\\\"to\\\\\\":\\\\\\"now\\\\\\"}\\" query=\\"{\\\\\\"query\\\\\\":\\\\\\"\\\\\\",\\\\\\"language\\\\\\":\\\\\\"lucene\\\\\\"}\\" filters=\\"[]\\"
|
||||
| lens_merge_tables layerIds=\\"first\\" tables={datasource_expression}
|
||||
| test
|
||||
| expression"
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { buildExistsFilter } from '@kbn/es-query';
|
||||
import { ExpressionRendererProps } from '../../../../../../../src/legacy/core_plugins/expressions/public';
|
||||
import { Visualization, FramePublicAPI, TableSuggestion } from '../../types';
|
||||
import {
|
||||
|
@ -153,7 +155,9 @@ describe('workspace_panel', () => {
|
|||
},
|
||||
Object {
|
||||
"arguments": Object {
|
||||
"filters": Array [],
|
||||
"filters": Array [
|
||||
"[]",
|
||||
],
|
||||
"query": Array [
|
||||
"{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}",
|
||||
],
|
||||
|
@ -244,39 +248,39 @@ describe('workspace_panel', () => {
|
|||
expect(
|
||||
(instance.find(expressionRendererMock).prop('expression') as Ast).chain[2].arguments.tables
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {},
|
||||
"function": "datasource",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"type": "expression",
|
||||
},
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {},
|
||||
"function": "datasource2",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"type": "expression",
|
||||
},
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {},
|
||||
"function": "datasource2",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"type": "expression",
|
||||
},
|
||||
]
|
||||
`);
|
||||
Array [
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {},
|
||||
"function": "datasource",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"type": "expression",
|
||||
},
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {},
|
||||
"function": "datasource2",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"type": "expression",
|
||||
},
|
||||
Object {
|
||||
"chain": Array [
|
||||
Object {
|
||||
"arguments": Object {},
|
||||
"function": "datasource2",
|
||||
"type": "function",
|
||||
},
|
||||
],
|
||||
"type": "expression",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should run the expression again if the date range changes', async () => {
|
||||
|
@ -332,6 +336,62 @@ describe('workspace_panel', () => {
|
|||
expect(expressionRendererMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should run the expression again if the filters change', async () => {
|
||||
const framePublicAPI = createMockFramePublicAPI();
|
||||
framePublicAPI.datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
};
|
||||
mockDatasource.getLayers.mockReturnValue(['first']);
|
||||
|
||||
mockDatasource.toExpression
|
||||
.mockReturnValueOnce('datasource')
|
||||
.mockReturnValueOnce('datasource second');
|
||||
|
||||
expressionRendererMock = jest.fn(_arg => <span />);
|
||||
|
||||
instance = mount(
|
||||
<InnerWorkspacePanel
|
||||
activeDatasourceId={'mock'}
|
||||
datasourceStates={{
|
||||
mock: {
|
||||
state: {},
|
||||
isLoading: false,
|
||||
},
|
||||
}}
|
||||
datasourceMap={{
|
||||
mock: mockDatasource,
|
||||
}}
|
||||
framePublicAPI={framePublicAPI}
|
||||
activeVisualizationId="vis"
|
||||
visualizationMap={{
|
||||
vis: { ...mockVisualization, toExpression: () => 'vis' },
|
||||
}}
|
||||
visualizationState={{}}
|
||||
dispatch={() => {}}
|
||||
ExpressionRenderer={expressionRendererMock}
|
||||
core={coreMock.createSetup()}
|
||||
/>
|
||||
);
|
||||
|
||||
// "wait" for the expression to execute
|
||||
await waitForPromises();
|
||||
instance.update();
|
||||
|
||||
expect(expressionRendererMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
instance.setProps({
|
||||
framePublicAPI: {
|
||||
...framePublicAPI,
|
||||
filters: [buildExistsFilter({ name: 'myfield' }, { id: 'index1' })],
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
instance.update();
|
||||
|
||||
expect(expressionRendererMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
describe('expression failures', () => {
|
||||
it('should show an error message if the expression fails to parse', () => {
|
||||
mockDatasource.toExpression.mockReturnValue('|||');
|
||||
|
|
|
@ -142,6 +142,7 @@ export function InnerWorkspacePanel({
|
|||
datasourceStates,
|
||||
framePublicAPI.dateRange,
|
||||
framePublicAPI.query,
|
||||
framePublicAPI.filters,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -77,6 +77,7 @@ export function createMockFramePublicAPI(): FrameMock {
|
|||
removeLayers: jest.fn(),
|
||||
dateRange: { fromDate: 'now-7d', toDate: 'now' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@ describe('editor_frame plugin', () => {
|
|||
onChange: jest.fn(),
|
||||
dateRange: { fromDate: '', toDate: '' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
});
|
||||
instance.unmount();
|
||||
}).not.toThrowError();
|
||||
|
@ -73,6 +74,7 @@ describe('editor_frame plugin', () => {
|
|||
onChange: jest.fn(),
|
||||
dateRange: { fromDate: '', toDate: '' },
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
});
|
||||
instance.unmount();
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@ export class EditorFramePlugin {
|
|||
const createInstance = (): EditorFrameInstance => {
|
||||
let domElement: Element;
|
||||
return {
|
||||
mount: (element, { doc, onError, dateRange, query, onChange }) => {
|
||||
mount: (element, { doc, onError, dateRange, query, filters, savedQuery, onChange }) => {
|
||||
domElement = element;
|
||||
const firstDatasourceId = Object.keys(this.datasources)[0];
|
||||
const firstVisualizationId = Object.keys(this.visualizations)[0];
|
||||
|
@ -97,6 +97,8 @@ export class EditorFramePlugin {
|
|||
doc={doc}
|
||||
dateRange={dateRange}
|
||||
query={query}
|
||||
filters={filters}
|
||||
savedQuery={savedQuery}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</I18nProvider>,
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { IndexPatternPrivateState, IndexPatternColumn } from './indexpattern';
|
||||
import { createMockedDragDropContext } from './mocks';
|
||||
|
@ -12,6 +11,7 @@ import { InnerIndexPatternDataPanel, IndexPatternDataPanel, MemoizedDataPanel }
|
|||
import { FieldItem } from './field_item';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { ChangeIndexPattern } from './change_indexpattern';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
|
@ -220,6 +220,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
toDate: 'now',
|
||||
},
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
showEmptyFields: false,
|
||||
onToggleEmptyFields: jest.fn(),
|
||||
};
|
||||
|
@ -231,7 +232,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
...initialState,
|
||||
layers: { first: { indexPatternId: '1', columnOrder: [], columns: {} } },
|
||||
};
|
||||
const wrapper = shallow(
|
||||
const wrapper = shallowWithIntl(
|
||||
<IndexPatternDataPanel
|
||||
{...defaultProps}
|
||||
state={state}
|
||||
|
@ -258,7 +259,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
second: { indexPatternId: '1', columnOrder: [], columns: {} },
|
||||
},
|
||||
};
|
||||
const wrapper = shallow(
|
||||
const wrapper = shallowWithIntl(
|
||||
<IndexPatternDataPanel
|
||||
{...defaultProps}
|
||||
state={state}
|
||||
|
@ -287,7 +288,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
const wrapper = shallow(
|
||||
const wrapper = shallowWithIntl(
|
||||
<IndexPatternDataPanel
|
||||
{...defaultProps}
|
||||
state={state}
|
||||
|
@ -305,14 +306,14 @@ describe('IndexPattern Data Panel', () => {
|
|||
});
|
||||
|
||||
it('should render a warning if there are no index patterns', () => {
|
||||
const wrapper = shallow(
|
||||
const wrapper = shallowWithIntl(
|
||||
<InnerIndexPatternDataPanel {...defaultProps} currentIndexPatternId="" indexPatterns={{}} />
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="indexPattern-no-indexpatterns"]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should call setState when the index pattern is switched', async () => {
|
||||
const wrapper = shallow(<InnerIndexPatternDataPanel {...defaultProps} />);
|
||||
const wrapper = shallowWithIntl(<InnerIndexPatternDataPanel {...defaultProps} />);
|
||||
|
||||
wrapper.find(ChangeIndexPattern).prop('onChangeIndexPattern')('2');
|
||||
|
||||
|
@ -333,7 +334,9 @@ describe('IndexPattern Data Panel', () => {
|
|||
},
|
||||
});
|
||||
const updateFields = jest.fn();
|
||||
mount(<InnerIndexPatternDataPanel {...defaultProps} updateFieldsWithCounts={updateFields} />);
|
||||
mountWithIntl(
|
||||
<InnerIndexPatternDataPanel {...defaultProps} updateFieldsWithCounts={updateFields} />
|
||||
);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
@ -400,7 +403,9 @@ describe('IndexPattern Data Panel', () => {
|
|||
|
||||
const props = { ...defaultProps, indexPatterns: newIndexPatterns };
|
||||
|
||||
mount(<InnerIndexPatternDataPanel {...props} updateFieldsWithCounts={updateFields} />);
|
||||
mountWithIntl(
|
||||
<InnerIndexPatternDataPanel {...props} updateFieldsWithCounts={updateFields} />
|
||||
);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
@ -410,7 +415,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
|
||||
describe('while showing empty fields', () => {
|
||||
it('should list all supported fields in the pattern sorted alphabetically', async () => {
|
||||
const wrapper = shallow(
|
||||
const wrapper = shallowWithIntl(
|
||||
<InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
|
||||
);
|
||||
|
||||
|
@ -424,7 +429,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
});
|
||||
|
||||
it('should filter down by name', () => {
|
||||
const wrapper = shallow(
|
||||
const wrapper = shallowWithIntl(
|
||||
<InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
|
||||
);
|
||||
|
||||
|
@ -440,7 +445,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
});
|
||||
|
||||
it('should filter down by type', () => {
|
||||
const wrapper = mount(
|
||||
const wrapper = mountWithIntl(
|
||||
<InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
|
||||
);
|
||||
|
||||
|
@ -461,7 +466,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
});
|
||||
|
||||
it('should toggle type if clicked again', () => {
|
||||
const wrapper = mount(
|
||||
const wrapper = mountWithIntl(
|
||||
<InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
|
||||
);
|
||||
|
||||
|
@ -489,7 +494,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
});
|
||||
|
||||
it('should filter down by type and by name', () => {
|
||||
const wrapper = mount(
|
||||
const wrapper = mountWithIntl(
|
||||
<InnerIndexPatternDataPanel {...defaultProps} showEmptyFields={true} />
|
||||
);
|
||||
|
||||
|
@ -537,7 +542,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
});
|
||||
|
||||
it('should list all supported fields in the pattern sorted alphabetically', async () => {
|
||||
const wrapper = shallow(<InnerIndexPatternDataPanel {...emptyFieldsTestProps} />);
|
||||
const wrapper = shallowWithIntl(<InnerIndexPatternDataPanel {...emptyFieldsTestProps} />);
|
||||
|
||||
expect(wrapper.find(FieldItem).map(fieldItem => fieldItem.prop('field').name)).toEqual([
|
||||
'bytes',
|
||||
|
@ -546,7 +551,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
});
|
||||
|
||||
it('should filter down by name', () => {
|
||||
const wrapper = shallow(
|
||||
const wrapper = shallowWithIntl(
|
||||
<InnerIndexPatternDataPanel {...emptyFieldsTestProps} showEmptyFields={true} />
|
||||
);
|
||||
|
||||
|
@ -562,7 +567,7 @@ describe('IndexPattern Data Panel', () => {
|
|||
});
|
||||
|
||||
it('should allow removing the filter for data', () => {
|
||||
const wrapper = mount(<InnerIndexPatternDataPanel {...emptyFieldsTestProps} />);
|
||||
const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...emptyFieldsTestProps} />);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="lnsIndexPatternFiltersToggle"]')
|
||||
|
|
|
@ -26,7 +26,6 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { Query } from 'src/plugins/data/common';
|
||||
import { DatasourceDataPanelProps, DataType } from '../types';
|
||||
import { IndexPatternPrivateState, IndexPatternField, IndexPattern } from './indexpattern';
|
||||
import { ChildDragDropProvider, DragContextState } from '../drag_drop';
|
||||
|
@ -66,6 +65,7 @@ export function IndexPatternDataPanel({
|
|||
dragDropContext,
|
||||
core,
|
||||
query,
|
||||
filters,
|
||||
dateRange,
|
||||
}: DatasourceDataPanelProps<IndexPatternPrivateState>) {
|
||||
const { indexPatterns, currentIndexPatternId } = state;
|
||||
|
@ -114,6 +114,7 @@ export function IndexPatternDataPanel({
|
|||
indexPatterns={indexPatterns}
|
||||
query={query}
|
||||
dateRange={dateRange}
|
||||
filters={filters}
|
||||
dragDropContext={dragDropContext}
|
||||
showEmptyFields={state.showEmptyFields}
|
||||
onToggleEmptyFields={onToggleEmptyFields}
|
||||
|
@ -146,18 +147,16 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
indexPatterns,
|
||||
query,
|
||||
dateRange,
|
||||
filters,
|
||||
dragDropContext,
|
||||
onChangeIndexPattern,
|
||||
updateFieldsWithCounts,
|
||||
showEmptyFields,
|
||||
onToggleEmptyFields,
|
||||
core,
|
||||
}: Partial<DatasourceDataPanelProps> & {
|
||||
}: Pick<DatasourceDataPanelProps, Exclude<keyof DatasourceDataPanelProps, 'state' | 'setState'>> & {
|
||||
currentIndexPatternId: string;
|
||||
indexPatterns: Record<string, IndexPattern>;
|
||||
dateRange: DatasourceDataPanelProps['dateRange'];
|
||||
query: Query;
|
||||
core: DatasourceDataPanelProps['core'];
|
||||
dragDropContext: DragContextState;
|
||||
showEmptyFields: boolean;
|
||||
onToggleEmptyFields: () => void;
|
||||
|
@ -487,6 +486,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
|
|||
exists={overallField ? !!overallField.exists : false}
|
||||
dateRange={dateRange}
|
||||
query={query}
|
||||
filters={filters}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { EuiLoadingSpinner, EuiPopover } from '@elastic/eui';
|
||||
import { FieldItem, FieldItemProps } from './field_item';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
// Formatter must be mocked to return a string, or the rendering will fail
|
||||
jest.mock('../../../../../../src/legacy/ui/public/registry/field_formats', () => ({
|
||||
fieldFormats: {
|
||||
getDefaultInstance: jest.fn().mockReturnValue({
|
||||
convert: jest.fn().mockReturnValue((s: unknown) => JSON.stringify(s)),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const waitForPromises = () => new Promise(resolve => setTimeout(resolve));
|
||||
|
||||
const indexPattern = {
|
||||
id: '1',
|
||||
title: 'my-fake-index-pattern',
|
||||
timeFieldName: 'timestamp',
|
||||
fields: [
|
||||
{
|
||||
name: 'timestamp',
|
||||
type: 'date',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'bytes',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'memory',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'unsupported',
|
||||
type: 'geo',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'source',
|
||||
type: 'string',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('IndexPattern Field Item', () => {
|
||||
let defaultProps: FieldItemProps;
|
||||
let core: ReturnType<typeof coreMock['createSetup']>;
|
||||
|
||||
beforeEach(() => {
|
||||
core = coreMock.createSetup();
|
||||
core.http.post.mockClear();
|
||||
defaultProps = {
|
||||
indexPattern,
|
||||
core,
|
||||
highlight: '',
|
||||
dateRange: {
|
||||
fromDate: 'now-7d',
|
||||
toDate: 'now',
|
||||
},
|
||||
query: { query: '', language: 'lucene' },
|
||||
filters: [],
|
||||
field: {
|
||||
name: 'bytes',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
exists: true,
|
||||
};
|
||||
});
|
||||
|
||||
it('should request field stats every time the button is clicked', async () => {
|
||||
let resolveFunction: (arg: unknown) => void;
|
||||
|
||||
core.http.post.mockImplementation(() => {
|
||||
return new Promise(resolve => {
|
||||
resolveFunction = resolve;
|
||||
});
|
||||
});
|
||||
|
||||
const wrapper = mountWithIntl(<FieldItem {...defaultProps} />);
|
||||
|
||||
wrapper.find('[data-test-subj="lnsFieldListPanelField-bytes"]').simulate('click');
|
||||
|
||||
expect(core.http.post).toHaveBeenCalledWith(
|
||||
`/api/lens/index_stats/my-fake-index-pattern/field`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
dslQuery: {
|
||||
bool: {
|
||||
must: [{ match_all: {} }],
|
||||
filter: [],
|
||||
should: [],
|
||||
must_not: [],
|
||||
},
|
||||
},
|
||||
fromDate: 'now-7d',
|
||||
toDate: 'now',
|
||||
timeFieldName: 'timestamp',
|
||||
field: {
|
||||
name: 'bytes',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true);
|
||||
|
||||
expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(1);
|
||||
|
||||
resolveFunction!({
|
||||
totalDocuments: 4633,
|
||||
sampledDocuments: 4633,
|
||||
sampledValues: 4633,
|
||||
histogram: {
|
||||
buckets: [{ count: 705, key: 0 }],
|
||||
},
|
||||
topValues: {
|
||||
buckets: [{ count: 147, key: 0 }],
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0);
|
||||
|
||||
wrapper.find('[data-test-subj="lnsFieldListPanelField-bytes"]').simulate('click');
|
||||
expect(core.http.post).toHaveBeenCalledTimes(1);
|
||||
|
||||
act(() => {
|
||||
const closePopover = wrapper.find(EuiPopover).prop('closePopover');
|
||||
|
||||
closePopover();
|
||||
});
|
||||
|
||||
expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(false);
|
||||
|
||||
act(() => {
|
||||
wrapper.setProps({
|
||||
dateRange: {
|
||||
fromDate: 'now-14d',
|
||||
toDate: 'now-7d',
|
||||
},
|
||||
query: { query: 'geo.src : "US"', language: 'kuery' },
|
||||
filters: [
|
||||
{
|
||||
match: { phrase: { 'geo.dest': 'US' } },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
wrapper.find('[data-test-subj="lnsFieldListPanelField-bytes"]').simulate('click');
|
||||
|
||||
expect(core.http.post).toHaveBeenCalledTimes(2);
|
||||
expect(core.http.post).toHaveBeenLastCalledWith(
|
||||
`/api/lens/index_stats/my-fake-index-pattern/field`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
dslQuery: {
|
||||
bool: {
|
||||
must: [],
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [{ match_phrase: { 'geo.src': 'US' } }],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
match: { phrase: { 'geo.dest': 'US' } },
|
||||
},
|
||||
],
|
||||
should: [],
|
||||
must_not: [],
|
||||
},
|
||||
},
|
||||
fromDate: 'now-14d',
|
||||
toDate: 'now-7d',
|
||||
timeFieldName: 'timestamp',
|
||||
field: {
|
||||
name: 'bytes',
|
||||
type: 'number',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -34,7 +34,7 @@ import {
|
|||
niceTimeFormatter,
|
||||
} from '@elastic/charts';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toElasticsearchQuery } from '@kbn/es-query';
|
||||
import { Filter, buildEsQuery, getEsQueryConfig } from '@kbn/es-query';
|
||||
import { Query } from 'src/plugins/data/common';
|
||||
// @ts-ignore
|
||||
import { fieldFormats } from '../../../../../../src/legacy/ui/public/registry/field_formats';
|
||||
|
@ -52,6 +52,7 @@ export interface FieldItemProps {
|
|||
exists: boolean;
|
||||
query: Query;
|
||||
dateRange: DatasourceDataPanelProps['dateRange'];
|
||||
filters: Filter[];
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -71,7 +72,7 @@ function wrapOnDot(str?: string) {
|
|||
}
|
||||
|
||||
export function FieldItem(props: FieldItemProps) {
|
||||
const { core, field, indexPattern, highlight, exists, query, dateRange } = props;
|
||||
const { core, field, indexPattern, highlight, exists, query, dateRange, filters } = props;
|
||||
|
||||
const [infoIsOpen, setOpen] = useState(false);
|
||||
|
||||
|
@ -112,7 +113,7 @@ export function FieldItem(props: FieldItemProps) {
|
|||
core.http
|
||||
.post(`/api/lens/index_stats/${indexPattern.title}/field`, {
|
||||
body: JSON.stringify({
|
||||
query: toElasticsearchQuery(query, indexPattern),
|
||||
dslQuery: buildEsQuery(indexPattern, query, filters, getEsQueryConfig(core.uiSettings)),
|
||||
fromDate: dateRange.fromDate,
|
||||
toDate: dateRange.toDate,
|
||||
timeFieldName: indexPattern.timeFieldName,
|
||||
|
|
|
@ -5,10 +5,7 @@
|
|||
*/
|
||||
|
||||
import chromeMock from 'ui/chrome';
|
||||
import { data as dataMock } from '../../../../../../src/legacy/core_plugins/data/public/setup';
|
||||
import { Storage } from 'ui/storage';
|
||||
import { functionsRegistry } from '../../../../../../src/legacy/core_plugins/interpreter/public/registries';
|
||||
import { SavedObjectsClientContract } from 'src/core/public';
|
||||
import {
|
||||
getIndexPatternDatasource,
|
||||
IndexPatternPersistedState,
|
||||
|
@ -25,7 +22,6 @@ jest.mock('../id_generator');
|
|||
jest.mock('ui/chrome');
|
||||
// Contains old and new platform data plugins, used for interpreter and filter ratio
|
||||
jest.mock('ui/new_platform');
|
||||
jest.mock('plugins/data/setup', () => ({ data: { query: { ui: {} } } }));
|
||||
|
||||
const expectedIndexPatterns = {
|
||||
1: {
|
||||
|
@ -138,10 +134,7 @@ describe('IndexPattern Data Source', () => {
|
|||
indexPatternDatasource = getIndexPatternDatasource({
|
||||
chrome: chromeMock,
|
||||
storage: {} as Storage,
|
||||
interpreter: { functionsRegistry },
|
||||
core: coreMock.createSetup(),
|
||||
data: dataMock,
|
||||
savedObjectsClient: {} as SavedObjectsClientContract,
|
||||
core: coreMock.createStart(),
|
||||
});
|
||||
|
||||
persistedState = {
|
||||
|
|
|
@ -8,7 +8,7 @@ import _ from 'lodash';
|
|||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { CoreSetup, SavedObjectsClientContract } from 'src/core/public';
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import { Storage } from 'ui/storage';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
|
@ -20,7 +20,7 @@ import {
|
|||
import { getIndexPatterns } from './loader';
|
||||
import { toExpression } from './to_expression';
|
||||
import { IndexPatternDimensionPanel } from './dimension_panel';
|
||||
import { IndexPatternDatasourcePluginPlugins } from './plugin';
|
||||
import { IndexPatternDatasourceSetupPlugins } from './plugin';
|
||||
import { IndexPatternDataPanel } from './datapanel';
|
||||
import {
|
||||
getDatasourceSuggestionsForField,
|
||||
|
@ -182,14 +182,14 @@ function removeProperty<T>(prop: string, object: Record<string, T>): Record<stri
|
|||
}
|
||||
|
||||
export function getIndexPatternDatasource({
|
||||
core,
|
||||
chrome,
|
||||
core,
|
||||
storage,
|
||||
savedObjectsClient,
|
||||
}: IndexPatternDatasourcePluginPlugins & {
|
||||
core: CoreSetup;
|
||||
}: Pick<IndexPatternDatasourceSetupPlugins, 'chrome'> & {
|
||||
// Core start is being required here because it contains the savedObject client
|
||||
// In the new platform, this plugin wouldn't be initialized until after setup
|
||||
core: CoreStart;
|
||||
storage: Storage;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
}) {
|
||||
const uiSettings = chrome.getUiSettingsClient();
|
||||
// Not stateful. State is persisted to the frame
|
||||
|
@ -307,7 +307,7 @@ export function getIndexPatternDatasource({
|
|||
setState={setState}
|
||||
uiSettings={uiSettings}
|
||||
storage={storage}
|
||||
savedObjectsClient={savedObjectsClient}
|
||||
savedObjectsClient={core.savedObjects.client}
|
||||
layerId={props.layerId}
|
||||
http={core.http}
|
||||
uniqueLabel={columnLabelMap[props.columnId]}
|
||||
|
|
|
@ -5,9 +5,6 @@
|
|||
*/
|
||||
|
||||
import chromeMock from 'ui/chrome';
|
||||
import { data as dataMock } from '../../../../../../src/legacy/core_plugins/data/public/setup';
|
||||
import { functionsRegistry } from '../../../../../../src/legacy/core_plugins/interpreter/public/registries';
|
||||
import { SavedObjectsClientContract } from 'src/core/public';
|
||||
import {
|
||||
getIndexPatternDatasource,
|
||||
IndexPatternPersistedState,
|
||||
|
@ -135,12 +132,9 @@ describe('IndexPattern Data Source suggestions', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
indexPatternDatasource = getIndexPatternDatasource({
|
||||
core: coreMock.createSetup(),
|
||||
core: coreMock.createStart(),
|
||||
chrome: chromeMock,
|
||||
storage: {} as Storage,
|
||||
interpreter: { functionsRegistry },
|
||||
data: dataMock,
|
||||
savedObjectsClient: {} as SavedObjectsClientContract,
|
||||
});
|
||||
|
||||
persistedState = {
|
||||
|
|
|
@ -9,22 +9,20 @@ import { CoreSetup } from 'src/core/public';
|
|||
// The following dependencies on ui/* and src/legacy/core_plugins must be mocked when testing
|
||||
import chrome, { Chrome } from 'ui/chrome';
|
||||
import { Storage } from 'ui/storage';
|
||||
import { npSetup } from 'ui/new_platform';
|
||||
import { npSetup, npStart } from 'ui/new_platform';
|
||||
import { ExpressionFunction } from '../../../../../../src/legacy/core_plugins/interpreter/public';
|
||||
import { functionsRegistry } from '../../../../../../src/legacy/core_plugins/interpreter/public/registries';
|
||||
import { getIndexPatternDatasource } from './indexpattern';
|
||||
import { renameColumns } from './rename_columns';
|
||||
import { calculateFilterRatio } from './filter_ratio';
|
||||
import { setup as dataSetup } from '../../../../../../src/legacy/core_plugins/data/public/legacy';
|
||||
|
||||
// TODO these are intermediary types because interpreter is not typed yet
|
||||
// They can get replaced by references to the real interfaces as soon as they
|
||||
// are available
|
||||
|
||||
export interface IndexPatternDatasourcePluginPlugins {
|
||||
export interface IndexPatternDatasourceSetupPlugins {
|
||||
chrome: Chrome;
|
||||
interpreter: InterpreterSetup;
|
||||
data: typeof dataSetup;
|
||||
}
|
||||
|
||||
export interface InterpreterSetup {
|
||||
|
@ -37,17 +35,9 @@ export interface InterpreterSetup {
|
|||
class IndexPatternDatasourcePlugin {
|
||||
constructor() {}
|
||||
|
||||
setup(core: CoreSetup, { interpreter, data }: IndexPatternDatasourcePluginPlugins) {
|
||||
setup(core: CoreSetup, { interpreter }: IndexPatternDatasourceSetupPlugins) {
|
||||
interpreter.functionsRegistry.register(() => renameColumns);
|
||||
interpreter.functionsRegistry.register(() => calculateFilterRatio);
|
||||
return getIndexPatternDatasource({
|
||||
core,
|
||||
chrome,
|
||||
interpreter,
|
||||
data,
|
||||
storage: new Storage(localStorage),
|
||||
savedObjectsClient: chrome.getSavedObjectsClient(),
|
||||
});
|
||||
}
|
||||
|
||||
stop() {}
|
||||
|
@ -55,12 +45,18 @@ class IndexPatternDatasourcePlugin {
|
|||
|
||||
const plugin = new IndexPatternDatasourcePlugin();
|
||||
|
||||
export const indexPatternDatasourceSetup = () =>
|
||||
export const indexPatternDatasourceSetup = () => {
|
||||
plugin.setup(npSetup.core, {
|
||||
chrome,
|
||||
interpreter: {
|
||||
functionsRegistry,
|
||||
},
|
||||
data: dataSetup,
|
||||
});
|
||||
|
||||
return getIndexPatternDatasource({
|
||||
core: npStart.core,
|
||||
chrome,
|
||||
storage: new Storage(localStorage),
|
||||
});
|
||||
};
|
||||
export const indexPatternDatasourceStop = () => plugin.stop();
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { SavedObjectAttributes } from 'src/core/server';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { Query } from 'src/plugins/data/common';
|
||||
import { FramePublicAPI } from '../types';
|
||||
|
||||
export interface Document {
|
||||
id?: string;
|
||||
|
@ -23,6 +24,7 @@ export interface Document {
|
|||
visualization: unknown;
|
||||
query: Query;
|
||||
filters: Filter[];
|
||||
dateRange?: FramePublicAPI['dateRange'];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
*/
|
||||
|
||||
import { Ast } from '@kbn/interpreter/common';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { EuiIconType } from '@elastic/eui/src/components/icon/icon';
|
||||
import { CoreSetup } from 'src/core/public';
|
||||
import { Query } from 'src/plugins/data/common';
|
||||
import { SavedQuery } from 'src/legacy/core_plugins/data/public';
|
||||
import { KibanaDatatable } from '../../../../../src/legacy/core_plugins/interpreter/common';
|
||||
import { DragContextState } from './drag_drop';
|
||||
import { Document } from './persistence';
|
||||
|
@ -25,9 +27,14 @@ export interface EditorFrameProps {
|
|||
toDate: string;
|
||||
};
|
||||
query: Query;
|
||||
filters: Filter[];
|
||||
savedQuery?: SavedQuery;
|
||||
|
||||
// Frame loader (app or embeddable) is expected to call this when it loads and updates
|
||||
onChange: (newState: { indexPatternTitles: string[]; doc: Document }) => void;
|
||||
onChange: (newState: {
|
||||
filterableIndexPatterns: DatasourceMetaData['filterableIndexPatterns'];
|
||||
doc: Document;
|
||||
}) => void;
|
||||
}
|
||||
export interface EditorFrameInstance {
|
||||
mount: (element: Element, props: EditorFrameProps) => void;
|
||||
|
@ -165,6 +172,7 @@ export interface DatasourceDataPanelProps<T = unknown> {
|
|||
core: Pick<CoreSetup, 'http' | 'notifications' | 'uiSettings'>;
|
||||
query: Query;
|
||||
dateRange: FramePublicAPI['dateRange'];
|
||||
filters: Filter[];
|
||||
}
|
||||
|
||||
// The only way a visualization has to restrict the query building
|
||||
|
@ -278,11 +286,13 @@ export interface VisualizationSuggestion<T = unknown> {
|
|||
|
||||
export interface FramePublicAPI {
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>;
|
||||
|
||||
dateRange: {
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
};
|
||||
query: Query;
|
||||
filters: Filter[];
|
||||
|
||||
// Adds a new layer. This has a side effect of updating the datasource state
|
||||
addNewLayer: () => string;
|
||||
|
|
|
@ -7,7 +7,7 @@ Run all tests from the `x-pack` root directory
|
|||
- Unit tests: `node scripts/jest --watch lens`
|
||||
- Functional tests:
|
||||
- Run `node scripts/functional_tests_server`
|
||||
- Run `node ../scripts/functional_test_runner.js --config ./test/functional/config.js`
|
||||
- Run `node ../scripts/functional_test_runner.js --config ./test/functional/config.js --grep="lens app"`
|
||||
- You may want to comment out all imports except for Lens in the config file.
|
||||
- API Functional tests:
|
||||
- Run `node scripts/functional_tests_server`
|
||||
|
|
|
@ -24,7 +24,7 @@ export async function initFieldsRoute(setup: CoreSetup) {
|
|||
}),
|
||||
body: schema.object(
|
||||
{
|
||||
query: schema.object({}, { allowUnknowns: true }),
|
||||
dslQuery: schema.object({}, { allowUnknowns: true }),
|
||||
fromDate: schema.string(),
|
||||
toDate: schema.string(),
|
||||
timeFieldName: schema.string(),
|
||||
|
@ -43,10 +43,10 @@ export async function initFieldsRoute(setup: CoreSetup) {
|
|||
},
|
||||
async (context, req, res) => {
|
||||
const requestClient = context.core.elasticsearch.dataClient;
|
||||
const { fromDate, toDate, timeFieldName, field, query } = req.body;
|
||||
const { fromDate, toDate, timeFieldName, field, dslQuery } = req.body;
|
||||
|
||||
try {
|
||||
const filters = {
|
||||
const query = {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
|
@ -57,7 +57,7 @@ export async function initFieldsRoute(setup: CoreSetup) {
|
|||
},
|
||||
},
|
||||
},
|
||||
query,
|
||||
dslQuery,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -66,7 +66,7 @@ export async function initFieldsRoute(setup: CoreSetup) {
|
|||
requestClient.callAsCurrentUser('search', {
|
||||
index: req.params.indexPatternTitle,
|
||||
body: {
|
||||
query: filters,
|
||||
query,
|
||||
aggs,
|
||||
},
|
||||
// The hits total changed in 7.0 from number to object, unless this flag is set
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import { Request } from 'hapi';
|
||||
|
||||
// @ts-ignore no module definition
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
// @ts-ignore no module definition
|
||||
import { createGenerateCsv } from '../../../csv/server/lib/generate_csv';
|
||||
|
|
|
@ -35,7 +35,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
.post('/api/lens/index_stats/logstash/field')
|
||||
.set(COMMON_HEADERS)
|
||||
.send({
|
||||
query: { match_all: {} },
|
||||
dslQuery: { match_all: {} },
|
||||
fromDate: TEST_START_TIME,
|
||||
toDate: TEST_END_TIME,
|
||||
timeFieldName: '@timestamp',
|
||||
|
@ -52,7 +52,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
.post('/api/lens/index_stats/logstash-2015.09.22/field')
|
||||
.set(COMMON_HEADERS)
|
||||
.send({
|
||||
query: { match_all: {} },
|
||||
dslQuery: { match_all: {} },
|
||||
fromDate: TEST_START_TIME,
|
||||
toDate: TEST_END_TIME,
|
||||
timeFieldName: '@timestamp',
|
||||
|
@ -163,7 +163,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
.post('/api/lens/index_stats/logstash-2015.09.22/field')
|
||||
.set(COMMON_HEADERS)
|
||||
.send({
|
||||
query: { match_all: {} },
|
||||
dslQuery: { match_all: {} },
|
||||
fromDate: TEST_START_TIME,
|
||||
toDate: TEST_END_TIME,
|
||||
timeFieldName: '@timestamp',
|
||||
|
@ -200,7 +200,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
.post('/api/lens/index_stats/logstash-2015.09.22/field')
|
||||
.set(COMMON_HEADERS)
|
||||
.send({
|
||||
query: { match_all: {} },
|
||||
dslQuery: { match_all: {} },
|
||||
fromDate: TEST_START_TIME,
|
||||
toDate: TEST_END_TIME,
|
||||
timeFieldName: '@timestamp',
|
||||
|
@ -261,6 +261,29 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply filters and queries', async () => {
|
||||
const { body } = await supertest
|
||||
.post('/api/lens/index_stats/logstash-2015.09.22/field')
|
||||
.set(COMMON_HEADERS)
|
||||
.send({
|
||||
dslQuery: {
|
||||
bool: {
|
||||
filter: [{ match: { 'geo.src': 'US' } }],
|
||||
},
|
||||
},
|
||||
fromDate: TEST_START_TIME,
|
||||
toDate: TEST_END_TIME,
|
||||
timeFieldName: '@timestamp',
|
||||
field: {
|
||||
name: 'bytes',
|
||||
type: 'number',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(body.totalDocuments).to.eql(425);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue