diff --git a/x-pack/platform/packages/shared/index-management/index_management_shared_types/moon.yml b/x-pack/platform/packages/shared/index-management/index_management_shared_types/moon.yml index e1121b2f19bdd..9849668c334ad 100644 --- a/x-pack/platform/packages/shared/index-management/index_management_shared_types/moon.yml +++ b/x-pack/platform/packages/shared/index-management/index_management_shared_types/moon.yml @@ -22,6 +22,7 @@ dependsOn: - '@kbn/utility-types' - '@kbn/share-plugin' - '@kbn/management-plugin' + - '@kbn/core' tags: - shared-common - package diff --git a/x-pack/platform/packages/shared/index-management/index_management_shared_types/src/types.ts b/x-pack/platform/packages/shared/index-management/index_management_shared_types/src/types.ts index ab1d0b3e410b8..8224ad7985aed 100644 --- a/x-pack/platform/packages/shared/index-management/index_management_shared_types/src/types.ts +++ b/x-pack/platform/packages/shared/index-management/index_management_shared_types/src/types.ts @@ -15,9 +15,24 @@ import type { ScopedHistory } from '@kbn/core-application-browser'; import type { SerializableRecord } from '@kbn/utility-types'; import type { LocatorPublic } from '@kbn/share-plugin/public'; import type { ManagementAppMountParams } from '@kbn/management-plugin/public'; +import type { HttpSetup } from '@kbn/core/public'; import type { ExtensionsSetup } from './services/extensions_service'; import type { PublicApiServiceSetup } from './services/public_api_service'; +export interface EnricherResponse { + source: string; + indices?: Index[]; + error?: boolean; + /** + * Apply this enricher's updates to any index aliases + */ + applyToAliases?: boolean; +} +export interface Enricher { + name: string; + fn: (client: HttpSetup, signal: AbortSignal) => Promise; +} + export type IndexManagementLocatorParams = SerializableRecord & ( | { @@ -82,6 +97,9 @@ export interface IndexManagementPluginSetup { extensionsService: ExtensionsSetup; renderIndexManagementApp: (params: IndexManagementAppMountParams) => Promise<() => void>; locator?: IndexManagementLocator; + indexDataEnricher: { + add: (enricher: Enricher) => void; + }; } export interface IndexManagementPluginStart { @@ -104,13 +122,15 @@ export interface IndexManagementPluginStart { }) => React.FC; } -export interface Index { +export interface Index extends IndexAttributes { name: string; +} +export interface IndexAttributes { primary?: number | string; replica?: number | string; - isFrozen: boolean; - hidden: boolean; - aliases: string | string[]; + isFrozen?: boolean; + hidden?: boolean; + aliases?: string | string[]; data_stream?: string; mode?: string; @@ -125,8 +145,8 @@ export interface Index { status?: IndicesStatsIndexMetadataState; uuid?: Uuid; documents?: number; - size?: string; - primary_size?: string; + size?: number; + primary_size?: number; documents_deleted?: number; } diff --git a/x-pack/platform/packages/shared/index-management/index_management_shared_types/tsconfig.json b/x-pack/platform/packages/shared/index-management/index_management_shared_types/tsconfig.json index 2f15478bd5975..67cfda437d90f 100644 --- a/x-pack/platform/packages/shared/index-management/index_management_shared_types/tsconfig.json +++ b/x-pack/platform/packages/shared/index-management/index_management_shared_types/tsconfig.json @@ -19,6 +19,7 @@ "@kbn/core-application-browser", "@kbn/utility-types", "@kbn/share-plugin", - "@kbn/management-plugin" + "@kbn/management-plugin", + "@kbn/core" ] } diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/moon.yml b/x-pack/platform/plugins/private/cross_cluster_replication/moon.yml index 62863a1f0ac41..8b23fd70580c2 100644 --- a/x-pack/platform/plugins/private/cross_cluster_replication/moon.yml +++ b/x-pack/platform/plugins/private/cross_cluster_replication/moon.yml @@ -38,6 +38,7 @@ dependsOn: - '@kbn/react-kibana-context-render' - '@kbn/licensing-types' - '@kbn/data-views-plugin' + - '@kbn/index-management-shared-types' tags: - plugin - prod diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/ccr_data_enricher.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/ccr_data_enricher.ts new file mode 100644 index 0000000000000..b79f1c88af8e0 --- /dev/null +++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/ccr_data_enricher.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HttpSetup } from '@kbn/core/public'; +import type { CcrFollowInfoResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { EnricherResponse } from '@kbn/index-management-shared-types'; +import { i18n } from '@kbn/i18n'; +const SOURCE = i18n.translate('xpack.crossClusterReplication.ccrDataEnricher.source', { + defaultMessage: 'cross cluster replication', +}); + +export const ccrDataEnricher = { + name: SOURCE, + fn: async (client: HttpSetup, signal: AbortSignal): Promise => + client + .get('/api/cross_cluster_replication/follower_info', { signal }) + .then((response) => { + return { + indices: response.follower_indices.map((followerIndex) => ({ + name: followerIndex.follower_index, + isFollowerIndex: true, + })), + source: SOURCE, + }; + }), +}; diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/public/plugin.ts b/x-pack/platform/plugins/private/cross_cluster_replication/public/plugin.ts index ad62564f24505..35d0115727def 100644 --- a/x-pack/platform/plugins/private/cross_cluster_replication/public/plugin.ts +++ b/x-pack/platform/plugins/private/cross_cluster_replication/public/plugin.ts @@ -14,8 +14,8 @@ import { PLUGIN, MANAGEMENT_ID } from '../common/constants'; import { init as initUiMetric } from './app/services/track_ui_metric'; import { init as initNotification } from './app/services/notifications'; import type { PluginDependencies, ClientConfigType } from './types'; +import { ccrDataEnricher } from './ccr_data_enricher'; -// @ts-ignore; import { setHttpClient } from './app/services/api'; export class CrossClusterReplicationPlugin implements Plugin { @@ -105,6 +105,7 @@ export class CrossClusterReplicationPlugin implements Plugin { }; indexManagement.extensionsService.addBadge(followerBadgeExtension); + indexManagement.indexDataEnricher.add(ccrDataEnricher); } } else { ccrApp.disable(); diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/index.ts b/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/index.ts index 62965856b617b..79230b1053c0b 100644 --- a/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/index.ts +++ b/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/index.ts @@ -13,6 +13,7 @@ import { registerPauseRoute } from './register_pause_route'; import { registerResumeRoute } from './register_resume_route'; import { registerUnfollowRoute } from './register_unfollow_route'; import { registerUpdateRoute } from './register_update_route'; +import { registerGetFollowerInfoRoute } from './register_get_follower_info'; export function registerFollowerIndexRoutes(dependencies: RouteDependencies) { registerCreateRoute(dependencies); @@ -22,4 +23,5 @@ export function registerFollowerIndexRoutes(dependencies: RouteDependencies) { registerResumeRoute(dependencies); registerUnfollowRoute(dependencies); registerUpdateRoute(dependencies); + registerGetFollowerInfoRoute(dependencies); } diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_get_follower_info.ts b/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_get_follower_info.ts new file mode 100644 index 0000000000000..e182e3d13b24a --- /dev/null +++ b/x-pack/platform/plugins/private/cross_cluster_replication/server/routes/api/follower_index/register_get_follower_info.ts @@ -0,0 +1,43 @@ +/* + * 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 { addBasePath } from '../../../services'; +import type { RouteDependencies } from '../../../types'; + +export const registerGetFollowerInfoRoute = ({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/follower_info'), + security: { + authz: { + enabled: false, + reason: 'Relies on es client for authorization', + }, + }, + validate: {}, + }, + license.guardApiRoute(async (context, request, response) => { + const { client } = (await context.core).elasticsearch; + + try { + const body = await client.asCurrentUser.ccr.followInfo({ + index: '_all', + }); + + return response.ok({ + body, + }); + } catch (error) { + return handleEsError({ error, response }); + } + }) + ); +}; diff --git a/x-pack/platform/plugins/private/cross_cluster_replication/tsconfig.json b/x-pack/platform/plugins/private/cross_cluster_replication/tsconfig.json index b0dad4e18cf09..383c6df2a9f6f 100644 --- a/x-pack/platform/plugins/private/cross_cluster_replication/tsconfig.json +++ b/x-pack/platform/plugins/private/cross_cluster_replication/tsconfig.json @@ -32,6 +32,7 @@ "@kbn/react-kibana-context-render", "@kbn/licensing-types", "@kbn/data-views-plugin", + "@kbn/index-management-shared-types", ], "exclude": [ "target/**/*", diff --git a/x-pack/platform/plugins/private/index_lifecycle_management/__jest__/extend_index_management.test.tsx b/x-pack/platform/plugins/private/index_lifecycle_management/__jest__/extend_index_management.test.tsx index 633b66a53bcb0..932f35198b7d8 100644 --- a/x-pack/platform/plugins/private/index_lifecycle_management/__jest__/extend_index_management.test.tsx +++ b/x-pack/platform/plugins/private/index_lifecycle_management/__jest__/extend_index_management.test.tsx @@ -12,7 +12,7 @@ import { useEuiTheme } from '@elastic/eui'; import { screen, waitFor } from '@testing-library/react'; import { renderWithI18n } from '@kbn/test-jest-helpers'; import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks'; -import type { Index } from '@kbn/index-management-plugin/common'; +import type { Index } from '../common/types'; import { init } from '../integration_tests/helpers/http_requests'; import { @@ -66,8 +66,8 @@ const indexWithoutLifecyclePolicy: Index = { replica: 1, documents: 1, documents_deleted: 0, - size: '3.4kb', - primary_size: '3.4kb', + size: 3480, + primary_size: 3480, aliases: 'none', isFrozen: false, hidden: false, @@ -86,8 +86,8 @@ const indexWithLifecyclePolicy: Index = { replica: 1, documents: 2, documents_deleted: 0, - size: '6.5kb', - primary_size: '6.5kb', + size: 6656, + primary_size: 6656, aliases: 'none', isFrozen: false, hidden: false, @@ -115,8 +115,8 @@ const indexWithLifecycleError: Index = { replica: 1, documents: 2, documents_deleted: 0, - size: '6.5kb', - primary_size: '6.5kb', + size: 6656, + primary_size: 6656, aliases: 'none', isFrozen: false, hidden: false, @@ -148,8 +148,8 @@ const indexWithLifecyclePhaseDefinition: Index = { replica: 1, documents: 2, documents_deleted: 0, - size: '6.5kb', - primary_size: '6.5kb', + size: 6656, + primary_size: 6656, aliases: 'none', isFrozen: false, hidden: false, @@ -182,8 +182,8 @@ const indexWithLifecycleWaitingStep: Index = { replica: 1, documents: 2, documents_deleted: 0, - size: '6.5kb', - primary_size: '6.5kb', + size: 6656, + primary_size: 6656, aliases: 'none', isFrozen: false, hidden: false, @@ -216,8 +216,8 @@ const indexWithNonExistentPolicyError: Index = { replica: 1, documents: 2, documents_deleted: 0, - size: '6.5kb', - primary_size: '6.5kb', + size: 6656, + primary_size: 6656, aliases: 'none', isFrozen: false, hidden: false, diff --git a/x-pack/platform/plugins/private/index_lifecycle_management/common/types/index.ts b/x-pack/platform/plugins/private/index_lifecycle_management/common/types/index.ts index 001abc6673242..bee31c373d69a 100644 --- a/x-pack/platform/plugins/private/index_lifecycle_management/common/types/index.ts +++ b/x-pack/platform/plugins/private/index_lifecycle_management/common/types/index.ts @@ -9,6 +9,8 @@ export type * from './api'; export * from '@kbn/index-lifecycle-management-common-shared'; +export type { Index } from '@kbn/index-management-plugin/common'; + /** * These roles reflect how nodes are stratified into different data tiers. */ diff --git a/x-pack/platform/plugins/private/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx b/x-pack/platform/plugins/private/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx index 6f4c70b54d491..9aa338c0ddc63 100644 --- a/x-pack/platform/plugins/private/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx +++ b/x-pack/platform/plugins/private/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx @@ -26,7 +26,7 @@ import { htmlIdGenerator, } from '@elastic/eui'; -import type { Index } from '@kbn/index-management-plugin/common'; +import type { Index } from '../../../common/types'; import { loadPolicies, addLifecyclePolicyToIndex } from '../../application/services/api'; import { showApiError } from '../../application/services/api_errors'; import { toasts } from '../../application/services/notification'; diff --git a/x-pack/platform/plugins/private/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx b/x-pack/platform/plugins/private/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx index 6426e0b2416c5..2e0516517134b 100644 --- a/x-pack/platform/plugins/private/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx +++ b/x-pack/platform/plugins/private/index_lifecycle_management/public/extend_index_management/components/index_lifecycle_summary.tsx @@ -25,8 +25,9 @@ import { } from '@elastic/eui'; import type { ApplicationStart } from '@kbn/core/public'; -import type { Index, IndexDetailsTab } from '@kbn/index-management-shared-types'; +import type { IndexDetailsTab } from '@kbn/index-management-shared-types'; import type { IlmExplainLifecycleLifecycleExplainManaged } from '@elastic/elasticsearch/lib/api/types'; +import type { Index } from '../../../common/types'; import type { Phase } from '../../../common/types'; import { getPolicyEditPath } from '../../application/services/navigation'; import { usePhaseColors } from '../../application/lib'; diff --git a/x-pack/platform/plugins/private/index_lifecycle_management/public/extend_index_management/index.tsx b/x-pack/platform/plugins/private/index_lifecycle_management/public/extend_index_management/index.tsx index cae5b8c5f5842..ecc1049861f3f 100644 --- a/x-pack/platform/plugins/private/index_lifecycle_management/public/extend_index_management/index.tsx +++ b/x-pack/platform/plugins/private/index_lifecycle_management/public/extend_index_management/index.tsx @@ -11,7 +11,8 @@ import { i18n } from '@kbn/i18n'; import { EuiSearchBar } from '@elastic/eui'; import type { ApplicationStart } from '@kbn/core/public'; -import type { Index, IndexManagementPluginSetup } from '@kbn/index-management-plugin/public'; +import type { IndexManagementPluginSetup } from '@kbn/index-management-plugin/public'; +import type { Index } from '../../common/types'; import { retryLifecycleForIndex } from '../application/services/api'; import { indexLifecycleTab } from './components/index_lifecycle_summary'; diff --git a/x-pack/platform/plugins/private/index_lifecycle_management/public/index_lifecycle_data_enricher.ts b/x-pack/platform/plugins/private/index_lifecycle_management/public/index_lifecycle_data_enricher.ts new file mode 100644 index 0000000000000..302f468f65b20 --- /dev/null +++ b/x-pack/platform/plugins/private/index_lifecycle_management/public/index_lifecycle_data_enricher.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IlmExplainLifecycleResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { HttpSetup } from '@kbn/core/public'; +import type { EnricherResponse } from '@kbn/index-management-shared-types'; +import { i18n } from '@kbn/i18n'; +const SOURCE = i18n.translate('xpack.indexLifecycleMgmt.indexLifecycleDataEnricher.source', { + defaultMessage: 'index lifecycle', +}); + +export const indexLifecycleDataEnricher = { + name: SOURCE, + fn: async (client: HttpSetup, signal: AbortSignal): Promise => + client + .get('/api/index_lifecycle_management/explain', { signal }) + .then((response) => { + return { + indices: Object.keys(response.indices).map((index) => ({ + name: index, + ilm: response.indices[index], + })), + source: SOURCE, + }; + }), +}; diff --git a/x-pack/platform/plugins/private/index_lifecycle_management/public/plugin.tsx b/x-pack/platform/plugins/private/index_lifecycle_management/public/plugin.tsx index 34400495806d7..a167028280479 100644 --- a/x-pack/platform/plugins/private/index_lifecycle_management/public/plugin.tsx +++ b/x-pack/platform/plugins/private/index_lifecycle_management/public/plugin.tsx @@ -21,6 +21,7 @@ import type { StartDependencies, } from './types'; import { IlmLocatorDefinition } from './locator'; +import { indexLifecycleDataEnricher } from './index_lifecycle_data_enricher'; import { PublicApiService } from './services'; export class IndexLifecycleManagementPlugin @@ -115,6 +116,7 @@ export class IndexLifecycleManagementPlugin if (indexManagement) { addAllExtensions(indexManagement.extensionsService); + indexManagement.indexDataEnricher.add(indexLifecycleDataEnricher); } plugins.share.url.locators.create( diff --git a/x-pack/platform/plugins/private/index_lifecycle_management/server/routes/api/policies/index.ts b/x-pack/platform/plugins/private/index_lifecycle_management/server/routes/api/policies/index.ts index e5d04609e458b..97084b0cc287f 100644 --- a/x-pack/platform/plugins/private/index_lifecycle_management/server/routes/api/policies/index.ts +++ b/x-pack/platform/plugins/private/index_lifecycle_management/server/routes/api/policies/index.ts @@ -9,9 +9,11 @@ import type { RouteDependencies } from '../../../types'; import { registerFetchRoute } from './register_fetch_route'; import { registerCreateRoute } from './register_create_route'; import { registerDeleteRoute } from './register_delete_route'; +import { registerExplainRoute } from './register_explain_route'; export function registerPoliciesRoutes(dependencies: RouteDependencies) { registerFetchRoute(dependencies); registerCreateRoute(dependencies); registerDeleteRoute(dependencies); + registerExplainRoute(dependencies); } diff --git a/x-pack/platform/plugins/private/index_lifecycle_management/server/routes/api/policies/register_explain_route.ts b/x-pack/platform/plugins/private/index_lifecycle_management/server/routes/api/policies/register_explain_route.ts new file mode 100644 index 0000000000000..c2b5753fa0bed --- /dev/null +++ b/x-pack/platform/plugins/private/index_lifecycle_management/server/routes/api/policies/register_explain_route.ts @@ -0,0 +1,37 @@ +/* + * 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 { addBasePath } from '../../../services'; +import type { RouteDependencies } from '../../../types'; + +export const registerExplainRoute = ({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/explain'), + security: { + authz: { + enabled: false, + reason: 'Relies on es client for authorization', + }, + }, + validate: {}, + }, + license.guardApiRoute(async (context, request, response) => { + const { client } = (await context.core).elasticsearch; + try { + const body = await client.asCurrentUser.ilm.explainLifecycle({ index: '_all' }); + return response.ok({ body }); + } catch (error) { + return handleEsError({ error, response }); + } + }) + ); +}; diff --git a/x-pack/platform/plugins/private/rollup/moon.yml b/x-pack/platform/plugins/private/rollup/moon.yml index 7e7be20e01fc3..8fa4b750b3337 100644 --- a/x-pack/platform/plugins/private/rollup/moon.yml +++ b/x-pack/platform/plugins/private/rollup/moon.yml @@ -41,6 +41,7 @@ dependsOn: - '@kbn/rollup' - '@kbn/licensing-types' - '@kbn/data-view-validation' + - '@kbn/index-management-shared-types' tags: - plugin - prod diff --git a/x-pack/platform/plugins/private/rollup/public/plugin.ts b/x-pack/platform/plugins/private/rollup/public/plugin.ts index 37f433215a1e4..951426697cc8a 100644 --- a/x-pack/platform/plugins/private/rollup/public/plugin.ts +++ b/x-pack/platform/plugins/private/rollup/public/plugin.ts @@ -17,6 +17,7 @@ import { UIM_APP_NAME } from '../common'; import { setHttp, init as initDocumentation } from './crud_app/services'; import { setNotifications, setFatalErrors, setUiStatsReporter } from './kibana_services'; import type { ClientConfigType } from './types'; +import { rollupDataEnricher } from './rollup_data_enricher'; export interface RollupPluginSetupDependencies { home?: HomePublicPluginSetup; @@ -46,6 +47,7 @@ export class RollupPlugin implements Plugin { if (indexManagement) { indexManagement.extensionsService.addBadge(rollupBadgeExtension); indexManagement.extensionsService.addToggle(rollupToggleExtension); + indexManagement.indexDataEnricher.add(rollupDataEnricher); } if (home) { diff --git a/x-pack/platform/plugins/private/rollup/public/rollup_data_enricher.ts b/x-pack/platform/plugins/private/rollup/public/rollup_data_enricher.ts new file mode 100644 index 0000000000000..79fa64fc525de --- /dev/null +++ b/x-pack/platform/plugins/private/rollup/public/rollup_data_enricher.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HttpSetup } from '@kbn/core/public'; +import type { RollupGetRollupIndexCapsResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { EnricherResponse } from '@kbn/index-management-shared-types'; +import { i18n } from '@kbn/i18n'; +const SOURCE = i18n.translate('xpack.rollupJobs.rollupDataEnricher.source', { + defaultMessage: 'rollup', +}); + +export const rollupDataEnricher = { + name: SOURCE, + fn: async (client: HttpSetup, signal: AbortSignal): Promise => + client + .get('/api/rollup/indices_caps', { signal }) + .then((response) => { + return { + applyToAliases: true, + indices: Object.keys(response).reduce<{ name: string; isRollupIndex: true }[]>( + (acc, rollupJob) => { + response[rollupJob].rollup_jobs.forEach((job) => { + acc.push({ + name: job.rollup_index, + isRollupIndex: true, + }); + }); + return acc; + }, + [] + ), + source: SOURCE, + }; + }), +}; diff --git a/x-pack/platform/plugins/private/rollup/server/routes/api/indices/index.ts b/x-pack/platform/plugins/private/rollup/server/routes/api/indices/index.ts index 7f667fd00db88..27e8a9a3929e4 100644 --- a/x-pack/platform/plugins/private/rollup/server/routes/api/indices/index.ts +++ b/x-pack/platform/plugins/private/rollup/server/routes/api/indices/index.ts @@ -8,8 +8,10 @@ import type { RouteDependencies } from '../../../types'; import { registerGetRoute } from './register_get_route'; import { registerValidateIndexPatternRoute } from './register_validate_index_pattern_route'; +import { registerGetIndexCapsRoute } from './register_get_index_caps'; export function registerIndicesRoutes(dependencies: RouteDependencies) { registerGetRoute(dependencies); registerValidateIndexPatternRoute(dependencies); + registerGetIndexCapsRoute(dependencies); } diff --git a/x-pack/platform/plugins/private/rollup/server/routes/api/indices/register_get_index_caps.ts b/x-pack/platform/plugins/private/rollup/server/routes/api/indices/register_get_index_caps.ts new file mode 100644 index 0000000000000..4e07fbc54575c --- /dev/null +++ b/x-pack/platform/plugins/private/rollup/server/routes/api/indices/register_get_index_caps.ts @@ -0,0 +1,43 @@ +/* + * 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 { addBasePath } from '../../../services'; +import type { RouteDependencies } from '../../../types'; + +/** + * Returns a list of all rollup index names + */ +export const registerGetIndexCapsRoute = ({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) => { + router.get( + { + // this endpoint is used by the data views plugin, see https://github.com/elastic/kibana/issues/152708 + path: addBasePath('/indices_caps'), + security: { + authz: { + enabled: false, + reason: 'Relies on es client for authorization', + }, + }, + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { client: clusterClient } = (await context.core).elasticsearch; + const body = await clusterClient.asCurrentUser.rollup.getRollupIndexCaps({ + index: '*', + }); + return response.ok({ body }); + } catch (err) { + return handleEsError({ error: err, response }); + } + }) + ); +}; diff --git a/x-pack/platform/plugins/private/rollup/tsconfig.json b/x-pack/platform/plugins/private/rollup/tsconfig.json index 7be6e059e980d..c8a9885135f95 100644 --- a/x-pack/platform/plugins/private/rollup/tsconfig.json +++ b/x-pack/platform/plugins/private/rollup/tsconfig.json @@ -36,6 +36,7 @@ "@kbn/rollup", "@kbn/licensing-types", "@kbn/data-view-validation", + "@kbn/index-management-shared-types", ], "exclude": [ diff --git a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/helpers/actions/data_stream_actions.ts b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/helpers/actions/data_stream_actions.ts index e4ce12ccec173..9aa7c65bf2fcd 100644 --- a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/helpers/actions/data_stream_actions.ts +++ b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/helpers/actions/data_stream_actions.ts @@ -399,7 +399,7 @@ export const createNonDataStreamIndex = (name: string) => ({ replica: 1, documents: 10000, documents_deleted: 100, - size: '156kb', - primary_size: '156kb', + size: 159744, + primary_size: 159744, name, }); diff --git a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/helpers/http_requests.ts index b17695ba5a958..bb0925741fa90 100644 --- a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/helpers/http_requests.ts @@ -66,8 +66,37 @@ const registerHttpRequestMockHelpers = ( const setLoadTemplatesResponse = (response?: HttpResponse, error?: ResponseError) => mockResponse('GET', `${API_BASE_PATH}/index_templates`, response, error); - const setLoadIndicesResponse = (response?: HttpResponse, error?: ResponseError) => - mockResponse('GET', `${API_BASE_PATH}/indices`, response, error); + const setLoadIndicesStatsResponse = (response?: HttpResponse, error?: ResponseError) => + mockResponse('GET', `${API_BASE_PATH}/indices_stats`, response, error); + + /** + * The indices list endpoint switched from returning an array (`/indices`) to returning + * a record keyed by index name (`/indices_get`). Most tests still pass an array, so we + * normalize here and mock both endpoints for compatibility. + */ + const setLoadIndicesResponse = (response?: HttpResponse, error?: ResponseError) => { + const normalizedIndicesGetResponse = (() => { + if (!response) return response; + if (Array.isArray(response)) { + return response.reduce>((acc, index: any) => { + if (index?.name) acc[index.name] = index; + return acc; + }, {}); + } + return response; + })(); + + // New endpoint (record keyed by index name) + mockResponse('GET', `${API_BASE_PATH}/indices_get`, normalizedIndicesGetResponse, error); + + // Legacy endpoint (array) - keep for older consumers/tests + const legacyResponse = Array.isArray(response) + ? response + : response && typeof response === 'object' + ? Object.values(response as Record) + : response; + mockResponse('GET', `${API_BASE_PATH}/indices`, legacyResponse, error); + }; const setReloadIndicesResponse = (response?: HttpResponse, error?: ResponseError) => mockResponse('POST', `${API_BASE_PATH}/indices/reload`, response, error); @@ -221,6 +250,7 @@ const registerHttpRequestMockHelpers = ( }; return { setLoadTemplatesResponse, + setLoadIndicesStatsResponse, setLoadIndicesResponse, setReloadIndicesResponse, setLoadDataStreamsResponse, diff --git a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/home/data_streams_tab.test.ts index 746eefa5c2a3b..1fbaec2e48c01 100644 --- a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/home/data_streams_tab.test.ts +++ b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/home/data_streams_tab.test.ts @@ -790,7 +790,7 @@ describe('Data Streams tab', () => { }) ); }); - }); + }, 10000); test('allows to set infinite retention period', async () => { setupBulkRetentionMocks(); @@ -833,7 +833,7 @@ describe('Data Streams tab', () => { }) ); }); - }); + }, 10000); }); describe('detail panel', () => { @@ -1004,7 +1004,7 @@ describe('Data Streams tab', () => { }) ); }); - }); + }, 10000); test('can disable lifecycle', async () => { setupDataStreamsMocks(); @@ -1047,7 +1047,7 @@ describe('Data Streams tab', () => { }) ); }); - }); + }, 10000); test('allows to set infinite retention period', async () => { setupDataStreamsMocks(); diff --git a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/home/indices_tab.test.tsx b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/home/indices_tab.test.tsx index ceaa86c5112ea..ecdbab7bb29bb 100644 --- a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/home/indices_tab.test.tsx +++ b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/home/indices_tab.test.tsx @@ -17,6 +17,7 @@ import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common'; import { setupEnvironment } from '../helpers/setup_environment'; import { renderHome } from '../helpers/render_home'; import { httpService } from '../../../public/application/services/http'; +import { indexDataEnricher } from '../../../public/services'; import { createIndexTableActions, createCreateIndexActions, @@ -368,7 +369,7 @@ describe('', () => { expect(screen.getByTestId('indexTableCell-primary')).toHaveTextContent('1'); expect(screen.getByTestId('indexTableCell-replica')).toHaveTextContent('1'); expect(screen.getByTestId('indexTableCell-documents')).toBeInTheDocument(); - expect(screen.getByTestId('indexTableCell-size')).toHaveTextContent('156kb'); + expect(screen.getByTestId('indexTableCell-size')).toHaveTextContent('156.00 KB'); }); test('renders only size and docs count when enableIndexStats is false, enableSizeAndDocCount is true', async () => { @@ -391,7 +392,7 @@ describe('', () => { expect(screen.getByTestId('indexTableCell-name')).toHaveTextContent('test'); // Size and docs should be shown expect(screen.getByTestId('indexTableCell-documents')).toBeInTheDocument(); - expect(screen.getByTestId('indexTableCell-size')).toHaveTextContent('156kb'); + expect(screen.getByTestId('indexTableCell-size')).toHaveTextContent('156.00 KB'); // Health, status, primary, replica should NOT be shown (enableIndexStats is false) expect(screen.queryByTestId('indexTableCell-health')).not.toBeInTheDocument(); expect(screen.queryByTestId('indexTableCell-status')).not.toBeInTheDocument(); @@ -421,6 +422,29 @@ describe('', () => { expect(screen.queryByTestId('indexTableCell-documents')).not.toBeInTheDocument(); expect(screen.queryByTestId('indexTableCell-size')).not.toBeInTheDocument(); }); + + test('shows a warning callout when an index enricher fails', async () => { + httpRequestsMockHelpers.setLoadIndicesResponse([createNonDataStreamIndex(indexName)]); + const originalEnrichers = [...indexDataEnricher.enrichers]; + indexDataEnricher.add({ + name: 'test enricher', + fn: async () => ({ source: 'test enricher', error: true }), + }); + + try { + await renderHome(httpSetup); + + await screen.findByTestId('indexTable'); + expect(screen.getByTestId('indexTableCell-name')).toHaveTextContent('test'); + + const callout = await screen.findByTestId('indicesEnrichmentErrorCallout'); + expect(callout).toHaveTextContent('test enricher'); + } finally { + // Restore enrichers to avoid polluting other tests. + (indexDataEnricher as any)._enrichers.length = 0; + originalEnrichers.forEach((enricher) => indexDataEnricher.add(enricher)); + } + }); }); describe('Create Index', () => { @@ -462,7 +486,11 @@ describe('', () => { const actions = createCreateIndexActions(); expect(httpSetup.get).toHaveBeenCalledTimes(1); - expect(httpSetup.get).toHaveBeenNthCalledWith(1, '/api/index_management/indices'); + expect(httpSetup.get).toHaveBeenNthCalledWith( + 1, + '/api/index_management/indices_get', + expect.anything() + ); await actions.clickCreateIndexButton(); @@ -482,7 +510,11 @@ describe('', () => { // It refreshes indices after saving; wait so the table's async state update settles (avoids act warnings). await waitFor(() => { expect(httpSetup.get).toHaveBeenCalledTimes(2); - expect(httpSetup.get).toHaveBeenNthCalledWith(2, '/api/index_management/indices'); + expect(httpSetup.get).toHaveBeenNthCalledWith( + 2, + '/api/index_management/indices_get', + expect.anything() + ); }); // Creating triggers modal state updates; wait for modal to close so updates don't land after test end. @@ -591,5 +623,50 @@ describe('', () => { expect(navigateToUrl).toHaveBeenCalledTimes(1); expect(navigateToUrl).toHaveBeenCalledWith(url); }); + + it('applies enricher updates to indices via alias when applyToAliases is true', async () => { + const indexName = 'concrete-index'; + const aliasName = 'my-alias'; + + httpRequestsMockHelpers.setLoadIndicesResponse([ + { ...createNonDataStreamIndex(indexName), aliases: [aliasName] }, + ]); + + const originalEnrichers = [...indexDataEnricher.enrichers]; + indexDataEnricher.add({ + name: 'alias enricher', + fn: async () => ({ + source: 'alias enricher', + applyToAliases: true, + indices: [{ name: aliasName, isRollupIndex: true }], + }), + }); + + try { + await renderHome(httpSetup, { + appServicesContext: { + services: { + extensionsService: { + _columns: [ + { + fieldName: 'isRollupIndex', + label: 'Rollup flag', + order: 999, + render: (index: Index) => (index.isRollupIndex ?
ROLLUP
: null), + }, + ], + }, + }, + }, + }); + + await screen.findByTestId('indexTable'); + expect(await screen.findByText('ROLLUP')).toBeInTheDocument(); + } finally { + // Restore enrichers to avoid polluting other tests. + (indexDataEnricher as any)._enrichers.length = 0; + originalEnrichers.forEach((enricher) => indexDataEnricher.add(enricher)); + } + }); }); }); diff --git a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx index db57e04968500..139a0d3bef5d1 100644 --- a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx +++ b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/index_details_page.test.tsx @@ -16,6 +16,7 @@ import type { RouteComponentProps } from 'react-router-dom'; import type { IndexDetailsTab, IndexDetailsTabId } from '../../../common/constants'; import { IndexDetailsSection } from '../../../common/constants'; import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common'; +import { formatBytes } from '../../../public'; import { DetailsPage } from '../../../public/application/sections/home/index_list/details_page/details_page'; import { TYPE_DEFINITION } from '../../../public/application/components/mappings_editor/constants'; @@ -373,7 +374,9 @@ describe('', () => { await renderPage(); const storageDetails = screen.getByTestId('indexDetailsStorage').textContent; expect(storageDetails).toBe( - `Storage${testIndexMock.primary_size}Primary${testIndexMock.size}TotalShards${testIndexMock.primary} Primary / ${testIndexMock.replica} Replicas ` + `Storage${formatBytes(testIndexMock.primary_size)}Primary${formatBytes( + testIndexMock.size + )}TotalShards${testIndexMock.primary} Primary / ${testIndexMock.replica} Replicas ` ); }); diff --git a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/mocks.ts b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/mocks.ts index dc24872972cb4..5a6d0b4f93387 100644 --- a/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/mocks.ts +++ b/x-pack/platform/plugins/shared/index_management/__jest__/client_integration/index_details_page/mocks.ts @@ -17,8 +17,8 @@ export const testIndexMock: Index = { replica: '2', documents: 1, documents_deleted: 0, - size: '20kb', - primary_size: '10kb', + size: 20480, + primary_size: 10240, isFrozen: false, aliases: 'none', hidden: false, diff --git a/x-pack/platform/plugins/shared/index_management/common/types/indices.ts b/x-pack/platform/plugins/shared/index_management/common/types/indices.ts index eeda7aafdd0d8..5f87d5a144245 100644 --- a/x-pack/platform/plugins/shared/index_management/common/types/indices.ts +++ b/x-pack/platform/plugins/shared/index_management/common/types/indices.ts @@ -7,8 +7,8 @@ import type { IndicesIndexSettingsKeys } from '@elastic/elasticsearch/lib/api/types'; -export type { Index } from '@kbn/index-management-shared-types'; - +import type { Index } from '@kbn/index-management-shared-types'; +export type { Index }; interface AnalysisModule { analyzer: { [key: string]: { @@ -31,3 +31,7 @@ export interface IndexSettingsResponse { settings: IndexSettings; defaults: IndexSettings; } + +export interface IndexDataResponse { + body: Index[]; +} diff --git a/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx b/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx index 3ee1a7d7c3e57..6a1c059496c9c 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx +++ b/x-pack/platform/plugins/shared/index_management/public/application/components/mappings_editor/__jest__/client_integration/mappings_editor.test.tsx @@ -453,7 +453,7 @@ describe('Mappings editor: core', () => { expect(isDynamicMappingsEnabled).toBe(false); isNumericDetectionVisible = screen.queryByTestId('numericDetection'); expect(isNumericDetectionVisible).not.toBeInTheDocument(); - }); + }, 10000); test('should keep default dynamic templates value when switching tabs', async () => { setup( diff --git a/x-pack/platform/plugins/shared/index_management/public/application/lib/format_bytes.ts b/x-pack/platform/plugins/shared/index_management/public/application/lib/format_bytes.ts new file mode 100644 index 0000000000000..c673f77c84595 --- /dev/null +++ b/x-pack/platform/plugins/shared/index_management/public/application/lib/format_bytes.ts @@ -0,0 +1,10 @@ +/* + * 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 numeral from '@elastic/numeral'; + +export const formatBytes = (bytes?: number): string => numeral(bytes || 0).format('0.00 b'); diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.tsx index ef4467a5a4ef5..429d1d2b7823b 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.tsx +++ b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/details_page_overview.tsx @@ -31,7 +31,8 @@ import { EisUpdateCallout, } from '@kbn/search-api-panels'; import { CLOUD_CONNECT_NAV_ID } from '@kbn/deeplinks-management/constants'; -import type { Index } from '../../../../../../../common'; +import { type Index } from '../../../../../../../common'; +import { formatBytes } from '../../../../../lib/format_bytes'; import { useAppContext } from '../../../../../app_context'; import { documentationService, useLoadIndexMappings } from '../../../../../services'; import { languageDefinitions, curlDefinition } from './languages'; @@ -84,6 +85,9 @@ export const DetailsPageOverview: React.FunctionComponent = ({ indexDetai const { data } = useUserPrivileges(indexDetails.name); const hasUpdateMappingsPrivileges = data?.privileges?.canManageIndex === true; + const sizeFormatted = formatBytes(size); + const primarySizeFormatted = formatBytes(primarySize); + const codeSnippetArguments: LanguageDefinitionSnippetArguments = { url: elasticsearchUrl, apiKey: 'your_api_key', @@ -141,7 +145,12 @@ export const DetailsPageOverview: React.FunctionComponent = ({ indexDetai )} - + = ({ indexDetai health={health} /> - + diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/size_doc_count_details.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/size_doc_count_details.tsx index 456d9096ba9fd..9ffc2b432bbb4 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/size_doc_count_details.tsx +++ b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/size_doc_count_details.tsx @@ -17,12 +17,13 @@ import { useEuiFontSize, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { EuiI18nNumber } from '@elastic/eui'; import type { Index } from '../../../../../../../common'; import { useAppContext } from '../../../../../app_context'; import { OverviewCard } from './overview_card'; export const SizeDocCountDetails: FunctionComponent<{ - size: Index['size']; + size: string; documents: Index['documents']; }> = ({ size, documents }) => { const largeFontSize = useEuiFontSize('l').fontSize; @@ -65,7 +66,9 @@ export const SizeDocCountDetails: FunctionComponent<{ - {documents} + + + {i18n.translate( diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/storage_details.tsx b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/storage_details.tsx index 470d5197938ae..ac4cbdc0dbefe 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/storage_details.tsx +++ b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/details_page/details_page_overview/storage_details.tsx @@ -23,8 +23,8 @@ import type { Index } from '../../../../../../../common'; import { OverviewCard } from './overview_card'; export const StorageDetails: FunctionComponent<{ - primarySize: Index['primary_size']; - size: Index['size']; + primarySize: string; + size: string; primary: Index['primary']; replica: Index['replica']; }> = ({ primarySize, size, primary, replica }) => { diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_table/index_table.container.js b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_table/index_table.container.js index 66406576902c2..3fb41b564e535 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_table/index_table.container.js +++ b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_table/index_table.container.js @@ -16,6 +16,7 @@ import { getIndicesAsArray, indicesLoading, indicesError, + indicesEnrichmentErrors, getTableState, } from '../../../../store/selectors'; import { @@ -40,6 +41,7 @@ const mapStateToProps = (state, props) => { isSortAscending: isSortAscending(state), indicesLoading: indicesLoading(state), indicesError: indicesError(state), + indicesEnrichmentErrors: indicesEnrichmentErrors(state), toggleNameToVisibleMap: getTableState(state).toggleNameToVisibleMap, }; }; diff --git a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_table/index_table.js index 37c257995f63c..c2801ee187585 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_table/index_table.js +++ b/x-pack/platform/plugins/shared/index_management/public/application/sections/home/index_list/index_table/index_table.js @@ -41,6 +41,7 @@ import { reactRouterNavigate, attemptToURIDecode, } from '../../../../../shared_imports'; +import { formatBytes } from '../../../../..'; import { getDataStreamDetailsLink, navigateToIndexDetailsPage } from '../../../../services/routing'; import { documentationService } from '../../../../services/documentation'; import { AppContextConsumer } from '../../../../app_context'; @@ -135,6 +136,7 @@ const getColumnConfigs = ({ label: i18n.translate('xpack.idxMgmt.indexTable.headers.storageSizeHeader', { defaultMessage: 'Storage size', }), + render: (index) => formatBytes(index.size), order: 70, } ); @@ -147,7 +149,7 @@ const getColumnConfigs = ({ defaultMessage: 'Health', }), order: 20, - render: (index) => , + render: (index) => (index.health ? : undefined), }, { fieldName: 'status', @@ -301,6 +303,35 @@ export class IndexTable extends Component { ); } + renderEnrichmentErrors() { + const { indicesEnrichmentErrors } = this.props; + if (!indicesEnrichmentErrors || indicesEnrichmentErrors.length === 0) { + return null; + } + + return ( + <> + + + + + + ); + } + onFilterChanged = ({ query, error }) => { if (error) { this.setState({ filterError: error }); @@ -707,6 +738,8 @@ export class IndexTable extends Component { {this.renderFilterError()} + {this.renderEnrichmentErrors()} +
diff --git a/x-pack/platform/plugins/shared/index_management/public/application/services/api.ts b/x-pack/platform/plugins/shared/index_management/public/application/services/api.ts index c33fcdd2d5a7e..09fdc05d7bb36 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/services/api.ts +++ b/x-pack/platform/plugins/shared/index_management/public/application/services/api.ts @@ -49,6 +49,7 @@ import type { UiMetricService } from './ui_metric'; import type { FieldFromIndicesRequest } from '../../../common'; import type { Fields } from '../components/mappings_editor/types'; import type { UserStartPrivilegesResponse } from '../../../server/lib/types'; +import { indexDataEnricher } from '../../services'; interface ReloadIndicesOptions { asSystemRequest?: boolean; @@ -140,9 +141,93 @@ export async function updateDSFailureStore( }); } -export async function loadIndices() { - const response = await httpService.httpClient.get(`${API_BASE_PATH}/indices`); - return response.data ? response.data : response; +export async function loadIndices( + onIndicesLoaded: (indices: Index[]) => void, + onEnrichmentError: (source: string) => void, + abortSignal: AbortSignal +) { + const indicesPromise = httpService.httpClient.get>( + `${API_BASE_PATH}/indices_get`, + { + signal: abortSignal, + } + ); + + // Run all requests in parallel + const enrichedPromises = indexDataEnricher.enrichIndices(httpService.httpClient, abortSignal); + + // we'll wait for the main request to complete first so the index list has stability + const indices = await indicesPromise.catch((error) => { + if (error.name === 'AbortError') { + // return undefined and exit early if the request was aborted + return; + } + throw error; + }); + + if (!indices) { + return; + } + + // Pre-compute an alias -> index names lookup for enrichers that return data keyed by alias. + const aliasToIndexNames = new Map(); + Object.entries(indices).forEach(([indexName, index]) => { + const aliases = index.aliases; + const aliasList = Array.isArray(aliases) + ? aliases + : typeof aliases === 'string' && aliases !== 'none' + ? [aliases] + : []; + + aliasList.forEach((alias) => { + if (!alias) return; + const existing = aliasToIndexNames.get(alias); + if (existing) { + existing.push(indexName); + } else { + aliasToIndexNames.set(alias, [indexName]); + } + }); + }); + + onIndicesLoaded(Object.values(indices)); + + // iterate over all the requests for additional info + enrichedPromises.forEach((enrichedPromise) => { + enrichedPromise.then((enriched) => { + // iterate over the array of additional data and merge it into the original index data + if (enriched.indices) { + enriched.indices.forEach((enrichedIndex) => { + const directMatch = indices[enrichedIndex.name]; + if (directMatch) { + Object.assign(directMatch, enrichedIndex); + return; + } + + if (enriched.applyToAliases) { + const targets = aliasToIndexNames.get(enrichedIndex.name); + if (targets && targets.length) { + // Don't overwrite the concrete index name with the alias name. + + const { name, ...rest } = enrichedIndex; + targets.forEach((targetIndexName) => { + const target = indices[targetIndexName]; + if (target) { + Object.assign(target, rest); + } + }); + } + } + }); + onIndicesLoaded(Object.values(indices)); + } + + // If an enricher fails, keep the index list stable but surface the issue to the UI. + if (enriched.error) { + onEnrichmentError(enriched.source); + } + }); + }); } export async function reloadIndices( diff --git a/x-pack/platform/plugins/shared/index_management/public/application/services/sort_table.test.ts b/x-pack/platform/plugins/shared/index_management/public/application/services/sort_table.test.ts index 1e5d509a4d130..421a568596ad8 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/services/sort_table.test.ts +++ b/x-pack/platform/plugins/shared/index_management/public/application/services/sort_table.test.ts @@ -81,42 +81,6 @@ describe('sortTable', () => { }); }); - describe('sorts by size', () => { - const indices = [{ size: '248b' }, { size: '2.35mb' }, { size: '6.36kb' }] as Index[]; - it('ascending', () => { - const sorted = sortTable(indices, 'size', true); - expect(sorted).toEqual([{ size: '248b' }, { size: '6.36kb' }, { size: '2.35mb' }]); - }); - it('descending', () => { - const sorted = sortTable(indices, 'size', false); - expect(sorted).toEqual([{ size: '2.35mb' }, { size: '6.36kb' }, { size: '248b' }]); - }); - }); - - describe('sorts by primary_size', () => { - const indices = [ - { primary_size: '248b' }, - { primary_size: '2.35mb' }, - { primary_size: '6.36kb' }, - ] as Index[]; - it('ascending', () => { - const sorted = sortTable(indices, 'primary_size', true); - expect(sorted).toEqual([ - { primary_size: '248b' }, - { primary_size: '6.36kb' }, - { primary_size: '2.35mb' }, - ]); - }); - it('descending', () => { - const sorted = sortTable(indices, 'primary_size', false); - expect(sorted).toEqual([ - { primary_size: '2.35mb' }, - { primary_size: '6.36kb' }, - { primary_size: '248b' }, - ]); - }); - }); - describe('sorts by data_stream', () => { const indices = [ { data_stream: 'test1' }, diff --git a/x-pack/platform/plugins/shared/index_management/public/application/services/sort_table.ts b/x-pack/platform/plugins/shared/index_management/public/application/services/sort_table.ts index 1a5463dcba19e..f21557789a3c3 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/services/sort_table.ts +++ b/x-pack/platform/plugins/shared/index_management/public/application/services/sort_table.ts @@ -20,16 +20,6 @@ type SortField = | 'primary_size' | 'data_stream'; -type Unit = 'kb' | 'mb' | 'gb' | 'tb' | 'pb'; - -const unitMagnitude = { - kb: 1, - mb: 2, - gb: 3, - tb: 4, - pb: 5, -}; - type SortFunction = (index: Index) => any; const numericSort = @@ -37,29 +27,11 @@ const numericSort = (item) => Number(item[fieldName]); -const byteSort = - (fieldName: SortField): SortFunction => - (item) => { - const rawValue = String(item[fieldName]); - // raw value can be missing if index is closed - if (!rawValue) { - return 0; - } - const matchResult = rawValue.match(/(.*)([kmgtp]b)/); - if (!matchResult) { - return 0; - } - const [, number, unit] = matchResult; - return +number * Math.pow(1024, unitMagnitude[unit as Unit]); - }; - const getSorters = (extensionsService?: ExtensionsService) => { const sorters = { primary: numericSort('primary'), replica: numericSort('replica'), documents: numericSort('documents'), - size: byteSort('size'), - primary_size: byteSort('primary_size'), } as any; const columns = extensionsService?.columns ?? []; for (const column of columns) { diff --git a/x-pack/platform/plugins/shared/index_management/public/application/store/actions/load_indices.js b/x-pack/platform/plugins/shared/index_management/public/application/store/actions/load_indices.js index f5a5679b02767..2ebc3bd65f482 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/store/actions/load_indices.js +++ b/x-pack/platform/plugins/shared/index_management/public/application/store/actions/load_indices.js @@ -11,14 +11,27 @@ import { loadIndices as request } from '../../services'; export const loadIndicesStart = createAction('INDEX_MANAGEMENT_LOAD_INDICES_START'); export const loadIndicesSuccess = createAction('INDEX_MANAGEMENT_LOAD_INDICES_SUCCESS'); export const loadIndicesError = createAction('INDEX_MANAGEMENT_LOAD_INDICES_ERROR'); +export const loadIndicesEnrichmentError = createAction( + 'INDEX_MANAGEMENT_LOAD_INDICES_ENRICHMENT_ERROR' +); + +let abortController; export const loadIndices = () => async (dispatch) => { + if (abortController && !abortController.signal.aborted) { + abortController.abort(); + } + + abortController = new AbortController(); + dispatch(loadIndicesStart()); - let indices; try { - indices = await request(); + await request( + (indices) => dispatch(loadIndicesSuccess({ indices })), + (source) => dispatch(loadIndicesEnrichmentError({ source })), + abortController.signal + ); } catch (error) { return dispatch(loadIndicesError(error)); } - dispatch(loadIndicesSuccess({ indices })); }; diff --git a/x-pack/platform/plugins/shared/index_management/public/application/store/reducers/indices.js b/x-pack/platform/plugins/shared/index_management/public/application/store/reducers/indices.js index a52e5638de7e1..78ba352bdbe6c 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/store/reducers/indices.js +++ b/x-pack/platform/plugins/shared/index_management/public/application/store/reducers/indices.js @@ -13,6 +13,7 @@ import { reloadIndicesSuccess, loadIndicesStart, loadIndicesError, + loadIndicesEnrichmentError, } from '../actions'; const byId = handleActions( @@ -101,13 +102,35 @@ const error = handleActions( const newState = { ...error }; return newState; }, + [loadIndicesStart]() { + return false; + }, + [loadIndicesSuccess]() { + return false; + }, }, false ); +const enrichmentErrors = handleActions( + { + [loadIndicesStart]() { + return []; + }, + [loadIndicesEnrichmentError](state, action) { + const { source } = action.payload; + if (!source) return state; + if (state.includes(source)) return state; + return [...state, source]; + }, + }, + [] +); + export const indices = combineReducers({ loading, error, + enrichmentErrors, byId, allIds, }); diff --git a/x-pack/platform/plugins/shared/index_management/public/application/store/selectors/index.js b/x-pack/platform/plugins/shared/index_management/public/application/store/selectors/index.js index 2e1e01f3122ae..d4bf1e1dbd0c0 100644 --- a/x-pack/platform/plugins/shared/index_management/public/application/store/selectors/index.js +++ b/x-pack/platform/plugins/shared/index_management/public/application/store/selectors/index.js @@ -17,6 +17,7 @@ export { extensionsService }; export const getIndices = (state) => state.indices.byId; export const indicesLoading = (state) => state.indices.loading; export const indicesError = (state) => state.indices.error; +export const indicesEnrichmentErrors = (state) => state.indices.enrichmentErrors; export const getIndicesAsArray = (state) => Object.values(state.indices.byId); export const getIndicesByName = (state, indexNames) => { const indices = getIndices(state); diff --git a/x-pack/platform/plugins/shared/index_management/public/index.ts b/x-pack/platform/plugins/shared/index_management/public/index.ts index 36d2cdefa9efb..e6c7982e4019f 100644 --- a/x-pack/platform/plugins/shared/index_management/public/index.ts +++ b/x-pack/platform/plugins/shared/index_management/public/index.ts @@ -24,3 +24,5 @@ export { getIndexListUri, getTemplateDetailsLink } from './application/services/ export type { IndexManagementLocatorParams } from '@kbn/index-management-shared-types'; export { INDEX_MANAGEMENT_LOCATOR_ID } from './locator'; + +export { formatBytes } from './application/lib/format_bytes'; diff --git a/x-pack/platform/plugins/shared/index_management/public/index_stats_enricher.ts b/x-pack/platform/plugins/shared/index_management/public/index_stats_enricher.ts new file mode 100644 index 0000000000000..56ccca366d1d0 --- /dev/null +++ b/x-pack/platform/plugins/shared/index_management/public/index_stats_enricher.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HttpSetup } from '@kbn/core/public'; +import type { IndicesStatsResponse } from '@elastic/elasticsearch/lib/api/types'; +import type { EnricherResponse } from '@kbn/index-management-shared-types'; +import { i18n } from '@kbn/i18n'; +import { API_BASE_PATH } from '../common/constants'; +const SOURCE = i18n.translate('xpack.idxMgmt.indexStatsEnricher.source', { + defaultMessage: 'index stats', +}); + +export const indexStatsEnricher = { + name: SOURCE, + fn: async (client: HttpSetup, signal: AbortSignal): Promise => + client + .get(`${API_BASE_PATH}/indices_stats`, { signal }) + .then((response) => { + const indices = response.indices || {}; + return { + indices: Object.keys(indices).map((name) => ({ + name, + health: indices[name]?.health, + status: indices[name]?.status, + uuid: indices[name]?.uuid, + documents_deleted: indices[name]?.primaries?.docs?.deleted ?? 0, + primary_size: indices[name]?.primaries?.store?.size_in_bytes ?? 0, + size: indices[name]?.total?.store?.size_in_bytes ?? 0, + })), + source: SOURCE, + }; + }), +}; diff --git a/x-pack/platform/plugins/shared/index_management/public/plugin.ts b/x-pack/platform/plugins/shared/index_management/public/plugin.ts index 9b057932979d2..986eed31261a1 100644 --- a/x-pack/platform/plugins/shared/index_management/public/plugin.ts +++ b/x-pack/platform/plugins/shared/index_management/public/plugin.ts @@ -46,6 +46,8 @@ import { IndexManagementLocatorDefinition } from './locator'; import { ComponentTemplateFlyout } from './application/components/component_templates/component_templates_flyout_embeddable'; import { DataStreamFlyout } from './application/sections/home/data_stream_list/data_stream_detail_panel/data_stream_flyout_embeddable'; import { IndexTemplateFlyout } from './application/sections/home/template_list/template_details/index_template_flyout_embeddable'; +import { indexDataEnricher, type IndexDataEnricher } from './services'; +import { indexStatsEnricher } from './index_stats_enricher'; export class IndexMgmtUIPlugin implements @@ -81,6 +83,8 @@ export class IndexMgmtUIPlugin private capabilities$ = new Subject(); + private readonly indexDataEnricher: IndexDataEnricher; + constructor(ctx: PluginInitializerContext) { // Temporary hack to provide the service instances in module files in order to avoid a big refactor // For the selectors we should expose them through app dependencies and read them from there on each container component. @@ -119,6 +123,8 @@ export class IndexMgmtUIPlugin enforceAdaptiveAllocations: isServerless, enableFailureStoreRetentionDisabling: enableFailureStoreRetentionDisabling ?? true, }; + + this.indexDataEnricher = indexDataEnricher; } public setup( @@ -167,6 +173,11 @@ export class IndexMgmtUIPlugin this.apiService = new PublicApiService(coreSetup.http); + // disabled in serverless + if (this.config.enableIndexStats) { + this.indexDataEnricher.add(indexStatsEnricher); + } + return { apiService: this.apiService, extensionsService: this.extensionsService.setup(), @@ -186,6 +197,9 @@ export class IndexMgmtUIPlugin }); }, locator: this.locator, + indexDataEnricher: { + add: this.indexDataEnricher.add.bind(this.indexDataEnricher), + }, }; } diff --git a/x-pack/platform/plugins/shared/index_management/public/services/extensions_service.ts b/x-pack/platform/plugins/shared/index_management/public/services/extensions_service.ts index 8641ca370949c..7dfa7e63287ca 100644 --- a/x-pack/platform/plugins/shared/index_management/public/services/extensions_service.ts +++ b/x-pack/platform/plugins/shared/index_management/public/services/extensions_service.ts @@ -24,7 +24,7 @@ export class ExtensionsService { private _badges: IndexBadge[] = [ { matchIndex: (index) => { - return index.isFrozen; + return !!index.isFrozen; }, label: i18n.translate('xpack.idxMgmt.frozenBadgeLabel', { defaultMessage: 'Frozen', @@ -36,7 +36,7 @@ export class ExtensionsService { private _toggles: IndexToggle[] = [ { matchIndex: (index) => { - return index.hidden; + return !!index.hidden; }, label: i18n.translate('xpack.idxMgmt.indexTable.hiddenIndicesSwitchLabel', { defaultMessage: 'Include hidden indices', diff --git a/x-pack/platform/plugins/shared/index_management/public/services/index.ts b/x-pack/platform/plugins/shared/index_management/public/services/index.ts index 92c6e95a8b432..03833cdd3357a 100644 --- a/x-pack/platform/plugins/shared/index_management/public/services/index.ts +++ b/x-pack/platform/plugins/shared/index_management/public/services/index.ts @@ -8,3 +8,6 @@ export { ExtensionsService } from './extensions_service'; export { PublicApiService } from './public_api_service'; + +export { IndexDataEnricher, indexDataEnricher } from './index_data_enricher'; +export type { Enricher } from '@kbn/index-management-shared-types'; diff --git a/x-pack/platform/plugins/shared/index_management/public/services/index_data_enricher.ts b/x-pack/platform/plugins/shared/index_management/public/services/index_data_enricher.ts new file mode 100644 index 0000000000000..50cecb12ca15f --- /dev/null +++ b/x-pack/platform/plugins/shared/index_management/public/services/index_data_enricher.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HttpSetup } from '@kbn/core/public'; +import type { Enricher, EnricherResponse } from '@kbn/index-management-shared-types'; + +export class IndexDataEnricher { + private readonly _enrichers: Enricher[] = []; + + public add(enricher: Enricher) { + this._enrichers.push(enricher); + } + + public enrichIndices = (client: HttpSetup, signal: AbortSignal): Promise[] => { + return this.enrichers.map((enricher) => + enricher.fn(client, signal).catch((error) => { + // aborted request, dont show error + if (error.name === 'AbortError') { + return { + source: enricher.name, + }; + } + return { + error: true, + source: enricher.name, + }; + }) + ); + }; + + public get enrichers() { + return this._enrichers; + } +} + +export const indexDataEnricher = new IndexDataEnricher(); diff --git a/x-pack/platform/plugins/shared/index_management/server/lib/fetch_indices.test.ts b/x-pack/platform/plugins/shared/index_management/server/lib/fetch_indices.test.ts index 6be9c85058f17..28bdbda9e1afb 100644 --- a/x-pack/platform/plugins/shared/index_management/server/lib/fetch_indices.test.ts +++ b/x-pack/platform/plugins/shared/index_management/server/lib/fetch_indices.test.ts @@ -160,8 +160,8 @@ describe('[Index management API Routes] fetch indices lib function', () => { health: undefined, status: undefined, documents: 0, - size: '0b', - primary_size: '0b', + size: 0, + primary_size: 0, }), ], }); @@ -198,7 +198,7 @@ describe('[Index management API Routes] fetch indices lib function', () => { hidden: false, data_stream: undefined, documents: 100, - size: '1000b', + size: 1000, }, ], }); diff --git a/x-pack/platform/plugins/shared/index_management/server/lib/fetch_indices.ts b/x-pack/platform/plugins/shared/index_management/server/lib/fetch_indices.ts index 1c38e43ac344c..3581e14ba6d22 100644 --- a/x-pack/platform/plugins/shared/index_management/server/lib/fetch_indices.ts +++ b/x-pack/platform/plugins/shared/index_management/server/lib/fetch_indices.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { ByteSizeValue } from '@kbn/config-schema'; import type { IScopedClusterClient } from '@kbn/core/server'; import type { IndexDataEnricher } from '../services'; import type { Index } from '..'; @@ -80,10 +79,8 @@ async function fetchIndicesCall( 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(), + size: indexStats?.total?.store?.size_in_bytes ?? 0, + primary_size: indexStats?.primaries?.store?.size_in_bytes ?? 0, }; } @@ -92,7 +89,7 @@ async function fetchIndicesCall( } // uses the _metering/stats API to get the number of documents and size of the index - // this API is only available in ES3 + // this API is only available in ES3 (serverless) if (config.isSizeAndDocCountEnabled) { const { indices: indicesStats } = await client.asSecondaryAuthUser.transport.request({ @@ -118,7 +115,7 @@ async function fetchIndicesCall( return { ...baseResponse, documents: indexStats?.num_docs ?? 0, - size: new ByteSizeValue(indexStats?.size_in_bytes ?? 0).toString(), + size: indexStats?.size_in_bytes ?? 0, }; } diff --git a/x-pack/platform/plugins/shared/index_management/server/lib/types.ts b/x-pack/platform/plugins/shared/index_management/server/lib/types.ts index 957270e529f05..902b5bf1669c6 100644 --- a/x-pack/platform/plugins/shared/index_management/server/lib/types.ts +++ b/x-pack/platform/plugins/shared/index_management/server/lib/types.ts @@ -10,6 +10,10 @@ export interface MeteringStats { num_docs: number; size_in_bytes: number; } + +export interface MeteringStatsResponse { + indices: MeteringStats[]; +} export interface UserStartPrivilegesResponse { privileges: { canManageIndex: boolean; diff --git a/x-pack/platform/plugins/shared/index_management/server/routes/api/indices/indices_get.ts b/x-pack/platform/plugins/shared/index_management/server/routes/api/indices/indices_get.ts new file mode 100644 index 0000000000000..97f06e8de683e --- /dev/null +++ b/x-pack/platform/plugins/shared/index_management/server/routes/api/indices/indices_get.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { RouteDependencies } from '../../../types'; +import { addBasePath } from '..'; +import type { MeteringStatsResponse } from '../../../lib/types'; +import type { Index } from '../../../../common/types/indices'; + +export function registerIndicesGet({ router, lib: { handleEsError }, config }: RouteDependencies) { + router.get( + { + path: addBasePath('/indices_get'), + security: { + authz: { + enabled: false, + reason: 'Relies on es client for authorization', + }, + }, + validate: false, + }, + async (context, request, response) => { + const { client } = (await context.core).elasticsearch; + + try { + const indicesPromise = client.asCurrentUser.indices.get({ + index: '*', + expand_wildcards: ['hidden', 'all'], + // only get specified index properties from ES to keep the response under 536MB + // node.js string length limit: https://github.com/nodejs/node/issues/33960 + filter_path: [ + '*.aliases', + '*.settings.index.number_of_shards', + '*.settings.index.number_of_replicas', + '*.settings.index.frozen', + '*.settings.index.hidden', + '*.settings.index.mode', + '*.data_stream', + ], + // for better performance only compute aliases and settings of indices but not mappings + features: ['aliases', 'settings'], + }); + + // Used for serverless + let statsPromise: Promise | undefined; + if (config.isSizeAndDocCountEnabled) { + // this api is internal only and therefore requires elevated privileges + statsPromise = client.asSecondaryAuthUser.transport + .request({ + method: 'GET', + path: '/_metering/stats/*', + }) + .catch(() => ({ indices: [] })); + } else { + statsPromise = Promise.resolve({ indices: [] }); + } + + const [indices, { indices: indicesStats }] = await Promise.all([ + indicesPromise, + statsPromise, + ]); + + const indexStatsMap = indicesStats.reduce< + Record + >((prev, index) => { + prev[index.name] = { size_in_bytes: index.size_in_bytes, num_docs: index.num_docs }; + return prev; + }, {}); + + const mappedIndices: Record = Object.keys(indices).reduce< + Record + >((prev, indexName: string) => { + const indexData = indices[indexName]; + const aliases = Object.keys(indexData.aliases!); + prev[indexName] = { + name: indexName, + primary: Number(indexData.settings?.index?.number_of_shards), + replica: Number(indexData.settings?.index?.number_of_replicas), + isFrozen: indexData.settings?.index?.frozen === 'true', + hidden: indexData.settings?.index?.hidden === 'true', + data_stream: indexData.data_stream, + mode: indexData.settings?.index?.mode, + }; + + if (aliases.length) { + prev[indexName].aliases = aliases; + } + + if (indexStatsMap[indexName]) { + prev[indexName].documents = indexStatsMap[indexName].num_docs ?? 0; + prev[indexName].size = indexStatsMap[indexName].size_in_bytes ?? 0; + } + return prev; + }, {}); + + return response.ok({ body: mappedIndices }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ); +} diff --git a/x-pack/platform/plugins/shared/index_management/server/routes/api/indices/indices_stats.ts b/x-pack/platform/plugins/shared/index_management/server/routes/api/indices/indices_stats.ts new file mode 100644 index 0000000000000..a028c012583cc --- /dev/null +++ b/x-pack/platform/plugins/shared/index_management/server/routes/api/indices/indices_stats.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { RouteDependencies } from '../../../types'; +import { addBasePath } from '..'; + +export function registerIndicesStats({ router, lib: { handleEsError } }: RouteDependencies) { + router.get( + { + path: addBasePath('/indices_stats'), + security: { + authz: { + enabled: false, + reason: 'Relies on es client for authorization', + }, + }, + validate: false, + }, + async (context, request, response) => { + const { client } = (await context.core).elasticsearch; + try { + const resp = await client.asCurrentUser.indices.stats({ + index: '*', + expand_wildcards: ['hidden', 'all'], + forbid_closed_indices: false, + metric: ['docs', 'store'], + filter_path: [ + '*.*.uuid', + '*.*.status', + '*.*.health', + '*.*.primaries.docs.deleted', + '*.*.total.store.size_in_bytes', + '*.*.primaries.store.size_in_bytes', + ], + }); + return response.ok({ body: resp }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ); +} diff --git a/x-pack/platform/plugins/shared/index_management/server/routes/api/indices/register_indices_routes.ts b/x-pack/platform/plugins/shared/index_management/server/routes/api/indices/register_indices_routes.ts index 63b7e92b5b039..8a37fcf3ec851 100644 --- a/x-pack/platform/plugins/shared/index_management/server/routes/api/indices/register_indices_routes.ts +++ b/x-pack/platform/plugins/shared/index_management/server/routes/api/indices/register_indices_routes.ts @@ -20,6 +20,9 @@ import { registerGetRoute } from './register_get_route'; import { registerCreateRoute } from './register_create_route'; import { registerPostIndexDocCountRoute } from './register_post_index_doc_count'; +import { registerIndicesGet } from './indices_get'; +import { registerIndicesStats } from './indices_stats'; + export function registerIndicesRoutes(dependencies: RouteDependencies) { registerClearCacheRoute(dependencies); registerCloseRoute(dependencies); @@ -33,4 +36,6 @@ export function registerIndicesRoutes(dependencies: RouteDependencies) { registerGetRoute(dependencies); registerCreateRoute(dependencies); registerPostIndexDocCountRoute(dependencies); + registerIndicesGet(dependencies); + registerIndicesStats(dependencies); } diff --git a/x-pack/platform/plugins/shared/index_management/server/test/helpers/indices_fixtures.ts b/x-pack/platform/plugins/shared/index_management/server/test/helpers/indices_fixtures.ts index 824fa30d02e5e..f2bed1b6eed6c 100644 --- a/x-pack/platform/plugins/shared/index_management/server/test/helpers/indices_fixtures.ts +++ b/x-pack/platform/plugins/shared/index_management/server/test/helpers/indices_fixtures.ts @@ -44,8 +44,8 @@ export const createTestIndexResponse = (index?: Partial) => { name: 'test_index', primary: 1, replica: 1, - size: '100b', - primary_size: '100b', + size: 100, + primary_size: 100, status: 'open', uuid: 'test_index', ...index, diff --git a/x-pack/solutions/search/plugins/search_indices/moon.yml b/x-pack/solutions/search/plugins/search_indices/moon.yml index f0507ee234df6..bc4df7abed64a 100644 --- a/x-pack/solutions/search/plugins/search_indices/moon.yml +++ b/x-pack/solutions/search/plugins/search_indices/moon.yml @@ -55,6 +55,7 @@ dependsOn: - '@kbn/react-query' - '@kbn/search-api-panels' - '@kbn/deeplinks-management' + - '@kbn/index-management-plugin' tags: - plugin - prod diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateful_storage_stat.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateful_storage_stat.tsx index dea0d05ce8b17..294e058844bbb 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateful_storage_stat.tsx +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateful_storage_stat.tsx @@ -7,6 +7,7 @@ import React from 'react'; import type { Index } from '@kbn/index-management-shared-types'; +import { formatBytes } from '@kbn/index-management-plugin/public'; import { useEuiTheme } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -36,17 +37,17 @@ export const StatefulIndexStorageStat = ({ defaultMessage: 'Storage', })} data-test-subj="QuickStatsStorage" - secondaryTitle={index.size ?? '0b'} + secondaryTitle={formatBytes(index.size)} stats={[ { title: INDEX_SIZE_LABEL, - description: index.size ?? '0b', + description: formatBytes(index.size), }, { title: i18n.translate('xpack.searchIndices.quickStats.primarySize_title', { defaultMessage: 'Primary Size', }), - description: index.primary_size ?? '0b', + description: formatBytes(index.primary_size), }, ]} /> diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateless_document_cout_stat.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateless_document_cout_stat.tsx index 3b6ecee0576a0..e23bae5e437f8 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateless_document_cout_stat.tsx +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateless_document_cout_stat.tsx @@ -7,6 +7,7 @@ import React from 'react'; import type { Index } from '@kbn/index-management-shared-types'; +import { formatBytes } from '@kbn/index-management-plugin/public'; import { EuiI18nNumber, useEuiTheme } from '@elastic/eui'; @@ -48,7 +49,7 @@ export const StatelessDocumentCountStat = ({ }, { title: INDEX_SIZE_LABEL, - description: index.size ?? '0b', + description: formatBytes(index.size), }, ]} tooltipContent={DOCUMENT_COUNT_TOOLTIP} diff --git a/x-pack/solutions/search/plugins/search_indices/tsconfig.json b/x-pack/solutions/search/plugins/search_indices/tsconfig.json index f254a41ee0b33..00ebee4130e9a 100644 --- a/x-pack/solutions/search/plugins/search_indices/tsconfig.json +++ b/x-pack/solutions/search/plugins/search_indices/tsconfig.json @@ -48,6 +48,7 @@ "@kbn/react-query", "@kbn/search-api-panels", "@kbn/deeplinks-management", + "@kbn/index-management-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/solutions/search/test/functional_search/tests/search_index_details.ts b/x-pack/solutions/search/test/functional_search/tests/search_index_details.ts index 462c4fd3803a1..4a6f02a6668d5 100644 --- a/x-pack/solutions/search/test/functional_search/tests/search_index_details.ts +++ b/x-pack/solutions/search/test/functional_search/tests/search_index_details.ts @@ -112,7 +112,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should have quick stats', async () => { await pageObjects.searchIndexDetailsPage.expectQuickStats(); await pageObjects.searchIndexDetailsPage.expectQuickStatsToHaveIndexStatus(); - await pageObjects.searchIndexDetailsPage.expectQuickStatsToHaveIndexStorage('227b'); + await pageObjects.searchIndexDetailsPage.expectQuickStatsToHaveIndexStorage('227.00 B'); await pageObjects.searchIndexDetailsPage.expectQuickStatsAIMappings(); }); diff --git a/x-pack/solutions/search/test/page_objects/search_index_details_page.ts b/x-pack/solutions/search/test/page_objects/search_index_details_page.ts index 4a1cba6b4924d..fa76ba22f4099 100644 --- a/x-pack/solutions/search/test/page_objects/search_index_details_page.ts +++ b/x-pack/solutions/search/test/page_objects/search_index_details_page.ts @@ -74,9 +74,9 @@ export function SearchIndexDetailPageProvider({ getService }: FtrProviderContext 'QuickStatsDocumentCount' ); expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Document count\n0'); - expect(await quickStatsDocumentElem.getVisibleText()).not.to.contain('Index Size\n0b'); + expect(await quickStatsDocumentElem.getVisibleText()).not.to.contain('Index Size\n0.00 B'); await quickStatsDocumentElem.click(); - expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Index Size\n0b'); + expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Index Size\n0.00 B'); }, async expectQuickStatsToHaveIndexStatus() {