[ML] Data frames: Advanced editor (#39659)

Adds an advanced editor to the data frame transform pivot wizard to allow adding custom group_by and aggregation configurations not supported natively by the UI.
The regular UI and the advanced editor stay in sync. Aggregations not editable by the UI display the configuration instead of an editable form. The aggregation name can still be edited.
This commit is contained in:
Walter Rafelsberger 2019-06-26 17:18:05 +02:00 committed by GitHub
parent e522e757ba
commit cbe91c29fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 593 additions and 171 deletions

View file

@ -8,6 +8,7 @@ export * from './aggregations';
export * from './fields';
export * from './dropdown';
export * from './kibana_context';
export * from './job';
export * from './navigation';
export * from './pivot_aggs';
export * from './pivot_group_by';

View file

@ -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 { PivotAggDict } from './pivot_aggs';
import { PivotGroupByDict } from './pivot_group_by';
export type IndexName = string;
export type IndexPattern = string;
export type JobId = string;
export interface DataFrameJob {
dest: {
index: IndexName;
};
source: {
index: IndexPattern;
};
sync?: object;
}
export interface DataFrameTransform extends DataFrameJob {
pivot: {
aggregations: PivotAggDict;
group_by: PivotGroupByDict;
};
}
export interface DataFrameTransformWithId extends DataFrameTransform {
id: string;
}

View file

@ -54,7 +54,7 @@ export const pivotAggsFieldSupport = {
[KBN_FIELD_TYPES.CONFLICT]: [PIVOT_SUPPORTED_AGGS.VALUE_COUNT],
};
type PivotAgg = {
export type PivotAgg = {
[key in PIVOT_SUPPORTED_AGGS]?: {
field: FieldName;
}
@ -63,11 +63,39 @@ type PivotAgg = {
export type PivotAggDict = { [key in AggName]: PivotAgg };
// The internal representation of an aggregation definition.
export interface PivotAggsConfig {
export interface PivotAggsConfigBase {
agg: PIVOT_SUPPORTED_AGGS;
field: FieldName;
aggName: AggName;
dropDownName: string;
}
export interface PivotAggsConfigWithUiSupport extends PivotAggsConfigBase {
field: FieldName;
}
export function isPivotAggsConfigWithUiSupport(arg: any): arg is PivotAggsConfigWithUiSupport {
return (
arg.hasOwnProperty('agg') &&
arg.hasOwnProperty('aggName') &&
arg.hasOwnProperty('dropDownName') &&
arg.hasOwnProperty('field') &&
pivotSupportedAggs.includes(arg.agg)
);
}
export type PivotAggsConfig = PivotAggsConfigBase | PivotAggsConfigWithUiSupport;
export type PivotAggsConfigWithUiSupportDict = Dictionary<PivotAggsConfigWithUiSupport>;
export type PivotAggsConfigDict = Dictionary<PivotAggsConfig>;
export function getEsAggFromAggConfig(groupByConfig: PivotAggsConfigBase): PivotAgg {
const esAgg = { ...groupByConfig };
delete esAgg.agg;
delete esAgg.aggName;
delete esAgg.dropDownName;
return {
[groupByConfig.agg]: esAgg,
};
}

View file

@ -47,7 +47,7 @@ export const pivotGroupByFieldSupport = {
};
interface GroupByConfigBase {
field: FieldName;
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS;
aggName: AggName;
dropDownName: string;
}
@ -69,31 +69,65 @@ export enum DATE_HISTOGRAM_FORMAT {
interface GroupByDateHistogram extends GroupByConfigBase {
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM;
field: FieldName;
format?: DATE_HISTOGRAM_FORMAT;
calendar_interval: string;
}
interface GroupByHistogram extends GroupByConfigBase {
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM;
field: FieldName;
interval: string;
}
interface GroupByTerms extends GroupByConfigBase {
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS;
field: FieldName;
}
export type GroupByConfigWithInterval = GroupByDateHistogram | GroupByHistogram;
export type PivotGroupByConfig = GroupByDateHistogram | GroupByHistogram | GroupByTerms;
export type GroupByConfigWithUiSupport = GroupByDateHistogram | GroupByHistogram | GroupByTerms;
export type PivotGroupByConfig =
| GroupByConfigBase
| GroupByDateHistogram
| GroupByHistogram
| GroupByTerms;
export type PivotGroupByConfigWithUiSupportDict = Dictionary<GroupByConfigWithUiSupport>;
export type PivotGroupByConfigDict = Dictionary<PivotGroupByConfig>;
export function isGroupByDateHistogram(arg: any): arg is GroupByDateHistogram {
return arg.hasOwnProperty('calendar_interval');
return (
arg.hasOwnProperty('agg') &&
arg.hasOwnProperty('field') &&
arg.hasOwnProperty('calendar_interval') &&
arg.agg === PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM
);
}
export function isGroupByHistogram(arg: any): arg is GroupByHistogram {
return arg.hasOwnProperty('interval');
return (
arg.hasOwnProperty('agg') &&
arg.hasOwnProperty('field') &&
arg.hasOwnProperty('interval') &&
arg.agg === PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM
);
}
export function isGroupByTerms(arg: any): arg is GroupByTerms {
return (
arg.hasOwnProperty('agg') &&
arg.hasOwnProperty('field') &&
arg.agg === PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS
);
}
export function isPivotGroupByConfigWithUiSupport(arg: any): arg is GroupByConfigWithUiSupport {
return isGroupByDateHistogram(arg) || isGroupByHistogram(arg) || isGroupByTerms(arg);
}
export type GenericAgg = object;
export interface TermsAgg {
terms: {
field: FieldName;
@ -115,5 +149,17 @@ export interface DateHistogramAgg {
};
}
type PivotGroupBy = TermsAgg | HistogramAgg | DateHistogramAgg;
export type PivotGroupBy = GenericAgg | TermsAgg | HistogramAgg | DateHistogramAgg;
export type PivotGroupByDict = Dictionary<PivotGroupBy>;
export function getEsAggFromGroupByConfig(groupByConfig: GroupByConfigBase): GenericAgg {
const esAgg = { ...groupByConfig };
delete esAgg.agg;
delete esAgg.aggName;
delete esAgg.dropDownName;
return {
[groupByConfig.agg]: esAgg,
};
}

View file

@ -96,6 +96,7 @@ describe('Data Frame: Common', () => {
const pivotState: DefinePivotExposedState = {
aggList: { 'the-agg-name': agg },
groupByList: { 'the-group-by-name': groupBy },
isAdvancedEditorEnabled: false,
search: 'the-query',
valid: true,
};

View file

@ -13,13 +13,16 @@ import { dictionaryToArray } from '../../../common/types/common';
import { DefinePivotExposedState } from '../components/define_pivot/define_pivot_form';
import { JobDetailsExposedState } from '../components/job_details/job_details_form';
import { PivotGroupByConfig } from '../common';
import {
dateHistogramIntervalFormatRegex,
DATE_HISTOGRAM_FORMAT,
PIVOT_SUPPORTED_GROUP_BY_AGGS,
} from './pivot_group_by';
getEsAggFromAggConfig,
getEsAggFromGroupByConfig,
isGroupByDateHistogram,
isGroupByHistogram,
isGroupByTerms,
PivotGroupByConfig,
} from '../common';
import { dateHistogramIntervalFormatRegex, DATE_HISTOGRAM_FORMAT } from './pivot_group_by';
import { PivotAggDict, PivotAggsConfig } from './pivot_aggs';
import { DateHistogramAgg, HistogramAgg, PivotGroupByDict, TermsAgg } from './pivot_group_by';
@ -97,14 +100,14 @@ export function getDataFramePreviewRequest(
}
groupBy.forEach(g => {
if (g.agg === PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS) {
if (isGroupByTerms(g)) {
const termsAgg: TermsAgg = {
terms: {
field: g.field,
},
};
request.pivot.group_by[g.aggName] = termsAgg;
} else if (g.agg === PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM) {
} else if (isGroupByHistogram(g)) {
const histogramAgg: HistogramAgg = {
histogram: {
field: g.field,
@ -112,7 +115,7 @@ export function getDataFramePreviewRequest(
},
};
request.pivot.group_by[g.aggName] = histogramAgg;
} else if (g.agg === PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM) {
} else if (isGroupByDateHistogram(g)) {
const dateHistogramAgg: DateHistogramAgg = {
date_histogram: {
field: g.field,
@ -135,15 +138,13 @@ export function getDataFramePreviewRequest(
}
}
request.pivot.group_by[g.aggName] = dateHistogramAgg;
} else {
request.pivot.group_by[g.aggName] = getEsAggFromGroupByConfig(g);
}
});
aggs.forEach(agg => {
request.pivot.aggregations[agg.aggName] = {
[agg.agg]: {
field: agg.field,
},
};
request.pivot.aggregations[agg.aggName] = getEsAggFromAggConfig(agg);
});
return request;

View file

@ -13,6 +13,7 @@ exports[`Data Frame: Aggregation <PopoverForm /> Minimal initialization 1`] = `
error={false}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText=""
isInvalid={false}
label="Aggregation name"
labelType="label"

View file

@ -10,14 +10,14 @@ import { i18n } from '@kbn/i18n';
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui';
import { AggName, PivotAggsConfig, PivotAggsConfigDict } from '../../common';
import { AggName, PivotAggsConfig, PivotAggsConfigWithUiSupportDict } from '../../common';
import { PopoverForm } from './popover_form';
interface Props {
item: PivotAggsConfig;
otherAggNames: AggName[];
options: PivotAggsConfigDict;
options: PivotAggsConfigWithUiSupportDict;
deleteHandler(l: AggName): void;
onChange(item: PivotAggsConfig): void;
}

View file

@ -8,13 +8,18 @@ import React, { Fragment } from 'react';
import { EuiPanel, EuiSpacer } from '@elastic/eui';
import { AggName, PivotAggsConfig, PivotAggsConfigDict } from '../../common';
import {
AggName,
PivotAggsConfig,
PivotAggsConfigDict,
PivotAggsConfigWithUiSupportDict,
} from '../../common';
import { AggLabelForm } from './agg_label_form';
export interface ListProps {
list: PivotAggsConfigDict;
options: PivotAggsConfigDict;
options: PivotAggsConfigWithUiSupportDict;
deleteHandler(l: string): void;
onChange(previousAggName: AggName, item: PivotAggsConfig): void;
}

View file

@ -8,15 +8,24 @@ import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui';
import {
EuiButton,
EuiCodeEditor,
EuiFieldText,
EuiForm,
EuiFormRow,
EuiSelect,
} from '@elastic/eui';
import { dictionaryToArray } from '../../../../common/types/common';
import {
AggName,
isAggName,
isPivotAggsConfigWithUiSupport,
getEsAggFromAggConfig,
PivotAggsConfig,
PivotAggsConfigDict,
PivotAggsConfigWithUiSupportDict,
PIVOT_SUPPORTED_AGGS,
} from '../../common';
@ -27,7 +36,7 @@ interface SelectOption {
interface Props {
defaultData: PivotAggsConfig;
otherAggNames: AggName[];
options: PivotAggsConfigDict;
options: PivotAggsConfigWithUiSupportDict;
onChange(d: PivotAggsConfig): void;
}
@ -37,21 +46,30 @@ export const PopoverForm: React.SFC<Props> = ({
onChange,
options,
}) => {
const isUnsupportedAgg = !isPivotAggsConfigWithUiSupport(defaultData);
const [aggName, setAggName] = useState(defaultData.aggName);
const [agg, setAgg] = useState(defaultData.agg);
const [field, setField] = useState(defaultData.field);
const [field, setField] = useState(
isPivotAggsConfigWithUiSupport(defaultData) ? defaultData.field : ''
);
const optionsArr = dictionaryToArray(options);
const availableFields: SelectOption[] = optionsArr
.filter(o => o.agg === defaultData.agg)
.map(o => {
return { text: o.field };
});
const availableAggs: SelectOption[] = optionsArr
.filter(o => o.field === defaultData.field)
.map(o => {
return { text: o.agg };
});
const availableFields: SelectOption[] = [];
const availableAggs: SelectOption[] = [];
if (!isUnsupportedAgg) {
const optionsArr = dictionaryToArray(options);
optionsArr
.filter(o => o.agg === defaultData.agg)
.forEach(o => {
availableFields.push({ text: o.field });
});
optionsArr
.filter(o => isPivotAggsConfigWithUiSupport(defaultData) && o.field === defaultData.field)
.forEach(o => {
availableAggs.push({ text: o.agg });
});
}
let aggNameError = '';
@ -77,6 +95,14 @@ export const PopoverForm: React.SFC<Props> = ({
<EuiFormRow
error={!validAggName && [aggNameError]}
isInvalid={!validAggName}
helpText={
isUnsupportedAgg
? i18n.translate('xpack.ml.dataframe.agg.popoverForm.unsupportedAggregationHelpText', {
defaultMessage:
'Only the aggregation name can be edited in this form. Please use the advanced editor to edit the other parts of the aggregation.',
})
: ''
}
label={i18n.translate('xpack.ml.dataframe.agg.popoverForm.nameLabel', {
defaultMessage: 'Aggregation name',
})}
@ -113,6 +139,18 @@ export const PopoverForm: React.SFC<Props> = ({
/>
</EuiFormRow>
)}
{isUnsupportedAgg && (
<EuiCodeEditor
mode="json"
theme="github"
width="100%"
height="200px"
value={JSON.stringify(getEsAggFromAggConfig(defaultData), null, 2)}
setOptions={{ fontSize: '12px', showLineNumbers: false }}
isReadOnly
aria-label="Read only code editor"
/>
)}
<EuiFormRow hasEmptyLabelSpace>
<EuiButton
isDisabled={!formValid}

View file

@ -39,6 +39,7 @@ exports[`Data Frame: <DefinePivotSummary /> Minimal initialization 1`] = `
},
}
}
isAdvancedEditorEnabled={false}
search="the-query"
valid={true}
/>

View file

@ -15,10 +15,10 @@ import {
DropDownLabel,
DropDownOption,
FieldName,
PivotAggsConfigDict,
GroupByConfigWithUiSupport,
PivotAggsConfigWithUiSupportDict,
pivotAggsFieldSupport,
PivotGroupByConfig,
PivotGroupByConfigDict,
PivotGroupByConfigWithUiSupportDict,
pivotGroupByFieldSupport,
PIVOT_SUPPORTED_GROUP_BY_AGGS,
} from '../../common';
@ -33,7 +33,7 @@ function getDefaultGroupByConfig(
dropDownName: string,
fieldName: FieldName,
groupByAgg: PIVOT_SUPPORTED_GROUP_BY_AGGS
): PivotGroupByConfig {
): GroupByConfigWithUiSupport {
switch (groupByAgg) {
case PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS:
return {
@ -66,11 +66,11 @@ const illegalEsAggNameChars = /[[\]>]/g;
export function getPivotDropdownOptions(indexPattern: IndexPattern) {
// The available group by options
const groupByOptions: EuiComboBoxOptionProps[] = [];
const groupByOptionsData: PivotGroupByConfigDict = {};
const groupByOptionsData: PivotGroupByConfigWithUiSupportDict = {};
// The available aggregations
const aggOptions: EuiComboBoxOptionProps[] = [];
const aggOptionsData: PivotAggsConfigDict = {};
const aggOptionsData: PivotAggsConfigWithUiSupportDict = {};
const ignoreFieldNames = ['_id', '_index', '_type'];
const fields = indexPattern.fields

View file

@ -8,16 +8,24 @@ import React, { ChangeEvent, Fragment, SFC, useContext, useEffect, useState } fr
import { i18n } from '@kbn/i18n';
import { metadata } from 'ui/metadata';
import { toastNotifications } from 'ui/notify';
import {
EuiButton,
EuiCodeEditor,
EuiConfirmModal,
EuiFieldSearch,
EuiFlexGroup,
EuiFlexItem,
EuiForm,
EuiFormHelpText,
EuiFormRow,
EuiLink,
EuiOverlayMask,
EuiPanel,
EuiSpacer,
EuiSwitch,
} from '@elastic/eui';
import { dictionaryToArray } from '../../../../common/types/common';
@ -31,15 +39,18 @@ import {
AggName,
DropDownLabel,
getPivotQuery,
isGroupByDateHistogram,
isGroupByHistogram,
getDataFramePreviewRequest,
isKibanaContext,
KibanaContext,
KibanaContextValue,
PivotAggDict,
PivotAggsConfig,
PivotAggsConfigDict,
PivotGroupByDict,
PivotGroupByConfig,
PivotGroupByConfigDict,
PivotSupportedGroupByAggs,
PIVOT_SUPPORTED_AGGS,
SavedSearchQuery,
} from '../../common';
@ -48,6 +59,7 @@ import { getPivotDropdownOptions } from './common';
export interface DefinePivotExposedState {
aggList: PivotAggsConfigDict;
groupByList: PivotGroupByConfigDict;
isAdvancedEditorEnabled: boolean;
search: string | SavedSearchQuery;
valid: boolean;
}
@ -59,6 +71,7 @@ export function getDefaultPivotState(kibanaContext: KibanaContextValue): DefineP
return {
aggList: {} as PivotAggsConfigDict,
groupByList: {} as PivotGroupByConfigDict,
isAdvancedEditorEnabled: false,
search:
kibanaContext.currentSavedSearch.id !== undefined
? kibanaContext.combinedQuery
@ -268,21 +281,121 @@ export const DefinePivotForm: SFC<Props> = React.memo(({ overrides = {}, onChang
const pivotGroupByArr = dictionaryToArray(groupByList);
const pivotQuery = getPivotQuery(search);
// Advanced editor state
const [isAdvancedEditorSwitchModalVisible, setAdvancedEditorSwitchModalVisible] = useState(false);
const [isAdvancedEditorApplyButtonEnabled, setAdvancedEditorApplyButtonEnabled] = useState(false);
const [isAdvancedEditorEnabled, setAdvancedEditorEnabled] = useState(
defaults.isAdvancedEditorEnabled
);
const previewRequest = getDataFramePreviewRequest(
indexPattern.title,
pivotQuery,
pivotGroupByArr,
pivotAggsArr
);
const stringifiedPivotConfig = JSON.stringify(previewRequest.pivot, null, 2);
const [advancedEditorConfigLastApplied, setAdvancedEditorConfigLastApplied] = useState(
stringifiedPivotConfig
);
const [advancedEditorConfig, setAdvancedEditorConfig] = useState(stringifiedPivotConfig);
const applyAdvancedEditorChanges = () => {
const pivotConfig = JSON.parse(advancedEditorConfig);
const newGroupByList: PivotGroupByConfigDict = {};
if (pivotConfig !== undefined && pivotConfig.group_by !== undefined) {
Object.entries(pivotConfig.group_by).forEach(d => {
const aggName = d[0];
const aggConfig = d[1] as PivotGroupByDict;
const aggConfigKeys = Object.keys(aggConfig);
const agg = aggConfigKeys[0] as PivotSupportedGroupByAggs;
newGroupByList[aggName] = {
agg,
aggName,
dropDownName: '',
...aggConfig[agg],
};
});
}
setGroupByList(newGroupByList);
const newAggList: PivotAggsConfigDict = {};
if (pivotConfig !== undefined && pivotConfig.aggregations !== undefined) {
Object.entries(pivotConfig.aggregations).forEach(d => {
const aggName = d[0];
const aggConfig = d[1] as PivotAggDict;
const aggConfigKeys = Object.keys(aggConfig);
const agg = aggConfigKeys[0] as PIVOT_SUPPORTED_AGGS;
newAggList[aggName] = {
agg,
aggName,
dropDownName: '',
...aggConfig[agg],
};
});
}
setAggList(newAggList);
const prettyPivotConfig = JSON.stringify(pivotConfig, null, 2);
setAdvancedEditorConfig(prettyPivotConfig);
setAdvancedEditorConfigLastApplied(prettyPivotConfig);
setAdvancedEditorApplyButtonEnabled(false);
};
const toggleAdvancedEditor = () => {
setAdvancedEditorConfig(advancedEditorConfig);
setAdvancedEditorEnabled(!isAdvancedEditorEnabled);
setAdvancedEditorApplyButtonEnabled(false);
if (isAdvancedEditorEnabled === false) {
setAdvancedEditorConfigLastApplied(advancedEditorConfig);
}
};
// metadata.branch corresponds to the version used in documentation links.
const docsUrl = `https://www.elastic.co/guide/en/elasticsearch/reference/${
metadata.branch
}/data-frame-transform-pivot.html`;
const advancedEditorHelpText = (
<Fragment>
{i18n.translate('xpack.ml.dataframe.definePivotForm.advancedEditorHelpText', {
defaultMessage:
'The advanced editor allows you to edit the pivot configuration of the data frame transform.',
})}{' '}
<EuiLink href={docsUrl} target="_blank">
{i18n.translate('xpack.ml.dataframe.definePivotForm.advancedEditorHelpTextLink', {
defaultMessage: 'Learn more about available options.',
})}
</EuiLink>
</Fragment>
);
const valid = pivotGroupByArr.length > 0 && pivotAggsArr.length > 0;
useEffect(
() => {
onChange({ aggList, groupByList, search, valid });
const previewRequestUpdate = getDataFramePreviewRequest(
indexPattern.title,
pivotQuery,
pivotGroupByArr,
pivotAggsArr
);
const stringifiedPivotConfigUpdate = JSON.stringify(previewRequestUpdate.pivot, null, 2);
setAdvancedEditorConfig(stringifiedPivotConfigUpdate);
onChange({
aggList,
groupByList,
isAdvancedEditorEnabled,
search,
valid,
});
},
[
pivotAggsArr.map(d => `${d.agg} ${d.field} ${d.aggName}`).join(' '),
pivotGroupByArr
.map(
d =>
`${d.agg} ${d.field} ${isGroupByHistogram(d) ? d.interval : ''} ${
isGroupByDateHistogram(d) ? d.calendar_interval : ''
} ${d.aggName}`
)
.join(' '),
JSON.stringify(pivotAggsArr),
JSON.stringify(pivotGroupByArr),
isAdvancedEditorEnabled,
search,
valid,
]
@ -352,62 +465,196 @@ export const DefinePivotForm: SFC<Props> = React.memo(({ overrides = {}, onChang
</EuiFormRow>
)}
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.definePivotForm.groupByLabel', {
defaultMessage: 'Group by',
})}
>
{!isAdvancedEditorEnabled && (
<Fragment>
<GroupByListForm
list={groupByList}
options={groupByOptionsData}
onChange={updateGroupBy}
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.groupByLabel', {
defaultMessage: 'Group by',
})}
>
<Fragment>
<GroupByListForm
list={groupByList}
options={groupByOptionsData}
onChange={updateGroupBy}
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}
options={aggOptionsData}
onChange={updateAggregation}
deleteHandler={deleteAggregation}
/>
<DropDown
changeHandler={addAggregation}
options={aggOptions}
placeholder={i18n.translate(
'xpack.ml.dataframe.definePivotForm.aggregationsPlaceholder',
{
defaultMessage: 'Add an aggregation ...',
}
)}
/>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.definePivotForm.aggregationsLabel', {
defaultMessage: 'Aggregations',
})}
>
<Fragment>
<AggListForm
list={aggList}
options={aggOptionsData}
onChange={updateAggregation}
deleteHandler={deleteAggregation}
/>
<DropDown
changeHandler={addAggregation}
options={aggOptions}
placeholder={i18n.translate(
'xpack.ml.dataframe.definePivotForm.aggregationsPlaceholder',
{
defaultMessage: 'Add an aggregation ...',
}
)}
/>
</Fragment>
</EuiFormRow>
</Fragment>
)}
{isAdvancedEditorEnabled && (
<Fragment>
<EuiFormRow
label={i18n.translate('xpack.ml.dataframe.definePivotForm.advancedEditorLabel', {
defaultMessage: 'Pivot configuration object',
})}
helpText={advancedEditorHelpText}
>
<EuiPanel grow={false} paddingSize="none">
<EuiCodeEditor
mode="json"
width="100%"
value={advancedEditorConfig}
onChange={(d: string) => {
setAdvancedEditorConfig(d);
// Disable the "Apply"-Button if the config hasn't changed.
if (advancedEditorConfigLastApplied === d) {
setAdvancedEditorApplyButtonEnabled(false);
return;
}
// Try to parse the string passed on from the editor.
// If parsing fails, the "Apply"-Button will be disabled
try {
JSON.parse(d);
setAdvancedEditorApplyButtonEnabled(true);
} catch (e) {
setAdvancedEditorApplyButtonEnabled(false);
}
}}
setOptions={{
fontSize: '12px',
}}
aria-label={i18n.translate(
'xpack.ml.dataframe.definePivotForm.advancedEditorAriaLabel',
{
defaultMessage: 'Advanced editor',
}
)}
/>
</EuiPanel>
</EuiFormRow>
</Fragment>
)}
<EuiFormRow>
<EuiFlexGroup gutterSize="none">
<EuiFlexItem>
<EuiSwitch
label={i18n.translate(
'xpack.ml.dataframe.definePivotForm.advancedEditorSwitchLabel',
{
defaultMessage: 'Advanced editor',
}
)}
checked={isAdvancedEditorEnabled}
onChange={() => {
if (
isAdvancedEditorEnabled &&
(isAdvancedEditorApplyButtonEnabled ||
advancedEditorConfig !== advancedEditorConfigLastApplied)
) {
setAdvancedEditorSwitchModalVisible(true);
return;
}
toggleAdvancedEditor();
}}
/>
{isAdvancedEditorSwitchModalVisible && (
<EuiOverlayMask>
<EuiConfirmModal
title={i18n.translate(
'xpack.ml.dataframe.definePivotForm.advancedEditorSwitchModalTitle',
{
defaultMessage: 'Unapplied changes',
}
)}
onCancel={() => setAdvancedEditorSwitchModalVisible(false)}
onConfirm={() => {
setAdvancedEditorSwitchModalVisible(false);
toggleAdvancedEditor();
}}
cancelButtonText={i18n.translate(
'xpack.ml.dataframe.definePivotForm.advancedEditorSwitchModalCancelButtonText',
{
defaultMessage: 'Cancel',
}
)}
confirmButtonText={i18n.translate(
'xpack.ml.dataframe.definePivotForm.advancedEditorSwitchModalConfirmButtonText',
{
defaultMessage: 'Disable advanced editor',
}
)}
buttonColor="danger"
defaultFocusedButton="confirm"
>
<p>
{i18n.translate(
'xpack.ml.dataframe.definePivotForm.advancedEditorSwitchModalBodyText',
{
defaultMessage: `The changes in the advanced editor haven't been applied yet. By disabling the advanced editor you will lose your edits.`,
}
)}
</p>
</EuiConfirmModal>
</EuiOverlayMask>
)}
</EuiFlexItem>
{isAdvancedEditorEnabled && (
<EuiButton
size="s"
fill
onClick={applyAdvancedEditorChanges}
disabled={!isAdvancedEditorApplyButtonEnabled}
>
{i18n.translate(
'xpack.ml.dataframe.definePivotForm.advancedEditorApplyButtonText',
{
defaultMessage: 'Apply changes',
}
)}
</EuiButton>
)}
</EuiFlexGroup>
</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>
<Fragment>
<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>
</Fragment>
)}
</EuiForm>
</EuiFlexItem>

View file

@ -47,6 +47,7 @@ describe('Data Frame: <DefinePivotSummary />', () => {
const props: DefinePivotExposedState = {
aggList: { 'the-agg-name': agg },
groupByList: { 'the-group-by-name': groupBy },
isAdvancedEditorEnabled: false,
search: 'the-query',
valid: true,
};

View file

@ -15,8 +15,6 @@ import { Dictionary } from '../../../../common/types/common';
import {
DataFramePreviewRequest,
getDataFramePreviewRequest,
isGroupByDateHistogram,
isGroupByHistogram,
PivotAggsConfigDict,
PivotGroupByConfigDict,
PivotQuery,
@ -75,19 +73,7 @@ export const usePivotPreviewData = (
() => {
getDataFramePreviewData();
},
[
indexPattern.title,
aggsArr.map(a => `${a.agg} ${a.field} ${a.aggName}`).join(' '),
groupByArr
.map(
g =>
`${g.agg} ${g.field} ${g.aggName} ${
isGroupByDateHistogram(g) ? g.calendar_interval : ''
} ${isGroupByHistogram(g) ? g.interval : ''}`
)
.join(' '),
JSON.stringify(query),
]
[indexPattern.title, JSON.stringify(aggsArr), JSON.stringify(groupByArr), JSON.stringify(query)]
);
return { errorMessage, status, dataFramePreviewData, previewRequest };
};

View file

@ -13,6 +13,7 @@ exports[`Data Frame: Group By <PopoverForm /> Minimal initialization 1`] = `
error={false}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText=""
isInvalid={false}
label="Group by name"
labelType="label"

View file

@ -15,7 +15,7 @@ import {
isGroupByDateHistogram,
isGroupByHistogram,
PivotGroupByConfig,
PivotGroupByConfigDict,
PivotGroupByConfigWithUiSupportDict,
} from '../../common';
import { PopoverForm } from './popover_form';
@ -23,7 +23,7 @@ import { PopoverForm } from './popover_form';
interface Props {
item: PivotGroupByConfig;
otherAggNames: AggName[];
options: PivotGroupByConfigDict;
options: PivotGroupByConfigWithUiSupportDict;
deleteHandler(l: string): void;
onChange(item: PivotGroupByConfig): void;
}

View file

@ -8,13 +8,18 @@ import React, { Fragment } from 'react';
import { EuiPanel, EuiSpacer } from '@elastic/eui';
import { AggName, PivotGroupByConfig, PivotGroupByConfigDict } from '../../common';
import {
AggName,
PivotGroupByConfig,
PivotGroupByConfigDict,
PivotGroupByConfigWithUiSupportDict,
} from '../../common';
import { GroupByLabelForm } from './group_by_label_form';
interface ListProps {
list: PivotGroupByConfigDict;
options: PivotGroupByConfigDict;
options: PivotGroupByConfigWithUiSupportDict;
deleteHandler(l: string): void;
onChange(id: string, item: PivotGroupByConfig): void;
}

View file

@ -8,19 +8,28 @@ import React, { Fragment, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui';
import {
EuiButton,
EuiCodeEditor,
EuiFieldText,
EuiForm,
EuiFormRow,
EuiSelect,
} from '@elastic/eui';
import { dictionaryToArray } from '../../../../common/types/common';
import {
AggName,
dateHistogramIntervalFormatRegex,
getEsAggFromGroupByConfig,
isGroupByDateHistogram,
isGroupByHistogram,
isPivotGroupByConfigWithUiSupport,
histogramIntervalFormatRegex,
isAggName,
PivotGroupByConfig,
PivotGroupByConfigDict,
PivotGroupByConfigWithUiSupportDict,
PivotSupportedGroupByAggs,
PivotSupportedGroupByAggsWithInterval,
PIVOT_SUPPORTED_GROUP_BY_AGGS,
@ -78,7 +87,7 @@ function getDefaultInterval(defaultData: PivotGroupByConfig): string | undefined
interface Props {
defaultData: PivotGroupByConfig;
otherAggNames: AggName[];
options: PivotGroupByConfigDict;
options: PivotGroupByConfigWithUiSupportDict;
onChange(item: PivotGroupByConfig): void;
}
@ -88,9 +97,13 @@ export const PopoverForm: React.SFC<Props> = ({
onChange,
options,
}) => {
const isUnsupportedAgg = !isPivotGroupByConfigWithUiSupport(defaultData);
const [agg, setAgg] = useState(defaultData.agg);
const [aggName, setAggName] = useState(defaultData.aggName);
const [field, setField] = useState(defaultData.field);
const [field, setField] = useState(
isPivotGroupByConfigWithUiSupport(defaultData) ? defaultData.field : ''
);
const [interval, setInterval] = useState(getDefaultInterval(defaultData));
function getUpdatedItem(): PivotGroupByConfig {
@ -107,17 +120,22 @@ export const PopoverForm: React.SFC<Props> = ({
return updatedItem as PivotGroupByConfig;
}
const optionsArr = dictionaryToArray(options);
const availableFields: SelectOption[] = optionsArr
.filter(o => o.agg === defaultData.agg)
.map(o => {
return { text: o.field };
});
const availableAggs: SelectOption[] = optionsArr
.filter(o => o.field === defaultData.field)
.map(o => {
return { text: o.agg };
});
const availableFields: SelectOption[] = [];
const availableAggs: SelectOption[] = [];
if (!isUnsupportedAgg) {
const optionsArr = dictionaryToArray(options);
optionsArr
.filter(o => o.agg === defaultData.agg)
.forEach(o => {
availableFields.push({ text: o.field });
});
optionsArr
.filter(o => isPivotGroupByConfigWithUiSupport(defaultData) && o.field === defaultData.field)
.forEach(o => {
availableAggs.push({ text: o.agg });
});
}
let aggNameError = '';
@ -156,6 +174,14 @@ export const PopoverForm: React.SFC<Props> = ({
<EuiFormRow
error={!validAggName && [aggNameError]}
isInvalid={!validAggName}
helpText={
isUnsupportedAgg
? i18n.translate('xpack.ml.dataframe.groupBy.popoverForm.unsupportedGroupByHelpText', {
defaultMessage:
'Only the group_by name can be edited in this form. Please use the advanced editor to edit the other parts of the group_by configuration.',
})
: ''
}
label={i18n.translate('xpack.ml.dataframe.groupBy.popoverForm.nameLabel', {
defaultMessage: 'Group by name',
})}
@ -232,6 +258,18 @@ export const PopoverForm: React.SFC<Props> = ({
</Fragment>
</EuiFormRow>
)}
{isUnsupportedAgg && (
<EuiCodeEditor
mode="json"
theme="github"
width="100%"
height="200px"
value={JSON.stringify(getEsAggFromGroupByConfig(defaultData), null, 2)}
setOptions={{ fontSize: '12px', showLineNumbers: false }}
isReadOnly
aria-label="Read only code editor"
/>
)}
<EuiFormRow hasEmptyLabelSpace>
<EuiButton isDisabled={!formValid} onClick={() => onChange(getUpdatedItem())}>
{i18n.translate('xpack.ml.dataframe.groupBy.popoverForm.submitButtonLabel', {

View file

@ -42,6 +42,7 @@ enum WIZARD_STEPS {
interface DefinePivotStepProps {
isCurrentStep: boolean;
jobConfig: any;
pivotState: DefinePivotExposedState;
setCurrentStep: React.Dispatch<React.SetStateAction<WIZARD_STEPS>>;
setPivot: React.Dispatch<React.SetStateAction<DefinePivotExposedState>>;
@ -49,6 +50,7 @@ interface DefinePivotStepProps {
const DefinePivotStep: SFC<DefinePivotStepProps> = ({
isCurrentStep,
jobConfig,
pivotState,
setCurrentStep,
setPivot,
@ -60,7 +62,7 @@ const DefinePivotStep: SFC<DefinePivotStepProps> = ({
<div ref={definePivotRef} />
{isCurrentStep && (
<Fragment>
<DefinePivotForm onChange={setPivot} overrides={pivotState} />
<DefinePivotForm onChange={setPivot} overrides={{ ...pivotState, jobConfig }} />
<WizardNav
next={() => setCurrentStep(WIZARD_STEPS.JOB_DETAILS)}
nextActive={pivotState.valid}
@ -97,6 +99,8 @@ export const Wizard: SFC = React.memo(() => {
<JobDetailsSummary {...jobDetailsState} />
);
const jobConfig = getDataFrameRequest(indexPattern.title, pivotState, jobDetailsState);
// The JOB_CREATE state
const [jobCreateState, setJobCreate] = useState(getDefaultJobCreateState);
@ -105,7 +109,7 @@ export const Wizard: SFC = React.memo(() => {
<JobCreateForm
createIndexPattern={jobDetailsState.createIndexPattern}
jobId={jobDetailsState.jobId}
jobConfig={getDataFrameRequest(indexPattern.title, pivotState, jobDetailsState)}
jobConfig={jobConfig}
onChange={setJobCreate}
overrides={jobCreateState}
/>
@ -133,6 +137,7 @@ export const Wizard: SFC = React.memo(() => {
children: (
<DefinePivotStep
isCurrentStep={currentStep === WIZARD_STEPS.DEFINE_PIVOT}
jobConfig={jobConfig}
pivotState={pivotState}
setCurrentStep={setCurrentStep}
setPivot={setPivot}

View file

@ -16,7 +16,8 @@ import {
RIGHT_ALIGNMENT,
} from '@elastic/eui';
import { DataFrameJobListColumn, DataFrameJobListRow, JobId } from './common';
import { JobId } from '../../../../common';
import { DataFrameJobListColumn, DataFrameJobListRow } from './common';
import { getActions } from './actions';
export const getColumns = (

View file

@ -6,14 +6,7 @@
import { Dictionary } from '../../../../../../common/types/common';
export type JobId = string;
export interface DataFrameJob {
dest: string;
id: JobId;
source: string;
sync?: object;
}
import { JobId, DataFrameTransformWithId } from '../../../../common';
export enum DATA_FRAME_RUNNING_STATE {
STARTED = 'started',
@ -54,7 +47,7 @@ export interface DataFrameJobListRow {
id: JobId;
state: DataFrameJobState;
stats: DataFrameJobStats;
config: DataFrameJob;
config: DataFrameTransformWithId;
}
// Used to pass on attribute names to table columns

View file

@ -14,14 +14,9 @@ import {
SortDirection,
} from '@elastic/eui';
import { moveToDataFrameWizard } from '../../../../common';
import { JobId, moveToDataFrameWizard } from '../../../../common';
import {
DataFrameJobListColumn,
DataFrameJobListRow,
ItemIdToExpandedRowMap,
JobId,
} from './common';
import { DataFrameJobListColumn, DataFrameJobListRow, ItemIdToExpandedRowMap } from './common';
import { getJobsFactory } from './job_service';
import { getColumns } from './columns';
import { ExpandedRow } from './expanded_row';

View file

@ -7,13 +7,8 @@
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';
import { DataFrameTransformWithId, JobId } from '../../../../../common';
import { DataFrameJobListRow, DataFrameJobState, DataFrameJobStats } from '../common';
interface DataFrameJobStateStats {
id: JobId;
@ -23,7 +18,7 @@ interface DataFrameJobStateStats {
interface GetDataFrameTransformsResponse {
count: number;
transforms: DataFrameJob[];
transforms: DataFrameTransformWithId[];
}
interface GetDataFrameTransformsStatsResponse {