Add modal with details to shard error toast (#41649)

* Add details to shard error toast

* Implement failure modal with lots of details

* Add jest tests

* Update core API change because of the new modal Prop 'className'
This commit is contained in:
Matthias Wilhelm 2019-07-29 18:27:31 +02:00 committed by GitHub
parent 5bd37d0c02
commit e452f436e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1304 additions and 8 deletions

View file

@ -77,6 +77,7 @@ export interface OverlayStart {
openModal: (
modalChildren: React.ReactNode,
modalProps?: {
className?: string;
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}

View file

@ -545,6 +545,7 @@ export interface OverlayStart {
}) => OverlayRef;
// (undocumented)
openModal: (modalChildren: React.ReactNode, modalProps?: {
className?: string;
closeButtonAriaLabel?: string;
'data-test-subj'?: string;
}) => OverlayRef;

View file

@ -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';

View file

@ -0,0 +1 @@
@import './fetch/components/shard_failure_modal';

View file

@ -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}
<EuiSpacer size="s"/>
<ShardFailureOpenModalButton
request={searchRequest.fetchParams.body}
response={response}
title={title}
/>
</>
);
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)
);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -0,0 +1,192 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ShardFailureDescription renders matching snapshot given valid properties 1`] = `
<div>
<ShardFailureDescriptionHeader
index="repro2"
intl={
Object {
"defaultFormats": Object {},
"defaultLocale": "en",
"formatDate": [Function],
"formatHTMLMessage": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatPlural": [Function],
"formatRelative": [Function],
"formatTime": [Function],
"formats": Object {
"date": Object {
"full": Object {
"day": "numeric",
"month": "long",
"weekday": "long",
"year": "numeric",
},
"long": Object {
"day": "numeric",
"month": "long",
"year": "numeric",
},
"medium": Object {
"day": "numeric",
"month": "short",
"year": "numeric",
},
"short": Object {
"day": "numeric",
"month": "numeric",
"year": "2-digit",
},
},
"number": Object {
"currency": Object {
"style": "currency",
},
"percent": Object {
"style": "percent",
},
},
"relative": Object {
"days": Object {
"units": "day",
},
"hours": Object {
"units": "hour",
},
"minutes": Object {
"units": "minute",
},
"months": Object {
"units": "month",
},
"seconds": Object {
"units": "second",
},
"years": Object {
"units": "year",
},
},
"time": Object {
"full": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"long": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
"timeZoneName": "short",
},
"medium": Object {
"hour": "numeric",
"minute": "numeric",
"second": "numeric",
},
"short": Object {
"hour": "numeric",
"minute": "numeric",
},
},
},
"formatters": Object {
"getDateTimeFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralFormat": [Function],
"getRelativeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": Symbol(react.fragment),
"timeZone": null,
}
}
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}
/>
<EuiSpacer
size="m"
/>
<EuiDescriptionList
className="shardFailureModal__desc"
compressed={true}
descriptionProps={
Object {
"className": "shardFailureModal__descValue",
}
}
listItems={
Array [
Object {
"description": "script_exception",
"title": "Type",
},
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="column"
/>
</div>
`;

View file

@ -0,0 +1,194 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ShardFailureModal renders matching snapshot given valid properties 1`] = `
<Fragment>
<EuiModalHeader>
<EuiModalHeaderTitle>
test
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiTabbedContent
autoFocus="selected"
initialSelectedTab={
Object {
"content": <ShardFailureTable
failures={
Array [
Object {
"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,
},
]
}
/>,
"id": "table",
"name": "Shard failures",
}
}
tabs={
Array [
Object {
"content": <ShardFailureTable
failures={
Array [
Object {
"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,
},
]
}
/>,
"id": "table",
"name": "Shard failures",
},
Object {
"content": <EuiCodeBlock
isCopyable={true}
language="json"
>
{
"version": true,
"size": 500,
"sort": [],
"_source": {
"excludes": []
},
"stored_fields": [
"*"
],
"script_fields": {},
"docvalue_fields": [],
"query": {},
"highlight": {}
}
</EuiCodeBlock>,
"id": "json-request",
"name": "Request",
},
Object {
"content": <EuiCodeBlock
isCopyable={true}
language="json"
>
{
"_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"
}
}
}
]
}
}
</EuiCodeBlock>,
"id": "json-response",
"name": "Response",
},
]
}
/>
</EuiModalBody>
<EuiModalFooter>
<EuiCopy
afterMessage="Copied"
textToCopy="{
\\"_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\\"
}
}
}
]
}
}"
>
<Component />
</EuiCopy>
<EuiButton
color="primary"
data-test-sub="closeShardFailureModal"
fill={true}
iconSide="left"
onClick={[Function]}
size="m"
type="button"
>
<FormattedMessage
defaultMessage="Close"
description="Closing the Modal"
id="common.ui.courier.fetch.shardsFailedModal.close"
values={Object {}}
/>
</EuiButton>
</EuiModalFooter>
</Fragment>
`;

View file

@ -0,0 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ShardFailureTable renders matching snapshot given valid properties 1`] = `
<EuiInMemoryTable
columns={
Array [
Object {
"align": "right",
"isExpander": true,
"render": [Function],
"width": "40px",
},
Object {
"field": "shard",
"name": "Shard",
"sortable": true,
"truncateText": true,
"width": "80px",
},
Object {
"field": "index",
"name": "Index",
"sortable": true,
"truncateText": true,
},
Object {
"field": "node",
"name": "Node",
"sortable": true,
"truncateText": true,
},
Object {
"field": "reason.type",
"name": "Reason",
"truncateText": true,
},
]
}
executeQueryOptions={Object {}}
itemId="id"
itemIdToExpandedRowMap={Object {}}
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={true}
responsive={true}
sorting={
Object {
"sort": Object {
"direction": "desc",
"field": "index",
},
}
}
/>
`;

View file

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

View file

@ -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(<ShardFailureDescription {...failure} />);
expect(component).toMatchSnapshot();
});
});

View file

@ -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 (
<EuiCodeBlock language="java" paddingSize="s" isCopyable>
{valueScript}
</EuiCodeBlock>
);
} 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 (
<div>
<ShardFailureDescriptionHeader {...props} />
<EuiSpacer size="m" />
<EuiDescriptionList
listItems={listItems}
type="column"
compressed
className="shardFailureModal__desc"
titleProps={{ className: 'shardFailureModal__descTitle' }}
descriptionProps={{ className: 'shardFailureModal__descValue' }}
/>
</div>
);
}

View file

@ -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<keyof ShardFailure> = ['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 => (
<span className="shardFailureModal__keyValueTitle" key={kv.key}>
<EuiCode>{kv.key}</EuiCode> {kv.value}
</span>
));
return (
<EuiTitle size="xs">
<h2>
{getFailureSummaryText(props, '')}
{failureDetails}
</h2>
</EuiTitle>
);
}

View file

@ -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(
<ShardFailureModal
title="test"
request={shardFailureRequest}
response={shardFailureResponse}
onClose={jest.fn()}
/>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -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 (
<EuiCallOut title="Sorry, there was an error" color="danger" iconType="alert">
The ShardFailureModal component received invalid properties
</EuiCallOut>
);
}
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: <ShardFailureTable failures={failures} />,
},
{
id: 'json-request',
name: i18n.translate('common.ui.courier.fetch.shardsFailedModal.tabHeaderRequest', {
defaultMessage: 'Request',
description: 'Name of the tab displaying the JSON request',
}),
content: (
<EuiCodeBlock language="json" isCopyable>
{requestJSON}
</EuiCodeBlock>
),
},
{
id: 'json-response',
name: i18n.translate('common.ui.courier.fetch.shardsFailedModal.tabHeaderResponse', {
defaultMessage: 'Response',
description: 'Name of the tab displaying the JSON response',
}),
content: (
<EuiCodeBlock language="json" isCopyable>
{responseJSON}
</EuiCodeBlock>
),
},
];
return (
<React.Fragment>
<EuiModalHeader>
<EuiModalHeaderTitle>{title}</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} autoFocus="selected" />
</EuiModalBody>
<EuiModalFooter>
<EuiCopy textToCopy={responseJSON}>
{copy => (
<EuiButtonEmpty onClick={copy}>
<FormattedMessage
id="common.ui.courier.fetch.shardsFailedModal.copyToClipboard"
defaultMessage="Copy response to clipboard"
/>
</EuiButtonEmpty>
)}
</EuiCopy>
<EuiButton onClick={() => onClose()} fill data-test-sub="closeShardFailureModal">
<FormattedMessage
id="common.ui.courier.fetch.shardsFailedModal.close"
defaultMessage="Close"
description="Closing the Modal"
/>
</EuiButton>
</EuiModalFooter>
</React.Fragment>
);
}

View file

@ -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,
},
},
},
};
});

View file

@ -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(
<ShardFailureOpenModalButton
request={shardFailureRequest}
response={shardFailureResponse}
title="test"
/>
);
findTestSubject(component, 'openShardFailureModalBtn').simulate('click');
expect(openModal).toHaveBeenCalled();
});
});

View file

@ -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(
<ShardFailureModal
request={request}
response={response}
title={title}
onClose={() => modal.close()}
/>,
{
className: 'shardFailureModal',
}
);
}
return (
<EuiTextAlign textAlign="right">
<EuiButton
color="warning"
size="s"
onClick={onClick}
data-test-subj="openShardFailureModalBtn"
>
<FormattedMessage
id="common.ui.courier.fetch.shardsFailedModal.showDetails"
defaultMessage="Show details"
description="Open the modal to show details"
/>
</EuiButton>
</EuiTextAlign>
);
}

View file

@ -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(<ShardFailureTable failures={failures} />);
expect(component).toMatchSnapshot();
});
});

View file

@ -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<string, ReactElement>;
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 (
<EuiButtonIcon
onClick={() => {
// 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] = <ShardFailureDescription {...item} />;
}
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 (
<EuiInMemoryTable
itemId="id"
items={itemList}
columns={columns}
pagination={true}
sorting={sorting}
itemIdToExpandedRowMap={expandMap}
/>
);
}

View file

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