Skip to content

Commit

Permalink
[Lens] Handle missing fields gracefully (#78173) (#79102)
Browse files Browse the repository at this point in the history
  • Loading branch information
flash1293 authored Oct 1, 2020
1 parent 1629b2b commit a93a323
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiHorizontalRule, EuiRadio, EuiSelect, htmlIdGenerator } from '@elastic/eui';
import { IndexPatternLayer, IndexPatternField } from '../types';
import { hasField } from '../utils';
import { IndexPatternColumn } from '../operations';

const generator = htmlIdGenerator('lens-nesting');

Expand All @@ -21,6 +22,10 @@ function nestColumn(columnOrder: string[], outer: string, inner: string) {
return result;
}

function getFieldName(fieldMap: Record<string, IndexPatternField>, column: IndexPatternColumn) {
return hasField(column) ? fieldMap[column.sourceField]?.displayName || column.sourceField : '';
}

export function BucketNestingEditor({
columnId,
layer,
Expand All @@ -39,15 +44,15 @@ export function BucketNestingEditor({
.map(([value, c]) => ({
value,
text: c.label,
fieldName: hasField(c) ? fieldMap[c.sourceField].displayName : '',
fieldName: getFieldName(fieldMap, c),
operationType: c.operationType,
}));

if (!column || !column.isBucketed || !aggColumns.length) {
return null;
}

const fieldName = hasField(column) ? fieldMap[column.sourceField].displayName : '';
const fieldName = getFieldName(fieldMap, column);

const prevColumn = layer.columnOrder[layer.columnOrder.indexOf(columnId) - 1];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
} from '../operations';
import { deleteColumn, changeColumn, updateColumnParam } from '../state_helpers';
import { FieldSelect } from './field_select';
import { hasField } from '../utils';
import { hasField, fieldIsInvalid } from '../utils';
import { BucketNestingEditor } from './bucket_nesting_editor';
import { IndexPattern, IndexPatternField } from '../types';
import { trackUiEvent } from '../../lens_ui_telemetry';
Expand Down Expand Up @@ -132,6 +132,15 @@ export function DimensionEditor(props: DimensionEditorProps) {
};
});

const selectedColumnSourceField =
selectedColumn && 'sourceField' in selectedColumn ? selectedColumn.sourceField : undefined;

const currentFieldIsInvalid = useMemo(
() =>
fieldIsInvalid(selectedColumnSourceField, selectedColumn?.operationType, currentIndexPattern),
[selectedColumnSourceField, selectedColumn?.operationType, currentIndexPattern]
);

const sideNavItems: EuiListGroupItemProps[] = operationsWithCompatibility.map(
({ operationType, compatibleWithCurrentField }) => {
const isActive = Boolean(
Expand Down Expand Up @@ -271,20 +280,16 @@ export function DimensionEditor(props: DimensionEditorProps) {
defaultMessage: 'Choose a field',
})}
fullWidth
isInvalid={Boolean(incompatibleSelectedOperationType)}
error={
selectedColumn && incompatibleSelectedOperationType
? selectedOperationDefinition?.input === 'field'
? i18n.translate('xpack.lens.indexPattern.invalidOperationLabel', {
defaultMessage: 'To use this function, select a different field.',
})
: i18n.translate('xpack.lens.indexPattern.chooseFieldLabel', {
defaultMessage: 'To use this function, select a field.',
})
: undefined
}
isInvalid={Boolean(incompatibleSelectedOperationType || currentFieldIsInvalid)}
error={getErrorMessage(
selectedColumn,
Boolean(incompatibleSelectedOperationType),
selectedOperationDefinition?.input,
currentFieldIsInvalid
)}
>
<FieldSelect
fieldIsInvalid={currentFieldIsInvalid}
currentIndexPattern={currentIndexPattern}
existingFields={state.existingFields}
fieldMap={fieldMap}
Expand Down Expand Up @@ -355,90 +360,117 @@ export function DimensionEditor(props: DimensionEditorProps) {
</EuiFormRow>
) : null}

{!incompatibleSelectedOperationType && selectedColumn && ParamEditor && (
<>
<ParamEditor
state={state}
setState={setState}
columnId={columnId}
currentColumn={state.layers[layerId].columns[columnId]}
storage={props.storage}
uiSettings={props.uiSettings}
savedObjectsClient={props.savedObjectsClient}
layerId={layerId}
http={props.http}
dateRange={props.dateRange}
data={props.data}
/>
</>
)}
{!currentFieldIsInvalid &&
!incompatibleSelectedOperationType &&
selectedColumn &&
ParamEditor && (
<>
<ParamEditor
state={state}
setState={setState}
columnId={columnId}
currentColumn={state.layers[layerId].columns[columnId]}
storage={props.storage}
uiSettings={props.uiSettings}
savedObjectsClient={props.savedObjectsClient}
layerId={layerId}
http={props.http}
dateRange={props.dateRange}
data={props.data}
/>
</>
)}
</div>

<EuiSpacer size="s" />

<div className="lnsIndexPatternDimensionEditor__section">
{!incompatibleSelectedOperationType && selectedColumn && (
<LabelInput
value={selectedColumn.label}
onChange={(value) => {
setState({
...state,
layers: {
...state.layers,
[layerId]: {
...state.layers[layerId],
columns: {
...state.layers[layerId].columns,
[columnId]: {
...selectedColumn,
label: value,
customLabel: true,
{!currentFieldIsInvalid && (
<div className="lnsIndexPatternDimensionEditor__section">
{!incompatibleSelectedOperationType && selectedColumn && (
<LabelInput
value={selectedColumn.label}
onChange={(value) => {
setState({
...state,
layers: {
...state.layers,
[layerId]: {
...state.layers[layerId],
columns: {
...state.layers[layerId].columns,
[columnId]: {
...selectedColumn,
label: value,
customLabel: true,
},
},
},
},
},
});
}}
/>
)}

{!hideGrouping && (
<BucketNestingEditor
fieldMap={fieldMap}
layer={state.layers[props.layerId]}
columnId={props.columnId}
setColumns={(columnOrder) => {
setState({
...state,
layers: {
...state.layers,
[props.layerId]: {
...state.layers[props.layerId],
columnOrder,
});
}}
/>
)}

{!hideGrouping && (
<BucketNestingEditor
fieldMap={fieldMap}
layer={state.layers[props.layerId]}
columnId={props.columnId}
setColumns={(columnOrder) => {
setState({
...state,
layers: {
...state.layers,
[props.layerId]: {
...state.layers[props.layerId],
columnOrder,
},
},
},
});
}}
/>
)}

{selectedColumn && selectedColumn.dataType === 'number' ? (
<FormatSelector
selectedColumn={selectedColumn}
onChange={(newFormat) => {
setState(
updateColumnParam({
state,
layerId,
currentColumn: selectedColumn,
paramName: 'format',
value: newFormat,
})
);
}}
/>
) : null}
</div>
});
}}
/>
)}

{selectedColumn && selectedColumn.dataType === 'number' ? (
<FormatSelector
selectedColumn={selectedColumn}
onChange={(newFormat) => {
setState(
updateColumnParam({
state,
layerId,
currentColumn: selectedColumn,
paramName: 'format',
value: newFormat,
})
);
}}
/>
) : null}
</div>
)}
</div>
);
}
function getErrorMessage(
selectedColumn: IndexPatternColumn | undefined,
incompatibleSelectedOperationType: boolean,
input: 'none' | 'field' | undefined,
fieldInvalid: boolean
) {
if (selectedColumn && incompatibleSelectedOperationType) {
if (input === 'field') {
return i18n.translate('xpack.lens.indexPattern.invalidOperationLabel', {
defaultMessage: 'To use this function, select a different field.',
});
}
return i18n.translate('xpack.lens.indexPattern.chooseFieldLabel', {
defaultMessage: 'To use this function, select a field.',
});
}
if (fieldInvalid) {
return i18n.translate('xpack.lens.indexPattern.invalidFieldLabel', {
defaultMessage: 'Invalid field. Check your index pattern or pick another field.',
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { IndexPatternPrivateState } from '../types';
import { IndexPatternColumn } from '../operations';
import { documentField } from '../document_field';
import { OperationMetadata } from '../../types';
import { DateHistogramIndexPatternColumn } from '../operations/definitions/date_histogram';

jest.mock('../loader');
jest.mock('../state_helpers');
Expand Down Expand Up @@ -801,6 +802,35 @@ describe('IndexPatternDimensionEditorPanel', () => {
});
});

it('should render invalid field if field reference is broken', () => {
wrapper = mount(
<IndexPatternDimensionEditorComponent
{...defaultProps}
state={{
...defaultProps.state,
layers: {
first: {
...defaultProps.state.layers.first,
columns: {
col1: {
...defaultProps.state.layers.first.columns.col1,
sourceField: 'nonexistent',
} as DateHistogramIndexPatternColumn,
},
},
},
}}
/>
);

expect(wrapper.find(EuiComboBox).prop('selectedOptions')).toEqual([
{
label: 'nonexistent',
value: { type: 'field', field: 'nonexistent' },
},
]);
});

it('should support selecting the operation before the field', () => {
wrapper = mount(<IndexPatternDimensionEditorComponent {...defaultProps} columnId={'col2'} />);

Expand Down
Loading

0 comments on commit a93a323

Please sign in to comment.