diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 40c8fbf2b338b..868ef59f47966 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -598,7 +598,7 @@ Index Management by running this series of requests in Console: |{kib-repo}blob/{branch}/x-pack/plugins/index_management/README.md[indexManagement] -|Create an index with special characters and verify it renders correctly: +|This service is exposed from the Index Management setup contract and can be used to add content to the indices list and the index details page. |{kib-repo}blob/{branch}/x-pack/plugins/infra/README.md[infra] diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx index be64e1a4674aa..244536ad4afef 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/extend_index_management.test.tsx @@ -5,12 +5,12 @@ * 2.0. */ +import React from 'react'; import moment from 'moment-timezone'; import { init } from '../integration_tests/helpers/http_requests'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks'; -import { Index } from '../common/types'; import { retryLifecycleActionExtension, removeLifecyclePolicyActionExtension, @@ -20,8 +20,8 @@ import { } from '../public/extend_index_management'; import { init as initHttp } from '../public/application/services/http'; import { init as initUiMetric } from '../public/application/services/ui_metric'; -import { IndexLifecycleSummary } from '../public/extend_index_management/components/index_lifecycle_summary'; -import React from 'react'; +import { indexLifecycleTab } from '../public/extend_index_management/components/index_lifecycle_summary'; +import { Index } from '@kbn/index-management-plugin/common'; const { httpSetup } = init(); @@ -113,6 +113,7 @@ const indexWithLifecycleError: Index = { }, phase_execution: { policy: 'testy', + // @ts-expect-error ILM type is incorrect https://github.com/elastic/elasticsearch-specification/issues/2326 phase_definition: { min_age: '0s', actions: { rollover: { max_size: '1gb' } } }, version: 1, modified_date_in_millis: 1544031699844, @@ -243,29 +244,31 @@ describe('extend index management', () => { }); describe('ilm summary extension', () => { - test('should render null when index has no index lifecycle policy', () => { - const extension = ( - - ); - const rendered = mountWithIntl(extension); - expect(rendered.isEmptyRender()).toBeTruthy(); + const IlmComponent = indexLifecycleTab.renderTabContent; + test('should not render the tab when index has no index lifecycle policy', () => { + const shouldRenderTab = + indexLifecycleTab.shouldRenderTab && + indexLifecycleTab.shouldRenderTab({ + index: indexWithoutLifecyclePolicy, + }); + expect(shouldRenderTab).toBeFalsy(); }); test('should return extension when index has lifecycle policy', () => { - const extension = ( - + const ilmContent = ( + ); - expect(extension).toBeDefined(); - const rendered = mountWithIntl(extension); + expect(ilmContent).toBeDefined(); + const rendered = mountWithIntl(ilmContent); expect(rendered.render()).toMatchSnapshot(); }); test('should return extension when index has lifecycle error', () => { - const extension = ( - + const ilmContent = ( + ); - expect(extension).toBeDefined(); - const rendered = mountWithIntl(extension); + expect(ilmContent).toBeDefined(); + const rendered = mountWithIntl(ilmContent); expect(rendered.render()).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index fcc9e89da4796..c241ce6b5c58d 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { Index as IndexInterface } from '@kbn/index-management-plugin/common/types'; - export type Phase = keyof Phases; export type PhaseWithAllocation = 'warm' | 'cold'; @@ -244,7 +242,3 @@ export interface IndexLifecyclePolicy { }; step_time_millis?: number; } - -export interface Index extends IndexInterface { - ilm: IndexLifecyclePolicy; -} diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx index 015c1576de5db..79af627f578b1 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx @@ -25,10 +25,11 @@ import { EuiModalHeaderTitle, } from '@elastic/eui'; +import { Index } from '@kbn/index-management-plugin/common'; import { loadPolicies, addLifecyclePolicyToIndex } from '../../application/services/api'; import { showApiError } from '../../application/services/api_errors'; import { toasts } from '../../application/services/notification'; -import { Index, PolicyFromES } from '../../../common/types'; +import { PolicyFromES } from '../../../common/types'; interface Props { indexName: string; diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx index f1468117dc53c..162457479788f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx @@ -25,10 +25,12 @@ import { } from '@elastic/eui'; import { ApplicationStart } from '@kbn/core/public'; +import { Index } from '@kbn/index-management-plugin/common'; +import { IndexDetailsTab } from '@kbn/index-management-plugin/common/constants'; +import { IlmExplainLifecycleLifecycleExplainManaged } from '@elastic/elasticsearch/lib/api/types'; import { getPolicyEditPath } from '../../application/services/navigation'; -import { Index, IndexLifecyclePolicy } from '../../../common/types'; -const getHeaders = (): Array<[keyof IndexLifecyclePolicy, string]> => { +const getHeaders = (): Array<[keyof IlmExplainLifecycleLifecycleExplainManaged, string]> => { return [ [ 'policy', @@ -85,7 +87,9 @@ interface Props { export const IndexLifecycleSummary: FunctionComponent = ({ index, getUrlForApp }) => { const [showPhaseExecutionPopover, setShowPhaseExecutionPopover] = useState(false); - const { ilm } = index; + const { ilm: ilmData } = index; + // only ILM managed indices render the ILM tab + const ilm = ilmData as IlmExplainLifecycleLifecycleExplainManaged; const togglePhaseExecutionPopover = () => { setShowPhaseExecutionPopover(!showPhaseExecutionPopover); @@ -144,15 +148,15 @@ export const IndexLifecycleSummary: FunctionComponent = ({ index, getUrlF right: [], }; headers.forEach(([fieldName, label], arrayIndex) => { - const value: any = ilm[fieldName]; + const value = ilm[fieldName]; let content; if (fieldName === 'action_time_millis') { - content = moment(value).format('YYYY-MM-DD HH:mm:ss'); + content = moment(value as string).format('YYYY-MM-DD HH:mm:ss'); } else if (fieldName === 'policy') { content = ( {value} @@ -184,9 +188,6 @@ export const IndexLifecycleSummary: FunctionComponent = ({ index, getUrlF return rows; }; - if (!ilm.managed) { - return null; - } const { left, right } = buildRows(); return ( <> @@ -243,3 +244,18 @@ export const IndexLifecycleSummary: FunctionComponent = ({ index, getUrlF ); }; + +export const indexLifecycleTab: IndexDetailsTab = { + id: 'ilm', + name: ( + + ), + order: 50, + renderTabContent: IndexLifecycleSummary, + shouldRenderTab: ({ index }) => { + return !!index.ilm && index.ilm.managed; + }, +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx index 9d3d14f11e685..519a0606c36aa 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/index.tsx @@ -11,20 +11,19 @@ import { i18n } from '@kbn/i18n'; import { EuiSearchBar } from '@elastic/eui'; import { ApplicationStart } from '@kbn/core/public'; -import { IndexManagementPluginSetup } from '@kbn/index-management-plugin/public'; +import { Index, IndexManagementPluginSetup } from '@kbn/index-management-plugin/public'; import { retryLifecycleForIndex } from '../application/services/api'; -import { IndexLifecycleSummary } from './components/index_lifecycle_summary'; +import { indexLifecycleTab } from './components/index_lifecycle_summary'; import { AddLifecyclePolicyConfirmModal } from './components/add_lifecycle_confirm_modal'; import { RemoveLifecyclePolicyConfirmModal } from './components/remove_lifecycle_confirm_modal'; -import { Index } from '../../common/types'; const stepPath = 'ilm.step'; export const retryLifecycleActionExtension = ({ indices }: { indices: Index[] }) => { const allHaveErrors = every(indices, (index) => { - return index.ilm && index.ilm.failed_step; + return index.ilm?.managed && index.ilm.failed_step; }); if (!allHaveErrors) { return null; @@ -224,6 +223,7 @@ export const addAllExtensions = ( extensionsService.addAction(addLifecyclePolicyActionExtension); extensionsService.addBanner(ilmBannerExtension); - extensionsService.addSummary(IndexLifecycleSummary); extensionsService.addFilter(ilmFilterExtension); + + extensionsService.addIndexDetailsTab(indexLifecycleTab); }; diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 011825ed9bf77..6b21a477866bb 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -9,9 +9,8 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Plugin, Logger, PluginInitializerContext } from '@kbn/core/server'; import { IScopedClusterClient } from '@kbn/core/server'; -import { Index as IndexWithoutIlm } from '@kbn/index-management-plugin/common/types'; +import { Index } from '@kbn/index-management-plugin/common/types'; import { PLUGIN } from '../common/constants'; -import { Index } from '../common/types'; import { Dependencies } from './types'; import { registerApiRoutes } from './routes'; import { License } from './services'; @@ -19,7 +18,7 @@ import { IndexLifecycleManagementConfig } from './config'; import { handleEsError } from './shared_imports'; const indexLifecycleDataEnricher = async ( - indicesList: IndexWithoutIlm[], + indicesList: Index[], client: IScopedClusterClient ): Promise => { if (!indicesList || !indicesList.length) { @@ -29,8 +28,7 @@ const indexLifecycleDataEnricher = async ( const { indices: ilmIndicesData } = await client.asCurrentUser.ilm.explainLifecycle({ index: '*', }); - // @ts-expect-error IndexLifecyclePolicy is not compatible with IlmExplainLifecycleResponse - return indicesList.map((index: IndexWithoutIlm) => { + return indicesList.map((index: Index) => { return { ...index, ilm: { ...(ilmIndicesData[index.name] || {}) }, diff --git a/x-pack/plugins/index_management/README.md b/x-pack/plugins/index_management/README.md index 8ac2837a683b6..8673447fc577c 100644 --- a/x-pack/plugins/index_management/README.md +++ b/x-pack/plugins/index_management/README.md @@ -1,4 +1,47 @@ # Index Management UI +## Extensions service +This service is exposed from the Index Management setup contract and can be used to add content to the indices list and the index details page. +### Extensions to the indices list +- `addBanner(banner: any)`: adds a banner on top of the indices list, for example when some indices run into an ILM issue +- `addFilter(filter: any)`: adds a filter to the indices list, for example to filter indices managed by ILM +- `addToggle(toggle: any)`: adds a toggle to the indices list, for example to display hidden indices + +#### Extensions to the indices list and the index details page +- `addAction(action: any)`: adds an option to the "manage index" menu, for example to add an ILM policy to the index +- `addBadge(badge: any)`: adds a badge to the index name, for example to indicate frozen, rollup or follower indices + +#### Extensions to the index details page +- `addIndexDetailsTab(tab: IndexDetailsTab)`: adds a tab to the index details page. The tab has the following interface: + +```ts +interface IndexDetailsTab { + // a unique key to identify the tab + id: IndexDetailsTabId; + // a text that is displayed on the tab label, usually a Formatted message component + name: ReactNode; + // a function that renders the content of the tab + renderTabContent: (args: { + index: Index; + getUrlForApp: ApplicationStart['getUrlForApp']; + }) => ReturnType; + // a number to specify the order of the tabs + order: number; + // an optional function to return a boolean for when to render the tab + // if omitted, the tab is always rendered + shouldRenderTab?: (args: { index: Index }) => boolean; +} +``` + +An example of adding an ILM tab can be found in [this file](https://github.com/elastic/kibana/blob/main/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx#L250). + +- `setIndexOverviewContent(content: IndexOverviewContent)`: replaces the default content in the overview tab (code block describing adding documents to the index) with the custom content. The custom content has the following interface: +```ts +interface IndexOverviewContent { + renderContent: (args: { + index: Index; + getUrlForApp: ApplicationStart['getUrlForApp']; + }) => ReturnType; +``` ## Indices tab diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts index 2085368fc7760..49216eb285498 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts @@ -10,15 +10,6 @@ import { act } from 'react-dom/test-utils'; import { setupEnvironment, nextTick } from '../helpers'; import { HomeTestBed, setup } from './home.helpers'; -/** - * The below import is required to avoid a console error warn from the "brace" package - * console.warn ../node_modules/brace/index.js:3999 - Could not load worker ReferenceError: Worker is not defined - at createWorker (//node_modules/brace/index.js:17992:5) - */ -import { stubWebWorker } from '@kbn/test-jest-helpers'; -stubWebWorker(); - describe('', () => { const { httpSetup, httpRequestsMockHelpers } = setupEnvironment(); let testBed: HomeTestBed; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts index 1158815a62fcc..f7990a3288f04 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -135,6 +135,10 @@ describe('', () => { it('navigates to the index details page when the index name is clicked', async () => { const indexName = 'testIndex'; httpRequestsMockHelpers.setLoadIndicesResponse([createNonDataStreamIndex(indexName)]); + httpRequestsMockHelpers.setLoadIndexDetailsResponse( + indexName, + createNonDataStreamIndex(indexName) + ); testBed = await setup(httpSetup, { history: createMemoryHistory(), @@ -150,6 +154,10 @@ describe('', () => { it('index page works with % character in index name', async () => { const indexName = 'test%'; httpRequestsMockHelpers.setLoadIndicesResponse([createNonDataStreamIndex(indexName)]); + httpRequestsMockHelpers.setLoadIndexDetailsResponse( + encodeURIComponent(indexName), + createNonDataStreamIndex(indexName) + ); testBed = await setup(httpSetup); const { component, actions } = testBed; 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 cd0c81715cbe9..732fc8f0456fa 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 @@ -14,7 +14,7 @@ import { import { HttpSetup } from '@kbn/core/public'; import { act } from 'react-dom/test-utils'; -import { IndexDetailsTabIds } from '../../../common/constants'; +import { IndexDetailsTabId } from '../../../common/constants'; import { IndexDetailsPage } from '../../../public/application/sections/home/index_list/details_page'; import { WithAppDependencies } from '../helpers'; import { testIndexName } from './mocks'; @@ -35,7 +35,7 @@ export interface IndexDetailsPageTestBed extends TestBed { routerMock: typeof reactRouterMock; actions: { getHeader: () => string; - clickIndexDetailsTab: (tab: IndexDetailsTabIds) => Promise; + clickIndexDetailsTab: (tab: IndexDetailsTabId) => Promise; getIndexDetailsTabs: () => string[]; getActiveTabContent: () => string; mappings: { @@ -88,7 +88,6 @@ export interface IndexDetailsPageTestBed extends TestBed { getDataStreamDetailsContent: () => string; reloadDataStreamDetails: () => Promise; addDocCodeBlockExists: () => boolean; - extensionSummaryExists: (index: number) => boolean; }; }; } @@ -127,7 +126,7 @@ export const setup = async ({ return component.find('[data-test-subj="indexDetailsHeader"] h1').text(); }; - const clickIndexDetailsTab = async (tab: IndexDetailsTabIds) => { + const clickIndexDetailsTab = async (tab: IndexDetailsTabId) => { await act(async () => { find(`indexDetailsTab-${tab}`).simulate('click'); }); @@ -178,9 +177,6 @@ export const setup = async ({ addDocCodeBlockExists: () => { return exists('codeBlockControlsPanel'); }, - extensionSummaryExists: (index: number) => { - return exists(`extensionsSummary-${index}`); - }, }; const mappings = { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx index 76247c483b3c4..9f9018edceead 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx @@ -11,11 +11,7 @@ import { act } from 'react-dom/test-utils'; import React from 'react'; -import { - IndexDetailsSection, - IndexDetailsTab, - IndexDetailsTabIds, -} from '../../../common/constants'; +import { IndexDetailsSection, IndexDetailsTab, IndexDetailsTabId } from '../../../common/constants'; import { API_BASE_PATH, Index, INTERNAL_API_BASE_PATH } from '../../../common'; import { @@ -399,57 +395,28 @@ describe('', () => { expect(testBed.actions.overview.addDocCodeBlockExists()).toBe(true); }); - describe('extension service summary', () => { - it('renders all summaries added to the extension service', async () => { + describe('extension service overview content', () => { + it('renders the content instead of the default code block', async () => { + const extensionsServiceOverview = 'Test content via extensions service'; await act(async () => { testBed = await setup({ httpSetup, dependencies: { services: { extensionsService: { - summaries: [() => test, () => test2], + _indexOverviewContent: { + renderContent: () => extensionsServiceOverview, + }, }, }, }, }); }); testBed.component.update(); - expect(testBed.actions.overview.extensionSummaryExists(0)).toBe(true); - expect(testBed.actions.overview.extensionSummaryExists(1)).toBe(true); - }); - it(`doesn't render empty panels if the summary renders null`, async () => { - await act(async () => { - testBed = await setup({ - httpSetup, - dependencies: { - services: { - extensionsService: { - summaries: [() => null], - }, - }, - }, - }); - }); - testBed.component.update(); - expect(testBed.actions.overview.extensionSummaryExists(0)).toBe(false); - }); - - it(`doesn't render anything when no summaries added to the extension service`, async () => { - await act(async () => { - testBed = await setup({ - httpSetup, - dependencies: { - services: { - extensionsService: { - summaries: [], - }, - }, - }, - }); - }); - testBed.component.update(); - expect(testBed.actions.overview.extensionSummaryExists(0)).toBe(false); + expect(testBed.actions.overview.addDocCodeBlockExists()).toBe(false); + const content = testBed.actions.getActiveTabContent(); + expect(content).toContain(extensionsServiceOverview); }); }); }); @@ -851,8 +818,9 @@ describe('', () => { ); }); }); + describe('extension service tabs', () => { - const testTabId = 'testTab' as IndexDetailsTabIds; + const testTabId = 'testTab' as IndexDetailsTabId; const testContent = 'Test content'; const additionalTab: IndexDetailsTab = { id: testTabId, diff --git a/x-pack/plugins/index_management/common/constants/home_sections.ts b/x-pack/plugins/index_management/common/constants/home_sections.ts index c478c9ea0ec67..436a9ae56aa1a 100644 --- a/x-pack/plugins/index_management/common/constants/home_sections.ts +++ b/x-pack/plugins/index_management/common/constants/home_sections.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { ReactNode } from 'react'; +import { FunctionComponent, ReactNode } from 'react'; +import { ApplicationStart } from '@kbn/core-application-browser'; import { Index } from '../types'; export enum Section { @@ -23,15 +24,21 @@ export enum IndexDetailsSection { Stats = 'stats', } -export type IndexDetailsTabIds = IndexDetailsSection | string; +export type IndexDetailsTabId = IndexDetailsSection | string; export interface IndexDetailsTab { // a unique key to identify the tab - id: IndexDetailsTabIds; + id: IndexDetailsTabId; // a text that is displayed on the tab label, usually a Formatted message component name: ReactNode; // a function that renders the content of the tab - renderTabContent: (indexName: string, index: Index) => ReactNode; + renderTabContent: (args: { + index: Index; + getUrlForApp: ApplicationStart['getUrlForApp']; + }) => ReturnType; // a number to specify the order of the tabs order: number; + // an optional function to return a boolean for when to render the tab + // if omitted, the tab is always rendered + shouldRenderTab?: (args: { index: Index }) => boolean; } diff --git a/x-pack/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts index b84442a89d49f..a41f3d71bc6bc 100644 --- a/x-pack/plugins/index_management/common/constants/index.ts +++ b/x-pack/plugins/index_management/common/constants/index.ts @@ -49,4 +49,4 @@ export { export { MAJOR_VERSION } from './plugin'; export { Section, IndexDetailsSection } from './home_sections'; -export type { IndexDetailsTab, IndexDetailsTabIds } from './home_sections'; +export type { IndexDetailsTab, IndexDetailsTabId } from './home_sections'; diff --git a/x-pack/plugins/index_management/common/types/indices.ts b/x-pack/plugins/index_management/common/types/indices.ts index ab6080f30fdb2..b5f70ec5e1463 100644 --- a/x-pack/plugins/index_management/common/types/indices.ts +++ b/x-pack/plugins/index_management/common/types/indices.ts @@ -7,6 +7,7 @@ import { HealthStatus, + IlmExplainLifecycleLifecycleExplain, IndicesStatsIndexMetadataState, Uuid, } from '@elastic/elasticsearch/lib/api/types'; @@ -56,6 +57,7 @@ export interface IndexSettings { analysis?: AnalysisModule; [key: string]: any; } + export interface Index { name: string; primary?: number | string; @@ -67,10 +69,7 @@ export interface Index { // The types below are added by extension services if corresponding plugins are enabled (ILM, Rollup, CCR) isRollupIndex?: boolean; - ilm?: { - index: string; - managed: boolean; - }; + ilm?: IlmExplainLifecycleLifecycleExplain; isFollowerIndex?: boolean; // The types from here below represent information returned from the index stats API; 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 f3e54ae946a99..f028dee3d8eee 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 @@ -6,142 +6,29 @@ */ import React, { useCallback, useEffect, useMemo, useState, FunctionComponent } from 'react'; -import { css } from '@emotion/react'; import { RouteComponentProps } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiPageHeader, - EuiSpacer, - EuiPageHeaderProps, - EuiPageSection, - EuiButton, - EuiPageTemplate, - EuiText, - EuiCode, -} from '@elastic/eui'; +import { EuiPageTemplate, EuiText, EuiCode } from '@elastic/eui'; import { SectionLoading } from '@kbn/es-ui-shared-plugin/public'; -import { - Section, - IndexDetailsSection, - IndexDetailsTab, - IndexDetailsTabIds, -} from '../../../../../../common/constants'; -import { getIndexDetailsLink } from '../../../../services/routing'; +import { IndexDetailsSection, IndexDetailsTabId } from '../../../../../../common/constants'; import { Index } from '../../../../../../common'; -import { INDEX_OPEN } from '../../../../../../common/constants'; import { Error } from '../../../../../shared_imports'; import { loadIndex } from '../../../../services'; -import { useAppContext } from '../../../../app_context'; -import { DiscoverLink } from '../../../../lib/discover_link'; import { DetailsPageError } from './details_page_error'; -import { ManageIndexButton } from './manage_index_button'; -import { DetailsPageStats } from './details_page_stats'; -import { DetailsPageMappings } from './details_page_mappings'; -import { DetailsPageOverview } from './details_page_overview'; -import { DetailsPageSettings } from './details_page_settings'; +import { DetailsPageContent } from './details_page_content'; -const defaultTabs: IndexDetailsTab[] = [ - { - id: IndexDetailsSection.Overview, - name: ( - - ), - renderTabContent: (indexName: string, index: Index) => ( - - ), - order: 10, - }, - { - id: IndexDetailsSection.Mappings, - name: ( - - ), - renderTabContent: (indexName: string, index: Index) => ( - - ), - order: 20, - }, - { - id: IndexDetailsSection.Settings, - name: ( - - ), - renderTabContent: (indexName: string, index: Index) => ( - - ), - order: 30, - }, -]; - -const statsTab: IndexDetailsTab = { - id: IndexDetailsSection.Stats, - name: , - renderTabContent: (indexName: string, index: Index) => ( - - ), - order: 40, -}; - -const getSelectedTabContent = ({ - tabs, - indexDetailsSection, - index, - indexName, -}: { - tabs: IndexDetailsTab[]; - indexDetailsSection: IndexDetailsTabIds; - index?: Index | null; - indexName: string; -}) => { - // if there is no index data, the tab content won't be rendered, so it's safe to return null here - if (!index) { - return null; - } - const selectedTab = tabs.find((tab) => tab.id === indexDetailsSection); - return selectedTab ? ( - selectedTab.renderTabContent(indexName, index) - ) : ( - - ); -}; export const DetailsPage: FunctionComponent< RouteComponentProps<{ indexName: string; indexDetailsSection: IndexDetailsSection }> > = ({ location: { search }, history }) => { - const { - config, - services: { extensionsService }, - } = useAppContext(); const queryParams = useMemo(() => new URLSearchParams(search), [search]); const indexName = queryParams.get('indexName') ?? ''; - - const tabs = useMemo(() => { - const sortedTabs = [...defaultTabs]; - if (config.enableIndexStats) { - sortedTabs.push(statsTab); - } - sortedTabs.push(...extensionsService.indexDetailsTabs); - - sortedTabs.sort((tabA, tabB) => { - return tabA.order - tabB.order; - }); - return sortedTabs; - }, [config.enableIndexStats, extensionsService.indexDetailsTabs]); - - const tabQueryParam = queryParams.get('tab') ?? IndexDetailsSection.Overview; - let indexDetailsSection = IndexDetailsSection.Overview; - if (tabs.map((tab) => tab.id).includes(tabQueryParam as IndexDetailsTabIds)) { - indexDetailsSection = tabQueryParam as IndexDetailsSection; - } + const tab: IndexDetailsTabId = queryParams.get('tab') ?? IndexDetailsSection.Overview; const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [index, setIndex] = useState(); - const selectedTabContent = useMemo(() => { - return getSelectedTabContent({ tabs, indexDetailsSection, index, indexName }); - }, [index, indexDetailsSection, indexName, tabs]); - const fetchIndexDetails = useCallback(async () => { if (indexName) { setIsLoading(true); @@ -161,27 +48,6 @@ export const DetailsPage: FunctionComponent< fetchIndexDetails(); }, [fetchIndexDetails]); - const onSectionChange = useCallback( - (newSection: IndexDetailsTabIds) => { - return history.push(getIndexDetailsLink(indexName, newSection)); - }, - [history, indexName] - ); - - const navigateToAllIndices = useCallback(() => { - history.push(`/${Section.Indices}`); - }, [history]); - - const headerTabs = useMemo(() => { - return tabs.map((tab) => ({ - onClick: () => onSectionChange(tab.id), - isSelected: tab.id === indexDetailsSection, - key: tab.id, - 'data-test-subj': `indexDetailsTab-${tab.id}`, - label: tab.name, - })); - }, [tabs, indexDetailsSection, onSectionChange]); - if (!indexName) { return ( ; } return ( - <> - - - - - - - - - , - , - ]} - rightSideGroupProps={{ - wrap: false, - }} - responsive="reverse" - tabs={headerTabs} - /> - - - -
- {selectedTabContent} -
- + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_content.tsx new file mode 100644 index 0000000000000..1a9d5935cca19 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_content.tsx @@ -0,0 +1,176 @@ +/* + * 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 } from 'react'; +import { + EuiButton, + EuiPageHeader, + EuiPageHeaderProps, + EuiPageSection, + EuiSpacer, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { RouteComponentProps } from 'react-router-dom'; + +import { Index } from '../../../../../../common'; +import { + INDEX_OPEN, + IndexDetailsSection, + IndexDetailsTab, + IndexDetailsTabId, + Section, +} from '../../../../../../common/constants'; +import { getIndexDetailsLink } from '../../../../services/routing'; +import { useAppContext } from '../../../../app_context'; +import { DiscoverLink } from '../../../../lib/discover_link'; +import { ManageIndexButton } from './manage_index_button'; +import { DetailsPageOverview } from './details_page_overview'; +import { DetailsPageMappings } from './details_page_mappings'; +import { DetailsPageSettings } from './details_page_settings'; +import { DetailsPageStats } from './details_page_stats'; +import { DetailsPageTab } from './details_page_tab'; + +const defaultTabs: IndexDetailsTab[] = [ + { + id: IndexDetailsSection.Overview, + name: ( + + ), + renderTabContent: ({ index }) => , + order: 10, + }, + { + id: IndexDetailsSection.Mappings, + name: ( + + ), + renderTabContent: ({ index }) => , + order: 20, + }, + { + id: IndexDetailsSection.Settings, + name: ( + + ), + renderTabContent: ({ index }) => ( + + ), + order: 30, + }, +]; + +const statsTab: IndexDetailsTab = { + id: IndexDetailsSection.Stats, + name: , + renderTabContent: ({ index }) => ( + + ), + order: 40, +}; + +interface Props { + index: Index; + tab: IndexDetailsTabId; + history: RouteComponentProps['history']; + fetchIndexDetails: () => Promise; +} +export const DetailsPageContent: FunctionComponent = ({ + index, + tab, + history, + fetchIndexDetails, +}) => { + const { + config: { enableIndexStats }, + services: { extensionsService }, + } = useAppContext(); + + const tabs = useMemo(() => { + const sortedTabs = [...defaultTabs]; + if (enableIndexStats) { + sortedTabs.push(statsTab); + } + extensionsService.indexDetailsTabs.forEach((dynamicTab) => { + if (!dynamicTab.shouldRenderTab || dynamicTab.shouldRenderTab({ index })) { + sortedTabs.push(dynamicTab); + } + }); + + sortedTabs.sort((tabA, tabB) => { + return tabA.order - tabB.order; + }); + return sortedTabs; + }, [enableIndexStats, extensionsService.indexDetailsTabs, index]); + + const onSectionChange = useCallback( + (newSection: IndexDetailsTabId) => { + return history.push(getIndexDetailsLink(index.name, newSection)); + }, + [history, index] + ); + + const navigateToAllIndices = useCallback(() => { + history.push(`/${Section.Indices}`); + }, [history]); + + const headerTabs = useMemo(() => { + return tabs.map((tabConfig) => ({ + onClick: () => onSectionChange(tabConfig.id), + isSelected: tabConfig.id === tab, + key: tabConfig.id, + 'data-test-subj': `indexDetailsTab-${tabConfig.id}`, + label: tabConfig.name, + })); + }, [tabs, tab, onSectionChange]); + + return ( + <> + + + + + + + , + , + ]} + rightSideGroupProps={{ + wrap: false, + }} + responsive="reverse" + tabs={headerTabs} + /> + +
+ +
+ + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.tsx index 624a58f6e722a..0a9503c56cb59 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.tsx @@ -32,7 +32,6 @@ import { useAppContext } from '../../../../../app_context'; import { documentationService } from '../../../../../services'; import { breadcrumbService, IndexManagementBreadcrumb } from '../../../../../services/breadcrumbs'; import { languageDefinitions, curlDefinition } from './languages'; -import { ExtensionsSummary } from './extensions_summary'; import { DataStreamDetails } from './data_stream_details'; import { StorageDetails } from './storage_details'; import { AliasesDetails } from './aliases_details'; @@ -55,7 +54,11 @@ export const DetailsPageOverview: React.FunctionComponent = ({ indexDetai size, primary_size: primarySize, } = indexDetails; - const { core, plugins } = useAppContext(); + const { + core, + plugins, + services: { extensionsService }, + } = useAppContext(); useEffect(() => { breadcrumbService.setBreadcrumbs(IndexManagementBreadcrumb.indexDetailsOverview); @@ -94,59 +97,64 @@ export const DetailsPageOverview: React.FunctionComponent = ({ indexDetai - - - - - -

- {i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.addMoreDataTitle', { - defaultMessage: 'Add data to this index', - })} -

-
- - - - - -

- - - - ), - }} - /> -

-
-
-
- - - - -
+ {extensionsService.indexOverviewContent ? ( + extensionsService.indexOverviewContent.renderContent({ + index: indexDetails, + getUrlForApp: core.getUrlForApp, + }) + ) : ( + + + +

+ {i18n.translate('xpack.idxMgmt.indexDetails.overviewTab.addMoreDataTitle', { + defaultMessage: 'Add data to this index', + })} +

+
+ + + + + +

+ + + + ), + }} + /> +

+
+
+
+ + + + +
+ )} ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_overview/extensions_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_overview/extensions_summary.tsx deleted file mode 100644 index b119a99cd1a0a..0000000000000 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_overview/extensions_summary.tsx +++ /dev/null @@ -1,34 +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, { Fragment, FunctionComponent } from 'react'; -import { EuiPanel, EuiSpacer } from '@elastic/eui'; -import { Index } from '../../../../../../../common'; -import { useAppContext } from '../../../../../app_context'; - -export const ExtensionsSummary: FunctionComponent<{ index: Index }> = ({ index }) => { - const { - services: { extensionsService }, - core: { getUrlForApp }, - } = useAppContext(); - const summaries = extensionsService.summaries.map((summaryExtension, i) => { - const summary = summaryExtension({ index, getUrlForApp }); - - if (!summary) { - return null; - } - return ( - - - {summary} - - - - ); - }); - return <>{summaries}; -}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_tab.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_tab.tsx new file mode 100644 index 0000000000000..9f760aab91324 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_tab.tsx @@ -0,0 +1,29 @@ +/* + * 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 { Index } from '../../../../../../common'; +import { IndexDetailsTab, IndexDetailsTabId } from '../../../../../../common/constants'; +import { useAppContext } from '../../../../app_context'; +import { DetailsPageOverview } from './details_page_overview'; + +interface Props { + tabs: IndexDetailsTab[]; + tab: IndexDetailsTabId; + index: Index; +} +export const DetailsPageTab: FunctionComponent = ({ tabs, tab, index }) => { + const selectedTab = tabs.find((tabConfig) => tabConfig.id === tab); + const { + core: { getUrlForApp }, + } = useAppContext(); + return selectedTab ? ( + selectedTab.renderTabContent({ index, getUrlForApp }) + ) : ( + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/manage_index_button.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/manage_index_button.tsx index 2d7d7aab0d23f..8bfaffa51273f 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/manage_index_button.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/manage_index_button.tsx @@ -41,21 +41,26 @@ const getIndexStatusByName = ( }; interface Props { - indexName: string; - indexDetails: Index; + index: Index; reloadIndexDetails: () => Promise; navigateToAllIndices: () => void; } + +/** + * This component is a wrapper for the underlying "index actions context menu" that is currently used + * in the indices list and works with redux. That is why all request helpers from the services are expecting + * an array of indices, for example "deleteIndices(indexNames)". + * + */ export const ManageIndexButton: FunctionComponent = ({ - indexName, - indexDetails, + index, 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]); + // the "index actions context menu" component is expecting an array of indices, the same as on the indices list + const indexNames = useMemo(() => [index.name], [index]); const reloadIndices = useCallback(async () => { setIsLoading(true); @@ -63,7 +68,8 @@ export const ManageIndexButton: FunctionComponent = ({ setIsLoading(false); }, [reloadIndexDetails]); - const indices = [indexDetails]; + // the "index actions context menu" component is expecting an array of indices, the same as on the indices list + const indices = [index]; const indexStatusByName = getIndexStatusByName(indexNames, indices); const closeIndices = useCallback(async () => { diff --git a/x-pack/plugins/index_management/public/application/services/routing.ts b/x-pack/plugins/index_management/public/application/services/routing.ts index 1d3a41f0d54a3..3ec13eca06516 100644 --- a/x-pack/plugins/index_management/public/application/services/routing.ts +++ b/x-pack/plugins/index_management/public/application/services/routing.ts @@ -6,7 +6,7 @@ */ import { Section } from '../../../common/constants'; -import type { IndexDetailsTabIds } from '../../../common/constants'; +import type { IndexDetailsTabId } from '../../../common/constants'; export const getTemplateListLink = () => `/templates`; @@ -58,7 +58,7 @@ export const getDataStreamDetailsLink = (name: string) => { return encodeURI(`/data_streams/${encodeURIComponent(name)}`); }; -export const getIndexDetailsLink = (indexName: string, tab?: IndexDetailsTabIds) => { +export const getIndexDetailsLink = (indexName: string, tab?: IndexDetailsTabId) => { let link = `/${Section.Indices}/index_details?indexName=${encodeURIComponent(indexName)}`; if (tab) { link = `${link}&tab=${tab}`; diff --git a/x-pack/plugins/index_management/public/services/extensions_service.mock.ts b/x-pack/plugins/index_management/public/services/extensions_service.mock.ts index 6ef42d7944d67..3c0886b0fe4a3 100644 --- a/x-pack/plugins/index_management/public/services/extensions_service.mock.ts +++ b/x-pack/plugins/index_management/public/services/extensions_service.mock.ts @@ -15,9 +15,9 @@ const createServiceMock = (): ExtensionsSetupMock => ({ addBadge: jest.fn(), addBanner: jest.fn(), addFilter: jest.fn(), - addSummary: jest.fn(), addToggle: jest.fn(), addIndexDetailsTab: jest.fn(), + setIndexOverviewContent: jest.fn(), }); const createMock = () => { diff --git a/x-pack/plugins/index_management/public/services/extensions_service.ts b/x-pack/plugins/index_management/public/services/extensions_service.ts index 5c81e825eb1b1..5ffef366a016c 100644 --- a/x-pack/plugins/index_management/public/services/extensions_service.ts +++ b/x-pack/plugins/index_management/public/services/extensions_service.ts @@ -6,21 +6,29 @@ */ import { i18n } from '@kbn/i18n'; +import { FunctionComponent } from 'react'; +import { ApplicationStart } from '@kbn/core-application-browser'; import type { IndexDetailsTab } from '../../common/constants'; +import { Index } from '..'; + +export interface IndexOverviewContent { + renderContent: (args: { + index: Index; + getUrlForApp: ApplicationStart['getUrlForApp']; + }) => ReturnType; +} export interface ExtensionsSetup { - addSummary(summary: any): void; addAction(action: any): void; addBanner(banner: any): void; addFilter(filter: any): void; addBadge(badge: any): void; addToggle(toggle: any): void; addIndexDetailsTab(tab: IndexDetailsTab): void; + setIndexOverviewContent(content: IndexOverviewContent): void; } export class ExtensionsService { - private _indexDetailsTabs: IndexDetailsTab[] = []; - private _summaries: any[] = []; private _actions: any[] = []; private _banners: any[] = []; private _filters: any[] = []; @@ -37,6 +45,8 @@ export class ExtensionsService { }, ]; private _toggles: any[] = []; + private _indexDetailsTabs: IndexDetailsTab[] = []; + private _indexOverviewContent: IndexOverviewContent | null = null; private service?: ExtensionsSetup; public setup(): ExtensionsSetup { @@ -45,18 +55,14 @@ export class ExtensionsService { addBadge: this.addBadge.bind(this), addBanner: this.addBanner.bind(this), addFilter: this.addFilter.bind(this), - addSummary: this.addSummary.bind(this), addToggle: this.addToggle.bind(this), addIndexDetailsTab: this.addIndexDetailsTab.bind(this), + setIndexOverviewContent: this.setIndexOverviewMainContent.bind(this), }; return this.service; } - private addSummary(summary: any) { - this._summaries.push(summary); - } - private addAction(action: any) { this._actions.push(action); } @@ -81,8 +87,12 @@ export class ExtensionsService { this._indexDetailsTabs.push(tab); } - public get summaries() { - return this._summaries; + private setIndexOverviewMainContent(content: IndexOverviewContent) { + if (this._indexOverviewContent) { + throw new Error(`The content for index overview has already been set.`); + } else { + this._indexOverviewContent = content; + } } public get actions() { @@ -108,4 +118,8 @@ export class ExtensionsService { public get indexDetailsTabs() { return this._indexDetailsTabs; } + + public get indexOverviewContent() { + return this._indexOverviewContent; + } } diff --git a/x-pack/plugins/index_management/tsconfig.json b/x-pack/plugins/index_management/tsconfig.json index 7b52920e5e5d4..b1ccc4b12cdd5 100644 --- a/x-pack/plugins/index_management/tsconfig.json +++ b/x-pack/plugins/index_management/tsconfig.json @@ -41,6 +41,7 @@ "@kbn/search-api-panels", "@kbn/cloud-plugin", "@kbn/ui-theme", + "@kbn/core-application-browser", ], "exclude": [ "target/**/*",