diff --git a/package.json b/package.json index 4b2303136e000..23fcac6bc90eb 100644 --- a/package.json +++ b/package.json @@ -1054,7 +1054,7 @@ "deepmerge": "^4.2.2", "del": "^6.1.0", "diff": "^5.1.0", - "elastic-apm-node": "^4.7.2", + "elastic-apm-node": "^4.7.3", "email-addresses": "^5.0.0", "eventsource-parser": "^1.1.1", "execa": "^5.1.1", diff --git a/x-pack/plugins/integration_assistant/server/util/samples.ts b/x-pack/plugins/integration_assistant/server/util/samples.ts index e8489d79cdca0..f6728653e75ca 100644 --- a/x-pack/plugins/integration_assistant/server/util/samples.ts +++ b/x-pack/plugins/integration_assistant/server/util/samples.ts @@ -55,22 +55,25 @@ function isEmptyValue(value: unknown): boolean { function merge(target: Record, source: Record): Record { for (const [key, sourceValue] of Object.entries(source)) { - const targetValue = target[key]; - if (Array.isArray(sourceValue)) { - // Directly assign arrays - target[key] = sourceValue; - } else if ( - typeof sourceValue === 'object' && - sourceValue !== null && - !Array.isArray(targetValue) - ) { - if (typeof targetValue !== 'object' || isEmptyValue(targetValue)) { - target[key] = merge({}, sourceValue); - } else { - target[key] = merge(targetValue, sourceValue); + if (key !== '__proto__' && key !== 'constructor') { + if (Object.prototype.hasOwnProperty.call(target, key)) { + const targetValue = target[key]; + if (Array.isArray(sourceValue)) { + target[key] = sourceValue; + } else if ( + typeof sourceValue === 'object' && + sourceValue !== null && + typeof targetValue === 'object' && + targetValue !== null && + !Array.isArray(targetValue) + ) { + target[key] = merge(targetValue, sourceValue); + } else if (isEmptyValue(targetValue) && !isEmptyValue(sourceValue)) { + target[key] = sourceValue; + } + } else if (!isEmptyValue(sourceValue)) { + target[key] = sourceValue; } - } else if (!(key in target) || (isEmptyValue(targetValue) && !isEmptyValue(sourceValue))) { - target[key] = sourceValue; } } return target; diff --git a/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.test.ts b/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.test.ts index 441922d3fb771..2513a3432742d 100644 --- a/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.test.ts +++ b/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.test.ts @@ -7,13 +7,43 @@ import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields'; import { ALERT_RULE_PARAMETERS, TIMESTAMP } from '@kbn/rule-data-utils'; +import rison from '@kbn/rison'; import { getInventoryViewInAppUrl, flatAlertRuleParams, getMetricsViewInAppUrl, } from './alert_link'; +import { + InventoryLocator, + AssetDetailsLocator, + InventoryLocatorParams, + AssetDetailsLocatorParams, +} from '@kbn/observability-shared-plugin/common'; + +jest.mock('@kbn/observability-shared-plugin/common'); + +const mockInventoryLocator = { + getRedirectUrl: jest + .fn() + .mockImplementation( + (params: InventoryLocatorParams) => + `/inventory-mock?receivedParams=${rison.encodeUnknown(params)}` + ), +} as unknown as jest.Mocked; + +const mockAssetDetailsLocator = { + getRedirectUrl: jest + .fn() + .mockImplementation( + ({ assetId, assetType, assetDetails }: AssetDetailsLocatorParams) => + `/node-mock/${assetType}/${assetId}?receivedParams=${rison.encodeUnknown(assetDetails)}` + ), +} as unknown as jest.Mocked; describe('Inventory Threshold Rule', () => { + afterEach(() => { + jest.clearAllMocks(); + }); describe('flatAlertRuleParams', () => { it('flat ALERT_RULE_PARAMETERS', () => { expect( @@ -85,9 +115,14 @@ describe('Inventory Threshold Rule', () => { [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`]: ['avg'], [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.field`]: ['system.cpu.user.pct'], } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockInventoryLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/inventory?customMetric=%28aggregation%3Aavg%2Cfield%3Asystem.cpu.user.pct%2Cid%3Aalert-custom-metric%2Ctype%3Acustom%29&metric=%28aggregation%3Aavg%2Cfield%3Asystem.cpu.user.pct%2Cid%3Aalert-custom-metric%2Ctype%3Acustom%29&nodeType=h×tamp=1640995200000' + "/inventory-mock?receivedParams=(customMetric:'(aggregation:avg,field:system.cpu.user.pct,id:alert-custom-metric,type:custom)',metric:'(aggregation:avg,field:system.cpu.user.pct,id:alert-custom-metric,type:custom)',nodeType:host,timestamp:1640995200000)" ); }); it('should work with non-custom metrics', () => { @@ -96,22 +131,50 @@ describe('Inventory Threshold Rule', () => { [`${ALERT_RULE_PARAMETERS}.nodeType`]: 'host', [`${ALERT_RULE_PARAMETERS}.criteria.metric`]: ['cpu'], } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockInventoryLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/inventory?customMetric=&metric=%28type%3Acpu%29&nodeType=h×tamp=1640995200000' + "/inventory-mock?receivedParams=(customMetric:'',metric:'(type:cpu)',nodeType:host,timestamp:1640995200000)" ); }); - it('should point to host-details when host.name is present', () => { + it('should point to asset details when nodeType is host and host.name is present', () => { const fields = { [TIMESTAMP]: '2022-01-01T00:00:00.000Z', - [`${ALERT_RULE_PARAMETERS}.nodeType`]: 'kubernetes', + [`${ALERT_RULE_PARAMETERS}.nodeType`]: 'host', [`${ALERT_RULE_PARAMETERS}.criteria.metric`]: ['cpu'], [`host.name`]: ['my-host'], } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/host-detail/my-host?from=1640995200000&to=1640996100000' + "/node-mock/host/my-host?receivedParams=(alertMetric:cpu,dateRange:(from:'2022-01-01T00:00:00.000Z',to:'2022-01-01T00:15:00.000Z'))" + ); + }); + + it('should point to asset details when nodeType is container and container.id is present', () => { + const fields = { + [TIMESTAMP]: '2022-01-01T00:00:00.000Z', + [`${ALERT_RULE_PARAMETERS}.nodeType`]: 'container', + [`${ALERT_RULE_PARAMETERS}.criteria.metric`]: ['cpu'], + [`container.id`]: ['my-container'], + } as unknown as ParsedTechnicalFields & Record; + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1); + expect(url).toEqual( + "/node-mock/container/my-container?receivedParams=(alertMetric:cpu,dateRange:(from:'2022-01-01T00:00:00.000Z',to:'2022-01-01T00:15:00.000Z'))" ); }); @@ -140,9 +203,14 @@ describe('Inventory Threshold Rule', () => { _id: 'eaa439aa-a4bb-4e7c-b7f8-fbe532ca7366', _index: '.internal.alerts-observability.metrics.alerts-default-000001', } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockInventoryLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/inventory?customMetric=%28aggregation%3Aavg%2Cfield%3Asystem.cpu.user.pct%2Cid%3Aalert-custom-metric%2Ctype%3Acustom%29&metric=%28aggregation%3Aavg%2Cfield%3Asystem.cpu.user.pct%2Cid%3Aalert-custom-metric%2Ctype%3Acustom%29&nodeType=host×tamp=1640995200000' + "/inventory-mock?receivedParams=(customMetric:'(aggregation:avg,field:system.cpu.user.pct,id:alert-custom-metric,type:custom)',metric:'(aggregation:avg,field:system.cpu.user.pct,id:alert-custom-metric,type:custom)',nodeType:host,timestamp:1640995200000)" ); }); @@ -165,32 +233,75 @@ describe('Inventory Threshold Rule', () => { _id: 'eaa439aa-a4bb-4e7c-b7f8-fbe532ca7366', _index: '.internal.alerts-observability.metrics.alerts-default-000001', } as unknown as ParsedTechnicalFields & Record; - const url = getInventoryViewInAppUrl(fields); + const url = getInventoryViewInAppUrl({ + fields, + inventoryLocator: mockInventoryLocator, + assetDetailsLocator: mockAssetDetailsLocator, + }); + expect(mockInventoryLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/inventory?customMetric=&metric=%28type%3Acpu%29&nodeType=host×tamp=1640995200000' + "/inventory-mock?receivedParams=(customMetric:'',metric:'(type:cpu)',nodeType:host,timestamp:1640995200000)" ); }); }); }); describe('Metrics Rule', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + describe('getMetricsViewInAppUrl', () => { - it('should point to host-details when host.name is present', () => { + it('should point to host details when host.name is present', () => { const fields = { [TIMESTAMP]: '2022-01-01T00:00:00.000Z', [`host.name`]: ['my-host'], } as unknown as ParsedTechnicalFields & Record; - const url = getMetricsViewInAppUrl(fields); + const url = getMetricsViewInAppUrl({ + fields, + assetDetailsLocator: mockAssetDetailsLocator, + groupBy: ['host.name'], + }); + expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1); + expect(url).toEqual( + "/node-mock/host/my-host?receivedParams=(dateRange:(from:'2022-01-01T00:00:00.000Z',to:'2022-01-01T00:15:00.000Z'))" + ); + }); + + it('should point to container details when host.name is present', () => { + const fields = { + [TIMESTAMP]: '2022-01-01T00:00:00.000Z', + [`container.id`]: ['my-host-5xyz'], + } as unknown as ParsedTechnicalFields & Record; + const url = getMetricsViewInAppUrl({ + fields, + assetDetailsLocator: mockAssetDetailsLocator, + groupBy: ['container.id'], + }); + expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1); expect(url).toEqual( - '/app/metrics/link-to/host-detail/my-host?from=1640995200000&to=1640996100000' + "/node-mock/container/my-host-5xyz?receivedParams=(dateRange:(from:'2022-01-01T00:00:00.000Z',to:'2022-01-01T00:15:00.000Z'))" ); }); + it('should point to metrics when group by field is not supported by the asset details', () => { + const fields = { + [TIMESTAMP]: '2022-01-01T00:00:00.000Z', + [`host.name`]: ['my-host'], + } as unknown as ParsedTechnicalFields & Record; + const url = getMetricsViewInAppUrl({ + fields, + assetDetailsLocator: mockAssetDetailsLocator, + groupBy: ['kubernetes.pod.name'], + }); + expect(url).toEqual('/app/metrics/explorer'); + }); + it('should point to metrics explorer', () => { const fields = { [TIMESTAMP]: '2022-01-01T00:00:00.000Z', } as unknown as ParsedTechnicalFields & Record; - const url = getMetricsViewInAppUrl(fields); + const url = getMetricsViewInAppUrl({ fields }); expect(url).toEqual('/app/metrics/explorer'); }); }); diff --git a/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts b/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts index 06d34a83f123a..3dc84f0406145 100644 --- a/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts +++ b/x-pack/plugins/observability_solution/infra/common/alerting/metrics/alert_link.ts @@ -6,16 +6,22 @@ */ import { ALERT_RULE_PARAMETERS, TIMESTAMP } from '@kbn/rule-data-utils'; +import moment from 'moment'; import { encode } from '@kbn/rison'; -import { stringify } from 'query-string'; import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common/parse_technical_fields'; -import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; +import { type InventoryItemType, findInventoryModel } from '@kbn/metrics-data-access-plugin/common'; +import type { LocatorPublic } from '@kbn/share-plugin/common'; import { - fifteenMinutesInMilliseconds, - HOST_NAME_FIELD, - LINK_TO_INVENTORY, - METRICS_EXPLORER_URL, -} from '../../constants'; + type AssetDetailsLocatorParams, + type InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; +import { castArray } from 'lodash'; +import { fifteenMinutesInMilliseconds, METRICS_EXPLORER_URL } from '../../constants'; +import { SupportedAssetTypes } from '../../asset_details/types'; + +const ALERT_RULE_PARAMTERS_INVENTORY_METRIC_ID = `${ALERT_RULE_PARAMETERS}.criteria.metric`; +export const ALERT_RULE_PARAMETERS_NODE_TYPE = `${ALERT_RULE_PARAMETERS}.nodeType`; +const CUSTOM_METRIC_TYPE = 'custom'; export const flatAlertRuleParams = (params: {}, pKey = ''): Record => { return Object.entries(params).reduce((acc, [key, field]) => { @@ -32,10 +38,18 @@ export const flatAlertRuleParams = (params: {}, pKey = ''): Record); }; -export const getInventoryViewInAppUrl = ( - fields: ParsedTechnicalFields & Record -): string => { - let inventoryFields = fields; +export const getInventoryViewInAppUrl = ({ + fields, + assetDetailsLocator, + inventoryLocator, +}: { + fields: ParsedTechnicalFields & Record; + assetDetailsLocator?: LocatorPublic; + inventoryLocator?: LocatorPublic; +}): string => { + if (!assetDetailsLocator || !inventoryLocator) { + return ''; + } /* Temporary Solution -> https://github.com/elastic/kibana/issues/137033 * In the alert table from timelines plugin (old table), we are using an API who is flattening all the response @@ -45,75 +59,131 @@ export const getInventoryViewInAppUrl = ( * triggersActionUI then we will stop using this flattening way and we will update the code to work with fields API, * it will be less magic. */ - if (fields[ALERT_RULE_PARAMETERS]) { - inventoryFields = { - ...fields, - ...flatAlertRuleParams(fields[ALERT_RULE_PARAMETERS] as {}, ALERT_RULE_PARAMETERS), - }; + const inventoryFields = fields[ALERT_RULE_PARAMETERS] + ? { + ...fields, + ...flatAlertRuleParams(fields[ALERT_RULE_PARAMETERS] as {}, ALERT_RULE_PARAMETERS), + } + : fields; + + const nodeType = castArray(inventoryFields[ALERT_RULE_PARAMETERS_NODE_TYPE])[0]; + + if (!nodeType) { + return ''; } - const nodeTypeField = `${ALERT_RULE_PARAMETERS}.nodeType`; - const nodeType = inventoryFields[nodeTypeField] as InventoryItemType; - const hostName = inventoryFields[HOST_NAME_FIELD]; + const assetIdField = findInventoryModel(nodeType).fields.id; + const assetId = inventoryFields[assetIdField]; + const assetDetailsSupported = Object.values(SupportedAssetTypes).includes( + nodeType as SupportedAssetTypes + ); + const criteriaMetric = inventoryFields[ALERT_RULE_PARAMTERS_INVENTORY_METRIC_ID][0]; - if (nodeType) { - if (hostName) { - return getLinkToHostDetails({ hostName, timestamp: inventoryFields[TIMESTAMP] }); - } - const linkToParams = { - nodeType: inventoryFields[nodeTypeField][0], - timestamp: Date.parse(inventoryFields[TIMESTAMP]), - customMetric: '', - metric: '', - }; - - // We always pick the first criteria metric for the URL - const criteriaMetric = inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.metric`][0]; - if (criteriaMetric === 'custom') { - const criteriaCustomMetricId = - inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`][0]; - const criteriaCustomMetricAggregation = - inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`][0]; - const criteriaCustomMetricField = - inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.field`][0]; - - const customMetric = encode({ - id: criteriaCustomMetricId, - type: 'custom', - field: criteriaCustomMetricField, - aggregation: criteriaCustomMetricAggregation, - }); - linkToParams.customMetric = customMetric; - linkToParams.metric = customMetric; - } else { - linkToParams.metric = encode({ type: criteriaMetric }); - } - return `${LINK_TO_INVENTORY}?${stringify(linkToParams)}`; + if (assetId && assetDetailsSupported) { + return getLinkToAssetDetails({ + assetId, + assetType: nodeType, + timestamp: inventoryFields[TIMESTAMP], + alertMetric: criteriaMetric, + assetDetailsLocator, + }); + } + + const linkToParams = { + nodeType, + timestamp: Date.parse(inventoryFields[TIMESTAMP]), + customMetric: '', + metric: '', + }; + + // We always pick the first criteria metric for the URL + + if (criteriaMetric === CUSTOM_METRIC_TYPE) { + const criteriaCustomMetricId = + inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`][0]; + const criteriaCustomMetricAggregation = + inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`][0]; + const criteriaCustomMetricField = + inventoryFields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.field`][0]; + + const customMetric = encode({ + id: criteriaCustomMetricId, + type: CUSTOM_METRIC_TYPE, + field: criteriaCustomMetricField, + aggregation: criteriaCustomMetricAggregation, + }); + linkToParams.customMetric = customMetric; + linkToParams.metric = customMetric; + } else { + linkToParams.metric = encode({ type: criteriaMetric }); } - return LINK_TO_INVENTORY; + return inventoryLocator.getRedirectUrl({ + ...linkToParams, + }); }; -export const getMetricsViewInAppUrl = (fields: ParsedTechnicalFields & Record) => { - const hostName = fields[HOST_NAME_FIELD]; - const timestamp = fields[TIMESTAMP]; +export const getMetricsViewInAppUrl = ({ + fields, + groupBy, + assetDetailsLocator, +}: { + fields: ParsedTechnicalFields & Record; + groupBy?: string[]; + assetDetailsLocator?: LocatorPublic; +}) => { + if (!groupBy || !assetDetailsLocator) { + return METRICS_EXPLORER_URL; + } + + // creates an object of asset details supported assetType by their assetId field name + const assetTypeByAssetId = Object.values(SupportedAssetTypes).reduce((acc, curr) => { + acc[findInventoryModel(curr).fields.id] = curr; + return acc; + }, {} as Record); + + // detemines if the groupBy has a field that the asset details supports + const supportedAssetId = groupBy?.find((field) => !!assetTypeByAssetId[field]); + // assigns a nodeType if the groupBy field is supported by asset details + const supportedAssetType = supportedAssetId ? assetTypeByAssetId[supportedAssetId] : undefined; + + if (supportedAssetType) { + const assetId = fields[findInventoryModel(supportedAssetType).fields.id]; + const timestamp = fields[TIMESTAMP]; - return hostName ? getLinkToHostDetails({ hostName, timestamp }) : METRICS_EXPLORER_URL; + return getLinkToAssetDetails({ + assetId, + assetType: supportedAssetType, + timestamp, + assetDetailsLocator, + }); + } else { + return METRICS_EXPLORER_URL; + } }; -export function getLinkToHostDetails({ - hostName, +function getLinkToAssetDetails({ + assetId, + assetType, timestamp, + alertMetric, + assetDetailsLocator, }: { - hostName: string; + assetId: string; + assetType: InventoryItemType; timestamp: string; + alertMetric?: string; + assetDetailsLocator: LocatorPublic; }): string { - const queryParams = { - from: Date.parse(timestamp), - to: Date.parse(timestamp) + fifteenMinutesInMilliseconds, - }; - - const encodedParams = encode(stringify(queryParams)); - - return `/app/metrics/link-to/host-detail/${hostName}?${encodedParams}`; + return assetDetailsLocator.getRedirectUrl({ + assetId, + assetType, + assetDetails: { + dateRange: { + from: timestamp, + to: moment(timestamp).add(fifteenMinutesInMilliseconds, 'ms').toISOString(), + }, + ...(alertMetric && alertMetric !== CUSTOM_METRIC_TYPE ? { alertMetric } : undefined), + }, + }); } diff --git a/x-pack/plugins/observability_solution/infra/common/asset_details/types.ts b/x-pack/plugins/observability_solution/infra/common/asset_details/types.ts new file mode 100644 index 0000000000000..685b2bcacb2e4 --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/common/asset_details/types.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. + */ +export enum SupportedAssetTypes { + container = 'container', + host = 'host', +} diff --git a/x-pack/plugins/observability_solution/infra/common/constants.ts b/x-pack/plugins/observability_solution/infra/common/constants.ts index 0bbabffbb17ce..63dfa663ce256 100644 --- a/x-pack/plugins/observability_solution/infra/common/constants.ts +++ b/x-pack/plugins/observability_solution/infra/common/constants.ts @@ -43,7 +43,6 @@ export const O11Y_AAD_FIELDS = [ 'tags', ]; -export const LINK_TO_INVENTORY = '/app/metrics/link-to/inventory'; export const METRICS_EXPLORER_URL = '/app/metrics/explorer'; export const fifteenMinutesInMilliseconds = 15 * 60 * 1000; diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/inventory/index.ts b/x-pack/plugins/observability_solution/infra/public/alerting/inventory/index.ts index d95440d7cac73..0d0fd398909a0 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/inventory/index.ts +++ b/x-pack/plugins/observability_solution/infra/public/alerting/inventory/index.ts @@ -9,12 +9,17 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { RuleTypeParams } from '@kbn/alerting-plugin/common'; import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public'; +import type { LocatorPublic } from '@kbn/share-plugin/common'; +import type { + AssetDetailsLocatorParams, + InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { InventoryMetricConditions, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, } from '../../../common/alerting/metrics'; import { validateMetricThreshold } from './components/validation'; -import { formatReason } from './rule_data_formatters'; +import { getRuleFormat } from './rule_data_formatters'; interface InventoryMetricRuleTypeParams extends RuleTypeParams { criteria: InventoryMetricConditions[]; @@ -50,7 +55,15 @@ const inventoryDefaultRecoveryMessage = i18n.translate( } ); -export function createInventoryMetricRuleType(): ObservabilityRuleTypeModel { +export function createInventoryMetricRuleType({ + assetDetailsLocator, + inventoryLocator, +}: { + assetDetailsLocator?: LocatorPublic; + inventoryLocator?: LocatorPublic; +}): ObservabilityRuleTypeModel { + const format = getRuleFormat({ assetDetailsLocator, inventoryLocator }); + return { id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.metrics.inventory.alertFlyout.alertDescription', { @@ -65,7 +78,7 @@ export function createInventoryMetricRuleType(): ObservabilityRuleTypeModel { - const reason = fields[ALERT_REASON] ?? '-'; +export const getRuleFormat = ({ + assetDetailsLocator, + inventoryLocator, +}: { + assetDetailsLocator?: LocatorPublic; + inventoryLocator?: LocatorPublic; +}): ObservabilityRuleTypeFormatter => { + return ({ fields }) => { + const reason = fields[ALERT_REASON] ?? '-'; - return { - reason, - link: getInventoryViewInAppUrl(fields), + return { + reason, + link: getInventoryViewInAppUrl({ fields, assetDetailsLocator, inventoryLocator }), + hasBasePath: true, + }; }; }; diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/index.ts b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/index.ts index 362c6a500dd8b..a37d14c061f76 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/index.ts +++ b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/index.ts @@ -9,12 +9,14 @@ import { i18n } from '@kbn/i18n'; import { lazy } from 'react'; import { RuleTypeParams } from '@kbn/alerting-plugin/common'; import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public'; +import { LocatorPublic } from '@kbn/share-plugin/common'; +import { AssetDetailsLocatorParams } from '@kbn/observability-shared-plugin/common'; import { MetricExpressionParams, METRIC_THRESHOLD_ALERT_TYPE_ID, } from '../../../common/alerting/metrics'; import { validateMetricThreshold } from './components/validation'; -import { formatReason } from './rule_data_formatters'; +import { getRuleFormat } from './rule_data_formatters'; export interface MetricThresholdRuleTypeParams extends RuleTypeParams { criteria: MetricExpressionParams[]; @@ -50,7 +52,11 @@ const metricThresholdDefaultRecoveryMessage = i18n.translate( } ); -export function createMetricThresholdRuleType(): ObservabilityRuleTypeModel { +export function createMetricThresholdRuleType({ + assetDetailsLocator, +}: { + assetDetailsLocator?: LocatorPublic; +}): ObservabilityRuleTypeModel { return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.metrics.alertFlyout.alertDescription', { @@ -65,7 +71,7 @@ export function createMetricThresholdRuleType(): ObservabilityRuleTypeModel import('./components/alert_details_app_section')), priority: 10, }; diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/rule_data_formatters.ts b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/rule_data_formatters.ts index 75d5bceb61327..85169903c68d9 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/rule_data_formatters.ts +++ b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/rule_data_formatters.ts @@ -5,14 +5,33 @@ * 2.0. */ -import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils'; import { ObservabilityRuleTypeFormatter } from '@kbn/observability-plugin/public'; +import { LocatorPublic } from '@kbn/share-plugin/common'; +import type { AssetDetailsLocatorParams } from '@kbn/observability-shared-plugin/common'; +import { castArray } from 'lodash'; +import { METRICS_EXPLORER_URL } from '../../../common/constants'; import { getMetricsViewInAppUrl } from '../../../common/alerting/metrics/alert_link'; -export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => { - const reason = fields[ALERT_REASON] ?? '-'; - return { - reason, - link: getMetricsViewInAppUrl(fields), +export const getRuleFormat = ({ + assetDetailsLocator, +}: { + assetDetailsLocator?: LocatorPublic; +}): ObservabilityRuleTypeFormatter => { + return ({ fields }) => { + const reason = fields[ALERT_REASON] ?? '-'; + const parameters = fields[ALERT_RULE_PARAMETERS]; + + const link = getMetricsViewInAppUrl({ + fields, + groupBy: castArray(parameters?.groupBy as string[] | string), + assetDetailsLocator, + }); + + return { + reason, + link, + hasBasePath: link !== METRICS_EXPLORER_URL, + }; }; }; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/constants.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/constants.ts index e189c8e3524f3..3b3db1b21bd09 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/constants.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/constants.ts @@ -5,8 +5,9 @@ * 2.0. */ +import { SupportedAssetTypes } from '../../../common/asset_details/types'; import type { DockerContainerMetrics, KubernetesContainerMetrics } from './charts/types'; -import { INTEGRATION_NAME, ASSET_DETAILS_ASSET_TYPE } from './types'; +import { IntegrationEventModules } from './types'; export const ASSET_DETAILS_FLYOUT_COMPONENT_NAME = 'infraAssetDetailsFlyout'; export const ASSET_DETAILS_PAGE_COMPONENT_NAME = 'infraAssetDetailsPage'; @@ -15,16 +16,16 @@ export const APM_HOST_FILTER_FIELD = 'host.hostname'; export const APM_CONTAINER_FILTER_FIELD = 'container.id'; export const APM_FILTER_FIELD_PER_ASSET_TYPE = { - [ASSET_DETAILS_ASSET_TYPE.container]: APM_CONTAINER_FILTER_FIELD, - [ASSET_DETAILS_ASSET_TYPE.host]: APM_HOST_FILTER_FIELD, + [SupportedAssetTypes.container]: APM_CONTAINER_FILTER_FIELD, + [SupportedAssetTypes.host]: APM_HOST_FILTER_FIELD, }; export const ASSET_DETAILS_URL_STATE_KEY = 'assetDetails'; export const INTEGRATIONS = { - [INTEGRATION_NAME.kubernetesNode]: 'kubernetes.node', - [INTEGRATION_NAME.kubernetesContainer]: 'kubernetes.container', - [INTEGRATION_NAME.docker]: 'docker', + [IntegrationEventModules.kubernetesNode]: 'kubernetes.node', + [IntegrationEventModules.kubernetesContainer]: 'kubernetes.container', + [IntegrationEventModules.docker]: 'docker', }; export const DOCKER_METRIC_TYPES: DockerContainerMetrics[] = ['cpu', 'memory', 'network', 'disk']; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts.tsx new file mode 100644 index 0000000000000..135c7e2ce77e0 --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts.tsx @@ -0,0 +1,48 @@ +/* + * 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 React from 'react'; + +import { + type SnapshotMetricType, + findInventoryModel, + type InventoryModels, + InventoryItemType, +} from '@kbn/metrics-data-access-plugin/common'; +import { useAssetDetailsUrlState } from '../hooks/use_asset_details_url_state'; +import { useAssetDetailsRenderPropsContext } from '../hooks/use_asset_details_render_props'; +import { LegacyAlertMetricCallout } from './callouts/legacy_metric_callout'; +import { ContentTabIds } from '../types'; + +const INCOMING_ALERT_CALLOUT_VISIBLE_FOR = [ContentTabIds.OVERVIEW, ContentTabIds.METRICS]; + +const isSnapshotMetricType = ( + inventoryModel: InventoryModels, + value?: string +): value is SnapshotMetricType => { + return !!value && !!inventoryModel.metrics.snapshot[value]; +}; + +export const Callouts = () => { + const { asset } = useAssetDetailsRenderPropsContext(); + const [state] = useAssetDetailsUrlState(); + + const assetConfig = findInventoryModel(asset.type); + const alertMetric = isSnapshotMetricType(assetConfig, state?.alertMetric) + ? state?.alertMetric + : undefined; + + if (asset.type === 'host' && alertMetric && assetConfig.legacyMetrics?.includes(alertMetric)) { + return ( + + ); + } + + return null; +}; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts/legacy_metric_callout.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts/legacy_metric_callout.tsx new file mode 100644 index 0000000000000..f38897155fac2 --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/callouts/legacy_metric_callout.tsx @@ -0,0 +1,82 @@ +/* + * 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 React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; +import { InventoryItemType, SnapshotMetricType } from '@kbn/metrics-data-access-plugin/common'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import { HOST_METRICS_DOC_HREF } from '../../../../common/visualizations'; +import { toMetricOpt } from '../../../../../common/snapshot_metric_i18n'; +import { useAssetDetailsRenderPropsContext } from '../../hooks/use_asset_details_render_props'; +import { ContentTabIds } from '../../types'; +import { useTabSwitcherContext } from '../../hooks/use_tab_switcher'; + +const DISMISSAL_LEGACY_ALERT_METRIC_STORAGE_KEY = 'infraAssetDetails:legacy_alert_metric_dismissed'; + +export const LegacyAlertMetricCallout = ({ + visibleFor, + metric, +}: { + visibleFor: ContentTabIds[]; + metric: SnapshotMetricType; +}) => { + const { activeTabId } = useTabSwitcherContext(); + const { asset } = useAssetDetailsRenderPropsContext(); + const [isDismissed, setDismissed] = useLocalStorage( + `${DISMISSAL_LEGACY_ALERT_METRIC_STORAGE_KEY}_${metric}`, + false + ); + + const onDismiss = () => { + setDismissed(true); + }; + + const metricLabel = toMetricOpt(metric, asset.id as InventoryItemType); + const hideCallout = isDismissed || !visibleFor.includes(activeTabId as ContentTabIds); + + if (hideCallout || !metricLabel) { + return null; + } + + return ( + + } + data-test-subj="infraAssetDetailsLegacyMetricAlertCallout" + onDismiss={onDismiss} + > + + + + ), + }} + /> + + ); +}; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx index 52bff06e75a33..eadcc74c5a8d3 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/content/content.tsx @@ -22,22 +22,30 @@ import { Profiling, } from '../tabs'; import { ContentTabIds } from '../types'; +import { Callouts } from './callouts'; export const Content = () => { return ( - + + + + + + + + diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts index f7d8ca564f293..d0694ef7f207f 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_asset_details_url_state.ts @@ -99,6 +99,7 @@ const AssetDetailsUrlStateRT = rt.partial({ profilingSearch: rt.string, alertStatus: AlertStatusRT, dashboardId: rt.string, + alertMetric: rt.string, }); const AssetDetailsUrlRT = rt.union([AssetDetailsUrlStateRT, rt.null]); diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx index be98902ad9c57..a3d94c5c6e14b 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/hooks/use_page_header.tsx @@ -15,7 +15,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { useUiSetting } from '@kbn/kibana-react-plugin/public'; import { enableInfrastructureAssetCustomDashboards } from '@kbn/observability-plugin/common'; import { useLinkProps } from '@kbn/observability-shared-plugin/public'; -import { capitalize } from 'lodash'; +import { capitalize, isEmpty } from 'lodash'; import React, { useCallback, useMemo } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { usePluginConfig } from '../../../containers/plugin_config_context'; @@ -62,7 +62,7 @@ export const useTemplateHeaderBreadcrumbs = () => { const breadcrumbs: EuiBreadcrumbsProps['breadcrumbs'] = // If there is a state object in location, it's persisted in case the page is opened in a new tab or after page refresh // With that, we can show the return button. Otherwise, it will be hidden (ex: the user opened a shared URL or opened the page from their bookmarks) - location.state || history.length > 1 + !isEmpty(location.state) || history.length > 1 ? [ { text: ( diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx b/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx index 5ac8809be5443..346acb6d8a164 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/template/page.tsx @@ -5,16 +5,13 @@ * 2.0. */ -import { EuiFlexGroup } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { i18n } from '@kbn/i18n'; import React, { useEffect } from 'react'; import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { SYSTEM_INTEGRATION } from '../../../../common/constants'; import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; import { useParentBreadcrumbResolver } from '../../../hooks/use_parent_breadcrumb_resolver'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; -import { InfraLoadingPanel } from '../../loading'; import { ASSET_DETAILS_PAGE_COMPONENT_NAME } from '../constants'; import { Content } from '../content/content'; import { useAssetDetailsRenderPropsContext } from '../hooks/use_asset_details_render_props'; @@ -86,7 +83,7 @@ export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => { onboardingFlow={asset.type === 'host' ? OnboardingFlow.Hosts : OnboardingFlow.Infra} dataAvailabilityModules={DATA_AVAILABILITY_PER_TYPE[asset.type] || undefined} pageHeader={{ - pageTitle: asset.name, + pageTitle: loading ? : asset.name, tabs: tabEntries, rightSideItems, breadcrumbs: headerBreadcrumbs, @@ -94,24 +91,7 @@ export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => { data-component-name={ASSET_DETAILS_PAGE_COMPONENT_NAME} data-asset-type={asset.type} > - {loading ? ( - - - - ) : ( - - )} + ); }; diff --git a/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts b/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts index 01700206285e8..064b82094a505 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts +++ b/x-pack/plugins/observability_solution/infra/public/components/asset_details/types.ts @@ -94,13 +94,8 @@ export interface RouteState { export type DataViewOrigin = 'logs' | 'metrics'; -export enum INTEGRATION_NAME { +export enum IntegrationEventModules { kubernetesNode = 'kubernetesNode', kubernetesContainer = 'kubernetesContainer', docker = 'docker', } - -export enum ASSET_DETAILS_ASSET_TYPE { - container = 'container', - host = 'host', -} diff --git a/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx b/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx index ca271b46146c8..d8522aa0f4d59 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_host_detail_via_ip.tsx @@ -10,7 +10,10 @@ import { RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import type { SerializableRecord } from '@kbn/utility-types'; -import { ASSET_DETAILS_LOCATOR_ID } from '@kbn/observability-shared-plugin/common'; +import { + ASSET_DETAILS_LOCATOR_ID, + type AssetDetailsLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { useHostIpToName } from './use_host_ip_to_name'; import { LoadingPage } from '../../components/loading_page'; import { Error } from '../error'; @@ -32,7 +35,7 @@ export const RedirectToHostDetailViaIP = ({ const { services: { share }, } = useKibanaContextForPlugin(); - const baseLocator = share.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const baseLocator = share.url.locators.get(ASSET_DETAILS_LOCATOR_ID); const { error, name } = useHostIpToName(hostIp, (metricsView && metricsView.indices) || null); diff --git a/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx b/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx index 714be106fad3f..d0bac8d8c9bf2 100644 --- a/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx +++ b/x-pack/plugins/observability_solution/infra/public/pages/link_to/redirect_to_node_detail.tsx @@ -14,7 +14,8 @@ import { type AssetDetailsLocatorParams, } from '@kbn/observability-shared-plugin/common'; import type { SerializableRecord } from '@kbn/utility-types'; -import { AssetDetailsUrlState } from '../../components/asset_details/types'; +import { SupportedAssetTypes } from '../../../common/asset_details/types'; +import { type AssetDetailsUrlState } from '../../components/asset_details/types'; import { ASSET_DETAILS_URL_STATE_KEY } from '../../components/asset_details/constants'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; @@ -22,7 +23,7 @@ export const REDIRECT_NODE_DETAILS_FROM_KEY = 'from'; export const REDIRECT_NODE_DETAILS_TO_KEY = 'to'; export const REDIRECT_ASSET_DETAILS_KEY = 'assetDetails'; -const getHostDetailSearch = (queryParams: URLSearchParams) => { +const getAssetDetailsQueryParams = (queryParams: URLSearchParams) => { const from = queryParams.get(REDIRECT_NODE_DETAILS_FROM_KEY); const to = queryParams.get(REDIRECT_NODE_DETAILS_TO_KEY); const assetDetailsParam = queryParams.get(REDIRECT_ASSET_DETAILS_KEY); @@ -59,7 +60,9 @@ const getNodeDetailSearch = (queryParams: URLSearchParams) => { }; export const getSearchParams = (nodeType: InventoryItemType, queryParams: URLSearchParams) => - nodeType === 'host' ? getHostDetailSearch(queryParams) : getNodeDetailSearch(queryParams); + Object.values(SupportedAssetTypes).includes(nodeType as SupportedAssetTypes) + ? getAssetDetailsQueryParams(queryParams) + : getNodeDetailSearch(queryParams); export const RedirectToNodeDetail = () => { const { diff --git a/x-pack/plugins/observability_solution/infra/public/plugin.ts b/x-pack/plugins/observability_solution/infra/public/plugin.ts index 6a4e813064eef..86d5e7816ce7a 100644 --- a/x-pack/plugins/observability_solution/infra/public/plugin.ts +++ b/x-pack/plugins/observability_solution/infra/public/plugin.ts @@ -23,6 +23,12 @@ import type { EmbeddableApiContext } from '@kbn/presentation-publishing'; import { apiCanAddNewPanel } from '@kbn/presentation-containers'; import { IncompatibleActionError, ADD_PANEL_TRIGGER } from '@kbn/ui-actions-plugin/public'; import { COMMON_EMBEDDABLE_GROUPING } from '@kbn/embeddable-plugin/public'; +import { + ASSET_DETAILS_LOCATOR_ID, + INVENTORY_LOCATOR_ID, + type AssetDetailsLocatorParams, + type InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import type { InfraPublicConfig } from '../common/plugin_config_types'; import { createInventoryMetricRuleType } from './alerting/inventory'; import { createLogThresholdRuleType } from './alerting/log_threshold'; @@ -80,12 +86,17 @@ export class Plugin implements InfraClientPluginClass { id: ObservabilityTriggerId.LogEntryContextMenu, }); + const assetDetailsLocator = + pluginsSetup.share.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const inventoryLocator = + pluginsSetup.share.url.locators.get(INVENTORY_LOCATOR_ID); + pluginsSetup.observability.observabilityRuleTypeRegistry.register( - createInventoryMetricRuleType() + createInventoryMetricRuleType({ assetDetailsLocator, inventoryLocator }) ); pluginsSetup.observability.observabilityRuleTypeRegistry.register( - createMetricThresholdRuleType() + createMetricThresholdRuleType({ assetDetailsLocator }) ); if (this.config.featureFlags.logsUIEnabled) { diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/common/utils.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/common/utils.ts index 0de0a5a0797b4..73a7ed749446e 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/common/utils.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/common/utils.ts @@ -21,7 +21,13 @@ import { set } from '@kbn/safer-lodash-set'; import { Alert } from '@kbn/alerts-as-data-utils'; import { type Group } from '@kbn/observability-alerting-rule-utils'; import { ParsedExperimentalFields } from '@kbn/rule-registry-plugin/common/parse_experimental_fields'; +import type { LocatorPublic } from '@kbn/share-plugin/common'; +import type { + AssetDetailsLocatorParams, + InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { + ALERT_RULE_PARAMETERS_NODE_TYPE, getInventoryViewInAppUrl, getMetricsViewInAppUrl, } from '../../../../common/alerting/metrics/alert_link'; @@ -130,6 +136,8 @@ export const getInventoryViewInAppUrlWithSpaceId = ({ spaceId, timestamp, hostName, + assetDetailsLocator, + inventoryLocator, }: { basePath: IBasePath; criteria: InventoryMetricConditions[]; @@ -137,6 +145,8 @@ export const getInventoryViewInAppUrlWithSpaceId = ({ spaceId: string; timestamp: string; hostName?: string; + assetDetailsLocator?: LocatorPublic; + inventoryLocator?: LocatorPublic; }) => { const { metric, customMetric } = criteria[0]; @@ -145,7 +155,7 @@ export const getInventoryViewInAppUrlWithSpaceId = ({ [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`]: [customMetric?.id], [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`]: [customMetric?.aggregation], [`${ALERT_RULE_PARAMETERS}.criteria.customMetric.field`]: [customMetric?.field], - [`${ALERT_RULE_PARAMETERS}.nodeType`]: [nodeType], + [ALERT_RULE_PARAMETERS_NODE_TYPE]: [nodeType], [TIMESTAMP]: timestamp, [HOST_NAME]: hostName, }; @@ -153,7 +163,11 @@ export const getInventoryViewInAppUrlWithSpaceId = ({ return addSpaceIdToPath( basePath.publicBaseUrl, spaceId, - getInventoryViewInAppUrl(parseTechnicalFields(fields, true)) + getInventoryViewInAppUrl({ + fields: parseTechnicalFields(fields, true), + assetDetailsLocator, + inventoryLocator, + }) ); }; @@ -161,22 +175,27 @@ export const getMetricsViewInAppUrlWithSpaceId = ({ basePath, spaceId, timestamp, - hostName, + groupBy, + assetDetailsLocator, }: { basePath: IBasePath; spaceId: string; timestamp: string; - hostName?: string; + groupBy?: string[]; + assetDetailsLocator?: LocatorPublic; }) => { const fields = { [TIMESTAMP]: timestamp, - [HOST_NAME]: hostName, }; return addSpaceIdToPath( basePath.publicBaseUrl, spaceId, - getMetricsViewInAppUrl(parseTechnicalFields(fields, true)) + getMetricsViewInAppUrl({ + fields: parseTechnicalFields(fields, true), + groupBy, + assetDetailsLocator, + }) ); }; diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts index f76a6e82e67d5..2f621d04f38df 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.test.ts @@ -23,6 +23,7 @@ import { InfraBackendLibs } from '../../infra_types'; import { infraPluginMock } from '../../../mocks'; import { logsSharedPluginMock } from '@kbn/logs-shared-plugin/server/mocks'; import { createLogSourcesServiceMock } from '@kbn/logs-data-access-plugin/common/services/log_sources_service/log_sources_service.mocks'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; jest.mock('./evaluate_condition', () => ({ evaluateCondition: jest.fn() })); @@ -136,6 +137,11 @@ const mockLibs = { publicBaseUrl: 'http://localhost:5601', prepend: (path: string) => path, }, + plugins: { + share: { + setup: sharePluginMock.createSetupContract(), + }, + }, logger, } as unknown as InfraBackendLibs; const alerts = new Map(); diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 80da1034df5ac..9f8b3b6d0bfa0 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -21,9 +21,20 @@ import { AlertInstanceState as AlertState, } from '@kbn/alerting-plugin/common'; import { AlertsClientError, RuleExecutorOptions, RuleTypeState } from '@kbn/alerting-plugin/server'; -import { convertToBuiltInComparators, getAlertUrl } from '@kbn/observability-plugin/common'; +import { + AlertsLocatorParams, + alertsLocatorID, + convertToBuiltInComparators, + getAlertUrl, +} from '@kbn/observability-plugin/common'; import type { InventoryItemType, SnapshotMetricType } from '@kbn/metrics-data-access-plugin/common'; import { ObservabilityMetricsAlert } from '@kbn/alerts-as-data-utils'; +import { + ASSET_DETAILS_LOCATOR_ID, + INVENTORY_LOCATOR_ID, + type AssetDetailsLocatorParams, + type InventoryLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { getOriginalActionGroup } from '../../../utils/get_original_action_group'; import { AlertStates, @@ -96,6 +107,13 @@ export const createInventoryMetricThresholdExecutor = getTimeRange, } = options; + const { share } = libs.plugins; + const alertsLocator = share.setup.url.locators.get(alertsLocatorID); + const assetDetailsLocator = + share.setup.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const inventoryLocator = + share.setup.url.locators.get(INVENTORY_LOCATOR_ID); + const startTime = Date.now(); const { criteria, filterQuery, sourceId = 'default', nodeType, alertOnNoData } = params; @@ -141,7 +159,7 @@ export const createInventoryMetricThresholdExecutor = uuid, spaceId, indexedStartedAt, - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), alertState: stateToAlertMessage[AlertStates.ERROR], @@ -156,6 +174,8 @@ export const createInventoryMetricThresholdExecutor = nodeType, timestamp: indexedStartedAt, spaceId, + assetDetailsLocator, + inventoryLocator, }), }, }); @@ -293,7 +313,7 @@ export const createInventoryMetricThresholdExecutor = uuid, spaceId, indexedStartedAt, - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), alertState: stateToAlertMessage[nextState], @@ -312,6 +332,8 @@ export const createInventoryMetricThresholdExecutor = timestamp: indexedStartedAt, spaceId, hostName: additionalContext?.host?.name, + assetDetailsLocator, + inventoryLocator, }), ...additionalContext, }; @@ -347,7 +369,7 @@ export const createInventoryMetricThresholdExecutor = alertUuid, spaceId, indexedStartedAt, - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), alertState: stateToAlertMessage[AlertStates.OK], @@ -362,6 +384,8 @@ export const createInventoryMetricThresholdExecutor = timestamp: indexedStartedAt, spaceId, hostName: additionalContext?.host?.name, + assetDetailsLocator, + inventoryLocator, }), originalAlertState: translateActionGroupToAlertState(originalActionGroup), originalAlertStateWasALERT: originalActionGroup === FIRED_ACTIONS_ID, diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 9b562e3d49143..44cd61943df49 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -31,6 +31,7 @@ import { ALERT_GROUP, } from '@kbn/rule-data-utils'; import { type Group } from '@kbn/observability-alerting-rule-utils'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; jest.mock('./lib/evaluate_rule', () => ({ evaluateRule: jest.fn() })); @@ -2473,6 +2474,11 @@ const mockLibs: any = { publicBaseUrl: 'http://localhost:5601', prepend: (path: string) => path, }, + plugins: { + share: { + setup: sharePluginMock.createSetupContract(), + }, + }, logger, }; diff --git a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index 4c0a19ae2e512..258a410d4775c 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -12,7 +12,7 @@ import { ALERT_GROUP, ALERT_REASON, } from '@kbn/rule-data-utils'; -import { isEqual } from 'lodash'; +import { castArray, isEqual } from 'lodash'; import { ActionGroupIdsOf, AlertInstanceContext as AlertContext, @@ -20,11 +20,20 @@ import { RecoveredActionGroup, } from '@kbn/alerting-plugin/common'; import { AlertsClientError, RuleExecutorOptions, RuleTypeState } from '@kbn/alerting-plugin/server'; -import { TimeUnitChar, getAlertUrl } from '@kbn/observability-plugin/common'; +import { + AlertsLocatorParams, + TimeUnitChar, + alertsLocatorID, + getAlertUrl, +} from '@kbn/observability-plugin/common'; import { ObservabilityMetricsAlert } from '@kbn/alerts-as-data-utils'; import { COMPARATORS } from '@kbn/alerting-comparators'; import { getEcsGroups, type Group } from '@kbn/observability-alerting-rule-utils'; import { convertToBuiltInComparators } from '@kbn/observability-plugin/common/utils/convert_legacy_outside_comparator'; +import { + ASSET_DETAILS_LOCATOR_ID, + AssetDetailsLocatorParams, +} from '@kbn/observability-shared-plugin/common'; import { getOriginalActionGroup } from '../../../utils/get_original_action_group'; import { AlertStates } from '../../../../common/alerting/metrics'; import { createFormatter } from '../../../../common/formatters'; @@ -111,6 +120,11 @@ export const createMetricThresholdExecutor = MetricThresholdAlert > ) => { + const { share } = libs.plugins; + const alertsLocator = share.setup.url.locators.get(alertsLocatorID); + const assetDetailsLocator = + share.setup.url.locators.get(ASSET_DETAILS_LOCATOR_ID); + const startTime = Date.now(); const { @@ -126,6 +140,8 @@ export const createMetricThresholdExecutor = const { criteria } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); + const groupBy = castArray(params.groupBy); + const logger = createScopedLogger(libs.logger, 'metricThresholdRule', { alertId: ruleId, executionId, @@ -167,7 +183,7 @@ export const createMetricThresholdExecutor = uuid, spaceId, start ?? startedAt.toISOString(), - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), }, @@ -203,6 +219,8 @@ export const createMetricThresholdExecutor = basePath: libs.basePath, spaceId, timestamp, + groupBy, + assetDetailsLocator, }), }; @@ -217,7 +235,7 @@ export const createMetricThresholdExecutor = state: { lastRunTimestamp: startedAt.valueOf(), missingGroups: [], - groupBy: params.groupBy, + groupBy, filterQuery: params.filterQuery, }, }; @@ -410,7 +428,8 @@ export const createMetricThresholdExecutor = basePath: libs.basePath, spaceId, timestamp, - hostName: additionalContext?.host?.name, + groupBy, + assetDetailsLocator, }), ...additionalContext, }; @@ -450,7 +469,7 @@ export const createMetricThresholdExecutor = alertUuid, spaceId, indexedStartedAt, - libs.alertsLocator, + alertsLocator, libs.basePath.publicBaseUrl ), alertState: stateToAlertMessage[AlertStates.OK], @@ -468,7 +487,8 @@ export const createMetricThresholdExecutor = basePath: libs.basePath, spaceId, timestamp: indexedStartedAt, - hostName: additionalContext?.host?.name, + groupBy, + assetDetailsLocator, }), originalAlertState: translateActionGroupToAlertState(originalActionGroup), @@ -486,7 +506,7 @@ export const createMetricThresholdExecutor = state: { lastRunTimestamp: startedAt.valueOf(), missingGroups: [...nextMissingGroups], - groupBy: params.groupBy, + groupBy, filterQuery: params.filterQuery, }, }; diff --git a/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_apm_data_access_client.ts b/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_apm_data_access_client.ts index 1936c59d7a637..e99d57eb4d6c8 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_apm_data_access_client.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/helpers/get_apm_data_access_client.ts @@ -28,12 +28,12 @@ export const getApmDataAccessClient = ({ request: KibanaRequest; }) => { const hasPrivileges = async () => { - const [, { apmDataAccess }] = await libs.getStartServices(); - return apmDataAccess.hasPrivileges({ request }); + const apmDataAccessStart = await libs.plugins.apmDataAccess.start(); + return apmDataAccessStart.hasPrivileges({ request }); }; const getServices = async () => { - const { apmDataAccess } = libs; + const apmDataAccess = libs.plugins.apmDataAccess.setup; const coreContext = await context.core; diff --git a/x-pack/plugins/observability_solution/infra/server/lib/infra_types.ts b/x-pack/plugins/observability_solution/infra/server/lib/infra_types.ts index 96c5cd9f311d7..f13424c6331d3 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/infra_types.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/infra_types.ts @@ -8,23 +8,30 @@ import type { Logger } from '@kbn/logging'; import type { IBasePath } from '@kbn/core/server'; import type { handleEsError } from '@kbn/es-ui-shared-plugin/server'; -import type { AlertsLocatorParams } from '@kbn/observability-plugin/common'; import { ObservabilityConfig } from '@kbn/observability-plugin/server'; -import type { LocatorPublic } from '@kbn/share-plugin/common'; import type { ILogsSharedLogEntriesDomain } from '@kbn/logs-shared-plugin/server'; -import type { ApmDataAccessPluginSetup } from '@kbn/apm-data-access-plugin/server'; import { RulesServiceSetup } from '../services/rules'; import { InfraConfig, InfraPluginStartServicesAccessor } from '../types'; import { KibanaFramework } from './adapters/framework/kibana_framework_adapter'; import { InfraMetricsDomain } from './domains/metrics_domain'; import { InfraSources } from './sources'; import { InfraSourceStatus } from './source_status'; +import type { InfraServerPluginSetupDeps, InfraServerPluginStartDeps } from './adapters/framework'; export interface InfraDomainLibs { logEntries: ILogsSharedLogEntriesDomain; metrics: InfraMetricsDomain; } +type Plugins = { + [key in keyof InfraServerPluginSetupDeps]: { + setup: Required[key]; + } & (key extends keyof InfraServerPluginStartDeps + ? { + start: () => Promise[key]>; + } + : {}); +}; export interface InfraBackendLibs extends InfraDomainLibs { basePath: IBasePath; configuration: InfraConfig; @@ -37,6 +44,5 @@ export interface InfraBackendLibs extends InfraDomainLibs { getStartServices: InfraPluginStartServicesAccessor; handleEsError: typeof handleEsError; logger: Logger; - alertsLocator?: LocatorPublic; - apmDataAccess: ApmDataAccessPluginSetup; + plugins: Plugins; } diff --git a/x-pack/plugins/observability_solution/infra/server/plugin.ts b/x-pack/plugins/observability_solution/infra/server/plugin.ts index 6e6f87776fbca..530dec8bc1ca4 100644 --- a/x-pack/plugins/observability_solution/infra/server/plugin.ts +++ b/x-pack/plugins/observability_solution/infra/server/plugin.ts @@ -16,9 +16,9 @@ import { import { handleEsError } from '@kbn/es-ui-shared-plugin/server'; import { i18n } from '@kbn/i18n'; import { Logger } from '@kbn/logging'; -import { alertsLocatorID } from '@kbn/observability-plugin/common'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { GetMetricIndicesOptions } from '@kbn/metrics-data-access-plugin/server'; +import { mapValues } from 'lodash'; import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants'; import { publicConfigKeys } from '../common/plugin_config_types'; import { LOGS_FEATURE, METRICS_FEATURE } from './features'; @@ -212,12 +212,24 @@ export class InfraServerPlugin metrics: new InfraMetricsDomain(new KibanaMetricsAdapter(framework)), }; + // Instead of passing plugins individually to `libs` on a necessity basis, + // this provides an object with all plugins infra depends on + const libsPlugins = mapValues(plugins, (value, key) => { + return { + setup: value, + start: () => + core.getStartServices().then((services) => { + const [, pluginsStartContracts] = services; + return pluginsStartContracts[key as keyof InfraServerPluginStartDeps]; + }), + }; + }) as InfraBackendLibs['plugins']; + this.libs = { configuration: this.config, framework, sources, sourceStatus, - apmDataAccess: plugins.apmDataAccess, ...domainLibs, handleEsError, logsRules: this.logsRules.setup(core, plugins), @@ -226,7 +238,7 @@ export class InfraServerPlugin getAlertDetailsConfig: () => plugins.observability.getAlertDetailsConfig(), logger: this.logger, basePath: core.http.basePath, - alertsLocator: plugins.share.url.locators.get(alertsLocatorID), + plugins: libsPlugins, }; plugins.features.registerKibanaFeature(METRICS_FEATURE); diff --git a/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts b/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts index e962a11f9a396..86af345d5175e 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts @@ -39,7 +39,7 @@ export const initServicesRoute = (libs: InfraBackendLibs) => { const client = createSearchClient(requestContext, framework, request); const soClient = savedObjects.getScopedClient(request); - const apmIndices = await libs.apmDataAccess.getApmIndices(soClient); + const apmIndices = await libs.plugins.apmDataAccess.setup.getApmIndices(soClient); const services = await getServices(client, apmIndices, { from, to, diff --git a/x-pack/plugins/observability_solution/metrics_data_access/common/index.ts b/x-pack/plugins/observability_solution/metrics_data_access/common/index.ts index 12a4b6c4e13ce..b0f801d2613c1 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/common/index.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/common/index.ts @@ -11,6 +11,7 @@ export { getFieldByType, findInventoryFields, metrics, + type InventoryModels, } from './inventory_models'; export { podSnapshotMetricTypes } from './inventory_models/kubernetes/pod'; diff --git a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/host/index.ts b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/host/index.ts index d77f15ad4ca38..731a84f1e83ab 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/host/index.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/host/index.ts @@ -56,4 +56,5 @@ export const host: InventoryModel = { ...nginxRequireMetrics, ], tooltipMetrics: ['cpuV2', 'memory', 'txV2', 'rxV2', 'cpu', 'tx', 'rx'], + legacyMetrics: ['cpu', 'tx', 'rx'], }; diff --git a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/index.ts b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/index.ts index 41115a95405e2..7dddfab593784 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/index.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/index.ts @@ -29,7 +29,7 @@ const catalog = { export const inventoryModels = Object.values(catalog); -type InventoryModels = (typeof catalog)[T]; +export type InventoryModels = (typeof catalog)[T]; export const findInventoryModel = (type: T): InventoryModels => { const model = inventoryModels.find((m) => m.id === type); diff --git a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/types.ts b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/types.ts index cc018b24eea13..042958ef7bd59 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/types.ts +++ b/x-pack/plugins/observability_solution/metrics_data_access/common/inventory_models/types.ts @@ -423,6 +423,7 @@ export interface InventoryModel { }; metrics: TMetrics; requiredMetrics: InventoryMetric[]; + legacyMetrics?: SnapshotMetricType[]; tooltipMetrics: SnapshotMetricType[]; nodeFilter?: object[]; } diff --git a/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/formatters/snapshot_metric_formats.ts b/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/formatters/snapshot_metric_formats.ts index 1715a28b1caab..81586bf25ed78 100644 --- a/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/formatters/snapshot_metric_formats.ts +++ b/x-pack/plugins/observability_solution/observability/common/custom_threshold_rule/formatters/snapshot_metric_formats.ts @@ -29,12 +29,18 @@ export const METRIC_FORMATTERS: MetricFormatters = { formatter: InfraFormatterType.percent, template: '{{value}}', }, + ['cpuV2']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, ['memory']: { formatter: InfraFormatterType.percent, template: '{{value}}', }, ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['rxV2']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['txV2']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, ['logRate']: { formatter: InfraFormatterType.abbreviatedNumber, template: '{{value}}/s', diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts index 59729aeb71f0e..ca44baa6de6e1 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/asset_details_locator.ts @@ -13,7 +13,7 @@ export type AssetDetailsLocator = LocatorPublic; export interface AssetDetailsLocatorParams extends SerializableRecord { assetType: string; assetId: string; - state?: SerializableRecord; + // asset types not migrated to use the asset details page _a?: { time?: { from?: string; @@ -23,11 +23,13 @@ export interface AssetDetailsLocatorParams extends SerializableRecord { }; assetDetails?: { tabId?: string; + name?: string; dashboardId?: string; dateRange?: { from: string; to: string; }; + alertMetric?: string; }; } @@ -36,12 +38,23 @@ export const ASSET_DETAILS_LOCATOR_ID = 'ASSET_DETAILS_LOCATOR'; export class AssetDetailsLocatorDefinition implements LocatorDefinition { public readonly id = ASSET_DETAILS_LOCATOR_ID; - public readonly getLocation = async (params: AssetDetailsLocatorParams) => { - const searchPath = rison.encodeUnknown(params._a); - const assetDetails = rison.encodeUnknown(params.assetDetails); + public readonly getLocation = async ( + params: AssetDetailsLocatorParams & { state?: SerializableRecord } + ) => { + const legacyNodeDetailsQueryParams = rison.encodeUnknown(params._a); + const assetDetailsQueryParams = rison.encodeUnknown(params.assetDetails); + + const queryParams = []; + if (assetDetailsQueryParams !== undefined) { + queryParams.push(`assetDetails=${assetDetailsQueryParams}`); + } + if (legacyNodeDetailsQueryParams !== undefined) { + queryParams.push(`_a=${legacyNodeDetailsQueryParams}`); + } + return { app: 'metrics', - path: `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}&_a=${searchPath}`, + path: `/detail/${params.assetType}/${params.assetId}?${queryParams.join('&')}`, state: params.state ? params.state : {}, }; }; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/inventory_locator.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/inventory_locator.ts index ca6e997468b5b..9f4cd58188edb 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/inventory_locator.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/inventory_locator.ts @@ -40,12 +40,12 @@ export interface InventoryLocatorParams extends SerializableRecord { metric: string; // encoded value nodeType: string; region?: string; - sort: { + sort?: { by: string; direction: 'desc' | 'async'; }; - timelineOpen: boolean; - view: 'map' | 'table'; + timelineOpen?: boolean; + view?: 'map' | 'table'; state?: SerializableRecord; } diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts index c7b5e16625e03..8c7dc0d4b6113 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/infra/locators.test.ts @@ -60,7 +60,7 @@ describe('Infra Locators', () => { expect(app).toBe('metrics'); expect(path).toBe( - `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}&_a=undefined` + `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}` ); expect(state).toBeDefined(); expect(Object.keys(state)).toHaveLength(0); @@ -72,7 +72,7 @@ describe('Infra Locators', () => { expect(app).toBe('metrics'); expect(path).toBe( - `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}&_a=undefined` + `/detail/${params.assetType}/${params.assetId}?assetDetails=${assetDetails}` ); expect(state).toBeDefined(); expect(Object.keys(state)).toHaveLength(0); diff --git a/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts b/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts index 8773ace90c71f..2ac6a69c6a0d9 100644 --- a/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts @@ -19,6 +19,7 @@ import { BehaviorSubject } from 'rxjs'; import { createLazyObservabilityPageTemplate } from './components/page_template'; import { createNavigationRegistry } from './components/page_template/helpers/navigation_registry'; import { registerProfilingComponent } from './components/profiling/helpers/component_registry'; +export { updateGlobalNavigation } from './services/update_global_navigation'; import { AssetDetailsFlyoutLocatorDefinition, AssetDetailsLocatorDefinition, diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.test.tsx index c1a57c6a9ab77..355ad1f9129d7 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.test.tsx @@ -8,12 +8,12 @@ import React from 'react'; import { render } from '@testing-library/react'; import { InvestigationGuideView } from './investigation_guide_view'; -import type { GetBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import type { UseBasicDataFromDetailsDataResult } from '../../../flyout/document_details/shared/hooks/use_basic_data_from_details_data'; const defaultProps = { basicData: { ruleId: 'rule-id', - } as unknown as GetBasicDataFromDetailsData, + } as unknown as UseBasicDataFromDetailsDataResult, ruleNote: 'test note', }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx index 29e2354f7454a..b3015bafe4536 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/investigation_guide_view.tsx @@ -8,7 +8,7 @@ import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; import React, { createContext } from 'react'; import styled from 'styled-components'; -import type { GetBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import type { UseBasicDataFromDetailsDataResult } from '../../../flyout/document_details/shared/hooks/use_basic_data_from_details_data'; import * as i18n from './translations'; import { MarkdownRenderer } from '../markdown_editor'; import { LineClamp } from '../line_clamp'; @@ -18,13 +18,13 @@ export const Indent = styled.div` word-break: break-word; `; -export const BasicAlertDataContext = createContext>({}); +export const BasicAlertDataContext = createContext>({}); interface InvestigationGuideViewProps { /** * An object of basic fields from the event details data */ - basicData: GetBasicDataFromDetailsData; + basicData: UseBasicDataFromDetailsDataResult; /** * The markdown text of rule.note */ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/isolate_host/content.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/isolate_host/content.tsx index 658624dfdbbf4..0c9f05391d82a 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/isolate_host/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/isolate_host/content.tsx @@ -9,7 +9,7 @@ import type { FC } from 'react'; import React, { useCallback } from 'react'; import { useExpandableFlyoutApi } from '@kbn/expandable-flyout'; import { DocumentDetailsRightPanelKey } from '../shared/constants/panel_keys'; -import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../shared/hooks/use_basic_data_from_details_data'; import { EndpointIsolateSuccess, HostIsolationPanel, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/cell_actions.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/cell_actions.tsx index 322568c6a53f9..173520ce2d55e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/components/cell_actions.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/components/cell_actions.tsx @@ -9,7 +9,7 @@ import type { FC } from 'react'; import React, { useMemo } from 'react'; import { useDocumentDetailsContext } from '../../shared/context'; import { getSourcererScopeId } from '../../../../helpers'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { SecurityCellActionType } from '../../../../app/actions/constants'; import { CellActionsMode, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts index 16e1c5ffe8df9..430eb52b09511 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.test.ts @@ -15,9 +15,9 @@ import { useDocumentDetailsContext } from '../../shared/context'; import { useInvestigationTimeEnrichment } from '../../shared/hooks/use_investigation_enrichment'; import type { RouteSpyState } from '../../../../common/utils/route/types'; import { - type GetBasicDataFromDetailsData, + type UseBasicDataFromDetailsDataResult, useBasicDataFromDetailsData, -} from '../../../../timelines/components/side_panel/event_details/helpers'; +} from '../../shared/hooks/use_basic_data_from_details_data'; import { mockContextValue } from '../../shared/mocks/mock_context'; jest.mock('../../../../timelines/containers/details'); @@ -25,7 +25,7 @@ jest.mock('../../../../sourcerer/containers'); jest.mock('../../../../common/utils/route/use_route_spy'); jest.mock('../../shared/context'); jest.mock('../../shared/hooks/use_investigation_enrichment'); -jest.mock('../../../../timelines/components/side_panel/event_details/helpers'); +jest.mock('../../shared/hooks/use_basic_data_from_details_data'); describe('useThreatIntelligenceDetails', () => { beforeEach(() => { @@ -42,7 +42,7 @@ describe('useThreatIntelligenceDetails', () => { jest .mocked(useBasicDataFromDetailsData) - .mockReturnValue({ isAlert: true } as unknown as GetBasicDataFromDetailsData); + .mockReturnValue({ isAlert: true } as unknown as UseBasicDataFromDetailsDataResult); jest.mocked(useSourcererDataView).mockReturnValue({ browserFields: {}, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.ts b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.ts index 15c934718981b..a7b8256b502f5 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/left/hooks/use_threat_intelligence_details.ts @@ -9,7 +9,7 @@ import { useMemo } from 'react'; import { SecurityPageName } from '@kbn/deeplinks-security'; import type { RunTimeMappings } from '../../../../../common/api/search_strategy'; import type { CtiEnrichment, EventFields } from '../../../../../common/search_strategy'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { filterDuplicateEnrichments, getEnrichmentFields, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx index b908185cd9d9d..ca01ac08d66a5 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_description.tsx @@ -15,7 +15,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../common/lib/kibana'; import { useDocumentDetailsContext } from '../../shared/context'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { ALERT_DESCRIPTION_DETAILS_TEST_ID, ALERT_DESCRIPTION_TITLE_TEST_ID, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx index b7625075b98da..8d3b0577230a8 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/alert_header_title.tsx @@ -15,7 +15,7 @@ import { DocumentStatus } from './status'; import { DocumentSeverity } from './severity'; import { RiskScore } from './risk_score'; import { useRefetchByScope } from '../hooks/use_refetch_by_scope'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { useDocumentDetailsContext } from '../../shared/context'; import { PreferenceFormattedDate } from '../../../../common/components/formatted_date'; import { FLYOUT_ALERT_HEADER_TITLE_TEST_ID, ALERT_SUMMARY_PANEL_TEST_ID } from './test_ids'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx index 4bed17e24b77e..953a2371ffa88 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/event_header_title.tsx @@ -11,7 +11,7 @@ import { EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FlyoutTitle } from '../../../shared/components/flyout_title'; import { DocumentSeverity } from './severity'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { useDocumentDetailsContext } from '../../shared/context'; import { PreferenceFormattedDate } from '../../../../common/components/formatted_date'; import { FLYOUT_EVENT_HEADER_TITLE_TEST_ID } from './test_ids'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx index 00fbd9303c332..f90d67f87f37e 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/header_actions.tsx @@ -11,7 +11,7 @@ import { EuiButtonIcon, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@ import { i18n } from '@kbn/i18n'; import { NewChatByTitle } from '@kbn/elastic-assistant'; import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { useAssistant } from '../hooks/use_assistant'; import { ALERT_SUMMARY_CONVERSATION_ID, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx index 22e5b65bdade8..32e170bf757d4 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/highlighted_fields.tsx @@ -12,7 +12,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiPanel, EuiTitle } from import { FormattedMessage } from '@kbn/i18n-react'; import { convertHighlightedFieldsToTableRow } from '../../shared/utils/highlighted_fields_helpers'; import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { HighlightedFieldsCell } from './highlighted_fields_cell'; import { CellActions } from './cell_actions'; import { HIGHLIGHTED_FIELDS_DETAILS_TEST_ID, HIGHLIGHTED_FIELDS_TITLE_TEST_ID } from './test_ids'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx index 7f137dc1815c8..d97821428745f 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/investigation_section.test.tsx @@ -21,9 +21,11 @@ import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_f import { TestProvider } from '@kbn/expandable-flyout/src/test/provider'; import { mockContextValue } from '../../shared/mocks/mock_context'; import { useExpandSection } from '../hooks/use_expand_section'; +import { useHighlightedFields } from '../../shared/hooks/use_highlighted_fields'; jest.mock('../../../../detection_engine/rule_management/logic/use_rule_with_fallback'); jest.mock('../hooks/use_expand_section'); +jest.mock('../../shared/hooks/use_highlighted_fields'); const panelContextValue = { ...mockContextValue, @@ -65,6 +67,7 @@ describe('', () => { it('should render the component expanded if value is true in local storage', () => { (useExpandSection as jest.Mock).mockReturnValue(true); + (useHighlightedFields as jest.Mock).mockReturnValue([]); const { getByTestId } = renderInvestigationSection(); expect(getByTestId(INVESTIGATION_SECTION_CONTENT_TEST_ID)).toBeVisible(); @@ -72,6 +75,7 @@ describe('', () => { it('should render investigation guide and highlighted fields when document is signal', () => { (useExpandSection as jest.Mock).mockReturnValue(true); + (useHighlightedFields as jest.Mock).mockReturnValue([]); const { getByTestId } = renderInvestigationSection(); expect(getByTestId(INVESTIGATION_GUIDE_TEST_ID)).toBeInTheDocument(); @@ -80,6 +84,7 @@ describe('', () => { it('should not render investigation guide when document is not signal', () => { (useExpandSection as jest.Mock).mockReturnValue(true); + (useHighlightedFields as jest.Mock).mockReturnValue([]); const mockGetFieldsData = (field: string) => { switch (field) { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx index 1ce7e9ed53949..c4b0e6e26a820 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/components/reason.tsx @@ -20,7 +20,7 @@ import { REASON_DETAILS_TEST_ID, REASON_TITLE_TEST_ID, } from './test_ids'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { useDocumentDetailsContext } from '../../shared/context'; export const ALERT_REASON_BANNER = { diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/header.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/header.test.tsx index 8130174ebbda6..97a15becda649 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/header.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/header.test.tsx @@ -11,12 +11,12 @@ import { renderReactTestingLibraryWithI18n as render } from '@kbn/test-jest-help import { PanelHeader } from './header'; import { allThreeTabs } from './hooks/use_tabs'; import { GuidedOnboardingTourStep } from '../../../common/components/guided_onboarding_tour/tour_step'; -import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../shared/hooks/use_basic_data_from_details_data'; jest.mock('../shared/context', () => ({ useDocumentDetailsContext: jest.fn().mockReturnValue({ dataFormattedForFieldBrowser: [] }), })); -jest.mock('../../../timelines/components/side_panel/event_details/helpers', () => ({ +jest.mock('../shared/hooks/use_basic_data_from_details_data', () => ({ useBasicDataFromDetailsData: jest.fn(), })); jest.mock('../../../common/components/guided_onboarding_tour/tour_step', () => ({ diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/header.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/header.tsx index b327fccea3bed..3bf4e1a741251 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/header.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/header.tsx @@ -16,7 +16,7 @@ import { FlyoutHeaderTabs } from '../../shared/components/flyout_header_tabs'; import { AlertHeaderTitle } from './components/alert_header_title'; import { EventHeaderTitle } from './components/event_header_title'; import { useDocumentDetailsContext } from '../shared/context'; -import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../shared/hooks/use_basic_data_from_details_data'; import { AlertsCasesTourSteps, getTourAnchor, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx index 13b6dc506d399..3cecf2b0acfe5 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx @@ -81,7 +81,11 @@ describe('useAssistant', () => { expect(await getPromptContext()).toEqual({ '@timestamp': ['2023-01-01T01:01:01.000Z'], + _id: ['_id'], + _index: ['index'], + 'agent.id': ['agent.id'], 'event.category': ['registry'], + 'host.name': ['host-name'], 'kibana.alert.ancestors.id': ['ancestors-id'], 'kibana.alert.rule.description': ['rule-description'], 'kibana.alert.rule.indices': ['rule-indices'], @@ -89,8 +93,10 @@ describe('useAssistant', () => { 'kibana.alert.rule.parameters.index': ['rule-parameters-index'], 'kibana.alert.rule.type': ['query'], 'kibana.alert.rule.uuid': ['rule-uuid'], + 'kibana.alert.url': ['alert-url'], 'kibana.alert.workflow_status': ['open'], 'process.entity_id': ['process-entity_id'], + 'user.name': ['user-name'], }); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_threat_intelligence.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_threat_intelligence.ts index ac59f6c802a87..7f9f26cb89da1 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_threat_intelligence.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_fetch_threat_intelligence.ts @@ -9,7 +9,7 @@ import { useMemo } from 'react'; import { groupBy } from 'lodash'; import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import type { CtiEnrichment } from '../../../../../common/search_strategy'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; import { filterDuplicateEnrichments, getEnrichmentFields, diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx index 28985471a3948..64e10766ad21b 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.test.tsx @@ -45,7 +45,7 @@ describe('useSessionPreview', () => { expect(hookResult.result.current).toEqual({ index: 'kibana.alert.ancestors.index', - investigatedAlertId: 'id', + investigatedAlertId: '_id', jumpToCursor: '2023-01-01T00:00:00.000Z', jumpToEntityId: 'process.entity_id', sessionEntityId: 'process.entry_leader.entity_id', @@ -79,8 +79,8 @@ describe('useSessionPreview', () => { }); expect(hookResult.result.current).toEqual({ - index: '.some-index', - investigatedAlertId: 'id', + index: 'index', + investigatedAlertId: '_id', jumpToCursor: '2023-01-01T00:00:00.000Z', jumpToEntityId: 'process.entity_id', sessionEntityId: 'process.entry_leader.entity_id', diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts index ec53a6a2e00f7..95c79e6815bfd 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/right/hooks/use_session_preview.ts @@ -9,7 +9,7 @@ import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; import type { SessionViewConfig } from '@kbn/securitysolution-data-table/common/types'; import type { GetFieldsData } from '../../../../common/hooks/use_get_fields_data'; import { getField } from '../../shared/utils'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from '../../shared/hooks/use_basic_data_from_details_data'; export interface UseSessionPreviewParams { /** diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx index 1197e39ad86cb..bdfae953303c6 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/context.tsx @@ -13,7 +13,7 @@ import { useEventDetails } from './hooks/use_event_details'; import { FlyoutError } from '../../shared/components/flyout_error'; import { FlyoutLoading } from '../../shared/components/flyout_loading'; import type { SearchHit } from '../../../../common/search_strategy'; -import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from './hooks/use_basic_data_from_details_data'; import type { DocumentDetailsProps } from './types'; import type { GetFieldsData } from '../../../common/hooks/use_get_fields_data'; import { useRuleWithFallback } from '../../../detection_engine/rule_management/logic/use_rule_with_fallback'; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.test.tsx new file mode 100644 index 0000000000000..b4cd7c35824a1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.test.tsx @@ -0,0 +1,48 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useBasicDataFromDetailsData } from './use_basic_data_from_details_data'; +import { mockDataFormattedForFieldBrowser } from '../mocks/mock_data_formatted_for_field_browser'; + +describe('useBasicDataFromDetailsData', () => { + it('should return all empty properties', () => { + const hookResult = renderHook(() => useBasicDataFromDetailsData(null)); + + expect(hookResult.result.current.agentId).toEqual(''); + expect(hookResult.result.current.alertId).toEqual(''); + expect(hookResult.result.current.alertUrl).toEqual(''); + expect(hookResult.result.current.data).toEqual(null); + expect(hookResult.result.current.hostName).toEqual(''); + expect(hookResult.result.current.indexName).toEqual(''); + expect(hookResult.result.current.isAlert).toEqual(false); + expect(hookResult.result.current.ruleDescription).toEqual(''); + expect(hookResult.result.current.ruleId).toEqual(''); + expect(hookResult.result.current.ruleName).toEqual(''); + expect(hookResult.result.current.timestamp).toEqual(''); + expect(hookResult.result.current.userName).toEqual(''); + }); + + it('should return all properties', () => { + const hookResult = renderHook(() => + useBasicDataFromDetailsData(mockDataFormattedForFieldBrowser) + ); + + expect(hookResult.result.current.agentId).toEqual('agent.id'); + expect(hookResult.result.current.alertId).toEqual('_id'); + expect(hookResult.result.current.alertUrl).toEqual('alert-url'); + expect(hookResult.result.current.data).toEqual(mockDataFormattedForFieldBrowser); + expect(hookResult.result.current.hostName).toEqual('host-name'); + expect(hookResult.result.current.indexName).toEqual('index'); + expect(hookResult.result.current.isAlert).toEqual(true); + expect(hookResult.result.current.ruleDescription).toEqual('rule-description'); + expect(hookResult.result.current.ruleId).toEqual('rule-uuid'); + expect(hookResult.result.current.ruleName).toEqual('rule-name'); + expect(hookResult.result.current.timestamp).toEqual('2023-01-01T01:01:01.000Z'); + expect(hookResult.result.current.userName).toEqual('user-name'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.tsx new file mode 100644 index 0000000000000..9eee4eb29b593 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_basic_data_from_details_data.tsx @@ -0,0 +1,120 @@ +/* + * 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 { some } from 'lodash/fp'; +import { useMemo } from 'react'; +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import { getAlertDetailsFieldValue } from '../../../../common/lib/endpoint/utils/get_event_details_field_values'; + +export interface UseBasicDataFromDetailsDataResult { + agentId: string; + alertId: string; + alertUrl: string; + data: TimelineEventsDetailsItem[] | null; + hostName: string; + indexName: string; + isAlert: boolean; + ruleDescription: string; + ruleId: string; + ruleName: string; + timestamp: string; + userName: string; +} + +export const useBasicDataFromDetailsData = ( + data: TimelineEventsDetailsItem[] | null +): UseBasicDataFromDetailsDataResult => { + const agentId = useMemo( + () => getAlertDetailsFieldValue({ category: 'agent', field: 'agent.id' }, data), + [data] + ); + + const alertId = useMemo( + () => getAlertDetailsFieldValue({ category: '_id', field: '_id' }, data), + [data] + ); + + const alertUrl = useMemo( + () => getAlertDetailsFieldValue({ category: 'kibana', field: 'kibana.alert.url' }, data), + [data] + ); + + const hostName = useMemo( + () => getAlertDetailsFieldValue({ category: 'host', field: 'host.name' }, data), + [data] + ); + + const indexName = useMemo( + () => getAlertDetailsFieldValue({ category: '_index', field: '_index' }, data), + [data] + ); + + const isAlert = some({ category: 'kibana', field: 'kibana.alert.rule.uuid' }, data); + + const ruleDescription = useMemo( + () => + getAlertDetailsFieldValue( + { category: 'kibana', field: 'kibana.alert.rule.description' }, + data + ), + [data] + ); + + const ruleId = useMemo( + () => + isAlert + ? getAlertDetailsFieldValue({ category: 'kibana', field: 'kibana.alert.rule.uuid' }, data) + : getAlertDetailsFieldValue({ category: 'signal', field: 'signal.rule.id' }, data), + [isAlert, data] + ); + + const ruleName = useMemo( + () => getAlertDetailsFieldValue({ category: 'kibana', field: 'kibana.alert.rule.name' }, data), + [data] + ); + + const timestamp = useMemo( + () => getAlertDetailsFieldValue({ category: 'base', field: '@timestamp' }, data), + [data] + ); + + const userName = useMemo( + () => getAlertDetailsFieldValue({ category: 'user', field: 'user.name' }, data), + [data] + ); + + return useMemo( + () => ({ + agentId, + alertId, + alertUrl, + data, + hostName, + indexName, + isAlert, + ruleDescription, + ruleId, + ruleName, + timestamp, + userName, + }), + [ + agentId, + alertId, + alertUrl, + data, + hostName, + indexName, + isAlert, + ruleDescription, + ruleId, + ruleName, + timestamp, + userName, + ] + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx index 2c56b3d67d82f..6eb8c242c79fe 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_highlighted_fields.test.tsx @@ -23,9 +23,15 @@ describe('useHighlightedFields', () => { it('should return data', () => { const hookResult = renderHook(() => useHighlightedFields({ dataFormattedForFieldBrowser })); expect(hookResult.result.current).toEqual({ + 'host.name': { + values: ['host-name'], + }, 'kibana.alert.rule.type': { values: ['query'], }, + 'user.name': { + values: ['user-name'], + }, }); }); @@ -63,9 +69,15 @@ describe('useHighlightedFields', () => { ); expect(hookResult.result.current).toEqual({ + 'host.name': { + values: ['host-name'], + }, 'kibana.alert.rule.type': { values: ['query'], }, + 'user.name': { + values: ['user-name'], + }, }); }); @@ -93,11 +105,17 @@ describe('useHighlightedFields', () => { ); expect(hookResult.result.current).toEqual({ + 'host.name': { + values: ['host-name'], + }, 'kibana.alert.rule.type': { values: ['query'], }, 'agent.id': { - values: ['deb35a20-70f8-458e-a64a-c9e6f7575893'], + values: ['agent.id'], + }, + 'user.name': { + values: ['user-name'], }, }); }); @@ -121,9 +139,15 @@ describe('useHighlightedFields', () => { ); expect(hookResult.result.current).toEqual({ + 'host.name': { + values: ['host-name'], + }, 'kibana.alert.rule.type': { values: ['query'], }, + 'user.name': { + values: ['user-name'], + }, }); }); @@ -143,9 +167,15 @@ describe('useHighlightedFields', () => { ); expect(hookResult.result.current).toEqual({ + 'host.name': { + values: ['host-name'], + }, 'kibana.alert.rule.type': { values: ['query'], }, + 'user.name': { + values: ['user-name'], + }, }); }); @@ -175,12 +205,18 @@ describe('useHighlightedFields', () => { ); expect(hookResult.result.current).toEqual({ + 'host.name': { + values: ['host-name'], + }, 'kibana.alert.rule.type': { values: ['query'], }, [agentIdField]: { values: ['deb35a20-70f8-458e-a64a-c9e6f7575893'], }, + 'user.name': { + values: ['user-name'], + }, }); } ); @@ -209,12 +245,18 @@ describe('useHighlightedFields', () => { ); expect(hookResult.result.current).toEqual({ + 'host.name': { + values: ['host-name'], + }, 'kibana.alert.rule.type': { values: ['query'], }, 'device.id': { values: ['expectedCrowdstrikeAgentId'], }, + 'user.name': { + values: ['user-name'], + }, }); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.test.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.test.ts index aef75b40f1990..f7e3a40e60c40 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.test.ts @@ -11,12 +11,12 @@ import type { UseInvestigationGuideParams, UseInvestigationGuideResult, } from './use_investigation_guide'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import { useBasicDataFromDetailsData } from './use_basic_data_from_details_data'; import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback'; import { mockDataFormattedForFieldBrowser } from '../mocks/mock_data_formatted_for_field_browser'; import { useInvestigationGuide } from './use_investigation_guide'; -jest.mock('../../../../timelines/components/side_panel/event_details/helpers'); +jest.mock('./use_basic_data_from_details_data'); jest.mock('../../../../detection_engine/rule_management/logic/use_rule_with_fallback'); const dataFormattedForFieldBrowser = mockDataFormattedForFieldBrowser; diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.ts index 306cdbbb5d63d..1de48b26f25b7 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/hooks/use_investigation_guide.ts @@ -6,8 +6,8 @@ */ import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; -import type { GetBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; -import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers'; +import type { UseBasicDataFromDetailsDataResult } from './use_basic_data_from_details_data'; +import { useBasicDataFromDetailsData } from './use_basic_data_from_details_data'; import { useRuleWithFallback } from '../../../../detection_engine/rule_management/logic/use_rule_with_fallback'; export interface UseInvestigationGuideParams { @@ -27,11 +27,11 @@ export interface UseInvestigationGuideResult { */ error: unknown; /** - * + * The basic alert fields and their value */ - basicAlertData: GetBasicDataFromDetailsData; + basicAlertData: UseBasicDataFromDetailsDataResult; /** - * + * The note from the rule */ ruleNote: string | undefined; } diff --git a/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_data_formatted_for_field_browser.ts b/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_data_formatted_for_field_browser.ts index b097215b72c27..d96ac6dfe01dc 100644 --- a/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_data_formatted_for_field_browser.ts +++ b/x-pack/plugins/security_solution/public/flyout/document_details/shared/mocks/mock_data_formatted_for_field_browser.ts @@ -16,6 +16,20 @@ export const ruleTypeField: TimelineEventsDetailsItem = { }; export const baseFields: TimelineEventsDetailsItem[] = [ + { + category: 'agent', + field: 'agent.id', + values: ['agent.id'], + originalValue: ['agent.id'], + isObjectArray: false, + }, + { + category: '_id', + field: '_id', + values: ['_id'], + originalValue: ['_id'], + isObjectArray: false, + }, { category: 'base', field: '@timestamp', @@ -65,6 +79,13 @@ export const baseFields: TimelineEventsDetailsItem[] = [ originalValue: ['rule-parameters-index'], isObjectArray: false, }, + { + category: 'kibana', + field: 'kibana.alert.url', + values: ['alert-url'], + originalValue: ['alert-url'], + isObjectArray: false, + }, { category: 'kibana', field: 'kibana.alert.rule.uuid', @@ -86,6 +107,27 @@ export const baseFields: TimelineEventsDetailsItem[] = [ originalValue: ['process-entity_id'], isObjectArray: false, }, + { + category: 'host', + field: 'host.name', + values: ['host-name'], + originalValue: ['host-name'], + isObjectArray: false, + }, + { + category: 'user', + field: 'user.name', + values: ['user-name'], + originalValue: ['user-name'], + isObjectArray: false, + }, + { + category: '_index', + field: '_index', + values: ['index'], + originalValue: ['index'], + isObjectArray: false, + }, ]; /** diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx index acfdde85b6826..fc1941b6824c2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/helpers.tsx @@ -5,121 +5,8 @@ * 2.0. */ -import { some } from 'lodash/fp'; -import { useMemo } from 'react'; -import { getAlertDetailsFieldValue } from '../../../../common/lib/endpoint/utils/get_event_details_field_values'; -import type { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; import { DEFAULT_ALERTS_INDEX, DEFAULT_PREVIEW_INDEX } from '../../../../../common/constants'; -export interface GetBasicDataFromDetailsData { - agentId?: string; - alertId: string; - alertUrl?: string; - data: TimelineEventsDetailsItem[] | null; - hostName: string; - indexName?: string; - isAlert: boolean; - ruleDescription: string; - ruleId: string; - ruleName: string; - timestamp: string; - userName: string; -} - -export const useBasicDataFromDetailsData = ( - data: TimelineEventsDetailsItem[] | null -): GetBasicDataFromDetailsData => { - const isAlert = some({ category: 'kibana', field: 'kibana.alert.rule.uuid' }, data); - - const ruleId = useMemo( - () => - isAlert - ? getAlertDetailsFieldValue({ category: 'kibana', field: 'kibana.alert.rule.uuid' }, data) - : getAlertDetailsFieldValue({ category: 'signal', field: 'signal.rule.id' }, data), - [isAlert, data] - ); - - const ruleName = useMemo( - () => getAlertDetailsFieldValue({ category: 'kibana', field: 'kibana.alert.rule.name' }, data), - [data] - ); - - const ruleDescription = useMemo( - () => - getAlertDetailsFieldValue( - { category: 'kibana', field: 'kibana.alert.rule.description' }, - data - ), - [data] - ); - - const alertId = useMemo( - () => getAlertDetailsFieldValue({ category: '_id', field: '_id' }, data), - [data] - ); - - const indexName = useMemo( - () => getAlertDetailsFieldValue({ category: '_index', field: '_index' }, data), - [data] - ); - - const alertUrl = useMemo( - () => getAlertDetailsFieldValue({ category: 'kibana', field: 'kibana.alert.url' }, data), - [data] - ); - - const agentId = useMemo( - () => getAlertDetailsFieldValue({ category: 'agent', field: 'agent.id' }, data), - [data] - ); - - const hostName = useMemo( - () => getAlertDetailsFieldValue({ category: 'host', field: 'host.name' }, data), - [data] - ); - - const userName = useMemo( - () => getAlertDetailsFieldValue({ category: 'user', field: 'user.name' }, data), - [data] - ); - - const timestamp = useMemo( - () => getAlertDetailsFieldValue({ category: 'base', field: '@timestamp' }, data), - [data] - ); - - return useMemo( - () => ({ - agentId, - alertId, - alertUrl, - data, - hostName, - indexName, - isAlert, - ruleDescription, - ruleId, - ruleName, - timestamp, - userName, - }), - [ - agentId, - alertId, - alertUrl, - data, - hostName, - indexName, - isAlert, - ruleDescription, - ruleId, - ruleName, - timestamp, - userName, - ] - ); -}; - /* The referenced alert _index in the flyout uses the `.internal.` such as `.internal.alerts-security.alerts-spaceId` in the alert page flyout and diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 51fdf88b1190b..af2ab4bc10df8 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -2544,6 +2544,11 @@ "discover.embeddable.search.displayName": "rechercher", "discover.errorCalloutShowErrorMessage": "Afficher les détails", "discover.esqlMode.selectedColumnsCallout": "Affichage de {selectedColumnsNumber} champs sur {esqlQueryColumnsNumber}. Ajoutez-en d’autres depuis la liste des champs disponibles.", + "discover.esqlToDataViewTransitionModal.closeButtonLabel": "Basculer sans sauvegarder", + "discover.esqlToDataViewTransitionModal.dismissButtonLabel": "Ne plus afficher cet avertissement", + "discover.esqlToDataViewTransitionModal.saveButtonLabel": "Sauvegarder et basculer", + "discover.esqlToDataViewTransitionModal.title": "Votre requête sera supprimée", + "discover.esqlToDataviewTransitionModalBody": "Modifier la vue de données supprime la requête ES|QL en cours. Sauvegardez cette recherche pour ne pas perdre de travail.", "discover.fieldChooser.availableFieldsTooltip": "Champs disponibles pour l'affichage dans le tableau.", "discover.fieldChooser.discoverField.addFieldTooltip": "Ajouter le champ en tant que colonne", "discover.fieldChooser.discoverField.removeFieldTooltip": "Supprimer le champ du tableau", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3b9d41876b444..602e8ea6b08ab 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2544,6 +2544,11 @@ "discover.embeddable.search.displayName": "検索", "discover.errorCalloutShowErrorMessage": "詳細を表示", "discover.esqlMode.selectedColumnsCallout": "{esqlQueryColumnsNumber}フィールド中{selectedColumnsNumber}フィールドを表示中です。利用可能なフィールドリストからさらに追加します。", + "discover.esqlToDataViewTransitionModal.closeButtonLabel": "保存せずに切り替え", + "discover.esqlToDataViewTransitionModal.dismissButtonLabel": "次回以降この警告を表示しない", + "discover.esqlToDataViewTransitionModal.saveButtonLabel": "保存して切り替え", + "discover.esqlToDataViewTransitionModal.title": "クエリは削除されます", + "discover.esqlToDataviewTransitionModalBody": "データビューを切り替えると、現在のES|QLクエリが削除されます。この検索を保存すると、作業内容が失われないことが保証されます。", "discover.fieldChooser.availableFieldsTooltip": "フィールドをテーブルに表示できます。", "discover.fieldChooser.discoverField.addFieldTooltip": "フィールドを列として追加", "discover.fieldChooser.discoverField.removeFieldTooltip": "フィールドを表から削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 09503197eda29..21e15ecd10f53 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2546,6 +2546,11 @@ "discover.embeddable.search.displayName": "搜索", "discover.errorCalloutShowErrorMessage": "查看详情", "discover.esqlMode.selectedColumnsCallout": "正在显示 {selectedColumnsNumber} 个字段,共 {esqlQueryColumnsNumber} 个。从可用字段列表中添加更多字段。", + "discover.esqlToDataViewTransitionModal.closeButtonLabel": "切换而不保存", + "discover.esqlToDataViewTransitionModal.dismissButtonLabel": "不再显示此警告", + "discover.esqlToDataViewTransitionModal.saveButtonLabel": "保存并切换", + "discover.esqlToDataViewTransitionModal.title": "将移除您的查询", + "discover.esqlToDataviewTransitionModalBody": "切换数据视图会移除当前的 ES|QL 查询。保存此搜索以确保不会丢失工作。", "discover.fieldChooser.availableFieldsTooltip": "适用于在表中显示的字段。", "discover.fieldChooser.discoverField.addFieldTooltip": "将字段添加为列", "discover.fieldChooser.discoverField.removeFieldTooltip": "从表中移除字段", diff --git a/x-pack/test/functional/apps/infra/node_details.ts b/x-pack/test/functional/apps/infra/node_details.ts index fbc442b5079c0..f960208ab4745 100644 --- a/x-pack/test/functional/apps/infra/node_details.ts +++ b/x-pack/test/functional/apps/infra/node_details.ts @@ -7,6 +7,7 @@ import moment from 'moment'; import expect from '@kbn/expect'; +import rison from '@kbn/rison'; import { InfraSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { enableInfrastructureContainerAssetView, @@ -42,6 +43,11 @@ const END_HOST_KUBERNETES_SECTION_DATE = moment.utc( const START_CONTAINER_DATE = moment.utc(DATE_WITH_DOCKER_DATA_FROM); const END_CONTAINER_DATE = moment.utc(DATE_WITH_DOCKER_DATA_TO); +interface QueryParams { + name?: string; + alertMetric?: string; +} + export default ({ getPageObjects, getService }: FtrProviderContext) => { const observability = getService('observability'); const browser = getService('browser'); @@ -59,19 +65,24 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'timePicker', ]); - const getNodeDetailsUrl = (assetName: string) => { - const queryParams = new URLSearchParams(); - - queryParams.set('assetName', assetName); - - return queryParams.toString(); + const getNodeDetailsUrl = (queryParams?: QueryParams) => { + return rison.encodeUnknown( + Object.entries(queryParams ?? {}).reduce>((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {}) + ); }; - const navigateToNodeDetails = async (assetId: string, assetName: string, assetType: string) => { + const navigateToNodeDetails = async ( + assetId: string, + assetType: string, + queryParams?: QueryParams + ) => { await pageObjects.common.navigateToUrlWithBrowserHistory( 'infraOps', `/${NODE_DETAILS_PATH}/${assetType}/${assetId}`, - getNodeDetailsUrl(assetName), + `assetDetails=${getNodeDetailsUrl(queryParams)}`, { insertTimestamp: false, ensureCurrentUrl: false, @@ -113,7 +124,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ]); await browser.setWindowSize(1600, 1200); - await navigateToNodeDetails('Jennys-MBP.fritz.box', 'Jennys-MBP.fritz.box', 'host'); + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'host', { + name: 'Jennys-MBP.fritz.box', + }); await pageObjects.header.waitUntilLoadingHasFinished(); }); @@ -270,7 +283,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const ALL_ALERTS = ACTIVE_ALERTS + RECOVERED_ALERTS; const COLUMNS = 11; before(async () => { - await navigateToNodeDetails('demo-stack-apache-01', 'demo-stack-apache-01', 'host'); + await navigateToNodeDetails('demo-stack-apache-01', 'host', { + name: 'demo-stack-apache-01', + }); await pageObjects.header.waitUntilLoadingHasFinished(); await pageObjects.timePicker.setAbsoluteRange( @@ -282,7 +297,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); after(async () => { - await navigateToNodeDetails('Jennys-MBP.fritz.box', 'Jennys-MBP.fritz.box', 'host'); + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'host', { + name: 'Jennys-MBP.fritz.box', + }); await pageObjects.header.waitUntilLoadingHasFinished(); await pageObjects.timePicker.setAbsoluteRange( @@ -505,7 +522,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Host with alerts and no processes', () => { before(async () => { - await navigateToNodeDetails('demo-stack-mysql-01', 'demo-stack-mysql-01', 'host'); + await navigateToNodeDetails('demo-stack-mysql-01', 'host', { + name: 'demo-stack-mysql-01', + }); await pageObjects.timePicker.setAbsoluteRange( START_HOST_ALERTS_DATE.format(DATE_PICKER_FORMAT), END_HOST_ALERTS_DATE.format(DATE_PICKER_FORMAT) @@ -539,11 +558,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('#With Kubernetes section', () => { before(async () => { - await navigateToNodeDetails( - 'demo-stack-kubernetes-01', - 'demo-stack-kubernetes-01', - 'host' - ); + await navigateToNodeDetails('demo-stack-kubernetes-01', 'host', { + name: 'demo-stack-kubernetes-01', + }); await pageObjects.header.waitUntilLoadingHasFinished(); }); @@ -623,6 +640,43 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); }); + + describe('Callouts', () => { + describe('Legacy alert metric callout', () => { + [{ metric: 'cpu' }, { metric: 'rx' }, { metric: 'tx' }].forEach(({ metric }) => { + it(`Should show for: ${metric}`, async () => { + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'host', { + name: 'Jennys-MBP.fritz.box', + alertMetric: metric, + }); + await pageObjects.header.waitUntilLoadingHasFinished(); + + await retry.try(async () => { + expect(await pageObjects.assetDetails.legacyMetricAlertCalloutExists()).to.be( + true + ); + }); + }); + }); + + [{ metric: 'cpuV2' }, { metric: 'rxV2' }, { metric: 'txV2' }].forEach(({ metric }) => { + it(`Should not show for: ${metric}`, async () => { + await navigateToNodeDetails('Jennys-MBP.fritz.box', 'host', { + name: 'Jennys-MBP.fritz.box', + alertMetric: metric, + }); + + await pageObjects.header.waitUntilLoadingHasFinished(); + + await retry.try(async () => { + expect(await pageObjects.assetDetails.legacyMetricAlertCalloutExists()).to.be( + false + ); + }); + }); + }); + }); + }); }); describe('#Asset Type: container', () => { @@ -647,7 +701,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('when container asset view is disabled', () => { it('should show old view of container details', async () => { await setInfrastructureContainerAssetViewUiSetting(false); - await navigateToNodeDetails('container-id-0', 'container-id-0', 'container'); + await navigateToNodeDetails('container-id-0', 'container', { + name: 'container-id-0', + }); await pageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.find('metricsEmptyViewState'); }); @@ -656,7 +712,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('when container asset view is enabled', () => { before(async () => { await setInfrastructureContainerAssetViewUiSetting(true); - await navigateToNodeDetails('container-id-0', 'container-id-0', 'container'); + await navigateToNodeDetails('container-id-0', 'container', { + name: 'container-id-0', + }); await pageObjects.header.waitUntilLoadingHasFinished(); await pageObjects.timePicker.setAbsoluteRange( START_CONTAINER_DATE.format(DATE_PICKER_FORMAT), diff --git a/x-pack/test/functional/page_objects/asset_details.ts b/x-pack/test/functional/page_objects/asset_details.ts index 4a56b3dce469c..4e3da871a91b6 100644 --- a/x-pack/test/functional/page_objects/asset_details.ts +++ b/x-pack/test/functional/page_objects/asset_details.ts @@ -352,5 +352,10 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) { return testSubjects.click(buttonSubject); }, + + // Callouts + async legacyMetricAlertCalloutExists() { + return testSubjects.exists('infraAssetDetailsLegacyMetricAlertCallout'); + }, }; } diff --git a/x-pack/test/functional/services/cases/list.ts b/x-pack/test/functional/services/cases/list.ts index 34db5e7345b3c..7f4e7346d9706 100644 --- a/x-pack/test/functional/services/cases/list.ts +++ b/x-pack/test/functional/services/cases/list.ts @@ -209,8 +209,8 @@ export function CasesTableServiceProvider( return; } + await testSubjects.click('options-filter-popover-button-owner'); await retry.waitFor(`filterByOwner popover opened`, async () => { - await testSubjects.click('options-filter-popover-button-owner'); return await testSubjects.exists('options-filter-popover-panel-owner'); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts b/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts index 49e8fb47db644..146e72ab3698e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts +++ b/x-pack/test/functional_with_es_ssl/apps/cases/group2/attachment_framework.ts @@ -268,8 +268,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/157642 - describe.skip('Modal', () => { + describe('Modal', () => { const createdCases = new Map(); const openModal = async () => { diff --git a/yarn.lock b/yarn.lock index 8d3f4a10b1265..b8332a6a4c375 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16589,10 +16589,10 @@ elastic-apm-node@3.46.0: traverse "^0.6.6" unicode-byte-truncate "^1.0.0" -elastic-apm-node@^4.7.2: - version "4.7.2" - resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-4.7.2.tgz#880b3df8e2266aac70f6370f916b0e66d5063455" - integrity sha512-9jsvAeHU6wztM+qUWJvgJCgdCVUI1sfg6a9quXmgkcjUJmRDJG0trfTScELZrfK5VJBQ88LVl05Q0nJW2j6TsA== +elastic-apm-node@^4.7.3: + version "4.7.3" + resolved "https://registry.yarnpkg.com/elastic-apm-node/-/elastic-apm-node-4.7.3.tgz#d819a9030f7321cc858c788f60b383de85461f24" + integrity sha512-x+cQKrXSCz6JgoTFAiBpLlC85ruqZ7sAl+jAS3+DeSmc6ZXLRTwTa2ay2PCGv3DxGLZjVZ+ItzGdHTj5B7PYKg== dependencies: "@elastic/ecs-pino-format" "^1.5.0" "@opentelemetry/api" "^1.4.1" @@ -16612,7 +16612,7 @@ elastic-apm-node@^4.7.2: fast-safe-stringify "^2.0.7" fast-stream-to-buffer "^1.0.0" http-headers "^3.0.2" - import-in-the-middle "1.9.1" + import-in-the-middle "1.11.0" json-bigint "^1.0.0" lru-cache "10.2.0" measured-reporting "^1.51.1" @@ -20003,10 +20003,10 @@ import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0: parent-module "^1.0.0" resolve-from "^4.0.0" -import-in-the-middle@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.9.1.tgz#83f68c0ca926709257562238e1993a1c31e01272" - integrity sha512-E+3tEOutU1MV0mxhuCwfSPNNWRkbTJ3/YyL5be+blNIbHwZc53uYHQfuIhAU77xWR0BoF2eT7cqDJ6VlU5APPg== +import-in-the-middle@1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz#a94c4925b8da18256cde3b3b7b38253e6ca5e708" + integrity sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q== dependencies: acorn "^8.8.2" acorn-import-attributes "^1.9.5"