[lens] Use top nav in Lens app (#46190) (#47238)

* [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:
Wylie Conlon 2019-10-04 11:16:05 -04:00 committed by GitHub
parent 100865a57e
commit f6c0ea623c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1097 additions and 394 deletions

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

View file

@ -17,5 +17,6 @@
* under the License.
*/
export * from './es_query';
export * from './kuery';
export * from './filters';

View file

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

View file

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

View file

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

View file

@ -11,7 +11,6 @@
}
.lnsApp__header {
padding: $euiSize;
border-bottom: $euiBorderThin;
}

View file

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

View file

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

View file

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

View file

@ -27,6 +27,7 @@ function mockFrame(): FramePublicAPI {
fromDate: 'now-7d',
toDate: 'now',
},
filters: [],
};
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,6 +26,7 @@ describe('editor_frame state management', () => {
core: coreMock.createSetup(),
dateRange: { fromDate: 'now-7d', toDate: 'now' },
query: { query: '', language: 'lucene' },
filters: [],
};
});

View file

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

View file

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

View file

@ -142,6 +142,7 @@ export function InnerWorkspacePanel({
datasourceStates,
framePublicAPI.dateRange,
framePublicAPI.query,
framePublicAPI.filters,
]);
useEffect(() => {

View file

@ -77,6 +77,7 @@ export function createMockFramePublicAPI(): FrameMock {
removeLayers: jest.fn(),
dateRange: { fromDate: 'now-7d', toDate: 'now' },
query: { query: '', language: 'lucene' },
filters: [],
};
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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