;
+}
+```
#### 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
@@ -44,6 +54,14 @@ interface IndexContent {
```
- `setIndexMappingsContent(content: IndexContent)`: adds content to the mappings tab of the index details page. The content is displayed in the right bottom corner, below the mappings docs link.
+## Index data enrichers
+The extensions service that allows to render additional UI elements in the indices list and on the index details page often
+relies on additional index data that is not available by default. To make these additional data available in the response of
+the `GET /indices` request, an index data enricher can be registered. A data enricher is essentially an extra request that is
+done for the array of indices and the information is added to the response. Currently, 3 data enrichers are registered
+by the ILM, Rollup and CCR plugins. Before adding a data enricher, the cost of the additional request should be taken
+in consideration (see [this file](https://github.com/elastic/kibana/blob/main/x-pack/plugins/index_management/server/services/index_data_enricher.ts) for more details).
+
## Indices tab
### Quick steps for testing
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.tsx
index b1b3c3f2014a0..8a8a2fc23d54d 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.tsx
@@ -25,10 +25,14 @@ jest.mock('@elastic/eui/lib/components/search_bar/search_box', () => {
import React from 'react';
import { act } from 'react-dom/test-utils';
-import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common';
+import { API_BASE_PATH, Index, INTERNAL_API_BASE_PATH } from '../../../common';
import { setupEnvironment } from '../helpers';
import { IndicesTestBed, setup } from './indices_tab.helpers';
-import { createDataStreamPayload, createNonDataStreamIndex } from './data_streams_tab.helpers';
+import {
+ createDataStreamBackingIndex,
+ createDataStreamPayload,
+ createNonDataStreamIndex,
+} from './data_streams_tab.helpers';
import { createMemoryHistory } from 'history';
import {
@@ -81,30 +85,8 @@ describe('', () => {
describe('data stream column', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setLoadIndicesResponse([
- {
- health: '',
- status: '',
- primary: '',
- replica: '',
- documents: '',
- documents_deleted: '',
- size: '',
- primary_size: '',
- name: 'data-stream-index',
- data_stream: 'dataStream1',
- },
- {
- health: '',
- status: '',
- primary: '',
- replica: '',
- documents: '',
- documents_deleted: '',
- size: '',
- primary_size: '',
- name: 'no-data-stream-index',
- data_stream: null,
- },
+ createDataStreamBackingIndex('data-stream-index', 'dataStream1'),
+ createNonDataStreamIndex('no-data-stream-index'),
]);
// The detail panel should still appear even if there are no data streams.
@@ -195,26 +177,6 @@ describe('', () => {
expect(testBed.exists('createIndexMessage')).toBe(true);
});
- it('displays an empty list content if set via extensions service', async () => {
- httpRequestsMockHelpers.setLoadIndicesResponse([]);
- await act(async () => {
- testBed = await setup(httpSetup, {
- services: {
- extensionsService: {
- _emptyListContent: {
- renderContent: () => {
- return Empty list content
;
- },
- },
- },
- },
- });
- });
- testBed.component.update();
-
- expect(testBed.component.text()).toContain('Empty list content');
- });
-
it('renders "no indices found" prompt for search', async () => {
const { find, component, exists } = testBed;
await act(async () => {
@@ -530,4 +492,70 @@ describe('', () => {
expect(httpSetup.get).toHaveBeenNthCalledWith(2, '/api/index_management/indices');
});
});
+
+ describe('extensions service', () => {
+ it('displays an empty list content if set via extensions service', async () => {
+ httpRequestsMockHelpers.setLoadIndicesResponse([]);
+ await act(async () => {
+ testBed = await setup(httpSetup, {
+ services: {
+ extensionsService: {
+ _emptyListContent: {
+ renderContent: () => {
+ return Empty list content
;
+ },
+ },
+ },
+ },
+ });
+ });
+ testBed.component.update();
+
+ expect(testBed.component.text()).toContain('Empty list content');
+ });
+
+ it('renders additional columns registered via extensions service', async () => {
+ httpRequestsMockHelpers.setLoadIndicesResponse([
+ {
+ ...createNonDataStreamIndex('test-index'),
+ ilm: {
+ phase: 'hot phase',
+ managed: true,
+ },
+ },
+ ]);
+ await act(async () => {
+ testBed = await setup(httpSetup, {
+ services: {
+ extensionsService: {
+ _columns: [
+ {
+ fieldName: 'ilm.phase',
+ label: 'ILM column 1',
+ order: 55,
+ },
+ {
+ fieldName: 'ilm.managed',
+ label: 'ILM column 2',
+ order: 56,
+ render: (index: Index) => {
+ if (index.ilm?.managed) {
+ return ILM managed
;
+ }
+ },
+ },
+ ],
+ },
+ },
+ });
+ });
+ testBed.component.update();
+
+ const text = testBed.component.text();
+ expect(text).toContain('ILM column 1');
+ expect(text).toContain('hot phase');
+ expect(text).toContain('ILM column 2');
+ expect(text).toContain('ILM managed');
+ });
+ });
});
diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js
index 137c4b8ca67e0..6d30f61946e23 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js
+++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js
@@ -34,6 +34,7 @@ import {
EuiTableRowCellCheckbox,
EuiText,
} from '@elastic/eui';
+import { get } from 'lodash';
import {
PageLoading,
@@ -51,44 +52,112 @@ import { CreateIndexButton } from '../create_index/create_index_button';
const PAGE_SIZE_OPTIONS = [10, 50, 100];
-const getHeaders = ({ showIndexStats }) => {
- const headers = {};
-
- headers.name = i18n.translate('xpack.idxMgmt.indexTable.headers.nameHeader', {
- defaultMessage: 'Name',
- });
+const getColumnConfigs = ({
+ showIndexStats,
+ history,
+ filterChanged,
+ extensionsService,
+ location,
+}) => {
+ const columns = [
+ {
+ fieldName: 'name',
+ label: i18n.translate('xpack.idxMgmt.indexTable.headers.nameHeader', {
+ defaultMessage: 'Name',
+ }),
+ order: 10,
+ render: (index) => (
+ <>
+ history.push(getIndexDetailsLink(index.name, location.search || ''))}
+ >
+ {index.name}
+
+ {renderBadges(index, extensionsService, filterChanged)}
+ >
+ ),
+ },
+ {
+ fieldName: 'data_stream',
+ label: i18n.translate('xpack.idxMgmt.indexTable.headers.dataStreamHeader', {
+ defaultMessage: 'Data stream',
+ }),
+ order: 80,
+ render: (index) => {
+ if (index.data_stream) {
+ return (
+
+ {index.data_stream}
+
+ );
+ }
+ },
+ },
+ ];
if (showIndexStats) {
- headers.health = i18n.translate('xpack.idxMgmt.indexTable.headers.healthHeader', {
- defaultMessage: 'Health',
- });
-
- headers.status = i18n.translate('xpack.idxMgmt.indexTable.headers.statusHeader', {
- defaultMessage: 'Status',
- });
-
- headers.primary = i18n.translate('xpack.idxMgmt.indexTable.headers.primaryHeader', {
- defaultMessage: 'Primaries',
- });
-
- headers.replica = i18n.translate('xpack.idxMgmt.indexTable.headers.replicaHeader', {
- defaultMessage: 'Replicas',
- });
-
- headers.documents = i18n.translate('xpack.idxMgmt.indexTable.headers.documentsHeader', {
- defaultMessage: 'Docs count',
- });
-
- headers.size = i18n.translate('xpack.idxMgmt.indexTable.headers.storageSizeHeader', {
- defaultMessage: 'Storage size',
- });
+ columns.push(
+ {
+ fieldName: 'health',
+ label: i18n.translate('xpack.idxMgmt.indexTable.headers.healthHeader', {
+ defaultMessage: 'Health',
+ }),
+ order: 20,
+ render: (index) => ,
+ },
+ {
+ fieldName: 'status',
+ label: i18n.translate('xpack.idxMgmt.indexTable.headers.statusHeader', {
+ defaultMessage: 'Status',
+ }),
+ order: 30,
+ },
+ {
+ fieldName: 'primary',
+ label: i18n.translate('xpack.idxMgmt.indexTable.headers.primaryHeader', {
+ defaultMessage: 'Primaries',
+ }),
+ order: 40,
+ },
+ {
+ fieldName: 'replica',
+ label: i18n.translate('xpack.idxMgmt.indexTable.headers.replicaHeader', {
+ defaultMessage: 'Replicas',
+ }),
+ order: 50,
+ },
+ {
+ fieldName: 'documents',
+ label: i18n.translate('xpack.idxMgmt.indexTable.headers.documentsHeader', {
+ defaultMessage: 'Docs count',
+ }),
+ order: 60,
+ render: (index) => {
+ if (index.documents) {
+ return Number(index.documents).toLocaleString();
+ }
+ },
+ },
+ {
+ fieldName: 'size',
+ label: i18n.translate('xpack.idxMgmt.indexTable.headers.storageSizeHeader', {
+ defaultMessage: 'Storage size',
+ }),
+ order: 70,
+ }
+ );
}
- headers.data_stream = i18n.translate('xpack.idxMgmt.indexTable.headers.dataStreamHeader', {
- defaultMessage: 'Data stream',
- });
+ columns.push(...extensionsService.columns);
- return headers;
+ return columns.sort(({ order: orderA }, { order: orderB }) => orderA - orderB);
};
export class IndexTable extends Component {
@@ -269,13 +338,13 @@ export class IndexTable extends Component {
return indexOfUnselectedItem === -1;
};
- buildHeader(headers) {
+ buildHeader(columnConfigs) {
const { sortField, isSortAscending } = this.props;
- return Object.entries(headers).map(([fieldName, label]) => {
+ return columnConfigs.map(({ fieldName, label }) => {
const isSorted = sortField === fieldName;
// we only want to make index name column 25% width when there are more columns displayed
const widthClassName =
- fieldName === 'name' && Object.keys(headers).length > 2 ? 'indTable__header__width' : '';
+ fieldName === 'name' && columnConfigs.length > 2 ? 'indTable__header__width' : '';
return (
;
- } else if (fieldName === 'name') {
- return (
-
- history.push(getIndexDetailsLink(value, location.search || ''))}
- >
- {value}
-
- {renderBadges(index, appServices.extensionsService, filterChanged)}
-
- );
- } else if (fieldName === 'data_stream' && value) {
- return (
-
- {value}
-
- );
- } else if (fieldName === 'documents' && value) {
- return Number(value).toLocaleString();
+ buildRowCell(index, columnConfig) {
+ if (columnConfig.render) {
+ return columnConfig.render(index);
}
-
- return value;
+ return get(index, columnConfig.fieldName);
}
- buildRowCells(index, appServices, config) {
- const headers = getHeaders({ showIndexStats: config.enableIndexStats });
- return Object.keys(headers).map((fieldName) => {
+ buildRowCells(index, columnConfigs) {
+ return columnConfigs.map((columnConfig) => {
const { name } = index;
- const value = index[fieldName];
-
+ const { fieldName } = columnConfig;
if (fieldName === 'name') {
return (
-
- {this.buildRowCell(fieldName, value, index, appServices)}
-
+ {this.buildRowCell(index, columnConfig)}
|
);
@@ -357,7 +393,7 @@ export class IndexTable extends Component {
className={'indTable__cell--' + fieldName}
header={fieldName}
>
- {this.buildRowCell(fieldName, value, index, appServices)}
+ {this.buildRowCell(index, columnConfig)}
);
});
@@ -389,7 +425,7 @@ export class IndexTable extends Component {
});
}
- buildRows(appServices, config) {
+ buildRows(columnConfigs) {
const { indices = [] } = this.props;
return indices.map((index) => {
const { name } = index;
@@ -414,7 +450,7 @@ export class IndexTable extends Component {
})}
/>
- {this.buildRowCells(index, appServices, config)}
+ {this.buildRowCells(index, columnConfigs)}
);
});
@@ -472,6 +508,7 @@ export class IndexTable extends Component {
indicesError,
allIndices,
pager,
+ history,
location,
} = this.props;
@@ -524,8 +561,14 @@ export class IndexTable extends Component {
{({ services, config }) => {
const { extensionsService } = services;
- const headers = getHeaders({ showIndexStats: config.enableIndexStats });
- const columnsCount = Object.keys(headers).length + 1;
+ const columnConfigs = getColumnConfigs({
+ showIndexStats: config.enableIndexStats,
+ extensionsService,
+ filterChanged,
+ history,
+ location,
+ });
+ const columnsCount = columnConfigs.length + 1;
return (
@@ -589,7 +632,7 @@ export class IndexTable extends Component {
) : null}
{(indicesLoading && allIndices.length === 0) || indicesError ? null : (
-
+ <>
-
+ >
)}
@@ -671,12 +714,12 @@ export class IndexTable extends Component {
)}
/>
- {this.buildHeader(headers)}
+ {this.buildHeader(columnConfigs)}
{indices.length > 0 ? (
- this.buildRows(services, config)
+ this.buildRows(columnConfigs)
) : (
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 616e81af9a7af..73053eec98556 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
@@ -16,6 +16,7 @@ const createServiceMock = (): ExtensionsSetupMock => ({
addBanner: jest.fn(),
addFilter: jest.fn(),
addToggle: jest.fn(),
+ addColumn: jest.fn(),
setEmptyListContent: jest.fn(),
addIndexDetailsTab: jest.fn(),
setIndexOverviewContent: jest.fn(),
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 98e707133c4d1..3e8f6118bf44e 100644
--- a/x-pack/plugins/index_management/public/services/extensions_service.ts
+++ b/x-pack/plugins/index_management/public/services/extensions_service.ts
@@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
-import { FunctionComponent } from 'react';
+import { FunctionComponent, ReactNode } from 'react';
import { ApplicationStart } from '@kbn/core-application-browser';
import { EuiBadgeProps } from '@elastic/eui';
import type { IndexDetailsTab } from '../../common/constants';
@@ -34,10 +34,18 @@ export interface IndexBadge {
export interface EmptyListContent {
renderContent: (args: {
+ // the button to open the "create index" modal
createIndexButton: ReturnType;
}) => ReturnType;
}
+export interface IndicesListColumn {
+ fieldName: string;
+ label: string;
+ order: number;
+ render?: (index: Index) => ReactNode;
+}
+
export interface ExtensionsSetup {
// adds an option to the "manage index" menu
addAction(action: any): void;
@@ -49,6 +57,8 @@ export interface ExtensionsSetup {
addBadge(badge: IndexBadge): void;
// adds a toggle to the indices list
addToggle(toggle: IndexToggle): void;
+ // adds a column to display additional information added via a data enricher
+ addColumn(column: IndicesListColumn): void;
// set the content to render when the indices list is empty
setEmptyListContent(content: EmptyListContent): void;
// adds a tab to the index details page
@@ -86,6 +96,7 @@ export class ExtensionsService {
name: 'includeHiddenIndices',
},
];
+ private _columns: IndicesListColumn[] = [];
private _emptyListContent: EmptyListContent | null = null;
private _indexDetailsTabs: IndexDetailsTab[] = [];
private _indexOverviewContent: IndexContent | null = null;
@@ -99,6 +110,7 @@ export class ExtensionsService {
addBanner: this.addBanner.bind(this),
addFilter: this.addFilter.bind(this),
addToggle: this.addToggle.bind(this),
+ addColumn: this.addColumn.bind(this),
setEmptyListContent: this.setEmptyListContent.bind(this),
addIndexDetailsTab: this.addIndexDetailsTab.bind(this),
setIndexOverviewContent: this.setIndexOverviewContent.bind(this),
@@ -128,6 +140,10 @@ export class ExtensionsService {
this._toggles.push(toggle);
}
+ private addColumn(column: IndicesListColumn) {
+ this._columns.push(column);
+ }
+
private setEmptyListContent(content: EmptyListContent) {
if (this._emptyListContent) {
throw new Error(`The empty list content has already been set.`);
@@ -176,6 +192,10 @@ export class ExtensionsService {
return this._toggles;
}
+ public get columns() {
+ return this._columns;
+ }
+
public get emptyListContent() {
return this._emptyListContent;
}