[8.13] [Infra] Add endpoints to manage Custom Dashboards (#176612) (#177569)

# Backport

This will backport the following commits from `main` to `8.13`:
- [[Infra] Add endpoints to manage Custom Dashboards
(#176612)](https://github.com/elastic/kibana/pull/176612)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Mykola
Harmash","email":"mykola.harmash@gmail.com"},"sourceCommit":{"committedDate":"2024-02-22T10:56:33Z","message":"[Infra]
Add endpoints to manage Custom Dashboards (#176612)\n\nCloses
https://github.com/elastic/kibana/issues/176069\r\n\r\n##
Summary\r\n\r\nThis adds the logic to register a new Saved Object type
to store custom\r\ndashboards for Asset Details and adds endpoints to
fetch and save custom\r\ndashboards.\r\n\r\nChanges highlights:\r\n*
Renamed the `enableInfrastructureHostsCustomDashboards`
to\r\n`enableInfrastructureAssetCustomDashboards` to make it more
generic and\r\nsupport additional asset types in the future\r\n* Added a
new Saved Object type\r\n* Moved initialization of all Infra endpoints
to plugin's `start`. This\r\none one of the points on [the BE tech
debt\r\nticket](https://github.com/elastic/kibana/issues/175975).
Having\r\nendpoint initialization in `start` makes it more convenient to
access\r\nstart dependencies which almost all endpoints require.\r\n*
Added `savedObjectClient` and `uiSettingsClient` to the custom
request\r\ncontext (also one of the ideas for endpoints improvement).
Right now\r\ninfra endpoints use custom `libs` object with all
dependencies required\r\nfor routes, the idea is to rely on the request
context instead because\r\nit automatically available for every route
handler and by default\r\nincludes some useful things like scoped
service clients.\r\n* Added a wrapper `handleRouteErrors` to avoid error
handling\r\nduplication which we now have in a few routes. In the future
we could do\r\nsomething similar right within `registerRoutes` framework
function, but\r\nthis would require a bit of refactoring.\r\n\r\n## Hot
to Test\r\n\r\n1. Toggle the UI setting off in Advanced
Settings\r\n![CleanShot 2024-02-13 at 16
01\r\n36@2x](fc3772a1-a075-42bd-bdc3-2c7e83278844)\r\n2.
Go to the Dev Tools and try the endpoints, both should respond
with\r\n403\r\n```\r\nGET
kbn:api/infra/custom-dashboards/host\r\n\r\nPOST
kbn:api/infra/custom-dashboards\r\n{\r\n \"assetType\": \"host\",\r\n
\"dashboardIdList\": [\"0\", \"1\"]\r\n}\r\n```\r\n3. Toggle the UI
setting on\r\n4. Try the endpoints again, now they should work as
expected\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"b50f5387fcf1e5e5e706a2f566455ee619f4b006","branchLabelMapping":{"^v8.14.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:obs-ux-infra_services","Team:obs-ux-management","v8.13.0","v8.14.0"],"title":"[Infra]
Add endpoints to manage Custom
Dashboards","number":176612,"url":"https://github.com/elastic/kibana/pull/176612","mergeCommit":{"message":"[Infra]
Add endpoints to manage Custom Dashboards (#176612)\n\nCloses
https://github.com/elastic/kibana/issues/176069\r\n\r\n##
Summary\r\n\r\nThis adds the logic to register a new Saved Object type
to store custom\r\ndashboards for Asset Details and adds endpoints to
fetch and save custom\r\ndashboards.\r\n\r\nChanges highlights:\r\n*
Renamed the `enableInfrastructureHostsCustomDashboards`
to\r\n`enableInfrastructureAssetCustomDashboards` to make it more
generic and\r\nsupport additional asset types in the future\r\n* Added a
new Saved Object type\r\n* Moved initialization of all Infra endpoints
to plugin's `start`. This\r\none one of the points on [the BE tech
debt\r\nticket](https://github.com/elastic/kibana/issues/175975).
Having\r\nendpoint initialization in `start` makes it more convenient to
access\r\nstart dependencies which almost all endpoints require.\r\n*
Added `savedObjectClient` and `uiSettingsClient` to the custom
request\r\ncontext (also one of the ideas for endpoints improvement).
Right now\r\ninfra endpoints use custom `libs` object with all
dependencies required\r\nfor routes, the idea is to rely on the request
context instead because\r\nit automatically available for every route
handler and by default\r\nincludes some useful things like scoped
service clients.\r\n* Added a wrapper `handleRouteErrors` to avoid error
handling\r\nduplication which we now have in a few routes. In the future
we could do\r\nsomething similar right within `registerRoutes` framework
function, but\r\nthis would require a bit of refactoring.\r\n\r\n## Hot
to Test\r\n\r\n1. Toggle the UI setting off in Advanced
Settings\r\n![CleanShot 2024-02-13 at 16
01\r\n36@2x](fc3772a1-a075-42bd-bdc3-2c7e83278844)\r\n2.
Go to the Dev Tools and try the endpoints, both should respond
with\r\n403\r\n```\r\nGET
kbn:api/infra/custom-dashboards/host\r\n\r\nPOST
kbn:api/infra/custom-dashboards\r\n{\r\n \"assetType\": \"host\",\r\n
\"dashboardIdList\": [\"0\", \"1\"]\r\n}\r\n```\r\n3. Toggle the UI
setting on\r\n4. Try the endpoints again, now they should work as
expected\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"b50f5387fcf1e5e5e706a2f566455ee619f4b006"}},"sourceBranch":"main","suggestedTargetBranches":["8.13"],"targetPullRequestStates":[{"branch":"8.13","label":"v8.13.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.14.0","branchLabelMappingKey":"^v8.14.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/176612","number":176612,"mergeCommit":{"message":"[Infra]
Add endpoints to manage Custom Dashboards (#176612)\n\nCloses
https://github.com/elastic/kibana/issues/176069\r\n\r\n##
Summary\r\n\r\nThis adds the logic to register a new Saved Object type
to store custom\r\ndashboards for Asset Details and adds endpoints to
fetch and save custom\r\ndashboards.\r\n\r\nChanges highlights:\r\n*
Renamed the `enableInfrastructureHostsCustomDashboards`
to\r\n`enableInfrastructureAssetCustomDashboards` to make it more
generic and\r\nsupport additional asset types in the future\r\n* Added a
new Saved Object type\r\n* Moved initialization of all Infra endpoints
to plugin's `start`. This\r\none one of the points on [the BE tech
debt\r\nticket](https://github.com/elastic/kibana/issues/175975).
Having\r\nendpoint initialization in `start` makes it more convenient to
access\r\nstart dependencies which almost all endpoints require.\r\n*
Added `savedObjectClient` and `uiSettingsClient` to the custom
request\r\ncontext (also one of the ideas for endpoints improvement).
Right now\r\ninfra endpoints use custom `libs` object with all
dependencies required\r\nfor routes, the idea is to rely on the request
context instead because\r\nit automatically available for every route
handler and by default\r\nincludes some useful things like scoped
service clients.\r\n* Added a wrapper `handleRouteErrors` to avoid error
handling\r\nduplication which we now have in a few routes. In the future
we could do\r\nsomething similar right within `registerRoutes` framework
function, but\r\nthis would require a bit of refactoring.\r\n\r\n## Hot
to Test\r\n\r\n1. Toggle the UI setting off in Advanced
Settings\r\n![CleanShot 2024-02-13 at 16
01\r\n36@2x](fc3772a1-a075-42bd-bdc3-2c7e83278844)\r\n2.
Go to the Dev Tools and try the endpoints, both should respond
with\r\n403\r\n```\r\nGET
kbn:api/infra/custom-dashboards/host\r\n\r\nPOST
kbn:api/infra/custom-dashboards\r\n{\r\n \"assetType\": \"host\",\r\n
\"dashboardIdList\": [\"0\", \"1\"]\r\n}\r\n```\r\n3. Toggle the UI
setting on\r\n4. Try the endpoints again, now they should work as
expected\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"b50f5387fcf1e5e5e706a2f566455ee619f4b006"}}]}]
BACKPORT-->

Co-authored-by: Mykola Harmash <mykola.harmash@gmail.com>
This commit is contained in:
Kibana Machine 2024-02-22 07:19:35 -05:00 committed by GitHub
parent 86d88017b1
commit ffe0d21436
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 596 additions and 41 deletions

View file

@ -463,8 +463,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.

View file

@ -459,6 +459,11 @@
"title",
"type"
],
"infra-custom-dashboards": [
"assetType",
"dashboardIdList",
"kuery"
],
"infrastructure-monitoring-log-view": [
"name"
],

View file

@ -1553,6 +1553,19 @@
}
}
},
"infra-custom-dashboards": {
"properties": {
"assetType": {
"type": "keyword"
},
"dashboardIdList": {
"type": "keyword"
},
"kuery": {
"type": "text"
}
}
},
"infrastructure-monitoring-log-view": {
"dynamic": false,
"properties": {

View file

@ -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 =

View file

@ -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,
];

View file

@ -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",

View file

@ -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',

View file

@ -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",

View file

@ -608,7 +608,7 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'observability:enableInfrastructureHostsCustomDashboards': {
'observability:enableInfrastructureAssetCustomDashboards': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},

View file

@ -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;

View file

@ -10228,7 +10228,7 @@
"description": "Non-default value of setting."
}
},
"observability:enableInfrastructureHostsCustomDashboards": {
"observability:enableInfrastructureAssetCustomDashboards": {
"type": "boolean",
"_meta": {
"description": "Non-default value of setting."

View file

@ -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;
}

View file

@ -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
>;

View file

@ -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);
};

