[inspector] clusters tab MVP (#166025)

Closes https://github.com/elastic/kibana/issues/166021
Closes https://github.com/elastic/kibana/issues/163381

This PR adds inspector cluster tab MVP

This PR does not:
1) include all UI elements in design. These will be added at later
dates.
2) show clusters tab when request fails. Somewhere between kibana server
elasticsearch request and the client, the raw response is getting
removed for failed requests. This will have to be sorted out in a
separate PR.
3) Opening clusters tab from "incomplete data" warnings

### Test setup
1. Start remote elasticsearch by running: `yarn es snapshot -E
transport.port=9500 -E http.port=9201 -E path.data=../remote1`
2. Install sample data to remote cluster
1. Add `elasticsearch.hosts: ["http://localhost:9201"]` to
kibana.dev.yml. **Note** create `config/kibana.dev.yml` if one does not
exist. kibana.dev.yml is not managed by git so it has to be created the
first time you add values.
    2. run `yarn start` to start kibana process
    3. install sample web logs data set on home page
    4. install sample flight data set on home page
    5. stop kibana process
    6. remove `elasticsearch.hosts` from kibana.dev.yml
3. Start local elasticsearch by running: `yarn es snapshot -E
path.data=../local1`
4. Start kibana
5. Add remote cluster under "Stack management -> Remote clusters"
    1. Set **Name** to "remote1"
    2. Set **Seed nodes** to "localhost:9500" 
    3. Enable **Skip if unavailable**
5. install sample web logs data set
6. install sample flights data set
7. Create data view.
1. Set **Index pattern** to
`kibana_sample_data*,remote1:kibana_sample_data*`
    2. Set **Time field** to `timestamp`

### Local cluster (status=successful)
1) Open discover
2) Select "Kibana sample data logs" data view
3) Open inspector
4) Open clusters tab
<img width="300" alt="Screen Shot 2023-09-22 at 9 38 38 AM"
src="e4e91555-8200-43bc-b2fe-7739f7178e43">

### Remote cluster (status=successful)
1) Open discover
2) Select "kibana_sample_data*,remote1:kibana_sample_data*" data view
3) Open inspector
4) Open clusters tab
<img width="300" alt="Screen Shot 2023-09-22 at 9 47 08 AM"
src="676897fc-e7e2-4c0b-8e35-c382c6ac89d6">

### Remote cluster (status=partial, failed shard)
1) Open discover
2) Select "kibana_sample_data*,remote1:kibana_sample_data*" data view
3) Add filter
    ```
    {
      "error_query": {
        "indices": [
          {
            "error_type": "exception",
            "message": "local shard failure message 123",
            "name": "remote1:kibana_sample_data_logs",
            "shard_ids": [
              0
            ]
          }
        ]
      }
    }
    ```
3) Open inspector
4) Open clusters tab
<img width="300" alt="Screen Shot 2023-09-22 at 9 50 49 AM"
src="6935f2b4-60ad-4704-8ee0-17890ca9d83a">
<img width="300" alt="Screen Shot 2023-09-22 at 9 51 12 AM"
src="ec0a6b4a-177f-40fd-96b3-c56102d5f425">

### Remote cluster (status=skipped, all shards fail)
1) Open discover
2) Select "kibana_sample_data*,remote1:kibana_sample_data*" data view
3) Add filter
    ```
    {
      "error_query": {
        "indices": [
          {
            "error_type": "exception",
            "message": "local shard failure message 123",
            "name": "remote1:*",
            "shard_ids": [
              0
            ]
          }
        ]
      }
    }
    ```
3) Open inspector
4) Open clusters tab
<img width="300" alt="Screen Shot 2023-09-22 at 9 52 49 AM"
src="a1ba947b-3cd1-4416-9756-29f3960a4ba6">

### Remote cluster (status=skipped, no remote)
1) Open discover
2) Kill process running remote1 elasticsearch
3) Select "kibana_sample_data*,remote1:kibana_sample_data*" data view
4) Open inspector
5) Open clusters tab
<img width="300" alt="Screen Shot 2023-09-22 at 9 55 45 AM"
src="f049b617-96e1-4ecc-bfeb-f75522f70fef">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2023-09-26 20:32:26 -06:00 committed by GitHub
parent ed8225f7bc
commit 7b3a8f842f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1681 additions and 1 deletions

View file

