[Index details page] Implement index actions (#164741)

## Summary

Addresses https://github.com/elastic/kibana/issues/164546

Follow up to https://github.com/elastic/kibana/pull/163521 and
https://github.com/elastic/kibana/pull/163955

This PR re-implements index actions in the context menu on the index
details page. The actions are implemented without redux which is used in
the old index details flyout (to be removed when this work is complete)
and in the indices list. The PR introduces a declaration file to list
all props of the component `IndexActionsContextMenu` written in JS.
There is also a new component `ManageIndexButton` that implements index
actions specifically to be executed on the new index details page. In
the future most of the code in the component `ManageIndexButton` can be
re-used when more refactorings will be made (switching to TS and not
using redux in the indices list).

All index actions are async and I added a loading indicator to the
context menu button to indicate that requests are in flight updating the
index.

### Screen recordings


c39f1450-b495-4c50-b4ca-8989a2259ed5

Add/remove ILM policy actions with a confirmation modal



964931c9-b926-4ed4-aa5c-218f52881131





### How to test
1. Add `xpack.index_management.dev.enableIndexDetailsPage: true` to your
`/config/kibana.dev.yml` file
7. Start ES and Kibana with `yarn es snapshot` and `yarn start`
8. Add several indices to test with the command `PUT /test_index` in Dev
Tools Console
9. Navigate to Index Management and click the name of any index
10. Check index actions: 

- [x] Close index
- [x] Open index
- [x] Force merge index
- [x] Refresh index
- [x] Clear index cache
- [x] Flush index
- [ ] Unfreeze index (not sure how to add a frozen index)
- [x] Delete index
- [x] ILM: add lifecycle policy
- [x] ILM: remove lifecycle policy
- [x] ILM: retry lifecycle policy (add any built-in policy and wait a
couple of minutes until the rollover fails)

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Yulia Čech 2023-08-28 16:19:44 +02:00 committed by GitHub
parent 0738a51b9c
commit 84b683bd7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 577 additions and 160 deletions

View file

@ -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<void>;
isOpened: () => boolean;
clickIndexAction: (indexAction: string) => Promise<void>;
confirmForcemerge: (numSegments: string) => Promise<void>;
confirmDelete: () => Promise<void>;
};
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,

View file

@ -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('<IndexDetailsPage />', () => {
let testBed: IndexDetailsPageTestBed;
@ -19,8 +20,8 @@ describe('<IndexDetailsPage />', () => {
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('<IndexDetailsPage />', () => {
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('<IndexDetailsPage />', () => {
});
});
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('<IndexDetailsPage />', () => {
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);
});
});
});

View file

@ -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',

View file

@ -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;

View file

@ -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<Index | null>();
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<EuiPageHeaderProps['tabs']>(() => {
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 (
<SectionLoading>
<FormattedMessage
@ -97,8 +124,8 @@ export const DetailsPage: React.FunctionComponent<
</SectionLoading>
);
}
if (error || !data) {
return <DetailsPageError indexName={indexName} resendRequest={resendRequest} />;
if (error || !index) {
return <DetailsPageError indexName={indexName} resendRequest={fetchIndexDetails} />;
}
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}
>
<FormattedMessage
id="xpack.idxMgmt.indexDetails.backToIndicesButtonLabel"
@ -127,10 +152,11 @@ export const DetailsPage: React.FunctionComponent<
bottomBorder
rightSideItems={[
<DiscoverLink indexName={indexName} asButton={true} />,
<IndexActionsContextMenuWithoutRedux
indexNames={[indexName]}
indices={[data]}
fill={false}
<ManageIndexButton
indexName={indexName}
indexDetails={index}
reloadIndexDetails={fetchIndexDetails}
navigateToAllIndices={navigateToAllIndices}
/>,
]}
tabs={headerTabs}
@ -166,6 +192,11 @@ export const DetailsPage: React.FunctionComponent<
/>
</Routes>
</div>
<EuiSpacer size="l" />
<div>
<pre>{JSON.stringify(index, null, 2)}</pre>
</div>
</>
);
};

