mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
- Adds ability to change the name of the group-by or aggregation configuration. Form validation matches the limitations of ES aggregation names. - Adds ability to change aggregation type and/or field where applicable for create aggregations Previously, internally we used separate information to store an ID and a display name for each configuration. Because ES aggregation names support all characters except []>, this is no longer necessary, so the internal attributes formRowLabel and optionsDataId are now merged to aggName. - Fixes an issue with EuiInMemoryTable where changing columns in the source index and pivot preview table would crash Kibana. - Improved handling of adding/updating/deleting items from group-by and agg configs.
This commit is contained in:
parent
d7be854275
commit
492198b5d3
41 changed files with 1123 additions and 495 deletions
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { isAggName } from './aggregations';
|
||||
|
||||
describe('Data Frame: Aggregations', () => {
|
||||
test('isAggName()', () => {
|
||||
expect(isAggName('avg(responsetime)')).toEqual(true);
|
||||
expect(isAggName('avg_responsetime')).toEqual(true);
|
||||
expect(isAggName('avg[responsetime]')).toEqual(false);
|
||||
expect(isAggName('avg<responsetime>')).toEqual(false);
|
||||
expect(isAggName('avg responsetime')).toEqual(true);
|
||||
expect(isAggName(' ')).toEqual(false);
|
||||
expect(isAggName(' avg responsetime')).toEqual(false);
|
||||
expect(isAggName('avg responsetime ')).toEqual(false);
|
||||
expect(isAggName(' avg responsetime ')).toEqual(false);
|
||||
expect(isAggName('date_histogram(@timestamp')).toEqual(true);
|
||||
});
|
||||
});
|
13
x-pack/plugins/ml/public/data_frame/common/aggregations.ts
Normal file
13
x-pack/plugins/ml/public/data_frame/common/aggregations.ts
Normal file
|
@ -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 type AggName = string;
|
||||
export type FieldName = string;
|
||||
|
||||
export function isAggName(arg: any): arg is AggName {
|
||||
// allow all characters except `[]>` and must not start or end with a space.
|
||||
return /^[^\s^\[\]>][^\[\]>]+[^\s^\[\]>]$/.test(arg);
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './aggregations';
|
||||
export * from './dropdown';
|
||||
export * from './index_pattern_context';
|
||||
export * from './pivot_aggs';
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
import { Dictionary } from '../../../common/types/common';
|
||||
|
||||
import { AggName, FieldName } from './aggregations';
|
||||
|
||||
export enum PIVOT_SUPPORTED_AGGS {
|
||||
AVG = 'avg',
|
||||
CARDINALITY = 'cardinality',
|
||||
|
@ -41,12 +43,10 @@ type PivotAgg = {
|
|||
export type PivotAggDict = { [key in AggName]: PivotAgg };
|
||||
|
||||
// The internal representation of an aggregation definition.
|
||||
type AggName = string;
|
||||
type FieldName = string;
|
||||
export interface PivotAggsConfig {
|
||||
agg: PivotAggSupportedAggs;
|
||||
field: FieldName;
|
||||
formRowLabel: AggName;
|
||||
aggName: AggName;
|
||||
}
|
||||
|
||||
export type PivotAggsConfigDict = Dictionary<PivotAggsConfig>;
|
||||
|
|
|
@ -6,17 +6,26 @@
|
|||
|
||||
import { Dictionary } from '../../../common/types/common';
|
||||
|
||||
import { AggName, FieldName } from './aggregations';
|
||||
|
||||
export enum PIVOT_SUPPORTED_GROUP_BY_AGGS {
|
||||
DATE_HISTOGRAM = 'date_histogram',
|
||||
HISTOGRAM = 'histogram',
|
||||
TERMS = 'terms',
|
||||
}
|
||||
|
||||
type FieldName = string;
|
||||
export type PivotSupportedGroupByAggs =
|
||||
| PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM
|
||||
| PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM
|
||||
| PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS;
|
||||
|
||||
export type PivotSupportedGroupByAggsWithInterval =
|
||||
| PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM
|
||||
| PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM;
|
||||
|
||||
interface GroupByConfigBase {
|
||||
field: FieldName;
|
||||
formRowLabel: string;
|
||||
aggName: AggName;
|
||||
}
|
||||
|
||||
// Don't allow an interval of '0', but allow a float interval of '0.1' with a leading zero.
|
||||
|
@ -49,8 +58,8 @@ interface GroupByTerms extends GroupByConfigBase {
|
|||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS;
|
||||
}
|
||||
|
||||
type GroupByConfigWithInterval = GroupByDateHistogram | GroupByHistogram;
|
||||
export type PivotGroupByConfig = GroupByConfigWithInterval | GroupByTerms;
|
||||
export type GroupByConfigWithInterval = GroupByDateHistogram | GroupByHistogram;
|
||||
export type PivotGroupByConfig = GroupByDateHistogram | GroupByHistogram | GroupByTerms;
|
||||
export type PivotGroupByConfigDict = Dictionary<PivotGroupByConfig>;
|
||||
|
||||
export function groupByConfigHasInterval(arg: any): arg is GroupByConfigWithInterval {
|
||||
|
|
|
@ -31,11 +31,11 @@ describe('Data Frame: Common', () => {
|
|||
{
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS,
|
||||
field: 'the-group-by-field',
|
||||
formRowLabel: 'the-group-by-label',
|
||||
aggName: 'the-group-by-label',
|
||||
},
|
||||
];
|
||||
const aggs: PivotAggsConfig[] = [
|
||||
{ agg: PIVOT_SUPPORTED_AGGS.AVG, field: 'the-agg-field', formRowLabel: 'the-agg-label' },
|
||||
{ agg: PIVOT_SUPPORTED_AGGS.AVG, field: 'the-agg-field', aggName: 'the-agg-label' },
|
||||
];
|
||||
const request = getDataFramePreviewRequest('the-index-pattern-title', query, groupBy, aggs);
|
||||
|
||||
|
@ -55,14 +55,15 @@ describe('Data Frame: Common', () => {
|
|||
const groupBy: PivotGroupByConfig = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS,
|
||||
field: 'the-group-by-field',
|
||||
formRowLabel: 'the-group-by-label',
|
||||
aggName: 'the-group-by-label',
|
||||
};
|
||||
const agg: PivotAggsConfig = {
|
||||
agg: PIVOT_SUPPORTED_AGGS.AVG,
|
||||
field: 'the-agg-field',
|
||||
aggName: 'the-agg-label',
|
||||
};
|
||||
const aggs: PivotAggsConfig[] = [
|
||||
{ agg: PIVOT_SUPPORTED_AGGS.AVG, field: 'the-agg-field', formRowLabel: 'the-agg-label' },
|
||||
];
|
||||
const pivotState: DefinePivotExposedState = {
|
||||
aggList: ['the-agg-name'],
|
||||
aggs,
|
||||
aggList: { 'the-agg-name': agg },
|
||||
groupByList: { 'the-group-by-name': groupBy },
|
||||
search: 'the-query',
|
||||
valid: true,
|
||||
|
|
|
@ -85,7 +85,7 @@ export function getDataFramePreviewRequest(
|
|||
field: g.field,
|
||||
},
|
||||
};
|
||||
request.pivot.group_by[g.formRowLabel] = termsAgg;
|
||||
request.pivot.group_by[g.aggName] = termsAgg;
|
||||
} else if (g.agg === PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM) {
|
||||
const histogramAgg: HistogramAgg = {
|
||||
histogram: {
|
||||
|
@ -93,7 +93,7 @@ export function getDataFramePreviewRequest(
|
|||
interval: g.interval,
|
||||
},
|
||||
};
|
||||
request.pivot.group_by[g.formRowLabel] = histogramAgg;
|
||||
request.pivot.group_by[g.aggName] = histogramAgg;
|
||||
} else if (g.agg === PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM) {
|
||||
const dateHistogramAgg: DateHistogramAgg = {
|
||||
date_histogram: {
|
||||
|
@ -116,12 +116,12 @@ export function getDataFramePreviewRequest(
|
|||
dateHistogramAgg.date_histogram.format = format;
|
||||
}
|
||||
}
|
||||
request.pivot.group_by[g.formRowLabel] = dateHistogramAgg;
|
||||
request.pivot.group_by[g.aggName] = dateHistogramAgg;
|
||||
}
|
||||
});
|
||||
|
||||
aggs.forEach(agg => {
|
||||
request.pivot.aggregations[agg.formRowLabel] = {
|
||||
request.pivot.aggregations[agg.aggName] = {
|
||||
[agg.agg]: {
|
||||
field: agg.field,
|
||||
},
|
||||
|
@ -141,7 +141,7 @@ export function getDataFrameRequest(
|
|||
indexPatternTitle,
|
||||
getPivotQuery(pivotState.search),
|
||||
dictionaryToArray(pivotState.groupByList),
|
||||
pivotState.aggs
|
||||
dictionaryToArray(pivotState.aggList)
|
||||
),
|
||||
dest: {
|
||||
index: jobDetailsState.targetIndex,
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Data Frame: <AggLabelForm /> Date histogram aggregation 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
the-group-by-label
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiPopover
|
||||
anchorPosition="downCenter"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label="Edit aggregation"
|
||||
color="primary"
|
||||
iconSize="m"
|
||||
iconType="pencil"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
type="button"
|
||||
/>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
hasArrow={true}
|
||||
id="mlFormPopover"
|
||||
isOpen={false}
|
||||
ownFocus={true}
|
||||
panelPaddingSize="m"
|
||||
>
|
||||
<PopoverForm
|
||||
defaultData={
|
||||
Object {
|
||||
"agg": "cardinality",
|
||||
"aggName": "the-group-by-label",
|
||||
"field": "the-group-by-field",
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
options={Object {}}
|
||||
otherAggNames={Array []}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label="Delete item"
|
||||
color="primary"
|
||||
iconSize="m"
|
||||
iconType="cross"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
type="button"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
|
@ -1,71 +1,32 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Data Frame: <AggListForm /> Minimal initialization 1`] = `
|
||||
<EuiListGroup
|
||||
bordered={false}
|
||||
flush={true}
|
||||
maxWidth={true}
|
||||
showToolTips={false}
|
||||
wrapText={false}
|
||||
>
|
||||
<EuiListGroupItem
|
||||
isActive={false}
|
||||
isDisabled={false}
|
||||
key="the-agg"
|
||||
label={
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Custom name"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
defaultValue="the-form-row-label"
|
||||
fullWidth={false}
|
||||
isLoading={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Aggregation"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
defaultValue="avg"
|
||||
fullWidth={false}
|
||||
isLoading={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
label="Field"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
defaultValue="the-field"
|
||||
fullWidth={false}
|
||||
isLoading={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
showToolTip={false}
|
||||
size="m"
|
||||
<Fragment>
|
||||
<EuiPanel
|
||||
grow={true}
|
||||
hasShadow={false}
|
||||
paddingSize="s"
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<AggLabelForm
|
||||
deleteHandler={[Function]}
|
||||
item={
|
||||
Object {
|
||||
"agg": "avg",
|
||||
"aggName": "the-form-row-label",
|
||||
"field": "the-field",
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
options={Object {}}
|
||||
otherAggNames={Array []}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
</EuiListGroup>
|
||||
</Fragment>
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Data Frame: Aggregation <PopoverForm /> Minimal initialization 1`] = `
|
||||
<EuiForm
|
||||
style={
|
||||
Object {
|
||||
"width": "300px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
error={false}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={false}
|
||||
label="Aggregation name"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
defaultValue="the-agg-name"
|
||||
fullWidth={false}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={true}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill={false}
|
||||
iconSide="left"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
type="button"
|
||||
>
|
||||
Apply
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
`;
|
|
@ -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 { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { AggName, PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from '../../common';
|
||||
|
||||
import { AggLabelForm } from './agg_label_form';
|
||||
|
||||
describe('Data Frame: <AggLabelForm />', () => {
|
||||
test('Date histogram aggregation', () => {
|
||||
const item: PivotAggsConfig = {
|
||||
agg: PIVOT_SUPPORTED_AGGS.CARDINALITY,
|
||||
field: 'the-group-by-field',
|
||||
aggName: 'the-group-by-label',
|
||||
};
|
||||
const props = {
|
||||
item,
|
||||
otherAggNames: [],
|
||||
options: {},
|
||||
deleteHandler(l: AggName) {},
|
||||
onChange() {},
|
||||
};
|
||||
|
||||
const wrapper = shallow(<AggLabelForm {...props} />);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover } from '@elastic/eui';
|
||||
|
||||
import { AggName, PivotAggsConfig, PivotAggsConfigDict } from '../../common';
|
||||
|
||||
import { PopoverForm } from './popover_form';
|
||||
|
||||
interface Props {
|
||||
item: PivotAggsConfig;
|
||||
otherAggNames: AggName[];
|
||||
options: PivotAggsConfigDict;
|
||||
deleteHandler(l: AggName): void;
|
||||
onChange(item: PivotAggsConfig): void;
|
||||
}
|
||||
|
||||
export const AggLabelForm: React.SFC<Props> = ({
|
||||
deleteHandler,
|
||||
item,
|
||||
otherAggNames,
|
||||
onChange,
|
||||
options,
|
||||
}) => {
|
||||
const [isPopoverVisible, setPopoverVisibility] = useState(false);
|
||||
|
||||
function update(updateItem: PivotAggsConfig) {
|
||||
onChange({ ...updateItem });
|
||||
setPopoverVisibility(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem>{item.aggName}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
id="mlFormPopover"
|
||||
ownFocus
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate('xpack.ml.dataframe.aggLabelForm.editAggAriaLabel', {
|
||||
defaultMessage: 'Edit aggregation',
|
||||
})}
|
||||
size="s"
|
||||
iconType="pencil"
|
||||
onClick={() => setPopoverVisibility(!isPopoverVisible)}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverVisible}
|
||||
closePopover={() => setPopoverVisibility(false)}
|
||||
>
|
||||
<PopoverForm
|
||||
defaultData={item}
|
||||
onChange={update}
|
||||
otherAggNames={otherAggNames}
|
||||
options={options}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate('xpack.ml.dataframe.aggLabelForm.deleteItemAriaLabel', {
|
||||
defaultMessage: 'Delete item',
|
||||
})}
|
||||
size="s"
|
||||
iconType="cross"
|
||||
onClick={() => deleteHandler(item.aggName)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -7,21 +7,22 @@
|
|||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { PIVOT_SUPPORTED_AGGS } from '../../common';
|
||||
import { PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from '../../common';
|
||||
|
||||
import { AggListForm, ListProps } from './list_form';
|
||||
|
||||
describe('Data Frame: <AggListForm />', () => {
|
||||
test('Minimal initialization', () => {
|
||||
const item: PivotAggsConfig = {
|
||||
agg: PIVOT_SUPPORTED_AGGS.AVG,
|
||||
field: 'the-field',
|
||||
aggName: 'the-form-row-label',
|
||||
};
|
||||
const props: ListProps = {
|
||||
list: ['the-agg'],
|
||||
optionsData: {
|
||||
'the-agg': {
|
||||
agg: PIVOT_SUPPORTED_AGGS.AVG,
|
||||
field: 'the-field',
|
||||
formRowLabel: 'the-form-row-label',
|
||||
},
|
||||
},
|
||||
list: { 'the-agg': item },
|
||||
options: {},
|
||||
deleteHandler() {},
|
||||
onChange() {},
|
||||
};
|
||||
|
||||
const wrapper = shallow(<AggListForm {...props} />);
|
||||
|
|
|
@ -4,74 +4,46 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import {
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiListGroup,
|
||||
EuiListGroupItem,
|
||||
} from '@elastic/eui';
|
||||
import { AggName, PivotAggsConfig, PivotAggsConfigDict } from '../../common';
|
||||
|
||||
import { PivotAggsConfigDict } from '../../common';
|
||||
import { AggLabelForm } from './agg_label_form';
|
||||
|
||||
export interface ListProps {
|
||||
list: string[];
|
||||
optionsData: PivotAggsConfigDict;
|
||||
deleteHandler?(l: string): void;
|
||||
list: PivotAggsConfigDict;
|
||||
options: PivotAggsConfigDict;
|
||||
deleteHandler(l: string): void;
|
||||
onChange(previousAggName: AggName, item: PivotAggsConfig): 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>
|
||||
);
|
||||
export const AggListForm: React.SFC<ListProps> = ({ deleteHandler, list, onChange, options }) => {
|
||||
const listKeys = Object.keys(list);
|
||||
return (
|
||||
<Fragment>
|
||||
{listKeys.map((aggName: AggName) => {
|
||||
const otherAggNames = listKeys.filter(k => k !== aggName);
|
||||
return (
|
||||
<Fragment key={aggName}>
|
||||
<EuiPanel paddingSize="s">
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<AggLabelForm
|
||||
deleteHandler={deleteHandler}
|
||||
item={list[aggName]}
|
||||
onChange={item => onChange(aggName, item)}
|
||||
otherAggNames={otherAggNames}
|
||||
options={options}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
{listKeys.length > 0 && <EuiSpacer size="s" />}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,26 +8,22 @@ import React from 'react';
|
|||
|
||||
import { EuiForm, EuiFormRow } from '@elastic/eui';
|
||||
|
||||
import { Dictionary } from '../../../../common/types/common';
|
||||
|
||||
interface OptionsDataElement {
|
||||
agg: string;
|
||||
field: string;
|
||||
formRowLabel: string;
|
||||
}
|
||||
import { AggName, PivotAggsConfigDict } from '../../common';
|
||||
|
||||
interface ListProps {
|
||||
list: string[];
|
||||
optionsData: Dictionary<OptionsDataElement>;
|
||||
list: PivotAggsConfigDict;
|
||||
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>
|
||||
);
|
||||
export const AggListSummary: React.SFC<ListProps> = ({ list }) => {
|
||||
const listKeys = Object.keys(list);
|
||||
return (
|
||||
<EuiForm>
|
||||
{listKeys.map((l: AggName) => (
|
||||
<EuiFormRow key={l} label={list[l].aggName}>
|
||||
<span>{l}</span>
|
||||
</EuiFormRow>
|
||||
))}
|
||||
</EuiForm>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { AggName, PIVOT_SUPPORTED_AGGS, PivotAggsConfig } from '../../common';
|
||||
|
||||
import { PopoverForm } from './popover_form';
|
||||
|
||||
describe('Data Frame: Aggregation <PopoverForm />', () => {
|
||||
test('Minimal initialization', () => {
|
||||
const defaultData: PivotAggsConfig = {
|
||||
agg: PIVOT_SUPPORTED_AGGS.CARDINALITY,
|
||||
aggName: 'the-agg-name',
|
||||
field: 'the-field',
|
||||
};
|
||||
const otherAggNames: AggName[] = [];
|
||||
const onChange = (item: PivotAggsConfig) => {};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PopoverForm
|
||||
defaultData={defaultData}
|
||||
otherAggNames={otherAggNames}
|
||||
options={{}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiButton, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
|
||||
import { dictionaryToArray } from '../../../../common/types/common';
|
||||
|
||||
import {
|
||||
AggName,
|
||||
isAggName,
|
||||
PivotAggsConfig,
|
||||
PivotAggsConfigDict,
|
||||
PIVOT_SUPPORTED_AGGS,
|
||||
} from '../../common';
|
||||
|
||||
interface SelectOption {
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
defaultData: PivotAggsConfig;
|
||||
otherAggNames: AggName[];
|
||||
options: PivotAggsConfigDict;
|
||||
onChange(d: PivotAggsConfig): void;
|
||||
}
|
||||
|
||||
export const PopoverForm: React.SFC<Props> = ({
|
||||
defaultData,
|
||||
otherAggNames,
|
||||
onChange,
|
||||
options,
|
||||
}) => {
|
||||
const [aggName, setAggName] = useState(defaultData.aggName);
|
||||
const [agg, setAgg] = useState(defaultData.agg);
|
||||
const [field, setField] = useState(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 };
|
||||
});
|
||||
|
||||
let aggNameError = '';
|
||||
|
||||
let validAggName = isAggName(aggName);
|
||||
if (!validAggName) {
|
||||
aggNameError = i18n.translate('xpack.ml.dataframe.agg.popoverForm.aggNameInvalidCharError', {
|
||||
defaultMessage:
|
||||
'Invalid name. The characters "[", "]", and ">" are not allowed and the name must not start or end with a space character.',
|
||||
});
|
||||
}
|
||||
|
||||
if (validAggName) {
|
||||
validAggName = !otherAggNames.includes(aggName);
|
||||
aggNameError = i18n.translate('xpack.ml.dataframe.agg.popoverForm.aggNameAlreadyUsedError', {
|
||||
defaultMessage: 'Another aggregation already uses that name.',
|
||||
});
|
||||
}
|
||||
|
||||
const formValid = validAggName;
|
||||
|
||||
return (
|
||||
<EuiForm style={{ width: '300px' }}>
|
||||
<EuiFormRow
|
||||
error={!validAggName && [aggNameError]}
|
||||
isInvalid={!validAggName}
|
||||
label={i18n.translate('xpack.ml.dataframe.agg.popoverForm.nameLabel', {
|
||||
defaultMessage: 'Aggregation name',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={aggName}
|
||||
isInvalid={!validAggName}
|
||||
onChange={e => setAggName(e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{availableAggs.length > 0 && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.agg.popoverForm.aggLabel', {
|
||||
defaultMessage: 'Aggregation',
|
||||
})}
|
||||
>
|
||||
<EuiSelect
|
||||
options={availableAggs}
|
||||
value={agg}
|
||||
onChange={e => setAgg(e.target.value as PIVOT_SUPPORTED_AGGS)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{availableFields.length > 0 && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.agg.popoverForm.fieldLabel', {
|
||||
defaultMessage: 'Field',
|
||||
})}
|
||||
>
|
||||
<EuiSelect
|
||||
options={availableFields}
|
||||
value={field}
|
||||
onChange={e => setField(e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<EuiFormRow hasEmptyLabelSpace>
|
||||
<EuiButton
|
||||
isDisabled={!formValid}
|
||||
onClick={() => onChange({ ...defaultData, aggName, agg, field })}
|
||||
>
|
||||
{i18n.translate('xpack.ml.dataframe.agg.popoverForm.submitButtonLabel', {
|
||||
defaultMessage: 'Apply',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
);
|
||||
};
|
|
@ -12,25 +12,20 @@ exports[`Data Frame: <DefinePivotSummary /> Minimal initialization 1`] = `
|
|||
>
|
||||
<DefinePivotSummary
|
||||
aggList={
|
||||
Array [
|
||||
"the-agg-name",
|
||||
]
|
||||
}
|
||||
aggs={
|
||||
Array [
|
||||
Object {
|
||||
Object {
|
||||
"the-agg-name": Object {
|
||||
"agg": "avg",
|
||||
"aggName": "the-agg-label",
|
||||
"field": "the-agg-field",
|
||||
"formRowLabel": "the-agg-label",
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
groupByList={
|
||||
Object {
|
||||
"the-group-by-name": Object {
|
||||
"agg": "terms",
|
||||
"aggName": "the-group-by-label",
|
||||
"field": "the-group-by-field",
|
||||
"formRowLabel": "the-group-by-label",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,20 +12,20 @@ exports[`Data Frame: <PivotPreview /> Minimal initialization 1`] = `
|
|||
>
|
||||
<Component
|
||||
aggs={
|
||||
Array [
|
||||
Object {
|
||||
Object {
|
||||
"the-agg-name": Object {
|
||||
"agg": "avg",
|
||||
"aggName": "the-agg-label",
|
||||
"field": "the-agg-field",
|
||||
"formRowLabel": "the-agg-label",
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
groupBy={
|
||||
Object {
|
||||
"the-group-by-name": Object {
|
||||
"agg": "terms",
|
||||
"aggName": "the-group-by-label",
|
||||
"field": "the-group-by-field",
|
||||
"formRowLabel": "the-group-by-label",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,14 +31,14 @@ describe('Data Frame: Define Pivot Common', () => {
|
|||
},
|
||||
],
|
||||
aggOptionsData: {
|
||||
'avg(the-field)': { agg: 'avg', field: 'the-field', formRowLabel: 'avg_the-field' },
|
||||
'max(the-field)': { agg: 'max', field: 'the-field', formRowLabel: 'max_the-field' },
|
||||
'min(the-field)': { agg: 'min', field: 'the-field', formRowLabel: 'min_the-field' },
|
||||
'sum(the-field)': { agg: 'sum', field: 'the-field', formRowLabel: 'sum_the-field' },
|
||||
'avg(the-field)': { agg: 'avg', field: 'the-field', aggName: 'avg(the-field)' },
|
||||
'max(the-field)': { agg: 'max', field: 'the-field', aggName: 'max(the-field)' },
|
||||
'min(the-field)': { agg: 'min', field: 'the-field', aggName: 'min(the-field)' },
|
||||
'sum(the-field)': { agg: 'sum', field: 'the-field', aggName: 'sum(the-field)' },
|
||||
'value_count(the-field)': {
|
||||
agg: 'value_count',
|
||||
field: 'the-field',
|
||||
formRowLabel: 'value_count_the-field',
|
||||
aggName: 'value_count(the-field)',
|
||||
},
|
||||
},
|
||||
groupByOptions: [{ label: 'histogram(the-field)' }],
|
||||
|
@ -46,7 +46,7 @@ describe('Data Frame: Define Pivot Common', () => {
|
|||
'histogram(the-field)': {
|
||||
agg: 'histogram',
|
||||
field: 'the-field',
|
||||
formRowLabel: 'histogram_the-field',
|
||||
aggName: 'histogram(the-field)',
|
||||
interval: '10',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -41,35 +41,32 @@ export function getPivotDropdownOptions(indexPattern: StaticIndexPattern) {
|
|||
fields.forEach(field => {
|
||||
// group by
|
||||
if (field.type === FIELD_TYPE.STRING) {
|
||||
const label = `${PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS}(${field.name})`;
|
||||
const groupByOption: DropDownLabel = { label };
|
||||
const aggName = `${PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS}(${field.name})`;
|
||||
const groupByOption: DropDownLabel = { label: aggName };
|
||||
groupByOptions.push(groupByOption);
|
||||
const formRowLabel = `${PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS}_${field.name}`;
|
||||
groupByOptionsData[label] = {
|
||||
groupByOptionsData[aggName] = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS,
|
||||
field: field.name,
|
||||
formRowLabel,
|
||||
aggName,
|
||||
};
|
||||
} else if (field.type === FIELD_TYPE.NUMBER) {
|
||||
const label = `${PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM}(${field.name})`;
|
||||
const groupByOption: DropDownLabel = { label };
|
||||
const aggName = `${PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM}(${field.name})`;
|
||||
const groupByOption: DropDownLabel = { label: aggName };
|
||||
groupByOptions.push(groupByOption);
|
||||
const formRowLabel = `${PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM}_${field.name}`;
|
||||
groupByOptionsData[label] = {
|
||||
groupByOptionsData[aggName] = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM,
|
||||
field: field.name,
|
||||
formRowLabel,
|
||||
aggName,
|
||||
interval: '10',
|
||||
};
|
||||
} else if (field.type === FIELD_TYPE.DATE) {
|
||||
const label = `${PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM}(${field.name})`;
|
||||
const groupByOption: DropDownLabel = { label };
|
||||
const aggName = `${PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM}(${field.name})`;
|
||||
const groupByOption: DropDownLabel = { label: aggName };
|
||||
groupByOptions.push(groupByOption);
|
||||
const formRowLabel = `${PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM}_${field.name}`;
|
||||
groupByOptionsData[label] = {
|
||||
groupByOptionsData[aggName] = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM,
|
||||
field: field.name,
|
||||
formRowLabel,
|
||||
aggName,
|
||||
interval: '1m',
|
||||
};
|
||||
}
|
||||
|
@ -82,10 +79,9 @@ export function getPivotDropdownOptions(indexPattern: StaticIndexPattern) {
|
|||
(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})`;
|
||||
aggOption.options.push({ label });
|
||||
const formRowLabel = `${agg}_${field.name}`;
|
||||
aggOptionsData[label] = { agg, field: field.name, formRowLabel };
|
||||
const aggName = `${agg}(${field.name})`;
|
||||
aggOption.options.push({ label: aggName });
|
||||
aggOptionsData[aggName] = { agg, field: field.name, aggName };
|
||||
}
|
||||
});
|
||||
aggOptions.push(aggOption);
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { uniq } from 'lodash';
|
||||
import React, { ChangeEvent, Fragment, SFC, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import React, { ChangeEvent, Fragment, SFC, useContext, useEffect, useState } from 'react';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
|
@ -28,12 +28,13 @@ import { SourceIndexPreview } from '../../components/source_index_preview';
|
|||
import { PivotPreview } from './pivot_preview';
|
||||
|
||||
import {
|
||||
AggName,
|
||||
DropDownLabel,
|
||||
getPivotQuery,
|
||||
groupByConfigHasInterval,
|
||||
IndexPatternContext,
|
||||
Label,
|
||||
PivotAggsConfig,
|
||||
PivotAggsConfigDict,
|
||||
PivotGroupByConfig,
|
||||
PivotGroupByConfigDict,
|
||||
} from '../../common';
|
||||
|
@ -41,8 +42,7 @@ import {
|
|||
import { getPivotDropdownOptions } from './common';
|
||||
|
||||
export interface DefinePivotExposedState {
|
||||
aggList: Label[];
|
||||
aggs: PivotAggsConfig[];
|
||||
aggList: PivotAggsConfigDict;
|
||||
groupByList: PivotGroupByConfigDict;
|
||||
search: string;
|
||||
valid: boolean;
|
||||
|
@ -53,8 +53,7 @@ const emptySearch = '';
|
|||
|
||||
export function getDefaultPivotState(): DefinePivotExposedState {
|
||||
return {
|
||||
aggList: [] as Label[],
|
||||
aggs: [] as PivotAggsConfig[],
|
||||
aggList: {} as PivotAggsConfigDict,
|
||||
groupByList: {} as PivotGroupByConfigDict,
|
||||
search: defaultSearch,
|
||||
valid: false,
|
||||
|
@ -98,52 +97,75 @@ export const DefinePivotForm: SFC<Props> = React.memo(({ overrides = {}, onChang
|
|||
aggOptionsData,
|
||||
} = getPivotDropdownOptions(indexPattern);
|
||||
|
||||
const addGroupByInterval = (label: Label, item: PivotGroupByConfig) => {
|
||||
groupByList[label] = item;
|
||||
setGroupByList({ ...groupByList });
|
||||
};
|
||||
|
||||
const addGroupBy = (d: DropDownLabel[]) => {
|
||||
const label: Label = d[0].label;
|
||||
groupByList[label] = groupByOptionsData[label];
|
||||
const label: AggName = d[0].label;
|
||||
if (groupByList[label] === undefined) {
|
||||
groupByList[label] = groupByOptionsData[label];
|
||||
setGroupByList({ ...groupByList });
|
||||
} else {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.dataframe.definePivot.groupByExistsErrorMessage', {
|
||||
defaultMessage: `A group by configuration with the name '{label}' already exists.`,
|
||||
values: { label },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const updateGroupBy = (previousAggName: AggName, item: PivotGroupByConfig) => {
|
||||
delete groupByList[previousAggName];
|
||||
groupByList[item.aggName] = item;
|
||||
setGroupByList({ ...groupByList });
|
||||
};
|
||||
|
||||
const deleteGroupBy = (label: Label) => {
|
||||
const deleteGroupBy = (label: AggName) => {
|
||||
delete groupByList[label];
|
||||
setGroupByList({ ...groupByList });
|
||||
};
|
||||
|
||||
// The list of selected aggregations
|
||||
const [aggList, setAggList] = useState(defaults.aggList as Label[]);
|
||||
const [aggList, setAggList] = useState(defaults.aggList);
|
||||
|
||||
const addAggregation = (d: DropDownLabel[]) => {
|
||||
const label: Label = d[0].label;
|
||||
const newList = uniq([...aggList, label]);
|
||||
setAggList(newList);
|
||||
const label: AggName = d[0].label;
|
||||
if (aggList[label] === undefined) {
|
||||
aggList[label] = aggOptionsData[label];
|
||||
setAggList({ ...aggList });
|
||||
} else {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.dataframe.definePivot.aggExistsErrorMessage', {
|
||||
defaultMessage: `An aggregation configuration with the name '{label}' already exists.`,
|
||||
values: { label },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteAggregation = (label: Label) => {
|
||||
const newList = aggList.filter(l => l !== label);
|
||||
setAggList(newList);
|
||||
const updateAggregation = (previousAggName: AggName, item: PivotAggsConfig) => {
|
||||
delete aggList[previousAggName];
|
||||
aggList[item.aggName] = item;
|
||||
setAggList({ ...aggList });
|
||||
};
|
||||
|
||||
const pivotAggs = aggList.map(l => aggOptionsData[l]);
|
||||
const deleteAggregation = (label: AggName) => {
|
||||
delete aggList[label];
|
||||
setAggList({ ...aggList });
|
||||
};
|
||||
|
||||
const pivotAggsArr = dictionaryToArray(aggList);
|
||||
const pivotGroupByArr = dictionaryToArray(groupByList);
|
||||
const pivotQuery = getPivotQuery(search);
|
||||
|
||||
const valid = pivotGroupByArr.length > 0 && aggList.length > 0;
|
||||
const valid = pivotGroupByArr.length > 0 && pivotAggsArr.length > 0;
|
||||
useEffect(
|
||||
() => {
|
||||
onChange({ aggList, aggs: pivotAggs, groupByList, search, valid });
|
||||
onChange({ aggList, groupByList, search, valid });
|
||||
},
|
||||
[
|
||||
aggList,
|
||||
pivotAggs.map(d => `${d.agg} ${d.field} ${d.formRowLabel}`).join(' '),
|
||||
pivotAggsArr.map(d => `${d.agg} ${d.field} ${d.aggName}`).join(' '),
|
||||
pivotGroupByArr
|
||||
.map(
|
||||
d =>
|
||||
`${d.agg} ${d.field} ${groupByConfigHasInterval(d) ? d.interval : ''} ${d.formRowLabel}`
|
||||
d => `${d.agg} ${d.field} ${groupByConfigHasInterval(d) ? d.interval : ''} ${d.aggName}`
|
||||
)
|
||||
.join(' '),
|
||||
search,
|
||||
|
@ -179,7 +201,8 @@ export const DefinePivotForm: SFC<Props> = React.memo(({ overrides = {}, onChang
|
|||
<Fragment>
|
||||
<GroupByListForm
|
||||
list={groupByList}
|
||||
onChange={addGroupByInterval}
|
||||
options={groupByOptionsData}
|
||||
onChange={updateGroupBy}
|
||||
deleteHandler={deleteGroupBy}
|
||||
/>
|
||||
<DropDown
|
||||
|
@ -203,7 +226,8 @@ export const DefinePivotForm: SFC<Props> = React.memo(({ overrides = {}, onChang
|
|||
<Fragment>
|
||||
<AggListForm
|
||||
list={aggList}
|
||||
optionsData={aggOptionsData}
|
||||
options={aggOptionsData}
|
||||
onChange={updateAggregation}
|
||||
deleteHandler={deleteAggregation}
|
||||
/>
|
||||
<DropDown
|
||||
|
@ -232,7 +256,7 @@ export const DefinePivotForm: SFC<Props> = React.memo(({ overrides = {}, onChang
|
|||
<EuiFlexItem>
|
||||
<SourceIndexPreview cellClick={addToSearch} query={pivotQuery} />
|
||||
<EuiSpacer size="l" />
|
||||
<PivotPreview aggs={pivotAggs} groupBy={groupByList} query={pivotQuery} />
|
||||
<PivotPreview aggs={aggList} groupBy={groupByList} query={pivotQuery} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -34,14 +34,15 @@ describe('Data Frame: <DefinePivotSummary />', () => {
|
|||
const groupBy: PivotGroupByConfig = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS,
|
||||
field: 'the-group-by-field',
|
||||
formRowLabel: 'the-group-by-label',
|
||||
aggName: 'the-group-by-label',
|
||||
};
|
||||
const agg: PivotAggsConfig = {
|
||||
agg: PIVOT_SUPPORTED_AGGS.AVG,
|
||||
field: 'the-agg-field',
|
||||
aggName: 'the-agg-label',
|
||||
};
|
||||
const aggs: PivotAggsConfig[] = [
|
||||
{ agg: PIVOT_SUPPORTED_AGGS.AVG, field: 'the-agg-field', formRowLabel: 'the-agg-label' },
|
||||
];
|
||||
const props: DefinePivotExposedState = {
|
||||
aggList: ['the-agg-name'],
|
||||
aggs,
|
||||
aggList: { 'the-agg-name': agg },
|
||||
groupByList: { 'the-group-by-name': groupBy },
|
||||
search: 'the-query',
|
||||
valid: true,
|
||||
|
|
|
@ -65,15 +65,13 @@ export const DefinePivotSummary: SFC<DefinePivotExposedState> = ({
|
|||
) {
|
||||
const label = `${agg}(${field.name})`;
|
||||
aggOption.options.push({ label });
|
||||
const formRowLabel = `${agg}_${field.name}`;
|
||||
aggOptionsData[label] = { agg, field: field.name, formRowLabel };
|
||||
const aggName = `${agg}_${field.name}`;
|
||||
aggOptionsData[label] = { agg, field: field.name, aggName };
|
||||
}
|
||||
});
|
||||
aggOptions.push(aggOption);
|
||||
});
|
||||
|
||||
const pivotAggs = aggList.map(l => aggOptionsData[l]);
|
||||
const pivotGroupBy = groupByList;
|
||||
const pivotQuery = getPivotQuery(search);
|
||||
|
||||
const displaySearch = search === defaultSearch ? emptySearch : search;
|
||||
|
@ -103,14 +101,14 @@ export const DefinePivotSummary: SFC<DefinePivotExposedState> = ({
|
|||
defaultMessage: 'Aggregations',
|
||||
})}
|
||||
>
|
||||
<AggListSummary list={aggList} optionsData={aggOptionsData} />
|
||||
<AggListSummary list={aggList} />
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<PivotPreview aggs={pivotAggs} groupBy={pivotGroupBy} query={pivotQuery} />
|
||||
<PivotPreview aggs={aggList} groupBy={groupByList} query={pivotQuery} />
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -34,13 +34,15 @@ describe('Data Frame: <PivotPreview />', () => {
|
|||
const groupBy: PivotGroupByConfig = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS,
|
||||
field: 'the-group-by-field',
|
||||
formRowLabel: 'the-group-by-label',
|
||||
aggName: 'the-group-by-label',
|
||||
};
|
||||
const agg: PivotAggsConfig = {
|
||||
agg: PIVOT_SUPPORTED_AGGS.AVG,
|
||||
field: 'the-agg-field',
|
||||
aggName: 'the-agg-label',
|
||||
};
|
||||
const aggs: PivotAggsConfig[] = [
|
||||
{ agg: PIVOT_SUPPORTED_AGGS.AVG, field: 'the-agg-field', formRowLabel: 'the-agg-label' },
|
||||
];
|
||||
const props = {
|
||||
aggs,
|
||||
aggs: { 'the-agg-name': agg },
|
||||
groupBy: { 'the-group-by-name': groupBy },
|
||||
query: getPivotQuery('the-query'),
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
|
@ -21,13 +21,38 @@ import { dictionaryToArray } from '../../../../common/types/common';
|
|||
|
||||
import {
|
||||
IndexPatternContext,
|
||||
PivotAggsConfig,
|
||||
PivotAggsConfigDict,
|
||||
PivotGroupByConfig,
|
||||
PivotGroupByConfigDict,
|
||||
SimpleQuery,
|
||||
} from '../../common';
|
||||
|
||||
import { PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data';
|
||||
|
||||
function sortColumns(groupByArr: PivotGroupByConfig[]) {
|
||||
return (a: string, b: string) => {
|
||||
// make sure groupBy fields are always most left columns
|
||||
if (groupByArr.some(d => d.aggName === a) && groupByArr.some(d => d.aggName === b)) {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
if (groupByArr.some(d => d.aggName === a)) {
|
||||
return -1;
|
||||
}
|
||||
if (groupByArr.some(d => d.aggName === b)) {
|
||||
return 1;
|
||||
}
|
||||
return a.localeCompare(b);
|
||||
};
|
||||
}
|
||||
|
||||
function usePrevious(value: any) {
|
||||
const ref = useRef(null);
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
const PreviewTitle = () => (
|
||||
<EuiTitle size="xs">
|
||||
<span>
|
||||
|
@ -39,12 +64,14 @@ const PreviewTitle = () => (
|
|||
);
|
||||
|
||||
interface Props {
|
||||
aggs: PivotAggsConfig[];
|
||||
aggs: PivotAggsConfigDict;
|
||||
groupBy: PivotGroupByConfigDict;
|
||||
query: SimpleQuery;
|
||||
}
|
||||
|
||||
export const PivotPreview: React.SFC<Props> = React.memo(({ aggs, groupBy, query }) => {
|
||||
const [clearTable, setClearTable] = useState(false);
|
||||
|
||||
const indexPattern = useContext(IndexPatternContext);
|
||||
|
||||
if (indexPattern === null) {
|
||||
|
@ -58,6 +85,35 @@ export const PivotPreview: React.SFC<Props> = React.memo(({ aggs, groupBy, query
|
|||
groupBy
|
||||
);
|
||||
|
||||
const groupByArr = dictionaryToArray(groupBy);
|
||||
|
||||
// EuiInMemoryTable has an issue with dynamic sortable columns
|
||||
// and will trigger a full page Kibana error in such a case.
|
||||
// The following is a workaround until this is solved upstream:
|
||||
// - If the sortable/columns config changes,
|
||||
// the table will be unmounted/not rendered.
|
||||
// This is what the useEffect() part does.
|
||||
// - After that the table gets re-enabled. To make sure React
|
||||
// doesn't consolidate the state updates, setTimeout is used.
|
||||
const firstColumnName =
|
||||
dataFramePreviewData.length > 0
|
||||
? Object.keys(dataFramePreviewData[0]).sort(sortColumns(groupByArr))[0]
|
||||
: undefined;
|
||||
|
||||
const firstColumnNameChanged = usePrevious(firstColumnName) !== firstColumnName;
|
||||
useEffect(() => {
|
||||
if (firstColumnNameChanged) {
|
||||
setClearTable(true);
|
||||
}
|
||||
if (clearTable) {
|
||||
setTimeout(() => setClearTable(false), 0);
|
||||
}
|
||||
});
|
||||
|
||||
if (firstColumnNameChanged) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status === PIVOT_PREVIEW_STATUS.ERROR) {
|
||||
return (
|
||||
<EuiPanel grow={false}>
|
||||
|
@ -104,21 +160,8 @@ export const PivotPreview: React.SFC<Props> = React.memo(({ aggs, groupBy, query
|
|||
);
|
||||
}
|
||||
|
||||
const groupByArr = dictionaryToArray(groupBy);
|
||||
const columnKeys = Object.keys(dataFramePreviewData[0]);
|
||||
columnKeys.sort((a, b) => {
|
||||
// make sure groupBy fields are always most left columns
|
||||
if (groupByArr.some(d => d.formRowLabel === a) && groupByArr.some(d => d.formRowLabel === b)) {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
if (groupByArr.some(d => d.formRowLabel === a)) {
|
||||
return -1;
|
||||
}
|
||||
if (groupByArr.some(d => d.formRowLabel === b)) {
|
||||
return 1;
|
||||
}
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
columnKeys.sort(sortColumns(groupByArr));
|
||||
|
||||
const columns = columnKeys.map(k => {
|
||||
return {
|
||||
|
@ -143,7 +186,7 @@ export const PivotPreview: React.SFC<Props> = React.memo(({ aggs, groupBy, query
|
|||
{status !== PIVOT_PREVIEW_STATUS.LOADING && (
|
||||
<EuiProgress size="xs" color="accent" max={1} value={0} />
|
||||
)}
|
||||
{dataFramePreviewData.length > 0 && (
|
||||
{dataFramePreviewData.length > 0 && clearTable === false && (
|
||||
<EuiInMemoryTable
|
||||
items={dataFramePreviewData}
|
||||
columns={columns}
|
||||
|
|
|
@ -45,7 +45,7 @@ let pivotPreviewObj: UsePivotPreviewDataReturnType;
|
|||
describe('usePivotPreviewData', () => {
|
||||
test('indexPattern not defined', () => {
|
||||
testHook(() => {
|
||||
pivotPreviewObj = usePivotPreviewData(null, query, [], {});
|
||||
pivotPreviewObj = usePivotPreviewData(null, query, {}, {});
|
||||
});
|
||||
|
||||
expect(pivotPreviewObj.errorMessage).toBe('');
|
||||
|
@ -56,7 +56,7 @@ describe('usePivotPreviewData', () => {
|
|||
|
||||
test('indexPattern set triggers loading', () => {
|
||||
testHook(() => {
|
||||
pivotPreviewObj = usePivotPreviewData({ title: 'lorem', fields: [] }, query, [], {});
|
||||
pivotPreviewObj = usePivotPreviewData({ title: 'lorem', fields: [] }, query, {}, {});
|
||||
});
|
||||
|
||||
expect(pivotPreviewObj.errorMessage).toBe('');
|
||||
|
|
|
@ -13,7 +13,7 @@ import { Dictionary } from '../../../../common/types/common';
|
|||
import {
|
||||
getDataFramePreviewRequest,
|
||||
groupByConfigHasInterval,
|
||||
PivotAggsConfig,
|
||||
PivotAggsConfigDict,
|
||||
PivotGroupByConfigDict,
|
||||
SimpleQuery,
|
||||
} from '../../common';
|
||||
|
@ -35,7 +35,7 @@ export interface UsePivotPreviewDataReturnType {
|
|||
export const usePivotPreviewData = (
|
||||
indexPattern: IndexPatternContextValue,
|
||||
query: SimpleQuery,
|
||||
aggs: PivotAggsConfig[],
|
||||
aggs: PivotAggsConfigDict,
|
||||
groupBy: PivotGroupByConfigDict
|
||||
): UsePivotPreviewDataReturnType => {
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
@ -43,10 +43,11 @@ export const usePivotPreviewData = (
|
|||
const [dataFramePreviewData, setDataFramePreviewData] = useState([]);
|
||||
|
||||
if (indexPattern !== null) {
|
||||
const aggsArr = dictionaryToArray(aggs);
|
||||
const groupByArr = dictionaryToArray(groupBy);
|
||||
|
||||
const getDataFramePreviewData = async () => {
|
||||
if (aggs.length === 0 || groupByArr.length === 0) {
|
||||
if (aggsArr.length === 0 || groupByArr.length === 0) {
|
||||
setDataFramePreviewData([]);
|
||||
return;
|
||||
}
|
||||
|
@ -54,7 +55,7 @@ export const usePivotPreviewData = (
|
|||
setErrorMessage('');
|
||||
setStatus(PIVOT_PREVIEW_STATUS.LOADING);
|
||||
|
||||
const request = getDataFramePreviewRequest(indexPattern.title, query, groupByArr, aggs);
|
||||
const request = getDataFramePreviewRequest(indexPattern.title, query, groupByArr, aggsArr);
|
||||
|
||||
try {
|
||||
const resp: any = await ml.dataFrame.getDataFrameTransformsPreview(request);
|
||||
|
@ -73,13 +74,10 @@ export const usePivotPreviewData = (
|
|||
},
|
||||
[
|
||||
indexPattern.title,
|
||||
aggs.map(a => `${a.agg} ${a.field} ${a.formRowLabel}`).join(' '),
|
||||
aggsArr.map(a => `${a.agg} ${a.field} ${a.aggName}`).join(' '),
|
||||
groupByArr
|
||||
.map(
|
||||
g =>
|
||||
`${g.agg} ${g.field} ${g.formRowLabel} ${
|
||||
groupByConfigHasInterval(g) ? g.interval : ''
|
||||
}`
|
||||
g => `${g.agg} ${g.field} ${g.aggName} ${groupByConfigHasInterval(g) ? g.interval : ''}`
|
||||
)
|
||||
.join(' '),
|
||||
query.query_string.query,
|
||||
|
|
|
@ -12,7 +12,7 @@ exports[`Data Frame: <GroupByLabelForm /> Date histogram aggregation 1`] = `
|
|||
<span
|
||||
className="mlGroupByLabel__text"
|
||||
>
|
||||
the-options-data-id
|
||||
the-group-by-label
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
|
@ -51,9 +51,17 @@ exports[`Data Frame: <GroupByLabelForm /> Date histogram aggregation 1`] = `
|
|||
panelPaddingSize="m"
|
||||
>
|
||||
<PopoverForm
|
||||
defaultInterval="10m"
|
||||
intervalType="date_histogram"
|
||||
defaultData={
|
||||
Object {
|
||||
"agg": "date_histogram",
|
||||
"aggName": "the-group-by-label",
|
||||
"field": "the-group-by-field",
|
||||
"interval": "10m",
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
options={Object {}}
|
||||
otherAggNames={Array []}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
|
@ -86,7 +94,7 @@ exports[`Data Frame: <GroupByLabelForm /> Histogram aggregation 1`] = `
|
|||
<span
|
||||
className="mlGroupByLabel__text"
|
||||
>
|
||||
the-options-data-id
|
||||
the-group-by-label
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
|
@ -125,9 +133,17 @@ exports[`Data Frame: <GroupByLabelForm /> Histogram aggregation 1`] = `
|
|||
panelPaddingSize="m"
|
||||
>
|
||||
<PopoverForm
|
||||
defaultInterval="100"
|
||||
intervalType="histogram"
|
||||
defaultData={
|
||||
Object {
|
||||
"agg": "histogram",
|
||||
"aggName": "the-group-by-label",
|
||||
"field": "the-group-by-field",
|
||||
"interval": "100",
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
options={Object {}}
|
||||
otherAggNames={Array []}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
|
@ -160,9 +176,47 @@ exports[`Data Frame: <GroupByLabelForm /> Terms aggregation 1`] = `
|
|||
<span
|
||||
className="mlGroupByLabel__text"
|
||||
>
|
||||
the-options-data-id
|
||||
the-group-by-label
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="mlGroupByLabel--button"
|
||||
grow={false}
|
||||
>
|
||||
<EuiPopover
|
||||
anchorPosition="downCenter"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label="Edit interval"
|
||||
color="primary"
|
||||
iconSize="m"
|
||||
iconType="pencil"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
type="button"
|
||||
/>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
hasArrow={true}
|
||||
id="mlIntervalFormPopover"
|
||||
isOpen={false}
|
||||
ownFocus={true}
|
||||
panelPaddingSize="m"
|
||||
>
|
||||
<PopoverForm
|
||||
defaultData={
|
||||
Object {
|
||||
"agg": "terms",
|
||||
"aggName": "the-group-by-label",
|
||||
"field": "the-group-by-field",
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
options={Object {}}
|
||||
otherAggNames={Array []}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
className="mlGroupByLabel--button"
|
||||
grow={false}
|
||||
|
|
|
@ -12,12 +12,13 @@ exports[`Data Frame: <GroupByListForm /> Minimal initialization 1`] = `
|
|||
item={
|
||||
Object {
|
||||
"agg": "terms",
|
||||
"aggName": "the-group-by-label",
|
||||
"field": "the-group-by-field",
|
||||
"formRowLabel": "the-group-by-label",
|
||||
}
|
||||
}
|
||||
onChange={[Function]}
|
||||
optionsDataId="the-options-data-id"
|
||||
options={Object {}}
|
||||
otherAggNames={Array []}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer
|
||||
|
|
|
@ -11,8 +11,8 @@ exports[`Data Frame: <GroupByListSummary /> Minimal initialization 1`] = `
|
|||
item={
|
||||
Object {
|
||||
"agg": "terms",
|
||||
"aggName": "the-group-by-label",
|
||||
"field": "the-group-by-field",
|
||||
"formRowLabel": "the-group-by-label",
|
||||
}
|
||||
}
|
||||
optionsDataId="the-options-data-id"
|
||||
|
|
|
@ -1,57 +1,66 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Data Frame: <PopoverForm /> Minimal initialization 1`] = `
|
||||
<EuiForm>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
style={
|
||||
Object {
|
||||
"width": 100,
|
||||
}
|
||||
}
|
||||
exports[`Data Frame: Group By <PopoverForm /> Minimal initialization 1`] = `
|
||||
<EuiForm
|
||||
style={
|
||||
Object {
|
||||
"width": "300px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
error={false}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={false}
|
||||
label="Group by name"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
defaultValue="the-agg-name"
|
||||
fullWidth={false}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
error={false}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={false}
|
||||
label="Interval"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
defaultValue="10m"
|
||||
fullWidth={false}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={true}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill={false}
|
||||
iconSide="left"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
type="button"
|
||||
>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
error={false}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={false}
|
||||
label="Interval"
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldText
|
||||
compressed={false}
|
||||
defaultValue="10m"
|
||||
fullWidth={false}
|
||||
isInvalid={false}
|
||||
isLoading={false}
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiFormRow
|
||||
describedByIds={Array []}
|
||||
fullWidth={false}
|
||||
hasEmptyLabelSpace={true}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
fill={false}
|
||||
iconSide="left"
|
||||
isDisabled={false}
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
type="button"
|
||||
>
|
||||
Apply
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
Apply
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
`;
|
||||
|
|
|
@ -16,12 +16,13 @@ describe('Data Frame: <GroupByLabelForm />', () => {
|
|||
const item: PivotGroupByConfig = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM,
|
||||
field: 'the-group-by-field',
|
||||
formRowLabel: 'the-group-by-label',
|
||||
aggName: 'the-group-by-label',
|
||||
interval: '10m',
|
||||
};
|
||||
const props = {
|
||||
item,
|
||||
optionsDataId: 'the-options-data-id',
|
||||
otherAggNames: [],
|
||||
options: {},
|
||||
deleteHandler() {},
|
||||
onChange() {},
|
||||
};
|
||||
|
@ -35,12 +36,13 @@ describe('Data Frame: <GroupByLabelForm />', () => {
|
|||
const item: PivotGroupByConfig = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM,
|
||||
field: 'the-group-by-field',
|
||||
formRowLabel: 'the-group-by-label',
|
||||
aggName: 'the-group-by-label',
|
||||
interval: '100',
|
||||
};
|
||||
const props = {
|
||||
item,
|
||||
optionsDataId: 'the-options-data-id',
|
||||
otherAggNames: [],
|
||||
options: {},
|
||||
deleteHandler() {},
|
||||
onChange() {},
|
||||
};
|
||||
|
@ -54,11 +56,12 @@ describe('Data Frame: <GroupByLabelForm />', () => {
|
|||
const item: PivotGroupByConfig = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS,
|
||||
field: 'the-group-by-field',
|
||||
formRowLabel: 'the-group-by-label',
|
||||
aggName: 'the-group-by-label',
|
||||
};
|
||||
const props = {
|
||||
item,
|
||||
optionsDataId: 'the-options-data-id',
|
||||
otherAggNames: [],
|
||||
options: {},
|
||||
deleteHandler() {},
|
||||
onChange() {},
|
||||
};
|
||||
|
|
|
@ -4,80 +4,83 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover, EuiTextColor } from '@elastic/eui';
|
||||
|
||||
import { groupByConfigHasInterval, PivotGroupByConfig } from '../../common';
|
||||
import {
|
||||
AggName,
|
||||
groupByConfigHasInterval,
|
||||
PivotGroupByConfig,
|
||||
PivotGroupByConfigDict,
|
||||
} from '../../common';
|
||||
|
||||
import { PopoverForm } from './popover_form';
|
||||
|
||||
interface Props {
|
||||
item: PivotGroupByConfig;
|
||||
optionsDataId: string;
|
||||
otherAggNames: AggName[];
|
||||
options: PivotGroupByConfigDict;
|
||||
deleteHandler(l: string): void;
|
||||
onChange(id: string, item: PivotGroupByConfig): void;
|
||||
onChange(item: PivotGroupByConfig): void;
|
||||
}
|
||||
|
||||
export const GroupByLabelForm: React.SFC<Props> = ({
|
||||
deleteHandler,
|
||||
item,
|
||||
otherAggNames,
|
||||
onChange,
|
||||
optionsDataId,
|
||||
options,
|
||||
}) => {
|
||||
const [isPopoverVisible, setPopoverVisibility] = useState(false);
|
||||
|
||||
function updateInterval(interval: string) {
|
||||
if (groupByConfigHasInterval(item)) {
|
||||
item.interval = interval;
|
||||
onChange(optionsDataId, item);
|
||||
setPopoverVisibility(false);
|
||||
}
|
||||
function update(updateItem: PivotGroupByConfig) {
|
||||
onChange({ ...updateItem });
|
||||
setPopoverVisibility(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem className="mlGroupByLabel--text">
|
||||
<span className="mlGroupByLabel__text">{optionsDataId}</span>
|
||||
<span className="mlGroupByLabel__text">{item.aggName}</span>
|
||||
</EuiFlexItem>
|
||||
{groupByConfigHasInterval(item) && (
|
||||
<Fragment>
|
||||
<EuiFlexItem grow={false} className="mlGroupByLabel--text mlGroupByLabel--interval">
|
||||
<EuiTextColor color="subdued" className="mlGroupByLabel__text">
|
||||
{item.interval}
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} className="mlGroupByLabel--button">
|
||||
<EuiPopover
|
||||
id="mlIntervalFormPopover"
|
||||
ownFocus
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.ml.dataframe.groupByLabelForm.editIntervalAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Edit interval',
|
||||
}
|
||||
)}
|
||||
size="s"
|
||||
iconType="pencil"
|
||||
onClick={() => setPopoverVisibility(!isPopoverVisible)}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverVisible}
|
||||
closePopover={() => setPopoverVisibility(false)}
|
||||
>
|
||||
<PopoverForm
|
||||
defaultInterval={item.interval}
|
||||
intervalType={item.agg}
|
||||
onChange={updateInterval}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</Fragment>
|
||||
<EuiFlexItem grow={false} className="mlGroupByLabel--text mlGroupByLabel--interval">
|
||||
<EuiTextColor color="subdued" className="mlGroupByLabel__text">
|
||||
{item.interval}
|
||||
</EuiTextColor>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false} className="mlGroupByLabel--button">
|
||||
<EuiPopover
|
||||
id="mlIntervalFormPopover"
|
||||
ownFocus
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate(
|
||||
'xpack.ml.dataframe.groupByLabelForm.editIntervalAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Edit interval',
|
||||
}
|
||||
)}
|
||||
size="s"
|
||||
iconType="pencil"
|
||||
onClick={() => setPopoverVisibility(!isPopoverVisible)}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverVisible}
|
||||
closePopover={() => setPopoverVisibility(false)}
|
||||
>
|
||||
<PopoverForm
|
||||
defaultData={item}
|
||||
onChange={update}
|
||||
otherAggNames={otherAggNames}
|
||||
options={options}
|
||||
/>
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} className="mlGroupByLabel--button">
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate('xpack.ml.dataframe.groupByLabelForm.deleteItemAriaLabel', {
|
||||
|
@ -85,7 +88,7 @@ export const GroupByLabelForm: React.SFC<Props> = ({
|
|||
})}
|
||||
size="s"
|
||||
iconType="cross"
|
||||
onClick={() => deleteHandler(optionsDataId)}
|
||||
onClick={() => deleteHandler(item.aggName)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -16,7 +16,7 @@ describe('Data Frame: <GroupByLabelSummary />', () => {
|
|||
const item: PivotGroupByConfig = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM,
|
||||
field: 'the-group-by-field',
|
||||
formRowLabel: 'the-group-by-label',
|
||||
aggName: 'the-group-by-label',
|
||||
interval: '10m',
|
||||
};
|
||||
const props = {
|
||||
|
@ -33,7 +33,7 @@ describe('Data Frame: <GroupByLabelSummary />', () => {
|
|||
const item: PivotGroupByConfig = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM,
|
||||
field: 'the-group-by-field',
|
||||
formRowLabel: 'the-group-by-label',
|
||||
aggName: 'the-group-by-label',
|
||||
interval: '100',
|
||||
};
|
||||
const props = {
|
||||
|
@ -50,7 +50,7 @@ describe('Data Frame: <GroupByLabelSummary />', () => {
|
|||
const item: PivotGroupByConfig = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS,
|
||||
field: 'the-group-by-field',
|
||||
formRowLabel: 'the-group-by-label',
|
||||
aggName: 'the-group-by-label',
|
||||
};
|
||||
const props = {
|
||||
item,
|
||||
|
|
|
@ -16,10 +16,11 @@ describe('Data Frame: <GroupByListForm />', () => {
|
|||
const item: PivotGroupByConfig = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS,
|
||||
field: 'the-group-by-field',
|
||||
formRowLabel: 'the-group-by-label',
|
||||
aggName: 'the-group-by-label',
|
||||
};
|
||||
const props = {
|
||||
list: { 'the-options-data-id': item },
|
||||
options: {},
|
||||
deleteHandler() {},
|
||||
onChange() {},
|
||||
};
|
||||
|
|
|
@ -8,29 +8,37 @@ import React, { Fragment } from 'react';
|
|||
|
||||
import { EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import { PivotGroupByConfig, PivotGroupByConfigDict } from '../../common';
|
||||
import { AggName, PivotGroupByConfig, PivotGroupByConfigDict } from '../../common';
|
||||
|
||||
import { GroupByLabelForm } from './group_by_label_form';
|
||||
|
||||
interface ListProps {
|
||||
list: PivotGroupByConfigDict;
|
||||
options: PivotGroupByConfigDict;
|
||||
deleteHandler(l: string): void;
|
||||
onChange(id: string, item: PivotGroupByConfig): void;
|
||||
}
|
||||
|
||||
export const GroupByListForm: React.SFC<ListProps> = ({ deleteHandler, list, onChange }) => {
|
||||
export const GroupByListForm: React.SFC<ListProps> = ({
|
||||
deleteHandler,
|
||||
list,
|
||||
onChange,
|
||||
options,
|
||||
}) => {
|
||||
const listKeys = Object.keys(list);
|
||||
return (
|
||||
<Fragment>
|
||||
{listKeys.map((optionsDataId: string) => {
|
||||
{listKeys.map((aggName: AggName) => {
|
||||
const otherAggNames = listKeys.filter(k => k !== aggName);
|
||||
return (
|
||||
<Fragment key={optionsDataId}>
|
||||
<Fragment key={aggName}>
|
||||
<EuiPanel paddingSize="s">
|
||||
<GroupByLabelForm
|
||||
deleteHandler={deleteHandler}
|
||||
item={list[optionsDataId]}
|
||||
onChange={onChange}
|
||||
optionsDataId={optionsDataId}
|
||||
item={list[aggName]}
|
||||
otherAggNames={otherAggNames}
|
||||
onChange={item => onChange(aggName, item)}
|
||||
options={options}
|
||||
/>
|
||||
</EuiPanel>
|
||||
{listKeys.length > 0 && <EuiSpacer size="s" />}
|
||||
|
|
|
@ -16,7 +16,7 @@ describe('Data Frame: <GroupByListSummary />', () => {
|
|||
const item: PivotGroupByConfig = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS,
|
||||
field: 'the-group-by-field',
|
||||
formRowLabel: 'the-group-by-label',
|
||||
aggName: 'the-group-by-label',
|
||||
};
|
||||
const props = {
|
||||
list: { 'the-options-data-id': item },
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import { PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../common';
|
||||
import { AggName, PIVOT_SUPPORTED_GROUP_BY_AGGS, PivotGroupByConfig } from '../../common';
|
||||
|
||||
import { isIntervalValid, PopoverForm, supportedIntervalTypes } from './popover_form';
|
||||
import { isIntervalValid, PopoverForm } from './popover_form';
|
||||
|
||||
describe('isIntervalValid()', () => {
|
||||
test('intervalType: histogram', () => {
|
||||
|
@ -73,15 +73,25 @@ describe('isIntervalValid()', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Data Frame: <PopoverForm />', () => {
|
||||
describe('Data Frame: Group By <PopoverForm />', () => {
|
||||
test('Minimal initialization', () => {
|
||||
const props = {
|
||||
defaultInterval: '10m',
|
||||
intervalType: 'date_histogram' as supportedIntervalTypes,
|
||||
onChange(interval: string) {},
|
||||
const defaultData: PivotGroupByConfig = {
|
||||
agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM,
|
||||
aggName: 'the-agg-name',
|
||||
field: 'the-field',
|
||||
interval: '10m',
|
||||
};
|
||||
const otherAggNames: AggName[] = [];
|
||||
const onChange = (item: PivotGroupByConfig) => {};
|
||||
|
||||
const wrapper = shallow(<PopoverForm {...props} />);
|
||||
const wrapper = shallow(
|
||||
<PopoverForm
|
||||
defaultData={defaultData}
|
||||
otherAggNames={otherAggNames}
|
||||
options={{}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
|
|
@ -8,27 +8,28 @@ import React, { useState } from 'react';
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
} from '@elastic/eui';
|
||||
import { EuiButton, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui';
|
||||
|
||||
import { dictionaryToArray } from '../../../../common/types/common';
|
||||
|
||||
import {
|
||||
AggName,
|
||||
dateHistogramIntervalFormatRegex,
|
||||
groupByConfigHasInterval,
|
||||
histogramIntervalFormatRegex,
|
||||
isAggName,
|
||||
PivotGroupByConfig,
|
||||
PivotGroupByConfigDict,
|
||||
PivotSupportedGroupByAggs,
|
||||
PivotSupportedGroupByAggsWithInterval,
|
||||
PIVOT_SUPPORTED_GROUP_BY_AGGS,
|
||||
} from '../../common';
|
||||
|
||||
export type supportedIntervalTypes =
|
||||
| PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM
|
||||
| PIVOT_SUPPORTED_GROUP_BY_AGGS.DATE_HISTOGRAM;
|
||||
|
||||
export function isIntervalValid(interval: string, intervalType: supportedIntervalTypes) {
|
||||
if (interval !== '') {
|
||||
export function isIntervalValid(
|
||||
interval: optionalInterval,
|
||||
intervalType: PivotSupportedGroupByAggsWithInterval
|
||||
) {
|
||||
if (interval !== '' && interval !== undefined) {
|
||||
if (intervalType === PIVOT_SUPPORTED_GROUP_BY_AGGS.HISTOGRAM) {
|
||||
if (!histogramIntervalFormatRegex.test(interval)) {
|
||||
return false;
|
||||
|
@ -57,51 +58,154 @@ export function isIntervalValid(interval: string, intervalType: supportedInterva
|
|||
return false;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
defaultInterval: string;
|
||||
intervalType: supportedIntervalTypes;
|
||||
onChange(interval: string): void;
|
||||
interface SelectOption {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const PopoverForm: React.SFC<Props> = ({ defaultInterval, intervalType, onChange }) => {
|
||||
const [interval, setInterval] = useState(defaultInterval);
|
||||
type optionalInterval = string | undefined;
|
||||
|
||||
const valid = isIntervalValid(interval, intervalType);
|
||||
interface Props {
|
||||
defaultData: PivotGroupByConfig;
|
||||
otherAggNames: AggName[];
|
||||
options: PivotGroupByConfigDict;
|
||||
onChange(item: PivotGroupByConfig): void;
|
||||
}
|
||||
|
||||
export const PopoverForm: React.SFC<Props> = ({
|
||||
defaultData,
|
||||
otherAggNames,
|
||||
onChange,
|
||||
options,
|
||||
}) => {
|
||||
const [agg, setAgg] = useState(defaultData.agg);
|
||||
const [aggName, setAggName] = useState(defaultData.aggName);
|
||||
const [field, setField] = useState(defaultData.field);
|
||||
const [interval, setInterval] = useState(
|
||||
groupByConfigHasInterval(defaultData) ? defaultData.interval : undefined
|
||||
);
|
||||
|
||||
function getUpdatedItem(): PivotGroupByConfig {
|
||||
const updatedItem = { ...defaultData, agg, aggName, field };
|
||||
if (groupByConfigHasInterval(updatedItem) && interval !== undefined) {
|
||||
updatedItem.interval = interval;
|
||||
}
|
||||
// Casting to PivotGroupByConfig because TS would otherwise complain about the
|
||||
// PIVOT_SUPPORTED_GROUP_BY_AGGS type for `agg`.
|
||||
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 };
|
||||
});
|
||||
|
||||
let aggNameError = '';
|
||||
|
||||
let validAggName = isAggName(aggName);
|
||||
if (!validAggName) {
|
||||
aggNameError = i18n.translate(
|
||||
'xpack.ml.dataframe.groupBy.popoverForm.aggNameInvalidCharError',
|
||||
{
|
||||
defaultMessage:
|
||||
'Invalid name. The characters "[", "]", and ">" are not allowed and the name must not start or end with a space character.',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (validAggName) {
|
||||
validAggName = !otherAggNames.includes(aggName);
|
||||
aggNameError = i18n.translate(
|
||||
'xpack.ml.dataframe.groupBy.popoverForm.aggNameAlreadyUsedError',
|
||||
{
|
||||
defaultMessage: 'Another group by configuration already uses that name.',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const validInterval =
|
||||
groupByConfigHasInterval(defaultData) && isIntervalValid(interval, defaultData.agg);
|
||||
|
||||
let formValid = validAggName;
|
||||
if (formValid && groupByConfigHasInterval(defaultData)) {
|
||||
formValid = isIntervalValid(interval, defaultData.agg);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiForm>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false} style={{ width: 100 }}>
|
||||
<EuiFormRow
|
||||
error={
|
||||
!valid && [
|
||||
i18n.translate('xpack.ml.dataframe.popoverForm.intervalError', {
|
||||
defaultMessage: 'Invalid interval.',
|
||||
}),
|
||||
]
|
||||
}
|
||||
isInvalid={!valid}
|
||||
label={i18n.translate('xpack.ml.dataframe.popoverForm.intervalLabel', {
|
||||
defaultMessage: 'Interval',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={interval}
|
||||
isInvalid={!valid}
|
||||
onChange={e => setInterval(e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow hasEmptyLabelSpace>
|
||||
<EuiButton isDisabled={!valid} onClick={() => onChange(interval)}>
|
||||
{i18n.translate('xpack.ml.dataframe.popoverForm.submitButtonLabel', {
|
||||
defaultMessage: 'Apply',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiForm style={{ width: '300px' }}>
|
||||
<EuiFormRow
|
||||
error={!validAggName && [aggNameError]}
|
||||
isInvalid={!validAggName}
|
||||
label={i18n.translate('xpack.ml.dataframe.groupBy.popoverForm.nameLabel', {
|
||||
defaultMessage: 'Group by name',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={aggName}
|
||||
isInvalid={!validAggName}
|
||||
onChange={e => setAggName(e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{availableAggs.length > 0 && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.groupby.popoverForm.aggLabel', {
|
||||
defaultMessage: 'Aggregation',
|
||||
})}
|
||||
>
|
||||
<EuiSelect
|
||||
options={availableAggs}
|
||||
value={agg}
|
||||
onChange={e => setAgg(e.target.value as PivotSupportedGroupByAggs)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{availableFields.length > 0 && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.ml.dataframe.groupBy.popoverForm.fieldLabel', {
|
||||
defaultMessage: 'Field',
|
||||
})}
|
||||
>
|
||||
<EuiSelect
|
||||
options={availableFields}
|
||||
value={field}
|
||||
onChange={e => setField(e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{groupByConfigHasInterval(defaultData) && (
|
||||
<EuiFormRow
|
||||
error={
|
||||
!validInterval && [
|
||||
i18n.translate('xpack.ml.dataframe.groupBy.popoverForm.intervalError', {
|
||||
defaultMessage: 'Invalid interval.',
|
||||
}),
|
||||
]
|
||||
}
|
||||
isInvalid={!validInterval}
|
||||
label={i18n.translate('xpack.ml.dataframe.groupBy.popoverForm.intervalLabel', {
|
||||
defaultMessage: 'Interval',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText
|
||||
defaultValue={interval}
|
||||
isInvalid={!validInterval}
|
||||
onChange={e => setInterval(e.target.value)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<EuiFormRow hasEmptyLabelSpace>
|
||||
<EuiButton isDisabled={!formValid} onClick={() => onChange(getUpdatedItem())}>
|
||||
{i18n.translate('xpack.ml.dataframe.groupBy.popoverForm.submitButtonLabel', {
|
||||
defaultMessage: 'Apply',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -89,6 +89,8 @@ interface Props {
|
|||
}
|
||||
|
||||
export const SourceIndexPreview: React.SFC<Props> = React.memo(({ cellClick, query }) => {
|
||||
const [clearTable, setClearTable] = useState(false);
|
||||
|
||||
const indexPattern = useContext(IndexPatternContext);
|
||||
|
||||
if (indexPattern === null) {
|
||||
|
@ -98,6 +100,18 @@ export const SourceIndexPreview: React.SFC<Props> = React.memo(({ cellClick, que
|
|||
const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]);
|
||||
const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false);
|
||||
|
||||
// EuiInMemoryTable has an issue with dynamic sortable columns
|
||||
// and will trigger a full page Kibana error in such a case.
|
||||
// The following is a workaround until this is solved upstream:
|
||||
// - If the sortable/columns config changes,
|
||||
// the table will be unmounted/not rendered.
|
||||
// This is what setClearTable(true) in toggleColumn() does.
|
||||
// - After that on next render it gets re-enabled. To make sure React
|
||||
// doesn't consolidate the state updates, setTimeout is used.
|
||||
if (clearTable) {
|
||||
setTimeout(() => setClearTable(false), 0);
|
||||
}
|
||||
|
||||
function toggleColumnsPopover() {
|
||||
setColumnsPopoverVisible(!isColumnsPopoverVisible);
|
||||
}
|
||||
|
@ -108,6 +122,7 @@ export const SourceIndexPreview: React.SFC<Props> = React.memo(({ cellClick, que
|
|||
|
||||
function toggleColumn(column: EsFieldName) {
|
||||
// spread to a new array otherwise the component wouldn't re-render
|
||||
setClearTable(true);
|
||||
setSelectedFields([...toggleSelectedField(selectedFields, column)]);
|
||||
}
|
||||
|
||||
|
@ -307,17 +322,19 @@ export const SourceIndexPreview: React.SFC<Props> = React.memo(({ cellClick, que
|
|||
{status !== SOURCE_INDEX_STATUS.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}
|
||||
/>
|
||||
{clearTable === false && (
|
||||
<ExpandableTable
|
||||
items={tableItems}
|
||||
columns={columns}
|
||||
pagination={true}
|
||||
hasActions={false}
|
||||
isSelectable={false}
|
||||
itemId="_id"
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
isExpandable={true}
|
||||
sorting={sorting}
|
||||
/>
|
||||
)}
|
||||
</EuiPanel>
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue