Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Index Management] Add "Stats" tab #165027

Merged
1 change: 1 addition & 0 deletions packages/kbn-doc-links/src/get_doc_links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
},
apis: {
bulkIndexAlias: `${ELASTICSEARCH_DOCS}indices-aliases.html`,
indexStats: `${ELASTICSEARCH_DOCS}indices-stats.html`,
byteSizeUnits: `${ELASTICSEARCH_DOCS}api-conventions.html#byte-units`,
createAutoFollowPattern: `${ELASTICSEARCH_DOCS}ccr-put-auto-follow-pattern.html`,
createFollower: `${ELASTICSEARCH_DOCS}ccr-put-follow.html`,
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-doc-links/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ export interface DocLinks {
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
indexStats: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,11 @@ const registerHttpRequestMockHelpers = (
const setLoadIndexMappingResponse = (response?: HttpResponse, error?: ResponseError) =>
mockResponse('GET', `${API_BASE_PATH}/mapping/:name`, response, error);

const setLoadIndexStatsResponse = (response?: HttpResponse, error?: ResponseError) =>
mockResponse('GET', `${API_BASE_PATH}/stats/:name`, response, error);
const setLoadIndexStatsResponse = (
indexName: string,
response?: HttpResponse,
error?: ResponseError
) => mockResponse('GET', `${API_BASE_PATH}/stats/${indexName}`, response, error);

const setUpdateIndexSettingsResponse = (
indexName: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ export interface IndexDetailsPageTestBed extends TestBed {
isDisplayed: () => boolean;
clickReloadButton: () => Promise<void>;
};
stats: {
getCodeBlockContent: () => string;
getDocsLinkHref: () => string;
isErrorDisplayed: () => boolean;
clickErrorReloadButton: () => Promise<void>;
indexStatsTabExists: () => boolean;
};
};
}

Expand Down Expand Up @@ -135,6 +142,27 @@ export const setup = async (
component.update();
},
};

const stats = {
indexStatsTabExists: () => {
return exists('indexDetailsTab-stats');
},
getCodeBlockContent: () => {
return find('indexDetailsStatsCodeBlock').text();
},
getDocsLinkHref: () => {
return find('indexDetailsStatsDocsLink').prop('href');
},
isErrorDisplayed: () => {
return exists('indexDetailsStatsError');
},
clickErrorReloadButton: async () => {
await act(async () => {
find('reloadIndexStatsButton').simulate('click');
});
component.update();
},
};
return {
...testBed,
routerMock,
Expand All @@ -146,6 +174,7 @@ export const setup = async (
discoverLinkExists,
contextMenu,
errorSection,
stats,
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { setupEnvironment } from '../helpers';
import { IndexDetailsPageTestBed, setup } from './index_details_page.helpers';
import { act } from 'react-dom/test-utils';
import { IndexDetailsSection } from '../../../public/application/sections/home/index_list/details_page';
import { testIndexMock, testIndexName } from './mocks';
import { testIndexMock, testIndexName, testIndexStats } from './mocks';
import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common';

describe('<IndexDetailsPage />', () => {
Expand All @@ -22,6 +22,7 @@ describe('<IndexDetailsPage />', () => {
({ httpSetup, httpRequestsMockHelpers } = mockEnvironment);
// testIndexName is configured in initialEntries of the memory router
httpRequestsMockHelpers.setLoadIndexDetailsResponse(testIndexName, testIndexMock);
httpRequestsMockHelpers.setLoadIndexStatsResponse(testIndexName, testIndexStats);

await act(async () => {
testBed = await setup(httpSetup, {
Expand Down Expand Up @@ -60,6 +61,71 @@ describe('<IndexDetailsPage />', () => {
});
});

describe('Stats tab', () => {
it('loads index stats from the API', async () => {
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Stats);
expect(httpSetup.get).toHaveBeenLastCalledWith(`${API_BASE_PATH}/stats/${testIndexName}`, {
asSystemRequest: undefined,
body: undefined,
query: undefined,
version: undefined,
});
});

it('renders index stats', async () => {
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Stats);
const tabContent = testBed.actions.stats.getCodeBlockContent();
expect(tabContent).toEqual(JSON.stringify(testIndexStats, null, 2));
});

it('sets the docs link href from the documenation service', async () => {
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Stats);
const docsLinkHref = testBed.actions.stats.getDocsLinkHref();
// the url from the mocked docs mock
expect(docsLinkHref).toEqual(
'https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/indices-stats.html'
);
});

it('hides index stats tab if enableIndexStats===false', async () => {
await act(async () => {
testBed = await setup(httpSetup, {
config: { enableIndexStats: false },
});
});
testBed.component.update();

expect(testBed.actions.stats.indexStatsTabExists()).toBe(false);
});

describe('Error handling', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setLoadIndexStatsResponse(testIndexName, undefined, {
statusCode: 500,
message: 'Error',
});
await act(async () => {
testBed = await setup(httpSetup);
});

testBed.component.update();
await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Stats);
});

it('there is an error prompt', async () => {
expect(testBed.actions.stats.isErrorDisplayed()).toBe(true);
});

it('resends a request when reload button is clicked', async () => {
// already sent 3 requests while setting up the component
const numberOfRequests = 3;
expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests);
await testBed.actions.stats.clickErrorReloadButton();
expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests + 1);
});
});
});

