[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:
Stratoula Kalafateli 2022-08-03 17:02:51 +03:00 committed by GitHub
parent 497e6c36b1
commit e6ea0a406f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1492 additions and 874 deletions

View file

@ -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(

View file

@ -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}>

View file

@ -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 (

View file

@ -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');
});
});

View file

@ -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>
)}
</>
);
}

View file

@ -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');
});
});

View file

@ -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>
)}
</>
);
}

View file

@ -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 [
{

View file

@ -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>
</>
}
/>
</>

View file

@ -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: [

View file

@ -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

View file

@ -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}>

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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,
]);
});

View file

@ -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>
);
};

View file

@ -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;

View file

@ -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,

View file

@ -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,

View file

@ -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);

View file

@ -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>
</>
);

View file

@ -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,

View file

@ -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">

View file

@ -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>
)}
</>
);
},

View file

@ -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,

View file

@ -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);

View file

@ -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>

View file

@ -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>
);
},
};

View file

@ -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
}
/>
</>
);

View file

@ -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'}

View file

@ -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({

View file

@ -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)',

View file

@ -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',
}),

View file

@ -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,

View file

@ -82,7 +82,7 @@ export function PalettePanelContainer({
>
<strong>
{i18n.translate('xpack.lens.table.palettePanelTitle', {
defaultMessage: 'Edit color',
defaultMessage: 'Color',
})}
</strong>
</h2>

View file

@ -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
*/

View file

@ -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' }],

View file

@ -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,

View file

@ -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,

View file

@ -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
? [

View file

@ -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,

View file

@ -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,

View file

@ -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);

View file

@ -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
? [

View file

@ -8,5 +8,4 @@
.lnsConfigPanelDate__label {
min-width: 56px; // makes both labels ("from" and "to") the same width
text-align: center;
}

View file

@ -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));
}}
/>
)}
</>
);
}

View file

@ -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",

View file

@ -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": "削除",

View file

@ -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": "删除",