@ -0,0 +1,115 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render should render local and remote cluster details from _clusters 1`] = `
<Fragment>
<EuiSpacer
size="m"
/>
<ClustersHealth
clusters={
Object {
"(local)": Object {
"_shards": Object {
"failed": 0,
"skipped": 0,
"successful": 2,
"total": 2,
},
"indices": "kibana_sample_data_logs,kibana_sample_data_flights",
"status": "successful",
"timed_out": false,
"took": 0,
},
"remote1": Object {
"_shards": Object {
"failed": 0,
"skipped": 0,
"successful": 2,
"total": 2,
},
"indices": "kibana_sample_data_logs,kibana_sample_data_flights",
"status": "successful",
"timed_out": false,
"took": 1,
},
}
}
/>
<ClustersTable
clusters={
Object {
"(local)": Object {
"_shards": Object {
"failed": 0,
"skipped": 0,
"successful": 2,
"total": 2,
},
"indices": "kibana_sample_data_logs,kibana_sample_data_flights",
"status": "successful",
"timed_out": false,
"took": 0,
},
"remote1": Object {
"_shards": Object {
"failed": 0,
"skipped": 0,
"successful": 2,
"total": 2,
},
"indices": "kibana_sample_data_logs,kibana_sample_data_flights",
"status": "successful",
"timed_out": false,
"took": 1,
},
}
}
/>
</Fragment>
`;
exports[`render should render local cluster details from _shards 1`] = `
<Fragment>
<EuiSpacer
size="m"
/>
<ClustersHealth
clusters={
Object {
"(local)": Object {
"_shards": Object {
"failed": 0,
"skipped": 0,
"successful": 2,
"total": 2,
},
"failures": undefined,
"indices": "",
"status": "successful",
"timed_out": undefined,
"took": undefined,
},
}
}
/>
<ClustersTable
clusters={
Object {
"(local)": Object {
"_shards": Object {
"failed": 0,
"skipped": 0,
"successful": 2,
"total": 2,
},
"failures": undefined,
"indices": "",
"status": "successful",
"timed_out": undefined,
"took": undefined,
},
}
}
/>
</Fragment>
`;

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiHealth, EuiText } from '@elastic/eui';
import { HEALTH_HEX_CODES } from './gradient';
interface Props {
count?: number;
status: string;
}
export function ClusterHealth({ count, status }: Props) {
if (typeof count === 'number' && count === 0) {
return null;
}
let color = 'subdued';
let statusLabel = status;
if (status === 'successful') {
color = HEALTH_HEX_CODES.successful;
statusLabel = i18n.translate('inspector.requests.clusters.successfulLabel', {
defaultMessage: 'successful',
});
} else if (status === 'partial') {
color = HEALTH_HEX_CODES.partial;
statusLabel = i18n.translate('inspector.requests.clusters.partialLabel', {
defaultMessage: 'partial',
});
} else if (status === 'skipped') {
color = HEALTH_HEX_CODES.skipped;
statusLabel = i18n.translate('inspector.requests.clusters.skippedLabel', {
defaultMessage: 'skipped',
});
} else if (status === 'failed') {
color = HEALTH_HEX_CODES.failed;
statusLabel = i18n.translate('inspector.requests.clusters.failedLabel', {
defaultMessage: 'failed',
});
}
const label = typeof count === 'number' ? `${count} ${statusLabel}` : statusLabel;
return (
<EuiHealth color={color}>
<EuiText size="xs" color="subdued">
{label}
</EuiText>
</EuiHealth>
);
}

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { i18n } from '@kbn/i18n';
import type { ClusterDetails } from '@kbn/es-types';
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { ClusterHealth } from './cluster_health';
import { getHeathBarLinearGradient } from './gradient';
interface Props {
clusters: Record<string, ClusterDetails>;
}
export function ClustersHealth({ clusters }: Props) {
let successful = 0;
let partial = 0;
let skipped = 0;
let failed = 0;
Object.values(clusters).forEach((clusterDetails) => {
if (clusterDetails.status === 'successful') {
successful++;
} else if (clusterDetails.status === 'partial') {
partial++;
} else if (clusterDetails.status === 'skipped') {
skipped++;
} else if (clusterDetails.status === 'failed') {
failed++;
}
});
return (
<>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued">
{i18n.translate('inspector.requests.clusters.totalClustersLabel', {
defaultMessage: '{total} {total, plural, one {cluster} other {clusters}}',
values: { total: Object.keys(clusters).length },
})}
</EuiText>
</EuiFlexItem>
{successful > 0 ? (
<EuiFlexItem grow={false}>
<ClusterHealth count={successful} status="successful" />
</EuiFlexItem>
) : null}
{partial > 0 ? (
<EuiFlexItem grow={false}>
<ClusterHealth count={partial} status="partial" />
</EuiFlexItem>
) : null}
{skipped > 0 ? (
<EuiFlexItem grow={false}>
<ClusterHealth count={skipped} status="skipped" />
</EuiFlexItem>
) : null}
{failed > 0 ? (
<EuiFlexItem grow={false}>
<ClusterHealth count={failed} status="failed" />
</EuiFlexItem>
) : null}
</EuiFlexGroup>
<div
css={css`
background: ${getHeathBarLinearGradient(successful, partial, skipped, failed)};
border-radius: ${euiThemeVars.euiBorderRadiusSmall};
height: ${euiThemeVars.euiSizeS};
margin-top: ${euiThemeVars.euiSizeXS};
margin-bottom: ${euiThemeVars.euiSizeS};
`}
/>
</>
);
}

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getHeathBarLinearGradient, HEALTH_HEX_CODES } from './gradient';
describe('getHeathBarLinearGradient', () => {
test('should return linear-gradient with percentages for each status', () => {
expect(getHeathBarLinearGradient(5, 1, 1, 2)).toBe(
`linear-gradient(to right, ${HEALTH_HEX_CODES.successful} 0% 56%, ${HEALTH_HEX_CODES.partial} 56% 67%, ${HEALTH_HEX_CODES.skipped} 67% 78%, ${HEALTH_HEX_CODES.failed} 78% 100%)`
);
});
test('should return linear-gradient with percentages for each status with count above zero', () => {
expect(getHeathBarLinearGradient(5, 0, 0, 2)).toBe(
`linear-gradient(to right, ${HEALTH_HEX_CODES.successful} 0% 71%, ${HEALTH_HEX_CODES.failed} 71% 100%)`
);
});
});

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { euiThemeVars } from '@kbn/ui-theme';
export const HEALTH_HEX_CODES = {
successful: euiThemeVars.euiColorSuccess,
partial: euiThemeVars.euiColorWarning,
skipped: '#DA8B45',
failed: euiThemeVars.euiColorDanger,
};
export function getHeathBarLinearGradient(
successful: number,
partial: number,
skipped: number,
failed: number
) {
const total = successful + partial + skipped + failed;
const stops: string[] = [];
let startPercent: number = 0;
function addStop(value: number, color: string) {
if (value <= 0) {
return;
}
const percent = Math.round((value / total) * 100);
const endPercent = startPercent + percent;
stops.push(`${color} ${startPercent}% ${endPercent}%`);
startPercent = endPercent;
}
addStop(successful, HEALTH_HEX_CODES.successful);
addStop(partial, HEALTH_HEX_CODES.partial);
addStop(skipped, HEALTH_HEX_CODES.skipped);
addStop(failed, HEALTH_HEX_CODES.failed);
const printedStops = stops
.map((stop, index) => {
return index === stops.length - 1 ? stop : stop + ', ';
})
.join('');
return `linear-gradient(to right, ${printedStops})`;
}

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { ClustersHealth } from './clusters_health';
export { ClusterHealth } from './cluster_health';

View file

@ -0,0 +1,165 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render partial should display callout when request timed out 1`] = `
<EuiText
size="xs"
style={
Object {
"width": "100%",
}
}
>
<EuiCallOut
color="warning"
iconType="warning"
size="s"
title="Request timed out before completion. Results may be incomplete or empty."
/>
<ShardsView
failures={Array []}
shardStats={
Object {
"failed": 0,
"skipped": 0,
"successful": 3,
"total": 3,
}
}
/>
</EuiText>
`;
exports[`render partial should show view shard failure button when there are shard failures 1`] = `
<EuiText
size="xs"
style={
Object {
"width": "100%",
}
}
>
<ShardsView
failures={
Array [
Object {
"index": "remote1:.ds-kibana_sample_data_logs-2023.08.21-000001",
"node": "NVzFRd6SS4qT9o0k2vIzlg",
"reason": Object {
"caused_by": Object {
"reason": "runtime_exception: [.ds-kibana_sample_data_logs-2023.08.21-000001][0] local shard failure message 123",
"type": "runtime_exception",
},
"index": "remote1:.ds-kibana_sample_data_logs-2023.08.21-000001",
"index_uuid": "z1sPO8E4TdWcijNgsL_BxQ",
"reason": "failed to create query: [.ds-kibana_sample_data_logs-2023.08.21-000001][0] local shard failure message 123",
"type": "query_shard_exception",
},
"shard": 0,
},
]
}
shardStats={
Object {
"failed": 1,
"skipped": 0,
"successful": 1,
"total": 2,
}
}
/>
</EuiText>
`;
exports[`render should display success 1`] = `
<EuiText
size="xs"
style={
Object {
"width": "100%",
}
}
>
<ShardsView
failures={Array []}
shardStats={
Object {
"failed": 0,
"skipped": 0,
"successful": 3,
"total": 3,
}
}
/>
</EuiText>
`;
exports[`render skipped or failed should display callout when cluster is unavailable 1`] = `
<EuiText
size="xs"
style={
Object {
"width": "100%",
}
}
>
<EuiCallOut
color="warning"
iconType="warning"
size="s"
title="Search failed"
>
<p>
no_such_remote_cluster_exception: "no such remote cluster: [remote1]"
</p>
</EuiCallOut>
<ShardsView
failures={Array []}
/>
</EuiText>
`;
exports[`render skipped or failed should display callout with view failed shards button when all shards fail 1`] = `
<EuiText
size="xs"
style={
Object {
"width": "100%",
}
}
>
<EuiCallOut
color="warning"
iconType="warning"
size="s"
title="Search failed"
>
<p>
search_phase_execution_exception: "all shards failed"
</p>
<OpenShardFailureFlyoutButton
failures={
Array [
Object {
"index": "remote1:.ds-kibana_sample_data_logs-2023.09.21-000001",
"node": "_JVoOnN5QKidGGXFJAlgpA",
"reason": Object {
"caused_by": Object {
"reason": "runtime_exception: [.ds-kibana_sample_data_logs-2023.09.21-000001][0] local shard failure message 123",
"type": "runtime_exception",
},
"index": "remote1:.ds-kibana_sample_data_logs-2023.09.21-000001",
"index_uuid": "PAa7v-dKRIyo4kv6b8dxkQ",
"reason": "failed to create query: [.ds-kibana_sample_data_logs-2023.09.21-000001][0] local shard failure message 123",
"type": "query_shard_exception",
},
"shard": 0,
},
]
}
/>
</EuiCallOut>
<ShardsView
failures={Array []}
/>
</EuiText>
`;

