Move ui/value_suggestions ⇒ NP data plugin (#45762) (#47425)

* Bind search bar

* create prewired data components

* Pass NP data plugin to shim plugin, to access autocomplete
Pass storage and autocomplete to createSearchBar method
Add appName and autocomplete to IDataPluginServices
QueryBarInput to consume autocomplete and appName from context
QueryBarTopRow to consume appName from context
Remove appName from SearchBar
Added AutocompletePublicPluginSetup and AutocompletePublicPluginStart types

* Use KibanaContextProvider in vis editor and graph

* Use KibanaContextProvider in maps

* Use prewirted SearchBar in TopNavMenu

* Use KibanaContextProbider in Lens

* Fix appName usage in query bar input

* fixed query bar top row appName

* update tests

* fixed bind search bar bug

* mock SearchBar

* Removed unnecessary mocks

* Delete unused mock

* Fixed exporting of data plugin types

* Updated maps snapshot

* Fixed some TS issues

* Fixed jest tests

* Context adjustments in TSVB

* componentWillMount

* Code review fixes

* Pass dataTestSubj to query bar input

* Graph data

* - Pass NP data plugin to KibanaReactContext
- Move value_suggestions to NP

* - Pass NP data plugin to KibanaReactContext
- Move value_suggestions to NP

* ts fixes

* Added karma getSuggestions fake

* Refactored kuery autocomplete tests to jest

* Filter bar context for directives

* updated snapshot

* fix diffs

* fixed lens test
This commit is contained in:
Liza Katz 2019-10-06 19:56:58 +03:00 committed by GitHub
parent c573fd1bf4
commit 8ffd187c99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 318 additions and 204 deletions

View file

@ -31,12 +31,13 @@ import {
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import classNames from 'classnames';
import React, { useState } from 'react';
import { UiSettingsClientContract } from 'src/core/public';
import { CoreStart } from 'src/core/public';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { IndexPattern } from '../../index_patterns';
import { FilterEditor } from './filter_editor';
import { FilterItem } from './filter_item';
import { FilterOptions } from './filter_options';
import { useKibana } from '../../../../../../plugins/kibana_react/public';
import { useKibana, KibanaContextProvider } from '../../../../../../plugins/kibana_react/public';
interface Props {
filters: Filter[];
@ -45,18 +46,45 @@ interface Props {
indexPatterns: IndexPattern[];
intl: InjectedIntl;
// Only for directives!
uiSettings?: UiSettingsClientContract;
// TODO: Only for filter-bar directive!
uiSettings?: CoreStart['uiSettings'];
docLinks?: CoreStart['docLinks'];
pluginDataStart?: DataPublicPluginStart;
}
function FilterBarUI(props: Props) {
const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false);
const kibana = useKibana();
let { uiSettings } = kibana.services;
if (!uiSettings) {
// Only for directives!
uiSettings = props.uiSettings;
const uiSettings = kibana.services.uiSettings || props.uiSettings;
if (!uiSettings) return null;
function hasContext() {
return Boolean(kibana.services.uiSettings);
}
function wrapInContextIfMissing(content: JSX.Element) {
// TODO: Relevant only as long as directives are used!
if (!hasContext()) {
if (props.docLinks && props.uiSettings && props.pluginDataStart) {
return (
<KibanaContextProvider
services={{
uiSettings: props.uiSettings,
docLinks: props.docLinks,
data: props.pluginDataStart,
}}
>
{content}
</KibanaContextProvider>
);
} else {
throw new Error(
'Rending filter bar requires providing sufficient context: uiSettings, docLinks and NP data plugin'
);
}
}
return content;
}
function onFiltersUpdated(filters: Filter[]) {
@ -100,7 +128,7 @@ function FilterBarUI(props: Props) {
</EuiButtonEmpty>
);
return (
return wrapInContextIfMissing(
<EuiFlexItem grow={false}>
<EuiPopover
id="addFilterPopover"
@ -120,7 +148,6 @@ function FilterBarUI(props: Props) {
onSubmit={onAdd}
onCancel={() => setIsAddFilterPopoverOpen(false)}
key={JSON.stringify(newFilter)}
uiSettings={uiSettings!}
/>
</div>
</EuiFlexItem>

View file

@ -36,7 +36,6 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { get } from 'lodash';
import React, { Component } from 'react';
import { UiSettingsClientContract } from 'src/core/public';
import { Field, IndexPattern } from '../../../index_patterns';
import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box';
import {
@ -62,7 +61,6 @@ interface Props {
onSubmit: (filter: Filter) => void;
onCancel: () => void;
intl: InjectedIntl;
uiSettings: UiSettingsClientContract;
}
interface State {
@ -343,7 +341,6 @@ class FilterEditorUI extends Component<Props, State> {
value={this.state.params}
onChange={this.onParamsChange}
data-test-subj="phraseValueInput"
uiSettings={this.props.uiSettings}
/>
);
case 'phrases':
@ -353,7 +350,6 @@ class FilterEditorUI extends Component<Props, State> {
field={this.state.selectedField}
values={this.state.params}
onChange={this.onParamsChange}
uiSettings={this.props.uiSettings}
/>
);
case 'range':

View file

@ -18,14 +18,17 @@
*/
import { Component } from 'react';
import { getSuggestions } from 'ui/value_suggestions';
import { UiSettingsClientContract } from 'src/core/public';
import { Field, IndexPattern } from '../../../index_patterns';
import {
withKibana,
KibanaReactContextValue,
} from '../../../../../../../plugins/kibana_react/public';
import { IDataPluginServices } from '../../../types';
export interface PhraseSuggestorProps {
kibana: KibanaReactContextValue<IDataPluginServices>;
indexPattern: IndexPattern;
field?: Field;
uiSettings: UiSettingsClientContract;
}
export interface PhraseSuggestorState {
@ -38,10 +41,11 @@ export interface PhraseSuggestorState {
* aggregatable), we pull out the common logic for requesting suggestions into this component
* which both of them extend.
*/
export class PhraseSuggestor<T extends PhraseSuggestorProps> extends Component<
export class PhraseSuggestorUI<T extends PhraseSuggestorProps> extends Component<
T,
PhraseSuggestorState
> {
private services = this.props.kibana.services;
public state: PhraseSuggestorState = {
suggestions: [],
isLoading: false,
@ -52,7 +56,7 @@ export class PhraseSuggestor<T extends PhraseSuggestorProps> extends Component<
}
protected isSuggestingValues() {
const shouldSuggestValues = this.props.uiSettings.get('filterEditor:suggestValues');
const shouldSuggestValues = this.services.uiSettings.get('filterEditor:suggestValues');
const { field } = this.props;
return shouldSuggestValues && field && field.aggregatable && field.type === 'string';
}
@ -67,7 +71,9 @@ export class PhraseSuggestor<T extends PhraseSuggestorProps> extends Component<
return;
}
this.setState({ isLoading: true });
const suggestions = await getSuggestions(indexPattern.title, field, value);
const suggestions = await this.services.data.getSuggestions(indexPattern.title, field, value);
this.setState({ suggestions, isLoading: false });
}
}
export const PhraseSuggestor = withKibana(PhraseSuggestorUI);

View file

@ -22,8 +22,9 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { uniq } from 'lodash';
import React from 'react';
import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box';
import { PhraseSuggestor, PhraseSuggestorProps } from './phrase_suggestor';
import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor';
import { ValueInputType } from './value_input_type';
import { withKibana } from '../../../../../../../plugins/kibana_react/public';
interface Props extends PhraseSuggestorProps {
value?: string;
@ -31,7 +32,7 @@ interface Props extends PhraseSuggestorProps {
intl: InjectedIntl;
}
class PhraseValueInputUI extends PhraseSuggestor<Props> {
class PhraseValueInputUI extends PhraseSuggestorUI<Props> {
public render() {
return (
<EuiFormRow
@ -87,4 +88,4 @@ function StringComboBox(props: GenericComboBoxProps<string>) {
return GenericComboBox(props);
}
export const PhraseValueInput = injectI18n(PhraseValueInputUI);
export const PhraseValueInput = injectI18n(withKibana(PhraseValueInputUI));

View file

@ -22,7 +22,8 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import { uniq } from 'lodash';
import React from 'react';
import { GenericComboBox, GenericComboBoxProps } from './generic_combo_box';
import { PhraseSuggestor, PhraseSuggestorProps } from './phrase_suggestor';
import { PhraseSuggestorUI, PhraseSuggestorProps } from './phrase_suggestor';
import { withKibana } from '../../../../../../../plugins/kibana_react/public';
interface Props extends PhraseSuggestorProps {
values?: string[];
@ -30,7 +31,7 @@ interface Props extends PhraseSuggestorProps {
intl: InjectedIntl;
}
class PhrasesValuesInputUI extends PhraseSuggestor<Props> {
class PhrasesValuesInputUI extends PhraseSuggestorUI<Props> {
public render() {
const { suggestions } = this.state;
const { values, intl, onChange } = this.props;
@ -64,4 +65,4 @@ function StringComboBox(props: GenericComboBoxProps<string>) {
return GenericComboBox(props);
}
export const PhrasesValuesInput = injectI18n(PhrasesValuesInputUI);
export const PhrasesValuesInput = injectI18n(withKibana(PhrasesValuesInputUI));

View file

@ -171,7 +171,6 @@ class FilterItemUI extends Component<Props, State> {
indexPatterns={this.props.indexPatterns}
onSubmit={this.onSubmit}
onCancel={this.closePopover}
uiSettings={this.props.uiSettings}
/>
</div>
),

View file

@ -127,10 +127,10 @@ export class DataPlugin
public start(core: CoreStart, { __LEGACY, data }: DataPluginStartDependencies) {
const SearchBar = createSearchBar({
core,
data,
store: __LEGACY.storage,
timefilter: this.setupApi.timefilter,
filterManager: this.setupApi.filter.filterManager,
autocomplete: data.autocomplete,
});
return {

View file

@ -134,8 +134,8 @@ export class QueryBarInputUI extends Component<Props, State> {
const queryString = this.getQueryString();
const recentSearchSuggestions = this.getRecentSearchSuggestions(queryString);
const autocompleteProvider = this.services.data.autocomplete.getProvider(language);
const autocompleteProvider = this.services.autocomplete.getProvider(language);
if (
!autocompleteProvider ||
!Array.isArray(this.state.indexPatterns) ||

View file

@ -20,7 +20,7 @@
import React from 'react';
import { Filter } from '@kbn/es-query';
import { CoreStart } from 'src/core/public';
import { AutocompletePublicPluginStart } from 'src/plugins/data/public';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { Storage } from '../../../types';
import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public';
import { TimefilterSetup } from '../../../timefilter';
@ -29,10 +29,10 @@ import { SearchBarOwnProps } from '.';
interface StatefulSearchBarDeps {
core: CoreStart;
data: DataPublicPluginStart;
store: Storage;
timefilter: TimefilterSetup;
filterManager: FilterManager;
autocomplete: AutocompletePublicPluginStart;
}
export type StatetfulSearchBarProps = SearchBarOwnProps & {
@ -59,7 +59,7 @@ export function createSearchBar({
store,
timefilter,
filterManager,
autocomplete,
data,
}: StatefulSearchBarDeps) {
// App name should come from the core application service.
// Until it's available, we'll ask the user to provide it for the pre-wired component.
@ -71,7 +71,7 @@ export function createSearchBar({
<KibanaContextProvider
services={{
appName: props.appName,
autocomplete,
data,
store,
...core,
}}

View file

@ -66,7 +66,6 @@ export interface SearchBarOwnProps {
showFilterBar?: boolean;
showDatePicker?: boolean;
showAutoRefreshOnly?: boolean;
onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void;
// Query bar - should be in SearchBarInjectedDeps
query?: Query;
// Show when user has privileges to save

View file

@ -24,7 +24,7 @@ import { Filter } from '@kbn/es-query';
// @ts-ignore
import { uiModules } from 'ui/modules';
import { npSetup, npStart } from 'ui/new_platform';
import { npStart } from 'ui/new_platform';
import { FilterBar, ApplyFiltersPopover } from '../filter';
import template from './apply_filter_directive.html';
@ -49,14 +49,16 @@ export const initLegacyModule = once((): void => {
}
child.setAttribute('ui-settings', 'uiSettings');
child.setAttribute('http', 'http');
child.setAttribute('doc-links', 'docLinks');
child.setAttribute('plugin-data-start', 'pluginDataStart');
// Append helper directive
elem.append(child);
const linkFn = ($scope: any) => {
$scope.uiSettings = npSetup.core.uiSettings;
$scope.http = npSetup.core.http;
$scope.uiSettings = npStart.core.uiSettings;
$scope.docLinks = npStart.core.docLinks;
$scope.pluginDataStart = npStart.plugins.data;
};
return linkFn;
@ -66,11 +68,12 @@ export const initLegacyModule = once((): void => {
.directive('filterBarHelper', (reactDirective: any) => {
return reactDirective(wrapInI18nContext(FilterBar), [
['uiSettings', { watchDepth: 'reference' }],
['http', { watchDepth: 'reference' }],
['docLinks', { watchDepth: 'reference' }],
['onFiltersUpdated', { watchDepth: 'reference' }],
['indexPatterns', { watchDepth: 'collection' }],
['filters', { watchDepth: 'collection' }],
['className', { watchDepth: 'reference' }],
['pluginDataStart', { watchDepth: 'reference' }],
]);
})
.directive('applyFiltersPopoverComponent', (reactDirective: any) =>

View file

@ -18,7 +18,7 @@
*/
import { UiSettingsClientContract, CoreStart } from 'src/core/public';
import { AutocompletePublicPluginStart } from 'src/plugins/data/public';
import { DataPublicPluginStart } from 'src/plugins/data/public';
export interface Storage {
get: (key: string) => any;
@ -34,5 +34,5 @@ export interface IDataPluginServices extends Partial<CoreStart> {
notifications: CoreStart['notifications'];
http: CoreStart['http'];
store: Storage;
autocomplete: AutocompletePublicPluginStart;
data: DataPublicPluginStart;
}

View file

@ -170,7 +170,7 @@ export class VisEditor extends Component {
services={{
appName: APP_NAME,
store: localStorage,
autocomplete: npStart.plugins.data.autocomplete,
data: npStart.plugins.data,
...npStart.core,
}}
>

View file

@ -65,7 +65,9 @@ export const npStart = {
registerRenderer: sinon.fake(),
registerType: sinon.fake(),
},
data: {},
data: {
getSuggestions: sinon.fake(),
},
inspector: {
isAvailable: () => false,
open: () => ({

View file

@ -95,7 +95,7 @@ function FilterRow({
services={{
appName: 'filtersAgg',
store: localStorage,
autocomplete: npStart.plugins.data.autocomplete,
data: npStart.plugins.data,
...npStart.core,
}}
>

View file

@ -38,6 +38,7 @@ const createSetupContract = (): Setup => {
const createStartContract = (): Start => {
const startContract: Start = {
autocomplete: autocompleteMock as Start['autocomplete'],
getSuggestions: jest.fn(),
};
return startContract;
};

View file

@ -20,6 +20,7 @@
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public';
import { AutocompleteProviderRegister } from './autocomplete_provider';
import { DataPublicPluginSetup, DataPublicPluginStart } from './types';
import { getSuggestionsProvider } from './suggestions_provider';
export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPublicPluginStart> {
private readonly autocomplete = new AutocompleteProviderRegister();
@ -35,6 +36,7 @@ export class DataPublicPlugin implements Plugin<DataPublicPluginSetup, DataPubli
public start(core: CoreStart): DataPublicPluginStart {
return {
autocomplete: this.autocomplete,
getSuggestions: getSuggestionsProvider(core.uiSettings, core.http),
};
}

View file

@ -17,8 +17,4 @@
* under the License.
*/
import chrome from 'ui/chrome';
import { kfetch } from 'ui/kfetch';
import { getSuggestionsProvider } from './value_suggestions';
export const getSuggestions = getSuggestionsProvider(chrome.getUiSettingsClient(), kfetch);
export { getSuggestionsProvider } from './value_suggestions';

View file

@ -0,0 +1,23 @@
/*
* 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.
*/
// Should be import { Field } from './index_patterns';
export type Field = any;
export type IGetSuggestions = (index: string, field: Field, query: string, boolFilter?: any) => any;

View file

@ -17,21 +17,23 @@
* under the License.
*/
// TODO: remove when index patterns are moved here.
jest.mock('ui/new_platform');
jest.mock('ui/index_patterns');
import { mockFields, mockIndexPattern } from 'ui/index_patterns';
import { getSuggestionsProvider } from './value_suggestions';
import { UiSettingsClientContract } from 'kibana/public';
describe('getSuggestions', () => {
let getSuggestions: any;
let fetch: any;
let http: any;
describe('with value suggestions disabled', () => {
beforeEach(() => {
const config = { get: () => false };
fetch = jest.fn();
getSuggestions = getSuggestionsProvider(config, fetch);
const config = { get: (key: string) => false } as UiSettingsClientContract;
http = { fetch: jest.fn() };
getSuggestions = getSuggestionsProvider(config, http);
});
it('should return an empty array', async () => {
@ -40,15 +42,15 @@ describe('getSuggestions', () => {
const query = '';
const suggestions = await getSuggestions(index, field, query);
expect(suggestions).toEqual([]);
expect(fetch).not.toHaveBeenCalled();
expect(http.fetch).not.toHaveBeenCalled();
});
});
describe('with value suggestions enabled', () => {
beforeEach(() => {
const config = { get: () => true };
fetch = jest.fn();
getSuggestions = getSuggestionsProvider(config, fetch);
const config = { get: (key: string) => true } as UiSettingsClientContract;
http = { fetch: jest.fn() };
getSuggestions = getSuggestionsProvider(config, http);
});
it('should return true/false for boolean fields', async () => {
@ -57,7 +59,7 @@ describe('getSuggestions', () => {
const query = '';
const suggestions = await getSuggestions(index, field, query);
expect(suggestions).toEqual([true, false]);
expect(fetch).not.toHaveBeenCalled();
expect(http.fetch).not.toHaveBeenCalled();
});
it('should return an empty array if the field type is not a string or boolean', async () => {
@ -66,7 +68,7 @@ describe('getSuggestions', () => {
const query = '';
const suggestions = await getSuggestions(index, field, query);
expect(suggestions).toEqual([]);
expect(fetch).not.toHaveBeenCalled();
expect(http.fetch).not.toHaveBeenCalled();
});
it('should return an empty array if the field is not aggregatable', async () => {
@ -75,7 +77,7 @@ describe('getSuggestions', () => {
const query = '';
const suggestions = await getSuggestions(index, field, query);
expect(suggestions).toEqual([]);
expect(fetch).not.toHaveBeenCalled();
expect(http.fetch).not.toHaveBeenCalled();
});
it('should otherwise request suggestions', async () => {
@ -85,7 +87,7 @@ describe('getSuggestions', () => {
);
const query = '';
await getSuggestions(index, field, query);
expect(fetch).toHaveBeenCalled();
expect(http.fetch).toHaveBeenCalled();
});
it('should cache results if using the same index/field/query/filter', async () => {
@ -96,7 +98,7 @@ describe('getSuggestions', () => {
const query = '';
await getSuggestions(index, field, query);
await getSuggestions(index, field, query);
expect(fetch).toHaveBeenCalledTimes(1);
expect(http.fetch).toHaveBeenCalledTimes(1);
});
it('should cache results for only one minute', async () => {
@ -113,7 +115,7 @@ describe('getSuggestions', () => {
await getSuggestions(index, field, query);
Date.now = now;
expect(fetch).toHaveBeenCalledTimes(2);
expect(http.fetch).toHaveBeenCalledTimes(2);
});
it('should not cache results if using a different index/field/query', async () => {
@ -128,7 +130,7 @@ describe('getSuggestions', () => {
await getSuggestions('logstash-*', fields[0], 'query');
await getSuggestions('logstash-*', fields[1], '');
await getSuggestions('logstash-*', fields[1], 'query');
expect(fetch).toHaveBeenCalledTimes(8);
expect(http.fetch).toHaveBeenCalledTimes(8);
});
});
});

View file

@ -18,16 +18,17 @@
*/
import { memoize } from 'lodash';
import { Field } from 'ui/index_patterns';
import { UiSettingsClientContract, HttpServiceBase } from 'src/core/public';
import { IGetSuggestions, Field } from './types';
export function getSuggestionsProvider(
config: { get: (key: string) => any },
fetch: (...options: any[]) => any
) {
uiSettings: UiSettingsClientContract,
http: HttpServiceBase
): IGetSuggestions {
const requestSuggestions = memoize(
(index: string, field: Field, query: string, boolFilter: any = []) => {
return fetch({
pathname: `/api/kibana/suggestions/values/${index}`,
return http.fetch(`/api/kibana/suggestions/values/${index}`, {
method: 'POST',
body: JSON.stringify({ query, field: field.name, boolFilter }),
});
@ -36,7 +37,7 @@ export function getSuggestionsProvider(
);
return async (index: string, field: Field, query: string, boolFilter?: any) => {
const shouldSuggestValues = config.get('filterEditor:suggestValues');
const shouldSuggestValues = uiSettings.get('filterEditor:suggestValues');
if (field.type === 'boolean') {
return [true, false];
} else if (!shouldSuggestValues || !field.aggregatable || field.type !== 'string') {

View file

@ -20,10 +20,14 @@
export * from './autocomplete_provider/types';
import { AutocompletePublicPluginSetup, AutocompletePublicPluginStart } from '.';
import { IGetSuggestions } from './suggestions_provider/types';
export interface DataPublicPluginSetup {
autocomplete: AutocompletePublicPluginSetup;
}
export interface DataPublicPluginStart {
autocomplete: AutocompletePublicPluginStart;
getSuggestions: IGetSuggestions;
}
export { IGetSuggestions } from './suggestions_provider/types';

View file

@ -17,7 +17,7 @@
is-initialized="workspaceInitialized || savedWorkspace.id"
initial-query="initialQuery"
on-fill-workspace="fillWorkspace"
autocomplete-start="autocompleteStart"
plugin-data-start="pluginDataStart"
core-start="coreStart"
store="store"
></graph-app>

View file

@ -106,9 +106,10 @@ app.directive('graphApp', function (reactDirective) {
['onQuerySubmit', { watchDepth: 'reference' }],
['initialQuery', { watchDepth: 'reference' }],
['confirmWipeWorkspace', { watchDepth: 'reference' }],
['autocompleteStart', { watchDepth: 'reference' }],
['coreStart', { watchDepth: 'reference' }],
['reduxStore', { watchDepth: 'reference' }]
['pluginDataStart', { watchDepth: 'reference' }],
['store', { watchDepth: 'reference' }],
['reduxStore', { watchDepth: 'reference' }],
]);
});
@ -321,9 +322,9 @@ app.controller('graphuiPlugin', function (
chrome,
});
$scope.pluginDataStart = npStart.plugins.data;
$scope.store = new Storage(window.localStorage);
$scope.coreStart = npStart.core;
$scope.autocompleteStart = npStart.plugins.data.autocomplete;
$scope.loading = false;
$scope.spymode = 'request';

View file

@ -5,12 +5,13 @@
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { Provider } from 'react-redux';
import React, { useState } from 'react';
import { I18nProvider } from '@kbn/i18n/react';
import { Storage } from 'ui/storage';
import { CoreStart } from 'kibana/public';
import { AutocompletePublicPluginStart } from 'src/plugins/data/public';
import { FieldManager } from './field_manager';
import { SearchBarProps, SearchBar } from './search_bar';
import { GraphStore } from '../state_management';
@ -20,7 +21,8 @@ import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_reac
export interface GraphAppProps extends SearchBarProps {
coreStart: CoreStart;
autocompleteStart: AutocompletePublicPluginStart;
// This is not named dataStart because of Angular treating data- prefix differently
pluginDataStart: DataPublicPluginStart;
store: Storage;
reduxStore: GraphStore;
isInitialized: boolean;
@ -29,16 +31,16 @@ export interface GraphAppProps extends SearchBarProps {
export function GraphApp(props: GraphAppProps) {
const [pickerOpen, setPickerOpen] = useState(false);
const { coreStart, autocompleteStart, store, reduxStore, ...searchBarProps } = props;
const { coreStart, pluginDataStart, store, reduxStore, ...searchBarProps } = props;
return (
<I18nProvider>
<KibanaContextProvider
services={{
appName: 'graph',
store: props.store,
autocomplete: props.autocompleteStart,
...props.coreStart,
store,
data: pluginDataStart,
...coreStart,
}}
>
<Provider store={reduxStore}>

View file

@ -17,6 +17,7 @@ import { I18nProvider } from '@kbn/i18n/react';
jest.mock('ui/new_platform');
import { openSourceModal } from '../services/source_modal';
import { GraphStore, setDatasource } from '../state_management';
import { ReactWrapper } from 'enzyme';
import { createMockGraphStore } from '../state_management/mocks';
@ -82,6 +83,7 @@ describe('search_bar', () => {
function mountSearchBar() {
jest.clearAllMocks();
const wrappedSearchBar = wrapSearchBarInContext({ ...defaultProps });
instance = mountWithIntl(<Provider store={store}>{wrappedSearchBar}</Provider>);
}

View file

@ -18,6 +18,7 @@ import {
IndexPattern,
} from '../../../../../../src/legacy/core_plugins/data/public';
import { openSourceModal } from '../services/source_modal';
import {
GraphState,
datasourceSelector,
@ -31,6 +32,7 @@ export interface OuterSearchBarProps {
isLoading: boolean;
initialQuery?: string;
onQuerySubmit: (query: string) => void;
confirmWipeWorkspace: (onConfirm: () => void) => void;
indexPatternProvider: IndexPatternProvider;
}

View file

@ -1,103 +0,0 @@
/*
* 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 expect from '@kbn/expect';
import sinon from 'sinon';
import fetchMock from 'fetch-mock';
import { getSuggestionsProvider } from '../value';
import indexPatternResponse from '../__fixtures__/index_pattern_response.json';
describe('Kuery value suggestions', function () {
let config;
let indexPatterns;
let getSuggestions;
const mockValues = ['foo', 'bar'];
const fetchUrlMatcher = /\/api\/kibana\/suggestions\/values\/*/;
beforeEach(() => fetchMock.post(fetchUrlMatcher, mockValues));
afterEach(() => fetchMock.restore());
beforeEach(() => {
config = getConfigStub(true);
indexPatterns = [indexPatternResponse];
getSuggestions = getSuggestionsProvider({ config, indexPatterns });
});
it('should return a function', function () {
expect(typeof getSuggestions).to.be('function');
});
it('should return boolean suggestions for boolean fields', async () => {
const fieldName = 'ssl';
const prefix = '';
const suffix = '';
const suggestions = await getSuggestions({ fieldName, prefix, suffix });
expect(suggestions.map(({ text }) => text)).to.eql(['true ', 'false ']);
});
it('should filter boolean suggestions for boolean fields', async () => {
const fieldName = 'ssl';
const prefix = 'fa';
const suffix = '';
const suggestions = await getSuggestions({ fieldName, prefix, suffix });
expect(suggestions.map(({ text }) => text)).to.eql(['false ']);
});
it('should not make a request for non-aggregatable fields', async () => {
const fieldName = 'non-sortable';
const prefix = '';
const suffix = '';
const suggestions = await getSuggestions({ fieldName, prefix, suffix });
expect(fetchMock.called(fetchUrlMatcher)).to.be(false);
expect(suggestions).to.eql([]);
});
it('should not make a request for non-string fields', async () => {
const fieldName = 'bytes';
const prefix = '';
const suffix = '';
const suggestions = await getSuggestions({ fieldName, prefix, suffix });
expect(fetchMock.called(fetchUrlMatcher)).to.be(false);
expect(suggestions).to.eql([]);
});
it('should make a request for string fields', async () => {
const fieldName = 'machine.os.raw';
const prefix = '';
const suffix = '';
const suggestions = await getSuggestions({ fieldName, prefix, suffix });
const lastCall = fetchMock.lastCall(fetchUrlMatcher, 'POST');
expect(lastCall.request._bodyInit, '{"query":"","field":"machine.os.raw","boolFilter":[]}');
expect(lastCall[0]).to.match(/\/api\/kibana\/suggestions\/values\/logstash-\*/);
expect(lastCall[1]).to.eql({
method: 'POST',
headers: {
'content-type': 'application/json',
'kbn-version': '1.2.3',
},
});
expect(suggestions.map(({ text }) => text)).to.eql(['"foo" ', '"bar" ']);
});
it('should not have descriptions', async () => {
const fieldName = 'ssl';
const prefix = '';
const suffix = '';
const suggestions = await getSuggestions({ fieldName, prefix, suffix });
expect(suggestions.length).to.be.greaterThan(0);
suggestions.forEach(suggestion => {
expect(suggestion.description).to.not.be.ok();
});
});
});
function getConfigStub(suggestValues) {
const get = sinon.stub().returns(suggestValues);
return { get };
}

View file

@ -6,7 +6,7 @@
import { flatten } from 'lodash';
import { escapeQuotes } from './escape_kuery';
import { getSuggestions } from 'ui/value_suggestions';
import { npStart } from 'ui/new_platform';
const type = 'value';
@ -29,6 +29,7 @@ export function getSuggestionsProvider({ indexPatterns, boolFilter }) {
}) {
const fields = allFields.filter(field => field.name === fieldName);
const query = `${prefix}${suffix}`;
const { getSuggestions } = npStart.plugins.data;
const suggestionsByField = fields.map(field => {
return getSuggestions(field.indexPatternTitle, field, query, boolFilter).then(data => {

View file

@ -0,0 +1,131 @@
/*
* 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 { getSuggestionsProvider } from './value';
import indexPatternResponse from './__fixtures__/index_pattern_response.json';
import { npStart } from 'ui/new_platform';
jest.mock('ui/new_platform', () => ({
npStart: {
plugins: {
data: {
getSuggestions: (_, field) => {
let res;
if (field.type === 'boolean') {
res = [true, false];
} else if (field.name === 'machine.os') {
res = ['Windo"ws', 'Mac\'', 'Linux'];
} else {
res = [];
}
return Promise.resolve(res);
}
},
}
}
}));
describe('Kuery value suggestions', function () {
let indexPatterns;
let getSuggestions;
beforeEach(() => {
indexPatterns = [indexPatternResponse];
getSuggestions = getSuggestionsProvider({ indexPatterns });
jest.clearAllMocks();
});
test('should return a function', function () {
expect(typeof getSuggestions).toBe('function');
});
test('should not search for non existing field', async () => {
const fieldName = 'i_dont_exist';
const prefix = '';
const suffix = '';
const spy = jest.spyOn(npStart.plugins.data, 'getSuggestions');
const suggestions = await getSuggestions({ fieldName, prefix, suffix });
expect(suggestions.map(({ text }) => text)).toEqual([]);
expect(spy).toHaveBeenCalledTimes(0);
});
test('should format suggestions', async () => {
const fieldName = 'ssl'; // Has results with quotes in mock
const prefix = '';
const suffix = '';
const start = 1;
const end = 5;
const suggestions = await getSuggestions({ fieldName, prefix, suffix, start, end });
expect(suggestions[0].type).toEqual('value');
expect(suggestions[0].start).toEqual(start);
expect(suggestions[0].end).toEqual(end);
});
describe('Boolean suggestions', function () {
test('should stringify boolean fields', async () => {
const fieldName = 'ssl';
const prefix = '';
const suffix = '';
const spy = jest.spyOn(npStart.plugins.data, 'getSuggestions');
const suggestions = await getSuggestions({ fieldName, prefix, suffix });
expect(suggestions.map(({ text }) => text)).toEqual(['true ', 'false ']);
expect(spy).toHaveBeenCalledTimes(1);
});
test('should filter out boolean suggestions', async () => {
const fieldName = 'ssl'; // Has results with quotes in mock
const prefix = 'fa';
const suffix = '';
const suggestions = await getSuggestions({ fieldName, prefix, suffix });
expect(suggestions.length).toEqual(1);
});
});
describe('String suggestions', function () {
test('should merge prefix and suffix', async () => {
const fieldName = 'machine.os.raw';
const prefix = 'he';
const suffix = 'llo';
const spy = jest.spyOn(npStart.plugins.data, 'getSuggestions');
await getSuggestions({ fieldName, prefix, suffix });
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toBeCalledWith(expect.any(String), expect.any(Object), prefix + suffix, undefined);
});
test('should escape quotes in suggestions', async () => {
const fieldName = 'machine.os'; // Has results with quotes in mock
const prefix = '';
const suffix = '';
const suggestions = await getSuggestions({ fieldName, prefix, suffix });
expect(suggestions[0].text).toEqual('"Windo\\"ws" ');
expect(suggestions[1].text).toEqual('"Mac\'" ');
expect(suggestions[2].text).toEqual('"Linux" ');
});
test('should filter out string suggestions', async () => {
const fieldName = 'machine.os'; // Has results with quotes in mock
const prefix = 'banana';
const suffix = '';
const suggestions = await getSuggestions({ fieldName, prefix, suffix });
expect(suggestions.length).toEqual(0);
});
test('should partially filter out string suggestions - case insensitive', async () => {
const fieldName = 'machine.os'; // Has results with quotes in mock
const prefix = 'ma';
const suffix = '';
const suggestions = await getSuggestions({ fieldName, prefix, suffix });
expect(suggestions.length).toEqual(1);
});
});
});

View file

@ -13,6 +13,10 @@ import { EditorFrameInstance } from '../types';
import { Storage } from 'ui/storage';
import { Document, SavedObjectStore } from '../persistence';
import { mount } from 'enzyme';
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
const dataStartMock = dataPluginMock.createStartContract();
import {
TopNavMenu,
TopNavMenuData,
@ -68,8 +72,9 @@ describe('Lens App', () => {
function makeDefaultArgs(): jest.Mocked<{
editorFrame: EditorFrameInstance;
data: typeof dataStartMock;
core: typeof core;
data: DataStart;
dataShim: DataStart;
store: Storage;
docId?: string;
docStorage: SavedObjectStore;
@ -87,7 +92,7 @@ describe('Lens App', () => {
},
},
},
data: {
dataShim: {
indexPatterns: {
indexPatterns: {
get: jest.fn(id => {
@ -110,8 +115,9 @@ describe('Lens App', () => {
redirectTo: jest.fn(id => {}),
} as unknown) as jest.Mocked<{
editorFrame: EditorFrameInstance;
data: typeof dataStartMock;
core: typeof core;
data: DataStart;
dataShim: DataStart;
store: Storage;
docId?: string;
docStorage: SavedObjectStore;
@ -224,7 +230,7 @@ describe('Lens App', () => {
await waitForPromises();
expect(args.docStorage.load).toHaveBeenCalledWith('1234');
expect(args.data.indexPatterns.indexPatterns.get).toHaveBeenCalledWith('1');
expect(args.dataShim.indexPatterns.indexPatterns.get).toHaveBeenCalledWith('1');
expect(TopNavMenu).toHaveBeenCalledWith(
expect.objectContaining({
query: 'fake query',
@ -492,7 +498,7 @@ describe('Lens App', () => {
const instance = mount(<App {...args} />);
args.data.filter.filterManager.setFilters([
args.dataShim.filter.filterManager.setFilters([
buildExistsFilter({ name: 'myfield' }, { id: 'index1' }),
]);
@ -623,7 +629,7 @@ describe('Lens App', () => {
query: { query: 'new', language: 'lucene' },
});
args.data.filter.filterManager.setFilters([
args.dataShim.filter.filterManager.setFilters([
buildExistsFilter({ name: 'myfield' }, { id: 'index1' }),
]);
instance.update();

View file

@ -9,6 +9,8 @@ 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 { DataPublicPluginStart } from 'src/plugins/data/public';
import { CoreStart, NotificationsStart } from 'src/core/public';
import {
DataStart,
@ -43,6 +45,7 @@ interface State {
export function App({
editorFrame,
data,
dataShim,
core,
store,
docId,
@ -50,8 +53,9 @@ export function App({
redirectTo,
}: {
editorFrame: EditorFrameInstance;
data: DataPublicPluginStart;
core: CoreStart;
data: DataStart;
dataShim: DataStart;
store: Storage;
docId?: string;
docStorage: SavedObjectStore;
@ -77,9 +81,9 @@ export function App({
const lastKnownDocRef = useRef<Document | undefined>(undefined);
useEffect(() => {
const subscription = data.filter.filterManager.getUpdates$().subscribe({
const subscription = dataShim.filter.filterManager.getUpdates$().subscribe({
next: () => {
setState(s => ({ ...s, filters: data.filter.filterManager.getFilters() }));
setState(s => ({ ...s, filters: dataShim.filter.filterManager.getFilters() }));
},
});
return () => {
@ -112,7 +116,7 @@ export function App({
.then(doc => {
getAllIndexPatterns(
doc.state.datasourceMetaData.filterableIndexPatterns,
data.indexPatterns.indexPatterns,
dataShim.indexPatterns.indexPatterns,
core.notifications
)
.then(indexPatterns => {
@ -164,6 +168,7 @@ export function App({
<KibanaContextProvider
services={{
appName: 'lens',
data,
store,
...core,
}}
@ -230,7 +235,7 @@ export function App({
setState(s => ({ ...s, savedQuery }));
}}
onSavedQueryUpdated={savedQuery => {
data.filter.filterManager.setFilters(
dataShim.filter.filterManager.setFilters(
savedQuery.attributes.filters || state.filters
);
setState(s => ({
@ -245,7 +250,7 @@ export function App({
}));
}}
onClearSavedQuery={() => {
data.filter.filterManager.removeAll();
dataShim.filter.filterManager.removeAll();
setState(s => ({
...s,
savedQuery: undefined,
@ -290,7 +295,7 @@ export function App({
) {
getAllIndexPatterns(
filterableIndexPatterns,
data.indexPatterns.indexPatterns,
dataShim.indexPatterns.indexPatterns,
core.notifications
).then(indexPatterns => {
if (indexPatterns) {

View file

@ -11,8 +11,9 @@ import chrome from 'ui/chrome';
import { Storage } from 'ui/storage';
import { CoreSetup, CoreStart } from 'src/core/public';
import { npSetup, npStart } from 'ui/new_platform';
import { DataPublicPluginStart } from 'src/plugins/data/public';
import { DataStart } from '../../../../../../src/legacy/core_plugins/data/public';
import { start as dataStart } from '../../../../../../src/legacy/core_plugins/data/public/legacy';
import { start as dataShimStart } 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';
@ -26,7 +27,8 @@ import { App } from './app';
import { EditorFrameInstance } from '../types';
export interface LensPluginStartDependencies {
data: DataStart;
data: DataPublicPluginStart;
dataShim: DataStart;
}
export class AppPlugin {
private instance: EditorFrameInstance | null = null;
@ -50,7 +52,7 @@ export class AppPlugin {
editorFrameSetupInterface.registerDatasource('indexpattern', indexPattern);
}
start(core: CoreStart, { data }: LensPluginStartDependencies) {
start(core: CoreStart, { data, dataShim }: LensPluginStartDependencies) {
if (this.store === null) {
throw new Error('Start lifecycle called before setup lifecycle');
}
@ -66,6 +68,7 @@ export class AppPlugin {
<App
core={core}
data={data}
dataShim={dataShim}
editorFrame={this.instance!}
store={new Storage(localStorage)}
docId={routeProps.match.params.id}
@ -115,5 +118,6 @@ export class AppPlugin {
const app = new AppPlugin();
export const appSetup = () => app.setup(npSetup.core, {});
export const appStart = () => app.start(npStart.core, { data: dataStart });
export const appStart = () =>
app.start(npStart.core, { dataShim: dataShimStart, data: npStart.plugins.data });
export const appStop = () => app.stop();

View file

@ -5,7 +5,7 @@ exports[`LayerPanel is rendered 1`] = `
services={
Object {
"appName": "maps",
"autocomplete": undefined,
"data": undefined,
"store": Storage {
"clear": [Function],
"get": [Function],

View file

@ -155,7 +155,7 @@ export class LayerPanel extends React.Component {
services={{
appName: 'maps',
store: localStorage,
autocomplete: npStart.plugins.autocomplete,
data: npStart.plugins.data,
...npStart.core,
}}
>