diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 1b1973e35ec52..a8d63137fa0a9 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -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`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index b29017f71d739..428ef86267dd1 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -361,6 +361,7 @@ export interface DocLinks { readonly visualize: Record; readonly apis: Readonly<{ bulkIndexAlias: string; + indexStats: string; byteSizeUnits: string; createAutoFollowPattern: string; createFollower: string; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts index 82ee841eb7398..9811255af25b0 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -103,8 +103,11 @@ const registerHttpRequestMockHelpers = ( error?: ResponseError ) => mockResponse('GET', `${API_BASE_PATH}/mapping/${indexName}`, 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, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.helpers.ts index e31e368628c86..cdf5e9f5ab841 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.helpers.ts @@ -57,6 +57,14 @@ export interface IndexDetailsPageTestBed extends TestBed { isDisplayed: () => boolean; clickReloadButton: () => Promise; }; + stats: { + getCodeBlockContent: () => string; + getDocsLinkHref: () => string; + isErrorDisplayed: () => boolean; + clickErrorReloadButton: () => Promise; + indexStatsTabExists: () => boolean; + isWarningDisplayed: () => boolean; + }; }; } @@ -159,6 +167,30 @@ 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'); + }, + isWarningDisplayed: () => { + return exists('indexStatsNotAvailableWarning'); + }, + clickErrorReloadButton: async () => { + await act(async () => { + find('reloadIndexStatsButton').simulate('click'); + }); + component.update(); + }, + }; return { ...testBed, routerMock, @@ -171,6 +203,7 @@ export const setup = async ( discoverLinkExists, contextMenu, errorSection, + stats, }, }; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.ts index e4b4c33489262..ddd1d44ac9440 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.ts @@ -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 { testIndexMappings, testIndexMock, testIndexName } from './mocks'; +import { testIndexMappings, testIndexMock, testIndexName, testIndexStats } from './mocks'; import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common'; describe('', () => { @@ -22,6 +22,7 @@ describe('', () => { ({ httpSetup, httpRequestsMockHelpers } = mockEnvironment); // testIndexName is configured in initialEntries of the memory router httpRequestsMockHelpers.setLoadIndexDetailsResponse(testIndexName, testIndexMock); + httpRequestsMockHelpers.setLoadIndexStatsResponse(testIndexName, testIndexStats); httpRequestsMockHelpers.setLoadIndexMappingResponse(testIndexName, testIndexMappings); await act(async () => { @@ -61,6 +62,91 @@ describe('', () => { }); }); + 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('renders a warning message if an index is not open', async () => { + const testIndexMockWithClosedStatus = { + ...testIndexMock, + status: 'closed', + }; + + httpRequestsMockHelpers.setLoadIndexDetailsResponse( + testIndexName, + testIndexMockWithClosedStatus + ); + + await act(async () => { + testBed = await setup(httpSetup); + }); + testBed.component.update(); + + await testBed.actions.clickIndexDetailsTab(IndexDetailsSection.Stats); + expect(testBed.actions.stats.isWarningDisplayed()).toBe(true); + }); + + 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}`, diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/mocks.ts b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/mocks.ts index bda26b3c2368c..7666c1ac0dee6 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/mocks.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/mocks.ts @@ -41,3 +41,29 @@ export const testIndexMappings = { }, }, }; + +// 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, + }, + }, + }, +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page.tsx index 5464e3ebf0ac2..80f79d9ab3c76 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page.tsx @@ -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'; @@ -18,13 +19,15 @@ import { } from '@elastic/eui'; import { SectionLoading } from '@kbn/es-ui-shared-plugin/public'; -import { css } from '@emotion/react'; import { Index } from '../../../../../../common'; +import { INDEX_OPEN } from '../../../../../../common/constants'; 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 './details_page_stats'; import { DetailsPageMappings } from './details_page_mappings'; export enum IndexDetailsSection { @@ -33,8 +36,9 @@ export enum IndexDetailsSection { Mappings = 'mappings', Settings = 'settings', Pipelines = 'pipelines', + Stats = 'stats', } -const tabs = [ +const defaultTabs = [ { id: IndexDetailsSection.Overview, name: ( @@ -66,6 +70,12 @@ const tabs = [ ), }, ]; + +const statsTab = { + id: IndexDetailsSection.Stats, + name: , +}; + export const DetailsPage: React.FunctionComponent< RouteComponentProps<{ indexName: string; indexDetailsSection: IndexDetailsSection }> > = ({ @@ -74,6 +84,7 @@ export const DetailsPage: React.FunctionComponent< }, history, }) => { + const { config } = useAppContext(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(); const [index, setIndex] = useState(); @@ -107,14 +118,16 @@ export const DetailsPage: React.FunctionComponent< }, [history]); const headerTabs = useMemo(() => { - 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 ( @@ -129,7 +142,6 @@ export const DetailsPage: React.FunctionComponent< if (error || !index) { return ; } - return ( <> @@ -193,6 +205,14 @@ export const DetailsPage: React.FunctionComponent< path={`/${Section.Indices}/${indexName}/${IndexDetailsSection.Pipelines}`} render={() =>
Pipelines
} /> + {config.enableIndexStats && ( + ) => ( + + )} + /> + )} & Props +> = ({ + match: { + params: { indexName }, + }, + isIndexOpen, +}) => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(); + const [indexStats, setIndexStats] = useState(); + + const fetchIndexStats = useCallback(async () => { + setIsLoading(true); + try { + const { data, error: loadingError } = await loadIndexStatistics(indexName); + setIsLoading(false); + setError(loadingError); + setIndexStats(data); + } catch (e) { + setIsLoading(false); + setError(e); + } + }, [indexName]); + + useEffect(() => { + if (isIndexOpen) { + fetchIndexStats(); + } + }, [fetchIndexStats, isIndexOpen]); + + if (isIndexOpen === false) { + return ( + + + + } + body={ +

+ +

+ } + /> + ); + } + + if (isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + + + + } + body={ + <> + + + + + + + + + } + /> + ); + } + + if (indexStats) { + // using "rowReverse" to keep docs links on the top of the stats code block on smaller screen + return ( + + + + + + + + + + +

+ +

+
+
+
+ + + + +

+ primaries, + totalField: total, + }} + /> +

