[ML] Data Frames: Fix saved search (#36894) (#36988)

- Fixes the use of Kibana saved searches for the data frame wizard. Previously users would end up on a blank page when choosing a saved search.
- Additionally, this fixes UI inconsistencies for the pivot creation form and summary page.
This commit is contained in:
Walter Rafelsberger 2019-05-24 09:27:56 +02:00 committed by GitHub
parent 265b570dcf
commit b18554edf8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 215 additions and 77 deletions

View file

@ -6,15 +6,19 @@
import React from 'react';
import { StaticIndexPattern } from 'ui/index_patterns';
import { IndexPattern } from 'ui/index_patterns';
interface KibanaContextValue {
currentIndexPattern: StaticIndexPattern;
export interface KibanaContextValue {
combinedQuery: any;
currentIndexPattern: IndexPattern;
currentSavedSearch: any;
indexPatterns: any;
kbnBaseUrl: string;
kibanaConfig: any;
}
export type SavedSearchQuery = object;
// Because we're only getting the actual contextvalue within a wrapping angular component,
// we need to initialize here with `null` because TypeScript doesn't allow createContext()
// without a default value. The nullable union type takes care of allowing
@ -24,7 +28,9 @@ export const KibanaContext = React.createContext<NullableKibanaContextValue>(nul
export function isKibanaContext(arg: any): arg is KibanaContextValue {
return (
arg.combinedQuery !== undefined &&
arg.currentIndexPattern !== undefined &&
arg.currentSavedSearch !== undefined &&
arg.indexPatterns !== undefined &&
typeof arg.kbnBaseUrl === 'string' &&
arg.kibanaConfig !== undefined

View file

@ -6,7 +6,7 @@
import { DefaultOperator } from 'elasticsearch';
import { StaticIndexPattern } from 'ui/index_patterns';
import { IndexPattern } from 'ui/index_patterns';
import { dictionaryToArray } from '../../../common/types/common';
@ -23,6 +23,7 @@ import {
import { PivotAggDict, PivotAggsConfig } from './pivot_aggs';
import { DateHistogramAgg, HistogramAgg, PivotGroupByDict, TermsAgg } from './pivot_group_by';
import { SavedSearchQuery } from './kibana_context';
export interface DataFramePreviewRequest {
pivot: {
@ -52,18 +53,24 @@ export interface SimpleQuery {
};
}
export function getPivotQuery(search: string): SimpleQuery {
return {
query_string: {
query: search,
default_operator: 'AND',
},
};
export type PivotQuery = SimpleQuery | SavedSearchQuery;
export function getPivotQuery(search: string | SavedSearchQuery): PivotQuery {
if (typeof search === 'string') {
return {
query_string: {
query: search,
default_operator: 'AND',
},
};
}
return search;
}
export function getDataFramePreviewRequest(
indexPatternTitle: StaticIndexPattern['title'],
query: SimpleQuery,
indexPatternTitle: IndexPattern['title'],
query: PivotQuery,
groupBy: PivotGroupByConfig[],
aggs: PivotAggsConfig[]
): DataFramePreviewRequest {
@ -132,7 +139,7 @@ export function getDataFramePreviewRequest(
}
export function getDataFrameRequest(
indexPatternTitle: StaticIndexPattern['title'],
indexPatternTitle: IndexPattern['title'],
pivotState: DefinePivotExposedState,
jobDetailsState: JobDetailsExposedState
): DataFrameRequest {

View file

@ -5,10 +5,13 @@ exports[`Data Frame: <DefinePivotForm /> Minimal initialization 1`] = `
<ContextProvider
value={
Object {
"combinedQuery": Object {},
"currentIndexPattern": Object {
"fields": Array [],
"id": "the-index-pattern-id",
"title": "the-index-pattern-title",
},
"currentSavedSearch": Object {},
"indexPatterns": Object {},
"kbnBaseUrl": "url",
"kibanaConfig": Object {},

View file

@ -5,10 +5,13 @@ exports[`Data Frame: <DefinePivotSummary /> Minimal initialization 1`] = `
<ContextProvider
value={
Object {
"combinedQuery": Object {},
"currentIndexPattern": Object {
"fields": Array [],
"id": "the-index-pattern-id",
"title": "the-index-pattern-title",
},
"currentSavedSearch": Object {},
"indexPatterns": Object {},
"kbnBaseUrl": "url",
"kibanaConfig": Object {},

View file

@ -5,10 +5,13 @@ exports[`Data Frame: <PivotPreview /> Minimal initialization 1`] = `
<ContextProvider
value={
Object {
"combinedQuery": Object {},
"currentIndexPattern": Object {
"fields": Array [],
"id": "the-index-pattern-id",
"title": "the-index-pattern-title",
},
"currentSavedSearch": Object {},
"indexPatterns": Object {},
"kbnBaseUrl": "url",
"kibanaConfig": Object {},

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { StaticIndexPattern } from 'ui/index_patterns';
import { IndexPattern } from 'ui/index_patterns';
import {
getDataFramePreviewRequest,
@ -22,9 +22,18 @@ describe('Data Frame: Define Pivot Common', () => {
// The field name includes the characters []> which cannot be used for aggregation names.
// The test results verifies that the characters should still be present in field and dropDownName values,
// but should be stripped for aggName values.
const indexPattern: StaticIndexPattern = {
const indexPattern: IndexPattern = {
id: 'the-index-pattern-id',
title: 'the-index-pattern-title',
fields: [{ name: 'the-f[i]e>ld', type: 'number', aggregatable: true, searchable: true }],
fields: [
{
name: 'the-f[i]e>ld',
type: 'number',
aggregatable: true,
filterable: true,
searchable: true,
},
],
};
const options = getPivotDropdownOptions(indexPattern);

View file

@ -6,7 +6,7 @@
import { EuiComboBoxOptionProps } from '@elastic/eui';
import { StaticIndexPattern } from 'ui/index_patterns';
import { IndexPattern } from 'ui/index_patterns';
import { KBN_FIELD_TYPES } from '../../../../common/constants/field_types';
@ -63,7 +63,7 @@ function getDefaultGroupByConfig(
const illegalEsAggNameChars = /[[\]>]/g;
export function getPivotDropdownOptions(indexPattern: StaticIndexPattern) {
export function getPivotDropdownOptions(indexPattern: IndexPattern) {
// The available group by options
const groupByOptions: EuiComboBoxOptionProps[] = [];
const groupByOptionsData: PivotGroupByConfigDict = {};

View file

@ -19,6 +19,7 @@ jest.mock('react', () => {
describe('Data Frame: <DefinePivotForm />', () => {
test('Minimal initialization', () => {
const currentIndexPattern = {
id: 'the-index-pattern-id',
title: 'the-index-pattern-title',
fields: [],
};
@ -28,7 +29,14 @@ describe('Data Frame: <DefinePivotForm />', () => {
const wrapper = shallow(
<div>
<KibanaContext.Provider
value={{ currentIndexPattern, indexPatterns: {}, kbnBaseUrl: 'url', kibanaConfig: {} }}
value={{
combinedQuery: {},
currentIndexPattern,
currentSavedSearch: {},
indexPatterns: {},
kbnBaseUrl: 'url',
kibanaConfig: {},
}}
>
<DefinePivotForm onChange={() => {}} />
</KibanaContext.Provider>

View file

@ -34,10 +34,12 @@ import {
groupByConfigHasInterval,
isKibanaContext,
KibanaContext,
KibanaContextValue,
PivotAggsConfig,
PivotAggsConfigDict,
PivotGroupByConfig,
PivotGroupByConfigDict,
SavedSearchQuery,
} from '../../common';
import { getPivotDropdownOptions } from './common';
@ -45,18 +47,21 @@ import { getPivotDropdownOptions } from './common';
export interface DefinePivotExposedState {
aggList: PivotAggsConfigDict;
groupByList: PivotGroupByConfigDict;
search: string;
search: string | SavedSearchQuery;
valid: boolean;
}
const defaultSearch = '*';
const emptySearch = '';
export function getDefaultPivotState(): DefinePivotExposedState {
export function getDefaultPivotState(kibanaContext: KibanaContextValue): DefinePivotExposedState {
return {
aggList: {} as PivotAggsConfigDict,
groupByList: {} as PivotGroupByConfigDict,
search: defaultSearch,
search:
kibanaContext.currentSavedSearch.id !== undefined
? kibanaContext.combinedQuery
: defaultSearch,
valid: false,
};
}
@ -67,8 +72,6 @@ interface Props {
}
export const DefinePivotForm: SFC<Props> = React.memo(({ overrides = {}, onChange }) => {
const defaults = { ...getDefaultPivotState(), ...overrides };
const kibanaContext = useContext(KibanaContext);
if (!isKibanaContext(kibanaContext)) {
@ -77,6 +80,8 @@ export const DefinePivotForm: SFC<Props> = React.memo(({ overrides = {}, onChang
const indexPattern = kibanaContext.currentIndexPattern;
const defaults = { ...getDefaultPivotState(kibanaContext), ...overrides };
// The search filter
const [search, setSearch] = useState(defaults.search);
@ -182,25 +187,50 @@ export const DefinePivotForm: SFC<Props> = React.memo(({ overrides = {}, onChang
]
);
const displaySearch = search === defaultSearch ? emptySearch : search;
return (
<EuiFlexGroup>
<EuiFlexItem grow={false} style={{ minWidth: '420px' }}>
<EuiForm>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.definePivotForm.queryLabel', {
defaultMessage: 'Query',
})}
>
<EuiFieldSearch
placeholder={i18n.translate('xpack.ml.dataframe.definePivotForm.queryPlaceholder', {
defaultMessage: 'Search...',
{kibanaContext.currentSavedSearch.id === undefined && typeof search === 'string' && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.definePivotForm.indexPatternLabel', {
defaultMessage: 'Index pattern',
})}
>
<span>{kibanaContext.currentIndexPattern.title}</span>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.definePivotForm.queryLabel', {
defaultMessage: 'Query',
})}
helpText={i18n.translate('xpack.ml.dataframe.definePivotForm.queryHelpText', {
defaultMessage: 'Use a query string to filter the source data (optional).',
})}
>
<EuiFieldSearch
placeholder={i18n.translate(
'xpack.ml.dataframe.definePivotForm.queryPlaceholder',
{
defaultMessage: 'Search...',
}
)}
onChange={searchHandler}
value={search === defaultSearch ? emptySearch : search}
/>
</EuiFormRow>
</Fragment>
)}
{kibanaContext.currentSavedSearch.id !== undefined && (
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.definePivotForm.savedSearchLabel', {
defaultMessage: 'Saved search',
})}
onChange={searchHandler}
value={displaySearch}
/>
</EuiFormRow>
>
<span>{kibanaContext.currentSavedSearch.title}</span>
</EuiFormRow>
)}
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.definePivotForm.groupByLabel', {

View file

@ -27,6 +27,7 @@ jest.mock('react', () => {
describe('Data Frame: <DefinePivotSummary />', () => {
test('Minimal initialization', () => {
const currentIndexPattern = {
id: 'the-index-pattern-id',
title: 'the-index-pattern-title',
fields: [],
};
@ -55,7 +56,14 @@ describe('Data Frame: <DefinePivotSummary />', () => {
const wrapper = shallow(
<div>
<KibanaContext.Provider
value={{ currentIndexPattern, indexPatterns: {}, kbnBaseUrl: 'url', kibanaConfig: {} }}
value={{
combinedQuery: {},
currentIndexPattern,
currentSavedSearch: {},
indexPatterns: {},
kbnBaseUrl: 'url',
kibanaConfig: {},
}}
>
<DefinePivotSummary {...props} />
</KibanaContext.Provider>

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { SFC, useContext } from 'react';
import React, { Fragment, SFC, useContext } from 'react';
import { i18n } from '@kbn/i18n';
@ -39,13 +39,36 @@ export const DefinePivotSummary: SFC<DefinePivotExposedState> = ({
<EuiFlexGroup>
<EuiFlexItem grow={false} style={{ minWidth: '420px' }}>
<EuiForm>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.definePivotSummary.queryLabel', {
defaultMessage: 'Query',
})}
>
<span>{displaySearch}</span>
</EuiFormRow>
{kibanaContext.currentSavedSearch.id === undefined && typeof search === 'string' && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.definePivotSummary.indexPatternLabel', {
defaultMessage: 'Index pattern',
})}
>
<span>{kibanaContext.currentIndexPattern.title}</span>
</EuiFormRow>
{displaySearch !== emptySearch && (
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.definePivotSummary.queryLabel', {
defaultMessage: 'Query',
})}
>
<span>{displaySearch}</span>
</EuiFormRow>
)}
</Fragment>
)}
{kibanaContext.currentSavedSearch.id !== undefined && (
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.definePivotForm.savedSearchLabel', {
defaultMessage: 'Saved search',
})}
>
<span>{kibanaContext.currentSavedSearch.title}</span>
</EuiFormRow>
)}
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.definePivotSummary.groupByLabel', {

View file

@ -27,6 +27,7 @@ jest.mock('react', () => {
describe('Data Frame: <PivotPreview />', () => {
test('Minimal initialization', () => {
const currentIndexPattern = {
id: 'the-index-pattern-id',
title: 'the-index-pattern-title',
fields: [],
};
@ -54,7 +55,14 @@ describe('Data Frame: <PivotPreview />', () => {
const wrapper = shallow(
<div>
<KibanaContext.Provider
value={{ currentIndexPattern, indexPatterns: {}, kbnBaseUrl: 'url', kibanaConfig: {} }}
value={{
combinedQuery: {},
currentIndexPattern,
currentSavedSearch: {},
indexPatterns: {},
kbnBaseUrl: 'url',
kibanaConfig: {},
}}
>
<PivotPreview {...props} />
</KibanaContext.Provider>

View file

@ -31,7 +31,7 @@ import {
PivotAggsConfigDict,
PivotGroupByConfig,
PivotGroupByConfigDict,
SimpleQuery,
PivotQuery,
} from '../../common';
import { getFlattenedFields } from '../source_index_preview/common';
@ -107,7 +107,7 @@ const PreviewTitle: SFC<PreviewTitleProps> = ({ previewRequest }) => {
interface PivotPreviewProps {
aggs: PivotAggsConfigDict;
groupBy: PivotGroupByConfigDict;
query: SimpleQuery;
query: PivotQuery;
}
export const PivotPreview: SFC<PivotPreviewProps> = React.memo(({ aggs, groupBy, query }) => {

View file

@ -45,7 +45,12 @@ let pivotPreviewObj: UsePivotPreviewDataReturnType;
describe('usePivotPreviewData', () => {
test('indexPattern not defined', () => {
testHook(() => {
pivotPreviewObj = usePivotPreviewData({ title: 'lorem', fields: [] }, query, {}, {});
pivotPreviewObj = usePivotPreviewData(
{ id: 'the-id', title: 'the-title', fields: [] },
query,
{},
{}
);
});
expect(pivotPreviewObj.errorMessage).toBe('');
@ -56,7 +61,12 @@ describe('usePivotPreviewData', () => {
test('indexPattern set triggers loading', () => {
testHook(() => {
pivotPreviewObj = usePivotPreviewData({ title: 'lorem', fields: [] }, query, {}, {});
pivotPreviewObj = usePivotPreviewData(
{ id: 'the-id', title: 'the-title', fields: [] },
query,
{},
{}
);
});
expect(pivotPreviewObj.errorMessage).toBe('');

View file

@ -6,7 +6,7 @@
import { useEffect, useState } from 'react';
import { StaticIndexPattern } from 'ui/index_patterns';
import { IndexPattern } from 'ui/index_patterns';
import { dictionaryToArray } from '../../../../common/types/common';
import { ml } from '../../../services/ml_api_service';
@ -18,7 +18,7 @@ import {
groupByConfigHasInterval,
PivotAggsConfigDict,
PivotGroupByConfigDict,
SimpleQuery,
PivotQuery,
} from '../../common';
export enum PIVOT_PREVIEW_STATUS {
@ -36,8 +36,8 @@ export interface UsePivotPreviewDataReturnType {
}
export const usePivotPreviewData = (
indexPattern: StaticIndexPattern,
query: SimpleQuery,
indexPattern: IndexPattern,
query: PivotQuery,
aggs: PivotAggsConfigDict,
groupBy: PivotGroupByConfigDict
): UsePivotPreviewDataReturnType => {
@ -82,7 +82,7 @@ export const usePivotPreviewData = (
g => `${g.agg} ${g.field} ${g.aggName} ${groupByConfigHasInterval(g) ? g.interval : ''}`
)
.join(' '),
query.query_string.query,
JSON.stringify(query),
]
);
return { errorMessage, status, dataFramePreviewData, previewRequest };

View file

@ -5,10 +5,13 @@ exports[`Data Frame: <JobCreateForm /> Minimal initialization 1`] = `
<ContextProvider
value={
Object {
"combinedQuery": Object {},
"currentIndexPattern": Object {
"fields": Array [],
"id": "the-index-pattern-id",
"title": "the-index-pattern-title",
},
"currentSavedSearch": Object {},
"indexPatterns": Object {},
"kbnBaseUrl": "url",
"kibanaConfig": Object {},

View file

@ -28,6 +28,7 @@ describe('Data Frame: <JobCreateForm />', () => {
};
const currentIndexPattern = {
id: 'the-index-pattern-id',
title: 'the-index-pattern-title',
fields: [],
};
@ -37,7 +38,14 @@ describe('Data Frame: <JobCreateForm />', () => {
const wrapper = shallow(
<div>
<KibanaContext.Provider
value={{ currentIndexPattern, indexPatterns: {}, kbnBaseUrl: 'url', kibanaConfig: {} }}
value={{
combinedQuery: {},
currentIndexPattern,
currentSavedSearch: {},
indexPatterns: {},
kbnBaseUrl: 'url',
kibanaConfig: {},
}}
>
<JobCreateForm {...props} />
</KibanaContext.Provider>

View file

@ -6,7 +6,7 @@
import { Dictionary } from '../../../../common/types/common';
import { SimpleQuery } from '../../common';
import { PivotQuery } from '../../common';
export type EsFieldName = string;
@ -81,10 +81,7 @@ export const toggleSelectedField = (
return selectedFields;
};
export const getSourceIndexDevConsoleStatement = (
query: SimpleQuery,
indexPatternTitle: string
) => {
export const getSourceIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => {
return `GET ${indexPatternTitle}/_search\n${JSON.stringify(
{
query,

View file

@ -38,7 +38,7 @@ const ExpandableTable = (EuiInMemoryTable as any) as FunctionComponent<Expandabl
import { Dictionary } from '../../../../common/types/common';
import { isKibanaContext, KibanaContext, SimpleQuery } from '../../common';
import { isKibanaContext, KibanaContext, PivotQuery } from '../../common';
import {
EsDoc,
@ -87,7 +87,7 @@ const SourceIndexPreviewTitle: React.SFC<SourceIndexPreviewTitle> = ({ indexPatt
);
interface Props {
query: SimpleQuery;
query: PivotQuery;
cellClick?(search: string): void;
}

View file

@ -49,7 +49,12 @@ describe('useSourceIndexData', () => {
test('indexPattern set triggers loading', () => {
testHook(() => {
act(() => {
sourceIndexObj = useSourceIndexData({ title: 'lorem', fields: [] }, query, [], () => {});
sourceIndexObj = useSourceIndexData(
{ id: 'the-id', title: 'the-title', fields: [] },
query,
[],
() => {}
);
});
});

View file

@ -8,11 +8,11 @@ import React, { useEffect, useState } from 'react';
import { SearchResponse } from 'elasticsearch';
import { StaticIndexPattern } from 'ui/index_patterns';
import { IndexPattern } from 'ui/index_patterns';
import { ml } from '../../../services/ml_api_service';
import { SimpleQuery } from '../../common';
import { PivotQuery } from '../../common';
import { EsDoc, EsFieldName, getDefaultSelectableFields } from './common';
const SEARCH_SIZE = 1000;
@ -31,8 +31,8 @@ export interface UseSourceIndexDataReturnType {
}
export const useSourceIndexData = (
indexPattern: StaticIndexPattern,
query: SimpleQuery,
indexPattern: IndexPattern,
query: PivotQuery,
selectedFields: EsFieldName[],
setSelectedFields: React.Dispatch<React.SetStateAction<EsFieldName[]>>
): UseSourceIndexDataReturnType => {
@ -71,7 +71,7 @@ export const useSourceIndexData = (
() => {
getSourceIndexData();
},
[indexPattern.title, query.query_string.query]
[indexPattern.title, JSON.stringify(query)]
);
return { errorMessage, status, tableItems };
};

View file

@ -11,7 +11,7 @@ import ReactDOM from 'react-dom';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml', ['react']);
import { StaticIndexPattern } from 'ui/index_patterns';
import { IndexPattern } from 'ui/index_patterns';
import { I18nContext } from 'ui/i18n';
import { IPrivate } from 'ui/private';
import { InjectorService } from '../../../../common/types/angular';
@ -19,7 +19,11 @@ import { InjectorService } from '../../../../common/types/angular';
// @ts-ignore
import { SearchItemsProvider } from '../../../jobs/new_job/utils/new_job_utils';
// Simple drop-in type until new_job_utils offers types.
type CreateSearchItems = () => { indexPattern: StaticIndexPattern };
type CreateSearchItems = () => {
indexPattern: IndexPattern;
savedSearch: any;
combinedQuery: any;
};
import { KibanaContext } from '../../common';
import { Page } from './page';
@ -35,10 +39,12 @@ module.directive('mlNewDataFrame', ($injector: InjectorService) => {
const Private: IPrivate = $injector.get('Private');
const createSearchItems: CreateSearchItems = Private(SearchItemsProvider);
const { indexPattern } = createSearchItems();
const { indexPattern, savedSearch, combinedQuery } = createSearchItems();
const kibanaContext = {
combinedQuery,
currentIndexPattern: indexPattern,
currentSavedSearch: savedSearch,
indexPatterns,
kbnBaseUrl,
kibanaConfig,

View file

@ -11,7 +11,7 @@ import { checkBasicLicense } from '../../../license/check_license';
// @ts-ignore
import { checkCreateDataFrameJobsPrivilege } from '../../../privilege/check_privilege';
// @ts-ignore
import { loadCurrentIndexPattern } from '../../../util/index_utils';
import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../util/index_utils';
// @ts-ignore
import { getDataFrameCreateBreadcrumbs } from '../../breadcrumbs';
@ -24,5 +24,6 @@ uiRoutes.when('/data_frames/new_job/step/pivot?', {
CheckLicense: checkBasicLicense,
privileges: checkCreateDataFrameJobsPrivilege,
indexPattern: loadCurrentIndexPattern,
savedSearch: loadCurrentSavedSearch,
},
});

View file

@ -85,7 +85,7 @@ export const Wizard: SFC = React.memo(() => {
const [currentStep, setCurrentStep] = useState(WIZARD_STEPS.DEFINE_PIVOT);
// The DEFINE_PIVOT state
const [pivotState, setPivot] = useState(getDefaultPivotState());
const [pivotState, setPivot] = useState(getDefaultPivotState(kibanaContext));
// The JOB_DETAILS state
const [jobDetailsState, setJobDetails] = useState(getDefaultJobDetailsState());