diff --git a/packages/kbn-search-response-warnings/src/__mocks__/search_response_warnings.ts b/packages/kbn-search-response-warnings/src/__mocks__/search_response_warnings.ts index d4110f1bb62b7..8d4bf65f5754e 100644 --- a/packages/kbn-search-response-warnings/src/__mocks__/search_response_warnings.ts +++ b/packages/kbn-search-response-warnings/src/__mocks__/search_response_warnings.ts @@ -37,4 +37,5 @@ export const searchResponseIncompleteWarningLocalCluster: SearchResponseWarning ], }, }, + openInInspector: () => {}, }; diff --git a/packages/kbn-search-response-warnings/src/utils/get_search_response_intercepted_warnings.test.tsx b/packages/kbn-search-response-warnings/src/utils/get_search_response_intercepted_warnings.test.tsx index b6bf93169ae63..6ec951898f0ed 100644 --- a/packages/kbn-search-response-warnings/src/utils/get_search_response_intercepted_warnings.test.tsx +++ b/packages/kbn-search-response-warnings/src/utils/get_search_response_intercepted_warnings.test.tsx @@ -36,49 +36,11 @@ describe('getSearchResponseInterceptedWarnings', () => { expect(warnings.length).toBe(1); expect(warnings[0].originalWarning).toEqual(searchResponseIncompleteWarningLocalCluster); expect(warnings[0].action).toMatchInlineSnapshot(` - `); }); diff --git a/packages/kbn-search-response-warnings/src/utils/get_search_response_intercepted_warnings.tsx b/packages/kbn-search-response-warnings/src/utils/get_search_response_intercepted_warnings.tsx index 6518d12109a1d..9916ddbf454f3 100644 --- a/packages/kbn-search-response-warnings/src/utils/get_search_response_intercepted_warnings.tsx +++ b/packages/kbn-search-response-warnings/src/utils/get_search_response_intercepted_warnings.tsx @@ -7,12 +7,8 @@ */ import React from 'react'; -import { - type DataPublicPluginStart, - OpenIncompleteResultsModalButton, -} from '@kbn/data-plugin/public'; +import { type DataPublicPluginStart, ViewWarningButton } from '@kbn/data-plugin/public'; import type { RequestAdapter } from '@kbn/inspector-plugin/common'; -import type { CoreStart } from '@kbn/core-lifecycle-browser'; import type { SearchResponseInterceptedWarning } from '../types'; /** @@ -27,28 +23,20 @@ export const getSearchResponseInterceptedWarnings = ({ }: { services: { data: DataPublicPluginStart; - theme: CoreStart['theme']; }; adapter: RequestAdapter; }): SearchResponseInterceptedWarning[] => { const interceptedWarnings: SearchResponseInterceptedWarning[] = []; - services.data.search.showWarnings(adapter, (warning, meta) => { - const { request, response } = meta; - + services.data.search.showWarnings(adapter, (warning) => { interceptedWarnings.push({ originalWarning: warning, action: warning.type === 'incomplete' ? ( - ({ - request, - response, - })} + ) : undefined, diff --git a/packages/kbn-search-response-warnings/src/utils/has_unsupported_downsampled_aggregation_failure.test.ts b/packages/kbn-search-response-warnings/src/utils/has_unsupported_downsampled_aggregation_failure.test.ts index e8d722feb131a..ec99f9ba8822b 100644 --- a/packages/kbn-search-response-warnings/src/utils/has_unsupported_downsampled_aggregation_failure.test.ts +++ b/packages/kbn-search-response-warnings/src/utils/has_unsupported_downsampled_aggregation_failure.test.ts @@ -40,6 +40,7 @@ describe('hasUnsupportedDownsampledAggregationFailure', () => { ], }, }, + openInInspector: () => {}, }) ).toBe(false); }); @@ -74,6 +75,7 @@ describe('hasUnsupportedDownsampledAggregationFailure', () => { ], }, }, + openInInspector: () => {}, }) ).toBe(true); }); diff --git a/packages/kbn-search-response-warnings/tsconfig.json b/packages/kbn-search-response-warnings/tsconfig.json index 77bffc521e15f..96095c1a93c44 100644 --- a/packages/kbn-search-response-warnings/tsconfig.json +++ b/packages/kbn-search-response-warnings/tsconfig.json @@ -10,7 +10,6 @@ "@kbn/i18n", "@kbn/inspector-plugin", "@kbn/core", - "@kbn/core-lifecycle-browser", ], "exclude": ["target/**/*"] } diff --git a/src/plugins/data/public/incomplete_results_modal/__snapshots__/incomplete_results_modal.test.tsx.snap b/src/plugins/data/public/incomplete_results_modal/__snapshots__/incomplete_results_modal.test.tsx.snap deleted file mode 100644 index 666b87f998c3e..0000000000000 --- a/src/plugins/data/public/incomplete_results_modal/__snapshots__/incomplete_results_modal.test.tsx.snap +++ /dev/null @@ -1,271 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`IncompleteResultsModal should render shard failures 1`] = ` - - - - - - - - - - , - "data-test-subj": "showClusterDetailsButton", - "id": "table", - "name": "Cluster details", - } - } - tabs={ - Array [ - Object { - "content": - - , - "data-test-subj": "showClusterDetailsButton", - "id": "table", - "name": "Cluster details", - }, - Object { - "content": - {} - , - "data-test-subj": "showRequestButton", - "id": "json-request", - "name": "Request", - }, - Object { - "content": - { - "_shards": { - "total": 4, - "successful": 3, - "skipped": 0, - "failed": 1, - "failures": [ - { - "shard": 0, - "index": "sample-01-rollup", - "node": "VFTFJxpHSdaoiGxJFLSExQ", - "reason": { - "type": "illegal_argument_exception", - "reason": "Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]" - } - } - ] - } -} - , - "data-test-subj": "showResponseButton", - "id": "json-response", - "name": "Response", - }, - ] - } - /> - - - - - - - - - - -`; - -exports[`IncompleteResultsModal should render time out 1`] = ` - - - - - - - - - -

- Request timed out -

-
- , - "data-test-subj": "showClusterDetailsButton", - "id": "table", - "name": "Cluster details", - } - } - tabs={ - Array [ - Object { - "content": - -

- Request timed out -

-
-
, - "data-test-subj": "showClusterDetailsButton", - "id": "table", - "name": "Cluster details", - }, - Object { - "content": - {} - , - "data-test-subj": "showRequestButton", - "id": "json-request", - "name": "Request", - }, - Object { - "content": - { - "timed_out": true, - "_shards": { - "total": 4, - "successful": 4, - "skipped": 0, - "failed": 0 - } -} - , - "data-test-subj": "showResponseButton", - "id": "json-response", - "name": "Response", - }, - ] - } - /> -
- - - - - - - - -
-`; diff --git a/src/plugins/data/public/incomplete_results_modal/_incomplete_results_modal.scss b/src/plugins/data/public/incomplete_results_modal/_incomplete_results_modal.scss deleted file mode 100644 index e2ca0f8f9b3b6..0000000000000 --- a/src/plugins/data/public/incomplete_results_modal/_incomplete_results_modal.scss +++ /dev/null @@ -1,15 +0,0 @@ -// set width and height to fixed values to prevent resizing when you switch tabs -.incompleteResultsModal { - min-height: 75vh; - width: 768px; - - // show buttons at the bottom of the modal - .kbnOverlayMountWrapper { - flex-grow: 1; - } - - // smaller gap between the modal title and body - .euiModalHeader { - padding-bottom: 0; - } -} \ No newline at end of file diff --git a/src/plugins/data/public/incomplete_results_modal/incomplete_results_modal.test.tsx b/src/plugins/data/public/incomplete_results_modal/incomplete_results_modal.test.tsx deleted file mode 100644 index 6e90740cf9888..0000000000000 --- a/src/plugins/data/public/incomplete_results_modal/incomplete_results_modal.test.tsx +++ /dev/null @@ -1,72 +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 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 { shallow } from 'enzyme'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { SearchResponseIncompleteWarning } from '../search'; -import { IncompleteResultsModal } from './incomplete_results_modal'; - -describe('IncompleteResultsModal', () => { - test('should render shard failures', () => { - const component = shallow( - - } - onClose={jest.fn()} - /> - ); - - expect(component).toMatchSnapshot(); - }); - - test('should render time out', () => { - const component = shallow( - - } - onClose={jest.fn()} - /> - ); - - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/data/public/incomplete_results_modal/incomplete_results_modal.tsx b/src/plugins/data/public/incomplete_results_modal/incomplete_results_modal.tsx deleted file mode 100644 index eb07d8d60e517..0000000000000 --- a/src/plugins/data/public/incomplete_results_modal/incomplete_results_modal.tsx +++ /dev/null @@ -1,148 +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 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 { FormattedMessage } from '@kbn/i18n-react'; -import { i18n } from '@kbn/i18n'; -import { - EuiCallOut, - EuiCodeBlock, - EuiTabbedContent, - EuiCopy, - EuiButton, - EuiModalBody, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalFooter, - EuiButtonEmpty, -} from '@elastic/eui'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { SearchRequest } from '..'; -import type { SearchResponseIncompleteWarning } from '../search'; -import { ShardFailureTable } from '../shard_failure_modal/shard_failure_table'; - -export interface Props { - onClose: () => void; - request: SearchRequest; - response: estypes.SearchResponse; - warning: SearchResponseIncompleteWarning; -} - -export function IncompleteResultsModal({ request, response, warning, onClose }: Props) { - const requestJSON = JSON.stringify(request, null, 2); - const responseJSON = JSON.stringify(response, null, 2); - - const tabs = [ - { - id: 'table', - name: i18n.translate( - 'data.search.searchSource.fetch.incompleteResultsModal.tabHeaderClusterDetails', - { - defaultMessage: 'Cluster details', - description: 'Name of the tab displaying cluster details', - } - ), - content: ( - <> - {response.timed_out ? ( - -

- {i18n.translate( - 'data.search.searchSource.fetch.incompleteResultsModal.requestTimedOutMessage', - { - defaultMessage: 'Request timed out', - } - )} -

-
- ) : null} - - {response._shards.failures?.length ? ( - - ) : null} - - ), - ['data-test-subj']: 'showClusterDetailsButton', - }, - { - id: 'json-request', - name: i18n.translate( - 'data.search.searchSource.fetch.incompleteResultsModal.tabHeaderRequest', - { - defaultMessage: 'Request', - description: 'Name of the tab displaying the JSON request', - } - ), - content: ( - - {requestJSON} - - ), - ['data-test-subj']: 'showRequestButton', - }, - { - id: 'json-response', - name: i18n.translate( - 'data.search.searchSource.fetch.incompleteResultsModal.tabHeaderResponse', - { - defaultMessage: 'Response', - description: 'Name of the tab displaying the JSON response', - } - ), - content: ( - - {responseJSON} - - ), - ['data-test-subj']: 'showResponseButton', - }, - ]; - - return ( - - - - - - - - - - - - {(copy) => ( - - - - )} - - onClose()} fill data-test-subj="closeIncompleteResultsModal"> - - - - - ); -} diff --git a/src/plugins/data/public/incomplete_results_modal/open_incomplete_results_modal_button.tsx b/src/plugins/data/public/incomplete_results_modal/open_incomplete_results_modal_button.tsx deleted file mode 100644 index 648eca08d525b..0000000000000 --- a/src/plugins/data/public/incomplete_results_modal/open_incomplete_results_modal_button.tsx +++ /dev/null @@ -1,78 +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 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, { useCallback } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiLink, EuiButton, EuiButtonProps } from '@elastic/eui'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import type { ThemeServiceStart } from '@kbn/core/public'; -import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import { getOverlays } from '../services'; -import type { SearchRequest } from '..'; -import { IncompleteResultsModal } from './incomplete_results_modal'; -import type { SearchResponseIncompleteWarning } from '../search'; -import './_incomplete_results_modal.scss'; - -// @internal -export interface OpenIncompleteResultsModalButtonProps { - theme: ThemeServiceStart; - warning: SearchResponseIncompleteWarning; - size?: EuiButtonProps['size']; - color?: EuiButtonProps['color']; - getRequestMeta: () => { - request: SearchRequest; - response: estypes.SearchResponse; - }; - isButtonEmpty?: boolean; -} - -// Needed for React.lazy -// eslint-disable-next-line import/no-default-export -export default function OpenIncompleteResultsModalButton({ - getRequestMeta, - theme, - warning, - size = 's', - color = 'warning', - isButtonEmpty = false, -}: OpenIncompleteResultsModalButtonProps) { - const onClick = useCallback(() => { - const { request, response } = getRequestMeta(); - const modal = getOverlays().openModal( - toMountPoint( - modal.close()} - />, - { theme$: theme.theme$ } - ), - { - className: 'incompleteResultsModal', - } - ); - }, [getRequestMeta, theme.theme$, warning]); - - const Component = isButtonEmpty ? EuiLink : EuiButton; - - return ( - - - - ); -} diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 48a2d9c10b71c..108b160b3b239 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -274,7 +274,7 @@ export type { } from './query'; // TODO: move to @kbn/search-response-warnings -export { OpenIncompleteResultsModalButton } from './incomplete_results_modal'; +export { ViewWarningButton } from './search/warnings'; export type { AggsStart } from './search/aggs'; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index f3679aa628560..8bd7dc786c05d 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -121,7 +121,7 @@ export class DataPublicPlugin public start( core: CoreStart, - { uiActions, fieldFormats, dataViews, screenshotMode }: DataStartDependencies + { uiActions, fieldFormats, dataViews, inspector, screenshotMode }: DataStartDependencies ): DataPublicPluginStart { const { uiSettings, notifications, overlays } = core; setNotifications(notifications); @@ -138,6 +138,7 @@ export class DataPublicPlugin const search = this.searchService.start(core, { fieldFormats, indexPatterns: dataViews, + inspector, screenshotMode, scriptedFieldsEnabled: dataViews.scriptedFieldsEnabled, }); diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index 784a41a299503..5f9e57d05fe6b 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -13,7 +13,7 @@ import { CoreSetup, CoreStart } from '@kbn/core/public'; import { coreMock } from '@kbn/core/public/mocks'; import { DataViewsContract } from '@kbn/data-views-plugin/common'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import { RequestAdapter } from '@kbn/inspector-plugin/public'; +import { Start as InspectorStartContract, RequestAdapter } from '@kbn/inspector-plugin/public'; import { managementPluginMock } from '@kbn/management-plugin/public/mocks'; import { screenshotModePluginMock } from '@kbn/screenshot-mode-plugin/public/mocks'; import type { MockedKeys } from '@kbn/utility-types-jest'; @@ -68,6 +68,7 @@ describe('Search service', () => { data = searchService.start(mockCoreStart, { fieldFormats: {} as FieldFormatsStart, indexPatterns: {} as DataViewsContract, + inspector: {} as InspectorStartContract, screenshotMode: screenshotModePluginMock.createStartContract(), scriptedFieldsEnabled: true, }); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 4631425696243..a8206faf636ea 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -23,6 +23,7 @@ import { Storage } from '@kbn/kibana-utils-plugin/public'; import { ManagementSetup } from '@kbn/management-plugin/public'; import { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; +import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public'; import React from 'react'; import { BehaviorSubject } from 'rxjs'; import { @@ -65,7 +66,7 @@ import { AggsService } from './aggs'; import { createUsageCollector, SearchUsageCollector } from './collectors'; import { getEql, getEsaggs, getEsdsl, getEssql, getEsql } from './expressions'; -import { handleWarnings } from './fetch/handle_warnings'; +import { handleWarnings } from './warnings'; import { ISearchInterceptor, SearchInterceptor } from './search_interceptor'; import { ISessionsClient, ISessionService, SessionsClient, SessionService } from './session'; import { registerSearchSessionsMgmt } from './session/sessions_mgmt'; @@ -85,6 +86,7 @@ export interface SearchServiceSetupDependencies { export interface SearchServiceStartDependencies { fieldFormats: FieldFormatsStart; indexPatterns: DataViewsContract; + inspector: InspectorStartContract; screenshotMode: ScreenshotModePluginStart; scriptedFieldsEnabled: boolean; } @@ -225,6 +227,7 @@ export class SearchService implements Plugin { { fieldFormats, indexPatterns, + inspector, screenshotMode, scriptedFieldsEnabled, }: SearchServiceStartDependencies @@ -250,8 +253,9 @@ export class SearchService implements Plugin { request: request.body, response: rawResponse, theme, - sessionId: options.sessionId, requestId: request.id, + inspector: options.inspector, + inspectorService: inspector, }); } return response; @@ -299,6 +303,12 @@ export class SearchService implements Plugin { theme, callback, requestId: request.id, + inspector: { + adapter, + title: request.name, + id: request.id, + }, + inspectorService: inspector, }); }); }, diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 2daceefeadb77..b1fda7e1efbd0 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -119,6 +119,10 @@ export interface SearchResponseIncompleteWarning { * clusters: cluster details. */ clusters: Record; + /** + * openInInspector: callback to open warning in inspector + */ + openInInspector: () => void; } /** diff --git a/src/plugins/data/public/search/fetch/extract_warnings.test.ts b/src/plugins/data/public/search/warnings/extract_warnings.test.ts similarity index 77% rename from src/plugins/data/public/search/fetch/extract_warnings.test.ts rename to src/plugins/data/public/search/warnings/extract_warnings.test.ts index fed0969c2004f..6a5fc046dc5d3 100644 --- a/src/plugins/data/public/search/fetch/extract_warnings.test.ts +++ b/src/plugins/data/public/search/warnings/extract_warnings.test.ts @@ -7,8 +7,11 @@ */ import { estypes } from '@elastic/elasticsearch'; +import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public'; import { extractWarnings } from './extract_warnings'; +const mockInspectorService = {} as InspectorStartContract; + describe('extract search response warnings', () => { describe('single cluster', () => { it('should extract incomplete warning from response with shard failures', () => { @@ -37,7 +40,7 @@ describe('extract search response warnings', () => { aggregations: {}, }; - expect(extractWarnings(response)).toEqual([ + expect(extractWarnings(response, mockInspectorService)).toEqual([ { type: 'incomplete', message: 'The data might be incomplete or wrong.', @@ -51,6 +54,7 @@ describe('extract search response warnings', () => { failures: response._shards.failures, }, }, + openInInspector: expect.any(Function), }, ]); }); @@ -62,7 +66,7 @@ describe('extract search response warnings', () => { _shards: {} as estypes.ShardStatistics, hits: { hits: [] }, }; - expect(extractWarnings(response)).toEqual([ + expect(extractWarnings(response, mockInspectorService)).toEqual([ { type: 'incomplete', message: 'The data might be incomplete or wrong.', @@ -76,18 +80,22 @@ describe('extract search response warnings', () => { failures: response._shards.failures, }, }, + openInInspector: expect.any(Function), }, ]); }); it('should not include warnings when there are none', () => { - const warnings = extractWarnings({ - timed_out: false, - _shards: { - failed: 0, - total: 9000, - }, - } as estypes.SearchResponse); + const warnings = extractWarnings( + { + timed_out: false, + _shards: { + failed: 0, + total: 9000, + }, + } as estypes.SearchResponse, + mockInspectorService + ); expect(warnings).toEqual([]); }); @@ -177,11 +185,12 @@ describe('extract search response warnings', () => { aggregations: {}, }; - expect(extractWarnings(response)).toEqual([ + expect(extractWarnings(response, mockInspectorService)).toEqual([ { type: 'incomplete', message: 'The data might be incomplete or wrong.', clusters: response._clusters.details, + openInInspector: expect.any(Function), }, ]); }); @@ -230,58 +239,62 @@ describe('extract search response warnings', () => { }, hits: { hits: [] }, }; - expect(extractWarnings(response)).toEqual([ + expect(extractWarnings(response, mockInspectorService)).toEqual([ { type: 'incomplete', message: 'The data might be incomplete or wrong.', clusters: response._clusters.details, + openInInspector: expect.any(Function), }, ]); }); it('should not include warnings when there are none', () => { - const warnings = extractWarnings({ - took: 10, - timed_out: false, - _shards: { - total: 4, - successful: 4, - skipped: 0, - failed: 0, - }, - _clusters: { - total: 2, - successful: 2, - skipped: 0, - details: { - '(local)': { - status: 'successful', - indices: 'kibana_sample_data_logs,kibana_sample_data_flights', - took: 0, - timed_out: false, - _shards: { - total: 2, - successful: 2, - skipped: 0, - failed: 0, + const warnings = extractWarnings( + { + took: 10, + timed_out: false, + _shards: { + total: 4, + successful: 4, + skipped: 0, + failed: 0, + }, + _clusters: { + total: 2, + successful: 2, + skipped: 0, + details: { + '(local)': { + status: 'successful', + indices: 'kibana_sample_data_logs,kibana_sample_data_flights', + took: 0, + timed_out: false, + _shards: { + total: 2, + successful: 2, + skipped: 0, + failed: 0, + }, }, - }, - remote1: { - status: 'successful', - indices: 'kibana_sample_data_logs,kibana_sample_data_flights', - took: 1, - timed_out: false, - _shards: { - total: 2, - successful: 2, - skipped: 0, - failed: 0, + remote1: { + status: 'successful', + indices: 'kibana_sample_data_logs,kibana_sample_data_flights', + took: 1, + timed_out: false, + _shards: { + total: 2, + successful: 2, + skipped: 0, + failed: 0, + }, }, }, }, - }, - hits: { hits: [] }, - } as estypes.SearchResponse); + hits: { hits: [] }, + } as estypes.SearchResponse, + mockInspectorService + ); expect(warnings).toEqual([]); }); diff --git a/src/plugins/data/public/search/fetch/extract_warnings.ts b/src/plugins/data/public/search/warnings/extract_warnings.ts similarity index 61% rename from src/plugins/data/public/search/fetch/extract_warnings.ts rename to src/plugins/data/public/search/warnings/extract_warnings.ts index 34b30d5f971bf..2a6a9df484036 100644 --- a/src/plugins/data/public/search/fetch/extract_warnings.ts +++ b/src/plugins/data/public/search/warnings/extract_warnings.ts @@ -9,12 +9,19 @@ import { estypes } from '@elastic/elasticsearch'; import { i18n } from '@kbn/i18n'; import type { ClusterDetails } from '@kbn/es-types'; +import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public'; +import { RequestAdapter } from '@kbn/inspector-plugin/common/adapters/request'; +import type { IInspectorInfo } from '../../../common/search/search_source'; import { SearchResponseWarning } from '../types'; /** * @internal */ -export function extractWarnings(rawResponse: estypes.SearchResponse): SearchResponseWarning[] { +export function extractWarnings( + rawResponse: estypes.SearchResponse, + inspectorService: InspectorStartContract, + inspector?: IInspectorInfo +): SearchResponseWarning[] { const warnings: SearchResponseWarning[] = []; const isPartial = rawResponse._clusters @@ -48,6 +55,29 @@ export function extractWarnings(rawResponse: estypes.SearchResponse): SearchResp failures: rawResponse._shards.failures, }, }, + openInInspector: () => { + const adapter = inspector?.adapter ? inspector.adapter : new RequestAdapter(); + if (!inspector?.adapter) { + const requestResponder = adapter.start( + i18n.translate('data.search.searchSource.anonymousRequestTitle', { + defaultMessage: 'Request', + }) + ); + requestResponder.ok({ json: rawResponse }); + } + + inspectorService.open( + { + requests: adapter, + }, + { + options: { + initialRequestId: inspector?.id, + initialTabs: ['clusters', 'response'], + }, + } + ); + }, }); } diff --git a/src/plugins/data/public/search/fetch/handle_warnings.tsx b/src/plugins/data/public/search/warnings/handle_warnings.tsx similarity index 83% rename from src/plugins/data/public/search/fetch/handle_warnings.tsx rename to src/plugins/data/public/search/warnings/handle_warnings.tsx index 3380ffe0c8c99..3409baaa4e2a4 100644 --- a/src/plugins/data/public/search/fetch/handle_warnings.tsx +++ b/src/plugins/data/public/search/warnings/handle_warnings.tsx @@ -6,20 +6,22 @@ * Side Public License, v 1. */ -import { estypes } from '@elastic/elasticsearch'; +import React from 'react'; import { EuiTextAlign } from '@elastic/eui'; +import { estypes } from '@elastic/elasticsearch'; import { ThemeServiceStart } from '@kbn/core/public'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import React from 'react'; +import type { Start as InspectorStartContract } from '@kbn/inspector-plugin/public'; import { SearchRequest } from '..'; import { getNotifications } from '../../services'; -import { OpenIncompleteResultsModalButton } from '../../incomplete_results_modal'; +import type { IInspectorInfo } from '../../../common/search/search_source'; import { SearchResponseIncompleteWarning, SearchResponseWarning, WarningHandlerCallback, } from '../types'; import { extractWarnings } from './extract_warnings'; +import { ViewWarningButton } from './view_warning_button'; /** * @internal @@ -30,17 +32,19 @@ export function handleWarnings({ response, theme, callback, - sessionId = '', requestId, + inspector, + inspectorService, }: { request: SearchRequest; response: estypes.SearchResponse; theme: ThemeServiceStart; callback?: WarningHandlerCallback; - sessionId?: string; requestId?: string; + inspector?: IInspectorInfo; + inspectorService: InspectorStartContract; }) { - const warnings = extractWarnings(response); + const warnings = extractWarnings(response, inspectorService, inspector); if (warnings.length === 0) { return; } @@ -63,14 +67,7 @@ export function handleWarnings({ title: incompleteWarning.message, text: toMountPoint( - ({ - request, - response, - })} - warning={incompleteWarning} - /> + , { theme$: theme.theme$ } ), diff --git a/src/plugins/data/public/search/fetch/index.ts b/src/plugins/data/public/search/warnings/index.ts similarity index 87% rename from src/plugins/data/public/search/fetch/index.ts rename to src/plugins/data/public/search/warnings/index.ts index 364bbeb322b5c..8b4fc162a6c54 100644 --- a/src/plugins/data/public/search/fetch/index.ts +++ b/src/plugins/data/public/search/warnings/index.ts @@ -7,3 +7,4 @@ */ export { handleWarnings } from './handle_warnings'; +export { ViewWarningButton } from './view_warning_button'; diff --git a/src/plugins/data/public/incomplete_results_modal/index.tsx b/src/plugins/data/public/search/warnings/view_warning_button/index.tsx similarity index 58% rename from src/plugins/data/public/incomplete_results_modal/index.tsx rename to src/plugins/data/public/search/warnings/view_warning_button/index.tsx index 9cb02367e67a5..4df4d1fa98104 100644 --- a/src/plugins/data/public/incomplete_results_modal/index.tsx +++ b/src/plugins/data/public/search/warnings/view_warning_button/index.tsx @@ -7,13 +7,13 @@ */ import React from 'react'; -import type { OpenIncompleteResultsModalButtonProps } from './open_incomplete_results_modal_button'; +import type { Props } from './view_warning_button'; const Fallback = () =>
; -const LazyOpenModalButton = React.lazy(() => import('./open_incomplete_results_modal_button')); -export const OpenIncompleteResultsModalButton = (props: OpenIncompleteResultsModalButtonProps) => ( +const LazyViewWarningButton = React.lazy(() => import('./view_warning_button')); +export const ViewWarningButton = (props: Props) => ( }> - + ); diff --git a/src/plugins/data/public/search/warnings/view_warning_button/view_warning_button.tsx b/src/plugins/data/public/search/warnings/view_warning_button/view_warning_button.tsx new file mode 100644 index 0000000000000..91eda6b18c296 --- /dev/null +++ b/src/plugins/data/public/search/warnings/view_warning_button/view_warning_button.tsx @@ -0,0 +1,39 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import { EuiLink, EuiButton, EuiButtonProps } from '@elastic/eui'; + +export interface Props { + onClick: () => void; + size?: EuiButtonProps['size']; + color?: EuiButtonProps['color']; + isButtonEmpty?: boolean; +} + +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default function ViewWarningButton({ + onClick, + size = 's', + color = 'warning', + isButtonEmpty = false, +}: Props) { + const Component = isButtonEmpty ? EuiLink : EuiButton; + + return ( + + + + ); +} diff --git a/src/plugins/data/public/shard_failure_modal/__mocks__/shard_failure_response.ts b/src/plugins/data/public/shard_failure_modal/__mocks__/shard_failure_response.ts deleted file mode 100644 index b45caefd5fe26..0000000000000 --- a/src/plugins/data/public/shard_failure_modal/__mocks__/shard_failure_response.ts +++ /dev/null @@ -1,36 +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 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 { estypes } from '@elastic/elasticsearch'; - -export const shardFailureResponse: estypes.SearchResponse = { - _shards: { - total: 2, - successful: 1, - skipped: 0, - failed: 1, - failures: [ - { - shard: 0, - index: 'repro2', - node: 'itsmeyournode', - reason: { - type: 'script_exception', - reason: 'runtime error', - script_stack: ["return doc['targetfield'].value;", ' ^---- HERE'], - script: "return doc['targetfield'].value;", - lang: 'painless', - caused_by: { - type: 'illegal_argument_exception', - reason: 'Gimme reason', - }, - }, - }, - ], - }, -} as unknown as estypes.SearchResponse; diff --git a/src/plugins/data/public/shard_failure_modal/__snapshots__/shard_failure_description.test.tsx.snap b/src/plugins/data/public/shard_failure_modal/__snapshots__/shard_failure_description.test.tsx.snap deleted file mode 100644 index c8635d69e1fde..0000000000000 --- a/src/plugins/data/public/shard_failure_modal/__snapshots__/shard_failure_description.test.tsx.snap +++ /dev/null @@ -1,184 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ShardFailureDescription renders matching snapshot given valid properties 1`] = ` - - - - - - - Show details - - - -`; - -exports[`ShardFailureDescription should show more details when button is pressed 1`] = ` - - - - return doc['targetfield'].value; - ^---- HERE - , - "title": "Script stack", - }, - Object { - "description": - return doc['targetfield'].value; - , - "title": "Script", - }, - Object { - "description": "painless", - "title": "Lang", - }, - Object { - "description": "illegal_argument_exception", - "title": "Caused by type", - }, - Object { - "description": "Gimme reason", - "title": "Caused by reason", - }, - ] - } - titleProps={ - Object { - "className": "shardFailureModal__descTitle", - } - } - type="responsiveColumn" - /> - - - - Show less - - - -`; diff --git a/src/plugins/data/public/shard_failure_modal/__snapshots__/shard_failure_table.test.tsx.snap b/src/plugins/data/public/shard_failure_modal/__snapshots__/shard_failure_table.test.tsx.snap deleted file mode 100644 index f5e3f66e407f9..0000000000000 --- a/src/plugins/data/public/shard_failure_modal/__snapshots__/shard_failure_table.test.tsx.snap +++ /dev/null @@ -1,89 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ShardFailureTable renders matching snapshot given valid properties 1`] = ` - -`; diff --git a/src/plugins/data/public/shard_failure_modal/shard_failure_description.test.tsx b/src/plugins/data/public/shard_failure_modal/shard_failure_description.test.tsx deleted file mode 100644 index 23a455ac04c3c..0000000000000 --- a/src/plugins/data/public/shard_failure_modal/shard_failure_description.test.tsx +++ /dev/null @@ -1,28 +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 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 { EuiButtonEmpty } from '@elastic/eui'; -import { shallowWithIntl } from '@kbn/test-jest-helpers'; -import { ShardFailureDescription } from './shard_failure_description'; -import { shardFailureResponse } from './__mocks__/shard_failure_response'; - -describe('ShardFailureDescription', () => { - it('renders matching snapshot given valid properties', () => { - const failure = shardFailureResponse._shards.failures![0]; - const component = shallowWithIntl(); - expect(component).toMatchSnapshot(); - }); - - it('should show more details when button is pressed', async () => { - const failure = shardFailureResponse._shards.failures![0]; - const component = shallowWithIntl(); - await component.find(EuiButtonEmpty).simulate('click'); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/data/public/shard_failure_modal/shard_failure_description.tsx b/src/plugins/data/public/shard_failure_modal/shard_failure_description.tsx deleted file mode 100644 index 32b54a87294d3..0000000000000 --- a/src/plugins/data/public/shard_failure_modal/shard_failure_description.tsx +++ /dev/null @@ -1,130 +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 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, { useState } from 'react'; -import { estypes } from '@elastic/elasticsearch'; -import { i18n } from '@kbn/i18n'; -import { css } from '@emotion/react'; -import { getFlattenedObject } from '@kbn/std'; -import { - EuiButtonEmpty, - EuiCodeBlock, - EuiDescriptionList, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; - -/** - * Provides pretty formatting of a given key string - * e.g. formats "this_key.is_nice" to "This key is nice" - * @param key - */ -export function formatKey(key: string): string { - const nameCapitalized = key.charAt(0).toUpperCase() + key.slice(1); - return nameCapitalized.replace(/[\._]/g, ' '); -} -/** - * Adds a EuiCodeBlock to values of `script` and `script_stack` key - * Values of other keys are handled a strings - * @param value - * @param key - */ -export function formatValueByKey(value: unknown, key: string): string | JSX.Element { - if (key === 'script' || key === 'script_stack') { - const valueScript = Array.isArray(value) ? value.join('\n') : String(value); - return ( - - {valueScript} - - ); - } else { - return String(value); - } -} - -export function ShardFailureDescription(props: estypes.ShardFailure) { - const [showDetails, setShowDetails] = useState(false); - - const flattendReason = getFlattenedObject(props.reason); - - const reasonItems = Object.entries(flattendReason) - .filter(([key]) => key !== 'type') - .map(([key, value]) => ({ - title: formatKey(key), - description: formatValueByKey(value, key), - })); - - const items = [ - { - title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.shardTitle', { - defaultMessage: 'Shard', - }), - description: props.shard, - }, - { - title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.indexTitle', { - defaultMessage: 'Index', - }), - description: props.index ?? '', - }, - { - title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.reasonTypeTitle', { - defaultMessage: 'Type', - }), - description: props.reason.type, - }, - ...(showDetails - ? [ - { - title: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.nodeTitle', { - defaultMessage: 'Node', - }), - description: props.node ?? '', - }, - ...reasonItems, - ] - : []), - ]; - - return ( - - - - - - setShowDetails((prev) => !prev)} flush="left"> - {showDetails - ? i18n.translate( - 'data.search.searchSource.fetch.shardsFailedModal.showLessButtonLabel', - { - defaultMessage: 'Show less', - } - ) - : i18n.translate( - 'data.search.searchSource.fetch.shardsFailedModal.showMoreButtonLabel', - { - defaultMessage: 'Show details', - } - )} - - - - ); -} diff --git a/src/plugins/data/public/shard_failure_modal/shard_failure_table.test.tsx b/src/plugins/data/public/shard_failure_modal/shard_failure_table.test.tsx deleted file mode 100644 index 567ad8ff80ac2..0000000000000 --- a/src/plugins/data/public/shard_failure_modal/shard_failure_table.test.tsx +++ /dev/null @@ -1,21 +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 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 { shallowWithIntl } from '@kbn/test-jest-helpers'; -import { ShardFailureTable } from './shard_failure_table'; -import { shardFailureResponse } from './__mocks__/shard_failure_response'; - -describe('ShardFailureTable', () => { - it('renders matching snapshot given valid properties', () => { - const component = shallowWithIntl( - - ); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/data/public/shard_failure_modal/shard_failure_table.tsx b/src/plugins/data/public/shard_failure_modal/shard_failure_table.tsx deleted file mode 100644 index cb4fed32f11eb..0000000000000 --- a/src/plugins/data/public/shard_failure_modal/shard_failure_table.tsx +++ /dev/null @@ -1,61 +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 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 { estypes } from '@elastic/elasticsearch'; -import { i18n } from '@kbn/i18n'; -import { css } from '@emotion/react'; -import { EuiInMemoryTable, EuiInMemoryTableProps, euiScreenReaderOnly } from '@elastic/eui'; -import { ShardFailureDescription } from './shard_failure_description'; - -export interface ListItem extends estypes.ShardFailure { - id: string; -} - -const SORTING: EuiInMemoryTableProps['sorting'] = { - sort: { - field: 'index', - direction: 'desc', - }, -}; - -export function ShardFailureTable({ failures }: { failures: estypes.ShardFailure[] }) { - const itemList = failures.map((failure, idx) => ({ ...{ id: String(idx) }, ...failure })); - - const columns = [ - { - name: i18n.translate('data.search.searchSource.fetch.shardsFailedModal.tableColReason', { - defaultMessage: 'Reason', - }), - render: (item: ListItem) => { - return ; - }, - mobileOptions: { - header: false, - }, - }, - ]; - - return ( - 10} - sorting={SORTING} - css={css` - & .euiTableHeaderCell { - ${euiScreenReaderOnly()} - } - & .euiTableRowCell { - border-top: none; - } - `} - /> - ); -} diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 924ddc87fb426..538f0412f2f8c 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -12,7 +12,10 @@ import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public'; import { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; -import { Setup as InspectorSetup } from '@kbn/inspector-plugin/public'; +import { + Setup as InspectorSetup, + Start as InspectorStartContract, +} from '@kbn/inspector-plugin/public'; import { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public'; import { SharePluginStart } from '@kbn/share-plugin/public'; import { ManagementSetup } from '@kbn/management-plugin/public'; @@ -41,6 +44,7 @@ export interface DataStartDependencies { uiActions: UiActionsStart; fieldFormats: FieldFormatsStart; dataViews: DataViewsPublicPluginStart; + inspector: InspectorStartContract; screenshotMode: ScreenshotModePluginStart; share: SharePluginStart; } diff --git a/src/plugins/data/tsconfig.json b/src/plugins/data/tsconfig.json index 81820900557d7..e86d61d8aa7c3 100644 --- a/src/plugins/data/tsconfig.json +++ b/src/plugins/data/tsconfig.json @@ -38,7 +38,6 @@ "@kbn/management-plugin", "@kbn/test-jest-helpers", "@kbn/core-notifications-browser-mocks", - "@kbn/std", "@kbn/i18n-react", "@kbn/analytics", "@kbn/core-http-browser", diff --git a/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_view.tsx b/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_view.tsx index c96b03e1d6795..1e35e0276139e 100644 --- a/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_view.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_view.tsx @@ -11,12 +11,12 @@ import { estypes } from '@elastic/elasticsearch'; import { EuiSpacer } from '@elastic/eui'; import type { ClusterDetails } from '@kbn/es-types'; import { Request } from '../../../../../../common/adapters/request/types'; -import type { RequestDetailsProps } from '../../types'; +import type { DetailViewProps } from '../types'; import { getLocalClusterDetails, LOCAL_CLUSTER_KEY } from './local_cluster'; import { ClustersHealth } from './clusters_health'; import { ClustersTable } from './clusters_table'; -export class ClustersView extends Component { +export class ClustersView extends Component { static shouldShow = (request: Request) => Boolean( (request.response?.json as { rawResponse?: estypes.SearchResponse })?.rawResponse?._shards || diff --git a/src/plugins/inspector/public/views/requests/components/details/index.ts b/src/plugins/inspector/public/views/requests/components/details/index.ts index 49356d695f47d..5bf920559adca 100644 --- a/src/plugins/inspector/public/views/requests/components/details/index.ts +++ b/src/plugins/inspector/public/views/requests/components/details/index.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +export type { DetailViewProps } from './types'; export { RequestDetailsRequest } from './req_details_request'; export { RequestDetailsResponse } from './req_details_response'; export { RequestDetailsStats } from './req_details_stats'; diff --git a/src/plugins/inspector/public/views/requests/components/details/req_details_request.tsx b/src/plugins/inspector/public/views/requests/components/details/req_details_request.tsx index d340cba2b2aae..0750ccfa4620b 100644 --- a/src/plugins/inspector/public/views/requests/components/details/req_details_request.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/req_details_request.tsx @@ -7,16 +7,11 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { Request } from '../../../../../common/adapters/request/types'; -import { RequestDetailsProps } from '../types'; +import { DetailViewProps } from './types'; import { RequestCodeViewer } from './req_code_viewer'; -export class RequestDetailsRequest extends Component { - static propTypes = { - request: PropTypes.object.isRequired, - }; - +export class RequestDetailsRequest extends Component { static shouldShow = (request: Request) => Boolean(request && request.json); render() { diff --git a/src/plugins/inspector/public/views/requests/components/details/req_details_response.tsx b/src/plugins/inspector/public/views/requests/components/details/req_details_response.tsx index f9960f3486286..023a44fb72fb9 100644 --- a/src/plugins/inspector/public/views/requests/components/details/req_details_response.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/req_details_response.tsx @@ -7,16 +7,11 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { Request } from '../../../../../common/adapters/request/types'; -import { RequestDetailsProps } from '../types'; +import { DetailViewProps } from './types'; import { RequestCodeViewer } from './req_code_viewer'; -export class RequestDetailsResponse extends Component { - static propTypes = { - request: PropTypes.object.isRequired, - }; - +export class RequestDetailsResponse extends Component { static shouldShow = (request: Request) => Boolean(RequestDetailsResponse.getResponseJson(request)); diff --git a/src/plugins/inspector/public/views/requests/components/details/req_details_stats.tsx b/src/plugins/inspector/public/views/requests/components/details/req_details_stats.tsx index 176eafa146485..fc0ab22e826ff 100644 --- a/src/plugins/inspector/public/views/requests/components/details/req_details_stats.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/req_details_stats.tsx @@ -7,7 +7,6 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { EuiIcon, EuiIconTip, @@ -18,18 +17,14 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Request, RequestStatistic } from '../../../../../common/adapters/request/types'; -import { RequestDetailsProps } from '../types'; +import { DetailViewProps } from './types'; // TODO: Replace by property once available interface RequestDetailsStatRow extends RequestStatistic { id: string; } -export class RequestDetailsStats extends Component { - static propTypes = { - request: PropTypes.object.isRequired, - }; - +export class RequestDetailsStats extends Component { static shouldShow = (request: Request) => Boolean(request.stats && Object.keys(request.stats).length); diff --git a/src/plugins/inspector/public/views/requests/components/details/types.ts b/src/plugins/inspector/public/views/requests/components/details/types.ts new file mode 100644 index 0000000000000..6cd33104a8696 --- /dev/null +++ b/src/plugins/inspector/public/views/requests/components/details/types.ts @@ -0,0 +1,13 @@ +/* + * 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 { Request } from '../../../../../common/adapters/request/types'; + +export interface DetailViewProps { + request: Request; +} diff --git a/src/plugins/inspector/public/views/requests/components/get_next_tab.test.ts b/src/plugins/inspector/public/views/requests/components/get_next_tab.test.ts new file mode 100644 index 0000000000000..27206e43c684f --- /dev/null +++ b/src/plugins/inspector/public/views/requests/components/get_next_tab.test.ts @@ -0,0 +1,62 @@ +/* + * 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 { getNextTab } from './get_next_tab'; + +const tabs = [ + { + name: 'Tab0', + label: 'Tab0', + component: null, + }, + { + name: 'Tab1', + label: 'Tab1', + component: null, + }, + { + name: 'Tab2', + label: 'Tab2', + component: null, + }, +]; + +describe('getNextTab', () => { + describe('no currentTab', () => { + test('should return first tab when preferred tabs are not requested', () => { + expect(getNextTab(null, tabs)).toEqual(tabs[0]); + }); + + test('should return first preferred tab when available', () => { + expect(getNextTab(null, tabs, ['tab1'])).toEqual(tabs[1]); + }); + + test('should return second preferred tab when first preferred tab is not available', () => { + expect(getNextTab(null, tabs, ['notAvailableTabName', 'tab2'])).toEqual(tabs[2]); + }); + + test('should return first tab when all preferred tabs are not available', () => { + expect(getNextTab(null, tabs, ['notAvailableTabName'])).toEqual(tabs[0]); + }); + }); + + describe('currentTab', () => { + const currentTab = { + name: 'noLongerAvailableTab', + label: 'noLongerAvailableTab', + component: null, + }; + test('should return first tab when preferred tabs are not requested', () => { + expect(getNextTab(currentTab, tabs)).toEqual(tabs[0]); + }); + + test('should ignore preferred tabs and return first tab', () => { + expect(getNextTab(currentTab, tabs, ['tab1'])).toEqual(tabs[0]); + }); + }); +}); diff --git a/src/plugins/inspector/public/views/requests/components/get_next_tab.ts b/src/plugins/inspector/public/views/requests/components/get_next_tab.ts new file mode 100644 index 0000000000000..de05f70dac130 --- /dev/null +++ b/src/plugins/inspector/public/views/requests/components/get_next_tab.ts @@ -0,0 +1,29 @@ +/* + * 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 type { DetailViewData } from './types'; + +export function getNextTab( + currentTab: DetailViewData | null, + tabs: DetailViewData[], + preferredTabs?: string[] +) { + const firstTab = tabs.length ? tabs[0] : null; + if (currentTab || !preferredTabs) { + return firstTab; + } + + const preferredTabName = preferredTabs.find((tabName) => { + return tabs.some(({ name }) => tabName.toLowerCase() === name.toLowerCase()); + }); + const preferredTab = preferredTabName + ? tabs.find(({ name }) => preferredTabName.toLowerCase() === name.toLowerCase()) + : undefined; + + return preferredTab ? preferredTab : firstTab; +} diff --git a/src/plugins/inspector/public/views/requests/components/request_details.tsx b/src/plugins/inspector/public/views/requests/components/request_details.tsx index 6130e9e33390a..9c1b4d3d51ba9 100644 --- a/src/plugins/inspector/public/views/requests/components/request_details.tsx +++ b/src/plugins/inspector/public/views/requests/components/request_details.tsx @@ -6,10 +6,12 @@ * Side Public License, v 1. */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiTab, EuiTabs } from '@elastic/eui'; +import type { DetailViewData } from './types'; +import { getNextTab } from './get_next_tab'; +import { Request } from '../../../../common/adapters/request/types'; import { ClustersView, @@ -17,17 +19,10 @@ import { RequestDetailsResponse, RequestDetailsStats, } from './details'; -import { RequestDetailsProps } from './types'; -interface RequestDetailsState { - availableDetails: DetailViewData[]; - selectedDetail: DetailViewData | null; -} - -export interface DetailViewData { - name: string; - label: string; - component: any; +interface Props { + initialTabs?: string[]; + request: Request; } const DETAILS: DetailViewData[] = [ @@ -39,7 +34,7 @@ const DETAILS: DetailViewData[] = [ component: RequestDetailsStats, }, { - name: 'clusters', + name: 'Clusters', label: i18n.translate('inspector.requests.clustersTabLabel', { defaultMessage: 'Clusters', }), @@ -61,72 +56,47 @@ const DETAILS: DetailViewData[] = [ }, ]; -export class RequestDetails extends Component { - static propTypes = { - request: PropTypes.object.isRequired, - }; - - state = { - availableDetails: [], - selectedDetail: null, - }; +export function RequestDetails(props: Props) { + const [availableDetails, setAvailableDetails] = useState([]); + const [selectedDetail, setSelectedDetail] = useState(null); - static getDerivedStateFromProps(nextProps: RequestDetailsProps, prevState: RequestDetailsState) { - const selectedDetail = prevState && prevState.selectedDetail; - const availableDetails = DETAILS.filter( - (detail: DetailViewData) => - !detail.component.shouldShow || detail.component.shouldShow(nextProps.request) + useEffect(() => { + const nextAvailableDetails = DETAILS.filter((detail: DetailViewData) => + detail.component.shouldShow?.(props.request) ); + setAvailableDetails(nextAvailableDetails); + // If the previously selected detail is still available we want to stay // on this tab and not set another selectedDetail. - if (selectedDetail && availableDetails.includes(selectedDetail)) { - return { availableDetails }; - } - - return { - availableDetails, - selectedDetail: availableDetails[0], - }; - } - - selectDetailsTab = (detail: DetailViewData) => { - if (detail !== this.state.selectedDetail) { - this.setState({ - selectedDetail: detail, - }); + if (selectedDetail && nextAvailableDetails.find(({ name }) => name === selectedDetail.name)) { + return; } - }; - - static getSelectedDetailComponent(detail: DetailViewData | null) { - return detail ? detail.component : null; - } - - renderDetailTab = (detail: DetailViewData) => { - return ( - this.selectDetailsTab(detail)} - data-test-subj={`inspectorRequestDetail${detail.name}`} - > - {detail.label} - - ); - }; - render() { - const { selectedDetail, availableDetails } = this.state; - const DetailComponent = RequestDetails.getSelectedDetailComponent(selectedDetail); + setSelectedDetail(getNextTab(selectedDetail, nextAvailableDetails, props.initialTabs)); - if (!availableDetails.length || !DetailComponent) { - return null; - } + // do not re-run on selectedDetail change + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [props.initialTabs, props.request]); - return ( - <> - {this.state.availableDetails.map(this.renderDetailTab)} - - - ); - } + return selectedDetail ? ( + <> + + {availableDetails.map((detail) => ( + { + if (detail.name !== selectedDetail.name) { + setSelectedDetail(detail); + } + }} + data-test-subj={`inspectorRequestDetail${detail.name}`} + > + {detail.label} + + ))} + + + + ) : null; } diff --git a/src/plugins/inspector/public/views/requests/components/requests_view.tsx b/src/plugins/inspector/public/views/requests/components/requests_view.tsx index a88bb9b768142..776158c999896 100644 --- a/src/plugins/inspector/public/views/requests/components/requests_view.tsx +++ b/src/plugins/inspector/public/views/requests/components/requests_view.tsx @@ -7,7 +7,6 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiEmptyPrompt, EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui'; @@ -19,17 +18,29 @@ import { RequestSelector } from './request_selector'; import { RequestDetails } from './request_details'; import { disambiguateRequestNames } from './disambiguate_request_names'; +function getInitialRequest(requests: Request[], initialRequestId?: string) { + const initialRequest = initialRequestId + ? requests.find(({ id }) => id === initialRequestId) + : undefined; + + if (initialRequest) { + return initialRequest; + } + + return requests.length ? requests[0] : null; +} + +interface RequestViewOptions { + initialRequestId?: string; + initialTabs?: string[]; +} + interface RequestSelectorState { requests: Request[]; request: Request | null; } export class RequestsViewComponent extends Component { - static propTypes = { - adapters: PropTypes.object.isRequired, - title: PropTypes.string.isRequired, - }; - constructor(props: InspectorViewProps) { super(props); @@ -38,7 +49,10 @@ export class RequestsViewComponent extends Component - {this.state.request && } + {this.state.request && ( + + )} ); } diff --git a/src/plugins/inspector/public/views/requests/components/types.ts b/src/plugins/inspector/public/views/requests/components/types.ts index ad3f1a528266e..fc9902577c1f7 100644 --- a/src/plugins/inspector/public/views/requests/components/types.ts +++ b/src/plugins/inspector/public/views/requests/components/types.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { Request } from '../../../../common/adapters/request/types'; - -export interface RequestDetailsProps { - request: Request; +export interface DetailViewData { + name: string; + label: string; + component: any; } diff --git a/x-pack/plugins/lens/public/datasources/form_based/utils.tsx b/x-pack/plugins/lens/public/datasources/form_based/utils.tsx index 73caafa33fc8e..de2421f672d9a 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/utils.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/utils.tsx @@ -19,7 +19,7 @@ import { groupBy, escape, uniq, uniqBy } from 'lodash'; import type { Query } from '@kbn/data-plugin/common'; import { SearchRequest } from '@kbn/data-plugin/common'; -import { SearchResponseWarning, OpenIncompleteResultsModalButton } from '@kbn/data-plugin/public'; +import { SearchResponseWarning, ViewWarningButton } from '@kbn/data-plugin/public'; import { estypes } from '@elastic/elasticsearch'; import { isQueryValid } from '@kbn/visualization-ui-components'; @@ -306,18 +306,16 @@ export function getSearchWarningMessages( fixableInEditor: true, displayLocations: [{ id: 'toolbar' }, { id: 'embeddableBadge' }], shortMessage: '', - longMessage: ( + longMessage: (closePopover) => ( <> {warning.message} - { + closePopover(); + warning.openInInspector(); + }} size="m" - getRequestMeta={() => ({ - request, - response, - })} color="primary" isButtonEmpty={true} /> diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/message_list.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/message_list.tsx index ebf221e9a4832..af4eabda5ffc1 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/message_list.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/message_list.tsx @@ -68,6 +68,7 @@ export const MessageList = ({ const onButtonClick = () => setIsPopoverOpen((isOpen) => !isOpen); const closePopover = () => setIsPopoverOpen(false); + return ( - {message.longMessage} + + {typeof message.longMessage === 'function' + ? message.longMessage(closePopover) + : message.longMessage} + diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 0c09d84df9adc..4060f1a43d5fa 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -285,7 +285,7 @@ export interface UserMessage { uniqueId?: string; severity: 'error' | 'warning' | 'info'; shortMessage: string; - longMessage: React.ReactNode | string; + longMessage: string | React.ReactNode | ((closePopover: () => void) => React.ReactNode); fixableInEditor: boolean; displayLocations: UserMessageDisplayLocation[]; } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 61e5306384a53..ccadfc4357b69 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2061,7 +2061,6 @@ "data.search.searchSource.dataViewDescription": "La vue de données qui a été interrogée.", "data.search.searchSource.dataViewIdLabel": "ID de vue de données", "data.search.searchSource.dataViewLabel": "Vue de données", - "data.search.searchSource.fetch.shardsFailedModal.tableColReason": "Raison", "data.search.searchSource.hitsDescription": "Le nombre de documents renvoyés par la requête.", "data.search.searchSource.hitsLabel": "Résultats", "data.search.searchSource.hitsTotalDescription": "Le nombre de documents correspondant à la requête.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e5dcbb8b17af5..4f7a83ea42aa9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2075,7 +2075,6 @@ "data.search.searchSource.dataViewDescription": "照会されたデータビュー。", "data.search.searchSource.dataViewIdLabel": "データビューID", "data.search.searchSource.dataViewLabel": "データビュー", - "data.search.searchSource.fetch.shardsFailedModal.tableColReason": "理由", "data.search.searchSource.hitsDescription": "クエリにより返されたドキュメントの数です。", "data.search.searchSource.hitsLabel": "ヒット数", "data.search.searchSource.hitsTotalDescription": "クエリに一致するドキュメントの数です。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fc12edb97a8e5..301a298bb2cfe 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2075,7 +2075,6 @@ "data.search.searchSource.dataViewDescription": "被查询的数据视图。", "data.search.searchSource.dataViewIdLabel": "数据视图 ID", "data.search.searchSource.dataViewLabel": "数据视图", - "data.search.searchSource.fetch.shardsFailedModal.tableColReason": "原因", "data.search.searchSource.hitsDescription": "查询返回的文档数目。", "data.search.searchSource.hitsLabel": "命中数", "data.search.searchSource.hitsTotalDescription": "与查询匹配的文档数目。",