diff --git a/src/plugins/inspector/public/views/requests/components/details/clusters_view/__snapshots__/clusters_view.test.tsx.snap b/src/plugins/inspector/public/views/requests/components/details/clusters_view/__snapshots__/clusters_view.test.tsx.snap
deleted file mode 100644
index 0186e1c713430..0000000000000
--- a/src/plugins/inspector/public/views/requests/components/details/clusters_view/__snapshots__/clusters_view.test.tsx.snap
+++ /dev/null
@@ -1,96 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render should render local and remote cluster details from _clusters 1`] = `
-
-
-
-
-
-`;
-
-exports[`render should render local cluster details from _shards 1`] = `
-
-
-
-
-`;
diff --git a/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_health/cluster_health.tsx b/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_health/cluster_health.tsx
index 4e4e57f5284c7..af066c3fac713 100644
--- a/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_health/cluster_health.tsx
+++ b/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_health/cluster_health.tsx
@@ -8,15 +8,21 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiHealth, EuiText } from '@elastic/eui';
+import { EuiHealth, EuiText, EuiTextProps } from '@elastic/eui';
import { HEALTH_HEX_CODES } from './gradient';
interface Props {
count?: number;
status: string;
+ textProps?: EuiTextProps;
}
-export function ClusterHealth({ count, status }: Props) {
+const defaultTextProps: EuiTextProps = {
+ size: 'xs',
+ color: 'subdued',
+};
+
+export function ClusterHealth({ count, status, textProps = defaultTextProps }: Props) {
if (typeof count === 'number' && count === 0) {
return null;
}
@@ -48,9 +54,7 @@ export function ClusterHealth({ count, status }: Props) {
const label = typeof count === 'number' ? `${count} ${statusLabel}` : statusLabel;
return (
-
- {label}
-
+ {label}
);
}
diff --git a/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_table/clusters_table.test.tsx b/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_table/clusters_table.test.tsx
new file mode 100644
index 0000000000000..19c269886041c
--- /dev/null
+++ b/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_table/clusters_table.test.tsx
@@ -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 type { ClusterDetails } from '@kbn/es-types';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { ClustersTable } from './clusters_table';
+
+describe('ClustersTable', () => {
+ describe('sorting', () => {
+ const clusters = {
+ remote1: {
+ status: 'successful',
+ took: 50,
+ } as unknown as ClusterDetails,
+ remote2: {
+ status: 'skipped',
+ took: 1000,
+ } as unknown as ClusterDetails,
+ remote3: {
+ status: 'failed',
+ took: 90,
+ } as unknown as ClusterDetails,
+ };
+
+ test('should render rows in native order', () => {
+ render();
+ const tableRows = screen.getAllByRole('row');
+ expect(tableRows.length).toBe(4); // 1 header row, 3 data rows
+ expect(tableRows[1]).toHaveTextContent('Nameremote1StatussuccessfulResponse time50ms');
+ expect(tableRows[2]).toHaveTextContent('Nameremote2StatusskippedResponse time1000ms');
+ expect(tableRows[3]).toHaveTextContent('Nameremote3StatusfailedResponse time90ms');
+ });
+
+ test('should sort by response time', () => {
+ render();
+ const button = screen.getByRole('button', {
+ name: 'Response time',
+ });
+ fireEvent.click(button);
+ const tableRowsAsc = screen.getAllByRole('row');
+ expect(tableRowsAsc.length).toBe(4); // 1 header row, 3 data rows
+ expect(tableRowsAsc[1]).toHaveTextContent('Nameremote1StatussuccessfulResponse time50ms');
+ expect(tableRowsAsc[2]).toHaveTextContent('Nameremote3StatusfailedResponse time90ms');
+ expect(tableRowsAsc[3]).toHaveTextContent('Nameremote2StatusskippedResponse time1000ms');
+
+ fireEvent.click(button);
+ const tableRowsDesc = screen.getAllByRole('row');
+ expect(tableRowsDesc.length).toBe(4); // 1 header row, 3 data rows
+ expect(tableRowsDesc[1]).toHaveTextContent('Nameremote2StatusskippedResponse time1000ms');
+ expect(tableRowsDesc[2]).toHaveTextContent('Nameremote3StatusfailedResponse time90ms');
+ expect(tableRowsDesc[3]).toHaveTextContent('Nameremote1StatussuccessfulResponse time50ms');
+ });
+ });
+});
diff --git a/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_table/clusters_table.tsx b/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_table/clusters_table.tsx
index c9fea1a470f49..03de4a0b999f4 100644
--- a/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_table/clusters_table.tsx
+++ b/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_table/clusters_table.tsx
@@ -6,10 +6,17 @@
* Side Public License, v 1.
*/
-import React, { useState, ReactNode } from 'react';
+import React, { useMemo, 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 {
+ Comparators,
+ EuiBasicTable,
+ type EuiBasicTableColumn,
+ EuiButtonIcon,
+ EuiText,
+ Criteria,
+} from '@elastic/eui';
import { ClusterView } from './cluster_view';
import { ClusterHealth } from '../clusters_health';
import { LOCAL_CLUSTER_KEY } from '../local_cluster';
@@ -21,7 +28,7 @@ function getInitialExpandedRow(clusters: Record) {
: {};
}
-interface ClusterColumn {
+interface ClusterItem {
name: string;
status: string;
responseTime?: number;
@@ -35,6 +42,8 @@ export function ClustersTable({ clusters }: Props) {
const [expandedRows, setExpandedRows] = useState>(
getInitialExpandedRow(clusters)
);
+ const [sortField, setSortField] = useState();
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const toggleDetails = (name: string) => {
const nextExpandedRows = { ...expandedRows };
@@ -46,7 +55,17 @@ export function ClustersTable({ clusters }: Props) {
setExpandedRows(nextExpandedRows);
};
- const columns: Array> = [
+ const items = useMemo(() => {
+ return Object.keys(clusters).map((key) => {
+ return {
+ name: key,
+ status: clusters[key].status,
+ responseTime: clusters[key].took,
+ };
+ });
+ }, [clusters]);
+
+ const columns: Array> = [
{
field: 'name',
name: i18n.translate('inspector.requests.clusters.table.nameLabel', {
@@ -78,6 +97,7 @@ export function ClustersTable({ clusters }: Props) {
>
);
},
+ sortable: items.length > 1,
width: '60%',
},
{
@@ -88,6 +108,7 @@ export function ClustersTable({ clusters }: Props) {
render: (status: string) => {
return ;
},
+ sortable: items.length > 1,
},
{
align: 'right' as 'right',
@@ -105,22 +126,38 @@ export function ClustersTable({ clusters }: Props) {
: null}
),
+ sortable: items.length > 1,
},
];
return (
{
- return {
- name: key,
- status: clusters[key].status,
- responseTime: clusters[key].took,
- };
- })}
+ items={
+ sortField
+ ? items.sort(Comparators.property(sortField, Comparators.default(sortDirection)))
+ : items
+ }
isExpandable={true}
itemIdToExpandedRowMap={expandedRows}
itemId="name"
columns={columns}
+ sorting={{
+ sort: sortField
+ ? {
+ field: sortField,
+ direction: sortDirection,
+ }
+ : undefined,
+ }}
+ onChange={({ sort }: Criteria) => {
+ if (sort) {
+ setSortField(sort.field);
+ setSortDirection(sort.direction);
+ }
+ }}
+ noItemsMessage={i18n.translate('inspector.requests.clusters.table.noItemsFound', {
+ defaultMessage: 'No clusters found',
+ })}
/>
);
}
diff --git a/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_view.test.tsx b/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_view.test.tsx
index 971c3bad5bef8..7fd1edf1ecc0b 100644
--- a/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_view.test.tsx
+++ b/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_view.test.tsx
@@ -7,7 +7,7 @@
*/
import React from 'react';
-import { shallow } from 'enzyme';
+import { render, screen, fireEvent } from '@testing-library/react';
import { ClustersView } from './clusters_view';
import { Request } from '../../../../../../common/adapters/request/types';
@@ -51,26 +51,33 @@ describe('shouldShow', () => {
});
describe('render', () => {
- test('should render local cluster details from _shards', () => {
- const request = {
- response: {
- json: {
- rawResponse: {
- _shards: {
- total: 2,
- successful: 2,
- skipped: 0,
- failed: 0,
+ describe('single cluster', () => {
+ test('should display table and not display search bar and health bar', () => {
+ const request = {
+ response: {
+ json: {
+ rawResponse: {
+ _shards: {
+ total: 2,
+ successful: 2,
+ skipped: 0,
+ failed: 0,
+ },
},
},
},
- },
- } as unknown as Request;
- const wrapper = shallow();
- expect(wrapper).toMatchSnapshot();
+ } as unknown as Request;
+ render();
+ const table = screen.getByRole('table');
+ expect(table).not.toBeNull();
+ const searchbar = screen.queryByRole('searchbox');
+ expect(searchbar).toBeNull();
+ const healthbar = screen.queryByText('2 clusters');
+ expect(healthbar).toBeNull();
+ });
});
- test('should render local and remote cluster details from _clusters', () => {
+ describe('multiple clusters', () => {
const request = {
response: {
json: {
@@ -110,7 +117,35 @@ describe('render', () => {
},
},
} as unknown as Request;
- const wrapper = shallow();
- expect(wrapper).toMatchSnapshot();
+
+ test('should display table, search bar, and health bar', () => {
+ render();
+ const table = screen.getByRole('table');
+ expect(table).not.toBeNull();
+ const searchbar = screen.getByRole('searchbox');
+ expect(searchbar).not.toBeNull();
+ const healthbar = screen.getByText('2 clusters');
+ expect(healthbar).not.toBeNull();
+ });
+
+ test('should filter table and health bar', () => {
+ render();
+ const searchbar = screen.getByRole('searchbox');
+ fireEvent.change(searchbar, { target: { value: 'remot' } });
+ const tableRows = screen.getAllByRole('row');
+ expect(tableRows.length).toBe(2); // table header and matching table row
+ const healthbar = screen.getByText('1 cluster');
+ expect(healthbar).not.toBeNull();
+ });
+
+ test('should display search bar when there are no matches for search', () => {
+ render();
+ const searchbar = screen.getByRole('searchbox');
+ fireEvent.change(searchbar, { target: { value: 'nevergonafindme' } });
+ const notFoundRow = screen.getByRole('row', { name: 'No clusters found' });
+ expect(notFoundRow).not.toBeNull();
+ const searchbarAfterSearch = screen.getByRole('searchbox');
+ expect(searchbarAfterSearch).not.toBeNull();
+ });
});
});
diff --git a/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_view.tsx b/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_view.tsx
index 8a96847832dc8..c14822d93561a 100644
--- a/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_view.tsx
+++ b/src/plugins/inspector/public/views/requests/components/details/clusters_view/clusters_view.tsx
@@ -8,45 +8,100 @@
import React, { Component } from 'react';
import { estypes } from '@elastic/elasticsearch';
-import { EuiSpacer } from '@elastic/eui';
+import { EuiSearchBar, type EuiSearchBarOnChangeArgs, EuiSpacer } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
import type { ClusterDetails } from '@kbn/es-types';
import { Request } from '../../../../../../common/adapters/request/types';
import type { DetailViewProps } from '../types';
-import { getLocalClusterDetails, LOCAL_CLUSTER_KEY } from './local_cluster';
-import { ClustersHealth } from './clusters_health';
+import { ClusterHealth, ClustersHealth } from './clusters_health';
import { ClustersTable } from './clusters_table';
+import { findClusters } from './find_clusters';
-export class ClustersView extends Component {
+interface State {
+ clusters: Record;
+ showSearchAndStatusBar: boolean;
+}
+
+export class ClustersView extends Component {
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;
- }
+ constructor(props: DetailViewProps) {
+ super(props);
+ const clusters = findClusters(this.props.request);
+ this.state = {
+ clusters,
+ showSearchAndStatusBar: Object.keys(clusters).length > 1,
+ };
+ }
- const clusters = rawResponse._clusters
- ? (
- rawResponse._clusters as estypes.ClusterStatistics & {
- details: Record;
- }
- ).details
- : {
- [LOCAL_CLUSTER_KEY]: getLocalClusterDetails(rawResponse),
- };
+ _onSearchChange = ({ query, error }: EuiSearchBarOnChangeArgs) => {
+ if (!error) {
+ this.setState({ clusters: findClusters(this.props.request, query) });
+ }
+ };
- return this.props.request.response?.json ? (
+ render() {
+ return (
<>
- {Object.keys(clusters).length > 1 ? : null}
-
+ {this.state.showSearchAndStatusBar ? (
+ <>
+
+ ),
+ },
+ {
+ value: 'partial',
+ view: (
+
+ ),
+ },
+ {
+ value: 'skipped',
+ view: (
+
+ ),
+ },
+ {
+ value: 'failed',
+ view: (
+
+ ),
+ },
+ ],
+ },
+ ]}
+ onChange={this._onSearchChange}
+ />
+
+
+ >
+ ) : null}
+
>
- ) : null;
+ );
}
}
diff --git a/src/plugins/inspector/public/views/requests/components/details/clusters_view/find_clusters.test.ts b/src/plugins/inspector/public/views/requests/components/details/clusters_view/find_clusters.test.ts
new file mode 100644
index 0000000000000..79cc378d89128
--- /dev/null
+++ b/src/plugins/inspector/public/views/requests/components/details/clusters_view/find_clusters.test.ts
@@ -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 { EuiSearchBar } from '@elastic/eui';
+import { findClusters } from './find_clusters';
+import { LOCAL_CLUSTER_KEY } from './local_cluster';
+import { Request } from '../../../../../../common/adapters/request/types';
+
+const request = {
+ response: {
+ json: {
+ rawResponse: {
+ _clusters: {
+ details: {
+ [LOCAL_CLUSTER_KEY]: {
+ status: 'successful',
+ took: 50,
+ },
+ remote1: {
+ status: 'skipped',
+ took: 1000,
+ },
+ remote2: {
+ status: 'failed',
+ took: 90,
+ },
+ },
+ },
+ },
+ },
+ },
+} as unknown as Request;
+
+describe('findClusters', () => {
+ test('should return all clusters when query is not provided', () => {
+ const clusters = findClusters(request);
+ expect(Object.keys(clusters)).toEqual([LOCAL_CLUSTER_KEY, 'remote1', 'remote2']);
+ });
+
+ test('should filter clusters by cluster name', () => {
+ const clusters = findClusters(request, EuiSearchBar.Query.parse('remo'));
+ expect(Object.keys(clusters)).toEqual(['remote1', 'remote2']);
+ });
+
+ test('should filter clusters by cluster name and status', () => {
+ const clusters = findClusters(
+ request,
+ EuiSearchBar.Query.parse('remo status:(successful or skipped)')
+ );
+ expect(Object.keys(clusters)).toEqual(['remote1']);
+ });
+
+ test('should filter by multiple status values', () => {
+ const clusters = findClusters(
+ request,
+ EuiSearchBar.Query.parse('status:(successful or skipped)')
+ );
+ expect(Object.keys(clusters)).toEqual([LOCAL_CLUSTER_KEY, 'remote1']);
+ });
+});
diff --git a/src/plugins/inspector/public/views/requests/components/details/clusters_view/find_clusters.ts b/src/plugins/inspector/public/views/requests/components/details/clusters_view/find_clusters.ts
new file mode 100644
index 0000000000000..8b62cf20d1b48
--- /dev/null
+++ b/src/plugins/inspector/public/views/requests/components/details/clusters_view/find_clusters.ts
@@ -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 { estypes } from '@elastic/elasticsearch';
+import type { ClusterDetails } from '@kbn/es-types';
+import { EuiSearchBar, type Query } from '@elastic/eui';
+import { Request } from '../../../../../../common/adapters/request/types';
+import { getLocalClusterDetails, LOCAL_CLUSTER_KEY } from './local_cluster';
+
+export function findClusters(request: Request, query?: Query): Record {
+ const rawResponse = (request.response?.json as { rawResponse?: estypes.SearchResponse })
+ ?.rawResponse;
+ if (!rawResponse) {
+ return {};
+ }
+
+ const clusters = rawResponse._clusters
+ ? (
+ rawResponse._clusters as estypes.ClusterStatistics & {
+ details: Record;
+ }
+ ).details
+ : {
+ [LOCAL_CLUSTER_KEY]: getLocalClusterDetails(rawResponse),
+ };
+
+ if (!query) {
+ return clusters;
+ }
+
+ const clusterItems = Object.keys(clusters).map((key) => {
+ return {
+ name: key,
+ status: clusters[key].status,
+ };
+ });
+
+ const narrowedClusterItems = EuiSearchBar.Query.execute(query, clusterItems, {
+ defaultFields: ['name'],
+ });
+
+ const narrowedClusers: Record = {};
+ narrowedClusterItems.forEach(({ name }) => {
+ narrowedClusers[name] = clusters[name];
+ });
+ return narrowedClusers;
+}
diff --git a/src/plugins/inspector/public/views/requests/components/request_details.tsx b/src/plugins/inspector/public/views/requests/components/request_details.tsx
index 7677cca66b369..cbe7c035afc8b 100644
--- a/src/plugins/inspector/public/views/requests/components/request_details.tsx
+++ b/src/plugins/inspector/public/views/requests/components/request_details.tsx
@@ -96,7 +96,7 @@ export function RequestDetails(props: Props) {
))}
-
+
>
) : null;
}