[SecuritySolution] Remove chartEmbeddablesEnabled feature flag and unused code (#173675)

This commit is contained in:
Angela Chuang 2024-04-15 13:58:08 +01:00 committed by GitHub
parent 786200791b
commit b84591786a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
224 changed files with 878 additions and 14735 deletions

View file

@ -12,7 +12,3 @@ export * from './details';
export * from './overview';
export * from './uncommon_processes';
export * from './kpi_hosts';
export * from './kpi_unique_ips';

View file

@ -1,24 +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 { z } from 'zod';
import { HostsKpiQueries } from '../model/factory_query_type';
import { pagination } from '../model/pagination';
import { requestBasicOptionsSchema } from '../model/request_basic_options';
import { timerange } from '../model/timerange';
import { sort } from './model/sort';
export const kpiHostsSchema = requestBasicOptionsSchema.extend({
sort,
pagination,
timerange,
factoryQueryType: z.literal(HostsKpiQueries.kpiHosts),
});
export type KpiHostsRequestOptionsInput = z.input<typeof kpiHostsSchema>;
export type KpiHostsRequestOptions = z.infer<typeof kpiHostsSchema>;

View file

@ -1,24 +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 { z } from 'zod';
import { HostsKpiQueries } from '../model/factory_query_type';
import { pagination } from '../model/pagination';
import { requestBasicOptionsSchema } from '../model/request_basic_options';
import { timerange } from '../model/timerange';
import { sort } from './model/sort';
export const kpiUniqueIpsSchema = requestBasicOptionsSchema.extend({
sort,
pagination,
timerange,
factoryQueryType: z.literal(HostsKpiQueries.kpiUniqueIps),
});
export type KpiUniqueIpsRequestOptionsInput = z.input<typeof kpiUniqueIpsSchema>;
export type KpiUniqueIpsRequestOptions = z.infer<typeof kpiUniqueIpsSchema>;

View file

@ -18,20 +18,11 @@ import {
hostDetailsSchema,
hostOverviewSchema,
hostUncommonProcessesSchema,
kpiHostsSchema,
kpiUniqueIpsSchema,
} from './hosts/hosts';
import { matrixHistogramSchema } from './matrix_histogram/matrix_histogram';
import { networkDetailsSchema } from './network/details';
import { networkDnsSchema } from './network/dns';
import { networkHttpSchema } from './network/http';
import {
networkKpiDns,
networkKpiEvents,
networkKpiTlsHandshakes,
networkKpiUniqueFlows,
networkKpiUniquePrivateIps,
} from './network/kpi';
import { networkOverviewSchema } from './network/overview';
import { networkTlsSchema } from './network/tls';
import { networkTopCountriesSchema } from './network/top_countries';
@ -50,10 +41,8 @@ import {
} from './risk_score/risk_score';
import {
authenticationsKpiSchema,
managedUserDetailsSchema,
observedUserDetailsSchema,
totalUsersKpiSchema,
userAuthenticationsSchema,
usersSchema,
} from './users/users';
@ -64,8 +53,6 @@ export * from './hosts/hosts';
export * from './users/users';
export * from './matrix_histogram/matrix_histogram';
export * from './network/network';
export * from './related_entities/related_entities';
@ -84,15 +71,11 @@ export const searchStrategyRequestSchema = z.discriminatedUnion('factoryQueryTyp
firstLastSeenRequestOptionsSchema,
allHostsSchema,
hostDetailsSchema,
kpiHostsSchema,
kpiUniqueIpsSchema,
hostOverviewSchema,
hostUncommonProcessesSchema,
usersSchema,
observedUserDetailsSchema,
managedUserDetailsSchema,
totalUsersKpiSchema,
authenticationsKpiSchema,
userAuthenticationsSchema,
hostsRiskScoreRequestOptionsSchema,
usersRiskScoreRequestOptionsSchema,
@ -108,12 +91,6 @@ export const searchStrategyRequestSchema = z.discriminatedUnion('factoryQueryTyp
networkTopNFlowSchema,
networkTopNFlowCountSchema,
networkUsersSchema,
networkKpiDns,
networkKpiEvents,
networkKpiTlsHandshakes,
networkKpiUniqueFlows,
networkKpiUniquePrivateIps,
matrixHistogramSchema,
threatIntelSourceRequestOptionsSchema,
eventEnrichmentRequestOptionsSchema,
]);

View file

@ -5,13 +5,6 @@
* 2.0.
*/
import { z } from 'zod';
import { MatrixHistogramQuery } from '../model/factory_query_type';
import { inspect } from '../model/inspect';
import { requestBasicOptionsSchema } from '../model/request_basic_options';
import { runtimeMappings } from '../model/runtime_mappings';
import { timerange } from '../model/timerange';
export enum MatrixHistogramType {
authentications = 'authentications',
anomalies = 'anomalies',
@ -20,37 +13,3 @@ export enum MatrixHistogramType {
dns = 'dns',
preview = 'preview',
}
export const matrixHistogramSchema = requestBasicOptionsSchema.extend({
histogramType: z.enum([
MatrixHistogramType.alerts,
MatrixHistogramType.anomalies,
MatrixHistogramType.authentications,
MatrixHistogramType.dns,
MatrixHistogramType.events,
MatrixHistogramType.preview,
]),
stackByField: z.string().optional(),
threshold: z
.object({
field: z.array(z.string()),
value: z.string(),
cardinality: z
.object({
field: z.array(z.string()),
value: z.string(),
})
.optional(),
})
.optional(),
inspect,
isPtrIncluded: z.boolean().default(false),
includeMissingData: z.boolean().default(true),
runtimeMappings,
timerange,
factoryQueryType: z.literal(MatrixHistogramQuery),
});
export type MatrixHistogramRequestOptionsInput = z.input<typeof matrixHistogramSchema>;
export type MatrixHistogramRequestOptions = z.infer<typeof matrixHistogramSchema>;

View file

@ -12,26 +12,11 @@ export enum HostsQueries {
uncommonProcesses = 'uncommonProcesses',
}
export enum NetworkKpiQueries {
dns = 'networkKpiDns',
networkEvents = 'networkKpiNetworkEvents',
tlsHandshakes = 'networkKpiTlsHandshakes',
uniqueFlows = 'networkKpiUniqueFlows',
uniquePrivateIps = 'networkKpiUniquePrivateIps',
}
export enum HostsKpiQueries {
kpiHosts = 'hostsKpiHosts',
kpiUniqueIps = 'hostsKpiUniqueIps',
}
export enum UsersQueries {
observedDetails = 'observedUserDetails',
managedDetails = 'managedUserDetails',
kpiTotalUsers = 'usersKpiTotalUsers',
users = 'allUsers',
authentications = 'authentications',
kpiAuthentications = 'usersKpiAuthentications',
}
export enum NetworkQueries {
@ -57,8 +42,6 @@ export enum CtiQueries {
dataSource = 'dataSource',
}
export const MatrixHistogramQuery = 'matrixHistogram';
export const FirstLastSeenQuery = 'firstlastseen';
export enum RelatedEntitiesQueries {
@ -68,12 +51,9 @@ export enum RelatedEntitiesQueries {
export type FactoryQueryTypes =
| HostsQueries
| HostsKpiQueries
| UsersQueries
| NetworkQueries
| NetworkKpiQueries
| RiskQueries
| CtiQueries
| typeof MatrixHistogramQuery
| typeof FirstLastSeenQuery
| RelatedEntitiesQueries;

View file

@ -1,21 +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 { z } from 'zod';
import { NetworkKpiQueries } from '../../model/factory_query_type';
import { requestBasicOptionsSchema } from '../../model/request_basic_options';
import { timerange } from '../../model/timerange';
export const networkKpiDns = requestBasicOptionsSchema.extend({
timerange,
factoryQueryType: z.literal(NetworkKpiQueries.dns),
});
export type NetworkKpiDnsRequestOptionsInput = z.input<typeof networkKpiDns>;
export type NetworkKpiDnsRequestOptions = z.infer<typeof networkKpiDns>;

View file

@ -1,21 +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 { z } from 'zod';
import { NetworkKpiQueries } from '../../model/factory_query_type';
import { requestBasicOptionsSchema } from '../../model/request_basic_options';
import { timerange } from '../../model/timerange';
export const networkKpiEvents = requestBasicOptionsSchema.extend({
timerange,
factoryQueryType: z.literal(NetworkKpiQueries.networkEvents),
});
export type NetworkKpiEventsRequestOptionsInput = z.input<typeof networkKpiEvents>;
export type NetworkKpiEventsRequestOptions = z.infer<typeof networkKpiEvents>;

View file

@ -1,16 +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.
*/
export * from './dns';
export * from './events';
export * from './tls_handshakes';
export * from './unique_flows';
export * from './unique_private_ips';

View file

@ -1,21 +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 { z } from 'zod';
import { NetworkKpiQueries } from '../../model/factory_query_type';
import { requestBasicOptionsSchema } from '../../model/request_basic_options';
import { timerange } from '../../model/timerange';
export const networkKpiTlsHandshakes = requestBasicOptionsSchema.extend({
timerange,
factoryQueryType: z.literal(NetworkKpiQueries.tlsHandshakes),
});
export type NetworkKpiTlsHandshakesRequestOptionsInput = z.input<typeof networkKpiTlsHandshakes>;
export type NetworkKpiTlsHandshakesRequestOptions = z.infer<typeof networkKpiTlsHandshakes>;

View file

@ -1,21 +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 { z } from 'zod';
import { NetworkKpiQueries } from '../../model/factory_query_type';
import { requestBasicOptionsSchema } from '../../model/request_basic_options';
import { timerange } from '../../model/timerange';
export const networkKpiUniqueFlows = requestBasicOptionsSchema.extend({
timerange,
factoryQueryType: z.literal(NetworkKpiQueries.uniqueFlows),
});
export type NetworkKpiUniqueFlowsRequestOptionsInput = z.input<typeof networkKpiUniqueFlows>;
export type NetworkKpiUniqueFlowsRequestOptions = z.infer<typeof networkKpiUniqueFlows>;

View file

@ -1,23 +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 { z } from 'zod';
import { NetworkKpiQueries } from '../../model/factory_query_type';
import { requestBasicOptionsSchema } from '../../model/request_basic_options';
import { timerange } from '../../model/timerange';
export const networkKpiUniquePrivateIps = requestBasicOptionsSchema.extend({
timerange,
factoryQueryType: z.literal(NetworkKpiQueries.uniquePrivateIps),
});
export type NetworkKpiUniquePrivateIpsRequestOptionsInput = z.input<
typeof networkKpiUniquePrivateIps
>;
export type NetworkKpiUniquePrivateIpsRequestOptions = z.infer<typeof networkKpiUniquePrivateIps>;

View file

@ -11,8 +11,6 @@ export * from './dns';
export * from './http';
export * from './kpi';
export * from './overview';
export * from './tls';

View file

@ -1,21 +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 { z } from 'zod';
import { UsersQueries } from '../../model/factory_query_type';
import { requestBasicOptionsSchema } from '../../model/request_basic_options';
import { timerange } from '../../model/timerange';
export const authenticationsKpiSchema = requestBasicOptionsSchema.extend({
timerange,
factoryQueryType: z.literal(UsersQueries.kpiAuthentications),
});
export type AuthenticationsKpiRequestOptionsInput = z.input<typeof authenticationsKpiSchema>;
export type AuthenticationsKpiRequestOptions = z.infer<typeof authenticationsKpiSchema>;

View file

@ -1,21 +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 { z } from 'zod';
import { UsersQueries } from '../../model/factory_query_type';
import { requestBasicOptionsSchema } from '../../model/request_basic_options';
import { timerange } from '../../model/timerange';
export const totalUsersKpiSchema = requestBasicOptionsSchema.extend({
timerange,
factoryQueryType: z.literal(UsersQueries.kpiTotalUsers),
});
export type TotalUsersKpiRequestOptionsInput = z.input<typeof totalUsersKpiSchema>;
export type TotalUsersKpiRequestOptions = z.infer<typeof totalUsersKpiSchema>;

View file

@ -9,10 +9,6 @@ export * from './observed_details';
export * from './managed_details';
export * from './kpi/total_users';
export * from './kpi/authentications';
export * from './all';
export * from './authentications';

View file

@ -19,7 +19,6 @@ export const allowedExperimentalValues = Object.freeze({
excludePoliciesInFilterEnabled: false,
kubernetesEnabled: true,
chartEmbeddablesEnabled: true,
donutChartEmbeddablesEnabled: false, // Depends on https://github.com/elastic/kibana/issues/136409 item 2 - 6
/**

View file

@ -19,23 +19,6 @@ export type Maybe<T> = T | null;
export type SearchHit = IEsSearchResponse<object>['rawResponse']['hits']['hits'][0];
export interface KpiHistogramData {
x?: Maybe<number>;
y?: Maybe<number>;
}
export interface KpiHistogram<T> {
key_as_string: string;
key: number;
doc_count: number;
count: T;
}
export interface KpiGeneralHistogramCount {
value?: number;
doc_count?: number;
}
export interface PageInfoPaginated {
activePage: number;
fakeTotalCount: number;
@ -76,3 +59,14 @@ export interface GenericBuckets {
}
export type StringOrNumber = string | number;
export type Fields<T = unknown[]> = Record<string, T | Array<Fields<T>>>;
export interface EventHit extends SearchHit {
sort: string[];
_source: EventSource;
fields: Fields;
aggregations: {
[agg: string]: unknown;
};
}

View file

@ -10,7 +10,6 @@ import { HostsFields } from '../../../api/search_strategy/hosts/model/sort';
export * from './all';
export * from './common';
export * from './details';
export * from './kpi';
export * from './overview';
export * from './uncommon_processes';

View file

@ -1,13 +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 type { Maybe } from '../../../../common';
export interface HostsKpiHistogramData {
x?: Maybe<number>;
y?: Maybe<number>;
}

View file

@ -1,16 +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 type { IEsSearchResponse } from '@kbn/data-plugin/common';
import type { Inspect, Maybe } from '../../../../common';
import type { HostsKpiHistogramData } from '../common';
export interface HostsKpiHostsStrategyResponse extends IEsSearchResponse {
hosts: Maybe<number>;
hostsHistogram: Maybe<HostsKpiHistogramData[]>;
inspect?: Maybe<Inspect>;
}

View file

@ -1,25 +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.
*/
export * from '../../users/kpi/authentications';
export * from './common';
export * from './hosts';
export * from './unique_ips';
import type { UsersKpiAuthenticationsStrategyResponse } from '../../users/kpi/authentications';
import type { HostsKpiHostsStrategyResponse } from './hosts';
import type { HostsKpiUniqueIpsStrategyResponse } from './unique_ips';
export enum HostsKpiQueries {
kpiHosts = 'hostsKpiHosts',
kpiUniqueIps = 'hostsKpiUniqueIps',
}
export type HostsKpiStrategyResponse =
| Omit<UsersKpiAuthenticationsStrategyResponse, 'rawResponse'>
| Omit<HostsKpiHostsStrategyResponse, 'rawResponse'>
| Omit<HostsKpiUniqueIpsStrategyResponse, 'rawResponse'>;

View file

@ -1,18 +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 type { IEsSearchResponse } from '@kbn/data-plugin/common';
import type { Inspect, Maybe } from '../../../../common';
import type { HostsKpiHistogramData } from '../common';
export interface HostsKpiUniqueIpsStrategyResponse extends IEsSearchResponse {
uniqueSourceIps: Maybe<number>;
uniqueSourceIpsHistogram: Maybe<HostsKpiHistogramData[]>;
uniqueDestinationIps: Maybe<number>;
uniqueDestinationIpsHistogram: Maybe<HostsKpiHistogramData[]>;
inspect?: Maybe<Inspect>;
}

View file

@ -11,9 +11,6 @@ import type {
HostsQueries,
HostsStrategyResponse,
HostsUncommonProcessesStrategyResponse,
HostsKpiQueries,
HostsKpiHostsStrategyResponse,
HostsKpiUniqueIpsStrategyResponse,
} from './hosts';
import type {
NetworkQueries,
@ -25,15 +22,8 @@ import type {
NetworkTopCountriesStrategyResponse,
NetworkTopNFlowStrategyResponse,
NetworkUsersStrategyResponse,
NetworkKpiQueries,
NetworkKpiDnsStrategyResponse,
NetworkKpiNetworkEventsStrategyResponse,
NetworkKpiTlsHandshakesStrategyResponse,
NetworkKpiUniqueFlowsStrategyResponse,
NetworkKpiUniquePrivateIpsStrategyResponse,
NetworkTopNFlowCountStrategyResponse,
} from './network';
import type { MatrixHistogramQuery, MatrixHistogramStrategyResponse } from './matrix_histogram';
import type {
CtiEventEnrichmentStrategyResponse,
CtiQueries,
@ -48,9 +38,6 @@ import type {
} from './risk_score';
import type { UsersQueries } from './users';
import type { ObservedUserDetailsStrategyResponse } from './users/observed_details';
import type { TotalUsersKpiStrategyResponse } from './users/kpi/total_users';
import type { UsersKpiAuthenticationsStrategyResponse } from './users/kpi/authentications';
import type { UsersStrategyResponse } from './users/all';
import type { UserAuthenticationsStrategyResponse } from './users/authentications';
@ -61,8 +48,6 @@ import type { UsersRelatedHostsStrategyResponse } from './related_entities/relat
import type { HostsRelatedUsersStrategyResponse } from './related_entities/related_users';
import type {
AuthenticationsKpiRequestOptions,
AuthenticationsKpiRequestOptionsInput,
EventEnrichmentRequestOptions,
EventEnrichmentRequestOptionsInput,
FirstLastSeenRequestOptions,
@ -75,30 +60,14 @@ import type {
HostsRequestOptionsInput,
HostUncommonProcessesRequestOptions,
HostUncommonProcessesRequestOptionsInput,
KpiHostsRequestOptions,
KpiHostsRequestOptionsInput,
KpiUniqueIpsRequestOptions,
KpiUniqueIpsRequestOptionsInput,
ManagedUserDetailsRequestOptions,
ManagedUserDetailsRequestOptionsInput,
MatrixHistogramRequestOptions,
MatrixHistogramRequestOptionsInput,
NetworkDetailsRequestOptions,
NetworkDetailsRequestOptionsInput,
NetworkDnsRequestOptions,
NetworkDnsRequestOptionsInput,
NetworkHttpRequestOptions,
NetworkHttpRequestOptionsInput,
NetworkKpiDnsRequestOptions,
NetworkKpiDnsRequestOptionsInput,
NetworkKpiEventsRequestOptions,
NetworkKpiEventsRequestOptionsInput,
NetworkKpiTlsHandshakesRequestOptions,
NetworkKpiTlsHandshakesRequestOptionsInput,
NetworkKpiUniqueFlowsRequestOptions,
NetworkKpiUniqueFlowsRequestOptionsInput,
NetworkKpiUniquePrivateIpsRequestOptions,
NetworkKpiUniquePrivateIpsRequestOptionsInput,
NetworkOverviewRequestOptions,
NetworkOverviewRequestOptionsInput,
NetworkTlsRequestOptions,
@ -123,8 +92,6 @@ import type {
RiskScoreRequestOptionsInput,
ThreatIntelSourceRequestOptions,
ThreatIntelSourceRequestOptionsInput,
TotalUsersKpiRequestOptions,
TotalUsersKpiRequestOptionsInput,
UserAuthenticationsRequestOptions,
UserAuthenticationsRequestOptionsInput,
UsersRequestOptions,
@ -134,7 +101,6 @@ import type {
export * from './cti';
export * from './hosts';
export * from './risk_score';
export * from './matrix_histogram';
export * from './network';
export * from './users';
export * from './first_last_seen';
@ -142,13 +108,10 @@ export * from './related_entities';
export type FactoryQueryTypes =
| HostsQueries
| HostsKpiQueries
| UsersQueries
| NetworkQueries
| NetworkKpiQueries
| RiskQueries
| CtiQueries
| typeof MatrixHistogramQuery
| typeof FirstLastSeenQuery
| RelatedEntitiesQueries;
@ -162,22 +125,14 @@ export type StrategyResponseType<T extends FactoryQueryTypes> = T extends HostsQ
? FirstLastSeenStrategyResponse
: T extends HostsQueries.uncommonProcesses
? HostsUncommonProcessesStrategyResponse
: T extends HostsKpiQueries.kpiHosts
? HostsKpiHostsStrategyResponse
: T extends HostsKpiQueries.kpiUniqueIps
? HostsKpiUniqueIpsStrategyResponse
: T extends UsersQueries.observedDetails
? ObservedUserDetailsStrategyResponse
: T extends UsersQueries.managedDetails
? ManagedUserDetailsStrategyResponse
: T extends UsersQueries.kpiTotalUsers
? TotalUsersKpiStrategyResponse
: T extends UsersQueries.authentications
? UserAuthenticationsStrategyResponse
: T extends UsersQueries.users
? UsersStrategyResponse
: T extends UsersQueries.kpiAuthentications
? UsersKpiAuthenticationsStrategyResponse
: T extends NetworkQueries.details
? NetworkDetailsStrategyResponse
: T extends NetworkQueries.dns
@ -196,18 +151,6 @@ export type StrategyResponseType<T extends FactoryQueryTypes> = T extends HostsQ
? NetworkTopNFlowCountStrategyResponse
: T extends NetworkQueries.users
? NetworkUsersStrategyResponse
: T extends NetworkKpiQueries.dns
? NetworkKpiDnsStrategyResponse
: T extends NetworkKpiQueries.networkEvents
? NetworkKpiNetworkEventsStrategyResponse
: T extends NetworkKpiQueries.tlsHandshakes
? NetworkKpiTlsHandshakesStrategyResponse
: T extends NetworkKpiQueries.uniqueFlows
? NetworkKpiUniqueFlowsStrategyResponse
: T extends NetworkKpiQueries.uniquePrivateIps
? NetworkKpiUniquePrivateIpsStrategyResponse
: T extends typeof MatrixHistogramQuery
? MatrixHistogramStrategyResponse
: T extends CtiQueries.eventEnrichment
? CtiEventEnrichmentStrategyResponse
: T extends CtiQueries.dataSource
@ -234,22 +177,14 @@ export type StrategyRequestInputType<T extends FactoryQueryTypes> = T extends Ho
? FirstLastSeenRequestOptionsInput
: T extends HostsQueries.uncommonProcesses
? HostUncommonProcessesRequestOptionsInput
: T extends HostsKpiQueries.kpiHosts
? KpiHostsRequestOptionsInput
: T extends HostsKpiQueries.kpiUniqueIps
? KpiUniqueIpsRequestOptionsInput
: T extends UsersQueries.authentications
? UserAuthenticationsRequestOptionsInput
: T extends UsersQueries.observedDetails
? ObservedUserDetailsRequestOptionsInput
: T extends UsersQueries.managedDetails
? ManagedUserDetailsRequestOptionsInput
: T extends UsersQueries.kpiTotalUsers
? TotalUsersKpiRequestOptionsInput
: T extends UsersQueries.users
? UsersRequestOptionsInput
: T extends UsersQueries.kpiAuthentications
? AuthenticationsKpiRequestOptionsInput
: T extends NetworkQueries.details
? NetworkDetailsRequestOptionsInput
: T extends NetworkQueries.dns
@ -268,18 +203,6 @@ export type StrategyRequestInputType<T extends FactoryQueryTypes> = T extends Ho
? NetworkTopNFlowCountRequestOptionsInput
: T extends NetworkQueries.users
? NetworkUsersRequestOptionsInput
: T extends NetworkKpiQueries.dns
? NetworkKpiDnsRequestOptionsInput
: T extends NetworkKpiQueries.networkEvents
? NetworkKpiEventsRequestOptionsInput
: T extends NetworkKpiQueries.tlsHandshakes
? NetworkKpiTlsHandshakesRequestOptionsInput
: T extends NetworkKpiQueries.uniqueFlows
? NetworkKpiUniqueFlowsRequestOptionsInput
: T extends NetworkKpiQueries.uniquePrivateIps
? NetworkKpiUniquePrivateIpsRequestOptionsInput
: T extends typeof MatrixHistogramQuery
? MatrixHistogramRequestOptionsInput
: T extends CtiQueries.eventEnrichment
? EventEnrichmentRequestOptionsInput
: T extends CtiQueries.dataSource
@ -306,22 +229,14 @@ export type StrategyRequestType<T extends FactoryQueryTypes> = T extends HostsQu
? FirstLastSeenRequestOptions
: T extends HostsQueries.uncommonProcesses
? HostUncommonProcessesRequestOptions
: T extends HostsKpiQueries.kpiHosts
? KpiHostsRequestOptions
: T extends HostsKpiQueries.kpiUniqueIps
? KpiUniqueIpsRequestOptions
: T extends UsersQueries.authentications
? UserAuthenticationsRequestOptions
: T extends UsersQueries.observedDetails
? ObservedUserDetailsRequestOptions
: T extends UsersQueries.managedDetails
? ManagedUserDetailsRequestOptions
: T extends UsersQueries.kpiTotalUsers
? TotalUsersKpiRequestOptions
: T extends UsersQueries.users
? UsersRequestOptions
: T extends UsersQueries.kpiAuthentications
? AuthenticationsKpiRequestOptions
: T extends NetworkQueries.details
? NetworkDetailsRequestOptions
: T extends NetworkQueries.dns
@ -340,18 +255,6 @@ export type StrategyRequestType<T extends FactoryQueryTypes> = T extends HostsQu
? NetworkTopNFlowCountRequestOptions
: T extends NetworkQueries.users
? NetworkUsersRequestOptions
: T extends NetworkKpiQueries.dns
? NetworkKpiDnsRequestOptions
: T extends NetworkKpiQueries.networkEvents
? NetworkKpiEventsRequestOptions
: T extends NetworkKpiQueries.tlsHandshakes
? NetworkKpiTlsHandshakesRequestOptions
: T extends NetworkKpiQueries.uniqueFlows
? NetworkKpiUniqueFlowsRequestOptions
: T extends NetworkKpiQueries.uniquePrivateIps
? NetworkKpiUniquePrivateIpsRequestOptions
: T extends typeof MatrixHistogramQuery
? MatrixHistogramRequestOptions
: T extends CtiQueries.eventEnrichment
? EventEnrichmentRequestOptions
: T extends CtiQueries.dataSource

View file

@ -1,16 +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 type { HistogramBucket } from '../common';
export interface AlertsGroupData {
key: string;
doc_count: number;
alerts: {
buckets: HistogramBucket[];
};
}

View file

@ -1,20 +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.
*/
interface AnomaliesOverTimeHistogramData {
key_as_string: string;
key: number;
doc_count: number;
}
export interface AnomaliesActionGroupData {
key: number;
anomalies: {
bucket: AnomaliesOverTimeHistogramData[];
};
doc_count: number;
}

View file

@ -1,20 +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.
*/
export interface AuthenticationsOverTimeHistogramData {
key_as_string: string;
key: number;
doc_count: number;
}
export interface AuthenticationsActionGroupData {
key: number;
events: {
bucket: AuthenticationsOverTimeHistogramData[];
};
doc_count: number;
}

View file

@ -1,11 +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.
*/
export interface HistogramBucket {
key: number;
doc_count: number;
}

View file

@ -1,26 +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.
*/
export interface DnsHistogramSubBucket {
key: string;
doc_count: number;
orderAgg: {
value: number;
};
}
interface DnsHistogramBucket {
doc_count_error_upper_bound: number;
sum_other_doc_count: number;
buckets: DnsHistogramSubBucket[];
}
export interface DnsHistogramGroupData {
key: number;
doc_count: number;
key_as_string: string;
histogram: DnsHistogramBucket;
}

View file

@ -1,41 +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 type { SearchHit } from '../../../common';
interface EventsMatrixHistogramData {
key_as_string: string;
key: number;
doc_count: number;
}
export interface EventSource {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[field: string]: any;
}
export interface EventsActionGroupData {
key: number | string;
events: {
bucket: EventsMatrixHistogramData[];
};
doc_count: number;
}
export interface Fields<T = unknown[]> {
[x: string]: T | Array<Fields<T>>;
}
export interface EventHit extends SearchHit {
sort: string[];
_source: EventSource;
fields: Fields;
aggregations: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[agg: string]: any;
};
}

View file

@ -1,87 +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 type { IEsSearchResponse } from '@kbn/data-plugin/common';
import type { MatrixHistogramRequestOptions } from '../../../api/search_strategy/matrix_histogram/matrix_histogram';
import type { Inspect, Maybe } from '../../common';
import type { AlertsGroupData } from './alerts';
import type { AnomaliesActionGroupData } from './anomalies';
import type { DnsHistogramGroupData } from './dns';
import type { AuthenticationsActionGroupData } from './authentications';
import type { EventsActionGroupData } from './events';
import type { PreviewHistogramGroupData } from './preview';
export * from './alerts';
export * from './anomalies';
export * from './authentications';
export * from './common';
export * from './dns';
export * from './events';
export * from './preview';
export { MatrixHistogramQuery } from '../../../api/search_strategy';
export enum MatrixHistogramType {
authentications = 'authentications',
anomalies = 'anomalies',
events = 'events',
alerts = 'alerts',
dns = 'dns',
preview = 'preview',
}
export const MatrixHistogramTypeToAggName = {
[MatrixHistogramType.alerts]: 'aggregations.alertsGroup.buckets',
[MatrixHistogramType.anomalies]: 'aggregations.anomalyActionGroup.buckets',
[MatrixHistogramType.authentications]: 'aggregations.eventActionGroup.buckets',
[MatrixHistogramType.dns]: 'aggregations.dns_name_query_count.buckets',
[MatrixHistogramType.events]: 'aggregations.eventActionGroup.buckets',
[MatrixHistogramType.preview]: 'aggregations.preview.buckets',
};
export interface MatrixHistogramStrategyResponse extends IEsSearchResponse {
inspect?: Maybe<Inspect>;
matrixHistogramData: MatrixHistogramData[];
totalCount: number;
}
export interface MatrixHistogramData {
x?: Maybe<number>;
y?: Maybe<number>;
g?: Maybe<string>;
}
export interface MatrixHistogramBucket {
key: number;
doc_count: number;
}
export interface MatrixHistogramSchema<T> {
buildDsl: (options: MatrixHistogramRequestOptions) => {};
aggName: string;
parseKey: string;
parser?: <U>(data: MatrixHistogramParseData<U>, keyBucket: string) => MatrixHistogramData[];
}
export type MatrixHistogramParseData<T> = T extends MatrixHistogramType.alerts
? AlertsGroupData[]
: T extends MatrixHistogramType.anomalies
? AnomaliesActionGroupData[]
: T extends MatrixHistogramType.dns
? DnsHistogramGroupData[]
: T extends MatrixHistogramType.authentications
? AuthenticationsActionGroupData[]
: T extends MatrixHistogramType.events
? EventsActionGroupData[]
: T extends MatrixHistogramType.preview
? PreviewHistogramGroupData[]
: never;
export type MatrixHistogramDataConfig = Record<
MatrixHistogramType,
MatrixHistogramSchema<MatrixHistogramType>
>;

View file

@ -1,16 +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 type { HistogramBucket } from '../common';
export interface PreviewHistogramGroupData {
key: string;
doc_count: number;
preview: {
buckets: HistogramBucket[];
};
}

View file

@ -9,7 +9,6 @@ export * from './common';
export * from './details';
export * from './dns';
export * from './http';
export * from './kpi';
export * from './overview';
export * from './tls';
export * from './top_countries';

View file

@ -1,14 +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 type { IEsSearchResponse } from '@kbn/data-plugin/common';
import type { Inspect, Maybe } from '../../../../common';
export interface NetworkKpiDnsStrategyResponse extends IEsSearchResponse {
dnsQueries: number;
inspect?: Maybe<Inspect>;
}

View file

@ -1,33 +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.
*/
export * from './dns';
export * from './network_events';
export * from './tls_handshakes';
export * from './unique_flows';
export * from './unique_private_ips';
import type { NetworkKpiDnsStrategyResponse } from './dns';
import type { NetworkKpiNetworkEventsStrategyResponse } from './network_events';
import type { NetworkKpiTlsHandshakesStrategyResponse } from './tls_handshakes';
import type { NetworkKpiUniqueFlowsStrategyResponse } from './unique_flows';
import type { NetworkKpiUniquePrivateIpsStrategyResponse } from './unique_private_ips';
export enum NetworkKpiQueries {
dns = 'networkKpiDns',
networkEvents = 'networkKpiNetworkEvents',
tlsHandshakes = 'networkKpiTlsHandshakes',
uniqueFlows = 'networkKpiUniqueFlows',
uniquePrivateIps = 'networkKpiUniquePrivateIps',
}
export type NetworkKpiStrategyResponse =
| Omit<NetworkKpiDnsStrategyResponse, 'rawResponse'>
| Omit<NetworkKpiNetworkEventsStrategyResponse, 'rawResponse'>
| Omit<NetworkKpiTlsHandshakesStrategyResponse, 'rawResponse'>
| Omit<NetworkKpiUniqueFlowsStrategyResponse, 'rawResponse'>
| Omit<NetworkKpiUniquePrivateIpsStrategyResponse, 'rawResponse'>;

View file

@ -1,14 +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 type { IEsSearchResponse } from '@kbn/data-plugin/common';
import type { Inspect, Maybe } from '../../../../common';
export interface NetworkKpiNetworkEventsStrategyResponse extends IEsSearchResponse {
networkEvents: number;
inspect?: Maybe<Inspect>;
}

View file

@ -1,14 +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 type { IEsSearchResponse } from '@kbn/data-plugin/common';
import type { Inspect, Maybe } from '../../../../common';
export interface NetworkKpiTlsHandshakesStrategyResponse extends IEsSearchResponse {
tlsHandshakes: number;
inspect?: Maybe<Inspect>;
}

View file

@ -1,14 +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 type { IEsSearchResponse } from '@kbn/data-plugin/common';
import type { Inspect, Maybe } from '../../../../common';
export interface NetworkKpiUniqueFlowsStrategyResponse extends IEsSearchResponse {
uniqueFlowId: number;
inspect?: Maybe<Inspect>;
}

View file

@ -1,24 +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 type { IEsSearchResponse } from '@kbn/data-plugin/common';
import type { Inspect, Maybe } from '../../../../common';
export interface NetworkKpiHistogramData {
x?: Maybe<number>;
y?: Maybe<number>;
}
export interface NetworkKpiUniquePrivateIpsStrategyResponse extends IEsSearchResponse {
uniqueSourcePrivateIps: number;
uniqueSourcePrivateIpsHistogram: NetworkKpiHistogramData[] | null;
uniqueDestinationPrivateIps: number;
uniqueDestinationPrivateIpsHistogram: NetworkKpiHistogramData[] | null;
inspect?: Maybe<Inspect>;
}
export type UniquePrivateAttributeQuery = 'source' | 'destination';

View file

@ -5,14 +5,9 @@
* 2.0.
*/
import type { TotalUsersKpiStrategyResponse } from './kpi/total_users';
export * from './all';
export * from './common';
export * from './kpi';
export * from './observed_details';
export * from './authentications';
export { UsersQueries } from '../../../api/search_strategy';
export type UsersKpiStrategyResponse = Omit<TotalUsersKpiStrategyResponse, 'rawResponse'>;

View file

@ -1,17 +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 type { IEsSearchResponse } from '@kbn/data-plugin/common';
import type { Inspect, KpiHistogramData, Maybe } from '../../../../common';
export interface UsersKpiAuthenticationsStrategyResponse extends IEsSearchResponse {
authenticationsSuccess: Maybe<number>;
authenticationsSuccessHistogram: Maybe<KpiHistogramData[]>;
authenticationsFailure: Maybe<number>;
authenticationsFailureHistogram: Maybe<KpiHistogramData[]>;
inspect?: Maybe<Inspect>;
}

View file

@ -1,8 +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.
*/
export * from './total_users';

View file

@ -1,15 +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 type { IEsSearchResponse } from '@kbn/data-plugin/common';
import type { Inspect, KpiHistogramData, Maybe } from '../../../../common';
export interface TotalUsersKpiStrategyResponse extends IEsSearchResponse {
users: Maybe<number>;
usersHistogram: Maybe<KpiHistogramData[]>;
inspect?: Maybe<Inspect>;
}

View file

@ -175,7 +175,6 @@ const EventsQueryTabBodyComponent: React.FC<EventsQueryTabBodyComponentProps> =
startDate={startDate}
endDate={endDate}
filterQuery={filterQuery}
indexNames={indexNames}
setQuery={setQuery}
{...(showExternalAlerts ? alertsHistogramConfig : eventsHistogramConfig)}
subtitle={getHistogramSubtitle}

View file

@ -6,7 +6,6 @@
*/
import numeral from '@elastic/numeral';
import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution';
import { getExternalAlertLensAttributes } from '../visualization_actions/lens_attributes/common/external_alert';
import { getEventsHistogramLensAttributes } from '../visualization_actions/lens_attributes/common/events';
import type { MatrixHistogramConfigs, MatrixHistogramOption } from '../matrix_histogram/types';
@ -38,8 +37,6 @@ export const eventsStackByOptions: MatrixHistogramOption[] = [
export const eventsHistogramConfig: MatrixHistogramConfigs = {
defaultStackByOption:
eventsStackByOptions.find((o) => o.text === DEFAULT_EVENTS_STACK_BY) ?? eventsStackByOptions[0],
errorMessage: i18n.ERROR_FETCHING_EVENTS_DATA,
histogramType: MatrixHistogramType.events,
stackByOptions: eventsStackByOptions,
subtitle: undefined,
title: i18n.EVENTS_GRAPH_TITLE,
@ -62,10 +59,7 @@ const DEFAULT_STACK_BY = 'event.module';
export const alertsHistogramConfig: MatrixHistogramConfigs = {
defaultStackByOption:
alertsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0],
errorMessage: i18n.ERROR_FETCHING_ALERTS_DATA,
histogramType: MatrixHistogramType.alerts,
stackByOptions: alertsStackByOptions,
subtitle: undefined,
title: i18n.ALERTS_GRAPH_TITLE,
getLensAttributes: getExternalAlertLensAttributes,
};

View file

@ -1,29 +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 type { BarChartComponentProps } from '../charts/barchart';
import { BarChart } from '../charts/barchart';
import { MatrixLoader } from './matrix_loader';
const MatrixHistogramChartContentComponent = ({
isInitialLoading,
barChart,
configs,
stackByField,
scopeId,
}: BarChartComponentProps & { isInitialLoading: boolean }) => {
return isInitialLoading ? (
<MatrixLoader />
) : (
<BarChart barChart={barChart} configs={configs} stackByField={stackByField} scopeId={scopeId} />
);
};
export const MatrixHistogramChartContent = React.memo(MatrixHistogramChartContentComponent);
MatrixHistogramChartContentComponent.displayName = 'MatrixHistogramChartContentComponent';

View file

@ -10,29 +10,13 @@ import { mount } from 'enzyme';
import React from 'react';
import { MatrixHistogram } from '.';
import { useMatrixHistogramCombined } from '../../containers/matrix_histogram';
import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution';
import { TestProviders } from '../../mock';
import { mockRuntimeMappings } from '../../containers/source/mock';
import { getDnsTopDomainsLensAttributes } from '../visualization_actions/lens_attributes/network/dns_top_domains';
import { useQueryToggle } from '../../containers/query_toggle';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
import type { ExperimentalFeatures } from '../../../../common/experimental_features';
import { allowedExperimentalValues } from '../../../../common/experimental_features';
import { VisualizationActions } from '../visualization_actions/actions';
jest.mock('../../containers/query_toggle');
jest.mock('./matrix_loader', () => ({
MatrixLoader: () => <div className="matrixLoader" />,
}));
jest.mock('../charts/barchart', () => ({
BarChart: () => <div className="barchart" />,
}));
jest.mock('../../containers/matrix_histogram');
jest.mock('../visualization_actions/actions');
jest.mock('../visualization_actions/visualization_embeddable');
@ -40,20 +24,16 @@ jest.mock('../../hooks/use_experimental_features', () => ({
useIsExperimentalFeatureEnabled: jest.fn(),
}));
jest.mock('./utils', () => ({
getBarchartConfigs: jest.fn(),
getCustomChartData: jest.fn().mockReturnValue(true),
}));
const mockUseVisualizationResponse = jest.fn(() => ({
responses: [{ aggregations: [{ buckets: [{ key: '1234' }] }], hits: { total: 999 } }],
requests: [],
loading: false,
}));
jest.mock('../visualization_actions/use_visualization_response', () => ({
useVisualizationResponse: () => mockUseVisualizationResponse(),
}));
const mockLocation = jest.fn().mockReturnValue({ pathname: '/test' });
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
@ -74,15 +54,11 @@ describe('Matrix Histogram Component', () => {
value: 'dns.question.registered_domain',
},
endDate: '2019-07-18T20:00:00.000Z',
errorMessage: 'error',
histogramType: MatrixHistogramType.alerts,
id: 'mockId',
indexNames: [],
isInspected: false,
isPtrIncluded: true,
setQuery: jest.fn(),
skip: false,
sourceId: 'default',
stackByOptions: [
{ text: 'dns.question.registered_domain', value: 'dns.question.registered_domain' },
],
@ -92,51 +68,51 @@ describe('Matrix Histogram Component', () => {
title: 'mockTitle',
runtimeMappings: mockRuntimeMappings,
};
const mockUseMatrix = useMatrixHistogramCombined as jest.Mock;
const mockUseQueryToggle = useQueryToggle as jest.Mock;
const mockSetToggle = jest.fn();
const getMockUseIsExperimentalFeatureEnabled =
(mockMapping?: Partial<ExperimentalFeatures>) =>
(flag: keyof typeof allowedExperimentalValues) =>
mockMapping ? mockMapping?.[flag] : allowedExperimentalValues?.[flag];
beforeEach(() => {
jest.clearAllMocks();
mockUseIsExperimentalFeatureEnabled.mockImplementation(
getMockUseIsExperimentalFeatureEnabled({ chartEmbeddablesEnabled: false })
);
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
mockUseMatrix.mockReturnValue([
false,
{
data: null,
inspect: false,
totalCount: null,
},
]);
});
describe('on initial load', () => {
describe('rendering', () => {
beforeEach(() => {
wrapper = mount(<MatrixHistogram {...mockMatrixOverTimeHistogramProps} />, {
wrappingComponent: TestProviders,
});
});
test('it requests Matrix Histogram', () => {
expect(mockUseMatrix).toHaveBeenCalledWith({
endDate: mockMatrixOverTimeHistogramProps.endDate,
errorMessage: mockMatrixOverTimeHistogramProps.errorMessage,
histogramType: mockMatrixOverTimeHistogramProps.histogramType,
indexNames: mockMatrixOverTimeHistogramProps.indexNames,
startDate: mockMatrixOverTimeHistogramProps.startDate,
stackByField: mockMatrixOverTimeHistogramProps.defaultStackByOption.value,
runtimeMappings: mockMatrixOverTimeHistogramProps.runtimeMappings,
isPtrIncluded: mockMatrixOverTimeHistogramProps.isPtrIncluded,
skip: mockMatrixOverTimeHistogramProps.skip,
});
test('it should not render VisualizationActions', () => {
expect(wrapper.find(`[data-test-subj="visualizationActions"]`).exists()).toEqual(false);
});
test('it renders MatrixLoader', () => {
expect(wrapper.find('MatrixLoader').exists()).toBe(true);
test('it should render Lens Visualization', () => {
expect(wrapper.find(`[data-test-subj="visualization-embeddable"]`).exists()).toEqual(true);
});
test('it should render visualization count as subtitle', () => {
wrapper.setProps({ endDate: 100 });
wrapper.update();
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).text()).toEqual(
'Showing: 999 events'
);
});
test('it should render 0 as subtitle when buckets are empty', () => {
mockUseVisualizationResponse.mockReturnValue({
requests: [],
responses: [{ aggregations: [{ buckets: [] }], hits: { total: 999 } }],
loading: false,
});
wrapper.setProps({ endDate: 100 });
wrapper.update();
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).text()).toEqual(
'Showing: 0 events'
);
});
});
@ -145,7 +121,7 @@ describe('Matrix Histogram Component', () => {
wrapper = mount(<MatrixHistogram {...mockMatrixOverTimeHistogramProps} />, {
wrappingComponent: TestProviders,
});
expect(wrapper.find('[data-test-subj="spacer"]').exists()).toBe(true);
expect(wrapper.find('[data-test-subj="spacer"]').exists()).toEqual(true);
});
test('it does NOT render a spacer when showSpacer is false', () => {
@ -155,39 +131,7 @@ describe('Matrix Histogram Component', () => {
wrappingComponent: TestProviders,
}
);
expect(wrapper.find('[data-test-subj="spacer"]').exists()).toBe(false);
});
});
describe('not initial load', () => {
beforeEach(() => {
wrapper = mount(<MatrixHistogram {...mockMatrixOverTimeHistogramProps} />, {
wrappingComponent: TestProviders,
});
mockUseMatrix.mockReturnValue([
false,
{
data: [
{ x: 1, y: 2, g: 'g1' },
{ x: 2, y: 4, g: 'g1' },
{ x: 3, y: 6, g: 'g1' },
{ x: 1, y: 1, g: 'g2' },
{ x: 2, y: 3, g: 'g2' },
{ x: 3, y: 5, g: 'g2' },
],
inspect: false,
totalCount: 1,
},
]);
wrapper.setProps({ endDate: 100 });
wrapper.update();
});
test('it renders no MatrixLoader', () => {
expect(wrapper.find(`MatrixLoader`).exists()).toBe(false);
});
test('it shows BarChart if data available', () => {
expect(wrapper.find(`.barchart`).exists()).toBe(true);
expect(wrapper.find('[data-test-subj="spacer"]').exists()).toEqual(false);
});
});
@ -196,12 +140,12 @@ describe('Matrix Histogram Component', () => {
wrapper = mount(<MatrixHistogram {...mockMatrixOverTimeHistogramProps} />, {
wrappingComponent: TestProviders,
});
expect(wrapper.find('EuiSelect').exists()).toBe(false);
expect(wrapper.find('EuiSelect').exists()).toEqual(false);
});
});
describe('Inspect button', () => {
test("it doesn't render Inspect button by default", () => {
test('it does not render Inspect button', () => {
const testProps = {
...mockMatrixOverTimeHistogramProps,
getLensAttributes: getDnsTopDomainsLensAttributes,
@ -209,46 +153,7 @@ describe('Matrix Histogram Component', () => {
wrapper = mount(<MatrixHistogram {...testProps} />, {
wrappingComponent: TestProviders,
});
expect(wrapper.find('[data-test-subj="inspect-icon-button"]').exists()).toBe(false);
});
});
describe('VisualizationActions', () => {
const testProps = {
...mockMatrixOverTimeHistogramProps,
getLensAttributes: jest.fn().mockReturnValue(getDnsTopDomainsLensAttributes()),
};
beforeEach(() => {
wrapper = mount(<MatrixHistogram {...testProps} />, {
wrappingComponent: TestProviders,
});
});
test('it renders VisualizationActions if getLensAttributes is provided', () => {
expect(wrapper.find('[data-test-subj="visualizationActions"]').exists()).toBe(true);
expect(wrapper.find('[data-test-subj="visualizationActions"]').prop('className')).toEqual(
'histogram-viz-actions'
);
});
test('it VisualizationActions with correct properties', () => {
expect((VisualizationActions as unknown as jest.Mock).mock.calls[0][0]).toEqual(
expect.objectContaining({
className: 'histogram-viz-actions',
extraOptions: {
dnsIsPtrIncluded: testProps.isPtrIncluded,
},
getLensAttributes: testProps.getLensAttributes,
lensAttributes: undefined,
isInspectButtonDisabled: true,
queryId: testProps.id,
stackByField: testProps.defaultStackByOption.value,
timerange: {
from: testProps.startDate,
to: testProps.endDate,
},
title: testProps.title,
})
);
expect(wrapper.find('[data-test-subj="inspect-icon-button"]').exists()).toEqual(false);
});
});
@ -262,25 +167,16 @@ describe('Matrix Histogram Component', () => {
wrapper = mount(<MatrixHistogram {...testProps} />, {
wrappingComponent: TestProviders,
});
expect(mockUseMatrix.mock.calls[0][0].skip).toEqual(false);
expect(wrapper.find('[data-test-subj="visualization-embeddable"]').exists()).toEqual(true);
wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click');
expect(mockSetToggle).toBeCalledWith(false);
expect(mockUseMatrix.mock.calls[1][0].skip).toEqual(true);
});
test('toggleStatus=true, do not skip', () => {
wrapper = mount(<MatrixHistogram {...testProps} />, {
wrappingComponent: TestProviders,
});
expect(mockUseMatrix.mock.calls[0][0].skip).toEqual(false);
});
test('toggleStatus=true, render components', () => {
wrapper = mount(<MatrixHistogram {...testProps} />, {
wrappingComponent: TestProviders,
});
expect(wrapper.find('MatrixLoader').exists()).toBe(true);
expect(wrapper.find('[data-test-subj="visualization-embeddable"]').exists()).toEqual(true);
});
test('toggleStatus=false, do not render components', () => {
@ -297,71 +193,7 @@ describe('Matrix Histogram Component', () => {
wrappingComponent: TestProviders,
});
expect(mockUseMatrix.mock.calls[0][0].skip).toEqual(true);
});
});
describe('when the chartEmbeddablesEnabled experimental feature flag is enabled', () => {
beforeEach(() => {
const mockMapping: Partial<ExperimentalFeatures> = {
chartEmbeddablesEnabled: true,
};
mockUseIsExperimentalFeatureEnabled.mockImplementation(
getMockUseIsExperimentalFeatureEnabled(mockMapping)
);
wrapper = mount(<MatrixHistogram {...mockMatrixOverTimeHistogramProps} />, {
wrappingComponent: TestProviders,
});
});
test('it should not render VisualizationActions', () => {
expect(wrapper.find(`[data-test-subj="visualizationActions"]`).exists()).toEqual(false);
});
test('it should not fetch Matrix Histogram data', () => {
expect(mockUseMatrix.mock.calls[0][0].skip).toEqual(true);
});
test('it should render Lens Embeddable', () => {
expect(wrapper.find(`[data-test-subj="visualization-embeddable"]`).exists()).toEqual(true);
});
test('it should render visualization count as subtitle', () => {
mockUseMatrix.mockReturnValue([
false,
{
data: [],
inspect: false,
totalCount: 0,
},
]);
wrapper.setProps({ endDate: 100 });
wrapper.update();
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).text()).toEqual(
'Showing: 999 events'
);
});
test('it should render 0 as subtitle when buckets are empty', () => {
mockUseVisualizationResponse.mockReturnValue({
responses: [{ aggregations: [{ buckets: [] }], hits: { total: 999 } }],
});
mockUseMatrix.mockReturnValue([
false,
{
data: [],
inspect: false,
totalCount: 0,
},
]);
wrapper.setProps({ endDate: 100 });
wrapper.update();
expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).text()).toEqual(
'Showing: 0 events'
);
expect(wrapper.find('[data-test-subj="visualization-embeddable"]').exists()).toEqual(false);
});
});
});

View file

@ -6,68 +6,37 @@
*/
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import type { Position } from '@elastic/charts';
import styled from 'styled-components';
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSelect, EuiSpacer } from '@elastic/eui';
import { useDispatch } from 'react-redux';
import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiSpacer } from '@elastic/eui';
import type { AggregationsTermsAggregateBase } from '@elastic/elasticsearch/lib/api/types';
import { isString } from 'lodash/fp';
import * as i18n from './translations';
import { HeaderSection } from '../header_section';
import { Panel } from '../panel';
import { getBarchartConfigs, getCustomChartData } from './utils';
import { useMatrixHistogramCombined } from '../../containers/matrix_histogram';
import type {
MatrixHistogramProps,
MatrixHistogramOption,
MatrixHistogramQueryProps,
MatrixHistogramMappingTypes,
GetTitle,
GetSubTitle,
MatrixHistogramConfigs,
} from './types';
import type { MatrixHistogramType } from '../../../../common/search_strategy/security_solution';
import type { GlobalTimeArgs } from '../../containers/use_global_time';
import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions';
import { InputsModelId } from '../../store/inputs/constants';
import { HoverVisibilityContainer } from '../hover_visibility_container';
import { VisualizationActions } from '../visualization_actions/actions';
import type {
GetLensAttributes,
LensAttributes,
VisualizationResponse,
} from '../visualization_actions/types';
import type { VisualizationResponse } from '../visualization_actions/types';
import { useQueryToggle } from '../../containers/query_toggle';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
import { VISUALIZATION_ACTIONS_BUTTON_CLASS } from '../visualization_actions/utils';
import { VisualizationEmbeddable } from '../visualization_actions/visualization_embeddable';
import { MatrixHistogramChartContent } from './chart_content';
import { useVisualizationResponse } from '../visualization_actions/use_visualization_response';
import type { SourcererScopeName } from '../../store/sourcerer/model';
export type MatrixHistogramComponentProps = MatrixHistogramProps &
Omit<MatrixHistogramQueryProps, 'stackByField'> & {
defaultStackByOption: MatrixHistogramOption;
errorMessage: string;
getLensAttributes?: GetLensAttributes;
export type MatrixHistogramComponentProps = MatrixHistogramQueryProps &
MatrixHistogramConfigs & {
headerChildren?: React.ReactNode;
hideHistogramIfEmpty?: boolean;
histogramType: MatrixHistogramType;
id: string;
legendPosition?: Position;
lensAttributes?: LensAttributes;
mapping?: MatrixHistogramMappingTypes;
onError?: () => void;
showSpacer?: boolean;
setQuery: GlobalTimeArgs['setQuery'];
showInspectButton?: boolean;
setAbsoluteRangeDatePickerTarget?: InputsModelId;
showLegend?: boolean;
stackByOptions: MatrixHistogramOption[];
subtitle?: string | GetSubTitle;
scopeId?: string;
sourcererScopeId?: SourcererScopeName;
title: string | GetTitle;
hideQueryToggle?: boolean;
applyGlobalQueriesAndFilters?: boolean;
};
@ -95,71 +64,28 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
chartHeight,
defaultStackByOption,
endDate,
errorMessage,
filterQuery,
getLensAttributes,
headerChildren,
histogramType,
hideHistogramIfEmpty = false,
id,
indexNames,
runtimeMappings,
isPtrIncluded,
legendPosition,
lensAttributes,
mapping,
onError,
paddingSize = 'm',
panelHeight = DEFAULT_PANEL_HEIGHT,
setAbsoluteRangeDatePickerTarget = InputsModelId.global,
setQuery,
showInspectButton = false,
showLegend,
showSpacer = true,
stackByOptions,
startDate,
subtitle,
scopeId,
sourcererScopeId,
title,
titleSize,
yTickFormatter,
skip,
hideQueryToggle = false,
applyGlobalQueriesAndFilters = true,
}) => {
const visualizationId = `${id}-embeddable`;
const dispatch = useDispatch();
const handleBrushEnd = useCallback(
({ x }) => {
if (!x) {
return;
}
const [min, max] = x;
dispatch(
setAbsoluteRangeDatePicker({
id: setAbsoluteRangeDatePickerTarget,
from: new Date(min).toISOString(),
to: new Date(max).toISOString(),
})
);
},
[dispatch, setAbsoluteRangeDatePickerTarget]
);
const barchartConfigs = useMemo(
() =>
getBarchartConfigs({
chartHeight,
from: startDate,
legendPosition,
to: endDate,
onBrushEnd: handleBrushEnd,
yTickFormatter,
showLegend,
}),
[chartHeight, startDate, legendPosition, endDate, handleBrushEnd, yTickFormatter, showLegend]
);
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [selectedStackByOption, setSelectedStackByOption] =
useState<MatrixHistogramOption>(defaultStackByOption);
@ -178,93 +104,48 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
);
const { toggleStatus, setToggleStatus } = useQueryToggle(id);
const [querySkip, setQuerySkip] = useState(skip || !toggleStatus);
useEffect(() => {
setQuerySkip(skip || !toggleStatus);
}, [skip, toggleStatus]);
const toggleQuery = useCallback(
(status: boolean) => {
setToggleStatus(status);
// toggle on = skipQuery false
setQuerySkip(!status);
},
[setQuerySkip, setToggleStatus]
[setToggleStatus]
);
const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled');
const matrixHistogramRequest = {
endDate,
errorMessage,
filterQuery,
histogramType,
indexNames,
onError,
startDate,
stackByField: selectedStackByOption.value,
runtimeMappings,
isPtrIncluded,
skip: querySkip || isChartEmbeddablesEnabled,
};
const [loading, { data, inspect, totalCount, refetch }] =
useMatrixHistogramCombined(matrixHistogramRequest);
const titleWithStackByField = useMemo(
() => (title != null && typeof title === 'function' ? title(selectedStackByOption) : title),
[title, selectedStackByOption]
);
const { responses } = useVisualizationResponse({ visualizationId });
const { responses: visualizationResponses } = useVisualizationResponse({ visualizationId });
const visualizationTotalCount: number | null = useMemo(() => {
if (!visualizationResponses || !visualizationResponseHasData(visualizationResponses)) {
return 0;
}
return visualizationResponses[0].hits.total;
}, [visualizationResponses]);
const subtitleWithCounts = useMemo(() => {
if (isInitialLoading) {
return null;
}
if (typeof subtitle === 'function') {
if (isChartEmbeddablesEnabled) {
if (!responses || !visualizationResponseHasData(responses)) {
return subtitle(0);
}
const visualizationCount = responses[0].hits.total;
return visualizationCount >= 0 ? subtitle(visualizationCount) : null;
} else {
return totalCount >= 0 ? subtitle(totalCount) : null;
}
return visualizationTotalCount >= 0 ? subtitle(visualizationTotalCount) : null;
}
return subtitle;
}, [isChartEmbeddablesEnabled, isInitialLoading, responses, subtitle, totalCount]);
}, [isInitialLoading, subtitle, visualizationTotalCount]);
const hideHistogram = useMemo(
() => (totalCount <= 0 && hideHistogramIfEmpty ? true : false),
[totalCount, hideHistogramIfEmpty]
() => ((visualizationTotalCount ?? 0) <= 0 && hideHistogramIfEmpty ? true : false),
[hideHistogramIfEmpty, visualizationTotalCount]
);
const barChartData = useMemo(() => getCustomChartData(data, mapping), [data, mapping]);
useEffect(() => {
if (!loading && !isInitialLoading) {
setQuery({
id,
inspect,
loading,
refetch,
});
}
if (isInitialLoading && !!barChartData && data) {
if (isInitialLoading && !!visualizationResponses) {
setIsInitialLoading(false);
}
}, [
barChartData,
data,
id,
inspect,
isChartEmbeddablesEnabled,
isInitialLoading,
loading,
refetch,
setIsInitialLoading,
setQuery,
]);
}, [id, isInitialLoading, visualizationResponses, setIsInitialLoading, setQuery]);
const timerange = useMemo(() => ({ from: startDate, to: endDate }), [startDate, endDate]);
const extraVisualizationOptions = useMemo(
@ -297,15 +178,6 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
height={toggleStatus ? panelHeight : undefined}
paddingSize={paddingSize}
>
{loading && !isInitialLoading && (
<EuiProgress
data-test-subj="initialLoadingPanelMatrixOverTime"
size="xs"
position="absolute"
color="accent"
/>
)}
<HeaderSection
id={id}
height={toggleStatus ? undefined : 0}
@ -314,26 +186,9 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
toggleStatus={toggleStatus}
toggleQuery={hideQueryToggle ? undefined : toggleQuery}
subtitle={subtitleWithCounts}
inspectMultiple
showInspectButton={showInspectButton && !isChartEmbeddablesEnabled}
isInspectDisabled={filterQuery === undefined}
showInspectButton={false}
>
<EuiFlexGroup alignItems="center" gutterSize="none">
{(getLensAttributes || lensAttributes) && timerange && !isChartEmbeddablesEnabled && (
<EuiFlexItem grow={false}>
<VisualizationActions
className="histogram-viz-actions"
extraOptions={extraVisualizationOptions}
getLensAttributes={getLensAttributes}
isInspectButtonDisabled={filterQuery === undefined}
lensAttributes={lensAttributes}
queryId={id}
stackByField={selectedStackByOption.value}
timerange={timerange}
title={title}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
{stackByOptions.length > 1 && (
<EuiSelect
@ -349,29 +204,19 @@ export const MatrixHistogramComponent: React.FC<MatrixHistogramComponentProps> =
</EuiFlexGroup>
</HeaderSection>
{toggleStatus ? (
isChartEmbeddablesEnabled ? (
<VisualizationEmbeddable
scopeId={sourcererScopeId}
applyGlobalQueriesAndFilters={applyGlobalQueriesAndFilters}
data-test-subj="embeddable-matrix-histogram"
extraOptions={extraVisualizationOptions}
getLensAttributes={getLensAttributes}
height={chartHeight ?? CHART_HEIGHT}
id={visualizationId}
inspectTitle={title as string}
lensAttributes={lensAttributes}
stackByField={selectedStackByOption.value}
timerange={timerange}
/>
) : (
<MatrixHistogramChartContent
isInitialLoading={isInitialLoading}
barChart={barChartData}
configs={barchartConfigs}
stackByField={selectedStackByOption.value}
scopeId={scopeId}
/>
)
<VisualizationEmbeddable
scopeId={sourcererScopeId}
applyGlobalQueriesAndFilters={applyGlobalQueriesAndFilters}
data-test-subj="embeddable-matrix-histogram"
extraOptions={extraVisualizationOptions}
getLensAttributes={getLensAttributes}
height={chartHeight ?? CHART_HEIGHT}
id={visualizationId}
inspectTitle={title as string}
lensAttributes={lensAttributes}
stackByField={selectedStackByOption.value}
timerange={timerange}
/>
) : null}
</HistogramPanel>
</HoverVisibilityContainer>

View file

@ -5,23 +5,11 @@
* 2.0.
*/
import type React from 'react';
import type { EuiTitleSize } from '@elastic/eui';
import type { ScaleType, Position, TickFormatter } from '@elastic/charts';
import type { ActionCreator } from 'redux';
import type { RunTimeMappings } from '@kbn/timelines-plugin/common/api/search_strategy';
import type { EuiPaddingSize, EuiTitleSize } from '@elastic/eui';
import type { Position } from '@elastic/charts';
import type { ESQuery } from '../../../../common/typed_json';
import type { InputsModelId } from '../../store/inputs/constants';
import type { MatrixHistogramType } from '../../../../common/search_strategy/security_solution';
import type { UpdateDateRange } from '../charts/common';
import type { GlobalTimeArgs } from '../../containers/use_global_time';
import type { FieldValueThreshold } from '../../../detection_engine/rule_creation_ui/components/threshold_input';
import type { GetLensAttributes, LensAttributes } from '../visualization_actions/types';
export type MatrixHistogramMappingTypes = Record<
string,
{ key: string; value: null; color?: string | undefined }
>;
export interface MatrixHistogramOption {
text: string;
value: string;
@ -31,103 +19,23 @@ export type GetSubTitle = (count: number) => string;
export type GetTitle = (matrixHistogramOption: MatrixHistogramOption) => string;
export interface MatrixHistogramConfigs {
chartHeight?: number;
defaultStackByOption: MatrixHistogramOption;
errorMessage: string;
getLensAttributes?: GetLensAttributes;
hideHistogramIfEmpty?: boolean;
histogramType: MatrixHistogramType;
legendPosition?: Position;
lensAttributes?: LensAttributes;
mapping?: MatrixHistogramMappingTypes;
paddingSize?: EuiPaddingSize;
panelHeight?: number;
stackByOptions: MatrixHistogramOption[];
subtitle?: string | GetSubTitle;
title: string | GetTitle;
titleSize?: EuiTitleSize;
}
interface MatrixHistogramBasicProps {
chartHeight?: number;
defaultStackByOption: MatrixHistogramOption;
endDate: GlobalTimeArgs['to'];
headerChildren?: React.ReactNode;
hideHistogramIfEmpty?: boolean;
id: string;
legendPosition?: Position;
mapping?: MatrixHistogramMappingTypes;
panelHeight?: number;
paddingSize?: 's' | 'm' | 'l' | 'none';
setQuery: GlobalTimeArgs['setQuery'];
startDate: GlobalTimeArgs['from'];
stackByOptions: MatrixHistogramOption[];
subtitle?: string | GetSubTitle;
title?: string | GetTitle;
titleSize?: EuiTitleSize;
}
export interface MatrixHistogramQueryProps {
endDate: string;
errorMessage: string;
indexNames: string[];
filterQuery?: ESQuery | string | undefined;
onError?: () => void;
setAbsoluteRangeDatePicker?: ActionCreator<{
id: InputsModelId;
from: string;
to: string;
}>;
setAbsoluteRangeDatePickerTarget?: InputsModelId;
stackByField: string;
startDate: string;
histogramType: MatrixHistogramType;
threshold?: FieldValueThreshold;
skip?: boolean;
isPtrIncluded?: boolean;
includeMissingData?: boolean;
runtimeMappings?: RunTimeMappings;
}
export interface MatrixHistogramProps extends MatrixHistogramBasicProps {
legendPosition?: Position;
scaleType?: ScaleType;
showLegend?: boolean;
showSpacer?: boolean;
timelineId?: string;
yTickFormatter?: (value: number) => string;
}
export interface BarchartConfigs {
series: {
xScaleType: ScaleType;
yScaleType: ScaleType;
stackAccessors: string[];
};
axis: {
xTickFormatter: TickFormatter;
yTickFormatter: TickFormatter;
tickSize: number;
};
settings: {
legendPosition: Position;
onBrushEnd: UpdateDateRange;
showLegend: boolean;
showLegendExtra: boolean;
theme: {
scales: {
barsPadding: number;
};
chartMargins: {
left: number;
right: number;
top: number;
bottom: number;
};
chartPaddings: {
left: number;
right: number;
top: number;
bottom: number;
};
};
};
customHeight: number;
}

View file

@ -1,142 +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 {
getBarchartConfigs,
DEFAULT_CHART_HEIGHT,
DEFAULT_Y_TICK_FORMATTER,
formatToChartDataItem,
getCustomChartData,
} from './utils';
import type { UpdateDateRange } from '../charts/common';
import { Position } from '@elastic/charts';
import type { MatrixHistogramData } from '../../../../common/search_strategy';
import type { BarchartConfigs } from './types';
describe('utils', () => {
describe('getBarchartConfigs', () => {
describe('it should get correct default values', () => {
let configs: BarchartConfigs;
beforeAll(() => {
configs = getBarchartConfigs({
from: '2020-07-07T08:20:18.966Z',
to: '2020-07-08T08:20:18.966Z',
onBrushEnd: jest.fn() as UpdateDateRange,
});
});
test('it should set default chartHeight', () => {
expect(configs.customHeight).toEqual(DEFAULT_CHART_HEIGHT);
});
test('it should show legend by default', () => {
expect(configs.settings.showLegend).toEqual(true);
});
test('it should put legend on the right', () => {
expect(configs.settings.legendPosition).toEqual(Position.Right);
});
test('it should format Y tick to local string', () => {
expect(configs.axis.yTickFormatter).toEqual(DEFAULT_Y_TICK_FORMATTER);
});
});
describe('it should set custom configs', () => {
let configs: BarchartConfigs;
const mockYTickFormatter = jest.fn();
const mockChartHeight = 100;
beforeAll(() => {
configs = getBarchartConfigs({
chartHeight: mockChartHeight,
from: '2020-07-07T08:20:18.966Z',
to: '2020-07-08T08:20:18.966Z',
onBrushEnd: jest.fn() as UpdateDateRange,
yTickFormatter: mockYTickFormatter,
showLegend: false,
});
});
test('it should set custom chart height', () => {
expect(configs.customHeight).toEqual(mockChartHeight);
});
test('it should hide legend', () => {
expect(configs.settings.showLegend).toEqual(false);
});
test('it should format y tick with custom formatter', () => {
expect(configs.axis.yTickFormatter).toEqual(mockYTickFormatter);
});
});
});
describe('formatToChartDataItem', () => {
test('it should format data correctly', () => {
const data: [string, MatrixHistogramData[]] = [
'g1',
[
{ x: 1, y: 2, g: 'g1' },
{ x: 2, y: 4, g: 'g1' },
{ x: 3, y: 6, g: 'g1' },
],
];
const result = formatToChartDataItem(data);
expect(result).toEqual({
key: 'g1',
value: [
{ x: 1, y: 2, g: 'g1' },
{ x: 2, y: 4, g: 'g1' },
{ x: 3, y: 6, g: 'g1' },
],
});
});
});
describe('getCustomChartData', () => {
test('should handle the case when no data provided', () => {
const data = null;
const result = getCustomChartData(data);
expect(result).toEqual([]);
});
test('should format data correctly', () => {
const data = [
{ x: 1, y: 2, g: 'g1' },
{ x: 2, y: 4, g: 'g1' },
{ x: 3, y: 6, g: 'g1' },
{ x: 1, y: 1, g: 'g2' },
{ x: 2, y: 3, g: 'g2' },
{ x: 3, y: 5, g: 'g2' },
];
const result = getCustomChartData(data);
expect(result).toEqual([
{
key: 'g1',
color: '#1EA593',
value: [
{ x: 1, y: 2, g: 'g1' },
{ x: 2, y: 4, g: 'g1' },
{ x: 3, y: 6, g: 'g1' },
],
},
{
key: 'g2',
color: '#2B70F7',
value: [
{ x: 1, y: 1, g: 'g2' },
{ x: 2, y: 3, g: 'g2' },
{ x: 3, y: 5, g: 'g2' },
],
},
]);
});
});
});

View file

@ -5,72 +5,9 @@
* 2.0.
*/
import { ScaleType, Position } from '@elastic/charts';
import { get, groupBy, map, toPairs } from 'lodash/fp';
import type { UpdateDateRange, ChartSeriesData } from '../charts/common';
import type { MatrixHistogramMappingTypes, BarchartConfigs } from './types';
import type { MatrixHistogramData } from '../../../../common/search_strategy';
import { histogramDateTimeFormatter } from '../utils';
interface GetBarchartConfigsProps {
chartHeight?: number;
from: string;
legendPosition?: Position;
to: string;
onBrushEnd: UpdateDateRange;
yTickFormatter?: (value: number) => string;
showLegend?: boolean;
}
export const DEFAULT_CHART_HEIGHT = 174;
export const DEFAULT_Y_TICK_FORMATTER = (value: string | number): string => value.toLocaleString();
export const getBarchartConfigs = ({
chartHeight,
from,
legendPosition,
to,
onBrushEnd,
yTickFormatter,
showLegend,
}: GetBarchartConfigsProps): BarchartConfigs => ({
series: {
xScaleType: ScaleType.Time,
yScaleType: ScaleType.Linear,
stackAccessors: ['g'],
},
axis: {
xTickFormatter: histogramDateTimeFormatter([from, to]),
yTickFormatter: yTickFormatter != null ? yTickFormatter : DEFAULT_Y_TICK_FORMATTER,
tickSize: 8,
},
settings: {
legendPosition: legendPosition ?? Position.Right,
onBrushEnd,
showLegend: showLegend ?? true,
showLegendExtra: true,
theme: {
scales: {
barsPadding: 0.08,
},
chartMargins: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
chartPaddings: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
},
},
customHeight: chartHeight ?? DEFAULT_CHART_HEIGHT,
});
export const defaultLegendColors = [
'#1EA593',
'#2B70F7',
@ -84,25 +21,3 @@ export const defaultLegendColors = [
'#34130C',
'#GGGGGG',
];
export const formatToChartDataItem = ([key, value]: [
string,
MatrixHistogramData[]
]): ChartSeriesData => ({
key,
value,
});
export const getCustomChartData = (
data: MatrixHistogramData[] | null,
mapping?: MatrixHistogramMappingTypes
): ChartSeriesData[] => {
if (!data) return [];
const dataGroupedByEvent = groupBy('g', data);
const dataGroupedEntries = toPairs(dataGroupedByEvent);
const formattedChartData = map(formatToChartDataItem, dataGroupedEntries);
return formattedChartData.map((item: ChartSeriesData, idx: number) => {
const mapItem = get(item.key, mapping);
return { ...item, color: mapItem?.color ?? defaultLegendColors[idx] };
});
};

View file

@ -7,10 +7,8 @@
import type { Position } from '@elastic/charts';
import { omit } from 'lodash/fp';
import type { MutableRefObject } from 'react';
import React, { useEffect } from 'react';
import type { ISessionService } from '@kbn/data-plugin/public';
import type { inputsModel } from '../../store';
import type { GlobalTimeArgs } from '../../containers/use_global_time';
import type { InputsModelId } from '../../store/inputs/constants';
@ -23,14 +21,13 @@ export interface OwnProps extends Pick<GlobalTimeArgs, 'deleteQuery' | 'setQuery
legendPosition?: Position;
loading: boolean;
refetch: inputsModel.Refetch;
session?: MutableRefObject<ISessionService>;
}
export function manageQuery<T>(
WrappedComponent: React.ComponentClass<T> | React.ComponentType<T>
): React.FC<OwnProps & T> {
const ManageQuery = (props: OwnProps & T) => {
const { deleteQuery, id, inspect = null, loading, refetch, setQuery, session } = props;
const { deleteQuery, id, inspect = null, loading, refetch, setQuery } = props;
useQueryInspector({
deleteQuery,
@ -38,7 +35,6 @@ export function manageQuery<T>(
loading,
queryId: id,
refetch,
session,
setQuery,
});
@ -57,7 +53,6 @@ interface UseQueryInspectorTypes extends Pick<GlobalTimeArgs, 'deleteQuery' | 's
loading: boolean;
refetch: inputsModel.Refetch;
inspect?: inputsModel.InspectQuery | null;
session?: MutableRefObject<ISessionService>;
}
export const useQueryInspector = ({
@ -67,7 +62,6 @@ export const useQueryInspector = ({
inspect,
loading,
queryId,
session,
}: UseQueryInspectorTypes) => {
useEffect(() => {
setQuery({
@ -75,9 +69,8 @@ export const useQueryInspector = ({
inspect: inspect ?? null,
loading,
refetch,
searchSessionId: session?.current.start(),
});
}, [deleteQuery, setQuery, queryId, refetch, inspect, loading, session]);
}, [deleteQuery, setQuery, queryId, refetch, inspect, loading]);
useEffect(() => {
return () => {

View file

@ -19,13 +19,11 @@ import type { Refetch } from '../../store/inputs/model';
interface UseRefetchByRestartingSessionProps {
inputId?: InputsModelId;
queryId: string;
skip?: boolean;
}
export const useRefetchByRestartingSession = ({
inputId,
queryId,
skip,
}: UseRefetchByRestartingSessionProps): {
session: MutableRefObject<ISessionService>;
refetchByRestartingSession: Refetch;
@ -56,10 +54,10 @@ export const useRefetchByRestartingSession = ({
* like most of our components, it refetches when receiving a new search
* session ID.
**/
searchSessionId: skip ? undefined : searchSessionId,
searchSessionId,
})
);
}, [dispatch, queryId, selectedInspectIndex, skip]);
}, [dispatch, queryId, selectedInspectIndex]);
/**
* This is for refetching alert index when the first rule just created

View file

@ -139,15 +139,11 @@ const TopNComponent: React.FC<Props> = ({
/>
) : (
<SignalsByCategory
combinedQueries={combinedQueries}
filters={applicableFilters}
headerChildren={headerChildren}
onlyField={field}
paddingSize={paddingSize}
query={query}
showLegend={showLegend}
setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget}
runtimeMappings={runtimeMappings}
hideQueryToggle
/>
)}

View file

@ -10,7 +10,6 @@ import type {
MatrixHistogramOption,
MatrixHistogramConfigs,
} from '../../../components/matrix_histogram/types';
import { MatrixHistogramType } from '../../../../../common/search_strategy/security_solution/matrix_histogram';
export const anomaliesStackByOptions: MatrixHistogramOption[] = [
{
@ -24,9 +23,7 @@ const DEFAULT_STACK_BY = i18n.ANOMALIES_STACK_BY_JOB_ID;
export const histogramConfigs: MatrixHistogramConfigs = {
defaultStackByOption:
anomaliesStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? anomaliesStackByOptions[0],
errorMessage: i18n.ERROR_FETCHING_ANOMALIES_DATA,
hideHistogramIfEmpty: true,
histogramType: MatrixHistogramType.anomalies,
stackByOptions: anomaliesStackByOptions,
subtitle: undefined,
title: i18n.ANOMALIES_TITLE,

View file

@ -60,7 +60,6 @@ const AnomaliesQueryTabBodyComponent: React.FC<AnomaliesQueryTabBodyProps> = ({
endDate={endDate}
filterQuery={mergedFilterQuery}
id={ID}
indexNames={indexNames}
setQuery={setQuery}
startDate={startDate}
{...histogramConfigs}

View file

@ -1,264 +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 { renderHook, act } from '@testing-library/react-hooks';
import { useKibana } from '../../lib/kibana';
import { useMatrixHistogram, useMatrixHistogramCombined } from '.';
import { MatrixHistogramType } from '../../../../common/search_strategy';
import { TestProviders } from '../../mock/test_providers';
import { useTrackHttpRequest } from '../../lib/apm/use_track_http_request';
jest.mock('../../lib/kibana');
jest.mock('../../lib/apm/use_track_http_request');
const mockEndTracking = jest.fn();
const mockStartTracking = jest.fn(() => ({ endTracking: mockEndTracking }));
(useTrackHttpRequest as jest.Mock).mockReturnValue({
startTracking: mockStartTracking,
});
const basicResponse = {
isPartial: false,
isRunning: false,
total: 0,
loaded: 0,
rawResponse: {
took: 1,
timed_out: false,
hits: {
max_score: 0,
hits: [],
total: 0,
},
},
};
describe('useMatrixHistogram', () => {
const props = {
endDate: new Date(Date.now()).toISOString(),
errorMessage: '',
filterQuery: {},
histogramType: MatrixHistogramType.events,
indexNames: [],
stackByField: 'event.module',
startDate: new Date(Date.now()).toISOString(),
skip: false,
};
afterEach(() => {
jest.clearAllMocks();
});
it('should update request when props has changed', async () => {
const localProps = { ...props };
const { rerender } = renderHook(() => useMatrixHistogram(localProps), {
wrapper: TestProviders,
});
localProps.stackByField = 'event.action';
rerender();
const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls;
expect(mockCalls.length).toBe(2);
expect(mockCalls[0][0].stackByField).toBe('event.module');
expect(mockCalls[1][0].stackByField).toBe('event.action');
});
it('returns a memoized value', async () => {
const { result, rerender } = renderHook(() => useMatrixHistogram(props), {
wrapper: TestProviders,
});
const result1 = result.current[1];
act(() => rerender());
const result2 = result.current[1];
expect(result1).toBe(result2);
});
it("returns buckets for histogram Type 'events'", async () => {
const localProps = { ...props, histogramType: MatrixHistogramType.events };
const mockEventsSearchStrategyResponse = {
...basicResponse,
rawResponse: {
...basicResponse.rawResponse,
aggregations: {
eventActionGroup: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: 'my dsn test buckets',
doc_count: 1,
},
],
},
},
},
};
(useKibana().services.data.search.search as jest.Mock).mockReturnValueOnce({
subscribe: ({ next }: { next: Function }) => next(mockEventsSearchStrategyResponse),
});
const {
result: { current },
} = renderHook(() => useMatrixHistogram(localProps), {
wrapper: TestProviders,
});
expect(current[1].buckets).toBe(
mockEventsSearchStrategyResponse.rawResponse.aggregations?.eventActionGroup.buckets
);
});
it("returns buckets for histogram Type 'dns'", async () => {
const mockDnsSearchStrategyResponse = {
...basicResponse,
rawResponse: {
...basicResponse.rawResponse,
aggregations: {
dns_name_query_count: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{
key: 'my dsn test buckets',
doc_count: 1,
},
],
},
},
},
};
const localProps = { ...props, histogramType: MatrixHistogramType.dns };
(useKibana().services.data.search.search as jest.Mock).mockReturnValueOnce({
subscribe: ({ next }: { next: Function }) => next(mockDnsSearchStrategyResponse),
});
const {
result: { current },
} = renderHook(() => useMatrixHistogram(localProps), {
wrapper: TestProviders,
});
expect(current[1].buckets).toBe(
mockDnsSearchStrategyResponse.rawResponse.aggregations?.dns_name_query_count.buckets
);
});
it('skip = true will cancel any running request', () => {
const abortSpy = jest.spyOn(AbortController.prototype, 'abort');
const localProps = { ...props };
const { rerender } = renderHook(() => useMatrixHistogram(localProps), {
wrapper: TestProviders,
});
localProps.skip = true;
act(() => rerender());
expect(abortSpy).toHaveBeenCalledTimes(3);
});
describe('trackHttpRequest', () => {
it('should start tracking when request starts', () => {
renderHook(useMatrixHistogram, {
initialProps: props,
wrapper: TestProviders,
});
expect(mockStartTracking).toHaveBeenCalledWith({
name: `securitySolutionUI matrixHistogram ${MatrixHistogramType.events}`,
});
});
it('should end tracking success when the request succeeds', () => {
(useKibana().services.data.search.search as jest.Mock).mockReturnValueOnce({
subscribe: ({ next }: { next: Function }) => next(basicResponse),
});
renderHook(useMatrixHistogram, {
initialProps: props,
wrapper: TestProviders,
});
expect(mockEndTracking).toHaveBeenCalledWith('success');
});
it('should end tracking error when the request fails', () => {
(useKibana().services.data.search.search as jest.Mock).mockReturnValueOnce({
subscribe: ({ error }: { error: Function }) => error('some error'),
});
renderHook(useMatrixHistogram, {
initialProps: props,
wrapper: TestProviders,
});
expect(mockEndTracking).toHaveBeenCalledWith('error');
});
});
});
describe('useMatrixHistogramCombined', () => {
const props = {
endDate: new Date(Date.now()).toISOString(),
errorMessage: '',
filterQuery: {},
histogramType: MatrixHistogramType.events,
indexNames: [],
stackByField: 'event.module',
startDate: new Date(Date.now()).toISOString(),
};
afterEach(() => {
(useKibana().services.data.search.search as jest.Mock).mockClear();
});
it('should update request when props has changed', async () => {
const localProps = { ...props };
const { rerender } = renderHook(() => useMatrixHistogramCombined(localProps), {
wrapper: TestProviders,
});
localProps.stackByField = 'event.action';
rerender();
const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls;
expect(mockCalls.length).toBe(2);
expect(mockCalls[0][0].stackByField).toBe('event.module');
expect(mockCalls[1][0].stackByField).toBe('event.action');
});
it('should do two request when stacking by ip field', async () => {
const localProps = { ...props, stackByField: 'source.ip' };
renderHook(() => useMatrixHistogramCombined(localProps), {
wrapper: TestProviders,
});
const mockCalls = (useKibana().services.data.search.search as jest.Mock).mock.calls;
expect(mockCalls.length).toBe(2);
expect(mockCalls[0][0].stackByField).toBe('source.ip');
expect(mockCalls[1][0].stackByField).toBe('source.ip');
});
it('returns a memoized value', async () => {
const { result, rerender } = renderHook(() => useMatrixHistogramCombined(props), {
wrapper: TestProviders,
});
const result1 = result.current[1];
act(() => rerender());
const result2 = result.current[1];
expect(result1).toBe(result2);
});
});

View file

@ -1,300 +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 deepEqual from 'fast-deep-equal';
import { getOr, noop } from 'lodash/fp';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Subscription } from 'rxjs';
import { isRunningResponse } from '@kbn/data-plugin/common';
import type { MatrixHistogramRequestOptionsInput } from '../../../../common/api/search_strategy';
import type { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types';
import type { inputsModel } from '../../store';
import { createFilter } from '../helpers';
import { useKibana } from '../../lib/kibana';
import type {
MatrixHistogramStrategyResponse,
MatrixHistogramData,
} from '../../../../common/search_strategy/security_solution';
import {
MatrixHistogramQuery,
MatrixHistogramTypeToAggName,
} from '../../../../common/search_strategy/security_solution';
import { getInspectResponse } from '../../../helpers';
import type { InspectResponse } from '../../../types';
import * as i18n from './translations';
import { useAppToasts } from '../../hooks/use_app_toasts';
import { useTrackHttpRequest } from '../../lib/apm/use_track_http_request';
import { APP_UI_ID } from '../../../../common/constants';
export type Buckets = Array<{
key: string;
doc_count: number;
}>;
const bucketEmpty: Buckets = [];
export interface UseMatrixHistogramArgs {
data: MatrixHistogramData[];
inspect: InspectResponse;
refetch: inputsModel.Refetch;
totalCount: number;
buckets: Array<{
key: string;
doc_count: number;
}>;
}
export const useMatrixHistogram = ({
endDate,
errorMessage,
filterQuery,
histogramType,
indexNames,
isPtrIncluded,
onError,
stackByField,
runtimeMappings,
startDate,
threshold,
skip = false,
includeMissingData = true,
}: MatrixHistogramQueryProps): [
boolean,
UseMatrixHistogramArgs,
(to: string, from: string) => void
] => {
const { data } = useKibana().services;
const refetch = useRef<inputsModel.Refetch>(noop);
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef(new Subscription());
const [loading, setLoading] = useState(false);
const { startTracking } = useTrackHttpRequest();
const [matrixHistogramRequest, setMatrixHistogramRequest] =
useState<MatrixHistogramRequestOptionsInput>({
defaultIndex: indexNames,
factoryQueryType: MatrixHistogramQuery,
filterQuery: createFilter(filterQuery),
histogramType: histogramType ?? histogramType,
timerange: {
interval: '12h',
from: startDate,
to: endDate,
},
stackByField,
runtimeMappings,
threshold,
...(isPtrIncluded != null ? { isPtrIncluded } : {}),
...(includeMissingData != null ? { includeMissingData } : {}),
});
const { addError } = useAppToasts();
const [matrixHistogramResponse, setMatrixHistogramResponse] = useState<UseMatrixHistogramArgs>({
data: [],
inspect: {
dsl: [],
response: [],
},
refetch: refetch.current,
totalCount: -1,
buckets: [],
});
const search = useCallback(
(request: MatrixHistogramRequestOptionsInput) => {
const asyncSearch = async () => {
abortCtrl.current = new AbortController();
setLoading(true);
const { endTracking } = startTracking({
name: `${APP_UI_ID} matrixHistogram ${histogramType}`,
});
searchSubscription$.current = data.search
.search<MatrixHistogramRequestOptionsInput, MatrixHistogramStrategyResponse>(request, {
strategy: 'securitySolutionSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
next: (response) => {
if (!isRunningResponse(response)) {
const histogramBuckets: Buckets = getOr(
bucketEmpty,
MatrixHistogramTypeToAggName[histogramType],
response.rawResponse
);
setLoading(false);
setMatrixHistogramResponse((prevResponse) => ({
...prevResponse,
data: response.matrixHistogramData,
inspect: getInspectResponse(response, prevResponse.inspect),
refetch: refetch.current,
totalCount: histogramBuckets.reduce((acc, bucket) => bucket.doc_count + acc, 0),
buckets: histogramBuckets,
}));
endTracking('success');
searchSubscription$.current.unsubscribe();
}
},
error: (msg) => {
setLoading(false);
addError(msg, {
title: errorMessage ?? i18n.FAIL_MATRIX_HISTOGRAM,
});
endTracking('error');
searchSubscription$.current.unsubscribe();
},
});
};
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
asyncSearch();
refetch.current = asyncSearch;
},
[data.search, histogramType, addError, errorMessage, startTracking]
);
useEffect(() => {
setMatrixHistogramRequest((prevRequest) => {
const myRequest = {
...prevRequest,
defaultIndex: indexNames,
filterQuery: createFilter(filterQuery),
histogramType,
timerange: {
interval: '12h',
from: startDate,
to: endDate,
},
stackByField,
threshold,
...(isPtrIncluded != null ? { isPtrIncluded } : {}),
};
if (!deepEqual(prevRequest, myRequest)) {
return myRequest;
}
return prevRequest;
});
}, [
indexNames,
endDate,
filterQuery,
startDate,
stackByField,
histogramType,
threshold,
isPtrIncluded,
]);
useEffect(() => {
// We want to search if it is not skipped, stackByField ends with ip and include missing data
if (!skip) {
search(matrixHistogramRequest);
}
return () => {
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
};
}, [matrixHistogramRequest, search, skip]);
useEffect(() => {
if (skip) {
setLoading(false);
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
}
}, [skip]);
const runMatrixHistogramSearch = useCallback(
(to: string, from: string) => {
search({
...matrixHistogramRequest,
timerange: {
interval: '12h',
from,
to,
},
});
},
[matrixHistogramRequest, search]
);
return [loading, matrixHistogramResponse, runMatrixHistogramSearch];
};
/* function needed to split ip histogram data requests due to elasticsearch bug https://github.com/elastic/kibana/issues/89205
* using includeMissingData parameter to do the "missing data" query separately
**/
export const useMatrixHistogramCombined = (
matrixHistogramQueryProps: MatrixHistogramQueryProps
): [boolean, UseMatrixHistogramArgs] => {
const [mainLoading, mainResponse] = useMatrixHistogram({
...matrixHistogramQueryProps,
includeMissingData: true,
});
const skipMissingData = useMemo(
() => !matrixHistogramQueryProps.stackByField.endsWith('.ip'),
[matrixHistogramQueryProps.stackByField]
);
const [missingDataLoading, missingDataResponse] = useMatrixHistogram({
...matrixHistogramQueryProps,
includeMissingData: false,
skip:
skipMissingData ||
matrixHistogramQueryProps.filterQuery === undefined ||
matrixHistogramQueryProps.skip,
});
const combinedLoading = useMemo<boolean>(
() => mainLoading || missingDataLoading,
[mainLoading, missingDataLoading]
);
const combinedResponse = useMemo<UseMatrixHistogramArgs>(() => {
if (skipMissingData) return mainResponse;
const { data, inspect, totalCount, refetch, buckets } = mainResponse;
const {
data: extraData,
inspect: extraInspect,
totalCount: extraTotalCount,
refetch: extraRefetch,
} = missingDataResponse;
const combinedRefetch = () => {
refetch();
extraRefetch();
};
if (combinedLoading) {
return {
data: [],
inspect: {
dsl: [],
response: [],
},
refetch: combinedRefetch,
totalCount: -1,
buckets: [],
};
}
return {
data: [...data, ...extraData],
inspect: {
dsl: [...inspect.dsl, ...extraInspect.dsl],
response: [...inspect.response, ...extraInspect.response],
},
totalCount: totalCount + extraTotalCount,
refetch: combinedRefetch,
buckets,
};
}, [combinedLoading, mainResponse, missingDataResponse, skipMissingData]);
return [combinedLoading, combinedResponse];
};

View file

@ -1,22 +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 { i18n } from '@kbn/i18n';
export const ERROR_MATRIX_HISTOGRAM = i18n.translate(
'xpack.securitySolution.matrixHistogram.errorSearchDescription',
{
defaultMessage: `An error has occurred on matrix histogram search`,
}
);
export const FAIL_MATRIX_HISTOGRAM = i18n.translate(
'xpack.securitySolution.matrixHistogram.failSearchDescription',
{
defaultMessage: `Failed to run search on matrix histogram`,
}
);

View file

@ -101,11 +101,11 @@ const PreviewHistogramComponent = ({
const previousPreviewId = usePrevious(previewId);
const previewQueryId = `${ID}-${previewId}`;
const previewEmbeddableId = `${previewQueryId}-embeddable`;
const { responses: visualizationResponse } = useVisualizationResponse({
const { responses: visualizationResponses } = useVisualizationResponse({
visualizationId: previewEmbeddableId,
});
const totalCount = visualizationResponse?.[0]?.hits?.total ?? 0;
const totalCount = visualizationResponses?.[0]?.hits?.total ?? 0;
useEffect(() => {
if (previousPreviewId !== previewId && totalCount > 0) {

View file

@ -724,11 +724,9 @@ const RuleDetailsPageComponent: React.FC<DetectionEngineComponentProps> = ({
<Display show={!globalFullScreen}>
<AlertsHistogramPanel
filters={alertMergedFilters}
query={query}
signalIndexName={signalIndexName}
defaultStackByOption={defaultRuleStackByOption}
updateDateRange={updateDateRangeCallback}
runtimeMappings={runtimeMappings}
/>
<EuiSpacer />
</Display>

View file

@ -1,61 +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 type { VisualizationEmbeddableProps } from '../../../../common/components/visualization_actions/types';
import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable';
import type { AlertSearchResponse } from '../../../containers/detection_engine/alerts/types';
import { AlertsCount } from './alerts_count';
import type { AlertsCountAggregation } from './types';
type ChartContentProps = {
isChartEmbeddablesEnabled: boolean;
} & VisualizationEmbeddableProps & {
isLoadingAlerts: boolean;
alertsData: AlertSearchResponse<unknown, AlertsCountAggregation> | null;
stackByField0: string;
stackByField1: string | undefined;
};
const ChartContentComponent = ({
alertsData,
extraActions,
extraOptions,
getLensAttributes,
height,
id,
inspectTitle,
isChartEmbeddablesEnabled,
isLoadingAlerts,
scopeId,
stackByField0,
stackByField1,
timerange,
}: ChartContentProps) => {
return isChartEmbeddablesEnabled ? (
<VisualizationEmbeddable
data-test-subj="embeddable-alerts-count"
extraActions={extraActions}
extraOptions={extraOptions}
getLensAttributes={getLensAttributes}
height={height}
id={id}
inspectTitle={inspectTitle}
scopeId={scopeId}
stackByField={stackByField0}
timerange={timerange}
/>
) : alertsData != null ? (
<AlertsCount
data={alertsData}
loading={isLoadingAlerts}
stackByField0={stackByField0}
stackByField1={stackByField1}
/>
) : null;
};
export const ChartContent = React.memo(ChartContentComponent);

View file

@ -6,8 +6,9 @@
*/
import React from 'react';
import { waitFor, act } from '@testing-library/react';
import { act } from '@testing-library/react';
import { mount } from 'enzyme';
import type { Action } from '@kbn/ui-actions-plugin/public';
import { AlertsCountPanel } from '.';
import type { Status } from '../../../../../common/api/detection_engine';
@ -17,7 +18,7 @@ import { TestProviders } from '../../../../common/mock';
import { ChartContextMenu } from '../../../pages/detection_engine/chart_panels/chart_context_menu';
import { TABLE } from '../../../pages/detection_engine/chart_panels/chart_select/translations';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { LensEmbeddable } from '../../../../common/components/visualization_actions/lens_embeddable';
import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable';
import type { ExperimentalFeatures } from '../../../../../common/experimental_features';
import { allowedExperimentalValues } from '../../../../../common/experimental_features';
@ -39,24 +40,9 @@ jest.mock('react-router-dom', () => {
return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
});
const defaultUseQueryAlertsReturn = {
loading: false,
data: {},
setQuery: () => {},
response: '',
request: '',
refetch: () => {},
};
const mockUseQueryAlerts = jest.fn().mockReturnValue(defaultUseQueryAlertsReturn);
jest.mock('../../../containers/detection_engine/alerts/use_query', () => {
return {
useQueryAlerts: (...props: unknown[]) => mockUseQueryAlerts(...props),
};
});
jest.mock('../../../../common/hooks/use_experimental_features');
jest.mock('../../../../common/components/page/use_refetch_by_session');
jest.mock('../../../../common/components/visualization_actions/lens_embeddable');
jest.mock('../../../../common/components/visualization_actions/visualization_embeddable');
jest.mock('../../../../common/components/page/use_refetch_by_session');
jest.mock('../common/hooks', () => ({
useInspectButton: jest.fn(),
@ -80,6 +66,7 @@ const defaultProps = {
showBuildingBlockAlerts: false,
showOnlyThreatIndicatorAlerts: false,
status: 'open' as Status,
extraActions: [{ id: 'resetGroupByFields' }] as Action[],
};
const mockSetToggle = jest.fn();
const mockUseQueryToggle = useQueryToggle as jest.Mock;
@ -90,7 +77,6 @@ describe('AlertsCountPanel', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
mockUseIsExperimentalFeatureEnabled.mockImplementation(
getMockUseIsExperimentalFeatureEnabled({
chartEmbeddablesEnabled: false,
alertsPageChartsEnabled: false,
})
);
@ -160,26 +146,6 @@ describe('AlertsCountPanel', () => {
});
});
describe('Query', () => {
it('it render with a illegal KQL', async () => {
jest.mock('@kbn/es-query', () => ({
buildEsQuery: jest.fn().mockImplementation(() => {
throw new Error('Something went wrong');
}),
}));
const props = { ...defaultProps, query: { query: 'host.name: "', language: 'kql' } };
const wrapper = mount(
<TestProviders>
<AlertsCountPanel {...props} />
</TestProviders>
);
await waitFor(() => {
expect(wrapper.find('[data-test-subj="alertsCountPanel"]').exists()).toBeTruthy();
});
});
});
describe('toggleQuery', () => {
it('toggles', async () => {
await act(async () => {
@ -199,7 +165,7 @@ describe('AlertsCountPanel', () => {
<AlertsCountPanel {...defaultProps} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="alertsCountTable"]').exists()).toEqual(true);
expect(wrapper.find('[data-test-subj="visualization-embeddable"]').exists()).toEqual(true);
});
});
it('alertsPageChartsEnabled is false and toggleStatus=false, hide', async () => {
@ -210,14 +176,13 @@ describe('AlertsCountPanel', () => {
<AlertsCountPanel {...defaultProps} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="alertsCountTable"]').exists()).toEqual(false);
expect(wrapper.find('[data-test-subj="visualization-embeddable"]').exists()).toEqual(false);
});
});
it('alertsPageChartsEnabled is true and isExpanded=true, render', async () => {
mockUseIsExperimentalFeatureEnabled.mockImplementation(
getMockUseIsExperimentalFeatureEnabled({
chartEmbeddablesEnabled: false,
alertsPageChartsEnabled: true,
})
);
@ -227,13 +192,12 @@ describe('AlertsCountPanel', () => {
<AlertsCountPanel {...defaultProps} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="alertsCountTable"]').exists()).toEqual(true);
expect(wrapper.find('[data-test-subj="visualization-embeddable"]').exists()).toEqual(true);
});
});
it('alertsPageChartsEnabled is true and isExpanded=false, hide', async () => {
mockUseIsExperimentalFeatureEnabled.mockImplementation(
getMockUseIsExperimentalFeatureEnabled({
chartEmbeddablesEnabled: false,
alertsPageChartsEnabled: true,
})
);
@ -243,54 +207,61 @@ describe('AlertsCountPanel', () => {
<AlertsCountPanel {...defaultProps} isExpanded={false} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="alertsCountTable"]').exists()).toEqual(false);
expect(wrapper.find('[data-test-subj="visualization-embeddable"]').exists()).toEqual(false);
});
});
});
describe('Visualization', () => {
it('should render embeddable', async () => {
await act(async () => {
const wrapper = mount(
<TestProviders>
<AlertsCountPanel {...defaultProps} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="visualization-embeddable"]').exists()).toBeTruthy();
});
});
it('should render with provided height', async () => {
await act(async () => {
mount(
<TestProviders>
<AlertsCountPanel {...defaultProps} />
</TestProviders>
);
expect((VisualizationEmbeddable as unknown as jest.Mock).mock.calls[0][0].height).toEqual(
218
);
});
});
it('should render with extra actions', async () => {
await act(async () => {
mount(
<TestProviders>
<AlertsCountPanel {...defaultProps} />
</TestProviders>
);
expect(
(VisualizationEmbeddable as unknown as jest.Mock).mock.calls[0][0].extraActions[0].id
).toEqual('resetGroupByFields');
});
});
it('should render with extra options', async () => {
await act(async () => {
mount(
<TestProviders>
<AlertsCountPanel {...defaultProps} />
</TestProviders>
);
expect(
(VisualizationEmbeddable as unknown as jest.Mock).mock.calls[0][0].extraOptions
.breakdownField
).toEqual(defaultProps.stackByField1);
});
});
});
});
describe('when the isChartEmbeddablesEnabled experimental feature flag is enabled', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
mockUseIsExperimentalFeatureEnabled.mockImplementation(
getMockUseIsExperimentalFeatureEnabled({
chartEmbeddablesEnabled: true,
alertsPageChartsEnabled: false,
})
);
});
it('renders LensEmbeddable', async () => {
await act(async () => {
const wrapper = mount(
<TestProviders>
<AlertsCountPanel {...defaultProps} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="embeddable-count-table"]').exists()).toBeTruthy();
});
});
it('renders LensEmbeddable with provided height', async () => {
await act(async () => {
mount(
<TestProviders>
<AlertsCountPanel {...defaultProps} />
</TestProviders>
);
expect((LensEmbeddable as unknown as jest.Mock).mock.calls[0][0].height).toEqual(218);
});
});
it('should skip calling getAlertsRiskQuery', async () => {
await act(async () => {
mount(
<TestProviders>
<AlertsCountPanel {...defaultProps} />
</TestProviders>
);
expect(mockUseQueryAlerts.mock.calls[0][0].skip).toBeTruthy();
});
});
});

View file

@ -5,33 +5,25 @@
* 2.0.
*/
import { EuiProgress } from '@elastic/eui';
import type { EuiComboBox } from '@elastic/eui';
import type { Action } from '@kbn/ui-actions-plugin/public';
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
import React, { memo, useMemo, useEffect, useCallback } from 'react';
import React, { memo, useMemo, useCallback } from 'react';
import { v4 as uuidv4 } from 'uuid';
import type { Filter, Query } from '@kbn/es-query';
import { buildEsQuery } from '@kbn/es-query';
import type { Filter } from '@kbn/es-query';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
import { HeaderSection } from '../../../../common/components/header_section';
import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query';
import { ALERTS_QUERY_NAMES } from '../../../containers/detection_engine/alerts/constants';
import { InspectButtonContainer } from '../../../../common/components/inspect';
import { getAlertsCountQuery } from './helpers';
import * as i18n from './translations';
import type { AlertsCountAggregation } from './types';
import { KpiPanel } from '../common/components';
import { useInspectButton } from '../common/hooks';
import { useQueryToggle } from '../../../../common/containers/query_toggle';
import { FieldSelection } from '../../../../common/components/field_selection';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { getAlertsTableLensAttributes as getLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/common/alerts/alerts_table';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { ChartContent } from './chart_content';
import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable';
export const DETECTIONS_ALERTS_COUNT_ID = 'detections-alerts-count';
@ -42,13 +34,10 @@ interface AlertsCountPanelProps {
filters?: Filter[];
inspectTitle: string;
panelHeight?: number;
query?: Query;
runtimeMappings?: MappingRuntimeFields;
setStackByField0: (stackBy: string) => void;
setStackByField0ComboboxInputRef?: (inputRef: HTMLInputElement | null) => void;
setStackByField1: (stackBy: string | undefined) => void;
setStackByField1ComboboxInputRef?: (inputRef: HTMLInputElement | null) => void;
signalIndexName: string | null;
stackByField0: string;
stackByField0ComboboxRef?: React.RefObject<EuiComboBox<string | number | string[] | undefined>>;
stackByField1: string | undefined;
@ -68,13 +57,10 @@ export const AlertsCountPanel = memo<AlertsCountPanelProps>(
filters,
inspectTitle,
panelHeight,
query,
runtimeMappings,
setStackByField0,
setStackByField0ComboboxInputRef,
setStackByField1,
setStackByField1ComboboxInputRef,
signalIndexName,
stackByField0,
stackByField0ComboboxRef,
stackByField1,
@ -84,29 +70,11 @@ export const AlertsCountPanel = memo<AlertsCountPanelProps>(
isExpanded,
setIsExpanded,
}) => {
const { to, from, deleteQuery, setQuery } = useGlobalTime();
const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled');
const { to, from } = useGlobalTime();
const isAlertsPageChartsEnabled = useIsExperimentalFeatureEnabled('alertsPageChartsEnabled');
// create a unique, but stable (across re-renders) query id
const uniqueQueryId = useMemo(() => `${DETECTIONS_ALERTS_COUNT_ID}-${uuidv4()}`, []);
// Disabling the fecth method in useQueryAlerts since it is defaulted to the old one
// const fetchMethod = fetchQueryRuleRegistryAlerts;
const additionalFilters = useMemo(() => {
try {
return [
buildEsQuery(
undefined,
query != null ? [query] : [],
filters?.filter((f) => f.meta.disabled === false) ?? []
),
];
} catch (e) {
return [];
}
}, [query, filters]);
const { toggleStatus, setToggleStatus } = useQueryToggle(DETECTIONS_ALERTS_COUNT_ID);
const toggleQuery = useCallback(
(newToggleStatus: boolean) => {
@ -119,11 +87,6 @@ export const AlertsCountPanel = memo<AlertsCountPanelProps>(
[setToggleStatus, setIsExpanded, isAlertsPageChartsEnabled]
);
const querySkip = useMemo(
() => (isAlertsPageChartsEnabled ? !isExpanded : !toggleStatus),
[isAlertsPageChartsEnabled, isExpanded, toggleStatus]
);
const timerange = useMemo(() => ({ from, to }), [from, to]);
const extraVisualizationOptions = useMemo(
@ -133,57 +96,7 @@ export const AlertsCountPanel = memo<AlertsCountPanelProps>(
}),
[filters, stackByField1]
);
const {
loading: isLoadingAlerts,
data: alertsData,
setQuery: setAlertsQuery,
response,
request,
refetch,
} = useQueryAlerts<{}, AlertsCountAggregation>({
query: getAlertsCountQuery({
stackByField0,
stackByField1,
from,
to,
additionalFilters,
runtimeMappings,
}),
indexName: signalIndexName,
skip: querySkip || isChartEmbeddablesEnabled,
queryName: ALERTS_QUERY_NAMES.COUNT,
});
useEffect(() => {
setAlertsQuery(
getAlertsCountQuery({
additionalFilters,
from,
runtimeMappings,
stackByField0,
stackByField1,
to,
})
);
}, [
additionalFilters,
from,
runtimeMappings,
setAlertsQuery,
stackByField0,
stackByField1,
to,
]);
useInspectButton({
deleteQuery,
loading: isLoadingAlerts,
refetch,
request,
response,
setQuery,
uniqueQueryId,
});
const showCount = useMemo(() => {
if (isAlertsPageChartsEnabled) {
return isExpanded;
@ -214,9 +127,6 @@ export const AlertsCountPanel = memo<AlertsCountPanelProps>(
toggleQuery={toggleQuery}
>
<FieldSelection
chartOptionsContextMenu={
isChartEmbeddablesEnabled ? undefined : chartOptionsContextMenu
}
setStackByField0={setStackByField0}
setStackByField0ComboboxInputRef={setStackByField0ComboboxInputRef}
setStackByField1={setStackByField1}
@ -227,31 +137,23 @@ export const AlertsCountPanel = memo<AlertsCountPanelProps>(
stackByField1ComboboxRef={stackByField1ComboboxRef}
stackByWidth={stackByWidth}
uniqueQueryId={uniqueQueryId}
useLensCompatibleFields={isChartEmbeddablesEnabled}
useLensCompatibleFields={true}
/>
</HeaderSection>
{showCount &&
(isLoadingAlerts ? (
<EuiProgress color="accent" data-test-subj="progress" position="absolute" size="xs" />
) : (
<ChartContent
alertsData={alertsData}
data-test-subj="embeddable-count-table"
extraActions={extraActions}
extraOptions={extraVisualizationOptions}
getLensAttributes={getLensAttributes}
height={CHART_HEIGHT}
id={`${uniqueQueryId}-embeddable`}
inspectTitle={inspectTitle}
isChartEmbeddablesEnabled={isChartEmbeddablesEnabled}
isLoadingAlerts={isLoadingAlerts}
scopeId={SourcererScopeName.detections}
stackByField0={stackByField0}
stackByField1={stackByField1}
stackByField={stackByField0}
timerange={timerange}
/>
))}
{showCount && (
<VisualizationEmbeddable
data-test-subj="embeddable-alerts-count"
extraActions={extraActions}
extraOptions={extraVisualizationOptions}
getLensAttributes={getLensAttributes}
height={CHART_HEIGHT}
id={`${uniqueQueryId}-embeddable`}
inspectTitle={inspectTitle}
scopeId={SourcererScopeName.detections}
stackByField={stackByField0}
timerange={timerange}
/>
)}
</KpiPanel>
</InspectButtonContainer>
);

View file

@ -11,17 +11,12 @@ import { mount } from 'enzyme';
import type { Filter } from '@kbn/es-query';
import { SecurityPageName } from '../../../../app/types';
import { CHART_SETTINGS_POPOVER_ARIA_LABEL } from '../../../../common/components/chart_settings_popover/translations';
import { DEFAULT_WIDTH } from '../../../../common/components/charts/draggable_legend';
import { MatrixLoader } from '../../../../common/components/matrix_histogram/matrix_loader';
import { DEFAULT_STACK_BY_FIELD, DEFAULT_STACK_BY_FIELD1 } from '../common/config';
import { useQueryToggle } from '../../../../common/containers/query_toggle';
import { TestProviders } from '../../../../common/mock';
import * as helpers from './helpers';
import { mockAlertSearchResponse } from './mock_data';
import { ChartContextMenu } from '../../../pages/detection_engine/chart_panels/chart_context_menu';
import { AlertsHistogramPanel, LEGEND_WITH_COUNTS_WIDTH } from '.';
import { LensEmbeddable } from '../../../../common/components/visualization_actions/lens_embeddable';
import { VisualizationEmbeddable } from '../../../../common/components/visualization_actions/visualization_embeddable';
import { AlertsHistogramPanel } from '.';
import type { ExperimentalFeatures } from '../../../../../common';
import { allowedExperimentalValues } from '../../../../../common';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
@ -81,30 +76,20 @@ jest.mock('../../../../common/lib/kibana', () => {
};
});
jest.mock('../../../../common/components/navigation/use_url_state_query_params');
jest.mock('../../../../common/components/visualization_actions/visualization_embeddable');
const defaultUseQueryAlertsReturn = {
loading: true,
setQuery: () => undefined,
data: null,
response: '',
request: '',
refetch: null,
};
const mockUseQueryAlerts = jest.fn().mockReturnValue(defaultUseQueryAlertsReturn);
jest.mock('../../../../common/hooks/use_experimental_features');
jest.mock('../../../containers/detection_engine/alerts/use_query', () => {
const original = jest.requireActual('../../../containers/detection_engine/alerts/use_query');
jest.mock('../../../../common/components/visualization_actions/use_visualization_response', () => {
const original = jest.requireActual(
'../../../../common/components/visualization_actions/use_visualization_response'
);
return {
...original,
useQueryAlerts: (...props: unknown[]) => mockUseQueryAlerts(...props),
useVisualizationResponse: jest.fn().mockReturnValue({ loading: false }),
};
});
jest.mock('../../../../common/hooks/use_experimental_features');
jest.mock('../../../../common/components/page/use_refetch_by_session');
jest.mock('../../../../common/components/visualization_actions/lens_embeddable');
jest.mock('../../../../common/components/page/use_refetch_by_session');
jest.mock('../common/hooks', () => {
const actual = jest.requireActual('../common/hooks');
return {
@ -115,7 +100,6 @@ jest.mock('../common/hooks', () => {
const mockUseIsExperimentalFeatureEnabled = jest.fn((feature: keyof ExperimentalFeatures) => {
if (feature === 'alertsPageChartsEnabled') return false;
if (feature === 'chartEmbeddablesEnabled') return false;
return allowedExperimentalValues[feature];
});
@ -146,6 +130,7 @@ const defaultProps = {
};
const mockSetToggle = jest.fn();
const mockUseQueryToggle = useQueryToggle as jest.Mock;
const mockUseVisualizationResponse = useVisualizationResponse as jest.Mock;
describe('AlertsHistogramPanel', () => {
beforeEach(() => {
@ -157,7 +142,7 @@ describe('AlertsHistogramPanel', () => {
);
});
it('renders correctly', () => {
test('renders correctly', () => {
const wrapper = mount(
<TestProviders>
<AlertsHistogramPanel {...defaultProps} />
@ -169,13 +154,9 @@ describe('AlertsHistogramPanel', () => {
describe('legend counts', () => {
beforeEach(() => {
mockUseQueryAlerts.mockReturnValue({
mockUseVisualizationResponse.mockReturnValue({
loading: false,
data: mockAlertSearchResponse,
setQuery: () => {},
response: '',
request: '',
refetch: () => {},
responses: mockAlertSearchResponse,
});
});
@ -188,16 +169,6 @@ describe('AlertsHistogramPanel', () => {
expect(wrapper.find('[data-test-subj="legendItemCount"]').exists()).toBe(false);
});
test('it renders counts in the legend when `showCountsInLegend` is true', () => {
const wrapper = mount(
<TestProviders>
<AlertsHistogramPanel {...defaultProps} showCountsInLegend={true} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="legendItemCount"]').exists()).toBe(true);
});
});
test('it renders the header with the specified `alignHeader` alignment', () => {
@ -212,41 +183,6 @@ describe('AlertsHistogramPanel', () => {
).toContain('flexEnd');
});
describe('inspect button', () => {
test('it renders the inspect button by default', () => {
const wrapper = mount(
<TestProviders>
<AlertsHistogramPanel {...defaultProps} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(true);
});
test('it does NOT render the inspect button when a `chartOptionsContextMenu` is provided', async () => {
const chartOptionsContextMenu = (queryId: string) => (
<ChartContextMenu
defaultStackByField={DEFAULT_STACK_BY_FIELD}
defaultStackByField1={DEFAULT_STACK_BY_FIELD1}
queryId={queryId}
setStackBy={jest.fn()}
setStackByField1={jest.fn()}
/>
);
const wrapper = mount(
<TestProviders>
<AlertsHistogramPanel
{...defaultProps}
chartOptionsContextMenu={chartOptionsContextMenu}
/>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(false);
});
});
test('it aligns the panel flex group at flex start to ensure the context menu is displayed at the top of the panel', () => {
const wrapper = mount(
<TestProviders>
@ -263,13 +199,9 @@ describe('AlertsHistogramPanel', () => {
const onFieldSelected = jest.fn();
const optionToSelect = 'agent.hostname';
mockUseQueryAlerts.mockReturnValue({
mockUseVisualizationResponse.mockReturnValue({
loading: false,
data: mockAlertSearchResponse,
setQuery: () => {},
response: '',
request: '',
refetch: () => {},
responses: mockAlertSearchResponse,
});
render(
@ -411,67 +343,6 @@ describe('AlertsHistogramPanel', () => {
});
});
test('it renders the chart options context menu when a `chartOptionsContextMenu` is provided', async () => {
const chartOptionsContextMenu = (queryId: string) => (
<ChartContextMenu
defaultStackByField={DEFAULT_STACK_BY_FIELD}
defaultStackByField1={DEFAULT_STACK_BY_FIELD1}
queryId={queryId}
setStackBy={jest.fn()}
setStackByField1={jest.fn()}
/>
);
render(
<TestProviders>
<AlertsHistogramPanel {...defaultProps} chartOptionsContextMenu={chartOptionsContextMenu} />
</TestProviders>
);
expect(
screen.getByRole('button', { name: CHART_SETTINGS_POPOVER_ARIA_LABEL })
).toBeInTheDocument();
});
describe('legend width', () => {
beforeEach(() => {
mockUseQueryAlerts.mockReturnValue({
loading: false,
data: mockAlertSearchResponse,
setQuery: () => {},
response: '',
request: '',
refetch: () => {},
});
});
test('it renders the legend with the expected default min-width', () => {
const wrapper = mount(
<TestProviders>
<AlertsHistogramPanel {...defaultProps} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="draggable-legend"]').first()).toHaveStyleRule(
'min-width',
`${DEFAULT_WIDTH}px`
);
});
test('it renders the legend with the expected min-width when `showCountsInLegend` is true', () => {
const wrapper = mount(
<TestProviders>
<AlertsHistogramPanel {...defaultProps} showCountsInLegend={true} />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="draggable-legend"]').first()).toHaveStyleRule(
'min-width',
`${LEGEND_WITH_COUNTS_WIDTH}px`
);
});
});
describe('Button view alerts', () => {
it('renders correctly', () => {
const props = { ...defaultProps, showLinkToAlerts: true };
@ -532,47 +403,8 @@ describe('AlertsHistogramPanel', () => {
});
});
describe('CombinedQueries', () => {
it('combinedQueries props is valid, alerts query include combinedQueries', async () => {
const mockGetAlertsHistogramQuery = jest.spyOn(helpers, 'getAlertsHistogramQuery');
const props = {
...defaultProps,
query: { query: 'host.name: "', language: 'kql' },
combinedQueries:
'{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"process.name"}}],"should":[],"must_not":[]}}',
};
const wrapper = mount(
<TestProviders>
<AlertsHistogramPanel {...props} />
</TestProviders>
);
await waitFor(() => {
expect(mockGetAlertsHistogramQuery.mock.calls[0]).toEqual([
'kibana.alert.rule.name',
'2020-07-07T08:20:18.966Z',
'2020-07-08T08:20:18.966Z',
[
{
bool: {
filter: [{ match_all: {} }, { exists: { field: 'process.name' } }],
must: [],
must_not: [],
should: [],
},
},
],
undefined,
]);
});
wrapper.unmount();
});
});
describe('Filters', () => {
it('filters props is valid, alerts query include filter', async () => {
const mockGetAlertsHistogramQuery = jest.spyOn(helpers, 'getAlertsHistogramQuery');
const statusFilter: Filter = {
meta: {
alias: null,
@ -593,7 +425,6 @@ describe('AlertsHistogramPanel', () => {
const props = {
...defaultProps,
query: { query: '', language: 'kql' },
filters: [statusFilter],
};
const wrapper = mount(
@ -603,109 +434,20 @@ describe('AlertsHistogramPanel', () => {
);
await waitFor(() => {
expect(mockGetAlertsHistogramQuery.mock.calls[1]).toEqual([
'kibana.alert.rule.name',
'2020-07-07T08:20:18.966Z',
'2020-07-08T08:20:18.966Z',
[
{
bool: {
filter: [{ term: { 'kibana.alert.workflow_status': 'open' } }],
must: [],
must_not: [],
should: [],
},
},
],
undefined,
]);
expect(
(VisualizationEmbeddable as unknown as jest.Mock).mock.calls[0][0].timerange
).toEqual({
from: '2020-07-07T08:20:18.966Z',
to: '2020-07-08T08:20:18.966Z',
});
expect(
(VisualizationEmbeddable as unknown as jest.Mock).mock.calls[0][0].extraOptions.filters
).toEqual(props.filters);
});
wrapper.unmount();
});
});
describe('parseCombinedQueries', () => {
it('return empty object when variables is undefined', async () => {
expect(helpers.parseCombinedQueries(undefined)).toEqual({});
});
it('return empty object when variables is empty string', async () => {
expect(helpers.parseCombinedQueries('')).toEqual({});
});
it('return empty object when variables is NOT a valid stringify json object', async () => {
expect(helpers.parseCombinedQueries('hello world')).toEqual({});
});
it('return a valid json object when variables is a valid json stringify', async () => {
expect(
helpers.parseCombinedQueries(
'{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"process.name"}}],"should":[],"must_not":[]}}'
)
).toMatchInlineSnapshot(`
Object {
"bool": Object {
"filter": Array [
Object {
"match_all": Object {},
},
Object {
"exists": Object {
"field": "process.name",
},
},
],
"must": Array [],
"must_not": Array [],
"should": Array [],
},
}
`);
});
});
describe('buildCombinedQueries', () => {
it('return empty array when variables is undefined', async () => {
expect(helpers.buildCombinedQueries(undefined)).toEqual([]);
});
it('return empty array when variables is empty string', async () => {
expect(helpers.buildCombinedQueries('')).toEqual([]);
});
it('return array with empty object when variables is NOT a valid stringify json object', async () => {
expect(helpers.buildCombinedQueries('hello world')).toEqual([{}]);
});
it('return a valid json object when variables is a valid json stringify', async () => {
expect(
helpers.buildCombinedQueries(
'{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"process.name"}}],"should":[],"must_not":[]}}'
)
).toMatchInlineSnapshot(`
Array [
Object {
"bool": Object {
"filter": Array [
Object {
"match_all": Object {},
},
Object {
"exists": Object {
"field": "process.name",
},
},
],
"must": Array [],
"must_not": Array [],
"should": Array [],
},
},
]
`);
});
});
describe('toggleQuery', () => {
it('toggles', async () => {
await act(async () => {
@ -722,7 +464,6 @@ describe('AlertsHistogramPanel', () => {
describe('when alertsPageChartsEnabled = false', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(false); // for chartEmbeddablesEnabled flag
mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(false); // for alertsPageChartsEnabled flag
});
@ -734,7 +475,10 @@ describe('AlertsHistogramPanel', () => {
</TestProviders>
);
expect(wrapper.find(MatrixLoader).exists()).toEqual(true);
expect(wrapper.find('[data-test-subj="panelFlexGroup"]').exists()).toEqual(true);
expect(wrapper.find('[data-test-subj="embeddable-matrix-histogram"]').exists()).toEqual(
true
);
});
});
it('toggleStatus=false, hide', async () => {
@ -745,7 +489,10 @@ describe('AlertsHistogramPanel', () => {
<AlertsHistogramPanel {...defaultProps} />
</TestProviders>
);
expect(wrapper.find(MatrixLoader).exists()).toEqual(false);
expect(wrapper.find('[data-test-subj="panelFlexGroup"]').exists()).toEqual(false);
expect(wrapper.find('[data-test-subj="embeddable-matrix-histogram"]').exists()).toEqual(
false
);
});
});
});
@ -753,7 +500,6 @@ describe('AlertsHistogramPanel', () => {
describe('when alertsPageChartsEnabled = true', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(false); // for chartEmbeddablesEnabled flag
mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(true); // for alertsPageChartsEnabled flag
});
@ -764,7 +510,10 @@ describe('AlertsHistogramPanel', () => {
<AlertsHistogramPanel {...defaultProps} isExpanded={true} />
</TestProviders>
);
expect(wrapper.find(MatrixLoader).exists()).toEqual(true);
expect(wrapper.find('[data-test-subj="panelFlexGroup"]').exists()).toEqual(true);
expect(wrapper.find('[data-test-subj="embeddable-matrix-histogram"]').exists()).toEqual(
true
);
});
});
it('isExpanded=false, hide', async () => {
@ -774,7 +523,10 @@ describe('AlertsHistogramPanel', () => {
<AlertsHistogramPanel {...defaultProps} isExpanded={false} />
</TestProviders>
);
expect(wrapper.find(MatrixLoader).exists()).toEqual(false);
expect(wrapper.find('[data-test-subj="panelFlexGroup"]').exists()).toEqual(false);
expect(wrapper.find('[data-test-subj="embeddable-matrix-histogram"]').exists()).toEqual(
false
);
});
});
it('isExpanded is not passed in and toggleStatus =true, render', async () => {
@ -784,7 +536,10 @@ describe('AlertsHistogramPanel', () => {
<AlertsHistogramPanel {...defaultProps} />
</TestProviders>
);
expect(wrapper.find(MatrixLoader).exists()).toEqual(true);
expect(wrapper.find('[data-test-subj="panelFlexGroup"]').exists()).toEqual(true);
expect(wrapper.find('[data-test-subj="embeddable-matrix-histogram"]').exists()).toEqual(
true
);
});
});
it('isExpanded is not passed in and toggleStatus =false, hide', async () => {
@ -795,17 +550,19 @@ describe('AlertsHistogramPanel', () => {
<AlertsHistogramPanel {...defaultProps} />
</TestProviders>
);
expect(wrapper.find(MatrixLoader).exists()).toEqual(false);
expect(wrapper.find('[data-test-subj="panelFlexGroup"]').exists()).toEqual(false);
expect(wrapper.find('[data-test-subj="embeddable-matrix-histogram"]').exists()).toEqual(
false
);
});
});
});
});
describe('when isChartEmbeddablesEnabled = true', () => {
describe('VisualizationEmbeddable', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle });
mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(true); // for chartEmbeddablesEnabled flag
mockUseIsExperimentalFeatureEnabled.mockReturnValueOnce(false); // for alertsPageChartsEnabled flag
});
@ -816,13 +573,14 @@ describe('AlertsHistogramPanel', () => {
</TestProviders>
);
mockUseQueryAlerts.mockReturnValue({
mockUseVisualizationResponse.mockReturnValue({
loading: false,
setQuery: () => undefined,
data: null,
response: '',
request: '',
refetch: null,
responses: [
{
hits: { total: 0 },
aggregations: { myAgg: { buckets: [{ key: 'A' }, { key: 'B' }, { key: 'C' }] } },
},
],
});
wrapper.setProps({ filters: [] });
wrapper.update();
@ -850,18 +608,9 @@ describe('AlertsHistogramPanel', () => {
<AlertsHistogramPanel {...defaultProps} />
</TestProviders>
);
expect((LensEmbeddable as unknown as jest.Mock).mock.calls[0][0].height).toEqual(155);
});
});
it('should skip calling getAlertsRiskQuery', async () => {
await act(async () => {
mount(
<TestProviders>
<AlertsHistogramPanel {...defaultProps} />
</TestProviders>
expect((VisualizationEmbeddable as unknown as jest.Mock).mock.calls[0][0].height).toEqual(
155
);
expect(mockUseQueryAlerts.mock.calls[0][0].skip).toBeTruthy();
});
});

View file

@ -5,9 +5,7 @@
* 2.0.
*/
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types';
import type { Action } from '@kbn/ui-actions-plugin/public';
import type { Position } from '@elastic/charts';
import type { EuiComboBox, EuiTitleSize } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiToolTip } from '@elastic/eui';
import React, { memo, useCallback, useMemo, useState, useEffect } from 'react';
@ -16,41 +14,26 @@ import { isEmpty, noop } from 'lodash/fp';
import { v4 as uuidv4 } from 'uuid';
import { sumBy } from 'lodash';
import type { Filter, Query } from '@kbn/es-query';
import { buildEsQuery } from '@kbn/es-query';
import { getEsQueryConfig } from '@kbn/data-plugin/common';
import type { Filter } from '@kbn/es-query';
import { useGlobalTime } from '../../../../common/containers/use_global_time';
import { APP_UI_ID } from '../../../../../common/constants';
import type { UpdateDateRange } from '../../../../common/components/charts/common';
import type { LegendItem } from '../../../../common/components/charts/draggable_legend_item';
import { escapeDataProviderId } from '../../../../common/components/drag_and_drop/helpers';
import { HeaderSection } from '../../../../common/components/header_section';
import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query';
import { ALERTS_QUERY_NAMES } from '../../../containers/detection_engine/alerts/constants';
import { getDetectionEngineUrl, useFormatUrl } from '../../../../common/components/link_to';
import { defaultLegendColors } from '../../../../common/components/matrix_histogram/utils';
import { InspectButtonContainer } from '../../../../common/components/inspect';
import { MatrixLoader } from '../../../../common/components/matrix_histogram/matrix_loader';
import { useKibana } from '../../../../common/lib/kibana';
import {
parseCombinedQueries,
buildCombinedQueries,
formatAlertsData,
getAlertsHistogramQuery,
showInitialLoadingSpinner,
createGenericSubtitle,
createEmbeddedDataSubtitle,
} from './helpers';
import { AlertsHistogram } from './alerts_histogram';
import * as i18n from './translations';
import type { AlertsAggregation, AlertsTotal } from './types';
import { LinkButton } from '../../../../common/components/links';
import { SecurityPageName } from '../../../../app/types';
import { DEFAULT_STACK_BY_FIELD, PANEL_HEIGHT } from '../common/config';
import type { AlertsStackByField } from '../common/types';
import { KpiPanel, StackByComboBox } from '../common/components';
import { useInspectButton } from '../common/hooks';
import { useQueryToggle } from '../../../../common/containers/query_toggle';
import { GROUP_BY_TOP_LABEL } from '../common/translations';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
@ -60,48 +43,30 @@ import { VisualizationEmbeddable } from '../../../../common/components/visualiza
import { useAlertHistogramCount } from '../../../hooks/alerts_visualization/use_alert_histogram_count';
import { useVisualizationResponse } from '../../../../common/components/visualization_actions/use_visualization_response';
const defaultTotalAlertsObj: AlertsTotal = {
value: 0,
relation: 'eq',
};
export const DETECTIONS_HISTOGRAM_ID = 'detections-histogram';
const ViewAlertsFlexItem = styled(EuiFlexItem)`
margin-left: ${({ theme }) => theme.eui.euiSizeL};
`;
const OptionsFlexItem = styled(EuiFlexItem)`
margin-left: ${({ theme }) => theme.eui.euiSizeS};
`;
export const LEGEND_WITH_COUNTS_WIDTH = 300; // px
const CHART_HEIGHT = 155; // px
interface AlertsHistogramPanelProps {
alignHeader?: 'center' | 'baseline' | 'stretch' | 'flexStart' | 'flexEnd';
chartHeight?: number;
chartOptionsContextMenu?: (queryId: string) => React.ReactNode;
combinedQueries?: string;
comboboxRef?: React.RefObject<EuiComboBox<string | number | string[] | undefined>>;
defaultStackByOption?: string;
extraActions?: Action[];
filters?: Filter[];
headerChildren?: React.ReactNode;
inspectTitle?: React.ReactNode;
legendPosition?: Position;
onFieldSelected?: (field: string) => void;
/** Override all defaults, and only display this field */
onlyField?: AlertsStackByField;
paddingSize?: 's' | 'm' | 'l' | 'none';
panelHeight?: number;
query?: Query;
runtimeMappings?: MappingRuntimeFields;
setComboboxInputRef?: (inputRef: HTMLInputElement | null) => void;
showCountsInLegend?: boolean;
showGroupByPlaceholder?: boolean;
showLegend?: boolean;
showLinkToAlerts?: boolean;
showStackBy?: boolean;
showTotalAlertsCount?: boolean;
@ -117,57 +82,42 @@ interface AlertsHistogramPanelProps {
setIsExpanded?: (status: boolean) => void;
}
const NO_LEGEND_DATA: LegendItem[] = [];
export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
({
alignHeader,
chartHeight = CHART_HEIGHT,
chartOptionsContextMenu,
combinedQueries,
comboboxRef,
defaultStackByOption = DEFAULT_STACK_BY_FIELD,
extraActions,
filters,
headerChildren,
inspectTitle,
legendPosition = 'right',
onFieldSelected,
onlyField,
paddingSize = 'm',
panelHeight = PANEL_HEIGHT,
query,
runtimeMappings,
setComboboxInputRef,
showCountsInLegend = false,
showGroupByPlaceholder = false,
showLegend = true,
showLinkToAlerts = false,
showStackBy = true,
showTotalAlertsCount = false,
signalIndexName,
stackByLabel,
stackByWidth,
timelineId,
title = i18n.HISTOGRAM_HEADER,
titleSize = 'm',
updateDateRange,
hideQueryToggle = false,
isExpanded,
setIsExpanded,
}) => {
const { to, from, deleteQuery, setQuery } = useGlobalTime();
const { to, from } = useGlobalTime();
// create a unique, but stable (across re-renders) query id
const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuidv4()}`, []);
const visualizationId = `alerts-trend-embeddable-${uniqueQueryId}`;
const [isInitialLoading, setIsInitialLoading] = useState(true);
const [isInspectDisabled, setIsInspectDisabled] = useState(false);
const [totalAlertsObj, setTotalAlertsObj] = useState<AlertsTotal>(defaultTotalAlertsObj);
const [selectedStackByOption, setSelectedStackByOption] = useState<string>(
onlyField == null ? defaultStackByOption : onlyField
);
const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled');
const isAlertsPageChartsEnabled = useIsExperimentalFeatureEnabled('alertsPageChartsEnabled');
const onSelect = useCallback(
@ -197,42 +147,16 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
[setToggleStatus, setIsExpanded, isAlertsPageChartsEnabled]
);
const querySkip = useMemo(
() =>
isAlertsPageChartsEnabled && setIsExpanded !== undefined ? !isExpanded : !toggleStatus,
[isAlertsPageChartsEnabled, setIsExpanded, isExpanded, toggleStatus]
);
const timerange = useMemo(() => ({ from, to }), [from, to]);
const {
loading: isLoadingAlerts,
data: alertsData,
setQuery: setAlertsQuery,
response,
request,
refetch,
} = useQueryAlerts<{}, AlertsAggregation>({
query: getAlertsHistogramQuery(
selectedStackByOption,
from,
to,
buildCombinedQueries(combinedQueries),
runtimeMappings
),
indexName: signalIndexName,
skip: querySkip || isChartEmbeddablesEnabled,
queryName: ALERTS_QUERY_NAMES.HISTOGRAM,
});
const kibana = useKibana();
const { navigateToApp } = kibana.services.application;
const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.alerts);
const totalAlerts = useAlertHistogramCount({
totalAlertsObj,
const { loading: isLoadingAlerts } = useVisualizationResponse({
visualizationId,
});
const totalAlerts = useAlertHistogramCount({
visualizationId,
isChartEmbeddablesEnabled,
});
const goToDetectionEngine = useCallback(
@ -245,30 +169,6 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
},
[navigateToApp, urlSearch]
);
const formattedAlertsData = useMemo(() => formatAlertsData(alertsData), [alertsData]);
const legendItems: LegendItem[] = useMemo(
() =>
showLegend && alertsData?.aggregations?.alertsByGrouping?.buckets != null
? alertsData.aggregations.alertsByGrouping.buckets.map((bucket, i) => ({
color: i < defaultLegendColors.length ? defaultLegendColors[i] : undefined,
count: showCountsInLegend ? bucket.doc_count : undefined,
dataProviderId: escapeDataProviderId(
`draggable-legend-item-${uuidv4()}-${selectedStackByOption}-${bucket.key}`
),
field: selectedStackByOption,
timelineId,
value: bucket?.key_as_string ?? bucket.key,
}))
: NO_LEGEND_DATA,
[
alertsData?.aggregations?.alertsByGrouping.buckets,
selectedStackByOption,
showCountsInLegend,
showLegend,
timelineId,
]
);
useEffect(() => {
let canceled = false;
@ -281,60 +181,6 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
};
}, [isInitialLoading, isLoadingAlerts, setIsInitialLoading]);
useInspectButton({
deleteQuery,
loading: isLoadingAlerts,
refetch,
request,
response,
setQuery,
uniqueQueryId,
});
useEffect(() => {
setTotalAlertsObj(
alertsData?.hits.total ?? {
value: 0,
relation: 'eq',
}
);
}, [alertsData]);
useEffect(() => {
try {
let converted = null;
if (combinedQueries != null) {
converted = parseCombinedQueries(combinedQueries);
} else {
converted = buildEsQuery(
undefined,
query != null ? [query] : [],
filters?.filter((f) => f.meta.disabled === false) ?? [],
{
...getEsQueryConfig(kibana.services.uiSettings),
dateFormatTZ: undefined,
}
);
}
setIsInspectDisabled(false);
setAlertsQuery(
getAlertsHistogramQuery(
selectedStackByOption,
from,
to,
!isEmpty(converted) ? [converted] : [],
runtimeMappings
)
);
} catch (e) {
setIsInspectDisabled(true);
setAlertsQuery(
getAlertsHistogramQuery(selectedStackByOption, from, to, [], runtimeMappings)
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedStackByOption, from, to, query, filters, combinedQueries, runtimeMappings]);
const linkButton = useMemo(() => {
if (showLinkToAlerts) {
return (
@ -385,114 +231,89 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
);
const embeddedDataAvailable = !!aggregationBucketsCount;
const showsEmbeddedData = showHistogram && isChartEmbeddablesEnabled;
const subtitle = showsEmbeddedData
const subtitle = showHistogram
? createEmbeddedDataSubtitle(embeddedDataLoaded, embeddedDataAvailable, totalAlerts)
: createGenericSubtitle(isInitialLoading, showTotalAlertsCount, totalAlerts);
return (
<InspectButtonContainer show={!isInitialLoading && showHistogram}>
<KpiPanel
height={panelHeight}
hasBorder
paddingSize={paddingSize}
data-test-subj="alerts-histogram-panel"
$toggleStatus={showHistogram}
<KpiPanel
height={panelHeight}
hasBorder
paddingSize={paddingSize}
data-test-subj="alerts-histogram-panel"
$toggleStatus={showHistogram}
>
<HeaderSection
alignHeader={alignHeader}
id={uniqueQueryId}
inspectTitle={inspectTitle}
outerDirection="column"
title={titleText}
titleSize={titleSize}
toggleStatus={showHistogram}
toggleQuery={hideQueryToggle ? undefined : toggleQuery}
showInspectButton={false}
subtitle={subtitle}
isInspectDisabled={false}
>
<HeaderSection
alignHeader={alignHeader}
id={uniqueQueryId}
inspectTitle={inspectTitle}
outerDirection="column"
title={titleText}
titleSize={titleSize}
toggleStatus={showHistogram}
toggleQuery={hideQueryToggle ? undefined : toggleQuery}
showInspectButton={isChartEmbeddablesEnabled ? false : chartOptionsContextMenu == null}
subtitle={subtitle}
isInspectDisabled={isInspectDisabled}
>
<EuiFlexGroup alignItems="flexStart" data-test-subj="panelFlexGroup" gutterSize="none">
<EuiFlexItem grow={false}>
{showStackBy && (
<>
<StackByComboBox
data-test-subj="stackByComboBox"
inputRef={setComboboxInputRef}
onSelect={onSelect}
prepend={stackByLabel}
ref={comboboxRef}
selected={selectedStackByOption}
useLensCompatibleFields={isChartEmbeddablesEnabled}
width={stackByWidth}
/>
{showGroupByPlaceholder && (
<>
<EuiSpacer data-test-subj="placeholderSpacer" size="s" />
<EuiToolTip
data-test-subj="placeholderTooltip"
content={i18n.NOT_AVAILABLE_TOOLTIP}
>
<StackByComboBox
data-test-subj="stackByPlaceholder"
isDisabled={true}
onSelect={noop}
prepend={GROUP_BY_TOP_LABEL}
selected=""
useLensCompatibleFields={isChartEmbeddablesEnabled}
width={stackByWidth}
/>
</EuiToolTip>
</>
)}
</>
)}
{headerChildren != null && headerChildren}
</EuiFlexItem>
{chartOptionsContextMenu != null && !isChartEmbeddablesEnabled && (
<OptionsFlexItem grow={false}>
{chartOptionsContextMenu(uniqueQueryId)}
</OptionsFlexItem>
<EuiFlexGroup alignItems="flexStart" data-test-subj="panelFlexGroup" gutterSize="none">
<EuiFlexItem grow={false}>
{showStackBy && (
<>
<StackByComboBox
data-test-subj="stackByComboBox"
inputRef={setComboboxInputRef}
onSelect={onSelect}
prepend={stackByLabel}
ref={comboboxRef}
selected={selectedStackByOption}
useLensCompatibleFields={true}
width={stackByWidth}
/>
{showGroupByPlaceholder && (
<>
<EuiSpacer data-test-subj="placeholderSpacer" size="s" />
<EuiToolTip
data-test-subj="placeholderTooltip"
content={i18n.NOT_AVAILABLE_TOOLTIP}
>
<StackByComboBox
data-test-subj="stackByPlaceholder"
isDisabled={true}
onSelect={noop}
prepend={GROUP_BY_TOP_LABEL}
selected=""
useLensCompatibleFields={true}
width={stackByWidth}
/>
</EuiToolTip>
</>
)}
</>
)}
{linkButton}
</EuiFlexGroup>
</HeaderSection>
{showHistogram ? (
isChartEmbeddablesEnabled ? (
<VisualizationEmbeddable
data-test-subj="embeddable-matrix-histogram"
extraActions={extraActions}
extraOptions={{
filters,
}}
getLensAttributes={getLensAttributes}
height={chartHeight ?? CHART_HEIGHT}
id={visualizationId}
inspectTitle={inspectTitle ?? title}
scopeId={SourcererScopeName.detections}
stackByField={selectedStackByOption}
timerange={timerange}
/>
) : isInitialLoading ? (
<MatrixLoader />
) : (
<AlertsHistogram
chartHeight={chartHeight}
data={formattedAlertsData}
from={from}
legendItems={legendItems}
legendPosition={legendPosition}
legendMinWidth={showCountsInLegend ? LEGEND_WITH_COUNTS_WIDTH : undefined}
loading={isLoadingAlerts}
to={to}
showLegend={showLegend}
updateDateRange={updateDateRange}
/>
)
) : null}
</KpiPanel>
</InspectButtonContainer>
{headerChildren != null && headerChildren}
</EuiFlexItem>
{linkButton}
</EuiFlexGroup>
</HeaderSection>
{showHistogram ? (
<VisualizationEmbeddable
data-test-subj="embeddable-matrix-histogram"
extraActions={extraActions}
extraOptions={{
filters,
}}
getLensAttributes={getLensAttributes}
height={chartHeight ?? CHART_HEIGHT}
id={visualizationId}
inspectTitle={inspectTitle ?? title}
scopeId={SourcererScopeName.detections}
stackByField={selectedStackByOption}
timerange={timerange}
/>
) : null}
</KpiPanel>
);
}
);

View file

@ -15,16 +15,10 @@ jest.mock('../../../common/components/visualization_actions/use_visualization_re
describe('useAlertHistogramCount', () => {
const props = {
totalAlertsObj: { value: 10, relation: '' },
visualizationId: 'mockVisualizationId',
isChartEmbeddablesEnabled: false,
};
it('returns total alerts count', () => {
const { result } = renderHook(() => useAlertHistogramCount(props), { wrapper: TestProviders });
expect(result.current).toEqual('Showing: 10 alerts');
});
it('returns visualization alerts count when isChartEmbeddablesEnabled is true', () => {
it('returns visualization alerts count', () => {
const testPops = { ...props, isChartEmbeddablesEnabled: true };
const { result } = renderHook(() => useAlertHistogramCount(testPops), {
wrapper: TestProviders,

View file

@ -11,40 +11,25 @@ import numeral from '@elastic/numeral';
import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants';
import { useUiSetting$ } from '../../../common/lib/kibana';
import { SHOWING_ALERTS } from '../../components/alerts_kpis/alerts_histogram_panel/translations';
import type { AlertsTotal } from '../../components/alerts_kpis/alerts_histogram_panel/types';
import { useVisualizationResponse } from '../../../common/components/visualization_actions/use_visualization_response';
export const useAlertHistogramCount = ({
totalAlertsObj,
visualizationId,
isChartEmbeddablesEnabled,
}: {
totalAlertsObj: AlertsTotal;
visualizationId: string;
isChartEmbeddablesEnabled: boolean;
}): string => {
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
const { responses: visualizationResponse } = useVisualizationResponse({ visualizationId });
const totalAlerts = useMemo(
() =>
SHOWING_ALERTS(
numeral(totalAlertsObj.value).format(defaultNumberFormat),
totalAlertsObj.value,
totalAlertsObj.relation === 'gte' ? '>' : totalAlertsObj.relation === 'lte' ? '<' : ''
),
[totalAlertsObj.value, totalAlertsObj.relation, defaultNumberFormat]
);
const { responses: visualizationResponses } = useVisualizationResponse({ visualizationId });
const visualizationAlerts = useMemo(() => {
const visualizationAlertsCount =
visualizationResponse != null ? visualizationResponse[0].hits.total : 0;
visualizationResponses != null ? visualizationResponses[0].hits.total : 0;
return SHOWING_ALERTS(
numeral(visualizationAlertsCount).format(defaultNumberFormat),
visualizationAlertsCount,
''
);
}, [defaultNumberFormat, visualizationResponse]);
}, [defaultNumberFormat, visualizationResponses]);
return isChartEmbeddablesEnabled ? visualizationAlerts : totalAlerts;
return visualizationAlerts;
};

View file

@ -18,6 +18,8 @@ import { TestProviders } from '../../../../common/mock';
import { ChartPanels } from '.';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useQueryToggle } from '../../../../common/containers/query_toggle';
import { LensEmbeddable } from '../../../../common/components/visualization_actions/lens_embeddable';
import { createResetGroupByFieldAction } from '../../../components/alerts_kpis/alerts_histogram_panel/helpers';
jest.mock('./alerts_local_storage');
jest.mock('../../../../common/containers/sourcerer');
@ -227,79 +229,164 @@ describe('ChartPanels', () => {
describe(`'Reset group by fields' context menu action`, () => {
describe('Group by', () => {
const alertViewSelections = ['trend', 'table', 'treemap'];
test(`it resets the 'Group by' field to the default value, even if the user has triggered validation errors, when 'alertViewSelection' is 'treemap'`, async () => {
(useAlertsLocalStorage as jest.Mock).mockReturnValue({
...defaultAlertSettings,
alertViewSelection: 'treemap',
});
alertViewSelections.forEach((alertViewSelection) => {
test(`it resets the 'Group by' field to the default value, even if the user has triggered validation errors, when 'alertViewSelection' is '${alertViewSelection}'`, async () => {
const defaultValue = 'kibana.alert.rule.name';
const invalidValue = 'an invalid value';
render(
<TestProviders>
<ChartPanels {...defaultProps} />
</TestProviders>
);
const initialInput = screen.getAllByTestId('comboBoxSearchInput')[0];
expect(initialInput).toHaveValue(defaultValue);
// update the EuiComboBox input to an invalid value:
fireEvent.change(initialInput, { target: { value: invalidValue } });
const afterInvalidInput = screen.getAllByTestId('comboBoxSearchInput')[0];
expect(afterInvalidInput).toHaveValue(invalidValue); // the 'Group by' EuiComboBox is now in the "error state"
expect(afterInvalidInput).toBeInvalid();
resetGroupByFields(); // invoke the `Reset group by fields` context menu action
await waitFor(() => {
const afterReset = screen.getAllByTestId('comboBoxSearchInput')[0];
expect(afterReset).toHaveValue(defaultValue); // back to the default
});
});
describe.each([['trend'], ['table']])(`when 'alertViewSelection' is '%s'`, (view) => {
test(`it has resets the 'Group by' field as an extra action`, async () => {
(useAlertsLocalStorage as jest.Mock).mockReturnValue({
...defaultAlertSettings,
alertViewSelection,
alertViewSelection: view,
});
const defaultValue = 'kibana.alert.rule.name';
const invalidValue = 'an invalid value';
const mockResetGroupByFieldsAction = [
createResetGroupByFieldAction({ callback: jest.fn(), order: 5 }),
];
const testProps = {
...defaultProps,
extraActions: mockResetGroupByFieldsAction,
};
render(
<TestProviders>
<ChartPanels {...defaultProps} />
<ChartPanels {...testProps} />
</TestProviders>
);
const initialInput = screen.getAllByTestId('comboBoxSearchInput')[0];
expect(initialInput).toHaveValue(defaultValue);
await waitFor(() => {
expect(
(LensEmbeddable as unknown as jest.Mock).mock.calls[0][0].extraActions.length
).toEqual(1);
expect(
(LensEmbeddable as unknown as jest.Mock).mock.calls[0][0].extraActions[0].id
).toEqual('resetGroupByField');
});
});
});
// update the EuiComboBox input to an invalid value:
fireEvent.change(initialInput, { target: { value: invalidValue } });
describe.each([
['trend', 'kibana.alert.rule.name'],
['table', 'kibana.alert.rule.name'],
])(`when 'alertViewSelection' is '%s'`, (view, defaultGroupBy) => {
test(`it has resets the 'Group by' field as an extra action, with default value ${defaultGroupBy}`, async () => {
(useAlertsLocalStorage as jest.Mock).mockReturnValue({
...defaultAlertSettings,
alertViewSelection: view,
});
const afterInvalidInput = screen.getAllByTestId('comboBoxSearchInput')[0];
expect(afterInvalidInput).toHaveValue(invalidValue); // the 'Group by' EuiComboBox is now in the "error state"
expect(afterInvalidInput).toBeInvalid();
const mockResetGroupByFieldsAction = [
createResetGroupByFieldAction({ callback: jest.fn(), order: 5 }),
];
resetGroupByFields(); // invoke the `Reset group by fields` context menu action
const testProps = {
...defaultProps,
extraActions: mockResetGroupByFieldsAction,
};
render(
<TestProviders>
<ChartPanels {...testProps} />
</TestProviders>
);
await waitFor(() => {
const afterReset = screen.getAllByTestId('comboBoxSearchInput')[0];
expect(afterReset).toHaveValue(defaultValue); // back to the default
expect(
(LensEmbeddable as unknown as jest.Mock).mock.calls[0][0].extraActions.length
).toEqual(1);
expect(
(LensEmbeddable as unknown as jest.Mock).mock.calls[0][0].extraActions[0].id
).toEqual('resetGroupByField');
expect((LensEmbeddable as unknown as jest.Mock).mock.calls[0][0].stackByField).toEqual(
defaultGroupBy
);
});
});
});
});
describe('Group by top', () => {
const justTableAndTreemap = ['table', 'treemap'];
test(`it resets the 'Group by top' field to the default value, even if the user has triggered validation errors, when 'alertViewSelection' is 'treemap'`, async () => {
(useAlertsLocalStorage as jest.Mock).mockReturnValue({
...defaultAlertSettings,
alertViewSelection: 'treemap',
});
justTableAndTreemap.forEach((alertViewSelection) => {
test(`it resets the 'Group by top' field to the default value, even if the user has triggered validation errors, when 'alertViewSelection' is '${alertViewSelection}'`, async () => {
(useAlertsLocalStorage as jest.Mock).mockReturnValue({
...defaultAlertSettings,
alertViewSelection,
});
const defaultValue = 'host.name';
const invalidValue = 'an-invalid-value';
const defaultValue = 'host.name';
const invalidValue = 'an-invalid-value';
render(
<TestProviders>
<ChartPanels {...defaultProps} />
</TestProviders>
);
render(
<TestProviders>
<ChartPanels {...defaultProps} />
</TestProviders>
);
const initialInput = screen.getAllByTestId('comboBoxSearchInput')[1];
expect(initialInput).toHaveValue(defaultValue);
const initialInput = screen.getAllByTestId('comboBoxSearchInput')[1];
expect(initialInput).toHaveValue(defaultValue);
// update the EuiComboBox input to an invalid value:
fireEvent.change(initialInput, { target: { value: invalidValue } });
// update the EuiComboBox input to an invalid value:
fireEvent.change(initialInput, { target: { value: invalidValue } });
const afterInvalidInput = screen.getAllByTestId('comboBoxSearchInput')[1];
expect(afterInvalidInput).toHaveValue(invalidValue); // the 'Group by top' EuiComboBox is now in the "error state"
expect(afterInvalidInput).toBeInvalid();
const afterInvalidInput = screen.getAllByTestId('comboBoxSearchInput')[1];
expect(afterInvalidInput).toHaveValue(invalidValue); // the 'Group by top' EuiComboBox is now in the "error state"
expect(afterInvalidInput).toBeInvalid();
resetGroupByFields(); // invoke the `Reset group by fields` context menu action
resetGroupByFields(); // invoke the `Reset group by fields` context menu action
await waitFor(() => {
const afterReset = screen.getAllByTestId('comboBoxSearchInput')[1];
expect(afterReset).toHaveValue(defaultValue); // back to the default
});
});
await waitFor(() => {
const afterReset = screen.getAllByTestId('comboBoxSearchInput')[1];
expect(afterReset).toHaveValue(defaultValue); // back to the default
});
test(`it renders the 'Group by top' field to the default value, when 'alertViewSelection' is 'table'`, async () => {
(useAlertsLocalStorage as jest.Mock).mockReturnValue({
...defaultAlertSettings,
alertViewSelection: 'table',
});
const defaultValue = 'host.name';
render(
<TestProviders>
<ChartPanels {...defaultProps} />
</TestProviders>
);
await waitFor(() => {
expect(
(LensEmbeddable as unknown as jest.Mock).mock.calls[0][0].extraOptions.breakdownField
).toEqual(defaultValue);
});
});
});

View file

@ -201,7 +201,6 @@ const ChartPanelsComponent: React.FC<Props> = ({
<AlertsHistogramPanel
alignHeader="flexStart"
chartHeight={TREND_CHART_HEIGHT}
chartOptionsContextMenu={chartOptionsContextMenu}
comboboxRef={stackByField0ComboboxRef}
defaultStackByOption={trendChartStackBy}
extraActions={resetGroupByFieldAction}
@ -209,10 +208,7 @@ const ChartPanelsComponent: React.FC<Props> = ({
inspectTitle={i18n.TREND}
onFieldSelected={updateCommonStackBy0}
panelHeight={CHART_PANEL_HEIGHT}
query={query}
runtimeMappings={runtimeMappings}
setComboboxInputRef={setStackByField0ComboboxInputRef}
showCountsInLegend={true}
showGroupByPlaceholder={false}
showTotalAlertsCount={false}
signalIndexName={signalIndexName}
@ -239,13 +235,10 @@ const ChartPanelsComponent: React.FC<Props> = ({
filters={alertsDefaultFilters}
inspectTitle={isAlertsPageChartsEnabled ? i18n.COUNTS : i18n.TABLE}
panelHeight={CHART_PANEL_HEIGHT}
query={query}
runtimeMappings={runtimeMappings}
setStackByField0={updateCommonStackBy0}
setStackByField0ComboboxInputRef={setStackByField0ComboboxInputRef}
setStackByField1={updateCommonStackBy1}
setStackByField1ComboboxInputRef={setStackByField1ComboboxInputRef}
signalIndexName={signalIndexName}
stackByField0={countTableStackBy0}
stackByField0ComboboxRef={stackByField0ComboboxRef}
stackByField1={countTableStackBy1}

View file

@ -7,13 +7,11 @@
import { render } from '@testing-library/react';
import React from 'react';
import { RiskScoreOverTime, scoreFormatter } from '.';
import { RiskScoreOverTime } from '.';
import { TestProviders } from '../../../common/mock';
import { LineSeries } from '@elastic/charts';
import { RiskScoreEntity } from '../../../../common/search_strategy';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
const mockLineSeries = LineSeries as jest.Mock;
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
jest.mock('@elastic/charts', () => {
const original = jest.requireActual('@elastic/charts');
@ -58,17 +56,7 @@ describe('Risk Score Over Time', () => {
expect(queryByTestId('RiskScoreOverTime')).toBeInTheDocument();
});
it('renders loader when loading', () => {
const { queryByTestId } = render(
<TestProviders>
<RiskScoreOverTime {...props} loading={true} />
</TestProviders>
);
expect(queryByTestId('RiskScoreOverTime-loading')).toBeInTheDocument();
});
it('renders VisualizationEmbeddable when isChartEmbeddablesEnabled = true and spaceId exists', () => {
it('renders VisualizationEmbeddable', () => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
const { queryByTestId } = render(
@ -79,25 +67,4 @@ describe('Risk Score Over Time', () => {
expect(queryByTestId('visualization-embeddable')).toBeInTheDocument();
});
describe('scoreFormatter', () => {
it('renders score formatted', () => {
render(
<TestProviders>
<RiskScoreOverTime {...props} />
</TestProviders>
);
const tickFormat = mockLineSeries.mock.calls[0][0].tickFormat;
expect(tickFormat).toBe(scoreFormatter);
});
it('renders a formatted score', () => {
expect(scoreFormatter(3.000001)).toEqual('3');
expect(scoreFormatter(3.4999)).toEqual('3');
expect(scoreFormatter(3.51111)).toEqual('4');
expect(scoreFormatter(3.9999)).toEqual('4');
});
});
});

View file

@ -5,38 +5,19 @@
* 2.0.
*/
import React, { useMemo, useCallback } from 'react';
import type { TooltipHeaderFormatter } from '@elastic/charts';
import {
Chart,
LineSeries,
ScaleType,
Settings,
Axis,
Position,
AnnotationDomainType,
LineAnnotation,
Tooltip,
} from '@elastic/charts';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart, EuiText, EuiPanel } from '@elastic/eui';
import styled from 'styled-components';
import { euiThemeVars } from '@kbn/ui-theme';
import { i18n } from '@kbn/i18n';
import { chartDefaultSettings, useThemes } from '../../../common/components/charts/common';
import { useTimeZone } from '../../../common/lib/kibana';
import { histogramDateTimeFormatter } from '../../../common/components/utils';
import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { HeaderSection } from '../../../common/components/header_section';
import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect';
import * as translations from './translations';
import { PreferenceFormattedDate } from '../../../common/components/formatted_date';
import { InspectButtonContainer } from '../../../common/components/inspect';
import type {
HostRiskScore,
RiskScoreEntity,
UserRiskScore,
} from '../../../../common/search_strategy';
import { isUserRiskScore } from '../../../../common/search_strategy';
import { useSpaceId } from '../../../common/hooks/use_space_id';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { VisualizationEmbeddable } from '../../../common/components/visualization_actions/visualization_embeddable';
import { getRiskScoreOverTimeAreaAttributes } from '../../lens_attributes/risk_score_over_time_area';
@ -52,19 +33,7 @@ export interface RiskScoreOverTimeProps {
toggleQuery?: (status: boolean) => void;
}
const RISKY_THRESHOLD = 70;
const DEFAULT_CHART_HEIGHT = 250;
const CHART_HEIGHT = 180;
const StyledEuiText = styled(EuiText)`
font-size: 9px;
font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
margin-right: ${({ theme }) => theme.eui.euiSizeXS};
`;
const LoadingChart = styled(EuiLoadingChart)`
display: block;
text-align: center;
`;
export const scoreFormatter = (d: number) => Math.round(d).toString();
@ -79,27 +48,7 @@ const RiskScoreOverTimeComponent: React.FC<RiskScoreOverTimeProps> = ({
toggleStatus,
toggleQuery,
}) => {
const timeZone = useTimeZone();
const dataTimeFormatter = useMemo(() => histogramDateTimeFormatter([from, to]), [from, to]);
const headerFormatter = useCallback<TooltipHeaderFormatter>(
({ value }) => <PreferenceFormattedDate value={value} />,
[]
);
const { baseTheme, theme } = useThemes();
const graphData = useMemo(
() =>
riskScore
?.map((data) => ({
x: data['@timestamp'],
y: (isUserRiskScore(data) ? data.user : data.host).risk.calculated_score_norm,
}))
.reverse() ?? [],
[riskScore]
);
const spaceId = useSpaceId();
const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled');
const timerange = useMemo(
() => ({
from,
@ -119,17 +68,12 @@ const RiskScoreOverTimeComponent: React.FC<RiskScoreOverTimeProps> = ({
toggleStatus={toggleStatus}
/>
</EuiFlexItem>
{toggleStatus && !isChartEmbeddablesEnabled && (
<EuiFlexItem grow={false}>
<InspectButton queryId={queryId} title={title} />
</EuiFlexItem>
)}
</EuiFlexGroup>
{toggleStatus && (
<EuiFlexGroup gutterSize="none" direction="column">
<EuiFlexItem grow={1}>
{isChartEmbeddablesEnabled && spaceId ? (
{spaceId && (
<VisualizationEmbeddable
applyGlobalQueriesAndFilters={false}
timerange={timerange}
@ -139,85 +83,6 @@ const RiskScoreOverTimeComponent: React.FC<RiskScoreOverTimeProps> = ({
height={CHART_HEIGHT}
extraOptions={{ spaceId }}
/>
) : (
<div style={{ height: DEFAULT_CHART_HEIGHT }}>
{loading ? (
<LoadingChart size="l" data-test-subj="RiskScoreOverTime-loading" />
) : (
<Chart>
<Tooltip headerFormatter={headerFormatter} />
<Settings
{...chartDefaultSettings}
baseTheme={baseTheme}
theme={theme}
locale={i18n.getLocale()}
/>
<Axis
id="bottom"
position={Position.Bottom}
tickFormat={dataTimeFormatter}
gridLine={{
visible: true,
strokeWidth: 1,
opacity: 1,
dash: [3, 5],
}}
/>
<Axis
domain={{
min: 0,
max: 100,
}}
id="left"
position={Position.Left}
ticks={3}
style={{
tickLine: {
visible: false,
},
tickLabel: {
padding: 10,
},
}}
/>
<LineSeries
id="RiskOverTime"
name={translations.RISK_SCORE}
xScaleType={ScaleType.Time}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
timeZone={timeZone}
data={graphData}
tickFormat={scoreFormatter}
/>
<LineAnnotation
id="RiskOverTime_annotation"
domainType={AnnotationDomainType.YDomain}
dataValues={[
{
dataValue: RISKY_THRESHOLD,
details: `${RISKY_THRESHOLD}`,
header: translations.RISK_THRESHOLD,
},
]}
markerPosition="left"
style={{
line: {
strokeWidth: 1,
stroke: euiThemeVars.euiColorDanger,
opacity: 1,
},
}}
marker={
<StyledEuiText color={euiThemeVars.euiColorDarkestShade}>
{translations.RISKY}
</StyledEuiText>
}
/>
</Chart>
)}
</div>
)}
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -19,11 +19,9 @@ import {
UserDetailsLink,
} from '../../../common/components/links';
import type { AuthenticationsEdges } from '../../../../common/search_strategy';
import { MatrixHistogramType } from '../../../../common/search_strategy';
import type { AuthTableColumns } from './types';
import type {
MatrixHistogramConfigs,
MatrixHistogramMappingTypes,
MatrixHistogramOption,
} from '../../../common/components/matrix_histogram/types';
import type { LensAttributes } from '../../../common/components/visualization_actions/types';
@ -195,36 +193,10 @@ export const authenticationsStackByOptions: MatrixHistogramOption[] = [
];
const DEFAULT_STACK_BY = 'event.outcome';
enum AuthenticationsMatrixDataGroup {
authenticationsSuccess = 'success',
authenticationsFailure = 'failure',
}
export enum ChartColors {
authenticationsSuccess = '#54B399',
authenticationsFailure = '#E7664C',
}
export const authenticationsMatrixDataMappingFields: MatrixHistogramMappingTypes = {
[AuthenticationsMatrixDataGroup.authenticationsSuccess]: {
key: AuthenticationsMatrixDataGroup.authenticationsSuccess,
value: null,
color: ChartColors.authenticationsSuccess,
},
[AuthenticationsMatrixDataGroup.authenticationsFailure]: {
key: AuthenticationsMatrixDataGroup.authenticationsFailure,
value: null,
color: ChartColors.authenticationsFailure,
},
};
export const histogramConfigs: MatrixHistogramConfigs = {
defaultStackByOption:
authenticationsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ??
authenticationsStackByOptions[0],
errorMessage: i18n.ERROR_FETCHING_AUTHENTICATIONS_DATA,
histogramType: MatrixHistogramType.authentications,
mapping: authenticationsMatrixDataMappingFields,
stackByOptions: authenticationsStackByOptions,
title: i18n.NAVIGATION_AUTHENTICATIONS_TITLE,
lensAttributes: authenticationLensAttributes as LensAttributes,

View file

@ -5,5 +5,6 @@
* 2.0.
*/
export * from './hosts';
export * from './unique_ips';
import React from 'react';
export const KpiBaseComponent = jest.fn().mockReturnValue(<div />);

View file

@ -0,0 +1,29 @@
/*
* 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 { EuiFlexGroup } from '@elastic/eui';
import type { StatItems } from '../stat_items';
import { StatItemsComponent } from '../stat_items';
interface KpiBaseComponentProps {
from: string;
id: string;
statItems: Readonly<StatItems[]>;
to: string;
}
export const KpiBaseComponent = React.memo<KpiBaseComponentProps>(({ statItems, ...props }) => (
<EuiFlexGroup wrap>
{statItems.map((statItem) => (
<StatItemsComponent {...props} key={`kpi-base-${statItem.key}`} statItems={statItem} />
))}
</EuiFlexGroup>
));
KpiBaseComponent.displayName = 'KpiBaseComponent';

View file

@ -7,4 +7,3 @@
export { StatItemsComponent } from './stat_items';
export type { StatItemsProps, StatItems } from './types';
export { useKpiMatrixStatus } from './use_kpi_matrix_status';

View file

@ -1,66 +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 type { MetricProps } from './metric';
import { Metric } from './metric';
import type { RenderResult } from '@testing-library/react';
import { render } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../../common/mock';
import type { LensAttributes } from '../../../common/components/visualization_actions/types';
jest.mock('../../../common/components/visualization_actions/actions');
describe('Metric', () => {
const testProps = {
fields: [
{
key: 'uniqueSourceIps',
description: 'Source',
value: 1714,
color: '#D36086',
icon: 'cross',
lensAttributes: {} as LensAttributes,
},
{
key: 'uniqueDestinationIps',
description: 'Dest.',
value: 2359,
color: '#9170B8',
icon: 'cross',
lensAttributes: {} as LensAttributes,
},
],
id: 'test',
timerange: { from: '', to: '' },
isAreaChartDataAvailable: true,
isBarChartDataAvailable: true,
} as MetricProps;
let res: RenderResult;
beforeEach(() => {
res = render(
<TestProviders>
<Metric {...testProps} />
</TestProviders>
);
});
it('renders icons', () => {
expect(res.getAllByTestId('stat-icon')).toHaveLength(2);
});
it('render titles', () => {
expect(res.getAllByTestId('stat-title')[0]).toHaveTextContent('1,714 Source');
expect(res.getAllByTestId('stat-title')[1]).toHaveTextContent('2,359 Dest.');
});
it('render actions', () => {
expect(res.getAllByTestId('visualizationActions')).toHaveLength(2);
});
});

View file

@ -1,78 +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 { EuiFlexGroup, EuiIcon } from '@elastic/eui';
import React from 'react';
import type { StatItem } from './types';
import { HoverVisibilityContainer } from '../../../common/components/hover_visibility_container';
import { VisualizationActions } from '../../../common/components/visualization_actions/actions';
import { FlexItem, StatValue } from './utils';
import { getEmptyTagValue } from '../../../common/components/empty_value';
import { VISUALIZATION_ACTIONS_BUTTON_CLASS } from '../../../common/components/visualization_actions/utils';
export interface MetricProps {
fields: StatItem[];
id: string;
timerange: { from: string; to: string };
isAreaChartDataAvailable: boolean;
isBarChartDataAvailable: boolean;
inspectTitle?: string;
inspectIndex?: number;
}
const MetricComponent = ({
fields,
id,
timerange,
isAreaChartDataAvailable,
isBarChartDataAvailable,
inspectTitle,
inspectIndex,
}: MetricProps) => {
return (
<EuiFlexGroup>
{fields.map((field) => (
<FlexItem key={`stat-items-field-${field.key}`}>
<EuiFlexGroup alignItems="center" gutterSize="m" responsive={false}>
{(isAreaChartDataAvailable || isBarChartDataAvailable) && field.icon && (
<FlexItem grow={false}>
<EuiIcon
type={field.icon}
color={field.color}
size="l"
data-test-subj="stat-icon"
/>
</FlexItem>
)}
<FlexItem>
<HoverVisibilityContainer targetClassNames={[VISUALIZATION_ACTIONS_BUTTON_CLASS]}>
<StatValue>
<p data-test-subj="stat-title">
{field.value != null ? field.value.toLocaleString() : getEmptyTagValue()}{' '}
{field.description}
</p>
</StatValue>
{field.lensAttributes && timerange && (
<VisualizationActions
lensAttributes={field.lensAttributes}
queryId={id}
inspectIndex={inspectIndex}
timerange={timerange}
title={inspectTitle}
className="viz-actions"
/>
)}
</HoverVisibilityContainer>
</FlexItem>
</EuiFlexGroup>
</FlexItem>
))}
</EuiFlexGroup>
);
};
export const Metric = React.memo(MetricComponent);

View file

@ -7,14 +7,14 @@
import { EuiFlexGroup, EuiIcon } from '@elastic/eui';
import React from 'react';
import { FlexItem, MetricItem, StatValue } from './utils';
import type { MetricStatItem } from './types';
import { VisualizationEmbeddable } from '../../../common/components/visualization_actions/visualization_embeddable';
import type { FieldConfigs } from './types';
export interface MetricEmbeddableProps {
fields: MetricStatItem[];
fields: FieldConfigs[];
id: string;
timerange: { from: string; to: string };
inspectTitle?: string;
timerange: { from: string; to: string };
}
const CHART_HEIGHT = 36;
@ -22,14 +22,9 @@ const CHART_HEIGHT = 36;
const MetricEmbeddableComponent = ({
fields,
id,
timerange,
inspectTitle,
}: {
fields: MetricStatItem[];
id: string;
timerange: { from: string; to: string };
inspectTitle?: string;
}) => {
timerange,
}: MetricEmbeddableProps) => {
return (
<EuiFlexGroup gutterSize="none" className="metricEmbeddable">
{fields.map((field) => (

View file

@ -5,245 +5,67 @@
* 2.0.
*/
import type { ReactWrapper } from 'enzyme';
import { mount } from 'enzyme';
import React from 'react';
import { ThemeProvider } from 'styled-components';
import type { StatItemsProps } from '.';
import { render, fireEvent, waitFor } from '@testing-library/react';
import { StatItemsComponent } from './stat_items';
import { BarChart } from '../../../common/components/charts/barchart';
import { AreaChart } from '../../../common/components/charts/areachart';
import { EuiHorizontalRule } from '@elastic/eui';
import { mockUpdateDateRange } from '../../network/components/kpi_network/mock';
import { createMockStore } from '../../../common/mock';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock';
import * as module from '../../../common/containers/query_toggle';
import type { LensAttributes } from '../../../common/components/visualization_actions/types';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
const from = '2019-06-15T06:00:00.000Z';
const to = '2019-06-18T06:00:00.000Z';
jest.mock('../../../common/components/charts/areachart', () => {
return { AreaChart: () => <div className="areachart" /> };
});
jest.mock('../../../common/components/charts/barchart', () => {
return { BarChart: () => <div className="barchart" /> };
});
jest.mock('../../../common/components/visualization_actions/actions');
jest.mock('../../../common/hooks/use_experimental_features', () => ({
useIsExperimentalFeatureEnabled: jest.fn(),
}));
import { TestProviders } from '../../../common/mock/test_providers';
import { useToggleStatus } from './use_toggle_status';
jest.mock('../../../common/components/visualization_actions/visualization_embeddable');
jest.mock('./use_toggle_status', () => ({
useToggleStatus: jest.fn().mockReturnValue({ isToggleExpanded: true, onToggle: jest.fn() }),
}));
const mockSetToggle = jest.fn();
const mockUseIsExperimentalFeatureEnabled = useIsExperimentalFeatureEnabled as jest.Mock;
jest
.spyOn(module, 'useQueryToggle')
.mockImplementation(() => ({ toggleStatus: true, setToggleStatus: mockSetToggle }));
const mockSetQuerySkip = jest.fn();
describe('Stat Items Component', () => {
const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } });
const store = createMockStore();
const testProps = {
description: 'HOSTS',
fields: [{ key: 'hosts', value: null, color: '#6092C0', icon: 'cross' }],
from,
id: 'hostsKpiHostsQuery',
key: 'mock-keys',
loading: false,
setQuerySkip: mockSetQuerySkip,
to,
updateDateRange: mockUpdateDateRange,
};
beforeEach(() => {
jest.clearAllMocks();
});
describe.each([
[
mount(
<ThemeProvider theme={mockTheme}>
<ReduxStoreProvider store={store}>
<StatItemsComponent {...testProps} />
</ReduxStoreProvider>
</ThemeProvider>
),
],
[
mount(
<ThemeProvider theme={mockTheme}>
<ReduxStoreProvider store={store}>
<StatItemsComponent areaChart={[]} barChart={[]} {...testProps} />
</ReduxStoreProvider>
</ThemeProvider>
),
],
])('disable charts', (wrapper) => {
test('should render titles', () => {
expect(wrapper.find('[data-test-subj="stat-title"]')).toBeTruthy();
});
test('should not render icons', () => {
expect(wrapper.find('[data-test-subj="stat-icon"]').filter('EuiIcon')).toHaveLength(0);
});
test('should not render barChart', () => {
expect(wrapper.find(BarChart)).toHaveLength(0);
});
test('should not render areaChart', () => {
expect(wrapper.find(AreaChart)).toHaveLength(0);
});
test('should not render spliter', () => {
expect(wrapper.find(EuiHorizontalRule)).toHaveLength(0);
});
});
const mockStatItemsData: StatItemsProps = {
...testProps,
id: 'UniqueIps',
areaChart: [
{
key: 'uniqueSourceIpsHistogram',
value: [
{ x: new Date('2019-05-03T13:00:00.000Z').toISOString(), y: 565975 },
{ x: new Date('2019-05-04T01:00:00.000Z').toISOString(), y: 1084366 },
{ x: new Date('2019-05-04T13:00:00.000Z').toISOString(), y: 12280 },
],
color: '#D36086',
},
{
key: 'uniqueDestinationIpsHistogram',
value: [
{ x: new Date('2019-05-03T13:00:00.000Z').toISOString(), y: 565975 },
{ x: new Date('2019-05-04T01:00:00.000Z').toISOString(), y: 1084366 },
{ x: new Date('2019-05-04T13:00:00.000Z').toISOString(), y: 12280 },
],
color: '#9170B8',
},
],
barChart: [
{ key: 'uniqueSourceIps', value: [{ x: 'uniqueSourceIps', y: '1714' }], color: '#D36086' },
{
key: 'uniqueDestinationIps',
value: [{ x: 'uniqueDestinationIps', y: 2354 }],
color: '#9170B8',
},
],
description: 'UNIQUE_PRIVATE_IPS',
enableAreaChart: true,
enableBarChart: true,
describe('StatItemsComponent', () => {
const mockStatItems = {
key: 'hosts',
fields: [
{
key: 'uniqueSourceIps',
description: 'Source',
value: 1714,
color: '#D36086',
icon: 'cross',
lensAttributes: {} as LensAttributes,
},
{
key: 'uniqueDestinationIps',
description: 'Dest.',
value: 2359,
color: '#9170B8',
icon: 'cross',
key: 'hosts',
value: null,
color: '#fff',
icon: 'storage',
lensAttributes: {} as LensAttributes,
},
],
barChartLensAttributes: {} as LensAttributes,
enableAreaChart: true,
description: 'Mock Description',
areaChartLensAttributes: {} as LensAttributes,
};
let wrapper: ReactWrapper;
describe('rendering kpis with charts', () => {
beforeAll(() => {
wrapper = mount(
<ReduxStoreProvider store={store}>
<StatItemsComponent {...mockStatItemsData} />
</ReduxStoreProvider>
);
const mockProps = {
statItems: mockStatItems,
from: new Date('2023-01-01').toISOString(),
to: new Date('2023-12-31').toISOString(),
id: 'mockId',
};
it('renders visualizations', () => {
const { getByText, getAllByTestId } = render(<StatItemsComponent {...mockProps} />, {
wrapper: TestProviders,
});
test('should handle multiple titles', () => {
expect(wrapper.find('[data-test-subj="stat-title"]').find('p')).toHaveLength(2);
});
expect(getByText('Mock Description')).toBeInTheDocument();
test('should render kpi icons', () => {
expect(wrapper.find('[data-test-subj="stat-icon"]').filter('EuiIcon')).toHaveLength(2);
});
test('should render barChart', () => {
expect(wrapper.find(BarChart)).toHaveLength(1);
});
test('should render areaChart', () => {
expect(wrapper.find(AreaChart)).toHaveLength(1);
});
test('should render separator', () => {
expect(wrapper.find(EuiHorizontalRule)).toHaveLength(1);
});
});
describe('Toggle query', () => {
test('toggleQuery updates toggleStatus', () => {
wrapper = mount(
<ReduxStoreProvider store={store}>
<StatItemsComponent {...mockStatItemsData} />
</ReduxStoreProvider>
);
wrapper.find('[data-test-subj="query-toggle-stat"]').first().simulate('click');
expect(mockSetToggle).toBeCalledWith(false);
expect(mockSetQuerySkip).toBeCalledWith(true);
});
test('toggleStatus=true, render all', () => {
wrapper = mount(
<ReduxStoreProvider store={store}>
<StatItemsComponent {...mockStatItemsData} />
</ReduxStoreProvider>
);
expect(wrapper.find(`.viz-actions`).exists()).toEqual(true);
expect(wrapper.find('[data-test-subj="stat-title"]').first().exists()).toEqual(true);
});
test('toggleStatus=false, render none', () => {
jest
.spyOn(module, 'useQueryToggle')
.mockImplementation(() => ({ toggleStatus: false, setToggleStatus: mockSetToggle }));
wrapper = mount(
<ReduxStoreProvider store={store}>
<StatItemsComponent {...mockStatItemsData} />
</ReduxStoreProvider>
);
expect(wrapper.find('.viz-actions').first().exists()).toEqual(false);
expect(wrapper.find('[data-test-subj="stat-title"]').first().exists()).toEqual(false);
});
expect(getAllByTestId('visualization-embeddable')).toHaveLength(2);
});
describe('when isChartEmbeddablesEnabled = true', () => {
beforeAll(() => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
jest
.spyOn(module, 'useQueryToggle')
.mockImplementation(() => ({ toggleStatus: true, setToggleStatus: mockSetToggle }));
wrapper = mount(
<ReduxStoreProvider store={store}>
<StatItemsComponent {...mockStatItemsData} />
</ReduxStoreProvider>
);
it('toggles visualizations', () => {
(useToggleStatus as jest.Mock).mockReturnValue({
isToggleExpanded: false,
onToggle: jest.fn(),
});
test('renders Lens Embeddables', () => {
expect(wrapper.find('[data-test-subj="visualization-embeddable"]').length).toEqual(4);
const { getByTestId, getAllByTestId } = render(<StatItemsComponent {...mockProps} />, {
wrapper: TestProviders,
});
const toggleButton = getByTestId('query-toggle-stat');
fireEvent.click(toggleButton);
waitFor(() => {
expect(getAllByTestId('visualization-embeddable')).toHaveLength(0);
});
});
});

View file

@ -5,191 +5,89 @@
* 2.0.
*/
import {
EuiFlexGroup,
EuiPanel,
EuiHorizontalRule,
EuiFlexItem,
EuiLoadingSpinner,
} from '@elastic/eui';
import { EuiFlexGroup, EuiPanel, EuiHorizontalRule } from '@elastic/eui';
import React, { useMemo } from 'react';
import deepEqual from 'fast-deep-equal';
import { AreaChart } from '../../../common/components/charts/areachart';
import { BarChart } from '../../../common/components/charts/barchart';
import { histogramDateTimeFormatter } from '../../../common/components/utils';
import { StatItemHeader } from './stat_item_header';
import { useToggleStatus } from './use_toggle_status';
import type { StatItemsProps } from './types';
import { areachartConfigs, barchartConfigs, FlexItem, ChartHeight } from './utils';
import { Metric } from './metric';
import { FlexItem, ChartHeight } from './utils';
import { MetricEmbeddable } from './metric_embeddable';
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
import { VisualizationEmbeddable } from '../../../common/components/visualization_actions/visualization_embeddable';
export const StatItemsComponent = React.memo<StatItemsProps>(
({
areaChart,
barChart,
export const StatItemsComponent = React.memo<StatItemsProps>(({ statItems, from, id, to }) => {
const timerange = useMemo(
() => ({
from,
to,
}),
[from, to]
);
const {
key,
description,
enableAreaChart,
enableBarChart,
fields,
from,
grow,
id,
loading = false,
index,
updateDateRange,
statKey = 'item',
to,
barChartLensAttributes,
areaChartLensAttributes,
setQuerySkip,
}) => {
const isBarChartDataAvailable = !!(
barChart &&
barChart.length &&
barChart.every((item) => item.value != null && item.value.length > 0)
);
const isAreaChartDataAvailable = !!(
areaChart &&
areaChart.length &&
areaChart.every((item) => item.value != null && item.value.length > 0)
);
} = statItems;
const timerange = useMemo(
() => ({
from,
to,
}),
[from, to]
);
const { isToggleExpanded, onToggle } = useToggleStatus({ id });
const { isToggleExpanded, onToggle } = useToggleStatus({ id, setQuerySkip });
return (
<FlexItem grow={1} data-test-subj={key}>
<EuiPanel hasBorder>
<StatItemHeader
onToggle={onToggle}
isToggleExpanded={isToggleExpanded}
description={description}
/>
const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled');
{isToggleExpanded && (
<>
<MetricEmbeddable
fields={fields}
id={id}
timerange={timerange}
inspectTitle={description}
/>
return (
<FlexItem grow={grow} data-test-subj={`stat-${statKey}`}>
<EuiPanel hasBorder>
<StatItemHeader
onToggle={onToggle}
isToggleExpanded={isToggleExpanded}
description={description}
/>
{loading && (
<EuiFlexGroup justifyContent="center" alignItems="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="l" data-test-subj="loading-spinner" />
</EuiFlexItem>
</EuiFlexGroup>
)}
{isToggleExpanded && !loading && (
<>
{isChartEmbeddablesEnabled ? (
<MetricEmbeddable
fields={fields}
id={id}
timerange={timerange}
inspectTitle={description}
/>
) : (
<Metric
fields={fields}
id={id}
timerange={timerange}
isAreaChartDataAvailable={isAreaChartDataAvailable}
isBarChartDataAvailable={isBarChartDataAvailable}
inspectTitle={description}
inspectIndex={index}
/>
{(enableAreaChart || enableBarChart) && <EuiHorizontalRule />}
<EuiFlexGroup gutterSize="none">
{enableBarChart && (
<FlexItem>
<VisualizationEmbeddable
data-test-subj="embeddable-bar-chart"
lensAttributes={barChartLensAttributes}
timerange={timerange}
id={`${id}-bar-embeddable`}
height={ChartHeight}
inspectTitle={description}
/>
</FlexItem>
)}
{(enableAreaChart || enableBarChart) && <EuiHorizontalRule />}
<EuiFlexGroup gutterSize={isChartEmbeddablesEnabled ? 'none' : 'l'}>
{enableBarChart && (
<FlexItem>
{isChartEmbeddablesEnabled && barChartLensAttributes ? (
<VisualizationEmbeddable
data-test-subj="embeddable-bar-chart"
lensAttributes={barChartLensAttributes}
timerange={timerange}
id={`${id}-bar-embeddable`}
height={ChartHeight}
inspectTitle={description}
/>
) : (
<BarChart
barChart={barChart}
configs={barchartConfigs()}
visualizationActionsOptions={{
lensAttributes: barChartLensAttributes,
queryId: id,
inspectIndex: index,
timerange,
title: description,
}}
/>
)}
</FlexItem>
)}
{enableAreaChart && from != null && to != null && (
<>
<FlexItem>
{isChartEmbeddablesEnabled && areaChartLensAttributes ? (
<VisualizationEmbeddable
data-test-subj="embeddable-area-chart"
lensAttributes={areaChartLensAttributes}
timerange={timerange}
id={`${id}-area-embeddable`}
height={ChartHeight}
inspectTitle={description}
/>
) : (
<AreaChart
areaChart={areaChart}
configs={areachartConfigs({
xTickFormatter: histogramDateTimeFormatter([from, to]),
onBrushEnd: updateDateRange,
})}
visualizationActionsOptions={{
lensAttributes: areaChartLensAttributes,
queryId: id,
inspectIndex: index,
timerange,
title: description,
}}
/>
)}
</FlexItem>
</>
)}
</EuiFlexGroup>
</>
)}
</EuiPanel>
</FlexItem>
);
},
(prevProps, nextProps) =>
prevProps.description === nextProps.description &&
prevProps.enableAreaChart === nextProps.enableAreaChart &&
prevProps.enableBarChart === nextProps.enableBarChart &&
prevProps.from === nextProps.from &&
prevProps.grow === nextProps.grow &&
prevProps.loading === nextProps.loading &&
prevProps.setQuerySkip === nextProps.setQuerySkip &&
prevProps.id === nextProps.id &&
prevProps.index === nextProps.index &&
prevProps.updateDateRange === nextProps.updateDateRange &&
prevProps.statKey === nextProps.statKey &&
prevProps.to === nextProps.to &&
deepEqual(prevProps.areaChart, nextProps.areaChart) &&
deepEqual(prevProps.barChart, nextProps.barChart) &&
deepEqual(prevProps.fields, nextProps.fields)
);
{enableAreaChart && from != null && to != null && (
<>
<FlexItem>
<VisualizationEmbeddable
data-test-subj="embeddable-area-chart"
lensAttributes={areaChartLensAttributes}
timerange={timerange}
id={`${id}-area-embeddable`}
height={ChartHeight}
inspectTitle={description}
/>
</FlexItem>
</>
)}
</EuiFlexGroup>
</>
)}
</EuiPanel>
</FlexItem>
);
});
StatItemsComponent.displayName = 'StatItemsComponent';

View file

@ -5,54 +5,31 @@
* 2.0.
*/
import type { IconType } from '@elastic/eui';
import type {
ChartSeriesConfigs,
ChartSeriesData,
UpdateDateRange,
} from '../../../common/components/charts/common';
import type { LensAttributes } from '../../../common/components/visualization_actions/types';
export interface MetricStatItem {
export interface FieldConfigs {
color?: string;
description?: string;
icon?: IconType;
key: string;
name?: string;
lensAttributes?: LensAttributes;
}
export interface StatItem {
color?: string;
description?: string;
icon?: IconType;
key: string;
name?: string;
value: number | undefined | null;
lensAttributes?: LensAttributes;
}
export interface StatItems {
areachartConfigs?: ChartSeriesConfigs;
barchartConfigs?: ChartSeriesConfigs;
description?: string;
enableAreaChart?: boolean;
enableBarChart?: boolean;
fields: StatItem[];
grow?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | true | false | null;
index?: number;
fields: FieldConfigs[];
key: string;
statKey?: string;
barChartLensAttributes?: LensAttributes;
areaChartLensAttributes?: LensAttributes;
}
export interface StatItemsProps extends StatItems {
areaChart?: ChartSeriesData[];
barChart?: ChartSeriesData[];
export interface StatItemsProps {
from: string;
id: string;
updateDateRange: UpdateDateRange;
statItems: StatItems;
to: string;
loading: boolean;
setQuerySkip: (skip: boolean) => void;
}

View file

@ -1,90 +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 { mount } from 'enzyme';
import React from 'react';
import type { StatItemsProps, StatItems } from '.';
import { fieldsMapping as fieldTitleChartMapping } from '../../network/components/kpi_network/unique_private_ips';
import {
mockData,
mockEnableChartsData,
mockNoChartMappings,
mockUpdateDateRange,
} from '../../network/components/kpi_network/mock';
import type {
HostsKpiStrategyResponse,
NetworkKpiStrategyResponse,
} from '../../../../common/search_strategy';
import { useKpiMatrixStatus } from './use_kpi_matrix_status';
const mockSetQuerySkip = jest.fn();
const from = '2019-06-15T06:00:00.000Z';
const to = '2019-06-18T06:00:00.000Z';
describe('useKpiMatrixStatus', () => {
const mockNetworkMappings = fieldTitleChartMapping;
const MockChildComponent = (mappedStatItemProps: StatItemsProps) => <span />;
const MockHookWrapperComponent = ({
fieldsMapping,
data,
}: {
fieldsMapping: Readonly<StatItems[]>;
data: NetworkKpiStrategyResponse | HostsKpiStrategyResponse;
}) => {
const statItemsProps: StatItemsProps[] = useKpiMatrixStatus(
fieldsMapping,
data,
'statItem',
from,
to,
mockUpdateDateRange,
mockSetQuerySkip,
false
);
return (
<div>
{statItemsProps.map((mappedStatItemProps) => {
return <MockChildComponent {...mappedStatItemProps} />;
})}
</div>
);
};
test('it updates status correctly', () => {
const wrapper = mount(
<>
<MockHookWrapperComponent fieldsMapping={mockNetworkMappings} data={mockData} />
</>
);
const result = { ...wrapper.find('MockChildComponent').get(0).props };
const { setQuerySkip, ...restResult } = result;
const { setQuerySkip: a, ...restExpect } = mockEnableChartsData;
expect(restResult).toEqual(restExpect);
});
test('it should not append areaChart if enableAreaChart is off', () => {
const wrapper = mount(
<>
<MockHookWrapperComponent fieldsMapping={mockNoChartMappings} data={mockData} />
</>
);
expect(wrapper.find('MockChildComponent').get(0).props.areaChart).toBeUndefined();
});
test('it should not append barChart if enableBarChart is off', () => {
const wrapper = mount(
<>
<MockHookWrapperComponent fieldsMapping={mockNoChartMappings} data={mockData} />
</>
);
expect(wrapper.find('MockChildComponent').get(0).props.barChart).toBeUndefined();
});
});

View file

@ -1,39 +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 type {
HostsKpiStrategyResponse,
NetworkKpiStrategyResponse,
UsersKpiStrategyResponse,
} from '../../../../common/search_strategy';
import type { UpdateDateRange } from '../../../common/components/charts/common';
import type { StatItems, StatItemsProps } from './types';
import { addValueToAreaChart, addValueToBarChart, addValueToFields } from './utils';
export const useKpiMatrixStatus = (
mappings: Readonly<StatItems[]>,
data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse | UsersKpiStrategyResponse,
id: string,
from: string,
to: string,
updateDateRange: UpdateDateRange,
setQuerySkip: (skip: boolean) => void,
loading: boolean
): StatItemsProps[] =>
mappings.map((stat) => ({
...stat,
areaChart: stat.enableAreaChart ? addValueToAreaChart(stat.fields, data) : undefined,
barChart: stat.enableBarChart ? addValueToBarChart(stat.fields, data) : undefined,
fields: addValueToFields(stat.fields, data),
id,
key: `kpi-summary-${stat.key}`,
statKey: `${stat.key}`,
from,
to,
updateDateRange,
setQuerySkip,
loading,
}));

View file

@ -7,24 +7,13 @@
import { useCallback } from 'react';
import { useQueryToggle } from '../../../common/containers/query_toggle';
export const useToggleStatus = ({
id,
setQuerySkip,
}: {
id: string;
setQuerySkip: (skip: boolean) => void;
}) => {
export const useToggleStatus = ({ id }: { id: string }) => {
const { toggleStatus, setToggleStatus } = useQueryToggle(id);
const toggleQuery = useCallback(
(status: boolean) => {
setToggleStatus(status);
// toggleStatus on = skipQuery false
setQuerySkip(!status);
},
[setQuerySkip, setToggleStatus]
const onToggle = useCallback(
() => setToggleStatus(!toggleStatus),
[setToggleStatus, toggleStatus]
);
const onToggle = useCallback(() => toggleQuery(!toggleStatus), [toggleQuery, toggleStatus]);
return {
isToggleExpanded: toggleStatus,

View file

@ -1,35 +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 { addValueToFields, addValueToAreaChart, addValueToBarChart } from './utils';
import { fieldsMapping as fieldTitleChartMapping } from '../../network/components/kpi_network/unique_private_ips';
import { mockData, mockEnableChartsData } from '../../network/components/kpi_network/mock';
describe('addValueToFields', () => {
const mockNetworkMappings = fieldTitleChartMapping[0];
test('should update value from data', () => {
const result = addValueToFields(mockNetworkMappings.fields, mockData);
expect(result).toEqual(mockEnableChartsData.fields);
});
});
describe('addValueToAreaChart', () => {
const mockNetworkMappings = fieldTitleChartMapping[0];
test('should add areaChart from data', () => {
const result = addValueToAreaChart(mockNetworkMappings.fields, mockData);
expect(result).toEqual(mockEnableChartsData.areaChart);
});
});
describe('addValueToBarChart', () => {
const mockNetworkMappings = fieldTitleChartMapping[0];
test('should add areaChart from data', () => {
const result = addValueToBarChart(mockNetworkMappings.fields, mockData);
expect(result).toEqual(mockEnableChartsData.barChart);
});
});

View file

@ -5,20 +5,9 @@
* 2.0.
*/
import type { BrushEndListener, ElementClickListener, Rotation } from '@elastic/charts';
import { ScaleType } from '@elastic/charts';
import styled from 'styled-components';
import { get, getOr } from 'lodash/fp';
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import type {
HostsKpiStrategyResponse,
NetworkKpiStrategyResponse,
UsersKpiStrategyResponse,
} from '../../../../common/search_strategy';
import type { ChartSeriesData, ChartData } from '../../../common/components/charts/common';
import type { StatItem } from './types';
export const ChartHeight = 120;
@ -54,87 +43,3 @@ StatValue.displayName = 'StatValue';
export const StyledTitle = styled.h6`
line-height: 200%;
`;
export const numberFormatter = (value: string | number): string => value.toLocaleString();
export const statItemBarchartRotation: Rotation = 90;
export const statItemChartCustomHeight = 74;
export const areachartConfigs = (config?: {
xTickFormatter: (value: number) => string;
onBrushEnd?: BrushEndListener;
}) => ({
series: {
xScaleType: ScaleType.Time,
yScaleType: ScaleType.Linear,
},
axis: {
xTickFormatter: get('xTickFormatter', config),
yTickFormatter: numberFormatter,
},
settings: {
onBrushEnd: getOr(() => {}, 'onBrushEnd', config),
},
customHeight: statItemChartCustomHeight,
});
export const barchartConfigs = (config?: { onElementClick?: ElementClickListener }) => ({
series: {
xScaleType: ScaleType.Ordinal,
yScaleType: ScaleType.Linear,
stackAccessors: ['y0'],
},
axis: {
xTickFormatter: numberFormatter,
},
settings: {
onElementClick: getOr(() => {}, 'onElementClick', config),
rotation: statItemBarchartRotation,
},
customHeight: statItemChartCustomHeight,
});
export const addValueToFields = (
fields: StatItem[],
data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse | UsersKpiStrategyResponse
): StatItem[] => fields.map((field) => ({ ...field, value: get(field.key, data) }));
export const addValueToAreaChart = (
fields: StatItem[],
data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse | UsersKpiStrategyResponse
): ChartSeriesData[] =>
fields
.filter((field) => get(`${field.key}Histogram`, data) != null)
.map(({ lensAttributes, ...field }) => ({
...field,
value: get(`${field.key}Histogram`, data),
key: `${field.key}Histogram`,
}));
export const addValueToBarChart = (
fields: StatItem[],
data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse | UsersKpiStrategyResponse
): ChartSeriesData[] => {
if (fields.length === 0) return [];
return fields.reduce((acc: ChartSeriesData[], field: StatItem, idx: number) => {
const { key, color } = field;
const y: number | null = getOr(null, key, data);
const x: string = get(`${idx}.name`, fields) || getOr('', `${idx}.description`, fields);
const value: [ChartData] = [
{
x,
y,
g: key,
y0: 0,
},
];
return [
...acc,
{
key,
color,
value,
},
];
}, []);
};

View file

@ -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 { EuiFlexGroup } from '@elastic/eui';
import styled from 'styled-components';
import deepEqual from 'fast-deep-equal';
import { manageQuery } from '../../../../../common/components/page/manage_query';
import type {
HostsKpiStrategyResponse,
NetworkKpiStrategyResponse,
} from '../../../../../../common/search_strategy';
import type { StatItemsProps, StatItems } from '../../../../components/stat_items';
import { StatItemsComponent, useKpiMatrixStatus } from '../../../../components/stat_items';
import type { UpdateDateRange } from '../../../../../common/components/charts/common';
import type { UsersKpiStrategyResponse } from '../../../../../../common/search_strategy/security_solution/users';
const kpiWidgetHeight = 247;
export const FlexGroup = styled(EuiFlexGroup)`
min-height: ${kpiWidgetHeight}px;
`;
FlexGroup.displayName = 'FlexGroup';
interface KpiBaseComponentProps {
fieldsMapping: Readonly<StatItems[]>;
data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse | UsersKpiStrategyResponse;
loading?: boolean;
id: string;
from: string;
to: string;
updateDateRange: UpdateDateRange;
setQuerySkip: (skip: boolean) => void;
}
export const KpiBaseComponent = React.memo<KpiBaseComponentProps>(
({ fieldsMapping, data, id, loading = false, from, to, updateDateRange, setQuerySkip }) => {
const statItemsProps: StatItemsProps[] = useKpiMatrixStatus(
fieldsMapping,
data,
id,
from,
to,
updateDateRange,
setQuerySkip,
loading
);
return (
<EuiFlexGroup wrap>
{statItemsProps.map((mappedStatItemProps) => (
<StatItemsComponent {...mappedStatItemProps} />
))}
</EuiFlexGroup>
);
},
(prevProps, nextProps) =>
prevProps.fieldsMapping === nextProps.fieldsMapping &&
prevProps.id === nextProps.id &&
prevProps.loading === nextProps.loading &&
prevProps.from === nextProps.from &&
prevProps.to === nextProps.to &&
prevProps.updateDateRange === nextProps.updateDateRange &&
deepEqual(prevProps.data, nextProps.data)
);
KpiBaseComponent.displayName = 'KpiBaseComponent';
export const KpiBaseComponentManage = manageQuery(KpiBaseComponent);

View file

@ -5,107 +5,29 @@
* 2.0.
*/
import { useHostsKpiHosts } from '../../../containers/kpi_hosts/hosts';
import { useQueryToggle } from '../../../../../common/containers/query_toggle';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../../../common/mock';
import React from 'react';
import { HostsKpiHosts } from '.';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { useRefetchByRestartingSession } from '../../../../../common/components/page/use_refetch_by_session';
import { KpiBaseComponentManage } from '../common';
import { hostsStatItems, HostsKpiHosts } from '.';
import { KpiBaseComponent } from '../../../../components/kpi';
jest.mock('../../../../../common/containers/query_toggle');
jest.mock('../../../containers/kpi_hosts/hosts');
jest.mock('../common', () => ({
KpiBaseComponentManage: jest
.fn()
.mockReturnValue(<span data-test-subj="KpiBaseComponentManage" />),
}));
jest.mock('../../../../../common/hooks/use_experimental_features', () => ({
useIsExperimentalFeatureEnabled: jest.fn(),
}));
jest.mock('../../../../../common/components/page/use_refetch_by_session', () => ({
useRefetchByRestartingSession: jest.fn(),
}));
jest.mock('../../../../components/kpi');
describe('Hosts KPI', () => {
const from = new Date('2023-12-30').toISOString();
const to = new Date('2023-12-31').toISOString();
const MockKpiBaseComponent = KpiBaseComponent as unknown as jest.Mock;
describe('KPI Hosts', () => {
const mockUseHostsKpiHosts = useHostsKpiHosts as jest.Mock;
const mockUseQueryToggle = useQueryToggle as jest.Mock;
const MockKpiBaseComponentManage = KpiBaseComponentManage as jest.Mock;
const mockRefetchByRestartingSession = jest.fn();
const mockRefetch = jest.fn();
const mockSession = { current: { start: jest.fn(() => 'mockNewSearchSessionId') } };
const defaultProps = {
from: '2019-06-25T04:31:59.345Z',
to: '2019-06-25T06:31:59.345Z',
indexNames: [],
updateDateRange: jest.fn(),
setQuery: jest.fn(),
skip: false,
};
beforeEach(() => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() });
mockUseHostsKpiHosts.mockReturnValue([
false,
{
id: '123',
inspect: {
dsl: [],
response: [],
},
refetch: mockRefetch,
},
]);
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false);
(useRefetchByRestartingSession as jest.Mock).mockReturnValue({
session: mockSession,
searchSessionId: 'mockSearchSessionId',
refetchByRestartingSession: mockRefetchByRestartingSession,
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('toggleStatus=true, do not skip', () => {
render(
<TestProviders>
<HostsKpiHosts {...defaultProps} />
</TestProviders>
);
expect(mockUseHostsKpiHosts.mock.calls[0][0].skip).toEqual(false);
});
it('toggleStatus=false, skip', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() });
render(
<TestProviders>
<HostsKpiHosts {...defaultProps} />
</TestProviders>
);
expect(mockUseHostsKpiHosts.mock.calls[0][0].skip).toEqual(true);
});
it('Refetches data', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() });
render(
<TestProviders>
<HostsKpiHosts {...defaultProps} />
</TestProviders>
);
expect(MockKpiBaseComponentManage.mock.calls[0][0].refetch).toEqual(mockRefetch);
expect(MockKpiBaseComponentManage.mock.calls[0][0].session).toBeUndefined();
});
it('Refetch by restarting search session ID if isChartEmbeddablesEnabled = true', () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true);
render(
<TestProviders>
<HostsKpiHosts {...defaultProps} />
</TestProviders>
);
expect(MockKpiBaseComponentManage.mock.calls[0][0].refetch).toEqual(
mockRefetchByRestartingSession
);
expect(MockKpiBaseComponentManage.mock.calls[0][0].session).toEqual(mockSession);
it('renders correctly', () => {
render(<HostsKpiHosts from={from} to={to} />, {
wrapper: TestProviders,
});
expect(MockKpiBaseComponent.mock.calls[0][0].statItems).toEqual(hostsStatItems);
expect(MockKpiBaseComponent.mock.calls[0][0].from).toEqual(from);
expect(MockKpiBaseComponent.mock.calls[0][0].to).toEqual(to);
});
});

View file

@ -5,28 +5,24 @@
* 2.0.
*/
import React, { useEffect, useState } from 'react';
import React from 'react';
import type { StatItems } from '../../../../components/stat_items';
import { kpiHostAreaLensAttributes } from '../../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_host_area';
import { kpiHostMetricLensAttributes } from '../../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_host_metric';
import { useHostsKpiHosts, ID } from '../../../containers/kpi_hosts/hosts';
import { KpiBaseComponentManage } from '../common';
import { KpiBaseComponent } from '../../../../components/kpi';
import type { HostsKpiProps } from '../types';
import { HostsKpiChartColors } from '../types';
import * as i18n from './translations';
import { useQueryToggle } from '../../../../../common/containers/query_toggle';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { InputsModelId } from '../../../../../common/store/inputs/constants';
import { useRefetchByRestartingSession } from '../../../../../common/components/page/use_refetch_by_session';
export const fieldsMapping: Readonly<StatItems[]> = [
export const ID = 'hostsKpiHostsQuery';
export const hostsStatItems: Readonly<StatItems[]> = [
{
key: 'hosts',
fields: [
{
key: 'hosts',
value: null,
color: HostsKpiChartColors.hosts,
icon: 'storage',
lensAttributes: kpiHostMetricLensAttributes,
@ -38,52 +34,8 @@ export const fieldsMapping: Readonly<StatItems[]> = [
},
];
const HostsKpiHostsComponent: React.FC<HostsKpiProps> = ({
filterQuery,
from,
indexNames,
to,
updateDateRange,
setQuery,
skip,
}) => {
const { toggleStatus } = useQueryToggle(ID);
const [querySkip, setQuerySkip] = useState(skip || !toggleStatus);
const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled');
useEffect(() => {
setQuerySkip(skip || !toggleStatus);
}, [skip, toggleStatus]);
const [loading, { refetch, id, inspect, ...data }] = useHostsKpiHosts({
filterQuery,
endDate: to,
indexNames,
startDate: from,
skip: querySkip || isChartEmbeddablesEnabled,
});
const { session, refetchByRestartingSession } = useRefetchByRestartingSession({
inputId: InputsModelId.global,
queryId: id,
});
return (
<KpiBaseComponentManage
data={data}
id={id}
inspect={inspect}
loading={loading}
fieldsMapping={fieldsMapping}
from={from}
to={to}
updateDateRange={updateDateRange}
refetch={isChartEmbeddablesEnabled ? refetchByRestartingSession : refetch}
setQuery={setQuery}
setQuerySkip={setQuerySkip}
session={isChartEmbeddablesEnabled ? session : undefined}
/>
);
const HostsKpiHostsComponent: React.FC<HostsKpiProps> = ({ from, to }) => {
return <KpiBaseComponent id={ID} statItems={hostsStatItems} from={from} to={to} />;
};
export const HostsKpiHosts = React.memo(HostsKpiHostsComponent);

View file

@ -12,55 +12,15 @@ import { HostsKpiHosts } from './hosts';
import { HostsKpiUniqueIps } from './unique_ips';
import type { HostsKpiProps } from './types';
export const HostsKpiComponent = React.memo<HostsKpiProps>(
({ filterQuery, from, indexNames, to, setQuery, skip, updateDateRange }) => (
<EuiFlexGroup wrap>
<EuiFlexItem grow={1}>
<HostsKpiHosts
filterQuery={filterQuery}
from={from}
indexNames={indexNames}
to={to}
updateDateRange={updateDateRange}
setQuery={setQuery}
skip={skip}
/>
</EuiFlexItem>
<EuiFlexItem grow={2}>
<HostsKpiUniqueIps
filterQuery={filterQuery}
from={from}
indexNames={indexNames}
to={to}
updateDateRange={updateDateRange}
setQuery={setQuery}
skip={skip}
/>
</EuiFlexItem>
</EuiFlexGroup>
)
);
export const HostsKpiComponent = React.memo<HostsKpiProps>(({ from, to }) => (
<EuiFlexGroup wrap>
<EuiFlexItem grow={1}>
<HostsKpiHosts from={from} to={to} />
</EuiFlexItem>
<EuiFlexItem grow={2}>
<HostsKpiUniqueIps from={from} to={to} />
</EuiFlexItem>
</EuiFlexGroup>
));
HostsKpiComponent.displayName = 'HostsKpiComponent';
export const HostsDetailsKpiComponent = React.memo<HostsKpiProps>(
({ filterQuery, from, indexNames, to, setQuery, skip, updateDateRange }) => {
return (
<EuiFlexGroup wrap>
<EuiFlexItem grow={1}>
<HostsKpiUniqueIps
filterQuery={filterQuery}
from={from}
indexNames={indexNames}
to={to}
updateDateRange={updateDateRange}
setQuery={setQuery}
skip={skip}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
);
HostsDetailsKpiComponent.displayName = 'HostsDetailsKpiComponent';

View file

@ -5,17 +5,9 @@
* 2.0.
*/
import type { UpdateDateRange } from '../../../../common/components/charts/common';
import type { GlobalTimeArgs } from '../../../../common/containers/use_global_time';
export interface HostsKpiProps {
filterQuery?: string;
from: string;
to: string;
indexNames: string[];
updateDateRange: UpdateDateRange;
setQuery: GlobalTimeArgs['setQuery'];
skip: boolean;
}
export enum HostsKpiChartColors {

View file

@ -5,107 +5,29 @@
* 2.0.
*/
import { useHostsKpiUniqueIps } from '../../../containers/kpi_hosts/unique_ips';
import { useQueryToggle } from '../../../../../common/containers/query_toggle';
import { render } from '@testing-library/react';
import { TestProviders } from '../../../../../common/mock';
import React from 'react';
import { HostsKpiUniqueIps } from '.';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { useRefetchByRestartingSession } from '../../../../../common/components/page/use_refetch_by_session';
import { KpiBaseComponentManage } from '../common';
import { uniqueIpsStatItems, HostsKpiUniqueIps } from '.';
import { KpiBaseComponent } from '../../../../components/kpi';
jest.mock('../../../../../common/containers/query_toggle');
jest.mock('../../../containers/kpi_hosts/unique_ips');
jest.mock('../common', () => ({
KpiBaseComponentManage: jest
.fn()
.mockReturnValue(<span data-test-subj="KpiBaseComponentManage" />),
}));
jest.mock('../../../../../common/hooks/use_experimental_features', () => ({
useIsExperimentalFeatureEnabled: jest.fn(),
}));
jest.mock('../../../../../common/components/page/use_refetch_by_session', () => ({
useRefetchByRestartingSession: jest.fn(),
}));
jest.mock('../../../../components/kpi');
describe('Hosts KPI', () => {
const from = new Date('2023-12-30').toISOString();
const to = new Date('2023-12-31').toISOString();
const MockKpiBaseComponent = KpiBaseComponent as unknown as jest.Mock;
describe('KPI Unique IPs', () => {
const mockUseHostsKpiUniqueIps = useHostsKpiUniqueIps as jest.Mock;
const mockUseQueryToggle = useQueryToggle as jest.Mock;
const MockKpiBaseComponentManage = KpiBaseComponentManage as jest.Mock;
const mockRefetchByRestartingSession = jest.fn();
const mockSession = { current: { start: jest.fn(() => 'mockNewSearchSessionId') } };
const mockRefetch = jest.fn();
const defaultProps = {
from: '2019-06-25T04:31:59.345Z',
to: '2019-06-25T06:31:59.345Z',
indexNames: [],
updateDateRange: jest.fn(),
setQuery: jest.fn(),
skip: false,
};
beforeEach(() => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() });
mockUseHostsKpiUniqueIps.mockReturnValue([
false,
{
id: '123',
inspect: {
dsl: [],
response: [],
},
refetch: mockRefetch,
},
]);
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false);
(useRefetchByRestartingSession as jest.Mock).mockReturnValue({
session: mockSession,
searchSessionId: 'mockSearchSessionId',
refetchByRestartingSession: mockRefetchByRestartingSession,
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('toggleStatus=true, do not skip', () => {
render(
<TestProviders>
<HostsKpiUniqueIps {...defaultProps} />
</TestProviders>
);
expect(mockUseHostsKpiUniqueIps.mock.calls[0][0].skip).toEqual(false);
});
it('toggleStatus=false, skip', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() });
render(
<TestProviders>
<HostsKpiUniqueIps {...defaultProps} />
</TestProviders>
);
expect(mockUseHostsKpiUniqueIps.mock.calls[0][0].skip).toEqual(true);
});
it('Refetches data', () => {
mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() });
render(
<TestProviders>
<HostsKpiUniqueIps {...defaultProps} />
</TestProviders>
);
expect(MockKpiBaseComponentManage.mock.calls[0][0].refetch).toEqual(mockRefetch);
expect(MockKpiBaseComponentManage.mock.calls[0][0].session).toBeUndefined();
});
it('Refetch by restarting search session ID if isChartEmbeddablesEnabled = true', () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(true);
render(
<TestProviders>
<HostsKpiUniqueIps {...defaultProps} />
</TestProviders>
);
expect(MockKpiBaseComponentManage.mock.calls[0][0].refetch).toEqual(
mockRefetchByRestartingSession
);
expect(MockKpiBaseComponentManage.mock.calls[0][0].session).toEqual(mockSession);
it('renders correctly', () => {
render(<HostsKpiUniqueIps from={from} to={to} />, {
wrapper: TestProviders,
});
expect(MockKpiBaseComponent.mock.calls[0][0].statItems).toEqual(uniqueIpsStatItems);
expect(MockKpiBaseComponent.mock.calls[0][0].from).toEqual(from);
expect(MockKpiBaseComponent.mock.calls[0][0].to).toEqual(to);
});
});

View file

@ -5,24 +5,21 @@
* 2.0.
*/
import React, { useEffect, useState } from 'react';
import React from 'react';
import type { StatItems } from '../../../../components/stat_items';
import { kpiUniqueIpsAreaLensAttributes } from '../../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_area';
import { kpiUniqueIpsBarLensAttributes } from '../../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_bar';
import { kpiUniqueIpsDestinationMetricLensAttributes } from '../../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_destination_metric';
import { kpiUniqueIpsSourceMetricLensAttributes } from '../../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_source_metric';
import { useHostsKpiUniqueIps, ID } from '../../../containers/kpi_hosts/unique_ips';
import { KpiBaseComponentManage } from '../common';
import { KpiBaseComponent } from '../../../../components/kpi';
import type { HostsKpiProps } from '../types';
import { HostsKpiChartColors } from '../types';
import * as i18n from './translations';
import { useQueryToggle } from '../../../../../common/containers/query_toggle';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { InputsModelId } from '../../../../../common/store/inputs/constants';
import { useRefetchByRestartingSession } from '../../../../../common/components/page/use_refetch_by_session';
export const fieldsMapping: Readonly<StatItems[]> = [
export const ID = 'hostsKpiUniqueIpsQuery';
export const uniqueIpsStatItems: Readonly<StatItems[]> = [
{
key: 'uniqueIps',
fields: [
@ -30,7 +27,6 @@ export const fieldsMapping: Readonly<StatItems[]> = [
key: 'uniqueSourceIps',
name: i18n.SOURCE_CHART_LABEL,
description: i18n.SOURCE_UNIT_LABEL,
value: null,
color: HostsKpiChartColors.uniqueSourceIps,
icon: 'visMapCoordinate',
lensAttributes: kpiUniqueIpsSourceMetricLensAttributes,
@ -39,7 +35,6 @@ export const fieldsMapping: Readonly<StatItems[]> = [
key: 'uniqueDestinationIps',
name: i18n.DESTINATION_CHART_LABEL,
description: i18n.DESTINATION_UNIT_LABEL,
value: null,
color: HostsKpiChartColors.uniqueDestinationIps,
icon: 'visMapCoordinate',
lensAttributes: kpiUniqueIpsDestinationMetricLensAttributes,
@ -53,52 +48,8 @@ export const fieldsMapping: Readonly<StatItems[]> = [
},
];
const HostsKpiUniqueIpsComponent: React.FC<HostsKpiProps> = ({
filterQuery,
from,
indexNames,
to,
updateDateRange,
setQuery,
skip,
}) => {
const { toggleStatus } = useQueryToggle(ID);
const [querySkip, setQuerySkip] = useState(skip || !toggleStatus);
const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled');
useEffect(() => {
setQuerySkip(skip || !toggleStatus);
}, [skip, toggleStatus]);
const [loading, { refetch, id, inspect, ...data }] = useHostsKpiUniqueIps({
filterQuery,
endDate: to,
indexNames,
startDate: from,
skip: querySkip || isChartEmbeddablesEnabled,
});
const { session, refetchByRestartingSession } = useRefetchByRestartingSession({
inputId: InputsModelId.global,
queryId: id,
});
return (
<KpiBaseComponentManage
data={data}
id={id}
inspect={inspect}
loading={loading}
fieldsMapping={fieldsMapping}
from={from}
to={to}
updateDateRange={updateDateRange}
refetch={isChartEmbeddablesEnabled ? refetchByRestartingSession : refetch}
setQuery={setQuery}
setQuerySkip={setQuerySkip}
session={isChartEmbeddablesEnabled ? session : undefined}
/>
);
const HostsKpiUniqueIpsComponent: React.FC<HostsKpiProps> = ({ from, to }) => {
return <KpiBaseComponent from={from} id={ID} statItems={uniqueIpsStatItems} to={to} />;
};
export const HostsKpiUniqueIps = React.memo(HostsKpiUniqueIpsComponent);

View file

@ -1,28 +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 { act, renderHook } from '@testing-library/react-hooks';
import { TestProviders } from '../../../../../common/mock';
import { useHostsKpiHosts } from '.';
describe('kpi hosts - hosts', () => {
it('skip = true will cancel any running request', () => {
const abortSpy = jest.spyOn(AbortController.prototype, 'abort');
const localProps = {
startDate: '2020-07-07T08:20:18.966Z',
endDate: '2020-07-08T08:20:18.966Z',
indexNames: ['cool'],
skip: false,
};
const { rerender } = renderHook(() => useHostsKpiHosts(localProps), {
wrapper: TestProviders,
});
localProps.skip = true;
act(() => rerender());
expect(abortSpy).toHaveBeenCalledTimes(4);
});
});

View file

@ -1,157 +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 deepEqual from 'fast-deep-equal';
import { noop } from 'lodash/fp';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Subscription } from 'rxjs';
import type { KpiHostsRequestOptionsInput } from '../../../../../../common/api/search_strategy';
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
import type { inputsModel } from '../../../../../common/store';
import { createFilter } from '../../../../../common/containers/helpers';
import { useKibana } from '../../../../../common/lib/kibana';
import type { HostsKpiHostsStrategyResponse } from '../../../../../../common/search_strategy';
import { HostsKpiQueries } from '../../../../../../common/search_strategy';
import type { ESTermQuery } from '../../../../../../common/typed_json';
import * as i18n from './translations';
import { getInspectResponse } from '../../../../../helpers';
import type { InspectResponse } from '../../../../../types';
export const ID = 'hostsKpiHostsQuery';
export interface HostsKpiHostsArgs extends Omit<HostsKpiHostsStrategyResponse, 'rawResponse'> {
id: string;
inspect: InspectResponse;
isInspected: boolean;
refetch: inputsModel.Refetch;
}
interface UseHostsKpiHosts {
filterQuery?: ESTermQuery | string;
endDate: string;
indexNames: string[];
skip?: boolean;
startDate: string;
}
export const useHostsKpiHosts = ({
filterQuery,
endDate,
indexNames,
skip = false,
startDate,
}: UseHostsKpiHosts): [boolean, HostsKpiHostsArgs] => {
const { data } = useKibana().services;
const refetch = useRef<inputsModel.Refetch>(noop);
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef(new Subscription());
const [loading, setLoading] = useState(false);
const [hostsKpiHostsRequest, setHostsKpiHostsRequest] =
useState<KpiHostsRequestOptionsInput | null>(null);
const [hostsKpiHostsResponse, setHostsKpiHostsResponse] = useState<HostsKpiHostsArgs>({
hosts: 0,
hostsHistogram: [],
id: ID,
inspect: {
dsl: [],
response: [],
},
isInspected: false,
refetch: refetch.current,
});
const { addError, addWarning } = useAppToasts();
const hostsKpiHostsSearch = useCallback(
(request: KpiHostsRequestOptionsInput | null) => {
if (request == null || skip) {
return;
}
const asyncSearch = async () => {
abortCtrl.current = new AbortController();
setLoading(true);
searchSubscription$.current = data.search
.search<KpiHostsRequestOptionsInput, HostsKpiHostsStrategyResponse>(request, {
strategy: 'securitySolutionSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
next: (response) => {
if (!response.isPartial && !response.isRunning) {
setLoading(false);
setHostsKpiHostsResponse((prevResponse) => ({
...prevResponse,
hosts: response.hosts,
hostsHistogram: response.hostsHistogram,
inspect: getInspectResponse(response, prevResponse.inspect),
refetch: refetch.current,
}));
searchSubscription$.current.unsubscribe();
} else if (response.isPartial && !response.isRunning) {
setLoading(false);
addWarning(i18n.ERROR_HOSTS_KPI_HOSTS);
searchSubscription$.current.unsubscribe();
}
},
error: (msg) => {
setLoading(false);
addError(msg, {
title: i18n.FAIL_HOSTS_KPI_HOSTS,
});
searchSubscription$.current.unsubscribe();
},
});
};
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
asyncSearch();
refetch.current = asyncSearch;
},
[data.search, addError, addWarning, skip]
);
useEffect(() => {
setHostsKpiHostsRequest((prevRequest) => {
const myRequest: KpiHostsRequestOptionsInput = {
...(prevRequest ?? {}),
defaultIndex: indexNames,
factoryQueryType: HostsKpiQueries.kpiHosts,
filterQuery: createFilter(filterQuery),
timerange: {
interval: '12h',
from: startDate,
to: endDate,
},
};
if (!deepEqual(prevRequest, myRequest)) {
return myRequest;
}
return prevRequest;
});
}, [indexNames, endDate, filterQuery, startDate]);
useEffect(() => {
hostsKpiHostsSearch(hostsKpiHostsRequest);
return () => {
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
};
}, [hostsKpiHostsRequest, hostsKpiHostsSearch]);
useEffect(() => {
if (skip) {
setLoading(false);
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
}
}, [skip]);
return [loading, hostsKpiHostsResponse];
};

View file

@ -1,22 +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 { i18n } from '@kbn/i18n';
export const ERROR_HOSTS_KPI_HOSTS = i18n.translate(
'xpack.securitySolution.hostsKpiHosts.errorSearchDescription',
{
defaultMessage: `An error has occurred on hosts kpi hosts search`,
}
);
export const FAIL_HOSTS_KPI_HOSTS = i18n.translate(
'xpack.securitySolution.hostsKpiHosts.failSearchDescription',
{
defaultMessage: `Failed to run search on hosts kpi hosts`,
}
);

View file

@ -1,28 +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 { act, renderHook } from '@testing-library/react-hooks';
import { TestProviders } from '../../../../../common/mock';
import { useHostsKpiUniqueIps } from '.';
describe('kpi hosts - Unique Ips', () => {
it('skip = true will cancel any running request', () => {
const abortSpy = jest.spyOn(AbortController.prototype, 'abort');
const localProps = {
startDate: '2020-07-07T08:20:18.966Z',
endDate: '2020-07-08T08:20:18.966Z',
indexNames: ['cool'],
skip: false,
};
const { rerender } = renderHook(() => useHostsKpiUniqueIps(localProps), {
wrapper: TestProviders,
});
localProps.skip = true;
act(() => rerender());
expect(abortSpy).toHaveBeenCalledTimes(4);
});
});

View file

@ -1,165 +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 deepEqual from 'fast-deep-equal';
import { noop } from 'lodash/fp';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Subscription } from 'rxjs';
import type { KpiUniqueIpsRequestOptionsInput } from '../../../../../../common/api/search_strategy';
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
import type { inputsModel } from '../../../../../common/store';
import { createFilter } from '../../../../../common/containers/helpers';
import { useKibana } from '../../../../../common/lib/kibana';
import type { HostsKpiUniqueIpsStrategyResponse } from '../../../../../../common/search_strategy';
import { HostsKpiQueries } from '../../../../../../common/search_strategy';
import type { ESTermQuery } from '../../../../../../common/typed_json';
import * as i18n from './translations';
import { getInspectResponse } from '../../../../../helpers';
import type { InspectResponse } from '../../../../../types';
export const ID = 'hostsKpiUniqueIpsQuery';
export interface HostsKpiUniqueIpsArgs
extends Omit<HostsKpiUniqueIpsStrategyResponse, 'rawResponse'> {
id: string;
inspect: InspectResponse;
isInspected: boolean;
refetch: inputsModel.Refetch;
}
interface UseHostsKpiUniqueIps {
filterQuery?: ESTermQuery | string;
endDate: string;
indexNames: string[];
skip?: boolean;
startDate: string;
}
export const useHostsKpiUniqueIps = ({
filterQuery,
endDate,
indexNames,
skip = false,
startDate,
}: UseHostsKpiUniqueIps): [boolean, HostsKpiUniqueIpsArgs] => {
const { data } = useKibana().services;
const refetch = useRef<inputsModel.Refetch>(noop);
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef(new Subscription());
const [loading, setLoading] = useState(false);
const [hostsKpiUniqueIpsRequest, setHostsKpiUniqueIpsRequest] =
useState<KpiUniqueIpsRequestOptionsInput | null>(null);
const [hostsKpiUniqueIpsResponse, setHostsKpiUniqueIpsResponse] = useState<HostsKpiUniqueIpsArgs>(
{
uniqueSourceIps: 0,
uniqueSourceIpsHistogram: [],
uniqueDestinationIps: 0,
uniqueDestinationIpsHistogram: [],
id: ID,
inspect: {
dsl: [],
response: [],
},
isInspected: false,
refetch: refetch.current,
}
);
const { addError, addWarning } = useAppToasts();
const hostsKpiUniqueIpsSearch = useCallback(
(request: KpiUniqueIpsRequestOptionsInput | null) => {
if (request == null || skip) {
return;
}
const asyncSearch = async () => {
abortCtrl.current = new AbortController();
setLoading(true);
searchSubscription$.current = data.search
.search<KpiUniqueIpsRequestOptionsInput, HostsKpiUniqueIpsStrategyResponse>(request, {
strategy: 'securitySolutionSearchStrategy',
abortSignal: abortCtrl.current.signal,
})
.subscribe({
next: (response) => {
if (!response.isPartial && !response.isRunning) {
setLoading(false);
setHostsKpiUniqueIpsResponse((prevResponse) => ({
...prevResponse,
uniqueSourceIps: response.uniqueSourceIps,
uniqueSourceIpsHistogram: response.uniqueSourceIpsHistogram,
uniqueDestinationIps: response.uniqueDestinationIps,
uniqueDestinationIpsHistogram: response.uniqueDestinationIpsHistogram,
inspect: getInspectResponse(response, prevResponse.inspect),
refetch: refetch.current,
}));
searchSubscription$.current.unsubscribe();
} else if (response.isPartial && !response.isRunning) {
setLoading(false);
addWarning(i18n.ERROR_HOSTS_KPI_UNIQUE_IPS);
searchSubscription$.current.unsubscribe();
}
},
error: (msg) => {
setLoading(false);
addError(msg, {
title: i18n.FAIL_HOSTS_KPI_UNIQUE_IPS,
});
searchSubscription$.current.unsubscribe();
},
});
};
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
asyncSearch();
refetch.current = asyncSearch;
},
[data.search, addError, addWarning, skip]
);
useEffect(() => {
setHostsKpiUniqueIpsRequest((prevRequest) => {
const myRequest: KpiUniqueIpsRequestOptionsInput = {
...(prevRequest ?? {}),
defaultIndex: indexNames,
factoryQueryType: HostsKpiQueries.kpiUniqueIps,
filterQuery: createFilter(filterQuery),
timerange: {
interval: '12h',
from: startDate,
to: endDate,
},
};
if (!deepEqual(prevRequest, myRequest)) {
return myRequest;
}
return prevRequest;
});
}, [indexNames, endDate, filterQuery, skip, startDate]);
useEffect(() => {
hostsKpiUniqueIpsSearch(hostsKpiUniqueIpsRequest);
return () => {
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
};
}, [hostsKpiUniqueIpsRequest, hostsKpiUniqueIpsSearch]);
useEffect(() => {
if (skip) {
setLoading(false);
searchSubscription$.current.unsubscribe();
abortCtrl.current.abort();
}
}, [skip]);
return [loading, hostsKpiUniqueIpsResponse];
};

View file

@ -1,22 +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 { i18n } from '@kbn/i18n';
export const ERROR_HOSTS_KPI_UNIQUE_IPS = i18n.translate(
'xpack.securitySolution.hostsKpiUniqueIps.errorSearchDescription',
{
defaultMessage: `An error has occurred on hosts kpi unique ips search`,
}
);
export const FAIL_HOSTS_KPI_UNIQUE_IPS = i18n.translate(
'xpack.securitySolution.hostsKpiUniqueIps.failSearchDescription',
{
defaultMessage: `Failed to run search on hosts kpi unique ips`,
}
);

Some files were not shown because too many files have changed in this diff Show more