- -
-
-

- Create your first index lifecycle policy -

-
+
+
-

- An index lifecycle policy helps you manage your indices as they age. -

-
-
- + class="emotion-euiButtonDisplayContent" + > + + Create policy + + +
-
+ `; exports[`policy table sorts when linked index templates header is clicked 1`] = ` diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx index 246de6e8ed25b..565a994e4e186 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx @@ -80,7 +80,7 @@ const indexWithLifecyclePolicy: Index = { }, }; -const indexWithLifecycleError = { +const indexWithLifecycleError: Index = { health: 'yellow', status: 'open', name: 'testy3', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx index 1290304ef6165..2bad87b149e0f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.container.tsx @@ -7,12 +7,7 @@ import React, { useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { - EuiButton, - EuiEmptyPrompt, - EuiLoadingSpinner, - EuiPageContent_Deprecated as EuiPageContent, -} from '@elastic/eui'; +import { EuiButton, EuiLoadingSpinner, EuiPageTemplate } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { MIN_SEARCHABLE_SNAPSHOT_LICENSE } from '../../../../common/constants'; @@ -45,47 +40,44 @@ export const EditPolicy: React.FunctionComponent - } - body={ - - } - /> - + } + body={ + + } + /> ); } if (error || !policies) { const { statusCode, message } = error ? error : { statusCode: '', message: '' }; return ( - - - - - } - body={ -

- {message} ({statusCode}) -

- } - actions={ - - - - } - /> -
+ + + + } + body={ +

+ {message} ({statusCode}) +

+ } + actions={ + + + + } + /> ); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.container.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.container.tsx index 7113b00cf4ec2..669f22ccb9b3f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.container.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.container.tsx @@ -6,12 +6,7 @@ */ import React, { useEffect } from 'react'; -import { - EuiButton, - EuiEmptyPrompt, - EuiLoadingSpinner, - EuiPageContent_Deprecated as EuiPageContent, -} from '@elastic/eui'; +import { EuiButton, EuiLoadingSpinner, EuiPageTemplate } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { PolicyList as PresentationComponent } from './policy_list'; import { useKibana } from '../../../shared_imports'; @@ -30,47 +25,44 @@ export const PolicyList: React.FunctionComponent = () => { if (isLoading) { return ( - - } - body={ - - } - /> - + } + body={ + + } + /> ); } if (error) { const { statusCode, message } = error ? error : { statusCode: '', message: '' }; return ( - - - - - } - body={ -

- {message} ({statusCode}) -

- } - actions={ - - - - } - /> -
+ + + + } + body={ +

+ {message} ({statusCode}) +

+ } + actions={ + + + + } + /> ); } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.tsx index 09fbc2d1c41c6..0a81f6b16bf43 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_list/policy_list.tsx @@ -8,13 +8,7 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiButton, - EuiEmptyPrompt, - EuiSpacer, - EuiPageHeader, - EuiPageContent_Deprecated as EuiPageContent, -} from '@elastic/eui'; +import { EuiButton, EuiSpacer, EuiPageHeader, EuiPageTemplate } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { reactRouterNavigate } from '@kbn/kibana-react-plugin/public'; import { PolicyFromES } from '../../../../common/types'; @@ -46,30 +40,28 @@ export const PolicyList: React.FunctionComponent = ({ policies, updatePol if (policies.length === 0) { return ( - - + + + + } + body={ + +

- - } - body={ - -

- -

-
- } - actions={createPolicyButton} - /> -
+

+ + } + actions={createPolicyButton} + /> ); } diff --git a/x-pack/plugins/index_lifecycle_management/server/config.ts b/x-pack/plugins/index_lifecycle_management/server/config.ts index 5a02e404ad2b7..f04c3cd062f5f 100644 --- a/x-pack/plugins/index_lifecycle_management/server/config.ts +++ b/x-pack/plugins/index_lifecycle_management/server/config.ts @@ -7,7 +7,7 @@ import { SemVer } from 'semver'; import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; -import { schema, TypeOf } from '@kbn/config-schema'; +import { offeringBasedSchema, schema, TypeOf } from '@kbn/config-schema'; import { PluginConfigDescriptor } from '@kbn/core/server'; import { MAJOR_VERSION } from '../common/constants'; @@ -28,14 +28,11 @@ const schemaLatest = schema.object( * Disables the plugin. * Added back in 8.8. */ - enabled: schema.conditional( - schema.contextRef('serverless'), - true, + enabled: offeringBasedSchema({ // ILM is disabled in serverless; refer to the serverless.yml file as the source of truth // We take this approach in order to have a central place (serverless.yml) to view disabled plugins across Kibana - schema.boolean({ defaultValue: true }), - schema.never() - ), + serverless: schema.boolean({ defaultValue: true }), + }), }, { defaultValue: undefined } ); 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 c49d3ccaba890..0a054fccdfcc1 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 @@ -6,7 +6,7 @@ */ import { httpServiceMock } from '@kbn/core/public/mocks'; -import { API_BASE_PATH } from '../../../common/constants'; +import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common/constants'; type HttpResponse = Record | any[]; type HttpMethod = 'GET' | 'PUT' | 'DELETE' | 'POST'; @@ -121,6 +121,12 @@ const registerHttpRequestMockHelpers = ( const setLoadTelemetryResponse = (response?: HttpResponse, error?: ResponseError) => mockResponse('GET', '/api/ui_counters/_report', response, error); + const setLoadIndexDetailsResponse = ( + indexName: string, + response?: HttpResponse, + error?: ResponseError + ) => mockResponse('GET', `${INTERNAL_API_BASE_PATH}/indices/${indexName}`, response, error); + return { setLoadTemplatesResponse, setLoadIndicesResponse, @@ -139,6 +145,7 @@ const registerHttpRequestMockHelpers = ( setLoadComponentTemplatesResponse, setLoadNodesPluginsResponse, setLoadTelemetryResponse, + setLoadIndexDetailsResponse, }; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx index b1dd1d748f309..0e485de1e7775 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx @@ -60,6 +60,7 @@ const appDependencies = { config: { enableLegacyTemplates: true, enableIndexActions: true, + enableIndexStats: true, }, } as any; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts index 2574594c7fcf8..7591ca90f1596 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts @@ -19,6 +19,7 @@ export type TestSubjects = | 'deleteSystemTemplateCallOut' | 'deleteTemplateButton' | 'deleteTemplatesConfirmation' + | 'descriptionTitle' | 'documentationLink' | 'emptyPrompt' | 'forcemergeIndexMenuButton' diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts index 16a3e1fd09bbd..80c03726c3d40 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -62,14 +62,13 @@ describe('', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadIndicesResponse([]); - testBed = await setup(httpSetup); - await act(async () => { - const { component } = testBed; - - await nextTick(); - component.update(); + testBed = await setup(httpSetup); }); + + const { component } = testBed; + + component.update(); }); test('toggles the include hidden button through URL hash correctly', () => { @@ -423,4 +422,103 @@ describe('', () => { expect(exists('updateIndexSettingsErrorCallout')).toBe(true); }); }); + + describe('Index stats', () => { + const indexName = 'test'; + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadIndicesResponse([createNonDataStreamIndex(indexName)]); + + await act(async () => { + testBed = await setup(httpSetup); + }); + + const { component } = testBed; + + component.update(); + }); + + test('renders the table column with index stats by default', () => { + const { table } = testBed; + const { tableCellsValues } = table.getMetaData('indexTable'); + + expect(tableCellsValues).toEqual([ + ['', 'test', 'green', 'open', '1', '1', '10000', '156kb', ''], + ]); + }); + + test('renders index stats in details flyout by default', async () => { + const { component, find } = testBed; + + await act(async () => { + find('indexTableIndexNameLink').at(0).simulate('click'); + }); + + component.update(); + + const descriptions = find('descriptionTitle'); + + const descriptionText = descriptions + .map((description) => { + return description.text(); + }) + .sort(); + + expect(descriptionText).toEqual([ + 'Aliases', + 'Docs count', + 'Docs deleted', + 'Health', + 'Primaries', + 'Primary storage size', + 'Replicas', + 'Status', + 'Storage size', + ]); + }); + + describe('Disabled', () => { + beforeEach(async () => { + await act(async () => { + testBed = await setup(httpSetup, { + config: { + enableLegacyTemplates: true, + enableIndexActions: true, + enableIndexStats: false, + }, + }); + }); + + const { component } = testBed; + + component.update(); + }); + + test('hides index stats information from table', async () => { + const { table } = testBed; + const { tableCellsValues } = table.getMetaData('indexTable'); + + expect(tableCellsValues).toEqual([['', 'test', '1', '1', '']]); + }); + + test('hides index stats information from details panel', async () => { + const { component, find } = testBed; + await act(async () => { + find('indexTableIndexNameLink').at(0).simulate('click'); + }); + + component.update(); + + const descriptions = find('descriptionTitle'); + + const descriptionText = descriptions + .map((description) => { + return description.text(); + }) + .sort(); + + expect(descriptionText).toEqual(['Aliases', 'Primaries', 'Replicas']); + }); + }); + }); }); 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 d6edefb6d724c..43d436c495799 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 @@ -5,7 +5,12 @@ * 2.0. */ -import { AsyncTestBedConfig, registerTestBed, TestBed } from '@kbn/test-jest-helpers'; +import { + AsyncTestBedConfig, + reactRouterMock, + registerTestBed, + TestBed, +} from '@kbn/test-jest-helpers'; import { HttpSetup } from '@kbn/core/public'; import { act } from 'react-dom/test-utils'; import { @@ -13,20 +18,39 @@ import { IndexDetailsSection, } from '../../../public/application/sections/home/index_list/details_page'; import { WithAppDependencies } from '../helpers'; +import { testIndexName } from './mocks'; +let routerMock: typeof reactRouterMock; const testBedConfig: AsyncTestBedConfig = { memoryRouter: { - initialEntries: [`/indices/test_index`], + initialEntries: [`/indices/${testIndexName}`], componentRoutePath: `/indices/:indexName/:indexDetailsSection?`, + onRouter: (router) => { + routerMock = router; + }, }, doMountAsync: true, }; export interface IndexDetailsPageTestBed extends TestBed { + routerMock: typeof reactRouterMock; actions: { getHeader: () => string; clickIndexDetailsTab: (tab: IndexDetailsSection) => Promise; getActiveTabContent: () => string; + clickBackToIndicesButton: () => Promise; + discoverLinkExists: () => boolean; + contextMenu: { + clickManageIndexButton: () => Promise; + isOpened: () => boolean; + clickIndexAction: (indexAction: string) => Promise; + confirmForcemerge: (numSegments: string) => Promise; + confirmDelete: () => Promise; + }; + errorSection: { + isDisplayed: () => boolean; + clickReloadButton: () => Promise; + }; }; } @@ -39,14 +63,24 @@ export const setup = async ( testBedConfig ); const testBed = await initTestBed(); + const { find, component, exists } = testBed; + const errorSection = { + isDisplayed: () => { + return exists('indexDetailsErrorLoadingDetails'); + }, + clickReloadButton: async () => { + await act(async () => { + find('indexDetailsReloadDetailsButton').simulate('click'); + }); + component.update(); + }, + }; const getHeader = () => { - return testBed.component.find('[data-test-subj="indexDetailsHeader"] h1').text(); + return component.find('[data-test-subj="indexDetailsHeader"] h1').text(); }; const clickIndexDetailsTab = async (tab: IndexDetailsSection) => { - const { find, component } = testBed; - await act(async () => { find(`indexDetailsTab-${tab}`).simulate('click'); }); @@ -54,15 +88,64 @@ export const setup = async ( }; const getActiveTabContent = () => { - return testBed.find('indexDetailsContent').text(); + return find('indexDetailsContent').text(); }; + const clickBackToIndicesButton = async () => { + await act(async () => { + find('indexDetailsBackToIndicesButton').simulate('click'); + }); + component.update(); + }; + + const discoverLinkExists = () => { + return exists('discoverButtonLink'); + }; + + const contextMenu = { + clickManageIndexButton: async () => { + await act(async () => { + find('indexActionsContextMenuButton').simulate('click'); + }); + component.update(); + }, + isOpened: () => { + return exists('indexContextMenu'); + }, + clickIndexAction: async (indexAction: string) => { + await act(async () => { + find(`indexContextMenu.${indexAction}`).simulate('click'); + }); + component.update(); + }, + confirmForcemerge: async (numSegments: string) => { + await act(async () => { + testBed.form.setInputValue('indexActionsForcemergeNumSegments', numSegments); + }); + component.update(); + await act(async () => { + find('confirmModalConfirmButton').simulate('click'); + }); + component.update(); + }, + confirmDelete: async () => { + await act(async () => { + find('confirmModalConfirmButton').simulate('click'); + }); + component.update(); + }, + }; return { ...testBed, + routerMock, actions: { getHeader, clickIndexDetailsTab, getActiveTabContent, + clickBackToIndicesButton, + discoverLinkExists, + contextMenu, + errorSection, }, }; }; 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 2863560469287..7b144fd0bad40 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 @@ -8,26 +8,69 @@ import { setupEnvironment } from '../helpers'; import { IndexDetailsPageTestBed, setup } from './index_details_page.helpers'; import { act } from 'react-dom/test-utils'; -import { httpServiceMock } from '@kbn/core/public/mocks'; import { IndexDetailsSection } from '../../../public/application/sections/home/index_list/details_page'; +import { testIndexMock, testIndexName } from './mocks'; +import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common'; describe('', () => { let testBed: IndexDetailsPageTestBed; let httpSetup: ReturnType['httpSetup']; + let httpRequestsMockHelpers: ReturnType['httpRequestsMockHelpers']; beforeEach(async () => { - httpSetup = httpServiceMock.createSetupContract(); + const mockEnvironment = setupEnvironment(); + ({ httpSetup, httpRequestsMockHelpers } = mockEnvironment); + // testIndexName is configured in initialEntries of the memory router + httpRequestsMockHelpers.setLoadIndexDetailsResponse(testIndexName, testIndexMock); await act(async () => { - testBed = await setup(httpSetup); + testBed = await setup(httpSetup, { + url: { + locators: { + get: () => ({ navigate: jest.fn() }), + }, + }, + }); }); testBed.component.update(); }); + describe('error section', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadIndexDetailsResponse(testIndexName, undefined, { + statusCode: 400, + message: `Data for index ${testIndexName} was not found`, + }); + await act(async () => { + testBed = await setup(httpSetup); + }); + + testBed.component.update(); + }); + it('displays an error callout when failed to load index details', async () => { + expect(testBed.actions.errorSection.isDisplayed()).toBe(true); + }); + + it('resends a request when reload button is clicked', async () => { + // already sent 2 requests while setting up the component + const numberOfRequests = 2; + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests); + await testBed.actions.errorSection.clickReloadButton(); + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests + 1); + }); + }); + + it('loads index details from the API', async () => { + expect(httpSetup.get).toHaveBeenLastCalledWith( + `${INTERNAL_API_BASE_PATH}/indices/${testIndexName}`, + { asSystemRequest: undefined, body: undefined, query: undefined, version: undefined } + ); + }); + it('displays index name in the header', () => { const header = testBed.actions.getHeader(); - // test_index is configured in initialEntries of the memory router - expect(header).toEqual('test_index'); + // testIndexName is configured in initialEntries of the memory router + expect(header).toEqual(testIndexName); }); it('defaults to overview tab', () => { @@ -58,4 +101,153 @@ describe('', () => { const tabContent = testBed.actions.getActiveTabContent(); expect(tabContent).toEqual('Pipelines'); }); + + it('navigates back to indices', async () => { + jest.spyOn(testBed.routerMock.history, 'push'); + await testBed.actions.clickBackToIndicesButton(); + expect(testBed.routerMock.history.push).toHaveBeenCalledTimes(1); + expect(testBed.routerMock.history.push).toHaveBeenCalledWith('/indices'); + }); + + it('renders a link to discover', () => { + // we only need to test that the link is rendered since the link component has its own tests for navigation + expect(testBed.actions.discoverLinkExists()).toBe(true); + }); + + describe('context menu', () => { + it('opens an index context menu when "manage index" button is clicked', async () => { + expect(testBed.actions.contextMenu.isOpened()).toBe(false); + await testBed.actions.contextMenu.clickManageIndexButton(); + expect(testBed.actions.contextMenu.isOpened()).toBe(true); + }); + + it('closes an index', async () => { + // already sent 1 request while setting up the component + const numberOfRequests = 1; + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests); + + await testBed.actions.contextMenu.clickManageIndexButton(); + await testBed.actions.contextMenu.clickIndexAction('closeIndexMenuButton'); + expect(httpSetup.post).toHaveBeenCalledWith(`${API_BASE_PATH}/indices/close`, { + body: JSON.stringify({ indices: [testIndexName] }), + }); + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests + 1); + }); + + it('opens an index', async () => { + httpRequestsMockHelpers.setLoadIndexDetailsResponse(testIndexName, { + ...testIndexMock, + status: 'close', + }); + + await act(async () => { + testBed = await setup(httpSetup); + }); + testBed.component.update(); + + // already sent 2 requests while setting up the component + const numberOfRequests = 2; + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests); + + await testBed.actions.contextMenu.clickManageIndexButton(); + await testBed.actions.contextMenu.clickIndexAction('openIndexMenuButton'); + expect(httpSetup.post).toHaveBeenCalledWith(`${API_BASE_PATH}/indices/open`, { + body: JSON.stringify({ indices: [testIndexName] }), + }); + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests + 1); + }); + + it('forcemerges an index', async () => { + // already sent 1 request while setting up the component + const numberOfRequests = 1; + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests); + + await testBed.actions.contextMenu.clickManageIndexButton(); + await testBed.actions.contextMenu.clickIndexAction('forcemergeIndexMenuButton'); + await testBed.actions.contextMenu.confirmForcemerge('2'); + expect(httpSetup.post).toHaveBeenCalledWith(`${API_BASE_PATH}/indices/forcemerge`, { + body: JSON.stringify({ indices: [testIndexName], maxNumSegments: '2' }), + }); + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests + 1); + }); + + it('refreshes an index', async () => { + // already sent 1 request while setting up the component + const numberOfRequests = 1; + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests); + + await testBed.actions.contextMenu.clickManageIndexButton(); + await testBed.actions.contextMenu.clickIndexAction('refreshIndexMenuButton'); + expect(httpSetup.post).toHaveBeenCalledWith(`${API_BASE_PATH}/indices/refresh`, { + body: JSON.stringify({ indices: [testIndexName] }), + }); + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests + 1); + }); + + it(`clears an index's cache`, async () => { + // already sent 1 request while setting up the component + const numberOfRequests = 1; + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests); + + await testBed.actions.contextMenu.clickManageIndexButton(); + await testBed.actions.contextMenu.clickIndexAction('clearCacheIndexMenuButton'); + expect(httpSetup.post).toHaveBeenCalledWith(`${API_BASE_PATH}/indices/clear_cache`, { + body: JSON.stringify({ indices: [testIndexName] }), + }); + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests + 1); + }); + + it(`flushes an index`, async () => { + // already sent 1 request while setting up the component + const numberOfRequests = 1; + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests); + + await testBed.actions.contextMenu.clickManageIndexButton(); + await testBed.actions.contextMenu.clickIndexAction('flushIndexMenuButton'); + expect(httpSetup.post).toHaveBeenCalledWith(`${API_BASE_PATH}/indices/flush`, { + body: JSON.stringify({ indices: [testIndexName] }), + }); + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests + 1); + }); + + it(`deletes an index`, async () => { + jest.spyOn(testBed.routerMock.history, 'push'); + // already sent 1 request while setting up the component + const numberOfRequests = 1; + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests); + + await testBed.actions.contextMenu.clickManageIndexButton(); + await testBed.actions.contextMenu.clickIndexAction('deleteIndexMenuButton'); + await testBed.actions.contextMenu.confirmDelete(); + expect(httpSetup.post).toHaveBeenCalledWith(`${API_BASE_PATH}/indices/delete`, { + body: JSON.stringify({ indices: [testIndexName] }), + }); + + expect(testBed.routerMock.history.push).toHaveBeenCalledTimes(1); + expect(testBed.routerMock.history.push).toHaveBeenCalledWith('/indices'); + }); + + it(`unfreezes a frozen index`, async () => { + httpRequestsMockHelpers.setLoadIndexDetailsResponse(testIndexName, { + ...testIndexMock, + isFrozen: true, + }); + + await act(async () => { + testBed = await setup(httpSetup); + }); + testBed.component.update(); + + // already sent 1 request while setting up the component + const numberOfRequests = 2; + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests); + + await testBed.actions.contextMenu.clickManageIndexButton(); + await testBed.actions.contextMenu.clickIndexAction('unfreezeIndexMenuButton'); + expect(httpSetup.post).toHaveBeenCalledWith(`${API_BASE_PATH}/indices/unfreeze`, { + body: JSON.stringify({ indices: [testIndexName] }), + }); + expect(httpSetup.get).toHaveBeenCalledTimes(numberOfRequests + 1); + }); + }); }); 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 new file mode 100644 index 0000000000000..5e165fe0702e6 --- /dev/null +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/mocks.ts @@ -0,0 +1,31 @@ +/* + * 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. + */ + +import { Index } from '../../../public'; + +export const testIndexName = 'test_index'; +export const testIndexMock: Index = { + health: 'green', + status: 'open', + name: testIndexName, + uuid: 'test1234', + primary: '1', + replica: '1', + documents: 1, + documents_deleted: 0, + size: '10kb', + primary_size: '10kb', + isFrozen: false, + aliases: 'none', + hidden: false, + isRollupIndex: false, + ilm: { + index: 'test_index', + managed: false, + }, + isFollowerIndex: false, +}; diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js index 5d8371010b3fe..d7380ef95c938 100644 --- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js +++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js @@ -172,6 +172,7 @@ describe('index table', () => { config: { enableLegacyTemplates: true, enableIndexActions: true, + enableIndexStats: true, }, }; @@ -336,7 +337,7 @@ describe('index table', () => { indexNameLink.simulate('click'); rendered.update(); expect(findTestSubject(rendered, 'indexDetailFlyout').length).toBe(1); - expect(findTestSubject(rendered, 'indexDetailFlyoutDiscover').length).toBe(1); + expect(findTestSubject(rendered, 'discoverIconLink').length).toBe(1); }); test('should show the right context menu options when one index is selected and open', async () => { diff --git a/x-pack/plugins/index_management/common/types/indices.ts b/x-pack/plugins/index_management/common/types/indices.ts index ba7b58453c36b..608ee392a3f9e 100644 --- a/x-pack/plugins/index_management/common/types/indices.ts +++ b/x-pack/plugins/index_management/common/types/indices.ts @@ -5,6 +5,12 @@ * 2.0. */ +import { + HealthStatus, + IndicesStatsIndexMetadataState, + Uuid, +} from '@elastic/elasticsearch/lib/api/types'; + interface IndexModule { number_of_shards: number | string; codec: string; @@ -50,21 +56,30 @@ export interface IndexSettings { analysis?: AnalysisModule; [key: string]: any; } - export interface Index { - health?: string; - status?: string; name: string; - uuid?: string; primary?: number | string; replica?: number | string; - documents: number; - documents_deleted: number; - size: string; - primary_size: string; isFrozen: boolean; hidden: boolean; aliases: string | string[]; data_stream?: string; - [key: string]: any; + + // The types below are added by extension services if corresponding plugins are enabled (ILM, Rollup, CCR) + isRollupIndex?: boolean; + ilm?: { + index: string; + managed: boolean; + }; + isFollowerIndex?: boolean; + + // The types from here below represent information returned from the index stats API; + // treated optional as the stats API is not available on serverless + health?: HealthStatus; + status?: IndicesStatsIndexMetadataState; + uuid?: Uuid; + documents?: number; + size?: string; + primary_size?: string; + documents_deleted?: number; } diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index f0b8598cfd04f..fcedba9a1b941 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -48,6 +48,7 @@ export interface AppDependencies { enableIndexActions: boolean; enableLegacyTemplates: boolean; enableIndexDetailsPage: boolean; + enableIndexStats: boolean; }; history: ScopedHistory; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx index 7c9fd03cd9535..bced972e1bf9c 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/component_template_list.tsx @@ -144,7 +144,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ if (isLoading) { return ( - + + @@ -85,6 +81,6 @@ export const ComponentTemplateCreate: React.FunctionComponent - + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx index 38e45ade11d01..8fa4694ee033a 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_edit/component_template_edit.tsx @@ -9,11 +9,7 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import { - EuiPageContentBody_Deprecated as EuiPageContentBody, - EuiPageHeader, - EuiSpacer, -} from '@elastic/eui'; +import { EuiPageSection, EuiPageHeader, EuiSpacer } from '@elastic/eui'; import { History } from 'history'; import { useComponentTemplatesContext } from '../../component_templates_context'; @@ -165,7 +161,7 @@ export const ComponentTemplateEdit: React.FunctionComponent + @@ -192,6 +188,6 @@ export const ComponentTemplateEdit: React.FunctionComponent - + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/scaled_float_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/scaled_float_datatype.test.tsx index 0b46189f72be1..e0cf56b964cb8 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/scaled_float_datatype.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/scaled_float_datatype.test.tsx @@ -7,7 +7,7 @@ import { act } from 'react-dom/test-utils'; -import { componentHelpers, MappingsEditorTestBed, kibanaVersion } from '../helpers'; +import { componentHelpers, MappingsEditorTestBed } from '../helpers'; const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; @@ -21,8 +21,7 @@ export const defaultScaledFloatParameters = { store: false, }; -// FLAKY: https://github.com/elastic/kibana/issues/145102 -describe.skip('Mappings editor: scaled float datatype', () => { +describe('Mappings editor: scaled float datatype', () => { /** * Variable to store the mappings data forwarded to the consumer component */ @@ -32,7 +31,7 @@ describe.skip('Mappings editor: scaled float datatype', () => { let testBed: MappingsEditorTestBed; beforeAll(() => { - jest.useFakeTimers({ legacyFakeTimers: true }); + jest.useFakeTimers(); }); afterAll(() => { @@ -90,15 +89,12 @@ describe.skip('Mappings editor: scaled float datatype', () => { await act(async () => { form.setInputValue('scalingFactor.input', '123'); }); + + component.update(); + await updateFieldAndCloseFlyout(); - expect(exists('mappingsEditorFieldEdit')).toBe(false); - if (kibanaVersion.major < 7) { - expect(exists('boostParameterToggle')).toBe(true); - } else { - // Since 8.x the boost parameter is deprecated - expect(exists('boostParameterToggle')).toBe(false); - } + expect(exists('mappingsEditorFieldEdit')).toBe(false); // It should have the default parameters values added, plus the scaling factor updatedMappings.properties.myField = { diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx index 764db2744f43f..7013a5df70800 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx @@ -9,9 +9,9 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import SemVer from 'semver/classes/semver'; -import { NormalizedField, Field as FieldType, ComboBoxOption } from '../../../../types'; +import { NormalizedField, Field as FieldType } from '../../../../types'; import { getFieldConfig } from '../../../../lib'; -import { UseField, FormDataProvider, NumericField, Field } from '../../../../shared_imports'; +import { UseField, useFormData, NumericField, Field } from '../../../../shared_imports'; import { StoreParameter, IndexParameter, @@ -48,28 +48,26 @@ interface Props { } export const NumericType = ({ field, kibanaVersion }: Props) => { + const [formData] = useFormData({ watch: 'subType' }); + return ( <> {/* scaling_factor */} - pathsToWatch="subType"> - {(formData) => { - return formData.subType?.[0]?.value === 'scaled_float' ? ( - - - - ) : null; - }} - + {formData.subType?.[0]?.value === 'scaled_float' ? ( + + + + ) : null} diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx index 406fd28b0e12e..89043c9fd7e69 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx @@ -20,7 +20,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { defaultMessage: 'Text', }), documentation: { - main: '/text.html', + main: 'text.html', }, description: () => (

@@ -49,7 +49,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { defaultMessage: 'Match only text', }), documentation: { - main: '/text.html#match-only-text-field-type', + main: 'text.html#match-only-text-field-type', }, description: () => (

@@ -78,7 +78,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { defaultMessage: 'Keyword', }), documentation: { - main: '/keyword.html', + main: 'keyword.html', }, description: () => (

@@ -107,7 +107,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { defaultMessage: 'Constant keyword', }), documentation: { - main: '/keyword.html#constant-keyword-field-type', + main: 'keyword.html#constant-keyword-field-type', }, description: () => (

@@ -127,7 +127,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { defaultMessage: 'Numeric', }), documentation: { - main: '/number.html', + main: 'number.html', }, subTypes: { label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.numericSubtypeDescription', { @@ -290,7 +290,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'date', documentation: { - main: '/date.html', + main: 'date.html', }, description: () => (

@@ -307,7 +307,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'date_nanos', documentation: { - main: '/date_nanos.html', + main: 'date_nanos.html', }, description: () => (

@@ -336,7 +336,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'binary', documentation: { - main: '/binary.html', + main: 'binary.html', }, description: () => (

@@ -353,7 +353,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'ip', documentation: { - main: '/ip.html', + main: 'ip.html', }, description: () => (

@@ -382,7 +382,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'boolean', documentation: { - main: '/boolean.html', + main: 'boolean.html', }, description: () => (

@@ -403,7 +403,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'range', documentation: { - main: '/range.html', + main: 'range.html', }, subTypes: { label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.rangeSubtypeDescription', { @@ -425,7 +425,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'object', documentation: { - main: '/object.html', + main: 'object.html', }, description: () => (

@@ -454,7 +454,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'nested', documentation: { - main: '/nested.html', + main: 'nested.html', }, description: () => (

@@ -483,7 +483,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'rank_feature', documentation: { - main: '/rank-feature.html', + main: 'rank-feature.html', }, description: () => (

@@ -512,7 +512,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'rank_features', documentation: { - main: '/rank-features.html', + main: 'rank-features.html', }, description: () => (

@@ -541,7 +541,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'dense_vector', documentation: { - main: '/dense-vector.html', + main: 'dense-vector.html', }, description: () => (

@@ -642,7 +642,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'geo_point', documentation: { - main: '/geo-point.html', + main: 'geo-point.html', }, description: () => (

@@ -659,7 +659,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'geo_shape', documentation: { - main: '/geo-shape.html', + main: 'geo-shape.html', learnMore: '/geo-shape.html#geoshape-indexing-approach', }, description: () => ( @@ -692,7 +692,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'completion', documentation: { - main: '/search-suggesters.html#completion-suggester', + main: 'search-suggesters.html#completion-suggester', }, description: () => (

@@ -709,7 +709,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'token_count', documentation: { - main: '/token-count.html', + main: 'token-count.html', }, description: () => (

@@ -726,7 +726,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'percolator', documentation: { - main: '/percolator.html', + main: 'percolator.html', }, description: () => (

@@ -755,7 +755,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'histogram', documentation: { - main: '/histogram.html', + main: 'histogram.html', }, description: () => (

@@ -772,7 +772,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'join', documentation: { - main: '/parent-join.html', + main: 'parent-join.html', }, description: () => (

@@ -789,7 +789,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'alias', documentation: { - main: '/alias.html', + main: 'alias.html', }, description: () => (

@@ -806,7 +806,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'search_as_you_type', documentation: { - main: '/search-as-you-type.html', + main: 'search-as-you-type.html', }, description: () => (

@@ -823,7 +823,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'flattened', documentation: { - main: '/flattened.html', + main: 'flattened.html', }, description: () => (

@@ -840,7 +840,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'shape', documentation: { - main: '/shape.html', + main: 'shape.html', }, description: () => (

@@ -857,7 +857,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'point', documentation: { - main: '/point.html', + main: 'point.html', }, description: () => (

@@ -877,7 +877,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'version', documentation: { - main: '/version.html', + main: 'version.html', }, description: () => (

@@ -906,7 +906,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { }), value: 'wildcard', documentation: { - main: '/keyword.html#wildcard-field-type', + main: 'keyword.html#wildcard-field-type', }, description: () => (

diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts index fb152a41db568..629cfdc413c10 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts @@ -26,6 +26,7 @@ export { useForm, useFormContext, UseMultiFields, + useFormData, VALIDATION_TYPES, } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases.tsx index a948b9a999fa8..9dccf1b3e2e9f 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_aliases.tsx @@ -22,6 +22,7 @@ import { CodeEditor } from '@kbn/kibana-react-plugin/public'; import { Forms } from '../../../../../shared_imports'; import { useJsonStep } from './use_json_step'; +import { documentationService } from '../../../mappings_editor/shared_imports'; interface Props { defaultValue?: { [key: string]: any }; @@ -65,7 +66,7 @@ export const StepAliases: React.FunctionComponent = React.memo( diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx index 3964e9d9d9c80..62685a05f7ff9 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_mappings.tsx @@ -25,6 +25,8 @@ import { IndexSettings, } from '../../../mappings_editor'; +import { documentationService } from '../../../mappings_editor/shared_imports'; + interface Props { onChange: (content: Forms.Content) => void; esDocsBase: string; @@ -90,7 +92,7 @@ export const StepMappings: React.FunctionComponent = React.memo( diff --git a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx index 0fd889de03921..252ca4c9b04ee 100644 --- a/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx +++ b/x-pack/plugins/index_management/public/application/components/shared/components/wizard_steps/step_settings.tsx @@ -22,6 +22,7 @@ import { CodeEditor } from '@kbn/kibana-react-plugin/public'; import { Forms } from '../../../../../shared_imports'; import { useJsonStep } from './use_json_step'; +import { documentationService } from '../../../mappings_editor/shared_imports'; interface Props { onChange: (content: Forms.Content) => void; @@ -65,7 +66,7 @@ export const StepSettings: React.FunctionComponent = React.memo( diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx index 9ca8a6878fb50..dc6ccf1a77528 100644 --- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx @@ -19,6 +19,7 @@ import { import { ComponentTemplateListItem } from '../../../../../common'; import { Forms } from '../../../../shared_imports'; import { ComponentTemplatesSelector } from '../../component_templates'; +import { documentationService } from '../../mappings_editor/shared_imports'; interface Props { esDocsBase: string; @@ -63,7 +64,7 @@ export const StepComponents = ({ defaultValue, onChange, esDocsBase }: Props) => ); const showHeader = state.isLoadingComponents === true || state.components.length > 0; - const docUri = `${esDocsBase}/indices-component-template.html`; + const docUri = documentationService.getIndicesComponentTemplate(); const renderHeader = () => { if (!showHeader) { diff --git a/x-pack/plugins/index_management/public/application/lib/discover_link.test.tsx b/x-pack/plugins/index_management/public/application/lib/discover_link.test.tsx new file mode 100644 index 0000000000000..0127f914715c3 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/lib/discover_link.test.tsx @@ -0,0 +1,89 @@ +/* + * 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. + */ +import React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; +import { EuiButtonIcon } from '@elastic/eui'; +import { DiscoverLink } from './discover_link'; +import { AppContextProvider, AppDependencies } from '../app_context'; + +describe('DiscoverLink', () => { + const indexName = 'my-fancy-index'; + + it('renders the link as an icon by default', async () => { + const navigateMock = jest.fn(); + const ctx = { + url: { + locators: { + get: () => ({ navigate: navigateMock }), + }, + }, + } as unknown as AppDependencies; + + const component = mountWithIntl( + + + + ); + + expect(component.exists('[data-test-subj="discoverIconLink"]')).toBe(true); + expect(component.exists('[data-test-subj="discoverButtonLink"]')).toBe(false); + }); + + it('renders the link as a button if the prop is set', async () => { + const navigateMock = jest.fn(); + const ctx = { + url: { + locators: { + get: () => ({ navigate: navigateMock }), + }, + }, + } as unknown as AppDependencies; + + const component = mountWithIntl( + + + + ); + + expect(component.exists('[data-test-subj="discoverIconLink"]')).toBe(false); + expect(component.exists('[data-test-subj="discoverButtonLink"]')).toBe(true); + }); + + it('calls navigate method when button is clicked', async () => { + const navigateMock = jest.fn(); + const ctx = { + url: { + locators: { + get: () => ({ navigate: navigateMock }), + }, + }, + } as unknown as AppDependencies; + + const component = mountWithIntl( + + + + ); + const button = component.find(EuiButtonIcon); + + await button.simulate('click'); + expect(navigateMock).toHaveBeenCalledWith({ dataViewSpec: { title: indexName } }); + }); + + it('does not render a button if locators is not defined', () => { + const ctx = {} as unknown as AppDependencies; + + const component = mountWithIntl( + + + + ); + const button = component.find(EuiButtonIcon); + + expect(button).toHaveLength(0); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/lib/discover_link.tsx b/x-pack/plugins/index_management/public/application/lib/discover_link.tsx new file mode 100644 index 0000000000000..aaec455c90932 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/lib/discover_link.tsx @@ -0,0 +1,62 @@ +/* + * 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. + */ + +import React from 'react'; +import { EuiButton, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useAppContext } from '../app_context'; + +export const DiscoverLink = ({ + indexName, + asButton = false, +}: { + indexName: string; + asButton?: boolean; +}) => { + const { url } = useAppContext(); + const discoverLocator = url?.locators.get('DISCOVER_APP_LOCATOR'); + if (!discoverLocator) { + return null; + } + const onClick = async () => { + await discoverLocator.navigate({ dataViewSpec: { title: indexName } }); + }; + + let link = ( + + ); + if (asButton) { + link = ( + + + + ); + } + + return ( + + {link} + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/lib/indices.ts b/x-pack/plugins/index_management/public/application/lib/indices.ts index 6d4bbc992a21c..61b2c7c2fbb49 100644 --- a/x-pack/plugins/index_management/public/application/lib/indices.ts +++ b/x-pack/plugins/index_management/public/application/lib/indices.ts @@ -4,25 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import SemVer from 'semver/classes/semver'; -import { MAJOR_VERSION } from '../../../common'; import { Index } from '../../../common'; -const kibanaVersion = new SemVer(MAJOR_VERSION); - export const isHiddenIndex = (index: Index): boolean => { - if (kibanaVersion.major < 8) { - // In 7.x we consider hidden index all indices whose name start with a dot - return (index.name ?? '').startsWith('.') || index.hidden === true; - } return index.hidden === true; }; - -export const isSystemIndex = (index: Index): boolean => { - if (kibanaVersion.major < 8) { - return (index.name ?? '').startsWith('.'); - } - // From 8.0 we won't surface system indices in Index management - return false; -}; diff --git a/x-pack/plugins/index_management/public/application/lib/manage_angular_lifecycle.ts b/x-pack/plugins/index_management/public/application/lib/manage_angular_lifecycle.ts deleted file mode 100644 index 3002b041ac3ee..0000000000000 --- a/x-pack/plugins/index_management/public/application/lib/manage_angular_lifecycle.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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. - */ - -import { unmountComponentAtNode } from 'react-dom'; - -export const manageAngularLifecycle = ($scope: any, $route: any, elem: HTMLElement | null) => { - const lastRoute = $route.current; - - const deregister = $scope.$on('$locationChangeSuccess', () => { - const currentRoute = $route.current; - if (lastRoute.$$route.template === currentRoute.$$route.template) { - $route.current = lastRoute; - } - }); - - $scope.$on('$destroy', () => { - if (deregister) { - deregister(); - } - - if (elem) { - unmountComponentAtNode(elem); - } - }); -}; diff --git a/x-pack/plugins/index_management/public/application/lib/render_discover_link.test.tsx b/x-pack/plugins/index_management/public/application/lib/render_discover_link.test.tsx deleted file mode 100644 index e768aae88581c..0000000000000 --- a/x-pack/plugins/index_management/public/application/lib/render_discover_link.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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. - */ -import React from 'react'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; -import { EuiButtonIcon } from '@elastic/eui'; -import { renderDiscoverLink } from './render_discover_link'; -import { AppContextProvider, AppDependencies } from '../app_context'; - -describe('renderDiscoverLink', () => { - const indexName = 'my-fancy-index'; - - it('calls navigate method when button is clicked', async () => { - const navigateMock = jest.fn(); - const ctx = { - url: { - locators: { - get: () => ({ navigate: navigateMock }), - }, - }, - } as unknown as AppDependencies; - - const component = mountWithIntl( - {renderDiscoverLink(indexName)} - ); - const button = component.find(EuiButtonIcon); - - await button.simulate('click'); - expect(navigateMock).toHaveBeenCalledWith({ dataViewSpec: { title: indexName } }); - }); - - it('does not render a button if locators is not defined', () => { - const ctx = {} as unknown as AppDependencies; - - const component = mountWithIntl( - {renderDiscoverLink(indexName)} - ); - const button = component.find(EuiButtonIcon); - - expect(button).toHaveLength(0); - }); -}); diff --git a/x-pack/plugins/index_management/public/application/lib/render_discover_link.tsx b/x-pack/plugins/index_management/public/application/lib/render_discover_link.tsx deleted file mode 100644 index d831f532c9b50..0000000000000 --- a/x-pack/plugins/index_management/public/application/lib/render_discover_link.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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. - */ - -import React from 'react'; -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { AppContextConsumer } from '../app_context'; - -export const renderDiscoverLink = (indexName: string) => { - return ( - - {(ctx) => { - const locators = ctx?.url?.locators.get('DISCOVER_APP_LOCATOR'); - - if (!locators) { - return null; - } - const onClick = async () => { - await locators.navigate({ dataViewSpec: { title: indexName } }); - }; - return ( - - - - ); - }} - - ); -}; diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index 9215b077c0bc3..df723e21ae287 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -56,6 +56,7 @@ export async function mountManagementSection({ enableIndexActions = true, enableLegacyTemplates = true, enableIndexDetailsPage = false, + enableIndexStats = true, }: { coreSetup: CoreSetup; usageCollection: UsageCollectionSetup; @@ -66,6 +67,7 @@ export async function mountManagementSection({ enableIndexActions?: boolean; enableLegacyTemplates?: boolean; enableIndexDetailsPage?: boolean; + enableIndexStats?: boolean; }) { const { element, setBreadcrumbs, history, theme$ } = params; const [core, startDependencies] = await coreSetup.getStartServices(); @@ -111,6 +113,7 @@ export async function mountManagementSection({ enableIndexActions, enableLegacyTemplates, enableIndexDetailsPage, + enableIndexStats, }, history, setBreadcrumbs, diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx index 5e42cecf2034f..d3d8b0bc64baf 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_detail_panel.tsx @@ -24,7 +24,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { renderDiscoverLink } from '../../../../lib/render_discover_link'; +import { DiscoverLink } from '../../../../lib/discover_link'; import { SectionLoading, reactRouterNavigate } from '../../../../../shared_imports'; import { SectionError, Error, DataHealth } from '../../../../components'; import { useLoadDataStream } from '../../../../services/api'; @@ -266,7 +266,7 @@ export const DataStreamDetailPanel: React.FunctionComponent = ({

{dataStreamName} - {renderDiscoverLink(dataStreamName)} + {dataStream && }

diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx index ecce9b92ffc2f..0d17936ec7553 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -16,7 +16,7 @@ import { EuiText, EuiIconTip, EuiSpacer, - EuiPageContent_Deprecated as EuiPageContent, + EuiPageSection, EuiEmptyPrompt, EuiLink, } from '@elastic/eui'; @@ -270,7 +270,7 @@ export const DataStreamList: React.FunctionComponent + {renderHeader()} @@ -285,7 +285,7 @@ export const DataStreamList: React.FunctionComponent - + ); } diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js index bac3e93978287..bddd3665f0b7c 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/detail_panel.js @@ -34,8 +34,8 @@ import { IndexActionsContextMenu } from '../index_actions_context_menu'; import { ShowJson } from './show_json'; import { Summary } from './summary'; import { EditSettingsJson } from './edit_settings_json'; -import { useServices } from '../../../../app_context'; -import { renderDiscoverLink } from '../../../../lib/render_discover_link'; +import { useServices, useAppContext } from '../../../../app_context'; +import { DiscoverLink } from '../../../../lib/discover_link'; const tabToHumanizedMap = { [TAB_SUMMARY]: ( @@ -58,12 +58,19 @@ const tabToHumanizedMap = { ), }; -const tabs = [TAB_SUMMARY, TAB_SETTINGS, TAB_MAPPING, TAB_STATS, TAB_EDIT_SETTINGS]; +const getTabs = (showStats) => { + if (showStats) { + return [TAB_SUMMARY, TAB_SETTINGS, TAB_MAPPING, TAB_STATS, TAB_EDIT_SETTINGS]; + } + return [TAB_SUMMARY, TAB_SETTINGS, TAB_MAPPING, TAB_EDIT_SETTINGS]; +}; export const DetailPanel = ({ panelType, indexName, index, openDetailPanel, closeDetailPanel }) => { const { extensionsService } = useServices(); + const { config } = useAppContext(); const renderTabs = () => { + const tabs = getTabs(config.enableIndexStats); return tabs.map((tab, i) => { const isSelected = tab === panelType; return ( @@ -108,10 +115,9 @@ export const DetailPanel = ({ panelType, indexName, index, openDetailPanel, clos key="menu" render={() => (

{indexName} - {renderDiscoverLink(indexName)} + {renderBadges(index, undefined, extensionsService)}

diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js index d01ee49320070..e41769daacbd5 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/detail_panel/summary/summary.js @@ -21,36 +21,43 @@ import { import { DataHealth } from '../../../../../components'; import { AppContextConsumer } from '../../../../../app_context'; -const getHeaders = () => { - return { - health: i18n.translate('xpack.idxMgmt.summary.headers.healthHeader', { - defaultMessage: 'Health', - }), - status: i18n.translate('xpack.idxMgmt.summary.headers.statusHeader', { - defaultMessage: 'Status', - }), +const getHeaders = (showStats) => { + const baseHeaders = { primary: i18n.translate('xpack.idxMgmt.summary.headers.primaryHeader', { defaultMessage: 'Primaries', }), replica: i18n.translate('xpack.idxMgmt.summary.headers.replicaHeader', { defaultMessage: 'Replicas', }), - documents: i18n.translate('xpack.idxMgmt.summary.headers.documentsHeader', { - defaultMessage: 'Docs count', - }), - documents_deleted: i18n.translate('xpack.idxMgmt.summary.headers.deletedDocumentsHeader', { - defaultMessage: 'Docs deleted', - }), - size: i18n.translate('xpack.idxMgmt.summary.headers.storageSizeHeader', { - defaultMessage: 'Storage size', - }), - primary_size: i18n.translate('xpack.idxMgmt.summary.headers.primaryStorageSizeHeader', { - defaultMessage: 'Primary storage size', - }), aliases: i18n.translate('xpack.idxMgmt.summary.headers.aliases', { defaultMessage: 'Aliases', }), }; + + if (showStats) { + return { + ...baseHeaders, + health: i18n.translate('xpack.idxMgmt.summary.headers.healthHeader', { + defaultMessage: 'Health', + }), + status: i18n.translate('xpack.idxMgmt.summary.headers.statusHeader', { + defaultMessage: 'Status', + }), + documents: i18n.translate('xpack.idxMgmt.summary.headers.documentsHeader', { + defaultMessage: 'Docs count', + }), + documents_deleted: i18n.translate('xpack.idxMgmt.summary.headers.deletedDocumentsHeader', { + defaultMessage: 'Docs deleted', + }), + size: i18n.translate('xpack.idxMgmt.summary.headers.storageSizeHeader', { + defaultMessage: 'Storage size', + }), + primary_size: i18n.translate('xpack.idxMgmt.summary.headers.primaryStorageSizeHeader', { + defaultMessage: 'Primary storage size', + }), + }; + } + return baseHeaders; }; export class Summary extends React.PureComponent { @@ -67,9 +74,9 @@ export class Summary extends React.PureComponent { }); } - buildRows() { + buildRows(config) { const { index } = this.props; - const headers = getHeaders(); + const headers = getHeaders(config.enableIndexStats); const rows = { left: [], right: [], @@ -84,7 +91,7 @@ export class Summary extends React.PureComponent { content = content.join(', '); } const cell = [ - + {headers[fieldName]} , @@ -103,8 +110,8 @@ export class Summary extends React.PureComponent { render() { return ( - {({ services, core }) => { - const { left, right } = this.buildRows(); + {({ services, core, config }) => { + const { left, right } = this.buildRows(config); const additionalContent = this.getAdditionalContent( services.extensionsService, core.getUrlForApp 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 80ba8cd4281d9..6d91b3a7991a1 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 @@ -5,12 +5,25 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Redirect, RouteComponentProps } from 'react-router-dom'; import { Route, Routes } from '@kbn/shared-ux-router'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiPageHeader, EuiSpacer, EuiPageHeaderProps } from '@elastic/eui'; +import { + EuiPageHeader, + EuiSpacer, + EuiPageHeaderProps, + EuiPageSection, + EuiButton, +} from '@elastic/eui'; +import { SectionLoading } from '@kbn/es-ui-shared-plugin/public'; + +import { Index } from '../../../../../../common'; +import { loadIndex } from '../../../../services'; +import { DiscoverLink } from '../../../../lib/discover_link'; import { Section } from '../../home'; +import { DetailsPageError } from './details_page_error'; +import { ManageIndexButton } from './manage_index_button'; export enum IndexDetailsSection { Overview = 'overview', @@ -59,6 +72,27 @@ export const DetailsPage: React.FunctionComponent< }, history, }) => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(); + const [index, setIndex] = useState(); + + const fetchIndexDetails = useCallback(async () => { + setIsLoading(true); + try { + const { data, error: loadingError } = await loadIndex(indexName); + setIsLoading(false); + setError(loadingError); + setIndex(data); + } catch (e) { + setIsLoading(false); + setError(e); + } + }, [indexName]); + + useEffect(() => { + fetchIndexDetails(); + }, [fetchIndexDetails]); + const onSectionChange = useCallback( (newSection: IndexDetailsSection) => { return history.push(encodeURI(`/indices/${indexName}/${newSection}`)); @@ -66,6 +100,10 @@ export const DetailsPage: React.FunctionComponent< [history, indexName] ); + const navigateToAllIndices = useCallback(() => { + history.push(`/${Section.Indices}`); + }, [history]); + const headerTabs = useMemo(() => { return tabs.map((tab) => ({ onClick: () => onSectionChange(tab.id), @@ -76,13 +114,51 @@ export const DetailsPage: React.FunctionComponent< })); }, [indexDetailsSection, onSectionChange]); + if (isLoading && !index) { + return ( + + + + ); + } + if (error || !index) { + return ; + } + return ( <> + + + + + + + + , + , + ]} tabs={headerTabs} /> @@ -116,6 +192,11 @@ export const DetailsPage: React.FunctionComponent< />