Skip to content

Commit

Permalink
[Index details page] Implement index actions (#164741)
Browse files Browse the repository at this point in the history
## Summary

Addresses #164546

Follow up to #163521 and
#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


https://github.com/elastic/kibana/assets/6585477/c39f1450-b495-4c50-b4ca-8989a2259ed5

Add/remove ILM policy actions with a confirmation modal



https://github.com/elastic/kibana/assets/6585477/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
  • Loading branch information
yuliacech authored Aug 28, 2023
1 parent 0738a51 commit 84b683b
Show file tree
Hide file tree
Showing 14 changed files with 577 additions and 160 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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, {
Expand All @@ -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);
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
9 changes: 9 additions & 0 deletions x-pack/plugins/index_management/common/types/indices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 84b683b

Please sign in to comment.