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 4caf94db5de79..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 @@ -18,11 +18,12 @@ 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; @@ -42,6 +43,9 @@ export interface IndexDetailsPageTestBed extends TestBed { contextMenu: { clickManageIndexButton: () => Promise; isOpened: () => boolean; + clickIndexAction: (indexAction: string) => Promise; + confirmForcemerge: (numSegments: string) => Promise; + confirmDelete: () => Promise; }; errorSection: { isDisplayed: () => boolean; @@ -108,6 +112,28 @@ export const setup = async ( 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, 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 a880933ec9fdc..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 @@ -9,7 +9,8 @@ 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 } from './mocks'; +import { testIndexMock, testIndexName } from './mocks'; +import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common'; describe('', () => { let testBed: IndexDetailsPageTestBed; @@ -19,8 +20,8 @@ describe('', () => { beforeEach(async () => { const mockEnvironment = setupEnvironment(); ({ httpSetup, httpRequestsMockHelpers } = mockEnvironment); - // test_index is configured in initialEntries of the memory router - httpRequestsMockHelpers.setLoadIndexDetailsResponse('test_index', testIndexMock); + // testIndexName is configured in initialEntries of the memory router + httpRequestsMockHelpers.setLoadIndexDetailsResponse(testIndexName, testIndexMock); await act(async () => { testBed = await setup(httpSetup, { @@ -36,9 +37,9 @@ describe('', () => { describe('error section', () => { beforeEach(async () => { - httpRequestsMockHelpers.setLoadIndexDetailsResponse('test_index', undefined, { + httpRequestsMockHelpers.setLoadIndexDetailsResponse(testIndexName, undefined, { statusCode: 400, - message: 'Data for index .apm-agent-configuration was not found', + message: `Data for index ${testIndexName} was not found`, }); await act(async () => { testBed = await setup(httpSetup); @@ -59,10 +60,17 @@ describe('', () => { }); }); + 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', () => { @@ -106,12 +114,140 @@ describe('', () => { expect(testBed.actions.discoverLinkExists()).toBe(true); }); - it('opens an index context menu when "manage index" button is clicked', async () => { - const { - actions: { contextMenu }, - } = testBed; - expect(contextMenu.isOpened()).toBe(false); - await testBed.actions.contextMenu.clickManageIndexButton(); - expect(contextMenu.isOpened()).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 index d96f1d0fac0dc..5e165fe0702e6 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 @@ -7,10 +7,11 @@ import { Index } from '../../../public'; +export const testIndexName = 'test_index'; export const testIndexMock: Index = { health: 'green', status: 'open', - name: 'test_index', + name: testIndexName, uuid: 'test1234', primary: '1', replica: '1', @@ -21,7 +22,6 @@ export const testIndexMock: Index = { isFrozen: false, aliases: 'none', hidden: false, - // @ts-expect-error ts upgrade v4.7.4 isRollupIndex: false, ilm: { index: 'test_index', diff --git a/x-pack/plugins/index_management/common/types/indices.ts b/x-pack/plugins/index_management/common/types/indices.ts index ab54812f2f0e3..608ee392a3f9e 100644 --- a/x-pack/plugins/index_management/common/types/indices.ts +++ b/x-pack/plugins/index_management/common/types/indices.ts @@ -64,6 +64,15 @@ export interface Index { hidden: boolean; aliases: string | string[]; data_stream?: string; + + // 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; 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 573bd1d7ee353..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,7 +5,7 @@ * 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'; @@ -17,11 +17,14 @@ import { 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 { useLoadIndex } from '../../../../services'; import { Section } from '../../home'; import { DetailsPageError } from './details_page_error'; -import { IndexActionsContextMenuWithoutRedux } from '../index_actions_context_menu/index_actions_context_menu.without_redux'; +import { ManageIndexButton } from './manage_index_button'; + export enum IndexDetailsSection { Overview = 'overview', Documents = 'documents', @@ -69,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}`)); @@ -76,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), @@ -86,8 +114,7 @@ export const DetailsPage: React.FunctionComponent< })); }, [indexDetailsSection, onSectionChange]); - const { isLoading, error, resendRequest, data } = useLoadIndex(indexName); - if (isLoading) { + if (isLoading && !index) { return ( ); } - if (error || !data) { - return ; + if (error || !index) { + return ; } return ( @@ -108,9 +135,7 @@ export const DetailsPage: React.FunctionComponent< data-test-subj="indexDetailsBackToIndicesButton" color="text" iconType="arrowLeft" - onClick={() => { - return history.push(`/${Section.Indices}`); - }} + onClick={navigateToAllIndices} > , - , ]} tabs={headerTabs} @@ -166,6 +192,11 @@ export const DetailsPage: React.FunctionComponent< /> + + +
+
{JSON.stringify(index, null, 2)}
+
); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_error.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_error.tsx index 6defc3d3db034..c1159cecfe09f 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_error.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_error.tsx @@ -8,14 +8,13 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiButton, EuiPageTemplate, EuiSpacer, EuiText } from '@elastic/eui'; -import { useLoadIndex } from '../../../../services'; export const DetailsPageError = ({ indexName, resendRequest, }: { indexName: string; - resendRequest: ReturnType['resendRequest']; + resendRequest: () => Promise; }) => { return ( { + const indexStatusByName: IndexActionsContextMenuProps['indexStatusByName'] = {}; + indexNames.forEach((indexName) => { + const { status } = indices.find((index) => index.name === indexName) ?? {}; + indexStatusByName[indexName] = status; + }); + return indexStatusByName; +}; + +interface Props { + indexName: string; + indexDetails: Index; + reloadIndexDetails: () => Promise; + navigateToAllIndices: () => void; +} +export const ManageIndexButton: FunctionComponent = ({ + indexName, + indexDetails, + reloadIndexDetails, + navigateToAllIndices, +}) => { + const [isLoading, setIsLoading] = useState(false); + + // the variables are created to write the index actions in a way to later re-use for indices list without redux + const indexNames = useMemo(() => [indexName], [indexName]); + + const reloadIndices = useCallback(async () => { + setIsLoading(true); + await reloadIndexDetails(); + setIsLoading(false); + }, [reloadIndexDetails]); + + const indices = [indexDetails]; + const indexStatusByName = getIndexStatusByName(indexNames, indices); + + const closeIndices = useCallback(async () => { + setIsLoading(true); + try { + await closeIndicesRequest(indexNames); + await reloadIndices(); + setIsLoading(false); + notificationService.showSuccessToast( + i18n.translate('xpack.idxMgmt.closeIndicesAction.successfullyClosedIndicesMessage', { + defaultMessage: 'Successfully closed: [{indexNames}]', + values: { indexNames: indexNames.join(', ') }, + }) + ); + } catch (error) { + setIsLoading(false); + notificationService.showDangerToast(error.body.message); + } + }, [reloadIndices, indexNames]); + + const openIndices = useCallback(async () => { + setIsLoading(true); + try { + await openIndicesRequest(indexNames); + await reloadIndices(); + setIsLoading(false); + notificationService.showSuccessToast( + i18n.translate('xpack.idxMgmt.openIndicesAction.successfullyOpenedIndicesMessage', { + defaultMessage: 'Successfully opened: [{indexNames}]', + values: { indexNames: indexNames.join(', ') }, + }) + ); + } catch (error) { + setIsLoading(false); + notificationService.showDangerToast(error.body.message); + } + }, [reloadIndices, indexNames]); + + const flushIndices = useCallback(async () => { + setIsLoading(true); + try { + await flushIndicesRequest(indexNames); + await reloadIndices(); + setIsLoading(false); + notificationService.showSuccessToast( + i18n.translate('xpack.idxMgmt.flushIndicesAction.successfullyFlushedIndicesMessage', { + defaultMessage: 'Successfully flushed: [{indexNames}]', + values: { indexNames: indexNames.join(', ') }, + }) + ); + } catch (error) { + setIsLoading(false); + notificationService.showDangerToast(error.body.message); + } + }, [reloadIndices, indexNames]); + + const refreshIndices = useCallback(async () => { + setIsLoading(true); + try { + await refreshIndicesRequest(indexNames); + await reloadIndices(); + setIsLoading(false); + notificationService.showSuccessToast( + i18n.translate('xpack.idxMgmt.refreshIndicesAction.successfullyRefreshedIndicesMessage', { + defaultMessage: 'Successfully refreshed: [{indexNames}]', + values: { indexNames: indexNames.join(', ') }, + }) + ); + } catch (error) { + setIsLoading(false); + notificationService.showDangerToast(error.body.message); + } + }, [reloadIndices, indexNames]); + + const clearCacheIndices = useCallback(async () => { + setIsLoading(true); + try { + await clearCacheIndicesRequest(indexNames); + await reloadIndices(); + setIsLoading(false); + notificationService.showSuccessToast( + i18n.translate('xpack.idxMgmt.clearCacheIndicesAction.successMessage', { + defaultMessage: 'Successfully cleared cache: [{indexNames}]', + values: { indexNames: indexNames.join(', ') }, + }) + ); + } catch (error) { + setIsLoading(false); + notificationService.showDangerToast(error.body.message); + } + }, [reloadIndices, indexNames]); + + const unfreezeIndices = useCallback(async () => { + setIsLoading(true); + try { + await unfreezeIndicesRequest(indexNames); + await reloadIndices(); + setIsLoading(false); + notificationService.showSuccessToast( + i18n.translate('xpack.idxMgmt.unfreezeIndicesAction.successfullyUnfrozeIndicesMessage', { + defaultMessage: 'Successfully unfroze: [{indexNames}]', + values: { indexNames: indexNames.join(', ') }, + }) + ); + } catch (error) { + setIsLoading(false); + notificationService.showDangerToast(error.body.message); + } + }, [reloadIndices, indexNames]); + + const forcemergeIndices = useCallback( + async (maxNumSegments: string) => { + setIsLoading(true); + try { + await forcemergeIndicesRequest(indexNames, maxNumSegments); + await reloadIndices(); + setIsLoading(false); + notificationService.showSuccessToast( + i18n.translate( + 'xpack.idxMgmt.forceMergeIndicesAction.successfullyForceMergedIndicesMessage', + { + defaultMessage: 'Successfully force merged: [{indexNames}]', + values: { indexNames: indexNames.join(', ') }, + } + ) + ); + } catch (error) { + setIsLoading(false); + notificationService.showDangerToast(error.body.message); + } + }, + [reloadIndices, indexNames] + ); + + const deleteIndices = useCallback(async () => { + setIsLoading(true); + try { + await deleteIndicesRequest(indexNames); + setIsLoading(false); + notificationService.showSuccessToast( + i18n.translate('xpack.idxMgmt.deleteIndicesAction.successfullyDeletedIndicesMessage', { + defaultMessage: 'Successfully deleted: [{indexNames}]', + values: { indexNames: indexNames.join(', ') }, + }) + ); + navigateToAllIndices(); + } catch (error) { + setIsLoading(false); + notificationService.showDangerToast(error.body.message); + } + }, [navigateToAllIndices, indexNames]); + + const performExtensionAction = useCallback( + async ( + requestMethod: (indexNames: string[], http: HttpSetup) => Promise, + successMessage: string + ) => { + setIsLoading(true); + try { + await requestMethod(indexNames, httpService.httpClient); + await reloadIndices(); + setIsLoading(false); + notificationService.showSuccessToast(successMessage); + } catch (error) { + setIsLoading(false); + notificationService.showDangerToast(error.body.message); + } + }, + [reloadIndices, indexNames] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.d.ts b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.d.ts new file mode 100644 index 0000000000000..896622ac0e9fb --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.d.ts @@ -0,0 +1,68 @@ +/* + * 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 type { ClassComponent, Component } from 'react'; +import type { HttpSetup } from '@kbn/core-http-browser'; +import type { EuiPopoverProps, EuiButtonProps } from '@elastic/eui'; +import type { Index } from '../../../../../../common'; + +export interface IndexActionsContextMenuProps { + // either an array of indices selected in the list view or an array of 1 index name on the details panel/page + indexNames: string[]; + // indices data + indices: Index[]; + + // indicates if the context menu is on the list view (to show additional actions) + isOnListView?: boolean; + // a callback used to reset selected indices on the list view + resetSelection?: () => void; + + // these props are only set on the details panel to change style + anchorPosition?: EuiPopoverProps['anchorPosition']; + iconSide?: EuiButtonProps['iconSide']; + iconType?: EuiButtonProps['iconType']; + label?: Component; + + // index actions: functions are called with indexNames prop so no need to pass it as argument here + closeIndices: () => Promise; + openIndices: () => Promise; + flushIndices: () => Promise; + refreshIndices: () => Promise; + clearCacheIndices: () => Promise; + unfreezeIndices: () => Promise; + forcemergeIndices: (maxNumSegments: string) => Promise; + deleteIndices: () => Promise; + + // following 4 actions are only added when on the list view and only 1 index is selected + showSettings?: () => void; // opens the settings tab for the 1st index in the indexNames array + showMapping?: () => void; // opens the mapping tab for the 1st index in the indexNames array + showStats?: () => void; // opens the stats tab for the 1st index in the indexNames array + editIndex?: () => void; // opens the edit settings tab for the 1st index in the indexNames array + + // used to determine if all indices are open + indexStatusByName: { + [indexName: string]: Index['status'] | undefined; + }; + + // this function is called with an extension service action + performExtensionAction: ( + requestMethod: (indexNames: string[], http: HttpSetup) => Promise, + successMessage: string + ) => Promise; + // this function is called to "refresh" the indices data after and extension service action that uses a modal + reloadIndices: () => void; + + /** + * Props added to use the context menu on the new index details page + */ + // makes the button secondary + fill?: boolean; + // sets the button's loading state + isLoading?: boolean; +} + +export const IndexActionsContextMenu: ClassComponent; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js index 8fb7fa5410f72..54ba5dee8250e 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js @@ -198,6 +198,7 @@ export class IndexActionsContextMenu extends Component { }); } items.push({ + 'data-test-subj': 'deleteIndexMenuButton', name: i18n.translate('xpack.idxMgmt.indexActionsMenu.deleteIndexLabel', { defaultMessage: 'Delete {selectedIndexCount, plural, one {index} other {indices} }', values: { selectedIndexCount }, @@ -372,6 +373,7 @@ export class IndexActionsContextMenu extends Component { helpText={helpText} > { this.setState({ forcemergeSegments: event.target.value }); }} @@ -464,6 +466,7 @@ export class IndexActionsContextMenu extends Component { }), iconType = 'arrowDown', fill = true, + isLoading = false, } = this.props; const panels = this.panels(appDependencies); @@ -480,6 +483,7 @@ export class IndexActionsContextMenu extends Component { onClick={this.onButtonClick} iconType={iconType} fill={fill} + isLoading={isLoading} > {label} diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.without_redux.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.without_redux.tsx deleted file mode 100644 index 177bfc7350af0..0000000000000 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.without_redux.tsx +++ /dev/null @@ -1,107 +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, { FunctionComponent } from 'react'; -import { HttpSetup } from '@kbn/core-http-browser'; -import { EuiButtonProps } from '@elastic/eui/src/components/button/button'; -import { EuiPopoverProps } from '@elastic/eui/src/components/popover/popover'; -import { Index } from '../../../../../../common'; -import { reloadIndices } from '../../../../services'; -// @ts-ignore this component needs to be refactored into TS -import { IndexActionsContextMenu } from './index_actions_context_menu'; - -export interface ReduxProps { - closeIndices: ({}: { indexNames: string[] }) => Promise; - openIndices: ({}: { indexNames: string[] }) => Promise; - flushIndices: ({}: { indexNames: string[] }) => Promise; - refreshIndices: ({}: { indexNames: string[] }) => Promise; - clearCacheIndices: ({}: { indexNames: string[] }) => Promise; - unfreezeIndices: ({}: { indexNames: string[] }) => Promise; - forcemergeIndices: ({}: { indexNames: string[]; maxNumSegments: number }) => Promise; - deleteIndices: ({}: { indexNames: string[] }) => Promise; - - // following 4 actions are only added when on the list view and only 1 index is selected - showSettings: ({}: { indexNames: string[] }) => void; // opens the settings tab for the 1st index - showMapping: ({}: { indexNames: string[] }) => void; // opens the mapping tab for the 1st index - showStats: ({}: { indexNames: string[] }) => void; // opens the stats tab for the 1st index - editIndex: ({}: { indexNames: string[] }) => void; // opens the edit settings tab for the 1st index - - indexStatusByName: { - [indexName: string]: Index['status'] | undefined; - }; - reloadIndices: typeof reloadIndices; - - // this comes from the extension service - performExtensionAction: ({}: { - requestMethod: (indexNames: string[], httpClient: HttpSetup) => Promise; - indexNames: string[]; - successMessage: string; - }) => Promise; -} - -interface Props { - // either an array of indices selected in the list view or an array of 1 index name on details panel/page - indexNames: string[]; - - // indicates if the context menu is on the list view (to show additional actions) - isOnListView?: boolean; - // a callback used to reset selected indices on the list view - resetSelection?: () => void; - - // these props are only set on the details panel to change style - anchorPosition?: EuiPopoverProps['anchorPosition']; - iconSide?: EuiButtonProps['iconSide']; - iconType?: EuiButtonProps['iconType']; - label?: React.Component; - - // a new prop to make the button secondary - fill?: boolean; - - // instead of getting indices data from the redux store, pass it as a prop - indices: Index[]; -} - -const getIndexStatusByName = ( - indexNames: string[], - indices: Index[] -): ReduxProps['indexStatusByName'] => { - const indexStatusByName: ReduxProps['indexStatusByName'] = {}; - indexNames.forEach((indexName) => { - const { status } = indices.find((index) => index.name === indexName) ?? {}; - indexStatusByName[indexName] = status; - }); - return indexStatusByName; -}; - -export const IndexActionsContextMenuWithoutRedux: FunctionComponent = ({ - indexNames, - indices, - ...rest -}) => { - const props: ReduxProps = { - closeIndices: async () => {}, - openIndices: async () => {}, - flushIndices: async () => {}, - refreshIndices: async () => {}, - clearCacheIndices: async () => {}, - unfreezeIndices: async () => {}, - forcemergeIndices: async () => {}, - deleteIndices: async () => {}, - - // there actions are not displayed on the index details page - showSettings: () => {}, - showMapping: () => {}, - showStats: () => {}, - editIndex: () => {}, - - indexStatusByName: getIndexStatusByName(indexNames, indices), - reloadIndices: async () => {}, - - performExtensionAction: async () => {}, - }; - return ; -}; 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 5defc62ece088..b19382364722c 100644 --- a/x-pack/plugins/index_management/public/application/services/api.ts +++ b/x-pack/plugins/index_management/public/application/services/api.ts @@ -31,8 +31,9 @@ import { UIM_TEMPLATE_UPDATE, UIM_TEMPLATE_CLONE, UIM_TEMPLATE_SIMULATE, + INTERNAL_API_BASE_PATH, } from '../../../common/constants'; -import { TemplateDeserialized, TemplateListItem, DataStream } from '../../../common'; +import { TemplateDeserialized, TemplateListItem, DataStream, Index } from '../../../common'; import { TAB_SETTINGS, TAB_MAPPING, TAB_STATS } from '../constants'; import { useRequest, sendRequest } from './use_request'; import { httpService } from './http'; @@ -311,3 +312,10 @@ export function useLoadNodesPlugins() { method: 'get', }); } + +export function loadIndex(indexName: string) { + return sendRequest({ + path: `${INTERNAL_API_BASE_PATH}/indices/${indexName}`, + method: 'get', + }); +} 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 96e961b718bd9..5cb65c04b6c9d 100644 --- a/x-pack/plugins/index_management/public/application/services/index.ts +++ b/x-pack/plugins/index_management/public/application/services/index.ts @@ -24,10 +24,9 @@ export { useLoadIndexTemplates, simulateIndexTemplate, useLoadNodesPlugins, + loadIndex, } from './api'; -export { useLoadIndex } from './indices_api'; - export { sortTable } from './sort_table'; export { UiMetricService } from './ui_metric'; diff --git a/x-pack/plugins/index_management/public/application/services/indices_api.ts b/x-pack/plugins/index_management/public/application/services/indices_api.ts deleted file mode 100644 index c7e4cc5775ce5..0000000000000 --- a/x-pack/plugins/index_management/public/application/services/indices_api.ts +++ /dev/null @@ -1,16 +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 { useRequest } from './use_request'; -import { Index, INTERNAL_API_BASE_PATH } from '../../../common'; - -export const useLoadIndex = (indexName: string) => { - return useRequest({ - path: `${INTERNAL_API_BASE_PATH}/indices/${indexName}`, - method: 'get', - }); -}; diff --git a/x-pack/plugins/index_management/public/application/services/use_request.ts b/x-pack/plugins/index_management/public/application/services/use_request.ts index 3b1d5cf22452d..4746890361d59 100644 --- a/x-pack/plugins/index_management/public/application/services/use_request.ts +++ b/x-pack/plugins/index_management/public/application/services/use_request.ts @@ -16,7 +16,9 @@ import { import { httpService } from './http'; -export const sendRequest = (config: SendRequestConfig): Promise => { +export const sendRequest = ( + config: SendRequestConfig +): Promise> => { return _sendRequest(httpService.httpClient, config); };