[7.x] [Data] Query Input String manager (#72093) (#73634)

* [Data] Query Input String manager (#72093)

* improve test stability

* query string input manager (needed for search demo)

* docs

* dashboard

* Fix jest

* mock fix

* Allow restoring a saved query

* sync url

* Luke's fix to test

* cleanup

* lens jest tests

* docs

* use queryStringManager.getDefaultQuery
Don't sync query to global state

* Update app.test.tsx

lens mock

* jest fix

* jest

* use new api in the example

* Rename state param to query to match url state

* Apply changes to discover

* Update src/plugins/data/public/query/query_string/index.ts

Co-authored-by: Anton Dosov <dosantappdev@gmail.com>

* Improve query string state manager

* Cleanup dashboard code

* Handle refresh button

* Set initial dashboard state

* visualize state

* remove unused

* docs

* fix example

* fix jest

* fix filter app state in discover

* fix maps test

* jest

Co-authored-by: Anton Dosov <anton.dosov@elastic.co>
Co-authored-by: Anton Dosov <dosantappdev@gmail.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
# Conflicts:
#	src/plugins/data/public/public.api.md

* docs
This commit is contained in:
Liza Katz 2020-07-29 13:47:55 +03:00 committed by GitHub
parent 9ed82273d6
commit 25ceed986b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 432 additions and 290 deletions

View file

@ -9,9 +9,10 @@ Helper to setup two-way syncing of global data and a state container
<b>Signature:</b>
```typescript
connectToQueryState: <S extends QueryState>({ timefilter: { timefilter }, filterManager, state$, }: Pick<QueryStart | QuerySetup, 'timefilter' | 'filterManager' | 'state$'>, stateContainer: BaseStateContainer<S>, syncConfig: {
connectToQueryState: <S extends QueryState>({ timefilter: { timefilter }, filterManager, queryString, state$, }: Pick<QueryStart | QuerySetup, 'timefilter' | 'filterManager' | 'queryString' | 'state$'>, stateContainer: BaseStateContainer<S>, syncConfig: {
time?: boolean;
refreshInterval?: boolean;
filters?: FilterStateStore | boolean;
query?: boolean;
}) => () => void
```

View file

@ -17,6 +17,7 @@ export interface QueryState
| Property | Type | Description |
| --- | --- | --- |
| [filters](./kibana-plugin-plugins-data-public.querystate.filters.md) | <code>Filter[]</code> | |
| [query](./kibana-plugin-plugins-data-public.querystate.query.md) | <code>Query</code> | |
| [refreshInterval](./kibana-plugin-plugins-data-public.querystate.refreshinterval.md) | <code>RefreshInterval</code> | |
| [time](./kibana-plugin-plugins-data-public.querystate.time.md) | <code>TimeRange</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [QueryState](./kibana-plugin-plugins-data-public.querystate.md) &gt; [query](./kibana-plugin-plugins-data-public.querystate.query.md)
## QueryState.query property
<b>Signature:</b>
```typescript
query?: Query;
```

View file

@ -9,7 +9,7 @@ Helper to setup syncing of global data with the URL
<b>Signature:</b>
```typescript
syncQueryStateWithUrl: (query: Pick<QueryStart | QuerySetup, 'filterManager' | 'timefilter' | 'state$'>, kbnUrlStateStorage: IKbnUrlStateStorage) => {
syncQueryStateWithUrl: (query: Pick<QueryStart | QuerySetup, 'filterManager' | 'timefilter' | 'queryString' | 'state$'>, kbnUrlStateStorage: IKbnUrlStateStorage) => {
stop: () => void;
hasInheritedQueryFromUrl: boolean;
}

View file

@ -17,7 +17,7 @@
* under the License.
*/
import React, { useEffect, useRef, useState, useCallback } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { History } from 'history';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { Router } from 'react-router-dom';
@ -85,16 +85,9 @@ const App = ({ navigation, data, history, kbnUrlStateStorage }: StateDemoAppDeps
useGlobalStateSyncing(data.query, kbnUrlStateStorage);
useAppStateSyncing(appStateContainer, data.query, kbnUrlStateStorage);
const onQuerySubmit = useCallback(
({ query }) => {
appStateContainer.set({ ...appState, query });
},
[appStateContainer, appState]
);
const indexPattern = useIndexPattern(data);
if (!indexPattern)
return <div>No index pattern found. Please create an intex patter before loading...</div>;
return <div>No index pattern found. Please create an index patter before loading...</div>;
// Render the application DOM.
// Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract.
@ -107,8 +100,6 @@ const App = ({ navigation, data, history, kbnUrlStateStorage }: StateDemoAppDeps
showSearchBar={true}
indexPatterns={[indexPattern]}
useDefaultBehaviors={true}
onQuerySubmit={onQuerySubmit}
query={appState.query}
showSaveQuery={true}
/>
<EuiPage restrictWidth="1000px">
@ -200,7 +191,7 @@ function useAppStateSyncing<AppState extends QueryState>(
const stopSyncingQueryAppStateWithStateContainer = connectToQueryState(
query,
appStateContainer,
{ filters: esFilters.FilterStateStore.APP_STATE }
{ filters: esFilters.FilterStateStore.APP_STATE, query: true }
);
// sets up syncing app state container with url

View file

@ -52,7 +52,10 @@ export interface DashboardAppScope extends ng.IScope {
expandedPanel?: string;
getShouldShowEditHelp: () => boolean;
getShouldShowViewHelp: () => boolean;
updateQueryAndFetch: ({ query, dateRange }: { query: Query; dateRange?: TimeRange }) => void;
handleRefresh: (
{ query, dateRange }: { query?: Query; dateRange: TimeRange },
isUpdate?: boolean
) => void;
topNavMenu: any;
showAddPanel: any;
showSaveQuery: boolean;

View file

@ -25,12 +25,11 @@ import React, { useState, ReactElement } from 'react';
import ReactDOM from 'react-dom';
import angular from 'angular';
import { Observable, pipe, Subscription } from 'rxjs';
import { filter, map, mapTo, startWith, switchMap } from 'rxjs/operators';
import { Observable, pipe, Subscription, merge } from 'rxjs';
import { filter, map, debounceTime, mapTo, startWith, switchMap } from 'rxjs/operators';
import { History } from 'history';
import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public';
import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public';
import { TimeRange } from 'src/plugins/data/public';
import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen';
import {
@ -38,11 +37,9 @@ import {
esFilters,
IndexPattern,
IndexPatternsContract,
Query,
QueryState,
SavedQuery,
syncQueryStateWithUrl,
UI_SETTINGS,
} from '../../../data/public';
import { getSavedObjectFinder, SaveResult, showSaveModal } from '../../../saved_objects/public';
@ -81,8 +78,8 @@ import {
addFatalError,
AngularHttpError,
KibanaLegacyStart,
migrateLegacyQuery,
subscribeWithScope,
migrateLegacyQuery,
} from '../../../kibana_legacy/public';
export interface DashboardAppControllerDependencies extends RenderDeps {
@ -127,7 +124,6 @@ export class DashboardAppController {
$route,
$routeParams,
dashboardConfig,
localStorage,
indexPatterns,
savedQueryService,
embeddable,
@ -153,8 +149,8 @@ export class DashboardAppController {
navigation,
}: DashboardAppControllerDependencies) {
const filterManager = queryService.filterManager;
const queryFilter = filterManager;
const timefilter = queryService.timefilter.timefilter;
const queryStringManager = queryService.queryString;
const isEmbeddedExternally = Boolean($routeParams.embed);
// url param rules should only apply when embedded (e.g. url?embed=true)
@ -188,20 +184,30 @@ export class DashboardAppController {
// sync initial app filters from state to filterManager
// if there is an existing similar global filter, then leave it as global
filterManager.setAppFilters(_.cloneDeep(dashboardStateManager.appState.filters));
queryStringManager.setQuery(migrateLegacyQuery(dashboardStateManager.appState.query));
// setup syncing of app filters between appState and filterManager
const stopSyncingAppFilters = connectToQueryState(
queryService,
{
set: ({ filters }) => dashboardStateManager.setFilters(filters || []),
get: () => ({ filters: dashboardStateManager.appState.filters }),
set: ({ filters, query }) => {
dashboardStateManager.setFilters(filters || []);
dashboardStateManager.setQuery(query || queryStringManager.getDefaultQuery());
},
get: () => ({
filters: dashboardStateManager.appState.filters,
query: dashboardStateManager.getQuery(),
}),
state$: dashboardStateManager.appState$.pipe(
map((state) => ({
filters: state.filters,
query: queryStringManager.formatQuery(state.query),
}))
),
},
{
filters: esFilters.FilterStateStore.APP_STATE,
query: true,
}
);
@ -331,7 +337,7 @@ export class DashboardAppController {
const isEmptyInReadonlyMode = shouldShowUnauthorizedEmptyState();
return {
id: dashboardStateManager.savedDashboard.id || '',
filters: queryFilter.getFilters(),
filters: filterManager.getFilters(),
hidePanelTitles: dashboardStateManager.getHidePanelTitles(),
query: $scope.model.query,
timeRange: {
@ -356,7 +362,7 @@ export class DashboardAppController {
// https://github.com/angular/angular.js/wiki/Understanding-Scopes
$scope.model = {
query: dashboardStateManager.getQuery(),
filters: queryFilter.getFilters(),
filters: filterManager.getFilters(),
timeRestore: dashboardStateManager.getTimeRestore(),
title: dashboardStateManager.getTitle(),
description: dashboardStateManager.getDescription(),
@ -420,12 +426,12 @@ export class DashboardAppController {
if (
!esFilters.compareFilters(
container.getInput().filters,
queryFilter.getFilters(),
filterManager.getFilters(),
esFilters.COMPARE_ALL_OPTIONS
)
) {
// Add filters modifies the object passed to it, hence the clone deep.
queryFilter.addFilters(_.cloneDeep(container.getInput().filters));
filterManager.addFilters(_.cloneDeep(container.getInput().filters));
dashboardStateManager.applyFilters(
$scope.model.query,
@ -487,13 +493,8 @@ export class DashboardAppController {
});
dashboardStateManager.applyFilters(
dashboardStateManager.getQuery() || {
query: '',
language:
localStorage.get('kibana.userQueryLanguage') ||
uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE),
},
queryFilter.getFilters()
dashboardStateManager.getQuery() || queryStringManager.getDefaultQuery(),
filterManager.getFilters()
);
timefilter.disableTimeRangeSelector();
@ -567,21 +568,13 @@ export class DashboardAppController {
}
};
$scope.updateQueryAndFetch = function ({ query, dateRange }) {
if (dateRange) {
timefilter.setTime(dateRange);
}
const oldQuery = $scope.model.query;
if (_.isEqual(oldQuery, query)) {
$scope.handleRefresh = function (_payload, isUpdate) {
if (isUpdate === false) {
// The user can still request a reload in the query bar, even if the
// query is the same, and in that case, we have to explicitly ask for
// a reload, since no state changes will cause it.
lastReloadRequestTime = new Date().getTime();
refreshDashboardContainer();
} else {
$scope.model.query = query;
dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters);
}
};
@ -600,7 +593,7 @@ export class DashboardAppController {
// Making this method sync broke the updates.
// Temporary fix, until we fix the complex state in this file.
setTimeout(() => {
queryFilter.setFilters(allFilters);
filterManager.setFilters(allFilters);
}, 0);
};
@ -633,11 +626,6 @@ export class DashboardAppController {
$scope.indexPatterns = [];
$scope.$watch('model.query', (newQuery: Query) => {
const query = migrateLegacyQuery(newQuery) as Query;
$scope.updateQueryAndFetch({ query });
});
$scope.$watch(
() => dashboardCapabilities.saveQuery,
(newCapability) => {
@ -678,18 +666,11 @@ export class DashboardAppController {
showFilterBar,
indexPatterns: $scope.indexPatterns,
showSaveQuery: $scope.showSaveQuery,
query: $scope.model.query,
savedQuery: $scope.savedQuery,
onSavedQueryIdChange,
savedQueryId: dashboardStateManager.getSavedQueryId(),
useDefaultBehaviors: true,
onQuerySubmit: (payload: { dateRange: TimeRange; query?: Query }): void => {
if (!payload.query) {
$scope.updateQueryAndFetch({ query: $scope.model.query, dateRange: payload.dateRange });
} else {
$scope.updateQueryAndFetch({ query: payload.query, dateRange: payload.dateRange });
}
},
onQuerySubmit: $scope.handleRefresh,
};
};
const dashboardNavBar = document.getElementById('dashboardChrome');
@ -704,25 +685,11 @@ export class DashboardAppController {
};
$scope.timefilterSubscriptions$ = new Subscription();
const timeChanges$ = merge(timefilter.getRefreshIntervalUpdate$(), timefilter.getTimeUpdate$());
$scope.timefilterSubscriptions$.add(
subscribeWithScope(
$scope,
timefilter.getRefreshIntervalUpdate$(),
{
next: () => {
updateState();
refreshDashboardContainer();
},
},
(error: AngularHttpError | Error | string) => addFatalError(fatalErrors, error)
)
);
$scope.timefilterSubscriptions$.add(
subscribeWithScope(
$scope,
timefilter.getTimeUpdate$(),
timeChanges$,
{
next: () => {
updateState();
@ -1095,13 +1062,21 @@ export class DashboardAppController {
updateViewMode(dashboardStateManager.getViewMode());
const filterChanges = merge(filterManager.getUpdates$(), queryStringManager.getUpdates$()).pipe(
debounceTime(100)
);
// update root source when filters update
const updateSubscription = queryFilter.getUpdates$().subscribe({
const updateSubscription = filterChanges.subscribe({
next: () => {
$scope.model.filters = queryFilter.getFilters();
$scope.model.filters = filterManager.getFilters();
$scope.model.query = queryStringManager.getQuery();
dashboardStateManager.applyFilters($scope.model.query, $scope.model.filters);
if (dashboardContainer) {
dashboardContainer.updateInput({ filters: $scope.model.filters });
dashboardContainer.updateInput({
filters: $scope.model.filters,
query: $scope.model.query,
});
}
},
});

View file

@ -199,10 +199,11 @@ export const castEsToKbnFieldTypeName: (esType: ES_FIELD_TYPES | string) => KBN_
// Warning: (ae-missing-release-tag) "connectToQueryState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export const connectToQueryState: <S extends QueryState>({ timefilter: { timefilter }, filterManager, state$, }: Pick<QueryStart | QuerySetup, 'timefilter' | 'filterManager' | 'state$'>, stateContainer: BaseStateContainer<S>, syncConfig: {
export const connectToQueryState: <S extends QueryState>({ timefilter: { timefilter }, filterManager, queryString, state$, }: Pick<QueryStart | QuerySetup, 'timefilter' | 'filterManager' | 'queryString' | 'state$'>, stateContainer: BaseStateContainer<S>, syncConfig: {
time?: boolean;
refreshInterval?: boolean;
filters?: FilterStateStore | boolean;
query?: boolean;
}) => () => void;
// Warning: (ae-missing-release-tag) "createSavedQueryService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@ -1397,6 +1398,8 @@ export interface QueryState {
// (undocumented)
filters?: Filter[];
// (undocumented)
query?: Query;
// (undocumented)
refreshInterval?: RefreshInterval;
// (undocumented)
time?: TimeRange;
@ -1771,7 +1774,7 @@ export type StatefulSearchBarProps = SearchBarOwnProps & {
// Warning: (ae-missing-release-tag) "syncQueryStateWithUrl" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export const syncQueryStateWithUrl: (query: Pick<QueryStart | QuerySetup, 'filterManager' | 'timefilter' | 'state$'>, kbnUrlStateStorage: IKbnUrlStateStorage) => {
export const syncQueryStateWithUrl: (query: Pick<QueryStart | QuerySetup, 'filterManager' | 'timefilter' | 'queryString' | 'state$'>, kbnUrlStateStorage: IKbnUrlStateStorage) => {
stop: () => void;
hasInheritedQueryFromUrl: boolean;
};
@ -1919,7 +1922,7 @@ export const UI_SETTINGS: {
// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:41:60 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:54:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:55:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/types.ts:63:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts

View file

@ -21,6 +21,7 @@ import { Observable } from 'rxjs';
import { QueryService, QuerySetup, QueryStart } from '.';
import { timefilterServiceMock } from './timefilter/timefilter_service.mock';
import { createFilterManagerMock } from './filter_manager/filter_manager.mock';
import { queryStringManagerMock } from './query_string/query_string_manager.mock';
type QueryServiceClientContract = PublicMethodsOf<QueryService>;
@ -28,6 +29,7 @@ const createSetupContractMock = () => {
const setupContract: jest.Mocked<QuerySetup> = {
filterManager: createFilterManagerMock(),
timefilter: timefilterServiceMock.createSetupContract(),
queryString: queryStringManagerMock.createSetupContract(),
state$: new Observable(),
};
@ -38,6 +40,7 @@ const createStartContractMock = () => {
const startContract: jest.Mocked<QueryStart> = {
addToQueryLog: jest.fn(),
filterManager: createFilterManagerMock(),
queryString: queryStringManagerMock.createStartContract(),
savedQueries: jest.fn() as any,
state$: new Observable(),
timefilter: timefilterServiceMock.createStartContract(),

View file

@ -25,6 +25,7 @@ import { createAddToQueryLog } from './lib';
import { TimefilterService, TimefilterSetup } from './timefilter';
import { createSavedQueryService } from './saved_query/saved_query_service';
import { createQueryStateObservable } from './state_sync/create_global_query_observable';
import { QueryStringManager, QueryStringContract } from './query_string';
/**
* Query Service
@ -45,6 +46,7 @@ interface QueryServiceStartDependencies {
export class QueryService {
filterManager!: FilterManager;
timefilter!: TimefilterSetup;
queryStringManager!: QueryStringContract;
state$!: ReturnType<typeof createQueryStateObservable>;
@ -57,14 +59,18 @@ export class QueryService {
storage,
});
this.queryStringManager = new QueryStringManager(storage, uiSettings);
this.state$ = createQueryStateObservable({
filterManager: this.filterManager,
timefilter: this.timefilter,
queryString: this.queryStringManager,
}).pipe(share());
return {
filterManager: this.filterManager,
timefilter: this.timefilter,
queryString: this.queryStringManager,
state$: this.state$,
};
}
@ -76,6 +82,7 @@ export class QueryService {
uiSettings,
}),
filterManager: this.filterManager,
queryString: this.queryStringManager,
savedQueries: createSavedQueryService(savedObjectsClient),
state$: this.state$,
timefilter: this.timefilter,

View file

@ -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 { QueryStringContract, QueryStringManager } from './query_string_manager';

View file

@ -0,0 +1,37 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { QueryStringContract } from '.';
const createSetupContractMock = () => {
const queryStringManagerMock: jest.Mocked<QueryStringContract> = {
getQuery: jest.fn(),
setQuery: jest.fn(),
getUpdates$: jest.fn(),
getDefaultQuery: jest.fn(),
formatQuery: jest.fn(),
clearQuery: jest.fn(),
};
return queryStringManagerMock;
};
export const queryStringManagerMock = {
createSetupContract: createSetupContractMock,
createStartContract: createSetupContractMock,
};

View file

@ -0,0 +1,90 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import { BehaviorSubject } from 'rxjs';
import { CoreStart } from 'kibana/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { Query, UI_SETTINGS } from '../../../common';
export class QueryStringManager {
private query$: BehaviorSubject<Query>;
constructor(
private readonly storage: IStorageWrapper,
private readonly uiSettings: CoreStart['uiSettings']
) {
this.query$ = new BehaviorSubject<Query>(this.getDefaultQuery());
}
private getDefaultLanguage() {
return (
this.storage.get('kibana.userQueryLanguage') ||
this.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE)
);
}
public getDefaultQuery() {
return {
query: '',
language: this.getDefaultLanguage(),
};
}
public formatQuery(query: Query | string | undefined): Query {
if (!query) {
return this.getDefaultQuery();
} else if (typeof query === 'string') {
return {
query,
language: this.getDefaultLanguage(),
};
} else {
return query;
}
}
public getUpdates$ = () => {
return this.query$.asObservable();
};
public getQuery = (): Query => {
return this.query$.getValue();
};
/**
* Updates the query.
* @param {Query} query
*/
public setQuery = (query: Query) => {
const curQuery = this.query$.getValue();
if (query?.language !== curQuery.language || query?.query !== curQuery.query) {
this.query$.next(query);
}
};
/**
* Resets the query to the default one.
*/
public clearQuery = () => {
this.setQuery(this.getDefaultQuery());
};
}
export type QueryStringContract = PublicMethodsOf<QueryStringManager>;

View file

@ -48,6 +48,8 @@ setupMock.uiSettings.get.mockImplementation((key: string) => {
switch (key) {
case UI_SETTINGS.FILTERS_PINNED_BY_DEFAULT:
return true;
case UI_SETTINGS.SEARCH_QUERY_LANGUAGE:
return 'kuery';
case 'timepicker:timeDefaults':
return { from: 'now-15m', to: 'now' };
case UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS:

View file

@ -35,15 +35,24 @@ export const connectToQueryState = <S extends QueryState>(
{
timefilter: { timefilter },
filterManager,
queryString,
state$,
}: Pick<QueryStart | QuerySetup, 'timefilter' | 'filterManager' | 'state$'>,
}: Pick<QueryStart | QuerySetup, 'timefilter' | 'filterManager' | 'queryString' | 'state$'>,
stateContainer: BaseStateContainer<S>,
syncConfig: { time?: boolean; refreshInterval?: boolean; filters?: FilterStateStore | boolean }
syncConfig: {
time?: boolean;
refreshInterval?: boolean;
filters?: FilterStateStore | boolean;
query?: boolean;
}
) => {
const syncKeys: Array<keyof QueryStateChange> = [];
if (syncConfig.time) {
syncKeys.push('time');
}
if (syncConfig.query) {
syncKeys.push('query');
}
if (syncConfig.refreshInterval) {
syncKeys.push('refreshInterval');
}
@ -133,6 +142,9 @@ export const connectToQueryState = <S extends QueryState>(
if (syncConfig.time && changes.time) {
newState.time = timefilter.getTime();
}
if (syncConfig.query && changes.query) {
newState.query = queryString.getQuery();
}
if (syncConfig.refreshInterval && changes.refreshInterval) {
newState.refreshInterval = timefilter.getRefreshInterval();
}
@ -173,6 +185,13 @@ export const connectToQueryState = <S extends QueryState>(
}
}
if (syncConfig.query) {
const curQuery = state.query || queryString.getQuery();
if (!_.isEqual(curQuery, queryString.getQuery())) {
queryString.setQuery(_.cloneDeep(curQuery));
}
}
if (syncConfig.filters) {
const filters = state.filters || [];
if (syncConfig.filters === true) {

View file

@ -24,23 +24,31 @@ import { FilterManager } from '../filter_manager';
import { QueryState, QueryStateChange } from './index';
import { createStateContainer } from '../../../../kibana_utils/public';
import { isFilterPinned, compareFilters, COMPARE_ALL_OPTIONS } from '../../../common';
import { QueryStringContract } from '../query_string';
export function createQueryStateObservable({
timefilter: { timefilter },
filterManager,
queryString,
}: {
timefilter: TimefilterSetup;
filterManager: FilterManager;
queryString: QueryStringContract;
}): Observable<{ changes: QueryStateChange; state: QueryState }> {
return new Observable((subscriber) => {
const state = createStateContainer<QueryState>({
time: timefilter.getTime(),
refreshInterval: timefilter.getRefreshInterval(),
filters: filterManager.getFilters(),
query: queryString.getQuery(),
});
let currentChange: QueryStateChange = {};
const subs: Subscription[] = [
queryString.getUpdates$().subscribe(() => {
currentChange.query = true;
state.set({ ...state.get(), query: queryString.getQuery() });
}),
timefilter.getTimeUpdate$().subscribe(() => {
currentChange.time = true;
state.set({ ...state.get(), time: timefilter.getTime() });

View file

@ -43,6 +43,8 @@ setupMock.uiSettings.get.mockImplementation((key: string) => {
return true;
case 'timepicker:timeDefaults':
return { from: 'now-15m', to: 'now' };
case 'search:queryLanguage':
return 'kuery';
case UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS:
return { pause: false, value: 0 };
default:

View file

@ -35,7 +35,7 @@ const GLOBAL_STATE_STORAGE_KEY = '_g';
* @param kbnUrlStateStorage to use for syncing
*/
export const syncQueryStateWithUrl = (
query: Pick<QueryStart | QuerySetup, 'filterManager' | 'timefilter' | 'state$'>,
query: Pick<QueryStart | QuerySetup, 'filterManager' | 'timefilter' | 'queryString' | 'state$'>,
kbnUrlStateStorage: IKbnUrlStateStorage
) => {
const {

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { Filter, RefreshInterval, TimeRange } from '../../../common';
import { Filter, RefreshInterval, TimeRange, Query } from '../../../common';
/**
* All query state service state
@ -26,6 +26,7 @@ export interface QueryState {
time?: TimeRange;
refreshInterval?: RefreshInterval;
filters?: Filter[];
query?: Query;
}
type QueryStateChangePartial = {

View file

@ -18,7 +18,7 @@
*/
import _ from 'lodash';
import React, { useState, useEffect, useRef } from 'react';
import React, { useEffect, useRef } from 'react';
import { CoreStart } from 'src/core/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { KibanaContextProvider } from '../../../../kibana_react/public';
@ -28,7 +28,8 @@ import { useFilterManager } from './lib/use_filter_manager';
import { useTimefilter } from './lib/use_timefilter';
import { useSavedQuery } from './lib/use_saved_query';
import { DataPublicPluginStart } from '../../types';
import { Filter, Query, TimeRange, UI_SETTINGS } from '../../../common';
import { Filter, Query, TimeRange } from '../../../common';
import { useQueryStringManager } from './lib/use_query_string_manager';
interface StatefulSearchBarDeps {
core: CoreStart;
@ -65,8 +66,7 @@ const defaultOnRefreshChange = (queryService: QueryStart) => {
const defaultOnQuerySubmit = (
props: StatefulSearchBarProps,
queryService: QueryStart,
currentQuery: Query,
setQueryStringState: Function
currentQuery: Query
) => {
if (!props.useDefaultBehaviors) return props.onQuerySubmit;
@ -78,7 +78,11 @@ const defaultOnQuerySubmit = (
!_.isEqual(payload.query, currentQuery);
if (isUpdate) {
timefilter.setTime(payload.dateRange);
setQueryStringState(payload.query);
if (payload.query) {
queryService.queryString.setQuery(payload.query);
} else {
queryService.queryString.clearQuery();
}
} else {
// Refresh button triggered for an update
if (props.onQuerySubmit)
@ -121,30 +125,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps)
return (props: StatefulSearchBarProps) => {
const { useDefaultBehaviors } = props;
// Handle queries
const queryRef = useRef(props.query);
const onQuerySubmitRef = useRef(props.onQuerySubmit);
const defaultQuery = {
query: '',
language:
storage.get('kibana.userQueryLanguage') ||
core.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE),
};
const [query, setQuery] = useState<Query>(props.query || defaultQuery);
useEffect(() => {
if (props.query !== queryRef.current) {
queryRef.current = props.query;
setQuery(props.query || defaultQuery);
}
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [defaultQuery, props.query]);
useEffect(() => {
if (props.onQuerySubmit !== onQuerySubmitRef.current) {
onQuerySubmitRef.current = props.onQuerySubmit;
}
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [props.onQuerySubmit]);
// handle service state updates.
// i.e. filters being added from a visualization directly to filterManager.
@ -152,6 +133,10 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps)
filters: props.filters,
filterManager: data.query.filterManager,
});
const { query } = useQueryStringManager({
query: props.query,
queryStringManager: data.query.queryString,
});
const { timeRange, refreshInterval } = useTimefilter({
dateRangeFrom: props.dateRangeFrom,
dateRangeTo: props.dateRangeTo,
@ -163,10 +148,8 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps)
// Fetch and update UI from saved query
const { savedQuery, setSavedQuery, clearSavedQuery } = useSavedQuery({
queryService: data.query,
setQuery,
savedQueryId: props.savedQueryId,
notifications: core.notifications,
defaultLanguage: defaultQuery.language,
});
// Fire onQuerySubmit on query or timerange change
@ -210,7 +193,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps)
onFiltersUpdated={defaultFiltersUpdated(data.query)}
onRefreshChange={defaultOnRefreshChange(data.query)}
savedQuery={savedQuery}
onQuerySubmit={defaultOnQuerySubmit(props, data.query, query, setQuery)}
onQuerySubmit={defaultOnQuerySubmit(props, data.query, query)}
onClearSavedQuery={defaultOnClearSavedQuery(props, clearSavedQuery)}
onSavedQueryUpdated={defaultOnSavedQueryUpdated(props, setSavedQuery)}
onSaved={defaultOnSavedQueryUpdated(props, setSavedQuery)}

View file

@ -21,10 +21,8 @@ import { clearStateFromSavedQuery } from './clear_saved_query';
import { dataPluginMock } from '../../../mocks';
import { DataPublicPluginStart } from '../../../types';
import { Query } from '../../..';
describe('clearStateFromSavedQuery', () => {
const DEFAULT_LANGUAGE = 'banana';
let dataMock: jest.Mocked<DataPublicPluginStart>;
beforeEach(() => {
@ -32,19 +30,9 @@ describe('clearStateFromSavedQuery', () => {
});
it('should clear filters and query', async () => {
const setQueryState = jest.fn();
dataMock.query.filterManager.removeAll = jest.fn();
clearStateFromSavedQuery(dataMock.query, setQueryState, DEFAULT_LANGUAGE);
expect(setQueryState).toHaveBeenCalled();
expect(dataMock.query.filterManager.removeAll).toHaveBeenCalled();
});
it('should use search:queryLanguage', async () => {
const setQueryState = jest.fn();
dataMock.query.filterManager.removeAll = jest.fn();
clearStateFromSavedQuery(dataMock.query, setQueryState, DEFAULT_LANGUAGE);
expect(setQueryState).toHaveBeenCalled();
expect((setQueryState.mock.calls[0][0] as Query).language).toBe(DEFAULT_LANGUAGE);
clearStateFromSavedQuery(dataMock.query);
expect(dataMock.query.queryString.clearQuery).toHaveBeenCalled();
expect(dataMock.query.filterManager.removeAll).toHaveBeenCalled();
});
});

View file

@ -18,14 +18,7 @@
*/
import { QueryStart } from '../../../query';
export const clearStateFromSavedQuery = (
queryService: QueryStart,
setQueryStringState: Function,
defaultLanguage: string
) => {
export const clearStateFromSavedQuery = (queryService: QueryStart) => {
queryService.filterManager.removeAll();
setQueryStringState({
query: '',
language: defaultLanguage,
});
queryService.queryString.clearQuery();
};

View file

@ -47,37 +47,34 @@ describe('populateStateFromSavedQuery', () => {
});
it('should set query', async () => {
const setQueryState = jest.fn();
const savedQuery: SavedQuery = {
...baseSavedQuery,
};
populateStateFromSavedQuery(dataMock.query, setQueryState, savedQuery);
expect(setQueryState).toHaveBeenCalled();
populateStateFromSavedQuery(dataMock.query, savedQuery);
expect(dataMock.query.queryString.setQuery).toHaveBeenCalled();
});
it('should set filters', async () => {
const setQueryState = jest.fn();
const savedQuery: SavedQuery = {
...baseSavedQuery,
};
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
savedQuery.attributes.filters = [f1];
populateStateFromSavedQuery(dataMock.query, setQueryState, savedQuery);
expect(setQueryState).toHaveBeenCalled();
populateStateFromSavedQuery(dataMock.query, savedQuery);
expect(dataMock.query.queryString.setQuery).toHaveBeenCalled();
expect(dataMock.query.filterManager.setFilters).toHaveBeenCalledWith([f1]);
});
it('should preserve global filters', async () => {
const globalFilter = getFilter(FilterStateStore.GLOBAL_STATE, false, false, 'age', 34);
dataMock.query.filterManager.getGlobalFilters = jest.fn().mockReturnValue([globalFilter]);
const setQueryState = jest.fn();
const savedQuery: SavedQuery = {
...baseSavedQuery,
};
const f1 = getFilter(FilterStateStore.APP_STATE, false, false, 'age', 34);
savedQuery.attributes.filters = [f1];
populateStateFromSavedQuery(dataMock.query, setQueryState, savedQuery);
expect(setQueryState).toHaveBeenCalled();
populateStateFromSavedQuery(dataMock.query, savedQuery);
expect(dataMock.query.queryString.setQuery).toHaveBeenCalled();
expect(dataMock.query.filterManager.setFilters).toHaveBeenCalledWith([globalFilter, f1]);
});
@ -97,7 +94,7 @@ describe('populateStateFromSavedQuery', () => {
dataMock.query.timefilter.timefilter.setTime = jest.fn();
dataMock.query.timefilter.timefilter.setRefreshInterval = jest.fn();
populateStateFromSavedQuery(dataMock.query, jest.fn(), savedQuery);
populateStateFromSavedQuery(dataMock.query, savedQuery);
expect(dataMock.query.timefilter.timefilter.setTime).toHaveBeenCalledWith({
from: savedQuery.attributes.timefilter.from,

View file

@ -19,14 +19,11 @@
import { QueryStart, SavedQuery } from '../../../query';
export const populateStateFromSavedQuery = (
queryService: QueryStart,
setQueryStringState: Function,
savedQuery: SavedQuery
) => {
export const populateStateFromSavedQuery = (queryService: QueryStart, savedQuery: SavedQuery) => {
const {
timefilter: { timefilter },
filterManager,
queryString,
} = queryService;
// timefilter
if (savedQuery.attributes.timefilter) {
@ -40,7 +37,7 @@ export const populateStateFromSavedQuery = (
}
// query string
setQueryStringState(savedQuery.attributes.query);
queryString.setQuery(savedQuery.attributes.query);
// filters
const savedQueryFilters = savedQuery.attributes.filters || [];

View file

@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useState, useEffect } from 'react';
import { Subscription } from 'rxjs';
import { Query } from '../../..';
import { QueryStringContract } from '../../../query/query_string';
interface UseQueryStringProps {
query?: Query;
queryStringManager: QueryStringContract;
}
export const useQueryStringManager = (props: UseQueryStringProps) => {
// Filters should be either what's passed in the initial state or the current state of the filter manager
const [query, setQuery] = useState<Query>(props.query || props.queryStringManager.getQuery());
useEffect(() => {
const subscriptions = new Subscription();
subscriptions.add(
props.queryStringManager.getUpdates$().subscribe({
next: () => {
const newQuery = props.queryStringManager.getQuery();
setQuery(newQuery);
},
})
);
return () => {
subscriptions.unsubscribe();
};
}, [props.queryStringManager]);
return { query };
};

View file

@ -27,10 +27,8 @@ import { clearStateFromSavedQuery } from './clear_saved_query';
interface UseSavedQueriesProps {
queryService: DataPublicPluginStart['query'];
setQuery: Function;
notifications: CoreStart['notifications'];
savedQueryId?: string;
defaultLanguage: string;
}
interface UseSavedQueriesReturn {
@ -41,7 +39,6 @@ interface UseSavedQueriesReturn {
export const useSavedQuery = (props: UseSavedQueriesProps): UseSavedQueriesReturn => {
// Handle saved queries
const defaultLanguage = props.defaultLanguage;
const [savedQuery, setSavedQuery] = useState<SavedQuery | undefined>();
// Effect is used to convert a saved query id into an object
@ -53,12 +50,12 @@ export const useSavedQuery = (props: UseSavedQueriesProps): UseSavedQueriesRetur
// Make sure we set the saved query to the most recent one
if (newSavedQuery && newSavedQuery.id === savedQueryId) {
setSavedQuery(newSavedQuery);
populateStateFromSavedQuery(props.queryService, props.setQuery, newSavedQuery);
populateStateFromSavedQuery(props.queryService, newSavedQuery);
}
} catch (error) {
// Clear saved query
setSavedQuery(undefined);
clearStateFromSavedQuery(props.queryService, props.setQuery, defaultLanguage);
clearStateFromSavedQuery(props.queryService);
// notify of saving error
props.notifications.toasts.addWarning({
title: i18n.translate('data.search.unableToGetSavedQueryToastTitle', {
@ -73,23 +70,21 @@ export const useSavedQuery = (props: UseSavedQueriesProps): UseSavedQueriesRetur
if (props.savedQueryId) fetchSavedQuery(props.savedQueryId);
else setSavedQuery(undefined);
}, [
defaultLanguage,
props.notifications.toasts,
props.queryService,
props.queryService.savedQueries,
props.savedQueryId,
props.setQuery,
]);
return {
savedQuery,
setSavedQuery: (q: SavedQuery) => {
setSavedQuery(q);
populateStateFromSavedQuery(props.queryService, props.setQuery, q);
populateStateFromSavedQuery(props.queryService, q);
},
clearSavedQuery: () => {
setSavedQuery(undefined);
clearStateFromSavedQuery(props.queryService, props.setQuery, defaultLanguage);
clearStateFromSavedQuery(props.queryService);
},
};
};

View file

@ -6,9 +6,8 @@
app-name="'discover'"
config="topNavMenu"
index-patterns="[indexPattern]"
on-query-submit="updateQuery"
on-query-submit="handleRefresh"
on-saved-query-id-change="updateSavedQueryId"
query="state.query"
saved-query-id="state.savedQuery"
screen-title="screenTitle"
show-date-picker="indexPattern.isTimeBased()"

View file

@ -70,9 +70,7 @@ import {
indexPatterns as indexPatternsUtils,
connectToQueryState,
syncQueryStateWithUrl,
getDefaultQuery,
search,
UI_SETTINGS,
} from '../../../../data/public';
import { getIndexPatternId } from '../helpers/get_index_pattern_id';
import { addFatalError } from '../../../../kibana_legacy/public';
@ -191,16 +189,7 @@ app.directive('discoverApp', function () {
};
});
function discoverController(
$element,
$route,
$scope,
$timeout,
$window,
Promise,
localStorage,
uiCapabilities
) {
function discoverController($element, $route, $scope, $timeout, $window, Promise, uiCapabilities) {
const { isDefault: isDefaultType } = indexPatternsUtils;
const subscriptions = new Subscription();
const $fetchObservable = new Subject();
@ -246,11 +235,15 @@ function discoverController(
// sync initial app filters from state to filterManager
filterManager.setAppFilters(_.cloneDeep(appStateContainer.getState().filters));
data.query.queryString.setQuery(appStateContainer.getState().query);
const stopSyncingQueryAppStateWithStateContainer = connectToQueryState(
data.query,
appStateContainer,
{ filters: esFilters.FilterStateStore.APP_STATE }
{
filters: esFilters.FilterStateStore.APP_STATE,
query: true,
}
);
const appStateUnsubscribe = appStateContainer.subscribe(async (newState) => {
@ -262,7 +255,7 @@ function discoverController(
$scope.state = { ...newState };
// detect changes that should trigger fetching of new data
const changes = ['interval', 'sort', 'query'].filter(
const changes = ['interval', 'sort'].filter(
(prop) => !_.isEqual(newStatePartial[prop], oldStatePartial[prop])
);
@ -593,12 +586,7 @@ function discoverController(
};
function getStateDefaults() {
const query =
$scope.searchSource.getField('query') ||
getDefaultQuery(
localStorage.get('kibana.userQueryLanguage') ||
config.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE)
);
const query = $scope.searchSource.getField('query') || data.query.queryString.getDefaultQuery();
return {
query,
sort: getSortArray(savedSearch.sort, $scope.indexPattern),
@ -635,12 +623,7 @@ function discoverController(
const init = _.once(() => {
$scope.updateDataSource().then(async () => {
const searchBarChanges = merge(
timefilter.getAutoRefreshFetch$(),
timefilter.getFetch$(),
filterManager.getFetches$(),
$fetchObservable
).pipe(debounceTime(100));
const searchBarChanges = merge(data.query.state$, $fetchObservable).pipe(debounceTime(100));
subscriptions.add(
subscribeWithScope(
@ -824,9 +807,8 @@ function discoverController(
});
};
$scope.updateQuery = function ({ query }, isUpdate = true) {
if (!_.isEqual(query, appStateContainer.getState().query) || isUpdate === false) {
setAppState({ query });
$scope.handleRefresh = function (_payload, isUpdate) {
if (isUpdate === false) {
$fetchObservable.next();
}
};
@ -976,7 +958,7 @@ function discoverController(
config.get(SORT_DEFAULT_ORDER_SETTING)
)
)
.setField('query', $scope.state.query || null)
.setField('query', data.query.queryString.getQuery() || null)
.setField('filter', filterManager.getFilters());
return Promise.resolve();
};

View file

@ -35,12 +35,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import {
esQuery,
IndexPattern,
Query,
UI_SETTINGS,
} from '../../../../../../../plugins/data/public';
import { esQuery, IndexPattern, Query } from '../../../../../../../plugins/data/public';
import { context as contextType } from '../../../../../../kibana_react/public';
import { IndexPatternManagmentContextValue } from '../../../../types';
import { ExecuteScript } from '../../types';
@ -248,10 +243,7 @@ export class TestScript extends Component<TestScriptProps, TestScriptState> {
showFilterBar={false}
showDatePicker={false}
showQueryInput={true}
query={{
language: this.context.services.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE),
query: '',
}}
query={this.context.services.data.query.queryString.getDefaultQuery()}
onQuerySubmit={this.previewScript}
indexPatterns={[this.props.indexPattern]}
customSubmitButton={

View file

@ -23,7 +23,7 @@ import { htmlIdGenerator, EuiButton, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { useMount } from 'react-use';
import { Query, UI_SETTINGS } from '../../../../data/public';
import { Query } from '../../../../data/public';
import { useKibana } from '../../../../kibana_react/public';
import { FilterRow } from './filter';
import { AggParamEditorProps } from '../agg_param_props';
@ -70,7 +70,7 @@ function FiltersParamEditor({ agg, value = [], setValue }: AggParamEditorProps<F
updateFilters([
...filters,
{
input: { query: '', language: services.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE) },
input: services.data.query.queryString.getDefaultQuery(),
label: '',
id: generateId(),
},

View file

@ -18,10 +18,8 @@
*/
import React, { memo, useCallback, useMemo, useState, useEffect } from 'react';
import { isEqual } from 'lodash';
import { OverlayRef } from 'kibana/public';
import { Query } from 'src/plugins/data/public';
import { useKibana } from '../../../../kibana_react/public';
import {
VisualizeServices,
@ -68,15 +66,13 @@ const TopNav = ({
setInspectorSession(session);
}, [embeddableHandler]);
const updateQuery = useCallback(
({ query }: { query?: Query }) => {
if (!isEqual(currentAppState.query, query)) {
stateContainer.transitions.set('query', query || currentAppState.query);
} else {
const handleRefresh = useCallback(
(_payload: any, isUpdate?: boolean) => {
if (isUpdate === false) {
savedVisInstance.embeddableHandler.reload();
}
},
[currentAppState.query, savedVisInstance.embeddableHandler, stateContainer.transitions]
[savedVisInstance.embeddableHandler]
);
const config = useMemo(() => {
@ -149,8 +145,7 @@ const TopNav = ({
<TopNavMenu
appName={APP_NAME}
config={config}
query={currentAppState.query}
onQuerySubmit={updateQuery}
onQuerySubmit={handleRefresh}
savedQueryId={currentAppState.savedQuery}
onSavedQueryIdChange={stateContainer.transitions.updateSavedQuery}
indexPatterns={indexPattern ? [indexPattern] : undefined}

View file

@ -105,10 +105,16 @@ describe('useEditorUpdates', () => {
to: 'now',
};
mockFilters = ['mockFilters'];
const mockQuery = {
query: '',
language: 'kuery',
};
// @ts-expect-error
mockServices.data.query.timefilter.timefilter.getTime.mockImplementation(() => timeRange);
// @ts-expect-error
mockServices.data.query.filterManager.getFilters.mockImplementation(() => mockFilters);
// @ts-expect-error
mockServices.data.query.queryString.getQuery.mockImplementation(() => mockQuery);
});
test('should set up current app state and render the editor', () => {

View file

@ -20,9 +20,7 @@
import { useEffect, useState } from 'react';
import { isEqual } from 'lodash';
import { EventEmitter } from 'events';
import { merge } from 'rxjs';
import { migrateLegacyQuery } from '../../../../../kibana_legacy/public';
import {
VisualizeServices,
VisualizeAppState,
@ -47,6 +45,8 @@ export const useEditorUpdates = (
const {
timefilter: { timefilter },
filterManager,
queryString,
state$,
} = services.data.query;
const { embeddableHandler, savedVis, savedSearch, vis } = savedVisInstance;
const initialState = appState.getState();
@ -60,7 +60,7 @@ export const useEditorUpdates = (
uiState: vis.uiState,
timeRange: timefilter.getTime(),
filters: filterManager.getFilters(),
query: appState.getState().query,
query: queryString.getQuery(),
linked: !!vis.data.savedSearchId,
savedSearch,
});
@ -68,17 +68,12 @@ export const useEditorUpdates = (
embeddableHandler.updateInput({
timeRange: timefilter.getTime(),
filters: filterManager.getFilters(),
query: appState.getState().query,
query: queryString.getQuery(),
});
}
};
const subscriptions = merge(
timefilter.getTimeUpdate$(),
timefilter.getAutoRefreshFetch$(),
timefilter.getFetch$(),
filterManager.getFetches$()
).subscribe({
const subscriptions = state$.subscribe({
next: reloadVisualization,
error: services.fatalErrors.add,
});
@ -116,10 +111,6 @@ export const useEditorUpdates = (
// and initializing different visualizations
return;
}
const newQuery = migrateLegacyQuery(state.query);
if (!isEqual(state.query, newQuery)) {
appState.transitions.set('query', newQuery);
}
if (!isEqual(state.uiState, vis.uiState.getChanges())) {
vis.uiState.set(state.uiState);

View file

@ -96,6 +96,7 @@ describe('useVisualizeAppState', () => {
);
expect(connectToQueryState).toHaveBeenCalledWith(mockServices.data.query, expect.any(Object), {
filters: 'appState',
query: true,
});
expect(result.current).toEqual({
appState: stateContainer,

View file

@ -24,6 +24,7 @@ import { EventEmitter } from 'events';
import { i18n } from '@kbn/i18n';
import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public';
import { migrateLegacyQuery } from '../../../../../kibana_legacy/public';
import { esFilters, connectToQueryState } from '../../../../../data/public';
import { VisualizeServices, VisualizeAppStateContainer, SavedVisInstance } from '../../types';
import { visStateToEditorState } from '../utils';
@ -61,19 +62,35 @@ export const useVisualizeAppState = (
eventEmitter.on('dirtyStateChange', onDirtyStateChange);
const { filterManager } = services.data.query;
// sync initial app filters from state to filterManager
const { filterManager, queryString } = services.data.query;
// sync initial app state from state to managers
filterManager.setAppFilters(cloneDeep(stateContainer.getState().filters));
// setup syncing of app filters between appState and filterManager
queryString.setQuery(migrateLegacyQuery(stateContainer.getState().query));
// setup syncing of app filters between appState and query services
const stopSyncingAppFilters = connectToQueryState(
services.data.query,
{
set: ({ filters }) => stateContainer.transitions.set('filters', filters),
get: () => ({ filters: stateContainer.getState().filters }),
state$: stateContainer.state$.pipe(map((state) => ({ filters: state.filters }))),
set: ({ filters, query }) => {
stateContainer.transitions.set('filters', filters);
stateContainer.transitions.set('query', query);
},
get: () => {
return {
filters: stateContainer.getState().filters,
query: stateContainer.getState().query,
};
},
state$: stateContainer.state$.pipe(
map((state) => ({
filters: state.filters,
query: state.query,
}))
),
},
{
filters: esFilters.FilterStateStore.APP_STATE,
query: true,
}
);

View file

@ -20,7 +20,7 @@
import { i18n } from '@kbn/i18n';
import { ChromeStart, DocLinksStart } from 'kibana/public';
import { Filter, UI_SETTINGS } from '../../../../data/public';
import { Filter } from '../../../../data/public';
import { VisualizeServices, SavedVisInstance } from '../types';
export const addHelpMenuToAppChrome = (chrome: ChromeStart, docLinks: DocLinksStart) => {
@ -49,12 +49,9 @@ export const addBadgeToAppChrome = (chrome: ChromeStart) => {
});
};
export const getDefaultQuery = ({ localStorage, uiSettings }: VisualizeServices) => ({
query: '',
language:
localStorage.get('kibana.userQueryLanguage') ||
uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE),
});
export const getDefaultQuery = ({ data }: VisualizeServices) => {
return data.query.queryString.getDefaultQuery();
};
export const visStateToEditorState = (
{ vis, savedVis }: SavedVisInstance,

View file

@ -563,6 +563,10 @@ export default function ({ getService, getPageObjects }) {
it('should display updated scaled label text after time range is changed', async () => {
await PageObjects.visEditor.setInterval('Millisecond');
// Apply interval
await testSubjects.clickWhenNotDisabled('visualizeEditorRenderButton');
const isHelperScaledLabelExists = await find.existsByCssSelector(
'[data-test-subj="currentlyScaledText"]'
);

View file

@ -95,6 +95,14 @@ function createMockFilterManager() {
};
}
function createMockQueryString() {
return {
getQuery: jest.fn(() => ({ query: '', language: 'kuery' })),
setQuery: jest.fn(),
getDefaultQuery: jest.fn(() => ({ query: '', language: 'kuery' })),
};
}
function createMockTimefilter() {
const unsubscribe = jest.fn();
@ -148,6 +156,7 @@ describe('Lens App', () => {
timefilter: {
timefilter: createMockTimefilter(),
},
queryString: createMockQueryString(),
state$: new Observable(),
},
indexPatterns: {

View file

@ -36,7 +36,6 @@ import {
IndexPattern as IndexPatternInstance,
IndexPatternsContract,
SavedQuery,
UI_SETTINGS,
} from '../../../../../src/plugins/data/public';
interface State {
@ -83,17 +82,13 @@ export function App({
onAppLeave: AppMountParameters['onAppLeave'];
history: History;
}) {
const language =
storage.get('kibana.userQueryLanguage') ||
core.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE);
const [state, setState] = useState<State>(() => {
const currentRange = data.query.timefilter.timefilter.getTime();
return {
isLoading: !!docId,
isSaveModalVisible: false,
indexPatternsForTopNav: [],
query: { query: '', language },
query: data.query.queryString.getDefaultQuery(),
dateRange: {
fromDate: currentRange.from,
toDate: currentRange.to,
@ -473,12 +468,7 @@ export function App({
...s,
savedQuery: undefined,
filters: data.query.filterManager.getGlobalFilters(),
query: {
query: '',
language:
storage.get('kibana.userQueryLanguage') ||
core.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE),
},
query: data.query.queryString.getDefaultQuery(),
}));
}}
query={state.query}

View file

@ -20,8 +20,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { UI_SETTINGS } from '../../../../../../../src/plugins/data/public';
import { getIndexPatternService, getUiSettings, getData } from '../../../kibana_services';
import { getIndexPatternService, getData } from '../../../kibana_services';
import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox';
export class FilterEditor extends Component {
@ -82,7 +81,6 @@ export class FilterEditor extends Component {
_renderQueryPopover() {
const layerQuery = this.props.layer.getQuery();
const uiSettings = getUiSettings();
const { SearchBar } = getData().ui;
return (
@ -99,11 +97,7 @@ export class FilterEditor extends Component {
showFilterBar={false}
showDatePicker={false}
showQueryInput={true}
query={
layerQuery
? layerQuery
: { language: uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), query: '' }
}
query={layerQuery ? layerQuery : getData().query.queryString.getDefaultQuery()}
onQuerySubmit={this._onQueryChange}
indexPatterns={this.state.indexPatterns}
customSubmitButton={

View file

@ -8,8 +8,7 @@ import React, { Component } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiPopover, EuiExpression, EuiFormHelpText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/public';
import { getUiSettings, getData } from '../../../../kibana_services';
import { getData } from '../../../../kibana_services';
export class WhereExpression extends Component {
state = {
@ -77,11 +76,7 @@ export class WhereExpression extends Component {
showFilterBar={false}
showDatePicker={false}
showQueryInput={true}
query={
whereQuery
? whereQuery
: { language: getUiSettings().get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE), query: '' }
}
query={whereQuery ? whereQuery : getData().query.queryString.getDefaultQuery()}
onQuerySubmit={this._onQueryChange}
indexPatterns={[indexPattern]}
customSubmitButton={

View file

@ -4,12 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getUiSettings } from '../../kibana_services';
import { UI_SETTINGS } from '../../../../../../src/plugins/data/public';
export function getInitialQuery({ mapStateJSON, appState = {}, userQueryLanguage }) {
const settings = getUiSettings();
import { getData } from '../../kibana_services';
export function getInitialQuery({ mapStateJSON, appState = {} }) {
if (appState.query) {
return appState.query;
}
@ -21,8 +18,5 @@ export function getInitialQuery({ mapStateJSON, appState = {}, userQueryLanguage
}
}
return {
query: '',
language: userQueryLanguage || settings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE),
};
return getData().query.queryString.getDefaultQuery();
}

View file

@ -14,7 +14,6 @@ import {
getToasts,
getCoreI18n,
getData,
getUiSettings,
} from '../../../kibana_services';
import {
SavedObjectSaveModal,
@ -46,16 +45,13 @@ export function MapsTopNavMenu({
isOpenSettingsDisabled,
}) {
const { TopNavMenu } = getNavigation().ui;
const { filterManager } = getData().query;
const { filterManager, queryString } = getData().query;
const showSaveQuery = getMapsCapabilities().saveQuery;
const onClearSavedQuery = () => {
onQuerySaved(undefined);
onQueryChange({
filters: filterManager.getGlobalFilters(),
query: {
query: '',
language: getUiSettings().get('search:queryLanguage'),
},
query: queryString.getDefaultQuery(),
});
};

View file

@ -13,7 +13,6 @@ import {
getIndexPatternService,
getToasts,
getData,
getUiSettings,
getCoreChrome,
} from '../../../kibana_services';
import { copyPersistentState } from '../../../reducers/util';
@ -274,6 +273,7 @@ export class MapsAppView extends React.Component {
_initQueryTimeRefresh() {
const { setRefreshConfig, savedMap } = this.props;
const { queryString } = getData().query;
// TODO: Handle null when converting to TS
const globalState = getGlobalState();
const mapStateJSON = savedMap ? savedMap.mapStateJSON : undefined;
@ -281,7 +281,6 @@ export class MapsAppView extends React.Component {
query: getInitialQuery({
mapStateJSON,
appState: this._appStateManager.getAppState(),
userQueryLanguage: getUiSettings().get('search:queryLanguage'),
}),
time: getInitialTimeFilters({
mapStateJSON,
@ -292,6 +291,8 @@ export class MapsAppView extends React.Component {
globalState,
}),
};
if (newState.query) queryString.setQuery(newState.query);
this.setState({ query: newState.query, time: newState.time });
updateGlobalState(
{

View file

@ -31,6 +31,7 @@ export function useAppStateSyncing(appStateManager) {
};
const stopSyncingQueryAppStateWithStateContainer = connectToQueryState(query, stateContainer, {
filters: esFilters.FilterStateStore.APP_STATE,
query: true,
});
// sets up syncing app state container with url