View file

@ -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<typeof useLoadIndex>['resendRequest'];
resendRequest: () => Promise<void>;
}) => {
return (
<EuiPageTemplate.EmptyPrompt

View file

@ -0,0 +1,258 @@
/*
* 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, useCallback, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { HttpSetup } from '@kbn/core-http-browser';
import { Index } from '../../../../../../common';
import {
clearCacheIndices as clearCacheIndicesRequest,
closeIndices as closeIndicesRequest,
deleteIndices as deleteIndicesRequest,
flushIndices as flushIndicesRequest,
forcemergeIndices as forcemergeIndicesRequest,
openIndices as openIndicesRequest,
refreshIndices as refreshIndicesRequest,
unfreezeIndices as unfreezeIndicesRequest,
} from '../../../../services';
import { notificationService } from '../../../../services/notification';
import { httpService } from '../../../../services/http';
import {
IndexActionsContextMenu,
IndexActionsContextMenuProps,
} from '../index_actions_context_menu/index_actions_context_menu';
const getIndexStatusByName = (
indexNames: string[],
indices: Index[]
): IndexActionsContextMenuProps['indexStatusByName'] => {
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<void>;
navigateToAllIndices: () => void;
}
export const ManageIndexButton: FunctionComponent<Props> = ({
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<void>,
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 (
<IndexActionsContextMenu
indexNames={indexNames}
indices={indices}
indexStatusByName={indexStatusByName}
fill={false}
isLoading={isLoading}
// index actions
closeIndices={closeIndices}
openIndices={openIndices}
flushIndices={flushIndices}
refreshIndices={refreshIndices}
clearCacheIndices={clearCacheIndices}
unfreezeIndices={unfreezeIndices}
forcemergeIndices={forcemergeIndices}
deleteIndices={deleteIndices}
performExtensionAction={performExtensionAction}
reloadIndices={reloadIndices}
/>
);
};

View file

@ -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<void>;
openIndices: () => Promise<void>;
flushIndices: () => Promise<void>;
refreshIndices: () => Promise<void>;
clearCacheIndices: () => Promise<void>;
unfreezeIndices: () => Promise<void>;
forcemergeIndices: (maxNumSegments: string) => Promise<void>;
deleteIndices: () => Promise<void>;
// 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<void>,
successMessage: string
) => Promise<void>;
// 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<Props>;

View file

@ -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}
>
<EuiFieldNumber
data-test-subj="indexActionsForcemergeNumSegments"
onChange={(event) => {
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}
</EuiButton>

View file

@ -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<void>;
openIndices: ({}: { indexNames: string[] }) => Promise<void>;
flushIndices: ({}: { indexNames: string[] }) => Promise<void>;
refreshIndices: ({}: { indexNames: string[] }) => Promise<void>;
clearCacheIndices: ({}: { indexNames: string[] }) => Promise<void>;
unfreezeIndices: ({}: { indexNames: string[] }) => Promise<void>;
forcemergeIndices: ({}: { indexNames: string[]; maxNumSegments: number }) => Promise<void>;
deleteIndices: ({}: { indexNames: string[] }) => Promise<void>;
// 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<void>;
indexNames: string[];
successMessage: string;
}) => Promise<void>;
}
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<Props> = ({
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 <IndexActionsContextMenu indexNames={indexNames} indices={indices} {...props} {...rest} />;
};

View file

@ -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<Index>({
path: `${INTERNAL_API_BASE_PATH}/indices/${indexName}`,
method: 'get',
});
}

View file

@ -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';

View file

@ -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<Index>({
path: `${INTERNAL_API_BASE_PATH}/indices/${indexName}`,
method: 'get',
});
};

View file

@ -16,7 +16,9 @@ import {
import { httpService } from './http';
export const sendRequest = (config: SendRequestConfig): Promise<SendRequestResponse> => {
export const sendRequest = <T = any>(
config: SendRequestConfig
): Promise<SendRequestResponse<T>> => {
return _sendRequest(httpService.httpClient, config);
};