View file

@ -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<Params, Query, Body, 'get'>, handler);
break;
case 'post':
this.router.post(routeConfig, handler);
this.router.post(routeConfig as RouteConfig<Params, Query, Body, 'post'>, handler);
break;
case 'delete':
this.router.delete(routeConfig, handler);
this.router.delete(routeConfig as RouteConfig<Params, Query, Body, 'delete'>, handler);
break;
case 'put':
this.router.put(routeConfig, handler);
this.router.put(routeConfig as RouteConfig<Params, Query, Body, 'put'>, handler);
break;
case 'patch':
this.router.patch(routeConfig, handler);
this.router.patch(routeConfig as RouteConfig<Params, Query, Body, 'patch'>, handler);
break;
}
}

View file

@ -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<InfraPluginRequestHandlerContext, 'infra'>(
'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,

View file

@ -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);
}

View file

@ -0,0 +1,61 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { 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,
}),
});
})
);
}

View file

@ -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',
})
);
}
}

View file

@ -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<InfraCustomDashboard>({
type: INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE,
search: assetType,
searchFields: ['assetType'] as [keyof InfraCustomDashboard],
});
}

View file

@ -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<InfraCustomDashboard>(
INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE,
payload
);
return response.ok({
body: InfraSaveCustomDashboardsResponseBodyRT.encode(savedCustomDashboard.attributes),
});
}
const savedCustomDashboard = await savedObjectsClient.update<InfraCustomDashboard>(
INFRA_CUSTOM_DASHBOARDS_SAVED_OBJECT_TYPE,
customDashboards.saved_objects[0].id,
payload
);
return response.ok({
body: InfraSaveCustomDashboardsResponseBodyRT.encode({
...payload,
...savedCustomDashboard.attributes,
}),
});
})
);
}

