[ML] Data Frame: Optional index pattern creation. (#36020) (#36336)

Adds a checkbox to the data frame pivot wizard to optionally create an index pattern when creating the transform job. By default it's not enabled.
This commit is contained in:
Walter Rafelsberger 2019-05-09 11:30:07 +02:00 committed by GitHub
parent e1bc386b20
commit e0cc92f624
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 404 additions and 244 deletions

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export interface InjectorService {
get<T>(name: string, caller?: string): T;
}

View file

@ -6,7 +6,7 @@
export * from './aggregations';
export * from './dropdown';
export * from './index_pattern_context';
export * from './kibana_context';
export * from './pivot_aggs';
export * from './pivot_group_by';
export * from './request';

View file

@ -1,16 +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 React from 'react';
import { StaticIndexPattern } from 'ui/index_patterns';
// 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 union type `IndexPatternContextValue` takes care of allowing
// the actual required type and `null`.
export type IndexPatternContextValue = StaticIndexPattern | null;
export const IndexPatternContext = React.createContext<IndexPatternContextValue>(null);

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { StaticIndexPattern } from 'ui/index_patterns';
interface KibanaContextValue {
currentIndexPattern: StaticIndexPattern;
indexPatterns: any;
kibanaConfig: any;
}
// 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
// the actual required type and `null`.
export type NullableKibanaContextValue = KibanaContextValue | null;
export const KibanaContext = React.createContext<NullableKibanaContextValue>(null);
export function isKibanaContext(arg: any): arg is KibanaContextValue {
return (
arg.currentIndexPattern !== undefined &&
arg.indexPatterns !== undefined &&
arg.kibanaConfig !== undefined
);
}

View file

@ -69,6 +69,7 @@ describe('Data Frame: Common', () => {
valid: true,
};
const jobDetailsState: JobDetailsExposedState = {
createIndexPattern: false,
jobId: 'the-job-id',
targetIndex: 'the-target-index',
touched: true,

View file

@ -5,8 +5,12 @@ exports[`Data Frame: <DefinePivotForm /> Minimal initialization 1`] = `
<ContextProvider
value={
Object {
"fields": Array [],
"title": "the-index-pattern-title",
"currentIndexPattern": Object {
"fields": Array [],
"title": "the-index-pattern-title",
},
"indexPatterns": Object {},
"kibanaConfig": Object {},
}
}
>

View file

@ -5,8 +5,12 @@ exports[`Data Frame: <DefinePivotSummary /> Minimal initialization 1`] = `
<ContextProvider
value={
Object {
"fields": Array [],
"title": "the-index-pattern-title",
"currentIndexPattern": Object {
"fields": Array [],
"title": "the-index-pattern-title",
},
"indexPatterns": Object {},
"kibanaConfig": Object {},
}
}
>

View file

@ -5,8 +5,12 @@ exports[`Data Frame: <PivotPreview /> Minimal initialization 1`] = `
<ContextProvider
value={
Object {
"fields": Array [],
"title": "the-index-pattern-title",
"currentIndexPattern": Object {
"fields": Array [],
"title": "the-index-pattern-title",
},
"indexPatterns": Object {},
"kibanaConfig": Object {},
}
}
>

View file

@ -7,7 +7,7 @@
import { shallow } from 'enzyme';
import React from 'react';
import { IndexPatternContext } from '../../common';
import { KibanaContext } from '../../common';
import { DefinePivotForm } from './define_pivot_form';
// workaround to make React.memo() work with enzyme
@ -18,7 +18,7 @@ jest.mock('react', () => {
describe('Data Frame: <DefinePivotForm />', () => {
test('Minimal initialization', () => {
const indexPattern = {
const currentIndexPattern = {
title: 'the-index-pattern-title',
fields: [],
};
@ -27,9 +27,11 @@ describe('Data Frame: <DefinePivotForm />', () => {
// with the Provider being the outer most component.
const wrapper = shallow(
<div>
<IndexPatternContext.Provider value={indexPattern}>
<KibanaContext.Provider
value={{ currentIndexPattern, indexPatterns: {}, kibanaConfig: {} }}
>
<DefinePivotForm onChange={() => {}} />
</IndexPatternContext.Provider>
</KibanaContext.Provider>
</div>
);

View file

@ -32,7 +32,8 @@ import {
DropDownLabel,
getPivotQuery,
groupByConfigHasInterval,
IndexPatternContext,
isKibanaContext,
KibanaContext,
PivotAggsConfig,
PivotAggsConfigDict,
PivotGroupByConfig,
@ -68,12 +69,14 @@ interface Props {
export const DefinePivotForm: SFC<Props> = React.memo(({ overrides = {}, onChange }) => {
const defaults = { ...getDefaultPivotState(), ...overrides };
const indexPattern = useContext(IndexPatternContext);
const kibanaContext = useContext(KibanaContext);
if (indexPattern === null) {
if (!isKibanaContext(kibanaContext)) {
return null;
}
const indexPattern = kibanaContext.currentIndexPattern;
// The search filter
const [search, setSearch] = useState(defaults.search);

View file

@ -8,7 +8,7 @@ import { shallow } from 'enzyme';
import React from 'react';
import {
IndexPatternContext,
KibanaContext,
PivotAggsConfig,
PivotGroupByConfig,
PIVOT_SUPPORTED_AGGS,
@ -26,7 +26,7 @@ jest.mock('react', () => {
describe('Data Frame: <DefinePivotSummary />', () => {
test('Minimal initialization', () => {
const indexPattern = {
const currentIndexPattern = {
title: 'the-index-pattern-title',
fields: [],
};
@ -52,9 +52,11 @@ describe('Data Frame: <DefinePivotSummary />', () => {
// with the Provider being the outer most component.
const wrapper = shallow(
<div>
<IndexPatternContext.Provider value={indexPattern}>
<KibanaContext.Provider
value={{ currentIndexPattern, indexPatterns: {}, kibanaConfig: {} }}
>
<DefinePivotSummary {...props} />
</IndexPatternContext.Provider>
</KibanaContext.Provider>
</div>
);

View file

@ -24,7 +24,8 @@ import { PivotPreview } from './pivot_preview';
import {
DropDownOption,
getPivotQuery,
IndexPatternContext,
isKibanaContext,
KibanaContext,
PivotAggsConfigDict,
PIVOT_SUPPORTED_AGGS,
pivotSupportedAggs,
@ -40,12 +41,14 @@ export const DefinePivotSummary: SFC<DefinePivotExposedState> = ({
groupByList,
aggList,
}) => {
const indexPattern = useContext(IndexPatternContext);
const kibanaContext = useContext(KibanaContext);
if (indexPattern === null) {
if (!isKibanaContext(kibanaContext)) {
return null;
}
const indexPattern = kibanaContext.currentIndexPattern;
const fields = indexPattern.fields
.filter(field => field.aggregatable === true)
.map(field => ({ name: field.name, type: field.type }));

View file

@ -9,7 +9,7 @@ import React from 'react';
import {
getPivotQuery,
IndexPatternContext,
KibanaContext,
PivotAggsConfig,
PivotGroupByConfig,
PIVOT_SUPPORTED_AGGS,
@ -26,7 +26,7 @@ jest.mock('react', () => {
describe('Data Frame: <PivotPreview />', () => {
test('Minimal initialization', () => {
const indexPattern = {
const currentIndexPattern = {
title: 'the-index-pattern-title',
fields: [],
};
@ -51,9 +51,11 @@ describe('Data Frame: <PivotPreview />', () => {
// with the Provider being the outer most component.
const wrapper = shallow(
<div>
<IndexPatternContext.Provider value={indexPattern}>
<KibanaContext.Provider
value={{ currentIndexPattern, indexPatterns: {}, kibanaConfig: {} }}
>
<PivotPreview {...props} />
</IndexPatternContext.Provider>
</KibanaContext.Provider>
</div>
);

View file

@ -26,7 +26,8 @@ import { dictionaryToArray } from '../../../../common/types/common';
import {
DataFramePreviewRequest,
IndexPatternContext,
isKibanaContext,
KibanaContext,
PivotAggsConfigDict,
PivotGroupByConfig,
PivotGroupByConfigDict,
@ -110,12 +111,13 @@ interface PivotPreviewProps {
export const PivotPreview: SFC<PivotPreviewProps> = React.memo(({ aggs, groupBy, query }) => {
const [clearTable, setClearTable] = useState(false);
const indexPattern = useContext(IndexPatternContext);
const kibanaContext = useContext(KibanaContext);
if (indexPattern === null) {
if (!isKibanaContext(kibanaContext)) {
return null;
}
const indexPattern = kibanaContext.currentIndexPattern;
const { dataFramePreviewData, errorMessage, previewRequest, status } = usePivotPreviewData(
indexPattern,
query,

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Fragment, SFC, useEffect, useState } from 'react';
import React, { Fragment, SFC, useContext, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
@ -21,6 +21,8 @@ import {
import { ml } from '../../../services/ml_api_service';
import { KibanaContext, isKibanaContext } from '../../common';
export interface JobDetailsExposedState {
created: boolean;
started: boolean;
@ -37,123 +39,175 @@ function gotToDataFrameJobManagement() {
window.location.href = '#/data_frames';
}
interface Props {
createIndexPattern: boolean;
jobId: string;
jobConfig: any;
overrides: JobDetailsExposedState;
onChange(s: JobDetailsExposedState): void;
}
export const JobCreateForm: SFC<Props> = React.memo(({ jobConfig, jobId, onChange, overrides }) => {
const defaults = { ...getDefaultJobCreateState(), ...overrides };
export const JobCreateForm: SFC<Props> = React.memo(
({ createIndexPattern, jobConfig, jobId, onChange, overrides }) => {
const defaults = { ...getDefaultJobCreateState(), ...overrides };
const [created, setCreated] = useState(defaults.created);
const [started, setStarted] = useState(defaults.started);
const [created, setCreated] = useState(defaults.created);
const [started, setStarted] = useState(defaults.started);
useEffect(
() => {
onChange({ created, started });
},
[created, started]
);
const kibanaContext = useContext(KibanaContext);
async function createDataFrame() {
setCreated(true);
if (!isKibanaContext(kibanaContext)) {
return null;
}
useEffect(
() => {
onChange({ created, started });
},
[created, started]
);
async function createDataFrame() {
setCreated(true);
try {
await ml.dataFrame.createDataFrameTransformsJob(jobId, jobConfig);
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.jobCreateForm.createJobSuccessMessage', {
defaultMessage: 'Data frame job {jobId} created successfully.',
values: { jobId },
})
);
} catch (e) {
setCreated(false);
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.jobCreateForm.createJobErrorMessage', {
defaultMessage: 'An error occurred creating the data frame job {jobId}: {error}',
values: { jobId, error: JSON.stringify(e) },
})
);
return false;
}
if (createIndexPattern) {
createKibanaIndexPattern();
}
try {
await ml.dataFrame.createDataFrameTransformsJob(jobId, jobConfig);
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.jobCreateForm.createJobSuccessMessage', {
defaultMessage: 'Data frame job {jobId} created successfully.',
values: { jobId },
})
);
return true;
} catch (e) {
setCreated(false);
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.jobCreateForm.createJobErrorMessage', {
defaultMessage: 'An error occurred creating the data frame job {jobId}: {error}',
values: { jobId, error: JSON.stringify(e) },
})
);
return false;
}
}
async function startDataFrame() {
setStarted(true);
async function startDataFrame() {
setStarted(true);
try {
await ml.dataFrame.startDataFrameTransformsJob(jobId);
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.jobCreateForm.startJobSuccessMessage', {
defaultMessage: 'Data frame job {jobId} started successfully.',
values: { jobId },
})
);
} catch (e) {
setStarted(false);
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.jobCreateForm.startJobErrorMessage', {
defaultMessage: 'An error occurred starting the data frame job {jobId}: {error}',
values: { jobId, error: JSON.stringify(e) },
})
);
try {
await ml.dataFrame.startDataFrameTransformsJob(jobId);
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.jobCreateForm.startJobSuccessMessage', {
defaultMessage: 'Data frame job {jobId} started successfully.',
values: { jobId },
})
);
} catch (e) {
setStarted(false);
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.jobCreateForm.startJobErrorMessage', {
defaultMessage: 'An error occurred starting the data frame job {jobId}: {error}',
values: { jobId, error: JSON.stringify(e) },
})
);
}
}
}
async function createAndStartDataFrame() {
const success = await createDataFrame();
if (success) {
await startDataFrame();
async function createAndStartDataFrame() {
const success = await createDataFrame();
if (success) {
await startDataFrame();
}
}
}
return (
<Fragment>
<EuiButton isDisabled={created} onClick={createDataFrame}>
{i18n.translate('xpack.ml.dataframe.jobCreateForm.createDataFrameButton', {
defaultMessage: 'Create data frame',
})}
</EuiButton>
&nbsp;
{!created && (
<EuiButton fill isDisabled={created && started} onClick={createAndStartDataFrame}>
{i18n.translate('xpack.ml.dataframe.jobCreateForm.createAndStartDataFrameButton', {
defaultMessage: 'Create and start data frame',
const createKibanaIndexPattern = async () => {
const indexPatternName = jobConfig.dest.index;
try {
const newIndexPattern = await kibanaContext.indexPatterns.get();
Object.assign(newIndexPattern, {
id: '',
title: indexPatternName,
});
const id = await newIndexPattern.create();
// check if there's a default index pattern, if not,
// set the newly created one as the default index pattern.
if (!kibanaContext.kibanaConfig.get('defaultIndex')) {
await kibanaContext.kibanaConfig.set('defaultIndex', id);
}
toastNotifications.addSuccess(
i18n.translate('xpack.ml.dataframe.jobCreateForm.reateIndexPatternSuccessMessage', {
defaultMessage: 'Kibana index pattern {indexPatternName} created successfully.',
values: { indexPatternName },
})
);
return true;
} catch (e) {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.jobCreateForm.createIndexPatternErrorMessage', {
defaultMessage:
'An error occurred creating the Kibana index pattern {indexPatternName}: {error}',
values: { indexPatternName, error: JSON.stringify(e) },
})
);
return false;
}
};
return (
<Fragment>
<EuiButton isDisabled={created} onClick={createDataFrame}>
{i18n.translate('xpack.ml.dataframe.jobCreateForm.createDataFrameButton', {
defaultMessage: 'Create data frame',
})}
</EuiButton>
)}
{created && (
<EuiButton isDisabled={created && started} onClick={startDataFrame}>
{i18n.translate('xpack.ml.dataframe.jobCreateForm.startDataFrameButton', {
defaultMessage: 'Start data frame',
})}
</EuiButton>
)}
{created && started && (
<Fragment>
<EuiSpacer size="m" />
&nbsp;
{!created && (
<EuiButton fill isDisabled={created && started} onClick={createAndStartDataFrame}>
{i18n.translate('xpack.ml.dataframe.jobCreateForm.createAndStartDataFrameButton', {
defaultMessage: 'Create and start data frame',
})}
</EuiButton>
)}
{created && (
<EuiButton isDisabled={created && started} onClick={startDataFrame}>
{i18n.translate('xpack.ml.dataframe.jobCreateForm.startDataFrameButton', {
defaultMessage: 'Start data frame',
})}
</EuiButton>
)}
{created && started && (
<Fragment>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="l">
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xxl" type="list" />}
title={i18n.translate('xpack.ml.dataframe.jobCreateForm.jobManagementCardTitle', {
defaultMessage: 'Job management',
})}
description={i18n.translate(
'xpack.ml.dataframe.jobCreateForm.jobManagementCardDescription',
{
defaultMessage: 'Return to the data frame job management page.',
}
)}
onClick={gotToDataFrameJobManagement}
/>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
)}
</Fragment>
);
});
<EuiFlexGroup gutterSize="l">
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xxl" type="list" />}
title={i18n.translate('xpack.ml.dataframe.jobCreateForm.jobManagementCardTitle', {
defaultMessage: 'Job management',
})}
description={i18n.translate(
'xpack.ml.dataframe.jobCreateForm.jobManagementCardDescription',
{
defaultMessage: 'Return to the data frame job management page.',
}
)}
onClick={gotToDataFrameJobManagement}
/>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
)}
</Fragment>
);
}
);

View file

@ -5,4 +5,5 @@
*/
export type JobId = string;
export type TargetIndex = string;
export type EsIndexName = string;
export type IndexPatternTitle = string;

View file

@ -4,27 +4,29 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { SFC, useEffect, useState } from 'react';
import React, { SFC, useContext, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { toastNotifications } from 'ui/notify';
import { EuiFieldText, EuiForm, EuiFormRow } from '@elastic/eui';
import { EuiSwitch, EuiFieldText, EuiForm, EuiFormRow } from '@elastic/eui';
import { ml } from '../../../services/ml_api_service';
import { DataFrameJobConfig } from '../../common';
import { JobId, TargetIndex } from './common';
import { DataFrameJobConfig, KibanaContext, isKibanaContext } from '../../common';
import { EsIndexName, IndexPatternTitle, JobId } from './common';
export interface JobDetailsExposedState {
createIndexPattern: boolean;
jobId: JobId;
targetIndex: TargetIndex;
targetIndex: EsIndexName;
touched: boolean;
valid: boolean;
}
export function getDefaultJobDetailsState(): JobDetailsExposedState {
return {
createIndexPattern: true,
jobId: '',
targetIndex: '',
touched: false,
@ -38,12 +40,20 @@ interface Props {
}
export const JobDetailsForm: SFC<Props> = React.memo(({ overrides = {}, onChange }) => {
const kibanaContext = useContext(KibanaContext);
if (!isKibanaContext(kibanaContext)) {
return null;
}
const defaults = { ...getDefaultJobDetailsState(), ...overrides };
const [jobId, setJobId] = useState(defaults.jobId);
const [targetIndex, setTargetIndex] = useState(defaults.targetIndex);
const [jobIds, setJobIds] = useState([]);
const [indexNames, setIndexNames] = useState([] as string[]);
const [jobId, setJobId] = useState<JobId>(defaults.jobId);
const [targetIndex, setTargetIndex] = useState<EsIndexName>(defaults.targetIndex);
const [jobIds, setJobIds] = useState<JobId[]>([]);
const [indexNames, setIndexNames] = useState<EsIndexName[]>([]);
const [indexPatternTitles, setIndexPatternTitles] = useState<IndexPatternTitle[]>([]);
const [createIndexPattern, setCreateIndexPattern] = useState(defaults.createIndexPattern);
// fetch existing job IDs and indices once for form validation
useEffect(() => {
@ -74,19 +84,36 @@ export const JobDetailsForm: SFC<Props> = React.memo(({ overrides = {}, onChange
})
);
}
try {
setIndexPatternTitles(await kibanaContext.indexPatterns.getTitles());
} catch (e) {
toastNotifications.addDanger(
i18n.translate('xpack.ml.dataframe.jobDetailsForm.errorGettingIndexPatternTitles', {
defaultMessage: 'An error occurred getting the existing index pattern titles: {error}',
values: { error: JSON.stringify(e) },
})
);
}
})();
}, []);
const jobIdExists = jobIds.some(id => jobId === id);
const indexNameExists = indexNames.some(name => targetIndex === name);
const valid = jobId !== '' && targetIndex !== '' && !jobIdExists && !indexNameExists;
const indexPatternTitleExists = indexPatternTitles.some(name => targetIndex === name);
const valid =
jobId !== '' &&
targetIndex !== '' &&
!jobIdExists &&
!indexNameExists &&
(!indexPatternTitleExists || !createIndexPattern);
// expose state to wizard
useEffect(
() => {
onChange({ jobId, targetIndex, touched: true, valid });
onChange({ createIndexPattern, jobId, targetIndex, touched: true, valid });
},
[jobId, targetIndex, valid]
[createIndexPattern, jobId, targetIndex, valid]
);
return (
@ -140,6 +167,26 @@ export const JobDetailsForm: SFC<Props> = React.memo(({ overrides = {}, onChange
isInvalid={indexNameExists}
/>
</EuiFormRow>
<EuiFormRow
isInvalid={createIndexPattern && indexPatternTitleExists}
error={
createIndexPattern &&
indexPatternTitleExists && [
i18n.translate('xpack.ml.dataframe.jobDetailsForm.indexPatternTitleError', {
defaultMessage: 'An index pattern with this title already exists.',
}),
]
}
>
<EuiSwitch
name="mlDataFrameCreateIndexPattern"
label={i18n.translate('xpack.ml.dataframe.jobCreateForm.createIndexPatternLabel', {
defaultMessage: 'Create index pattern',
})}
checked={createIndexPattern === true}
onChange={() => setCreateIndexPattern(!createIndexPattern)}
/>
</EuiFormRow>
</EuiForm>
);
});

View file

@ -8,37 +8,40 @@ import React, { Fragment, SFC } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRow } from '@elastic/eui';
import { EuiFieldText, EuiFormRow } from '@elastic/eui';
import { JobId, TargetIndex } from './common';
import { JobDetailsExposedState } from './job_details_form';
interface Props {
jobId: JobId;
targetIndex: TargetIndex;
touched: boolean;
}
export const JobDetailsSummary: SFC<JobDetailsExposedState> = React.memo(
({ createIndexPattern, jobId, targetIndex, touched }) => {
if (touched === false) {
return null;
}
export const JobDetailsSummary: SFC<Props> = React.memo(({ jobId, targetIndex, touched }) => {
if (touched === false) {
return null;
const targetIndexHelpText = createIndexPattern
? i18n.translate('xpack.ml.dataframe.jobDetailsSummary.createIndexPatternMessage', {
defaultMessage: 'A Kibana index pattern will be created for this job.',
})
: '';
return (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.jobDetailsSummary.jobIdLabel', {
defaultMessage: 'Job id',
})}
>
<EuiFieldText defaultValue={jobId} disabled={true} />
</EuiFormRow>
<EuiFormRow
helpText={targetIndexHelpText}
label={i18n.translate('xpack.ml.dataframe.jobDetailsSummary.targetIndexLabel', {
defaultMessage: 'Target index',
})}
>
<EuiFieldText defaultValue={targetIndex} disabled={true} />
</EuiFormRow>
</Fragment>
);
}
return (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.jobDetailsSummary.jobIdLabel', {
defaultMessage: 'Job id',
})}
>
<span>{jobId}</span>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.jobDetailsSummary.targetIndexLabel', {
defaultMessage: 'Target index',
})}
>
<span>{targetIndex}</span>
</EuiFormRow>
</Fragment>
);
});
);

View file

@ -38,7 +38,7 @@ const ExpandableTable = (EuiInMemoryTable as any) as FunctionComponent<Expandabl
import { Dictionary } from '../../../../common/types/common';
import { IndexPatternContext, SimpleQuery } from '../../common';
import { isKibanaContext, KibanaContext, SimpleQuery } from '../../common';
import {
EsDoc,
@ -94,12 +94,14 @@ interface Props {
export const SourceIndexPreview: React.SFC<Props> = React.memo(({ cellClick, query }) => {
const [clearTable, setClearTable] = useState(false);
const indexPattern = useContext(IndexPatternContext);
const kibanaContext = useContext(KibanaContext);
if (indexPattern === null) {
if (!isKibanaContext(kibanaContext)) {
return null;
}
const indexPattern = kibanaContext.currentIndexPattern;
const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]);
const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false);

View file

@ -46,19 +46,6 @@ const query: SimpleQuery = {
let sourceIndexObj: UseSourceIndexDataReturnType;
describe('useSourceIndexData', () => {
test('indexPattern not defined', () => {
testHook(() => {
act(() => {
sourceIndexObj = useSourceIndexData(null, query, [], () => {});
});
});
expect(sourceIndexObj.errorMessage).toBe('');
expect(sourceIndexObj.status).toBe(SOURCE_INDEX_STATUS.UNUSED);
expect(sourceIndexObj.tableItems).toEqual([]);
expect(ml.esSearch).not.toHaveBeenCalled();
});
test('indexPattern set triggers loading', () => {
testHook(() => {
act(() => {

View file

@ -8,10 +8,11 @@ import React, { useEffect, useState } from 'react';
import { SearchResponse } from 'elasticsearch';
import { StaticIndexPattern } from 'ui/index_patterns';
import { ml } from '../../../services/ml_api_service';
import { SimpleQuery } from '../../common';
import { IndexPatternContextValue } from '../../common/index_pattern_context';
import { EsDoc, EsFieldName, getDefaultSelectableFields } from './common';
const SEARCH_SIZE = 1000;
@ -30,7 +31,7 @@ export interface UseSourceIndexDataReturnType {
}
export const useSourceIndexData = (
indexPattern: IndexPatternContextValue,
indexPattern: StaticIndexPattern,
query: SimpleQuery,
selectedFields: EsFieldName[],
setSelectedFields: React.Dispatch<React.SetStateAction<EsFieldName[]>>
@ -39,40 +40,38 @@ export const useSourceIndexData = (
const [status, setStatus] = useState(SOURCE_INDEX_STATUS.UNUSED);
const [tableItems, setTableItems] = useState([] as EsDoc[]);
if (indexPattern !== null) {
const getSourceIndexData = async function() {
setErrorMessage('');
setStatus(SOURCE_INDEX_STATUS.LOADING);
const getSourceIndexData = async function() {
setErrorMessage('');
setStatus(SOURCE_INDEX_STATUS.LOADING);
try {
const resp: SearchResponse<any> = await ml.esSearch({
index: indexPattern.title,
size: SEARCH_SIZE,
body: { query },
});
try {
const resp: SearchResponse<any> = await ml.esSearch({
index: indexPattern.title,
size: SEARCH_SIZE,
body: { query },
});
const docs = resp.hits.hits;
const docs = resp.hits.hits;
if (selectedFields.length === 0) {
const newSelectedFields = getDefaultSelectableFields(docs);
setSelectedFields(newSelectedFields);
}
setTableItems(docs as EsDoc[]);
setStatus(SOURCE_INDEX_STATUS.LOADED);
} catch (e) {
setErrorMessage(JSON.stringify(e));
setTableItems([] as EsDoc[]);
setStatus(SOURCE_INDEX_STATUS.ERROR);
if (selectedFields.length === 0) {
const newSelectedFields = getDefaultSelectableFields(docs);
setSelectedFields(newSelectedFields);
}
};
useEffect(
() => {
getSourceIndexData();
},
[indexPattern.title, query.query_string.query]
);
}
setTableItems(docs as EsDoc[]);
setStatus(SOURCE_INDEX_STATUS.LOADED);
} catch (e) {
setErrorMessage(JSON.stringify(e));
setTableItems([] as EsDoc[]);
setStatus(SOURCE_INDEX_STATUS.ERROR);
}
};
useEffect(
() => {
getSourceIndexData();
},
[indexPattern.title, query.query_string.query]
);
return { errorMessage, status, tableItems };
};

View file

@ -11,27 +11,42 @@ import ReactDOM from 'react-dom';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml', ['react']);
import { StaticIndexPattern } from 'ui/index_patterns';
import { I18nContext } from 'ui/i18n';
import { IPrivate } from 'ui/private';
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 };
import { IndexPatternContext } from '../../common';
import { KibanaContext } from '../../common';
import { Page } from './page';
module.directive('mlNewDataFrame', ($route: any, Private: any) => {
module.directive('mlNewDataFrame', ($injector: InjectorService) => {
return {
scope: {},
restrict: 'E',
link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => {
const createSearchItems = Private(SearchItemsProvider);
const indexPatterns = $injector.get('indexPatterns');
const kibanaConfig = $injector.get('config');
const Private: IPrivate = $injector.get('Private');
const createSearchItems: CreateSearchItems = Private(SearchItemsProvider);
const { indexPattern } = createSearchItems();
const kibanaContext = {
currentIndexPattern: indexPattern,
indexPatterns,
kibanaConfig,
};
ReactDOM.render(
<I18nContext>
<IndexPatternContext.Provider value={indexPattern}>
<KibanaContext.Provider value={kibanaContext}>
{React.createElement(Page)}
</IndexPatternContext.Provider>
</KibanaContext.Provider>
</I18nContext>,
element[0]
);

View file

@ -32,7 +32,7 @@ import {
JobDetailsSummary,
} from '../../components/job_details';
import { IndexPatternContext } from '../../common';
import { isKibanaContext, KibanaContext } from '../../common';
enum WIZARD_STEPS {
DEFINE_PIVOT,
@ -73,13 +73,14 @@ const DefinePivotStep: SFC<DefinePivotStepProps> = ({
};
export const Wizard: SFC = React.memo(() => {
// indexPattern from context
const indexPattern = useContext(IndexPatternContext);
const kibanaContext = useContext(KibanaContext);
if (indexPattern === null) {
if (!isKibanaContext(kibanaContext)) {
return null;
}
const indexPattern = kibanaContext.currentIndexPattern;
// The current WIZARD_STEP
const [currentStep, setCurrentStep] = useState(WIZARD_STEPS.DEFINE_PIVOT);
@ -102,6 +103,7 @@ export const Wizard: SFC = React.memo(() => {
const jobCreate =
currentStep === WIZARD_STEPS.JOB_CREATE ? (
<JobCreateForm
createIndexPattern={jobDetailsState.createIndexPattern}
jobId={jobDetailsState.jobId}
jobConfig={getDataFrameRequest(indexPattern.title, pivotState, jobDetailsState)}
onChange={setJobCreate}