mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
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:
parent
e1bc386b20
commit
e0cc92f624
23 changed files with 404 additions and 244 deletions
9
x-pack/plugins/ml/common/types/angular.ts
Normal file
9
x-pack/plugins/ml/common/types/angular.ts
Normal 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;
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
30
x-pack/plugins/ml/public/data_frame/common/kibana_context.ts
Normal file
30
x-pack/plugins/ml/public/data_frame/common/kibana_context.ts
Normal 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
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 {},
|
||||
}
|
||||
}
|
||||
>
|
||||
|
|
|
@ -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 {},
|
||||
}
|
||||
}
|
||||
>
|
||||
|
|
|
@ -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 {},
|
||||
}
|
||||
}
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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 }));
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
||||
{!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" />
|
||||
|
||||
{!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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -5,4 +5,5 @@
|
|||
*/
|
||||
|
||||
export type JobId = string;
|
||||
export type TargetIndex = string;
|
||||
export type EsIndexName = string;
|
||||
export type IndexPatternTitle = string;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue