diff --git a/packages/kbn-es-query/src/es_query/index.d.ts b/packages/kbn-es-query/src/es_query/index.d.ts new file mode 100644 index 0000000000000..9510a18441e53 --- /dev/null +++ b/packages/kbn-es-query/src/es_query/index.d.ts @@ -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; +}; diff --git a/packages/kbn-es-query/src/index.d.ts b/packages/kbn-es-query/src/index.d.ts index 873636a28889f..ca4455da33f45 100644 --- a/packages/kbn-es-query/src/index.d.ts +++ b/packages/kbn-es-query/src/index.d.ts @@ -17,5 +17,6 @@ * under the License. */ +export * from './es_query'; export * from './kuery'; export * from './filters'; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index d96e3be8d7e38..b9554310e2413 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -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 { diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx index ed2a6638aba11..06ceace7e9e44 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx @@ -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; } diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts index 399a65041b664..c64757de5fb0a 100644 --- a/x-pack/legacy/plugins/lens/index.ts +++ b/x-pack/legacy/plugins/lens/index.ts @@ -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'], }, diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/_app.scss b/x-pack/legacy/plugins/lens/public/app_plugin/_app.scss index 382a3f5522daf..ed3a178cdd5ea 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/_app.scss +++ b/x-pack/legacy/plugins/lens/public/app_plugin/_app.scss @@ -11,7 +11,6 @@ } .lnsApp__header { - padding: $euiSize; border-bottom: $euiBorderThin; } diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx index a8df5eafe71ff..103697ef9148a 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.test.tsx @@ -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 { }; } +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; let core: ReturnType; 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(() =>
), 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(); + + 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(); - 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(); 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(); - 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(); - 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(TopNavMenu).toHaveBeenCalledWith( + expect.objectContaining({ + 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(QueryBarTopRow).toHaveBeenCalledWith( + expect(TopNavMenu).toHaveBeenLastCalledWith( expect.objectContaining({ - indexPatterns: ['newIndex'], + 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(); - 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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', () => { diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index 9c484e19789e9..3ee901d201aa5 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -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({ 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(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({
- - { + 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, - }); - }} - onChange={localQueryBarState => { - setState({ ...state, localQueryBarState }); + query: query || s.query, + })); }} - 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} />
@@ -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({ ); } + +export async function getAllIndexPatterns( + ids: Array<{ id: string }>, + indexPatternsService: IndexPatternsService, + notifications: NotificationsStart +): Promise { + 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); + } +} diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx index 5e81785132616..3b3b12533d74b 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -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(); diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx index 177dfc9577028..f649564b2231a 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx @@ -27,6 +27,7 @@ function mockFrame(): FramePublicAPI { fromDate: 'now-7d', toDate: 'now', }, + filters: [], }; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx index 6229e558d1dab..115e8cbf002c3 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx @@ -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); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx index 0b4d7ba217532..22766b86a4b15 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx @@ -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\\":\\"\\"}", - ], - }, - "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", - }, - ], + 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": "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( + + ); + }); + + 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); + }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 054229bde98fb..5d623fa86cd86 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -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 ( } configPanel={ diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts index da7ddee67453e..1ddfc54cc187b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts @@ -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( diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts index 6bfe8f70d93c4..b898d33f7a7b1 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts @@ -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', diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts index 6c414d9866033..fc567f2d5dab8 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts @@ -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, }, }; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts index aa6d7ded87ed9..5168059a33258 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts @@ -26,6 +26,7 @@ describe('editor_frame state management', () => { core: coreMock.createSetup(), dateRange: { fromDate: 'now-7d', toDate: 'now' }, query: { query: '', language: 'lucene' }, + filters: [], }; }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx index 93283534e1186..82ca3d01b73ca 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx @@ -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" diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index fc224db743dca..ddb82565e4b8b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -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 => ); + + instance = mount( + '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('|||'); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx index 6d0ab402a2971..66fac5d6cf705 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx @@ -142,6 +142,7 @@ export function InnerWorkspacePanel({ datasourceStates, framePublicAPI.dateRange, framePublicAPI.query, + framePublicAPI.filters, ]); useEffect(() => { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx index 97e1fe8393fc3..f349585ce88a4 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx @@ -77,6 +77,7 @@ export function createMockFramePublicAPI(): FrameMock { removeLayers: jest.fn(), dateRange: { fromDate: 'now-7d', toDate: 'now' }, query: { query: '', language: 'lucene' }, + filters: [], }; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx index 7b21ec0cac1c2..f48a8b467e728 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx @@ -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(); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index e27c2e54500cf..cb81ec3d69985 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -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} /> , diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx index 9e20db9276ae3..891eb9415d3a4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx @@ -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( { second: { indexPatternId: '1', columnOrder: [], columns: {} }, }, }; - const wrapper = shallow( + const wrapper = shallowWithIntl( { }, }, }; - const wrapper = shallow( + const wrapper = shallowWithIntl( { }); it('should render a warning if there are no index patterns', () => { - const wrapper = shallow( + const wrapper = shallowWithIntl( ); 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(); + const wrapper = shallowWithIntl(); wrapper.find(ChangeIndexPattern).prop('onChangeIndexPattern')('2'); @@ -333,7 +334,9 @@ describe('IndexPattern Data Panel', () => { }, }); const updateFields = jest.fn(); - mount(); + mountWithIntl( + + ); await waitForPromises(); @@ -400,7 +403,9 @@ describe('IndexPattern Data Panel', () => { const props = { ...defaultProps, indexPatterns: newIndexPatterns }; - mount(); + mountWithIntl( + + ); 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( ); @@ -424,7 +429,7 @@ describe('IndexPattern Data Panel', () => { }); it('should filter down by name', () => { - const wrapper = shallow( + const wrapper = shallowWithIntl( ); @@ -440,7 +445,7 @@ describe('IndexPattern Data Panel', () => { }); it('should filter down by type', () => { - const wrapper = mount( + const wrapper = mountWithIntl( ); @@ -461,7 +466,7 @@ describe('IndexPattern Data Panel', () => { }); it('should toggle type if clicked again', () => { - const wrapper = mount( + const wrapper = mountWithIntl( ); @@ -489,7 +494,7 @@ describe('IndexPattern Data Panel', () => { }); it('should filter down by type and by name', () => { - const wrapper = mount( + const wrapper = mountWithIntl( ); @@ -537,7 +542,7 @@ describe('IndexPattern Data Panel', () => { }); it('should list all supported fields in the pattern sorted alphabetically', async () => { - const wrapper = shallow(); + const wrapper = shallowWithIntl(); 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( ); @@ -562,7 +567,7 @@ describe('IndexPattern Data Panel', () => { }); it('should allow removing the filter for data', () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); wrapper .find('[data-test-subj="lnsIndexPatternFiltersToggle"]') diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx index da42113b4e7b4..11d6228251025 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx @@ -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) { 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 & { +}: Pick> & { currentIndexPatternId: string; indexPatterns: Record; - 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} /> ); })} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx new file mode 100644 index 0000000000000..9956c0ec33061 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx @@ -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; + + 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(); + + 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, + }, + }), + } + ); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx index 62591bdf1e081..af0612be8dc2f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx @@ -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, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts index 3576916e81868..98d6ed6f26869 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts @@ -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 = { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index afdbcf5b684c7..f285de4dcbf9d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -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(prop: string, object: Record): Record & { + // 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]} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx index 841d59b602ee8..b7e23b36d55c3 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx @@ -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 = { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx index 581c08f832b67..7e2956cf2bb4b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx @@ -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(); diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts index 5fa7e3f0aca4a..77155b2add87a 100644 --- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts @@ -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']; }; } diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 217a694a1861c..71ee2d4c25963 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -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 { core: Pick; 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 { export interface FramePublicAPI { datasourceLayers: Record; + 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; diff --git a/x-pack/legacy/plugins/lens/readme.md b/x-pack/legacy/plugins/lens/readme.md index 0ea0778dd17ef..60b4266edadb3 100644 --- a/x-pack/legacy/plugins/lens/readme.md +++ b/x-pack/legacy/plugins/lens/readme.md @@ -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` diff --git a/x-pack/legacy/plugins/lens/server/routes/field_stats.ts b/x-pack/legacy/plugins/lens/server/routes/field_stats.ts index a57811362c6cf..b1b4cdccc3538 100644 --- a/x-pack/legacy/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/legacy/plugins/lens/server/routes/field_stats.ts @@ -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 diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts index fee17baecce28..da2c184d2d6e0 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts @@ -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'; diff --git a/x-pack/test/api_integration/apis/lens/field_stats.ts b/x-pack/test/api_integration/apis/lens/field_stats.ts index 9eba9392c4f7f..b2bb791e2da7f 100644 --- a/x-pack/test/api_integration/apis/lens/field_stats.ts +++ b/x-pack/test/api_integration/apis/lens/field_stats.ts @@ -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); + }); }); }); };