mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[ML] Data Frame UI (#33427)
This commit is contained in:
parent
8a7d570ce3
commit
d316dca98a
55 changed files with 2998 additions and 1 deletions
|
@ -21,6 +21,7 @@ import mappings from './mappings';
|
|||
import { makeMlUsageCollector } from './server/lib/ml_telemetry';
|
||||
import { notificationRoutes } from './server/routes/notification_settings';
|
||||
import { systemRoutes } from './server/routes/system';
|
||||
import { dataFrameRoutes } from './server/routes/data_frame';
|
||||
import { dataRecognizer } from './server/routes/modules';
|
||||
import { dataVisualizerRoutes } from './server/routes/data_visualizer';
|
||||
import { calendars } from './server/routes/calendars';
|
||||
|
@ -128,6 +129,7 @@ export const ml = (kibana) => {
|
|||
annotationRoutes(server, commonRouteConfig);
|
||||
jobRoutes(server, commonRouteConfig);
|
||||
dataFeedRoutes(server, commonRouteConfig);
|
||||
dataFrameRoutes(server, commonRouteConfig);
|
||||
indicesRoutes(server, commonRouteConfig);
|
||||
jobValidationRoutes(server, commonRouteConfig);
|
||||
notificationRoutes(server, commonRouteConfig);
|
||||
|
|
|
@ -20,6 +20,7 @@ import 'plugins/ml/lib/angular_bootstrap_patch';
|
|||
import 'plugins/ml/jobs';
|
||||
import 'plugins/ml/services/calendar_service';
|
||||
import 'plugins/ml/components/messagebar';
|
||||
import 'plugins/ml/data_frame';
|
||||
import 'plugins/ml/datavisualizer';
|
||||
import 'plugins/ml/explorer';
|
||||
import 'plugins/ml/timeseriesexplorer';
|
||||
|
|
|
@ -7,4 +7,3 @@
|
|||
|
||||
|
||||
import './form_filter_input_directive';
|
||||
|
||||
|
|
|
@ -24,6 +24,13 @@
|
|||
i18n-default-message="Single Metric Viewer"
|
||||
></span>
|
||||
</a>
|
||||
<a kbn-href="#/data_frame" class="kuiLocalTab" role="tab"
|
||||
ng-class="{'kuiLocalTab-isSelected': isActiveTab('data_frame')}">
|
||||
<span
|
||||
i18n-id="xpack.ml.navMenu.dataFrameTabLinkText"
|
||||
i18n-default-message="Data Frame"
|
||||
></span>
|
||||
</a>
|
||||
<a kbn-href="#/datavisualizer" class="kuiLocalTab" role="tab"
|
||||
ng-class="{'kuiLocalTab-isSelected': isActiveTab('datavisualizer')}">
|
||||
<span
|
||||
|
|
|
@ -26,6 +26,7 @@ module.directive('mlNavMenu', function () {
|
|||
scope.showTabs = false;
|
||||
if (scope.name === 'jobs' ||
|
||||
scope.name === 'settings' ||
|
||||
scope.name === 'data_frame' ||
|
||||
scope.name === 'datavisualizer' ||
|
||||
scope.name === 'filedatavisualizer' ||
|
||||
scope.name === 'timeseriesexplorer' ||
|
||||
|
|
61
x-pack/plugins/ml/public/data_frame/breadcrumbs.ts
Normal file
61
x-pack/plugins/ml/public/data_frame/breadcrumbs.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
// @ts-ignore
|
||||
import { ML_BREADCRUMB } from '../breadcrumbs';
|
||||
|
||||
export function getJobManagementBreadcrumbs() {
|
||||
// Whilst top level nav menu with tabs remains,
|
||||
// use root ML breadcrumb.
|
||||
return [ML_BREADCRUMB];
|
||||
}
|
||||
|
||||
export function getDataFrameBreadcrumbs() {
|
||||
return [
|
||||
ML_BREADCRUMB,
|
||||
{
|
||||
text: i18n.translate('xpack.ml.dataFrameBreadcrumbs.dataFrameLabel', {
|
||||
defaultMessage: 'Data Frame',
|
||||
}),
|
||||
href: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const DATA_FRAME_HOME = {
|
||||
text: i18n.translate('xpack.ml.dataFrameBreadcrumbs.dataFrameLabel', {
|
||||
defaultMessage: 'Data Frame',
|
||||
}),
|
||||
href: '#/data_frame',
|
||||
};
|
||||
|
||||
export function getDataFrameCreateBreadcrumbs() {
|
||||
return [
|
||||
ML_BREADCRUMB,
|
||||
DATA_FRAME_HOME,
|
||||
{
|
||||
text: i18n.translate('xpack.ml.dataFrameBreadcrumbs.dataFrameCreateLabel', {
|
||||
defaultMessage: 'Create data frame',
|
||||
}),
|
||||
href: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getDataFrameIndexOrSearchBreadcrumbs() {
|
||||
return [
|
||||
ML_BREADCRUMB,
|
||||
DATA_FRAME_HOME,
|
||||
{
|
||||
text: i18n.translate('xpack.ml.dataFrameBreadcrumbs.selectIndexOrSearchLabel', {
|
||||
defaultMessage: 'Select index or search',
|
||||
}),
|
||||
href: '',
|
||||
},
|
||||
];
|
||||
}
|
171
x-pack/plugins/ml/public/data_frame/common/index.ts
Normal file
171
x-pack/plugins/ml/public/data_frame/common/index.ts
Normal file
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* 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 { DefaultOperator } from 'elasticsearch';
|
||||
|
||||
import { StaticIndexPattern } from 'ui/index_patterns';
|
||||
|
||||
import { Dictionary } from '../../../common/types/common';
|
||||
|
||||
import { DefinePivotExposedState } from '../components/define_pivot/define_pivot_form';
|
||||
|
||||
// The display label used for an aggregation e.g. sum(bytes).
|
||||
export type Label = string;
|
||||
|
||||
// Label object structured for EUI's ComboBox.
|
||||
export interface DropDownLabel {
|
||||
label: Label;
|
||||
}
|
||||
|
||||
// Label object structure for EUI's ComboBox with support for nesting.
|
||||
export interface DropDownOption {
|
||||
label: Label;
|
||||
options: DropDownLabel[];
|
||||
}
|
||||
|
||||
// The internal representation of an aggregation definition.
|
||||
type AggName = string;
|
||||
type FieldName = string;
|
||||
export interface OptionsDataElement {
|
||||
agg: PivotAggSupportedAggs;
|
||||
field: FieldName;
|
||||
formRowLabel: AggName;
|
||||
}
|
||||
|
||||
export type OptionsDataElementDict = Dictionary<OptionsDataElement>;
|
||||
|
||||
export interface SimpleQuery {
|
||||
query_string: {
|
||||
query: string;
|
||||
default_operator: DefaultOperator;
|
||||
};
|
||||
}
|
||||
|
||||
// DataFramePreviewRequest
|
||||
type PivotGroupBySupportedAggs = 'terms';
|
||||
type PivotGroupBy = {
|
||||
[key in PivotGroupBySupportedAggs]: {
|
||||
field: string;
|
||||
}
|
||||
};
|
||||
type PivotGroupByDict = Dictionary<PivotGroupBy>;
|
||||
|
||||
export enum PIVOT_SUPPORTED_AGGS {
|
||||
AVG = 'avg',
|
||||
CARDINALITY = 'cardinality',
|
||||
MAX = 'max',
|
||||
MIN = 'min',
|
||||
SUM = 'sum',
|
||||
VALUE_COUNT = 'value_count',
|
||||
}
|
||||
|
||||
type PivotAggSupportedAggs =
|
||||
| PIVOT_SUPPORTED_AGGS.AVG
|
||||
| PIVOT_SUPPORTED_AGGS.CARDINALITY
|
||||
| PIVOT_SUPPORTED_AGGS.MAX
|
||||
| PIVOT_SUPPORTED_AGGS.MIN
|
||||
| PIVOT_SUPPORTED_AGGS.SUM
|
||||
| PIVOT_SUPPORTED_AGGS.VALUE_COUNT;
|
||||
|
||||
type PivotAgg = {
|
||||
[key in PivotAggSupportedAggs]?: {
|
||||
field: FieldName;
|
||||
}
|
||||
};
|
||||
type PivotAggDict = { [key in AggName]: PivotAgg };
|
||||
|
||||
export interface DataFramePreviewRequest {
|
||||
pivot: {
|
||||
group_by: PivotGroupByDict;
|
||||
aggregations: PivotAggDict;
|
||||
};
|
||||
source: {
|
||||
index: string;
|
||||
query?: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DataFrameRequest extends DataFramePreviewRequest {
|
||||
dest: {
|
||||
index: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const pivotSupportedAggs = [
|
||||
PIVOT_SUPPORTED_AGGS.AVG,
|
||||
PIVOT_SUPPORTED_AGGS.CARDINALITY,
|
||||
PIVOT_SUPPORTED_AGGS.MAX,
|
||||
PIVOT_SUPPORTED_AGGS.MIN,
|
||||
PIVOT_SUPPORTED_AGGS.SUM,
|
||||
PIVOT_SUPPORTED_AGGS.VALUE_COUNT,
|
||||
] as PivotAggSupportedAggs[];
|
||||
|
||||
export function getPivotQuery(search: string): SimpleQuery {
|
||||
return {
|
||||
query_string: {
|
||||
query: search,
|
||||
default_operator: 'AND',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getDataFramePreviewRequest(
|
||||
indexPatternTitle: StaticIndexPattern['title'],
|
||||
query: SimpleQuery,
|
||||
groupBy: string[],
|
||||
aggs: OptionsDataElement[]
|
||||
): DataFramePreviewRequest {
|
||||
const request: DataFramePreviewRequest = {
|
||||
source: {
|
||||
index: indexPatternTitle,
|
||||
query,
|
||||
},
|
||||
pivot: {
|
||||
group_by: {},
|
||||
aggregations: {},
|
||||
},
|
||||
};
|
||||
|
||||
groupBy.forEach(g => {
|
||||
request.pivot.group_by[g] = {
|
||||
terms: {
|
||||
field: g,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
aggs.forEach(agg => {
|
||||
request.pivot.aggregations[agg.formRowLabel] = {
|
||||
[agg.agg]: {
|
||||
field: agg.field,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
export function getDataFrameRequest(
|
||||
indexPatternTitle: StaticIndexPattern['title'],
|
||||
pivotState: DefinePivotExposedState,
|
||||
jobDetailsState: any
|
||||
): DataFrameRequest {
|
||||
const request: DataFrameRequest = {
|
||||
...getDataFramePreviewRequest(
|
||||
indexPatternTitle,
|
||||
getPivotQuery(pivotState.search),
|
||||
pivotState.groupBy,
|
||||
pivotState.aggs
|
||||
),
|
||||
dest: {
|
||||
index: jobDetailsState.targetIndex,
|
||||
},
|
||||
};
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
export * from './index_pattern_context';
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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);
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
options: EuiComboBoxOptionProps[];
|
||||
placeholder?: string;
|
||||
changeHandler(d: EuiComboBoxOptionProps[]): void;
|
||||
}
|
||||
|
||||
export const DropDown: React.SFC<Props> = ({
|
||||
changeHandler,
|
||||
options,
|
||||
placeholder = 'Search ...',
|
||||
}) => {
|
||||
return (
|
||||
<EuiComboBox
|
||||
placeholder={placeholder}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
options={options}
|
||||
selectedOptions={[]}
|
||||
onChange={changeHandler}
|
||||
isClearable={false}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 * from './list_form';
|
||||
export * from './list_summary';
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiListGroup,
|
||||
EuiListGroupItem,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { Dictionary } from '../../../../common/types/common';
|
||||
|
||||
interface OptionsDataElement {
|
||||
agg: string;
|
||||
field: string;
|
||||
formRowLabel: string;
|
||||
}
|
||||
|
||||
interface ListProps {
|
||||
list: string[];
|
||||
optionsData: Dictionary<OptionsDataElement>;
|
||||
deleteHandler?(l: string): void;
|
||||
}
|
||||
|
||||
export const AggListForm: React.SFC<ListProps> = ({ deleteHandler, list, optionsData }) => (
|
||||
<EuiListGroup flush={true}>
|
||||
{list.map((optionsDataId: string) => (
|
||||
<EuiListGroupItem
|
||||
key={optionsDataId}
|
||||
label={
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.aggregationListForm.customNameLabel', {
|
||||
defaultMessage: 'Custom name',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText defaultValue={optionsData[optionsDataId].formRowLabel} />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.aggregationListForm.aggregationLabel', {
|
||||
defaultMessage: 'Aggregation',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText defaultValue={optionsData[optionsDataId].agg} />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.aggregationListForm.fieldLabel', {
|
||||
defaultMessage: 'Field',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText defaultValue={optionsData[optionsDataId].field} />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
extraAction={
|
||||
(deleteHandler && {
|
||||
onClick: () => deleteHandler(optionsDataId),
|
||||
iconType: 'cross',
|
||||
iconSize: 's',
|
||||
'aria-label': optionsDataId,
|
||||
alwaysShow: false,
|
||||
}) ||
|
||||
undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</EuiListGroup>
|
||||
);
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { EuiForm, EuiFormRow } from '@elastic/eui';
|
||||
|
||||
import { Dictionary } from '../../../../common/types/common';
|
||||
|
||||
interface OptionsDataElement {
|
||||
agg: string;
|
||||
field: string;
|
||||
formRowLabel: string;
|
||||
}
|
||||
|
||||
interface ListProps {
|
||||
list: string[];
|
||||
optionsData: Dictionary<OptionsDataElement>;
|
||||
deleteHandler?(l: string): void;
|
||||
}
|
||||
|
||||
export const AggListSummary: React.SFC<ListProps> = ({ list, optionsData }) => (
|
||||
<EuiForm>
|
||||
{list.map((l: string) => (
|
||||
<EuiFormRow key={l} label={optionsData[l].formRowLabel}>
|
||||
<span>{l}</span>
|
||||
</EuiFormRow>
|
||||
))}
|
||||
</EuiForm>
|
||||
);
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 enum FIELD_TYPE {
|
||||
IP = 'ip',
|
||||
NUMBER = 'number',
|
||||
STRING = 'string',
|
||||
}
|
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
* 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 { uniq } from 'lodash';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import React, { ChangeEvent, Fragment, SFC, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiComboBoxOptionProps,
|
||||
EuiFieldSearch,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormHelpText,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { DropDown } from '../../components/aggregation_dropdown/dropdown';
|
||||
import { AggListForm } from '../../components/aggregation_list';
|
||||
import { GroupByList } from '../../components/group_by_list/list';
|
||||
import { SourceIndexPreview } from '../../components/source_index_preview';
|
||||
import { PivotPreview } from './pivot_preview';
|
||||
|
||||
import { Dictionary } from '../../../../common/types/common';
|
||||
import {
|
||||
DropDownLabel,
|
||||
DropDownOption,
|
||||
getPivotQuery,
|
||||
Label,
|
||||
OptionsDataElement,
|
||||
pivotSupportedAggs,
|
||||
PIVOT_SUPPORTED_AGGS,
|
||||
} from '../../common';
|
||||
|
||||
import { IndexPatternContext } from '../../common';
|
||||
import { FIELD_TYPE } from './common';
|
||||
|
||||
export interface DefinePivotExposedState {
|
||||
aggList: Label[];
|
||||
aggs: OptionsDataElement[];
|
||||
groupBy: Label[];
|
||||
search: string;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
const defaultSearch = '*';
|
||||
const emptySearch = '';
|
||||
|
||||
export function getDefaultPivotState(): DefinePivotExposedState {
|
||||
return {
|
||||
aggList: [] as Label[],
|
||||
aggs: [] as OptionsDataElement[],
|
||||
groupBy: [] as Label[],
|
||||
search: defaultSearch,
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
overrides?: DefinePivotExposedState;
|
||||
onChange(s: DefinePivotExposedState): void;
|
||||
}
|
||||
|
||||
export const DefinePivotForm: SFC<Props> = React.memo(({ overrides = {}, onChange }) => {
|
||||
const defaults = { ...getDefaultPivotState(), ...overrides };
|
||||
|
||||
const indexPattern = useContext(IndexPatternContext);
|
||||
|
||||
if (indexPattern === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fields = indexPattern.fields
|
||||
.filter(field => field.aggregatable === true)
|
||||
.map(field => ({ name: field.name, type: field.type }));
|
||||
|
||||
// The search filter
|
||||
const [search, setSearch] = useState(defaults.search);
|
||||
|
||||
const addToSearch = (newSearch: string) => {
|
||||
const currentDisplaySearch = search === defaultSearch ? emptySearch : search;
|
||||
setSearch(`${currentDisplaySearch} ${newSearch}`.trim());
|
||||
};
|
||||
|
||||
const searchHandler = (d: ChangeEvent<HTMLInputElement>) => {
|
||||
const newSearch = d.currentTarget.value === emptySearch ? defaultSearch : d.currentTarget.value;
|
||||
setSearch(newSearch);
|
||||
};
|
||||
|
||||
// The list of selected group by fields
|
||||
const [groupBy, setGroupBy] = useState(defaults.groupBy as Label[]);
|
||||
|
||||
const addGroupBy = (d: DropDownLabel[]) => {
|
||||
const label: Label = d[0].label;
|
||||
const newList = uniq([...groupBy, label]);
|
||||
setGroupBy(newList);
|
||||
};
|
||||
|
||||
const deleteGroupBy = (label: Label) => {
|
||||
const newList = groupBy.filter(l => l !== label);
|
||||
setGroupBy(newList);
|
||||
};
|
||||
|
||||
// The list of selected aggregations
|
||||
const [aggList, setAggList] = useState(defaults.aggList as Label[]);
|
||||
|
||||
const addAggregation = (d: DropDownLabel[]) => {
|
||||
const label: Label = d[0].label;
|
||||
const newList = uniq([...aggList, label]);
|
||||
setAggList(newList);
|
||||
};
|
||||
|
||||
const deleteAggregation = (label: Label) => {
|
||||
const newList = aggList.filter(l => l !== label);
|
||||
setAggList(newList);
|
||||
};
|
||||
|
||||
// The available fields for group by
|
||||
const groupByOptions: EuiComboBoxOptionProps[] = [];
|
||||
fields.forEach(field => {
|
||||
const o: DropDownLabel = { label: field.name };
|
||||
groupByOptions.push(o);
|
||||
});
|
||||
|
||||
// The available aggregations
|
||||
const aggOptions: EuiComboBoxOptionProps[] = [];
|
||||
const aggOptionsData: Dictionary<OptionsDataElement> = {};
|
||||
|
||||
fields.forEach(field => {
|
||||
const o: DropDownOption = { label: field.name, options: [] };
|
||||
pivotSupportedAggs.forEach(agg => {
|
||||
if (
|
||||
(agg === PIVOT_SUPPORTED_AGGS.CARDINALITY &&
|
||||
(field.type === FIELD_TYPE.STRING || field.type === FIELD_TYPE.IP)) ||
|
||||
(agg !== PIVOT_SUPPORTED_AGGS.CARDINALITY && field.type === FIELD_TYPE.NUMBER)
|
||||
) {
|
||||
const label = `${agg}(${field.name})`;
|
||||
o.options.push({ label });
|
||||
const formRowLabel = `${agg}_${field.name}`;
|
||||
aggOptionsData[label] = { agg, field: field.name, formRowLabel };
|
||||
}
|
||||
});
|
||||
aggOptions.push(o);
|
||||
});
|
||||
|
||||
const pivotAggs = aggList.map(l => aggOptionsData[l]);
|
||||
const pivotGroupBy = groupBy;
|
||||
const pivotQuery = getPivotQuery(search);
|
||||
|
||||
const valid = pivotGroupBy.length > 0 && aggList.length > 0;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
onChange({ aggList, aggs: pivotAggs, groupBy: pivotGroupBy, search, valid });
|
||||
},
|
||||
[
|
||||
aggList,
|
||||
pivotAggs.map(d => `${d.agg} ${d.field} ${d.formRowLabel}`).join(' '),
|
||||
pivotGroupBy,
|
||||
search,
|
||||
valid,
|
||||
]
|
||||
);
|
||||
|
||||
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...',
|
||||
})}
|
||||
onChange={searchHandler}
|
||||
value={displaySearch}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.definePivotForm.groupByLabel', {
|
||||
defaultMessage: 'Group by',
|
||||
})}
|
||||
>
|
||||
<Fragment>
|
||||
<GroupByList list={pivotGroupBy} deleteHandler={deleteGroupBy} />
|
||||
<DropDown
|
||||
changeHandler={addGroupBy}
|
||||
options={groupByOptions}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.ml.dataframe.definePivotForm.groupByPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Add a group by field ...',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.definePivotForm.aggregationsLabel', {
|
||||
defaultMessage: 'Aggregations',
|
||||
})}
|
||||
>
|
||||
<Fragment>
|
||||
<AggListForm
|
||||
list={aggList}
|
||||
optionsData={aggOptionsData}
|
||||
deleteHandler={deleteAggregation}
|
||||
/>
|
||||
<DropDown
|
||||
changeHandler={addAggregation}
|
||||
options={aggOptions}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.ml.dataframe.definePivotForm.aggregationsPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Add an aggregation ...',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
</EuiFormRow>
|
||||
{!valid && (
|
||||
<EuiFormHelpText style={{ maxWidth: '320px' }}>
|
||||
{i18n.translate('xpack.ml.dataframe.definePivotForm.formHelp', {
|
||||
defaultMessage:
|
||||
'Data frame transforms are scalable and automated processes for pivoting. Choose at least one group-by and aggregation to get started.',
|
||||
})}
|
||||
</EuiFormHelpText>
|
||||
)}
|
||||
</EuiForm>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<SourceIndexPreview cellClick={addToSearch} query={pivotQuery} />
|
||||
<EuiSpacer size="l" />
|
||||
<PivotPreview aggs={pivotAggs} groupBy={pivotGroupBy} query={pivotQuery} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* 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, { SFC, useContext } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
EuiComboBoxOptionProps,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { AggListSummary } from '../../components/aggregation_list';
|
||||
import { GroupByList } from '../../components/group_by_list/list';
|
||||
import { PivotPreview } from './pivot_preview';
|
||||
|
||||
import { Dictionary } from '../../../../common/types/common';
|
||||
import {
|
||||
DropDownLabel,
|
||||
DropDownOption,
|
||||
IndexPatternContext,
|
||||
Label,
|
||||
OptionsDataElement,
|
||||
PIVOT_SUPPORTED_AGGS,
|
||||
pivotSupportedAggs,
|
||||
SimpleQuery,
|
||||
} from '../../common';
|
||||
import { FIELD_TYPE } from './common';
|
||||
|
||||
const defaultSearch = '*';
|
||||
const emptySearch = '';
|
||||
|
||||
interface Props {
|
||||
search: string;
|
||||
groupBy: Label[];
|
||||
aggList: Label[];
|
||||
}
|
||||
|
||||
export const DefinePivotSummary: SFC<Props> = ({ search, groupBy, aggList }) => {
|
||||
const indexPattern = useContext(IndexPatternContext);
|
||||
|
||||
if (indexPattern === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fields = indexPattern.fields
|
||||
.filter(field => field.aggregatable === true)
|
||||
.map(field => ({ name: field.name, type: field.type }));
|
||||
|
||||
// The available fields for group by
|
||||
const groupByOptions: EuiComboBoxOptionProps[] = [];
|
||||
fields.forEach(field => {
|
||||
const o: DropDownLabel = { label: field.name };
|
||||
groupByOptions.push(o);
|
||||
});
|
||||
|
||||
// The available aggregations
|
||||
const aggOptions: EuiComboBoxOptionProps[] = [];
|
||||
const aggOptionsData: Dictionary<OptionsDataElement> = {};
|
||||
|
||||
fields.forEach(field => {
|
||||
const o: DropDownOption = { label: field.name, options: [] };
|
||||
pivotSupportedAggs.forEach(agg => {
|
||||
if (
|
||||
(agg === PIVOT_SUPPORTED_AGGS.CARDINALITY &&
|
||||
(field.type === FIELD_TYPE.STRING || field.type === FIELD_TYPE.IP)) ||
|
||||
(agg !== PIVOT_SUPPORTED_AGGS.CARDINALITY && field.type === FIELD_TYPE.NUMBER)
|
||||
) {
|
||||
const label = `${agg}(${field.name})`;
|
||||
o.options.push({ label });
|
||||
const formRowLabel = `${agg}_${field.name}`;
|
||||
aggOptionsData[label] = { agg, field: field.name, formRowLabel };
|
||||
}
|
||||
});
|
||||
aggOptions.push(o);
|
||||
});
|
||||
|
||||
const pivotAggs = aggList.map(l => aggOptionsData[l]);
|
||||
const pivotGroupBy = groupBy;
|
||||
|
||||
const pivotQuery: SimpleQuery = {
|
||||
query_string: {
|
||||
query: search,
|
||||
default_operator: 'AND',
|
||||
},
|
||||
};
|
||||
|
||||
const displaySearch = search === defaultSearch ? emptySearch : search;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false} style={{ minWidth: '420px' }}>
|
||||
<EuiForm>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.definePivotSummary.queryLabel', {
|
||||
defaultMessage: 'Query',
|
||||
})}
|
||||
>
|
||||
<span>{displaySearch}</span>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.definePivotSummary.groupByLabel', {
|
||||
defaultMessage: 'Group by',
|
||||
})}
|
||||
>
|
||||
<GroupByList list={pivotGroupBy} />
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.definePivotSummary.aggregationsLabel', {
|
||||
defaultMessage: 'Aggregations',
|
||||
})}
|
||||
>
|
||||
<AggListSummary list={aggList} optionsData={aggOptionsData} />
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<PivotPreview aggs={pivotAggs} groupBy={pivotGroupBy} query={pivotQuery} />
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 {
|
||||
DefinePivotExposedState,
|
||||
DefinePivotForm,
|
||||
getDefaultPivotState,
|
||||
} from './define_pivot_form';
|
||||
|
||||
export { DefinePivotSummary } from './define_pivot_summary';
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* 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, { useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiInMemoryTable, EuiPanel, EuiProgress, EuiTitle, SortDirection } from '@elastic/eui';
|
||||
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
|
||||
import {
|
||||
getDataFramePreviewRequest,
|
||||
IndexPatternContext,
|
||||
OptionsDataElement,
|
||||
SimpleQuery,
|
||||
} from '../../common';
|
||||
|
||||
interface Props {
|
||||
aggs: OptionsDataElement[];
|
||||
groupBy: string[];
|
||||
query: SimpleQuery;
|
||||
}
|
||||
|
||||
export const PivotPreview: React.SFC<Props> = React.memo(({ aggs, groupBy, query }) => {
|
||||
const indexPattern = useContext(IndexPatternContext);
|
||||
|
||||
if (indexPattern === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [dataFramePreviewData, setDataFramePreviewData] = useState([]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (aggs.length === 0) {
|
||||
setDataFramePreviewData([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const request = getDataFramePreviewRequest(indexPattern.title, query, groupBy, aggs);
|
||||
|
||||
ml.dataFrame
|
||||
.getDataFrameTransformsPreview(request)
|
||||
.then((resp: any) => {
|
||||
setDataFramePreviewData(resp.preview);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((resp: any) => {
|
||||
setDataFramePreviewData([]);
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[indexPattern.title, aggs, groupBy, query]
|
||||
);
|
||||
|
||||
if (dataFramePreviewData.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const columnKeys = Object.keys(dataFramePreviewData[0]);
|
||||
columnKeys.sort((a, b) => {
|
||||
// make sure groupBy fields are always most left columns
|
||||
if (groupBy.some(d => d === a) && groupBy.some(d => d === b)) {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
if (groupBy.some(d => d === a)) {
|
||||
return -1;
|
||||
}
|
||||
if (groupBy.some(d => d === b)) {
|
||||
return 1;
|
||||
}
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
const columns = columnKeys.map(k => {
|
||||
return {
|
||||
field: k,
|
||||
name: k,
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
};
|
||||
});
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: columns[0].field,
|
||||
direction: SortDirection.ASC,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiTitle size="xs">
|
||||
<span>
|
||||
{i18n.translate('xpack.ml.dataframe.pivotPreview.dataFramePivotPreviewTitle', {
|
||||
defaultMessage: 'Data Frame Pivot Preview',
|
||||
})}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
{loading && <EuiProgress size="xs" color="accent" />}
|
||||
{!loading && <EuiProgress size="xs" color="accent" max={1} value={0} />}
|
||||
{dataFramePreviewData.length > 0 && (
|
||||
<EuiInMemoryTable
|
||||
items={dataFramePreviewData}
|
||||
columns={columns}
|
||||
pagination={true}
|
||||
sorting={sorting}
|
||||
/>
|
||||
)}
|
||||
</EuiPanel>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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, { Fragment } from 'react';
|
||||
|
||||
import { EuiListGroup, EuiListGroupItem, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
interface ListProps {
|
||||
list: string[];
|
||||
deleteHandler?(l: string): void;
|
||||
}
|
||||
|
||||
export const GroupByList: React.SFC<ListProps> = ({ deleteHandler, list }) => (
|
||||
<EuiListGroup flush={true}>
|
||||
{list.map((fieldName: string) => (
|
||||
<Fragment key={fieldName}>
|
||||
<EuiPanel paddingSize="s">
|
||||
<EuiListGroupItem
|
||||
iconType="string"
|
||||
label={fieldName}
|
||||
extraAction={
|
||||
(deleteHandler && {
|
||||
onClick: () => deleteHandler(fieldName),
|
||||
iconType: 'cross',
|
||||
iconSize: 's',
|
||||
'aria-label': fieldName,
|
||||
alwaysShow: false,
|
||||
}) ||
|
||||
undefined
|
||||
}
|
||||
style={{ padding: 0 }}
|
||||
/>
|
||||
</EuiPanel>
|
||||
{list.length > 0 && <EuiSpacer size="s" />}
|
||||
</Fragment>
|
||||
))}
|
||||
</EuiListGroup>
|
||||
);
|
|
@ -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 { JobCreateForm, getDefaultJobCreateState } from './job_create_form';
|
||||
|
||||
export { JobCreateSummary } from './job_create_summary';
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* 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, { Fragment, SFC, useEffect, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
// Module '"@elastic/eui"' has no exported member 'EuiCard'.
|
||||
// @ts-ignore
|
||||
EuiCard,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
|
||||
export interface JobDetailsExposedState {
|
||||
created: boolean;
|
||||
started: boolean;
|
||||
}
|
||||
|
||||
export function getDefaultJobCreateState(): JobDetailsExposedState {
|
||||
return {
|
||||
created: false,
|
||||
started: false,
|
||||
};
|
||||
}
|
||||
|
||||
function gotToDataFrameJobManagement() {
|
||||
window.location.href = '#/data_frame';
|
||||
}
|
||||
interface Props {
|
||||
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 };
|
||||
|
||||
const [created, setCreated] = useState(defaults.created);
|
||||
const [started, setStarted] = useState(defaults.started);
|
||||
|
||||
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 },
|
||||
})
|
||||
);
|
||||
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);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
})}
|
||||
</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>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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, { SFC } from 'react';
|
||||
|
||||
export const JobCreateSummary: SFC = React.memo(() => {
|
||||
return null;
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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 type JobId = string;
|
||||
export type TargetIndex = string;
|
|
@ -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 { JobDetailsForm, getDefaultJobDetailsState } from './job_details_form';
|
||||
|
||||
export { JobDetailsSummary } from './job_details_summary';
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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, { Fragment, SFC, useEffect, useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiFieldText, EuiFormRow } from '@elastic/eui';
|
||||
|
||||
import { JobId, TargetIndex } from './common';
|
||||
|
||||
export interface JobDetailsExposedState {
|
||||
jobId: JobId;
|
||||
targetIndex: TargetIndex;
|
||||
touched: boolean;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
export function getDefaultJobDetailsState(): JobDetailsExposedState {
|
||||
return {
|
||||
jobId: '',
|
||||
targetIndex: '',
|
||||
touched: false,
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
overrides?: JobDetailsExposedState;
|
||||
onChange(s: JobDetailsExposedState): void;
|
||||
}
|
||||
|
||||
export const JobDetailsForm: SFC<Props> = React.memo(({ overrides = {}, onChange }) => {
|
||||
const defaults = { ...getDefaultJobDetailsState(), ...overrides };
|
||||
|
||||
const [jobId, setJobId] = useState(defaults.jobId);
|
||||
const [targetIndex, setTargetIndex] = useState(defaults.targetIndex);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const valid = jobId !== '' && targetIndex !== '';
|
||||
onChange({ jobId, targetIndex, touched: true, valid });
|
||||
},
|
||||
[jobId, targetIndex]
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.jobDetailsForm.jobIdLabel', {
|
||||
defaultMessage: 'Job id',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText
|
||||
placeholder="job id"
|
||||
value={jobId}
|
||||
onChange={e => setJobId(e.target.value)}
|
||||
aria-label={i18n.translate('xpack.ml.dataframe.jobDetailsForm.jobIdInputAriaLabel', {
|
||||
defaultMessage: 'Choose a unique job id.',
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.jobDetailsForm.targetIndexLabel', {
|
||||
defaultMessage: 'Target index',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText
|
||||
placeholder="target index"
|
||||
value={targetIndex}
|
||||
onChange={e => setTargetIndex(e.target.value)}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.ml.dataframe.jobDetailsForm.targetIndexInputAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Choose a unique target index name.',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</Fragment>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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, { Fragment, SFC } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
|
||||
import { JobId, TargetIndex } from './common';
|
||||
|
||||
interface Props {
|
||||
jobId: JobId;
|
||||
targetIndex: TargetIndex;
|
||||
touched: boolean;
|
||||
}
|
||||
|
||||
export const JobDetailsSummary: SFC<Props> = React.memo(({ jobId, targetIndex, touched }) => {
|
||||
if (touched === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { Dictionary } from '../../../../common/types/common';
|
||||
|
||||
export type EsFieldName = string;
|
||||
|
||||
type EsId = string;
|
||||
type EsDocSource = Dictionary<any>;
|
||||
|
||||
export interface EsDoc extends Dictionary<any> {
|
||||
_id: EsId;
|
||||
_source: EsDocSource;
|
||||
}
|
||||
|
||||
export const MAX_COLUMNS = 5;
|
||||
|
||||
function getFlattenedFields(obj: EsDocSource): EsFieldName[] {
|
||||
const flatDocFields: EsFieldName[] = [];
|
||||
const newDocFields = Object.keys(obj);
|
||||
newDocFields.forEach(f => {
|
||||
const fieldValue = obj[f];
|
||||
if (typeof fieldValue !== 'object' || fieldValue === null || Array.isArray(fieldValue)) {
|
||||
flatDocFields.push(f);
|
||||
} else {
|
||||
const innerFields = getFlattenedFields(fieldValue);
|
||||
const flattenedFields = innerFields.map(d => `${f}.${d}`);
|
||||
flatDocFields.push(...flattenedFields);
|
||||
}
|
||||
});
|
||||
return flatDocFields;
|
||||
}
|
||||
|
||||
export const getSelectableFields = (docs: EsDoc[]): EsFieldName[] => {
|
||||
if (docs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const newDocFields = getFlattenedFields(docs[0]._source);
|
||||
newDocFields.sort();
|
||||
return newDocFields;
|
||||
};
|
||||
|
||||
export const getDefaultSelectableFields = (docs: EsDoc[]): EsFieldName[] => {
|
||||
if (docs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const newDocFields = getFlattenedFields(docs[0]._source);
|
||||
newDocFields.sort();
|
||||
return newDocFields
|
||||
.filter(k => {
|
||||
let value = false;
|
||||
docs.forEach(row => {
|
||||
const source = row._source;
|
||||
if (source[k] !== null) {
|
||||
value = true;
|
||||
}
|
||||
});
|
||||
return value;
|
||||
})
|
||||
.slice(0, MAX_COLUMNS);
|
||||
};
|
||||
|
||||
export const toggleSelectedField = (
|
||||
selectedFields: EsFieldName[],
|
||||
column: EsFieldName
|
||||
): EsFieldName[] => {
|
||||
const index = selectedFields.indexOf(column);
|
||||
if (index === -1) {
|
||||
selectedFields.push(column);
|
||||
} else {
|
||||
selectedFields.splice(index, 1);
|
||||
}
|
||||
selectedFields.sort();
|
||||
return selectedFields;
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { get } from 'lodash';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiBadge, EuiText } from '@elastic/eui';
|
||||
|
||||
import { EsDoc } from './common';
|
||||
|
||||
import { getSelectableFields } from './common';
|
||||
|
||||
interface ExpandedRowProps {
|
||||
item: EsDoc;
|
||||
}
|
||||
|
||||
export const ExpandedRow: React.SFC<ExpandedRowProps> = ({ item }) => {
|
||||
const keys = getSelectableFields([item]);
|
||||
const list = keys.map(k => {
|
||||
const value = get(item._source, k, '');
|
||||
return (
|
||||
<span key={k}>
|
||||
<EuiBadge>{k}:</EuiBadge>
|
||||
<small> {value} </small>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
return <EuiText>{list}</EuiText>;
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { SourceIndexPreview } from './source_index_preview';
|
|
@ -0,0 +1,305 @@
|
|||
/*
|
||||
* 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, { FunctionComponent, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { SearchResponse } from 'elasticsearch';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
EuiCheckbox,
|
||||
EuiEmptyPrompt,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiInMemoryTable,
|
||||
EuiInMemoryTableProps,
|
||||
EuiPanel,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiProgress,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
RIGHT_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
|
||||
// TODO EUI's types for EuiInMemoryTable is missing these props
|
||||
interface ExpandableTableProps extends EuiInMemoryTableProps {
|
||||
itemIdToExpandedRowMap: ItemIdToExpandedRowMap;
|
||||
isExpandable: boolean;
|
||||
}
|
||||
|
||||
const ExpandableTable = (EuiInMemoryTable as any) as FunctionComponent<ExpandableTableProps>;
|
||||
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
|
||||
import { Dictionary } from '../../../../common/types/common';
|
||||
|
||||
import { IndexPatternContext, SimpleQuery } from '../../common';
|
||||
|
||||
import {
|
||||
EsDoc,
|
||||
EsFieldName,
|
||||
getDefaultSelectableFields,
|
||||
getSelectableFields,
|
||||
MAX_COLUMNS,
|
||||
toggleSelectedField,
|
||||
} from './common';
|
||||
import { ExpandedRow } from './expanded_row';
|
||||
|
||||
type ItemIdToExpandedRowMap = Dictionary<JSX.Element>;
|
||||
|
||||
// Defining our own ENUM here.
|
||||
// EUI's SortDirection wasn't usable as a union type
|
||||
// required for the Sorting interface.
|
||||
enum SORT_DIRECTON {
|
||||
ASC = 'asc',
|
||||
DESC = 'desc',
|
||||
}
|
||||
|
||||
interface Sorting {
|
||||
sort: {
|
||||
field: string;
|
||||
direction: SORT_DIRECTON.ASC | SORT_DIRECTON.DESC;
|
||||
};
|
||||
}
|
||||
|
||||
type TableSorting = Sorting | boolean;
|
||||
|
||||
interface Props {
|
||||
query: SimpleQuery;
|
||||
cellClick?(search: string): void;
|
||||
}
|
||||
|
||||
const SEARCH_SIZE = 1000;
|
||||
|
||||
export const SourceIndexPreview: React.SFC<Props> = React.memo(({ cellClick, query }) => {
|
||||
const indexPattern = useContext(IndexPatternContext);
|
||||
|
||||
if (indexPattern === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [tableItems, setTableItems] = useState([] as EsDoc[]);
|
||||
const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]);
|
||||
const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false);
|
||||
|
||||
function toggleColumnsPopover() {
|
||||
setColumnsPopoverVisible(!isColumnsPopoverVisible);
|
||||
}
|
||||
|
||||
function closeColumnsPopover() {
|
||||
setColumnsPopoverVisible(false);
|
||||
}
|
||||
|
||||
function toggleColumn(column: EsFieldName) {
|
||||
// spread to a new array otherwise the component wouldn't re-render
|
||||
setSelectedFields([...toggleSelectedField(selectedFields, column)]);
|
||||
}
|
||||
|
||||
let docFields: EsFieldName[] = [];
|
||||
let docFieldsCount = 0;
|
||||
if (tableItems.length > 0) {
|
||||
docFields = getSelectableFields(tableItems);
|
||||
docFields.sort();
|
||||
docFieldsCount = docFields.length;
|
||||
}
|
||||
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState(
|
||||
{} as ItemIdToExpandedRowMap
|
||||
);
|
||||
|
||||
function toggleDetails(item: EsDoc) {
|
||||
if (itemIdToExpandedRowMap[item._id]) {
|
||||
delete itemIdToExpandedRowMap[item._id];
|
||||
} else {
|
||||
itemIdToExpandedRowMap[item._id] = <ExpandedRow item={item} />;
|
||||
}
|
||||
// spread to a new object otherwise the component wouldn't re-render
|
||||
setItemIdToExpandedRowMap({ ...itemIdToExpandedRowMap });
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
setLoading(true);
|
||||
|
||||
ml.esSearch({
|
||||
index: indexPattern.title,
|
||||
rest_total_hits_as_int: true,
|
||||
size: SEARCH_SIZE,
|
||||
body: { query },
|
||||
})
|
||||
.then((resp: SearchResponse<any>) => {
|
||||
const docs = resp.hits.hits;
|
||||
|
||||
if (selectedFields.length === 0) {
|
||||
const newSelectedFields = getDefaultSelectableFields(docs);
|
||||
setSelectedFields(newSelectedFields);
|
||||
}
|
||||
|
||||
setTableItems(docs as EsDoc[]);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((resp: any) => {
|
||||
setTableItems([] as EsDoc[]);
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[indexPattern.title, query.query_string.query]
|
||||
);
|
||||
|
||||
const columns = selectedFields.map(k => {
|
||||
const column = {
|
||||
field: `_source.${k}`,
|
||||
name: k,
|
||||
render: undefined,
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
} as Dictionary<any>;
|
||||
|
||||
if (cellClick) {
|
||||
column.render = (d: string) => (
|
||||
<EuiButtonEmpty size="xs" onClick={() => cellClick(`${k}:(${d})`)}>
|
||||
{d}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
|
||||
return column;
|
||||
});
|
||||
|
||||
let sorting: TableSorting = false;
|
||||
|
||||
if (columns.length > 0) {
|
||||
sorting = {
|
||||
sort: {
|
||||
field: columns[0].field,
|
||||
direction: SORT_DIRECTON.ASC,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (docFieldsCount > MAX_COLUMNS || docFieldsCount > selectedFields.length) {
|
||||
columns.unshift({
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
render: (item: EsDoc) => (
|
||||
<EuiButtonIcon
|
||||
onClick={() => toggleDetails(item)}
|
||||
aria-label={
|
||||
itemIdToExpandedRowMap[item._id]
|
||||
? i18n.translate('xpack.ml.dataframe.sourceIndexPreview.rowCollapse', {
|
||||
defaultMessage: 'Collapse',
|
||||
})
|
||||
: i18n.translate('xpack.ml.dataframe.sourceIndexPreview.rowExpand', {
|
||||
defaultMessage: 'Expand',
|
||||
})
|
||||
}
|
||||
iconType={itemIdToExpandedRowMap[item._id] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (!loading && tableItems.length === 0) {
|
||||
return (
|
||||
<EuiEmptyPrompt title={<h2>No results</h2>} body={<p>Check the syntax of your query.</p>} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<span>
|
||||
{i18n.translate('xpack.ml.dataframe.sourceIndexPreview.sourceIndexPatternTitle', {
|
||||
defaultMessage: 'Source Index {indexPatternTitle}',
|
||||
values: { indexPatternTitle: indexPattern.title },
|
||||
})}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
{docFieldsCount > MAX_COLUMNS && (
|
||||
<EuiText size="s">
|
||||
{i18n.translate('xpack.ml.dataframe.sourceIndexPreview.fieldSelection', {
|
||||
defaultMessage:
|
||||
'showing {selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}}',
|
||||
values: { selectedFieldsLength: selectedFields.length, docFieldsCount },
|
||||
})}
|
||||
</EuiText>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">
|
||||
<EuiPopover
|
||||
id="popover"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
iconType="gear"
|
||||
onClick={toggleColumnsPopover}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.ml.dataframe.sourceIndexPreview.selectColumnsAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Select columns',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
}
|
||||
isOpen={isColumnsPopoverVisible}
|
||||
closePopover={closeColumnsPopover}
|
||||
ownFocus
|
||||
>
|
||||
<EuiPopoverTitle>
|
||||
{i18n.translate(
|
||||
'xpack.ml.dataframe.sourceIndexPreview.selectFieldsPopoverTitle',
|
||||
{
|
||||
defaultMessage: 'Select fields',
|
||||
}
|
||||
)}
|
||||
</EuiPopoverTitle>
|
||||
<div style={{ maxHeight: '400px', overflowY: 'scroll' }}>
|
||||
{docFields.map(d => (
|
||||
<EuiCheckbox
|
||||
key={d}
|
||||
id={d}
|
||||
label={d}
|
||||
checked={selectedFields.includes(d)}
|
||||
onChange={() => toggleColumn(d)}
|
||||
disabled={selectedFields.includes(d) && selectedFields.length === 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</EuiPopover>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{loading && <EuiProgress size="xs" color="accent" />}
|
||||
{!loading && <EuiProgress size="xs" color="accent" max={1} value={0} />}
|
||||
<ExpandableTable
|
||||
items={tableItems}
|
||||
columns={columns}
|
||||
pagination={true}
|
||||
hasActions={false}
|
||||
isSelectable={false}
|
||||
itemId="_id"
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
isExpandable={true}
|
||||
sorting={sorting}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 * from './wizard_nav';
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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, { SFC } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
interface StepsNavProps {
|
||||
previousActive?: boolean;
|
||||
nextActive?: boolean;
|
||||
previous?(): void;
|
||||
next?(): void;
|
||||
}
|
||||
|
||||
export const WizardNav: SFC<StepsNavProps> = ({
|
||||
previous,
|
||||
previousActive = true,
|
||||
next,
|
||||
nextActive = true,
|
||||
}) => (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem />
|
||||
{previous && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton disabled={!previousActive} onClick={previous} iconType="arrowLeft" size="s">
|
||||
{i18n.translate('xpack.ml.dataframe.wizard.previousStepButton', {
|
||||
defaultMessage: 'Previous',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{next && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton disabled={!nextActive} onClick={next} iconType="arrowRight" size="s">
|
||||
{i18n.translate('xpack.ml.dataframe.wizard.nextStepButton', {
|
||||
defaultMessage: 'Next',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
10
x-pack/plugins/ml/public/data_frame/index.ts
Normal file
10
x-pack/plugins/ml/public/data_frame/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 './pages/job_management/directive';
|
||||
import './pages/job_management/route';
|
||||
import './pages/data_frame_new_pivot/directive';
|
||||
import './pages/data_frame_new_pivot/route';
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 ReactDOM from 'react-dom';
|
||||
|
||||
// @ts-ignore
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
|
||||
// @ts-ignore
|
||||
import { SearchItemsProvider } from '../../../jobs/new_job/utils/new_job_utils';
|
||||
|
||||
import { IndexPatternContext } from '../../common';
|
||||
import { Page } from './page';
|
||||
|
||||
module.directive('mlNewDataFrame', ($route: any, Private: any) => {
|
||||
return {
|
||||
scope: {},
|
||||
restrict: 'E',
|
||||
link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => {
|
||||
const createSearchItems = Private(SearchItemsProvider);
|
||||
const { indexPattern } = createSearchItems();
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nContext>
|
||||
<IndexPatternContext.Provider value={indexPattern}>
|
||||
{React.createElement(Page)}
|
||||
</IndexPatternContext.Provider>
|
||||
</I18nContext>,
|
||||
element[0]
|
||||
);
|
||||
|
||||
element.on('$destroy', () => {
|
||||
ReactDOM.unmountComponentAtNode(element[0]);
|
||||
scope.$destroy();
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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, { SFC } from 'react';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContentBody,
|
||||
EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { Wizard } from './wizard';
|
||||
|
||||
export const Page: SFC = () => (
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageContentHeader>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiTitle>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.transformsWizard.newDataFrameTitle"
|
||||
defaultMessage="New data frame"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageContentHeaderSection>
|
||||
</EuiPageContentHeader>
|
||||
<EuiPageContentBody>
|
||||
<EuiSpacer size="l" />
|
||||
<Wizard />
|
||||
</EuiPageContentBody>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 uiRoutes from 'ui/routes';
|
||||
|
||||
// @ts-ignore
|
||||
import { checkFullLicense } from '../../../license/check_license';
|
||||
// @ts-ignore
|
||||
import { checkGetJobsPrivilege } from '../../../privilege/check_privilege';
|
||||
// @ts-ignore
|
||||
import { loadCurrentIndexPattern } from '../../../util/index_utils';
|
||||
// @ts-ignore
|
||||
import { getDataFrameCreateBreadcrumbs } from '../../breadcrumbs';
|
||||
|
||||
const template = `<ml-nav-menu name="new_data_frame" /><ml-new-data-frame />`;
|
||||
|
||||
uiRoutes.when('/data_frame/new_job/step/pivot?', {
|
||||
template,
|
||||
k7Breadcrumbs: getDataFrameCreateBreadcrumbs,
|
||||
resolve: {
|
||||
CheckLicense: checkFullLicense,
|
||||
privileges: checkGetJobsPrivilege,
|
||||
indexPattern: loadCurrentIndexPattern,
|
||||
},
|
||||
});
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* 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, { Fragment, SFC, useContext, useRef, useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiSteps, EuiStepStatus } from '@elastic/eui';
|
||||
|
||||
import { WizardNav } from '../../components/wizard_nav';
|
||||
|
||||
import {
|
||||
DefinePivotExposedState,
|
||||
DefinePivotForm,
|
||||
DefinePivotSummary,
|
||||
getDefaultPivotState,
|
||||
} from '../../components/define_pivot';
|
||||
|
||||
import {
|
||||
getDefaultJobCreateState,
|
||||
JobCreateForm,
|
||||
JobCreateSummary,
|
||||
} from '../../components/job_create';
|
||||
|
||||
import { getDataFrameRequest } from '../../common';
|
||||
import {
|
||||
getDefaultJobDetailsState,
|
||||
JobDetailsForm,
|
||||
JobDetailsSummary,
|
||||
} from '../../components/job_details';
|
||||
|
||||
import { IndexPatternContext } from '../../common';
|
||||
|
||||
enum WIZARD_STEPS {
|
||||
DEFINE_PIVOT,
|
||||
JOB_DETAILS,
|
||||
JOB_CREATE,
|
||||
}
|
||||
|
||||
interface DefinePivotStepProps {
|
||||
isCurrentStep: boolean;
|
||||
pivotState: DefinePivotExposedState;
|
||||
setCurrentStep: React.Dispatch<React.SetStateAction<WIZARD_STEPS>>;
|
||||
setPivot: React.Dispatch<React.SetStateAction<DefinePivotExposedState>>;
|
||||
}
|
||||
|
||||
const DefinePivotStep: SFC<DefinePivotStepProps> = ({
|
||||
isCurrentStep,
|
||||
pivotState,
|
||||
setCurrentStep,
|
||||
setPivot,
|
||||
}) => {
|
||||
const definePivotRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div ref={definePivotRef} />
|
||||
{isCurrentStep && (
|
||||
<Fragment>
|
||||
<DefinePivotForm onChange={setPivot} overrides={pivotState} />
|
||||
<WizardNav
|
||||
next={() => setCurrentStep(WIZARD_STEPS.JOB_DETAILS)}
|
||||
nextActive={pivotState.valid}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
{!isCurrentStep && <DefinePivotSummary {...pivotState} />}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export const Wizard: SFC = React.memo(() => {
|
||||
// indexPattern from context
|
||||
const indexPattern = useContext(IndexPatternContext);
|
||||
|
||||
if (indexPattern === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The current WIZARD_STEP
|
||||
const [currentStep, setCurrentStep] = useState(WIZARD_STEPS.DEFINE_PIVOT);
|
||||
|
||||
// The DEFINE_PIVOT state
|
||||
const [pivotState, setPivot] = useState(getDefaultPivotState());
|
||||
|
||||
// The JOB_DETAILS state
|
||||
const [jobDetailsState, setJobDetails] = useState(getDefaultJobDetailsState());
|
||||
|
||||
const jobDetails =
|
||||
currentStep === WIZARD_STEPS.JOB_DETAILS ? (
|
||||
<JobDetailsForm onChange={setJobDetails} overrides={jobDetailsState} />
|
||||
) : (
|
||||
<JobDetailsSummary {...jobDetailsState} />
|
||||
);
|
||||
|
||||
// The JOB_CREATE state
|
||||
const [jobCreateState, setJobCreate] = useState(getDefaultJobCreateState);
|
||||
|
||||
const jobCreate =
|
||||
currentStep === WIZARD_STEPS.JOB_CREATE ? (
|
||||
<JobCreateForm
|
||||
jobId={jobDetailsState.jobId}
|
||||
jobConfig={getDataFrameRequest(indexPattern.title, pivotState, jobDetailsState)}
|
||||
onChange={setJobCreate}
|
||||
overrides={jobCreateState}
|
||||
/>
|
||||
) : (
|
||||
<JobCreateSummary />
|
||||
);
|
||||
|
||||
// scroll to the currently selected wizard step
|
||||
/*
|
||||
function scrollToRef() {
|
||||
if (definePivotRef !== null && definePivotRef.current !== null) {
|
||||
// TODO Fix types
|
||||
const dummy = definePivotRef as any;
|
||||
const headerOffset = 70;
|
||||
window.scrollTo(0, dummy.current.offsetTop - headerOffset);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
const stepsConfig = [
|
||||
{
|
||||
title: i18n.translate('xpack.ml.dataframe.transformsWizard.definePivotStepTitle', {
|
||||
defaultMessage: 'Define pivot',
|
||||
}),
|
||||
children: (
|
||||
<DefinePivotStep
|
||||
isCurrentStep={currentStep === WIZARD_STEPS.DEFINE_PIVOT}
|
||||
pivotState={pivotState}
|
||||
setCurrentStep={setCurrentStep}
|
||||
setPivot={setPivot}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.ml.dataframe.transformsWizard.jobDetailsStepTitle', {
|
||||
defaultMessage: 'Job details',
|
||||
}),
|
||||
children: (
|
||||
<Fragment>
|
||||
{jobDetails}
|
||||
{currentStep === WIZARD_STEPS.JOB_DETAILS && (
|
||||
<WizardNav
|
||||
previous={() => {
|
||||
setCurrentStep(WIZARD_STEPS.DEFINE_PIVOT);
|
||||
// scrollToRef();
|
||||
}}
|
||||
next={() => setCurrentStep(WIZARD_STEPS.JOB_CREATE)}
|
||||
nextActive={jobDetailsState.valid}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
),
|
||||
status: currentStep >= WIZARD_STEPS.JOB_DETAILS ? undefined : ('incomplete' as EuiStepStatus),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.ml.dataframe.transformsWizard.createStepTitle', {
|
||||
defaultMessage: 'Create',
|
||||
}),
|
||||
children: (
|
||||
<Fragment>
|
||||
{jobCreate}
|
||||
{currentStep === WIZARD_STEPS.JOB_CREATE && (
|
||||
<WizardNav
|
||||
previous={() => setCurrentStep(WIZARD_STEPS.JOB_DETAILS)}
|
||||
previousActive={!jobCreateState.created}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
),
|
||||
status: currentStep >= WIZARD_STEPS.JOB_CREATE ? undefined : ('incomplete' as EuiStepStatus),
|
||||
},
|
||||
];
|
||||
|
||||
return <EuiSteps steps={stepsConfig} />;
|
||||
});
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* 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, { Fragment, SFC, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiConfirmModal,
|
||||
EuiOverlayMask,
|
||||
EUI_MODAL_CONFIRM_BUTTON,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { DataFrameJobListRow, DATA_FRAME_RUNNING_STATE } from './common';
|
||||
import { deleteJobFactory, startJobFactory, stopJobFactory } from './job_service';
|
||||
|
||||
interface DeleteActionProps {
|
||||
disabled: boolean;
|
||||
item: DataFrameJobListRow;
|
||||
deleteJob(d: DataFrameJobListRow): void;
|
||||
}
|
||||
|
||||
const DeleteAction: SFC<DeleteActionProps> = ({ deleteJob, disabled, item }) => {
|
||||
const [isModalVisible, setModalVisible] = useState(false);
|
||||
|
||||
const closeModal = () => setModalVisible(false);
|
||||
const deleteAndCloseModal = () => {
|
||||
setModalVisible(false);
|
||||
deleteJob(item);
|
||||
};
|
||||
const openModal = () => setModalVisible(true);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
disabled={disabled}
|
||||
iconType="trash"
|
||||
onClick={openModal}
|
||||
aria-label={i18n.translate('xpack.ml.dataframe.jobsList.deleteActionName', {
|
||||
defaultMessage: 'Delete',
|
||||
})}
|
||||
/>
|
||||
{isModalVisible && (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={i18n.translate('xpack.ml.dataframe.jobsList.deleteModalTitle', {
|
||||
defaultMessage: 'Delete {jobId}',
|
||||
values: { jobId: item.config.id },
|
||||
})}
|
||||
onCancel={closeModal}
|
||||
onConfirm={deleteAndCloseModal}
|
||||
cancelButtonText={i18n.translate(
|
||||
'xpack.ml.dataframe.jobsList.deleteModalCancelButton',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
)}
|
||||
confirmButtonText={i18n.translate(
|
||||
'xpack.ml.dataframe.jobsList.deleteModalDeleteButton',
|
||||
{
|
||||
defaultMessage: 'Delete',
|
||||
}
|
||||
)}
|
||||
defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON}
|
||||
buttonColor="danger"
|
||||
>
|
||||
<p>
|
||||
{i18n.translate('xpack.ml.dataframe.jobsList.deleteModalBody', {
|
||||
defaultMessage: 'Are you sure you want to delete this job?',
|
||||
})}
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
</EuiOverlayMask>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export const getActions = (getJobs: () => void) => {
|
||||
const deleteJob = deleteJobFactory(getJobs);
|
||||
const startJob = startJobFactory(getJobs);
|
||||
const stopJob = stopJobFactory(getJobs);
|
||||
|
||||
return [
|
||||
{
|
||||
isPrimary: true,
|
||||
render: (item: DataFrameJobListRow) => {
|
||||
if (
|
||||
item.state.indexer_state !== DATA_FRAME_RUNNING_STATE.STARTED &&
|
||||
item.state.task_state !== DATA_FRAME_RUNNING_STATE.STARTED
|
||||
) {
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
iconType="play"
|
||||
onClick={() => startJob(item)}
|
||||
aria-label={i18n.translate('xpack.ml.dataframe.jobsList.startActionName', {
|
||||
defaultMessage: 'Start',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiButtonEmpty
|
||||
color="danger"
|
||||
iconType="stop"
|
||||
onClick={() => stopJob(item)}
|
||||
aria-label={i18n.translate('xpack.ml.dataframe.jobsList.stopActionName', {
|
||||
defaultMessage: 'Stop',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
render: (item: DataFrameJobListRow) => {
|
||||
return (
|
||||
<DeleteAction
|
||||
deleteJob={deleteJob}
|
||||
disabled={
|
||||
item.state.indexer_state === DATA_FRAME_RUNNING_STATE.STARTED ||
|
||||
item.state.task_state === DATA_FRAME_RUNNING_STATE.STARTED
|
||||
}
|
||||
item={item}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonIcon, RIGHT_ALIGNMENT } from '@elastic/eui';
|
||||
|
||||
import { DataFrameJobListColumn, DataFrameJobListRow, ItemIdToExpandedRowMap } from './common';
|
||||
import { getActions } from './actions';
|
||||
|
||||
export const getColumns = (
|
||||
getJobs: () => void,
|
||||
itemIdToExpandedRowMap: ItemIdToExpandedRowMap,
|
||||
setItemIdToExpandedRowMap: React.Dispatch<React.SetStateAction<ItemIdToExpandedRowMap>>
|
||||
) => {
|
||||
const actions = getActions(getJobs);
|
||||
|
||||
function toggleDetails(item: DataFrameJobListRow) {
|
||||
if (itemIdToExpandedRowMap[item.config.id]) {
|
||||
delete itemIdToExpandedRowMap[item.config.id];
|
||||
} else {
|
||||
itemIdToExpandedRowMap[item.config.id] = <div>EXPAND {item.config.id}</div>;
|
||||
}
|
||||
// spread to a new object otherwise the component wouldn't re-render
|
||||
setItemIdToExpandedRowMap({ ...itemIdToExpandedRowMap });
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
render: (item: DataFrameJobListRow) => (
|
||||
<EuiButtonIcon
|
||||
onClick={() => toggleDetails(item)}
|
||||
aria-label={
|
||||
itemIdToExpandedRowMap[item.config.id]
|
||||
? i18n.translate('xpack.ml.dataframe.jobsList.rowCollapse', {
|
||||
defaultMessage: 'Collapse',
|
||||
})
|
||||
: i18n.translate('xpack.ml.dataframe.jobsList.rowExpand', {
|
||||
defaultMessage: 'Expand',
|
||||
})
|
||||
}
|
||||
iconType={itemIdToExpandedRowMap[item.config.id] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: DataFrameJobListColumn.id,
|
||||
name: 'ID',
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: DataFrameJobListColumn.configSourceIndex,
|
||||
name: i18n.translate('xpack.ml.dataframe.sourceIndex', { defaultMessage: 'Source index' }),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: DataFrameJobListColumn.configDestIndex,
|
||||
name: i18n.translate('xpack.ml.dataframe.targetIndex', { defaultMessage: 'Target index' }),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.dataframe.tableActionLabel', { defaultMessage: 'Actions' }),
|
||||
actions,
|
||||
},
|
||||
];
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { Dictionary } from '../../../../../../common/types/common';
|
||||
|
||||
export type jobId = string;
|
||||
|
||||
export interface DataFrameJob {
|
||||
dest: string;
|
||||
id: jobId;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export enum DATA_FRAME_RUNNING_STATE {
|
||||
STARTED = 'started',
|
||||
STOPPED = 'stopped',
|
||||
}
|
||||
type RunningState = DATA_FRAME_RUNNING_STATE.STARTED | DATA_FRAME_RUNNING_STATE.STOPPED;
|
||||
|
||||
export interface DataFrameJobState {
|
||||
checkpoint: number;
|
||||
current_position: Dictionary<any>;
|
||||
indexer_state: RunningState;
|
||||
task_state: RunningState;
|
||||
}
|
||||
|
||||
export interface DataFrameJobStats {
|
||||
documents_indexed: number;
|
||||
documents_processed: number;
|
||||
index_failures: number;
|
||||
index_time_in_ms: number;
|
||||
index_total: number;
|
||||
pages_processed: number;
|
||||
search_failures: number;
|
||||
search_time_in_ms: number;
|
||||
search_total: number;
|
||||
trigger_count: number;
|
||||
}
|
||||
|
||||
export interface DataFrameJobListRow {
|
||||
state: DataFrameJobState;
|
||||
stats: DataFrameJobStats;
|
||||
config: DataFrameJob;
|
||||
}
|
||||
|
||||
// Used to pass on attribute names to table columns
|
||||
export enum DataFrameJobListColumn {
|
||||
configDestIndex = 'config.dest.index',
|
||||
configSourceIndex = 'config.source.index',
|
||||
id = 'id',
|
||||
}
|
||||
|
||||
export type ItemIdToExpandedRowMap = Dictionary<JSX.Element>;
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { DataFrameJobList } from './job_list';
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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, { FunctionComponent, SFC, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiEmptyPrompt,
|
||||
EuiInMemoryTable,
|
||||
EuiInMemoryTableProps,
|
||||
SortDirection,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { DataFrameJobListColumn, DataFrameJobListRow, ItemIdToExpandedRowMap } from './common';
|
||||
import { getJobsFactory } from './job_service';
|
||||
import { getColumns } from './columns';
|
||||
|
||||
// TODO EUI's types for EuiInMemoryTable is missing these props
|
||||
interface ExpandableTableProps extends EuiInMemoryTableProps {
|
||||
itemIdToExpandedRowMap: ItemIdToExpandedRowMap;
|
||||
isExpandable: boolean;
|
||||
}
|
||||
|
||||
const ExpandableTable = (EuiInMemoryTable as any) as FunctionComponent<ExpandableTableProps>;
|
||||
|
||||
export const DataFrameJobList: SFC = () => {
|
||||
const [dataFrameJobs, setDataFrameJobs] = useState<DataFrameJobListRow[]>([]);
|
||||
const getJobs = getJobsFactory(setDataFrameJobs);
|
||||
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<ItemIdToExpandedRowMap>({});
|
||||
|
||||
// use this pattern so we don't return a promise, useEffects doesn't like that
|
||||
useEffect(() => {
|
||||
getJobs();
|
||||
}, []);
|
||||
|
||||
if (dataFrameJobs.length === 0) {
|
||||
return <EuiEmptyPrompt title={<h2>Here be Data Frame dragons!</h2>} iconType="editorStrike" />;
|
||||
}
|
||||
|
||||
const columns = getColumns(getJobs, itemIdToExpandedRowMap, setItemIdToExpandedRowMap);
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: DataFrameJobListColumn.id,
|
||||
direction: SortDirection.ASC,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ExpandableTable
|
||||
columns={columns}
|
||||
hasActions={false}
|
||||
isExpandable={true}
|
||||
isSelectable={false}
|
||||
items={dataFrameJobs}
|
||||
itemId={DataFrameJobListColumn.id}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
pagination={true}
|
||||
sorting={sorting}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { ml } from '../../../../../../services/ml_api_service';
|
||||
|
||||
import { DataFrameJobListRow } from '../common';
|
||||
|
||||
export const deleteJobFactory = (getJobs: () => void) => async (d: DataFrameJobListRow) => {
|
||||
try {
|
||||
await ml.dataFrame.deleteDataFrameTransformsJob(d.config.id);
|
||||
getJobs();
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.ml.dataframe.jobsList.deleteJobSuccessMessage', {
|
||||
defaultMessage: 'Data frame job {jobId} deleted successfully.',
|
||||
values: { jobId: d.config.id },
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.dataframe.jobsList.deleteJobErrorMessage', {
|
||||
defaultMessage: 'An error occurred deleting the data frame job {jobId}: {error}',
|
||||
values: { jobId: d.config.id, error: JSON.stringify(e) },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { ml } from '../../../../../../services/ml_api_service';
|
||||
import {
|
||||
DataFrameJob,
|
||||
DataFrameJobListRow,
|
||||
DataFrameJobState,
|
||||
DataFrameJobStats,
|
||||
jobId,
|
||||
} from '../common';
|
||||
|
||||
interface DataFrameJobStateStats {
|
||||
id: jobId;
|
||||
state: DataFrameJobState;
|
||||
stats: DataFrameJobStats;
|
||||
}
|
||||
|
||||
interface GetDataFrameTransformsResponse {
|
||||
count: number;
|
||||
transforms: DataFrameJob[];
|
||||
}
|
||||
|
||||
interface GetDataFrameTransformsStatsResponse {
|
||||
count: number;
|
||||
transforms: DataFrameJobStateStats[];
|
||||
}
|
||||
|
||||
export const getJobsFactory = (
|
||||
setDataFrameJobs: React.Dispatch<React.SetStateAction<DataFrameJobListRow[]>>
|
||||
) => async () => {
|
||||
try {
|
||||
const jobConfigs: GetDataFrameTransformsResponse = await ml.dataFrame.getDataFrameTransforms();
|
||||
const jobStats: GetDataFrameTransformsStatsResponse = await ml.dataFrame.getDataFrameTransformsStats();
|
||||
|
||||
const tableRows = jobConfigs.transforms.map(config => {
|
||||
const stats = jobStats.transforms.find(d => config.id === d.id);
|
||||
|
||||
if (stats === undefined) {
|
||||
throw new Error('job stats not available');
|
||||
}
|
||||
|
||||
// table with expandable rows requires `id` on the outer most level
|
||||
return { config, id: config.id, state: stats.state, stats: stats.stats };
|
||||
});
|
||||
|
||||
setDataFrameJobs(tableRows);
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.dataframe.jobsList.errorGettingDataFrameJobsList', {
|
||||
defaultMessage: 'An error occurred getting the data frame jobs list: {error}',
|
||||
values: { error: JSON.stringify(e) },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 { getJobsFactory } from './get_jobs';
|
||||
export { deleteJobFactory } from './delete_job';
|
||||
export { startJobFactory } from './start_job';
|
||||
export { stopJobFactory } from './stop_job';
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { ml } from '../../../../../../services/ml_api_service';
|
||||
|
||||
import { DataFrameJobListRow } from '../common';
|
||||
|
||||
export const startJobFactory = (getJobs: () => void) => async (d: DataFrameJobListRow) => {
|
||||
try {
|
||||
await ml.dataFrame.startDataFrameTransformsJob(d.config.id);
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.ml.dataframe.jobsList.startJobSuccessMessage', {
|
||||
defaultMessage: 'Data frame job {jobId} started successfully.',
|
||||
values: { jobId: d.config.id },
|
||||
})
|
||||
);
|
||||
getJobs();
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.dataframe.jobsList.startJobErrorMessage', {
|
||||
defaultMessage: 'An error occurred starting the data frame job {jobId}: {error}',
|
||||
values: { jobId: d.config.id, error: JSON.stringify(e) },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { ml } from '../../../../../../services/ml_api_service';
|
||||
|
||||
import { DataFrameJobListRow } from '../common';
|
||||
|
||||
export const stopJobFactory = (getJobs: () => void) => async (d: DataFrameJobListRow) => {
|
||||
try {
|
||||
await ml.dataFrame.stopDataFrameTransformsJob(d.config.id);
|
||||
getJobs();
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.ml.dataframe.jobsList.stopJobSuccessMessage', {
|
||||
defaultMessage: 'Data frame job {jobId} stopped successfully.',
|
||||
values: { jobId: d.config.id },
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.dataframe.jobsList.stopJobErrorMessage', {
|
||||
defaultMessage: 'An error occurred stopping the data frame job {jobId}: {error}',
|
||||
values: { jobId: d.config.id, error: JSON.stringify(e) },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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 ReactDOM from 'react-dom';
|
||||
|
||||
// @ts-ignore
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml', ['react']);
|
||||
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import { Page } from './page';
|
||||
|
||||
module.directive('mlDataFramePage', () => {
|
||||
return {
|
||||
scope: {},
|
||||
restrict: 'E',
|
||||
link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => {
|
||||
ReactDOM.render(<I18nContext>{React.createElement(Page)}</I18nContext>, element[0]);
|
||||
|
||||
element.on('$destroy', () => {
|
||||
ReactDOM.unmountComponentAtNode(element[0]);
|
||||
scope.$destroy();
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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, { SFC } from 'react';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContentBody,
|
||||
EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { DataFrameJobList } from './components/job_list';
|
||||
|
||||
function newJob() {
|
||||
window.location.href = `#/data_frame/new_job`;
|
||||
}
|
||||
|
||||
export const Page: SFC = () => (
|
||||
<EuiPage>
|
||||
<EuiPageBody>
|
||||
<EuiPageContentHeader>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiTitle>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.jobsList.dataFrameTitle"
|
||||
defaultMessage="Data frame jobs"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageContentHeaderSection>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiButton fill onClick={newJob} iconType="plusInCircle" size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.jobsList.createDataFrameButton"
|
||||
defaultMessage="Create data frame"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiPageContentHeaderSection>
|
||||
</EuiPageContentHeader>
|
||||
<EuiPageContentBody>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiPanel>
|
||||
<DataFrameJobList />
|
||||
</EuiPanel>
|
||||
</EuiPageContentBody>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 uiRoutes from 'ui/routes';
|
||||
|
||||
// @ts-ignore
|
||||
import { checkFullLicense } from '../../../license/check_license';
|
||||
// @ts-ignore
|
||||
import { checkGetJobsPrivilege } from '../../../privilege/check_privilege';
|
||||
// @ts-ignore
|
||||
import { loadIndexPatterns } from '../../../util/index_utils';
|
||||
// @ts-ignore
|
||||
import { getDataFrameBreadcrumbs } from '../../breadcrumbs';
|
||||
|
||||
const template = `<ml-nav-menu name="data_frame" /><ml-data-frame-page />`;
|
||||
|
||||
uiRoutes.when('/data_frame/?', {
|
||||
template,
|
||||
k7Breadcrumbs: getDataFrameBreadcrumbs,
|
||||
resolve: {
|
||||
CheckLicense: checkFullLicense,
|
||||
privileges: checkGetJobsPrivilege,
|
||||
indexPatterns: loadIndexPatterns,
|
||||
},
|
||||
});
|
|
@ -14,6 +14,7 @@
|
|||
import uiRoutes from 'ui/routes';
|
||||
import { checkLicenseExpired, checkBasicLicense } from 'plugins/ml/license/check_license';
|
||||
import { getCreateJobBreadcrumbs, getDataVisualizerIndexOrSearchBreadcrumbs } from 'plugins/ml/jobs/breadcrumbs';
|
||||
import { getDataFrameIndexOrSearchBreadcrumbs } from 'plugins/ml/data_frame/breadcrumbs';
|
||||
import { preConfiguredJobRedirect } from 'plugins/ml/jobs/new_job/wizard/preconfigured_job_redirect';
|
||||
import { checkCreateJobsPrivilege, checkFindFileStructurePrivilege } from 'plugins/ml/privilege/check_privilege';
|
||||
import { loadIndexPatterns, getIndexPatterns } from 'plugins/ml/util/index_utils';
|
||||
|
@ -28,6 +29,11 @@ uiRoutes
|
|||
redirectTo: '/jobs/new_job/step/index_or_search'
|
||||
});
|
||||
|
||||
uiRoutes
|
||||
.when('/data_frame/new_job', {
|
||||
redirectTo: '/data_frame/new_job/step/index_or_search'
|
||||
});
|
||||
|
||||
uiRoutes
|
||||
.when('/jobs/new_job/step/index_or_search', {
|
||||
template,
|
||||
|
@ -54,6 +60,18 @@ uiRoutes
|
|||
}
|
||||
});
|
||||
|
||||
uiRoutes
|
||||
.when('/data_frame/new_job/step/index_or_search', {
|
||||
template,
|
||||
k7Breadcrumbs: getDataFrameIndexOrSearchBreadcrumbs,
|
||||
resolve: {
|
||||
CheckLicense: checkBasicLicense,
|
||||
privileges: checkFindFileStructurePrivilege,
|
||||
indexPatterns: loadIndexPatterns,
|
||||
nextStepPath: () => '#data_frame/new_job/step/pivot',
|
||||
}
|
||||
});
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 chrome from 'ui/chrome';
|
||||
|
||||
import { http } from '../../services/http_service';
|
||||
|
||||
const basePath = chrome.addBasePath('/api/ml');
|
||||
|
||||
export const dataFrame = {
|
||||
getDataFrameTransforms() {
|
||||
return http({
|
||||
url: `${basePath}/_data_frame/transforms`,
|
||||
method: 'GET'
|
||||
});
|
||||
},
|
||||
getDataFrameTransformsStats() {
|
||||
return http({
|
||||
url: `${basePath}/_data_frame/transforms/_stats`,
|
||||
method: 'GET'
|
||||
});
|
||||
},
|
||||
createDataFrameTransformsJob(jobId, jobConfig) {
|
||||
return http({
|
||||
url: `${basePath}/_data_frame/transforms/${jobId}`,
|
||||
method: 'PUT',
|
||||
data: jobConfig
|
||||
});
|
||||
},
|
||||
deleteDataFrameTransformsJob(jobId) {
|
||||
return http({
|
||||
url: `${basePath}/_data_frame/transforms/${jobId}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
getDataFrameTransformsPreview(obj) {
|
||||
return http({
|
||||
url: `${basePath}/_data_frame/transforms/_preview`,
|
||||
method: 'POST',
|
||||
data: obj
|
||||
});
|
||||
},
|
||||
startDataFrameTransformsJob(jobId) {
|
||||
return http({
|
||||
url: `${basePath}/_data_frame/transforms/${jobId}/_start`,
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
stopDataFrameTransformsJob(jobId) {
|
||||
return http({
|
||||
url: `${basePath}/_data_frame/transforms/${jobId}/_stop`,
|
||||
method: 'POST',
|
||||
});
|
||||
},
|
||||
};
|
|
@ -16,6 +16,17 @@ declare interface Ml {
|
|||
indexAnnotation(annotation: Annotation): Promise<object>;
|
||||
};
|
||||
|
||||
dataFrame: {
|
||||
getDataFrameTransforms(): Promise<any>;
|
||||
getDataFrameTransformsStats(): Promise<any>;
|
||||
createDataFrameTransformsJob(jobId: string, jobConfig: any): Promise<any>;
|
||||
deleteDataFrameTransformsJob(jobId: string): Promise<any>;
|
||||
getDataFrameTransformsPreview(payload: any): Promise<any>;
|
||||
startDataFrameTransformsJob(jobId: string): Promise<any>;
|
||||
stopDataFrameTransformsJob(jobId: string): Promise<any>;
|
||||
};
|
||||
esSearch: any;
|
||||
|
||||
getTimeFieldRange(obj: object): Promise<any>;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import chrome from 'ui/chrome';
|
|||
import { http } from '../../services/http_service';
|
||||
|
||||
import { annotations } from './annotations';
|
||||
import { dataFrame } from './data_frame';
|
||||
import { filters } from './filters';
|
||||
import { results } from './results';
|
||||
import { jobs } from './jobs';
|
||||
|
@ -427,6 +428,7 @@ export const ml = {
|
|||
},
|
||||
|
||||
annotations,
|
||||
dataFrame,
|
||||
filters,
|
||||
results,
|
||||
jobs,
|
||||
|
|
|
@ -105,6 +105,91 @@ export const elasticsearchJsPlugin = (Client, config, components) => {
|
|||
method: 'POST'
|
||||
});
|
||||
|
||||
ml.getDataFrameTransforms = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_data_frame/transforms',
|
||||
}
|
||||
],
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
ml.getDataFrameTransformsStats = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_data_frame/transforms/_stats',
|
||||
}
|
||||
],
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
ml.createDataFrameTransformsJob = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_data_frame/transforms/<%=jobId%>',
|
||||
req: {
|
||||
jobId: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
needBody: true,
|
||||
method: 'PUT'
|
||||
});
|
||||
|
||||
ml.deleteDataFrameTransformsJob = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_data_frame/transforms/<%=jobId%>',
|
||||
req: {
|
||||
jobId: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
ml.getDataFrameTransformsPreview = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_data_frame/transforms/_preview'
|
||||
}
|
||||
],
|
||||
needBody: true,
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
ml.startDataFrameTransformsJob = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_data_frame/transforms/<%=jobId%>/_start',
|
||||
req: {
|
||||
jobId: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
ml.stopDataFrameTransformsJob = ca({
|
||||
urls: [
|
||||
{
|
||||
fmt: '/_data_frame/transforms/<%=jobId%>/_stop',
|
||||
req: {
|
||||
jobId: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
ml.deleteJob = ca({
|
||||
urls: [
|
||||
{
|
||||
|
|
107
x-pack/plugins/ml/server/routes/data_frame.js
Normal file
107
x-pack/plugins/ml/server/routes/data_frame.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 { callWithRequestFactory } from '../client/call_with_request_factory';
|
||||
import { wrapError } from '../client/errors';
|
||||
|
||||
export function dataFrameRoutes(server, commonRouteConfig) {
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/api/ml/_data_frame/transforms',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(server, request);
|
||||
return callWithRequest('ml.getDataFrameTransforms')
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/api/ml/_data_frame/transforms/_stats',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(server, request);
|
||||
return callWithRequest('ml.getDataFrameTransformsStats')
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'PUT',
|
||||
path: '/api/ml/_data_frame/transforms/{jobId}',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(server, request);
|
||||
const { jobId } = request.params;
|
||||
return callWithRequest('ml.createDataFrameTransformsJob', { body: request.payload, jobId })
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'DELETE',
|
||||
path: '/api/ml/_data_frame/transforms/{jobId}',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(server, request);
|
||||
const { jobId } = request.params;
|
||||
return callWithRequest('ml.deleteDataFrameTransformsJob', { jobId })
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: '/api/ml/_data_frame/transforms/_preview',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(server, request);
|
||||
return callWithRequest('ml.getDataFrameTransformsPreview', { body: request.payload })
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: '/api/ml/_data_frame/transforms/{jobId}/_start',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(server, request);
|
||||
const { jobId } = request.params;
|
||||
return callWithRequest('ml.startDataFrameTransformsJob', { jobId })
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'POST',
|
||||
path: '/api/ml/_data_frame/transforms/{jobId}/_stop',
|
||||
handler(request) {
|
||||
const callWithRequest = callWithRequestFactory(server, request);
|
||||
const { jobId } = request.params;
|
||||
return callWithRequest('ml.stopDataFrameTransformsJob', { jobId })
|
||||
.catch(resp => wrapError(resp));
|
||||
},
|
||||
config: {
|
||||
...commonRouteConfig
|
||||
}
|
||||
});
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue