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; +}