({ rawResponse: { @@ -35,9 +36,13 @@ jest.spyOn(RxApi, 'lastValueFrom').mockImplementation(async () => ({ })); async function mountAndFindSubjects( - props: Omit + props: Omit< + DiscoverNoResultsProps, + 'onDisableFilters' | 'data' | 'isTimeBased' | 'stateContainer' + > ) { const services = createDiscoverServicesMock(); + const isTimeBased = props.dataView.isTimeBased(); let component: ReactWrapper; @@ -45,7 +50,8 @@ async function mountAndFindSubjects( component = await mountWithIntl( {}} {...props} /> diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results.tsx index bd010502df1493..86f73e18ca4d00 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results.tsx @@ -10,10 +10,14 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { DataView } from '@kbn/data-views-plugin/common'; import type { AggregateQuery, Filter, Query } from '@kbn/es-query'; +import { SearchResponseWarnings } from '@kbn/search-response-warnings'; import { NoResultsSuggestions } from './no_results_suggestions'; +import type { DiscoverStateContainer } from '../../services/discover_state'; +import { useDataState } from '../../hooks/use_data_state'; import './_no_results.scss'; export interface DiscoverNoResultsProps { + stateContainer: DiscoverStateContainer; isTimeBased?: boolean; query: Query | AggregateQuery | undefined; filters: Filter[] | undefined; @@ -22,12 +26,26 @@ export interface DiscoverNoResultsProps { } export function DiscoverNoResults({ + stateContainer, isTimeBased, query, filters, dataView, onDisableFilters, }: DiscoverNoResultsProps) { + const { documents$ } = stateContainer.dataState.data$; + const interceptedWarnings = useDataState(documents$).interceptedWarnings; + + if (interceptedWarnings?.length) { + return ( + + ); + } + return ( diff --git a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx index c55e8de773942d..633f082c4792bf 100644 --- a/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx +++ b/src/plugins/discover/public/application/main/components/no_results/no_results_suggestions/no_results_suggestions.tsx @@ -121,6 +121,7 @@ export const NoResultsSuggestions: React.FC = ({ layout="horizontal" color="plain" icon={} + hasBorder title={

{ + .then(({ records, textBasedQueryColumns, interceptedWarnings }) => { if (services.analytics) { const duration = window.performance.now() - startTime; reportPerformanceMetricEvent(services.analytics, { @@ -131,6 +131,7 @@ export function fetchAll( fetchStatus, result: records, textBasedQueryColumns, + interceptedWarnings, recordRawType, query, }); diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.ts index 4a4e388a273673..bce5f266d6def2 100644 --- a/src/plugins/discover/public/application/main/utils/fetch_documents.ts +++ b/src/plugins/discover/public/application/main/utils/fetch_documents.ts @@ -11,7 +11,9 @@ import { lastValueFrom } from 'rxjs'; import { isCompleteResponse, ISearchSource } from '@kbn/data-plugin/public'; import { SAMPLE_SIZE_SETTING, buildDataTableRecordList } from '@kbn/discover-utils'; import type { EsHitRecord } from '@kbn/discover-utils/types'; +import { getSearchResponseInterceptedWarnings } from '@kbn/search-response-warnings'; import type { RecordsFetchResponse } from '../../../types'; +import { DISABLE_SHARD_FAILURE_WARNING } from '../../../../common/constants'; import { FetchDeps } from './fetch_all'; /** @@ -53,6 +55,7 @@ export const fetchDocuments = ( }), }, executionContext, + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, }) .pipe( filter((res) => isCompleteResponse(res)), @@ -61,5 +64,21 @@ export const fetchDocuments = ( }) ); - return lastValueFrom(fetch$).then((records) => ({ records })); + return lastValueFrom(fetch$).then((records) => { + const adapter = inspectorAdapters.requests; + const interceptedWarnings = adapter + ? getSearchResponseInterceptedWarnings({ + services, + adapter, + options: { + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, + }, + }) + : []; + + return { + records, + interceptedWarnings, + }; + }); }; diff --git a/src/plugins/discover/public/components/common/error_callout.tsx b/src/plugins/discover/public/components/common/error_callout.tsx index 0f1fbb722bf823..d0e914a81e851e 100644 --- a/src/plugins/discover/public/components/common/error_callout.tsx +++ b/src/plugins/discover/public/components/common/error_callout.tsx @@ -10,9 +10,6 @@ import { EuiButton, EuiCallOut, EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, EuiLink, EuiModal, EuiModalBody, @@ -104,36 +101,31 @@ export const ErrorCallout = ({ /> ) : ( - - - - -

{formattedTitle}

-
- - } + title={

{formattedTitle}

} body={ - overrideDisplay?.body ?? ( - <> -

- {error.message} -

- {showErrorMessage} - - ) +
+ {overrideDisplay?.body ?? ( + <> +

+ {error.message} +

+ {showErrorMessage} + + )} +
} - css={css` - text-align: left; - `} data-test-subj={dataTestSubj} /> )} diff --git a/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx b/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx index e45faec8cbaa1e..570c980e649e5f 100644 --- a/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx +++ b/src/plugins/discover/public/components/doc_table/create_doc_table_embeddable.tsx @@ -33,6 +33,7 @@ export function DiscoverDocTableEmbeddable(renderProps: DocTableEmbeddableProps) sharedItemTitle={renderProps.sharedItemTitle} isLoading={renderProps.isLoading} isPlainRecord={renderProps.isPlainRecord} + interceptedWarnings={renderProps.interceptedWarnings} dataTestSubj="embeddedSavedSearchDocTable" DocViewer={DocViewer} /> diff --git a/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx b/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx index 97ed5f3af9d14e..6901df855984e2 100644 --- a/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx +++ b/src/plugins/discover/public/components/doc_table/doc_table_embeddable.tsx @@ -11,6 +11,7 @@ import './index.scss'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiText } from '@elastic/eui'; import { SAMPLE_SIZE_SETTING, usePager } from '@kbn/discover-utils'; +import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings'; import { ToolBarPagination, MAX_ROWS_PER_PAGE_OPTION, @@ -22,6 +23,7 @@ import { SavedSearchEmbeddableBase } from '../../embeddable/saved_search_embedda export interface DocTableEmbeddableProps extends DocTableProps { totalHitCount: number; rowsPerPageState?: number; + interceptedWarnings?: SearchResponseInterceptedWarning[]; onUpdateRowsPerPage?: (rowsPerPage?: number) => void; } @@ -101,6 +103,7 @@ export const DocTableEmbeddable = (props: DocTableEmbeddableProps) => { return ( & filter?: (field: DataViewField, value: string[], operator: string) => void; hits?: DataTableRecord[]; totalHitCount?: number; + interceptedWarnings?: SearchResponseInterceptedWarning[]; onMoveColumn?: (column: string, index: number) => void; onUpdateRowHeight?: (rowHeight?: number) => void; onUpdateRowsPerPage?: (rowsPerPage?: number) => void; @@ -279,6 +284,7 @@ export class SavedSearchEmbeddable this.inspectorAdapters.requests!.reset(); searchProps.isLoading = true; + searchProps.interceptedWarnings = undefined; const wasAlreadyRendered = this.getOutput().rendered; @@ -357,9 +363,20 @@ export class SavedSearchEmbeddable }), }, executionContext, + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, }) ); + if (this.inspectorAdapters.requests) { + searchProps.interceptedWarnings = getSearchResponseInterceptedWarnings({ + services: this.services, + adapter: this.inspectorAdapters.requests, + options: { + disableShardFailureWarning: DISABLE_SHARD_FAILURE_WARNING, + }, + }); + } + this.updateOutput({ ...this.getOutput(), loading: false, diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable_badge.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable_badge.tsx new file mode 100644 index 00000000000000..9944adb4be33ca --- /dev/null +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable_badge.tsx @@ -0,0 +1,37 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { + SearchResponseWarnings, + type SearchResponseInterceptedWarning, +} from '@kbn/search-response-warnings'; + +export interface SavedSearchEmbeddableBadgeProps { + interceptedWarnings: SearchResponseInterceptedWarning[] | undefined; +} + +export const SavedSearchEmbeddableBadge: React.FC = ({ + interceptedWarnings, +}) => { + return interceptedWarnings?.length ? ( + + ) : null; +}; diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable_base.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable_base.tsx index 17785570b94878..b41c70676c7546 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable_base.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable_base.tsx @@ -9,7 +9,9 @@ import React from 'react'; import { css } from '@emotion/react'; import { EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; +import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings'; import { TotalDocuments } from '../application/main/components/total_documents/total_documents'; +import { SavedSearchEmbeddableBadge } from './saved_search_embeddable_badge'; const containerStyles = css` width: 100%; @@ -22,6 +24,7 @@ export interface SavedSearchEmbeddableBaseProps { prepend?: React.ReactElement; append?: React.ReactElement; dataTestSubj?: string; + interceptedWarnings?: SearchResponseInterceptedWarning[]; } export const SavedSearchEmbeddableBase: React.FC = ({ @@ -30,6 +33,7 @@ export const SavedSearchEmbeddableBase: React.FC prepend, append, dataTestSubj, + interceptedWarnings, children, }) => { return ( @@ -62,6 +66,12 @@ export const SavedSearchEmbeddableBase: React.FC {children} {Boolean(append) && {append}} + + {Boolean(interceptedWarnings?.length) && ( +
+ +
+ )} ); }; diff --git a/src/plugins/discover/public/embeddable/saved_search_grid.tsx b/src/plugins/discover/public/embeddable/saved_search_grid.tsx index 075a3ca9302353..87258347b474e2 100644 --- a/src/plugins/discover/public/embeddable/saved_search_grid.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_grid.tsx @@ -7,6 +7,7 @@ */ import React, { useState, memo } from 'react'; import type { DataTableRecord } from '@kbn/discover-utils/types'; +import type { SearchResponseInterceptedWarning } from '@kbn/search-response-warnings'; import { DiscoverGrid, DiscoverGridProps } from '../components/discover_grid/discover_grid'; import './saved_search_grid.scss'; import { DiscoverGridFlyout } from '../components/discover_grid/discover_grid_flyout'; @@ -14,11 +15,13 @@ import { SavedSearchEmbeddableBase } from './saved_search_embeddable_base'; export interface DiscoverGridEmbeddableProps extends DiscoverGridProps { totalHitCount: number; + interceptedWarnings?: SearchResponseInterceptedWarning[]; } export const DataGridMemoized = memo(DiscoverGrid); export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { + const { interceptedWarnings, ...gridProps } = props; const [expandedDoc, setExpandedDoc] = useState(undefined); return ( @@ -26,9 +29,10 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) { totalHitCount={props.totalHitCount} isLoading={props.isLoading} dataTestSubj="embeddedSavedSearchDocTable" + interceptedWarnings={props.interceptedWarnings} > { const embeddable = unifiedHistogramServicesMock.lens.EmbeddableComponent; const onLoad = component.find(embeddable).props().onLoad; const adapters = createDefaultInspectorAdapters(); + adapters.tables.tables.unifiedHistogram = { meta: { statistics: { totalCount: 100 } } } as any; const rawResponse = { _shards: { total: 1, @@ -215,14 +216,21 @@ describe('Histogram', () => { failed: 1, failures: [], }, + hits: { + total: 100, + max_score: null, + hits: [], + }, }; jest .spyOn(adapters.requests, 'getRequests') .mockReturnValue([{ response: { json: { rawResponse } } } as any]); - onLoad(false, adapters); + act(() => { + onLoad(false, adapters); + }); expect(props.onTotalHitsChange).toHaveBeenLastCalledWith( - UnifiedHistogramFetchStatus.error, - undefined + UnifiedHistogramFetchStatus.complete, + 100 ); expect(props.onChartLoad).toHaveBeenLastCalledWith({ adapters }); }); diff --git a/src/plugins/unified_histogram/public/chart/histogram.tsx b/src/plugins/unified_histogram/public/chart/histogram.tsx index 9983f2e0841dda..761e701e8f9a63 100644 --- a/src/plugins/unified_histogram/public/chart/histogram.tsx +++ b/src/plugins/unified_histogram/public/chart/histogram.tsx @@ -101,10 +101,10 @@ export function Histogram({ | undefined; const response = json?.rawResponse; - // Lens will swallow shard failures and return `isLoading: false` because it displays - // its own errors, but this causes us to emit onTotalHitsChange(UnifiedHistogramFetchStatus.complete, 0). - // This is incorrect, so we check for request failures and shard failures here, and emit an error instead. - if (requestFailed || response?._shards.failed) { + // The response can have `response?._shards.failed` but we should still be able to show hits number + // TODO: show shards warnings as a badge next to the total hits number + + if (requestFailed) { onTotalHitsChange?.(UnifiedHistogramFetchStatus.error, undefined); onChartLoad?.({ adapters: adapters ?? {} }); return; diff --git a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts index 6903cdf6b4256b..c260d3171697bd 100644 --- a/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts +++ b/src/plugins/unified_histogram/public/chart/hooks/use_total_hits.ts @@ -206,6 +206,7 @@ const fetchTotalHitsSearchSource = async ({ executionContext: { description: 'fetch total hits', }, + disableShardFailureWarning: true, // TODO: show warnings as a badge next to total hits number }) .pipe( filter((res) => isCompleteResponse(res)), diff --git a/tsconfig.base.json b/tsconfig.base.json index 12504320663e30..8efe5c62421dee 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1172,6 +1172,8 @@ "@kbn/screenshotting-plugin/*": ["x-pack/plugins/screenshotting/*"], "@kbn/search-examples-plugin": ["examples/search_examples"], "@kbn/search-examples-plugin/*": ["examples/search_examples/*"], + "@kbn/search-response-warnings": ["packages/kbn-search-response-warnings"], + "@kbn/search-response-warnings/*": ["packages/kbn-search-response-warnings/*"], "@kbn/searchprofiler-plugin": ["x-pack/plugins/searchprofiler"], "@kbn/searchprofiler-plugin/*": ["x-pack/plugins/searchprofiler/*"], "@kbn/security-api-integration-helpers": ["x-pack/test/security_api_integration/packages/helpers"], diff --git a/x-pack/test/functional/apps/discover/async_scripted_fields.js b/x-pack/test/functional/apps/discover/async_scripted_fields.js index 9a9d5e0d450f21..0d48f42c5ba1e9 100644 --- a/x-pack/test/functional/apps/discover/async_scripted_fields.js +++ b/x-pack/test/functional/apps/discover/async_scripted_fields.js @@ -15,9 +15,17 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const log = getService('log'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'settings', 'discover', 'timePicker']); + const PageObjects = getPageObjects([ + 'common', + 'settings', + 'discover', + 'timePicker', + 'header', + 'dashboard', + ]); const queryBar = getService('queryBar'); const security = getService('security'); + const dashboardAddPanel = getService('dashboardAddPanel'); describe('async search with scripted fields', function () { this.tags(['skipFirefox']); @@ -43,7 +51,7 @@ export default function ({ getService, getPageObjects }) { await security.testUser.restoreDefaults(); }); - it('query should show failed shards pop up', async function () { + it('query should show failed shards callout', async function () { if (false) { /* If you had to modify the scripted fields, you could un-comment all this, run it, use es_archiver to update 'kibana_scripted_fields_on_logstash' */ @@ -69,12 +77,39 @@ export default function ({ getService, getPageObjects }) { await retry.tryForTime(20000, async function () { // wait for shards failed message - const shardMessage = await testSubjects.getVisibleText('euiToastHeader'); + const shardMessage = await testSubjects.getVisibleText( + 'dscNoResultsInterceptedWarningsCallout_warningTitle' + ); log.debug(shardMessage); expect(shardMessage).to.be('1 of 3 shards failed'); }); }); + it('query should show failed shards badge on dashboard', async function () { + await security.testUser.setRoles([ + 'test_logstash_reader', + 'global_discover_all', + 'global_dashboard_all', + ]); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.selectIndexPattern('logsta*'); + + await PageObjects.discover.saveSearch('search with warning'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + + await dashboardAddPanel.addSavedSearch('search with warning'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await retry.tryForTime(20000, async function () { + // wait for shards failed message + await testSubjects.existOrFail('savedSearchEmbeddableWarningsCallout_trigger'); + }); + }); + it('query return results with valid scripted field', async function () { if (false) { /* the skipped steps below were used to create the scripted fields in the logstash-* index pattern diff --git a/yarn.lock b/yarn.lock index 39981aa19923e2..a7e1003917f3d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5220,6 +5220,10 @@ version "0.0.0" uid "" +"@kbn/search-response-warnings@link:packages/kbn-search-response-warnings": + version "0.0.0" + uid "" + "@kbn/searchprofiler-plugin@link:x-pack/plugins/searchprofiler": version "0.0.0" uid ""