diff --git a/packages/kbn-esql-utils/index.ts b/packages/kbn-esql-utils/index.ts index 9519e84624cb..0956816c59ed 100644 --- a/packages/kbn-esql-utils/index.ts +++ b/packages/kbn-esql-utils/index.ts @@ -33,6 +33,7 @@ export { isESQLColumnGroupable, isESQLFieldGroupable, TextBasedLanguages, + queryCannotBeSampled, } from './src'; export { ENABLE_ESQL, FEEDBACK_LINK } from './constants'; diff --git a/packages/kbn-esql-utils/src/index.ts b/packages/kbn-esql-utils/src/index.ts index 6cf2dd031bf0..d56a56c62d6b 100644 --- a/packages/kbn-esql-utils/src/index.ts +++ b/packages/kbn-esql-utils/src/index.ts @@ -22,6 +22,7 @@ export { retrieveMetadataColumns, getQueryColumnsFromESQLQuery, } from './utils/query_parsing_helpers'; +export { queryCannotBeSampled } from './utils/query_cannot_be_sampled'; export { appendToESQLQuery, appendWhereClauseToESQLQuery } from './utils/append_to_query'; export { getESQLQueryColumns, diff --git a/packages/kbn-esql-utils/src/utils/query_cannot_be_sampled.test.ts b/packages/kbn-esql-utils/src/utils/query_cannot_be_sampled.test.ts new file mode 100644 index 000000000000..e0ff5d01be41 --- /dev/null +++ b/packages/kbn-esql-utils/src/utils/query_cannot_be_sampled.test.ts @@ -0,0 +1,44 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import { queryCannotBeSampled } from './query_cannot_be_sampled'; +describe('queryCannotBeSampled', () => { + it('should return true if query contains "match" function', () => { + expect(queryCannotBeSampled({ esql: 'FROM index | where match(field, "value")' })).toBe(true); + expect(queryCannotBeSampled({ esql: 'FROM index | where match()' })).toBe(true); + expect(queryCannotBeSampled({ esql: 'FROM index | where MATCH()' })).toBe(true); + expect(queryCannotBeSampled({ esql: 'FROM index | where MATCH(fieldName,)' })).toBe(true); + expect(queryCannotBeSampled({ esql: 'FROM index | where MATCH(,)' })).toBe(true); + }); + + it('should return true if query contains "qstr" function', () => { + expect(queryCannotBeSampled({ esql: 'FROM index | where qstr(field, "value")' })).toBe(true); + expect(queryCannotBeSampled({ esql: 'FROM index | where qstr()' })).toBe(true); + expect(queryCannotBeSampled({ esql: 'FROM index | where QSTR()' })).toBe(true); + }); + + it('should return false if query contains names', () => { + expect(queryCannotBeSampled({ esql: 'FROM index | eval match =' })).toBe(false); + expect(queryCannotBeSampled({ esql: 'FROM index | eval MATCH =' })).toBe(false); + expect(queryCannotBeSampled({ esql: 'FROM index | eval qstr =' })).toBe(false); + }); + + it('should return false if query does not contain unsamplable functions', () => { + expect(queryCannotBeSampled({ esql: 'FROM index | eval otherFunction(field, "value")' })).toBe( + false + ); + expect(queryCannotBeSampled({ esql: 'FROM index | where otherFunction(field, "value")' })).toBe( + false + ); + }); + + it('should return false if query is undefined', () => { + expect(queryCannotBeSampled(undefined)).toBe(false); + expect(queryCannotBeSampled(null)).toBe(false); + }); +}); diff --git a/packages/kbn-esql-utils/src/utils/query_cannot_be_sampled.ts b/packages/kbn-esql-utils/src/utils/query_cannot_be_sampled.ts new file mode 100644 index 000000000000..c4cbd34e7537 --- /dev/null +++ b/packages/kbn-esql-utils/src/utils/query_cannot_be_sampled.ts @@ -0,0 +1,46 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import type { AggregateQuery, Query } from '@kbn/es-query'; +import { Walker } from '@kbn/esql-ast'; +import { parse } from '@kbn/esql-ast'; +import { isOfAggregateQueryType } from '@kbn/es-query'; + +/** + * Check if the query contains any of the function names being passed in + * @param query + * @param functions list of function names to check for + * @returns + */ +export const queryContainsFunction = ( + query: AggregateQuery | Query | { [key: string]: any } | undefined | null, + functions: string[] +): boolean => { + if (query && isOfAggregateQueryType(query)) { + const { root } = parse(query.esql); + return functions.some( + (f) => + Walker.hasFunction(root, f) || + // Walker API expects valid queries so we need to do additional check for partial matches + root.commands.some((c) => c.text.toLowerCase().includes(`${f}(`)) + ); + } + return false; +}; + +const UNSAMPLABLE_FUNCTIONS = ['match', 'qstr']; +/** + * Check if the query contains any function that cannot be used after LIMIT clause + * @param query + * @returns + */ +export const queryCannotBeSampled = ( + query: AggregateQuery | Query | { [key: string]: any } | undefined | null +): boolean => { + return queryContainsFunction(query, UNSAMPLABLE_FUNCTIONS); +}; diff --git a/packages/kbn-unified-field-list/src/components/field_stats/field_summary_message.tsx b/packages/kbn-unified-field-list/src/components/field_stats/field_summary_message.tsx index aab65e08f3a6..2ed392abf188 100755 --- a/packages/kbn-unified-field-list/src/components/field_stats/field_summary_message.tsx +++ b/packages/kbn-unified-field-list/src/components/field_stats/field_summary_message.tsx @@ -12,8 +12,16 @@ import { EuiText } from '@elastic/eui'; export interface FieldSummaryMessageProps { message: string; + dataTestSubj?: string; } -export const FieldSummaryMessage: React.FC = ({ message }) => { - return {message}; +export const FieldSummaryMessage: React.FC = ({ + message, + dataTestSubj, +}) => { + return ( + + {message} + + ); }; diff --git a/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_tab.tsx b/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_tab.tsx index c9ac877474e8..5a106b50360b 100644 --- a/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_tab.tsx +++ b/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_tab.tsx @@ -9,9 +9,10 @@ import React from 'react'; import { useQuerySubscriber } from '@kbn/unified-field-list/src/hooks/use_query_subscriber'; -import { FieldStatisticsTable, type FieldStatisticsTableProps } from './field_stats_table'; -import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useAdditionalFieldGroups } from '../../hooks/sidebar/use_additional_field_groups'; +import { useDiscoverServices } from '../../../../hooks/use_discover_services'; +import { FieldStatisticsTable, type FieldStatisticsTableProps } from './field_stats_table'; +import { useIsEsqlMode } from '../../hooks/use_is_esql_mode'; export const FieldStatisticsTab: React.FC> = React.memo((props) => { @@ -20,8 +21,13 @@ export const FieldStatisticsTab: React.FC; + } - if (!services.dataVisualizer) return null; return ( { expect(findTestSubject(component, 'dscViewModeFieldStatsButton').exists()).toBe(false); }); - it('should show document and field stats view if ES|QL', async () => { + it('should not show document and field stats view if ES|QL', async () => { const component = await mountComponent({ isEsqlMode: true }); - expect(findTestSubject(component, 'dscViewModeToggle').exists()).toBe(true); + expect(findTestSubject(component, 'dscViewModeToggle').exists()).toBe(false); expect(findTestSubject(component, 'discoverQueryTotalHits').exists()).toBe(true); - expect(findTestSubject(component, 'dscViewModeDocumentButton').exists()).toBe(true); + expect(findTestSubject(component, 'dscViewModeDocumentButton').exists()).toBe(false); expect(findTestSubject(component, 'dscViewModePatternAnalysisButton').exists()).toBe(false); - expect(findTestSubject(component, 'dscViewModeFieldStatsButton').exists()).toBe(true); - expect(findTestSubject(component, 'dscViewModeDocumentButton').text()).toBe('Results (10)'); + expect(findTestSubject(component, 'dscViewModeFieldStatsButton').exists()).toBe(false); + expect(findTestSubject(component, 'discoverQueryHits').text()).toBe('10'); }); it('should set the view mode to VIEW_MODE.DOCUMENT_LEVEL when dscViewModeDocumentButton is clicked', async () => { diff --git a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx index 22c4aaa11b43..43960f98ea2b 100644 --- a/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx +++ b/src/plugins/discover/public/components/view_mode_toggle/view_mode_toggle.tsx @@ -46,10 +46,16 @@ export const DocumentViewModeToggle = ({ () => isLegacyTableEnabled({ uiSettings, isEsqlMode }), [uiSettings, isEsqlMode] ); + const [showPatternAnalysisTab, setShowPatternAnalysisTab] = useState(null); const showFieldStatisticsTab = useMemo( - () => uiSettings.get(SHOW_FIELD_STATISTICS) && dataVisualizerService !== undefined, - [dataVisualizerService, uiSettings] + () => + // If user opens saved search with field stats in ES|QL, + // we show the toggle with the mode disabled so user can switch to document view + // instead of auto-directing + (viewMode === VIEW_MODE.AGGREGATED_LEVEL && isEsqlMode) || + (!isEsqlMode && uiSettings.get(SHOW_FIELD_STATISTICS) && dataVisualizerService !== undefined), + [dataVisualizerService, uiSettings, isEsqlMode, viewMode] ); const isMounted = useMountedState(); @@ -100,6 +106,12 @@ export const DocumentViewModeToggle = ({ } `; + useEffect(() => { + if (viewMode === VIEW_MODE.AGGREGATED_LEVEL && isEsqlMode) { + setDiscoverViewMode(VIEW_MODE.DOCUMENT_LEVEL); + } + }, [viewMode, isEsqlMode, setDiscoverViewMode]); + return ( setDiscoverViewMode(VIEW_MODE.AGGREGATED_LEVEL)} data-test-subj="dscViewModeFieldStatsButton" diff --git a/src/plugins/discover/public/embeddable/components/search_embeddable_field_stats_table_component.tsx b/src/plugins/discover/public/embeddable/components/search_embeddable_field_stats_table_component.tsx index ede93bf6b6f7..cb1cd2f9eebc 100644 --- a/src/plugins/discover/public/embeddable/components/search_embeddable_field_stats_table_component.tsx +++ b/src/plugins/discover/public/embeddable/components/search_embeddable_field_stats_table_component.tsx @@ -13,10 +13,10 @@ import { BehaviorSubject } from 'rxjs'; import type { DataView } from '@kbn/data-views-plugin/common'; import { FetchContext, useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; import { DocViewFilterFn } from '@kbn/unified-doc-viewer/types'; - import { FieldStatisticsTable } from '../../application/main/components/field_stats_table'; import { isEsqlMode } from '../initialize_fetch'; import type { SearchEmbeddableApi, SearchEmbeddableStateManager } from '../types'; +import { useDiscoverServices } from '../../hooks/use_discover_services'; interface SavedSearchEmbeddableComponentProps { api: SearchEmbeddableApi & { @@ -37,8 +37,13 @@ export function SearchEmbeddablFieldStatsTableComponent({ api.fetchContext$, api.savedSearch$ ); - const isEsql = useMemo(() => isEsqlMode(savedSearch), [savedSearch]); + const services = useDiscoverServices(); + + // Quit early if we know it's in ES|QL mode + if (isEsql && services.dataVisualizer?.FieldStatsUnavailableMessage) { + return ; + } return ( { + it('should not show Field Statistics data in ES|QL mode', async () => { await discover.selectTextBaseLang(); - await discover.waitUntilSearchingHasFinished(); - - await testSubjects.click('dscViewModeFieldStatsButton'); await header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('dataVisualizerTableContainer'); + await discover.waitUntilSearchingHasFinished(); + await testSubjects.missingOrFail('dscViewModeFieldStatsButton'); }); }); }); diff --git a/test/functional/apps/discover/group6/_view_mode_toggle.ts b/test/functional/apps/discover/group6/_view_mode_toggle.ts index aec1cb6f459a..2591716c87d0 100644 --- a/test/functional/apps/discover/group6/_view_mode_toggle.ts +++ b/test/functional/apps/discover/group6/_view_mode_toggle.ts @@ -95,25 +95,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should not show', async function () { await testSubjects.missingOrFail('dscViewModePatternAnalysisButton'); - await retry.try(async () => { - const documentTab = await testSubjects.find('dscViewModeDocumentButton'); - expect(await documentTab.getAttribute('aria-selected')).to.be('true'); - }); }); }); - it('should show Field Statistics tab', async () => { - await testSubjects.click('dscViewModeFieldStatsButton'); - - await retry.try(async () => { - const fieldStatsTab = await testSubjects.find('dscViewModeFieldStatsButton'); - expect(await fieldStatsTab.getAttribute('aria-selected')).to.be('true'); - }); - + it('should not show Field Statistics tab', async () => { await testSubjects.existOrFail('dscViewModeToggle'); }); - it('should still show view mode toggle for ES|QL searches', async () => { + it('should not show view mode toggle for ES|QL searches', async () => { await testSubjects.click('dscViewModeDocumentButton'); await retry.try(async () => { @@ -125,7 +114,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await discover.selectTextBaseLang(); - await testSubjects.existOrFail('dscViewModeToggle'); + await testSubjects.missingOrFail('dscViewModeToggle'); + await testSubjects.existOrFail('discoverQueryTotalHits'); if (!useLegacyTable) { await testSubjects.existOrFail('unifiedDataTableToolbar'); diff --git a/x-pack/packages/ml/date_picker/src/components/date_picker_wrapper.tsx b/x-pack/packages/ml/date_picker/src/components/date_picker_wrapper.tsx index 8f42d755144a..c8792ab3f9d9 100644 --- a/x-pack/packages/ml/date_picker/src/components/date_picker_wrapper.tsx +++ b/x-pack/packages/ml/date_picker/src/components/date_picker_wrapper.tsx @@ -18,6 +18,7 @@ import { EuiFlexItem, EuiSuperDatePicker, type EuiSuperDatePickerProps, + EuiToolTip, } from '@elastic/eui'; import type { TimeRange } from '@kbn/es-query'; @@ -103,6 +104,10 @@ interface DatePickerWrapperProps { * when EuiSuperDatePicker's 'Refresh'|'Update' button is clicked */ onRefresh?: () => void; + /** + * Tooltip message for the update button + */ + tooltipMessage?: string; } /** @@ -122,6 +127,7 @@ export const DatePickerWrapper: FC = (props) => { isDisabled = false, needsUpdate, onRefresh, + tooltipMessage, } = props; const { data, @@ -318,34 +324,40 @@ export const DatePickerWrapper: FC = (props) => { recentlyUsedRanges={recentlyUsedRanges} dateFormat={dateFormat} commonlyUsedRanges={commonlyUsedRanges} + isDisabled={isDisabled} updateButtonProps={{ iconOnly: isWithinLBreakpoint, fill: false, ...(needsUpdate ? { needsUpdate } : {}), }} width={width} - isDisabled={isDisabled} /> {showRefresh === true || !isTimeRangeSelectorEnabled ? ( - - {needsUpdate ? ( - - ) : ( - - )} - + + + {needsUpdate ? ( + + ) : ( + + )} + + ) : null} diff --git a/x-pack/plugins/data_visualizer/kibana.jsonc b/x-pack/plugins/data_visualizer/kibana.jsonc index 1e83c34113be..72ba380fcec3 100644 --- a/x-pack/plugins/data_visualizer/kibana.jsonc +++ b/x-pack/plugins/data_visualizer/kibana.jsonc @@ -41,7 +41,6 @@ "uiActions", "lens", "esql", - "visualizations" ] } -} \ No newline at end of file +} diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx index 811d69b6bfeb..3b591a85ff47 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/data_visualizer_stats_table.tsx @@ -65,7 +65,7 @@ interface DataVisualizerTableProps { error?: Error | string; } -export const DataVisualizerTable = ({ +const UnmemoizedDataVisualizerTable = ({ items, pageState, updatePageState, @@ -506,3 +506,7 @@ export const DataVisualizerTable = ({ ); }; + +export const DataVisualizerTable = React.memo( + UnmemoizedDataVisualizerTable +) as typeof UnmemoizedDataVisualizerTable; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx index f2ac83e1d4bf..c6190c87bcae 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/index_data_visualizer_view/index_data_visualizer_esql.tsx @@ -24,12 +24,12 @@ import { EuiPanel, EuiProgress, EuiSpacer, + EuiCallOut, } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/common'; import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils'; import { getOrCreateDataViewByIndexPattern } from '../../search_strategy/requests/get_data_view_by_index_pattern'; import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme'; -import type { FieldVisConfig } from '../../../common/components/stats_table/types'; import { DATA_VISUALIZER_INDEX_VIEWER } from '../../constants/index_data_visualizer_viewer'; import { useDataVisualizerKibana } from '../../../kibana_context'; import type { GetAdditionalLinks } from '../../../common/components/results_links'; @@ -49,6 +49,7 @@ import type { import type { ESQLQuery } from '../../search_strategy/requests/esql_utils'; import { isESQLQuery } from '../../search_strategy/requests/esql_utils'; import { FieldStatsComponentType } from '../../constants/field_stats_component_type'; +import { getReasonIfFieldStatsUnavailableForQuery } from '../../utils/get_reason_fieldstats_unavailable_for_esql_query'; export interface IndexDataVisualizerESQLProps { getAdditionalLinks?: GetAdditionalLinks; @@ -64,6 +65,8 @@ export const IndexDataVisualizerESQL: FC = (dataVi const [query, setQuery] = useState(DEFAULT_ESQL_QUERY); const [currentDataView, setCurrentDataView] = useState(); + const unsupportedReasonForQuery = getReasonIfFieldStatsUnavailableForQuery(localQuery); + const toggleShowEmptyFields = () => { setDataVisualizerListState({ ...dataVisualizerListState, @@ -202,8 +205,11 @@ export const IndexDataVisualizerESQL: FC = (dataVi const onTextLangQuerySubmit = useCallback( async (q: AggregateQuery | undefined) => { if (isESQLQuery(q)) { - resetData(); - setQuery(q); + const isUnsupported = getReasonIfFieldStatsUnavailableForQuery(q) !== undefined; + if (!isUnsupported) { + resetData(); + setQuery(q); + } } }, [resetData] @@ -224,8 +230,19 @@ export const IndexDataVisualizerESQL: FC = (dataVi data-test-subj="dataViewTitleHeader" direction="row" alignItems="center" - css={{ padding: `${euiTheme.euiSizeS} 0`, marginRight: `${euiTheme.euiSize}` }} - /> + css={{ padding: 0, marginRight: 0 }} + > + {unsupportedReasonForQuery ? ( + + + + ) : null} + {isWithinLargeBreakpoint ? : null} = (dataVi width="full" needsUpdate={queryNeedsUpdate} onRefresh={handleRefresh} - isDisabled={!hasValidTimeField} + isDisabled={unsupportedReasonForQuery !== undefined} + tooltipMessage={unsupportedReasonForQuery} /> @@ -276,6 +294,7 @@ export const IndexDataVisualizerESQL: FC = (dataVi hideRunQueryText={false} isLoading={queryHistoryStatus ?? false} displayDocumentationAsFlyout + disableSubmitAction={unsupportedReasonForQuery !== undefined} /> @@ -312,7 +331,7 @@ export const IndexDataVisualizerESQL: FC = (dataVi - + void; onQuerySubmit: (query: AggregateQuery, abortController?: AbortController) => Promise; + disableSubmitAction?: boolean; } export const FieldStatsESQLEditor = ({ canEditTextBasedQuery = true, query, setQuery, onQuerySubmit, + disableSubmitAction = false, }: FieldStatsESQLEditorProps) => { const prevQuery = useRef(query); const [isVisualizationLoading, setIsVisualizationLoading] = useState(false); @@ -48,8 +50,8 @@ export const FieldStatsESQLEditor = ({ editorIsInline hideRunQueryText onTextLangQuerySubmit={onTextLangQuerySubmit} - isDisabled={false} - allowQueryCancellation + allowQueryCancellation={false} + disableSubmitAction={disableSubmitAction} isLoading={isVisualizationLoading} /> diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_initializer.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_initializer.tsx index 0e678e328894..eb829e9a20cd 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_initializer.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/field_stats/field_stats_initializer.tsx @@ -38,6 +38,7 @@ import type { import { FieldStatsInitializerViewType } from '../grid_embeddable/types'; import { isESQLQuery } from '../../search_strategy/requests/esql_utils'; import { DataSourceTypeSelector } from './field_stats_initializer_view_type'; +import { getReasonIfFieldStatsUnavailableForQuery } from '../../utils/get_reason_fieldstats_unavailable_for_esql_query'; export interface FieldStatsInitializerProps { initialInput?: Partial; @@ -94,6 +95,12 @@ export const FieldStatisticsInitializer: FC = ({ }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [dataViewId, viewType, esqlQuery.esql, isEsqlMode]); + + const unsupportedReason = useMemo( + () => getReasonIfFieldStatsUnavailableForQuery(esqlQuery), + [esqlQuery] + ); + const onESQLQuerySubmit = useCallback( async (query: AggregateQuery, abortController?: AbortController) => { const adhocDataView = await getESQLAdHocDataview(query.esql, dataViews); @@ -101,11 +108,14 @@ export const FieldStatisticsInitializer: FC = ({ setDataViewId(adhocDataView.id); } - await onPreview({ - viewType, - dataViewId: adhocDataView?.id, - query, - }); + const supported = getReasonIfFieldStatsUnavailableForQuery(query) === undefined; + if (supported) { + await onPreview({ + viewType, + dataViewId: adhocDataView?.id, + query, + }); + } }, // eslint-disable-next-line react-hooks/exhaustive-deps [isEsqlMode] @@ -202,6 +212,7 @@ export const FieldStatisticsInitializer: FC = ({ } /> ) : null} + {initialInput?.viewType === FieldStatsInitializerViewType.ESQL && !isEsqlEnabled ? ( <> @@ -247,6 +258,7 @@ export const FieldStatisticsInitializer: FC = ({ query={esqlQuery} setQuery={setQuery} onQuerySubmit={onESQLQuerySubmit} + disableSubmitAction={!!unsupportedReason} /> ) : null} @@ -272,26 +284,39 @@ export const FieldStatisticsInitializer: FC = ({ /> - - - - - + + {unsupportedReason ? ( + + + + ) : null} + + + + + + + diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/embeddable_error_msg.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/embeddable_error_msg.tsx new file mode 100644 index 000000000000..9e49570ab6bf --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/embeddable_error_msg.tsx @@ -0,0 +1,41 @@ +/* + * 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 { EuiFlexItem } from '@elastic/eui'; +import { EmptyPlaceholder } from '@kbn/charts-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { css } from '@emotion/react'; +const FIELD_STATS_UNAVAILABLE_TITLE = i18n.translate( + 'xpack.dataVisualizer.fieldStats.unavailableTitle', + { + defaultMessage: 'Field statistics not supported for ES|QL queries', + } +); + +const FieldStatsUnavailableMessage = ({ + id, + title = FIELD_STATS_UNAVAILABLE_TITLE, +}: { + id?: string; + title?: string; +}) => { + return ( + + + + ); +}; + +// Default export for lazy loading +// eslint-disable-next-line import/no-default-export +export default FieldStatsUnavailableMessage; diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/field_stats_wrapper.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/field_stats_wrapper.tsx index 03dbb9c7af8c..d991f02fb395 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/field_stats_wrapper.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/embeddables/grid_embeddable/field_stats_wrapper.tsx @@ -16,11 +16,13 @@ import { DatePickerContextProvider } from '@kbn/ml-date-picker'; import type { DatePickerDependencies } from '@kbn/ml-date-picker'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { pick } from 'lodash'; +import { isOfAggregateQueryType } from '@kbn/es-query'; import { getCoreStart, getPluginsStart } from '../../../../kibana_services'; import type { FieldStatisticTableEmbeddableProps, ESQLDataVisualizerGridEmbeddableState, } from './types'; +import FieldStatsUnavailableMessage from './embeddable_error_msg'; const EmbeddableESQLFieldStatsTableWrapper = dynamic( () => import('./embeddable_esql_field_stats_table') @@ -41,7 +43,10 @@ function isFieldStatisticTableEmbeddableState( const FieldStatisticsWrapperContent = (props: FieldStatisticTableEmbeddableProps) => { if (isESQLFieldStatisticTableEmbeddableState(props)) { - return ( + const isEsql = props.esqlQuery && isOfAggregateQueryType(props.esqlQuery); + return isEsql ? ( + + ) : ( => { - const { apiIsPresentationContainer } = await import('@kbn/presentation-containers'); - // we cannot have an async type check, so return the casted parentApi rather than a boolean - return apiIsPresentationContainer(parentApi) ? (parentApi as PresentationContainer) : undefined; -}; - -interface FieldStatsActionContext extends EmbeddableApiContext { - embeddable: FieldStatisticsTableEmbeddableApi; -} - -async function updatePanelFromFlyoutEdits({ - api, - isNewPanel, - deletePanel, - coreStart, - pluginStart, - initialState, -}: { - api: FieldStatisticsTableEmbeddableApi; - isNewPanel: boolean; - deletePanel?: () => void; - coreStart: CoreStart; - pluginStart: DataVisualizerStartDependencies; - initialState: FieldStatsInitialState; - fieldStatsControlsApi?: FieldStatsControlsApi; -}) { - const [ - { getOrCreateDataViewByIndexPattern }, - { FieldStatisticsInitializer }, - { tracksOverlays }, - { toMountPoint }, - { KibanaContextProvider }, - { isDefined }, - ] = await Promise.all([ - import('../search_strategy/requests/get_data_view_by_index_pattern'), - import('../embeddables/field_stats/field_stats_initializer'), - import('@kbn/presentation-containers'), - import('@kbn/react-kibana-mount'), - import('@kbn/kibana-react-plugin/public'), - import('@kbn/ml-is-defined'), - ]); - const parentApi = api.parentApi; - const overlayTracker = tracksOverlays(parentApi) ? parentApi : undefined; - const services = { - ...coreStart, - ...pluginStart, - }; - let hasChanged = false; - const cancelChanges = () => { - // Reset to initialState in case user has changed the preview state - if (hasChanged && api && initialState) { - api.updateUserInput(initialState); - } - - if (isNewPanel && deletePanel) { - deletePanel(); - } - flyoutSession.close(); - overlayTracker?.clearOverlays(); - }; - - const update = async (nextUpdate: FieldStatsInitialState) => { - const esqlQuery = nextUpdate?.query?.esql; - if (isDefined(esqlQuery)) { - const dv = await getOrCreateDataViewByIndexPattern( - pluginStart.data.dataViews, - esqlQuery, - undefined - ); - if (dv?.id && nextUpdate.dataViewId !== dv.id) { - nextUpdate.dataViewId = dv.id; - } - } - if (api) { - api.updateUserInput(nextUpdate); - } - - flyoutSession.close(); - overlayTracker?.clearOverlays(); - }; - const flyoutSession = services.overlays.openFlyout( - toMountPoint( - - { - if (api.updateUserInput) { - api.updateUserInput(nextUpdate); - hasChanged = true; - } - }} - onCreate={update} - onCancel={cancelChanges} - isNewPanel={isNewPanel} - /> - , - coreStart - ), - { - ownFocus: true, - size: 's', - paddingSize: 'm', - hideCloseButton: true, - type: 'push', - 'data-test-subj': 'fieldStatisticsInitializerFlyout', - onClose: cancelChanges, - } - ); - overlayTracker?.openOverlay(flyoutSession, { focusedPanelId: api.uuid }); -} - -export function createAddFieldStatsTableAction( - coreStart: CoreStart, - pluginStart: DataVisualizerStartDependencies -): UiActionsActionDefinition { - return { - id: 'create-field-stats-table', - grouping: COMMON_VISUALIZATION_GROUPING, - order: 10, - getIconType: () => 'fieldStatistics', - getDisplayName: () => - i18n.translate('xpack.dataVisualizer.fieldStatistics.displayName', { - defaultMessage: 'Field statistics', - }), - disabled: !coreStart.uiSettings.get(ENABLE_ESQL), - async isCompatible(context: EmbeddableApiContext) { - return ( - Boolean(await parentApiIsCompatible(context.embeddable)) && - coreStart.uiSettings.get(ENABLE_ESQL) - ); - }, - async execute(context) { - const [ - { IncompatibleActionError }, - { FIELD_STATS_EMBEDDABLE_TYPE }, - { FieldStatsInitializerViewType }, - ] = await Promise.all([ - import('@kbn/ui-actions-plugin/public'), - import('../embeddables/field_stats/constants'), - import('../embeddables/grid_embeddable/types'), - ]); - - const presentationContainerParent = await parentApiIsCompatible(context.embeddable); - if (!presentationContainerParent) throw new IncompatibleActionError(); - - const isEsqlEnabled = coreStart.uiSettings.get(ENABLE_ESQL); - try { - const defaultIndexPattern = await pluginStart.data.dataViews.getDefault(); - const defaultInitialState: FieldStatsInitialState = isEsqlEnabled - ? { - viewType: FieldStatsInitializerViewType.ESQL, - query: { - // Initial default query - esql: `from ${defaultIndexPattern?.getIndexPattern()} | limit 10`, - }, - } - : { - viewType: FieldStatsInitializerViewType.DATA_VIEW, - }; - const embeddable = await presentationContainerParent.addNewPanel< - object, - FieldStatisticsTableEmbeddableApi - >({ - panelType: FIELD_STATS_EMBEDDABLE_TYPE, - initialState: defaultInitialState, - }); - // open the flyout if embeddable has been created successfully - if (embeddable) { - const deletePanel = () => { - presentationContainerParent.removePanel(embeddable.uuid); - }; - - updatePanelFromFlyoutEdits({ - api: embeddable, - isNewPanel: true, - deletePanel, - coreStart, - pluginStart, - initialState: defaultInitialState, - }); - } - } catch (e) { - return Promise.reject(e); - } - }, - }; -} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/ui_actions/index.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/ui_actions/index.ts deleted file mode 100644 index ac2f73860e4f..000000000000 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/ui_actions/index.ts +++ /dev/null @@ -1,20 +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 type { CoreStart } from '@kbn/core-lifecycle-browser'; -import type { UiActionsSetup } from '@kbn/ui-actions-plugin/public'; -import type { DataVisualizerStartDependencies } from '../../common/types/data_visualizer_plugin'; -import { createAddFieldStatsTableAction } from './create_field_stats_table'; - -export function registerDataVisualizerUiActions( - uiActions: UiActionsSetup, - coreStart: CoreStart, - pluginStart: DataVisualizerStartDependencies -) { - const addFieldStatsAction = createAddFieldStatsTableAction(coreStart, pluginStart); - uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addFieldStatsAction); -} diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/get_reason_fieldstats_unavailable_for_esql_query.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/get_reason_fieldstats_unavailable_for_esql_query.ts new file mode 100644 index 000000000000..f6a9996aed42 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/get_reason_fieldstats_unavailable_for_esql_query.ts @@ -0,0 +1,22 @@ +/* + * 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 type { AggregateQuery } from '@kbn/es-query'; + +import type { Query } from '@kbn/es-query'; +import { queryCannotBeSampled } from '@kbn/esql-utils'; +import { i18n } from '@kbn/i18n'; + +export const getReasonIfFieldStatsUnavailableForQuery = ( + query?: AggregateQuery | Query | { [key: string]: any } +): string | undefined => { + if (queryCannotBeSampled(query)) { + return i18n.translate('xpack.dataVisualizer.fieldStats.unavailableForESQLQueryDescription', { + defaultMessage: `Field statistics are not available for ES|QL queries with 'MATCH' or 'QSTR' functions.`, + }); + } +}; diff --git a/x-pack/plugins/data_visualizer/public/plugin.ts b/x-pack/plugins/data_visualizer/public/plugin.ts index deaf4bdc9967..282ec23e3c30 100644 --- a/x-pack/plugins/data_visualizer/public/plugin.ts +++ b/x-pack/plugins/data_visualizer/public/plugin.ts @@ -21,7 +21,6 @@ import type { DataVisualizerStartDependencies, } from './application/common/types/data_visualizer_plugin'; import { registerEmbeddables } from './application/index_data_visualizer/embeddables/field_stats'; -import { registerDataVisualizerUiActions } from './application/index_data_visualizer/ui_actions'; export type DataVisualizerPluginSetup = ReturnType; export type DataVisualizerPluginStart = ReturnType; @@ -57,12 +56,6 @@ export class DataVisualizerPlugin registerEmbeddables(plugins.embeddable, core); } - const [coreStart, pluginStart] = await core.getStartServices(); - - if (plugins.uiActions) { - registerDataVisualizerUiActions(plugins.uiActions, coreStart, pluginStart); - } - if (plugins.home) { registerHomeAddData(plugins.home, this.resultsLinks); registerHomeFeatureCatalogue(plugins.home); @@ -84,6 +77,12 @@ export class DataVisualizerPlugin getIndexDataVisualizerComponent, getDataDriftComponent, getMaxBytesFormatted, + FieldStatsUnavailableMessage: dynamic( + async () => + import( + './application/index_data_visualizer/embeddables/grid_embeddable/embeddable_error_msg' + ) + ), FieldStatisticsTable: dynamic( async () => import( diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index 7df4f57dc987..970526cdf464 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -86,7 +86,6 @@ "@kbn/core-lifecycle-browser", "@kbn/presentation-containers", "@kbn/react-kibana-mount", - "@kbn/visualizations-plugin", "@kbn/core-ui-settings-browser" ], "exclude": [ diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts b/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts index 83aea5e94f42..ae3d2da4238a 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover/esql/_esql_view.ts @@ -90,8 +90,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.missingOrFail('showQueryBarMenu'); await testSubjects.missingOrFail('addFilter'); - await testSubjects.existOrFail('dscViewModeToggle'); - await testSubjects.existOrFail('dscViewModeDocumentButton'); + await testSubjects.missingOrFail('dscViewModeToggle'); + await testSubjects.missingOrFail('dscViewModeDocumentButton'); // when Lens suggests a table, we render an ESQL based histogram await testSubjects.existOrFail('unifiedHistogramChart'); await testSubjects.existOrFail('discoverQueryHits');