it('loads index details from the API', async () => {
expect(httpSetup.get).toHaveBeenLastCalledWith(
`${INTERNAL_API_BASE_PATH}/indices/${testIndexName}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,29 @@ export const testIndexMock: Index = {
},
isFollowerIndex: false,
};

// Mocking partial index stats response
export const testIndexStats = {
_shards: {
total: 1,
successful: 1,
failed: 0,
},
stats: {
uuid: 'tQ-n6sriQzC84xn58VYONQ',
health: 'green',
status: 'open',
primaries: {
docs: {
count: 1000,
deleted: 0,
},
},
total: {
docs: {
count: 1000,
deleted: 0,
},
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { css } from '@emotion/react';
import { Redirect, RouteComponentProps } from 'react-router-dom';
import { Route, Routes } from '@kbn/shared-ux-router';
import { FormattedMessage } from '@kbn/i18n-react';
Expand All @@ -20,19 +21,22 @@ import { SectionLoading } from '@kbn/es-ui-shared-plugin/public';

import { Index } from '../../../../../../common';
import { loadIndex } from '../../../../services';
import { useAppContext } from '../../../../app_context';
import { DiscoverLink } from '../../../../lib/discover_link';
import { Section } from '../../home';
import { DetailsPageError } from './details_page_error';
import { ManageIndexButton } from './manage_index_button';
import { DetailsPageStats } from './tabs';

export enum IndexDetailsSection {
Overview = 'overview',
Documents = 'documents',
Mappings = 'mappings',
Settings = 'settings',
Pipelines = 'pipelines',
Stats = 'stats',
}
const tabs = [
const defaultTabs = [
{
id: IndexDetailsSection.Overview,
name: (
Expand Down Expand Up @@ -64,6 +68,12 @@ const tabs = [
),
},
];

const statsTab = {
id: IndexDetailsSection.Stats,
name: <FormattedMessage id="xpack.idxMgmt.indexDetails.statsTitle" defaultMessage="Stats" />,
Copy link
Contributor

@gchaps gchaps Aug 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Elsewhere we spell out statistics. For example, Field Statistics in Discover, and the Statistics tab in the Inspector. Should we spell it out for this tab as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. I agree we should be consistent if "Statistics" is used elsewhere in Kibana.

This PR is part of a larger effort to make UX improvements to Index Management. I'm going to hold off on making this change here, as @yuliacech had planned to do an overall copy review with the docs team once the initial work is complete.

};

export const DetailsPage: React.FunctionComponent<
RouteComponentProps<{ indexName: string; indexDetailsSection: IndexDetailsSection }>
> = ({
Expand All @@ -72,6 +82,7 @@ export const DetailsPage: React.FunctionComponent<
},
history,
}) => {
const { config } = useAppContext();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
const [index, setIndex] = useState<Index | null>();
Expand Down Expand Up @@ -105,14 +116,16 @@ export const DetailsPage: React.FunctionComponent<
}, [history]);

const headerTabs = useMemo<EuiPageHeaderProps['tabs']>(() => {
return tabs.map((tab) => ({
const visibleTabs = config.enableIndexStats ? [...defaultTabs, statsTab] : defaultTabs;

return visibleTabs.map((tab) => ({
onClick: () => onSectionChange(tab.id),
isSelected: tab.id === indexDetailsSection,
key: tab.id,
'data-test-subj': `indexDetailsTab-${tab.id}`,
label: tab.name,
}));
}, [indexDetailsSection, onSectionChange]);
}, [indexDetailsSection, onSectionChange, config]);

if (isLoading && !index) {
return (
Expand All @@ -127,7 +140,6 @@ export const DetailsPage: React.FunctionComponent<
if (error || !index) {
return <DetailsPageError indexName={indexName} resendRequest={fetchIndexDetails} />;
}

return (
<>
<EuiPageSection paddingSize="none">
Expand Down Expand Up @@ -164,7 +176,12 @@ export const DetailsPage: React.FunctionComponent<

<EuiSpacer size="l" />

<div data-test-subj={`indexDetailsContent`}>
<div
data-test-subj={`indexDetailsContent`}
css={css`
height: 100%;
`}
>
<Routes>
<Route
path={`/${Section.Indices}/${indexName}/${IndexDetailsSection.Overview}`}
Expand All @@ -186,17 +203,18 @@ export const DetailsPage: React.FunctionComponent<
path={`/${Section.Indices}/${indexName}/${IndexDetailsSection.Pipelines}`}
render={() => <div>Pipelines</div>}
/>
{config.enableIndexStats && (
<Route
path={`/${Section.Indices}/:indexName/${IndexDetailsSection.Stats}`}
component={DetailsPageStats}
/>
)}
<Redirect
from={`/${Section.Indices}/${indexName}`}
to={`/${Section.Indices}/${indexName}/${IndexDetailsSection.Overview}`}
/>
</Routes>
</div>

<EuiSpacer size="l" />
<div>
<pre>{JSON.stringify(index, null, 2)}</pre>
</div>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export { DetailsPageStats } from './stats';
Loading