mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Lens] Dimension editor design changes (#136544)
* [Lens] Dimension editor design changes * Add none to timeshift as an option * Fix tests * Fix date_histogram test * Rest changes * Update gauge viz unit test * Fix heatmap viz unit test * Address PR comments * Some of the changes * Fix jest test * Further fixes * Move to summary to additional row * Final changes * Fix hover on advanced accordions * Replace with eui variables * Address PR comments * Fix CI failure * Fix the translation key * Update packages/kbn-coloring/src/shared_components/coloring/color_ranges/color_ranges_item.tsx Co-authored-by: Michael Marcialis <michael@marcial.is> * Fixed height in terms multifields drag drop Co-authored-by: Joe Reuter <johannes.reuter@elastic.co> Co-authored-by: Michael Marcialis <michael@marcial.is>
This commit is contained in:
parent
497e6c36b1
commit
e6ea0a406f
49 changed files with 1492 additions and 874 deletions
|
@ -51,7 +51,7 @@ export function ColorRangesExtraActions({
|
|||
});
|
||||
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="flexStart" gutterSize="none" wrap={true}>
|
||||
<EuiFlexGroup justifyContent="flexStart" gutterSize="m" wrap={true}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate(
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import useUpdateEffect from 'react-use/lib/useUpdateEffect';
|
||||
import React, { useState, useCallback, Dispatch, FocusEvent, useContext } from 'react';
|
||||
import React, { useState, useCallback, Dispatch, FocusEvent, useContext, useMemo } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import {
|
||||
EuiFieldNumber,
|
||||
|
@ -19,6 +20,7 @@ import {
|
|||
EuiColorPickerSwatch,
|
||||
EuiButtonIcon,
|
||||
EuiFieldNumberProps,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import {
|
||||
|
@ -111,6 +113,8 @@ export function ColorRangeItem({
|
|||
const ActionButton = getActionButton(mode);
|
||||
const isValid = validation?.isValid ?? true;
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const onLeaveFocus = useCallback(
|
||||
(e: FocusEvent<HTMLDivElement>) => {
|
||||
const prevStartValue = colorRanges[index - 1]?.start ?? Number.NEGATIVE_INFINITY;
|
||||
|
@ -162,15 +166,28 @@ export function ColorRangeItem({
|
|||
}
|
||||
);
|
||||
|
||||
const styles = useMemo(
|
||||
() => css`
|
||||
display: block;
|
||||
min-width: ${euiTheme.size.xl};
|
||||
text-align: center;
|
||||
`,
|
||||
[euiTheme.size.xl]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" wrap={false} responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexItem grow={false} css={isLast ? styles : null}>
|
||||
{!isLast ? (
|
||||
<EuiColorPicker
|
||||
onChange={onUpdateColor}
|
||||
button={
|
||||
isColorValid ? (
|
||||
<EuiColorPickerSwatch color={colorRange.color} aria-label={selectNewColorText} />
|
||||
<EuiColorPickerSwatch
|
||||
color={colorRange.color}
|
||||
aria-label={selectNewColorText}
|
||||
style={{ width: euiTheme.size.xl, height: euiTheme.size.xl }}
|
||||
/>
|
||||
) : (
|
||||
<EuiButtonIcon
|
||||
color="danger"
|
||||
|
@ -190,7 +207,7 @@ export function ColorRangeItem({
|
|||
isInvalid={!isColorValid}
|
||||
/>
|
||||
) : (
|
||||
<EuiIcon type={RelatedIcon} size="l" />
|
||||
<EuiIcon type={RelatedIcon} size="m" color={euiTheme.colors.disabled} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>
|
||||
|
|
|
@ -76,10 +76,8 @@ export const CustomizablePalette = ({
|
|||
const styles = useMemo(
|
||||
() => css`
|
||||
padding: ${euiTheme.size.base};
|
||||
background-color: ${euiTheme.colors.lightestShade};
|
||||
border-bottom: ${euiTheme.border.thin};
|
||||
`,
|
||||
[euiTheme.size.base, euiTheme.colors.lightestShade, euiTheme.border.thin]
|
||||
[euiTheme.size.base]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButtonGroup, EuiComboBox, EuiFieldText } from '@elastic/eui';
|
||||
import { EuiButtonGroup } from '@elastic/eui';
|
||||
import type { PaletteRegistry } from '@kbn/coloring';
|
||||
import {
|
||||
FramePublicAPI,
|
||||
|
@ -242,56 +242,4 @@ describe('data table dimension editor', () => {
|
|||
);
|
||||
expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_label"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should set the summary row function default to "none"', () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
const instance = mountWithIntl(<TableDimensionEditor {...props} />);
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsDatatable_summaryrow_function"]')
|
||||
.find(EuiComboBox)
|
||||
.prop('selectedOptions')
|
||||
).toEqual([{ value: 'none', label: 'None' }]);
|
||||
|
||||
expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_label"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should show the summary row label input ony when summary row is different from "none"', () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
state.columns[0].summaryRow = 'sum';
|
||||
const instance = mountWithIntl(<TableDimensionEditor {...props} />);
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsDatatable_summaryrow_function"]')
|
||||
.find(EuiComboBox)
|
||||
.prop('selectedOptions')
|
||||
).toEqual([{ value: 'sum', label: 'Sum' }]);
|
||||
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsDatatable_summaryrow_label"]')
|
||||
.find(EuiFieldText)
|
||||
.prop('value')
|
||||
).toBe('Sum');
|
||||
});
|
||||
|
||||
it("should show the correct summary row name when user's changes summary label", () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
state.columns[0].summaryRow = 'sum';
|
||||
state.columns[0].summaryLabel = 'MySum';
|
||||
const instance = mountWithIntl(<TableDimensionEditor {...props} />);
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsDatatable_summaryrow_function"]')
|
||||
.find(EuiComboBox)
|
||||
.prop('selectedOptions')
|
||||
).toEqual([{ value: 'sum', label: 'Sum' }]);
|
||||
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsDatatable_summaryrow_label"]')
|
||||
.find(EuiFieldText)
|
||||
.prop('value')
|
||||
).toBe('MySum');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFormRow,
|
||||
|
@ -16,8 +16,6 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
EuiButtonEmpty,
|
||||
EuiFieldText,
|
||||
EuiComboBox,
|
||||
} from '@elastic/eui';
|
||||
import { CustomizablePalette, PaletteRegistry, FIXED_PROGRESSION } from '@kbn/coloring';
|
||||
import { VisualizationDimensionEditorProps } from '../../types';
|
||||
|
@ -26,18 +24,10 @@ import { DatatableVisualizationState } from '../visualization';
|
|||
import {
|
||||
applyPaletteParams,
|
||||
defaultPaletteParams,
|
||||
useDebouncedValue,
|
||||
PalettePanelContainer,
|
||||
findMinMaxByColumnId,
|
||||
} from '../../shared_components';
|
||||
import type { ColumnState } from '../../../common/expressions';
|
||||
import {
|
||||
isNumericFieldForDatatable,
|
||||
getDefaultSummaryLabel,
|
||||
getFinalSummaryConfiguration,
|
||||
getSummaryRowOptions,
|
||||
getOriginalId,
|
||||
} from '../../../common/expressions';
|
||||
import { isNumericFieldForDatatable, getOriginalId } from '../../../common/expressions';
|
||||
|
||||
import './dimension_editor.scss';
|
||||
import { CollapseSetting } from '../../shared_components/collapse_setting';
|
||||
|
@ -45,7 +35,6 @@ import { CollapseSetting } from '../../shared_components/collapse_setting';
|
|||
const idPrefix = htmlIdGenerator()();
|
||||
|
||||
type ColumnType = DatatableVisualizationState['columns'][number];
|
||||
type SummaryRowType = Extract<ColumnState['summaryRow'], string>;
|
||||
|
||||
function updateColumnWith(
|
||||
state: DatatableVisualizationState,
|
||||
|
@ -69,24 +58,6 @@ export function TableDimensionEditor(
|
|||
const { state, setState, frame, accessor } = props;
|
||||
const column = state.columns.find(({ columnId }) => accessor === columnId);
|
||||
const [isPaletteOpen, setIsPaletteOpen] = useState(false);
|
||||
const onSummaryLabelChangeToDebounce = useCallback(
|
||||
(newSummaryLabel: string | undefined) => {
|
||||
setState({
|
||||
...state,
|
||||
columns: updateColumnWith(state, accessor, { summaryLabel: newSummaryLabel }),
|
||||
});
|
||||
},
|
||||
[accessor, setState, state]
|
||||
);
|
||||
const { inputValue: summaryLabel, handleInputChange: onSummaryLabelChange } = useDebouncedValue<
|
||||
string | undefined
|
||||
>(
|
||||
{
|
||||
onChange: onSummaryLabelChangeToDebounce,
|
||||
value: column?.summaryLabel,
|
||||
},
|
||||
{ allowFalsyValue: true } // falsy values are valid for this feature
|
||||
);
|
||||
|
||||
if (!column) return null;
|
||||
if (column.isTransposed) return null;
|
||||
|
@ -98,12 +69,6 @@ export function TableDimensionEditor(
|
|||
const currentAlignment = column?.alignment || (isNumeric ? 'right' : 'left');
|
||||
const currentColorMode = column?.colorMode || 'none';
|
||||
const hasDynamicColoring = currentColorMode !== 'none';
|
||||
// when switching from one operation to another, make sure to keep the configuration consistent
|
||||
const { summaryRow, summaryLabel: fallbackSummaryLabel } = getFinalSummaryConfiguration(
|
||||
accessor,
|
||||
column,
|
||||
currentData
|
||||
);
|
||||
|
||||
const datasource = frame.datasourceLayers[state.layerId];
|
||||
const showDynamicColoringFeature = Boolean(
|
||||
|
@ -188,97 +153,6 @@ export function TableDimensionEditor(
|
|||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{!column.isTransposed && (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.table.columnVisibilityLabel', {
|
||||
defaultMessage: 'Hide column',
|
||||
})}
|
||||
display="columnCompressedSwitch"
|
||||
>
|
||||
<EuiSwitch
|
||||
compressed
|
||||
label={i18n.translate('xpack.lens.table.columnVisibilityLabel', {
|
||||
defaultMessage: 'Hide column',
|
||||
})}
|
||||
showLabel={false}
|
||||
data-test-subj="lns-table-column-hidden"
|
||||
checked={Boolean(column?.hidden)}
|
||||
disabled={!column.hidden && visibleColumnsCount <= 1}
|
||||
onChange={() => {
|
||||
const newState = {
|
||||
...state,
|
||||
columns: state.columns.map((currentColumn) => {
|
||||
if (currentColumn.columnId === accessor) {
|
||||
return {
|
||||
...currentColumn,
|
||||
hidden: !column.hidden,
|
||||
};
|
||||
} else {
|
||||
return currentColumn;
|
||||
}
|
||||
}),
|
||||
};
|
||||
setState(newState);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{isNumeric && (
|
||||
<>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.table.summaryRow.label', {
|
||||
defaultMessage: 'Summary Row',
|
||||
})}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
compressed
|
||||
isClearable={false}
|
||||
data-test-subj="lnsDatatable_summaryrow_function"
|
||||
placeholder={i18n.translate('xpack.lens.indexPattern.fieldPlaceholder', {
|
||||
defaultMessage: 'Field',
|
||||
})}
|
||||
options={getSummaryRowOptions()}
|
||||
selectedOptions={[
|
||||
{
|
||||
label: getDefaultSummaryLabel(summaryRow),
|
||||
value: summaryRow,
|
||||
},
|
||||
]}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
onChange={(choices) => {
|
||||
const newValue = choices[0].value as SummaryRowType;
|
||||
setState({
|
||||
...state,
|
||||
columns: updateColumnWith(state, accessor, { summaryRow: newValue }),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{summaryRow !== 'none' && (
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.table.summaryRow.customlabel', {
|
||||
defaultMessage: 'Summary label',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
compressed
|
||||
data-test-subj="lnsDatatable_summaryrow_label"
|
||||
value={summaryLabel ?? fallbackSummaryLabel}
|
||||
onChange={(e) => {
|
||||
onSummaryLabelChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{showDynamicColoringFeature && (
|
||||
<>
|
||||
<EuiFormRow
|
||||
|
@ -408,6 +282,42 @@ export function TableDimensionEditor(
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
{!column.isTransposed && (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.table.columnVisibilityLabel', {
|
||||
defaultMessage: 'Hide column',
|
||||
})}
|
||||
display="columnCompressedSwitch"
|
||||
>
|
||||
<EuiSwitch
|
||||
compressed
|
||||
label={i18n.translate('xpack.lens.table.columnVisibilityLabel', {
|
||||
defaultMessage: 'Hide column',
|
||||
})}
|
||||
showLabel={false}
|
||||
data-test-subj="lns-table-column-hidden"
|
||||
checked={Boolean(column?.hidden)}
|
||||
disabled={!column.hidden && visibleColumnsCount <= 1}
|
||||
onChange={() => {
|
||||
const newState = {
|
||||
...state,
|
||||
columns: state.columns.map((currentColumn) => {
|
||||
if (currentColumn.columnId === accessor) {
|
||||
return {
|
||||
...currentColumn,
|
||||
hidden: !column.hidden,
|
||||
};
|
||||
} else {
|
||||
return currentColumn;
|
||||
}
|
||||
}),
|
||||
};
|
||||
setState(newState);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiComboBox, EuiFieldText } from '@elastic/eui';
|
||||
import type { PaletteRegistry } from '@kbn/coloring';
|
||||
import { FramePublicAPI, VisualizationDimensionEditorProps } from '../../types';
|
||||
import { DatatableVisualizationState } from '../visualization';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../../mocks';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { TableDimensionEditorAdditionalSection } from './dimension_editor_addtional_section';
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { layerTypes } from '../../../common';
|
||||
|
||||
describe('data table dimension editor additional section', () => {
|
||||
let frame: FramePublicAPI;
|
||||
let state: DatatableVisualizationState;
|
||||
let setState: (newState: DatatableVisualizationState) => void;
|
||||
let props: VisualizationDimensionEditorProps<DatatableVisualizationState> & {
|
||||
paletteService: PaletteRegistry;
|
||||
};
|
||||
|
||||
function testState(): DatatableVisualizationState {
|
||||
return {
|
||||
layerId: 'first',
|
||||
layerType: layerTypes.DATA,
|
||||
columns: [
|
||||
{
|
||||
columnId: 'foo',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
state = testState();
|
||||
frame = createMockFramePublicAPI();
|
||||
frame.datasourceLayers = {
|
||||
first: createMockDatasource('test').publicAPIMock,
|
||||
};
|
||||
frame.activeData = {
|
||||
first: {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{
|
||||
id: 'foo',
|
||||
name: 'foo',
|
||||
meta: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
],
|
||||
rows: [],
|
||||
},
|
||||
};
|
||||
setState = jest.fn();
|
||||
props = {
|
||||
accessor: 'foo',
|
||||
frame,
|
||||
groupId: 'columns',
|
||||
layerId: 'first',
|
||||
state,
|
||||
setState,
|
||||
paletteService: chartPluginMock.createPaletteRegistry(),
|
||||
panelRef: React.createRef(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should set the summary row function default to "none"', () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
const instance = mountWithIntl(<TableDimensionEditorAdditionalSection {...props} />);
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsDatatable_summaryrow_function"]')
|
||||
.find(EuiComboBox)
|
||||
.prop('selectedOptions')
|
||||
).toEqual([{ value: 'none', label: 'None' }]);
|
||||
|
||||
expect(instance.find('[data-test-subj="lnsDatatable_summaryrow_label"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should show the summary row label input ony when summary row is different from "none"', () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
state.columns[0].summaryRow = 'sum';
|
||||
const instance = mountWithIntl(<TableDimensionEditorAdditionalSection {...props} />);
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsDatatable_summaryrow_function"]')
|
||||
.find(EuiComboBox)
|
||||
.prop('selectedOptions')
|
||||
).toEqual([{ value: 'sum', label: 'Sum' }]);
|
||||
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsDatatable_summaryrow_label"]')
|
||||
.find(EuiFieldText)
|
||||
.prop('value')
|
||||
).toBe('Sum');
|
||||
});
|
||||
|
||||
it("should show the correct summary row name when user's changes summary label", () => {
|
||||
frame.activeData!.first.columns[0].meta.type = 'number';
|
||||
state.columns[0].summaryRow = 'sum';
|
||||
state.columns[0].summaryLabel = 'MySum';
|
||||
const instance = mountWithIntl(<TableDimensionEditorAdditionalSection {...props} />);
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsDatatable_summaryrow_function"]')
|
||||
.find(EuiComboBox)
|
||||
.prop('selectedOptions')
|
||||
).toEqual([{ value: 'sum', label: 'Sum' }]);
|
||||
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj="lnsDatatable_summaryrow_label"]')
|
||||
.find(EuiFieldText)
|
||||
.prop('value')
|
||||
).toBe('MySum');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import { EuiFormRow, EuiFieldText, EuiText, useEuiTheme, EuiComboBox } from '@elastic/eui';
|
||||
import { PaletteRegistry } from '@kbn/coloring';
|
||||
import { VisualizationDimensionEditorProps } from '../../types';
|
||||
import { DatatableVisualizationState } from '../visualization';
|
||||
|
||||
import { useDebouncedValue } from '../../shared_components';
|
||||
import type { ColumnState } from '../../../common/expressions';
|
||||
import {
|
||||
isNumericFieldForDatatable,
|
||||
getDefaultSummaryLabel,
|
||||
getFinalSummaryConfiguration,
|
||||
getSummaryRowOptions,
|
||||
} from '../../../common/expressions';
|
||||
|
||||
import './dimension_editor.scss';
|
||||
|
||||
type ColumnType = DatatableVisualizationState['columns'][number];
|
||||
type SummaryRowType = Extract<ColumnState['summaryRow'], string>;
|
||||
|
||||
function updateColumnWith(
|
||||
state: DatatableVisualizationState,
|
||||
columnId: string,
|
||||
newColumnProps: Partial<ColumnType>
|
||||
) {
|
||||
return state.columns.map((currentColumn) => {
|
||||
if (currentColumn.columnId === columnId) {
|
||||
return { ...currentColumn, ...newColumnProps };
|
||||
} else {
|
||||
return currentColumn;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function TableDimensionEditorAdditionalSection(
|
||||
props: VisualizationDimensionEditorProps<DatatableVisualizationState> & {
|
||||
paletteService: PaletteRegistry;
|
||||
}
|
||||
) {
|
||||
const { state, setState, frame, accessor } = props;
|
||||
const column = state.columns.find(({ columnId }) => accessor === columnId);
|
||||
const onSummaryLabelChangeToDebounce = useCallback(
|
||||
(newSummaryLabel: string | undefined) => {
|
||||
setState({
|
||||
...state,
|
||||
columns: updateColumnWith(state, accessor, { summaryLabel: newSummaryLabel }),
|
||||
});
|
||||
},
|
||||
[accessor, setState, state]
|
||||
);
|
||||
const { inputValue: summaryLabel, handleInputChange: onSummaryLabelChange } = useDebouncedValue<
|
||||
string | undefined
|
||||
>(
|
||||
{
|
||||
onChange: onSummaryLabelChangeToDebounce,
|
||||
value: column?.summaryLabel,
|
||||
},
|
||||
{ allowFalsyValue: true } // falsy values are valid for this feature
|
||||
);
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
if (!column) return null;
|
||||
if (column.isTransposed) return null;
|
||||
|
||||
const currentData = frame.activeData?.[state.layerId];
|
||||
|
||||
// either read config state or use same logic as chart itself
|
||||
const isNumeric = isNumericFieldForDatatable(currentData, accessor);
|
||||
// when switching from one operation to another, make sure to keep the configuration consistent
|
||||
const { summaryRow, summaryLabel: fallbackSummaryLabel } = getFinalSummaryConfiguration(
|
||||
accessor,
|
||||
column,
|
||||
currentData
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isNumeric && (
|
||||
<div className="lnsIndexPatternDimensionEditor--padded lnsIndexPatternDimensionEditor--collapseNext">
|
||||
<EuiText
|
||||
size="s"
|
||||
css={css`
|
||||
margin-bottom: ${euiTheme.size.base};
|
||||
`}
|
||||
>
|
||||
<h4>
|
||||
{i18n.translate('xpack.lens.indexPattern.dimensionEditor.headingSummary', {
|
||||
defaultMessage: 'Summary',
|
||||
})}
|
||||
</h4>
|
||||
</EuiText>
|
||||
|
||||
<>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.table.summaryRow.label', {
|
||||
defaultMessage: 'Summary Row',
|
||||
})}
|
||||
display="columnCompressed"
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
compressed
|
||||
isClearable={false}
|
||||
data-test-subj="lnsDatatable_summaryrow_function"
|
||||
placeholder={i18n.translate('xpack.lens.indexPattern.fieldPlaceholder', {
|
||||
defaultMessage: 'Field',
|
||||
})}
|
||||
options={getSummaryRowOptions()}
|
||||
selectedOptions={[
|
||||
{
|
||||
label: getDefaultSummaryLabel(summaryRow),
|
||||
value: summaryRow,
|
||||
},
|
||||
]}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
onChange={(choices) => {
|
||||
const newValue = choices[0].value as SummaryRowType;
|
||||
setState({
|
||||
...state,
|
||||
columns: updateColumnWith(state, accessor, { summaryRow: newValue }),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{summaryRow !== 'none' && (
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.table.summaryRow.customlabel', {
|
||||
defaultMessage: 'Summary label',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
compressed
|
||||
data-test-subj="lnsDatatable_summaryrow_label"
|
||||
value={summaryLabel ?? fallbackSummaryLabel}
|
||||
onChange={(e) => {
|
||||
onSummaryLabelChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -22,6 +22,7 @@ import type {
|
|||
} from '../types';
|
||||
import { LensIconChartDatatable } from '../assets/chart_datatable';
|
||||
import { TableDimensionEditor } from './components/dimension_editor';
|
||||
import { TableDimensionEditorAdditionalSection } from './components/dimension_editor_addtional_section';
|
||||
import { LayerType, layerTypes } from '../../common';
|
||||
import { getDefaultSummaryLabel, PagingState } from '../../common/expressions';
|
||||
import type { ColumnState, SortingState } from '../../common/expressions';
|
||||
|
@ -190,6 +191,9 @@ export const getDatatableVisualization = ({
|
|||
groupLabel: i18n.translate('xpack.lens.datatable.breakdownRows', {
|
||||
defaultMessage: 'Rows',
|
||||
}),
|
||||
dimensionEditorGroupLabel: i18n.translate('xpack.lens.datatable.breakdownRow', {
|
||||
defaultMessage: 'Row',
|
||||
}),
|
||||
groupTooltip: i18n.translate('xpack.lens.datatable.breakdownRows.description', {
|
||||
defaultMessage:
|
||||
'Split table rows by field. This is recommended for high cardinality breakdowns.',
|
||||
|
@ -221,6 +225,9 @@ export const getDatatableVisualization = ({
|
|||
groupLabel: i18n.translate('xpack.lens.datatable.breakdownColumns', {
|
||||
defaultMessage: 'Columns',
|
||||
}),
|
||||
dimensionEditorGroupLabel: i18n.translate('xpack.lens.datatable.breakdownColumn', {
|
||||
defaultMessage: 'Column',
|
||||
}),
|
||||
groupTooltip: i18n.translate('xpack.lens.datatable.breakdownColumns.description', {
|
||||
defaultMessage:
|
||||
"Split metric columns by field. It's recommended to keep the number of columns low to avoid horizontal scrolling.",
|
||||
|
@ -245,6 +252,14 @@ export const getDatatableVisualization = ({
|
|||
groupLabel: i18n.translate('xpack.lens.datatable.metrics', {
|
||||
defaultMessage: 'Metrics',
|
||||
}),
|
||||
dimensionEditorGroupLabel: i18n.translate('xpack.lens.datatable.metric', {
|
||||
defaultMessage: 'Metric',
|
||||
}),
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.datatable.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
layerId: state.layerId,
|
||||
accessors: sortedColumns
|
||||
.filter((c) => !datasource!.getOperationForColumnId(c)?.isBucketed)
|
||||
|
@ -313,6 +328,17 @@ export const getDatatableVisualization = ({
|
|||
);
|
||||
},
|
||||
|
||||
renderDimensionEditorAdditionalSection(domElement, props) {
|
||||
render(
|
||||
<KibanaThemeProvider theme$={theme.theme$}>
|
||||
<I18nProvider>
|
||||
<TableDimensionEditorAdditionalSection {...props} paletteService={paletteService} />
|
||||
</I18nProvider>
|
||||
</KibanaThemeProvider>,
|
||||
domElement
|
||||
);
|
||||
},
|
||||
|
||||
getSupportedLayers() {
|
||||
return [
|
||||
{
|
||||
|
|
|
@ -554,7 +554,7 @@ export function LayerPanel(
|
|||
panelRef={(el) => (panelRef.current = el)}
|
||||
isOpen={isDimensionPanelOpen}
|
||||
isFullscreen={isFullscreen}
|
||||
groupLabel={activeGroup?.groupLabel || ''}
|
||||
groupLabel={activeGroup?.dimensionEditorGroupLabel ?? (activeGroup?.groupLabel || '')}
|
||||
handleClose={() => {
|
||||
if (layerDatasource) {
|
||||
if (
|
||||
|
@ -582,7 +582,7 @@ export function LayerPanel(
|
|||
return true;
|
||||
}}
|
||||
panel={
|
||||
<div>
|
||||
<>
|
||||
{activeGroup && activeId && layerDatasource && (
|
||||
<NativeRenderer
|
||||
render={layerDatasource.renderDimensionEditor}
|
||||
|
@ -611,20 +611,34 @@ export function LayerPanel(
|
|||
!activeDimension.isNew &&
|
||||
activeVisualization.renderDimensionEditor &&
|
||||
activeGroup?.enableDimensionEditor && (
|
||||
<div className="lnsLayerPanel__styleEditor">
|
||||
<NativeRenderer
|
||||
render={activeVisualization.renderDimensionEditor}
|
||||
nativeProps={{
|
||||
...layerVisualizationConfigProps,
|
||||
groupId: activeGroup.groupId,
|
||||
accessor: activeId,
|
||||
setState: props.updateVisualization,
|
||||
panelRef,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="lnsLayerPanel__styleEditor">
|
||||
<NativeRenderer
|
||||
render={activeVisualization.renderDimensionEditor}
|
||||
nativeProps={{
|
||||
...layerVisualizationConfigProps,
|
||||
groupId: activeGroup.groupId,
|
||||
accessor: activeId,
|
||||
setState: props.updateVisualization,
|
||||
panelRef,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{activeVisualization.renderDimensionEditorAdditionalSection && (
|
||||
<NativeRenderer
|
||||
render={activeVisualization.renderDimensionEditorAdditionalSection}
|
||||
nativeProps={{
|
||||
...layerVisualizationConfigProps,
|
||||
groupId: activeGroup.groupId,
|
||||
accessor: activeId,
|
||||
setState: props.updateVisualization,
|
||||
panelRef,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -151,6 +151,9 @@ describe('heatmap', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
},
|
||||
groupId: GROUP_ID.CELL,
|
||||
groupLabel: 'Cell value',
|
||||
accessors: [
|
||||
|
@ -206,6 +209,9 @@ describe('heatmap', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
},
|
||||
groupId: GROUP_ID.CELL,
|
||||
groupLabel: 'Cell value',
|
||||
accessors: [],
|
||||
|
@ -259,6 +265,9 @@ describe('heatmap', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
},
|
||||
groupId: GROUP_ID.CELL,
|
||||
groupLabel: 'Cell value',
|
||||
accessors: [
|
||||
|
|
|
@ -200,6 +200,11 @@ export const getHeatmapVisualization = ({
|
|||
groupLabel: i18n.translate('xpack.lens.heatmap.cellValueLabel', {
|
||||
defaultMessage: 'Cell value',
|
||||
}),
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.heatmap.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
accessors: state.valueAccessor
|
||||
? [
|
||||
// When data is not available and the range type is numeric, return a placeholder while refreshing
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiSpacer, EuiAccordion, EuiText, useEuiTheme } from '@elastic/eui';
|
||||
import { EuiSpacer, EuiAccordion, EuiTextColor, EuiTitle, useEuiTheme } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { AdvancedOption } from '../operations/definitions';
|
||||
|
||||
export function AdvancedOptions(props: { options: AdvancedOption[] }) {
|
||||
|
@ -17,13 +18,21 @@ export function AdvancedOptions(props: { options: AdvancedOption[] }) {
|
|||
id="advancedOptionsAccordion"
|
||||
arrowProps={{ color: 'primary' }}
|
||||
data-test-subj="indexPattern-advanced-accordion"
|
||||
className="lnsIndexPatternDimensionEditor-advancedOptions"
|
||||
buttonContent={
|
||||
<EuiText size="s" color={euiTheme.colors.primary}>
|
||||
{i18n.translate('xpack.lens.indexPattern.advancedSettings', {
|
||||
defaultMessage: 'Advanced',
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiTitle size="xxs">
|
||||
<h5>
|
||||
<EuiTextColor color={euiTheme.colors.primary}>
|
||||
{i18n.translate('xpack.lens.indexPattern.advancedSettings', {
|
||||
defaultMessage: 'Advanced',
|
||||
})}
|
||||
</EuiTextColor>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
}
|
||||
css={css`
|
||||
padding: 0 ${euiTheme.size.base} ${euiTheme.size.base};
|
||||
`}
|
||||
>
|
||||
{props.options.map(({ dataTestSubj, inlineElement }) => (
|
||||
<div key={dataTestSubj} data-test-subj={dataTestSubj}>
|
||||
|
|
|
@ -16,21 +16,19 @@
|
|||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
|
||||
.lnsIndexPatternDimensionEditor__section {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.lnsIndexPatternDimensionEditor__section--padded {
|
||||
.lnsIndexPatternDimensionEditor--padded {
|
||||
padding: $euiSize;
|
||||
}
|
||||
|
||||
.lnsIndexPatternDimensionEditor__section--collapseNext {
|
||||
.lnsIndexPatternDimensionEditor--collapseNext {
|
||||
margin-bottom: -$euiSizeL;
|
||||
border-top: $euiBorderThin;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
.lnsIndexPatternDimensionEditor__section--shaded {
|
||||
.lnsIndexPatternDimensionEditor--shaded {
|
||||
background-color: $euiColorLightestShade;
|
||||
border-bottom: $euiBorderThin;
|
||||
}
|
||||
|
@ -48,3 +46,23 @@
|
|||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.lnsIndexPatternDimensionEditor__warning {
|
||||
margin-bottom: $euiSize;
|
||||
margin-top: $euiSizeS;
|
||||
}
|
||||
|
||||
.lnsIndexPatternDimensionEditor__droppable {
|
||||
padding: $euiSizeXS;
|
||||
border-radius: $euiBorderRadius;
|
||||
}
|
||||
|
||||
.lnsIndexPatternDimensionEditor__droppableItem {
|
||||
margin-right: $euiSizeS;
|
||||
}
|
||||
|
||||
.lnsIndexPatternDimensionEditor-advancedOptions button {
|
||||
&:hover, &:focus {
|
||||
text-decoration-color: $euiColorPrimary;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,13 +8,18 @@
|
|||
import './dimension_editor.scss';
|
||||
import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import {
|
||||
EuiListGroup,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiListGroupItemProps,
|
||||
EuiFormLabel,
|
||||
EuiToolTip,
|
||||
EuiText,
|
||||
EuiIconTip,
|
||||
useEuiTheme,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import ReactDOM from 'react-dom';
|
||||
import type { IndexPatternDimensionEditorProps } from './dimension_panel';
|
||||
|
@ -51,9 +56,9 @@ import {
|
|||
isQuickFunction,
|
||||
getParamEditor,
|
||||
formulaOperationName,
|
||||
DimensionEditorTabs,
|
||||
DimensionEditorButtonGroups,
|
||||
CalloutWarning,
|
||||
DimensionEditorTab,
|
||||
DimensionEditorGroupsOptions,
|
||||
} from './dimensions_editor_helpers';
|
||||
import type { TemporaryState } from './dimensions_editor_helpers';
|
||||
import { FieldInput } from './field_input';
|
||||
|
@ -110,6 +115,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
|
||||
const temporaryQuickFunction = Boolean(temporaryState === quickFunctionsName);
|
||||
const temporaryStaticValue = Boolean(temporaryState === staticValueOperationName);
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const updateLayer = useCallback(
|
||||
(newLayer) => setState((prevState) => mergeLayer({ state: prevState, layerId, newLayer })),
|
||||
|
@ -314,6 +320,10 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
[selectedColumn, currentIndexPattern]
|
||||
);
|
||||
|
||||
const shouldDisplayDots =
|
||||
temporaryState === 'none' ||
|
||||
(selectedColumn?.operationType != null && isQuickFunction(selectedColumn?.operationType));
|
||||
|
||||
const sideNavItems: EuiListGroupItemProps[] = operationsWithCompatibility.map(
|
||||
({ operationType, compatibleWithCurrentField, disabledStatus }) => {
|
||||
const isActive = Boolean(
|
||||
|
@ -321,13 +331,6 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
(!incompleteOperation && selectedColumn && selectedColumn.operationType === operationType)
|
||||
);
|
||||
|
||||
let color: EuiListGroupItemProps['color'] = 'primary';
|
||||
if (isActive) {
|
||||
color = 'text';
|
||||
} else if (!compatibleWithCurrentField) {
|
||||
color = 'subdued';
|
||||
}
|
||||
|
||||
let label: EuiListGroupItemProps['label'] = operationDisplay[operationType].displayName;
|
||||
if (isActive && disabledStatus) {
|
||||
label = (
|
||||
|
@ -343,14 +346,33 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
<span>{operationDisplay[operationType].displayName}</span>
|
||||
</EuiToolTip>
|
||||
);
|
||||
} else if (isActive) {
|
||||
label = <strong>{operationDisplay[operationType].displayName}</strong>;
|
||||
} else if (!compatibleWithCurrentField) {
|
||||
label = (
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center">
|
||||
<EuiFlexItem grow={false} style={{ marginRight: euiTheme.size.xs }}>
|
||||
{label}
|
||||
</EuiFlexItem>
|
||||
{shouldDisplayDots && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.lens.indexPattern.helpIncompatibleFieldDotLabel', {
|
||||
defaultMessage:
|
||||
'This function is not compatible with the current selected field',
|
||||
})}
|
||||
position="left"
|
||||
size="s"
|
||||
type="dot"
|
||||
color="warning"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
id: operationType as string,
|
||||
label,
|
||||
color,
|
||||
isActive,
|
||||
size: 's',
|
||||
isDisabled: !!disabledStatus,
|
||||
|
@ -544,16 +566,16 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
|
||||
const quickFunctions = (
|
||||
<>
|
||||
<div className="lnsIndexPatternDimensionEditor__section lnsIndexPatternDimensionEditor__section--padded lnsIndexPatternDimensionEditor__section--shaded">
|
||||
<EuiFormLabel>
|
||||
{i18n.translate('xpack.lens.indexPattern.functionsLabel', {
|
||||
defaultMessage: 'Functions',
|
||||
})}
|
||||
</EuiFormLabel>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.indexPattern.functionsLabel', {
|
||||
defaultMessage: 'Function',
|
||||
})}
|
||||
fullWidth
|
||||
>
|
||||
<EuiListGroup
|
||||
className={sideNavItems.length > 3 ? 'lnsIndexPatternDimensionEditor__columns' : ''}
|
||||
gutterSize="none"
|
||||
color="primary"
|
||||
listItems={
|
||||
// add a padding item containing a non breakable space if the number of operations is not even
|
||||
// otherwise the column layout will break within an element
|
||||
|
@ -561,136 +583,132 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
}
|
||||
maxWidth={false}
|
||||
/>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
|
||||
<div className="lnsIndexPatternDimensionEditor__section lnsIndexPatternDimensionEditor__section--padded lnsIndexPatternDimensionEditor__section--shaded">
|
||||
{shouldDisplayReferenceEditor ? (
|
||||
<>
|
||||
{selectedColumn.references.map((referenceId, index) => {
|
||||
const validation = selectedOperationDefinition.requiredReferences[index];
|
||||
const layer = state.layers[layerId];
|
||||
return (
|
||||
<ReferenceEditor
|
||||
operationDefinitionMap={operationDefinitionMap}
|
||||
key={index}
|
||||
layer={layer}
|
||||
layerId={layerId}
|
||||
activeData={props.activeData}
|
||||
columnId={referenceId}
|
||||
column={layer.columns[referenceId]}
|
||||
incompleteColumn={
|
||||
layer.incompleteColumns ? layer.incompleteColumns[referenceId] : undefined
|
||||
{shouldDisplayReferenceEditor ? (
|
||||
<>
|
||||
{selectedColumn.references.map((referenceId, index) => {
|
||||
const validation = selectedOperationDefinition.requiredReferences[index];
|
||||
const layer = state.layers[layerId];
|
||||
return (
|
||||
<ReferenceEditor
|
||||
operationDefinitionMap={operationDefinitionMap}
|
||||
key={index}
|
||||
layer={layer}
|
||||
layerId={layerId}
|
||||
activeData={props.activeData}
|
||||
columnId={referenceId}
|
||||
column={layer.columns[referenceId]}
|
||||
incompleteColumn={
|
||||
layer.incompleteColumns ? layer.incompleteColumns[referenceId] : undefined
|
||||
}
|
||||
onDeleteColumn={() => {
|
||||
updateLayer(
|
||||
deleteColumn({
|
||||
layer,
|
||||
columnId: referenceId,
|
||||
indexPattern: currentIndexPattern,
|
||||
})
|
||||
);
|
||||
}}
|
||||
onChooseFunction={(operationType: string, field?: IndexPatternField) => {
|
||||
updateLayer(
|
||||
insertOrReplaceColumn({
|
||||
layer,
|
||||
columnId: referenceId,
|
||||
op: operationType,
|
||||
indexPattern: currentIndexPattern,
|
||||
field,
|
||||
visualizationGroups: dimensionGroups,
|
||||
})
|
||||
);
|
||||
}}
|
||||
onChooseField={(choice: FieldChoiceWithOperationType) => {
|
||||
updateLayer(
|
||||
insertOrReplaceColumn({
|
||||
layer,
|
||||
columnId: referenceId,
|
||||
indexPattern: currentIndexPattern,
|
||||
op: choice.operationType,
|
||||
field: currentIndexPattern.getFieldByName(choice.field),
|
||||
visualizationGroups: dimensionGroups,
|
||||
})
|
||||
);
|
||||
}}
|
||||
paramEditorUpdater={(
|
||||
setter:
|
||||
| IndexPatternLayer
|
||||
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
|
||||
| GenericIndexPatternColumn
|
||||
) => {
|
||||
let newLayer: IndexPatternLayer;
|
||||
if (typeof setter === 'function') {
|
||||
newLayer = setter(layer);
|
||||
} else if (isColumn(setter)) {
|
||||
newLayer = {
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
[referenceId]: setter,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
newLayer = setter;
|
||||
}
|
||||
onDeleteColumn={() => {
|
||||
updateLayer(
|
||||
deleteColumn({
|
||||
layer,
|
||||
columnId: referenceId,
|
||||
indexPattern: currentIndexPattern,
|
||||
})
|
||||
);
|
||||
}}
|
||||
onChooseFunction={(operationType: string, field?: IndexPatternField) => {
|
||||
updateLayer(
|
||||
insertOrReplaceColumn({
|
||||
layer,
|
||||
columnId: referenceId,
|
||||
op: operationType,
|
||||
indexPattern: currentIndexPattern,
|
||||
field,
|
||||
visualizationGroups: dimensionGroups,
|
||||
})
|
||||
);
|
||||
}}
|
||||
onChooseField={(choice: FieldChoiceWithOperationType) => {
|
||||
updateLayer(
|
||||
insertOrReplaceColumn({
|
||||
layer,
|
||||
columnId: referenceId,
|
||||
indexPattern: currentIndexPattern,
|
||||
op: choice.operationType,
|
||||
field: currentIndexPattern.getFieldByName(choice.field),
|
||||
visualizationGroups: dimensionGroups,
|
||||
})
|
||||
);
|
||||
}}
|
||||
paramEditorUpdater={(
|
||||
setter:
|
||||
| IndexPatternLayer
|
||||
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
|
||||
| GenericIndexPatternColumn
|
||||
) => {
|
||||
let newLayer: IndexPatternLayer;
|
||||
if (typeof setter === 'function') {
|
||||
newLayer = setter(layer);
|
||||
} else if (isColumn(setter)) {
|
||||
newLayer = {
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
[referenceId]: setter,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
newLayer = setter;
|
||||
}
|
||||
return updateLayer(
|
||||
adjustColumnReferencesForChangedColumn(newLayer, referenceId)
|
||||
);
|
||||
}}
|
||||
validation={validation}
|
||||
currentIndexPattern={currentIndexPattern}
|
||||
existingFields={state.existingFields}
|
||||
selectionStyle={selectedOperationDefinition.selectionStyle}
|
||||
dateRange={dateRange}
|
||||
labelAppend={selectedOperationDefinition?.getHelpMessage?.({
|
||||
data: props.data,
|
||||
uiSettings: props.uiSettings,
|
||||
currentColumn: layer.columns[columnId],
|
||||
})}
|
||||
isFullscreen={isFullscreen}
|
||||
toggleFullscreen={toggleFullscreen}
|
||||
setIsCloseable={setIsCloseable}
|
||||
paramEditorCustomProps={paramEditorCustomProps}
|
||||
{...services}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{selectedOperationDefinition.selectionStyle !== 'field' ? <EuiSpacer size="s" /> : null}
|
||||
</>
|
||||
) : null}
|
||||
return updateLayer(adjustColumnReferencesForChangedColumn(newLayer, referenceId));
|
||||
}}
|
||||
validation={validation}
|
||||
currentIndexPattern={currentIndexPattern}
|
||||
existingFields={state.existingFields}
|
||||
selectionStyle={selectedOperationDefinition.selectionStyle}
|
||||
dateRange={dateRange}
|
||||
labelAppend={selectedOperationDefinition?.getHelpMessage?.({
|
||||
data: props.data,
|
||||
uiSettings: props.uiSettings,
|
||||
currentColumn: layer.columns[columnId],
|
||||
})}
|
||||
isFullscreen={isFullscreen}
|
||||
toggleFullscreen={toggleFullscreen}
|
||||
setIsCloseable={setIsCloseable}
|
||||
paramEditorCustomProps={paramEditorCustomProps}
|
||||
{...services}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{selectedOperationDefinition.selectionStyle !== 'field' ? <EuiSpacer size="s" /> : null}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{shouldDisplayFieldInput ? (
|
||||
<FieldInputComponent
|
||||
layer={state.layers[layerId]}
|
||||
selectedColumn={selectedColumn as FieldBasedIndexPatternColumn}
|
||||
columnId={columnId}
|
||||
indexPattern={currentIndexPattern}
|
||||
existingFields={state.existingFields}
|
||||
operationSupportMatrix={operationSupportMatrix}
|
||||
updateLayer={(newLayer) => {
|
||||
if (temporaryQuickFunction) {
|
||||
setTemporaryState('none');
|
||||
}
|
||||
setStateWrapper(newLayer, { forceRender: temporaryQuickFunction });
|
||||
}}
|
||||
incompleteField={incompleteField}
|
||||
incompleteOperation={incompleteOperation}
|
||||
incompleteParams={incompleteParams}
|
||||
currentFieldIsInvalid={currentFieldIsInvalid}
|
||||
helpMessage={selectedOperationDefinition?.getHelpMessage?.({
|
||||
data: props.data,
|
||||
uiSettings: props.uiSettings,
|
||||
currentColumn: state.layers[layerId].columns[columnId],
|
||||
})}
|
||||
dimensionGroups={dimensionGroups}
|
||||
groupId={props.groupId}
|
||||
operationDefinitionMap={operationDefinitionMap}
|
||||
/>
|
||||
) : null}
|
||||
{shouldDisplayFieldInput ? (
|
||||
<FieldInputComponent
|
||||
layer={state.layers[layerId]}
|
||||
selectedColumn={selectedColumn as FieldBasedIndexPatternColumn}
|
||||
columnId={columnId}
|
||||
indexPattern={currentIndexPattern}
|
||||
existingFields={state.existingFields}
|
||||
operationSupportMatrix={operationSupportMatrix}
|
||||
updateLayer={(newLayer) => {
|
||||
if (temporaryQuickFunction) {
|
||||
setTemporaryState('none');
|
||||
}
|
||||
setStateWrapper(newLayer, { forceRender: temporaryQuickFunction });
|
||||
}}
|
||||
incompleteField={incompleteField}
|
||||
incompleteOperation={incompleteOperation}
|
||||
incompleteParams={incompleteParams}
|
||||
currentFieldIsInvalid={currentFieldIsInvalid}
|
||||
helpMessage={selectedOperationDefinition?.getHelpMessage?.({
|
||||
data: props.data,
|
||||
uiSettings: props.uiSettings,
|
||||
currentColumn: state.layers[layerId].columns[columnId],
|
||||
})}
|
||||
dimensionGroups={dimensionGroups}
|
||||
groupId={props.groupId}
|
||||
operationDefinitionMap={operationDefinitionMap}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{shouldDisplayExtraOptions && <ParamEditor {...paramEditorProps} />}
|
||||
</div>
|
||||
{shouldDisplayExtraOptions && <ParamEditor {...paramEditorProps} />}
|
||||
</>
|
||||
);
|
||||
|
||||
|
@ -719,7 +737,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
</>
|
||||
) : null;
|
||||
|
||||
const TabContent = showQuickFunctions ? quickFunctions : customParamEditor;
|
||||
const ButtonGroupContent = showQuickFunctions ? quickFunctions : customParamEditor;
|
||||
|
||||
const onFormatChange = useCallback(
|
||||
(newFormat) => {
|
||||
|
@ -738,9 +756,24 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
const hasFormula =
|
||||
!isFullscreen && operationSupportMatrix.operationWithoutField.has(formulaOperationName);
|
||||
|
||||
const hasTabs = !isFullscreen && (hasFormula || supportStaticValue);
|
||||
const hasButtonGroups = !isFullscreen && (hasFormula || supportStaticValue);
|
||||
const initialMethod = useMemo(() => {
|
||||
let methodId = '';
|
||||
if (showStaticValueFunction) {
|
||||
methodId = staticValueOperationName;
|
||||
} else if (showQuickFunctions) {
|
||||
methodId = quickFunctionsName;
|
||||
} else if (
|
||||
temporaryState === 'none' &&
|
||||
selectedColumn?.operationType === formulaOperationName
|
||||
) {
|
||||
methodId = formulaOperationName;
|
||||
}
|
||||
return methodId;
|
||||
}, [selectedColumn?.operationType, showQuickFunctions, showStaticValueFunction, temporaryState]);
|
||||
const [selectedMethod, setSelectedMethod] = useState(initialMethod);
|
||||
|
||||
const tabs: DimensionEditorTab[] = [
|
||||
const options: DimensionEditorGroupsOptions[] = [
|
||||
{
|
||||
id: staticValueOperationName,
|
||||
enabled: Boolean(supportStaticValue),
|
||||
|
@ -768,7 +801,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
}
|
||||
},
|
||||
label: i18n.translate('xpack.lens.indexPattern.quickFunctionsLabel', {
|
||||
defaultMessage: 'Quick functions',
|
||||
defaultMessage: 'Quick function',
|
||||
}),
|
||||
},
|
||||
{
|
||||
|
@ -820,118 +853,157 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
|
||||
return (
|
||||
<div id={columnId}>
|
||||
{hasTabs ? <DimensionEditorTabs tabs={tabs} /> : null}
|
||||
<CalloutWarning
|
||||
currentOperationType={selectedColumn?.operationType}
|
||||
temporaryStateType={temporaryState}
|
||||
/>
|
||||
{TabContent}
|
||||
<div className="lnsIndexPatternDimensionEditor--padded">
|
||||
<EuiText
|
||||
size="s"
|
||||
css={css`
|
||||
margin-bottom: ${euiTheme.size.base};
|
||||
`}
|
||||
>
|
||||
<h4>
|
||||
{paramEditorCustomProps?.headingLabel ??
|
||||
i18n.translate('xpack.lens.indexPattern.dimensionEditor.headingData', {
|
||||
defaultMessage: 'Data',
|
||||
})}
|
||||
</h4>
|
||||
</EuiText>
|
||||
<>
|
||||
{hasButtonGroups ? (
|
||||
<DimensionEditorButtonGroups
|
||||
options={options}
|
||||
onMethodChange={(optionId: string) => {
|
||||
setSelectedMethod(optionId);
|
||||
}}
|
||||
selectedMethod={selectedMethod}
|
||||
/>
|
||||
) : null}
|
||||
<CalloutWarning
|
||||
currentOperationType={selectedColumn?.operationType}
|
||||
temporaryStateType={temporaryState}
|
||||
/>
|
||||
{ButtonGroupContent}
|
||||
</>
|
||||
</div>
|
||||
|
||||
{shouldDisplayAdvancedOptions && (
|
||||
<div className="lnsIndexPatternDimensionEditor__section lnsIndexPatternDimensionEditor__section--padded lnsIndexPatternDimensionEditor__section--shaded">
|
||||
<AdvancedOptions
|
||||
options={[
|
||||
{
|
||||
dataTestSubj: 'indexPattern-time-scaling-enable',
|
||||
inlineElement: selectedOperationDefinition.timeScalingMode ? (
|
||||
<TimeScaling
|
||||
selectedColumn={selectedColumn}
|
||||
columnId={columnId}
|
||||
layer={state.layers[layerId]}
|
||||
updateLayer={setStateWrapper}
|
||||
/>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
dataTestSubj: 'indexPattern-filter-by-enable',
|
||||
inlineElement: selectedOperationDefinition.filterable ? (
|
||||
<Filtering
|
||||
indexPattern={currentIndexPattern}
|
||||
selectedColumn={selectedColumn}
|
||||
columnId={columnId}
|
||||
layer={state.layers[layerId]}
|
||||
updateLayer={setStateWrapper}
|
||||
helpMessage={
|
||||
selectedOperationDefinition.filterable &&
|
||||
typeof selectedOperationDefinition.filterable !== 'boolean'
|
||||
? selectedOperationDefinition.filterable.helpMessage
|
||||
: null
|
||||
}
|
||||
/>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
dataTestSubj: 'indexPattern-time-shift-enable',
|
||||
inlineElement: Boolean(
|
||||
selectedOperationDefinition.shiftable &&
|
||||
(currentIndexPattern.timeFieldName ||
|
||||
Object.values(state.layers[layerId].columns).some(
|
||||
(col) => col.operationType === 'date_histogram'
|
||||
))
|
||||
) ? (
|
||||
<TimeShift
|
||||
datatableUtilities={services.data.datatableUtilities}
|
||||
indexPattern={currentIndexPattern}
|
||||
selectedColumn={selectedColumn}
|
||||
columnId={columnId}
|
||||
layer={state.layers[layerId]}
|
||||
updateLayer={setStateWrapper}
|
||||
activeData={props.activeData}
|
||||
layerId={layerId}
|
||||
/>
|
||||
) : null,
|
||||
},
|
||||
...(operationDefinitionMap[selectedColumn.operationType].getAdvancedOptions?.(
|
||||
paramEditorProps
|
||||
) || []),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<AdvancedOptions
|
||||
options={[
|
||||
{
|
||||
dataTestSubj: 'indexPattern-time-scaling-enable',
|
||||
inlineElement: selectedOperationDefinition.timeScalingMode ? (
|
||||
<TimeScaling
|
||||
selectedColumn={selectedColumn}
|
||||
columnId={columnId}
|
||||
layer={state.layers[layerId]}
|
||||
updateLayer={setStateWrapper}
|
||||
/>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
dataTestSubj: 'indexPattern-filter-by-enable',
|
||||
inlineElement: selectedOperationDefinition.filterable ? (
|
||||
<Filtering
|
||||
indexPattern={currentIndexPattern}
|
||||
selectedColumn={selectedColumn}
|
||||
columnId={columnId}
|
||||
layer={state.layers[layerId]}
|
||||
updateLayer={setStateWrapper}
|
||||
helpMessage={
|
||||
selectedOperationDefinition.filterable &&
|
||||
typeof selectedOperationDefinition.filterable !== 'boolean'
|
||||
? selectedOperationDefinition.filterable.helpMessage
|
||||
: null
|
||||
}
|
||||
/>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
dataTestSubj: 'indexPattern-time-shift-enable',
|
||||
inlineElement: Boolean(
|
||||
selectedOperationDefinition.shiftable &&
|
||||
(currentIndexPattern.timeFieldName ||
|
||||
Object.values(state.layers[layerId].columns).some(
|
||||
(col) => col.operationType === 'date_histogram'
|
||||
))
|
||||
) ? (
|
||||
<TimeShift
|
||||
datatableUtilities={services.data.datatableUtilities}
|
||||
indexPattern={currentIndexPattern}
|
||||
selectedColumn={selectedColumn}
|
||||
columnId={columnId}
|
||||
layer={state.layers[layerId]}
|
||||
updateLayer={setStateWrapper}
|
||||
activeData={props.activeData}
|
||||
layerId={layerId}
|
||||
/>
|
||||
) : null,
|
||||
},
|
||||
...(operationDefinitionMap[selectedColumn.operationType].getAdvancedOptions?.(
|
||||
paramEditorProps
|
||||
) || []),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isFullscreen && !currentFieldIsInvalid && (
|
||||
<div className="lnsIndexPatternDimensionEditor__section lnsIndexPatternDimensionEditor__section--padded lnsIndexPatternDimensionEditor__section--collapseNext">
|
||||
{!incompleteInfo && selectedColumn && temporaryState === 'none' && (
|
||||
<NameInput
|
||||
// re-render the input from scratch to obtain new "initial value" if the underlying default label changes
|
||||
key={defaultLabel}
|
||||
value={selectedColumn.label}
|
||||
defaultValue={defaultLabel}
|
||||
onChange={(value) => {
|
||||
updateLayer({
|
||||
columns: {
|
||||
...state.layers[layerId].columns,
|
||||
[columnId]: {
|
||||
...selectedColumn,
|
||||
label: value,
|
||||
customLabel:
|
||||
operationDefinitionMap[selectedColumn.operationType].getDefaultLabel(
|
||||
selectedColumn,
|
||||
state.indexPatterns[state.layers[layerId].indexPatternId],
|
||||
state.layers[layerId].columns
|
||||
) !== value,
|
||||
<div className="lnsIndexPatternDimensionEditor--padded lnsIndexPatternDimensionEditor--collapseNext">
|
||||
{!incompleteInfo && temporaryState === 'none' && selectedColumn && (
|
||||
<EuiText
|
||||
size="s"
|
||||
css={css`
|
||||
margin-bottom: ${euiTheme.size.base};
|
||||
`}
|
||||
>
|
||||
<h4>
|
||||
{i18n.translate('xpack.lens.indexPattern.dimensionEditor.headingAppearance', {
|
||||
defaultMessage: 'Appearance',
|
||||
})}
|
||||
</h4>
|
||||
</EuiText>
|
||||
)}
|
||||
<>
|
||||
{!incompleteInfo && selectedColumn && temporaryState === 'none' && (
|
||||
<NameInput
|
||||
// re-render the input from scratch to obtain new "initial value" if the underlying default label changes
|
||||
key={defaultLabel}
|
||||
value={selectedColumn.label}
|
||||
defaultValue={defaultLabel}
|
||||
onChange={(value) => {
|
||||
updateLayer({
|
||||
columns: {
|
||||
...state.layers[layerId].columns,
|
||||
[columnId]: {
|
||||
...selectedColumn,
|
||||
label: value,
|
||||
customLabel:
|
||||
operationDefinitionMap[selectedColumn.operationType].getDefaultLabel(
|
||||
selectedColumn,
|
||||
state.indexPatterns[state.layers[layerId].indexPatternId],
|
||||
state.layers[layerId].columns
|
||||
) !== value,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isFullscreen && !incompleteInfo && !hideGrouping && temporaryState === 'none' && (
|
||||
<BucketNestingEditor
|
||||
layer={state.layers[props.layerId]}
|
||||
columnId={props.columnId}
|
||||
setColumns={(columnOrder) => updateLayer({ columnOrder })}
|
||||
getFieldByName={currentIndexPattern.getFieldByName}
|
||||
/>
|
||||
)}
|
||||
{!isFullscreen && !incompleteInfo && !hideGrouping && temporaryState === 'none' && (
|
||||
<BucketNestingEditor
|
||||
layer={state.layers[props.layerId]}
|
||||
columnId={props.columnId}
|
||||
setColumns={(columnOrder) => updateLayer({ columnOrder })}
|
||||
getFieldByName={currentIndexPattern.getFieldByName}
|
||||
/>
|
||||
)}
|
||||
|
||||
{supportFieldFormat &&
|
||||
!isFullscreen &&
|
||||
selectedColumn &&
|
||||
(selectedColumn.dataType === 'number' || selectedColumn.operationType === 'range') ? (
|
||||
<FormatSelector selectedColumn={selectedColumn} onChange={onFormatChange} />
|
||||
) : null}
|
||||
{supportFieldFormat &&
|
||||
!isFullscreen &&
|
||||
selectedColumn &&
|
||||
(selectedColumn.dataType === 'number' || selectedColumn.operationType === 'range') ? (
|
||||
<FormatSelector selectedColumn={selectedColumn} onChange={onFormatChange} />
|
||||
) : null}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { ReactWrapper, ShallowWrapper } from 'enzyme';
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import {
|
||||
EuiComboBox,
|
||||
EuiListGroupItemProps,
|
||||
|
@ -1108,7 +1109,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
|
||||
it('should default to None if time scaling is not set', () => {
|
||||
wrapper = mount(<IndexPatternDimensionEditorComponent {...getProps({})} />);
|
||||
wrapper.find('[data-test-subj="indexPattern-advanced-accordion"]').first().simulate('click');
|
||||
findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="indexPattern-time-scaling-enable"]')).toHaveLength(1);
|
||||
expect(
|
||||
wrapper
|
||||
|
@ -1120,7 +1121,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
|
||||
it('should show current time scaling if set', () => {
|
||||
wrapper = mount(<IndexPatternDimensionEditorComponent {...getProps({ timeScale: 'd' })} />);
|
||||
wrapper.find('[data-test-subj="indexPattern-advanced-accordion"]').first().simulate('click');
|
||||
findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click');
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="indexPattern-time-scaling-unit"]')
|
||||
|
@ -1132,7 +1133,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
it('should allow to set time scaling initially', () => {
|
||||
const props = getProps({});
|
||||
wrapper = mount(<IndexPatternDimensionEditorComponent {...props} />);
|
||||
wrapper.find('[data-test-subj="indexPattern-advanced-accordion"]').first().simulate('click');
|
||||
findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click');
|
||||
wrapper
|
||||
.find('[data-test-subj="indexPattern-time-scaling-unit"]')
|
||||
.find(EuiSelect)
|
||||
|
@ -1214,7 +1215,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
it('should allow to change time scaling', () => {
|
||||
const props = getProps({ timeScale: 's', label: 'Count of records per second' });
|
||||
wrapper = mount(<IndexPatternDimensionEditorComponent {...props} />);
|
||||
wrapper.find('[data-test-subj="indexPattern-advanced-accordion"]').first().simulate('click');
|
||||
findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click');
|
||||
|
||||
wrapper.find('[data-test-subj="indexPattern-time-scaling-unit"] select').simulate('change', {
|
||||
target: { value: 'h' },
|
||||
|
@ -1320,7 +1321,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
}}
|
||||
/>
|
||||
);
|
||||
wrapper.find('[data-test-subj="indexPattern-advanced-accordion"]').first().simulate('click');
|
||||
findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click');
|
||||
expect(wrapper.find('[data-test-subj="indexPattern-time-shift-enable"]')).toHaveLength(1);
|
||||
expect(wrapper.find(TimeShift)).toHaveLength(0);
|
||||
});
|
||||
|
@ -1347,7 +1348,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
it('should allow to set time shift initially', () => {
|
||||
const props = getProps({});
|
||||
wrapper = mount(<IndexPatternDimensionEditorComponent {...props} />);
|
||||
wrapper.find('[data-test-subj="indexPattern-advanced-accordion"]').first().simulate('click');
|
||||
findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click');
|
||||
wrapper.find(TimeShift).find(EuiComboBox).prop('onChange')!([{ value: '1h', label: '' }]);
|
||||
expect((props.setState as jest.Mock).mock.calls[0][0](props.state)).toEqual({
|
||||
...props.state,
|
||||
|
@ -1480,7 +1481,7 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
|
||||
it('should show custom options if filtering is available', () => {
|
||||
wrapper = mount(<IndexPatternDimensionEditorComponent {...getProps({})} />);
|
||||
wrapper.find('[data-test-subj="indexPattern-advanced-accordion"]').first().simulate('click');
|
||||
findTestSubject(wrapper, 'indexPattern-advanced-accordion').simulate('click');
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="indexPattern-filter-by-enable"]').hostNodes()
|
||||
).toHaveLength(1);
|
||||
|
@ -1778,23 +1779,23 @@ describe('IndexPatternDimensionEditorPanel', () => {
|
|||
|
||||
const items: EuiListGroupItemProps[] = wrapper.find(EuiListGroup).prop('listItems') || [];
|
||||
|
||||
expect(items.map(({ label }: { label: React.ReactNode }) => label)).toEqual([
|
||||
'Average',
|
||||
'Count',
|
||||
'Counter rate',
|
||||
'Cumulative sum',
|
||||
'Differences',
|
||||
'Last value',
|
||||
'Maximum',
|
||||
'Median',
|
||||
'Minimum',
|
||||
'Moving average',
|
||||
'Percentile',
|
||||
'Percentile rank',
|
||||
'Standard deviation',
|
||||
'Sum',
|
||||
'Unique count',
|
||||
' ',
|
||||
expect(items.map(({ id }) => id)).toEqual([
|
||||
'average',
|
||||
'count',
|
||||
'counter_rate',
|
||||
'cumulative_sum',
|
||||
'differences',
|
||||
'last_value',
|
||||
'max',
|
||||
'median',
|
||||
'min',
|
||||
'moving_average',
|
||||
'percentile',
|
||||
'percentile_rank',
|
||||
'standard_deviation',
|
||||
'sum',
|
||||
'unique_count',
|
||||
undefined,
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
import './dimension_editor.scss';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiTabs, EuiTab, EuiCallOut } from '@elastic/eui';
|
||||
import { EuiCallOut, EuiButtonGroup, EuiFormRow } from '@elastic/eui';
|
||||
import { operationDefinitionMap } from '../operations';
|
||||
|
||||
export const formulaOperationName = 'formula';
|
||||
|
@ -112,7 +112,7 @@ export const CalloutWarning = ({
|
|||
);
|
||||
};
|
||||
|
||||
export interface DimensionEditorTab {
|
||||
export interface DimensionEditorGroupsOptions {
|
||||
enabled: boolean;
|
||||
state: boolean;
|
||||
onClick: () => void;
|
||||
|
@ -120,25 +120,47 @@ export interface DimensionEditorTab {
|
|||
label: string;
|
||||
}
|
||||
|
||||
export const DimensionEditorTabs = ({ tabs }: { tabs: DimensionEditorTab[] }) => {
|
||||
export const DimensionEditorButtonGroups = ({
|
||||
options,
|
||||
onMethodChange,
|
||||
selectedMethod,
|
||||
}: {
|
||||
options: DimensionEditorGroupsOptions[];
|
||||
onMethodChange: (id: string) => void;
|
||||
selectedMethod: string;
|
||||
}) => {
|
||||
const enabledGroups = options.filter(({ enabled }) => enabled);
|
||||
const groups = enabledGroups.map(({ id, label }) => {
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
'data-test-subj': `lens-dimensionTabs-${id}`,
|
||||
};
|
||||
});
|
||||
|
||||
const onChange = (optionId: string) => {
|
||||
onMethodChange(optionId);
|
||||
const selectedOption = options.find(({ id }) => id === optionId);
|
||||
selectedOption?.onClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiTabs
|
||||
size="s"
|
||||
className="lnsIndexPatternDimensionEditor__header"
|
||||
data-test-subj="lens-dimensionTabs"
|
||||
>
|
||||
{tabs.map(({ id, enabled, state, onClick, label }) => {
|
||||
return enabled ? (
|
||||
<EuiTab
|
||||
key={id}
|
||||
isSelected={state}
|
||||
data-test-subj={`lens-dimensionTabs-${id}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
</EuiTab>
|
||||
) : null;
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.indexPattern.dimensionEditor.headingMethod', {
|
||||
defaultMessage: 'Method',
|
||||
})}
|
||||
</EuiTabs>
|
||||
fullWidth
|
||||
>
|
||||
<EuiButtonGroup
|
||||
legend={i18n.translate('xpack.lens.indexPattern.dimensionEditorModes', {
|
||||
defaultMessage: 'Dimension editor configuration modes',
|
||||
})}
|
||||
buttonSize="compressed"
|
||||
isFullWidth
|
||||
options={groups}
|
||||
idSelected={selectedMethod}
|
||||
onChange={(id) => onChange(id)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -108,13 +108,16 @@ export function TimeShift({
|
|||
const localValueNotMultiple = parsedLocalValue && isValueNotMultiple(parsedLocalValue);
|
||||
|
||||
function getSelectedOption() {
|
||||
if (!localValue) return [];
|
||||
const goodPick = timeShiftOptions.filter(({ value }) => value === localValue);
|
||||
if (goodPick.length > 0) return goodPick;
|
||||
return [
|
||||
{
|
||||
value: localValue,
|
||||
label: localValue,
|
||||
value: localValue ?? '',
|
||||
label:
|
||||
localValue ??
|
||||
i18n.translate('xpack.lens.timeShift.none', {
|
||||
defaultMessage: 'None',
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -180,7 +183,7 @@ export function TimeShift({
|
|||
}
|
||||
}}
|
||||
onChange={(choices) => {
|
||||
if (choices.length === 0) {
|
||||
if (choices.length === 0 || (choices.length && choices[0].value === '')) {
|
||||
updateLayer(setTimeShift(columnId, layer, ''));
|
||||
setLocalValue('');
|
||||
return;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { EuiSwitch } from '@elastic/eui';
|
||||
import { EuiSwitch, EuiText } from '@elastic/eui';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { AggFunctionsMapping } from '@kbn/data-plugin/public';
|
||||
import { buildExpressionFunction } from '@kbn/expressions-plugin/public';
|
||||
|
@ -136,9 +136,13 @@ export const cardinalityOperation: OperationDefinition<
|
|||
dataTestSubj: 'hide-zero-values',
|
||||
inlineElement: (
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.lens.indexPattern.hideZero', {
|
||||
defaultMessage: 'Hide zero values',
|
||||
})}
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('xpack.lens.indexPattern.hideZero', {
|
||||
defaultMessage: 'Hide zero values',
|
||||
})}
|
||||
</EuiText>
|
||||
}
|
||||
labelProps={{
|
||||
style: {
|
||||
fontWeight: euiThemeVars.euiFontWeightMedium,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { EuiSwitch } from '@elastic/eui';
|
||||
import { EuiSwitch, EuiText } from '@elastic/eui';
|
||||
import { AggFunctionsMapping } from '@kbn/data-plugin/public';
|
||||
import { buildExpressionFunction } from '@kbn/expressions-plugin/public';
|
||||
import { TimeScaleUnit } from '../../../../common/expressions';
|
||||
|
@ -141,9 +141,13 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field
|
|||
dataTestSubj: 'hide-zero-values',
|
||||
inlineElement: (
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.lens.indexPattern.hideZero', {
|
||||
defaultMessage: 'Hide zero values',
|
||||
})}
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('xpack.lens.indexPattern.hideZero', {
|
||||
defaultMessage: 'Hide zero values',
|
||||
})}
|
||||
</EuiText>
|
||||
}
|
||||
labelProps={{
|
||||
style: {
|
||||
fontWeight: euiThemeVars.euiFontWeightMedium,
|
||||
|
|
|
@ -429,6 +429,7 @@ describe('date_histogram', () => {
|
|||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={thirdLayer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
indexPattern={{ ...indexPattern1, timeFieldName: '@timestamp' }}
|
||||
/>
|
||||
);
|
||||
instance
|
||||
|
@ -558,7 +559,9 @@ describe('date_histogram', () => {
|
|||
indexPattern={{ ...indexPattern1, timeFieldName: 'other_timestamp' }}
|
||||
/>
|
||||
);
|
||||
expect(instance.find(EuiSwitch).first().prop('disabled')).toBeTruthy();
|
||||
expect(
|
||||
instance.find('[data-test-subj="lensDropPartialIntervals"]').prop('disabled')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should force calendar values to 1', () => {
|
||||
|
@ -754,12 +757,9 @@ describe('date_histogram', () => {
|
|||
currentColumn={thirdLayer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
/>
|
||||
);
|
||||
instance
|
||||
.find(EuiSwitch)
|
||||
.first()
|
||||
.simulate('change', {
|
||||
target: { checked: true },
|
||||
});
|
||||
instance.find('[data-test-subj="lensDropPartialIntervals"]').simulate('change', {
|
||||
target: { checked: true },
|
||||
});
|
||||
expect(updateLayerSpy).toHaveBeenCalled();
|
||||
const newLayer = updateLayerSpy.mock.calls[0][0](layer);
|
||||
expect(newLayer).toHaveProperty('columns.col1.params.dropPartials', true);
|
||||
|
|
|
@ -17,6 +17,8 @@ import {
|
|||
EuiIconTip,
|
||||
EuiSwitch,
|
||||
EuiSwitchEvent,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
AggFunctionsMapping,
|
||||
|
@ -264,38 +266,115 @@ export const dateHistogramOperation: OperationDefinition<
|
|||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFormRow display="rowCompressed" hasChildLabel={false}>
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.lens.indexPattern.dateHistogram.dropPartialBucketsHelp',
|
||||
{
|
||||
defaultMessage:
|
||||
'Drop partial buckets is disabled as these can be computed only for a time field bound to global time picker in the top right.',
|
||||
}
|
||||
)}
|
||||
condition={!bindToGlobalTimePickerValue}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.lens.indexPattern.dateHistogram.dropPartialBuckets', {
|
||||
defaultMessage: 'Drop partial buckets',
|
||||
})}
|
||||
checked={Boolean(currentColumn.params.dropPartials)}
|
||||
onChange={onChangeDropPartialBuckets}
|
||||
compressed
|
||||
disabled={!bindToGlobalTimePickerValue}
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('xpack.lens.indexPattern.dateHistogram.includeEmptyRows', {
|
||||
defaultMessage: 'Include empty rows',
|
||||
})}
|
||||
</EuiText>
|
||||
}
|
||||
checked={Boolean(currentColumn.params.includeEmptyRows)}
|
||||
data-test-subj="indexPattern-include-empty-rows"
|
||||
onChange={() => {
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'includeEmptyRows',
|
||||
value: !currentColumn.params.includeEmptyRows,
|
||||
})
|
||||
);
|
||||
}}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{indexPattern.timeFieldName !== field?.name && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFormRow display="rowCompressed" hasChildLabel={false}>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
{i18n.translate(
|
||||
'xpack.lens.indexPattern.dateHistogram.bindToGlobalTimePicker',
|
||||
{
|
||||
defaultMessage: 'Bind to global time picker',
|
||||
}
|
||||
)}{' '}
|
||||
<EuiIconTip
|
||||
color="subdued"
|
||||
content={i18n.translate(
|
||||
'xpack.lens.indexPattern.dateHistogram.globalTimePickerHelp',
|
||||
{
|
||||
defaultMessage:
|
||||
"Filter the selected field by the global time picker in the top right. This setting can't be turned off for the default time field of the current data view.",
|
||||
}
|
||||
)}
|
||||
iconProps={{
|
||||
className: 'eui-alignTop',
|
||||
}}
|
||||
position="top"
|
||||
size="s"
|
||||
type="questionInCircle"
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
disabled={indexPattern.timeFieldName === field?.name}
|
||||
checked={bindToGlobalTimePickerValue}
|
||||
onChange={() => {
|
||||
let newLayer = updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'ignoreTimeRange',
|
||||
value: !currentColumn.params.ignoreTimeRange,
|
||||
});
|
||||
if (
|
||||
!currentColumn.params.ignoreTimeRange &&
|
||||
currentColumn.params.interval === autoInterval
|
||||
) {
|
||||
const newFixedInterval =
|
||||
data.search.aggs.calculateAutoTimeExpression({
|
||||
from: dateRange.fromDate,
|
||||
to: dateRange.toDate,
|
||||
}) || '1h';
|
||||
newLayer = updateColumnParam({
|
||||
layer: newLayer,
|
||||
columnId,
|
||||
paramName: 'interval',
|
||||
value: newFixedInterval,
|
||||
});
|
||||
setIntervalInput(newFixedInterval);
|
||||
}
|
||||
paramEditorUpdater(newLayer);
|
||||
}}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
)}
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.indexPattern.dateHistogram.minimumInterval', {
|
||||
defaultMessage: 'Minimum interval',
|
||||
})}
|
||||
fullWidth
|
||||
display="rowCompressed"
|
||||
helpText={i18n.translate('xpack.lens.indexPattern.dateHistogram.selectOptionHelpText', {
|
||||
defaultMessage:
|
||||
'Select an option or create a custom value. Examples: 30s, 20m, 24h, 2d, 1w, 1M',
|
||||
})}
|
||||
helpText={
|
||||
<>
|
||||
{i18n.translate('xpack.lens.indexPattern.dateHistogram.selectOptionHelpText', {
|
||||
defaultMessage: `Select an option or create a custom value.`,
|
||||
})}
|
||||
<br />
|
||||
{i18n.translate(
|
||||
'xpack.lens.indexPattern.dateHistogram.selectOptionExamplesHelpText',
|
||||
{
|
||||
defaultMessage: `Examples: 30s, 20m, 24h, 2d, 1w, 1M`,
|
||||
}
|
||||
)}
|
||||
</>
|
||||
}
|
||||
isInvalid={!isValid}
|
||||
error={
|
||||
!isValid &&
|
||||
|
@ -346,81 +425,33 @@ export const dateHistogramOperation: OperationDefinition<
|
|||
/>
|
||||
)}
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFormRow display="rowCompressed" hasChildLabel={false}>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<>
|
||||
{i18n.translate('xpack.lens.indexPattern.dateHistogram.bindToGlobalTimePicker', {
|
||||
defaultMessage: 'Bind to global time picker',
|
||||
})}{' '}
|
||||
<EuiIconTip
|
||||
color="subdued"
|
||||
content={i18n.translate(
|
||||
'xpack.lens.indexPattern.dateHistogram.globalTimePickerHelp',
|
||||
{
|
||||
defaultMessage:
|
||||
"Filter the selected field by the global time picker in the top right. This setting can't be turned off for the default time field of the current data view.",
|
||||
}
|
||||
)}
|
||||
iconProps={{
|
||||
className: 'eui-alignTop',
|
||||
}}
|
||||
position="top"
|
||||
size="s"
|
||||
type="questionInCircle"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
disabled={indexPattern.timeFieldName === field?.name}
|
||||
checked={bindToGlobalTimePickerValue}
|
||||
onChange={() => {
|
||||
let newLayer = updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'ignoreTimeRange',
|
||||
value: !currentColumn.params.ignoreTimeRange,
|
||||
});
|
||||
if (
|
||||
!currentColumn.params.ignoreTimeRange &&
|
||||
currentColumn.params.interval === autoInterval
|
||||
) {
|
||||
const newFixedInterval =
|
||||
data.search.aggs.calculateAutoTimeExpression({
|
||||
from: dateRange.fromDate,
|
||||
to: dateRange.toDate,
|
||||
}) || '1h';
|
||||
newLayer = updateColumnParam({
|
||||
layer: newLayer,
|
||||
columnId,
|
||||
paramName: 'interval',
|
||||
value: newFixedInterval,
|
||||
});
|
||||
setIntervalInput(newFixedInterval);
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.lens.indexPattern.dateHistogram.dropPartialBucketsHelp',
|
||||
{
|
||||
defaultMessage:
|
||||
'Drop partial intervals is disabled as these can be computed only for a time field bound to global time picker in the top right.',
|
||||
}
|
||||
paramEditorUpdater(newLayer);
|
||||
}}
|
||||
compressed
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow display="rowCompressed" hasChildLabel={false}>
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.lens.indexPattern.dateHistogram.includeEmptyRows', {
|
||||
defaultMessage: 'Include empty rows',
|
||||
})}
|
||||
checked={Boolean(currentColumn.params.includeEmptyRows)}
|
||||
data-test-subj="indexPattern-include-empty-rows"
|
||||
onChange={() => {
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'includeEmptyRows',
|
||||
value: !currentColumn.params.includeEmptyRows,
|
||||
})
|
||||
);
|
||||
}}
|
||||
compressed
|
||||
/>
|
||||
)}
|
||||
condition={!bindToGlobalTimePickerValue}
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('xpack.lens.indexPattern.dateHistogram.dropPartialBuckets', {
|
||||
defaultMessage: 'Drop partial intervals',
|
||||
})}
|
||||
</EuiText>
|
||||
}
|
||||
data-test-subj="lensDropPartialIntervals"
|
||||
checked={Boolean(currentColumn.params.dropPartials)}
|
||||
onChange={onChangeDropPartialBuckets}
|
||||
compressed
|
||||
disabled={!bindToGlobalTimePickerValue}
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -30,13 +30,15 @@
|
|||
|
||||
.lnsFormula__editorHeader,
|
||||
.lnsFormula__editorFooter {
|
||||
padding: $euiSizeS $euiSize;
|
||||
padding: $euiSizeS;
|
||||
}
|
||||
|
||||
.lnsFormula__editorFooter {
|
||||
// make sure docs are rendered in front of monaco
|
||||
z-index: 1;
|
||||
background-color: $euiColorLightestShade;
|
||||
border-bottom-right-radius: $euiBorderRadius;
|
||||
border-bottom-left-radius: $euiBorderRadius;
|
||||
}
|
||||
|
||||
.lnsFormula__editorHeaderGroup,
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
|
||||
import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiButtonEmpty,
|
||||
EuiFormLabel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
|
@ -18,6 +20,7 @@ import {
|
|||
EuiText,
|
||||
EuiToolTip,
|
||||
EuiSpacer,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import useUnmount from 'react-use/lib/useUnmount';
|
||||
import { monaco } from '@kbn/monaco';
|
||||
|
@ -112,6 +115,8 @@ export function FormulaEditor({
|
|||
const disposables = React.useRef<monaco.IDisposable[]>([]);
|
||||
const editor1 = React.useRef<monaco.editor.IStandaloneCodeEditor>();
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const visibleOperationsMap = useMemo(
|
||||
() => filterByVisibleOperation(operationDefinitionMap),
|
||||
[operationDefinitionMap]
|
||||
|
@ -651,7 +656,26 @@ export function FormulaEditor({
|
|||
'lnsIndexPatternDimensionEditor-isFullscreen': isFullscreen,
|
||||
})}
|
||||
>
|
||||
<div className="lnsIndexPatternDimensionEditor__section lnsIndexPatternDimensionEditor__section--shaded">
|
||||
{!isFullscreen && (
|
||||
<EuiFormLabel
|
||||
css={css`
|
||||
margin-top: ${euiTheme.size.base};
|
||||
margin-bottom: ${euiTheme.size.xs};
|
||||
`}
|
||||
>
|
||||
{i18n.translate('xpack.lens.indexPattern.dimensionEditor.headingFormula', {
|
||||
defaultMessage: 'Formula',
|
||||
})}
|
||||
</EuiFormLabel>
|
||||
)}
|
||||
<div
|
||||
className="lnsIndexPatternDimensionEditor--shaded"
|
||||
css={css`
|
||||
border: ${!isFullscreen ? euiTheme.border.thin : 'none'};
|
||||
border-radius: ${!isFullscreen ? euiTheme.border.radius.medium : 0};
|
||||
height: ${isFullscreen ? '100%' : 'auto'};
|
||||
`}
|
||||
>
|
||||
<div className="lnsFormula">
|
||||
<div className="lnsFormula__editor">
|
||||
<div className="lnsFormula__editorHeader">
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
EuiComboBoxOptionOption,
|
||||
EuiSwitch,
|
||||
EuiToolTip,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { AggFunctionsMapping } from '@kbn/data-plugin/public';
|
||||
import { buildExpressionFunction } from '@kbn/expressions-plugin/public';
|
||||
|
@ -296,6 +297,46 @@ export const lastValueOperation: OperationDefinition<
|
|||
|
||||
return (
|
||||
<>
|
||||
{!isReferenced && (
|
||||
<EuiFormRow
|
||||
error={i18n.translate(
|
||||
'xpack.lens.indexPattern.lastValue.showArrayValuesWithTopValuesWarning',
|
||||
{
|
||||
defaultMessage:
|
||||
'When you show array values, you are unable to use this field to rank top values.',
|
||||
}
|
||||
)}
|
||||
isInvalid={currentColumn.params.showArrayValues && usingTopValues}
|
||||
display="rowCompressed"
|
||||
fullWidth
|
||||
data-test-subj="lns-indexPattern-lastValue-showArrayValues"
|
||||
>
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.lens.indexPattern.lastValue.showArrayValuesExplanation',
|
||||
{
|
||||
defaultMessage:
|
||||
'Displays all values associated with this field in each last document.',
|
||||
}
|
||||
)}
|
||||
position="left"
|
||||
>
|
||||
<EuiSwitch
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('xpack.lens.indexPattern.lastValue.showArrayValues', {
|
||||
defaultMessage: 'Show array values',
|
||||
})}
|
||||
</EuiText>
|
||||
}
|
||||
compressed={true}
|
||||
checked={Boolean(currentColumn.params.showArrayValues)}
|
||||
disabled={isScriptedField(currentColumn.sourceField, indexPattern)}
|
||||
onChange={() => setShowArrayValues(!currentColumn.params.showArrayValues)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<FormRow
|
||||
isInline={isInline}
|
||||
label={sortByFieldLabel}
|
||||
|
@ -349,42 +390,6 @@ export const lastValueOperation: OperationDefinition<
|
|||
}
|
||||
/>
|
||||
</FormRow>
|
||||
{!isReferenced && (
|
||||
<EuiFormRow
|
||||
error={i18n.translate(
|
||||
'xpack.lens.indexPattern.lastValue.showArrayValuesWithTopValuesWarning',
|
||||
{
|
||||
defaultMessage:
|
||||
'When you show array values, you are unable to use this field to rank top values.',
|
||||
}
|
||||
)}
|
||||
isInvalid={currentColumn.params.showArrayValues && usingTopValues}
|
||||
display="rowCompressed"
|
||||
fullWidth
|
||||
data-test-subj="lns-indexPattern-lastValue-showArrayValues"
|
||||
>
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.lens.indexPattern.lastValue.showArrayValuesExplanation',
|
||||
{
|
||||
defaultMessage:
|
||||
'Displays all values associated with this field in each last document.',
|
||||
}
|
||||
)}
|
||||
position="left"
|
||||
>
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.lens.indexPattern.lastValue.showArrayValues', {
|
||||
defaultMessage: 'Show array values',
|
||||
})}
|
||||
compressed={true}
|
||||
checked={Boolean(currentColumn.params.showArrayValues)}
|
||||
disabled={isScriptedField(currentColumn.sourceField, indexPattern)}
|
||||
onChange={() => setShowArrayValues(!currentColumn.params.showArrayValues)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { EuiSwitch } from '@elastic/eui';
|
||||
import { EuiSwitch, EuiText } from '@elastic/eui';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { buildExpressionFunction } from '@kbn/expressions-plugin/public';
|
||||
import { OperationDefinition, ParamEditorProps } from '.';
|
||||
|
@ -160,9 +160,13 @@ function buildMetricOperation<T extends MetricColumn<string>>({
|
|||
dataTestSubj: 'hide-zero-values',
|
||||
inlineElement: (
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.lens.indexPattern.hideZero', {
|
||||
defaultMessage: 'Hide zero values',
|
||||
})}
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('xpack.lens.indexPattern.hideZero', {
|
||||
defaultMessage: 'Hide zero values',
|
||||
})}
|
||||
</EuiText>
|
||||
}
|
||||
labelProps={{
|
||||
style: {
|
||||
fontWeight: euiThemeVars.euiFontWeightMedium,
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
EuiToolTip,
|
||||
EuiSwitch,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import type { IFieldFormat } from '@kbn/field-formats-plugin/common';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/public';
|
||||
|
@ -179,9 +180,13 @@ const BaseRangeEditor = ({
|
|||
<EuiSpacer size="s" />
|
||||
<EuiFormRow display="rowCompressed" hasChildLabel={false}>
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.lens.indexPattern.ranges.includeEmptyRows', {
|
||||
defaultMessage: 'Include empty rows',
|
||||
})}
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('xpack.lens.indexPattern.ranges.includeEmptyRows', {
|
||||
defaultMessage: 'Include empty rows',
|
||||
})}
|
||||
</EuiText>
|
||||
}
|
||||
checked={Boolean(includeEmptyRows)}
|
||||
onChange={() => {
|
||||
onChangeIncludeEmptyRows(!includeEmptyRows);
|
||||
|
|
|
@ -37,6 +37,7 @@ export const NewBucketButton = ({
|
|||
iconType="plusInCircle"
|
||||
onClick={onClick}
|
||||
isDisabled={isDisabled}
|
||||
flush="left"
|
||||
>
|
||||
{label}
|
||||
</EuiButtonEmpty>
|
||||
|
@ -131,12 +132,14 @@ export const DragDropBuckets = ({
|
|||
onDragEnd,
|
||||
droppableId,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
items: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
onDragStart: () => void;
|
||||
onDragEnd: (items: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
droppableId: string;
|
||||
children: React.ReactElement[];
|
||||
className?: string;
|
||||
}) => {
|
||||
const handleDragEnd = ({
|
||||
source,
|
||||
|
@ -152,7 +155,7 @@ export const DragDropBuckets = ({
|
|||
};
|
||||
return (
|
||||
<EuiDragDropContext onDragEnd={handleDragEnd} onDragStart={onDragStart}>
|
||||
<EuiDroppable droppableId={droppableId} spacing="none">
|
||||
<EuiDroppable droppableId={droppableId} spacing="none" className={className}>
|
||||
{children}
|
||||
</EuiDroppable>
|
||||
</EuiDragDropContext>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFieldNumber, EuiFormLabel, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiFieldNumber, EuiFormRow } from '@elastic/eui';
|
||||
import { OperationDefinition } from '.';
|
||||
import {
|
||||
ReferenceBasedIndexPatternColumn,
|
||||
|
@ -215,9 +215,7 @@ export const staticValueOperation: OperationDefinition<
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="lnsIndexPatternDimensionEditor__section lnsIndexPatternDimensionEditor__section--padded lnsIndexPatternDimensionEditor__section--shaded">
|
||||
<EuiFormLabel>{paramEditorCustomProps?.labels?.[0] || defaultLabel}</EuiFormLabel>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFormRow label={paramEditorCustomProps?.labels?.[0] || defaultLabel} fullWidth>
|
||||
<EuiFieldNumber
|
||||
fullWidth
|
||||
data-test-subj="lns-indexPattern-static_value-input"
|
||||
|
@ -226,7 +224,7 @@ export const staticValueOperation: OperationDefinition<
|
|||
onChange={onChangeHandler}
|
||||
step="any"
|
||||
/>
|
||||
</div>
|
||||
</EuiFormRow>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiDraggable,
|
||||
|
@ -13,6 +13,8 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
htmlIdGenerator,
|
||||
EuiPanel,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DragDropBuckets, NewBucketButton } from '../shared_components/buckets';
|
||||
|
@ -55,6 +57,9 @@ export function FieldInputs({
|
|||
operationSupportMatrix,
|
||||
invalidFields,
|
||||
}: FieldInputsProps) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const onChangeWrapped = useCallback(
|
||||
(values: WrappedValue[]) =>
|
||||
onChange(values.filter(removeNewEmptyField).map(({ value }) => value)),
|
||||
|
@ -87,152 +92,155 @@ export function FieldInputs({
|
|||
[localValues, indexPattern, handleInputChange]
|
||||
);
|
||||
|
||||
// diminish attention to adding fields alternative
|
||||
if (localValues.length === 1) {
|
||||
const [{ value }] = localValues;
|
||||
return (
|
||||
<>
|
||||
<FieldSelect
|
||||
fieldIsInvalid={Boolean(invalidFields?.[0])}
|
||||
currentIndexPattern={indexPattern}
|
||||
existingFields={existingFields}
|
||||
operationByField={operationSupportMatrix.operationByField}
|
||||
selectedOperationType={column?.operationType}
|
||||
selectedField={value}
|
||||
onChoose={onFieldSelectChange}
|
||||
/>
|
||||
<NewBucketButton
|
||||
data-test-subj={`indexPattern-terms-add-field`}
|
||||
onClick={() => {
|
||||
handleInputChange([
|
||||
...localValues,
|
||||
{ id: generateId(), value: undefined, isNew: true },
|
||||
]);
|
||||
}}
|
||||
label={i18n.translate('xpack.lens.indexPattern.terms.addField', {
|
||||
defaultMessage: 'Add field',
|
||||
})}
|
||||
isDisabled={column.params.orderBy.type === 'rare'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
const disableActions = localValues.length === 2 && localValues.some(({ isNew }) => isNew);
|
||||
const disableActions =
|
||||
(localValues.length === 2 && localValues.some(({ isNew }) => isNew)) ||
|
||||
localValues.length === 1;
|
||||
const localValuesFilled = localValues.filter(({ isNew }) => !isNew);
|
||||
return (
|
||||
<>
|
||||
<DragDropBuckets
|
||||
onDragEnd={(updatedValues: WrappedValue[]) => {
|
||||
handleInputChange(updatedValues);
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: isDragging ? 'transparent' : euiTheme.colors.lightestShade,
|
||||
borderRadius: euiTheme.size.xs,
|
||||
marginBottom: euiTheme.size.xs,
|
||||
}}
|
||||
onDragStart={() => {}}
|
||||
droppableId="TOP_TERMS_DROPPABLE_AREA"
|
||||
items={localValues}
|
||||
>
|
||||
{localValues.map(({ id, value, isNew }, index) => {
|
||||
// need to filter the available fields for multiple terms
|
||||
// * a scripted field should be removed
|
||||
// * a field of unsupported type should be removed
|
||||
// * a field that has been used
|
||||
// * a scripted field was used in a singular term, should be marked as invalid for multi-terms
|
||||
const filteredOperationByField = Object.keys(operationSupportMatrix.operationByField)
|
||||
.filter((key) => {
|
||||
if (key === value) {
|
||||
return true;
|
||||
}
|
||||
const field = indexPattern.getFieldByName(key);
|
||||
return (
|
||||
!rawValuesLookup.has(key) &&
|
||||
field &&
|
||||
!field.scripted &&
|
||||
supportedTypes.has(field.type)
|
||||
);
|
||||
})
|
||||
.reduce<OperationSupportMatrix['operationByField']>((memo, key) => {
|
||||
memo[key] = operationSupportMatrix.operationByField[key];
|
||||
return memo;
|
||||
}, {});
|
||||
<DragDropBuckets
|
||||
onDragEnd={(updatedValues: WrappedValue[]) => {
|
||||
handleInputChange(updatedValues);
|
||||
setIsDragging(false);
|
||||
}}
|
||||
className="lnsIndexPatternDimensionEditor__droppable"
|
||||
onDragStart={() => {
|
||||
setIsDragging(true);
|
||||
}}
|
||||
droppableId="TOP_TERMS_DROPPABLE_AREA"
|
||||
items={localValues}
|
||||
>
|
||||
{localValues.map(({ id, value, isNew }, index) => {
|
||||
// need to filter the available fields for multiple terms
|
||||
// * a scripted field should be removed
|
||||
// * a field of unsupported type should be removed
|
||||
// * a field that has been used
|
||||
// * a scripted field was used in a singular term, should be marked as invalid for multi-terms
|
||||
const filteredOperationByField = Object.keys(operationSupportMatrix.operationByField)
|
||||
.filter((key) => {
|
||||
if (key === value) {
|
||||
return true;
|
||||
}
|
||||
const field = indexPattern.getFieldByName(key);
|
||||
if (index === 0) {
|
||||
return !rawValuesLookup.has(key) && field && supportedTypes.has(field.type);
|
||||
} else {
|
||||
return (
|
||||
!rawValuesLookup.has(key) &&
|
||||
field &&
|
||||
!field.scripted &&
|
||||
supportedTypes.has(field.type)
|
||||
);
|
||||
}
|
||||
})
|
||||
.reduce<OperationSupportMatrix['operationByField']>((memo, key) => {
|
||||
memo[key] = operationSupportMatrix.operationByField[key];
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
const shouldShowError = Boolean(
|
||||
value &&
|
||||
((indexPattern.getFieldByName(value)?.scripted && localValuesFilled.length > 1) ||
|
||||
invalidFields?.includes(value))
|
||||
);
|
||||
return (
|
||||
<EuiDraggable
|
||||
style={{ marginBottom: 4 }}
|
||||
spacing="none"
|
||||
index={index}
|
||||
draggableId={value || 'newField'}
|
||||
key={id}
|
||||
disableInteractiveElementBlocking
|
||||
>
|
||||
{(provided) => (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>{/* Empty for spacing */}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon
|
||||
size="s"
|
||||
color="subdued"
|
||||
type="grab"
|
||||
title={i18n.translate('xpack.lens.indexPattern.terms.dragToReorder', {
|
||||
defaultMessage: 'Drag to reorder',
|
||||
})}
|
||||
data-test-subj={`indexPattern-terms-dragToReorder-${index}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true} style={{ minWidth: 0 }}>
|
||||
<FieldSelect
|
||||
fieldIsInvalid={shouldShowError}
|
||||
currentIndexPattern={indexPattern}
|
||||
existingFields={existingFields}
|
||||
operationByField={filteredOperationByField}
|
||||
selectedOperationType={column.operationType}
|
||||
selectedField={value}
|
||||
autoFocus={isNew}
|
||||
onChoose={(choice) => {
|
||||
onFieldSelectChange(choice, index);
|
||||
}}
|
||||
isInvalid={shouldShowError}
|
||||
data-test-subj={`indexPattern-dimension-field-${index}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.lens.indexPattern.terms.deleteButtonDisabled',
|
||||
{
|
||||
defaultMessage: 'This function requires a minimum of one field defined',
|
||||
}
|
||||
)}
|
||||
condition={disableActions}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.lens.indexPattern.terms.deleteButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Delete',
|
||||
const shouldShowError = Boolean(
|
||||
value &&
|
||||
((indexPattern.getFieldByName(value)?.scripted && localValuesFilled.length > 1) ||
|
||||
invalidFields?.includes(value))
|
||||
);
|
||||
return (
|
||||
<EuiDraggable
|
||||
spacing="none"
|
||||
index={index}
|
||||
draggableId={value || 'newField'}
|
||||
key={id}
|
||||
disableInteractiveElementBlocking
|
||||
>
|
||||
{(provided) => (
|
||||
<EuiPanel paddingSize="xs" hasShadow={false} color="transparent">
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
className="lnsIndexPatternDimensionEditor__droppableItem"
|
||||
>
|
||||
<EuiIcon
|
||||
size="s"
|
||||
color="text"
|
||||
type="grab"
|
||||
title={i18n.translate('xpack.lens.indexPattern.terms.dragToReorder', {
|
||||
defaultMessage: 'Drag to reorder',
|
||||
})}
|
||||
data-test-subj={`indexPattern-terms-dragToReorder-${index}`}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={true}
|
||||
style={{ minWidth: 0 }}
|
||||
className="lnsIndexPatternDimensionEditor__droppableItem"
|
||||
>
|
||||
<FieldSelect
|
||||
fieldIsInvalid={shouldShowError}
|
||||
currentIndexPattern={indexPattern}
|
||||
existingFields={existingFields}
|
||||
operationByField={filteredOperationByField}
|
||||
selectedOperationType={column.operationType}
|
||||
selectedField={value}
|
||||
autoFocus={isNew}
|
||||
onChoose={(choice) => {
|
||||
onFieldSelectChange(choice, index);
|
||||
}}
|
||||
isInvalid={shouldShowError}
|
||||
data-test-subj={
|
||||
localValues.length !== 1
|
||||
? `indexPattern-dimension-field-${index}`
|
||||
: undefined
|
||||
}
|
||||
)}
|
||||
title={i18n.translate('xpack.lens.indexPattern.terms.deleteButtonLabel', {
|
||||
defaultMessage: 'Delete',
|
||||
})}
|
||||
onClick={() => {
|
||||
handleInputChange(localValues.filter((_, i) => i !== index));
|
||||
}}
|
||||
data-test-subj={`indexPattern-terms-removeField-${index}`}
|
||||
isDisabled={disableActions && !isNew}
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiDraggable>
|
||||
);
|
||||
})}
|
||||
</DragDropBuckets>
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.lens.indexPattern.terms.deleteButtonDisabled',
|
||||
{
|
||||
defaultMessage:
|
||||
'This function requires a minimum of one field defined',
|
||||
}
|
||||
)}
|
||||
condition={disableActions}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.lens.indexPattern.terms.deleteButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Delete',
|
||||
}
|
||||
)}
|
||||
title={i18n.translate(
|
||||
'xpack.lens.indexPattern.terms.deleteButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Delete',
|
||||
}
|
||||
)}
|
||||
onClick={() => {
|
||||
handleInputChange(localValues.filter((_, i) => i !== index));
|
||||
}}
|
||||
data-test-subj={`indexPattern-terms-removeField-${index}`}
|
||||
isDisabled={disableActions && !isNew}
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
)}
|
||||
</EuiDraggable>
|
||||
);
|
||||
})}
|
||||
</DragDropBuckets>
|
||||
</div>
|
||||
<NewBucketButton
|
||||
onClick={() => {
|
||||
handleInputChange([...localValues, { id: generateId(), value: undefined, isNew: true }]);
|
||||
|
@ -241,7 +249,9 @@ export function FieldInputs({
|
|||
label={i18n.translate('xpack.lens.indexPattern.terms.addaFilter', {
|
||||
defaultMessage: 'Add field',
|
||||
})}
|
||||
isDisabled={localValues.length > MAX_MULTI_FIELDS_SIZE}
|
||||
isDisabled={
|
||||
column.params.orderBy.type === 'rare' || localValues.length > MAX_MULTI_FIELDS_SIZE
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -19,6 +19,8 @@ import {
|
|||
EuiButtonGroup,
|
||||
EuiText,
|
||||
useEuiTheme,
|
||||
EuiTitle,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
import { uniq } from 'lodash';
|
||||
import { AggFunctionsMapping } from '@kbn/data-plugin/public';
|
||||
|
@ -916,44 +918,33 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
</EuiFormRow>
|
||||
{!hasRestrictions && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSpacer size="m" />
|
||||
<EuiAccordion
|
||||
id="lnsTermsAdvanced"
|
||||
arrowProps={{ color: 'primary' }}
|
||||
buttonContent={
|
||||
<EuiText size="s" color={euiTheme.colors.primary}>
|
||||
{i18n.translate('xpack.lens.indexPattern.terms.advancedSettings', {
|
||||
defaultMessage: 'Advanced',
|
||||
})}
|
||||
</EuiText>
|
||||
<EuiTitle size="xxs">
|
||||
<h5>
|
||||
<EuiTextColor color={euiTheme.colors.primary}>
|
||||
{i18n.translate('xpack.lens.indexPattern.terms.advancedSettings', {
|
||||
defaultMessage: 'Advanced',
|
||||
})}
|
||||
</EuiTextColor>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
}
|
||||
data-test-subj="indexPattern-terms-advanced"
|
||||
className="lnsIndexPatternDimensionEditor-advancedOptions"
|
||||
>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.lens.indexPattern.terms.otherBucketDescription', {
|
||||
defaultMessage: 'Group other values as "Other"',
|
||||
})}
|
||||
compressed
|
||||
data-test-subj="indexPattern-terms-other-bucket"
|
||||
checked={Boolean(currentColumn.params.otherBucket)}
|
||||
disabled={currentColumn.params.orderBy.type === 'rare'}
|
||||
onChange={(e: EuiSwitchEvent) =>
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'otherBucket',
|
||||
value: e.target.checked,
|
||||
})
|
||||
)
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('xpack.lens.indexPattern.terms.missingBucketDescription', {
|
||||
defaultMessage: 'Include documents without the selected field',
|
||||
})}
|
||||
</EuiText>
|
||||
}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.lens.indexPattern.terms.missingBucketDescription', {
|
||||
defaultMessage: 'Include documents without this field',
|
||||
})}
|
||||
compressed
|
||||
disabled={
|
||||
!currentColumn.params.otherBucket ||
|
||||
|
@ -973,10 +964,34 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
)
|
||||
}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSwitch
|
||||
label={
|
||||
<>
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('xpack.lens.indexPattern.terms.otherBucketDescription', {
|
||||
defaultMessage: 'Group remaining values as "Other"',
|
||||
})}
|
||||
</EuiText>
|
||||
}
|
||||
compressed
|
||||
data-test-subj="indexPattern-terms-other-bucket"
|
||||
checked={Boolean(currentColumn.params.otherBucket)}
|
||||
disabled={currentColumn.params.orderBy.type === 'rare'}
|
||||
onChange={(e: EuiSwitchEvent) =>
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'otherBucket',
|
||||
value: e.target.checked,
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiSwitch
|
||||
label={
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('xpack.lens.indexPattern.terms.accuracyModeDescription', {
|
||||
defaultMessage: 'Enable accuracy mode',
|
||||
})}{' '}
|
||||
|
@ -992,7 +1007,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
size="s"
|
||||
type="questionInCircle"
|
||||
/>
|
||||
</>
|
||||
</EuiText>
|
||||
}
|
||||
compressed
|
||||
disabled={currentColumn.params.orderBy.type === 'rare'}
|
||||
|
|
|
@ -1346,7 +1346,7 @@ describe('terms', () => {
|
|||
).toBe('Invalid field: "timestamp". Check your data view or pick another field.');
|
||||
});
|
||||
|
||||
it('should render the an add button for single layer, but no other hints', () => {
|
||||
it('should render the an add button for single layer and disabled the remove button', () => {
|
||||
const updateLayerSpy = jest.fn();
|
||||
const existingFields = getExistingFields();
|
||||
const operationSupportMatrix = getDefaultOperationSupportMatrix('col1', existingFields);
|
||||
|
@ -1366,7 +1366,15 @@ describe('terms', () => {
|
|||
instance.find('[data-test-subj="indexPattern-terms-add-field"]').exists()
|
||||
).toBeTruthy();
|
||||
|
||||
expect(instance.find('[data-test-subj^="indexPattern-terms-removeField-"]').length).toBe(0);
|
||||
expect(instance.find('[data-test-subj^="indexPattern-terms-removeField-"]').length).not.toBe(
|
||||
0
|
||||
);
|
||||
expect(
|
||||
instance
|
||||
.find('[data-test-subj^="indexPattern-terms-removeField-"]')
|
||||
.first()
|
||||
.prop('isDisabled')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should switch to the first supported operation when in single term mode and the picked field is not supported', () => {
|
||||
|
@ -1583,7 +1591,7 @@ describe('terms', () => {
|
|||
);
|
||||
|
||||
expect(
|
||||
instance.find('[data-test-subj="indexPattern-dimension-field"]').first().prop('options')
|
||||
instance.find('[data-test-subj="indexPattern-dimension-field"]').at(1).prop('options')
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
|
|
|
@ -22,6 +22,12 @@ import {
|
|||
import { FramePublicAPI } from '../types';
|
||||
|
||||
export const timeShiftOptions = [
|
||||
{
|
||||
label: i18n.translate('xpack.lens.indexPattern.timeShift.none', {
|
||||
defaultMessage: 'None',
|
||||
}),
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
label: i18n.translate('xpack.lens.indexPattern.timeShift.hour', {
|
||||
defaultMessage: '1 hour ago (1h)',
|
||||
|
|
|
@ -235,6 +235,11 @@ export const getLegacyMetricVisualization = ({
|
|||
groups: [
|
||||
{
|
||||
groupId: 'metric',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.metric.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
groupLabel: i18n.translate('xpack.lens.legacyMetric.label', {
|
||||
defaultMessage: 'Legacy Metric',
|
||||
}),
|
||||
|
|
|
@ -149,6 +149,9 @@ export const getPieVisualization = ({
|
|||
groupLabel: i18n.translate('xpack.lens.pie.sliceGroupLabel', {
|
||||
defaultMessage: 'Slice by',
|
||||
}),
|
||||
dimensionEditorGroupLabel: i18n.translate('xpack.lens.pie.sliceDimensionGroupLabel', {
|
||||
defaultMessage: 'Slice',
|
||||
}),
|
||||
supportsMoreColumns: sortedColumns.length < PartitionChartsMeta.pie.maxBuckets,
|
||||
dataTestSubj: 'lnsPie_sliceByDimensionPanel',
|
||||
};
|
||||
|
@ -158,6 +161,9 @@ export const getPieVisualization = ({
|
|||
groupLabel: i18n.translate('xpack.lens.pie.treemapGroupLabel', {
|
||||
defaultMessage: 'Group by',
|
||||
}),
|
||||
dimensionEditorGroupLabel: i18n.translate('xpack.lens.pie.treemapDimensionGroupLabel', {
|
||||
defaultMessage: 'Group',
|
||||
}),
|
||||
supportsMoreColumns: sortedColumns.length < PartitionChartsMeta[state.shape].maxBuckets,
|
||||
dataTestSubj: 'lnsPie_groupByDimensionPanel',
|
||||
requiredMinDimensionCount: PartitionChartsMeta[state.shape].requiredMinDimensionCount,
|
||||
|
@ -170,6 +176,14 @@ export const getPieVisualization = ({
|
|||
groupLabel: i18n.translate('xpack.lens.pie.groupsizeLabel', {
|
||||
defaultMessage: 'Size by',
|
||||
}),
|
||||
dimensionEditorGroupLabel: i18n.translate('xpack.lens.pie.groupSizeLabel', {
|
||||
defaultMessage: 'Size',
|
||||
}),
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.pie.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
accessors: layer.metric ? [{ columnId: layer.metric }] : [],
|
||||
supportsMoreColumns: !layer.metric,
|
||||
filterOperations: numberMetricOperations,
|
||||
|
|
|
@ -82,7 +82,7 @@ export function PalettePanelContainer({
|
|||
>
|
||||
<strong>
|
||||
{i18n.translate('xpack.lens.table.palettePanelTitle', {
|
||||
defaultMessage: 'Edit color',
|
||||
defaultMessage: 'Color',
|
||||
})}
|
||||
</strong>
|
||||
</h2>
|
||||
|
|
|
@ -444,6 +444,7 @@ export type DatasourceDimensionProps<T> = SharedDimensionProps & {
|
|||
export type ParamEditorCustomProps = Record<string, unknown> & {
|
||||
labels?: string[];
|
||||
isInline?: boolean;
|
||||
headingLabel?: string;
|
||||
};
|
||||
// The only way a visualization has to restrict the query building
|
||||
export type DatasourceDimensionEditorProps<T = unknown> = DatasourceDimensionProps<T> & {
|
||||
|
@ -586,6 +587,7 @@ export interface AccessorConfig {
|
|||
|
||||
export type VisualizationDimensionGroupConfig = SharedDimensionProps & {
|
||||
groupLabel: string;
|
||||
dimensionEditorGroupLabel?: string;
|
||||
groupTooltip?: string;
|
||||
|
||||
/** ID is passed back to visualization. For example, `x` */
|
||||
|
@ -907,6 +909,14 @@ export interface Visualization<T = unknown> {
|
|||
domElement: Element,
|
||||
props: VisualizationDimensionEditorProps<T>
|
||||
) => ((cleanupElement: Element) => void) | void;
|
||||
/**
|
||||
* Additional editor that gets rendered inside the dimension popover.
|
||||
* This can be used to configure dimension-specific options
|
||||
*/
|
||||
renderDimensionEditorAdditionalSection?: (
|
||||
domElement: Element,
|
||||
props: VisualizationDimensionEditorProps<T>
|
||||
) => ((cleanupElement: Element) => void) | void;
|
||||
/**
|
||||
* Renders dimension trigger. Used only for noDatasource layers
|
||||
*/
|
||||
|
|
|
@ -97,6 +97,9 @@ describe('gauge', () => {
|
|||
groups: [
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
},
|
||||
groupId: GROUP_ID.METRIC,
|
||||
groupLabel: 'Metric',
|
||||
accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }],
|
||||
|
@ -109,6 +112,10 @@ describe('gauge', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
labels: ['Minimum value'],
|
||||
},
|
||||
groupId: GROUP_ID.MIN,
|
||||
groupLabel: 'Minimum value',
|
||||
accessors: [{ columnId: 'min-accessor' }],
|
||||
|
@ -122,6 +129,10 @@ describe('gauge', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
labels: ['Maximum value'],
|
||||
},
|
||||
groupId: GROUP_ID.MAX,
|
||||
groupLabel: 'Maximum value',
|
||||
accessors: [{ columnId: 'max-accessor' }],
|
||||
|
@ -135,6 +146,10 @@ describe('gauge', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
labels: ['Goal value'],
|
||||
},
|
||||
groupId: GROUP_ID.GOAL,
|
||||
groupLabel: 'Goal value',
|
||||
accessors: [{ columnId: 'goal-accessor' }],
|
||||
|
@ -164,6 +179,9 @@ describe('gauge', () => {
|
|||
groups: [
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
},
|
||||
groupId: GROUP_ID.METRIC,
|
||||
groupLabel: 'Metric',
|
||||
accessors: [],
|
||||
|
@ -176,6 +194,10 @@ describe('gauge', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
labels: ['Minimum value'],
|
||||
},
|
||||
groupId: GROUP_ID.MIN,
|
||||
groupLabel: 'Minimum value',
|
||||
accessors: [{ columnId: 'min-accessor' }],
|
||||
|
@ -189,6 +211,10 @@ describe('gauge', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
labels: ['Maximum value'],
|
||||
},
|
||||
groupId: GROUP_ID.MAX,
|
||||
groupLabel: 'Maximum value',
|
||||
accessors: [],
|
||||
|
@ -202,6 +228,10 @@ describe('gauge', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
labels: ['Goal value'],
|
||||
},
|
||||
groupId: GROUP_ID.GOAL,
|
||||
groupLabel: 'Goal value',
|
||||
accessors: [],
|
||||
|
@ -237,6 +267,9 @@ describe('gauge', () => {
|
|||
groups: [
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
},
|
||||
groupId: GROUP_ID.METRIC,
|
||||
groupLabel: 'Metric',
|
||||
accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }],
|
||||
|
@ -249,6 +282,10 @@ describe('gauge', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
labels: ['Minimum value'],
|
||||
},
|
||||
groupId: GROUP_ID.MIN,
|
||||
groupLabel: 'Minimum value',
|
||||
accessors: [{ columnId: 'min-accessor' }],
|
||||
|
@ -262,6 +299,10 @@ describe('gauge', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
labels: ['Maximum value'],
|
||||
},
|
||||
groupId: GROUP_ID.MAX,
|
||||
groupLabel: 'Maximum value',
|
||||
accessors: [{ columnId: 'max-accessor' }],
|
||||
|
@ -275,6 +316,10 @@ describe('gauge', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
labels: ['Goal value'],
|
||||
},
|
||||
groupId: GROUP_ID.GOAL,
|
||||
groupLabel: 'Goal value',
|
||||
accessors: [{ columnId: 'goal-accessor' }],
|
||||
|
@ -315,6 +360,9 @@ describe('gauge', () => {
|
|||
groups: [
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
},
|
||||
groupId: GROUP_ID.METRIC,
|
||||
groupLabel: 'Metric',
|
||||
accessors: [{ columnId: 'metric-accessor', triggerIcon: 'none' }],
|
||||
|
@ -327,6 +375,10 @@ describe('gauge', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
labels: ['Minimum value'],
|
||||
},
|
||||
groupId: GROUP_ID.MIN,
|
||||
groupLabel: 'Minimum value',
|
||||
accessors: [{ columnId: 'min-accessor' }],
|
||||
|
@ -342,6 +394,10 @@ describe('gauge', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
labels: ['Maximum value'],
|
||||
},
|
||||
groupId: GROUP_ID.MAX,
|
||||
groupLabel: 'Maximum value',
|
||||
accessors: [{ columnId: 'max-accessor' }],
|
||||
|
@ -357,6 +413,10 @@ describe('gauge', () => {
|
|||
},
|
||||
{
|
||||
layerId: 'first',
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: 'Value',
|
||||
labels: ['Goal value'],
|
||||
},
|
||||
groupId: GROUP_ID.GOAL,
|
||||
groupLabel: 'Goal value',
|
||||
accessors: [{ columnId: 'goal-accessor' }],
|
||||
|
|
|
@ -255,6 +255,11 @@ export const getGaugeVisualization = ({
|
|||
groupLabel: i18n.translate('xpack.lens.gauge.metricLabel', {
|
||||
defaultMessage: 'Metric',
|
||||
}),
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.gauge.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
accessors: metricAccessor
|
||||
? [
|
||||
palette
|
||||
|
@ -283,6 +288,16 @@ export const getGaugeVisualization = ({
|
|||
groupLabel: i18n.translate('xpack.lens.gauge.minValueLabel', {
|
||||
defaultMessage: 'Minimum value',
|
||||
}),
|
||||
paramEditorCustomProps: {
|
||||
labels: [
|
||||
i18n.translate('xpack.lens.gauge.minValueLabel', {
|
||||
defaultMessage: 'Minimum value',
|
||||
}),
|
||||
],
|
||||
headingLabel: i18n.translate('xpack.lens.gauge.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
accessors: state.minAccessor ? [{ columnId: state.minAccessor }] : [],
|
||||
filterOperations: isNumericMetric,
|
||||
supportsMoreColumns: !state.minAccessor,
|
||||
|
@ -299,6 +314,16 @@ export const getGaugeVisualization = ({
|
|||
groupLabel: i18n.translate('xpack.lens.gauge.maxValueLabel', {
|
||||
defaultMessage: 'Maximum value',
|
||||
}),
|
||||
paramEditorCustomProps: {
|
||||
labels: [
|
||||
i18n.translate('xpack.lens.gauge.maxValueLabel', {
|
||||
defaultMessage: 'Maximum value',
|
||||
}),
|
||||
],
|
||||
headingLabel: i18n.translate('xpack.lens.gauge.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
accessors: state.maxAccessor ? [{ columnId: state.maxAccessor }] : [],
|
||||
filterOperations: isNumericMetric,
|
||||
supportsMoreColumns: !state.maxAccessor,
|
||||
|
@ -315,6 +340,16 @@ export const getGaugeVisualization = ({
|
|||
groupLabel: i18n.translate('xpack.lens.gauge.goalValueLabel', {
|
||||
defaultMessage: 'Goal value',
|
||||
}),
|
||||
paramEditorCustomProps: {
|
||||
labels: [
|
||||
i18n.translate('xpack.lens.gauge.goalValueLabel', {
|
||||
defaultMessage: 'Goal value',
|
||||
}),
|
||||
],
|
||||
headingLabel: i18n.translate('xpack.lens.gauge.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
accessors: state.goalAccessor ? [{ columnId: state.goalAccessor }] : [],
|
||||
filterOperations: isNumericMetric,
|
||||
supportsMoreColumns: !state.goalAccessor,
|
||||
|
|
|
@ -16,6 +16,9 @@ Object {
|
|||
"groupId": "metric",
|
||||
"groupLabel": "Primary metric",
|
||||
"layerId": "first",
|
||||
"paramEditorCustomProps": Object {
|
||||
"headingLabel": "Value",
|
||||
},
|
||||
"required": true,
|
||||
"supportFieldFormat": false,
|
||||
"supportsMoreColumns": false,
|
||||
|
@ -31,6 +34,9 @@ Object {
|
|||
"groupId": "secondaryMetric",
|
||||
"groupLabel": "Secondary metric",
|
||||
"layerId": "first",
|
||||
"paramEditorCustomProps": Object {
|
||||
"headingLabel": "Value",
|
||||
},
|
||||
"required": false,
|
||||
"supportFieldFormat": false,
|
||||
"supportsMoreColumns": false,
|
||||
|
@ -47,6 +53,9 @@ Object {
|
|||
"groupLabel": "Maximum value",
|
||||
"groupTooltip": "If the maximum value is specified, the minimum value is fixed at zero.",
|
||||
"layerId": "first",
|
||||
"paramEditorCustomProps": Object {
|
||||
"headingLabel": "Value",
|
||||
},
|
||||
"required": false,
|
||||
"supportFieldFormat": false,
|
||||
"supportStaticValue": true,
|
||||
|
|
|
@ -233,6 +233,11 @@ export const getMetricVisualization = ({
|
|||
groupLabel: i18n.translate('xpack.lens.primaryMetric.label', {
|
||||
defaultMessage: 'Primary metric',
|
||||
}),
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
layerId: props.state.layerId,
|
||||
accessors: props.state.metricAccessor
|
||||
? [
|
||||
|
@ -254,6 +259,11 @@ export const getMetricVisualization = ({
|
|||
groupLabel: i18n.translate('xpack.lens.metric.secondaryMetric', {
|
||||
defaultMessage: 'Secondary metric',
|
||||
}),
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
layerId: props.state.layerId,
|
||||
accessors: props.state.secondaryMetricAccessor
|
||||
? [
|
||||
|
@ -271,6 +281,11 @@ export const getMetricVisualization = ({
|
|||
{
|
||||
groupId: GROUP_ID.MAX,
|
||||
groupLabel: i18n.translate('xpack.lens.metric.max', { defaultMessage: 'Maximum value' }),
|
||||
paramEditorCustomProps: {
|
||||
headingLabel: i18n.translate('xpack.lens.primaryMetric.headingLabel', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
},
|
||||
layerId: props.state.layerId,
|
||||
accessors: props.state.maxAccessor
|
||||
? [
|
||||
|
|
|
@ -402,6 +402,13 @@ export const getAnnotationsConfiguration = ({
|
|||
{
|
||||
groupId: 'xAnnotations',
|
||||
groupLabel,
|
||||
dimensionEditorGroupLabel: i18n.translate(
|
||||
'xpack.lens.indexPattern.annotationsDimensionEditorLabel',
|
||||
{
|
||||
defaultMessage: '{groupLabel} annotation',
|
||||
values: { groupLabel },
|
||||
}
|
||||
),
|
||||
accessors: getAnnotationsAccessorColorConfig(layer),
|
||||
dataTestSubj: 'lnsXY_xAnnotationsPanel',
|
||||
invalid: !hasDateHistogram,
|
||||
|
|
|
@ -451,6 +451,13 @@ export const getReferenceConfiguration = ({
|
|||
groups: groupsToShow.map(({ config = [], id, label, dataTestSubj, valid }) => ({
|
||||
groupId: id,
|
||||
groupLabel: getAxisName(label, { isHorizontal }),
|
||||
dimensionEditorGroupLabel: i18n.translate(
|
||||
'xpack.lens.indexPattern.referenceLineDimensionEditorLabel',
|
||||
{
|
||||
defaultMessage: '{groupLabel} reference line',
|
||||
values: { groupLabel: getAxisName(label, { isHorizontal }) },
|
||||
}
|
||||
),
|
||||
accessors: config.map(({ forAccessor, color }) => getSingleColorConfig(forAccessor, color)),
|
||||
filterOperations: isNumericMetric,
|
||||
supportsMoreColumns: true,
|
||||
|
@ -463,6 +470,9 @@ export const getReferenceConfiguration = ({
|
|||
defaultMessage: 'Reference line value',
|
||||
}),
|
||||
],
|
||||
headingLabel: i18n.translate('xpack.lens.staticValue.headingLabel', {
|
||||
defaultMessage: 'Placement',
|
||||
}),
|
||||
},
|
||||
supportFieldFormat: false,
|
||||
dataTestSubj,
|
||||
|
|
|
@ -1165,7 +1165,7 @@ describe('xy_visualization', () => {
|
|||
expect(options.map((o) => o.groupLabel)).toEqual([
|
||||
'Horizontal axis',
|
||||
'Vertical axis',
|
||||
'Break down by',
|
||||
'Breakdown',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -1183,7 +1183,7 @@ describe('xy_visualization', () => {
|
|||
expect(options.map((o) => o.groupLabel)).toEqual([
|
||||
'Vertical axis',
|
||||
'Horizontal axis',
|
||||
'Break down by',
|
||||
'Breakdown',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -1982,7 +1982,7 @@ describe('xy_visualization', () => {
|
|||
expect(accessorConfig.triggerIcon).toEqual('disabled');
|
||||
});
|
||||
|
||||
it('should show current palette for break down by dimension', () => {
|
||||
it('should show current palette for breakdown dimension', () => {
|
||||
const palette = paletteServiceMock.get('mock');
|
||||
const customColors = ['yellow', 'green'];
|
||||
(palette.getCategoricalColors as jest.Mock).mockReturnValue(customColors);
|
||||
|
|
|
@ -279,7 +279,7 @@ export const getXyVisualization = ({
|
|||
{
|
||||
groupId: 'breakdown',
|
||||
groupLabel: i18n.translate('xpack.lens.xyChart.splitSeries', {
|
||||
defaultMessage: 'Break down by',
|
||||
defaultMessage: 'Breakdown',
|
||||
}),
|
||||
accessors: dataLayer.splitAccessor
|
||||
? [
|
||||
|
|
|
@ -8,5 +8,4 @@
|
|||
|
||||
.lnsConfigPanelDate__label {
|
||||
min-width: 56px; // makes both labels ("from" and "to") the same width
|
||||
text-align: center;
|
||||
}
|
|
@ -136,13 +136,15 @@ export function DataDimensionEditor(
|
|||
setLocalState(updateLayer(localState, { ...layer, collapseFn }, index));
|
||||
}}
|
||||
/>
|
||||
<PalettePicker
|
||||
palettes={props.paletteService}
|
||||
activePalette={localLayer?.palette}
|
||||
setPalette={(newPalette) => {
|
||||
setState(updateLayer(localState, { ...localLayer, palette: newPalette }, index));
|
||||
}}
|
||||
/>
|
||||
{!layer.collapseFn && (
|
||||
<PalettePicker
|
||||
palettes={props.paletteService}
|
||||
activePalette={localLayer?.palette}
|
||||
setPalette={(newPalette) => {
|
||||
setState(updateLayer(localState, { ...localLayer, palette: newPalette }, index));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -638,7 +638,6 @@
|
|||
"xpack.lens.indexPattern.terms.accuracyModeDescription": "Activer le mode de précision",
|
||||
"xpack.lens.indexPattern.terms.accuracyModeHelp": "Améliore les données à haute cardinalité, mais augmente la charge sur le cluster Elasticsearch.",
|
||||
"xpack.lens.indexPattern.terms.addaFilter": "Ajouter un champ",
|
||||
"xpack.lens.indexPattern.terms.addField": "Ajouter un champ",
|
||||
"xpack.lens.indexPattern.terms.advancedSettings": "Avancé",
|
||||
"xpack.lens.indexPattern.terms.chooseFields": "{count, plural, zero {Champ} other {Champs}}",
|
||||
"xpack.lens.indexPattern.terms.deleteButtonAriaLabel": "Supprimer",
|
||||
|
|
|
@ -639,7 +639,6 @@
|
|||
"xpack.lens.indexPattern.terms.accuracyModeDescription": "精度モードを有効にする",
|
||||
"xpack.lens.indexPattern.terms.accuracyModeHelp": "高カーディナリティデータの結果が改善されますが、Elasticsearchの負荷が大きくなります。",
|
||||
"xpack.lens.indexPattern.terms.addaFilter": "フィールドの追加",
|
||||
"xpack.lens.indexPattern.terms.addField": "フィールドの追加",
|
||||
"xpack.lens.indexPattern.terms.advancedSettings": "高度な設定",
|
||||
"xpack.lens.indexPattern.terms.chooseFields": "{count, plural, other {個のフィールド}}",
|
||||
"xpack.lens.indexPattern.terms.deleteButtonAriaLabel": "削除",
|
||||
|
|
|
@ -639,7 +639,6 @@
|
|||
"xpack.lens.indexPattern.terms.accuracyModeDescription": "启用准确性模式",
|
||||
"xpack.lens.indexPattern.terms.accuracyModeHelp": "改进结果以获得高基数数据,但会增加 Elasticsearch 集群的负载。",
|
||||
"xpack.lens.indexPattern.terms.addaFilter": "添加字段",
|
||||
"xpack.lens.indexPattern.terms.addField": "添加字段",
|
||||
"xpack.lens.indexPattern.terms.advancedSettings": "高级",
|
||||
"xpack.lens.indexPattern.terms.chooseFields": "{count, plural, other {字段}}",
|
||||
"xpack.lens.indexPattern.terms.deleteButtonAriaLabel": "删除",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue