mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[search source] open incomplete response warning in inspector (#167205)
Closes https://github.com/elastic/kibana/issues/167098 PR updates "View details" button in incomplete response callouts to open inspector to request id and cluster tab. PR them removes shards model as its no longer used. Clicking "View details" <img width="400" alt="Screenshot 2023-09-28 at 9 19 49 AM" src="1b91e70e
-3dc6-4757-89eb-0c58482fff2c"> Opens <img width="400" alt="Screenshot 2023-09-28 at 9 21 35 AM" src="e26031e8
-b2b7-45c9-9339-7206ce73e551"> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
398ae8d767
commit
5bbdec9416
47 changed files with 385 additions and 1388 deletions
|
@ -37,4 +37,5 @@ export const searchResponseIncompleteWarningLocalCluster: SearchResponseWarning
|
|||
],
|
||||
},
|
||||
},
|
||||
openInInspector: () => {},
|
||||
};
|
||||
|
|
|
@ -36,49 +36,11 @@ describe('getSearchResponseInterceptedWarnings', () => {
|
|||
expect(warnings.length).toBe(1);
|
||||
expect(warnings[0].originalWarning).toEqual(searchResponseIncompleteWarningLocalCluster);
|
||||
expect(warnings[0].action).toMatchInlineSnapshot(`
|
||||
<OpenIncompleteResultsModalButton
|
||||
<ViewWarningButton
|
||||
color="primary"
|
||||
getRequestMeta={[Function]}
|
||||
isButtonEmpty={true}
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
theme={
|
||||
Object {
|
||||
"theme$": Observable {
|
||||
"_subscribe": [Function],
|
||||
},
|
||||
}
|
||||
}
|
||||
warning={
|
||||
Object {
|
||||
"clusters": Object {
|
||||
"(local)": Object {
|
||||
"_shards": Object {
|
||||
"failed": 1,
|
||||
"skipped": 0,
|
||||
"successful": 3,
|
||||
"total": 4,
|
||||
},
|
||||
"failures": Array [
|
||||
Object {
|
||||
"index": "sample-01-rollup",
|
||||
"node": "VFTFJxpHSdaoiGxJFLSExQ",
|
||||
"reason": Object {
|
||||
"reason": "Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]",
|
||||
"type": "illegal_argument_exception",
|
||||
},
|
||||
"shard": 0,
|
||||
},
|
||||
],
|
||||
"indices": "",
|
||||
"status": "partial",
|
||||
"timed_out": false,
|
||||
"took": 25,
|
||||
},
|
||||
},
|
||||
"message": "The data might be incomplete or wrong.",
|
||||
"type": "incomplete",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -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' ? (
|
||||
<OpenIncompleteResultsModalButton
|
||||
theme={services.theme}
|
||||
warning={warning}
|
||||
size="s"
|
||||
getRequestMeta={() => ({
|
||||
request,
|
||||
response,
|
||||
})}
|
||||
<ViewWarningButton
|
||||
color="primary"
|
||||
size="s"
|
||||
onClick={warning.openInInspector}
|
||||
isButtonEmpty={true}
|
||||
/>
|
||||
) : undefined,
|
||||
|
|
|
@ -40,6 +40,7 @@ describe('hasUnsupportedDownsampledAggregationFailure', () => {
|
|||
],
|
||||
},
|
||||
},
|
||||
openInInspector: () => {},
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
@ -74,6 +75,7 @@ describe('hasUnsupportedDownsampledAggregationFailure', () => {
|
|||
],
|
||||
},
|
||||
},
|
||||
openInInspector: () => {},
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
"@kbn/i18n",
|
||||
"@kbn/inspector-plugin",
|
||||
"@kbn/core",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -1,271 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`IncompleteResultsModal should render shard failures 1`] = `
|
||||
<Fragment>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Response contains incomplete results"
|
||||
id="data.search.searchSource.fetch.incompleteResultsModal.headerTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiTabbedContent
|
||||
autoFocus="selected"
|
||||
initialSelectedTab={
|
||||
Object {
|
||||
"content": <React.Fragment>
|
||||
<ShardFailureTable
|
||||
failures={
|
||||
Array [
|
||||
Object {
|
||||
"index": "sample-01-rollup",
|
||||
"node": "VFTFJxpHSdaoiGxJFLSExQ",
|
||||
"reason": Object {
|
||||
"reason": "Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]",
|
||||
"type": "illegal_argument_exception",
|
||||
},
|
||||
"shard": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</React.Fragment>,
|
||||
"data-test-subj": "showClusterDetailsButton",
|
||||
"id": "table",
|
||||
"name": "Cluster details",
|
||||
}
|
||||
}
|
||||
tabs={
|
||||
Array [
|
||||
Object {
|
||||
"content": <React.Fragment>
|
||||
<ShardFailureTable
|
||||
failures={
|
||||
Array [
|
||||
Object {
|
||||
"index": "sample-01-rollup",
|
||||
"node": "VFTFJxpHSdaoiGxJFLSExQ",
|
||||
"reason": Object {
|
||||
"reason": "Field [kubernetes.container.memory.available.bytes] of type [aggregate_metric_double] is not supported for aggregation [percentiles]",
|
||||
"type": "illegal_argument_exception",
|
||||
},
|
||||
"shard": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</React.Fragment>,
|
||||
"data-test-subj": "showClusterDetailsButton",
|
||||
"id": "table",
|
||||
"name": "Cluster details",
|
||||
},
|
||||
Object {
|
||||
"content": <EuiCodeBlock
|
||||
data-test-subj="incompleteResultsModalRequestBlock"
|
||||
isCopyable={true}
|
||||
language="json"
|
||||
>
|
||||
{}
|
||||
</EuiCodeBlock>,
|
||||
"data-test-subj": "showRequestButton",
|
||||
"id": "json-request",
|
||||
"name": "Request",
|
||||
},
|
||||
Object {
|
||||
"content": <EuiCodeBlock
|
||||
data-test-subj="incompleteResultsModalResponseBlock"
|
||||
isCopyable={true}
|
||||
language="json"
|
||||
>
|
||||
{
|
||||
"_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]"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
</EuiCodeBlock>,
|
||||
"data-test-subj": "showResponseButton",
|
||||
"id": "json-response",
|
||||
"name": "Response",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiCopy
|
||||
afterMessage="Copied"
|
||||
textToCopy="{
|
||||
\\"_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]\\"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}"
|
||||
>
|
||||
<Component />
|
||||
</EuiCopy>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="closeIncompleteResultsModal"
|
||||
fill={true}
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Close"
|
||||
description="Closing the Modal"
|
||||
id="data.search.searchSource.fetch.incompleteResultsModal.close"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`IncompleteResultsModal should render time out 1`] = `
|
||||
<Fragment>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle
|
||||
size="xs"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Response contains incomplete results"
|
||||
id="data.search.searchSource.fetch.incompleteResultsModal.headerTitle"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiTabbedContent
|
||||
autoFocus="selected"
|
||||
initialSelectedTab={
|
||||
Object {
|
||||
"content": <React.Fragment>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
>
|
||||
<p>
|
||||
Request timed out
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</React.Fragment>,
|
||||
"data-test-subj": "showClusterDetailsButton",
|
||||
"id": "table",
|
||||
"name": "Cluster details",
|
||||
}
|
||||
}
|
||||
tabs={
|
||||
Array [
|
||||
Object {
|
||||
"content": <React.Fragment>
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
>
|
||||
<p>
|
||||
Request timed out
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</React.Fragment>,
|
||||
"data-test-subj": "showClusterDetailsButton",
|
||||
"id": "table",
|
||||
"name": "Cluster details",
|
||||
},
|
||||
Object {
|
||||
"content": <EuiCodeBlock
|
||||
data-test-subj="incompleteResultsModalRequestBlock"
|
||||
isCopyable={true}
|
||||
language="json"
|
||||
>
|
||||
{}
|
||||
</EuiCodeBlock>,
|
||||
"data-test-subj": "showRequestButton",
|
||||
"id": "json-request",
|
||||
"name": "Request",
|
||||
},
|
||||
Object {
|
||||
"content": <EuiCodeBlock
|
||||
data-test-subj="incompleteResultsModalResponseBlock"
|
||||
isCopyable={true}
|
||||
language="json"
|
||||
>
|
||||
{
|
||||
"timed_out": true,
|
||||
"_shards": {
|
||||
"total": 4,
|
||||
"successful": 4,
|
||||
"skipped": 0,
|
||||
"failed": 0
|
||||
}
|
||||
}
|
||||
</EuiCodeBlock>,
|
||||
"data-test-subj": "showResponseButton",
|
||||
"id": "json-response",
|
||||
"name": "Response",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiCopy
|
||||
afterMessage="Copied"
|
||||
textToCopy="{
|
||||
\\"timed_out\\": true,
|
||||
\\"_shards\\": {
|
||||
\\"total\\": 4,
|
||||
\\"successful\\": 4,
|
||||
\\"skipped\\": 0,
|
||||
\\"failed\\": 0
|
||||
}
|
||||
}"
|
||||
>
|
||||
<Component />
|
||||
</EuiCopy>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="closeIncompleteResultsModal"
|
||||
fill={true}
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Close"
|
||||
description="Closing the Modal"
|
||||
id="data.search.searchSource.fetch.incompleteResultsModal.close"
|
||||
values={Object {}}
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</Fragment>
|
||||
`;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
<IncompleteResultsModal
|
||||
warning={{} as unknown as SearchResponseIncompleteWarning}
|
||||
request={{}}
|
||||
response={
|
||||
{
|
||||
_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]',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as estypes.SearchResponse<any>
|
||||
}
|
||||
onClose={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render time out', () => {
|
||||
const component = shallow(
|
||||
<IncompleteResultsModal
|
||||
warning={{} as unknown as SearchResponseIncompleteWarning}
|
||||
request={{}}
|
||||
response={
|
||||
{
|
||||
timed_out: true,
|
||||
_shards: {
|
||||
total: 4,
|
||||
successful: 4,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
} as unknown as estypes.SearchResponse<any>
|
||||
}
|
||||
onClose={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -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<any>;
|
||||
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 ? (
|
||||
<EuiCallOut color="warning">
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'data.search.searchSource.fetch.incompleteResultsModal.requestTimedOutMessage',
|
||||
{
|
||||
defaultMessage: 'Request timed out',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
) : null}
|
||||
|
||||
{response._shards.failures?.length ? (
|
||||
<ShardFailureTable failures={response._shards.failures ?? []} />
|
||||
) : 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: (
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
isCopyable
|
||||
data-test-subj="incompleteResultsModalRequestBlock"
|
||||
>
|
||||
{requestJSON}
|
||||
</EuiCodeBlock>
|
||||
),
|
||||
['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: (
|
||||
<EuiCodeBlock
|
||||
language="json"
|
||||
isCopyable
|
||||
data-test-subj="incompleteResultsModalResponseBlock"
|
||||
>
|
||||
{responseJSON}
|
||||
</EuiCodeBlock>
|
||||
),
|
||||
['data-test-subj']: 'showResponseButton',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle size="xs">
|
||||
<FormattedMessage
|
||||
id="data.search.searchSource.fetch.incompleteResultsModal.headerTitle"
|
||||
defaultMessage="Response contains incomplete results"
|
||||
/>
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} autoFocus="selected" />
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiCopy textToCopy={responseJSON}>
|
||||
{(copy) => (
|
||||
<EuiButtonEmpty onClick={copy}>
|
||||
<FormattedMessage
|
||||
id="data.search.searchSource.fetch.incompleteResultsModal.copyToClipboard"
|
||||
defaultMessage="Copy response to clipboard"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiCopy>
|
||||
<EuiButton onClick={() => onClose()} fill data-test-subj="closeIncompleteResultsModal">
|
||||
<FormattedMessage
|
||||
id="data.search.searchSource.fetch.incompleteResultsModal.close"
|
||||
defaultMessage="Close"
|
||||
description="Closing the Modal"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
|
@ -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<any>;
|
||||
};
|
||||
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(
|
||||
<IncompleteResultsModal
|
||||
request={request}
|
||||
response={response}
|
||||
warning={warning}
|
||||
onClose={() => modal.close()}
|
||||
/>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
||||
{
|
||||
className: 'incompleteResultsModal',
|
||||
}
|
||||
);
|
||||
}, [getRequestMeta, theme.theme$, warning]);
|
||||
|
||||
const Component = isButtonEmpty ? EuiLink : EuiButton;
|
||||
|
||||
return (
|
||||
<Component
|
||||
color={color}
|
||||
size={size}
|
||||
onClick={onClick}
|
||||
data-test-subj="openIncompleteResultsModalBtn"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="data.search.searchSource.fetch.incompleteResultsModal.viewDetails"
|
||||
defaultMessage="View details"
|
||||
description="Open the modal to show details"
|
||||
/>
|
||||
</Component>
|
||||
);
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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<ISearchSetup, ISearchStart> {
|
|||
{
|
||||
fieldFormats,
|
||||
indexPatterns,
|
||||
inspector,
|
||||
screenshotMode,
|
||||
scriptedFieldsEnabled,
|
||||
}: SearchServiceStartDependencies
|
||||
|
@ -250,8 +253,9 @@ export class SearchService implements Plugin<ISearchSetup, ISearchStart> {
|
|||
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<ISearchSetup, ISearchStart> {
|
|||
theme,
|
||||
callback,
|
||||
requestId: request.id,
|
||||
inspector: {
|
||||
adapter,
|
||||
title: request.name,
|
||||
id: request.id,
|
||||
},
|
||||
inspectorService: inspector,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
@ -119,6 +119,10 @@ export interface SearchResponseIncompleteWarning {
|
|||
* clusters: cluster details.
|
||||
*/
|
||||
clusters: Record<string, ClusterDetails>;
|
||||
/**
|
||||
* openInInspector: callback to open warning in inspector
|
||||
*/
|
||||
openInInspector: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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([]);
|
||||
});
|
|
@ -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'],
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -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(
|
||||
<EuiTextAlign textAlign="right">
|
||||
<OpenIncompleteResultsModalButton
|
||||
theme={theme}
|
||||
getRequestMeta={() => ({
|
||||
request,
|
||||
response,
|
||||
})}
|
||||
warning={incompleteWarning}
|
||||
/>
|
||||
<ViewWarningButton onClick={incompleteWarning.openInInspector} />
|
||||
</EuiTextAlign>,
|
||||
{ theme$: theme.theme$ }
|
||||
),
|
|
@ -7,3 +7,4 @@
|
|||
*/
|
||||
|
||||
export { handleWarnings } from './handle_warnings';
|
||||
export { ViewWarningButton } from './view_warning_button';
|
|
@ -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 = () => <div />;
|
||||
|
||||
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) => (
|
||||
<React.Suspense fallback={<Fallback />}>
|
||||
<LazyOpenModalButton {...props} />
|
||||
<LazyViewWarningButton {...props} />
|
||||
</React.Suspense>
|
||||
);
|
|
@ -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 (
|
||||
<Component color={color} size={size} onClick={onClick} data-test-subj="viewWarningBtn">
|
||||
<FormattedMessage
|
||||
id="data.search.searchSource.warning.viewDetailsButtonLabel"
|
||||
defaultMessage="View details"
|
||||
description="View warning details button label"
|
||||
/>
|
||||
</Component>
|
||||
);
|
||||
}
|
|
@ -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<any> = {
|
||||
_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<any>;
|
|
@ -1,184 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ShardFailureDescription renders matching snapshot given valid properties 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiDescriptionList
|
||||
columnWidths={
|
||||
Array [
|
||||
1,
|
||||
6,
|
||||
]
|
||||
}
|
||||
compressed={true}
|
||||
descriptionProps={
|
||||
Object {
|
||||
"className": "shardFailureModal__descValue",
|
||||
}
|
||||
}
|
||||
listItems={
|
||||
Array [
|
||||
Object {
|
||||
"description": 0,
|
||||
"title": "Shard",
|
||||
},
|
||||
Object {
|
||||
"description": "repro2",
|
||||
"title": "Index",
|
||||
},
|
||||
Object {
|
||||
"description": "script_exception",
|
||||
"title": "Type",
|
||||
},
|
||||
]
|
||||
}
|
||||
titleProps={
|
||||
Object {
|
||||
"className": "shardFailureModal__descTitle",
|
||||
}
|
||||
}
|
||||
type="responsiveColumn"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1eu43fv",
|
||||
"next": undefined,
|
||||
"styles": "
|
||||
align-self: flex-start;
|
||||
",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
flush="left"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
>
|
||||
Show details
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
||||
|
||||
exports[`ShardFailureDescription should show more details when button is pressed 1`] = `
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiDescriptionList
|
||||
columnWidths={
|
||||
Array [
|
||||
1,
|
||||
6,
|
||||
]
|
||||
}
|
||||
compressed={true}
|
||||
descriptionProps={
|
||||
Object {
|
||||
"className": "shardFailureModal__descValue",
|
||||
}
|
||||
}
|
||||
listItems={
|
||||
Array [
|
||||
Object {
|
||||
"description": 0,
|
||||
"title": "Shard",
|
||||
},
|
||||
Object {
|
||||
"description": "repro2",
|
||||
"title": "Index",
|
||||
},
|
||||
Object {
|
||||
"description": "script_exception",
|
||||
"title": "Type",
|
||||
},
|
||||
Object {
|
||||
"description": "itsmeyournode",
|
||||
"title": "Node",
|
||||
},
|
||||
Object {
|
||||
"description": "runtime error",
|
||||
"title": "Reason",
|
||||
},
|
||||
Object {
|
||||
"description": <EuiCodeBlock
|
||||
isCopyable={true}
|
||||
language="java"
|
||||
paddingSize="s"
|
||||
>
|
||||
return doc['targetfield'].value;
|
||||
^---- HERE
|
||||
</EuiCodeBlock>,
|
||||
"title": "Script stack",
|
||||
},
|
||||
Object {
|
||||
"description": <EuiCodeBlock
|
||||
isCopyable={true}
|
||||
language="java"
|
||||
paddingSize="s"
|
||||
>
|
||||
return doc['targetfield'].value;
|
||||
</EuiCodeBlock>,
|
||||
"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"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1eu43fv",
|
||||
"next": undefined,
|
||||
"styles": "
|
||||
align-self: flex-start;
|
||||
",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
flush="left"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
>
|
||||
Show less
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
|
@ -1,89 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ShardFailureTable renders matching snapshot given valid properties 1`] = `
|
||||
<EuiInMemoryTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"mobileOptions": Object {
|
||||
"header": false,
|
||||
},
|
||||
"name": "Reason",
|
||||
"render": [Function],
|
||||
},
|
||||
]
|
||||
}
|
||||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1vv0je1",
|
||||
"next": undefined,
|
||||
"styles": "
|
||||
& .euiTableHeaderCell {
|
||||
|
||||
/* Take the element out of the layout */
|
||||
position: absolute;
|
||||
/* Keep it vertically inline */
|
||||
inset-block-start: auto;
|
||||
/* Chrome requires a left value, and Selenium (used by Kibana's FTR) requires an off-screen position for its .getVisibleText() to not register SR-only text */
|
||||
inset-inline-start: -10000px;
|
||||
/* The element must have a size (for some screen readers) */
|
||||
|
||||
inline-size: 1px;
|
||||
block-size: 1px;
|
||||
|
||||
/* But reduce the visible size to nothing */
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
/* And ensure no overflows occur */
|
||||
overflow: hidden;
|
||||
/* Chrome requires the negative margin to not cause overflows of parent containers */
|
||||
margin: -1px;
|
||||
|
||||
}
|
||||
& .euiTableRowCell {
|
||||
border-top: none;
|
||||
}
|
||||
",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
itemId="id"
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"id": "0",
|
||||
"index": "repro2",
|
||||
"node": "itsmeyournode",
|
||||
"reason": Object {
|
||||
"caused_by": Object {
|
||||
"reason": "Gimme reason",
|
||||
"type": "illegal_argument_exception",
|
||||
},
|
||||
"lang": "painless",
|
||||
"reason": "runtime error",
|
||||
"script": "return doc['targetfield'].value;",
|
||||
"script_stack": Array [
|
||||
"return doc['targetfield'].value;",
|
||||
" ^---- HERE",
|
||||
],
|
||||
"type": "script_exception",
|
||||
},
|
||||
"shard": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
pagination={false}
|
||||
responsive={true}
|
||||
searchFormat="eql"
|
||||
sorting={
|
||||
Object {
|
||||
"sort": Object {
|
||||
"direction": "desc",
|
||||
"field": "index",
|
||||
},
|
||||
}
|
||||
}
|
||||
tableLayout="fixed"
|
||||
/>
|
||||
`;
|
|
@ -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(<ShardFailureDescription {...failure} />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should show more details when button is pressed', async () => {
|
||||
const failure = shardFailureResponse._shards.failures![0];
|
||||
const component = shallowWithIntl(<ShardFailureDescription {...failure} />);
|
||||
await component.find(EuiButtonEmpty).simulate('click');
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -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 (
|
||||
<EuiCodeBlock language="java" paddingSize="s" isCopyable>
|
||||
{valueScript}
|
||||
</EuiCodeBlock>
|
||||
);
|
||||
} else {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function ShardFailureDescription(props: estypes.ShardFailure) {
|
||||
const [showDetails, setShowDetails] = useState<boolean>(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 (
|
||||
<EuiFlexGroup direction="column" gutterSize="none" alignItems="stretch">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiDescriptionList
|
||||
type="responsiveColumn"
|
||||
columnWidths={[1, 6]}
|
||||
listItems={items}
|
||||
compressed
|
||||
titleProps={{ className: 'shardFailureModal__descTitle' }}
|
||||
descriptionProps={{ className: 'shardFailureModal__descValue' }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
align-self: flex-start;
|
||||
`}
|
||||
>
|
||||
<EuiButtonEmpty size="s" onClick={() => 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',
|
||||
}
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -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(
|
||||
<ShardFailureTable failures={shardFailureResponse._shards.failures!} />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -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<ListItem>['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 <ShardFailureDescription {...item} />;
|
||||
},
|
||||
mobileOptions: {
|
||||
header: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
itemId="id"
|
||||
items={itemList}
|
||||
columns={columns}
|
||||
pagination={itemList.length > 10}
|
||||
sorting={SORTING}
|
||||
css={css`
|
||||
& .euiTableHeaderCell {
|
||||
${euiScreenReaderOnly()}
|
||||
}
|
||||
& .euiTableRowCell {
|
||||
border-top: none;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<RequestDetailsProps> {
|
||||
export class ClustersView extends Component<DetailViewProps> {
|
||||
static shouldShow = (request: Request) =>
|
||||
Boolean(
|
||||
(request.response?.json as { rawResponse?: estypes.SearchResponse })?.rawResponse?._shards ||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<RequestDetailsProps> {
|
||||
static propTypes = {
|
||||
request: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export class RequestDetailsRequest extends Component<DetailViewProps> {
|
||||
static shouldShow = (request: Request) => Boolean(request && request.json);
|
||||
|
||||
render() {
|
||||
|
|
|
@ -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<RequestDetailsProps> {
|
||||
static propTypes = {
|
||||
request: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export class RequestDetailsResponse extends Component<DetailViewProps> {
|
||||
static shouldShow = (request: Request) =>
|
||||
Boolean(RequestDetailsResponse.getResponseJson(request));
|
||||
|
||||
|
|
|
@ -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<RequestDetailsProps> {
|
||||
static propTypes = {
|
||||
request: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export class RequestDetailsStats extends Component<DetailViewProps> {
|
||||
static shouldShow = (request: Request) =>
|
||||
Boolean(request.stats && Object.keys(request.stats).length);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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<RequestDetailsProps, RequestDetailsState> {
|
||||
static propTypes = {
|
||||
request: PropTypes.object.isRequired,
|
||||
};
|
||||
export function RequestDetails(props: Props) {
|
||||
const [availableDetails, setAvailableDetails] = useState<DetailViewData[]>([]);
|
||||
const [selectedDetail, setSelectedDetail] = useState<DetailViewData | null>(null);
|
||||
|
||||
state = {
|
||||
availableDetails: [],
|
||||
selectedDetail: 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 };
|
||||
if (selectedDetail && nextAvailableDetails.find(({ name }) => name === selectedDetail.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
availableDetails,
|
||||
selectedDetail: availableDetails[0],
|
||||
};
|
||||
}
|
||||
setSelectedDetail(getNextTab(selectedDetail, nextAvailableDetails, props.initialTabs));
|
||||
|
||||
selectDetailsTab = (detail: DetailViewData) => {
|
||||
if (detail !== this.state.selectedDetail) {
|
||||
this.setState({
|
||||
selectedDetail: detail,
|
||||
});
|
||||
}
|
||||
};
|
||||
// do not re-run on selectedDetail change
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.initialTabs, props.request]);
|
||||
|
||||
static getSelectedDetailComponent(detail: DetailViewData | null) {
|
||||
return detail ? detail.component : null;
|
||||
}
|
||||
|
||||
renderDetailTab = (detail: DetailViewData) => {
|
||||
return (
|
||||
<EuiTab
|
||||
key={detail.name}
|
||||
isSelected={detail === this.state.selectedDetail}
|
||||
onClick={() => this.selectDetailsTab(detail)}
|
||||
data-test-subj={`inspectorRequestDetail${detail.name}`}
|
||||
>
|
||||
{detail.label}
|
||||
</EuiTab>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { selectedDetail, availableDetails } = this.state;
|
||||
const DetailComponent = RequestDetails.getSelectedDetailComponent(selectedDetail);
|
||||
|
||||
if (!availableDetails.length || !DetailComponent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTabs size="s">{this.state.availableDetails.map(this.renderDetailTab)}</EuiTabs>
|
||||
<DetailComponent request={this.props.request} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return selectedDetail ? (
|
||||
<>
|
||||
<EuiTabs size="s">
|
||||
{availableDetails.map((detail) => (
|
||||
<EuiTab
|
||||
key={detail.name}
|
||||
isSelected={detail.name === selectedDetail.name}
|
||||
onClick={() => {
|
||||
if (detail.name !== selectedDetail.name) {
|
||||
setSelectedDetail(detail);
|
||||
}
|
||||
}}
|
||||
data-test-subj={`inspectorRequestDetail${detail.name}`}
|
||||
>
|
||||
{detail.label}
|
||||
</EuiTab>
|
||||
))}
|
||||
</EuiTabs>
|
||||
<selectedDetail.component request={props.request} />
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
|
|
|
@ -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<InspectorViewProps, RequestSelectorState> {
|
||||
static propTypes = {
|
||||
adapters: PropTypes.object.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor(props: InspectorViewProps) {
|
||||
super(props);
|
||||
|
||||
|
@ -38,7 +49,10 @@ export class RequestsViewComponent extends Component<InspectorViewProps, Request
|
|||
const requests = this.getRequests();
|
||||
this.state = {
|
||||
requests,
|
||||
request: requests.length ? requests[0] : null,
|
||||
request: getInitialRequest(
|
||||
requests,
|
||||
(this.props.options as RequestViewOptions | undefined)?.initialRequestId
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -168,7 +182,12 @@ export class RequestsViewComponent extends Component<InspectorViewProps, Request
|
|||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
{this.state.request && <RequestDetails request={this.state.request} />}
|
||||
{this.state.request && (
|
||||
<RequestDetails
|
||||
initialTabs={(this.props.options as RequestViewOptions | undefined)?.initialTabs}
|
||||
request={this.state.request}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) => (
|
||||
<>
|
||||
<EuiText size="s">{warning.message}</EuiText>
|
||||
<EuiSpacer size="s" />
|
||||
<OpenIncompleteResultsModalButton
|
||||
theme={theme}
|
||||
warning={warning}
|
||||
<ViewWarningButton
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
warning.openInInspector();
|
||||
}}
|
||||
size="m"
|
||||
getRequestMeta={() => ({
|
||||
request,
|
||||
response,
|
||||
})}
|
||||
color="primary"
|
||||
isButtonEmpty={true}
|
||||
/>
|
||||
|
|
|
@ -68,6 +68,7 @@ export const MessageList = ({
|
|||
|
||||
const onButtonClick = () => setIsPopoverOpen((isOpen) => !isOpen);
|
||||
const closePopover = () => setIsPopoverOpen(false);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
panelPaddingSize="none"
|
||||
|
@ -121,7 +122,11 @@ export const MessageList = ({
|
|||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1} className="lnsWorkspaceWarningList__description">
|
||||
<EuiText size="s">{message.longMessage}</EuiText>
|
||||
<EuiText size="s">
|
||||
{typeof message.longMessage === 'function'
|
||||
? message.longMessage(closePopover)
|
||||
: message.longMessage}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</li>
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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": "クエリに一致するドキュメントの数です。",
|
||||
|
|
|
@ -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": "与查询匹配的文档数目。",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue