[Infra] Handle view in app for legacy metrics (#190295)

closes [#189625](https://github.com/elastic/kibana/issues/189625)

## Summary

This PR changes the asset details to display a call if the user comes
from the alerts page via an inventory rule created with one of the
legacy metrics.

Besides that, it changes how the link is built to use locators.

Legacy metrics example


https://github.com/user-attachments/assets/12308f4e-e269-4580-b86d-808ae9f6fe10

**Regression**

Metrics Threshold


https://github.com/user-attachments/assets/94032f51-6b2c-4760-8019-158746a1aa13

Inventory Rule (new/hosts view metrics)


https://github.com/user-attachments/assets/0f872f3a-7bdb-4fb8-a925-7ed3621fee2d

Inventory Rule (custom metric)



https://github.com/user-attachments/assets/f2e5ded5-b2e6-45ff-878d-6361c4540140

### Fix

While working on it, I discovered that alerts for containers were not
redirecting the users to the asset details page for containers. That was
fixed too

Inventory rule for containers


https://github.com/user-attachments/assets/05f20c12-6fdc-45c0-bc38-b756bfbf3658

Metrics threshold rule for containers


### How to test

- Start a local Kibana instance (easier if pointed to an oblt cluster)
- Create Inventory Rule alerts for:  
  - host: 1 legacy metric and 1 non-legacy metric
  - container
- Create Metric Threshold alerts with
  - avg on `system.cpu.total.norm.pct` grouped by `host.name` 
- avg on `kubernetes.container.cpu.usage.limit.pct` grouped by
`container.id`
- Navigate to the alerts page and click on the `view in app` button, as
shown in the recordings above
- Test if the navigation to the asset details page works
   - For a legacy metric, the callout should be displayed
- Once dismissed, the callout should not appear again for that metric
This commit is contained in:
Carlos Crespo 2024-08-19 11:31:16 +02:00 committed by GitHub
parent 29c5381935
commit d69e598e30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 776 additions and 223 deletions

View file

@ -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<InventoryLocator>;
const mockAssetDetailsLocator = {
getRedirectUrl: jest
.fn()
.mockImplementation(
({ assetId, assetType, assetDetails }: AssetDetailsLocatorParams) =>
`/node-mock/${assetType}/${assetId}?receivedParams=${rison.encodeUnknown(assetDetails)}`
),
} as unknown as jest.Mocked<AssetDetailsLocator>;
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<string, any>;
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&timestamp=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<string, any>;
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&timestamp=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<string, any>;
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<string, any>;
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<string, any>;
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&timestamp=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<string, any>;
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&timestamp=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<string, any>;
const url = getMetricsViewInAppUrl(fields);
const url = getMetricsViewInAppUrl({
fields,
assetDetailsLocator: mockAssetDetailsLocator,
groupBy: ['host.name'],
});
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=(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<string, any>;
const url = getMetricsViewInAppUrl({
fields,
assetDetailsLocator: mockAssetDetailsLocator,
groupBy: ['container.id'],
});
expect(mockAssetDetailsLocator.getRedirectUrl).toHaveBeenCalledTimes(1);
expect(url).toEqual(
"/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<string, any>;
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<string, any>;
const url = getMetricsViewInAppUrl(fields);
const url = getMetricsViewInAppUrl({ fields });
expect(url).toEqual('/app/metrics/explorer');
});
});

View file

@ -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<string, unknown[]> => {
return Object.entries(params).reduce((acc, [key, field]) => {
@ -32,10 +38,18 @@ export const flatAlertRuleParams = (params: {}, pKey = ''): Record<string, unkno
}, {} as Record<string, unknown[]>);
};
export const getInventoryViewInAppUrl = (
fields: ParsedTechnicalFields & Record<string, any>
): string => {
let inventoryFields = fields;
export const getInventoryViewInAppUrl = ({
fields,
assetDetailsLocator,
inventoryLocator,
}: {
fields: ParsedTechnicalFields & Record<string, any>;
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
inventoryLocator?: LocatorPublic<InventoryLocatorParams>;
}): 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,
});
}
return LINK_TO_INVENTORY;
};
export const getMetricsViewInAppUrl = (fields: ParsedTechnicalFields & Record<string, any>) => {
const hostName = fields[HOST_NAME_FIELD];
const timestamp = fields[TIMESTAMP];
return hostName ? getLinkToHostDetails({ hostName, timestamp }) : METRICS_EXPLORER_URL;
};
export function getLinkToHostDetails({
hostName,
timestamp,
}: {
hostName: string;
timestamp: string;
}): string {
const queryParams = {
from: Date.parse(timestamp),
to: Date.parse(timestamp) + fifteenMinutesInMilliseconds,
const linkToParams = {
nodeType,
timestamp: Date.parse(inventoryFields[TIMESTAMP]),
customMetric: '',
metric: '',
};
const encodedParams = encode(stringify(queryParams));
// We always pick the first criteria metric for the URL
return `/app/metrics/link-to/host-detail/${hostName}?${encodedParams}`;
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 inventoryLocator.getRedirectUrl({
...linkToParams,
});
};
export const getMetricsViewInAppUrl = ({
fields,
groupBy,
assetDetailsLocator,
}: {
fields: ParsedTechnicalFields & Record<string, any>;
groupBy?: string[];
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
}) => {
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<string, InventoryItemType>);
// 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 getLinkToAssetDetails({
assetId,
assetType: supportedAssetType,
timestamp,
assetDetailsLocator,
});
} else {
return METRICS_EXPLORER_URL;
}
};
function getLinkToAssetDetails({
assetId,
assetType,
timestamp,
alertMetric,
assetDetailsLocator,
}: {
assetId: string;
assetType: InventoryItemType;
timestamp: string;
alertMetric?: string;
assetDetailsLocator: LocatorPublic<AssetDetailsLocatorParams>;
}): string {
return assetDetailsLocator.getRedirectUrl({
assetId,
assetType,
assetDetails: {
dateRange: {
from: timestamp,
to: moment(timestamp).add(fifteenMinutesInMilliseconds, 'ms').toISOString(),
},
...(alertMetric && alertMetric !== CUSTOM_METRIC_TYPE ? { alertMetric } : undefined),
},
});
}

View file

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

View file

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

View file

@ -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<InventoryMetricRuleTypeParams> {
export function createInventoryMetricRuleType({
assetDetailsLocator,
inventoryLocator,
}: {
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
inventoryLocator?: LocatorPublic<InventoryLocatorParams>;
}): ObservabilityRuleTypeModel<InventoryMetricRuleTypeParams> {
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<Inve
defaultActionMessage: inventoryDefaultActionMessage,
defaultRecoveryMessage: inventoryDefaultRecoveryMessage,
requiresAppContext: false,
format: formatReason,
format,
priority: 20,
};
}

View file

@ -7,13 +7,27 @@
import { ALERT_REASON } from '@kbn/rule-data-utils';
import { ObservabilityRuleTypeFormatter } from '@kbn/observability-plugin/public';
import type { LocatorPublic } from '@kbn/share-plugin/common';
import type {
AssetDetailsLocatorParams,
InventoryLocatorParams,
} from '@kbn/observability-shared-plugin/common';
import { getInventoryViewInAppUrl } from '../../../common/alerting/metrics/alert_link';
export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => {
const reason = fields[ALERT_REASON] ?? '-';
export const getRuleFormat = ({
assetDetailsLocator,
inventoryLocator,
}: {
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
inventoryLocator?: LocatorPublic<InventoryLocatorParams>;
}): ObservabilityRuleTypeFormatter => {
return ({ fields }) => {
const reason = fields[ALERT_REASON] ?? '-';
return {
reason,
link: getInventoryViewInAppUrl(fields),
return {
reason,
link: getInventoryViewInAppUrl({ fields, assetDetailsLocator, inventoryLocator }),
hasBasePath: true,
};
};
};

View file

@ -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<MetricThresholdRuleTypeParams> {
export function createMetricThresholdRuleType({
assetDetailsLocator,
}: {
assetDetailsLocator?: LocatorPublic<AssetDetailsLocatorParams>;
}): ObservabilityRuleTypeModel<MetricThresholdRuleTypeParams> {
return {
id: METRIC_THRESHOLD_ALERT_TYPE_ID,
description: i18n.translate('xpack.infra.metrics.alertFlyout.alertDescription', {
@ -65,7 +71,7 @@ export function createMetricThresholdRuleType(): ObservabilityRuleTypeModel<Metr
defaultActionMessage: metricThresholdDefaultActionMessage,
defaultRecoveryMessage: metricThresholdDefaultRecoveryMessage,
requiresAppContext: false,
format: formatReason,
format: getRuleFormat({ assetDetailsLocator }),
alertDetailsAppSection: lazy(() => import('./components/alert_details_app_section')),
priority: 10,
};

View file

@ -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<AssetDetailsLocatorParams>;
}): ObservabilityRuleTypeFormatter => {
return ({ fields }) => {
const reason = fields[ALERT_REASON] ?? '-';
const parameters = fields[ALERT_RULE_PARAMETERS];
const link = getMetricsViewInAppUrl({
fields,
groupBy: castArray<string>(parameters?.groupBy as string[] | string),
assetDetailsLocator,
});
return {
reason,
link,
hasBasePath: link !== METRICS_EXPLORER_URL,
};
};
};

View file

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

View file

@ -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 = <T extends InventoryItemType>(
inventoryModel: InventoryModels<T>,
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 (
<LegacyAlertMetricCallout
visibleFor={INCOMING_ALERT_CALLOUT_VISIBLE_FOR}
metric={alertMetric}
/>
);
}
return null;
};

View file

@ -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<boolean>(
`${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 (
<EuiCallOut
color="warning"
iconType="warning"
title={
<FormattedMessage
id="xpack.infra.assetDetails.callouts.legacyMetricAlertCallout.title"
defaultMessage="We have an updated definition for {metric}"
values={{
metric: metricLabel.text,
}}
/>
}
data-test-subj="infraAssetDetailsLegacyMetricAlertCallout"
onDismiss={onDismiss}
>
<FormattedMessage
id="xpack.infra.assetDetails.callouts.legacyMetricAlertCallout"
defaultMessage="The alert you have clicked through is using the legacy {metric}. {learnMoreLink}"
values={{
metric: metricLabel.text,
learnMoreLink: (
<EuiLink
data-test-subj="infraAssetDetailsLegacyMetricAlertCalloutLink"
href={HOST_METRICS_DOC_HREF}
target="_blank"
>
<FormattedMessage
id="xpack.infra.assetDetails.callouts.legacyMetricAlertCallout.learnMoreLinkLabel"
defaultMessage="Learn More"
/>
</EuiLink>
),
}}
/>
</EuiCallOut>
);
};

View file

@ -22,22 +22,30 @@ import {
Profiling,
} from '../tabs';
import { ContentTabIds } from '../types';
import { Callouts } from './callouts';
export const Content = () => {
return (
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem grow={false}>
<DatePickerWrapper
visibleFor={[
ContentTabIds.OVERVIEW,
ContentTabIds.LOGS,
ContentTabIds.METADATA,
ContentTabIds.METRICS,
ContentTabIds.PROCESSES,
ContentTabIds.ANOMALIES,
ContentTabIds.DASHBOARDS,
]}
/>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
<Callouts />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<DatePickerWrapper
visibleFor={[
ContentTabIds.OVERVIEW,
ContentTabIds.LOGS,
ContentTabIds.METADATA,
ContentTabIds.METRICS,
ContentTabIds.PROCESSES,
ContentTabIds.ANOMALIES,
ContentTabIds.DASHBOARDS,
]}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TabPanel activeWhen={ContentTabIds.ANOMALIES}>

View file

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

View file

@ -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: (

View file

@ -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 ? <EuiLoadingSpinner size="m" /> : 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 ? (
<EuiFlexGroup
direction="column"
css={css`
height: calc(100vh - var(--kbnAppHeadersOffset, var(--euiFixedHeadersOffset, 0)));
`}
>
<InfraLoadingPanel
height="100%"
width="auto"
text={i18n.translate('xpack.infra.waffle.loadingDataText', {
defaultMessage: 'Loading data',
})}
/>
</EuiFlexGroup>
) : (
<Content />
)}
<Content />
</InfraPageTemplate>
);
};

View file

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

View file

@ -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<AssetDetailsLocatorParams>(ASSET_DETAILS_LOCATOR_ID);
const { error, name } = useHostIpToName(hostIp, (metricsView && metricsView.indices) || null);

View file

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

View file

@ -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<AssetDetailsLocatorParams>(ASSET_DETAILS_LOCATOR_ID);
const inventoryLocator =
pluginsSetup.share.url.locators.get<InventoryLocatorParams>(INVENTORY_LOCATOR_ID);
pluginsSetup.observability.observabilityRuleTypeRegistry.register(
createInventoryMetricRuleType()
createInventoryMetricRuleType({ assetDetailsLocator, inventoryLocator })
);
pluginsSetup.observability.observabilityRuleTypeRegistry.register(
createMetricThresholdRuleType()
createMetricThresholdRuleType({ assetDetailsLocator })
);
if (this.config.featureFlags.logsUIEnabled) {

View file

@ -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<AssetDetailsLocatorParams>;
inventoryLocator?: LocatorPublic<InventoryLocatorParams>;
}) => {
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<AssetDetailsLocatorParams>;
}) => {
const fields = {
[TIMESTAMP]: timestamp,
[HOST_NAME]: hostName,
};
return addSpaceIdToPath(
basePath.publicBaseUrl,
spaceId,
getMetricsViewInAppUrl(parseTechnicalFields(fields, true))
getMetricsViewInAppUrl({
fields: parseTechnicalFields(fields, true),
groupBy,
assetDetailsLocator,
})
);
};

View file

@ -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<string, AlertTestInstance>();

View file

@ -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<AlertsLocatorParams>(alertsLocatorID);
const assetDetailsLocator =
share.setup.url.locators.get<AssetDetailsLocatorParams>(ASSET_DETAILS_LOCATOR_ID);
const inventoryLocator =
share.setup.url.locators.get<InventoryLocatorParams>(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,

View file

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

View file

@ -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<AlertsLocatorParams>(alertsLocatorID);
const assetDetailsLocator =
share.setup.url.locators.get<AssetDetailsLocatorParams>(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<string>(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,
},
};

View file

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

View file

@ -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<InfraServerPluginSetupDeps>[key];
} & (key extends keyof InfraServerPluginStartDeps
? {
start: () => Promise<Required<InfraServerPluginStartDeps>[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<AlertsLocatorParams>;
apmDataAccess: ApmDataAccessPluginSetup;
plugins: Plugins;
}

View file

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

View file

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

View file

@ -11,6 +11,7 @@ export {
getFieldByType,
findInventoryFields,
metrics,
type InventoryModels,
} from './inventory_models';
export { podSnapshotMetricTypes } from './inventory_models/kubernetes/pod';

View file

@ -56,4 +56,5 @@ export const host: InventoryModel<typeof metrics> = {
...nginxRequireMetrics,
],
tooltipMetrics: ['cpuV2', 'memory', 'txV2', 'rxV2', 'cpu', 'tx', 'rx'],
legacyMetrics: ['cpu', 'tx', 'rx'],
};

View file

@ -29,7 +29,7 @@ const catalog = {
export const inventoryModels = Object.values(catalog);
type InventoryModels<T extends InventoryItemType> = (typeof catalog)[T];
export type InventoryModels<T extends InventoryItemType> = (typeof catalog)[T];
export const findInventoryModel = <T extends InventoryItemType>(type: T): InventoryModels<T> => {
const model = inventoryModels.find((m) => m.id === type);

View file

@ -423,6 +423,7 @@ export interface InventoryModel<TMetrics = InventoryMetrics> {
};
metrics: TMetrics;
requiredMetrics: InventoryMetric[];
legacyMetrics?: SnapshotMetricType[];
tooltipMetrics: SnapshotMetricType[];
nodeFilter?: object[];
}

View file

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

View file

@ -13,7 +13,7 @@ export type AssetDetailsLocator = LocatorPublic<AssetDetailsLocatorParams>;
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<AssetDetailsLocatorParams> {
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 : {},
};
};

View file

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

View file

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

View file

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

View file

@ -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<Record<string, string>>((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),

View file

@ -352,5 +352,10 @@ export function AssetDetailsProvider({ getService }: FtrProviderContext) {
return testSubjects.click(buttonSubject);
},
// Callouts
async legacyMetricAlertCalloutExists() {
return testSubjects.exists('infraAssetDetailsLegacyMetricAlertCallout');
},
};
}