View file

@ -0,0 +1,162 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { shallow } from 'enzyme';
import type { ClusterDetails } from '@kbn/es-types';
import { ClusterView } from './cluster_view';
describe('render', () => {
test('should display success', () => {
const clusterDetails = {
status: 'successful',
indices: 'kibana_sample_data*',
took: 10005,
timed_out: false,
_shards: {
total: 3,
successful: 3,
skipped: 0,
failed: 0,
},
} as ClusterDetails;
const wrapper = shallow(<ClusterView clusterDetails={clusterDetails} />);
expect(wrapper).toMatchSnapshot();
});
describe('partial', () => {
test('should show view shard failure button when there are shard failures', () => {
const clusterDetails = {
status: 'partial',
indices: 'kibana_sample_data_logs,kibana_sample_data_flights',
took: 5,
timed_out: false,
_shards: {
total: 2,
successful: 1,
skipped: 0,
failed: 1,
},
failures: [
{
shard: 0,
index: 'remote1:.ds-kibana_sample_data_logs-2023.08.21-000001',
node: 'NVzFRd6SS4qT9o0k2vIzlg',
reason: {
type: 'query_shard_exception',
reason:
'failed to create query: [.ds-kibana_sample_data_logs-2023.08.21-000001][0] local shard failure message 123',
index_uuid: 'z1sPO8E4TdWcijNgsL_BxQ',
index: 'remote1:.ds-kibana_sample_data_logs-2023.08.21-000001',
caused_by: {
type: 'runtime_exception',
reason:
'runtime_exception: [.ds-kibana_sample_data_logs-2023.08.21-000001][0] local shard failure message 123',
},
},
},
],
} as ClusterDetails;
const wrapper = shallow(<ClusterView clusterDetails={clusterDetails} />);
expect(wrapper).toMatchSnapshot();
});
test('should display callout when request timed out', () => {
const clusterDetails = {
status: 'partial',
indices: 'kibana_sample_data*',
took: 10005,
timed_out: true,
_shards: {
total: 3,
successful: 3,
skipped: 0,
failed: 0,
},
} as ClusterDetails;
const wrapper = shallow(<ClusterView clusterDetails={clusterDetails} />);
expect(wrapper).toMatchSnapshot();
});
});
describe('skipped or failed', () => {
test('should display callout when cluster is unavailable', () => {
const clusterDetails = {
status: 'skipped',
indices: 'kibana_sample_data*',
timed_out: false,
failures: [
{
shard: -1,
index: null,
reason: {
type: 'no_such_remote_cluster_exception',
reason: 'no such remote cluster: [remote1]',
},
},
],
} as unknown as ClusterDetails;
const wrapper = shallow(<ClusterView clusterDetails={clusterDetails} />);
expect(wrapper).toMatchSnapshot();
});
test('should display callout with view failed shards button when all shards fail', () => {
const clusterDetails = {
status: 'skipped',
indices: 'kibana_sample_data*',
timed_out: false,
failures: [
{
shard: -1,
index: null,
reason: {
type: 'search_phase_execution_exception',
reason: 'all shards failed',
phase: 'query',
grouped: true,
failed_shards: [
{
shard: 0,
index: 'remote1:.ds-kibana_sample_data_logs-2023.09.21-000001',
node: '_JVoOnN5QKidGGXFJAlgpA',
reason: {
type: 'query_shard_exception',
reason:
'failed to create query: [.ds-kibana_sample_data_logs-2023.09.21-000001][0] local shard failure message 123',
index_uuid: 'PAa7v-dKRIyo4kv6b8dxkQ',
index: 'remote1:.ds-kibana_sample_data_logs-2023.09.21-000001',
caused_by: {
type: 'runtime_exception',
reason:
'runtime_exception: [.ds-kibana_sample_data_logs-2023.09.21-000001][0] local shard failure message 123',
},
},
},
],
caused_by: {
type: 'query_shard_exception',
reason:
'failed to create query: [.ds-kibana_sample_data_logs-2023.09.21-000001][0] local shard failure message 123',
index_uuid: 'PAa7v-dKRIyo4kv6b8dxkQ',
index: 'remote1:.ds-kibana_sample_data_logs-2023.09.21-000001',
caused_by: {
type: 'runtime_exception',
reason:
'runtime_exception: [.ds-kibana_sample_data_logs-2023.09.21-000001][0] local shard failure message 123',
},
},
},
},
],
} as unknown as ClusterDetails;
const wrapper = shallow(<ClusterView clusterDetails={clusterDetails} />);
expect(wrapper).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import type { ClusterDetails } from '@kbn/es-types';
import { EuiCallOut, EuiText } from '@elastic/eui';
import { ShardsView } from './shards_view';
import { OpenShardFailureFlyoutButton } from './shards_view';
interface Props {
clusterDetails: ClusterDetails;
}
export function ClusterView({ clusterDetails }: Props) {
const clusterFailure = (clusterDetails.failures ?? []).find((failure) => {
return failure.shard < 0;
});
const shardFailures = (clusterDetails.failures ?? []).filter((failure) => {
return failure.shard >= 0;
});
return (
<EuiText style={{ width: '100%' }} size="xs">
{clusterDetails.timed_out ? (
<EuiCallOut
size="s"
color="warning"
title={i18n.translate('inspector.requests.clusters.timedOutMessage', {
defaultMessage:
'Request timed out before completion. Results may be incomplete or empty.',
})}
iconType="warning"
/>
) : null}
{clusterFailure ? (
<EuiCallOut
size="s"
color="warning"
title={i18n.translate('inspector.requests.clusters.failedClusterMessage', {
defaultMessage: 'Search failed',
})}
iconType="warning"
>
<p>
{clusterFailure.reason.reason
? `${clusterFailure.reason.type}: "${clusterFailure.reason.reason}"`
: clusterFailure.reason.type}
</p>
{clusterFailure.reason.failed_shards ? (
<OpenShardFailureFlyoutButton failures={clusterFailure.reason.failed_shards} />
) : null}
</EuiCallOut>
) : null}
<ShardsView failures={shardFailures} shardStats={clusterDetails._shards} />
</EuiText>
);
}

View file

@ -0,0 +1,126 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState, ReactNode } from 'react';
import type { ClusterDetails } from '@kbn/es-types';
import { i18n } from '@kbn/i18n';
import { EuiBasicTable, type EuiBasicTableColumn, EuiButtonIcon, EuiText } from '@elastic/eui';
import { ClusterView } from './cluster_view';
import { ClusterHealth } from '../clusters_health';
import { LOCAL_CLUSTER_KEY } from '../local_cluster';
function getInitialExpandedRow(clusters: Record<string, ClusterDetails>) {
const clusterNames = Object.keys(clusters);
return clusterNames.length === 1
? { [clusterNames[0]]: <ClusterView clusterDetails={clusters[clusterNames[0]]} /> }
: {};
}
interface ClusterColumn {
name: string;
status: string;
responseTime?: number;
}
interface Props {
clusters: Record<string, ClusterDetails>;
}
export function ClustersTable({ clusters }: Props) {
const [expandedRows, setExpandedRows] = useState<Record<string, ReactNode>>(
getInitialExpandedRow(clusters)
);
const toggleDetails = (name: string) => {
const nextExpandedRows = { ...expandedRows };
if (name in nextExpandedRows) {
delete nextExpandedRows[name];
} else {
nextExpandedRows[name] = <ClusterView clusterDetails={clusters[name]} />;
}
setExpandedRows(nextExpandedRows);
};
const columns: Array<EuiBasicTableColumn<ClusterColumn>> = [
{
field: 'name',
name: i18n.translate('inspector.requests.clusters.table.nameLabel', {
defaultMessage: 'Name',
}),
render: (name: string) => {
return (
<>
<EuiButtonIcon
onClick={() => toggleDetails(name)}
aria-label={
name in expandedRows
? i18n.translate('inspector.requests.clusters.table.collapseRow', {
defaultMessage: 'Collapse table row to hide cluster details',
})
: i18n.translate('inspector.requests.clusters.table.expandRow', {
defaultMessage: 'Expand table row to view cluster details',
})
}
iconType={name in expandedRows ? 'arrowDown' : 'arrowRight'}
/>
<EuiText size="xs" color="subdued">
{name === LOCAL_CLUSTER_KEY
? i18n.translate('inspector.requests.clusters.table.localClusterDisplayName', {
defaultMessage: 'Local cluster',
})
: name}
</EuiText>
</>
);
},
width: '60%',
},
{
field: 'status',
name: i18n.translate('inspector.requests.clusters.table.statusLabel', {
defaultMessage: 'Status',
}),
render: (status: string) => {
return <ClusterHealth status={status} />;
},
},
{
align: 'right' as 'right',
field: 'responseTime',
name: i18n.translate('inspector.requests.clusters.table.responseTimeLabel', {
defaultMessage: 'Response time',
}),
render: (responseTime: number | undefined) => (
<EuiText size="xs" color="subdued">
{responseTime
? i18n.translate('inspector.requests.clusters.table.responseTimeInMilliseconds', {
defaultMessage: '{responseTime}ms',
values: { responseTime },
})
: null}
</EuiText>
),
},
];
return (
<EuiBasicTable
items={Object.keys(clusters).map((key) => {
return {
name: key,
status: clusters[key].status,
responseTime: clusters[key].took,
};
})}
isExpandable={true}
itemIdToExpandedRowMap={expandedRows}
itemId="name"
columns={columns}
/>
);
}

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { ClustersTable } from './clusters_table';

View file

@ -0,0 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`render should not render when no shard details are provided 1`] = `""`;
exports[`render should render with failures 1`] = `
<Fragment>
<EuiFlexGroup
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiTitle
size="xxxs"
>
<h4>
Shards
</h4>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<OpenShardFailureFlyoutButton
failures={
Array [
Object {},
]
}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
2 total shards
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
1 of 2 successful
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
`;
exports[`render should render with no failures 1`] = `
<Fragment>
<EuiFlexGroup
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
<EuiTitle
size="xxxs"
>
<h4>
Shards
</h4>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<OpenShardFailureFlyoutButton
failures={Array []}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup
justifyContent="spaceBetween"
>
<EuiFlexItem
grow={false}
>
2 total shards
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
2 of 2 successful
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
`;

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { ShardsView } from './shards_view';
export { OpenShardFailureFlyoutButton } from './open_shard_failure_flyout_button';

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState } from 'react';
import { estypes } from '@elastic/elasticsearch';
import { i18n } from '@kbn/i18n';
import { EuiButtonEmpty } from '@elastic/eui';
import { ShardFailureFlyout } from './shard_failure_flyout';
interface Props {
failures: estypes.ShardFailure[];
}
export function OpenShardFailureFlyoutButton({ failures }: Props) {
const [showFailures, setShowFailures] = useState(false);
return (
<>
{failures.length ? (
<EuiButtonEmpty
flush="both"
onClick={() => {
setShowFailures(!showFailures);
}}
size="xs"
>
{i18n.translate('inspector.requests.clusters.shards.openShardFailureFlyoutButtonLabel', {
defaultMessage:
'View {failedShardCount} failed {failedShardCount, plural, one {shard} other {shards}}',
values: { failedShardCount: failures.length },
})}
</EuiButtonEmpty>
) : null}
{showFailures ? (
<ShardFailureFlyout
failures={failures}
onClose={() => {
setShowFailures(false);
}}
/>
) : null}
</>
);
}

View file

@ -0,0 +1,77 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { estypes } from '@elastic/elasticsearch';
import { i18n } from '@kbn/i18n';
import { EuiDescriptionList, EuiCodeBlock, EuiText } from '@elastic/eui';
import { getFlattenedObject } from '@kbn/std';
/**
* 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): 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>
);
}
return <EuiText size="xs">{String(value)}</EuiText>;
}
interface Props {
failure: estypes.ShardFailure;
}
export function ShardFailureDetails({ failure }: Props) {
const flattendReason = getFlattenedObject(failure.reason);
const reasonItems = Object.entries(flattendReason)
.filter(([key]) => key !== 'type')
.map(([key, value]) => ({
title: formatKey(key),
description: formatValueByKey(value, key),
}));
const items = [
{
title: i18n.translate('inspector.requests.clusters.shards.details.nodeLabel', {
defaultMessage: 'Node',
}),
description: formatValueByKey(failure.node, 'node'),
},
...reasonItems,
];
return (
<EuiText size="xs">
<EuiDescriptionList
type="responsiveColumn"
columnWidths={['30%', '70%']}
listItems={items}
compressed
/>
</EuiText>
);
}

View file

@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { estypes } from '@elastic/elasticsearch';
import { i18n } from '@kbn/i18n';
import {
EuiButtonIcon,
EuiButtonEmpty,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiTitle,
} from '@elastic/eui';
import { ShardFailureTable } from './shard_failure_table';
interface Props {
failures: estypes.ShardFailure[];
onClose: () => void;
}
export function ShardFailureFlyout({ failures, onClose }: Props) {
return (
<EuiFlyout onClose={onClose} ownFocus={false} hideCloseButton={true}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h1>
<EuiButtonIcon iconType="sortLeft" onClick={onClose} />
{i18n.translate('inspector.requests.clusters.shards.flyoutTitle', {
defaultMessage:
'{failedShardCount} failured {failedShardCount, plural, one {shard} other {shards}}',
values: { failedShardCount: failures.length },
})}
</h1>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<ShardFailureTable failures={failures} />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiButtonEmpty iconType="sortLeft" onClick={onClose} flush="left">
{i18n.translate('inspector.requests.clusters.shards.backButtonLabel', {
defaultMessage: 'Back',
})}
</EuiButtonEmpty>
</EuiFlyoutFooter>
</EuiFlyout>
);
}

View file

@ -0,0 +1,117 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState, ReactNode } from 'react';
import { estypes } from '@elastic/elasticsearch';
import { i18n } from '@kbn/i18n';
import { EuiBasicTable, type EuiBasicTableColumn, EuiButtonIcon, EuiText } from '@elastic/eui';
import { ShardFailureDetails } from './shard_failure_details';
function getRowId(failure: estypes.ShardFailure) {
return `${failure.shard}${failure.index}`;
}
interface ShardRow {
rowId: string;
shard: number;
index?: string;
failureType: string;
}
interface Props {
failures: estypes.ShardFailure[];
}
export function ShardFailureTable({ failures }: Props) {
const [expandedRows, setExpandedRows] = useState<Record<string, ReactNode>>({});
const toggleDetails = (rowId: string) => {
const nextExpandedRows = { ...expandedRows };
if (rowId in nextExpandedRows) {
delete nextExpandedRows[rowId];
} else {
const shardFailure = failures.find((failure) => rowId === getRowId(failure));
nextExpandedRows[rowId] = shardFailure ? (
<ShardFailureDetails failure={shardFailure} />
) : null;
}
setExpandedRows(nextExpandedRows);
};
const columns: Array<EuiBasicTableColumn<ShardRow>> = [
{
field: 'shard',
name: i18n.translate('inspector.requests.clusters.shards.table.shardLabel', {
defaultMessage: 'Shard',
}),
render: (shard: number, item: ShardRow) => {
return (
<>
<EuiButtonIcon
onClick={() => toggleDetails(item.rowId)}
aria-label={
item.rowId in expandedRows
? i18n.translate('inspector.requests.clusters.shards.table.collapseRow', {
defaultMessage: 'Collapse table row to hide shard details',
})
: i18n.translate('inspector.requests.clusters.shards.table.expandRow', {
defaultMessage: 'Expand table row to view shard details',
})
}
iconType={item.rowId in expandedRows ? 'arrowDown' : 'arrowRight'}
/>
<EuiText size="xs" color="subdued">
{shard}
</EuiText>
</>
);
},
width: '20%',
},
{
field: 'index',
name: i18n.translate('inspector.requests.clusters.shards.table.indexLabel', {
defaultMessage: 'Index',
}),
render: (index?: string) =>
index ? (
<EuiText size="xs" color="subdued">
{index}
</EuiText>
) : null,
},
{
field: 'failureType',
name: i18n.translate('inspector.requests.clusters.shards.table.failureTypeLabel', {
defaultMessage: 'Failure type',
}),
render: (failureType: string) => (
<EuiText size="xs" color="subdued">
{failureType}
</EuiText>
),
},
];
return (
<EuiBasicTable
items={failures.map((failure) => {
return {
rowId: getRowId(failure),
shard: failure.shard,
index: failure.index,
failureType: failure.reason.type,
};
})}
isExpandable={true}
itemIdToExpandedRowMap={expandedRows}
itemId="rowId"
columns={columns}
/>
);
}

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { estypes } from '@elastic/elasticsearch';
import { shallow } from 'enzyme';
import { ShardsView } from './shards_view';
describe('render', () => {
test('should render with no failures', () => {
const shardStats = {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
};
const wrapper = shallow(<ShardsView failures={[]} shardStats={shardStats} />);
expect(wrapper).toMatchSnapshot();
});
test('should render with failures', () => {
const shardStats = {
total: 2,
successful: 1,
skipped: 0,
failed: 1,
};
const wrapper = shallow(
<ShardsView failures={[{} as unknown as estypes.ShardFailure]} shardStats={shardStats} />
);
expect(wrapper).toMatchSnapshot();
});
test('should not render when no shard details are provided', () => {
const wrapper = shallow(<ShardsView failures={[]} />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { estypes } from '@elastic/elasticsearch';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { OpenShardFailureFlyoutButton } from './open_shard_failure_flyout_button';
interface Props {
failures: estypes.ShardFailure[];
shardStats?: estypes.ShardStatistics;
}
export function ShardsView({ failures, shardStats }: Props) {
return !shardStats && failures.length === 0 ? null : (
<>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiTitle size="xxxs">
<h4>
{i18n.translate('inspector.requests.clusters.shards.shardsTitle', {
defaultMessage: 'Shards',
})}
</h4>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<OpenShardFailureFlyoutButton failures={failures} />
</EuiFlexItem>
</EuiFlexGroup>
{shardStats ? (
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
{i18n.translate('inspector.requests.clusters.shards.totalShardsLabel', {
defaultMessage: '{total} total shards',
values: { total: shardStats.total },
})}
</EuiFlexItem>
<EuiFlexItem grow={false}>
{i18n.translate('inspector.requests.clusters.shards.successfulShardsLabel', {
defaultMessage: '{successful} of {total} successful',
values: {
successful: shardStats.successful,
total: shardStats.total,
},
})}
</EuiFlexItem>
</EuiFlexGroup>
) : null}
</>
);
}

View file

@ -0,0 +1,116 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { ClustersView } from './clusters_view';
import { Request } from '../../../../../../common/adapters/request/types';
describe('shouldShow', () => {
test('should return true when response contains _shards', () => {
const request = {
response: {
json: {
rawResponse: {
_shards: {},
},
},
},
} as unknown as Request;
expect(ClustersView.shouldShow(request)).toBe(true);
});
test('should return true when response contains _clusters', () => {
const request = {
response: {
json: {
rawResponse: {
_clusters: {},
},
},
},
} as unknown as Request;
expect(ClustersView.shouldShow(request)).toBe(true);
});
test('should return false when response does not contains _shards or _clusters', () => {
const request = {
response: {
json: {
rawResponse: {},
},
},
} as unknown as Request;
expect(ClustersView.shouldShow(request)).toBe(false);
});
});
describe('render', () => {
test('should render local cluster details from _shards', () => {
const request = {
response: {
json: {
rawResponse: {
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
},
},
},
} as unknown as Request;
const wrapper = shallow(<ClustersView request={request} />);
expect(wrapper).toMatchSnapshot();
});
test('should render local and remote cluster details from _clusters', () => {
const request = {
response: {
json: {
rawResponse: {
_clusters: {
total: 2,
successful: 2,
skipped: 0,
details: {
'(local)': {
status: 'successful',
indices: 'kibana_sample_data_logs,kibana_sample_data_flights',
took: 0,
timed_out: false,
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
},
remote1: {
status: 'successful',
indices: 'kibana_sample_data_logs,kibana_sample_data_flights',
took: 1,
timed_out: false,
_shards: {
total: 2,
successful: 2,
skipped: 0,
failed: 0,
},
},
},
},
},
},
},
} as unknown as Request;
const wrapper = shallow(<ClustersView request={request} />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { Component } from 'react';
import { estypes } from '@elastic/elasticsearch';
import { EuiSpacer } from '@elastic/eui';
import type { ClusterDetails } from '@kbn/es-types';
import { Request } from '../../../../../../common/adapters/request/types';
import type { RequestDetailsProps } from '../../types';
import { getLocalClusterDetails, LOCAL_CLUSTER_KEY } from './local_cluster';
import { ClustersHealth } from './clusters_health';
import { ClustersTable } from './clusters_table';
export class ClustersView extends Component<RequestDetailsProps> {
static shouldShow = (request: Request) =>
Boolean(
(request.response?.json as { rawResponse?: estypes.SearchResponse })?.rawResponse?._shards ||
(request.response?.json as { rawResponse?: estypes.SearchResponse })?.rawResponse?._clusters
);
render() {
const rawResponse = (
this.props.request.response?.json as { rawResponse?: estypes.SearchResponse }
)?.rawResponse;
if (!rawResponse) {
return null;
}
const clusters = rawResponse._clusters
? (
rawResponse._clusters as estypes.ClusterStatistics & {
details: Record<string, ClusterDetails>;
}
).details
: {
[LOCAL_CLUSTER_KEY]: getLocalClusterDetails(rawResponse),
};
return this.props.request.response?.json ? (
<>
<EuiSpacer size="m" />
<ClustersHealth clusters={clusters} />
<ClustersTable clusters={clusters} />
</>
) : null;
}
}

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { ClustersView } from './clusters_view';

View file

@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { estypes } from '@elastic/elasticsearch';
import type { ClusterDetails } from '@kbn/es-types';
export const LOCAL_CLUSTER_KEY = '(local)';
function getLocalClusterStatus(rawResponse: estypes.SearchResponse): ClusterDetails['status'] {
if (rawResponse._shards?.successful === 0) {
return 'failed';
}
if (rawResponse.timed_out || rawResponse._shards.failed) {
return 'partial';
}
return 'successful';
}
export function getLocalClusterDetails(rawResponse: estypes.SearchResponse) {
const shards = {
...rawResponse._shards,
};
delete shards.failures;
return {
status: getLocalClusterStatus(rawResponse),
indices: '',
took: rawResponse.took,
timed_out: rawResponse.timed_out,
_shards: shards,
failures: rawResponse._shards.failures,
};
}

View file

@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { estypes } from '@elastic/elasticsearch';
import { getLocalClusterDetails } from './local_cluster';
describe('getLocalClusterDetails', () => {
test('should convert local cluster SearchResponseBody into ClusterDetails', () => {
expect(
getLocalClusterDetails({
took: 14,
timed_out: false,
_shards: {
total: 2,
successful: 1,
skipped: 0,
failed: 1,
failures: [
{
shard: 0,
index: '.ds-kibana_sample_data_logs-2023.09.20-000001',
node: 'tGUEVPsHR4uhEAdL0oANsA',
reason: {
type: 'query_shard_exception',
reason:
'failed to create query: [.ds-kibana_sample_data_logs-2023.09.20-000001][0] local shard failure message 123',
index_uuid: 'z31al9BiSk2prpzZED-hTA',
index: '.ds-kibana_sample_data_logs-2023.09.20-000001',
caused_by: {
type: 'runtime_exception',
reason:
'[.ds-kibana_sample_data_logs-2023.09.20-000001][0] local shard failure message 123',
},
},
},
],
},
} as unknown as estypes.SearchResponse)
).toEqual({
_shards: {
failed: 1,
skipped: 0,
successful: 1,
total: 2,
},
failures: [
{
index: '.ds-kibana_sample_data_logs-2023.09.20-000001',
node: 'tGUEVPsHR4uhEAdL0oANsA',
reason: {
caused_by: {
reason:
'[.ds-kibana_sample_data_logs-2023.09.20-000001][0] local shard failure message 123',
type: 'runtime_exception',
},
index: '.ds-kibana_sample_data_logs-2023.09.20-000001',
index_uuid: 'z31al9BiSk2prpzZED-hTA',
reason:
'failed to create query: [.ds-kibana_sample_data_logs-2023.09.20-000001][0] local shard failure message 123',
type: 'query_shard_exception',
},
shard: 0,
},
],
indices: '',
status: 'partial',
timed_out: false,
took: 14,
});
});
});

View file

@ -9,3 +9,4 @@
export { RequestDetailsRequest } from './req_details_request';
export { RequestDetailsResponse } from './req_details_response';
export { RequestDetailsStats } from './req_details_stats';
export { ClustersView } from './clusters_view';

View file

@ -11,7 +11,12 @@ import PropTypes from 'prop-types';
import { i18n } from '@kbn/i18n';
import { EuiTab, EuiTabs } from '@elastic/eui';
import { RequestDetailsRequest, RequestDetailsResponse, RequestDetailsStats } from './details';
import {
ClustersView,
RequestDetailsRequest,
RequestDetailsResponse,
RequestDetailsStats,
} from './details';
import { RequestDetailsProps } from './types';
interface RequestDetailsState {
@ -33,6 +38,13 @@ const DETAILS: DetailViewData[] = [
}),
component: RequestDetailsStats,
},
{
name: 'clusters',
label: i18n.translate('inspector.requests.clustersTabLabel', {
defaultMessage: 'Clusters',
}),
component: ClustersView,
},
{
name: 'Request',
label: i18n.translate('inspector.requests.requestTabLabel', {

View file

@ -14,6 +14,9 @@
"@kbn/monaco",
"@kbn/core-ui-settings-browser-mocks",
"@kbn/core-ui-settings-browser",
"@kbn/std",
"@kbn/es-types",
"@kbn/ui-theme"
],
"exclude": [
"target/**/*",