[8.9] [Lens] Relax counter field checks for saved visualizations with unsupported operations (#163515) - manual backport (#164191)

# Backport

This will backport the following commits from `main` to `8.9`:
- [[Lens] Relax counter field checks for saved visualizations with
unsupported operations
(#163515)](https://github.com/elastic/kibana/pull/163515)

This is a manual backport which adds a missing mock function, not
available in the automated backport cherry-pick of the PR.
The extra code is here:
`x-pack/plugins/lens/public/datasources/form_based/mocks.ts`

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Marco
Liberati","email":"dej611@users.noreply.github.com"},"sourceCommit":{"committedDate":"2023-08-11T09:52:17Z","message":"[Lens]
Relax counter field checks for saved visualizations with unsupported
operations (#163515)\n\n## Summary\r\n\r\nFix #163473\r\n\r\nThis PR
relaxes a bit the checks on the Lens side for
old/saved\r\nvisualizations with unsupported operations for the
`counter` field type,\r\nwhile preserving those checks for newer
visualizations.\r\n\r\nDashboards with \"meaningless\" operations will
now show a warning\r\nmessage:\r\n<img width=\"556\" alt=\"Screenshot
2023-08-09 at 18 31
21\"\r\nsrc=\"7c8f3739-4957-4d1d-8aaa-e9457b8a4426\">\r\n\r\nWhen
in editor the warning is shown at the top-right corner as well:\r\n<img
width=\"845\" alt=\"Screenshot 2023-08-09 at 18 30
31\"\r\nsrc=\"c52a7823-d4b9-4efd-9c5d-ca654f3f03a0\">\r\n\r\nNew
visualizations still prevent the user from using the
unsupported\r\noperations:\r\n<img width=\"410\" alt=\"Screenshot
2023-08-09 at 18 30
55\"\r\nsrc=\"d2364a01-0dc3-409a-9c0b-3e3a77cb2566\">\r\n<img
width=\"848\" alt=\"Screenshot 2023-08-09 at 18 31
48\"\r\nsrc=\"086a7360-6b1a-40a2-90d9-f4e8c7bf3f3a\">\r\n\r\nThere's
theoretically a last case where users in old SOs might create a\r\nnew
metric dimension trying to force to use a unsupported operation for\r\na
counter field: in this case the logic for a \"new\" visualization
will\r\nkick-in, clean the data in the workspace and show a full
error.\r\nCancelling such metric dimension will lead to the previous
\"relaxed\"\r\nstate.\r\n\r\nMessages are grouped by field and by top
referencing column (i.e. a\r\nformula): this means that if a formula
uses the same `counter` field\r\nwith two different dimensions (i.e.
`sum(counter_field) +\r\nmedian(counter_field)` as `myFormula`) will
show up as a single column\r\n(`myFormula`).\r\n\r\nThe wording of the
message mimics the same documentation copy provided\r\nin the ES
documentation.
Ref:\r\nhttps://github.com/elastic/elasticsearch/pull/97974\r\n\r\nTesting
SO:\r\n\r\n\r\n[export.ndjson.txt](12304924/export.ndjson.txt)\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Stratoula Kalafateli
<efstratia.kalafateli@elastic.co>","sha":"4c812e3b6d1a4e477768dae04047e79bbdc940ff","branchLabelMapping":{"^v8.10.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Visualizations","release_note:skip","Feature:Lens","backport:prev-minor","v8.10.0"],"number":163515,"url":"https://github.com/elastic/kibana/pull/163515","mergeCommit":{"message":"[Lens]
Relax counter field checks for saved visualizations with unsupported
operations (#163515)\n\n## Summary\r\n\r\nFix #163473\r\n\r\nThis PR
relaxes a bit the checks on the Lens side for
old/saved\r\nvisualizations with unsupported operations for the
`counter` field type,\r\nwhile preserving those checks for newer
visualizations.\r\n\r\nDashboards with \"meaningless\" operations will
now show a warning\r\nmessage:\r\n<img width=\"556\" alt=\"Screenshot
2023-08-09 at 18 31
21\"\r\nsrc=\"7c8f3739-4957-4d1d-8aaa-e9457b8a4426\">\r\n\r\nWhen
in editor the warning is shown at the top-right corner as well:\r\n<img
width=\"845\" alt=\"Screenshot 2023-08-09 at 18 30
31\"\r\nsrc=\"c52a7823-d4b9-4efd-9c5d-ca654f3f03a0\">\r\n\r\nNew
visualizations still prevent the user from using the
unsupported\r\noperations:\r\n<img width=\"410\" alt=\"Screenshot
2023-08-09 at 18 30
55\"\r\nsrc=\"d2364a01-0dc3-409a-9c0b-3e3a77cb2566\">\r\n<img
width=\"848\" alt=\"Screenshot 2023-08-09 at 18 31
48\"\r\nsrc=\"086a7360-6b1a-40a2-90d9-f4e8c7bf3f3a\">\r\n\r\nThere's
theoretically a last case where users in old SOs might create a\r\nnew
metric dimension trying to force to use a unsupported operation for\r\na
counter field: in this case the logic for a \"new\" visualization
will\r\nkick-in, clean the data in the workspace and show a full
error.\r\nCancelling such metric dimension will lead to the previous
\"relaxed\"\r\nstate.\r\n\r\nMessages are grouped by field and by top
referencing column (i.e. a\r\nformula): this means that if a formula
uses the same `counter` field\r\nwith two different dimensions (i.e.
`sum(counter_field) +\r\nmedian(counter_field)` as `myFormula`) will
show up as a single column\r\n(`myFormula`).\r\n\r\nThe wording of the
message mimics the same documentation copy provided\r\nin the ES
documentation.
Ref:\r\nhttps://github.com/elastic/elasticsearch/pull/97974\r\n\r\nTesting
SO:\r\n\r\n\r\n[export.ndjson.txt](12304924/export.ndjson.txt)\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Stratoula Kalafateli
<efstratia.kalafateli@elastic.co>","sha":"4c812e3b6d1a4e477768dae04047e79bbdc940ff"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.10.0","labelRegex":"^v8.10.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/163515","number":163515,"mergeCommit":{"message":"[Lens]
Relax counter field checks for saved visualizations with unsupported
operations (#163515)\n\n## Summary\r\n\r\nFix #163473\r\n\r\nThis PR
relaxes a bit the checks on the Lens side for
old/saved\r\nvisualizations with unsupported operations for the
`counter` field type,\r\nwhile preserving those checks for newer
visualizations.\r\n\r\nDashboards with \"meaningless\" operations will
now show a warning\r\nmessage:\r\n<img width=\"556\" alt=\"Screenshot
2023-08-09 at 18 31
21\"\r\nsrc=\"7c8f3739-4957-4d1d-8aaa-e9457b8a4426\">\r\n\r\nWhen
in editor the warning is shown at the top-right corner as well:\r\n<img
width=\"845\" alt=\"Screenshot 2023-08-09 at 18 30
31\"\r\nsrc=\"c52a7823-d4b9-4efd-9c5d-ca654f3f03a0\">\r\n\r\nNew
visualizations still prevent the user from using the
unsupported\r\noperations:\r\n<img width=\"410\" alt=\"Screenshot
2023-08-09 at 18 30
55\"\r\nsrc=\"d2364a01-0dc3-409a-9c0b-3e3a77cb2566\">\r\n<img
width=\"848\" alt=\"Screenshot 2023-08-09 at 18 31
48\"\r\nsrc=\"086a7360-6b1a-40a2-90d9-f4e8c7bf3f3a\">\r\n\r\nThere's
theoretically a last case where users in old SOs might create a\r\nnew
metric dimension trying to force to use a unsupported operation for\r\na
counter field: in this case the logic for a \"new\" visualization
will\r\nkick-in, clean the data in the workspace and show a full
error.\r\nCancelling such metric dimension will lead to the previous
\"relaxed\"\r\nstate.\r\n\r\nMessages are grouped by field and by top
referencing column (i.e. a\r\nformula): this means that if a formula
uses the same `counter` field\r\nwith two different dimensions (i.e.
`sum(counter_field) +\r\nmedian(counter_field)` as `myFormula`) will
show up as a single column\r\n(`myFormula`).\r\n\r\nThe wording of the
message mimics the same documentation copy provided\r\nin the ES
documentation.
Ref:\r\nhttps://github.com/elastic/elasticsearch/pull/97974\r\n\r\nTesting
SO:\r\n\r\n\r\n[export.ndjson.txt](12304924/export.ndjson.txt)\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Stratoula Kalafateli
<efstratia.kalafateli@elastic.co>","sha":"4c812e3b6d1a4e477768dae04047e79bbdc940ff"}}]}]
BACKPORT-->

---------

Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
Marco Liberati 2023-08-21 15:24:25 +02:00 committed by GitHub
parent 2774922290
commit 01f486e0d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 338 additions and 19 deletions

View file

@ -71,6 +71,7 @@ import {
isColumnInvalid,
cloneLayer,
getNotifiableFeatures,
getUnsupportedOperationsWarningMessage,
} from './utils';
import { getUniqueLabelGenerator, isDraggedDataViewField } from '../../utils';
import { hasField, normalizeOperationDataType } from './pure_utils';
@ -891,6 +892,7 @@ export function getFormBasedDatasource({
core.docLinks,
setState
),
...getUnsupportedOperationsWarningMessage(state, frameDatasourceAPI, core.docLinks),
];
const infoMessages = getNotifiableFeatures(state, frameDatasourceAPI, visualizationInfo);

View file

@ -232,3 +232,15 @@ export function createMockedDragDropContext(): jest.Mocked<DragContextState> {
registerDropTarget: jest.fn(),
};
}
export const createMockedIndexPatternWithAdditionalFields = (
newFields: IndexPatternField[]
): IndexPattern => {
const { fields, ...otherIndexPatternProps } = createMockedIndexPattern();
const completeFields = fields.concat(newFields);
return {
...otherIndexPatternProps,
fields: completeFields,
getFieldByName: getFieldByNameFactory(completeFields),
};
};

View file

@ -126,7 +126,6 @@ function buildMetricOperation<T extends MetricColumn<string>>({
newField &&
supportedTypes.includes(newField.type) &&
newField.aggregatable &&
isTimeSeriesCompatible(type, newField.timeSeriesMetric) &&
(!newField.aggregationRestrictions || newField.aggregationRestrictions![type])
);
},

View file

@ -8,15 +8,20 @@
import React from 'react';
import { shallow } from 'enzyme';
import { createDatatableUtilitiesMock } from '@kbn/data-plugin/common/mocks';
import { getPrecisionErrorWarningMessages, cloneLayer } from './utils';
import {
getPrecisionErrorWarningMessages,
cloneLayer,
getUnsupportedOperationsWarningMessage,
} from './utils';
import type { FormBasedPrivateState, GenericIndexPatternColumn } from './types';
import type { FramePublicAPI } from '../../types';
import type { FramePublicAPI, IndexPattern } from '../../types';
import type { DocLinksStart } from '@kbn/core/public';
import { EuiLink } from '@elastic/eui';
import { TermsIndexPatternColumn } from './operations';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { FormattedMessage } from '@kbn/i18n-react';
import { FormBasedLayer } from './types';
import { createMockedIndexPatternWithAdditionalFields } from './mocks';
describe('indexpattern_datasource utils', () => {
describe('getPrecisionErrorWarningMessages', () => {
@ -240,4 +245,195 @@ describe('indexpattern_datasource utils', () => {
).toMatchSnapshot();
});
});
describe('getUnsupportedOperationsWarningMessage', () => {
let docLinks: DocLinksStart;
const affectedOperations = [
'sum',
'average',
'percentile',
'percentile_rank',
'count',
'unique_count',
'standard_deviation',
];
function createColumnsForField(field: string, colOffset: number = 0) {
return Object.fromEntries(
affectedOperations.map((operationType, i) => [
`col_${i + colOffset}`,
{ operationType, sourceField: field, label: `${operationType} of ${field}` },
])
);
}
function createState(fields: string[]) {
return {
layers: {
id: {
indexPatternId: '0',
columns: Object.assign(
{},
...fields.map((field, i) =>
createColumnsForField(field, i * affectedOperations.length)
)
),
},
},
} as unknown as FormBasedPrivateState;
}
function createFramePublic(indexPattern: IndexPattern): FramePublicAPI {
return {
dataViews: {
indexPatterns: Object.fromEntries([indexPattern].map((dataView, i) => [i, dataView])),
},
} as unknown as FramePublicAPI;
}
function createFormulaColumns(formulaParts: string[], field: string, colOffset: number = 0) {
const fullFormula = formulaParts.map((part) => `${part}(${field})`).join(' + ');
// just assume it's a sum of all the parts for testing
const rootId = `col-formula${colOffset}`;
return Object.fromEntries([
[
rootId,
{
operationType: 'formula',
label: `Formula: ${fullFormula}`,
params: { formula: fullFormula },
},
],
...formulaParts.map((part, i) => [
`${rootId}X${i}`,
{ operationType: part, sourceField: field, label: 'Part of formula' },
]),
[
`${rootId}X${formulaParts.length}`,
{ operationType: 'math', references: formulaParts.map((_, i) => `${rootId}X${i}`) },
],
]);
}
beforeEach(() => {
docLinks = {
links: {
fleet: {
datastreamsTSDSMetrics: 'http://tsdb_metric_doc',
},
},
} as DocLinksStart;
});
it.each([['bytes'], ['bytes_gauge']])(
'should return no warning for non-counter fields: %s',
(fieldName: string) => {
const warnings = getUnsupportedOperationsWarningMessage(
createState([fieldName]),
createFramePublic(
createMockedIndexPatternWithAdditionalFields([
{
name: 'bytes_gauge',
displayName: 'bytes_gauge',
type: 'number',
aggregatable: true,
searchable: true,
timeSeriesMetric: 'gauge',
},
])
),
docLinks
);
expect(warnings).toHaveLength(0);
}
);
it('should return a warning for a counter field grouped by field', () => {
const warnings = getUnsupportedOperationsWarningMessage(
createState(['bytes_counter']),
createFramePublic(
createMockedIndexPatternWithAdditionalFields([
{
name: 'bytes_counter',
displayName: 'bytes_counter',
type: 'number',
aggregatable: true,
searchable: true,
timeSeriesMetric: 'counter',
},
])
),
docLinks
);
expect(warnings).toHaveLength(1);
});
it('should group multiple warnings by field', () => {
const warnings = getUnsupportedOperationsWarningMessage(
createState(['bytes_counter', 'bytes_counter2']),
createFramePublic(
createMockedIndexPatternWithAdditionalFields([
{
name: 'bytes_counter',
displayName: 'bytes_counter',
type: 'number',
aggregatable: true,
searchable: true,
timeSeriesMetric: 'counter',
},
{
name: 'bytes_counter2',
displayName: 'bytes_counter2',
type: 'number',
aggregatable: true,
searchable: true,
timeSeriesMetric: 'counter',
},
])
),
docLinks
);
expect(warnings).toHaveLength(2);
});
it('should handle formula reporting only the top visible dimension', () => {
const warnings = getUnsupportedOperationsWarningMessage(
{
layers: {
id: {
indexPatternId: '0',
columns: Object.assign(
{},
...['bytes_counter', 'bytes_counter2'].map((field, i) =>
createFormulaColumns(affectedOperations, field, i * affectedOperations.length)
)
),
},
},
} as unknown as FormBasedPrivateState,
createFramePublic(
createMockedIndexPatternWithAdditionalFields([
{
name: 'bytes_counter',
displayName: 'bytes_counter',
type: 'number',
aggregatable: true,
searchable: true,
timeSeriesMetric: 'counter',
},
{
name: 'bytes_counter2',
displayName: 'bytes_counter2',
type: 'number',
aggregatable: true,
searchable: true,
timeSeriesMetric: 'counter',
},
])
),
docLinks
);
expect(warnings).toHaveLength(2);
});
});
});

View file

@ -14,7 +14,7 @@ import { TimeRange } from '@kbn/es-query';
import { EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
import type { DatatableColumn } from '@kbn/expressions-plugin/common';
import { groupBy, escape, uniq } from 'lodash';
import { groupBy, escape, uniq, uniqBy } from 'lodash';
import type { Query } from '@kbn/data-plugin/common';
import { SearchRequest } from '@kbn/data-plugin/common';
@ -40,16 +40,19 @@ import type { ReferenceBasedIndexPatternColumn } from './operations/definitions/
import {
operationDefinitionMap,
GenericIndexPatternColumn,
TermsIndexPatternColumn,
CountIndexPatternColumn,
getReferenceRoot,
updateColumnParam,
updateDefaultLabels,
RangeIndexPatternColumn,
FormulaIndexPatternColumn,
DateHistogramIndexPatternColumn,
MaxIndexPatternColumn,
MinIndexPatternColumn,
type GenericIndexPatternColumn,
type TermsIndexPatternColumn,
type CountIndexPatternColumn,
type RangeIndexPatternColumn,
type FormulaIndexPatternColumn,
type DateHistogramIndexPatternColumn,
type MaxIndexPatternColumn,
type MinIndexPatternColumn,
type GenericOperationDefinition,
type FieldBasedIndexPatternColumn,
} from './operations';
import { getInvalidFieldMessage, isColumnOfType } from './operations/definitions/helpers';
@ -338,6 +341,113 @@ export function getShardFailuresWarningMessages(
return [];
}
export function getUnsupportedOperationsWarningMessage(
state: FormBasedPrivateState,
{ dataViews }: FramePublicAPI,
docLinks: DocLinksStart
) {
const warningMessages: UserMessage[] = [];
const columnsWithUnsupportedOperations: Array<
[FieldBasedIndexPatternColumn, ReferenceBasedIndexPatternColumn | undefined]
> = Object.values(state.layers)
// filter layers without dataView loaded yet
.filter(({ indexPatternId }) => dataViews.indexPatterns[indexPatternId])
.flatMap((layer) => {
const dataView = dataViews.indexPatterns[layer.indexPatternId];
const columnsEntries = Object.entries(layer.columns);
return columnsEntries
.filter(([_, column]) => {
if (!hasField(column)) {
return false;
}
const field = dataView.getFieldByName(column.sourceField);
if (!field) {
return false;
}
return (
!(
operationDefinitionMap[column.operationType] as Extract<
GenericOperationDefinition,
{ input: 'field' }
>
).getPossibleOperationForField(field) && field?.timeSeriesMetric === 'counter'
);
})
.map(
([id, fieldColumn]) =>
[fieldColumn, layer.columns[getReferenceRoot(layer, id)]] as [
FieldBasedIndexPatternColumn,
ReferenceBasedIndexPatternColumn | undefined
]
);
});
if (columnsWithUnsupportedOperations.length) {
// group the columns by field
// then group together columns of a formula/referenced operation who use the same field
const columnsGroupedByField = Object.values(
groupBy(columnsWithUnsupportedOperations, ([column]) => column.sourceField)
).map((columnsList) => uniqBy(columnsList, ([column, rootColumn]) => rootColumn ?? column));
for (const columnsGrouped of columnsGroupedByField) {
const sourceField = columnsGrouped[0][0].sourceField;
warningMessages.push({
severity: 'warning',
fixableInEditor: false,
displayLocations: [{ id: 'toolbar' }, { id: 'embeddableBadge' }],
shortMessage: i18n.translate(
'xpack.lens.indexPattern.tsdbErrorWarning.unsupportedCounterOperationErrorWarning.shortMessage',
{
defaultMessage:
'The result of {count} {count, plural, one {operation} other {operations}} might be meaningless for {field}: {operations}',
values: {
count: columnsGrouped.length,
operations: columnsGrouped
.map(([affectedColumn, rootColumn]) => (rootColumn ?? affectedColumn).label)
.join(', '),
field: sourceField,
},
}
),
longMessage: (
<>
<FormattedMessage
id="xpack.lens.indexPattern.unsupportedCounterOperationErrorWarning"
defaultMessage="While {count} {count, plural, one {operation} other {operations}} for {field} {count, plural, one {is} other {are}} allowed the result might be meaningless: {operations}. To learn more about this, {link}."
values={{
count: columnsGrouped.length,
operations: (
<>
{columnsGrouped.map(([affectedColumn, rootColumn], i) => (
<React.Fragment key={(rootColumn ?? affectedColumn).label}>
<strong>{(rootColumn ?? affectedColumn).label}</strong>
{i < columnsGrouped.length - 1 ? ', ' : ''}
</React.Fragment>
))}
</>
),
field: sourceField,
link: (
<EuiLink
href={docLinks.links.fleet.datastreamsTSDSMetrics}
target="_blank"
external={true}
>
<FormattedMessage
defaultMessage="visit the Time series documentation"
id="xpack.lens.indexPattern.unsupportedCounterOperationErrorWarning.link"
/>
</EuiLink>
),
}}
/>
</>
),
});
}
}
return warningMessages;
}
export function getPrecisionErrorWarningMessages(
datatableUtilities: DatatableUtilitiesService,
state: FormBasedPrivateState,
@ -349,18 +459,18 @@ export function getPrecisionErrorWarningMessages(
if (state && activeData) {
Object.entries(activeData)
.reduce(
(acc, [layerId, { columns }]) => [
...acc,
...columns.map((column) => ({ layerId, column })),
],
[] as Array<{ layerId: string; column: DatatableColumn }>
)
.reduce((acc, [layerId, { columns }]) => {
acc.push(...columns.map((column) => ({ layerId, column })));
return acc;
}, [] as Array<{ layerId: string; column: DatatableColumn }>)
.forEach(({ layerId, column }) => {
const currentLayer = state.layers[layerId];
const currentColumn = currentLayer?.columns[column.id];
if (currentLayer && currentColumn && datatableUtilities.hasPrecisionError(column)) {
const indexPattern = dataViews.indexPatterns[currentLayer.indexPatternId];
if (!indexPattern) {
return;
}
// currentColumnIsTerms is mostly a type guard. If there's a precision error,
// we already know that we're dealing with a terms-based operation (at least for now).
const currentColumnIsTerms = isColumnOfType<TermsIndexPatternColumn>(