View file

@ -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<keyof InfraCustomDashboard, SavedObjectsFieldMapping> = {
dashboardIdList: { type: 'keyword' },
assetType: { type: 'keyword' },
kuery: { type: 'text' },
};
const createSchema: Record<keyof InfraCustomDashboard, Type<any>> = {
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),
},
},
},
};

View file

@ -7,3 +7,4 @@
export * from './inventory_view';
export * from './metrics_explorer_view';
export * from './custom_dashboards/custom_dashboards_saved_object';

View file

@ -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<MlPluginSetup['mlSystemProvider']>;
export type MlAnomalyDetectors = ReturnType<MlPluginSetup['anomalyDetectorsProvider']>;
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
*/

View file

@ -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<Params = any, Query = any, Body = any>(
handler: RequestHandler<Params, Query, Body, InfraPluginRequestHandlerContext>
): RequestHandler<Params, Query, Body, InfraPluginRequestHandlerContext> {
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',
},
});
}
};
}

View file

@ -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/**/*"]

View file

@ -33,7 +33,7 @@ export {
apmLabsButton,
enableInfrastructureHostsView,
enableInfrastructureProfilingIntegration,
enableInfrastructureHostsCustomDashboards,
enableInfrastructureAssetCustomDashboards,
enableAwsLambdaMetrics,
enableAgentExplorerView,
apmEnableTableSearchBar,

View file

@ -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';

View file

@ -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<string, UiSettings> = {
),
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: `<em>[${betaLabel}]</em>`,
},

View file

@ -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'));
});
}

View file

@ -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);
});
});
});
}