mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Lens] Rank top values by custom metric (#134811)
* [Lens] order by custom agg
* type check to only allow allowed columns to be included
* remove unused code
* adjusting to the design, correcting full Width everywhere
* change that will make a lot of tests to break
* fix updating only ref column, not full column
* fix cyclic dependency
* remove outdated comment
* added custom labels
* inline modules, ugly code
* fix tests
* clone the aggConfigParams to avoid the Cannot assign to read only property schema of object
* feedback
* Revert "clone the aggConfigParams to avoid the Cannot assign to read only property schema of object"
This reverts commit c4931aad06
.
* cr feedback
Co-authored-by: Joe Reuter <johannes.reuter@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
e3c5e1c327
commit
f0965a39b6
55 changed files with 1880 additions and 924 deletions
|
@ -110,6 +110,7 @@ export const CustomizablePalette = ({
|
|||
</EuiFormRow>
|
||||
{showRangeTypeSelector && (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<>
|
||||
{i18n.translate('coloring.dynamicColoring.rangeType.label', {
|
||||
|
@ -131,6 +132,7 @@ export const CustomizablePalette = ({
|
|||
display="rowCompressed"
|
||||
>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
legend={i18n.translate('coloring.dynamicColoring.rangeType.label', {
|
||||
defaultMessage: 'Value type',
|
||||
})}
|
||||
|
@ -169,7 +171,6 @@ export const CustomizablePalette = ({
|
|||
payload: { rangeType: newRangeType, dataBounds, palettes },
|
||||
});
|
||||
}}
|
||||
isFullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
|
|
|
@ -98,6 +98,7 @@ export function PalettePicker({
|
|||
}
|
||||
return (
|
||||
<EuiColorPalettePicker
|
||||
fullWidth
|
||||
data-test-subj="lns-palettePicker"
|
||||
compressed
|
||||
palettes={palettesToShow}
|
||||
|
|
|
@ -152,7 +152,7 @@ export class AggConfig {
|
|||
const isDeserialized = isType || isObject;
|
||||
|
||||
if (!isDeserialized) {
|
||||
val = aggParam.deserialize(val, this);
|
||||
val = aggParam.deserialize(_.cloneDeep(val), this);
|
||||
}
|
||||
|
||||
to[aggParam.name] = val;
|
||||
|
|
|
@ -190,6 +190,7 @@ export function TableDimensionEditor(
|
|||
</EuiFormRow>
|
||||
{!column.isTransposed && (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.table.columnVisibilityLabel', {
|
||||
defaultMessage: 'Hide column',
|
||||
})}
|
||||
|
@ -266,6 +267,7 @@ export function TableDimensionEditor(
|
|||
})}
|
||||
>
|
||||
<EuiFieldText
|
||||
fullWidth
|
||||
compressed
|
||||
data-test-subj="lnsDatatable_summaryrow_label"
|
||||
value={summaryLabel ?? fallbackSummaryLabel}
|
||||
|
|
|
@ -64,7 +64,7 @@ export function BucketNestingEditor({
|
|||
defaultMessage: 'Group by this field first',
|
||||
});
|
||||
return (
|
||||
<EuiFormRow label={useAsTopLevelAggCopy} display="columnCompressedSwitch">
|
||||
<EuiFormRow label={useAsTopLevelAggCopy} display="columnCompressedSwitch" fullWidth>
|
||||
<EuiSwitch
|
||||
compressed
|
||||
label={useAsTopLevelAggCopy}
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
import ReactDOM from 'react-dom';
|
||||
import type { IndexPatternDimensionEditorProps } from './dimension_panel';
|
||||
import type { OperationSupportMatrix } from './operation_support';
|
||||
import type { GenericIndexPatternColumn } from '../indexpattern';
|
||||
import { deleteColumn, GenericIndexPatternColumn } from '../indexpattern';
|
||||
import {
|
||||
operationDefinitionMap,
|
||||
getOperationDisplay,
|
||||
|
@ -31,12 +31,13 @@ import {
|
|||
FieldBasedIndexPatternColumn,
|
||||
canTransition,
|
||||
DEFAULT_TIME_SCALE,
|
||||
adjustColumnReferencesForChangedColumn,
|
||||
} from '../operations';
|
||||
import { mergeLayer } from '../state_helpers';
|
||||
import { hasField } from '../pure_utils';
|
||||
import { fieldIsInvalid } from '../utils';
|
||||
import { BucketNestingEditor } from './bucket_nesting_editor';
|
||||
import type { IndexPattern, IndexPatternLayer } from '../types';
|
||||
import type { IndexPattern, IndexPatternField, IndexPatternLayer } from '../types';
|
||||
import { trackUiEvent } from '../../lens_ui_telemetry';
|
||||
import { FormatSelector } from './format_selector';
|
||||
import { ReferenceEditor } from './reference_editor';
|
||||
|
@ -60,8 +61,8 @@ import { FieldInput } from './field_input';
|
|||
import { NameInput } from '../../shared_components';
|
||||
import { ParamEditorProps } from '../operations/definitions';
|
||||
import { WrappingHelpPopover } from '../help_popover';
|
||||
|
||||
const operationPanels = getOperationDisplay();
|
||||
import { FieldChoice } from './field_select';
|
||||
import { isColumn } from '../operations/definitions/helpers';
|
||||
|
||||
export interface DimensionEditorProps extends IndexPatternDimensionEditorProps {
|
||||
selectedColumn?: GenericIndexPatternColumn;
|
||||
|
@ -70,6 +71,8 @@ export interface DimensionEditorProps extends IndexPatternDimensionEditorProps {
|
|||
currentIndexPattern: IndexPattern;
|
||||
}
|
||||
|
||||
const operationDisplay = getOperationDisplay();
|
||||
|
||||
export function DimensionEditor(props: DimensionEditorProps) {
|
||||
const {
|
||||
selectedColumn,
|
||||
|
@ -114,15 +117,47 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
);
|
||||
|
||||
const setStateWrapper = (
|
||||
setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer),
|
||||
setter:
|
||||
| IndexPatternLayer
|
||||
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
|
||||
| GenericIndexPatternColumn,
|
||||
options: { forceRender?: boolean } = {}
|
||||
) => {
|
||||
const hypotheticalLayer = typeof setter === 'function' ? setter(state.layers[layerId]) : setter;
|
||||
const isDimensionComplete = Boolean(hypotheticalLayer.columns[columnId]);
|
||||
const layer = state.layers[layerId];
|
||||
let hypotethicalLayer: IndexPatternLayer;
|
||||
if (isColumn(setter)) {
|
||||
hypotethicalLayer = {
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
[columnId]: setter,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
hypotethicalLayer = typeof setter === 'function' ? setter(state.layers[layerId]) : setter;
|
||||
}
|
||||
const isDimensionComplete = Boolean(hypotethicalLayer.columns[columnId]);
|
||||
|
||||
setState(
|
||||
(prevState) => {
|
||||
const layer = typeof setter === 'function' ? setter(prevState.layers[layerId]) : setter;
|
||||
return mergeLayer({ state: prevState, layerId, newLayer: layer });
|
||||
let outputLayer: IndexPatternLayer;
|
||||
const prevLayer = prevState.layers[layerId];
|
||||
if (isColumn(setter)) {
|
||||
outputLayer = {
|
||||
...prevLayer,
|
||||
columns: {
|
||||
...prevLayer.columns,
|
||||
[columnId]: setter,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
outputLayer = typeof setter === 'function' ? setter(prevState.layers[layerId]) : setter;
|
||||
}
|
||||
return mergeLayer({
|
||||
state: prevState,
|
||||
layerId,
|
||||
newLayer: adjustColumnReferencesForChangedColumn(outputLayer, columnId),
|
||||
});
|
||||
},
|
||||
{
|
||||
isDimensionComplete,
|
||||
|
@ -189,7 +224,10 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
// Note: it forced a rerender at this point to avoid UI glitches in async updates (another hack upstream)
|
||||
// TODO: revisit this once we get rid of updateDatasourceAsync upstream
|
||||
const moveDefinetelyToStaticValueAndUpdate = (
|
||||
setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
|
||||
setter:
|
||||
| IndexPatternLayer
|
||||
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
|
||||
| GenericIndexPatternColumn
|
||||
) => {
|
||||
if (temporaryStaticValue) {
|
||||
setTemporaryState('none');
|
||||
|
@ -206,6 +244,9 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
}
|
||||
);
|
||||
}
|
||||
if (isColumn(setter)) {
|
||||
throw new Error('static value should only be updated by the whole layer');
|
||||
}
|
||||
};
|
||||
|
||||
const ParamEditor = getParamEditor(
|
||||
|
@ -290,23 +331,23 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
color = 'subdued';
|
||||
}
|
||||
|
||||
let label: EuiListGroupItemProps['label'] = operationPanels[operationType].displayName;
|
||||
let label: EuiListGroupItemProps['label'] = operationDisplay[operationType].displayName;
|
||||
if (isActive && disabledStatus) {
|
||||
label = (
|
||||
<EuiToolTip content={disabledStatus} display="block" position="left">
|
||||
<EuiText color="danger" size="s">
|
||||
<strong>{operationPanels[operationType].displayName}</strong>
|
||||
<strong>{operationDisplay[operationType].displayName}</strong>
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
);
|
||||
} else if (disabledStatus) {
|
||||
label = (
|
||||
<EuiToolTip content={disabledStatus} display="block" position="left">
|
||||
<span>{operationPanels[operationType].displayName}</span>
|
||||
<span>{operationDisplay[operationType].displayName}</span>
|
||||
</EuiToolTip>
|
||||
);
|
||||
} else if (isActive) {
|
||||
label = <strong>{operationPanels[operationType].displayName}</strong>;
|
||||
label = <strong>{operationDisplay[operationType].displayName}</strong>;
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -438,6 +479,7 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
if (temporaryQuickFunction) {
|
||||
setTemporaryState('none');
|
||||
}
|
||||
|
||||
const newLayer = replaceColumn({
|
||||
layer: props.state.layers[props.layerId],
|
||||
indexPattern: currentIndexPattern,
|
||||
|
@ -475,11 +517,16 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
|
||||
const FieldInputComponent = selectedOperationDefinition?.renderFieldInput || FieldInput;
|
||||
|
||||
const paramEditorProps: ParamEditorProps<GenericIndexPatternColumn> = {
|
||||
const paramEditorProps: ParamEditorProps<
|
||||
GenericIndexPatternColumn,
|
||||
| IndexPatternLayer
|
||||
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
|
||||
| GenericIndexPatternColumn
|
||||
> = {
|
||||
layer: state.layers[layerId],
|
||||
layerId,
|
||||
activeData: props.activeData,
|
||||
updateLayer: (setter) => {
|
||||
paramEditorUpdater: (setter) => {
|
||||
if (temporaryQuickFunction) {
|
||||
setTemporaryState('none');
|
||||
}
|
||||
|
@ -494,6 +541,8 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
isFullscreen,
|
||||
setIsCloseable,
|
||||
paramEditorCustomProps,
|
||||
ReferenceEditor,
|
||||
existingFields: state.existingFields,
|
||||
...services,
|
||||
};
|
||||
|
||||
|
@ -523,21 +572,75 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
<>
|
||||
{selectedColumn.references.map((referenceId, index) => {
|
||||
const validation = selectedOperationDefinition.requiredReferences[index];
|
||||
|
||||
const layer = state.layers[layerId];
|
||||
return (
|
||||
<ReferenceEditor
|
||||
operationDefinitionMap={operationDefinitionMap}
|
||||
key={index}
|
||||
layer={state.layers[layerId]}
|
||||
layer={layer}
|
||||
layerId={layerId}
|
||||
activeData={props.activeData}
|
||||
columnId={referenceId}
|
||||
updateLayer={(
|
||||
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: FieldChoice) => {
|
||||
trackUiEvent('indexpattern_dimension_field_changed');
|
||||
updateLayer(
|
||||
insertOrReplaceColumn({
|
||||
layer,
|
||||
columnId: referenceId,
|
||||
indexPattern: currentIndexPattern,
|
||||
op: choice.operationType,
|
||||
field: currentIndexPattern.getFieldByName(choice.field),
|
||||
visualizationGroups: dimensionGroups,
|
||||
})
|
||||
);
|
||||
}}
|
||||
paramEditorUpdater={(
|
||||
setter:
|
||||
| IndexPatternLayer
|
||||
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
|
||||
| GenericIndexPatternColumn
|
||||
) => {
|
||||
updateLayer(
|
||||
typeof setter === 'function' ? setter(state.layers[layerId]) : setter
|
||||
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}
|
||||
|
@ -548,9 +651,8 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
labelAppend={selectedOperationDefinition?.getHelpMessage?.({
|
||||
data: props.data,
|
||||
uiSettings: props.uiSettings,
|
||||
currentColumn: state.layers[layerId].columns[columnId],
|
||||
currentColumn: layer.columns[columnId],
|
||||
})}
|
||||
dimensionGroups={dimensionGroups}
|
||||
isFullscreen={isFullscreen}
|
||||
toggleFullscreen={toggleFullscreen}
|
||||
setIsCloseable={setIsCloseable}
|
||||
|
@ -600,19 +702,23 @@ export function DimensionEditor(props: DimensionEditorProps) {
|
|||
const customParamEditor = ParamEditor ? (
|
||||
<>
|
||||
<ParamEditor
|
||||
existingFields={state.existingFields}
|
||||
layer={state.layers[layerId]}
|
||||
layerId={layerId}
|
||||
activeData={props.activeData}
|
||||
updateLayer={temporaryStaticValue ? moveDefinetelyToStaticValueAndUpdate : setStateWrapper}
|
||||
paramEditorUpdater={
|
||||
temporaryStaticValue ? moveDefinetelyToStaticValueAndUpdate : setStateWrapper
|
||||
}
|
||||
columnId={columnId}
|
||||
currentColumn={state.layers[layerId].columns[columnId]}
|
||||
dateRange={dateRange}
|
||||
indexPattern={currentIndexPattern}
|
||||
operationDefinitionMap={operationDefinitionMap}
|
||||
toggleFullscreen={toggleFullscreen}
|
||||
isFullscreen={isFullscreen}
|
||||
setIsCloseable={setIsCloseable}
|
||||
layerId={layerId}
|
||||
paramEditorCustomProps={paramEditorCustomProps}
|
||||
dateRange={dateRange}
|
||||
isFullscreen={isFullscreen}
|
||||
indexPattern={currentIndexPattern}
|
||||
toggleFullscreen={toggleFullscreen}
|
||||
setIsCloseable={setIsCloseable}
|
||||
ReferenceEditor={ReferenceEditor}
|
||||
{...services}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -49,11 +49,16 @@ import { DimensionEditor } from './dimension_editor';
|
|||
import { AdvancedOptions } from './advanced_options';
|
||||
import { layerTypes } from '../../../common';
|
||||
|
||||
jest.mock('./reference_editor', () => ({
|
||||
ReferenceEditor: () => null,
|
||||
}));
|
||||
jest.mock('../loader');
|
||||
jest.mock('../query_input', () => ({
|
||||
QueryInput: () => null,
|
||||
}));
|
||||
|
||||
jest.mock('../operations');
|
||||
|
||||
jest.mock('lodash', () => {
|
||||
const original = jest.requireActual('lodash');
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ interface GetDropPropsArgs {
|
|||
|
||||
type DropProps = { dropTypes: DropType[]; nextLabel?: string } | undefined;
|
||||
|
||||
const operationLabels = getOperationDisplay();
|
||||
const operationDisplay = getOperationDisplay();
|
||||
|
||||
export function getNewOperation(
|
||||
field: IndexPatternField | undefined | false,
|
||||
|
@ -133,7 +133,7 @@ function getDropPropsForField({
|
|||
const newOperation = getNewOperation(source.field, target.filterOperations, targetColumn);
|
||||
|
||||
if (isTheSameIndexPattern && newOperation) {
|
||||
const nextLabel = operationLabels[newOperation].displayName;
|
||||
const nextLabel = operationDisplay[newOperation].displayName;
|
||||
|
||||
if (!targetColumn) {
|
||||
return { dropTypes: ['field_add'], nextLabel };
|
||||
|
@ -227,7 +227,7 @@ function getDropPropsFromIncompatibleGroup(
|
|||
|
||||
return {
|
||||
dropTypes,
|
||||
nextLabel: operationLabels[newOperationForSource].displayName,
|
||||
nextLabel: operationDisplay[newOperationForSource].displayName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,7 +77,6 @@ export function FieldSelect({
|
|||
fields,
|
||||
(field) => currentIndexPattern.getFieldByName(field)?.type === 'document'
|
||||
);
|
||||
|
||||
const containsData = (field: string) =>
|
||||
currentIndexPattern.getFieldByName(field)?.type === 'document' ||
|
||||
fieldExists(existingFields, currentIndexPattern.title, field);
|
||||
|
@ -207,10 +206,11 @@ export function FieldSelect({
|
|||
(selectedOperationType && selectedField
|
||||
? [
|
||||
{
|
||||
label: fieldIsInvalid
|
||||
? selectedField
|
||||
: currentIndexPattern.getFieldByName(selectedField)?.displayName ??
|
||||
selectedField,
|
||||
label:
|
||||
(selectedOperationType &&
|
||||
selectedField &&
|
||||
currentIndexPattern.getFieldByName(selectedField)?.displayName) ??
|
||||
selectedField,
|
||||
value: { type: 'field', field: selectedField },
|
||||
},
|
||||
]
|
||||
|
|
|
@ -21,25 +21,41 @@ import { ReferenceEditor, ReferenceEditorProps } from './reference_editor';
|
|||
import {
|
||||
insertOrReplaceColumn,
|
||||
LastValueIndexPatternColumn,
|
||||
operationDefinitionMap,
|
||||
TermsIndexPatternColumn,
|
||||
} from '../operations';
|
||||
import { FieldSelect } from './field_select';
|
||||
import { IndexPatternLayer } from '../types';
|
||||
|
||||
jest.mock('../operations');
|
||||
|
||||
describe('reference editor', () => {
|
||||
let wrapper: ReactWrapper | ShallowWrapper;
|
||||
let updateLayer: jest.Mock<ReferenceEditorProps['updateLayer']>;
|
||||
let paramEditorUpdater: jest.Mock<ReferenceEditorProps['paramEditorUpdater']>;
|
||||
|
||||
const layer = {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Top values of dest',
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
operationType: 'terms',
|
||||
sourceField: 'dest',
|
||||
params: { size: 5, orderBy: { type: 'alphabetical' }, orderDirection: 'desc' },
|
||||
} as TermsIndexPatternColumn,
|
||||
},
|
||||
};
|
||||
function getDefaultArgs() {
|
||||
return {
|
||||
layer: {
|
||||
indexPatternId: '1',
|
||||
columns: {},
|
||||
columnOrder: [],
|
||||
},
|
||||
layer,
|
||||
column: layer.columns.ref,
|
||||
onChooseField: jest.fn(),
|
||||
onChooseFunction: jest.fn(),
|
||||
onDeleteColumn: jest.fn(),
|
||||
columnId: 'ref',
|
||||
updateLayer,
|
||||
paramEditorUpdater,
|
||||
selectionStyle: 'full' as const,
|
||||
currentIndexPattern: createMockedIndexPattern(),
|
||||
existingFields: {
|
||||
|
@ -63,11 +79,12 @@ describe('reference editor', () => {
|
|||
toggleFullscreen: jest.fn(),
|
||||
setIsCloseable: jest.fn(),
|
||||
layerId: '1',
|
||||
operationDefinitionMap,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
updateLayer = jest.fn().mockImplementation((newLayer) => {
|
||||
paramEditorUpdater = jest.fn().mockImplementation((newLayer) => {
|
||||
if (wrapper instanceof ReactWrapper) {
|
||||
wrapper.setProps({ layer: newLayer });
|
||||
}
|
||||
|
@ -90,6 +107,7 @@ describe('reference editor', () => {
|
|||
input: ['field'],
|
||||
validateMetadata: (meta: OperationMetadata) => meta.dataType === 'number',
|
||||
}}
|
||||
column={undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -115,27 +133,67 @@ describe('reference editor', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should indicate functions and fields that are incompatible with the current', () => {
|
||||
it('should indicate fields that are incompatible with the current', () => {
|
||||
const newLayer = {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'average',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
} as IndexPatternLayer;
|
||||
wrapper = mount(
|
||||
<ReferenceEditor
|
||||
{...getDefaultArgs()}
|
||||
layer={{
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Top values of dest',
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
operationType: 'terms',
|
||||
sourceField: 'dest',
|
||||
params: { size: 5, orderBy: { type: 'alphabetical' }, orderDirection: 'desc' },
|
||||
} as TermsIndexPatternColumn,
|
||||
},
|
||||
}}
|
||||
layer={newLayer}
|
||||
column={newLayer.columns.ref}
|
||||
validation={{
|
||||
input: ['field'],
|
||||
validateMetadata: (meta: OperationMetadata) => meta.isBucketed,
|
||||
validateMetadata: (meta: OperationMetadata) => !meta.isBucketed,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const fields = wrapper
|
||||
.find(EuiComboBox)
|
||||
.filter('[data-test-subj="indexPattern-dimension-field"]')
|
||||
.prop('options');
|
||||
|
||||
const findFieldDataTestSubj = (l: string) => {
|
||||
return fields![0].options!.find(({ label }) => label === l)!['data-test-subj'];
|
||||
};
|
||||
expect(findFieldDataTestSubj('timestampLabel')).toContain('Incompatible');
|
||||
expect(findFieldDataTestSubj('source')).toContain('Incompatible');
|
||||
expect(findFieldDataTestSubj('memory')).toContain('lns-fieldOption-memory');
|
||||
});
|
||||
|
||||
it('should indicate functions that are incompatible with the current', () => {
|
||||
const newLayer = {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Unique count of dest',
|
||||
dataType: 'string',
|
||||
isBucketed: false,
|
||||
operationType: 'unique_count',
|
||||
sourceField: 'dest',
|
||||
},
|
||||
},
|
||||
} as IndexPatternLayer;
|
||||
wrapper = mount(
|
||||
<ReferenceEditor
|
||||
{...getDefaultArgs()}
|
||||
layer={newLayer}
|
||||
column={newLayer.columns.ref}
|
||||
validation={{
|
||||
input: ['field'],
|
||||
validateMetadata: (meta: OperationMetadata) => !meta.isBucketed,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -144,36 +202,31 @@ describe('reference editor', () => {
|
|||
.find(EuiComboBox)
|
||||
.filter('[data-test-subj="indexPattern-reference-function"]')
|
||||
.prop('options');
|
||||
expect(functions.find(({ label }) => label === 'Date histogram')!['data-test-subj']).toContain(
|
||||
|
||||
expect(functions.find(({ label }) => label === 'Average')!['data-test-subj']).toContain(
|
||||
'incompatible'
|
||||
);
|
||||
|
||||
const fields = wrapper
|
||||
.find(EuiComboBox)
|
||||
.filter('[data-test-subj="indexPattern-dimension-field"]')
|
||||
.prop('options');
|
||||
expect(
|
||||
fields![0].options!.find(({ label }) => label === 'timestampLabel')!['data-test-subj']
|
||||
).toContain('Incompatible');
|
||||
});
|
||||
|
||||
it('should not update when selecting the same operation', () => {
|
||||
const newLayer = {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'average',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
} as IndexPatternLayer;
|
||||
wrapper = mount(
|
||||
<ReferenceEditor
|
||||
{...getDefaultArgs()}
|
||||
layer={{
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'average',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
}}
|
||||
layer={newLayer}
|
||||
column={newLayer.columns.ref}
|
||||
validation={{
|
||||
input: ['field'],
|
||||
validateMetadata: (meta: OperationMetadata) => meta.dataType === 'number',
|
||||
|
@ -193,26 +246,30 @@ describe('reference editor', () => {
|
|||
});
|
||||
|
||||
it('should keep the field when replacing an existing reference with a compatible function', () => {
|
||||
const onChooseFunction = jest.fn();
|
||||
const newLayer = {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'average',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
} as IndexPatternLayer;
|
||||
wrapper = mount(
|
||||
<ReferenceEditor
|
||||
{...getDefaultArgs()}
|
||||
layer={{
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'average',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
}}
|
||||
layer={newLayer}
|
||||
column={newLayer.columns.ref}
|
||||
validation={{
|
||||
input: ['field'],
|
||||
validateMetadata: (meta: OperationMetadata) => meta.dataType === 'number',
|
||||
}}
|
||||
onChooseFunction={onChooseFunction}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -225,31 +282,35 @@ describe('reference editor', () => {
|
|||
comboBox.prop('onChange')!([option]);
|
||||
});
|
||||
|
||||
expect(insertOrReplaceColumn).toHaveBeenCalledWith(
|
||||
expect(onChooseFunction).toHaveBeenCalledWith(
|
||||
'max',
|
||||
expect.objectContaining({
|
||||
op: 'max',
|
||||
field: expect.objectContaining({ name: 'bytes' }),
|
||||
name: 'bytes',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should transition to another function with incompatible field', () => {
|
||||
const newLayer = {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Unique count of dest',
|
||||
dataType: 'string',
|
||||
isBucketed: false,
|
||||
operationType: 'unique_count',
|
||||
sourceField: 'dest',
|
||||
},
|
||||
},
|
||||
} as IndexPatternLayer;
|
||||
const onChooseFunction = jest.fn();
|
||||
wrapper = mount(
|
||||
<ReferenceEditor
|
||||
{...getDefaultArgs()}
|
||||
layer={{
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'average',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
}}
|
||||
onChooseFunction={onChooseFunction}
|
||||
column={newLayer.columns.ref}
|
||||
layer={newLayer}
|
||||
validation={{
|
||||
input: ['field'],
|
||||
validateMetadata: (meta: OperationMetadata) => true,
|
||||
|
@ -260,39 +321,36 @@ describe('reference editor', () => {
|
|||
const comboBox = wrapper
|
||||
.find(EuiComboBox)
|
||||
.filter('[data-test-subj="indexPattern-reference-function"]');
|
||||
const option = comboBox.prop('options')!.find(({ label }) => label === 'Date histogram')!;
|
||||
const option = comboBox.prop('options')!.find(({ label }) => label === 'Average')!;
|
||||
|
||||
act(() => {
|
||||
comboBox.prop('onChange')!([option]);
|
||||
});
|
||||
|
||||
expect(insertOrReplaceColumn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
op: 'date_histogram',
|
||||
field: undefined,
|
||||
})
|
||||
);
|
||||
expect(onChooseFunction).toHaveBeenCalledWith('average', undefined);
|
||||
});
|
||||
|
||||
it("should show the sub-function as invalid if there's no field compatible with it", () => {
|
||||
// This may happen for saved objects after changing the type of a field
|
||||
const newLayer = {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'average',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
} as IndexPatternLayer;
|
||||
wrapper = mount(
|
||||
<ReferenceEditor
|
||||
{...getDefaultArgs()}
|
||||
currentIndexPattern={createMockedIndexPatternWithoutType('number')}
|
||||
layer={{
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'average',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
}}
|
||||
column={newLayer.columns.ref}
|
||||
layer={newLayer}
|
||||
validation={{
|
||||
input: ['field'],
|
||||
validateMetadata: (meta: OperationMetadata) => true,
|
||||
|
@ -321,6 +379,8 @@ describe('reference editor', () => {
|
|||
wrapper = mount(
|
||||
<ReferenceEditor
|
||||
{...getDefaultArgs()}
|
||||
column={undefined}
|
||||
currentIndexPattern={createMockedIndexPatternWithoutType('number')}
|
||||
validation={{
|
||||
input: ['field', 'fullReference', 'managedReference'],
|
||||
validateMetadata: (meta: OperationMetadata) => true,
|
||||
|
@ -331,8 +391,8 @@ describe('reference editor', () => {
|
|||
const subFunctionSelect = wrapper
|
||||
.find('[data-test-subj="indexPattern-reference-function"]')
|
||||
.first();
|
||||
|
||||
expect(subFunctionSelect.prop('isInvalid')).toEqual(true);
|
||||
|
||||
expect(subFunctionSelect.prop('selectedOptions')).not.toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ value: 'math' })])
|
||||
);
|
||||
|
@ -345,6 +405,7 @@ describe('reference editor', () => {
|
|||
wrapper = mount(
|
||||
<ReferenceEditor
|
||||
{...getDefaultArgs()}
|
||||
column={undefined}
|
||||
selectionStyle={'field' as const}
|
||||
validation={{
|
||||
input: ['field'],
|
||||
|
@ -360,25 +421,28 @@ describe('reference editor', () => {
|
|||
});
|
||||
|
||||
it('should pass the incomplete operation info to FieldSelect', () => {
|
||||
const newLayer = {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'average',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
incompleteColumns: {
|
||||
ref: { operationType: 'max' },
|
||||
},
|
||||
} as IndexPatternLayer;
|
||||
wrapper = mount(
|
||||
<ReferenceEditor
|
||||
{...getDefaultArgs()}
|
||||
layer={{
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'average',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
incompleteColumns: {
|
||||
ref: { operationType: 'max' },
|
||||
},
|
||||
}}
|
||||
incompleteColumn={newLayer.incompleteColumns?.ref}
|
||||
column={newLayer.columns.ref}
|
||||
layer={newLayer}
|
||||
validation={{
|
||||
input: ['field'],
|
||||
validateMetadata: (meta: OperationMetadata) => true,
|
||||
|
@ -395,25 +459,28 @@ describe('reference editor', () => {
|
|||
});
|
||||
|
||||
it('should pass the incomplete field info to FieldSelect', () => {
|
||||
const newLayer = {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'average',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
incompleteColumns: {
|
||||
ref: { sourceField: 'timestamp' },
|
||||
},
|
||||
} as IndexPatternLayer;
|
||||
wrapper = mount(
|
||||
<ReferenceEditor
|
||||
{...getDefaultArgs()}
|
||||
layer={{
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Average of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'average',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
incompleteColumns: {
|
||||
ref: { sourceField: 'timestamp' },
|
||||
},
|
||||
}}
|
||||
layer={newLayer}
|
||||
incompleteColumn={newLayer.incompleteColumns?.ref}
|
||||
column={newLayer.columns.ref}
|
||||
validation={{
|
||||
input: ['field'],
|
||||
validateMetadata: (meta: OperationMetadata) => true,
|
||||
|
@ -432,6 +499,7 @@ describe('reference editor', () => {
|
|||
wrapper = mount(
|
||||
<ReferenceEditor
|
||||
{...getDefaultArgs()}
|
||||
column={undefined}
|
||||
selectionStyle="field"
|
||||
validation={{
|
||||
input: ['field'],
|
||||
|
@ -449,22 +517,24 @@ describe('reference editor', () => {
|
|||
});
|
||||
|
||||
it('should show the FieldSelect as invalid if the selected field is missing', () => {
|
||||
const newLayer = {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Average of missing',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'average',
|
||||
sourceField: 'missing',
|
||||
},
|
||||
},
|
||||
} as IndexPatternLayer;
|
||||
wrapper = mount(
|
||||
<ReferenceEditor
|
||||
{...getDefaultArgs()}
|
||||
layer={{
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Average of missing',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'average',
|
||||
sourceField: 'missing',
|
||||
},
|
||||
},
|
||||
}}
|
||||
layer={newLayer}
|
||||
column={newLayer.columns.ref}
|
||||
validation={{
|
||||
input: ['field'],
|
||||
validateMetadata: (meta: OperationMetadata) => true,
|
||||
|
@ -481,25 +551,27 @@ describe('reference editor', () => {
|
|||
});
|
||||
|
||||
it('should show the ParamEditor for functions that offer one', () => {
|
||||
const lastValueLayer = {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Last value of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'last_value',
|
||||
sourceField: 'bytes',
|
||||
params: {
|
||||
sortField: 'timestamp',
|
||||
},
|
||||
} as LastValueIndexPatternColumn,
|
||||
},
|
||||
};
|
||||
wrapper = mount(
|
||||
<ReferenceEditor
|
||||
{...getDefaultArgs()}
|
||||
layer={{
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Last value of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'last_value',
|
||||
sourceField: 'bytes',
|
||||
params: {
|
||||
sortField: 'timestamp',
|
||||
},
|
||||
} as LastValueIndexPatternColumn,
|
||||
},
|
||||
}}
|
||||
column={lastValueLayer.columns.ref}
|
||||
layer={lastValueLayer}
|
||||
validation={{
|
||||
input: ['field'],
|
||||
validateMetadata: (meta: OperationMetadata) => true,
|
||||
|
@ -513,28 +585,31 @@ describe('reference editor', () => {
|
|||
});
|
||||
|
||||
it('should hide the ParamEditor for incomplete functions', () => {
|
||||
const lastValueLayer = {
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Last value of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'last_value',
|
||||
sourceField: 'bytes',
|
||||
params: {
|
||||
sortField: 'timestamp',
|
||||
},
|
||||
} as LastValueIndexPatternColumn,
|
||||
},
|
||||
incompleteColumns: {
|
||||
ref: { operationType: 'max' },
|
||||
},
|
||||
};
|
||||
wrapper = mount(
|
||||
<ReferenceEditor
|
||||
{...getDefaultArgs()}
|
||||
layer={{
|
||||
indexPatternId: '1',
|
||||
columnOrder: ['ref'],
|
||||
columns: {
|
||||
ref: {
|
||||
label: 'Last value of bytes',
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
operationType: 'last_value',
|
||||
sourceField: 'bytes',
|
||||
params: {
|
||||
sortField: 'timestamp',
|
||||
},
|
||||
} as LastValueIndexPatternColumn,
|
||||
},
|
||||
incompleteColumns: {
|
||||
ref: { operationType: 'max' },
|
||||
},
|
||||
}}
|
||||
incompleteColumn={lastValueLayer.incompleteColumns.ref}
|
||||
column={lastValueLayer.columns.ref}
|
||||
layer={lastValueLayer}
|
||||
validation={{
|
||||
input: ['field'],
|
||||
validateMetadata: (meta: OperationMetadata) => true,
|
||||
|
|
|
@ -8,13 +8,7 @@
|
|||
import './dimension_editor.scss';
|
||||
import React, { useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFormRow,
|
||||
EuiFormRowProps,
|
||||
EuiSpacer,
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
} from '@elastic/eui';
|
||||
import { EuiFormRowProps, EuiSpacer, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from '@kbn/core/public';
|
||||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
|
@ -22,24 +16,58 @@ import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
|||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { DateRange } from '../../../common';
|
||||
import type { OperationSupportMatrix } from './operation_support';
|
||||
import type { OperationType } from '../indexpattern';
|
||||
import type { GenericIndexPatternColumn, OperationType } from '../indexpattern';
|
||||
import {
|
||||
operationDefinitionMap,
|
||||
getOperationDisplay,
|
||||
insertOrReplaceColumn,
|
||||
deleteColumn,
|
||||
isOperationAllowedAsReference,
|
||||
FieldBasedIndexPatternColumn,
|
||||
RequiredReference,
|
||||
IncompleteColumn,
|
||||
GenericOperationDefinition,
|
||||
} from '../operations';
|
||||
import { FieldSelect } from './field_select';
|
||||
import { FieldChoice, FieldSelect } from './field_select';
|
||||
import { hasField } from '../pure_utils';
|
||||
import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../types';
|
||||
import type {
|
||||
IndexPattern,
|
||||
IndexPatternField,
|
||||
IndexPatternLayer,
|
||||
IndexPatternPrivateState,
|
||||
} from '../types';
|
||||
import { trackUiEvent } from '../../lens_ui_telemetry';
|
||||
import type { ParamEditorCustomProps, VisualizationDimensionGroupConfig } from '../../types';
|
||||
import type { ParamEditorCustomProps } from '../../types';
|
||||
import type { IndexPatternDimensionEditorProps } from './dimension_panel';
|
||||
import { FormRow } from '../operations/definitions/shared_components';
|
||||
|
||||
const operationPanels = getOperationDisplay();
|
||||
const operationDisplay = getOperationDisplay();
|
||||
|
||||
const getFunctionOptions = (
|
||||
operationSupportMatrix: OperationSupportMatrix & {
|
||||
operationTypes: Set<OperationType>;
|
||||
},
|
||||
operationDefinitionMap: Record<string, GenericOperationDefinition>,
|
||||
column?: GenericIndexPatternColumn
|
||||
): Array<EuiComboBoxOptionOption<OperationType>> => {
|
||||
return Array.from(operationSupportMatrix.operationTypes).map((operationType) => {
|
||||
const def = operationDefinitionMap[operationType];
|
||||
const label = operationDisplay[operationType].displayName;
|
||||
const isCompatible =
|
||||
!column ||
|
||||
(column &&
|
||||
hasField(column) &&
|
||||
def.input === 'field' &&
|
||||
operationSupportMatrix.fieldByOperation[operationType]?.has(column.sourceField)) ||
|
||||
(column && !hasField(column) && def.input !== 'field');
|
||||
|
||||
return {
|
||||
label,
|
||||
value: operationType,
|
||||
className: 'lnsIndexPatternDimensionEditor__operation',
|
||||
'data-test-subj': `lns-indexPatternDimension-${operationType}${
|
||||
isCompatible ? '' : ' incompatible'
|
||||
}`,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export interface ReferenceEditorProps {
|
||||
layer: IndexPatternLayer;
|
||||
|
@ -48,18 +76,29 @@ export interface ReferenceEditorProps {
|
|||
selectionStyle: 'full' | 'field' | 'hidden';
|
||||
validation: RequiredReference;
|
||||
columnId: string;
|
||||
updateLayer: (
|
||||
setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
|
||||
) => void;
|
||||
column?: GenericIndexPatternColumn;
|
||||
incompleteColumn?: IncompleteColumn;
|
||||
currentIndexPattern: IndexPattern;
|
||||
|
||||
functionLabel?: string;
|
||||
fieldLabel?: string;
|
||||
operationDefinitionMap: Record<string, GenericOperationDefinition>;
|
||||
isInline?: boolean;
|
||||
existingFields: IndexPatternPrivateState['existingFields'];
|
||||
dateRange: DateRange;
|
||||
labelAppend?: EuiFormRowProps['labelAppend'];
|
||||
dimensionGroups: VisualizationDimensionGroupConfig[];
|
||||
isFullscreen: boolean;
|
||||
toggleFullscreen: () => void;
|
||||
setIsCloseable: (isCloseable: boolean) => void;
|
||||
paramEditorCustomProps?: ParamEditorCustomProps;
|
||||
paramEditorUpdater: (
|
||||
setter:
|
||||
| IndexPatternLayer
|
||||
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
|
||||
| GenericIndexPatternColumn
|
||||
) => void;
|
||||
onChooseField: (choice: FieldChoice) => void;
|
||||
onDeleteColumn: () => void;
|
||||
onChooseFunction: (operationType: string, field?: IndexPatternField) => void;
|
||||
|
||||
// Services
|
||||
uiSettings: IUiSettingsClient;
|
||||
|
@ -69,39 +108,28 @@ export interface ReferenceEditorProps {
|
|||
data: DataPublicPluginStart;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
paramEditorCustomProps?: ParamEditorCustomProps;
|
||||
}
|
||||
|
||||
export function ReferenceEditor(props: ReferenceEditorProps) {
|
||||
export const ReferenceEditor = (props: ReferenceEditorProps) => {
|
||||
const {
|
||||
layer,
|
||||
layerId,
|
||||
activeData,
|
||||
columnId,
|
||||
updateLayer,
|
||||
currentIndexPattern,
|
||||
existingFields,
|
||||
validation,
|
||||
selectionStyle,
|
||||
dateRange,
|
||||
labelAppend,
|
||||
dimensionGroups,
|
||||
isFullscreen,
|
||||
toggleFullscreen,
|
||||
setIsCloseable,
|
||||
paramEditorCustomProps,
|
||||
...services
|
||||
column,
|
||||
incompleteColumn,
|
||||
functionLabel,
|
||||
onChooseField,
|
||||
onDeleteColumn,
|
||||
onChooseFunction,
|
||||
fieldLabel,
|
||||
operationDefinitionMap,
|
||||
isInline,
|
||||
} = props;
|
||||
|
||||
const column = layer.columns[columnId];
|
||||
const selectedOperationDefinition = column && operationDefinitionMap[column.operationType];
|
||||
|
||||
const ParamEditor = selectedOperationDefinition?.paramEditor;
|
||||
|
||||
const incompleteInfo = layer.incompleteColumns ? layer.incompleteColumns[columnId] : undefined;
|
||||
const incompleteOperation = incompleteInfo?.operationType;
|
||||
const incompleteField = incompleteInfo?.sourceField ?? null;
|
||||
|
||||
// Basically the operation support matrix, but different validation
|
||||
const operationSupportMatrix: OperationSupportMatrix & {
|
||||
operationTypes: Set<OperationType>;
|
||||
|
@ -111,7 +139,7 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
|
|||
const operationByField: Partial<Record<string, Set<OperationType>>> = {};
|
||||
const fieldByOperation: Partial<Record<OperationType, Set<string>>> = {};
|
||||
Object.values(operationDefinitionMap)
|
||||
.filter(({ hidden }) => !hidden)
|
||||
.filter(({ hidden, allowAsReference }) => !hidden && allowAsReference)
|
||||
.sort((op1, op2) => {
|
||||
return op1.displayName.localeCompare(op2.displayName);
|
||||
})
|
||||
|
@ -152,81 +180,44 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
|
|||
operationByField,
|
||||
fieldByOperation,
|
||||
};
|
||||
}, [currentIndexPattern, validation]);
|
||||
|
||||
const functionOptions: Array<EuiComboBoxOptionOption<OperationType>> = Array.from(
|
||||
operationSupportMatrix.operationTypes
|
||||
).map((operationType) => {
|
||||
const def = operationDefinitionMap[operationType];
|
||||
const label = operationPanels[operationType].displayName;
|
||||
const isCompatible =
|
||||
!column ||
|
||||
(column &&
|
||||
hasField(column) &&
|
||||
def.input === 'field' &&
|
||||
operationSupportMatrix.fieldByOperation[operationType]?.has(column.sourceField)) ||
|
||||
(column && !hasField(column) && def.input !== 'field');
|
||||
|
||||
return {
|
||||
label,
|
||||
value: operationType,
|
||||
className: 'lnsIndexPatternDimensionEditor__operation',
|
||||
'data-test-subj': `lns-indexPatternDimension-${operationType}${
|
||||
isCompatible ? '' : ' incompatible'
|
||||
}`,
|
||||
};
|
||||
});
|
||||
|
||||
function onChooseFunction(operationType: OperationType) {
|
||||
if (column?.operationType === operationType) {
|
||||
return;
|
||||
}
|
||||
const possibleFieldNames = operationSupportMatrix.fieldByOperation[operationType];
|
||||
if (column && 'sourceField' in column && possibleFieldNames?.has(column.sourceField)) {
|
||||
// Reuse the current field if possible
|
||||
updateLayer(
|
||||
insertOrReplaceColumn({
|
||||
layer,
|
||||
columnId,
|
||||
op: operationType,
|
||||
indexPattern: currentIndexPattern,
|
||||
field: currentIndexPattern.getFieldByName(column.sourceField),
|
||||
visualizationGroups: dimensionGroups,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// If reusing the field is impossible, we generally can't choose for the user.
|
||||
// The one exception is if the field is the only possible field, like Count of Records.
|
||||
const possibleField =
|
||||
possibleFieldNames?.size === 1
|
||||
? currentIndexPattern.getFieldByName(possibleFieldNames.values().next().value)
|
||||
: undefined;
|
||||
|
||||
updateLayer(
|
||||
insertOrReplaceColumn({
|
||||
layer,
|
||||
columnId,
|
||||
op: operationType,
|
||||
indexPattern: currentIndexPattern,
|
||||
field: possibleField,
|
||||
visualizationGroups: dimensionGroups,
|
||||
})
|
||||
);
|
||||
}
|
||||
trackUiEvent(`indexpattern_dimension_operation_${operationType}`);
|
||||
return;
|
||||
}
|
||||
}, [currentIndexPattern, validation, operationDefinitionMap]);
|
||||
|
||||
if (selectionStyle === 'hidden') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const incompleteOperation = incompleteColumn?.operationType;
|
||||
const incompleteField = incompleteColumn?.sourceField ?? null;
|
||||
|
||||
const functionOptions = getFunctionOptions(
|
||||
operationSupportMatrix,
|
||||
operationDefinitionMap,
|
||||
column
|
||||
);
|
||||
|
||||
const selectedOption = incompleteOperation
|
||||
? [functionOptions.find(({ value }) => value === incompleteOperation)!]
|
||||
? [functionOptions?.find(({ value }) => value === incompleteOperation)!]
|
||||
: column
|
||||
? [functionOptions.find(({ value }) => value === column.operationType)!]
|
||||
? [functionOptions?.find(({ value }) => value === column.operationType)!]
|
||||
: [];
|
||||
|
||||
// what about a field changing type and becoming invalid?
|
||||
// Let's say this change makes the indexpattern without any number field but the operation was set to a numeric operation.
|
||||
// At this point the ComboBox will crash.
|
||||
// Therefore check if the selectedOption is in functionOptions and in case fill it in as disabled option
|
||||
const showSelectionFunctionInvalid = Boolean(selectedOption.length && selectedOption[0] == null);
|
||||
if (showSelectionFunctionInvalid) {
|
||||
const selectedOperationType = incompleteOperation || column?.operationType;
|
||||
const brokenFunctionOption = {
|
||||
label: selectedOperationType && operationDisplay[selectedOperationType].displayName,
|
||||
value: selectedOperationType,
|
||||
className: 'lnsIndexPatternDimensionEditor__operation',
|
||||
'data-test-subj': `lns-indexPatternDimension-${selectedOperationType} incompatible`,
|
||||
} as EuiComboBoxOptionOption<string>;
|
||||
functionOptions?.push(brokenFunctionOption);
|
||||
selectedOption[0] = brokenFunctionOption;
|
||||
}
|
||||
|
||||
// If the operationType is incomplete, the user needs to select a field- so
|
||||
// the function is marked as valid.
|
||||
const showOperationInvalid = !column && !Boolean(incompleteOperation);
|
||||
|
@ -238,144 +229,114 @@ export function ReferenceEditor(props: ReferenceEditorProps) {
|
|||
incompleteField ?? (column as FieldBasedIndexPatternColumn)?.sourceField
|
||||
);
|
||||
|
||||
// what about a field changing type and becoming invalid?
|
||||
// Let's say this change makes the indexpattern without any number field but the operation was set to a numeric operation.
|
||||
// At this point the ComboBox will crash.
|
||||
// Therefore check if the selectedOption is in functionOptions and in case fill it in as disabled option
|
||||
const showSelectionFunctionInvalid = Boolean(selectedOption.length && selectedOption[0] == null);
|
||||
if (showSelectionFunctionInvalid) {
|
||||
const selectedOperationType = incompleteOperation || column.operationType;
|
||||
const brokenFunctionOption = {
|
||||
label: operationPanels[selectedOperationType].displayName,
|
||||
value: selectedOperationType,
|
||||
className: 'lnsIndexPatternDimensionEditor__operation',
|
||||
'data-test-subj': `lns-indexPatternDimension-${selectedOperationType} incompatible`,
|
||||
};
|
||||
functionOptions.push(brokenFunctionOption);
|
||||
selectedOption[0] = brokenFunctionOption;
|
||||
}
|
||||
const ParamEditor = selectedOperationDefinition?.paramEditor;
|
||||
|
||||
return (
|
||||
<div id={columnId}>
|
||||
<div>
|
||||
{selectionStyle !== 'field' ? (
|
||||
<>
|
||||
<EuiFormRow
|
||||
data-test-subj="indexPattern-subFunction-selection-row"
|
||||
label={i18n.translate('xpack.lens.indexPattern.chooseSubFunction', {
|
||||
<div>
|
||||
{selectionStyle !== 'field' ? (
|
||||
<>
|
||||
<FormRow
|
||||
isInline={isInline}
|
||||
data-test-subj="indexPattern-subFunction-selection-row"
|
||||
label={
|
||||
functionLabel ||
|
||||
i18n.translate('xpack.lens.indexPattern.chooseSubFunction', {
|
||||
defaultMessage: 'Choose a sub-function',
|
||||
})}
|
||||
fullWidth
|
||||
isInvalid={showOperationInvalid || showSelectionFunctionInvalid}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
compressed
|
||||
isClearable={false}
|
||||
data-test-subj="indexPattern-reference-function"
|
||||
placeholder={i18n.translate(
|
||||
'xpack.lens.indexPattern.referenceFunctionPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Sub-function',
|
||||
}
|
||||
)}
|
||||
options={functionOptions}
|
||||
isInvalid={showOperationInvalid || showSelectionFunctionInvalid}
|
||||
selectedOptions={selectedOption}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
onChange={(choices) => {
|
||||
if (choices.length === 0) {
|
||||
updateLayer(
|
||||
deleteColumn({
|
||||
layer,
|
||||
columnId,
|
||||
indexPattern: currentIndexPattern,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
trackUiEvent('indexpattern_dimension_field_changed');
|
||||
|
||||
onChooseFunction(choices[0].value!);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{!column || selectedOperationDefinition.input === 'field' ? (
|
||||
<EuiFormRow
|
||||
data-test-subj="indexPattern-reference-field-selection-row"
|
||||
label={i18n.translate('xpack.lens.indexPattern.chooseField', {
|
||||
defaultMessage: 'Field',
|
||||
})}
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
isInvalid={showFieldInvalid || showFieldMissingInvalid}
|
||||
labelAppend={labelAppend}
|
||||
isInvalid={showOperationInvalid || showSelectionFunctionInvalid}
|
||||
>
|
||||
<FieldSelect
|
||||
fieldIsInvalid={showFieldInvalid || showFieldMissingInvalid}
|
||||
currentIndexPattern={currentIndexPattern}
|
||||
existingFields={existingFields}
|
||||
operationByField={operationSupportMatrix.operationByField}
|
||||
selectedOperationType={
|
||||
// Allows operation to be selected before creating a valid column
|
||||
column ? column.operationType : incompleteOperation
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
compressed
|
||||
isClearable={false}
|
||||
data-test-subj="indexPattern-reference-function"
|
||||
placeholder={
|
||||
functionLabel ||
|
||||
i18n.translate('xpack.lens.indexPattern.referenceFunctionPlaceholder', {
|
||||
defaultMessage: 'Sub-function',
|
||||
})
|
||||
}
|
||||
selectedField={
|
||||
// Allows field to be selected
|
||||
incompleteField ?? (column as FieldBasedIndexPatternColumn)?.sourceField
|
||||
}
|
||||
incompleteOperation={incompleteOperation}
|
||||
markAllFieldsCompatible={selectionStyle === 'field'}
|
||||
onDeleteColumn={() => {
|
||||
updateLayer(
|
||||
deleteColumn({
|
||||
layer,
|
||||
columnId,
|
||||
indexPattern: currentIndexPattern,
|
||||
})
|
||||
);
|
||||
}}
|
||||
onChoose={(choice) => {
|
||||
updateLayer(
|
||||
insertOrReplaceColumn({
|
||||
layer,
|
||||
columnId,
|
||||
indexPattern: currentIndexPattern,
|
||||
op: choice.operationType,
|
||||
field: currentIndexPattern.getFieldByName(choice.field),
|
||||
visualizationGroups: dimensionGroups,
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
) : null}
|
||||
options={functionOptions}
|
||||
isInvalid={showOperationInvalid || showSelectionFunctionInvalid}
|
||||
selectedOptions={selectedOption}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
onChange={(choices: Array<EuiComboBoxOptionOption<string>>) => {
|
||||
if (choices.length === 0) {
|
||||
return onDeleteColumn();
|
||||
}
|
||||
|
||||
{column && !incompleteInfo && ParamEditor && (
|
||||
<>
|
||||
<ParamEditor
|
||||
updateLayer={updateLayer}
|
||||
currentColumn={column}
|
||||
layer={layer}
|
||||
layerId={layerId}
|
||||
activeData={activeData}
|
||||
columnId={columnId}
|
||||
indexPattern={currentIndexPattern}
|
||||
dateRange={dateRange}
|
||||
operationDefinitionMap={operationDefinitionMap}
|
||||
isFullscreen={isFullscreen}
|
||||
toggleFullscreen={toggleFullscreen}
|
||||
setIsCloseable={setIsCloseable}
|
||||
paramEditorCustomProps={paramEditorCustomProps}
|
||||
{...services}
|
||||
const operationType = choices[0].value!;
|
||||
if (column?.operationType === operationType) {
|
||||
return;
|
||||
}
|
||||
const possibleFieldNames = operationSupportMatrix.fieldByOperation[operationType];
|
||||
|
||||
const field =
|
||||
column && 'sourceField' in column && possibleFieldNames?.has(column.sourceField)
|
||||
? currentIndexPattern.getFieldByName(column.sourceField)
|
||||
: possibleFieldNames?.size === 1
|
||||
? currentIndexPattern.getFieldByName(possibleFieldNames.values().next().value)
|
||||
: undefined;
|
||||
|
||||
onChooseFunction(operationType, field);
|
||||
trackUiEvent(`indexpattern_dimension_operation_${operationType}`);
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</FormRow>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{!column || selectedOperationDefinition?.input === 'field' ? (
|
||||
<FormRow
|
||||
isInline={isInline}
|
||||
data-test-subj="indexPattern-reference-field-selection-row"
|
||||
label={
|
||||
fieldLabel ||
|
||||
i18n.translate('xpack.lens.indexPattern.chooseField', {
|
||||
defaultMessage: 'Field',
|
||||
})
|
||||
}
|
||||
fullWidth
|
||||
isInvalid={showFieldInvalid || showFieldMissingInvalid}
|
||||
labelAppend={labelAppend}
|
||||
>
|
||||
<FieldSelect
|
||||
fieldIsInvalid={showFieldInvalid || showFieldMissingInvalid}
|
||||
currentIndexPattern={currentIndexPattern}
|
||||
existingFields={existingFields}
|
||||
operationByField={operationSupportMatrix.operationByField}
|
||||
selectedOperationType={
|
||||
// Allows operation to be selected before creating a valid column
|
||||
column ? column.operationType : incompleteOperation
|
||||
}
|
||||
selectedField={
|
||||
// Allows field to be selected
|
||||
incompleteField ?? (column as FieldBasedIndexPatternColumn)?.sourceField
|
||||
}
|
||||
incompleteOperation={incompleteOperation}
|
||||
markAllFieldsCompatible={selectionStyle === 'field'}
|
||||
onDeleteColumn={onDeleteColumn}
|
||||
onChoose={onChooseField}
|
||||
/>
|
||||
</FormRow>
|
||||
) : null}
|
||||
|
||||
{column && !incompleteColumn && ParamEditor && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<ParamEditor
|
||||
{...props}
|
||||
isReferenced={true}
|
||||
operationDefinitionMap={operationDefinitionMap}
|
||||
currentColumn={column}
|
||||
indexPattern={props.currentIndexPattern}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -101,6 +101,7 @@ export function TimeScaling({
|
|||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
compressed
|
||||
options={Object.entries(unitSuffixesLong).map(([unit, text]) => ({
|
||||
value: unit,
|
||||
|
|
|
@ -163,7 +163,7 @@ export function TimeShift({
|
|||
}
|
||||
isInvalid={Boolean(isLocalValueInvalid || localValueTooSmall || localValueNotMultiple)}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
|
|
|
@ -42,6 +42,9 @@ import { DatatableColumn } from '@kbn/expressions-plugin';
|
|||
jest.mock('./loader');
|
||||
jest.mock('../id_generator');
|
||||
jest.mock('./operations');
|
||||
jest.mock('./dimension_panel/reference_editor', () => ({
|
||||
ReferenceEditor: () => null,
|
||||
}));
|
||||
|
||||
const fieldsOne = [
|
||||
{
|
||||
|
|
|
@ -16,6 +16,7 @@ jest.spyOn(actualHelpers, 'copyColumn');
|
|||
jest.spyOn(actualHelpers, 'insertOrReplaceColumn');
|
||||
jest.spyOn(actualHelpers, 'insertNewColumn');
|
||||
jest.spyOn(actualHelpers, 'replaceColumn');
|
||||
jest.spyOn(actualHelpers, 'adjustColumnReferencesForChangedColumn');
|
||||
jest.spyOn(actualHelpers, 'getErrorMessages');
|
||||
jest.spyOn(actualHelpers, 'getColumnOrder');
|
||||
|
||||
|
@ -50,6 +51,7 @@ export const {
|
|||
isOperationAllowedAsReference,
|
||||
canTransition,
|
||||
isColumnValidAsReference,
|
||||
adjustColumnReferencesForChangedColumn,
|
||||
getManagedColumnsFrom,
|
||||
} = actualHelpers;
|
||||
|
||||
|
|
|
@ -173,7 +173,7 @@ Example: Smooth a line of measurements:
|
|||
|
||||
function MovingAverageParamEditor({
|
||||
layer,
|
||||
updateLayer,
|
||||
paramEditorUpdater,
|
||||
currentColumn,
|
||||
columnId,
|
||||
}: ParamEditorProps<MovingAverageIndexPatternColumn>) {
|
||||
|
@ -183,7 +183,7 @@ function MovingAverageParamEditor({
|
|||
() => {
|
||||
if (!isValidNumber(inputValue, true, undefined, 1)) return;
|
||||
const inputNumber = parseInt(inputValue, 10);
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
|
@ -207,6 +207,7 @@ function MovingAverageParamEditor({
|
|||
isInvalid={!isValidNumber(inputValue)}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
fullWidth
|
||||
compressed
|
||||
value={inputValue}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInputValue(e.target.value)}
|
||||
|
|
|
@ -65,11 +65,17 @@ export interface CardinalityIndexPatternColumn extends FieldBasedIndexPatternCol
|
|||
};
|
||||
}
|
||||
|
||||
export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternColumn, 'field'> = {
|
||||
export const cardinalityOperation: OperationDefinition<
|
||||
CardinalityIndexPatternColumn,
|
||||
'field',
|
||||
{},
|
||||
true
|
||||
> = {
|
||||
type: OPERATION_TYPE,
|
||||
displayName: i18n.translate('xpack.lens.indexPattern.cardinality', {
|
||||
defaultMessage: 'Unique count',
|
||||
}),
|
||||
allowAsReference: true,
|
||||
input: 'field',
|
||||
getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => {
|
||||
if (
|
||||
|
@ -123,7 +129,7 @@ export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternCo
|
|||
layer,
|
||||
columnId,
|
||||
currentColumn,
|
||||
updateLayer,
|
||||
paramEditorUpdater,
|
||||
}: ParamEditorProps<CardinalityIndexPatternColumn>) => {
|
||||
return [
|
||||
{
|
||||
|
@ -141,7 +147,7 @@ export const cardinalityOperation: OperationDefinition<CardinalityIndexPatternCo
|
|||
}}
|
||||
checked={Boolean(currentColumn.params?.emptyAsNull)}
|
||||
onChange={() => {
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
|
|
|
@ -40,7 +40,7 @@ export type CountIndexPatternColumn = FieldBasedIndexPatternColumn & {
|
|||
};
|
||||
};
|
||||
|
||||
export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field'> = {
|
||||
export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field', {}, true> = {
|
||||
type: 'count',
|
||||
priority: 2,
|
||||
displayName: i18n.translate('xpack.lens.indexPattern.count', {
|
||||
|
@ -52,6 +52,7 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field
|
|||
getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern),
|
||||
getDisallowedPreviousShiftMessage(layer, columnId),
|
||||
]),
|
||||
allowAsReference: true,
|
||||
onFieldChange: (oldColumn, field) => {
|
||||
return {
|
||||
...oldColumn,
|
||||
|
@ -112,7 +113,7 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field
|
|||
layer,
|
||||
columnId,
|
||||
currentColumn,
|
||||
updateLayer,
|
||||
paramEditorUpdater,
|
||||
}: ParamEditorProps<CountIndexPatternColumn>) => {
|
||||
return [
|
||||
{
|
||||
|
@ -130,7 +131,7 @@ export const countOperation: OperationDefinition<CountIndexPatternColumn, 'field
|
|||
}}
|
||||
checked={Boolean(currentColumn.params?.emptyAsNull)}
|
||||
onChange={() => {
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
|
|
|
@ -106,6 +106,14 @@ const defaultOptions = {
|
|||
isFullscreen: false,
|
||||
toggleFullscreen: jest.fn(),
|
||||
setIsCloseable: jest.fn(),
|
||||
existingFields: {
|
||||
my_index_pattern: {
|
||||
timestamp: true,
|
||||
bytes: true,
|
||||
memory: true,
|
||||
source: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('date_histogram', () => {
|
||||
|
@ -310,7 +318,7 @@ describe('date_histogram', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
/>
|
||||
|
@ -346,7 +354,7 @@ describe('date_histogram', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={secondLayer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={secondLayer.columns.col2 as DateHistogramIndexPatternColumn}
|
||||
indexPattern={indexPattern2}
|
||||
|
@ -382,7 +390,7 @@ describe('date_histogram', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={thirdLayer}
|
||||
updateLayer={jest.fn()}
|
||||
paramEditorUpdater={jest.fn()}
|
||||
columnId="col1"
|
||||
currentColumn={thirdLayer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
indexPattern={indexPattern1}
|
||||
|
@ -418,7 +426,7 @@ describe('date_histogram', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={thirdLayer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={thirdLayer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
/>
|
||||
|
@ -459,7 +467,7 @@ describe('date_histogram', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={thirdLayer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={thirdLayer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
indexPattern={{ ...indexPattern1, timeFieldName: undefined }}
|
||||
|
@ -502,7 +510,7 @@ describe('date_histogram', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={thirdLayer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={thirdLayer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
indexPattern={{ ...indexPattern1, timeFieldName: undefined }}
|
||||
|
@ -544,7 +552,7 @@ describe('date_histogram', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={thirdLayer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={thirdLayer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
indexPattern={{ ...indexPattern1, timeFieldName: 'other_timestamp' }}
|
||||
|
@ -559,7 +567,7 @@ describe('date_histogram', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
/>
|
||||
|
@ -581,7 +589,7 @@ describe('date_histogram', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={testLayer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={testLayer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
/>
|
||||
|
@ -598,7 +606,7 @@ describe('date_histogram', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={testLayer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={testLayer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
/>
|
||||
|
@ -615,7 +623,7 @@ describe('date_histogram', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={testLayer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={testLayer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
/>
|
||||
|
@ -631,7 +639,7 @@ describe('date_histogram', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
/>
|
||||
|
@ -655,7 +663,7 @@ describe('date_histogram', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={testLayer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={testLayer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
/>
|
||||
|
@ -707,7 +715,7 @@ describe('date_histogram', () => {
|
|||
{...defaultOptions}
|
||||
layer={layer}
|
||||
indexPattern={indexPattern}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
/>
|
||||
|
@ -741,7 +749,7 @@ describe('date_histogram', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={thirdLayer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={thirdLayer.columns.col1 as DateHistogramIndexPatternColumn}
|
||||
/>
|
||||
|
|
|
@ -171,7 +171,7 @@ export const dateHistogramOperation: OperationDefinition<
|
|||
layer,
|
||||
columnId,
|
||||
currentColumn,
|
||||
updateLayer,
|
||||
paramEditorUpdater,
|
||||
dateRange,
|
||||
data,
|
||||
indexPattern,
|
||||
|
@ -200,7 +200,7 @@ export const dateHistogramOperation: OperationDefinition<
|
|||
// updateColumnParam will be called async
|
||||
// store the checked value before the event pooling clears it
|
||||
const value = ev.target.checked;
|
||||
updateLayer((newLayer) =>
|
||||
paramEditorUpdater((newLayer) =>
|
||||
updateColumnParam({
|
||||
layer: newLayer,
|
||||
columnId,
|
||||
|
@ -209,7 +209,7 @@ export const dateHistogramOperation: OperationDefinition<
|
|||
})
|
||||
);
|
||||
},
|
||||
[columnId, updateLayer]
|
||||
[columnId, paramEditorUpdater]
|
||||
);
|
||||
|
||||
const setInterval = useCallback(
|
||||
|
@ -221,11 +221,11 @@ export const dateHistogramOperation: OperationDefinition<
|
|||
? autoInterval
|
||||
: `${isCalendarInterval ? '1' : newInterval.value}${newInterval.unit || 'd'}`;
|
||||
|
||||
updateLayer((newLayer) =>
|
||||
paramEditorUpdater((newLayer) =>
|
||||
updateColumnParam({ layer: newLayer, columnId, paramName: 'interval', value })
|
||||
);
|
||||
},
|
||||
[columnId, updateLayer]
|
||||
[columnId, paramEditorUpdater]
|
||||
);
|
||||
|
||||
const options = (intervalOptions || [])
|
||||
|
@ -323,7 +323,7 @@ export const dateHistogramOperation: OperationDefinition<
|
|||
const newValue = opts.length ? opts[0].key! : '';
|
||||
setIntervalInput(newValue);
|
||||
if (newValue === autoInterval && currentColumn.params.ignoreTimeRange) {
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
|
@ -397,7 +397,7 @@ export const dateHistogramOperation: OperationDefinition<
|
|||
});
|
||||
setIntervalInput(newFixedInterval);
|
||||
}
|
||||
updateLayer(newLayer);
|
||||
paramEditorUpdater(newLayer);
|
||||
}}
|
||||
compressed
|
||||
/>
|
||||
|
@ -410,7 +410,7 @@ export const dateHistogramOperation: OperationDefinition<
|
|||
checked={Boolean(currentColumn.params.includeEmptyRows)}
|
||||
data-test-subj="indexPattern-include-empty-rows"
|
||||
onChange={() => {
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
|
|
|
@ -36,6 +36,14 @@ const defaultProps = {
|
|||
toggleFullscreen: jest.fn(),
|
||||
setIsCloseable: jest.fn(),
|
||||
layerId: '1',
|
||||
existingFields: {
|
||||
my_index_pattern: {
|
||||
timestamp: true,
|
||||
bytes: true,
|
||||
memory: true,
|
||||
source: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// mocking random id generator function
|
||||
|
@ -304,7 +312,7 @@ describe('filters', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as FiltersIndexPatternColumn}
|
||||
/>
|
||||
|
@ -357,7 +365,7 @@ describe('filters', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as FiltersIndexPatternColumn}
|
||||
/>
|
||||
|
@ -382,7 +390,7 @@ describe('filters', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as FiltersIndexPatternColumn}
|
||||
/>
|
||||
|
|
|
@ -145,11 +145,11 @@ export const filtersOperation: OperationDefinition<FiltersIndexPatternColumn, 'n
|
|||
}).toAst();
|
||||
},
|
||||
|
||||
paramEditor: ({ layer, columnId, currentColumn, indexPattern, updateLayer, data }) => {
|
||||
paramEditor: ({ layer, columnId, currentColumn, indexPattern, paramEditorUpdater }) => {
|
||||
const filters = currentColumn.params.filters;
|
||||
|
||||
const setFilters = (newFilters: Filter[]) =>
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
|
@ -159,7 +159,7 @@ export const filtersOperation: OperationDefinition<FiltersIndexPatternColumn, 'n
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow>
|
||||
<EuiFormRow fullWidth>
|
||||
<FilterList
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
|
|
|
@ -85,7 +85,7 @@ const MemoizedFormulaEditor = React.memo(FormulaEditor);
|
|||
|
||||
export function FormulaEditor({
|
||||
layer,
|
||||
updateLayer,
|
||||
paramEditorUpdater,
|
||||
currentColumn,
|
||||
columnId,
|
||||
indexPattern,
|
||||
|
@ -153,7 +153,7 @@ export function FormulaEditor({
|
|||
setIsCloseable(true);
|
||||
// If the text is not synced, update the column.
|
||||
if (text !== currentColumn.params.formula) {
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
(prevLayer) =>
|
||||
insertOrReplaceFormulaColumn(
|
||||
columnId,
|
||||
|
@ -183,7 +183,7 @@ export function FormulaEditor({
|
|||
monaco.editor.setModelMarkers(editorModel.current, 'LENS', []);
|
||||
if (currentColumn.params.formula) {
|
||||
// Only submit if valid
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
insertOrReplaceFormulaColumn(
|
||||
columnId,
|
||||
{
|
||||
|
@ -232,7 +232,7 @@ export function FormulaEditor({
|
|||
if (previousFormulaWasBroken || previousFormulaWasOkButNoData) {
|
||||
// If the formula is already broken, show the latest error message in the workspace
|
||||
if (currentColumn.params.formula !== text) {
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
insertOrReplaceFormulaColumn(
|
||||
columnId,
|
||||
{
|
||||
|
@ -314,7 +314,7 @@ export function FormulaEditor({
|
|||
}
|
||||
);
|
||||
|
||||
updateLayer(newLayer);
|
||||
paramEditorUpdater(newLayer);
|
||||
|
||||
const managedColumns = getManagedColumnsFrom(columnId, newLayer.columns);
|
||||
const markers: monaco.editor.IMarkerData[] = managedColumns
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
FormattedIndexPatternColumn,
|
||||
ReferenceBasedIndexPatternColumn,
|
||||
} from './column_types';
|
||||
import { IndexPattern, IndexPatternField } from '../../types';
|
||||
import { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../types';
|
||||
import { hasField } from '../../pure_utils';
|
||||
|
||||
export function getInvalidFieldMessage(
|
||||
|
@ -128,6 +128,15 @@ export function isColumnOfType<C extends GenericIndexPatternColumn>(
|
|||
return column.operationType === type;
|
||||
}
|
||||
|
||||
export const isColumn = (
|
||||
setter:
|
||||
| GenericIndexPatternColumn
|
||||
| IndexPatternLayer
|
||||
| ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
|
||||
): setter is GenericIndexPatternColumn => {
|
||||
return 'operationType' in setter;
|
||||
};
|
||||
|
||||
export function isColumnFormatted(
|
||||
column: GenericIndexPatternColumn
|
||||
): column is FormattedIndexPatternColumn {
|
||||
|
|
|
@ -64,6 +64,7 @@ import { DateRange, LayerType } from '../../../../common';
|
|||
import { rangeOperation } from './ranges';
|
||||
import { IndexPatternDimensionEditorProps, OperationSupportMatrix } from '../../dimension_panel';
|
||||
import type { OriginalColumn } from '../../to_expression';
|
||||
import { ReferenceEditorProps } from '../../dimension_panel/reference_editor';
|
||||
|
||||
export type {
|
||||
IncompleteColumn,
|
||||
|
@ -160,12 +161,14 @@ export { staticValueOperation } from './static_value';
|
|||
/**
|
||||
* Properties passed to the operation-specific part of the popover editor
|
||||
*/
|
||||
export interface ParamEditorProps<C> {
|
||||
export interface ParamEditorProps<
|
||||
C,
|
||||
U = IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
|
||||
> {
|
||||
currentColumn: C;
|
||||
layer: IndexPatternLayer;
|
||||
updateLayer: (
|
||||
setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer)
|
||||
) => void;
|
||||
paramEditorUpdater: (setter: U) => void;
|
||||
ReferenceEditor?: (props: ReferenceEditorProps) => JSX.Element | null;
|
||||
toggleFullscreen: () => void;
|
||||
setIsCloseable: (isCloseable: boolean) => void;
|
||||
isFullscreen: boolean;
|
||||
|
@ -183,6 +186,8 @@ export interface ParamEditorProps<C> {
|
|||
activeData?: IndexPatternDimensionEditorProps['activeData'];
|
||||
operationDefinitionMap: Record<string, GenericOperationDefinition>;
|
||||
paramEditorCustomProps?: ParamEditorCustomProps;
|
||||
existingFields: Record<string, Record<string, boolean>>;
|
||||
isReferenced?: boolean;
|
||||
}
|
||||
|
||||
export interface FieldInputProps<C> {
|
||||
|
@ -227,7 +232,11 @@ export interface AdvancedOption {
|
|||
helpPopup?: string | null;
|
||||
}
|
||||
|
||||
interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn, P = {}> {
|
||||
interface BaseOperationDefinitionProps<
|
||||
C extends BaseIndexPatternColumn,
|
||||
AR extends boolean,
|
||||
P = {}
|
||||
> {
|
||||
type: C['operationType'];
|
||||
/**
|
||||
* The priority of the operation. If multiple operations are possible in
|
||||
|
@ -258,7 +267,10 @@ interface BaseOperationDefinitionProps<C extends BaseIndexPatternColumn, P = {}>
|
|||
/**
|
||||
* React component for operation specific settings shown in the flyout editor
|
||||
*/
|
||||
paramEditor?: React.ComponentType<ParamEditorProps<C>>;
|
||||
allowAsReference?: AR;
|
||||
paramEditor?: React.ComponentType<
|
||||
AR extends true ? ParamEditorProps<C, GenericIndexPatternColumn> : ParamEditorProps<C>
|
||||
>;
|
||||
getAdvancedOptions?: (params: ParamEditorProps<C>) => AdvancedOption[] | undefined;
|
||||
/**
|
||||
* Returns true if the `column` can also be used on `newIndexPattern`.
|
||||
|
@ -498,7 +510,8 @@ interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn, P = {}
|
|||
indexPattern: IndexPattern,
|
||||
layer: IndexPatternLayer,
|
||||
uiSettings: IUiSettingsClient,
|
||||
orderedColumnIds: string[]
|
||||
orderedColumnIds: string[],
|
||||
operationDefinitionMap?: Record<string, GenericOperationDefinition>
|
||||
) => ExpressionAstFunction;
|
||||
/**
|
||||
* Validate that the operation has the right preconditions in the state. For example:
|
||||
|
@ -646,8 +659,9 @@ interface OperationDefinitionMap<C extends BaseIndexPatternColumn, P = {}> {
|
|||
export type OperationDefinition<
|
||||
C extends BaseIndexPatternColumn,
|
||||
Input extends keyof OperationDefinitionMap<C>,
|
||||
P = {}
|
||||
> = BaseOperationDefinitionProps<C> & OperationDefinitionMap<C, P>[Input];
|
||||
P = {},
|
||||
AR extends boolean = false
|
||||
> = BaseOperationDefinitionProps<C, AR> & OperationDefinitionMap<C, P>[Input];
|
||||
|
||||
/**
|
||||
* A union type of all available operation types. The operation type is a unique id of an operation.
|
||||
|
|
|
@ -40,6 +40,14 @@ const defaultProps = {
|
|||
toggleFullscreen: jest.fn(),
|
||||
setIsCloseable: jest.fn(),
|
||||
layerId: '1',
|
||||
existingFields: {
|
||||
my_index_pattern: {
|
||||
timestamp: true,
|
||||
bytes: true,
|
||||
memory: true,
|
||||
source: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('last_value', () => {
|
||||
|
@ -642,6 +650,13 @@ describe('last_value', () => {
|
|||
return this.showArrayValuesSwitch.prop('disabled');
|
||||
}
|
||||
|
||||
public get arrayValuesSwitchNotExisiting() {
|
||||
return (
|
||||
this._instance.find('[data-test-subj="lns-indexPattern-lastValue-showArrayValues"]')
|
||||
.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
changeSortFieldOptions(options: Array<{ label: string; value: string }>) {
|
||||
this.sortField.find(EuiComboBox).prop('onChange')!([
|
||||
{ label: 'datefield2', value: 'datefield2' },
|
||||
|
@ -659,7 +674,7 @@ describe('last_value', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
|
||||
/>
|
||||
|
@ -676,7 +691,7 @@ describe('last_value', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
|
||||
/>
|
||||
|
@ -685,16 +700,10 @@ describe('last_value', () => {
|
|||
new Harness(instance).changeSortFieldOptions([{ label: 'datefield2', value: 'datefield2' }]);
|
||||
|
||||
expect(updateLayerSpy).toHaveBeenCalledWith({
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
col2: {
|
||||
...layer.columns.col2,
|
||||
params: {
|
||||
...(layer.columns.col2 as LastValueIndexPatternColumn).params,
|
||||
sortField: 'datefield2',
|
||||
},
|
||||
},
|
||||
...layer.columns.col2,
|
||||
params: {
|
||||
...(layer.columns.col2 as LastValueIndexPatternColumn).params,
|
||||
sortField: 'datefield2',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -707,7 +716,7 @@ describe('last_value', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
|
||||
/>
|
||||
|
@ -718,27 +727,29 @@ describe('last_value', () => {
|
|||
harness.toggleShowArrayValues();
|
||||
|
||||
expect(updateLayerSpy).toHaveBeenCalledWith({
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
col2: {
|
||||
...layer.columns.col2,
|
||||
params: {
|
||||
...(layer.columns.col2 as LastValueIndexPatternColumn).params,
|
||||
showArrayValues: true,
|
||||
},
|
||||
},
|
||||
...layer.columns.col2,
|
||||
params: {
|
||||
...(layer.columns.col2 as LastValueIndexPatternColumn).params,
|
||||
showArrayValues: true,
|
||||
},
|
||||
});
|
||||
|
||||
// have to do this manually, but it happens automatically in the app
|
||||
const newLayer = updateLayerSpy.mock.calls[0][0];
|
||||
const newColumn = updateLayerSpy.mock.calls[0][0];
|
||||
const newLayer = {
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
col2: newColumn,
|
||||
},
|
||||
};
|
||||
instance.setProps({ layer: newLayer, currentColumn: newLayer.columns.col2 });
|
||||
|
||||
expect(harness.showingTopValuesWarning).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not warn user when top-values not in use', () => {
|
||||
// todo: move to dimension editor
|
||||
const updateLayerSpy = jest.fn();
|
||||
const localLayer = {
|
||||
...layer,
|
||||
|
@ -754,7 +765,7 @@ describe('last_value', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={localLayer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
|
||||
/>
|
||||
|
@ -764,7 +775,15 @@ describe('last_value', () => {
|
|||
harness.toggleShowArrayValues();
|
||||
|
||||
// have to do this manually, but it happens automatically in the app
|
||||
const newLayer = updateLayerSpy.mock.calls[0][0];
|
||||
const newColumn = updateLayerSpy.mock.calls[0][0];
|
||||
const newLayer = {
|
||||
...localLayer,
|
||||
columns: {
|
||||
...localLayer.columns,
|
||||
col2: newColumn,
|
||||
},
|
||||
};
|
||||
|
||||
instance.setProps({ layer: newLayer, currentColumn: newLayer.columns.col2 });
|
||||
|
||||
expect(harness.showingTopValuesWarning).toBeFalsy();
|
||||
|
@ -778,7 +797,7 @@ describe('last_value', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
|
||||
/>
|
||||
|
@ -786,6 +805,21 @@ describe('last_value', () => {
|
|||
|
||||
expect(new Harness(instance).showArrayValuesSwitchDisabled).toBeTruthy();
|
||||
});
|
||||
it('should not display an array for the last value if the column is referenced', () => {
|
||||
const updateLayerSpy = jest.fn();
|
||||
const instance = shallow(
|
||||
<InlineOptions
|
||||
{...defaultProps}
|
||||
isReferenced={true}
|
||||
layer={layer}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col2 as LastValueIndexPatternColumn}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(new Harness(instance).arrayValuesSwitchNotExisiting).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -829,6 +863,7 @@ describe('last_value', () => {
|
|||
'Field notExisting was not found',
|
||||
]);
|
||||
});
|
||||
|
||||
it('shows error message if the sortField does not exist in index pattern', () => {
|
||||
errorLayer = {
|
||||
...errorLayer,
|
||||
|
|
|
@ -20,7 +20,6 @@ import { buildExpressionFunction } from '@kbn/expressions-plugin/public';
|
|||
import { OperationDefinition } from '.';
|
||||
import { FieldBasedIndexPatternColumn, ValueFormatConfig } from './column_types';
|
||||
import { IndexPatternField, IndexPattern } from '../../types';
|
||||
import { adjustColumnReferencesForChangedColumn, updateColumnParam } from '../layer_helpers';
|
||||
import { DataType } from '../../../types';
|
||||
import {
|
||||
getFormatFromPreviousColumn,
|
||||
|
@ -31,6 +30,7 @@ import {
|
|||
import { adjustTimeScaleLabelSuffix } from '../time_scale_utils';
|
||||
import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils';
|
||||
import { isScriptedField } from './terms/helpers';
|
||||
import { FormRow } from './shared_components/form_row';
|
||||
|
||||
function ofName(name: string, timeShift: string | undefined) {
|
||||
return adjustTimeScaleLabelSuffix(
|
||||
|
@ -122,7 +122,8 @@ function getExistsFilter(field: string) {
|
|||
export const lastValueOperation: OperationDefinition<
|
||||
LastValueIndexPatternColumn,
|
||||
'field',
|
||||
Partial<LastValueIndexPatternColumn['params']>
|
||||
Partial<LastValueIndexPatternColumn['params']>,
|
||||
true
|
||||
> = {
|
||||
type: 'last_value',
|
||||
displayName: i18n.translate('xpack.lens.indexPattern.lastValue', {
|
||||
|
@ -257,8 +258,22 @@ export const lastValueOperation: OperationDefinition<
|
|||
supportedTypes.has(newField.type)
|
||||
);
|
||||
},
|
||||
allowAsReference: true,
|
||||
paramEditor: ({
|
||||
layer,
|
||||
paramEditorUpdater,
|
||||
currentColumn,
|
||||
indexPattern,
|
||||
isReferenced,
|
||||
paramEditorCustomProps,
|
||||
}) => {
|
||||
const { labels, isInline } = paramEditorCustomProps || {};
|
||||
const sortByFieldLabel =
|
||||
labels?.[0] ||
|
||||
i18n.translate('xpack.lens.indexPattern.lastValue.sortField', {
|
||||
defaultMessage: 'Sort by date field',
|
||||
});
|
||||
|
||||
paramEditor: ({ layer, updateLayer, columnId, currentColumn, indexPattern }) => {
|
||||
const dateFields = getDateFields(indexPattern);
|
||||
const isSortFieldInvalid = !!getInvalidSortFieldMessage(
|
||||
currentColumn.params.sortField,
|
||||
|
@ -270,27 +285,20 @@ export const lastValueOperation: OperationDefinition<
|
|||
);
|
||||
|
||||
const setShowArrayValues = (use: boolean) => {
|
||||
let updatedLayer = updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'showArrayValues',
|
||||
value: use,
|
||||
});
|
||||
|
||||
updatedLayer = {
|
||||
...updatedLayer,
|
||||
columns: adjustColumnReferencesForChangedColumn(updatedLayer, columnId),
|
||||
};
|
||||
|
||||
updateLayer(updatedLayer);
|
||||
return paramEditorUpdater({
|
||||
...currentColumn,
|
||||
params: {
|
||||
...currentColumn.params,
|
||||
showArrayValues: use,
|
||||
},
|
||||
} as LastValueIndexPatternColumn);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.indexPattern.lastValue.sortField', {
|
||||
defaultMessage: 'Sort by date field',
|
||||
})}
|
||||
<FormRow
|
||||
isInline={isInline}
|
||||
label={sortByFieldLabel}
|
||||
display="rowCompressed"
|
||||
fullWidth
|
||||
error={i18n.translate('xpack.lens.indexPattern.sortField.invalid', {
|
||||
|
@ -302,14 +310,13 @@ export const lastValueOperation: OperationDefinition<
|
|||
placeholder={i18n.translate('xpack.lens.indexPattern.lastValue.sortFieldPlaceholder', {
|
||||
defaultMessage: 'Sort field',
|
||||
})}
|
||||
fullWidth
|
||||
compressed
|
||||
isClearable={false}
|
||||
data-test-subj="lns-indexPattern-lastValue-sortField"
|
||||
isInvalid={isSortFieldInvalid}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.lastValue.sortField', {
|
||||
defaultMessage: 'Sort by date field',
|
||||
})}
|
||||
aria-label={sortByFieldLabel}
|
||||
options={dateFields?.map((field: IndexPatternField) => {
|
||||
return {
|
||||
value: field.name,
|
||||
|
@ -320,14 +327,13 @@ export const lastValueOperation: OperationDefinition<
|
|||
if (choices.length === 0) {
|
||||
return;
|
||||
}
|
||||
updateLayer(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'sortField',
|
||||
value: choices[0].value,
|
||||
})
|
||||
);
|
||||
return paramEditorUpdater({
|
||||
...currentColumn,
|
||||
params: {
|
||||
...currentColumn.params,
|
||||
sortField: choices[0].value,
|
||||
},
|
||||
} as LastValueIndexPatternColumn);
|
||||
}}
|
||||
selectedOptions={
|
||||
(currentColumn.params?.sortField
|
||||
|
@ -342,41 +348,43 @@ export const lastValueOperation: OperationDefinition<
|
|||
: []) as unknown as EuiComboBoxOptionOption[]
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<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',
|
||||
</FormRow>
|
||||
{!isReferenced && (
|
||||
<EuiFormRow
|
||||
error={i18n.translate(
|
||||
'xpack.lens.indexPattern.lastValue.showArrayValuesWithTopValuesWarning',
|
||||
{
|
||||
defaultMessage:
|
||||
'Displays all values associated with this field in each last document.',
|
||||
'When you show array values, you are unable to use this field to rank top values.',
|
||||
}
|
||||
)}
|
||||
position="left"
|
||||
isInvalid={currentColumn.params.showArrayValues && usingTopValues}
|
||||
display="rowCompressed"
|
||||
fullWidth
|
||||
data-test-subj="lns-indexPattern-lastValue-showArrayValues"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -81,6 +81,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
|
|||
|
||||
return {
|
||||
type,
|
||||
allowAsReference: true,
|
||||
priority,
|
||||
displayName,
|
||||
description,
|
||||
|
@ -142,7 +143,12 @@ function buildMetricOperation<T extends MetricColumn<string>>({
|
|||
sourceField: field.name,
|
||||
};
|
||||
},
|
||||
getAdvancedOptions: ({ layer, columnId, currentColumn, updateLayer }: ParamEditorProps<T>) => {
|
||||
getAdvancedOptions: ({
|
||||
layer,
|
||||
columnId,
|
||||
currentColumn,
|
||||
paramEditorUpdater,
|
||||
}: ParamEditorProps<T>) => {
|
||||
if (!hideZeroOption) return [];
|
||||
return [
|
||||
{
|
||||
|
@ -160,7 +166,7 @@ function buildMetricOperation<T extends MetricColumn<string>>({
|
|||
}}
|
||||
checked={Boolean(currentColumn.params?.emptyAsNull)}
|
||||
onChange={() => {
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
|
@ -221,7 +227,7 @@ Example: Get the {metric} of price for orders from the UK:
|
|||
}),
|
||||
},
|
||||
shiftable: true,
|
||||
} as OperationDefinition<T, 'field'>;
|
||||
} as OperationDefinition<T, 'field', {}, true>;
|
||||
}
|
||||
|
||||
export type SumIndexPatternColumn = MetricColumn<'sum'>;
|
||||
|
|
|
@ -56,6 +56,14 @@ const defaultProps = {
|
|||
toggleFullscreen: jest.fn(),
|
||||
setIsCloseable: jest.fn(),
|
||||
layerId: '1',
|
||||
existingFields: {
|
||||
my_index_pattern: {
|
||||
timestamp: true,
|
||||
bytes: true,
|
||||
memory: true,
|
||||
source: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('percentile', () => {
|
||||
|
@ -715,7 +723,7 @@ describe('percentile', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as PercentileIndexPatternColumn}
|
||||
/>
|
||||
|
@ -732,7 +740,7 @@ describe('percentile', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as PercentileIndexPatternColumn}
|
||||
/>
|
||||
|
@ -752,17 +760,11 @@ describe('percentile', () => {
|
|||
instance.update();
|
||||
|
||||
expect(updateLayerSpy).toHaveBeenCalledWith({
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
col2: {
|
||||
...layer.columns.col2,
|
||||
params: {
|
||||
percentile: 27,
|
||||
},
|
||||
label: '27th percentile of a',
|
||||
},
|
||||
...layer.columns.col2,
|
||||
params: {
|
||||
percentile: 27,
|
||||
},
|
||||
label: '27th percentile of a',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -772,7 +774,7 @@ describe('percentile', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as PercentileIndexPatternColumn}
|
||||
/>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFormRow, EuiRange, EuiRangeProps } from '@elastic/eui';
|
||||
import { EuiFieldNumber, EuiRange } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AggFunctionsMapping, METRIC_TYPES } from '@kbn/data-plugin/public';
|
||||
|
@ -29,6 +29,7 @@ import { FieldBasedIndexPatternColumn } from './column_types';
|
|||
import { adjustTimeScaleLabelSuffix } from '../time_scale_utils';
|
||||
import { useDebouncedValue } from '../../../shared_components';
|
||||
import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils';
|
||||
import { FormRow } from './shared_components';
|
||||
|
||||
export interface PercentileIndexPatternColumn extends FieldBasedIndexPatternColumn {
|
||||
operationType: 'percentile';
|
||||
|
@ -64,9 +65,11 @@ const supportedFieldTypes = ['number', 'histogram'];
|
|||
export const percentileOperation: OperationDefinition<
|
||||
PercentileIndexPatternColumn,
|
||||
'field',
|
||||
{ percentile: number }
|
||||
{ percentile: number },
|
||||
true
|
||||
> = {
|
||||
type: 'percentile',
|
||||
allowAsReference: true,
|
||||
displayName: i18n.translate('xpack.lens.indexPattern.percentile', {
|
||||
defaultMessage: 'Percentile',
|
||||
}),
|
||||
|
@ -268,12 +271,17 @@ export const percentileOperation: OperationDefinition<
|
|||
getDisallowedPreviousShiftMessage(layer, columnId),
|
||||
]),
|
||||
paramEditor: function PercentileParamEditor({
|
||||
layer,
|
||||
updateLayer,
|
||||
paramEditorUpdater,
|
||||
currentColumn,
|
||||
columnId,
|
||||
indexPattern,
|
||||
paramEditorCustomProps,
|
||||
}) {
|
||||
const { labels, isInline } = paramEditorCustomProps || {};
|
||||
const percentileLabel =
|
||||
labels?.[0] ||
|
||||
i18n.translate('xpack.lens.indexPattern.percentile.percentileValue', {
|
||||
defaultMessage: 'Percentile',
|
||||
});
|
||||
const onChange = useCallback(
|
||||
(value) => {
|
||||
if (
|
||||
|
@ -282,29 +290,23 @@ export const percentileOperation: OperationDefinition<
|
|||
) {
|
||||
return;
|
||||
}
|
||||
updateLayer({
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
[columnId]: {
|
||||
...currentColumn,
|
||||
label: currentColumn.customLabel
|
||||
? currentColumn.label
|
||||
: ofName(
|
||||
indexPattern.getFieldByName(currentColumn.sourceField)?.displayName ||
|
||||
currentColumn.sourceField,
|
||||
Number(value),
|
||||
currentColumn.timeShift
|
||||
),
|
||||
params: {
|
||||
...currentColumn.params,
|
||||
percentile: Number(value),
|
||||
},
|
||||
} as PercentileIndexPatternColumn,
|
||||
paramEditorUpdater({
|
||||
...currentColumn,
|
||||
label: currentColumn.customLabel
|
||||
? currentColumn.label
|
||||
: ofName(
|
||||
indexPattern.getFieldByName(currentColumn.sourceField)?.displayName ||
|
||||
currentColumn.sourceField,
|
||||
Number(value),
|
||||
currentColumn.timeShift
|
||||
),
|
||||
params: {
|
||||
...currentColumn.params,
|
||||
percentile: Number(value),
|
||||
},
|
||||
});
|
||||
} as PercentileIndexPatternColumn);
|
||||
},
|
||||
[updateLayer, layer, columnId, currentColumn, indexPattern]
|
||||
[paramEditorUpdater, currentColumn, indexPattern]
|
||||
);
|
||||
const { inputValue, handleInputChange: handleInputChangeWithoutValidation } = useDebouncedValue<
|
||||
string | undefined
|
||||
|
@ -314,16 +316,15 @@ export const percentileOperation: OperationDefinition<
|
|||
});
|
||||
const inputValueIsValid = isValidNumber(inputValue, true, 99, 1);
|
||||
|
||||
const handleInputChange: EuiRangeProps['onChange'] = useCallback(
|
||||
const handleInputChange = useCallback(
|
||||
(e) => handleInputChangeWithoutValidation(String(e.currentTarget.value)),
|
||||
[handleInputChangeWithoutValidation]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.indexPattern.percentile.percentileValue', {
|
||||
defaultMessage: 'Percentile',
|
||||
})}
|
||||
<FormRow
|
||||
isInline={isInline}
|
||||
label={percentileLabel}
|
||||
data-test-subj="lns-indexPattern-percentile-form"
|
||||
display="rowCompressed"
|
||||
fullWidth
|
||||
|
@ -335,20 +336,33 @@ export const percentileOperation: OperationDefinition<
|
|||
})
|
||||
}
|
||||
>
|
||||
<EuiRange
|
||||
data-test-subj="lns-indexPattern-percentile-input"
|
||||
compressed
|
||||
value={inputValue ?? ''}
|
||||
min={1}
|
||||
max={99}
|
||||
step={1}
|
||||
onChange={handleInputChange}
|
||||
showInput
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.percentile.percentileValue', {
|
||||
defaultMessage: 'Percentile',
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{isInline ? (
|
||||
<EuiFieldNumber
|
||||
fullWidth
|
||||
data-test-subj="lns-indexPattern-percentile-input"
|
||||
compressed
|
||||
value={inputValue ?? ''}
|
||||
min={1}
|
||||
max={99}
|
||||
step={1}
|
||||
onChange={handleInputChange}
|
||||
aria-label={percentileLabel}
|
||||
/>
|
||||
) : (
|
||||
<EuiRange
|
||||
fullWidth
|
||||
data-test-subj="lns-indexPattern-percentile-input"
|
||||
compressed
|
||||
value={inputValue ?? ''}
|
||||
min={1}
|
||||
max={99}
|
||||
step={1}
|
||||
onChange={handleInputChange}
|
||||
showInput
|
||||
aria-label={percentileLabel}
|
||||
/>
|
||||
)}
|
||||
</FormRow>
|
||||
);
|
||||
},
|
||||
documentation: {
|
||||
|
|
|
@ -50,6 +50,14 @@ const defaultProps = {
|
|||
toggleFullscreen: jest.fn(),
|
||||
setIsCloseable: jest.fn(),
|
||||
layerId: '1',
|
||||
existingFields: {
|
||||
my_index_pattern: {
|
||||
timestamp: true,
|
||||
bytes: true,
|
||||
memory: true,
|
||||
source: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('percentile ranks', () => {
|
||||
|
@ -274,7 +282,7 @@ describe('percentile ranks', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as PercentileRanksIndexPatternColumn}
|
||||
/>
|
||||
|
@ -291,7 +299,7 @@ describe('percentile ranks', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as PercentileRanksIndexPatternColumn}
|
||||
/>
|
||||
|
@ -310,17 +318,11 @@ describe('percentile ranks', () => {
|
|||
instance.update();
|
||||
|
||||
expect(updateLayerSpy).toHaveBeenCalledWith({
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
col2: {
|
||||
...layer.columns.col2,
|
||||
params: {
|
||||
value: 103,
|
||||
},
|
||||
label: 'Percentile rank (103) of a',
|
||||
},
|
||||
...layer.columns.col2,
|
||||
params: {
|
||||
value: 103,
|
||||
},
|
||||
label: 'Percentile rank (103) of a',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -330,7 +332,7 @@ describe('percentile ranks', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as PercentileRanksIndexPatternColumn}
|
||||
/>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFormRow, EuiFieldNumberProps, EuiFieldNumber } from '@elastic/eui';
|
||||
import { EuiFieldNumberProps, EuiFieldNumber } from '@elastic/eui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AggFunctionsMapping } from '@kbn/data-plugin/public';
|
||||
|
@ -24,6 +24,7 @@ import { FieldBasedIndexPatternColumn } from './column_types';
|
|||
import { adjustTimeScaleLabelSuffix } from '../time_scale_utils';
|
||||
import { useDebouncedValue } from '../../../shared_components';
|
||||
import { getDisallowedPreviousShiftMessage } from '../../time_shift_utils';
|
||||
import { FormRow } from './shared_components';
|
||||
|
||||
export interface PercentileRanksIndexPatternColumn extends FieldBasedIndexPatternColumn {
|
||||
operationType: 'percentile_rank';
|
||||
|
@ -52,9 +53,11 @@ const supportedFieldTypes = ['number', 'histogram'];
|
|||
export const percentileRanksOperation: OperationDefinition<
|
||||
PercentileRanksIndexPatternColumn,
|
||||
'field',
|
||||
{ value: number }
|
||||
{ value: number },
|
||||
true
|
||||
> = {
|
||||
type: 'percentile_rank',
|
||||
allowAsReference: true,
|
||||
displayName: i18n.translate('xpack.lens.indexPattern.percentileRank', {
|
||||
defaultMessage: 'Percentile rank',
|
||||
}),
|
||||
|
@ -143,40 +146,39 @@ export const percentileRanksOperation: OperationDefinition<
|
|||
getDisallowedPreviousShiftMessage(layer, columnId),
|
||||
]),
|
||||
paramEditor: function PercentileParamEditor({
|
||||
layer,
|
||||
updateLayer,
|
||||
paramEditorUpdater,
|
||||
currentColumn,
|
||||
columnId,
|
||||
indexPattern,
|
||||
paramEditorCustomProps,
|
||||
}) {
|
||||
const { labels, isInline } = paramEditorCustomProps || {};
|
||||
const percentileRanksLabel =
|
||||
labels?.[0] ||
|
||||
i18n.translate('xpack.lens.indexPattern.percentile.percentileRanksValue', {
|
||||
defaultMessage: 'Percentile ranks value',
|
||||
});
|
||||
const onChange = useCallback(
|
||||
(value) => {
|
||||
if (!isValidNumber(value) || Number(value) === currentColumn.params.value) {
|
||||
return;
|
||||
}
|
||||
updateLayer({
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
[columnId]: {
|
||||
...currentColumn,
|
||||
label: currentColumn.customLabel
|
||||
? currentColumn.label
|
||||
: ofName(
|
||||
indexPattern.getFieldByName(currentColumn.sourceField)?.displayName ||
|
||||
currentColumn.sourceField,
|
||||
Number(value),
|
||||
currentColumn.timeShift
|
||||
),
|
||||
params: {
|
||||
...currentColumn.params,
|
||||
value: Number(value),
|
||||
},
|
||||
} as PercentileRanksIndexPatternColumn,
|
||||
paramEditorUpdater({
|
||||
...currentColumn,
|
||||
label: currentColumn.customLabel
|
||||
? currentColumn.label
|
||||
: ofName(
|
||||
indexPattern.getFieldByName(currentColumn.sourceField)?.displayName ||
|
||||
currentColumn.sourceField,
|
||||
Number(value),
|
||||
currentColumn.timeShift
|
||||
),
|
||||
params: {
|
||||
...currentColumn.params,
|
||||
value: Number(value),
|
||||
},
|
||||
});
|
||||
} as PercentileRanksIndexPatternColumn);
|
||||
},
|
||||
[updateLayer, layer, columnId, currentColumn, indexPattern]
|
||||
[paramEditorUpdater, currentColumn, indexPattern]
|
||||
);
|
||||
const { inputValue, handleInputChange: handleInputChangeWithoutValidation } = useDebouncedValue<
|
||||
string | undefined
|
||||
|
@ -197,10 +199,9 @@ export const percentileRanksOperation: OperationDefinition<
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.indexPattern.percentile.percentileRanksValue', {
|
||||
defaultMessage: 'Percentile ranks value',
|
||||
})}
|
||||
<FormRow
|
||||
isInline={isInline}
|
||||
label={percentileRanksLabel}
|
||||
data-test-subj="lns-indexPattern-percentile_ranks-form"
|
||||
display="rowCompressed"
|
||||
fullWidth
|
||||
|
@ -213,16 +214,15 @@ export const percentileRanksOperation: OperationDefinition<
|
|||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
fullWidth
|
||||
data-test-subj="lns-indexPattern-percentile_ranks-input"
|
||||
compressed
|
||||
value={inputValue ?? ''}
|
||||
onChange={handleInputChange}
|
||||
step="any"
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.percentile.percentileRanksValue', {
|
||||
defaultMessage: 'Percentile ranks value',
|
||||
})}
|
||||
aria-label={percentileRanksLabel}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</FormRow>
|
||||
);
|
||||
},
|
||||
documentation: {
|
||||
|
|
|
@ -246,6 +246,7 @@ export const AdvancedRangeEditor = ({
|
|||
|
||||
return (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate('xpack.lens.indexPattern.ranges.customRanges', {
|
||||
defaultMessage: 'Ranges',
|
||||
})}
|
||||
|
|
|
@ -83,6 +83,14 @@ const defaultOptions = {
|
|||
storage: {} as IStorageWrapper,
|
||||
uiSettings: uiSettingsMock,
|
||||
savedObjectsClient: {} as SavedObjectsClientContract,
|
||||
existingFields: {
|
||||
my_index_pattern: {
|
||||
timestamp: true,
|
||||
bytes: true,
|
||||
memory: true,
|
||||
source: true,
|
||||
},
|
||||
},
|
||||
dateRange: {
|
||||
fromDate: 'now-1y',
|
||||
toDate: 'now',
|
||||
|
@ -374,7 +382,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
/>
|
||||
|
@ -390,7 +398,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
/>
|
||||
|
@ -433,7 +441,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
/>
|
||||
|
@ -503,7 +511,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
/>
|
||||
|
@ -519,7 +527,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
/>
|
||||
|
@ -539,7 +547,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={
|
||||
{
|
||||
|
@ -565,7 +573,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
/>
|
||||
|
@ -620,7 +628,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
/>
|
||||
|
@ -675,7 +683,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
/>
|
||||
|
@ -722,7 +730,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
/>
|
||||
|
@ -772,7 +780,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
/>
|
||||
|
@ -810,7 +818,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
/>
|
||||
|
@ -842,7 +850,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
indexPattern={{
|
||||
|
@ -872,7 +880,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
indexPattern={{
|
||||
|
@ -896,7 +904,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
/>
|
||||
|
@ -916,7 +924,7 @@ describe('ranges', () => {
|
|||
<InlineOptions
|
||||
{...defaultOptions}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as RangeIndexPatternColumn}
|
||||
/>
|
||||
|
|
|
@ -180,7 +180,7 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field
|
|||
layer,
|
||||
columnId,
|
||||
currentColumn,
|
||||
updateLayer,
|
||||
paramEditorUpdater,
|
||||
indexPattern,
|
||||
uiSettings,
|
||||
data,
|
||||
|
@ -208,7 +208,7 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field
|
|||
|
||||
// Used to change one param at the time
|
||||
const setParam: UpdateParamsFnType = (paramName, value) => {
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
|
@ -226,7 +226,7 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field
|
|||
newMode === MODES.Range
|
||||
? { id: 'range', params: { template: 'arrow_right', replaceInfinity: true } }
|
||||
: undefined;
|
||||
updateLayer({
|
||||
paramEditorUpdater({
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.lnsIndexPatternDimensionEditor__labelCustomRank {
|
||||
min-width: 96px;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFormLabel, EuiFormRow, EuiFormRowProps } from '@elastic/eui';
|
||||
import './form_row.scss';
|
||||
|
||||
type FormRowProps = EuiFormRowProps & { isInline?: boolean };
|
||||
|
||||
export const FormRow = ({ children, label, isInline, ...props }: FormRowProps) => {
|
||||
return !isInline ? (
|
||||
<EuiFormRow {...props} label={label}>
|
||||
{children}
|
||||
</EuiFormRow>
|
||||
) : (
|
||||
<div data-test-subj={props['data-test-subj']}>
|
||||
{React.cloneElement(children, {
|
||||
prepend: (
|
||||
<EuiFormLabel className="lnsIndexPatternDimensionEditor__labelCustomRank">
|
||||
{label}
|
||||
</EuiFormLabel>
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -7,3 +7,4 @@
|
|||
|
||||
export * from './label_input';
|
||||
export * from './buckets';
|
||||
export * from './form_row';
|
||||
|
|
|
@ -49,6 +49,14 @@ const defaultProps = {
|
|||
toggleFullscreen: jest.fn(),
|
||||
setIsCloseable: jest.fn(),
|
||||
layerId: '1',
|
||||
existingFields: {
|
||||
my_index_pattern: {
|
||||
timestamp: true,
|
||||
bytes: true,
|
||||
memory: true,
|
||||
source: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('static_value', () => {
|
||||
|
@ -340,7 +348,7 @@ describe('static_value', () => {
|
|||
<ParamEditor
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as StaticValueIndexPatternColumn}
|
||||
/>
|
||||
|
@ -371,7 +379,7 @@ describe('static_value', () => {
|
|||
<ParamEditor
|
||||
{...defaultProps}
|
||||
layer={zeroLayer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={zeroLayer.columns.col2 as StaticValueIndexPatternColumn}
|
||||
/>
|
||||
|
@ -387,7 +395,7 @@ describe('static_value', () => {
|
|||
<ParamEditor
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as StaticValueIndexPatternColumn}
|
||||
/>
|
||||
|
@ -428,7 +436,7 @@ describe('static_value', () => {
|
|||
<ParamEditor
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col2"
|
||||
currentColumn={layer.columns.col2 as StaticValueIndexPatternColumn}
|
||||
/>
|
||||
|
|
|
@ -153,7 +153,7 @@ export const staticValueOperation: OperationDefinition<
|
|||
},
|
||||
|
||||
paramEditor: function StaticValueEditor({
|
||||
updateLayer,
|
||||
paramEditorUpdater,
|
||||
currentColumn,
|
||||
columnId,
|
||||
activeData,
|
||||
|
@ -168,7 +168,7 @@ export const staticValueOperation: OperationDefinition<
|
|||
}
|
||||
// Because of upstream specific UX flows, we need fresh layer state here
|
||||
// so need to use the updater pattern
|
||||
updateLayer((newLayer) => {
|
||||
paramEditorUpdater((newLayer) => {
|
||||
const newColumn = newLayer.columns[columnId] as StaticValueIndexPatternColumn;
|
||||
return {
|
||||
...newLayer,
|
||||
|
@ -186,7 +186,7 @@ export const staticValueOperation: OperationDefinition<
|
|||
};
|
||||
});
|
||||
},
|
||||
[columnId, updateLayer, currentColumn?.params?.value]
|
||||
[columnId, paramEditorUpdater, currentColumn?.params?.value]
|
||||
);
|
||||
|
||||
// Pick the data from the current activeData (to be used when the current operation is not static_value)
|
||||
|
@ -216,9 +216,10 @@ export const staticValueOperation: OperationDefinition<
|
|||
|
||||
return (
|
||||
<div className="lnsIndexPatternDimensionEditor__section lnsIndexPatternDimensionEditor__section--padded lnsIndexPatternDimensionEditor__section--shaded">
|
||||
<EuiFormLabel>{paramEditorCustomProps?.label || defaultLabel}</EuiFormLabel>
|
||||
<EuiFormLabel>{paramEditorCustomProps?.labels?.[0] || defaultLabel}</EuiFormLabel>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFieldNumber
|
||||
fullWidth
|
||||
data-test-subj="lns-indexPattern-static_value-input"
|
||||
compressed
|
||||
value={inputValue ?? ''}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFormRow,
|
||||
|
@ -21,12 +21,17 @@ import {
|
|||
import { uniq } from 'lodash';
|
||||
import { AggFunctionsMapping } from '@kbn/data-plugin/public';
|
||||
import { buildExpressionFunction } from '@kbn/expressions-plugin/public';
|
||||
import { DOCUMENT_FIELD_NAME } from '../../../../../common';
|
||||
import { insertOrReplaceColumn, updateColumnParam, updateDefaultLabels } from '../../layer_helpers';
|
||||
import type { DataType } from '../../../../types';
|
||||
import type { DataType, OperationMetadata } from '../../../../types';
|
||||
import { OperationDefinition } from '..';
|
||||
import { FieldBasedIndexPatternColumn } from '../column_types';
|
||||
import {
|
||||
FieldBasedIndexPatternColumn,
|
||||
GenericIndexPatternColumn,
|
||||
IncompleteColumn,
|
||||
} from '../column_types';
|
||||
import { ValuesInput } from './values_input';
|
||||
import { getInvalidFieldMessage } from '../helpers';
|
||||
import { getInvalidFieldMessage, isColumn } from '../helpers';
|
||||
import { FieldInputs, getInputFieldErrorMessage, MAX_MULTI_FIELDS_SIZE } from './field_inputs';
|
||||
import {
|
||||
FieldInput as FieldInputBase,
|
||||
|
@ -226,7 +231,15 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
},
|
||||
};
|
||||
},
|
||||
toEsAggsFn: (column, columnId, _indexPattern, layer, uiSettings, orderedColumnIds) => {
|
||||
toEsAggsFn: (
|
||||
column,
|
||||
columnId,
|
||||
_indexPattern,
|
||||
layer,
|
||||
uiSettings,
|
||||
orderedColumnIds,
|
||||
operationDefinitionMap
|
||||
) => {
|
||||
if (column.params?.orderBy.type === 'rare') {
|
||||
return buildExpressionFunction<AggFunctionsMapping['aggRareTerms']>('aggRareTerms', {
|
||||
id: columnId,
|
||||
|
@ -236,7 +249,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
max_doc_count: column.params.orderBy.maxDocCount,
|
||||
}).toAst();
|
||||
}
|
||||
let orderBy = '_key';
|
||||
let orderBy: string = '_key';
|
||||
|
||||
if (column.params?.orderBy.type === 'column') {
|
||||
const orderColumn = layer.columns[column.params.orderBy.columnId];
|
||||
|
@ -254,6 +267,29 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
? Math.max(1000, column.params.size * 1.5 + 10)
|
||||
: undefined;
|
||||
|
||||
const orderAggColumn = column.params.orderAgg;
|
||||
let orderAgg;
|
||||
if (orderAggColumn) {
|
||||
orderBy = 'custom';
|
||||
const def = operationDefinitionMap?.[orderAggColumn?.operationType];
|
||||
if (def && 'toEsAggsFn' in def) {
|
||||
orderAgg = [
|
||||
{
|
||||
type: 'expression' as const,
|
||||
chain: [
|
||||
def.toEsAggsFn(
|
||||
orderAggColumn,
|
||||
`${columnId}-orderAgg`,
|
||||
_indexPattern,
|
||||
layer,
|
||||
uiSettings,
|
||||
orderedColumnIds
|
||||
),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
if (column.params?.secondaryFields?.length) {
|
||||
return buildExpressionFunction<AggFunctionsMapping['aggMultiTerms']>('aggMultiTerms', {
|
||||
id: columnId,
|
||||
|
@ -262,6 +298,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
fields: [column.sourceField, ...column.params.secondaryFields],
|
||||
orderBy,
|
||||
order: column.params.orderDirection,
|
||||
orderAgg,
|
||||
size: column.params.size,
|
||||
shardSize,
|
||||
otherBucket: Boolean(column.params.otherBucket),
|
||||
|
@ -270,6 +307,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
}),
|
||||
}).toAst();
|
||||
}
|
||||
|
||||
return buildExpressionFunction<AggFunctionsMapping['aggTerms']>('aggTerms', {
|
||||
id: columnId,
|
||||
enabled: true,
|
||||
|
@ -277,6 +315,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
field: column.sourceField,
|
||||
orderBy,
|
||||
order: column.params.orderDirection,
|
||||
orderAgg,
|
||||
size: column.params.size,
|
||||
shardSize,
|
||||
otherBucket: Boolean(column.params.otherBucket),
|
||||
|
@ -498,7 +537,22 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
</EuiFormRow>
|
||||
);
|
||||
},
|
||||
paramEditor: function ParamEditor({ layer, updateLayer, currentColumn, columnId, indexPattern }) {
|
||||
paramEditor: function ParamEditor({
|
||||
layer,
|
||||
paramEditorUpdater,
|
||||
currentColumn,
|
||||
columnId,
|
||||
indexPattern,
|
||||
existingFields,
|
||||
operationDefinitionMap,
|
||||
ReferenceEditor,
|
||||
paramEditorCustomProps,
|
||||
...rest
|
||||
}) {
|
||||
const [incompleteColumn, setIncompleteColumn] = useState<IncompleteColumn | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const hasRestrictions = indexPattern.hasRestrictions;
|
||||
|
||||
const SEPARATOR = '$$$';
|
||||
|
@ -516,6 +570,9 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
if (value === 'rare') {
|
||||
return { type: 'rare', maxDocCount: DEFAULT_MAX_DOC_COUNT };
|
||||
}
|
||||
if (value === 'custom') {
|
||||
return { type: 'custom' };
|
||||
}
|
||||
const parts = value.split(SEPARATOR);
|
||||
return {
|
||||
type: 'column',
|
||||
|
@ -548,6 +605,12 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
}),
|
||||
});
|
||||
}
|
||||
orderOptions.push({
|
||||
value: toValue({ type: 'custom' }),
|
||||
text: i18n.translate('xpack.lens.indexPattern.terms.orderCustomMetric', {
|
||||
defaultMessage: 'Custom',
|
||||
}),
|
||||
});
|
||||
|
||||
const secondaryFieldsCount = currentColumn.params.secondaryFields
|
||||
? currentColumn.params.secondaryFields.length
|
||||
|
@ -559,7 +622,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
value={currentColumn.params.size}
|
||||
disabled={currentColumn.params.orderBy.type === 'rare'}
|
||||
onChange={(value) => {
|
||||
updateLayer({
|
||||
paramEditorUpdater({
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
|
@ -590,7 +653,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
})}
|
||||
maxValue={MAXIMUM_MAX_DOC_COUNT}
|
||||
onChange={(value) => {
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
|
@ -626,12 +689,13 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
>
|
||||
<EuiSelect
|
||||
compressed
|
||||
fullWidth
|
||||
data-test-subj="indexPattern-terms-orderBy"
|
||||
options={orderOptions}
|
||||
value={toValue(currentColumn.params.orderBy)}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newOrderByValue = fromValue(e.target.value);
|
||||
const updatedLayer = updateDefaultLabels(
|
||||
let updatedLayer = updateDefaultLabels(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
|
@ -640,8 +704,33 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
}),
|
||||
indexPattern
|
||||
);
|
||||
|
||||
updateLayer(
|
||||
if (newOrderByValue.type === 'custom') {
|
||||
const initialOperation = (
|
||||
operationDefinitionMap.count as OperationDefinition<
|
||||
GenericIndexPatternColumn,
|
||||
'field'
|
||||
>
|
||||
).buildColumn({
|
||||
layer,
|
||||
indexPattern,
|
||||
field: indexPattern.getFieldByName(DOCUMENT_FIELD_NAME)!,
|
||||
});
|
||||
updatedLayer = updateColumnParam({
|
||||
layer: updatedLayer,
|
||||
columnId,
|
||||
paramName: 'orderAgg',
|
||||
value: initialOperation,
|
||||
});
|
||||
} else {
|
||||
updatedLayer = updateColumnParam({
|
||||
layer: updatedLayer,
|
||||
columnId,
|
||||
paramName: 'orderAgg',
|
||||
value: undefined,
|
||||
});
|
||||
}
|
||||
setIncompleteColumn(undefined);
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer: updatedLayer,
|
||||
columnId,
|
||||
|
@ -655,6 +744,113 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{currentColumn.params.orderAgg && ReferenceEditor && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<ReferenceEditor
|
||||
operationDefinitionMap={operationDefinitionMap}
|
||||
functionLabel={i18n.translate('xpack.lens.indexPattern.terms.orderAgg.rankFunction', {
|
||||
defaultMessage: 'Rank function',
|
||||
})}
|
||||
fieldLabel={i18n.translate('xpack.lens.indexPattern.terms.orderAgg.rankField', {
|
||||
defaultMessage: 'Rank field',
|
||||
})}
|
||||
isInline={true}
|
||||
paramEditorCustomProps={{
|
||||
...paramEditorCustomProps,
|
||||
isInline: true,
|
||||
labels: getLabelForRankFunctions(currentColumn.params.orderAgg.operationType),
|
||||
}}
|
||||
layer={layer}
|
||||
selectionStyle="full"
|
||||
columnId={`${columnId}-orderAgg`}
|
||||
currentIndexPattern={indexPattern}
|
||||
paramEditorUpdater={(setter) => {
|
||||
if (!isColumn(setter)) {
|
||||
throw new Error('Setter should always be a column when ran here.');
|
||||
}
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'orderAgg',
|
||||
value: setter,
|
||||
})
|
||||
);
|
||||
}}
|
||||
column={currentColumn.params.orderAgg}
|
||||
incompleteColumn={incompleteColumn}
|
||||
existingFields={existingFields}
|
||||
onDeleteColumn={() => {
|
||||
throw new Error('Should not be called');
|
||||
}}
|
||||
onChooseField={(choice) => {
|
||||
const field = choice.field && indexPattern.getFieldByName(choice.field);
|
||||
if (field) {
|
||||
const hypotethicalColumn = (
|
||||
operationDefinitionMap[choice.operationType] as OperationDefinition<
|
||||
GenericIndexPatternColumn,
|
||||
'field'
|
||||
>
|
||||
).buildColumn({
|
||||
previousColumn: currentColumn.params.orderAgg,
|
||||
layer,
|
||||
indexPattern,
|
||||
field,
|
||||
});
|
||||
setIncompleteColumn(undefined);
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'orderAgg',
|
||||
value: hypotethicalColumn,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setIncompleteColumn({
|
||||
sourceField: choice.field,
|
||||
operationType: choice.operationType,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onChooseFunction={(operationType: string, field?: IndexPatternField) => {
|
||||
if (field) {
|
||||
const hypotethicalColumn = (
|
||||
operationDefinitionMap[operationType] as OperationDefinition<
|
||||
GenericIndexPatternColumn,
|
||||
'field'
|
||||
>
|
||||
).buildColumn({
|
||||
previousColumn: currentColumn.params.orderAgg,
|
||||
layer,
|
||||
indexPattern,
|
||||
field,
|
||||
});
|
||||
setIncompleteColumn(undefined);
|
||||
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName: 'orderAgg',
|
||||
value: hypotethicalColumn,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
setIncompleteColumn({ operationType });
|
||||
}
|
||||
}}
|
||||
validation={{
|
||||
input: ['field', 'managedReference'],
|
||||
validateMetadata: (meta: OperationMetadata) =>
|
||||
meta.dataType === 'number' && !meta.isBucketed,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.lens.indexPattern.terms.orderDirection', {
|
||||
defaultMessage: 'Rank direction',
|
||||
|
@ -698,7 +894,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
idPrefix,
|
||||
''
|
||||
) as TermsIndexPatternColumn['params']['orderDirection'];
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
|
@ -729,7 +925,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
checked={Boolean(currentColumn.params.otherBucket)}
|
||||
disabled={currentColumn.params.orderBy.type === 'rare'}
|
||||
onChange={(e: EuiSwitchEvent) =>
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
|
@ -753,7 +949,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
data-test-subj="indexPattern-terms-missing-bucket"
|
||||
checked={Boolean(currentColumn.params.missingBucket)}
|
||||
onChange={(e: EuiSwitchEvent) =>
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
|
@ -791,7 +987,7 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
currentColumn.params.accuracyMode && currentColumn.params.orderBy.type !== 'rare'
|
||||
)}
|
||||
onChange={(e: EuiSwitchEvent) =>
|
||||
updateLayer(
|
||||
paramEditorUpdater(
|
||||
updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
|
@ -808,3 +1004,21 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
|
|||
);
|
||||
},
|
||||
};
|
||||
function getLabelForRankFunctions(operationType: string) {
|
||||
switch (operationType) {
|
||||
case 'last_value':
|
||||
return [
|
||||
i18n.translate('xpack.lens.indexPattern.terms.lastValue.sortRankBy', {
|
||||
defaultMessage: 'Sort rank by',
|
||||
}),
|
||||
];
|
||||
case 'percentile_rank':
|
||||
return [
|
||||
i18n.translate('xpack.lens.indexPattern.terms.percentile.', {
|
||||
defaultMessage: 'Percentile ranks',
|
||||
}),
|
||||
];
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,12 +22,18 @@ import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
|||
import { createMockedIndexPattern } from '../../../mocks';
|
||||
import { ValuesInput } from './values_input';
|
||||
import type { TermsIndexPatternColumn } from '.';
|
||||
import { GenericOperationDefinition, termsOperation, LastValueIndexPatternColumn } from '..';
|
||||
import {
|
||||
GenericOperationDefinition,
|
||||
termsOperation,
|
||||
LastValueIndexPatternColumn,
|
||||
operationDefinitionMap,
|
||||
} from '..';
|
||||
import { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from '../../../types';
|
||||
import { FrameDatasourceAPI } from '../../../../types';
|
||||
import { DateHistogramIndexPatternColumn } from '../date_histogram';
|
||||
import { getOperationSupportMatrix } from '../../../dimension_panel/operation_support';
|
||||
import { FieldSelect } from '../../../dimension_panel/field_select';
|
||||
import { ReferenceEditor } from '../../../dimension_panel/reference_editor';
|
||||
|
||||
// mocking random id generator function
|
||||
jest.mock('@elastic/eui', () => {
|
||||
|
@ -65,11 +71,20 @@ const defaultProps = {
|
|||
http: {} as HttpSetup,
|
||||
indexPattern: createMockedIndexPattern(),
|
||||
// need to provide the terms operation as some helpers use operation specific features
|
||||
operationDefinitionMap: { terms: termsOperation as unknown as GenericOperationDefinition },
|
||||
operationDefinitionMap,
|
||||
isFullscreen: false,
|
||||
toggleFullscreen: jest.fn(),
|
||||
setIsCloseable: jest.fn(),
|
||||
layerId: '1',
|
||||
ReferenceEditor,
|
||||
existingFields: {
|
||||
'my-fake-index-pattern': {
|
||||
timestamp: true,
|
||||
bytes: true,
|
||||
memory: true,
|
||||
source: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('terms', () => {
|
||||
|
@ -136,7 +151,8 @@ describe('terms', () => {
|
|||
{} as IndexPattern,
|
||||
layer,
|
||||
uiSettingsMock,
|
||||
[]
|
||||
[],
|
||||
operationDefinitionMap
|
||||
);
|
||||
expect(esAggsFn).toEqual(
|
||||
expect.objectContaining({
|
||||
|
@ -229,6 +245,59 @@ describe('terms', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should pass orderAgg correctly', () => {
|
||||
const termsColumn = layer.columns.col1 as TermsIndexPatternColumn;
|
||||
const esAggsFn = termsOperation.toEsAggsFn(
|
||||
{
|
||||
...termsColumn,
|
||||
params: {
|
||||
...termsColumn.params,
|
||||
orderAgg: {
|
||||
label: 'Maximum of price',
|
||||
dataType: 'number',
|
||||
operationType: 'max',
|
||||
sourceField: 'price',
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
},
|
||||
orderBy: {
|
||||
type: 'custom',
|
||||
},
|
||||
},
|
||||
},
|
||||
'col1',
|
||||
{} as IndexPattern,
|
||||
layer,
|
||||
uiSettingsMock,
|
||||
[],
|
||||
operationDefinitionMap
|
||||
);
|
||||
expect(esAggsFn).toEqual(
|
||||
expect.objectContaining({
|
||||
arguments: expect.objectContaining({
|
||||
orderAgg: [
|
||||
{
|
||||
chain: [
|
||||
{
|
||||
arguments: {
|
||||
enabled: [true],
|
||||
field: ['price'],
|
||||
id: ['col1-orderAgg'],
|
||||
schema: ['metric'],
|
||||
},
|
||||
function: 'aggMax',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
type: 'expression',
|
||||
},
|
||||
],
|
||||
orderBy: ['custom'],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should default percentile rank with non integer value to alphabetical sort', () => {
|
||||
const newLayer = {
|
||||
...layer,
|
||||
|
@ -1801,7 +1870,7 @@ describe('terms', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
|
||||
/>
|
||||
|
@ -1824,7 +1893,7 @@ describe('terms', () => {
|
|||
...createMockedIndexPattern(),
|
||||
hasRestrictions: true,
|
||||
}}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
|
||||
/>
|
||||
|
@ -1839,7 +1908,7 @@ describe('terms', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
|
||||
/>
|
||||
|
@ -1858,7 +1927,7 @@ describe('terms', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={
|
||||
{
|
||||
|
@ -1885,7 +1954,7 @@ describe('terms', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={{
|
||||
...(layer.columns.col1 as TermsIndexPatternColumn),
|
||||
|
@ -1916,7 +1985,7 @@ describe('terms', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={() => {}}
|
||||
paramEditorUpdater={() => {}}
|
||||
columnId="col1"
|
||||
currentColumn={
|
||||
{
|
||||
|
@ -1965,7 +2034,7 @@ describe('terms', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={{
|
||||
...(layer.columns.col1 as TermsIndexPatternColumn),
|
||||
|
@ -1992,7 +2061,7 @@ describe('terms', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={
|
||||
{
|
||||
|
@ -2020,7 +2089,7 @@ describe('terms', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
|
||||
/>
|
||||
|
@ -2056,7 +2125,7 @@ describe('terms', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
|
||||
/>
|
||||
|
@ -2070,6 +2139,7 @@ describe('terms', () => {
|
|||
'column$$$col2',
|
||||
'alphabetical',
|
||||
'rare',
|
||||
'custom',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -2079,7 +2149,7 @@ describe('terms', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={
|
||||
{ ...layer.columns.col1, sourceField: 'memory' } as TermsIndexPatternColumn
|
||||
|
@ -2094,6 +2164,7 @@ describe('terms', () => {
|
|||
expect(select.prop('options')!.map(({ value }) => value)).toEqual([
|
||||
'column$$$col2',
|
||||
'alphabetical',
|
||||
'custom',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -2103,7 +2174,7 @@ describe('terms', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
|
||||
/>
|
||||
|
@ -2143,7 +2214,7 @@ describe('terms', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
|
||||
/>
|
||||
|
@ -2160,7 +2231,7 @@ describe('terms', () => {
|
|||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
|
||||
/>
|
||||
|
@ -2183,13 +2254,210 @@ describe('terms', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should render reference editor when order is set to custom metric', () => {
|
||||
const updateLayerSpy = jest.fn();
|
||||
const currentLayer = {
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
col1: {
|
||||
...layer.columns.col1,
|
||||
params: {
|
||||
...(layer.columns.col1 as TermsIndexPatternColumn).params,
|
||||
type: 'custom',
|
||||
orderDirection: 'desc',
|
||||
orderAgg: {
|
||||
label: 'Median of bytes',
|
||||
dataType: 'number',
|
||||
operationType: 'median',
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const instance = shallow(
|
||||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={currentLayer}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={currentLayer.columns.col1 as TermsIndexPatternColumn}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(instance.find(`ReferenceEditor`)).toHaveLength(1);
|
||||
|
||||
instance
|
||||
.find(EuiSelect)
|
||||
.find('[data-test-subj="indexPattern-terms-orderBy"]')
|
||||
.simulate('change', {
|
||||
target: {
|
||||
value: 'column$$$col2',
|
||||
},
|
||||
});
|
||||
|
||||
expect(updateLayerSpy).toHaveBeenCalledWith({
|
||||
...currentLayer,
|
||||
columns: {
|
||||
...currentLayer.columns,
|
||||
col1: {
|
||||
...currentLayer.columns.col1,
|
||||
params: {
|
||||
...(currentLayer.columns.col1 as TermsIndexPatternColumn).params,
|
||||
orderAgg: undefined,
|
||||
orderBy: {
|
||||
columnId: 'col2',
|
||||
type: 'column',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should update column when changing the operation for orderAgg', () => {
|
||||
const updateLayerSpy = jest.fn();
|
||||
const currentLayer = {
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
col1: {
|
||||
...layer.columns.col1,
|
||||
params: {
|
||||
...(layer.columns.col1 as TermsIndexPatternColumn).params,
|
||||
type: 'custom',
|
||||
orderDirection: 'desc',
|
||||
orderAgg: {
|
||||
label: 'Median of bytes',
|
||||
dataType: 'number',
|
||||
operationType: 'median',
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const instance = mount(
|
||||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={currentLayer}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={currentLayer.columns.col1 as TermsIndexPatternColumn}
|
||||
/>
|
||||
);
|
||||
const refEditor = instance.find(`ReferenceEditor`);
|
||||
expect(refEditor).toHaveLength(1);
|
||||
|
||||
const functionComboBox = refEditor
|
||||
.find(EuiComboBox)
|
||||
.filter('[data-test-subj="indexPattern-reference-function"]');
|
||||
const option = functionComboBox.prop('options')!.find(({ label }) => label === 'Average')!;
|
||||
|
||||
act(() => {
|
||||
functionComboBox.prop('onChange')!([option]);
|
||||
});
|
||||
|
||||
expect(updateLayerSpy).toHaveBeenCalledWith({
|
||||
...currentLayer,
|
||||
columns: {
|
||||
...currentLayer.columns,
|
||||
col1: {
|
||||
...currentLayer.columns.col1,
|
||||
params: {
|
||||
...(currentLayer.columns.col1 as TermsIndexPatternColumn).params,
|
||||
orderAgg: expect.objectContaining({
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Average of bytes',
|
||||
operationType: 'average',
|
||||
sourceField: 'bytes',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should update column when changing the field for orderAgg', () => {
|
||||
const updateLayerSpy = jest.fn();
|
||||
const currentLayer = {
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
col1: {
|
||||
...layer.columns.col1,
|
||||
params: {
|
||||
...(layer.columns.col1 as TermsIndexPatternColumn).params,
|
||||
type: 'custom',
|
||||
orderDirection: 'desc',
|
||||
orderAgg: {
|
||||
label: 'Median of bytes',
|
||||
dataType: 'number',
|
||||
operationType: 'median',
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
sourceField: 'bytes',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const instance = mount(
|
||||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={currentLayer}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={currentLayer.columns.col1 as TermsIndexPatternColumn}
|
||||
/>
|
||||
);
|
||||
const refEditor = instance.find(`ReferenceEditor`);
|
||||
expect(refEditor).toHaveLength(1);
|
||||
|
||||
const comboBoxes = refEditor.find(EuiComboBox);
|
||||
|
||||
const fieldComboBox = comboBoxes.filter('[data-test-subj="indexPattern-dimension-field"]');
|
||||
|
||||
const option = fieldComboBox
|
||||
.prop('options')[0]
|
||||
.options!.find(({ label }) => label === 'memory')!;
|
||||
act(() => {
|
||||
fieldComboBox.prop('onChange')!([option]);
|
||||
});
|
||||
expect(updateLayerSpy).toHaveBeenCalledWith({
|
||||
...currentLayer,
|
||||
columns: {
|
||||
...currentLayer.columns,
|
||||
col1: {
|
||||
...currentLayer.columns.col1,
|
||||
params: {
|
||||
...(currentLayer.columns.col1 as TermsIndexPatternColumn).params,
|
||||
orderAgg: expect.objectContaining({
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Median of memory',
|
||||
operationType: 'median',
|
||||
sourceField: 'memory',
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should render current size value', () => {
|
||||
const updateLayerSpy = jest.fn();
|
||||
const instance = mount(
|
||||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
|
||||
/>
|
||||
|
@ -2198,13 +2466,64 @@ describe('terms', () => {
|
|||
expect(instance.find(EuiFieldNumber).prop('value')).toEqual('3');
|
||||
});
|
||||
|
||||
it('should not update the column when the change creates incomplete column', () => {
|
||||
const updateLayerSpy = jest.fn();
|
||||
const currentLayer = {
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
col1: {
|
||||
...layer.columns.col1,
|
||||
params: {
|
||||
...(layer.columns.col1 as TermsIndexPatternColumn).params,
|
||||
type: 'custom',
|
||||
orderDirection: 'desc',
|
||||
orderAgg: {
|
||||
label: 'Count of records',
|
||||
dataType: 'number',
|
||||
operationType: 'count',
|
||||
isBucketed: false,
|
||||
scale: 'ratio',
|
||||
sourceField: '___records___',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const instance = mount(
|
||||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={currentLayer}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={currentLayer.columns.col1 as TermsIndexPatternColumn}
|
||||
/>
|
||||
);
|
||||
const refEditor = instance.find(`ReferenceEditor`);
|
||||
expect(refEditor).toHaveLength(1);
|
||||
|
||||
const comboBoxes = refEditor.find(EuiComboBox);
|
||||
|
||||
const functionComboBox = comboBoxes.filter(
|
||||
'[data-test-subj="indexPattern-reference-function"]'
|
||||
);
|
||||
const fieldComboBox = comboBoxes.filter('[data-test-subj="indexPattern-dimension-field"]');
|
||||
const option = functionComboBox.prop('options')!.find(({ label }) => label === 'Average')!;
|
||||
act(() => {
|
||||
functionComboBox.prop('onChange')!([option]);
|
||||
});
|
||||
|
||||
expect(fieldComboBox.prop('isInvalid')).toBeTruthy();
|
||||
expect(updateLayerSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update state with the size value', () => {
|
||||
const updateLayerSpy = jest.fn();
|
||||
const instance = mount(
|
||||
<InlineOptions
|
||||
{...defaultProps}
|
||||
layer={layer}
|
||||
updateLayer={updateLayerSpy}
|
||||
paramEditorUpdater={updateLayerSpy}
|
||||
columnId="col1"
|
||||
currentColumn={layer.columns.col1 as TermsIndexPatternColumn}
|
||||
/>
|
||||
|
|
|
@ -18,7 +18,9 @@ export interface TermsIndexPatternColumn extends FieldBasedIndexPatternColumn {
|
|||
orderBy:
|
||||
| { type: 'alphabetical'; fallback?: boolean }
|
||||
| { type: 'rare'; maxDocCount: number }
|
||||
| { type: 'column'; columnId: string };
|
||||
| { type: 'column'; columnId: string }
|
||||
| { type: 'custom' };
|
||||
orderAgg?: FieldBasedIndexPatternColumn;
|
||||
orderDirection: 'asc' | 'desc';
|
||||
otherBucket?: boolean;
|
||||
missingBucket?: boolean;
|
||||
|
|
|
@ -77,6 +77,7 @@ export const ValuesInput = ({
|
|||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
fullWidth
|
||||
min={minValue}
|
||||
max={maxValue}
|
||||
step={1}
|
||||
|
|
|
@ -41,6 +41,9 @@ import { CoreStart } from '@kbn/core/public';
|
|||
|
||||
jest.mock('.');
|
||||
jest.mock('../../id_generator');
|
||||
jest.mock('../dimension_panel/reference_editor', () => ({
|
||||
ReferenceEditor: () => null,
|
||||
}));
|
||||
|
||||
const indexPatternFields = [
|
||||
{
|
||||
|
|
|
@ -503,17 +503,14 @@ export function replaceColumn({
|
|||
|
||||
tempLayer = {
|
||||
...tempLayer,
|
||||
columnOrder: getColumnOrder(tempLayer),
|
||||
columns: {
|
||||
...tempLayer.columns,
|
||||
[columnId]: column,
|
||||
},
|
||||
};
|
||||
return updateDefaultLabels(
|
||||
{
|
||||
...tempLayer,
|
||||
columnOrder: getColumnOrder(tempLayer),
|
||||
columns: adjustColumnReferencesForChangedColumn(tempLayer, columnId),
|
||||
},
|
||||
adjustColumnReferencesForChangedColumn(tempLayer, columnId),
|
||||
indexPattern
|
||||
);
|
||||
} else if (
|
||||
|
@ -573,11 +570,14 @@ export function replaceColumn({
|
|||
}
|
||||
|
||||
return updateDefaultLabels(
|
||||
{
|
||||
...tempLayer,
|
||||
columnOrder: getColumnOrder(newLayer),
|
||||
columns: adjustColumnReferencesForChangedColumn(newLayer, columnId),
|
||||
},
|
||||
adjustColumnReferencesForChangedColumn(
|
||||
{
|
||||
...tempLayer,
|
||||
columnOrder: getColumnOrder(newLayer),
|
||||
columns: newLayer.columns,
|
||||
},
|
||||
columnId
|
||||
),
|
||||
indexPattern
|
||||
);
|
||||
}
|
||||
|
@ -592,13 +592,18 @@ export function replaceColumn({
|
|||
indexPattern
|
||||
);
|
||||
|
||||
const newLayer = { ...tempLayer, columns: { ...tempLayer.columns, [columnId]: newColumn } };
|
||||
const newLayer = {
|
||||
...tempLayer,
|
||||
columns: { ...tempLayer.columns, [columnId]: newColumn },
|
||||
};
|
||||
return updateDefaultLabels(
|
||||
{
|
||||
...tempLayer,
|
||||
columnOrder: getColumnOrder(newLayer),
|
||||
columns: adjustColumnReferencesForChangedColumn(newLayer, columnId),
|
||||
},
|
||||
adjustColumnReferencesForChangedColumn(
|
||||
{
|
||||
...newLayer,
|
||||
columnOrder: getColumnOrder(newLayer),
|
||||
},
|
||||
columnId
|
||||
),
|
||||
indexPattern
|
||||
);
|
||||
}
|
||||
|
@ -650,11 +655,13 @@ export function replaceColumn({
|
|||
}
|
||||
const newLayer = { ...tempLayer, columns: { ...tempLayer.columns, [columnId]: newColumn } };
|
||||
return updateDefaultLabels(
|
||||
{
|
||||
...tempLayer,
|
||||
columnOrder: getColumnOrder(newLayer),
|
||||
columns: adjustColumnReferencesForChangedColumn(newLayer, columnId),
|
||||
},
|
||||
adjustColumnReferencesForChangedColumn(
|
||||
{
|
||||
...newLayer,
|
||||
columnOrder: getColumnOrder(newLayer),
|
||||
},
|
||||
columnId
|
||||
),
|
||||
indexPattern
|
||||
);
|
||||
} else if (
|
||||
|
@ -677,11 +684,13 @@ export function replaceColumn({
|
|||
{ ...layer, columns: { ...layer.columns, [columnId]: newColumn } },
|
||||
columnId
|
||||
);
|
||||
return {
|
||||
...newLayer,
|
||||
columnOrder: getColumnOrder(newLayer),
|
||||
columns: adjustColumnReferencesForChangedColumn(newLayer, columnId),
|
||||
};
|
||||
return adjustColumnReferencesForChangedColumn(
|
||||
{
|
||||
...newLayer,
|
||||
columnOrder: getColumnOrder(newLayer),
|
||||
},
|
||||
columnId
|
||||
);
|
||||
} else {
|
||||
throw new Error('nothing changed');
|
||||
}
|
||||
|
@ -836,11 +845,13 @@ function applyReferenceTransition({
|
|||
},
|
||||
},
|
||||
};
|
||||
layer = {
|
||||
...layer,
|
||||
columnOrder: getColumnOrder(newLayer),
|
||||
columns: adjustColumnReferencesForChangedColumn(newLayer, newId),
|
||||
};
|
||||
layer = adjustColumnReferencesForChangedColumn(
|
||||
{
|
||||
...newLayer,
|
||||
columnOrder: getColumnOrder(newLayer),
|
||||
},
|
||||
newId
|
||||
);
|
||||
return newId;
|
||||
}
|
||||
|
||||
|
@ -977,11 +988,13 @@ function applyReferenceTransition({
|
|||
},
|
||||
};
|
||||
return updateDefaultLabels(
|
||||
{
|
||||
...layer,
|
||||
columnOrder: getColumnOrder(layer),
|
||||
columns: adjustColumnReferencesForChangedColumn(layer, columnId),
|
||||
},
|
||||
adjustColumnReferencesForChangedColumn(
|
||||
{
|
||||
...layer,
|
||||
columnOrder: getColumnOrder(layer),
|
||||
},
|
||||
columnId
|
||||
),
|
||||
indexPattern
|
||||
);
|
||||
}
|
||||
|
@ -1044,11 +1057,13 @@ function addBucket(
|
|||
columns: { ...layer.columns, [addedColumnId]: column },
|
||||
columnOrder: updatedColumnOrder,
|
||||
};
|
||||
return {
|
||||
...tempLayer,
|
||||
columns: adjustColumnReferencesForChangedColumn(tempLayer, addedColumnId),
|
||||
columnOrder: getColumnOrder(tempLayer),
|
||||
};
|
||||
return adjustColumnReferencesForChangedColumn(
|
||||
{
|
||||
...tempLayer,
|
||||
columnOrder: getColumnOrder(tempLayer),
|
||||
},
|
||||
addedColumnId
|
||||
);
|
||||
}
|
||||
|
||||
export function reorderByGroups(
|
||||
|
@ -1108,11 +1123,13 @@ function addMetric(
|
|||
[addedColumnId]: column,
|
||||
},
|
||||
};
|
||||
return {
|
||||
...tempLayer,
|
||||
columnOrder: getColumnOrder(tempLayer),
|
||||
columns: adjustColumnReferencesForChangedColumn(tempLayer, addedColumnId),
|
||||
};
|
||||
return adjustColumnReferencesForChangedColumn(
|
||||
{
|
||||
...tempLayer,
|
||||
columnOrder: getColumnOrder(tempLayer),
|
||||
},
|
||||
addedColumnId
|
||||
);
|
||||
}
|
||||
|
||||
export function getMetricOperationTypes(field: IndexPatternField) {
|
||||
|
@ -1146,7 +1163,7 @@ export function updateColumnLabel<C extends GenericIndexPatternColumn>({
|
|||
};
|
||||
}
|
||||
|
||||
export function updateColumnParam<C extends GenericIndexPatternColumn>({
|
||||
export function updateColumnParam({
|
||||
layer,
|
||||
columnId,
|
||||
paramName,
|
||||
|
@ -1157,15 +1174,15 @@ export function updateColumnParam<C extends GenericIndexPatternColumn>({
|
|||
paramName: string;
|
||||
value: unknown;
|
||||
}): IndexPatternLayer {
|
||||
const oldColumn = layer.columns[columnId];
|
||||
const currentColumn = layer.columns[columnId];
|
||||
return {
|
||||
...layer,
|
||||
columns: {
|
||||
...layer.columns,
|
||||
[columnId]: {
|
||||
...oldColumn,
|
||||
...currentColumn,
|
||||
params: {
|
||||
...('params' in oldColumn ? oldColumn.params : {}),
|
||||
...('params' in currentColumn ? currentColumn.params : {}),
|
||||
[paramName]: value,
|
||||
},
|
||||
},
|
||||
|
@ -1210,7 +1227,10 @@ export function adjustColumnReferencesForChangedColumn(
|
|||
: currentColumn;
|
||||
}
|
||||
});
|
||||
return newColumns;
|
||||
return {
|
||||
...layer,
|
||||
columns: newColumns,
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteColumn({
|
||||
|
@ -1238,13 +1258,13 @@ export function deleteColumn({
|
|||
const hypotheticalColumns = { ...layer.columns };
|
||||
delete hypotheticalColumns[columnId];
|
||||
|
||||
let newLayer = {
|
||||
...layer,
|
||||
columns: adjustColumnReferencesForChangedColumn(
|
||||
{ ...layer, columns: hypotheticalColumns },
|
||||
columnId
|
||||
),
|
||||
};
|
||||
let newLayer = adjustColumnReferencesForChangedColumn(
|
||||
{
|
||||
...layer,
|
||||
columns: hypotheticalColumns,
|
||||
},
|
||||
columnId
|
||||
);
|
||||
|
||||
extraDeletions.forEach((id) => {
|
||||
newLayer = deleteColumn({ layer: newLayer, columnId: id, indexPattern });
|
||||
|
|
|
@ -39,7 +39,7 @@ export function getOperations(): OperationType[] {
|
|||
/**
|
||||
* Returns a list of the display names of all operations with any guaranteed order.
|
||||
*/
|
||||
export function getOperationDisplay() {
|
||||
export const getOperationDisplay = memoize(() => {
|
||||
const display = {} as Record<
|
||||
OperationType,
|
||||
{
|
||||
|
@ -54,7 +54,7 @@ export function getOperationDisplay() {
|
|||
};
|
||||
});
|
||||
return display;
|
||||
}
|
||||
});
|
||||
|
||||
export function getSortScoreByPriority(
|
||||
a: GenericOperationDefinition,
|
||||
|
|
|
@ -130,7 +130,8 @@ function getExpressionForLayer(
|
|||
indexPattern,
|
||||
layer,
|
||||
uiSettings,
|
||||
orderedColumnIds
|
||||
orderedColumnIds,
|
||||
operationDefinitionMap
|
||||
);
|
||||
if (wrapInFilter) {
|
||||
aggAst = buildExpressionFunction<AggFunctionsMapping['aggFilteredMetric']>(
|
||||
|
|
|
@ -46,6 +46,7 @@ export function CollapseSetting({
|
|||
fullWidth
|
||||
>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
compressed
|
||||
data-test-subj="indexPattern-terms-orderBy"
|
||||
options={options}
|
||||
|
|
|
@ -44,6 +44,7 @@ export function PalettePicker({
|
|||
>
|
||||
<>
|
||||
<EuiColorPalettePicker
|
||||
fullWidth
|
||||
data-test-subj="lns-palettePicker"
|
||||
compressed
|
||||
palettes={palettesToShow}
|
||||
|
|
|
@ -430,7 +430,10 @@ export type DatasourceDimensionProps<T> = SharedDimensionProps & {
|
|||
invalid?: boolean;
|
||||
invalidMessage?: string;
|
||||
};
|
||||
export type ParamEditorCustomProps = Record<string, unknown> & { label?: string };
|
||||
export type ParamEditorCustomProps = Record<string, unknown> & {
|
||||
labels?: string[];
|
||||
isInline?: boolean;
|
||||
};
|
||||
// The only way a visualization has to restrict the query building
|
||||
export type DatasourceDimensionEditorProps<T = unknown> = DatasourceDimensionProps<T> & {
|
||||
// Not a StateSetter because we have this unique use case of determining valid columns
|
||||
|
|
|
@ -458,9 +458,11 @@ export const getReferenceConfiguration = ({
|
|||
enableDimensionEditor: true,
|
||||
supportStaticValue: true,
|
||||
paramEditorCustomProps: {
|
||||
label: i18n.translate('xpack.lens.indexPattern.staticValue.label', {
|
||||
defaultMessage: 'Reference line value',
|
||||
}),
|
||||
labels: [
|
||||
i18n.translate('xpack.lens.indexPattern.staticValue.label', {
|
||||
defaultMessage: 'Reference line value',
|
||||
}),
|
||||
],
|
||||
},
|
||||
supportFieldFormat: false,
|
||||
dataTestSubj,
|
||||
|
|
|
@ -72,7 +72,7 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext
|
|||
loadTestFile(require.resolve('./add_to_dashboard'));
|
||||
loadTestFile(require.resolve('./runtime_fields'));
|
||||
loadTestFile(require.resolve('./dashboard'));
|
||||
loadTestFile(require.resolve('./multi_terms'));
|
||||
loadTestFile(require.resolve('./terms'));
|
||||
loadTestFile(require.resolve('./epoch_millis'));
|
||||
loadTestFile(require.resolve('./show_underlying_data'));
|
||||
loadTestFile(require.resolve('./show_underlying_data_dashboard'));
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']);
|
||||
const elasticChart = getService('elasticChart');
|
||||
|
||||
describe('lens multi terms suite', () => {
|
||||
it('should allow creation of lens xy chart with multi terms categories', async () => {
|
||||
await PageObjects.visualize.navigateToNewVisualization();
|
||||
await PageObjects.visualize.clickVisType('lens');
|
||||
await elasticChart.setNewChartUiDebugFlag(true);
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
|
||||
operation: 'average',
|
||||
field: 'bytes',
|
||||
});
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
|
||||
operation: 'terms',
|
||||
field: 'geo.src',
|
||||
keepOpen: true,
|
||||
});
|
||||
|
||||
await PageObjects.lens.addTermToAgg('geo.dest');
|
||||
|
||||
await PageObjects.lens.closeDimensionEditor();
|
||||
|
||||
expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel', 0)).to.eql(
|
||||
'Top values of geo.src + 1 other'
|
||||
);
|
||||
|
||||
await PageObjects.lens.openDimensionEditor('lnsXY_xDimensionPanel');
|
||||
|
||||
await PageObjects.lens.addTermToAgg('bytes');
|
||||
|
||||
await PageObjects.lens.closeDimensionEditor();
|
||||
|
||||
expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel', 0)).to.eql(
|
||||
'Top values of geo.src + 2 others'
|
||||
);
|
||||
|
||||
const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
|
||||
expect(data!.bars![0].bars[0].x).to.eql('PE › US › 19,986');
|
||||
});
|
||||
|
||||
it('should allow creation of lens xy chart with multi terms categories split', async () => {
|
||||
await PageObjects.lens.removeDimension('lnsXY_xDimensionPanel');
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
|
||||
operation: 'date_histogram',
|
||||
field: '@timestamp',
|
||||
});
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension',
|
||||
operation: 'terms',
|
||||
field: 'geo.src',
|
||||
keepOpen: true,
|
||||
});
|
||||
|
||||
await PageObjects.lens.addTermToAgg('geo.dest');
|
||||
await PageObjects.lens.addTermToAgg('bytes');
|
||||
|
||||
await PageObjects.lens.closeDimensionEditor();
|
||||
|
||||
const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
|
||||
expect(data?.bars?.[0]?.name).to.eql('PE › US › 19,986');
|
||||
});
|
||||
|
||||
it('should not show existing defined fields for new term', async () => {
|
||||
await PageObjects.lens.openDimensionEditor('lnsXY_splitDimensionPanel');
|
||||
|
||||
await PageObjects.lens.checkTermsAreNotAvailableToAgg(['bytes', 'geo.src', 'geo.dest']);
|
||||
|
||||
await PageObjects.lens.closeDimensionEditor();
|
||||
});
|
||||
});
|
||||
}
|
156
x-pack/test/functional/apps/lens/group2/terms.ts
Normal file
156
x-pack/test/functional/apps/lens/group2/terms.ts
Normal file
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']);
|
||||
const elasticChart = getService('elasticChart');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const comboBox = getService('comboBox');
|
||||
const find = getService('find');
|
||||
const retry = getService('retry');
|
||||
|
||||
describe('lens terms', () => {
|
||||
describe('lens multi terms suite', () => {
|
||||
it('should allow creation of lens xy chart with multi terms categories', async () => {
|
||||
await PageObjects.visualize.navigateToNewVisualization();
|
||||
await PageObjects.visualize.clickVisType('lens');
|
||||
await elasticChart.setNewChartUiDebugFlag(true);
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
|
||||
operation: 'average',
|
||||
field: 'bytes',
|
||||
});
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
|
||||
operation: 'terms',
|
||||
field: 'geo.src',
|
||||
keepOpen: true,
|
||||
});
|
||||
|
||||
await PageObjects.lens.addTermToAgg('geo.dest');
|
||||
|
||||
await PageObjects.lens.closeDimensionEditor();
|
||||
|
||||
expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel', 0)).to.eql(
|
||||
'Top values of geo.src + 1 other'
|
||||
);
|
||||
|
||||
await PageObjects.lens.openDimensionEditor('lnsXY_xDimensionPanel');
|
||||
|
||||
await PageObjects.lens.addTermToAgg('bytes');
|
||||
|
||||
await PageObjects.lens.closeDimensionEditor();
|
||||
|
||||
expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel', 0)).to.eql(
|
||||
'Top values of geo.src + 2 others'
|
||||
);
|
||||
|
||||
const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
|
||||
expect(data!.bars![0].bars[0].x).to.eql('PE › US › 19,986');
|
||||
});
|
||||
|
||||
it('should allow creation of lens xy chart with multi terms categories split', async () => {
|
||||
await PageObjects.lens.removeDimension('lnsXY_xDimensionPanel');
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
|
||||
operation: 'date_histogram',
|
||||
field: '@timestamp',
|
||||
});
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension',
|
||||
operation: 'terms',
|
||||
field: 'geo.src',
|
||||
keepOpen: true,
|
||||
});
|
||||
|
||||
await PageObjects.lens.addTermToAgg('geo.dest');
|
||||
await PageObjects.lens.addTermToAgg('bytes');
|
||||
|
||||
await PageObjects.lens.closeDimensionEditor();
|
||||
|
||||
const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
|
||||
expect(data?.bars?.[0]?.name).to.eql('PE › US › 19,986');
|
||||
});
|
||||
|
||||
it('should not show existing defined fields for new term', async () => {
|
||||
await PageObjects.lens.openDimensionEditor('lnsXY_splitDimensionPanel');
|
||||
|
||||
await PageObjects.lens.checkTermsAreNotAvailableToAgg(['bytes', 'geo.src', 'geo.dest']);
|
||||
|
||||
await PageObjects.lens.closeDimensionEditor();
|
||||
});
|
||||
});
|
||||
describe('sorting by custom metric', () => {
|
||||
it('should allow sort by custom metric', async () => {
|
||||
await PageObjects.visualize.navigateToNewVisualization();
|
||||
await PageObjects.visualize.clickVisType('lens');
|
||||
await elasticChart.setNewChartUiDebugFlag(true);
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
|
||||
operation: 'average',
|
||||
field: 'bytes',
|
||||
});
|
||||
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
|
||||
operation: 'terms',
|
||||
field: 'geo.src',
|
||||
keepOpen: true,
|
||||
});
|
||||
await find.clickByCssSelector(
|
||||
'select[data-test-subj="indexPattern-terms-orderBy"] > option[value="custom"]'
|
||||
);
|
||||
|
||||
const fnTarget = await testSubjects.find('indexPattern-reference-function');
|
||||
await comboBox.openOptionsList(fnTarget);
|
||||
await comboBox.setElement(fnTarget, 'percentile');
|
||||
|
||||
const fieldTarget = await testSubjects.find(
|
||||
'indexPattern-reference-field-selection-row>indexPattern-dimension-field'
|
||||
);
|
||||
await comboBox.openOptionsList(fieldTarget);
|
||||
await comboBox.setElement(fieldTarget, 'bytes');
|
||||
|
||||
await retry.try(async () => {
|
||||
// Can not use testSubjects because data-test-subj is placed range input and number input
|
||||
const percentileInput = await find.byCssSelector(
|
||||
`input[data-test-subj="lns-indexPattern-percentile-input"][type='number']`
|
||||
);
|
||||
await percentileInput.click();
|
||||
await percentileInput.clearValue();
|
||||
await percentileInput.type('60');
|
||||
|
||||
const percentileValue = await percentileInput.getAttribute('value');
|
||||
if (percentileValue !== '60') {
|
||||
throw new Error('layerPanelTopHitsSize not set to 60');
|
||||
}
|
||||
});
|
||||
|
||||
await PageObjects.lens.waitForVisualization('xyVisChart');
|
||||
await PageObjects.lens.closeDimensionEditor();
|
||||
|
||||
expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel', 0)).to.eql(
|
||||
'Top 5 values of geo.src'
|
||||
);
|
||||
|
||||
const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
|
||||
expect(data!.bars![0].bars[0].x).to.eql('BN');
|
||||
expect(data!.bars![0].bars[0].y).to.eql(19265);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue