[Index Management] Disable index stats on serverless (#163849)

This commit is contained in:
Alison Goryachev 2023-08-23 08:18:48 -04:00 committed by GitHub
parent 615c450b37
commit a14f76d96c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 406 additions and 102 deletions

View file

@ -37,6 +37,8 @@ xpack.license_management.enabled: false
xpack.index_management.enableIndexActions: false xpack.index_management.enableIndexActions: false
# Disable legacy index templates from Index Management UI # Disable legacy index templates from Index Management UI
xpack.index_management.enableLegacyTemplates: false xpack.index_management.enableLegacyTemplates: false
# Disable index stats information from Index Management UI
xpack.index_management.enableIndexStats: false
# Keep deeplinks visible so that they are shown in the sidenav # Keep deeplinks visible so that they are shown in the sidenav
dev_tools.deeplinks.navLinkStatus: visible dev_tools.deeplinks.navLinkStatus: visible

View file

@ -243,6 +243,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'xpack.index_management.enableIndexActions (any)', 'xpack.index_management.enableIndexActions (any)',
'xpack.index_management.enableLegacyTemplates (any)', 'xpack.index_management.enableLegacyTemplates (any)',
'xpack.index_management.dev.enableIndexDetailsPage (boolean)', 'xpack.index_management.dev.enableIndexDetailsPage (boolean)',
'xpack.index_management.enableIndexStats (any)',
'xpack.infra.sources.default.fields.message (array)', 'xpack.infra.sources.default.fields.message (array)',
/** /**
* xpack.infra.logs is conditional and will resolve to an object of properties * xpack.infra.logs is conditional and will resolve to an object of properties

View file

@ -80,7 +80,7 @@ const indexWithLifecyclePolicy: Index = {
}, },
}; };
const indexWithLifecycleError = { const indexWithLifecycleError: Index = {
health: 'yellow', health: 'yellow',
status: 'open', status: 'open',
name: 'testy3', name: 'testy3',

View file

@ -60,6 +60,7 @@ const appDependencies = {
config: { config: {
enableLegacyTemplates: true, enableLegacyTemplates: true,
enableIndexActions: true, enableIndexActions: true,
enableIndexStats: true,
}, },
} as any; } as any;

View file

@ -19,6 +19,7 @@ export type TestSubjects =
| 'deleteSystemTemplateCallOut' | 'deleteSystemTemplateCallOut'
| 'deleteTemplateButton' | 'deleteTemplateButton'
| 'deleteTemplatesConfirmation' | 'deleteTemplatesConfirmation'
| 'descriptionTitle'
| 'documentationLink' | 'documentationLink'
| 'emptyPrompt' | 'emptyPrompt'
| 'forcemergeIndexMenuButton' | 'forcemergeIndexMenuButton'

View file

@ -62,14 +62,13 @@ describe('<IndexManagementHome />', () => {
beforeEach(async () => { beforeEach(async () => {
httpRequestsMockHelpers.setLoadIndicesResponse([]); httpRequestsMockHelpers.setLoadIndicesResponse([]);
testBed = await setup(httpSetup);
await act(async () => { await act(async () => {
const { component } = testBed; testBed = await setup(httpSetup);
await nextTick();
component.update();
}); });
const { component } = testBed;
component.update();
}); });
test('toggles the include hidden button through URL hash correctly', () => { test('toggles the include hidden button through URL hash correctly', () => {
@ -423,4 +422,103 @@ describe('<IndexManagementHome />', () => {
expect(exists('updateIndexSettingsErrorCallout')).toBe(true); expect(exists('updateIndexSettingsErrorCallout')).toBe(true);
}); });
}); });
describe('Index stats', () => {
const indexName = 'test';
beforeEach(async () => {
httpRequestsMockHelpers.setLoadIndicesResponse([createNonDataStreamIndex(indexName)]);
await act(async () => {
testBed = await setup(httpSetup);
});
const { component } = testBed;
component.update();
});
test('renders the table column with index stats by default', () => {
const { table } = testBed;
const { tableCellsValues } = table.getMetaData('indexTable');
expect(tableCellsValues).toEqual([
['', 'test', 'green', 'open', '1', '1', '10000', '156kb', ''],
]);
});
test('renders index stats in details flyout by default', async () => {
const { component, find } = testBed;
await act(async () => {
find('indexTableIndexNameLink').at(0).simulate('click');
});
component.update();
const descriptions = find('descriptionTitle');
const descriptionText = descriptions
.map((description) => {
return description.text();
})
.sort();
expect(descriptionText).toEqual([
'Aliases',
'Docs count',
'Docs deleted',
'Health',
'Primaries',
'Primary storage size',
'Replicas',
'Status',
'Storage size',
]);
});
describe('Disabled', () => {
beforeEach(async () => {
await act(async () => {
testBed = await setup(httpSetup, {
config: {
enableLegacyTemplates: true,
enableIndexActions: true,
enableIndexStats: false,
},
});
});
const { component } = testBed;
component.update();
});
test('hides index stats information from table', async () => {
const { table } = testBed;
const { tableCellsValues } = table.getMetaData('indexTable');
expect(tableCellsValues).toEqual([['', 'test', '1', '1', '']]);
});
test('hides index stats information from details panel', async () => {
const { component, find } = testBed;
await act(async () => {
find('indexTableIndexNameLink').at(0).simulate('click');
});
component.update();
const descriptions = find('descriptionTitle');
const descriptionText = descriptions
.map((description) => {
return description.text();
})
.sort();
expect(descriptionText).toEqual(['Aliases', 'Primaries', 'Replicas']);
});
});
});
}); });

View file

@ -172,6 +172,7 @@ describe('index table', () => {
config: { config: {
enableLegacyTemplates: true, enableLegacyTemplates: true,
enableIndexActions: true, enableIndexActions: true,
enableIndexStats: true,
}, },
}; };

View file

@ -5,6 +5,12 @@
* 2.0. * 2.0.
*/ */
import {
HealthStatus,
IndicesStatsIndexMetadataState,
Uuid,
} from '@elastic/elasticsearch/lib/api/types';
interface IndexModule { interface IndexModule {
number_of_shards: number | string; number_of_shards: number | string;
codec: string; codec: string;
@ -50,21 +56,21 @@ export interface IndexSettings {
analysis?: AnalysisModule; analysis?: AnalysisModule;
[key: string]: any; [key: string]: any;
} }
export interface Index { export interface Index {
health?: string;
status?: string;
name: string; name: string;
uuid?: string;
primary?: number | string; primary?: number | string;
replica?: number | string; replica?: number | string;
documents: number;
documents_deleted: number;
size: string;
primary_size: string;
isFrozen: boolean; isFrozen: boolean;
hidden: boolean; hidden: boolean;
aliases: string | string[]; aliases: string | string[];
data_stream?: string; data_stream?: string;
[key: string]: any; // 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;
status?: IndicesStatsIndexMetadataState;
uuid?: Uuid;
documents?: number;
size?: string;
primary_size?: string;
documents_deleted?: number;
} }

View file

@ -48,6 +48,7 @@ export interface AppDependencies {
enableIndexActions: boolean; enableIndexActions: boolean;
enableLegacyTemplates: boolean; enableLegacyTemplates: boolean;
enableIndexDetailsPage: boolean; enableIndexDetailsPage: boolean;
enableIndexStats: boolean;
}; };
history: ScopedHistory; history: ScopedHistory;
setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs'];

View file

@ -56,6 +56,7 @@ export async function mountManagementSection({
enableIndexActions = true, enableIndexActions = true,
enableLegacyTemplates = true, enableLegacyTemplates = true,
enableIndexDetailsPage = false, enableIndexDetailsPage = false,
enableIndexStats = true,
}: { }: {
coreSetup: CoreSetup<StartDependencies>; coreSetup: CoreSetup<StartDependencies>;
usageCollection: UsageCollectionSetup; usageCollection: UsageCollectionSetup;
@ -66,6 +67,7 @@ export async function mountManagementSection({
enableIndexActions?: boolean; enableIndexActions?: boolean;
enableLegacyTemplates?: boolean; enableLegacyTemplates?: boolean;
enableIndexDetailsPage?: boolean; enableIndexDetailsPage?: boolean;
enableIndexStats?: boolean;
}) { }) {
const { element, setBreadcrumbs, history, theme$ } = params; const { element, setBreadcrumbs, history, theme$ } = params;
const [core, startDependencies] = await coreSetup.getStartServices(); const [core, startDependencies] = await coreSetup.getStartServices();
@ -111,6 +113,7 @@ export async function mountManagementSection({
enableIndexActions, enableIndexActions,
enableLegacyTemplates, enableLegacyTemplates,
enableIndexDetailsPage, enableIndexDetailsPage,
enableIndexStats,
}, },
history, history,
setBreadcrumbs, setBreadcrumbs,

View file

@ -34,7 +34,7 @@ import { IndexActionsContextMenu } from '../index_actions_context_menu';
import { ShowJson } from './show_json'; import { ShowJson } from './show_json';
import { Summary } from './summary'; import { Summary } from './summary';
import { EditSettingsJson } from './edit_settings_json'; import { EditSettingsJson } from './edit_settings_json';
import { useServices } from '../../../../app_context'; import { useServices, useAppContext } from '../../../../app_context';
import { renderDiscoverLink } from '../../../../lib/render_discover_link'; import { renderDiscoverLink } from '../../../../lib/render_discover_link';
const tabToHumanizedMap = { const tabToHumanizedMap = {
@ -58,12 +58,19 @@ const tabToHumanizedMap = {
), ),
}; };
const tabs = [TAB_SUMMARY, TAB_SETTINGS, TAB_MAPPING, TAB_STATS, TAB_EDIT_SETTINGS]; const getTabs = (showStats) => {
if (showStats) {
return [TAB_SUMMARY, TAB_SETTINGS, TAB_MAPPING, TAB_STATS, TAB_EDIT_SETTINGS];
}
return [TAB_SUMMARY, TAB_SETTINGS, TAB_MAPPING, TAB_EDIT_SETTINGS];
};
export const DetailPanel = ({ panelType, indexName, index, openDetailPanel, closeDetailPanel }) => { export const DetailPanel = ({ panelType, indexName, index, openDetailPanel, closeDetailPanel }) => {
const { extensionsService } = useServices(); const { extensionsService } = useServices();
const { config } = useAppContext();
const renderTabs = () => { const renderTabs = () => {
const tabs = getTabs(config.enableIndexStats);
return tabs.map((tab, i) => { return tabs.map((tab, i) => {
const isSelected = tab === panelType; const isSelected = tab === panelType;
return ( return (

View file

@ -21,36 +21,43 @@ import {
import { DataHealth } from '../../../../../components'; import { DataHealth } from '../../../../../components';
import { AppContextConsumer } from '../../../../../app_context'; import { AppContextConsumer } from '../../../../../app_context';
const getHeaders = () => { const getHeaders = (showStats) => {
return { const baseHeaders = {
health: i18n.translate('xpack.idxMgmt.summary.headers.healthHeader', {
defaultMessage: 'Health',
}),
status: i18n.translate('xpack.idxMgmt.summary.headers.statusHeader', {
defaultMessage: 'Status',
}),
primary: i18n.translate('xpack.idxMgmt.summary.headers.primaryHeader', { primary: i18n.translate('xpack.idxMgmt.summary.headers.primaryHeader', {
defaultMessage: 'Primaries', defaultMessage: 'Primaries',
}), }),
replica: i18n.translate('xpack.idxMgmt.summary.headers.replicaHeader', { replica: i18n.translate('xpack.idxMgmt.summary.headers.replicaHeader', {
defaultMessage: 'Replicas', defaultMessage: 'Replicas',
}), }),
documents: i18n.translate('xpack.idxMgmt.summary.headers.documentsHeader', {
defaultMessage: 'Docs count',
}),
documents_deleted: i18n.translate('xpack.idxMgmt.summary.headers.deletedDocumentsHeader', {
defaultMessage: 'Docs deleted',
}),
size: i18n.translate('xpack.idxMgmt.summary.headers.storageSizeHeader', {
defaultMessage: 'Storage size',
}),
primary_size: i18n.translate('xpack.idxMgmt.summary.headers.primaryStorageSizeHeader', {
defaultMessage: 'Primary storage size',
}),
aliases: i18n.translate('xpack.idxMgmt.summary.headers.aliases', { aliases: i18n.translate('xpack.idxMgmt.summary.headers.aliases', {
defaultMessage: 'Aliases', defaultMessage: 'Aliases',
}), }),
}; };
if (showStats) {
return {
...baseHeaders,
health: i18n.translate('xpack.idxMgmt.summary.headers.healthHeader', {
defaultMessage: 'Health',
}),
status: i18n.translate('xpack.idxMgmt.summary.headers.statusHeader', {
defaultMessage: 'Status',
}),
documents: i18n.translate('xpack.idxMgmt.summary.headers.documentsHeader', {
defaultMessage: 'Docs count',
}),
documents_deleted: i18n.translate('xpack.idxMgmt.summary.headers.deletedDocumentsHeader', {
defaultMessage: 'Docs deleted',
}),
size: i18n.translate('xpack.idxMgmt.summary.headers.storageSizeHeader', {
defaultMessage: 'Storage size',
}),
primary_size: i18n.translate('xpack.idxMgmt.summary.headers.primaryStorageSizeHeader', {
defaultMessage: 'Primary storage size',
}),
};
}
return baseHeaders;
}; };
export class Summary extends React.PureComponent { export class Summary extends React.PureComponent {
@ -67,9 +74,9 @@ export class Summary extends React.PureComponent {
}); });
} }
buildRows() { buildRows(config) {
const { index } = this.props; const { index } = this.props;
const headers = getHeaders(); const headers = getHeaders(config.enableIndexStats);
const rows = { const rows = {
left: [], left: [],
right: [], right: [],
@ -84,7 +91,7 @@ export class Summary extends React.PureComponent {
content = content.join(', '); content = content.join(', ');
} }
const cell = [ const cell = [
<EuiDescriptionListTitle key={fieldName}> <EuiDescriptionListTitle key={fieldName} data-test-subj="descriptionTitle">
<strong>{headers[fieldName]}</strong> <strong>{headers[fieldName]}</strong>
</EuiDescriptionListTitle>, </EuiDescriptionListTitle>,
<EuiDescriptionListDescription key={fieldName + '_desc'}> <EuiDescriptionListDescription key={fieldName + '_desc'}>
@ -103,8 +110,8 @@ export class Summary extends React.PureComponent {
render() { render() {
return ( return (
<AppContextConsumer> <AppContextConsumer>
{({ services, core }) => { {({ services, core, config }) => {
const { left, right } = this.buildRows(); const { left, right } = this.buildRows(config);
const additionalContent = this.getAdditionalContent( const additionalContent = this.getAdditionalContent(
services.extensionsService, services.extensionsService,
core.getUrlForApp core.getUrlForApp

View file

@ -51,31 +51,46 @@ import { renderBadges } from '../../../../lib/render_badges';
import { NoMatch, DataHealth } from '../../../../components'; import { NoMatch, DataHealth } from '../../../../components';
import { IndexActionsContextMenu } from '../index_actions_context_menu'; import { IndexActionsContextMenu } from '../index_actions_context_menu';
const HEADERS = { const getHeaders = ({ showIndexStats }) => {
name: i18n.translate('xpack.idxMgmt.indexTable.headers.nameHeader', { const headers = {};
headers.name = i18n.translate('xpack.idxMgmt.indexTable.headers.nameHeader', {
defaultMessage: 'Name', defaultMessage: 'Name',
}), });
health: i18n.translate('xpack.idxMgmt.indexTable.headers.healthHeader', {
defaultMessage: 'Health', if (showIndexStats) {
}), headers.health = i18n.translate('xpack.idxMgmt.indexTable.headers.healthHeader', {
status: i18n.translate('xpack.idxMgmt.indexTable.headers.statusHeader', { defaultMessage: 'Health',
defaultMessage: 'Status', });
}),
primary: i18n.translate('xpack.idxMgmt.indexTable.headers.primaryHeader', { headers.status = i18n.translate('xpack.idxMgmt.indexTable.headers.statusHeader', {
defaultMessage: 'Status',
});
}
headers.primary = i18n.translate('xpack.idxMgmt.indexTable.headers.primaryHeader', {
defaultMessage: 'Primaries', defaultMessage: 'Primaries',
}), });
replica: i18n.translate('xpack.idxMgmt.indexTable.headers.replicaHeader', {
headers.replica = i18n.translate('xpack.idxMgmt.indexTable.headers.replicaHeader', {
defaultMessage: 'Replicas', defaultMessage: 'Replicas',
}), });
documents: i18n.translate('xpack.idxMgmt.indexTable.headers.documentsHeader', {
defaultMessage: 'Docs count', if (showIndexStats) {
}), headers.documents = i18n.translate('xpack.idxMgmt.indexTable.headers.documentsHeader', {
size: i18n.translate('xpack.idxMgmt.indexTable.headers.storageSizeHeader', { defaultMessage: 'Docs count',
defaultMessage: 'Storage size', });
}),
data_stream: i18n.translate('xpack.idxMgmt.indexTable.headers.dataStreamHeader', { headers.size = i18n.translate('xpack.idxMgmt.indexTable.headers.storageSizeHeader', {
defaultMessage: 'Storage size',
});
}
headers.data_stream = i18n.translate('xpack.idxMgmt.indexTable.headers.dataStreamHeader', {
defaultMessage: 'Data stream', defaultMessage: 'Data stream',
}), });
return headers;
}; };
export class IndexTable extends Component { export class IndexTable extends Component {
@ -246,9 +261,10 @@ export class IndexTable extends Component {
return indexOfUnselectedItem === -1; return indexOfUnselectedItem === -1;
}; };
buildHeader() { buildHeader(config) {
const { sortField, isSortAscending } = this.props; const { sortField, isSortAscending } = this.props;
return Object.entries(HEADERS).map(([fieldName, label]) => { const headers = getHeaders({ showIndexStats: config.enableIndexStats });
return Object.entries(headers).map(([fieldName, label]) => {
const isSorted = sortField === fieldName; const isSorted = sortField === fieldName;
return ( return (
<EuiTableHeaderCell <EuiTableHeaderCell
@ -302,8 +318,9 @@ export class IndexTable extends Component {
return value; return value;
} }
buildRowCells(index, appServices) { buildRowCells(index, appServices, config) {
return Object.keys(HEADERS).map((fieldName) => { const headers = getHeaders({ showIndexStats: config.enableIndexStats });
return Object.keys(headers).map((fieldName) => {
const { name } = index; const { name } = index;
const value = index[fieldName]; const value = index[fieldName];
@ -363,7 +380,7 @@ export class IndexTable extends Component {
}); });
} }
buildRows(appServices) { buildRows(appServices, config) {
const { indices = [], detailPanelIndexName } = this.props; const { indices = [], detailPanelIndexName } = this.props;
return indices.map((index) => { return indices.map((index) => {
const { name } = index; const { name } = index;
@ -388,7 +405,7 @@ export class IndexTable extends Component {
})} })}
/> />
</EuiTableRowCellCheckbox> </EuiTableRowCellCheckbox>
{this.buildRowCells(index, appServices)} {this.buildRowCells(index, appServices, config)}
</EuiTableRow> </EuiTableRow>
); );
}); });
@ -479,7 +496,7 @@ export class IndexTable extends Component {
return ( return (
<AppContextConsumer> <AppContextConsumer>
{({ services }) => { {({ services, config }) => {
const { extensionsService } = services; const { extensionsService } = services;
return ( return (
@ -639,10 +656,10 @@ export class IndexTable extends Component {
)} )}
/> />
</EuiTableHeaderCellCheckbox> </EuiTableHeaderCellCheckbox>
{this.buildHeader()} {this.buildHeader(config)}
</EuiTableHeader> </EuiTableHeader>
<EuiTableBody>{this.buildRows(services)}</EuiTableBody> <EuiTableBody>{this.buildRows(services, config)}</EuiTableBody>
</EuiTable> </EuiTable>
</div> </div>
) : ( ) : (

View file

@ -123,15 +123,19 @@ export const getPageOfIndices = createSelector(
const { firstItemIndex, lastItemIndex } = pager; const { firstItemIndex, lastItemIndex } = pager;
const pagedIndexes = sortedIndexes.slice(firstItemIndex, lastItemIndex + 1); const pagedIndexes = sortedIndexes.slice(firstItemIndex, lastItemIndex + 1);
return pagedIndexes.map((index) => { return pagedIndexes.map((index) => {
const status = if (index.status) {
indexStatusLabels[rowStatuses[index.name]] || // user friendly version of row status const status =
rowStatuses[index.name] || // row status indexStatusLabels[rowStatuses[index.name]] || // user friendly version of row status
indexStatusLabels[index.status] || // user friendly version of index status rowStatuses[index.name] || // row status
index.status; // index status indexStatusLabels[index.status] || // user friendly version of index status
return { index.status; // index status
...index, return {
status, ...index,
}; status,
};
}
return index;
}); });
} }
); );

View file

@ -40,6 +40,7 @@ export class IndexMgmtUIPlugin {
ui: { enabled: isIndexManagementUiEnabled }, ui: { enabled: isIndexManagementUiEnabled },
enableIndexActions, enableIndexActions,
enableLegacyTemplates, enableLegacyTemplates,
enableIndexStats,
dev: { enableIndexDetailsPage }, dev: { enableIndexDetailsPage },
} = this.ctx.config.get<ClientConfigType>(); } = this.ctx.config.get<ClientConfigType>();
@ -62,6 +63,7 @@ export class IndexMgmtUIPlugin {
enableIndexActions, enableIndexActions,
enableLegacyTemplates, enableLegacyTemplates,
enableIndexDetailsPage, enableIndexDetailsPage,
enableIndexStats,
}); });
}, },
}); });

View file

@ -31,6 +31,7 @@ export interface ClientConfigType {
}; };
enableIndexActions?: boolean; enableIndexActions?: boolean;
enableLegacyTemplates?: boolean; enableLegacyTemplates?: boolean;
enableIndexStats?: boolean;
dev: { dev: {
enableIndexDetailsPage?: boolean; enableIndexDetailsPage?: boolean;
}; };

View file

@ -33,6 +33,11 @@ const schemaLatest = schema.object(
serverless: schema.boolean({ defaultValue: true }), serverless: schema.boolean({ defaultValue: true }),
}), }),
dev: schema.object({ enableIndexDetailsPage: schema.boolean({ defaultValue: false }) }), dev: schema.object({ enableIndexDetailsPage: schema.boolean({ defaultValue: false }) }),
enableIndexStats: offeringBasedSchema({
// Index stats information is disabled in serverless; refer to the serverless.yml file as the source of truth
// We take this approach in order to have a central place (serverless.yml) for serverless config across Kibana
serverless: schema.boolean({ defaultValue: true }),
}),
}, },
{ defaultValue: undefined } { defaultValue: undefined }
); );
@ -45,6 +50,7 @@ const configLatest: PluginConfigDescriptor<IndexManagementConfig> = {
dev: { dev: {
enableIndexDetailsPage: true, enableIndexDetailsPage: true,
}, },
enableIndexStats: true,
}, },
schema: schemaLatest, schema: schemaLatest,
deprecations: () => [], deprecations: () => [],

View file

@ -9,9 +9,11 @@ import { ByteSizeValue } from '@kbn/config-schema';
import { IScopedClusterClient } from '@kbn/core/server'; import { IScopedClusterClient } from '@kbn/core/server';
import { IndexDataEnricher } from '../services'; import { IndexDataEnricher } from '../services';
import { Index } from '..'; import { Index } from '..';
import { RouteDependencies } from '../types';
async function fetchIndicesCall( async function fetchIndicesCall(
client: IScopedClusterClient, client: IScopedClusterClient,
config: RouteDependencies['config'],
indexNames?: string[] indexNames?: string[]
): Promise<Index[]> { ): Promise<Index[]> {
const indexNamesString = indexNames && indexNames.length ? indexNames.join(',') : '*'; const indexNamesString = indexNames && indexNames.length ? indexNames.join(',') : '*';
@ -38,41 +40,77 @@ async function fetchIndicesCall(
return []; return [];
} }
const { indices: indicesStats = {} } = await client.asCurrentUser.indices.stats({ const indicesNames = Object.keys(indices);
// Return response without index stats, if isIndexStatsEnabled === false
if (config.isIndexStatsEnabled === false) {
return indicesNames.map((indexName: string) => {
const indexData = indices[indexName];
const aliases = Object.keys(indexData.aliases!);
return {
name: indexName,
primary: indexData.settings?.index?.number_of_shards,
replica: indexData.settings?.index?.number_of_replicas,
isFrozen: indexData.settings?.index?.frozen === 'true',
aliases: aliases.length ? aliases : 'none',
hidden: indexData.settings?.index?.hidden === 'true',
data_stream: indexData.data_stream,
};
});
}
const { indices: indicesStats } = await client.asCurrentUser.indices.stats({
index: indexNamesString, index: indexNamesString,
expand_wildcards: ['hidden', 'all'], expand_wildcards: ['hidden', 'all'],
forbid_closed_indices: false, forbid_closed_indices: false,
metric: ['docs', 'store'], metric: ['docs', 'store'],
}); });
const indicesNames = Object.keys(indices);
return indicesNames.map((indexName: string) => { return indicesNames.map((indexName: string) => {
const indexData = indices[indexName]; const indexData = indices[indexName];
const indexStats = indicesStats[indexName];
const aliases = Object.keys(indexData.aliases!); const aliases = Object.keys(indexData.aliases!);
return { const baseResponse = {
health: indexStats?.health,
status: indexStats?.status,
name: indexName, name: indexName,
uuid: indexStats?.uuid,
primary: indexData.settings?.index?.number_of_shards, primary: indexData.settings?.index?.number_of_shards,
replica: indexData.settings?.index?.number_of_replicas, replica: indexData.settings?.index?.number_of_replicas,
documents: indexStats?.primaries?.docs?.count ?? 0,
documents_deleted: indexStats?.primaries?.docs?.deleted ?? 0,
size: new ByteSizeValue(indexStats?.total?.store?.size_in_bytes ?? 0).toString(),
primary_size: new ByteSizeValue(indexStats?.primaries?.store?.size_in_bytes ?? 0).toString(),
isFrozen: indexData.settings?.index?.frozen === 'true', isFrozen: indexData.settings?.index?.frozen === 'true',
aliases: aliases.length ? aliases : 'none', aliases: aliases.length ? aliases : 'none',
hidden: indexData.settings?.index?.hidden === 'true', hidden: indexData.settings?.index?.hidden === 'true',
data_stream: indexData.data_stream, data_stream: indexData.data_stream,
}; };
if (indicesStats) {
const indexStats = indicesStats[indexName];
return {
...baseResponse,
health: indexStats?.health,
status: indexStats?.status,
uuid: indexStats?.uuid,
documents: indexStats?.primaries?.docs?.count ?? 0,
documents_deleted: indexStats?.primaries?.docs?.deleted ?? 0,
size: new ByteSizeValue(indexStats?.total?.store?.size_in_bytes ?? 0).toString(),
primary_size: new ByteSizeValue(
indexStats?.primaries?.store?.size_in_bytes ?? 0
).toString(),
};
}
return baseResponse;
}); });
} }
export const fetchIndices = async ( export const fetchIndices = async ({
client: IScopedClusterClient, client,
indexDataEnricher: IndexDataEnricher, indexDataEnricher,
indexNames?: string[] config,
) => { indexNames,
const indices = await fetchIndicesCall(client, indexNames); }: {
client: IScopedClusterClient;
indexDataEnricher: IndexDataEnricher;
config: RouteDependencies['config'];
indexNames?: string[];
}) => {
const indices = await fetchIndicesCall(client, config, indexNames);
return await indexDataEnricher.enrichIndices(indices, client); return await indexDataEnricher.enrichIndices(indices, client);
}; };

View file

@ -55,6 +55,7 @@ export class IndexMgmtServerPlugin implements Plugin<IndexManagementPluginSetup,
config: { config: {
isSecurityEnabled: () => security !== undefined && security.license.isEnabled(), isSecurityEnabled: () => security !== undefined && security.license.isEnabled(),
isLegacyTemplatesEnabled: this.config.enableLegacyTemplates, isLegacyTemplatesEnabled: this.config.enableLegacyTemplates,
isIndexStatsEnabled: this.config.enableIndexStats,
}, },
indexDataEnricher: this.indexDataEnricher, indexDataEnricher: this.indexDataEnricher,
lib: { lib: {

View file

@ -47,6 +47,7 @@ describe('GET privileges', () => {
config: { config: {
isSecurityEnabled: () => true, isSecurityEnabled: () => true,
isLegacyTemplatesEnabled: true, isLegacyTemplatesEnabled: true,
isIndexStatsEnabled: true,
}, },
indexDataEnricher: mockedIndexDataEnricher, indexDataEnricher: mockedIndexDataEnricher,
lib: { lib: {
@ -114,6 +115,7 @@ describe('GET privileges', () => {
config: { config: {
isSecurityEnabled: () => false, isSecurityEnabled: () => false,
isLegacyTemplatesEnabled: true, isLegacyTemplatesEnabled: true,
isIndexStatsEnabled: true,
}, },
indexDataEnricher: mockedIndexDataEnricher, indexDataEnricher: mockedIndexDataEnricher,
lib: { lib: {

View file

@ -13,13 +13,14 @@ export function registerListRoute({
router, router,
indexDataEnricher, indexDataEnricher,
lib: { handleEsError }, lib: { handleEsError },
config,
}: RouteDependencies) { }: RouteDependencies) {
router.get( router.get(
{ path: addBasePath('/indices'), validate: false }, { path: addBasePath('/indices'), validate: false },
async (context, request, response) => { async (context, request, response) => {
const { client } = (await context.core).elasticsearch; const { client } = (await context.core).elasticsearch;
try { try {
const indices = await fetchIndices(client, indexDataEnricher); const indices = await fetchIndices({ client, indexDataEnricher, config });
return response.ok({ body: indices }); return response.ok({ body: indices });
} catch (error) { } catch (error) {
return handleEsError({ error, response }); return handleEsError({ error, response });

View file

@ -21,6 +21,7 @@ export function registerReloadRoute({
router, router,
indexDataEnricher, indexDataEnricher,
lib: { handleEsError }, lib: { handleEsError },
config,
}: RouteDependencies) { }: RouteDependencies) {
router.post( router.post(
{ path: addBasePath('/indices/reload'), validate: { body: bodySchema } }, { path: addBasePath('/indices/reload'), validate: { body: bodySchema } },
@ -29,7 +30,7 @@ export function registerReloadRoute({
const { indexNames = [] } = (request.body as typeof bodySchema.type) ?? {}; const { indexNames = [] } = (request.body as typeof bodySchema.type) ?? {};
try { try {
const indices = await fetchIndices(client, indexDataEnricher, indexNames); const indices = await fetchIndices({ client, indexDataEnricher, config, indexNames });
return response.ok({ body: indices }); return response.ok({ body: indices });
} catch (error) { } catch (error) {
return handleEsError({ error, response }); return handleEsError({ error, response });

View file

@ -23,11 +23,14 @@ export class ApiRoutes {
registerIndicesRoutes(dependencies); registerIndicesRoutes(dependencies);
registerTemplateRoutes(dependencies); registerTemplateRoutes(dependencies);
registerSettingsRoutes(dependencies); registerSettingsRoutes(dependencies);
registerStatsRoute(dependencies);
registerMappingRoute(dependencies); registerMappingRoute(dependencies);
registerComponentTemplateRoutes(dependencies); registerComponentTemplateRoutes(dependencies);
registerNodesRoute(dependencies); registerNodesRoute(dependencies);
registerEnrichPoliciesRoute(dependencies); registerEnrichPoliciesRoute(dependencies);
if (dependencies.config.isIndexStatsEnabled !== false) {
registerStatsRoute(dependencies);
}
} }
start() {} start() {}

View file

@ -13,6 +13,7 @@ export const routeDependencies: Omit<RouteDependencies, 'router'> = {
config: { config: {
isSecurityEnabled: jest.fn().mockReturnValue(true), isSecurityEnabled: jest.fn().mockReturnValue(true),
isLegacyTemplatesEnabled: true, isLegacyTemplatesEnabled: true,
isIndexStatsEnabled: true,
}, },
indexDataEnricher: new IndexDataEnricher(), indexDataEnricher: new IndexDataEnricher(),
lib: { lib: {

View file

@ -24,6 +24,7 @@ export interface RouteDependencies {
config: { config: {
isSecurityEnabled: () => boolean; isSecurityEnabled: () => boolean;
isLegacyTemplatesEnabled: boolean; isLegacyTemplatesEnabled: boolean;
isIndexStatsEnabled: boolean;
}; };
indexDataEnricher: IndexDataEnricher; indexDataEnricher: IndexDataEnricher;
lib: { lib: {

View file

@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) { export default function ({ loadTestFile }: FtrProviderContext) {
describe('Index Management APIs', function () { describe('Index Management APIs', function () {
loadTestFile(require.resolve('./index_templates')); loadTestFile(require.resolve('./index_templates'));
loadTestFile(require.resolve('./indices'));
}); });
} }

View file

@ -0,0 +1,61 @@
/*
* 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 expect from 'expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
const API_BASE_PATH = '/api/index_management';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const es = getService('es');
const log = getService('log');
describe('Indices', function () {
const indexName = `index-${Math.random()}`;
before(async () => {
// Create a new index to test against
try {
await es.indices.create({ index: indexName });
} catch (err) {
log.debug('[Setup error] Error creating index');
throw err;
}
});
after(async () => {
// Cleanup index created for testing purposes
try {
await es.indices.delete({
index: indexName,
});
} catch (err) {
log.debug('[Cleanup error] Error deleting index');
throw err;
}
});
describe('get all', () => {
it('should list indices with the expected parameters', async () => {
const { body: indices } = await supertest
.get(`${API_BASE_PATH}/indices`)
.set('kbn-xsrf', 'xxx')
.set('x-elastic-internal-origin', 'xxx')
.expect(200);
const indexFound = indices.find((index: { name: string }) => index.name === indexName);
expect(indexFound).toBeTruthy();
const expectedKeys = ['aliases', 'hidden', 'isFrozen', 'primary', 'replica', 'name'].sort();
expect(Object.keys(indexFound).sort()).toEqual(expectedKeys);
});
});
});
}

View file

@ -10,5 +10,6 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ loadTestFile }: FtrProviderContext) => { export default ({ loadTestFile }: FtrProviderContext) => {
describe('Index Management', function () { describe('Index Management', function () {
loadTestFile(require.resolve('./index_templates')); loadTestFile(require.resolve('./index_templates'));
loadTestFile(require.resolve('./indices'));
}); });
}; };

View file

@ -0,0 +1,35 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
const testSubjects = getService('testSubjects');
const pageObjects = getPageObjects(['common', 'indexManagement', 'header']);
const browser = getService('browser');
const security = getService('security');
const retry = getService('retry');
describe('Indices', function () {
before(async () => {
await security.testUser.setRoles(['index_management_user']);
await pageObjects.common.navigateToApp('indexManagement');
// Navigate to the indices tab
await pageObjects.indexManagement.changeTabs('indicesTab');
});
it('renders the indices tab', async () => {
await retry.waitFor('indices list to be visible', async () => {
return await testSubjects.exists('indicesList');
});
const url = await browser.getCurrentUrl();
expect(url).to.contain(`/indices`);
});
});
};