diff --git a/src/core/public/overlays/overlay_service.ts b/src/core/public/overlays/overlay_service.ts
index 0f9dec0b311c2..a9c44f63013c7 100644
--- a/src/core/public/overlays/overlay_service.ts
+++ b/src/core/public/overlays/overlay_service.ts
@@ -77,6 +77,7 @@ export interface OverlayStart {
openModal: (
modalChildren: React.ReactNode,
modalProps?: {
+ className?: string;
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 0bd485e34ecdb..1f5b1abebe70c 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -545,6 +545,7 @@ export interface OverlayStart {
}) => OverlayRef;
// (undocumented)
openModal: (modalChildren: React.ReactNode, modalProps?: {
+ className?: string;
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => OverlayRef;
diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss
index 0ccfe4b84a1ff..a54c100d52eae 100644
--- a/src/legacy/ui/public/_index.scss
+++ b/src/legacy/ui/public/_index.scss
@@ -10,6 +10,7 @@
@import './accessibility/index';
@import './chrome/index';
+@import './courier/index';
@import './collapsible_sidebar/index';
@import './directives/index';
@import './error_allow_explicit_index/index';
diff --git a/src/legacy/ui/public/courier/_index.scss b/src/legacy/ui/public/courier/_index.scss
new file mode 100644
index 0000000000000..a5b3911b1d53c
--- /dev/null
+++ b/src/legacy/ui/public/courier/_index.scss
@@ -0,0 +1 @@
+@import './fetch/components/shard_failure_modal';
\ No newline at end of file
diff --git a/src/legacy/ui/public/courier/fetch/call_response_handlers.js b/src/legacy/ui/public/courier/fetch/call_response_handlers.js
index 112de6c54ddf0..379ea68a99b51 100644
--- a/src/legacy/ui/public/courier/fetch/call_response_handlers.js
+++ b/src/legacy/ui/public/courier/fetch/call_response_handlers.js
@@ -16,12 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
-
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiSpacer } from '@elastic/eui';
import { toastNotifications } from '../../notify';
import { RequestFailure } from '../../errors';
import { RequestStatus } from './req_status';
import { SearchError } from '../search_strategy/search_error';
-import { i18n } from '@kbn/i18n';
+import { ShardFailureOpenModalButton } from './components/shard_failure_open_modal_button';
export function CallResponseHandlersProvider(Promise) {
const ABORTED = RequestStatus.ABORTED;
@@ -39,16 +41,37 @@ export function CallResponseHandlersProvider(Promise) {
toastNotifications.addWarning({
title: i18n.translate('common.ui.courier.fetch.requestTimedOutNotificationMessage', {
defaultMessage: 'Data might be incomplete because your request timed out',
- })
+ }),
});
}
if (response._shards && response._shards.failed) {
+ const title = i18n.translate('common.ui.courier.fetch.shardsFailedNotificationMessage', {
+ defaultMessage: '{shardsFailed} of {shardsTotal} shards failed',
+ values: {
+ shardsFailed: response._shards.failed,
+ shardsTotal: response._shards.total,
+ },
+ });
+ const description = i18n.translate('common.ui.courier.fetch.shardsFailedNotificationDescription', {
+ defaultMessage: 'The data you are seeing might be incomplete or wrong.',
+ });
+
+ const text = (
+ <>
+ {description}
+
+
+ >
+ );
+
toastNotifications.addWarning({
- title: i18n.translate('common.ui.courier.fetch.shardsFailedNotificationMessage', {
- defaultMessage: '{shardsFailed} of {shardsTotal} shards failed',
- values: { shardsFailed: response._shards.failed, shardsTotal: response._shards.total }
- })
+ title,
+ text,
});
}
@@ -65,7 +88,11 @@ export function CallResponseHandlersProvider(Promise) {
if (searchRequest.filterError(response)) {
return progress();
} else {
- return searchRequest.handleFailure(response.error instanceof SearchError ? response.error : new RequestFailure(null, response));
+ return searchRequest.handleFailure(
+ response.error instanceof SearchError
+ ? response.error
+ : new RequestFailure(null, response)
+ );
}
}
diff --git a/src/legacy/ui/public/courier/fetch/components/__mocks__/shard_failure_request.ts b/src/legacy/ui/public/courier/fetch/components/__mocks__/shard_failure_request.ts
new file mode 100644
index 0000000000000..701ff19a38ab9
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/__mocks__/shard_failure_request.ts
@@ -0,0 +1,32 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Request } from '../shard_failure_types';
+export const shardFailureRequest = {
+ version: true,
+ size: 500,
+ sort: [],
+ _source: {
+ excludes: [],
+ },
+ stored_fields: ['*'],
+ script_fields: {},
+ docvalue_fields: [],
+ query: {},
+ highlight: {},
+} as Request;
diff --git a/src/legacy/ui/public/courier/fetch/components/__mocks__/shard_failure_response.ts b/src/legacy/ui/public/courier/fetch/components/__mocks__/shard_failure_response.ts
new file mode 100644
index 0000000000000..7a519b62a9cc7
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/__mocks__/shard_failure_response.ts
@@ -0,0 +1,46 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { ResponseWithShardFailure } from '../shard_failure_types';
+
+export const shardFailureResponse = {
+ _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 ResponseWithShardFailure;
diff --git a/src/legacy/ui/public/courier/fetch/components/__snapshots__/shard_failure_description.test.tsx.snap b/src/legacy/ui/public/courier/fetch/components/__snapshots__/shard_failure_description.test.tsx.snap
new file mode 100644
index 0000000000000..fca130dd33640
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/__snapshots__/shard_failure_description.test.tsx.snap
@@ -0,0 +1,192 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ShardFailureDescription renders matching snapshot given valid properties 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="column"
+ />
+
+`;
diff --git a/src/legacy/ui/public/courier/fetch/components/__snapshots__/shard_failure_modal.test.tsx.snap b/src/legacy/ui/public/courier/fetch/components/__snapshots__/shard_failure_modal.test.tsx.snap
new file mode 100644
index 0000000000000..f1bd4cbcc0669
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/__snapshots__/shard_failure_modal.test.tsx.snap
@@ -0,0 +1,194 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ShardFailureModal renders matching snapshot given valid properties 1`] = `
+
+
+
+ test
+
+
+
+ ,
+ "id": "table",
+ "name": "Shard failures",
+ }
+ }
+ tabs={
+ Array [
+ Object {
+ "content": ,
+ "id": "table",
+ "name": "Shard failures",
+ },
+ Object {
+ "content":
+ {
+ "version": true,
+ "size": 500,
+ "sort": [],
+ "_source": {
+ "excludes": []
+ },
+ "stored_fields": [
+ "*"
+ ],
+ "script_fields": {},
+ "docvalue_fields": [],
+ "query": {},
+ "highlight": {}
+}
+ ,
+ "id": "json-request",
+ "name": "Request",
+ },
+ Object {
+ "content":
+ {
+ "_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"
+ }
+ }
+ }
+ ]
+ }
+}
+ ,
+ "id": "json-response",
+ "name": "Response",
+ },
+ ]
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/legacy/ui/public/courier/fetch/components/__snapshots__/shard_failure_table.test.tsx.snap b/src/legacy/ui/public/courier/fetch/components/__snapshots__/shard_failure_table.test.tsx.snap
new file mode 100644
index 0000000000000..1c7ae60fa4d9e
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/__snapshots__/shard_failure_table.test.tsx.snap
@@ -0,0 +1,77 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ShardFailureTable renders matching snapshot given valid properties 1`] = `
+
+`;
diff --git a/src/legacy/ui/public/courier/fetch/components/_shard_failure_modal.scss b/src/legacy/ui/public/courier/fetch/components/_shard_failure_modal.scss
new file mode 100644
index 0000000000000..6527289f8021f
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/_shard_failure_modal.scss
@@ -0,0 +1,41 @@
+// set width and height to fixed values to prevent resizing when you switch tabs
+.shardFailureModal {
+ min-height: 75vh;
+ width: 768px;
+}
+
+.shardFailureModal__desc {
+ // set for IE11, since without it depending on the content the width of the list
+ // could be much higher than the available screenspace
+ max-width: 686px;
+}
+
+.shardFailureModal__descTitle {
+ width: 20% !important;
+ margin-top: $euiSizeS;
+}
+
+.shardFailureModal__descValue {
+ width: 80% !important;
+ margin-top: $euiSizeS;
+}
+.shardFailureModal__keyValueTitle {
+ padding-right: $euiSizeS;
+}
+
+@include euiBreakpoint('xs','s') {
+ .shardFailureModal__keyValueTitle {
+ display: block;
+ width: 100%;
+ }
+
+ .shardFailureModal__descTitle {
+ display: block;
+ width: 100% !important;
+ }
+
+ .shardFailureModal__descValue {
+ display: block;
+ width: 100% !important;
+ }
+}
\ No newline at end of file
diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_description.test.tsx b/src/legacy/ui/public/courier/fetch/components/shard_failure_description.test.tsx
new file mode 100644
index 0000000000000..49983c9926381
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_description.test.tsx
@@ -0,0 +1,31 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { shallowWithIntl } from 'test_utils/enzyme_helpers';
+import { ShardFailureDescription } from './shard_failure_description';
+import { shardFailureResponse } from './__mocks__/shard_failure_response';
+import { ShardFailure } from './shard_failure_types';
+
+describe('ShardFailureDescription', () => {
+ it('renders matching snapshot given valid properties', () => {
+ const failure = shardFailureResponse._shards.failures[0] as ShardFailure;
+ const component = shallowWithIntl();
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_description.tsx b/src/legacy/ui/public/courier/fetch/components/shard_failure_description.tsx
new file mode 100644
index 0000000000000..6028a50cf9c3e
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_description.tsx
@@ -0,0 +1,75 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { EuiCodeBlock, EuiDescriptionList, EuiSpacer } from '@elastic/eui';
+import { ShardFailure } from './shard_failure_types';
+import { getFlattenedObject } from '../../../../../../legacy/utils/get_flattened_object';
+import { ShardFailureDescriptionHeader } from './shard_failure_description_header';
+
+/**
+ * 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: ShardFailure) {
+ const flattendReason = getFlattenedObject(props.reason);
+
+ const listItems = Object.entries(flattendReason).map(([key, value]) => ({
+ title: formatKey(key),
+ description: formatValueByKey(value, key),
+ }));
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_description_header.tsx b/src/legacy/ui/public/courier/fetch/components/shard_failure_description_header.tsx
new file mode 100644
index 0000000000000..ea4f33f9e914e
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_description_header.tsx
@@ -0,0 +1,65 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { EuiCode, EuiTitle } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { ShardFailure } from './shard_failure_types';
+
+export function getFailurePropsForSummary(
+ failure: ShardFailure
+): Array<{ key: string; value: string }> {
+ const failureDetailProps: Array = ['shard', 'index', 'node'];
+ return failureDetailProps
+ .filter(key => typeof failure[key] === 'number' || typeof failure[key] === 'string')
+ .map(key => ({ key, value: String(failure[key]) }));
+}
+
+export function getFailureSummaryText(failure: ShardFailure, failureDetails?: string): string {
+ const failureName = failure.reason.type;
+ const displayDetails =
+ typeof failureDetails === 'string' ? failureDetails : getFailureSummaryDetailsText(failure);
+
+ return i18n.translate('common.ui.courier.fetch.shardsFailedModal.failureHeader', {
+ defaultMessage: '{failureName} at {failureDetails}',
+ values: { failureName, failureDetails: displayDetails },
+ description: 'Summary of shard failures, e.g. "IllegalArgumentException at shard 0 node xyz"',
+ });
+}
+
+export function getFailureSummaryDetailsText(failure: ShardFailure): string {
+ return getFailurePropsForSummary(failure)
+ .map(({ key, value }) => `${key}: ${value}`)
+ .join(', ');
+}
+
+export function ShardFailureDescriptionHeader(props: ShardFailure) {
+ const failureDetails = getFailurePropsForSummary(props).map(kv => (
+
+ {kv.key} {kv.value}
+
+ ));
+ return (
+
+
+ {getFailureSummaryText(props, '')}
+ {failureDetails}
+
+
+ );
+}
diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_modal.test.tsx b/src/legacy/ui/public/courier/fetch/components/shard_failure_modal.test.tsx
new file mode 100644
index 0000000000000..245ff8b7bdbfc
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_modal.test.tsx
@@ -0,0 +1,38 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { shallowWithIntl } from 'test_utils/enzyme_helpers';
+import { ShardFailureModal } from './shard_failure_modal';
+import { shardFailureRequest } from './__mocks__/shard_failure_request';
+import { shardFailureResponse } from './__mocks__/shard_failure_response';
+
+describe('ShardFailureModal', () => {
+ it('renders matching snapshot given valid properties', () => {
+ const component = shallowWithIntl(
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_modal.tsx b/src/legacy/ui/public/courier/fetch/components/shard_failure_modal.tsx
new file mode 100644
index 0000000000000..d028a831a6e39
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_modal.tsx
@@ -0,0 +1,123 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiCodeBlock,
+ EuiTabbedContent,
+ EuiCopy,
+ EuiButton,
+ EuiModalBody,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiModalFooter,
+ EuiButtonEmpty,
+ EuiCallOut,
+} from '@elastic/eui';
+import { ShardFailureTable } from './shard_failure_table';
+import { ResponseWithShardFailure, Request } from './shard_failure_types';
+
+export interface Props {
+ onClose: () => void;
+ request: Request;
+ response: ResponseWithShardFailure;
+ title: string;
+}
+
+export function ShardFailureModal({ request, response, title, onClose }: Props) {
+ if (!response || !response._shards || !Array.isArray(response._shards.failures) || !request) {
+ // this should never ever happen, but just in case
+ return (
+
+ The ShardFailureModal component received invalid properties
+
+ );
+ }
+
+ const requestJSON = JSON.stringify(request, null, 2);
+ const responseJSON = JSON.stringify(response, null, 2);
+ const failures = response._shards.failures;
+
+ const tabs = [
+ {
+ id: 'table',
+ name: i18n.translate('common.ui.courier.fetch.shardsFailedModal.tabHeaderShardFailures', {
+ defaultMessage: 'Shard failures',
+ description: 'Name of the tab displaying shard failures',
+ }),
+ content: ,
+ },
+ {
+ id: 'json-request',
+ name: i18n.translate('common.ui.courier.fetch.shardsFailedModal.tabHeaderRequest', {
+ defaultMessage: 'Request',
+ description: 'Name of the tab displaying the JSON request',
+ }),
+ content: (
+
+ {requestJSON}
+
+ ),
+ },
+ {
+ id: 'json-response',
+ name: i18n.translate('common.ui.courier.fetch.shardsFailedModal.tabHeaderResponse', {
+ defaultMessage: 'Response',
+ description: 'Name of the tab displaying the JSON response',
+ }),
+ content: (
+
+ {responseJSON}
+
+ ),
+ },
+ ];
+
+ return (
+
+
+ {title}
+
+
+
+
+
+
+ {copy => (
+
+
+
+ )}
+
+ onClose()} fill data-test-sub="closeShardFailureModal">
+
+
+
+
+ );
+}
diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_open_modal_button.test.mocks.tsx b/src/legacy/ui/public/courier/fetch/components/shard_failure_open_modal_button.test.mocks.tsx
new file mode 100644
index 0000000000000..b09270881cc05
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_open_modal_button.test.mocks.tsx
@@ -0,0 +1,31 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export const openModal = jest.fn();
+
+jest.doMock('ui/new_platform', () => {
+ return {
+ npStart: {
+ core: {
+ overlays: {
+ openModal,
+ },
+ },
+ },
+ };
+});
diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_open_modal_button.test.tsx b/src/legacy/ui/public/courier/fetch/components/shard_failure_open_modal_button.test.tsx
new file mode 100644
index 0000000000000..18b1237895f79
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_open_modal_button.test.tsx
@@ -0,0 +1,40 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { openModal } from './shard_failure_open_modal_button.test.mocks';
+import React from 'react';
+import { mountWithIntl } from 'test_utils/enzyme_helpers';
+import { ShardFailureOpenModalButton } from './shard_failure_open_modal_button';
+import { shardFailureRequest } from './__mocks__/shard_failure_request';
+import { shardFailureResponse } from './__mocks__/shard_failure_response';
+// @ts-ignore
+import { findTestSubject } from '@elastic/eui/lib/test';
+
+describe('ShardFailureOpenModalButton', () => {
+ it('triggers the openModal function when "Show details" button is clicked', () => {
+ const component = mountWithIntl(
+
+ );
+ findTestSubject(component, 'openShardFailureModalBtn').simulate('click');
+ expect(openModal).toHaveBeenCalled();
+ });
+});
diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_open_modal_button.tsx b/src/legacy/ui/public/courier/fetch/components/shard_failure_open_modal_button.tsx
new file mode 100644
index 0000000000000..5e53477b8ec04
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_open_modal_button.tsx
@@ -0,0 +1,64 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+// @ts-ignore
+import { npStart } from 'ui/new_platform';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiButton, EuiTextAlign } from '@elastic/eui';
+
+import { ShardFailureModal } from './shard_failure_modal';
+import { ResponseWithShardFailure, Request } from './shard_failure_types';
+
+interface Props {
+ request: Request;
+ response: ResponseWithShardFailure;
+ title: string;
+}
+
+export function ShardFailureOpenModalButton({ request, response, title }: Props) {
+ function onClick() {
+ const modal = npStart.core.overlays.openModal(
+ modal.close()}
+ />,
+ {
+ className: 'shardFailureModal',
+ }
+ );
+ }
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_table.test.tsx b/src/legacy/ui/public/courier/fetch/components/shard_failure_table.test.tsx
new file mode 100644
index 0000000000000..9d00233d37f8c
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_table.test.tsx
@@ -0,0 +1,31 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { shallowWithIntl } from 'test_utils/enzyme_helpers';
+import { ShardFailureTable } from './shard_failure_table';
+import { shardFailureResponse } from './__mocks__/shard_failure_response';
+import { ShardFailure } from './shard_failure_types';
+
+describe('ShardFailureTable', () => {
+ it('renders matching snapshot given valid properties', () => {
+ const failures = shardFailureResponse._shards.failures as ShardFailure[];
+ const component = shallowWithIntl();
+ expect(component).toMatchSnapshot();
+ });
+});
diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_table.tsx b/src/legacy/ui/public/courier/fetch/components/shard_failure_table.tsx
new file mode 100644
index 0000000000000..7b19102647701
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_table.tsx
@@ -0,0 +1,133 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React, { useState, ReactElement } from 'react';
+// @ts-ignore
+import { EuiInMemoryTable, EuiButtonIcon } from '@elastic/eui';
+// @ts-ignore
+import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
+import { i18n } from '@kbn/i18n';
+import { ShardFailureDescription } from './shard_failure_description';
+import { ShardFailure } from './shard_failure_types';
+import { getFailureSummaryText } from './shard_failure_description_header';
+
+export interface ListItem extends ShardFailure {
+ id: string;
+}
+
+export function ShardFailureTable({ failures }: { failures: ShardFailure[] }) {
+ const itemList = failures.map((failure, idx) => ({ ...{ id: String(idx) }, ...failure }));
+ const initalMap = {} as Record;
+
+ const [expandMap, setExpandMap] = useState(initalMap);
+
+ const columns = [
+ {
+ align: RIGHT_ALIGNMENT,
+ width: '40px',
+ isExpander: true,
+ render: (item: ListItem) => {
+ const failureSummeryText = getFailureSummaryText(item);
+ const collapseLabel = i18n.translate(
+ 'common.ui.courier.fetch.shardsFailedModal.tableRowCollapse',
+ {
+ defaultMessage: 'Collapse {rowDescription}',
+ description: 'Collapse a row of a table with failures',
+ values: { rowDescription: failureSummeryText },
+ }
+ );
+
+ const expandLabel = i18n.translate(
+ 'common.ui.courier.fetch.shardsFailedModal.tableRowExpand',
+ {
+ defaultMessage: 'Expand {rowDescription}',
+ description: 'Expand a row of a table with failures',
+ values: { rowDescription: failureSummeryText },
+ }
+ );
+
+ return (
+ {
+ // toggle displaying the expanded view of the given list item
+ const map = Object.assign({}, expandMap);
+ if (map[item.id]) {
+ delete map[item.id];
+ } else {
+ map[item.id] = ;
+ }
+ setExpandMap(map);
+ }}
+ aria-label={expandMap[item.id] ? collapseLabel : expandLabel}
+ iconType={expandMap[item.id] ? 'arrowUp' : 'arrowDown'}
+ />
+ );
+ },
+ },
+ {
+ field: 'shard',
+ name: i18n.translate('common.ui.courier.fetch.shardsFailedModal.tableColShard', {
+ defaultMessage: 'Shard',
+ }),
+ sortable: true,
+ truncateText: true,
+ width: '80px',
+ },
+ {
+ field: 'index',
+ name: i18n.translate('common.ui.courier.fetch.shardsFailedModal.tableColIndex', {
+ defaultMessage: 'Index',
+ }),
+ sortable: true,
+ truncateText: true,
+ },
+ {
+ field: 'node',
+ name: i18n.translate('common.ui.courier.fetch.shardsFailedModal.tableColNode', {
+ defaultMessage: 'Node',
+ }),
+ sortable: true,
+ truncateText: true,
+ },
+ {
+ field: 'reason.type',
+ name: i18n.translate('common.ui.courier.fetch.shardsFailedModal.tableColReason', {
+ defaultMessage: 'Reason',
+ }),
+ truncateText: true,
+ },
+ ];
+
+ const sorting = {
+ sort: {
+ field: 'index',
+ direction: 'desc',
+ },
+ };
+
+ return (
+
+ );
+}
diff --git a/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts b/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts
new file mode 100644
index 0000000000000..de32b9d7b3087
--- /dev/null
+++ b/src/legacy/ui/public/courier/fetch/components/shard_failure_types.ts
@@ -0,0 +1,52 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export interface Request {
+ docvalue_fields: string[];
+ _source: unknown;
+ query: unknown;
+ script_fields: unknown;
+ sort: unknown;
+ stored_fields: string[];
+}
+export interface ResponseWithShardFailure {
+ _shards: {
+ failed: number;
+ failures: ShardFailure[];
+ skipped: number;
+ successful: number;
+ total: number;
+ };
+}
+
+export interface ShardFailure {
+ index: string;
+ node: string;
+ reason: {
+ caused_by: {
+ reason: string;
+ type: string;
+ };
+ reason: string;
+ lang?: string;
+ script?: string;
+ script_stack?: string[];
+ type: string;
+ };
+ shard: number;
+}