From 27b6824924b1f48c4e303dca140154ec12ccb8e6 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 19 Sep 2019 13:39:05 -0400 Subject: [PATCH 01/12] [lens] Use top nav in Lens app --- .../search_bar/components/search_bar.tsx | 6 +- .../plugins/lens/public/app_plugin/app.tsx | 188 +++++++++--------- .../plugins/lens/public/app_plugin/plugin.tsx | 20 +- .../editor_frame/data_panel_wrapper.tsx | 3 + .../editor_frame/editor_frame.tsx | 40 +++- .../editor_frame/expression_helpers.ts | 3 +- .../public/editor_frame_plugin/plugin.tsx | 4 +- x-pack/legacy/plugins/lens/public/types.ts | 12 +- 8 files changed, 168 insertions(+), 108 deletions(-) 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 d3fe259c9f463..5eca6cc54f892 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 @@ -78,10 +78,14 @@ export interface SearchBarProps { isRefreshPaused?: boolean; refreshInterval?: number; showAutoRefreshOnly?: boolean; - showSaveQuery?: boolean; onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; + // Show when user has privileges to save + showSaveQuery?: boolean; + // 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, which should not clear filters onClearSavedQuery?: () => void; customSubmitButton?: React.ReactNode; } 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 c62351954feaa..59bb749772f95 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -8,11 +8,16 @@ 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, SavedObjectsClientContract } from 'src/core/public'; +import { + DataSetup, + IndexPattern as IndexPatternInstance, + SavedQuery, +} from 'src/legacy/core_plugins/data/public'; +import { Filter } from '@kbn/es-query'; +import { TopNavMenu } from '../../../../../../src/legacy/core_plugins/kibana_react/public'; import { Query } from '../../../../../../src/legacy/core_plugins/data/public/query'; -import { QueryBarTopRow } from '../../../../../../src/legacy/core_plugins/data/public/query/query_bar'; import { Document, SavedObjectStore } from '../persistence'; import { EditorFrameInstance } from '../types'; import { NativeRenderer } from '../native_renderer'; @@ -20,37 +25,24 @@ import { NativeRenderer } from '../native_renderer'; interface State { isLoading: boolean; isDirty: boolean; + indexPatternTitles: string[]; + indexPatterns: 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({ editorFrame, core, + data, store, docId, docStorage, @@ -59,6 +51,7 @@ export function App({ }: { editorFrame: EditorFrameInstance; core: CoreStart; + data: DataSetup; store: Storage; docId?: string; docStorage: SavedObjectStore; @@ -72,19 +65,15 @@ export function App({ const [state, setState] = useState({ isLoading: !!docId, isDirty: false, + indexPatternTitles: [], + indexPatterns: [], + 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); @@ -117,10 +106,6 @@ export function App({ isLoading: false, persistedDoc: doc, query: doc.state.query, - localQueryBarState: { - ...state.localQueryBarState, - query: doc.state.query, - }, indexPatternTitles: doc.state.datasourceMetaData.filterableIndexPatterns.map( ({ title }) => title ), @@ -157,51 +142,46 @@ export function App({
- - { + core.notifications.toasts.addDanger( + i18n.translate('xpack.lens.editorFrame.docSavingError', { + defaultMessage: 'Error saving document {reason}', + values: { reason }, + }) + ); + }); + } + }, + testId: 'lnsApp_saveButton', + disableButton: !isSaveable, + }, + ]} data-test-subj="lnsApp_queryBar" screenTitle={'lens'} - onSubmit={payload => { + onQuerySubmit={payload => { const { dateRange, query } = payload; setState({ ...state, @@ -210,25 +190,44 @@ export function App({ toDate: dateRange.to, }, query: query || state.query, - localQueryBarState: payload, }); }} - onChange={localQueryBarState => { - setState({ ...state, localQueryBarState }); + filters={state.filters} + onFiltersUpdated={filters => { + setState({ ...state, filters }); }} - isDirty={isLocalStateDirty(state.localQueryBarState, state.query, state.dateRange)} appName={'lens'} - indexPatterns={state.indexPatternTitles} + indexPatterns={state.indexPatterns} store={store} + 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 - } + showFilterBar={true} + showSaveQuery={true /* TODO: Use permissions */} + savedQuery={state.savedQuery} + onSaved={savedQuery => { + setState({ ...state, savedQuery }); + }} + onSavedQueryUpdated={savedQuery => { + setState({ + ...state, + savedQuery, + filters: savedQuery.attributes.filters || state.filters, + query: savedQuery.attributes.query || state.query, + dateRange: savedQuery.attributes.timefilter + ? { + fromDate: savedQuery.attributes.timefilter.from, + toDate: savedQuery.attributes.timefilter.from, + } + : state.dateRange, + }); + }} + onClearSavedQuery={() => { + setState({ ...state, savedQuery: undefined }); + }} + query={state.query} + dateRangeFrom={state.dateRange.fromDate} + dateRangeTo={state.dateRange.toDate} toasts={core.notifications.toasts} uiSettings={core.uiSettings} savedObjectsClient={savedObjectsClient} @@ -243,9 +242,18 @@ export function App({ nativeProps={{ dateRange: state.dateRange, query: state.query, + filters: state.filters, + savedQuery: state.savedQuery, doc: state.persistedDoc, onError, - onChange: ({ indexPatternTitles, doc }) => { + onChange: ({ filterableIndexPatterns, doc }) => { + Promise.all( + filterableIndexPatterns.map(({ id }) => data.indexPatterns.indexPatterns.get(id)) + ).then(indexPatterns => { + setState({ ...state, indexPatterns }); + }); + + const indexPatternTitles = filterableIndexPatterns.map(pattern => pattern.id); const indexPatternChange = !_.isEqual(state.indexPatternTitles, indexPatternTitles); const docChange = !_.isEqual(state.persistedDoc, doc); if (indexPatternChange || docChange) { 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 a29356a084063..bc309240788bd 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -11,6 +11,11 @@ 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 { DataSetup } from 'src/legacy/core_plugins/data/public'; +import { + setup as dataSetup, + 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'; @@ -29,7 +34,7 @@ export class AppPlugin { constructor() {} - setup(core: CoreSetup) { + setup(core: CoreSetup, plugins: { data: DataSetup }) { // 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(); @@ -45,7 +50,7 @@ export class AppPlugin { editorFrameSetupInterface.registerVisualization(metricVisualization); } - start(core: CoreStart) { + start(core: CoreStart, plugins: { data: DataSetup }) { if (this.store === null) { throw new Error('Start lifecycle called before setup lifecycle'); } @@ -60,6 +65,7 @@ export class AppPlugin { return ( app.setup(npSetup.core); -export const appStart = () => app.start(npStart.core); +export const appSetup = () => + app.setup(npSetup.core, { + data: dataSetup, + }); +export const appStart = () => + app.start(npStart.core, { + data: dataStart, + }); export const appStop = () => app.stop(); 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 0edf79417882d..c4ea4d2ac96c1 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.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index c0a2990015138..05b4f15f9b781 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..f5c6aed0d3f4e 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 @@ -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/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/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 7945d439f75cd..d33b7e8c4bd31 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; From 2c6817cff72d6b66375f40a749bb17679331c6d9 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 23 Sep 2019 18:31:42 -0400 Subject: [PATCH 02/12] Add tests for saved query, pass filters around more places --- packages/kbn-es-query/src/es_query/index.d.ts | 20 ++ .../search_bar/components/search_bar.tsx | 2 +- .../lens/public/app_plugin/app.test.tsx | 262 ++++++++++++++---- .../plugins/lens/public/app_plugin/app.tsx | 95 ++++--- .../visualization.test.tsx | 1 + .../editor_frame/editor_frame.test.tsx | 116 ++++---- .../editor_frame/save.test.ts | 15 +- .../editor_frame_plugin/editor_frame/save.ts | 3 +- .../editor_frame/state_management.test.ts | 1 + .../lens/public/editor_frame_plugin/mocks.tsx | 1 + .../editor_frame_plugin/plugin.test.tsx | 2 + .../indexpattern_plugin/datapanel.test.tsx | 37 +-- .../public/indexpattern_plugin/datapanel.tsx | 12 +- .../public/indexpattern_plugin/field_item.tsx | 6 +- .../public/persistence/saved_object_store.ts | 2 + .../plugins/lens/server/routes/field_stats.ts | 5 +- .../api_integration/apis/lens/field_stats.ts | 125 +++++++++ 17 files changed, 530 insertions(+), 175 deletions(-) create mode 100644 packages/kbn-es-query/src/es_query/index.d.ts 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..4ca68499ed3c3 --- /dev/null +++ b/packages/kbn-es-query/src/es_query/index.d.ts @@ -0,0 +1,20 @@ +/* + * 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; 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 5eca6cc54f892..4829fc04a715e 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 @@ -85,7 +85,7 @@ export interface SearchBarProps { 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, which should not clear filters + // User has cleared the active query, your app should clear the entire query bar onClearSavedQuery?: () => void; customSubmitButton?: React.ReactNode; } 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 c2cd20f702f2b..c170f2fc1afba 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,17 +5,24 @@ */ 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 { + TopNavMenu, + TopNavMenuData, +} from '../../../../../../src/legacy/core_plugins/kibana_react/public'; import { SavedObjectsClientContract } from 'src/core/public'; import { coreMock } from 'src/core/public/mocks'; +import { DataSetup } from 'src/legacy/core_plugins/data/public'; -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'); @@ -38,6 +45,7 @@ describe('Lens App', () => { function makeDefaultArgs(): jest.Mocked<{ editorFrame: EditorFrameInstance; core: typeof core; + data: DataSetup; store: Storage; docId?: string; docStorage: SavedObjectStore; @@ -47,6 +55,15 @@ describe('Lens App', () => { return ({ editorFrame: createMockFrame(), core, + data: { + indexPatterns: { + indexPatterns: { + get: jest.fn(id => { + return new Promise(resolve => resolve({ id })); + }), + }, + }, + }, store: { get: jest.fn(), }, @@ -54,12 +71,13 @@ describe('Lens App', () => { load: jest.fn(), save: jest.fn(), }, - QueryBarTopRow: jest.fn(() =>
), + TopNavMenu: jest.fn(() =>
), redirectTo: jest.fn(id => {}), savedObjectsClient: jest.fn(), } as unknown) as jest.Mocked<{ editorFrame: EditorFrameInstance; core: typeof core; + data: DataSetup; store: Storage; docId?: string; docStorage: SavedObjectStore; @@ -106,12 +124,14 @@ describe('Lens App', () => { "toDate": "now", }, "doc": undefined, + "filters": Array [], "onChange": [Function], "onError": [Function], "query": Object { "language": "kuery", "query": "", }, + "savedQuery": undefined, }, ], ] @@ -171,12 +191,13 @@ 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' }], }), {} ); @@ -230,30 +251,28 @@ 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 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 () => { @@ -266,21 +285,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 }); @@ -292,12 +305,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 () => { @@ -308,27 +316,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); }); }); }); @@ -340,7 +343,7 @@ describe('Lens App', () => { mount(); - expect(QueryBarTopRow).toHaveBeenCalledWith( + expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ dateRangeFrom: 'now-7d', dateRangeTo: 'now', @@ -363,7 +366,7 @@ describe('Lens App', () => { const instance = mount(); - expect(QueryBarTopRow).toHaveBeenCalledWith( + expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ indexPatterns: [], }), @@ -372,37 +375,34 @@ 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, }); instance.update(); - expect(QueryBarTopRow).toHaveBeenCalledWith( + expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ - indexPatterns: ['newIndex'], + indexPatterns: [{ id: '1' }], }), {} ); }); - 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', @@ -418,6 +418,148 @@ describe('Lens App', () => { }) ); }); + + it('updates the filters when the user changes them', () => { + const args = makeDefaultArgs(); + args.editorFrame = frame; + + const instance = mount(); + + instance.find(TopNavMenu).prop('onFiltersUpdated')!([ + buildExistsFilter({ name: 'myfield' }, { id: 'index1' }), + ]); + + instance.update(); + + expect(TopNavMenu).toHaveBeenCalledWith( + expect.objectContaining({ + // dateRangeFrom: 'now-14d', + // dateRangeTo: 'now-7d', + // query: { query: 'new', language: 'lucene' }, + filters: [buildExistsFilter({ name: 'myfield' }, { id: 'index1' })], + }), + {} + ); + expect(frame.mount).toHaveBeenCalledWith( + expect.any(Element), + expect.objectContaining({ + // dateRange: { fromDate: 'now-14d', toDate: 'now-7d' }, + // query: { query: 'new', language: 'lucene' }, + filters: [buildExistsFilter({ name: 'myfield' }, { id: 'index1' })], + }) + ); + }); + }); + + describe('saved query handling', () => { + 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' }, + }); + + instance.find(TopNavMenu).prop('onFiltersUpdated')!([ + buildExistsFilter({ name: 'myfield' }, { id: 'index1' }), + ]); + + instance.update(); + + instance.find(TopNavMenu).prop('onClearSavedQuery')!(); + instance.update(); + + expect(instance.find(TopNavMenu).prop('filters')).toEqual([]); + }); }); 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 59bb749772f95..b2e2cfda44d3b 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -25,7 +25,7 @@ import { NativeRenderer } from '../native_renderer'; interface State { isLoading: boolean; isDirty: boolean; - indexPatternTitles: string[]; + // indexPatternTitles: string[]; indexPatterns: IndexPatternInstance[]; persistedDoc?: Document; @@ -65,7 +65,7 @@ export function App({ const [state, setState] = useState({ isLoading: !!docId, isDirty: false, - indexPatternTitles: [], + // indexPatternTitles: [], indexPatterns: [], query: { query: '', language }, @@ -101,15 +101,33 @@ export function App({ docStorage .load(docId) .then(doc => { - setState({ - ...state, - isLoading: false, - persistedDoc: doc, - query: doc.state.query, - indexPatternTitles: doc.state.datasourceMetaData.filterableIndexPatterns.map( - ({ title }) => title - ), - }); + Promise.all( + doc.state.datasourceMetaData.filterableIndexPatterns.map(({ id }) => + data.indexPatterns.indexPatterns.get(id) + ) + ) + .then(indexPatterns => { + setState({ + ...state, + isLoading: false, + persistedDoc: doc, + query: doc.state.query, + filters: doc.state.filters, + dateRange: doc.state.dateRange || state.dateRange, + indexPatterns, + }); + }) + .catch(() => { + setState({ ...state, isLoading: false }); + + core.notifications.toasts.addDanger( + i18n.translate('xpack.lens.editorFrame.indexPatternLoadingError', { + defaultMessage: 'Error loading index patterns', + }) + ); + + redirectTo(); + }); }) .catch(() => { setState({ ...state, isLoading: false }); @@ -179,7 +197,7 @@ export function App({ disableButton: !isSaveable, }, ]} - data-test-subj="lnsApp_queryBar" + data-test-subj="lnsApp_topNav" screenTitle={'lens'} onQuerySubmit={payload => { const { dateRange, query } = payload; @@ -213,17 +231,21 @@ export function App({ ...state, savedQuery, filters: savedQuery.attributes.filters || state.filters, - query: savedQuery.attributes.query || state.query, - dateRange: savedQuery.attributes.timefilter - ? { - fromDate: savedQuery.attributes.timefilter.from, - toDate: savedQuery.attributes.timefilter.from, - } - : state.dateRange, + query: savedQuery.attributes.query, }); }} onClearSavedQuery={() => { - setState({ ...state, savedQuery: undefined }); + setState({ + ...state, + savedQuery: undefined, + filters: [], + query: { + query: '', + language: + store.get('kibana.userQueryLanguage') || + core.uiSettings.get('search:queryLanguage'), + }, + }); }} query={state.query} dateRangeFrom={state.dateRange.fromDate} @@ -247,23 +269,28 @@ export function App({ doc: state.persistedDoc, onError, onChange: ({ filterableIndexPatterns, doc }) => { + lastKnownDocRef.current = doc; + + if (!_.isEqual(state.persistedDoc, doc)) { + setState({ ...state, isDirty: true }); + } + Promise.all( filterableIndexPatterns.map(({ id }) => data.indexPatterns.indexPatterns.get(id)) - ).then(indexPatterns => { - setState({ ...state, indexPatterns }); - }); - - const indexPatternTitles = filterableIndexPatterns.map(pattern => pattern.id); - const indexPatternChange = !_.isEqual(state.indexPatternTitles, indexPatternTitles); - const docChange = !_.isEqual(state.persistedDoc, doc); - if (indexPatternChange || docChange) { - setState({ - ...state, - indexPatternTitles, - isDirty: docChange, + ) + .then(indexPatterns => { + setState({ + ...state, + indexPatterns, + }); + }) + .catch(() => { + core.notifications.toasts.addDanger( + i18n.translate('xpack.lens.editorFrame.indexPatternLoadingError', { + defaultMessage: 'Error loading index patterns', + }) + ); }); - } - lastKnownDocRef.current = doc; }, }} /> 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/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx index 8023946d42127..7cecccba972e1 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 @@ -48,6 +48,7 @@ function getDefaultProps() { onChange: jest.fn(), dateRange: { fromDate: '', toDate: '' }, query: { query: '', language: 'lucene' }, + filters: [], core: coreMock.createSetup(), }; } @@ -256,6 +257,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 +411,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 +529,9 @@ describe('editor_frame', () => { }, Object { "arguments": Object { - "filters": Array [], + "filters": Array [ + "[]", + ], "query": Array [ "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", ], @@ -1397,7 +1403,7 @@ describe('editor_frame', () => { expect(onChange).toHaveBeenCalledTimes(2); expect(onChange).toHaveBeenNthCalledWith(1, { - indexPatternTitles: ['resolved'], + filterableIndexPatterns: [{ id: '1', title: 'resolved' }], doc: { expression: '', id: undefined, @@ -1407,6 +1413,7 @@ describe('editor_frame', () => { datasourceStates: { testDatasource: undefined }, query: { query: '', language: 'lucene' }, filters: [], + dateRange: { fromDate: '', toDate: '' }, }, title: 'New visualization', type: 'lens', @@ -1414,7 +1421,8 @@ describe('editor_frame', () => { }, }); expect(onChange).toHaveBeenLastCalledWith({ - indexPatternTitles: ['resolved'], + // indexPatternTitles: ['resolved'], + filterableIndexPatterns: [{ id: '1', title: 'resolved' }], doc: { expression: '', id: undefined, @@ -1426,6 +1434,7 @@ describe('editor_frame', () => { datasourceStates: { testDatasource: undefined }, query: { query: '', language: 'lucene' }, filters: [], + dateRange: { fromDate: '', toDate: '' }, }, title: 'New visualization', type: 'lens', @@ -1473,7 +1482,7 @@ describe('editor_frame', () => { await waitForPromises(); expect(onChange).toHaveBeenCalledTimes(3); expect(onChange).toHaveBeenNthCalledWith(3, { - indexPatternTitles: [], + filterableIndexPatterns: [], doc: { expression: expect.stringContaining('vis "expression"'), id: undefined, @@ -1483,6 +1492,7 @@ describe('editor_frame', () => { visualization: { initialState: true }, query: { query: 'new query', language: 'lucene' }, filters: [], + dateRange: { fromDate: '', toDate: '' }, }, title: 'New visualization', type: 'lens', 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..36a4511c4271d 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,12 @@ describe('save editor frame state', () => { }, query: { query: '', language: 'lucene' }, dateRange: { fromDate: 'now-7d', toDate: 'now' }, + filters: [ + buildExistsFilter({ name: '@timestamp' }, { id: 'indexpattern' }), + // meta: { index: 'indexpattern', negate: false, disabled: false, alias: null }, + // exists: { field: '@timestamp' }, + // }, + ], }, }; @@ -83,7 +90,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/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/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx index b8ef8b7689627..20a637318d298 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 { EuiComboBox } from '@elastic/eui'; import { IndexPatternPrivateState, IndexPatternColumn } from './indexpattern'; @@ -13,6 +12,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'; jest.mock('ui/new_platform'); jest.mock('./loader'); @@ -216,6 +216,7 @@ describe('IndexPattern Data Panel', () => { toDate: 'now', }, query: { query: '', language: 'lucene' }, + filters: [], showEmptyFields: false, onToggleEmptyFields: jest.fn(), }; @@ -223,7 +224,7 @@ describe('IndexPattern Data Panel', () => { it('should update index pattern of layer on switch if it is a single empty one', async () => { const setStateSpy = jest.fn(); - 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('[data-test-subj="indexPattern-switch-link"]').simulate('click'); @@ -344,7 +345,9 @@ describe('IndexPattern Data Panel', () => { }, }); const updateFields = jest.fn(); - mount(); + mountWithIntl( + + ); await waitForPromises(); @@ -407,7 +410,9 @@ describe('IndexPattern Data Panel', () => { const props = { ...defaultProps, indexPatterns: newIndexPatterns }; - mount(); + mountWithIntl( + + ); await waitForPromises(); @@ -417,7 +422,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( ); @@ -430,7 +435,7 @@ describe('IndexPattern Data Panel', () => { }); it('should filter down by name', () => { - const wrapper = shallow( + const wrapper = shallowWithIntl( ); @@ -446,7 +451,7 @@ describe('IndexPattern Data Panel', () => { }); it('should filter down by type', () => { - const wrapper = mount( + const wrapper = mountWithIntl( ); @@ -467,7 +472,7 @@ describe('IndexPattern Data Panel', () => { }); it('should toggle type if clicked again', () => { - const wrapper = mount( + const wrapper = mountWithIntl( ); @@ -494,7 +499,7 @@ describe('IndexPattern Data Panel', () => { }); it('should filter down by type and by name', () => { - const wrapper = mount( + const wrapper = mountWithIntl( ); @@ -542,7 +547,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', @@ -551,7 +556,7 @@ describe('IndexPattern Data Panel', () => { }); it('should filter down by name', () => { - const wrapper = shallow( + const wrapper = shallowWithIntl( ); @@ -567,7 +572,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 8009c4ebf3e6d..2fa259cc70239 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx @@ -29,7 +29,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Query } from 'src/plugins/data/common'; +// import { Query } from 'src/plugins/data/common'; import { DatasourceDataPanelProps, DataType } from '../types'; import { IndexPatternPrivateState, IndexPatternField, IndexPattern } from './indexpattern'; import { ChildDragDropProvider, DragContextState } from '../drag_drop'; @@ -151,6 +151,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ indexPatterns, query, dateRange, + filters, dragDropContext, showIndexPatternSwitcher, setShowIndexPatternSwitcher, @@ -159,12 +160,12 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ showEmptyFields, onToggleEmptyFields, core, -}: Partial & { +}: Pick> & { currentIndexPatternId: string; indexPatterns: Record; - dateRange: DatasourceDataPanelProps['dateRange']; - query: Query; - core: DatasourceDataPanelProps['core']; + // dateRange: DatasourceDataPanelProps['dateRange']; + // query: Query; + // core: DatasourceDataPanelProps['core']; dragDropContext: DragContextState; showIndexPatternSwitcher: boolean; setShowIndexPatternSwitcher: (show: boolean) => void; @@ -540,6 +541,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.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx index 4d3d1d378c328..ee2931d98c6ec 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 @@ -33,7 +33,7 @@ import { niceTimeFormatter, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { toElasticsearchQuery } from '@kbn/es-query'; +import { Filter, toElasticsearchQuery, buildQueryFromFilters } from '@kbn/es-query'; import { Query } from 'src/plugins/data/common'; // @ts-ignore import { fieldFormats } from '../../../../../../src/legacy/ui/public/registry/field_formats'; @@ -51,6 +51,7 @@ export interface FieldItemProps { exists: boolean; query: Query; dateRange: DatasourceDataPanelProps['dateRange']; + filters: Filter[]; } interface State { @@ -70,7 +71,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); @@ -111,6 +112,7 @@ export function FieldItem(props: FieldItemProps) { .post(`/api/lens/index_stats/${indexPattern.title}/field`, { body: JSON.stringify({ query: toElasticsearchQuery(query, indexPattern), + filters: buildQueryFromFilters(query, indexPattern), fromDate: dateRange.fromDate, toDate: dateRange.toDate, timeFieldName: indexPattern.timeFieldName, 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/server/routes/field_stats.ts b/x-pack/legacy/plugins/lens/server/routes/field_stats.ts index a57811362c6cf..c4f2e066966b0 100644 --- a/x-pack/legacy/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/legacy/plugins/lens/server/routes/field_stats.ts @@ -25,6 +25,7 @@ export async function initFieldsRoute(setup: CoreSetup) { body: schema.object( { query: schema.object({}, { allowUnknowns: true }), + filters: schema.arrayOf(schema.object({}, { allowUnknowns: true })), fromDate: schema.string(), toDate: schema.string(), timeFieldName: schema.string(), @@ -48,7 +49,7 @@ export async function initFieldsRoute(setup: CoreSetup) { try { const filters = { bool: { - filter: [ + filter: req.body.filters.concat([ { range: { [timeFieldName]: { @@ -58,7 +59,7 @@ export async function initFieldsRoute(setup: CoreSetup) { }, }, query, - ], + ]), }, }; 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..d837c0d1eaa72 100644 --- a/x-pack/test/api_integration/apis/lens/field_stats.ts +++ b/x-pack/test/api_integration/apis/lens/field_stats.ts @@ -36,6 +36,7 @@ export default ({ getService }: FtrProviderContext) => { .set(COMMON_HEADERS) .send({ query: { match_all: {} }, + filters: [], fromDate: TEST_START_TIME, toDate: TEST_END_TIME, timeFieldName: '@timestamp', @@ -53,6 +54,7 @@ export default ({ getService }: FtrProviderContext) => { .set(COMMON_HEADERS) .send({ query: { match_all: {} }, + filters: [], fromDate: TEST_START_TIME, toDate: TEST_END_TIME, timeFieldName: '@timestamp', @@ -164,6 +166,7 @@ export default ({ getService }: FtrProviderContext) => { .set(COMMON_HEADERS) .send({ query: { match_all: {} }, + filters: [], fromDate: TEST_START_TIME, toDate: TEST_END_TIME, timeFieldName: '@timestamp', @@ -201,6 +204,7 @@ export default ({ getService }: FtrProviderContext) => { .set(COMMON_HEADERS) .send({ query: { match_all: {} }, + filters: [], fromDate: TEST_START_TIME, toDate: TEST_END_TIME, timeFieldName: '@timestamp', @@ -261,6 +265,127 @@ 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({ + query: { match_all: {} }, + filters: [ + { + match_all: {}, + meta: { type: 'match_all' }, + }, + { + exists: { field: 'bytes' }, + meta: { type: 'exists' }, + }, + ], + fromDate: TEST_START_TIME, + toDate: TEST_END_TIME, + timeFieldName: '@timestamp', + field: { + name: 'bytes', + type: 'number', + }, + }) + .expect(200); + + expect(body).to.eql({ + totalDocuments: 4633, + sampledDocuments: 4633, + sampledValues: 4633, + histogram: { + buckets: [ + { + count: 705, + key: 0, + }, + { + count: 898, + key: 1999, + }, + { + count: 885, + key: 3998, + }, + { + count: 970, + key: 5997, + }, + { + count: 939, + key: 7996, + }, + { + count: 44, + key: 9995, + }, + { + count: 43, + key: 11994, + }, + { + count: 43, + key: 13993, + }, + { + count: 57, + key: 15992, + }, + { + count: 49, + key: 17991, + }, + ], + }, + topValues: { + buckets: [ + { + count: 147, + key: 0, + }, + { + count: 5, + key: 3954, + }, + { + count: 5, + key: 6497, + }, + { + count: 4, + key: 1840, + }, + { + count: 4, + key: 4206, + }, + { + count: 4, + key: 4328, + }, + { + count: 4, + key: 4669, + }, + { + count: 4, + key: 5846, + }, + { + count: 4, + key: 5863, + }, + { + count: 4, + key: 6631, + }, + ], + }, + }); + }); }); }); }; From a787f94a09c8315427207306a7b0fc48883c6fca Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 24 Sep 2019 13:50:15 -0400 Subject: [PATCH 03/12] Fix filter passing --- .../lens/public/app_plugin/app.test.tsx | 1 + .../plugins/lens/public/app_plugin/app.tsx | 3 +- .../editor_frame/expression_helpers.ts | 2 +- .../editor_frame/suggestion_panel.test.tsx | 2 +- .../editor_frame/workspace_panel.test.tsx | 70 ++++++++++--------- .../public/indexpattern_plugin/field_item.tsx | 2 +- 6 files changed, 42 insertions(+), 38 deletions(-) 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 4eddb86302bcc..b83f7f74ddbbf 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 @@ -63,6 +63,7 @@ describe('Lens App', () => { }), }, }, + timefilter: { history: {} }, }, store: { get: jest.fn(), 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 b0429804e97ef..1afba657e174b 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -254,6 +254,7 @@ export function App({ uiSettings={core.uiSettings} savedObjectsClient={savedObjectsClient} http={core.http} + timeHistory={data.timefilter.history} />
@@ -272,7 +273,7 @@ export function App({ lastKnownDocRef.current = doc; if (!_.isEqual(state.persistedDoc, doc)) { - setState({ ...state, isDirty: true }); + setState(s => ({ ...s, isDirty: true })); } Promise.all( 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 f5c6aed0d3f4e..d67c0c9d2c7a3 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: filters ? [JSON.stringify(filters)] : undefined, }, }, ...parsedExpression.chain, 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 0b172ff6df7af..ec325ebeb9d94 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 @@ -287,7 +287,7 @@ describe('suggestion_panel', () => { }, Object { "arguments": Object { - "filters": Array [], + "filters": undefined, "query": Array [ "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", ], 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 86a0e5c8a833a..41eeef1c5723f 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 @@ -149,7 +149,9 @@ describe('workspace_panel', () => { }, Object { "arguments": Object { - "filters": Array [], + "filters": Array [ + "[]", + ], "query": Array [ "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", ], @@ -239,39 +241,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 () => { 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 445f98f7094cb..5a3d041c1423a 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 @@ -112,7 +112,7 @@ export function FieldItem(props: FieldItemProps) { .post(`/api/lens/index_stats/${indexPattern.title}/field`, { body: JSON.stringify({ query: toElasticsearchQuery(query, indexPattern), - filters: buildQueryFromFilters(query, indexPattern), + filters: buildQueryFromFilters(filters, indexPattern), fromDate: dateRange.fromDate, toDate: dateRange.toDate, timeFieldName: indexPattern.timeFieldName, From 761be14d92865d5a9adb37579214635cb3eca693 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 24 Sep 2019 16:07:56 -0400 Subject: [PATCH 04/12] Add unit test for field popover making correct queries --- packages/kbn-es-query/src/es_query/index.d.ts | 11 + packages/kbn-es-query/src/index.d.ts | 1 + .../editor_frame/expression_helpers.ts | 2 +- .../editor_frame/suggestion_panel.test.tsx | 4 +- .../public/indexpattern_plugin/datapanel.tsx | 2 + .../indexpattern_plugin/field_item.test.tsx | 215 ++++++++++++++++++ .../public/indexpattern_plugin/field_item.tsx | 5 +- x-pack/legacy/plugins/lens/readme.md | 2 +- .../plugins/lens/server/routes/field_stats.ts | 15 +- .../server/lib/generate_csv_search.ts | 1 - .../api_integration/apis/lens/field_stats.ts | 120 +--------- 11 files changed, 252 insertions(+), 126 deletions(-) create mode 100644 x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.test.tsx diff --git a/packages/kbn-es-query/src/es_query/index.d.ts b/packages/kbn-es-query/src/es_query/index.d.ts index 4ca68499ed3c3..36d7552c22316 100644 --- a/packages/kbn-es-query/src/es_query/index.d.ts +++ b/packages/kbn-es-query/src/es_query/index.d.ts @@ -18,3 +18,14 @@ */ 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; 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/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 d67c0c9d2c7a3..a78ce1ce27793 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)] : undefined, + filters: filters ? [JSON.stringify(filters)] : ['[]'], }, }, ...parsedExpression.chain, 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 ec325ebeb9d94..e2673f73b6e4d 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 @@ -287,7 +287,9 @@ describe('suggestion_panel', () => { }, Object { "arguments": Object { - "filters": undefined, + "filters": Array [ + "[]", + ], "query": Array [ "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", ], 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 7868c23568b86..3d4d77368499b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx @@ -67,6 +67,7 @@ export function IndexPatternDataPanel({ dragDropContext, core, query, + filters, dateRange, }: DatasourceDataPanelProps) { const { indexPatterns, currentIndexPatternId } = state; @@ -118,6 +119,7 @@ export function IndexPatternDataPanel({ indexPatterns={indexPatterns} query={query} dateRange={dateRange} + filters={filters} dragDropContext={dragDropContext} showEmptyFields={state.showEmptyFields} onToggleEmptyFields={onToggleEmptyFields} 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 5a3d041c1423a..62643d3fdfe86 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 @@ -33,7 +33,7 @@ import { niceTimeFormatter, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import { Filter, toElasticsearchQuery, buildQueryFromFilters } from '@kbn/es-query'; +import { Filter, buildEsQuery } from '@kbn/es-query'; import { Query } from 'src/plugins/data/common'; // @ts-ignore import { fieldFormats } from '../../../../../../src/legacy/ui/public/registry/field_formats'; @@ -111,8 +111,7 @@ export function FieldItem(props: FieldItemProps) { core.http .post(`/api/lens/index_stats/${indexPattern.title}/field`, { body: JSON.stringify({ - query: toElasticsearchQuery(query, indexPattern), - filters: buildQueryFromFilters(filters, indexPattern), + dslQuery: buildEsQuery(indexPattern, query, filters), fromDate: dateRange.fromDate, toDate: dateRange.toDate, timeFieldName: indexPattern.timeFieldName, 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 c4f2e066966b0..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,8 +24,7 @@ export async function initFieldsRoute(setup: CoreSetup) { }), body: schema.object( { - query: schema.object({}, { allowUnknowns: true }), - filters: schema.arrayOf(schema.object({}, { allowUnknowns: true })), + dslQuery: schema.object({}, { allowUnknowns: true }), fromDate: schema.string(), toDate: schema.string(), timeFieldName: schema.string(), @@ -44,12 +43,12 @@ 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: req.body.filters.concat([ + filter: [ { range: { [timeFieldName]: { @@ -58,8 +57,8 @@ export async function initFieldsRoute(setup: CoreSetup) { }, }, }, - query, - ]), + dslQuery, + ], }, }; @@ -67,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 d837c0d1eaa72..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,8 +35,7 @@ export default ({ getService }: FtrProviderContext) => { .post('/api/lens/index_stats/logstash/field') .set(COMMON_HEADERS) .send({ - query: { match_all: {} }, - filters: [], + dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, timeFieldName: '@timestamp', @@ -53,8 +52,7 @@ export default ({ getService }: FtrProviderContext) => { .post('/api/lens/index_stats/logstash-2015.09.22/field') .set(COMMON_HEADERS) .send({ - query: { match_all: {} }, - filters: [], + dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, timeFieldName: '@timestamp', @@ -165,8 +163,7 @@ export default ({ getService }: FtrProviderContext) => { .post('/api/lens/index_stats/logstash-2015.09.22/field') .set(COMMON_HEADERS) .send({ - query: { match_all: {} }, - filters: [], + dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, timeFieldName: '@timestamp', @@ -203,8 +200,7 @@ export default ({ getService }: FtrProviderContext) => { .post('/api/lens/index_stats/logstash-2015.09.22/field') .set(COMMON_HEADERS) .send({ - query: { match_all: {} }, - filters: [], + dslQuery: { match_all: {} }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, timeFieldName: '@timestamp', @@ -271,17 +267,11 @@ export default ({ getService }: FtrProviderContext) => { .post('/api/lens/index_stats/logstash-2015.09.22/field') .set(COMMON_HEADERS) .send({ - query: { match_all: {} }, - filters: [ - { - match_all: {}, - meta: { type: 'match_all' }, - }, - { - exists: { field: 'bytes' }, - meta: { type: 'exists' }, + dslQuery: { + bool: { + filter: [{ match: { 'geo.src': 'US' } }], }, - ], + }, fromDate: TEST_START_TIME, toDate: TEST_END_TIME, timeFieldName: '@timestamp', @@ -292,99 +282,7 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - expect(body).to.eql({ - totalDocuments: 4633, - sampledDocuments: 4633, - sampledValues: 4633, - histogram: { - buckets: [ - { - count: 705, - key: 0, - }, - { - count: 898, - key: 1999, - }, - { - count: 885, - key: 3998, - }, - { - count: 970, - key: 5997, - }, - { - count: 939, - key: 7996, - }, - { - count: 44, - key: 9995, - }, - { - count: 43, - key: 11994, - }, - { - count: 43, - key: 13993, - }, - { - count: 57, - key: 15992, - }, - { - count: 49, - key: 17991, - }, - ], - }, - topValues: { - buckets: [ - { - count: 147, - key: 0, - }, - { - count: 5, - key: 3954, - }, - { - count: 5, - key: 6497, - }, - { - count: 4, - key: 1840, - }, - { - count: 4, - key: 4206, - }, - { - count: 4, - key: 4328, - }, - { - count: 4, - key: 4669, - }, - { - count: 4, - key: 5846, - }, - { - count: 4, - key: 5863, - }, - { - count: 4, - key: 6631, - }, - ], - }, - }); + expect(body.totalDocuments).to.eql(425); }); }); }); From 0bf1b44426d0e7d057a5162f476a9a3ba17850a7 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 25 Sep 2019 14:41:36 -0400 Subject: [PATCH 05/12] Respond to review feedback --- .../lens/public/app_plugin/app.test.tsx | 28 +++++++++++++++++++ .../plugins/lens/public/app_plugin/app.tsx | 26 +++++++++++------ .../plugins/lens/public/app_plugin/plugin.tsx | 3 +- .../indexpattern_plugin/indexpattern.test.ts | 9 +----- .../indexpattern_plugin/indexpattern.tsx | 16 +++++------ .../indexpattern_suggestions.test.tsx | 5 +--- .../public/indexpattern_plugin/plugin.tsx | 28 ++++++++----------- 7 files changed, 68 insertions(+), 47 deletions(-) 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 b83f7f74ddbbf..ce21369241791 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 @@ -38,6 +38,31 @@ 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; @@ -64,6 +89,9 @@ describe('Lens App', () => { }, }, timefilter: { history: {} }, + filter: { + filterManager: createMockFilterManager(), + }, }, store: { get: jest.fn(), 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 1afba657e174b..d4904fcf09c4a 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -9,15 +9,15 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Storage } from 'ui/storage'; -import { CoreStart, SavedObjectsClientContract } from 'src/core/public'; +import { CoreStart } from 'src/core/public'; import { DataSetup, IndexPattern as IndexPatternInstance, 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 { Query } from '../../../../../../src/legacy/core_plugins/data/public/query'; import { Document, SavedObjectStore } from '../persistence'; import { EditorFrameInstance } from '../types'; import { NativeRenderer } from '../native_renderer'; @@ -25,7 +25,6 @@ import { NativeRenderer } from '../native_renderer'; interface State { isLoading: boolean; isDirty: boolean; - // indexPatternTitles: string[]; indexPatterns: IndexPatternInstance[]; persistedDoc?: Document; @@ -47,7 +46,6 @@ export function App({ docId, docStorage, redirectTo, - savedObjectsClient, }: { editorFrame: EditorFrameInstance; core: CoreStart; @@ -56,7 +54,6 @@ export function App({ docId?: string; docStorage: SavedObjectStore; redirectTo: (id?: string) => void; - savedObjectsClient: SavedObjectsClientContract; }) { const timeDefaults = core.uiSettings.get('timepicker:timeDefaults'); const language = @@ -65,7 +62,6 @@ export function App({ const [state, setState] = useState({ isLoading: !!docId, isDirty: false, - // indexPatternTitles: [], indexPatterns: [], query: { query: '', language }, @@ -78,6 +74,17 @@ export function App({ const lastKnownDocRef = useRef(undefined); + useEffect(() => { + const subscription = data.filter.filterManager.getUpdates$().subscribe({ + next: () => { + setState({ ...state, filters: data.filter.filterManager.getFilters() }); + }, + }); + return () => { + subscription.unsubscribe(); + }; + }, []); + // Sync Kibana breadcrumbs any time the saved document's title changes useEffect(() => { core.chrome.setBreadcrumbs([ @@ -212,7 +219,7 @@ export function App({ }} filters={state.filters} onFiltersUpdated={filters => { - setState({ ...state, filters }); + data.filter.filterManager.setFilters(filters); }} appName={'lens'} indexPatterns={state.indexPatterns} @@ -227,14 +234,15 @@ export function App({ setState({ ...state, savedQuery }); }} onSavedQueryUpdated={savedQuery => { + data.filter.filterManager.setFilters(savedQuery.attributes.filters || state.filters); setState({ ...state, savedQuery, - filters: savedQuery.attributes.filters || state.filters, query: savedQuery.attributes.query, }); }} onClearSavedQuery={() => { + data.filter.filterManager.removeAll(); setState({ ...state, savedQuery: undefined, @@ -252,7 +260,7 @@ export function App({ dateRangeTo={state.dateRange.toDate} toasts={core.notifications.toasts} uiSettings={core.uiSettings} - savedObjectsClient={savedObjectsClient} + savedObjectsClient={core.savedObjects.client} http={core.http} timeHistory={data.timefilter.history} /> 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 bc309240788bd..ee08e46837ce4 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -44,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, plugins: { data: DataSetup }) { @@ -68,7 +68,6 @@ export class AppPlugin { data={plugins.data} editorFrame={this.instance!} store={new Storage(localStorage)} - savedObjectsClient={chrome.getSavedObjectsClient()} docId={routeProps.match.params.id} docStorage={store} redirectTo={id => { 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 dbbdb368b44e6..b43ab03004302 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, @@ -24,7 +21,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: { @@ -137,10 +133,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 92c0f6e89aecb..06cf92f3406e5 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 { DatasourceDimensionPanelProps, @@ -19,7 +19,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, @@ -150,14 +150,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 @@ -273,7 +273,7 @@ export function getIndexPatternDatasource({ setState={setState} uiSettings={uiSettings} storage={storage} - savedObjectsClient={savedObjectsClient} + savedObjectsClient={core.savedObjects.client} layerId={props.layerId} http={core.http} {...props} 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 57003f84bc06c..e70f518107d62 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 @@ -135,12 +135,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..744cc89c6e919 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx @@ -5,26 +5,24 @@ */ import { Registry } from '@kbn/interpreter/target/common'; -import { CoreSetup } from 'src/core/public'; +import { CoreSetup, CoreStart } 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(); From c8361c9b223c34fd39c49518c3e3848a8ba70de5 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 26 Sep 2019 12:05:14 -0400 Subject: [PATCH 06/12] Fix type errors --- x-pack/legacy/plugins/lens/public/app_plugin/app.tsx | 3 --- .../indexpattern_plugin/indexpattern_suggestions.test.tsx | 3 --- .../legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) 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 cc11b349c2095..b13c049782415 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -18,8 +18,6 @@ import { } from 'src/legacy/core_plugins/data/public'; import { Filter } from '@kbn/es-query'; import { TopNavMenu } from '../../../../../../src/legacy/core_plugins/kibana_react/public'; -import { Query } from '../../../../../../src/legacy/core_plugins/data/public'; -import { QueryBarTopRow } from '../../../../../../src/legacy/core_plugins/data/public/query/query_bar'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Document, SavedObjectStore } from '../persistence'; import { EditorFrameInstance } from '../types'; @@ -271,7 +269,6 @@ export function App({ query={state.query} dateRangeFrom={state.dateRange.fromDate} dateRangeTo={state.dateRange.toDate} - toasts={core.notifications.toasts} savedObjectsClient={core.savedObjects.client} timeHistory={data.timefilter.history} /> 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 2b632b5620530..1e51825014abd 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, 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 744cc89c6e919..7e2956cf2bb4b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx @@ -5,7 +5,7 @@ */ import { Registry } from '@kbn/interpreter/target/common'; -import { CoreSetup, CoreStart } from 'src/core/public'; +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'; From 124b1e9024d1b986fb8bcaa3355c31f742ea8a56 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 1 Oct 2019 14:40:02 -0400 Subject: [PATCH 07/12] Respond to all review comments --- .../public/top_nav_menu/top_nav_menu.test.tsx | 30 ++-- .../public/top_nav_menu/top_nav_menu.tsx | 85 +++++------ x-pack/legacy/plugins/lens/index.ts | 8 +- .../plugins/lens/public/app_plugin/_app.scss | 1 - .../lens/public/app_plugin/app.test.tsx | 69 +++++++-- .../plugins/lens/public/app_plugin/app.tsx | 135 ++++++++++-------- .../editor_frame/editor_frame.test.tsx | 1 - .../editor_frame/expression_helpers.ts | 2 +- .../editor_frame/save.test.ts | 7 +- .../editor_frame/suggestion_panel.test.tsx | 2 +- 10 files changed, 200 insertions(+), 140 deletions(-) diff --git a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx b/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx index 39bf299cd8d12..9b49052cd6ee1 100644 --- a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.test.tsx @@ -18,9 +18,12 @@ */ import React from 'react'; +import { mount } from 'enzyme'; import { TopNavMenu } from './top_nav_menu'; import { TopNavMenuData } from './top_nav_menu_data'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; +import { I18nProvider } from '@kbn/i18n/react'; import { coreMock } from '../../../../../core/public/mocks'; const startMock = coreMock.createStart(); @@ -75,16 +78,23 @@ describe('TopNavMenu', () => { }); it('Should render search bar', () => { - const component = shallowWithIntl( - + const services = { + uiSettings: startMock.uiSettings, + savedObjects: startMock.savedObjects, + notifications: startMock.notifications, + http: startMock.http, + }; + + const component = mount( + + + + + ); expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); diff --git a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx b/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx index c99c71f97e1af..ea8410b7b609b 100644 --- a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx +++ b/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx @@ -21,10 +21,9 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { I18nProvider } from '@kbn/i18n/react'; -import { UiSettingsClientContract, CoreStart } from 'src/core/public'; import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; -import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; +import { useKibana } from '../../../../../plugins/kibana_react/public'; import { SearchBar, SearchBarProps, @@ -37,11 +36,7 @@ type Props = Partial & { showSearchBar?: boolean; // Search Bar dependencies - uiSettings?: UiSettingsClientContract; - savedObjects?: CoreStart['savedObjects']; - notifications?: CoreStart['notifications']; timeHistory?: TimeHistoryContract; - http?: CoreStart['http']; }; /* @@ -54,6 +49,8 @@ type Props = Partial & { **/ export function TopNavMenu(props: Props) { + const kibana = useKibana(); + function renderItems() { if (!props.config) return; return props.config.map((menuItem: TopNavMenuData, i: number) => { @@ -66,53 +63,47 @@ export function TopNavMenu(props: Props) { } function renderSearchBar() { - // Validate presense of all required fields + const { uiSettings, http, notifications, savedObjects } = kibana.services; + + // If any fields are missing, render nothing if ( !props.showSearchBar || - !props.savedObjects || - !props.http || - !props.notifications || + !savedObjects || + !http || + !notifications || + !uiSettings || !props.timeHistory ) return; return ( - - - + ); } 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 ce21369241791..206987841ecca 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 @@ -17,7 +17,7 @@ import { TopNavMenu, TopNavMenuData, } from '../../../../../../src/legacy/core_plugins/kibana_react/public'; -import { SavedObjectsClientContract } from 'src/core/public'; +import { SavedObjectsStart, ApplicationStart } from 'src/core/public'; import { coreMock } from 'src/core/public/mocks'; import { DataSetup } from 'src/legacy/core_plugins/data/public'; @@ -75,11 +75,21 @@ describe('Lens App', () => { docId?: string; docStorage: SavedObjectStore; redirectTo: (id?: string) => void; - savedObjectsClient: SavedObjectsClientContract; + savedObjects: SavedObjectsStart; + application: ApplicationStart; }> { return ({ editorFrame: createMockFrame(), - core, + core: { + ...core, + application: { + ...core.application, + capabilities: { + ...core.application.capabilities, + lens: { save: true, saveQuery: true, show: true }, + }, + }, + }, data: { indexPatterns: { indexPatterns: { @@ -102,7 +112,9 @@ describe('Lens App', () => { }, TopNavMenu: jest.fn(() =>
), redirectTo: jest.fn(id => {}), - savedObjectsClient: jest.fn(), + savedObjects: { + client: jest.fn(), + }, } as unknown) as jest.Mocked<{ editorFrame: EditorFrameInstance; core: typeof core; @@ -111,7 +123,8 @@ describe('Lens App', () => { docId?: string; docStorage: SavedObjectStore; redirectTo: (id?: string) => void; - savedObjectsClient: SavedObjectsClientContract; + savedObjects: SavedObjectsStart; + application: ApplicationStart; }>; } @@ -288,6 +301,29 @@ describe('Lens App', () => { )!; } + 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; @@ -462,9 +498,6 @@ describe('Lens App', () => { expect(TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ - // dateRangeFrom: 'now-14d', - // dateRangeTo: 'now-7d', - // query: { query: 'new', language: 'lucene' }, filters: [buildExistsFilter({ name: 'myfield' }, { id: 'index1' })], }), {} @@ -472,8 +505,6 @@ describe('Lens App', () => { expect(frame.mount).toHaveBeenCalledWith( expect.any(Element), expect.objectContaining({ - // dateRange: { fromDate: 'now-14d', toDate: 'now-7d' }, - // query: { query: 'new', language: 'lucene' }, filters: [buildExistsFilter({ name: 'myfield' }, { id: 'index1' })], }) ); @@ -481,6 +512,24 @@ describe('Lens App', () => { }); 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; 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 b13c049782415..e06b7bd428576 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -9,10 +9,11 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { Storage } from 'ui/storage'; -import { CoreStart } from 'src/core/public'; +import { CoreStart, NotificationsStart } from 'src/core/public'; import { DataSetup, IndexPattern as IndexPatternInstance, + IndexPatterns as IndexPatternsService, SavedQuery, Query, } from 'src/legacy/core_plugins/data/public'; @@ -26,7 +27,7 @@ import { NativeRenderer } from '../native_renderer'; interface State { isLoading: boolean; isDirty: boolean; - indexPatterns: IndexPatternInstance[]; + indexPatternsForTopNav: IndexPatternInstance[]; persistedDoc?: Document; // Properties needed to interface with TopNav @@ -63,7 +64,7 @@ export function App({ const [state, setState] = useState({ isLoading: !!docId, isDirty: false, - indexPatterns: [], + indexPatternsForTopNav: [], query: { query: '', language }, dateRange: { @@ -78,7 +79,7 @@ export function App({ useEffect(() => { const subscription = data.filter.filterManager.getUpdates$().subscribe({ next: () => { - setState({ ...state, filters: data.filter.filterManager.getFilters() }); + setState(s => ({ ...s, filters: data.filter.filterManager.getFilters() })); }, }); return () => { @@ -105,40 +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 => { - Promise.all( - doc.state.datasourceMetaData.filterableIndexPatterns.map(({ id }) => - data.indexPatterns.indexPatterns.get(id) - ) + getAllIndexPatterns( + doc.state.datasourceMetaData.filterableIndexPatterns, + data.indexPatterns.indexPatterns, + core.notifications ) .then(indexPatterns => { - setState({ - ...state, + setState(s => ({ + ...s, isLoading: false, persistedDoc: doc, query: doc.state.query, filters: doc.state.filters, - dateRange: doc.state.dateRange || state.dateRange, - indexPatterns, - }); + dateRange: doc.state.dateRange || s.dateRange, + indexPatternsForTopNav: indexPatterns, + })); }) .catch(() => { - setState({ ...state, isLoading: false }); - - core.notifications.toasts.addDanger( - i18n.translate('xpack.lens.editorFrame.indexPatternLoadingError', { - defaultMessage: 'Error loading index patterns', - }) - ); + 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', { @@ -154,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 }) => @@ -190,11 +185,11 @@ export function App({ .then(({ id }) => { // Prevents unnecessary network request and disables save button const newDoc = { ...lastKnownDocRef.current!, id }; - setState({ - ...state, + setState(s => ({ + ...s, isDirty: false, persistedDoc: newDoc, - }); + })); if (docId !== id) { redirectTo(id); } @@ -217,45 +212,50 @@ export function App({ screenTitle={'lens'} onQuerySubmit={payload => { const { dateRange, query } = payload; - setState({ - ...state, + setState(s => ({ + ...s, dateRange: { fromDate: dateRange.from, toDate: dateRange.to, }, - query: query || state.query, - }); + query: query || s.query, + })); }} filters={state.filters} onFiltersUpdated={filters => { data.filter.filterManager.setFilters(filters); }} appName={'lens'} - indexPatterns={state.indexPatterns} + indexPatterns={state.indexPatternsForTopNav} store={store} showSearchBar={true} showDatePicker={true} showQueryInput={true} showFilterBar={true} - showSaveQuery={true /* TODO: Use permissions */} + showSaveQuery={core.application.capabilities.lens.saveQuery as boolean} savedQuery={state.savedQuery} onSaved={savedQuery => { - setState({ ...state, savedQuery }); + setState(s => ({ ...s, savedQuery })); }} onSavedQueryUpdated={savedQuery => { data.filter.filterManager.setFilters( savedQuery.attributes.filters || state.filters ); - setState({ - ...state, - savedQuery, - query: savedQuery.attributes.query, - }); + 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({ - ...state, + setState(s => ({ + ...s, savedQuery: undefined, filters: [], query: { @@ -264,12 +264,11 @@ export function App({ store.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'), }, - }); + })); }} query={state.query} dateRangeFrom={state.dateRange.fromDate} dateRangeTo={state.dateRange.toDate} - savedObjectsClient={core.savedObjects.client} timeHistory={data.timefilter.history} />
@@ -292,24 +291,24 @@ export function App({ setState(s => ({ ...s, isDirty: true })); } - Promise.all( - filterableIndexPatterns.map(({ id }) => - data.indexPatterns.indexPatterns.get(id) - ) - ) - .then(indexPatterns => { - setState({ - ...state, - indexPatterns, - }); - }) - .catch(() => { - core.notifications.toasts.addDanger( - i18n.translate('xpack.lens.editorFrame.indexPatternLoadingError', { - defaultMessage: 'Error loading index patterns', - }) - ); + // Update the cached index patterns if the user made a change to any of them + if ( + state.indexPatternsForTopNav.length !== filterableIndexPatterns.length || + filterableIndexPatterns.filter( + ({ id }) => + !state.indexPatternsForTopNav.find(indexPattern => indexPattern.id === id) + ).length !== state.indexPatternsForTopNav.length + ) { + getAllIndexPatterns( + filterableIndexPatterns, + data.indexPatterns.indexPatterns, + core.notifications + ).then(indexPatterns => { + if (indexPatterns) { + setState(s => ({ ...s, indexPatternsForTopNav: indexPatterns })); + } }); + } }, }} /> @@ -319,3 +318,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/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 fa9bcc2829587..0d8886f8f8ea1 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 @@ -1515,7 +1515,6 @@ describe('editor_frame', () => { }, }); expect(onChange).toHaveBeenLastCalledWith({ - // indexPatternTitles: ['resolved'], filterableIndexPatterns: [{ id: '1', title: 'resolved' }], doc: { expression: '', 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 a78ce1ce27793..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, 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 36a4511c4271d..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 @@ -37,12 +37,7 @@ describe('save editor frame state', () => { }, query: { query: '', language: 'lucene' }, dateRange: { fromDate: 'now-7d', toDate: 'now' }, - filters: [ - buildExistsFilter({ name: '@timestamp' }, { id: 'indexpattern' }), - // meta: { index: 'indexpattern', negate: false, disabled: false, alias: null }, - // exists: { field: '@timestamp' }, - // }, - ], + filters: [buildExistsFilter({ name: '@timestamp' }, { id: 'indexpattern' })], }, }; 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" From 29abf4c36a787a28fcb2af7eb218107507b19af0 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 1 Oct 2019 14:53:13 -0400 Subject: [PATCH 08/12] Remove commented code --- .../plugins/lens/public/indexpattern_plugin/datapanel.tsx | 4 ---- 1 file changed, 4 deletions(-) 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 276fd0412db25..621ba25c3f8f5 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'; @@ -158,9 +157,6 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ }: Pick> & { currentIndexPatternId: string; indexPatterns: Record; - // dateRange: DatasourceDataPanelProps['dateRange']; - // query: Query; - // core: DatasourceDataPanelProps['core']; dragDropContext: DragContextState; showEmptyFields: boolean; onToggleEmptyFields: () => void; From 9bc82146518fcb0a8f564d7b72d736ed709993d7 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 1 Oct 2019 16:22:46 -0400 Subject: [PATCH 09/12] Top nav should be compatible as angular directive --- .../public/top_nav_menu/top_nav_menu.tsx | 78 ++++++++++++------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx b/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx index ea8410b7b609b..b373260328f71 100644 --- a/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx +++ b/src/legacy/core_plugins/kibana_react/public/top_nav_menu/top_nav_menu.tsx @@ -21,8 +21,10 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { I18nProvider } from '@kbn/i18n/react'; +import { UiSettingsClientContract, CoreStart } from 'src/core/public'; import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; +import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public'; import { useKibana } from '../../../../../plugins/kibana_react/public'; import { SearchBar, @@ -36,7 +38,11 @@ type Props = Partial & { showSearchBar?: boolean; // Search Bar dependencies + uiSettings?: UiSettingsClientContract; + savedObjects?: CoreStart['savedObjects']; + notifications?: CoreStart['notifications']; timeHistory?: TimeHistoryContract; + http?: CoreStart['http']; }; /* @@ -63,47 +69,59 @@ export function TopNavMenu(props: Props) { } function renderSearchBar() { - const { uiSettings, http, notifications, savedObjects } = kibana.services; + const http = kibana.services.http || props.http; + const notifications = kibana.services.notifications || props.notifications; + const savedObjects = kibana.services.savedObjects || props.savedObjects; + const uiSettings = kibana.services.uiSettings || props.uiSettings; // If any fields are missing, render nothing if ( !props.showSearchBar || - !savedObjects || !http || !notifications || + !savedObjects || !uiSettings || !props.timeHistory ) return; return ( - + + + ); } From 063913859feb30215bcecba38cfa5e31270c5189 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 1 Oct 2019 22:39:07 -0400 Subject: [PATCH 10/12] Fix rendering issue with filter updates --- .../editor_frame/workspace_panel.test.tsx | 56 +++++++++++++++++++ .../editor_frame/workspace_panel.tsx | 1 + 2 files changed, 57 insertions(+) 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 41eeef1c5723f..4fbd94dc9c779 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 @@ -6,6 +6,7 @@ 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 { @@ -328,6 +329,61 @@ 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} + /> + ); + + // "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 314b10796435b..854d1f9f26a86 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 @@ -121,6 +121,7 @@ export function InnerWorkspacePanel({ datasourceStates, framePublicAPI.dateRange, framePublicAPI.query, + framePublicAPI.filters, ]); useEffect(() => { From 3bf24ef18d85999487e503c15de05fe47d10a63c Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 2 Oct 2019 16:24:42 -0400 Subject: [PATCH 11/12] Respond to review comments and add onChange test --- packages/kbn-es-query/src/es_query/index.d.ts | 8 ++++ .../plugins/lens/public/app_plugin/app.tsx | 9 ++-- .../editor_frame/editor_frame.test.tsx | 44 ++++++++++++++++++- .../public/indexpattern_plugin/field_item.tsx | 4 +- 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/packages/kbn-es-query/src/es_query/index.d.ts b/packages/kbn-es-query/src/es_query/index.d.ts index 36d7552c22316..9510a18441e53 100644 --- a/packages/kbn-es-query/src/es_query/index.d.ts +++ b/packages/kbn-es-query/src/es_query/index.d.ts @@ -29,3 +29,11 @@ export function buildEsQuery( dateFormatTZ?: string | null; } ): unknown; +export function getEsQueryConfig(config: { + get: (name: string) => unknown; +}): { + allowLeadingWildcards: boolean; + queryStringOptions: unknown; + ignoreFilterIfFieldNotInIndex: boolean; + dateFormatTZ?: string | null; +}; 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 801a63dda2b51..3ee901d201aa5 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -192,11 +192,10 @@ export function App({ redirectTo(id); } }) - .catch(reason => { + .catch(() => { core.notifications.toasts.addDanger( i18n.translate('xpack.lens.editorFrame.docSavingError', { - defaultMessage: 'Error saving document {reason}', - values: { reason }, + defaultMessage: 'Error saving document', }) ); }); @@ -284,10 +283,10 @@ export function App({ // Update the cached index patterns if the user made a change to any of them if ( state.indexPatternsForTopNav.length !== filterableIndexPatterns.length || - filterableIndexPatterns.filter( + filterableIndexPatterns.find( ({ id }) => !state.indexPatternsForTopNav.find(indexPattern => indexPattern.id === id) - ).length !== state.indexPatternsForTopNav.length + ) ) { getAllIndexPatterns( filterableIndexPatterns, 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 0d8886f8f8ea1..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. @@ -1593,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/indexpattern_plugin/field_item.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/field_item.tsx index 90d6cdfc7b149..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 { Filter, buildEsQuery } 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'; @@ -113,7 +113,7 @@ export function FieldItem(props: FieldItemProps) { core.http .post(`/api/lens/index_stats/${indexPattern.title}/field`, { body: JSON.stringify({ - dslQuery: buildEsQuery(indexPattern, query, filters), + dslQuery: buildEsQuery(indexPattern, query, filters, getEsQueryConfig(core.uiSettings)), fromDate: dateRange.fromDate, toDate: dateRange.toDate, timeFieldName: indexPattern.timeFieldName, From dcd249521f9d6aa7069b9801911bd434e394942e Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 2 Oct 2019 17:45:20 -0400 Subject: [PATCH 12/12] Add specific test for the index pattern bug from Tina --- .../plugins/lens/public/app_plugin/app.test.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 a97a82a892e65..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 @@ -440,6 +440,22 @@ describe('Lens App', () => { }), {} ); + + // 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 in the search bar', () => {