mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Observability] Related alerts based on scoring !! (#215673)
## Summary Copying most of https://github.com/elastic/kibana/pull/214017 !! Fixes https://github.com/elastic/kibana/issues/214372 ### Implementation We are now using response ops alerts table with custom score querying based on tags/groups matches and Jaccard similarity on documents !! <img width="1728" alt="image" src="https://github.com/user-attachments/assets/b3a69280-c05d-4100-be6a-2c8dadcc051d" /> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Dominique Clarke <dominique.clarke@elastic.co> Co-authored-by: Kevin Delemme <kevin.delemme@elastic.co>
This commit is contained in:
parent
f8e688f881
commit
760106eb86
44 changed files with 1154 additions and 656 deletions
|
@ -13,6 +13,7 @@ import type { JsonValue } from '@kbn/utility-types';
|
|||
export interface MetaAlertFields {
|
||||
_id: string;
|
||||
_index: string;
|
||||
_score?: number;
|
||||
}
|
||||
|
||||
export interface LegacyField {
|
||||
|
@ -29,7 +30,7 @@ export type KnownAlertFields = {
|
|||
[Property in TechnicalRuleDataFieldName]?: JsonValue[];
|
||||
};
|
||||
|
||||
export type UnknownAlertFields = Record<string, string | JsonValue[]>;
|
||||
export type UnknownAlertFields = Record<string, string | number | JsonValue[]>;
|
||||
|
||||
/**
|
||||
* Alert document type as returned by alerts search requests
|
||||
|
|
|
@ -24,6 +24,8 @@ export type RuleRegistrySearchRequest = IEsSearchRequest & {
|
|||
sort?: SortCombinations[];
|
||||
pagination?: RuleRegistrySearchRequestPagination;
|
||||
runtimeMappings?: MappingRuntimeFields;
|
||||
minScore?: number;
|
||||
trackScores?: boolean;
|
||||
};
|
||||
|
||||
export interface RuleRegistrySearchRequestPagination {
|
||||
|
|
|
@ -85,6 +85,7 @@ const parsedAlerts = {
|
|||
{
|
||||
_index: '.internal.alerts-security.alerts-default-000001',
|
||||
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
|
||||
_score: 1,
|
||||
'@timestamp': ['2022-03-22T16:48:07.518Z'],
|
||||
'host.name': ['Host-4dbzugdlqd'],
|
||||
'kibana.alert.reason': [
|
||||
|
@ -99,6 +100,7 @@ const parsedAlerts = {
|
|||
{
|
||||
_index: '.internal.alerts-security.alerts-default-000001',
|
||||
_id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
|
||||
_score: 1,
|
||||
'@timestamp': ['2022-03-22T16:17:50.769Z'],
|
||||
'host.name': ['Host-4dbzugdlqd'],
|
||||
'kibana.alert.reason': [
|
||||
|
@ -130,6 +132,7 @@ const parsedAlerts = {
|
|||
host: { name: ['Host-4dbzugdlqd'] },
|
||||
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
|
||||
_index: '.internal.alerts-security.alerts-default-000001',
|
||||
_score: 1,
|
||||
},
|
||||
{
|
||||
kibana: {
|
||||
|
@ -148,6 +151,7 @@ const parsedAlerts = {
|
|||
host: { name: ['Host-4dbzugdlqd'] },
|
||||
_id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
|
||||
_index: '.internal.alerts-security.alerts-default-000001',
|
||||
_score: 1,
|
||||
},
|
||||
],
|
||||
oldAlertsData: [
|
||||
|
@ -169,6 +173,10 @@ const parsedAlerts = {
|
|||
field: '_id',
|
||||
value: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
|
||||
},
|
||||
{
|
||||
field: '_score',
|
||||
value: 1,
|
||||
},
|
||||
{ field: '_index', value: '.internal.alerts-security.alerts-default-000001' },
|
||||
],
|
||||
[
|
||||
|
@ -189,6 +197,10 @@ const parsedAlerts = {
|
|||
field: '_id',
|
||||
value: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
|
||||
},
|
||||
{
|
||||
field: '_score',
|
||||
value: 1,
|
||||
},
|
||||
{ field: '_index', value: '.internal.alerts-security.alerts-default-000001' },
|
||||
],
|
||||
],
|
||||
|
|
|
@ -7,7 +7,12 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { catchError, filter, lastValueFrom, map, of } from 'rxjs';
|
||||
import type {
|
||||
MappingRuntimeFields,
|
||||
QueryDslFieldAndFormat,
|
||||
QueryDslQueryContainer,
|
||||
SortCombinations,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import type {
|
||||
Alert,
|
||||
EsQuerySnapshot,
|
||||
|
@ -15,14 +20,9 @@ import type {
|
|||
RuleRegistrySearchRequest,
|
||||
RuleRegistrySearchResponse,
|
||||
} from '@kbn/alerting-types';
|
||||
import { set } from '@kbn/safer-lodash-set';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type {
|
||||
MappingRuntimeFields,
|
||||
QueryDslFieldAndFormat,
|
||||
QueryDslQueryContainer,
|
||||
SortCombinations,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import { set } from '@kbn/safer-lodash-set';
|
||||
import { catchError, filter, lastValueFrom, map, of } from 'rxjs';
|
||||
|
||||
export interface SearchAlertsParams {
|
||||
// Dependencies
|
||||
|
@ -68,6 +68,14 @@ export interface SearchAlertsParams {
|
|||
* The page size to fetch
|
||||
*/
|
||||
pageSize: number;
|
||||
/**
|
||||
* The minimum score to apply to the query
|
||||
*/
|
||||
minScore?: number;
|
||||
/**
|
||||
* Whether to track the score of the query
|
||||
*/
|
||||
trackScores?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchAlertsResult {
|
||||
|
@ -92,6 +100,8 @@ export const searchAlerts = ({
|
|||
runtimeMappings,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
minScore,
|
||||
trackScores,
|
||||
}: SearchAlertsParams): Promise<SearchAlertsResult> =>
|
||||
lastValueFrom(
|
||||
data.search
|
||||
|
@ -104,6 +114,8 @@ export const searchAlerts = ({
|
|||
pagination: { pageIndex, pageSize },
|
||||
sort,
|
||||
runtimeMappings,
|
||||
minScore,
|
||||
trackScores,
|
||||
},
|
||||
{
|
||||
strategy: 'privateRuleRegistryAlertsSearchStrategy',
|
||||
|
@ -167,6 +179,7 @@ const parseAlerts = (rawResponse: RuleRegistrySearchResponse['rawResponse']) =>
|
|||
acc.push({
|
||||
...hit.fields,
|
||||
_id: hit._id,
|
||||
_score: hit._score,
|
||||
_index: hit._index,
|
||||
} as Alert);
|
||||
}
|
||||
|
|
|
@ -166,6 +166,7 @@ describe('useSearchAlertsQuery', () => {
|
|||
{
|
||||
_index: '.internal.alerts-security.alerts-default-000001',
|
||||
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
|
||||
_score: 1,
|
||||
'@timestamp': ['2022-03-22T16:48:07.518Z'],
|
||||
'host.name': ['Host-4dbzugdlqd'],
|
||||
'kibana.alert.reason': [
|
||||
|
@ -180,6 +181,7 @@ describe('useSearchAlertsQuery', () => {
|
|||
{
|
||||
_index: '.internal.alerts-security.alerts-default-000001',
|
||||
_id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
|
||||
_score: 1,
|
||||
'@timestamp': ['2022-03-22T16:17:50.769Z'],
|
||||
'host.name': ['Host-4dbzugdlqd'],
|
||||
'kibana.alert.reason': [
|
||||
|
@ -211,6 +213,7 @@ describe('useSearchAlertsQuery', () => {
|
|||
host: { name: ['Host-4dbzugdlqd'] },
|
||||
_id: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
|
||||
_index: '.internal.alerts-security.alerts-default-000001',
|
||||
_score: 1,
|
||||
},
|
||||
{
|
||||
kibana: {
|
||||
|
@ -229,6 +232,7 @@ describe('useSearchAlertsQuery', () => {
|
|||
host: { name: ['Host-4dbzugdlqd'] },
|
||||
_id: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
|
||||
_index: '.internal.alerts-security.alerts-default-000001',
|
||||
_score: 1,
|
||||
},
|
||||
],
|
||||
oldAlertsData: [
|
||||
|
@ -250,6 +254,10 @@ describe('useSearchAlertsQuery', () => {
|
|||
field: '_id',
|
||||
value: '38dd308706a127696cc63b8f142e8e4d66f8f79bc7d491dd79a42ea4ead62dd1',
|
||||
},
|
||||
{
|
||||
field: '_score',
|
||||
value: 1,
|
||||
},
|
||||
{ field: '_index', value: '.internal.alerts-security.alerts-default-000001' },
|
||||
],
|
||||
[
|
||||
|
@ -270,6 +278,10 @@ describe('useSearchAlertsQuery', () => {
|
|||
field: '_id',
|
||||
value: '8361363c0db6f30ca2dfb4aeb4835e7d6ec57bc195b96d9ee5a4ead1bb9f8b86',
|
||||
},
|
||||
{
|
||||
field: '_score',
|
||||
value: 1,
|
||||
},
|
||||
{ field: '_index', value: '.internal.alerts-security.alerts-default-000001' },
|
||||
],
|
||||
],
|
||||
|
|
|
@ -43,6 +43,8 @@ export const useSearchAlertsQuery = ({ data, ...params }: UseSearchAlertsQueryPa
|
|||
runtimeMappings,
|
||||
pageIndex = 0,
|
||||
pageSize = DEFAULT_ALERTS_PAGE_SIZE,
|
||||
minScore,
|
||||
trackScores,
|
||||
} = params;
|
||||
return useQuery({
|
||||
queryKey: queryKeyPrefix.concat(JSON.stringify(params)),
|
||||
|
@ -58,6 +60,8 @@ export const useSearchAlertsQuery = ({ data, ...params }: UseSearchAlertsQueryPa
|
|||
runtimeMappings,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
minScore,
|
||||
trackScores,
|
||||
}),
|
||||
refetchOnWindowFocus: false,
|
||||
context: AlertsQueryContext,
|
||||
|
|
|
@ -170,6 +170,8 @@ const AlertsTableContent = typedForwardRef(
|
|||
ruleTypeIds,
|
||||
consumers,
|
||||
query,
|
||||
minScore,
|
||||
trackScores = false,
|
||||
initialSort = DEFAULT_SORT,
|
||||
initialPageSize = DEFAULT_ALERTS_PAGE_SIZE,
|
||||
leadingControlColumns = DEFAULT_LEADING_CONTROL_COLUMNS,
|
||||
|
@ -277,6 +279,8 @@ const AlertsTableContent = typedForwardRef(
|
|||
runtimeMappings,
|
||||
pageIndex: 0,
|
||||
pageSize: initialPageSize,
|
||||
minScore,
|
||||
trackScores,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -287,6 +291,8 @@ const AlertsTableContent = typedForwardRef(
|
|||
query,
|
||||
sort,
|
||||
runtimeMappings,
|
||||
minScore,
|
||||
trackScores,
|
||||
// Go back to the first page if the query changes
|
||||
pageIndex: !deepEqual(prevQueryParams, {
|
||||
ruleTypeIds,
|
||||
|
@ -300,7 +306,7 @@ const AlertsTableContent = typedForwardRef(
|
|||
: oldPageIndex,
|
||||
pageSize: oldPageSize,
|
||||
}));
|
||||
}, [ruleTypeIds, fields, query, runtimeMappings, sort, consumers]);
|
||||
}, [ruleTypeIds, fields, query, runtimeMappings, sort, consumers, minScore, trackScores]);
|
||||
|
||||
const {
|
||||
data: alertsData,
|
||||
|
|
|
@ -12,6 +12,7 @@ import { EuiDescriptionList, EuiPanel, EuiTabbedContentTab, EuiTitle } from '@el
|
|||
import { ALERT_RULE_NAME } from '@kbn/rule-data-utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ScrollableFlyoutTabbedContent, AlertFieldsTable } from '@kbn/alerts-ui-shared';
|
||||
import { JsonValue } from '@kbn/utility-types';
|
||||
import { AdditionalContext, FlyoutSectionProps } from '../types';
|
||||
import { defaultAlertsTableColumns } from '../configuration';
|
||||
import { DefaultCellValue } from './default_cell_value';
|
||||
|
@ -43,7 +44,7 @@ export const DefaultAlertsFlyoutBody = <AC extends AdditionalContext>(
|
|||
<EuiPanel hasShadow={false} data-test-subj="overviewTabPanel">
|
||||
<EuiDescriptionList
|
||||
listItems={(columns ?? defaultAlertsTableColumns).map((column) => {
|
||||
const value = alert[column.id]?.[0];
|
||||
const value = (alert[column.id] as JsonValue[])?.[0];
|
||||
|
||||
return {
|
||||
title: (column.displayAsText as string) ?? column.id,
|
||||
|
|
|
@ -94,7 +94,7 @@ export const DefaultCellValue = ({
|
|||
/**
|
||||
* Extracts the value from the raw json ES field
|
||||
*/
|
||||
const extractFieldValue = (rawValue: string | JsonValue[]) => {
|
||||
const extractFieldValue = (rawValue: string | number | JsonValue[]) => {
|
||||
const value = Array.isArray(rawValue) ? rawValue.join() : rawValue;
|
||||
|
||||
if (!isEmpty(value)) {
|
||||
|
|
|
@ -372,6 +372,8 @@ export interface PublicAlertsDataGridProps
|
|||
| 'columns'
|
||||
> {
|
||||
ruleTypeIds: string[];
|
||||
minScore?: number;
|
||||
trackScores?: boolean;
|
||||
consumers?: string[];
|
||||
/**
|
||||
* If true, shows a button in the table toolbar to inspect the search alerts request
|
||||
|
|
|
@ -32435,8 +32435,6 @@
|
|||
"xpack.observability.pages.alertDetails.pageTitle.ruleName": "Règle",
|
||||
"xpack.observability.pages.alertDetails.pageTitle.title": "{ruleCategory} {ruleCategory, select, Anomaly {détectée} Inventory {seuil dépassé} other {dépassés}}",
|
||||
"xpack.observability.pages.alertDetails.pageTitle.triggered": "Déclenché",
|
||||
"xpack.observability.pages.alertDetails.relatedAlerts.empty.description": "En raison d'une erreur inattendue, aucune alerte associée ne peut être trouvée.",
|
||||
"xpack.observability.pages.alertDetails.relatedAlerts.empty.title": "Problème de chargement des alertes associées",
|
||||
"xpack.observability.profilingAWSCostDiscountRateUiSettingDescription": "Si vous êtes inscrits au programme de réduction AWS Enterprise Discount Program (EDP), entrez votre taux de réduction pour mettre à jour le calcul des coûts de profilage.",
|
||||
"xpack.observability.profilingAWSCostDiscountRateUiSettingName": "Taux de réduction AWS EDP (%)",
|
||||
"xpack.observability.profilingAzureCostDiscountRateUiSettingDescription": "Si vous avez un accord Azure Enterprise avec Microsoft, saisissez votre taux de réduction pour mettre à jour le calcul du coût de profilage.",
|
||||
|
|
|
@ -32415,8 +32415,6 @@
|
|||
"xpack.observability.pages.alertDetails.pageTitle.ruleName": "ルール",
|
||||
"xpack.observability.pages.alertDetails.pageTitle.title": "{ruleCategory} {ruleCategory, select, Anomaly {検出されました} Inventory {しきい値に違反しました} other {違反しました}}",
|
||||
"xpack.observability.pages.alertDetails.pageTitle.triggered": "実行済み",
|
||||
"xpack.observability.pages.alertDetails.relatedAlerts.empty.description": "予期しないエラーのため、関連するアラートが見つかりません。",
|
||||
"xpack.observability.pages.alertDetails.relatedAlerts.empty.title": "関連するアラートの読み込みエラー",
|
||||
"xpack.observability.profilingAWSCostDiscountRateUiSettingDescription": "AWS Enterprise Discount Program(EDP)に加入している場合は、割引率を入力してプロファイリング費用の計算を更新します。",
|
||||
"xpack.observability.profilingAWSCostDiscountRateUiSettingName": "AWS EDP割引率(%)",
|
||||
"xpack.observability.profilingAzureCostDiscountRateUiSettingDescription": "MicrosoftとのAzureエンタープライズ契約がある場合は、割引率を入力して、プロファイリングコスト計算を更新してください。",
|
||||
|
|
|
@ -32470,8 +32470,6 @@
|
|||
"xpack.observability.pages.alertDetails.pageTitle.ruleName": "规则",
|
||||
"xpack.observability.pages.alertDetails.pageTitle.title": "{ruleCategory} {ruleCategory, select, Anomaly {已检测到} Inventory {超出阈值} other {已超出}}",
|
||||
"xpack.observability.pages.alertDetails.pageTitle.triggered": "已触发",
|
||||
"xpack.observability.pages.alertDetails.relatedAlerts.empty.description": "由于出现意外错误,找不到相关告警。",
|
||||
"xpack.observability.pages.alertDetails.relatedAlerts.empty.title": "加载相关告警时出现问题",
|
||||
"xpack.observability.profilingAWSCostDiscountRateUiSettingDescription": "如果已加入 AWS 企业折扣计划 (EDP),请输入您的折扣率以更新分析成本计算。",
|
||||
"xpack.observability.profilingAWSCostDiscountRateUiSettingName": "AWS EDP 折扣率 (%)",
|
||||
"xpack.observability.profilingAzureCostDiscountRateUiSettingDescription": "如果与 Microsoft 签署了 Azure 企业协议,请输入您的折扣率以更新分析成本计算。",
|
||||
|
|
|
@ -716,4 +716,107 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('passes the min_score if minScore is provided', async () => {
|
||||
const minScore = 0.5;
|
||||
const request: RuleRegistrySearchRequest = {
|
||||
ruleTypeIds: ['siem.esqlRule'],
|
||||
minScore,
|
||||
};
|
||||
const options = {};
|
||||
const deps = {
|
||||
request: {},
|
||||
};
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([]);
|
||||
getAlertIndicesAliasMock.mockReturnValue(['security-siem']);
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(data, alerting, logger, security, spaces);
|
||||
|
||||
await lastValueFrom(
|
||||
strategy.search(request, options, deps as unknown as SearchStrategyDependencies)
|
||||
);
|
||||
|
||||
const arg0 = searchStrategySearch.mock.calls[0][0];
|
||||
expect(arg0.params.body.fields.length).toEqual(
|
||||
// +2 because of fields.push({ field: 'kibana.alert.*', include_unmapped: false }); and
|
||||
// fields.push({ field: 'signal.*', include_unmapped: false });
|
||||
ALERT_EVENTS_FIELDS.length + 2
|
||||
);
|
||||
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
x: 2,
|
||||
y: 3,
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(arg0).toEqual(
|
||||
expect.objectContaining({
|
||||
id: undefined,
|
||||
params: expect.objectContaining({
|
||||
allow_no_indices: true,
|
||||
body: expect.objectContaining({
|
||||
_source: false,
|
||||
fields: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: '@timestamp',
|
||||
include_unmapped: true,
|
||||
}),
|
||||
]),
|
||||
from: 0,
|
||||
min_score: minScore,
|
||||
size: 1000,
|
||||
sort: [],
|
||||
}),
|
||||
ignore_unavailable: true,
|
||||
index: ['security-siem'],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('passes track_scores if trackScores is provided', async () => {
|
||||
const request: RuleRegistrySearchRequest = {
|
||||
ruleTypeIds: ['siem.esqlRule'],
|
||||
trackScores: true,
|
||||
};
|
||||
const options = {};
|
||||
const deps = {
|
||||
request: {},
|
||||
};
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([]);
|
||||
getAlertIndicesAliasMock.mockReturnValue(['security-siem']);
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(data, alerting, logger, security, spaces);
|
||||
|
||||
await lastValueFrom(
|
||||
strategy.search(request, options, deps as unknown as SearchStrategyDependencies)
|
||||
);
|
||||
|
||||
const arg0 = searchStrategySearch.mock.calls[0][0];
|
||||
|
||||
expect(arg0).toEqual(
|
||||
expect.objectContaining({
|
||||
id: undefined,
|
||||
params: expect.objectContaining({
|
||||
allow_no_indices: true,
|
||||
body: expect.objectContaining({
|
||||
_source: false,
|
||||
fields: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: '@timestamp',
|
||||
include_unmapped: true,
|
||||
}),
|
||||
]),
|
||||
from: 0,
|
||||
size: 1000,
|
||||
sort: [],
|
||||
track_scores: true,
|
||||
}),
|
||||
ignore_unavailable: true,
|
||||
index: ['security-siem'],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -172,6 +172,8 @@ export const ruleRegistrySearchStrategyProvider = (
|
|||
from: request.pagination ? request.pagination.pageIndex * size : 0,
|
||||
query,
|
||||
...(request.runtimeMappings ? { runtime_mappings: request.runtimeMappings } : {}),
|
||||
...(request.minScore ? { min_score: request.minScore } : {}),
|
||||
...(request.trackScores ? { track_scores: request.trackScores } : {}),
|
||||
},
|
||||
};
|
||||
return (isAnyRuleTypeESAuthorized ? requestUserEs : internalUserEs).search(
|
||||
|
|
|
@ -9,6 +9,7 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { PerformanceContextProvider } from '@kbn/ebt-tools';
|
||||
import { InspectorContextProvider } from '@kbn/observability-shared-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Router, Routes, Route } from '@kbn/shared-ux-router';
|
||||
import { AppMountParameters, APP_WRAPPER_CLASS, CoreStart } from '@kbn/core/public';
|
||||
|
@ -114,8 +115,10 @@ export const renderApp = ({
|
|||
<RedirectAppLinks coreStart={core} data-test-subj="observabilityMainContainer">
|
||||
<PerformanceContextProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
<HideableReactQueryDevTools />
|
||||
<InspectorContextProvider>
|
||||
<App />
|
||||
<HideableReactQueryDevTools />
|
||||
</InspectorContextProvider>
|
||||
</QueryClientProvider>
|
||||
</PerformanceContextProvider>
|
||||
</RedirectAppLinks>
|
||||
|
|
|
@ -29,7 +29,7 @@ import { ObservabilityRuleTypeRegistry } from '../../rules/create_observability_
|
|||
import type { GetObservabilityAlertsTableProp } from '../..';
|
||||
import { AlertsTableContextProvider } from '@kbn/response-ops-alerts-table/contexts/alerts_table_context';
|
||||
import { AdditionalContext, RenderContext } from '@kbn/response-ops-alerts-table/types';
|
||||
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
const refresh = jest.fn();
|
||||
const caseHooksReturnedValue = {
|
||||
open: () => {
|
||||
|
@ -82,6 +82,15 @@ jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
|
|||
ObservabilityPageTemplate: KibanaPageTemplate,
|
||||
ObservabilityAIAssistantContextualInsight,
|
||||
}));
|
||||
jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
|
||||
appMountParameters: {} as AppMountParameters,
|
||||
core: {} as CoreStart,
|
||||
config,
|
||||
plugins: {} as ObservabilityPublicPluginsStart,
|
||||
observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(),
|
||||
ObservabilityPageTemplate: KibanaPageTemplate,
|
||||
ObservabilityAIAssistantContextualInsight,
|
||||
}));
|
||||
|
||||
describe('ObservabilityActions component', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -145,15 +154,17 @@ describe('ObservabilityActions component', () => {
|
|||
|
||||
const wrapper = mountWithIntl(
|
||||
<Router history={createMemoryHistory()}>
|
||||
<AlertsTableContextProvider value={context}>
|
||||
<QueryClientProvider client={queryClient} context={AlertsQueryContext}>
|
||||
<AlertActions
|
||||
{...(props as unknown as ComponentProps<
|
||||
GetObservabilityAlertsTableProp<'renderActionsCell'>
|
||||
>)}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</AlertsTableContextProvider>
|
||||
<KibanaContextProvider services={mockKibana.services}>
|
||||
<AlertsTableContextProvider value={context}>
|
||||
<QueryClientProvider client={queryClient} context={AlertsQueryContext}>
|
||||
<AlertActions
|
||||
{...(props as unknown as ComponentProps<
|
||||
GetObservabilityAlertsTableProp<'renderActionsCell'>
|
||||
>)}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</AlertsTableContextProvider>
|
||||
</KibanaContextProvider>
|
||||
</Router>
|
||||
);
|
||||
await act(async () => {
|
||||
|
|
|
@ -16,86 +16,45 @@ import {
|
|||
|
||||
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { CaseAttachmentsWithoutOwner, CasesPublicStart } from '@kbn/cases-plugin/public';
|
||||
import { AttachmentType } from '@kbn/cases-plugin/common';
|
||||
import { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
|
||||
import { CasesPublicStart } from '@kbn/cases-plugin/public';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { SLO_ALERTS_TABLE_ID } from '@kbn/observability-shared-plugin/common';
|
||||
import { DefaultAlertActions } from '@kbn/response-ops-alerts-table/components/default_alert_actions';
|
||||
import { useAlertsTableContext } from '@kbn/response-ops-alerts-table/contexts/alerts_table_context';
|
||||
import { ALERT_UUID } from '@kbn/rule-data-utils';
|
||||
import type { EventNonEcsData } from '../../../common/typings';
|
||||
import { GetObservabilityAlertsTableProp } from '../alerts_table/types';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { useCaseActions } from './use_case_actions';
|
||||
import { RULE_DETAILS_PAGE_ID } from '../../pages/rule_details/constants';
|
||||
import { paths, SLO_DETAIL_PATH } from '../../../common/locators/paths';
|
||||
import { parseAlert } from '../../pages/alerts/helpers/parse_alert';
|
||||
import { observabilityFeatureId } from '../..';
|
||||
import {
|
||||
GetObservabilityAlertsTableProp,
|
||||
ObservabilityAlertsTableContext,
|
||||
observabilityFeatureId,
|
||||
} from '../..';
|
||||
import { ALERT_DETAILS_PAGE_ID } from '../../pages/alert_details/alert_details';
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export const AlertActions: GetObservabilityAlertsTableProp<'renderActionsCell'> = ({
|
||||
config,
|
||||
observabilityRuleTypeRegistry,
|
||||
alert,
|
||||
id,
|
||||
tableId,
|
||||
dataGridRef,
|
||||
refresh,
|
||||
isLoading,
|
||||
isLoadingAlerts,
|
||||
alerts,
|
||||
oldAlertsData,
|
||||
ecsAlertsData,
|
||||
alertsCount,
|
||||
browserFields,
|
||||
isLoadingMutedAlerts,
|
||||
mutedAlerts,
|
||||
isLoadingCases,
|
||||
cases,
|
||||
isLoadingMaintenanceWindows,
|
||||
maintenanceWindows,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
openAlertInFlyout,
|
||||
showAlertStatusWithFlapping,
|
||||
bulkActionsStore,
|
||||
columns,
|
||||
renderCellValue,
|
||||
renderCellPopover,
|
||||
renderActionsCell,
|
||||
renderFlyoutHeader,
|
||||
renderFlyoutBody,
|
||||
renderFlyoutFooter,
|
||||
parentAlert,
|
||||
...rest
|
||||
}) => {
|
||||
const { services } = useAlertsTableContext();
|
||||
const services = useKibana().services;
|
||||
const {
|
||||
http: {
|
||||
basePath: { prepend },
|
||||
},
|
||||
} = services;
|
||||
const {
|
||||
helpers: { getRuleIdFromEvent, canUseCases },
|
||||
hooks: { useCasesAddToNewCaseFlyout, useCasesAddToExistingCaseModal },
|
||||
helpers: { canUseCases },
|
||||
} = services.cases! as unknown as CasesPublicStart; // Cases is guaranteed to be defined in Observability
|
||||
const isSLODetailsPage = useRouteMatch(SLO_DETAIL_PATH);
|
||||
|
||||
const isInApp = Boolean(id === SLO_ALERTS_TABLE_ID && isSLODetailsPage);
|
||||
const data = useMemo(
|
||||
() =>
|
||||
Object.entries(alert ?? {}).reduce<EventNonEcsData[]>(
|
||||
(acc, [field, value]) => [...acc, { field, value: value as string[] }],
|
||||
[]
|
||||
),
|
||||
[alert]
|
||||
);
|
||||
const isInApp = Boolean(tableId === SLO_ALERTS_TABLE_ID && isSLODetailsPage);
|
||||
|
||||
const ecsData = useMemo<Ecs>(
|
||||
() => ({
|
||||
_id: alert._id,
|
||||
_index: alert._index,
|
||||
}),
|
||||
[alert._id, alert._index]
|
||||
);
|
||||
const userCasesPermissions = canUseCases([observabilityFeatureId]);
|
||||
const [viewInAppUrl, setViewInAppUrl] = useState<string>();
|
||||
|
||||
|
@ -125,128 +84,20 @@ export const AlertActions: GetObservabilityAlertsTableProp<'renderActionsCell'>
|
|||
}
|
||||
}, [observabilityAlert.link, observabilityAlert.hasBasePath, prepend]);
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
const { isPopoverOpen, setIsPopoverOpen, handleAddToExistingCaseClick, handleAddToNewCaseClick } =
|
||||
useCaseActions({
|
||||
refresh,
|
||||
alerts: [alert],
|
||||
});
|
||||
|
||||
const caseAttachments: CaseAttachmentsWithoutOwner = useMemo(() => {
|
||||
return ecsData?._id
|
||||
? [
|
||||
{
|
||||
alertId: ecsData?._id ?? '',
|
||||
index: ecsData?._index ?? '',
|
||||
type: AttachmentType.alert,
|
||||
rule: getRuleIdFromEvent({ ecs: ecsData, data: data ?? [] }),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
}, [ecsData, getRuleIdFromEvent, data]);
|
||||
|
||||
const onSuccess = useCallback(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const createCaseFlyout = useCasesAddToNewCaseFlyout({ onSuccess });
|
||||
const selectCaseModal = useCasesAddToExistingCaseModal({ onSuccess });
|
||||
|
||||
const closeActionsPopover = () => {
|
||||
const closeActionsPopover = useCallback(() => {
|
||||
setIsPopoverOpen(false);
|
||||
};
|
||||
}, [setIsPopoverOpen]);
|
||||
|
||||
const toggleActionsPopover = () => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
};
|
||||
|
||||
const handleAddToNewCaseClick = () => {
|
||||
createCaseFlyout.open({ attachments: caseAttachments });
|
||||
closeActionsPopover();
|
||||
};
|
||||
|
||||
const handleAddToExistingCaseClick = () => {
|
||||
selectCaseModal.open({ getAttachments: () => caseAttachments });
|
||||
closeActionsPopover();
|
||||
};
|
||||
|
||||
const defaultRowActions = useMemo(
|
||||
() => (
|
||||
<DefaultAlertActions
|
||||
key="defaultRowActions"
|
||||
onActionExecuted={closeActionsPopover}
|
||||
isAlertDetailsEnabled={true}
|
||||
resolveRulePagePath={(ruleId, currentPageId) =>
|
||||
currentPageId !== RULE_DETAILS_PAGE_ID ? paths.observability.ruleDetails(ruleId) : null
|
||||
}
|
||||
resolveAlertPagePath={(alertId, currentPageId) =>
|
||||
currentPageId !== ALERT_DETAILS_PAGE_ID ? paths.observability.alertDetails(alertId) : null
|
||||
}
|
||||
tableId={tableId}
|
||||
dataGridRef={dataGridRef}
|
||||
refresh={refresh}
|
||||
isLoading={isLoading}
|
||||
isLoadingAlerts={isLoadingAlerts}
|
||||
alert={alert}
|
||||
alerts={alerts}
|
||||
oldAlertsData={oldAlertsData}
|
||||
ecsAlertsData={ecsAlertsData}
|
||||
alertsCount={alertsCount}
|
||||
browserFields={browserFields}
|
||||
isLoadingMutedAlerts={isLoadingMutedAlerts}
|
||||
mutedAlerts={mutedAlerts}
|
||||
isLoadingCases={isLoadingCases}
|
||||
cases={cases}
|
||||
isLoadingMaintenanceWindows={isLoadingMaintenanceWindows}
|
||||
maintenanceWindows={maintenanceWindows}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
openAlertInFlyout={openAlertInFlyout}
|
||||
showAlertStatusWithFlapping={showAlertStatusWithFlapping}
|
||||
bulkActionsStore={bulkActionsStore}
|
||||
columns={columns}
|
||||
renderCellValue={renderCellValue}
|
||||
renderCellPopover={renderCellPopover}
|
||||
renderActionsCell={renderActionsCell}
|
||||
renderFlyoutHeader={renderFlyoutHeader}
|
||||
renderFlyoutBody={renderFlyoutBody}
|
||||
renderFlyoutFooter={renderFlyoutFooter}
|
||||
services={services}
|
||||
config={config}
|
||||
observabilityRuleTypeRegistry={observabilityRuleTypeRegistry}
|
||||
/>
|
||||
),
|
||||
[
|
||||
alert,
|
||||
alerts,
|
||||
alertsCount,
|
||||
browserFields,
|
||||
bulkActionsStore,
|
||||
cases,
|
||||
columns,
|
||||
config,
|
||||
dataGridRef,
|
||||
ecsAlertsData,
|
||||
isLoading,
|
||||
isLoadingAlerts,
|
||||
isLoadingCases,
|
||||
isLoadingMaintenanceWindows,
|
||||
isLoadingMutedAlerts,
|
||||
maintenanceWindows,
|
||||
mutedAlerts,
|
||||
observabilityRuleTypeRegistry,
|
||||
oldAlertsData,
|
||||
openAlertInFlyout,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
refresh,
|
||||
renderActionsCell,
|
||||
renderCellPopover,
|
||||
renderCellValue,
|
||||
renderFlyoutBody,
|
||||
renderFlyoutFooter,
|
||||
renderFlyoutHeader,
|
||||
services,
|
||||
showAlertStatusWithFlapping,
|
||||
tableId,
|
||||
]
|
||||
);
|
||||
|
||||
const actionsMenuItems = [
|
||||
...(userCasesPermissions.createComment && userCasesPermissions.read
|
||||
? [
|
||||
|
@ -272,7 +123,38 @@ export const AlertActions: GetObservabilityAlertsTableProp<'renderActionsCell'>
|
|||
</EuiContextMenuItem>,
|
||||
]
|
||||
: []),
|
||||
defaultRowActions,
|
||||
useMemo(
|
||||
() => (
|
||||
<DefaultAlertActions<ObservabilityAlertsTableContext>
|
||||
observabilityRuleTypeRegistry={observabilityRuleTypeRegistry}
|
||||
key="defaultRowActions"
|
||||
onActionExecuted={closeActionsPopover}
|
||||
isAlertDetailsEnabled={true}
|
||||
resolveRulePagePath={(ruleId, currentPageId) =>
|
||||
currentPageId !== RULE_DETAILS_PAGE_ID ? paths.observability.ruleDetails(ruleId) : null
|
||||
}
|
||||
resolveAlertPagePath={(alertId, currentPageId) =>
|
||||
currentPageId !== ALERT_DETAILS_PAGE_ID
|
||||
? paths.observability.alertDetails(alertId)
|
||||
: null
|
||||
}
|
||||
tableId={tableId}
|
||||
refresh={refresh}
|
||||
alert={alert}
|
||||
openAlertInFlyout={openAlertInFlyout}
|
||||
{...rest}
|
||||
/>
|
||||
),
|
||||
[
|
||||
alert,
|
||||
closeActionsPopover,
|
||||
observabilityRuleTypeRegistry,
|
||||
openAlertInFlyout,
|
||||
refresh,
|
||||
rest,
|
||||
tableId,
|
||||
]
|
||||
),
|
||||
];
|
||||
|
||||
const actionsToolTip =
|
||||
|
@ -286,24 +168,27 @@ export const AlertActions: GetObservabilityAlertsTableProp<'renderActionsCell'>
|
|||
|
||||
const onExpandEvent = () => {
|
||||
const parsedAlert = parseAlert(observabilityRuleTypeRegistry)(alert);
|
||||
|
||||
openAlertInFlyout?.(parsedAlert.fields[ALERT_UUID]);
|
||||
};
|
||||
|
||||
const hideViewInApp = isInApp || viewInAppUrl === '' || parentAlert;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip data-test-subj="expand-event-tool-tip" content={VIEW_DETAILS}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="expand-event"
|
||||
iconType="expand"
|
||||
onClick={onExpandEvent}
|
||||
size="s"
|
||||
color="text"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
{viewInAppUrl !== '' && !isInApp ? (
|
||||
{!parentAlert && (
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip data-test-subj="expand-event-tool-tip" content={VIEW_DETAILS}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="expand-event"
|
||||
iconType="expand"
|
||||
onClick={onExpandEvent}
|
||||
size="s"
|
||||
color="text"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{!hideViewInApp && (
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.observability.alertsTable.viewInAppTextLabel', {
|
||||
|
@ -323,12 +208,13 @@ export const AlertActions: GetObservabilityAlertsTableProp<'renderActionsCell'>
|
|||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
<EuiFlexItem
|
||||
css={{
|
||||
textAlign: 'center',
|
||||
}}
|
||||
grow={parentAlert ? false : undefined}
|
||||
>
|
||||
<EuiPopover
|
||||
anchorPosition="downLeft"
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 { CaseAttachmentsWithoutOwner, CasesPublicStart } from '@kbn/cases-plugin/public';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { AttachmentType } from '@kbn/cases-plugin/common';
|
||||
import type { Alert } from '@kbn/alerting-types';
|
||||
import type { EventNonEcsData } from '../../../common/typings';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
|
||||
export const useCaseActions = ({ alerts, refresh }: { alerts: Alert[]; refresh?: () => void }) => {
|
||||
const services = useKibana().services;
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
const {
|
||||
helpers: { getRuleIdFromEvent },
|
||||
hooks: { useCasesAddToNewCaseFlyout, useCasesAddToExistingCaseModal },
|
||||
} = services.cases! as unknown as CasesPublicStart; // Cases is guaranteed to be defined in Observability
|
||||
|
||||
const onSuccess = useCallback(() => {
|
||||
refresh?.();
|
||||
}, [refresh]);
|
||||
|
||||
const selectCaseModal = useCasesAddToExistingCaseModal({ onSuccess });
|
||||
|
||||
function getCaseAttachments(): CaseAttachmentsWithoutOwner {
|
||||
return alerts.map((alert) => ({
|
||||
alertId: alert?._id ?? '',
|
||||
index: alert?._index ?? '',
|
||||
type: AttachmentType.alert,
|
||||
rule: getRuleIdFromEvent({
|
||||
ecs: {
|
||||
_id: alert?._id ?? '',
|
||||
_index: alert?._index ?? '',
|
||||
},
|
||||
data:
|
||||
Object.entries(alert ?? {}).reduce<EventNonEcsData[]>(
|
||||
(acc, [field, value]) => [...acc, { field, value: value as string[] }],
|
||||
[]
|
||||
) ?? [],
|
||||
}),
|
||||
}));
|
||||
}
|
||||
const createCaseFlyout = useCasesAddToNewCaseFlyout({ onSuccess });
|
||||
const closeActionsPopover = () => {
|
||||
setIsPopoverOpen(false);
|
||||
};
|
||||
|
||||
const handleAddToNewCaseClick = () => {
|
||||
createCaseFlyout.open({ attachments: getCaseAttachments() });
|
||||
closeActionsPopover();
|
||||
};
|
||||
|
||||
const handleAddToExistingCaseClick = () => {
|
||||
selectCaseModal.open({ getAttachments: () => getCaseAttachments() });
|
||||
closeActionsPopover();
|
||||
};
|
||||
|
||||
return {
|
||||
isPopoverOpen,
|
||||
setIsPopoverOpen,
|
||||
handleAddToExistingCaseClick,
|
||||
handleAddToNewCaseClick,
|
||||
};
|
||||
};
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { EuiToolTip } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
value: React.ReactNode;
|
||||
tooltipContent: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/
|
|||
import { render } from '../../../utils/test_helper';
|
||||
import { AlertsTableCellValue } from './cell_value';
|
||||
import { Alert } from '@kbn/alerting-types';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
|
||||
interface AlertsTableRow {
|
||||
alertStatus: typeof ALERT_STATUS_ACTIVE | typeof ALERT_STATUS_RECOVERED;
|
||||
|
@ -66,4 +67,5 @@ const requiredProperties = {
|
|||
isDraggable: false,
|
||||
linkValues: [],
|
||||
scopeId: '',
|
||||
services: coreMock.createStart(),
|
||||
} as unknown as ComponentProps<typeof AlertsTableCellValue>;
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { EuiLink, EuiText, EuiFlexGroup } from '@elastic/eui';
|
||||
import React, { ReactNode } from 'react';
|
||||
import {
|
||||
ALERT_DURATION,
|
||||
ALERT_SEVERITY,
|
||||
|
@ -21,20 +21,31 @@ import {
|
|||
ALERT_RULE_CATEGORY,
|
||||
ALERT_START,
|
||||
ALERT_RULE_EXECUTION_TIMESTAMP,
|
||||
ALERT_RULE_UUID,
|
||||
ALERT_CASE_IDS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { isEmpty } from 'lodash';
|
||||
import type { Alert } from '@kbn/alerting-types';
|
||||
import type { JsonValue } from '@kbn/utility-types';
|
||||
import {
|
||||
RELATED_ACTIONS_COL,
|
||||
RELATED_ALERT_REASON,
|
||||
RELATION_COL,
|
||||
} from '../../../pages/alert_details/components/related_alerts/get_related_columns';
|
||||
import { RelationCol } from '../../../pages/alert_details/components/related_alerts/relation_col';
|
||||
import { paths } from '../../../../common/locators/paths';
|
||||
import { asDuration } from '../../../../common/utils/formatters';
|
||||
import { AlertSeverityBadge } from '../../alert_severity_badge';
|
||||
import { AlertStatusIndicator } from '../../alert_status_indicator';
|
||||
import { parseAlert } from '../../../pages/alerts/helpers/parse_alert';
|
||||
import { CellTooltip } from './cell_tooltip';
|
||||
import { TimestampTooltip } from './timestamp_tooltip';
|
||||
import type { GetObservabilityAlertsTableProp } from '../types';
|
||||
import { GetObservabilityAlertsTableProp } from '../types';
|
||||
import AlertActions from '../../alert_actions/alert_actions';
|
||||
|
||||
const getAlertFieldValue = (alert: Alert, fieldName: string) => {
|
||||
export const getAlertFieldValue = (alert: Alert, fieldName: string) => {
|
||||
// can be updated when working on https://github.com/elastic/kibana/issues/140819
|
||||
const rawValue = alert[fieldName];
|
||||
const rawValue = alert[fieldName] as JsonValue[];
|
||||
const value = Array.isArray(rawValue) ? rawValue.join() : rawValue;
|
||||
|
||||
if (!isEmpty(value)) {
|
||||
|
@ -51,40 +62,49 @@ const getAlertFieldValue = (alert: Alert, fieldName: string) => {
|
|||
return '--';
|
||||
};
|
||||
|
||||
export type AlertCellRenderers = Record<string, (value: string) => ReactNode>;
|
||||
|
||||
/**
|
||||
* This implementation of `EuiDataGrid`'s `renderCellValue`
|
||||
* accepts `EuiDataGridCellValueElementProps`, plus `data`
|
||||
* from the TGrid
|
||||
*/
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
export const AlertsTableCellValue: GetObservabilityAlertsTableProp<'renderCellValue'> = ({
|
||||
columnId,
|
||||
alert,
|
||||
openAlertInFlyout,
|
||||
observabilityRuleTypeRegistry,
|
||||
}) => {
|
||||
const value = getAlertFieldValue(alert, columnId);
|
||||
export const AlertsTableCellValue: GetObservabilityAlertsTableProp<'renderCellValue'> = (props) => {
|
||||
const {
|
||||
columnId,
|
||||
alert,
|
||||
openAlertInFlyout,
|
||||
observabilityRuleTypeRegistry,
|
||||
services: { http },
|
||||
parentAlert,
|
||||
} = props;
|
||||
|
||||
switch (columnId) {
|
||||
case ALERT_STATUS:
|
||||
const cellRenderers: AlertCellRenderers = {
|
||||
[ALERT_STATUS]: (value) => {
|
||||
if (value !== ALERT_STATUS_ACTIVE && value !== ALERT_STATUS_RECOVERED) {
|
||||
// NOTE: This should only be needed to narrow down the type.
|
||||
// Status should be either "active" or "recovered".
|
||||
return null;
|
||||
}
|
||||
return <AlertStatusIndicator alertStatus={value} />;
|
||||
case TIMESTAMP:
|
||||
case ALERT_START:
|
||||
case ALERT_RULE_EXECUTION_TIMESTAMP:
|
||||
return <TimestampTooltip time={new Date(value ?? '').getTime()} timeUnit="milliseconds" />;
|
||||
case ALERT_DURATION:
|
||||
return <>{asDuration(Number(value))}</>;
|
||||
case ALERT_SEVERITY:
|
||||
return <AlertSeverityBadge severityLevel={value ?? undefined} />;
|
||||
case ALERT_EVALUATION_VALUE:
|
||||
},
|
||||
[TIMESTAMP]: (value) => (
|
||||
<TimestampTooltip time={new Date(value ?? '').getTime()} timeUnit="milliseconds" />
|
||||
),
|
||||
[ALERT_START]: (value) => (
|
||||
<TimestampTooltip time={new Date(value ?? '').getTime()} timeUnit="milliseconds" />
|
||||
),
|
||||
[ALERT_RULE_EXECUTION_TIMESTAMP]: (value) => (
|
||||
<TimestampTooltip time={new Date(value ?? '').getTime()} timeUnit="milliseconds" />
|
||||
),
|
||||
[ALERT_DURATION]: (value) => <>{asDuration(Number(value))}</>,
|
||||
[ALERT_SEVERITY]: (value) => <AlertSeverityBadge severityLevel={value ?? undefined} />,
|
||||
[ALERT_EVALUATION_VALUE]: (value) => {
|
||||
const multipleValues = getAlertFieldValue(alert, ALERT_EVALUATION_VALUES);
|
||||
return <>{multipleValues ?? value}</>;
|
||||
case ALERT_REASON:
|
||||
},
|
||||
[ALERT_REASON]: (value) => {
|
||||
if (!observabilityRuleTypeRegistry) return <>{value}</>;
|
||||
const parsedAlert = parseAlert(observabilityRuleTypeRegistry)(alert);
|
||||
return (
|
||||
|
@ -96,10 +116,42 @@ export const AlertsTableCellValue: GetObservabilityAlertsTableProp<'renderCellVa
|
|||
{parsedAlert.reason}
|
||||
</EuiLink>
|
||||
);
|
||||
case ALERT_RULE_NAME:
|
||||
},
|
||||
[ALERT_RULE_NAME]: (value) => {
|
||||
const ruleCategory = getAlertFieldValue(alert, ALERT_RULE_CATEGORY);
|
||||
return <CellTooltip value={value} tooltipContent={ruleCategory} />;
|
||||
default:
|
||||
const ruleId = getAlertFieldValue(alert, ALERT_RULE_UUID);
|
||||
const ruleLink = ruleId ? http.basePath.prepend(paths.observability.ruleDetails(ruleId)) : '';
|
||||
return (
|
||||
<CellTooltip
|
||||
value={
|
||||
<EuiLink data-test-subj="o11yCellRenderersLink" href={ruleLink}>
|
||||
{value}
|
||||
</EuiLink>
|
||||
}
|
||||
tooltipContent={ruleCategory}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[RELATION_COL]: (value) => {
|
||||
return <RelationCol alert={alert} parentAlert={parentAlert!} />;
|
||||
},
|
||||
[RELATED_ALERT_REASON]: (value) => {
|
||||
const val = getAlertFieldValue(alert, ALERT_REASON);
|
||||
return <EuiText size="s">{val}</EuiText>;
|
||||
},
|
||||
[RELATED_ACTIONS_COL]: (val) => {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<AlertActions {...props} />
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
[ALERT_CASE_IDS]: (value) => {
|
||||
return <>{value}</>;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const val = getAlertFieldValue(alert, columnId);
|
||||
|
||||
return cellRenderers[columnId] ? cellRenderers[columnId](val) : <>{val}</>;
|
||||
};
|
||||
|
|
|
@ -10,12 +10,12 @@ import {
|
|||
ALERT_EVALUATION_VALUE,
|
||||
ALERT_EVALUATION_THRESHOLD,
|
||||
ALERT_DURATION,
|
||||
ALERT_REASON,
|
||||
ALERT_RULE_NAME,
|
||||
ALERT_START,
|
||||
ALERT_STATUS,
|
||||
ALERT_INSTANCE_ID,
|
||||
TAGS,
|
||||
ALERT_REASON,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
|
|
|
@ -7,11 +7,12 @@
|
|||
|
||||
import { SetOptional } from 'type-fest';
|
||||
import type { AlertsTablePropsWithRef } from '@kbn/response-ops-alerts-table/types';
|
||||
import type { ConfigSchema, ObservabilityRuleTypeRegistry } from '../..';
|
||||
import type { ConfigSchema, ObservabilityRuleTypeRegistry, TopAlert } from '../..';
|
||||
|
||||
export interface ObservabilityAlertsTableContext {
|
||||
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry;
|
||||
config: ConfigSchema;
|
||||
parentAlert?: TopAlert;
|
||||
}
|
||||
|
||||
export type ObservabilityAlertsTableProps = SetOptional<
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
// TODO: https://github.com/elastic/kibana/issues/110905
|
||||
|
||||
import { PluginInitializer, PluginInitializerContext } from '@kbn/core/public';
|
||||
import { lazy } from 'react';
|
||||
import {
|
||||
Plugin,
|
||||
ObservabilityPublicPluginsStart,
|
||||
|
@ -57,8 +56,6 @@ export { getCoreVitalsComponent } from './pages/overview/components/sections/ux/
|
|||
export { ObservabilityAlertSearchBar } from './components/alert_search_bar/get_alert_search_bar_lazy';
|
||||
export { DatePicker } from './pages/overview/components/date_picker';
|
||||
|
||||
export const LazyAlertsFlyout = lazy(() => import('./components/alerts_flyout/alerts_flyout'));
|
||||
|
||||
export type {
|
||||
Stat,
|
||||
Coordinates,
|
||||
|
|
|
@ -35,7 +35,7 @@ import { AlertFieldsTable } from '@kbn/alerts-ui-shared/src/alert_fields_table';
|
|||
import { css } from '@emotion/react';
|
||||
import { omit } from 'lodash';
|
||||
import { BetaBadge } from '../../components/experimental_badge';
|
||||
import { RelatedAlerts } from './components/related_alerts';
|
||||
import { RelatedAlerts } from './components/related_alerts/related_alerts';
|
||||
import { AlertDetailsSource } from './types';
|
||||
import { SourceBar } from './components';
|
||||
import { StatusBar } from './components/status_bar';
|
||||
|
@ -305,7 +305,7 @@ export function AlertDetails() {
|
|||
</>
|
||||
),
|
||||
'data-test-subj': 'relatedAlertsTab',
|
||||
content: <RelatedAlerts alert={alertDetail?.formatted} />,
|
||||
content: <RelatedAlerts alertData={alertDetail} />,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -342,15 +342,21 @@ export function AlertDetails() {
|
|||
}}
|
||||
data-test-subj="alertDetails"
|
||||
>
|
||||
<StatusBar alert={alertDetail?.formatted ?? null} alertStatus={alertStatus} />
|
||||
<EuiSpacer size="l" />
|
||||
<HeaderMenu />
|
||||
<EuiTabbedContent
|
||||
data-test-subj="alertDetailsTabbedContent"
|
||||
tabs={tabs}
|
||||
selectedTab={tabs.find((tab) => tab.id === activeTabId)}
|
||||
onTabClick={(tab) => handleSetTabId(tab.id as TabId)}
|
||||
/>
|
||||
<CasesContext
|
||||
owner={[observabilityFeatureId]}
|
||||
permissions={userCasesPermissions}
|
||||
features={{ alerts: { sync: false } }}
|
||||
>
|
||||
<StatusBar alert={alertDetail?.formatted ?? null} alertStatus={alertStatus} />
|
||||
<EuiSpacer size="l" />
|
||||
<HeaderMenu />
|
||||
<EuiTabbedContent
|
||||
data-test-subj="alertDetailsTabbedContent"
|
||||
tabs={tabs}
|
||||
selectedTab={tabs.find((tab) => tab.id === activeTabId)}
|
||||
onTabClick={(tab) => handleSetTabId(tab.id as TabId)}
|
||||
/>
|
||||
</CasesContext>
|
||||
</ObservabilityPageTemplate>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { EuiHeaderLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useInspectorContext } from '@kbn/observability-shared-plugin/public';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
|
||||
export function InspectorHeaderLink() {
|
||||
const {
|
||||
services: { inspector },
|
||||
} = useKibana();
|
||||
|
||||
const { inspectorAdapters } = useInspectorContext();
|
||||
|
||||
const inspect = () => {
|
||||
inspector.open(inspectorAdapters);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiHeaderLink color="primary" onClick={inspect}>
|
||||
{i18n.translate('xpack.observability.inspectButtonText', {
|
||||
defaultMessage: 'Inspect',
|
||||
})}
|
||||
</EuiHeaderLink>
|
||||
);
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
* 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 { render } from '../../../utils/test_helper';
|
||||
import { alertWithGroupsAndTags } from '../mock/alert';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { kibanaStartMock } from '../../../utils/kibana_react.mock';
|
||||
import { RelatedAlerts } from './related_alerts';
|
||||
import { ObservabilityAlertsTable } from '../../../components/alerts_table/alerts_table_lazy';
|
||||
import {
|
||||
OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES,
|
||||
observabilityAlertFeatureIds,
|
||||
} from '../../../../common/constants';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../utils/kibana_react');
|
||||
|
||||
jest.mock('../../../components/alerts_table/alerts_table_lazy');
|
||||
const mockAlertsTable = jest.mocked(ObservabilityAlertsTable).mockReturnValue(<div />);
|
||||
|
||||
jest.mock('@kbn/alerts-grouping', () => ({
|
||||
AlertsGrouping: jest.fn().mockImplementation(({ children }) => <div>{children([])}</div>),
|
||||
}));
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mock;
|
||||
const mockKibana = () => {
|
||||
const services = kibanaStartMock.startContract().services;
|
||||
services.spaces.getActiveSpace = jest
|
||||
.fn()
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({ id: 'space-id', name: 'space-name', disabledFeatures: [] })
|
||||
);
|
||||
useKibanaMock.mockReturnValue({
|
||||
services: {
|
||||
...services,
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('Related alerts', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockKibana();
|
||||
});
|
||||
|
||||
it('should pass the correct configuration options to the alerts table', async () => {
|
||||
render(<RelatedAlerts alert={alertWithGroupsAndTags} />);
|
||||
|
||||
expect(mockAlertsTable).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'xpack.observability.related.alerts.table',
|
||||
ruleTypeIds: OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES,
|
||||
consumers: observabilityAlertFeatureIds,
|
||||
initialPageSize: 50,
|
||||
renderAdditionalToolbarControls: expect.any(Function),
|
||||
showInspectButton: true,
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,235 +0,0 @@
|
|||
/*
|
||||
* 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, { useState, useRef, useEffect } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiImage,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
|
||||
import {
|
||||
ALERT_END,
|
||||
ALERT_GROUP,
|
||||
ALERT_RULE_UUID,
|
||||
ALERT_START,
|
||||
ALERT_UUID,
|
||||
TAGS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { BoolQuery, Filter } from '@kbn/es-query';
|
||||
import { AlertsGrouping } from '@kbn/alerts-grouping';
|
||||
import { GroupingToolbarControls } from '../../../components/alerts_table/grouping/grouping_toolbar_controls';
|
||||
import { ObservabilityFields } from '../../../../common/utils/alerting/types';
|
||||
|
||||
import {
|
||||
OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES,
|
||||
observabilityAlertFeatureIds,
|
||||
} from '../../../../common/constants';
|
||||
import {
|
||||
getRelatedAlertKuery,
|
||||
getSharedFields,
|
||||
} from '../../../../common/utils/alerting/get_related_alerts_query';
|
||||
import { ObservabilityAlertsTable, TopAlert } from '../../..';
|
||||
import {
|
||||
AlertSearchBarContainerState,
|
||||
DEFAULT_STATE,
|
||||
} from '../../../components/alert_search_bar/containers/state_container';
|
||||
import { ObservabilityAlertSearchbarWithUrlSync } from '../../../components/alert_search_bar/alert_search_bar_with_url_sync';
|
||||
import { renderGroupPanel } from '../../../components/alerts_table/grouping/render_group_panel';
|
||||
import { getGroupStats } from '../../../components/alerts_table/grouping/get_group_stats';
|
||||
import { getAggregationsByGroupingField } from '../../../components/alerts_table/grouping/get_aggregations_by_grouping_field';
|
||||
import { DEFAULT_GROUPING_OPTIONS } from '../../../components/alerts_table/grouping/constants';
|
||||
import { ACTIVE_ALERTS, ALERT_STATUS_FILTER } from '../../../components/alert_search_bar/constants';
|
||||
import { AlertsByGroupingAgg } from '../../../components/alerts_table/types';
|
||||
import {
|
||||
alertSearchBarStateContainer,
|
||||
Provider,
|
||||
useAlertSearchBarStateContainer,
|
||||
} from '../../../components/alert_search_bar/containers';
|
||||
import { RELATED_ALERTS_TABLE_CONFIG_ID, SEARCH_BAR_URL_STORAGE_KEY } from '../../../constants';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { buildEsQuery } from '../../../utils/build_es_query';
|
||||
import { mergeBoolQueries } from '../../alerts/helpers/merge_bool_queries';
|
||||
import icon from './assets/illustration_product_no_results_magnifying_glass.svg';
|
||||
|
||||
const ALERTS_PER_PAGE = 50;
|
||||
const RELATED_ALERTS_SEARCH_BAR_ID = 'related-alerts-search-bar-o11y';
|
||||
const ALERTS_TABLE_ID = 'xpack.observability.related.alerts.table';
|
||||
|
||||
interface Props {
|
||||
alert?: TopAlert<ObservabilityFields>;
|
||||
}
|
||||
|
||||
// TODO: Bring back setting default status filter as active
|
||||
const defaultState: AlertSearchBarContainerState = { ...DEFAULT_STATE };
|
||||
const DEFAULT_FILTERS: Filter[] = [];
|
||||
|
||||
export function InternalRelatedAlerts({ alert }: Props) {
|
||||
const kibanaServices = useKibana().services;
|
||||
const { http, notifications, dataViews } = kibanaServices;
|
||||
const alertSearchBarStateProps = useAlertSearchBarStateContainer(SEARCH_BAR_URL_STORAGE_KEY, {
|
||||
replace: false,
|
||||
});
|
||||
|
||||
const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>();
|
||||
const alertStart = alert?.fields[ALERT_START];
|
||||
const alertEnd = alert?.fields[ALERT_END];
|
||||
const alertId = alert?.fields[ALERT_UUID];
|
||||
const tags = alert?.fields[TAGS];
|
||||
const groups = alert?.fields[ALERT_GROUP];
|
||||
const ruleId = alert?.fields[ALERT_RULE_UUID];
|
||||
const sharedFields = getSharedFields(alert?.fields);
|
||||
const kuery = getRelatedAlertKuery({ tags, groups, ruleId, sharedFields });
|
||||
|
||||
const defaultFilters = useRef<Filter[]>([
|
||||
{
|
||||
query: {
|
||||
match_phrase: {
|
||||
'kibana.alert.uuid': alertId,
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
negate: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (alertStart) {
|
||||
const defaultTimeRange = getPaddedAlertTimeRange(alertStart, alertEnd);
|
||||
alertSearchBarStateProps.onRangeFromChange(defaultTimeRange.from);
|
||||
alertSearchBarStateProps.onRangeToChange(defaultTimeRange.to);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [alertStart, alertEnd]);
|
||||
|
||||
if (!kuery || !alert) return <EmptyState />;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer size="l" />
|
||||
<ObservabilityAlertSearchbarWithUrlSync
|
||||
appName={RELATED_ALERTS_SEARCH_BAR_ID}
|
||||
onEsQueryChange={setEsQuery}
|
||||
urlStorageKey={SEARCH_BAR_URL_STORAGE_KEY}
|
||||
defaultFilters={defaultFilters.current}
|
||||
disableLocalStorageSync={true}
|
||||
defaultState={{
|
||||
...defaultState,
|
||||
kuery,
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{esQuery && (
|
||||
<AlertsGrouping<AlertsByGroupingAgg>
|
||||
ruleTypeIds={OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES}
|
||||
consumers={observabilityAlertFeatureIds}
|
||||
defaultFilters={
|
||||
ALERT_STATUS_FILTER[alertSearchBarStateProps.status ?? ACTIVE_ALERTS.status] ??
|
||||
DEFAULT_FILTERS
|
||||
}
|
||||
from={alertSearchBarStateProps.rangeFrom}
|
||||
to={alertSearchBarStateProps.rangeTo}
|
||||
globalFilters={alertSearchBarStateProps.filters ?? DEFAULT_FILTERS}
|
||||
globalQuery={{ query: alertSearchBarStateProps.kuery, language: 'kuery' }}
|
||||
groupingId={RELATED_ALERTS_TABLE_CONFIG_ID}
|
||||
defaultGroupingOptions={DEFAULT_GROUPING_OPTIONS}
|
||||
getAggregationsByGroupingField={getAggregationsByGroupingField}
|
||||
renderGroupPanel={renderGroupPanel}
|
||||
getGroupStats={getGroupStats}
|
||||
services={{
|
||||
notifications,
|
||||
dataViews,
|
||||
http,
|
||||
}}
|
||||
>
|
||||
{(groupingFilters) => {
|
||||
const groupQuery = buildEsQuery({
|
||||
filters: groupingFilters,
|
||||
});
|
||||
return (
|
||||
<ObservabilityAlertsTable
|
||||
id={ALERTS_TABLE_ID}
|
||||
ruleTypeIds={OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES}
|
||||
consumers={observabilityAlertFeatureIds}
|
||||
query={mergeBoolQueries(esQuery, groupQuery)}
|
||||
initialPageSize={ALERTS_PER_PAGE}
|
||||
renderAdditionalToolbarControls={() => (
|
||||
<GroupingToolbarControls
|
||||
groupingId={RELATED_ALERTS_TABLE_CONFIG_ID}
|
||||
ruleTypeIds={OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES}
|
||||
/>
|
||||
)}
|
||||
showInspectButton
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AlertsGrouping>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
const heights = {
|
||||
tall: 490,
|
||||
short: 250,
|
||||
};
|
||||
const panelStyle = {
|
||||
maxWidth: 500,
|
||||
};
|
||||
|
||||
function EmptyState() {
|
||||
return (
|
||||
<EuiPanel color="subdued" data-test-subj="relatedAlertsTabEmptyState">
|
||||
<EuiFlexGroup style={{ height: heights.tall }} alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPanel hasBorder={true} style={panelStyle}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<EuiTitle>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.pages.alertDetails.relatedAlerts.empty.title"
|
||||
defaultMessage="Problem loading related alerts"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.observability.pages.alertDetails.relatedAlerts.empty.description"
|
||||
defaultMessage="Due to an unexpected error, no related alerts can be found."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiImage style={{ width: 200, height: 148 }} size="200" alt="" url={icon} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export function RelatedAlerts(props: Props) {
|
||||
return (
|
||||
<Provider value={alertSearchBarStateContainer}>
|
||||
<InternalRelatedAlerts {...props} />
|
||||
</Provider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { EuiDataGridColumn } from '@elastic/eui';
|
||||
import { ALERT_CASE_IDS, ALERT_RULE_NAME, ALERT_STATUS } from '@kbn/rule-data-utils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const RELATED_ALERT_REASON = 'relatedAlertReason';
|
||||
export const RELATION_COL = 'relatedRelation';
|
||||
export const RELATED_ACTIONS_COL = 'relatedActions';
|
||||
|
||||
export const getRelatedColumns = (): EuiDataGridColumn[] => {
|
||||
return [
|
||||
{
|
||||
id: ALERT_STATUS,
|
||||
displayAsText: i18n.translate('xpack.observability.alertsTGrid.statusColumnDescription', {
|
||||
defaultMessage: 'Alert Status',
|
||||
}),
|
||||
initialWidth: 120,
|
||||
isSortable: false,
|
||||
actions: false,
|
||||
},
|
||||
{
|
||||
id: ALERT_RULE_NAME,
|
||||
displayAsText: i18n.translate('xpack.observability.alertsTGrid.ruleNameColumnDescription', {
|
||||
defaultMessage: 'Rule name',
|
||||
}),
|
||||
initialWidth: 250,
|
||||
isSortable: false,
|
||||
actions: false,
|
||||
},
|
||||
{
|
||||
id: RELATED_ALERT_REASON,
|
||||
displayAsText: i18n.translate('xpack.observability.alertsTGrid.reasonDescription', {
|
||||
defaultMessage: 'Reason',
|
||||
}),
|
||||
initialWidth: 400,
|
||||
isSortable: false,
|
||||
actions: false,
|
||||
},
|
||||
{
|
||||
id: RELATION_COL,
|
||||
displayAsText: i18n.translate('xpack.observability.alertsTGrid.relationColumnDescription', {
|
||||
defaultMessage: 'Relation',
|
||||
}),
|
||||
initialWidth: 350,
|
||||
isSortable: false,
|
||||
actions: false,
|
||||
},
|
||||
{
|
||||
id: ALERT_CASE_IDS,
|
||||
displayAsText: i18n.translate('xpack.observability.alertsTGrid.caseIdsColumnDescription', {
|
||||
defaultMessage: 'Attached cases',
|
||||
}),
|
||||
initialWidth: 150,
|
||||
isSortable: false,
|
||||
actions: false,
|
||||
},
|
||||
{
|
||||
id: RELATED_ACTIONS_COL,
|
||||
displayAsText: i18n.translate('xpack.observability.alertsTGrid.actionsColumnDescription', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
initialWidth: 75,
|
||||
isResizable: false,
|
||||
isSortable: false,
|
||||
actions: false,
|
||||
},
|
||||
];
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiLoadingChart } from '@elastic/eui';
|
||||
import { RelatedAlertsTable } from './related_alerts_table';
|
||||
import { AlertData } from '../../../../hooks/use_fetch_alert_detail';
|
||||
|
||||
interface Props {
|
||||
alertData?: AlertData | null;
|
||||
}
|
||||
|
||||
export function RelatedAlerts({ alertData }: Props) {
|
||||
if (!alertData) {
|
||||
return <EuiLoadingChart />;
|
||||
}
|
||||
|
||||
return <RelatedAlertsTable alertData={alertData} />;
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { ALERT_START, ALERT_UUID } from '@kbn/rule-data-utils';
|
||||
import { AlertsTable } from '@kbn/response-ops-alerts-table';
|
||||
import { SortOrder } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { getRelatedColumns } from './get_related_columns';
|
||||
import { useBuildRelatedAlertsQuery } from '../../hooks/related_alerts/use_build_related_alerts_query';
|
||||
import { AlertData } from '../../../../hooks/use_fetch_alert_detail';
|
||||
import {
|
||||
GetObservabilityAlertsTableProp,
|
||||
ObservabilityAlertsTableContext,
|
||||
observabilityFeatureId,
|
||||
} from '../../../..';
|
||||
import { usePluginContext } from '../../../../hooks/use_plugin_context';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
import { AlertsFlyoutBody } from '../../../../components/alerts_flyout/alerts_flyout_body';
|
||||
import { AlertsFlyoutFooter } from '../../../../components/alerts_flyout/alerts_flyout_footer';
|
||||
import { OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES } from '../../../../../common/constants';
|
||||
import { AlertsTableCellValue } from '../../../../components/alerts_table/common/cell_value';
|
||||
import { casesFeatureIdV2 } from '../../../../../common';
|
||||
|
||||
interface Props {
|
||||
alertData: AlertData;
|
||||
}
|
||||
|
||||
const columns = getRelatedColumns();
|
||||
const initialSort: Array<Record<string, SortOrder>> = [
|
||||
{
|
||||
_score: 'desc',
|
||||
},
|
||||
{
|
||||
[ALERT_START]: 'desc',
|
||||
},
|
||||
{
|
||||
[ALERT_UUID]: 'desc',
|
||||
},
|
||||
];
|
||||
|
||||
const caseConfiguration: GetObservabilityAlertsTableProp<'casesConfiguration'> = {
|
||||
featureId: casesFeatureIdV2,
|
||||
owner: [observabilityFeatureId],
|
||||
};
|
||||
|
||||
const RELATED_ALERTS_TABLE_ID = 'xpack.observability.alerts.relatedAlerts';
|
||||
|
||||
export function RelatedAlertsTable({ alertData }: Props) {
|
||||
const { formatted: alert } = alertData;
|
||||
const esQuery = useBuildRelatedAlertsQuery({ alert });
|
||||
const { observabilityRuleTypeRegistry, config } = usePluginContext();
|
||||
|
||||
const services = useKibana().services;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiSpacer size="s" />
|
||||
<AlertsTable<ObservabilityAlertsTableContext>
|
||||
id={RELATED_ALERTS_TABLE_ID}
|
||||
query={esQuery}
|
||||
columns={columns}
|
||||
ruleTypeIds={OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES}
|
||||
minScore={1.5}
|
||||
trackScores={true}
|
||||
initialSort={initialSort}
|
||||
casesConfiguration={caseConfiguration}
|
||||
additionalContext={{
|
||||
observabilityRuleTypeRegistry,
|
||||
config,
|
||||
parentAlert: alert,
|
||||
}}
|
||||
toolbarVisibility={{
|
||||
showSortSelector: false,
|
||||
}}
|
||||
renderCellValue={AlertsTableCellValue}
|
||||
renderFlyoutBody={AlertsFlyoutBody}
|
||||
renderFlyoutFooter={AlertsFlyoutFooter}
|
||||
showAlertStatusWithFlapping
|
||||
services={services}
|
||||
gridStyle={{
|
||||
border: 'horizontal',
|
||||
header: 'underline',
|
||||
cellPadding: 'l',
|
||||
fontSize: 'm',
|
||||
}}
|
||||
rowHeightsOptions={{
|
||||
defaultHeight: 'auto',
|
||||
}}
|
||||
height="600px"
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import {
|
||||
ALERT_INSTANCE_ID,
|
||||
ALERT_RULE_TAGS,
|
||||
ALERT_RULE_UUID,
|
||||
ALERT_RULE_NAME,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { intersection } from 'lodash';
|
||||
import { EuiDescriptionList, EuiLink } from '@elastic/eui';
|
||||
import type { Alert } from '@kbn/alerting-types';
|
||||
import { TagsList } from '@kbn/observability-shared-plugin/public';
|
||||
import type { TopAlert } from '../../../..';
|
||||
import { getAlertFieldValue } from '../../../../components/alerts_table/common/cell_value';
|
||||
import { paths } from '../../../../../common/locators/paths';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
|
||||
export function RelationCol({ alert, parentAlert }: { alert: Alert; parentAlert: TopAlert }) {
|
||||
const {
|
||||
services: { http },
|
||||
} = useKibana();
|
||||
const instanceId = getAlertFieldValue(alert, ALERT_INSTANCE_ID);
|
||||
const tags = getAlertFieldValue(alert, ALERT_RULE_TAGS);
|
||||
const ruleId = getAlertFieldValue(alert, ALERT_RULE_UUID);
|
||||
const ruleName = getAlertFieldValue(alert, ALERT_RULE_NAME);
|
||||
const ruleLink = ruleId ? http.basePath.prepend(paths.observability.ruleDetails(ruleId)) : '';
|
||||
const hasSomeRelationWithInstance =
|
||||
intersection(parentAlert.fields[ALERT_INSTANCE_ID].split(','), instanceId.split(',')).length >
|
||||
0;
|
||||
const hasSomeRelationWithTags =
|
||||
intersection(parentAlert.fields[ALERT_RULE_TAGS], tags.split(',')).length > 0;
|
||||
const hasRelationWithRule = ruleId === parentAlert.fields[ALERT_RULE_UUID];
|
||||
const relations = [];
|
||||
if (hasSomeRelationWithInstance) {
|
||||
relations.push({
|
||||
title: i18n.translate('xpack.observability.columns.groupsBadgeLabel', {
|
||||
defaultMessage: 'Groups',
|
||||
}),
|
||||
description: instanceId,
|
||||
});
|
||||
}
|
||||
if (hasSomeRelationWithTags) {
|
||||
relations.push({
|
||||
title: i18n.translate('xpack.observability.columns.tagsBadgeLabel', {
|
||||
defaultMessage: 'Tags',
|
||||
}),
|
||||
description: (
|
||||
<TagsList tags={(alert[ALERT_RULE_TAGS] as string[]) || []} ignoreEmpty color="default" />
|
||||
),
|
||||
});
|
||||
}
|
||||
if (hasRelationWithRule) {
|
||||
relations.push({
|
||||
title: i18n.translate('xpack.observability.columns.ruleBadgeLabel', {
|
||||
defaultMessage: 'Rule',
|
||||
}),
|
||||
description: (
|
||||
<EuiLink href={ruleLink} data-test-subj="obsAlertDetailsRelatedAlertsRelationRuleLink">
|
||||
{ruleName}
|
||||
</EuiLink>
|
||||
),
|
||||
});
|
||||
}
|
||||
return (
|
||||
<EuiDescriptionList
|
||||
type="column"
|
||||
listItems={relations}
|
||||
css={{ maxWidth: '400px' }}
|
||||
compressed
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import {
|
||||
ALERT_END,
|
||||
ALERT_GROUP,
|
||||
ALERT_INSTANCE_ID,
|
||||
ALERT_RULE_TAGS,
|
||||
ALERT_RULE_UUID,
|
||||
ALERT_START,
|
||||
ALERT_STATUS,
|
||||
ALERT_UUID,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import dedent from 'dedent';
|
||||
import moment from 'moment';
|
||||
import { ObservabilityFields } from '../../../../../common/utils/alerting/types';
|
||||
import { TopAlert } from '../../../../typings/alerts';
|
||||
|
||||
interface Props {
|
||||
alert: TopAlert<ObservabilityFields>;
|
||||
}
|
||||
|
||||
export function useBuildRelatedAlertsQuery({ alert }: Props): QueryDslQueryContainer {
|
||||
const groups = alert.fields[ALERT_GROUP];
|
||||
const shouldGroups: QueryDslQueryContainer[] = [];
|
||||
groups?.forEach(({ field, value }) => {
|
||||
if (!field || !value) return;
|
||||
shouldGroups.push({
|
||||
bool: {
|
||||
boost: 2.0,
|
||||
must: [
|
||||
{ term: { 'kibana.alert.group.field': field } },
|
||||
{ term: { 'kibana.alert.group.value': value } },
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const shouldRule = alert.fields[ALERT_RULE_UUID]
|
||||
? [
|
||||
{
|
||||
term: {
|
||||
'kibana.alert.rule.uuid': {
|
||||
value: alert.fields[ALERT_RULE_UUID],
|
||||
boost: 1.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const startDate = moment(alert.fields[ALERT_START]);
|
||||
const endDate = alert.fields[ALERT_END] ? moment(alert.fields[ALERT_END]) : undefined;
|
||||
const tags = alert.fields[ALERT_RULE_TAGS] ?? [];
|
||||
const instanceId = alert.fields[ALERT_INSTANCE_ID]?.split(',') ?? [];
|
||||
|
||||
return {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
[ALERT_START]: {
|
||||
gte: startDate.clone().subtract(1, 'days').toISOString(),
|
||||
lte: startDate.clone().add(1, 'days').toISOString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
must_not: [
|
||||
{
|
||||
term: {
|
||||
[ALERT_UUID]: {
|
||||
value: alert.fields[ALERT_UUID],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
should: [
|
||||
...shouldGroups,
|
||||
...shouldRule,
|
||||
{
|
||||
term: {
|
||||
[ALERT_STATUS]: {
|
||||
value: alert.fields[ALERT_STATUS],
|
||||
boost: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
function_score: {
|
||||
functions: [
|
||||
{
|
||||
exp: {
|
||||
[ALERT_START]: {
|
||||
origin: startDate.toISOString(),
|
||||
scale: '10m',
|
||||
offset: '10m',
|
||||
decay: 0.5,
|
||||
},
|
||||
},
|
||||
weight: 10,
|
||||
},
|
||||
...(endDate
|
||||
? [
|
||||
{
|
||||
exp: {
|
||||
[ALERT_END]: {
|
||||
origin: endDate.toISOString(),
|
||||
scale: '10m',
|
||||
offset: '10m',
|
||||
decay: 0.5,
|
||||
},
|
||||
},
|
||||
weight: 10,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
script_score: {
|
||||
script: {
|
||||
source: dedent(`
|
||||
double jaccardSimilarity(Set a, Set b) {
|
||||
if (a.size() == 0 || b.size() == 0) return 0.0;
|
||||
Set intersection = new HashSet(a);
|
||||
intersection.retainAll(b);
|
||||
Set union = new HashSet(a);
|
||||
union.addAll(b);
|
||||
return (double) intersection.size() / union.size();
|
||||
}
|
||||
Set tagsQuery = new HashSet(params.tags);
|
||||
Set tagsDoc = new HashSet(doc.containsKey("kibana.alert.rule.tags") && !doc.get("kibana.alert.rule.tags").empty ? doc.get("kibana.alert.rule.tags") : []);
|
||||
return 1.0 + jaccardSimilarity(tagsQuery, tagsDoc);
|
||||
`),
|
||||
params: {
|
||||
tags,
|
||||
},
|
||||
},
|
||||
},
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
script_score: {
|
||||
script: {
|
||||
source: dedent(`
|
||||
double jaccardSimilarity(Set a, Set b) {
|
||||
if (a.size() == 0 || b.size() == 0) return 0.0;
|
||||
Set intersection = new HashSet(a);
|
||||
intersection.retainAll(b);
|
||||
Set union = new HashSet(a);
|
||||
union.addAll(b);
|
||||
return (double) intersection.size() / union.size();
|
||||
}
|
||||
Set instanceIdQuery = new HashSet(params.instanceId);
|
||||
Set instanceIdDoc = new HashSet();
|
||||
if (doc.containsKey('kibana.alert.instance.id')) {
|
||||
String instanceIdStr = doc['kibana.alert.instance.id'].value;
|
||||
if (instanceIdStr != null && !instanceIdStr.isEmpty()) {
|
||||
StringTokenizer tokenizer = new StringTokenizer(instanceIdStr, ',');
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
instanceIdDoc.add(tokenizer.nextToken());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 1.0 + jaccardSimilarity(instanceIdQuery, instanceIdDoc);
|
||||
`),
|
||||
params: {
|
||||
instanceId,
|
||||
},
|
||||
},
|
||||
},
|
||||
weight: 5,
|
||||
},
|
||||
],
|
||||
score_mode: 'multiply',
|
||||
boost_mode: 'sum',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -5,14 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Suspense, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { CasesPermissions } from '@kbn/cases-plugin/common';
|
||||
import AlertsFlyout from '../../../components/alerts_flyout/alerts_flyout';
|
||||
import { observabilityFeatureId } from '../../../../common';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
import { usePluginContext } from '../../../hooks/use_plugin_context';
|
||||
import { useFetchAlertDetail } from '../../../hooks/use_fetch_alert_detail';
|
||||
import { useFetchAlertData } from '../../../hooks/use_fetch_alert_data';
|
||||
import { LazyAlertsFlyout, ObservabilityAlertsTable } from '../../..';
|
||||
import { ObservabilityAlertsTable } from '../../..';
|
||||
import { CASES_PATH, paths } from '../../../../common/locators/paths';
|
||||
|
||||
export interface CasesProps {
|
||||
|
@ -67,13 +68,11 @@ export function Cases({ permissions }: CasesProps) {
|
|||
/>
|
||||
|
||||
{alertDetail && selectedAlertId !== '' && !alertLoading ? (
|
||||
<Suspense fallback={null}>
|
||||
<LazyAlertsFlyout
|
||||
alert={alertDetail.raw}
|
||||
observabilityRuleTypeRegistry={observabilityRuleTypeRegistry}
|
||||
onClose={handleFlyoutClose}
|
||||
/>
|
||||
</Suspense>
|
||||
<AlertsFlyout
|
||||
alert={alertDetail.raw}
|
||||
observabilityRuleTypeRegistry={observabilityRuleTypeRegistry}
|
||||
onClose={handleFlyoutClose}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -12,10 +12,10 @@ import {
|
|||
} from '@kbn/deeplinks-observability';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public';
|
||||
import { usePluginContext } from '../../../../hooks/use_plugin_context';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
// FIXME: import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public'
|
||||
import HeaderMenuPortal from './header_menu_portal';
|
||||
import { InspectorHeaderLink } from '../../../alert_details/components/inspector_header_link';
|
||||
|
||||
export function HeaderMenu(): React.ReactElement | null {
|
||||
const { share, theme, http } = useKibana().services;
|
||||
|
@ -49,6 +49,7 @@ export function HeaderMenu(): React.ReactElement | null {
|
|||
defaultMessage: 'Annotations',
|
||||
})}
|
||||
</EuiHeaderLink>
|
||||
<InspectorHeaderLink />
|
||||
</EuiHeaderLinks>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import HeaderMenuPortal from './header_menu_portal';
|
||||
import { themeServiceMock } from '@kbn/core/public/mocks';
|
||||
|
||||
describe('HeaderMenuPortal', () => {
|
||||
describe('when unmounted', () => {
|
||||
it('calls setHeaderActionMenu with undefined', () => {
|
||||
const setHeaderActionMenu = jest.fn();
|
||||
const theme$ = themeServiceMock.createTheme$();
|
||||
|
||||
const { unmount } = render(
|
||||
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu} theme$={theme$}>
|
||||
test
|
||||
</HeaderMenuPortal>
|
||||
);
|
||||
|
||||
unmount();
|
||||
|
||||
expect(setHeaderActionMenu).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
* 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, { ReactNode, useEffect, useMemo } from 'react';
|
||||
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { AppMountParameters } from '@kbn/core/public';
|
||||
import { useKibana } from '../../../../utils/kibana_react';
|
||||
export interface HeaderMenuPortalProps {
|
||||
children: ReactNode;
|
||||
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
|
||||
theme$: AppMountParameters['theme$'];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function HeaderMenuPortal({
|
||||
children,
|
||||
setHeaderActionMenu,
|
||||
theme$,
|
||||
}: HeaderMenuPortalProps) {
|
||||
const { i18n } = useKibana().services;
|
||||
const portalNode = useMemo(() => createHtmlPortalNode(), []);
|
||||
|
||||
useEffect(() => {
|
||||
setHeaderActionMenu((element) => {
|
||||
const mount = toMountPoint(<OutPortal node={portalNode} />, {
|
||||
...{ theme: { theme$ }, i18n },
|
||||
});
|
||||
return mount(element);
|
||||
});
|
||||
|
||||
return () => {
|
||||
portalNode.unmount();
|
||||
setHeaderActionMenu(undefined);
|
||||
};
|
||||
}, [portalNode, setHeaderActionMenu, i18n, theme$]);
|
||||
|
||||
return <InPortal node={portalNode}>{children}</InPortal>;
|
||||
}
|
|
@ -71,6 +71,7 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/
|
|||
import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
|
||||
import type { StreamsPluginStart, StreamsPluginSetup } from '@kbn/streams-plugin/public';
|
||||
import { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public';
|
||||
import { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public';
|
||||
import { observabilityAppId, observabilityFeatureId } from '../common';
|
||||
import {
|
||||
ALERTS_PATH,
|
||||
|
@ -165,6 +166,7 @@ export interface ObservabilityPublicPluginsStart {
|
|||
toastNotifications: ToastsStart;
|
||||
streams?: StreamsPluginStart;
|
||||
fieldsMetadata: FieldsMetadataPublicStart;
|
||||
inspector: InspectorPluginStart;
|
||||
}
|
||||
export type ObservabilityPublicStart = ReturnType<Plugin['start']>;
|
||||
|
||||
|
|
|
@ -78,7 +78,6 @@
|
|||
"@kbn/alerting-rule-utils",
|
||||
"@kbn/ui-theme",
|
||||
"@kbn/core-application-common",
|
||||
"@kbn/securitysolution-ecs",
|
||||
"@kbn/alerts-as-data-utils",
|
||||
"@kbn/datemath",
|
||||
"@kbn/logs-shared-plugin",
|
||||
|
|
|
@ -120,11 +120,12 @@ journey(`DefaultStatusAlert`, async ({ page, params }) => {
|
|||
|
||||
await retry.tryForTime(3 * 60 * 1000, async () => {
|
||||
await page.click(byTestId('querySubmitButton'));
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
const alerts = await page.waitForSelector(`text=1 Alert`, { timeout: 5 * 1000 });
|
||||
expect(await alerts.isVisible()).toBe(true);
|
||||
|
||||
const text = await page.textContent(`${byTestId('dataGridRowCell')} .euiLink`);
|
||||
const text = await page.getByTestId('o11yGetRenderCellValueLink').textContent();
|
||||
|
||||
expect(text).toBe(reasonMessage);
|
||||
});
|
||||
|
|
|
@ -47,6 +47,23 @@ describe('CellValue', () => {
|
|||
expect(getByText('value1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle a number value', () => {
|
||||
const alert: Alert = {
|
||||
_id: '_id',
|
||||
_index: '_index',
|
||||
field1: 123,
|
||||
};
|
||||
const columnId = 'field1';
|
||||
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
<CellValue alert={alert} columnId={columnId} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(getByText('123')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle array of booleans', () => {
|
||||
const alert: Alert = {
|
||||
_id: '_id',
|
||||
|
|
|
@ -30,7 +30,7 @@ export interface CellValueProps {
|
|||
*/
|
||||
export const CellValue = memo(({ alert, columnId }: CellValueProps) => {
|
||||
const displayValue: string | null = useMemo(() => {
|
||||
const cellValues: string | JsonValue[] = alert[columnId];
|
||||
const cellValues: string | number | JsonValue[] = alert[columnId];
|
||||
|
||||
// Displays string as is.
|
||||
// Joins values of array with more than one element.
|
||||
|
@ -38,6 +38,8 @@ export const CellValue = memo(({ alert, columnId }: CellValueProps) => {
|
|||
// Return the string of the value otherwise.
|
||||
if (typeof cellValues === 'string') {
|
||||
return cellValues;
|
||||
} else if (typeof cellValues === 'number') {
|
||||
return cellValues.toString();
|
||||
} else if (Array.isArray(cellValues)) {
|
||||
if (cellValues.length > 1) {
|
||||
return cellValues.join(', ');
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { ALERT_START } from '@kbn/rule-data-utils';
|
||||
import type { RuleRegistrySearchResponse } from '@kbn/rule-registry-plugin/common';
|
||||
import type { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import {
|
||||
|
@ -692,6 +693,194 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('observability', () => {
|
||||
const apmRuleTypeIds = ['apm.transaction_error_rate', 'apm.error_rate'];
|
||||
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
|
||||
});
|
||||
|
||||
it('should omit alerts when score is less than min score', async () => {
|
||||
const query = {
|
||||
bool: {
|
||||
filter: [],
|
||||
should: [
|
||||
{
|
||||
function_score: {
|
||||
functions: [
|
||||
{
|
||||
exp: {
|
||||
[ALERT_START]: {
|
||||
origin: '2021-10-19T14:58:08.539Z',
|
||||
scale: '10m',
|
||||
offset: '10m',
|
||||
decay: 0.5,
|
||||
},
|
||||
},
|
||||
weight: 10,
|
||||
},
|
||||
],
|
||||
boost_mode: 'sum',
|
||||
},
|
||||
},
|
||||
],
|
||||
must: [],
|
||||
must_not: [],
|
||||
},
|
||||
};
|
||||
const resultWithoutMinScore = await secureSearch.send<RuleRegistrySearchResponse>({
|
||||
supertestWithoutAuth,
|
||||
auth: {
|
||||
username: obsOnlySpacesAll.username,
|
||||
password: obsOnlySpacesAll.password,
|
||||
},
|
||||
referer: 'test',
|
||||
kibanaVersion,
|
||||
internalOrigin: 'Kibana',
|
||||
options: {
|
||||
ruleTypeIds: apmRuleTypeIds,
|
||||
query,
|
||||
},
|
||||
strategy: 'privateRuleRegistryAlertsSearchStrategy',
|
||||
space: 'default',
|
||||
});
|
||||
|
||||
expect(resultWithoutMinScore.rawResponse.hits.total).to.eql(9);
|
||||
|
||||
validateRuleTypeIds(resultWithoutMinScore, apmRuleTypeIds);
|
||||
|
||||
const resultWithMinScore = await secureSearch.send<RuleRegistrySearchResponse>({
|
||||
supertestWithoutAuth,
|
||||
auth: {
|
||||
username: obsOnlySpacesAll.username,
|
||||
password: obsOnlySpacesAll.password,
|
||||
},
|
||||
referer: 'test',
|
||||
kibanaVersion,
|
||||
internalOrigin: 'Kibana',
|
||||
options: {
|
||||
ruleTypeIds: apmRuleTypeIds,
|
||||
query,
|
||||
minScore: 11,
|
||||
},
|
||||
strategy: 'privateRuleRegistryAlertsSearchStrategy',
|
||||
space: 'default',
|
||||
});
|
||||
|
||||
const allScores = resultWithMinScore.rawResponse.hits.hits.map((hit) => {
|
||||
return hit._score;
|
||||
});
|
||||
allScores.forEach((score) => {
|
||||
if (score) {
|
||||
expect(score >= 11).to.be(true);
|
||||
} else {
|
||||
throw new Error('Score is null');
|
||||
}
|
||||
});
|
||||
|
||||
expect(resultWithMinScore.rawResponse.hits.total).to.eql(8);
|
||||
|
||||
validateRuleTypeIds(resultWithMinScore, apmRuleTypeIds);
|
||||
});
|
||||
|
||||
it('should track scores alerts when sorting when trackScores is true', async () => {
|
||||
const query = {
|
||||
bool: {
|
||||
filter: [],
|
||||
should: [
|
||||
{
|
||||
function_score: {
|
||||
functions: [
|
||||
{
|
||||
exp: {
|
||||
[ALERT_START]: {
|
||||
origin: '2021-10-19T14:58:08.539Z',
|
||||
scale: '10m',
|
||||
offset: '10m',
|
||||
decay: 0.5,
|
||||
},
|
||||
},
|
||||
weight: 10,
|
||||
},
|
||||
],
|
||||
boost_mode: 'sum',
|
||||
},
|
||||
},
|
||||
],
|
||||
must: [],
|
||||
must_not: [],
|
||||
},
|
||||
};
|
||||
const resultWithoutTrackScore = await secureSearch.send<RuleRegistrySearchResponse>({
|
||||
supertestWithoutAuth,
|
||||
auth: {
|
||||
username: obsOnlySpacesAll.username,
|
||||
password: obsOnlySpacesAll.password,
|
||||
},
|
||||
referer: 'test',
|
||||
kibanaVersion,
|
||||
internalOrigin: 'Kibana',
|
||||
options: {
|
||||
ruleTypeIds: apmRuleTypeIds,
|
||||
query,
|
||||
sort: [
|
||||
{
|
||||
'kibana.alert.start': {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
minScore: 11,
|
||||
},
|
||||
strategy: 'privateRuleRegistryAlertsSearchStrategy',
|
||||
space: 'default',
|
||||
});
|
||||
|
||||
resultWithoutTrackScore.rawResponse.hits.hits.forEach((hit) => {
|
||||
expect(hit._score).to.be(null);
|
||||
});
|
||||
|
||||
validateRuleTypeIds(resultWithoutTrackScore, apmRuleTypeIds);
|
||||
|
||||
const resultWithTrackScore = await secureSearch.send<RuleRegistrySearchResponse>({
|
||||
supertestWithoutAuth,
|
||||
auth: {
|
||||
username: obsOnlySpacesAll.username,
|
||||
password: obsOnlySpacesAll.password,
|
||||
},
|
||||
referer: 'test',
|
||||
kibanaVersion,
|
||||
internalOrigin: 'Kibana',
|
||||
options: {
|
||||
ruleTypeIds: apmRuleTypeIds,
|
||||
query,
|
||||
sort: [
|
||||
{
|
||||
'kibana.alert.start': {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
minScore: 11,
|
||||
trackScores: true,
|
||||
},
|
||||
strategy: 'privateRuleRegistryAlertsSearchStrategy',
|
||||
space: 'default',
|
||||
});
|
||||
|
||||
resultWithTrackScore.rawResponse.hits.hits.forEach((hit) => {
|
||||
expect(hit._score).not.to.be(null);
|
||||
expect(hit._score).to.be(11);
|
||||
});
|
||||
|
||||
validateRuleTypeIds(resultWithTrackScore, apmRuleTypeIds);
|
||||
});
|
||||
});
|
||||
|
||||
describe('discover', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue