/* * 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 { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { DocLinksStart } from 'kibana/public'; import { EuiLink, EuiTextColor, EuiButton, EuiSpacer } from '@elastic/eui'; import { DatatableColumn } from 'src/plugins/expressions'; import { groupBy, escape } from 'lodash'; import type { FramePublicAPI, StateSetter } from '../types'; import type { IndexPattern, IndexPatternLayer, IndexPatternPrivateState } from './types'; import type { ReferenceBasedIndexPatternColumn } from './operations/definitions/column_types'; import { operationDefinitionMap, GenericIndexPatternColumn, TermsIndexPatternColumn, CountIndexPatternColumn, updateColumnParam, updateDefaultLabels, RangeIndexPatternColumn, FormulaIndexPatternColumn, } from './operations'; import { getInvalidFieldMessage, isColumnOfType } from './operations/definitions/helpers'; import { FiltersIndexPatternColumn, isQueryValid } from './operations/definitions/filters'; import { checkColumnForPrecisionError, Query } from '../../../../../src/plugins/data/common'; import { hasField } from './pure_utils'; import { mergeLayer } from './state_helpers'; import { supportsRarityRanking } from './operations/definitions/terms'; import { DEFAULT_MAX_DOC_COUNT } from './operations/definitions/terms/constants'; import { getOriginalId } from '../../common/expressions'; export function isColumnInvalid( layer: IndexPatternLayer, columnId: string, indexPattern: IndexPattern ) { const column: GenericIndexPatternColumn | undefined = layer.columns[columnId]; if (!column || !indexPattern) return; const operationDefinition = column.operationType && operationDefinitionMap[column.operationType]; // check also references for errors const referencesHaveErrors = true && 'references' in column && Boolean(getReferencesErrors(layer, column, indexPattern).filter(Boolean).length); const operationErrorMessages = operationDefinition && operationDefinition.getErrorMessage?.(layer, columnId, indexPattern, operationDefinitionMap); const filterHasError = column.filter ? !isQueryValid(column.filter, indexPattern) : false; return ( (operationErrorMessages && operationErrorMessages.length > 0) || referencesHaveErrors || filterHasError ); } function getReferencesErrors( layer: IndexPatternLayer, column: ReferenceBasedIndexPatternColumn, indexPattern: IndexPattern ) { return column.references?.map((referenceId: string) => { const referencedOperation = layer.columns[referenceId]?.operationType; const referencedDefinition = operationDefinitionMap[referencedOperation]; return referencedDefinition?.getErrorMessage?.( layer, referenceId, indexPattern, operationDefinitionMap ); }); } export function fieldIsInvalid( column: GenericIndexPatternColumn | undefined, indexPattern: IndexPattern ) { if (!column || !hasField(column)) { return false; } return !!getInvalidFieldMessage(column, indexPattern)?.length; } export function getPrecisionErrorWarningMessages( state: IndexPatternPrivateState, { activeData }: FramePublicAPI, docLinks: DocLinksStart, setState: StateSetter ) { const warningMessages: React.ReactNode[] = []; if (state && activeData) { Object.entries(activeData) .reduce( (acc, [layerId, { columns }]) => [ ...acc, ...columns.map((column) => ({ layerId, column })), ], [] as Array<{ layerId: string; column: DatatableColumn }> ) .forEach(({ layerId, column }) => { const currentLayer = state.layers[layerId]; const currentColumn = currentLayer?.columns[column.id]; if (currentLayer && currentColumn && checkColumnForPrecisionError(column)) { const indexPattern = state.indexPatterns[currentLayer.indexPatternId]; const isAscendingCountSorting = isColumnOfType('terms', currentColumn) && currentColumn.params.orderBy.type === 'column' && currentColumn.params.orderDirection === 'asc' && isColumnOfType( 'count', currentLayer.columns[currentColumn.params.orderBy.columnId] ); const usesFloatingPointField = isColumnOfType('terms', currentColumn) && !supportsRarityRanking(indexPattern.getFieldByName(currentColumn.sourceField)); const usesMultipleFields = isColumnOfType('terms', currentColumn) && (currentColumn.params.secondaryFields || []).length > 0; if (!isAscendingCountSorting || usesFloatingPointField || usesMultipleFields) { warningMessages.push( {column.name}, topValues: ( ), filters: ( ), link: ( ), }} /> ); } else { warningMessages.push( <> {column.name}, link: ( ), }} /> { setState((prevState) => mergeLayer({ state: prevState, layerId, newLayer: updateDefaultLabels( updateColumnParam({ layer: currentLayer, columnId: column.id, paramName: 'orderBy', value: { type: 'rare', maxDocCount: DEFAULT_MAX_DOC_COUNT, }, }), indexPattern ), }) ); }} > {i18n.translate('xpack.lens.indexPattern.switchToRare', { defaultMessage: 'Rank by rarity', })} ); } } }); } return warningMessages; } export function getVisualDefaultsForLayer(layer: IndexPatternLayer) { return Object.keys(layer.columns).reduce>>( (memo, columnId) => { const column = layer.columns[columnId]; if (column?.operationType) { const opDefinition = operationDefinitionMap[column.operationType]; const params = opDefinition.getDefaultVisualSettings?.(column); if (params) { memo[columnId] = params; } } return memo; }, {} ); } /** * Some utilities to extract queries/filters from specific column types */ /** * Given a Filters column, extract and filter useful queries from it */ function extractQueriesFromFilters( queries: FiltersIndexPatternColumn['params']['filters'] | undefined ) { return queries?.map(({ input }) => input).filter(({ query }) => query?.trim() && query !== '*'); } /** * Given an Interval column in range mode transform the ranges into KQL queries */ function extractQueriesFromRanges(column: RangeIndexPatternColumn) { return column.params.ranges .map(({ from, to }) => { let rangeQuery = ''; if (from != null && isFinite(from)) { rangeQuery += `${column.sourceField} >= ${from}`; } if (to != null && isFinite(to)) { if (rangeQuery.length) { rangeQuery += ' AND '; } rangeQuery += `${column.sourceField} <= ${to}`; } return { query: rangeQuery, language: 'kuery', }; }) .filter(({ query }) => query?.trim()); } /** * Given an Terms/Top values column transform each entry into a "field: term" KQL query * This works also for multi-terms variant */ function extractQueriesFromTerms( column: TermsIndexPatternColumn, colId: string, data: NonNullable[string] ): Query[] { const fields = [column.sourceField] .concat(column.params.secondaryFields || []) .filter(Boolean) as string[]; // extract the filters from the columns of the activeData const queries = data.rows .map(({ [colId]: value }) => { if (value == null) { return; } if (typeof value !== 'string' && Array.isArray(value.keys)) { return value.keys .map( (term: string, index: number) => `${fields[index]}: ${`"${term === '' ? escape(term) : term}"`}` ) .join(' AND '); } return `${column.sourceField}: ${`"${value === '' ? escape(value) : value}"`}`; }) .filter(Boolean) as string[]; // dedup queries before returning return [...new Set(queries)].map((query) => ({ language: 'kuery', query })); } /** * Used for a Terms column to decide whether to use a simple existence query (fallback) instead * of more specific queries. * The check targets the scenarios where no data is available, or when there's a transposed table * and it's not yet possible to track it back to the original table */ function shouldUseTermsFallback( data: NonNullable[string] | undefined, colId: string ) { const dataId = data?.columns.find(({ id }) => getOriginalId(id) === colId)?.id; return !dataId || dataId !== colId; } /** * Collect filters from metrics: * * if there's at least one unfiltered metric, then just return an empty list of filters * * otherwise get all the filters, with the only exception of those from formula (referenced columns will have it anyway) */ function collectFiltersFromMetrics(layer: IndexPatternLayer, columnIds: string[]) { // Isolate filtered metrics first // mind to ignore non-filterable columns and formula columns const metricColumns = Object.keys(layer.columns).filter((colId) => { const column = layer.columns[colId]; const operationDefinition = operationDefinitionMap[column?.operationType]; return ( !column?.isBucketed && // global filters for formulas are picked up by referenced columns !isColumnOfType('formula', column) && operationDefinition?.filterable ); }); const { filtered = [], unfiltered = [] } = groupBy(metricColumns, (colId) => layer.columns[colId]?.filter ? 'filtered' : 'unfiltered' ); // extract filters from filtered metrics // consider all the columns, included referenced ones to cover also the formula case return ( filtered // if there are metric columns not filtered, then ignore filtered columns completely .filter(() => !unfiltered.length) .map((colId) => layer.columns[colId]?.filter) // filter out empty filters as well .filter((filter) => filter?.query?.trim()) as Query[] ); } interface GroupedQueries { kuery?: Query[]; lucene?: Query[]; } function collectOnlyValidQueries( filteredQueries: GroupedQueries, operationQueries: GroupedQueries[], queryLanguage: 'kuery' | 'lucene' ) { return [ filteredQueries[queryLanguage], ...operationQueries.map(({ [queryLanguage]: filter }) => filter), ].filter((filters) => filters?.length) as Query[][]; } export function getFiltersInLayer( layer: IndexPatternLayer, columnIds: string[], layerData: NonNullable[string] | undefined ) { const filtersFromMetricsByLanguage = groupBy( collectFiltersFromMetrics(layer, columnIds), 'language' ) as unknown as GroupedQueries; const filterOperation = columnIds .map((colId) => { const column = layer.columns[colId]; if (isColumnOfType('filters', column)) { const groupsByLanguage = groupBy( column.params.filters, ({ input }) => input.language ) as Record<'lucene' | 'kuery', FiltersIndexPatternColumn['params']['filters']>; return { kuery: extractQueriesFromFilters(groupsByLanguage.kuery), lucene: extractQueriesFromFilters(groupsByLanguage.lucene), }; } if (isColumnOfType('range', column) && column.sourceField) { return { kuery: extractQueriesFromRanges(column), }; } if ( isColumnOfType('terms', column) && !(column.params.otherBucket || column.params.missingBucket) ) { if (!layerData || shouldUseTermsFallback(layerData, colId)) { const fields = operationDefinitionMap[column.operationType]!.getCurrentFields!(column); return { kuery: fields.map((field) => ({ query: `${field}: *`, language: 'kuery', })), }; } return { kuery: extractQueriesFromTerms(column, colId, layerData), }; } }) .filter(Boolean) as GroupedQueries[]; return { kuery: collectOnlyValidQueries(filtersFromMetricsByLanguage, filterOperation, 'kuery'), lucene: collectOnlyValidQueries(filtersFromMetricsByLanguage, filterOperation, 'lucene'), }; }