mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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 recordingsc39f1450
-b495-4c50-b4ca-8989a2259ed5 Add/remove ILM policy actions with a confirmation modal964931c9
-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:
parent
0738a51b9c
commit
84b683bd7a
14 changed files with 577 additions and 160 deletions
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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>;
|
|
@ -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>
|
||||
|
|
|
@ -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} />;
|
||||
};
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue