diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 0550247a2918f..4ab341571668e 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -465,8 +465,8 @@ preview:[] Enable the APM Trace Explorer feature, that allows you to search and [[observability-infrastructure-profiling-integration]]`observability:enableInfrastructureProfilingIntegration`:: preview:[] Enables the Profiling view in Host details within Infrastructure. -[[observability-infrastructure-hosts-custom-dashboard]]`observability:enableInfrastructureHostsCustomDashboards`:: -preview:[] Enables option to link custom dashboards in the Host Details view. +[[observability-infrastructure-asset-custom-dashboard]]`observability:enableInfrastructureAssetCustomDashboards`:: +preview:[] Enables option to link custom dashboards in the Asset Details view. [[observability-profiling-per-vcpu-watt-x86]]`observability:profilingPervCPUWattX86`:: The average amortized per-core power consumption (based on 100% CPU utilization) for x86 architecture. diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index e43f7cab87278..fa70473b0b0a8 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -459,6 +459,11 @@ "title", "type" ], + "infra-custom-dashboards": [ + "assetType", + "dashboardIdList", + "kuery" + ], "infrastructure-monitoring-log-view": [ "name" ], diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 24012cccb9fcc..5bfe7f6f20de9 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -1553,6 +1553,19 @@ } } }, + "infra-custom-dashboards": { + "properties": { + "assetType": { + "type": "keyword" + }, + "dashboardIdList": { + "type": "keyword" + }, + "kuery": { + "type": "text" + } + } + }, "infrastructure-monitoring-log-view": { "dynamic": false, "properties": { diff --git a/packages/kbn-management/settings/setting_ids/index.ts b/packages/kbn-management/settings/setting_ids/index.ts index 9a41ac77c82b4..57c19d5cf6cc3 100644 --- a/packages/kbn-management/settings/setting_ids/index.ts +++ b/packages/kbn-management/settings/setting_ids/index.ts @@ -123,8 +123,8 @@ export const OBSERVABILITY_ENABLE_COMPARISON_BY_DEFAULT_ID = 'observability:enableComparisonByDefault'; export const OBSERVABILITY_ENABLE_INFRASTRUCTURE_HOSTS_VIEW_ID = 'observability:enableInfrastructureHostsView'; -export const OBSERVABILITY_ENABLE_INFRASTRUCTURE_HOSTS_CUSTOM_DASHBOARDS_ID = - 'observability:enableInfrastructureHostsCustomDashboards'; +export const OBSERVABILITY_ENABLE_INFRASTRUCTURE_ASSET_CUSTOM_DASHBOARDS_ID = + 'observability:enableInfrastructureAssetCustomDashboards'; export const OBSERVABILITY_ENABLE_INSPECT_ES_QUERIES_ID = 'observability:enableInspectEsQueries'; export const OBSERVABILITY_MAX_SUGGESTIONS_ID = 'observability:maxSuggestions'; export const OBSERVABILITY_PROFILING_ELASTICSEARCH_PLUGIN_ID = diff --git a/packages/serverless/settings/observability_project/index.ts b/packages/serverless/settings/observability_project/index.ts index c95c044614e51..cab35bca0f142 100644 --- a/packages/serverless/settings/observability_project/index.ts +++ b/packages/serverless/settings/observability_project/index.ts @@ -28,5 +28,5 @@ export const OBSERVABILITY_PROJECT_SETTINGS = [ settings.OBSERVABILITY_ENABLE_AWS_LAMBDA_METRICS_ID, settings.OBSERVABILITY_APM_ENABLE_CRITICAL_PATH_ID, settings.OBSERVABILITY_ENABLE_INFRASTRUCTURE_HOSTS_VIEW_ID, - settings.OBSERVABILITY_ENABLE_INFRASTRUCTURE_HOSTS_CUSTOM_DASHBOARDS_ID, + settings.OBSERVABILITY_ENABLE_INFRASTRUCTURE_ASSET_CUSTOM_DASHBOARDS_ID, ]; diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 3dcfe8302ea28..4214797859380 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -103,6 +103,7 @@ describe('checking migration metadata changes on all registered SO types', () => "guided-onboarding-guide-state": "d338972ed887ac480c09a1a7fbf582d6a3827c91", "guided-onboarding-plugin-state": "bc109e5ef46ca594fdc179eda15f3095ca0a37a4", "index-pattern": "997108a9ea1e8076e22231e1c95517cdb192b9c5", + "infra-custom-dashboards": "b92b6db1c1f8998af6e2951a17b76cf886c6bee5", "infrastructure-monitoring-log-view": "5f86709d3c27aed7a8379153b08ee5d3d90d77f5", "infrastructure-ui-source": "113182d6895764378dfe7fa9fa027244f3a457c4", "ingest-agent-policies": "7633e578f60c074f8267bc50ec4763845e431437", diff --git a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts index 99e2692523f6d..7e41abff9b024 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group3/type_registrations.test.ts @@ -73,6 +73,7 @@ const previouslyRegisteredTypes = [ 'index-pattern', 'infrastructure-monitoring-log-view', 'infrastructure-ui-source', + 'infra-custom-dashboards', 'ingest-agent-policies', 'ingest-download-sources', 'ingest-outputs', diff --git a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts index 9f9c3a3c7bd58..25c548d4e0d6b 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts @@ -223,6 +223,7 @@ describe('split .kibana index into multiple system indices', () => { "guided-onboarding-guide-state", "guided-onboarding-plugin-state", "index-pattern", + "infra-custom-dashboards", "infrastructure-monitoring-log-view", "infrastructure-ui-source", "ingest-agent-policies", diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 59cee0f8a689c..361b5f5d4b3ee 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -608,7 +608,7 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, - 'observability:enableInfrastructureHostsCustomDashboards': { + 'observability:enableInfrastructureAssetCustomDashboards': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 16a8dd493ab47..4583aaf922e7d 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -47,7 +47,7 @@ export interface UsageStats { 'observability:apmAWSLambdaRequestCostPerMillion': number; 'observability:enableInfrastructureHostsView': boolean; 'observability:enableInfrastructureProfilingIntegration': boolean; - 'observability:enableInfrastructureHostsCustomDashboards': boolean; + 'observability:enableInfrastructureAssetCustomDashboards': boolean; 'observability:apmAgentExplorerView': boolean; 'observability:apmEnableTableSearchBar': boolean; 'visualization:heatmap:maxBuckets': number; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 6cad0714a4119..16b7e362480f5 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -10228,7 +10228,7 @@ "description": "Non-default value of setting." } }, - "observability:enableInfrastructureHostsCustomDashboards": { + "observability:enableInfrastructureAssetCustomDashboards": { "type": "boolean", "_meta": { "description": "Non-default value of setting." diff --git a/x-pack/plugins/infra/common/custom_dashboards.ts b/x-pack/plugins/infra/common/custom_dashboards.ts new file mode 100644 index 0000000000000..ef32f2fb9cf60 --- /dev/null +++ b/x-pack/plugins/infra/common/custom_dashboards.ts @@ -0,0 +1,16 @@ +/* + * 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 { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; + +export type InfraCustomDashboardAssetType = InventoryItemType; + +export interface InfraCustomDashboard { + dashboardIdList: string[]; + assetType: InfraCustomDashboardAssetType; + kuery?: string; +} diff --git a/x-pack/plugins/infra/common/http_api/custom_dashboards_api.ts b/x-pack/plugins/infra/common/http_api/custom_dashboards_api.ts new file mode 100644 index 0000000000000..9b1bafaa19da1 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/custom_dashboards_api.ts @@ -0,0 +1,47 @@ +/* + * 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 { ItemTypeRT } from '@kbn/metrics-data-access-plugin/common'; +import * as rt from 'io-ts'; + +const AssetTypeRT = rt.type({ + assetType: ItemTypeRT, +}); + +const CustomDashboardRT = rt.intersection([ + AssetTypeRT, + rt.type({ + dashboardIdList: rt.array(rt.string), + }), + rt.partial({ + kuery: rt.string, + }), +]); + +/** + GET endpoint +*/ +export const InfraGetCustomDashboardsRequestParamsRT = AssetTypeRT; +export const InfraGetCustomDashboardsResponseBodyRT = CustomDashboardRT; +export type InfraGetCustomDashboardsRequestParams = rt.TypeOf< + typeof InfraGetCustomDashboardsRequestParamsRT +>; +export type InfraGetCustomDashboardsResponseBody = rt.TypeOf< + typeof InfraGetCustomDashboardsResponseBodyRT +>; + +/** + * POST endpoint + */ +export const InfraSaveCustomDashboardsRequestPayloadRT = CustomDashboardRT; +export const InfraSaveCustomDashboardsResponseBodyRT = CustomDashboardRT; +export type InfraSaveCustomDashboardsRequestPayload = rt.TypeOf< + typeof InfraSaveCustomDashboardsRequestPayloadRT +>; +export type InfraSaveCustomDashboardsResponseBody = rt.TypeOf< + typeof InfraSaveCustomDashboardsResponseBodyRT +>; diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 5bba62e104b5c..5576253588486 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { CoreStart } from '@kbn/core-lifecycle-server'; import { InfraBackendLibs } from './lib/infra_types'; import { initGetHostsAnomaliesRoute, initGetK8sAnomaliesRoute } from './routes/infra_ml'; import { initInventoryMetaRoute } from './routes/inventory_metadata'; @@ -34,8 +35,14 @@ import { initInfraMetricsRoute } from './routes/infra'; import { initMetricsExplorerViewRoutes } from './routes/metrics_explorer_views'; import { initProfilingRoutes } from './routes/profiling'; import { initServicesRoute } from './routes/services'; +import { initCustomDashboardsRoutes } from './routes/custom_dashboards/custom_dashboards'; +import { type InfraServerPluginStartDeps } from './lib/adapters/framework'; -export const initInfraServer = (libs: InfraBackendLibs) => { +export const initInfraServer = ( + libs: InfraBackendLibs, + coreStart: CoreStart, + infraPluginsStart: InfraServerPluginStartDeps +) => { initIpToHostName(libs); initGetLogEntryCategoriesRoute(libs); initGetLogEntryCategoryDatasetsRoute(libs); @@ -63,4 +70,5 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initInfraMetricsRoute(libs); initProfilingRoutes(libs); initServicesRoute(libs); + initCustomDashboardsRoutes(libs.framework); }; diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 9a593af54a010..c16dad5445af4 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -7,7 +7,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { TransportRequestParams } from '@elastic/elasticsearch'; -import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; +import { ElasticsearchClient, RouteConfig, SavedObjectsClientContract } from '@kbn/core/server'; import { CoreSetup, IRouter, KibanaRequest, RequestHandler, RouteMethod } from '@kbn/core/server'; import { UI_SETTINGS } from '@kbn/data-plugin/server'; import { TimeseriesVisData } from '@kbn/vis-type-timeseries-plugin/server'; @@ -59,25 +59,30 @@ export class KibanaFramework { const routeConfig = { path: config.path, validate: config.validate, - // Currently we have no use of custom options beyond tags, this can be extended - // beyond defaultOptions if it's needed. - options: defaultOptions, + /** + * Supported `options` for each type of request method + * are a bit different and generic method like this cannot + * properly ensure type safety. Hence the need to cast + * using `as ...` below to ensure the route config has + * the correct options type. + */ + options: { ...config.options, ...defaultOptions }, }; switch (config.method) { case 'get': - this.router.get(routeConfig, handler); + this.router.get(routeConfig as RouteConfig, handler); break; case 'post': - this.router.post(routeConfig, handler); + this.router.post(routeConfig as RouteConfig, handler); break; case 'delete': - this.router.delete(routeConfig, handler); + this.router.delete(routeConfig as RouteConfig, handler); break; case 'put': - this.router.put(routeConfig, handler); + this.router.put(routeConfig as RouteConfig, handler); break; case 'patch': - this.router.patch(routeConfig, handler); + this.router.patch(routeConfig as RouteConfig, handler); break; } } diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 78501be3f0f26..a9401a064d931 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -38,7 +38,11 @@ import { InfraMetricsDomain } from './lib/domains/metrics_domain'; import { InfraBackendLibs, InfraDomainLibs } from './lib/infra_types'; import { infraSourceConfigurationSavedObjectType, InfraSources } from './lib/sources'; import { InfraSourceStatus } from './lib/source_status'; -import { inventoryViewSavedObjectType, metricsExplorerViewSavedObjectType } from './saved_objects'; +import { + infraCustomDashboardsSavedObjectType, + inventoryViewSavedObjectType, + metricsExplorerViewSavedObjectType, +} from './saved_objects'; import { InventoryViewsService } from './services/inventory_views'; import { MetricsExplorerViewsService } from './services/metrics_explorer_views'; import { RulesService } from './services/rules'; @@ -199,6 +203,7 @@ export class InfraServerPlugin // Register saved object types core.savedObjects.registerType(infraSourceConfigurationSavedObjectType); core.savedObjects.registerType(inventoryViewSavedObjectType); + core.savedObjects.registerType(infraCustomDashboardsSavedObjectType); if (this.config.featureFlags.metricsExplorerEnabled) { core.savedObjects.registerType(metricsExplorerViewSavedObjectType); } @@ -259,21 +264,27 @@ export class InfraServerPlugin ]); } - initInfraServer(this.libs); registerRuleTypes(plugins.alerting, this.libs, this.config); core.http.registerRouteHandlerContext( 'infra', async (context, request) => { - const soClient = (await context.core).savedObjects.client; - const mlSystem = plugins.ml?.mlSystemProvider(request, soClient); - const mlAnomalyDetectors = plugins.ml?.anomalyDetectorsProvider(request, soClient); + const coreContext = await context.core; + const savedObjectsClient = coreContext.savedObjects.client; + const uiSettingsClient = coreContext.uiSettings.client; + const mlSystem = plugins.ml?.mlSystemProvider(request, savedObjectsClient); + const mlAnomalyDetectors = plugins.ml?.anomalyDetectorsProvider( + request, + savedObjectsClient + ); const spaceId = plugins.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; return { mlAnomalyDetectors, mlSystem, spaceId, + savedObjectsClient, + uiSettingsClient, }; } ); @@ -287,7 +298,7 @@ export class InfraServerPlugin } as InfraPluginSetup; } - start(core: CoreStart) { + start(core: CoreStart, pluginsStart: InfraServerPluginStartDeps) { const inventoryViews = this.inventoryViews.start({ infraSources: this.libs.sources, savedObjects: core.savedObjects, @@ -298,6 +309,8 @@ export class InfraServerPlugin savedObjects: core.savedObjects, }); + initInfraServer(this.libs, core, pluginsStart); + return { inventoryViews, metricsExplorerViews, diff --git a/x-pack/plugins/infra/server/routes/custom_dashboards/custom_dashboards.ts b/x-pack/plugins/infra/server/routes/custom_dashboards/custom_dashboards.ts new file mode 100644 index 0000000000000..6ed41cb83db71 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/custom_dashboards/custom_dashboards.ts @@ -0,0 +1,15 @@ +/* + * 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 { KibanaFramework } from '../../lib/adapters/framework/kibana_framework_adapter'; +import { initGetCustomDashboardRoute } from './get_custom_dashboard'; +import { initSaveCustomDashboardRoute } from './save_custom_dashboard'; + +export function initCustomDashboardsRoutes(framework: KibanaFramework) { + initGetCustomDashboardRoute(framework); + initSaveCustomDashboardRoute(framework); +} diff --git a/x-pack/plugins/infra/server/routes/custom_dashboards/get_custom_dashboard.ts b/x-pack/plugins/infra/server/routes/custom_dashboards/get_custom_dashboard.ts new file mode 100644 index 0000000000000..5b5136b104f9d --- /dev/null +++ b/x-pack/plugins/infra/server/routes/custom_dashboards/get_custom_dashboard.ts @@ -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 { createRouteValidationFunction } from '@kbn/io-ts-utils'; +import { + InfraGetCustomDashboardsRequestParamsRT, + InfraGetCustomDashboardsResponseBodyRT, +} from '../../../common/http_api/custom_dashboards_api'; +import { KibanaFramework } from '../../lib/adapters/framework/kibana_framework_adapter'; +import { handleRouteErrors } from '../../utils/handle_route_errors'; +import { checkCustomDashboardsEnabled } from './lib/check_custom_dashboards_enabled'; +import { findCustomDashboard } from './lib/find_custom_dashboard'; + +export function initGetCustomDashboardRoute(framework: KibanaFramework) { + const validateParams = createRouteValidationFunction(InfraGetCustomDashboardsRequestParamsRT); + + framework.registerRoute( + { + method: 'get', + path: '/api/infra/custom-dashboards/{assetType}', + validate: { + params: validateParams, + }, + options: { + access: 'internal', + }, + }, + handleRouteErrors(async (context, request, response) => { + const { savedObjectsClient, uiSettingsClient } = await context.infra; + + await checkCustomDashboardsEnabled(uiSettingsClient); + + const params = request.params; + const customDashboards = await findCustomDashboard(params.assetType, savedObjectsClient); + + if (customDashboards.total === 0) { + return response.ok({ + body: InfraGetCustomDashboardsResponseBodyRT.encode({ + assetType: params.assetType, + dashboardIdList: [], + kuery: undefined, + }), + }); + } + + const attributes = customDashboards.saved_objects[0].attributes; + + return response.ok({ + body: InfraGetCustomDashboardsResponseBodyRT.encode({ + assetType: attributes.assetType, + dashboardIdList: attributes.dashboardIdList, + kuery: attributes.kuery, + }), + }); + }) + ); +} diff --git a/x-pack/plugins/infra/server/routes/custom_dashboards/lib/check_custom_dashboards_enabled.ts b/x-pack/plugins/infra/server/routes/custom_dashboards/lib/check_custom_dashboards_enabled.ts new file mode 100644 index 0000000000000..086e920726449 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/custom_dashboards/lib/check_custom_dashboards_enabled.ts @@ -0,0 +1,23 @@ +/* + * 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 Boom from '@hapi/boom'; +import { i18n } from '@kbn/i18n'; +import { IUiSettingsClient } from '@kbn/core/server'; +import { enableInfrastructureAssetCustomDashboards } from '@kbn/observability-plugin/common'; + +export async function checkCustomDashboardsEnabled(uiSettingsClient: IUiSettingsClient) { + const isEnabled = await uiSettingsClient.get(enableInfrastructureAssetCustomDashboards); + + if (!isEnabled) { + throw Boom.forbidden( + i18n.translate('xpack.infra.routes.customDashboards', { + defaultMessage: 'Custom dashboards are not enabled', + }) + ); + } +} diff --git a/x-pack/plugins/infra/server/routes/custom_dashboards/lib/find_custom_dashboard.ts b/x-pack/plugins/infra/server/routes/custom_dashboards/lib/find_custom_dashboard.ts new file mode 100644 index 0000000000000..5f5956d5c8fc6 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/custom_dashboards/lib/find_custom_dashboard.ts @@ -0,0 +1,24 @@ +/* + * 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 SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; +import { INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE } from '../../../saved_objects'; +import type { + InfraCustomDashboard, + InfraCustomDashboardAssetType, +} from '../../../../common/custom_dashboards'; + +export function findCustomDashboard( + assetType: InfraCustomDashboardAssetType, + savedObjectsClient: SavedObjectsClientContract +) { + return savedObjectsClient.find({ + type: INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE, + search: assetType, + searchFields: ['assetType'] as [keyof InfraCustomDashboard], + }); +} diff --git a/x-pack/plugins/infra/server/routes/custom_dashboards/save_custom_dashboard.ts b/x-pack/plugins/infra/server/routes/custom_dashboards/save_custom_dashboard.ts new file mode 100644 index 0000000000000..dc811b99b47de --- /dev/null +++ b/x-pack/plugins/infra/server/routes/custom_dashboards/save_custom_dashboard.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createRouteValidationFunction } from '@kbn/io-ts-utils'; +import { InfraCustomDashboard } from '../../../common/custom_dashboards'; +import { + InfraSaveCustomDashboardsRequestPayloadRT, + InfraSaveCustomDashboardsRequestPayload, + InfraSaveCustomDashboardsResponseBodyRT, +} from '../../../common/http_api/custom_dashboards_api'; +import { KibanaFramework } from '../../lib/adapters/framework/kibana_framework_adapter'; +import { INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE } from '../../saved_objects'; +import { checkCustomDashboardsEnabled } from './lib/check_custom_dashboards_enabled'; +import { findCustomDashboard } from './lib/find_custom_dashboard'; +import { handleRouteErrors } from '../../utils/handle_route_errors'; + +export function initSaveCustomDashboardRoute(framework: KibanaFramework) { + const validatePayload = createRouteValidationFunction(InfraSaveCustomDashboardsRequestPayloadRT); + + framework.registerRoute( + { + method: 'post', + path: '/api/infra/custom-dashboards', + validate: { + body: validatePayload, + }, + options: { + access: 'internal', + }, + }, + handleRouteErrors(async (context, request, response) => { + const { savedObjectsClient, uiSettingsClient } = await context.infra; + + await checkCustomDashboardsEnabled(uiSettingsClient); + + const payload: InfraSaveCustomDashboardsRequestPayload = request.body; + const customDashboards = await findCustomDashboard(payload.assetType, savedObjectsClient); + + if (customDashboards.total === 0) { + const savedCustomDashboard = await savedObjectsClient.create( + INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE, + payload + ); + + return response.ok({ + body: InfraSaveCustomDashboardsResponseBodyRT.encode(savedCustomDashboard.attributes), + }); + } + + const savedCustomDashboard = await savedObjectsClient.update( + INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE, + customDashboards.saved_objects[0].id, + payload + ); + + return response.ok({ + body: InfraSaveCustomDashboardsResponseBodyRT.encode({ + ...payload, + ...savedCustomDashboard.attributes, + }), + }); + }) + ); +} diff --git a/x-pack/plugins/infra/server/saved_objects/custom_dashboards/custom_dashboards_saved_object.ts b/x-pack/plugins/infra/server/saved_objects/custom_dashboards/custom_dashboards_saved_object.ts new file mode 100644 index 0000000000000..8075a51336f36 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/custom_dashboards/custom_dashboards_saved_object.ts @@ -0,0 +1,49 @@ +/* + * 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 { SavedObjectsFieldMapping, SavedObjectsType } from '@kbn/core/server'; +import { i18n } from '@kbn/i18n'; +import { schema, Type } from '@kbn/config-schema'; +import { InfraCustomDashboard } from '../../../common/custom_dashboards'; + +export const INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE = 'infra-custom-dashboards'; + +const properties: Record = { + dashboardIdList: { type: 'keyword' }, + assetType: { type: 'keyword' }, + kuery: { type: 'text' }, +}; +const createSchema: Record> = { + dashboardIdList: schema.arrayOf(schema.string()), + assetType: schema.string(), + kuery: schema.maybe(schema.string()), +}; + +export const infraCustomDashboardsSavedObjectType: SavedObjectsType = { + name: INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'multiple', + mappings: { + properties, + }, + management: { + importableAndExportable: true, + icon: 'infraApp', + getTitle: () => + i18n.translate('xpack.infra.infraAssetCustomDashboards.', { + defaultMessage: 'Infrastructure Asset Custom Dashboards', + }), + }, + modelVersions: { + '1': { + changes: [], + schemas: { + create: schema.object(createSchema), + }, + }, + }, +}; diff --git a/x-pack/plugins/infra/server/saved_objects/index.ts b/x-pack/plugins/infra/server/saved_objects/index.ts index c64f4b46808c4..dfc18e6edfbaa 100644 --- a/x-pack/plugins/infra/server/saved_objects/index.ts +++ b/x-pack/plugins/infra/server/saved_objects/index.ts @@ -7,3 +7,4 @@ export * from './inventory_view'; export * from './metrics_explorer_view'; +export * from './custom_dashboards/custom_dashboards_saved_object'; diff --git a/x-pack/plugins/infra/server/types.ts b/x-pack/plugins/infra/server/types.ts index c7fde8c4c0bd3..6b270f8dd1998 100644 --- a/x-pack/plugins/infra/server/types.ts +++ b/x-pack/plugins/infra/server/types.ts @@ -5,7 +5,12 @@ * 2.0. */ -import type { CoreSetup, CustomRequestHandlerContext } from '@kbn/core/server'; +import type { + CoreSetup, + CustomRequestHandlerContext, + IUiSettingsClient, + SavedObjectsClientContract, +} from '@kbn/core/server'; import type { SearchRequestHandlerContext } from '@kbn/data-plugin/server'; import type { MlPluginSetup } from '@kbn/ml-plugin/server'; import { InfraServerPluginStartDeps } from './lib/adapters/framework'; @@ -33,18 +38,14 @@ export interface InfraPluginStart { export type MlSystem = ReturnType; export type MlAnomalyDetectors = ReturnType; -export interface InfraMlRequestHandlerContext { +export interface InfraRequestHandlerContext { mlAnomalyDetectors?: MlAnomalyDetectors; mlSystem?: MlSystem; -} - -export interface InfraSpacesRequestHandlerContext { spaceId: string; + savedObjectsClient: SavedObjectsClientContract; + uiSettingsClient: IUiSettingsClient; } -export type InfraRequestHandlerContext = InfraMlRequestHandlerContext & - InfraSpacesRequestHandlerContext; - /** * @internal */ diff --git a/x-pack/plugins/infra/server/utils/handle_route_errors.ts b/x-pack/plugins/infra/server/utils/handle_route_errors.ts new file mode 100644 index 0000000000000..3e76f2d4e8839 --- /dev/null +++ b/x-pack/plugins/infra/server/utils/handle_route_errors.ts @@ -0,0 +1,34 @@ +/* + * 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 Boom from '@hapi/boom'; +import { type RequestHandler } from '@kbn/core/server'; +import type { InfraPluginRequestHandlerContext } from '../types'; + +export function handleRouteErrors( + handler: RequestHandler +): RequestHandler { + return async (context, request, response) => { + try { + return await handler(context, request, response); + } catch (err) { + if (Boom.isBoom(err)) { + return response.customError({ + statusCode: err.output.statusCode, + body: { message: err.output.payload.message }, + }); + } + + return response.customError({ + statusCode: err.statusCode ?? 500, + body: { + message: err.message ?? 'An unexpected error occurred', + }, + }); + } + }; +} diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index 5a12a83b5fd58..acce31a2e36fe 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -86,6 +86,7 @@ "@kbn/core-ui-settings-browser", "@kbn/core-saved-objects-api-server", "@kbn/securitysolution-io-ts-utils", + "@kbn/core-lifecycle-server", "@kbn/elastic-agent-utils" ], "exclude": ["target/**/*"] diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index 1ccebf7b00aac..2502639bc0d4d 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -33,7 +33,7 @@ export { apmLabsButton, enableInfrastructureHostsView, enableInfrastructureProfilingIntegration, - enableInfrastructureHostsCustomDashboards, + enableInfrastructureAssetCustomDashboards, enableAwsLambdaMetrics, enableAgentExplorerView, apmEnableTableSearchBar, diff --git a/x-pack/plugins/observability/common/ui_settings_keys.ts b/x-pack/plugins/observability/common/ui_settings_keys.ts index bd603b6c3bd3f..74fd6cd09bac4 100644 --- a/x-pack/plugins/observability/common/ui_settings_keys.ts +++ b/x-pack/plugins/observability/common/ui_settings_keys.ts @@ -19,8 +19,8 @@ export const apmLabsButton = 'observability:apmLabsButton'; export const enableInfrastructureHostsView = 'observability:enableInfrastructureHostsView'; export const enableInfrastructureProfilingIntegration = 'observability:enableInfrastructureProfilingIntegration'; -export const enableInfrastructureHostsCustomDashboards = - 'observability:enableInfrastructureHostsCustomDashboards'; +export const enableInfrastructureAssetCustomDashboards = + 'observability:enableInfrastructureAssetCustomDashboards'; export const enableAwsLambdaMetrics = 'observability:enableAwsLambdaMetrics'; export const enableAgentExplorerView = 'observability:apmAgentExplorerView'; export const apmEnableTableSearchBar = 'observability:apmEnableTableSearchBar'; diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index f908043d7a070..f7cdfada7ad85 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -41,7 +41,7 @@ import { profilingAzureCostDiscountRate, enableInfrastructureProfilingIntegration, apmEnableTransactionProfiling, - enableInfrastructureHostsCustomDashboards, + enableInfrastructureAssetCustomDashboards, } from '../common/ui_settings_keys'; const betaLabel = i18n.translate('xpack.observability.uiSettings.betaLabel', { @@ -255,17 +255,17 @@ export const uiSettings: Record = { ), schema: schema.boolean(), }, - [enableInfrastructureHostsCustomDashboards]: { + [enableInfrastructureAssetCustomDashboards]: { category: [observabilityFeatureId], - name: i18n.translate('xpack.observability.enableInfrastructureHostsCustomDashboards', { - defaultMessage: 'Custom dashboards for Host Details in Infrastructure', + name: i18n.translate('xpack.observability.enableInfrastructureAssetCustomDashboards', { + defaultMessage: 'Custom dashboards for asset details in Infrastructure', }), value: false, description: i18n.translate( - 'xpack.observability.enableInfrastructureHostsCustomDashboardsDescription', + 'xpack.observability.enableInfrastructureAssetCustomDashboardsDescription', { defaultMessage: - '{betaLabel} Enable option to link custom dashboards in the Host Details view.', + '{betaLabel} Enable option to link custom dashboards in the asset details view.', values: { betaLabel: `[${betaLabel}]`, }, diff --git a/x-pack/test/api_integration/apis/metrics_ui/index.js b/x-pack/test/api_integration/apis/metrics_ui/index.js index ddc444c0e2a70..9e203ac5a3ada 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/index.js +++ b/x-pack/test/api_integration/apis/metrics_ui/index.js @@ -25,5 +25,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./infra')); loadTestFile(require.resolve('./inventory_threshold_alert')); loadTestFile(require.resolve('./services')); + loadTestFile(require.resolve('./infra_custom_dashboards')); }); } diff --git a/x-pack/test/api_integration/apis/metrics_ui/infra_custom_dashboards.ts b/x-pack/test/api_integration/apis/metrics_ui/infra_custom_dashboards.ts new file mode 100644 index 0000000000000..86c69d8ccd413 --- /dev/null +++ b/x-pack/test/api_integration/apis/metrics_ui/infra_custom_dashboards.ts @@ -0,0 +1,167 @@ +/* + * 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 { InfraCustomDashboard } from '@kbn/infra-plugin/common/custom_dashboards'; +import { InfraSaveCustomDashboardsRequestPayload } from '@kbn/infra-plugin/common/http_api/custom_dashboards_api'; +import { INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE } from '@kbn/infra-plugin/server/saved_objects'; +import { enableInfrastructureAssetCustomDashboards } from '@kbn/observability-plugin/common'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const CUSTOM_DASHBOARDS_API_URL = '/api/infra/custom-dashboards'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + + describe('Infra Custom Dashboards API', () => { + beforeEach(async () => { + await kibanaServer.savedObjects.clean({ + types: [INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE], + }); + }); + + describe('GET endpoint for fetching custom dashboard', () => { + it('responds with an error if Custom Dashboards UI setting is not enabled', async () => { + await kibanaServer.uiSettings.update({ + [enableInfrastructureAssetCustomDashboards]: false, + }); + + await supertest.get(`${CUSTOM_DASHBOARDS_API_URL}/host`).expect(403); + }); + + it('responds with an error when trying to request a custom dashboard for unsupported asset type', async () => { + await kibanaServer.uiSettings.update({ + [enableInfrastructureAssetCustomDashboards]: false, + }); + + await supertest.get(`${CUSTOM_DASHBOARDS_API_URL}/unsupported-asset-type`).expect(400); + }); + + it('responds with an empty configuration if custom dashboard saved object does not exist', async () => { + await kibanaServer.uiSettings.update({ + [enableInfrastructureAssetCustomDashboards]: true, + }); + await kibanaServer.savedObjects.clean({ + types: [INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE], + }); + + const response = await supertest.get(`${CUSTOM_DASHBOARDS_API_URL}/host`).expect(200); + + expect(response.body).to.be.eql({ + assetType: 'host', + dashboardIdList: [], + }); + }); + + it('responds with the custom dashboard configuration for a given asset type when it exists', async () => { + const customDashboard: InfraCustomDashboard = { + assetType: 'host', + dashboardIdList: ['123'], + }; + await kibanaServer.uiSettings.update({ + [enableInfrastructureAssetCustomDashboards]: true, + }); + await kibanaServer.savedObjects.create({ + type: INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE, + attributes: customDashboard, + overwrite: true, + }); + + const response = await supertest.get(`${CUSTOM_DASHBOARDS_API_URL}/host`).expect(200); + + expect(response.body).to.be.eql(customDashboard); + }); + }); + + describe('POST endpoint for saving (creating or updating) custom dashboard', () => { + it('responds with an error if Custom Dashboards UI setting is not enabled', async () => { + const payload: InfraSaveCustomDashboardsRequestPayload = { + assetType: 'host', + dashboardIdList: ['123'], + }; + await kibanaServer.uiSettings.update({ + [enableInfrastructureAssetCustomDashboards]: false, + }); + + await supertest + .post(`${CUSTOM_DASHBOARDS_API_URL}`) + .set('kbn-xsrf', 'xxx') + .send(payload) + .expect(403); + }); + + it('responds with an error when trying to update a custom dashboard for unsupported asset type', async () => { + const payload = { + assetType: 'unsupported-asset-type', + dashboardIdList: ['123'], + }; + await kibanaServer.uiSettings.update({ + [enableInfrastructureAssetCustomDashboards]: true, + }); + + await supertest + .post(`${CUSTOM_DASHBOARDS_API_URL}`) + .set('kbn-xsrf', 'xxx') + .send(payload) + .expect(400); + }); + + it('creates a new dashboard configuration when saving for the first time', async () => { + const payload: InfraSaveCustomDashboardsRequestPayload = { + assetType: 'host', + dashboardIdList: ['123'], + }; + await kibanaServer.uiSettings.update({ + [enableInfrastructureAssetCustomDashboards]: true, + }); + await kibanaServer.savedObjects.clean({ + types: [INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE], + }); + + const response = await supertest + .post(`${CUSTOM_DASHBOARDS_API_URL}`) + .set('kbn-xsrf', 'xxx') + .send(payload) + .expect(200); + + expect(response.body).to.be.eql(payload); + }); + + it('updates existing dashboard configuration when for a given asset type', async () => { + await kibanaServer.uiSettings.update({ + [enableInfrastructureAssetCustomDashboards]: true, + }); + await kibanaServer.savedObjects.clean({ + types: [INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE], + }); + await kibanaServer.savedObjects.create({ + type: INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE, + attributes: { + assetType: 'host', + dashboardIdList: ['123'], + }, + overwrite: true, + }); + + const payload: InfraSaveCustomDashboardsRequestPayload = { + assetType: 'host', + dashboardIdList: ['123', '456'], + }; + const updateResponse = await supertest + .post(`${CUSTOM_DASHBOARDS_API_URL}`) + .set('kbn-xsrf', 'xxx') + .send(payload) + .expect(200); + const getResponse = await supertest.get(`${CUSTOM_DASHBOARDS_API_URL}/host`).expect(200); + + expect(updateResponse.body).to.be.eql(payload); + expect(getResponse.body).to.be.eql(payload); + }); + }); + }); +}