+
+ + + + + +
+
+ + + + + {JSON.stringify(indexStats, null, 2)} + + + +
+ ); + } + + return null; +}; diff --git a/x-pack/plugins/index_management/public/application/services/api.ts b/x-pack/plugins/index_management/public/application/services/api.ts index 829e4dda357b0..3daee29ec62e8 100644 --- a/x-pack/plugins/index_management/public/application/services/api.ts +++ b/x-pack/plugins/index_management/public/application/services/api.ts @@ -6,6 +6,7 @@ */ import { METRIC_TYPE } from '@kbn/analytics'; +import { IndicesStatsResponse } from '@elastic/elasticsearch/lib/api/types'; import { API_BASE_PATH, UIM_UPDATE_SETTINGS, @@ -326,3 +327,10 @@ export function useLoadIndexMappings(indexName: string) { method: 'get', }); } + +export function loadIndexStatistics(indexName: string) { + return sendRequest({ + path: `${API_BASE_PATH}/stats/${encodeURIComponent(indexName)}`, + method: 'get', + }); +} diff --git a/x-pack/plugins/index_management/public/application/services/documentation.ts b/x-pack/plugins/index_management/public/application/services/documentation.ts index 0eb9f62fffd09..3fc34477cacae 100644 --- a/x-pack/plugins/index_management/public/application/services/documentation.ts +++ b/x-pack/plugins/index_management/public/application/services/documentation.ts @@ -59,6 +59,7 @@ class DocumentationService { private runtimeFields: string = ''; private indicesComponentTemplate: string = ''; private bulkIndexAlias: string = ''; + private indexStats: string = ''; public setup(docLinks: DocLinksStart): void { const { links } = docLinks; @@ -111,6 +112,7 @@ class DocumentationService { this.runtimeFields = links.runtimeFields.overview; this.indicesComponentTemplate = links.apis.putComponentTemplate; this.bulkIndexAlias = links.apis.bulkIndexAlias; + this.indexStats = links.apis.indexStats; } public getEsDocsBase() { @@ -311,6 +313,10 @@ class DocumentationService { return this.bulkIndexAlias; } + public getIndexStats() { + return this.indexStats; + } + public getWellKnownTextLink() { return 'http://docs.opengeospatial.org/is/12-063r5/12-063r5.html'; } diff --git a/x-pack/plugins/index_management/public/application/services/index.ts b/x-pack/plugins/index_management/public/application/services/index.ts index 5d3cd1f52efe6..5512171b358af 100644 --- a/x-pack/plugins/index_management/public/application/services/index.ts +++ b/x-pack/plugins/index_management/public/application/services/index.ts @@ -26,6 +26,7 @@ export { useLoadNodesPlugins, loadIndex, useLoadIndexMappings, + loadIndexStatistics, } from './api'; export { sortTable } from './sort_table';