mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[SecuritySolution] Remove chartEmbeddablesEnabled feature flag and unused code (#173675)
This commit is contained in:
parent
786200791b
commit
b84591786a
224 changed files with 878 additions and 14735 deletions
|
@ -12,7 +12,3 @@ export * from './details';
|
|||
export * from './overview';
|
||||
|
||||
export * from './uncommon_processes';
|
||||
|
||||
export * from './kpi_hosts';
|
||||
|
||||
export * from './kpi_unique_ips';
|
||||
|
|
|
@ -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>;
|
|
@ -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>;
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>;
|
|
@ -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>;
|
|
@ -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';
|
|
@ -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>;
|
|
@ -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>;
|
|
@ -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>;
|
|
@ -11,8 +11,6 @@ export * from './dns';
|
|||
|
||||
export * from './http';
|
||||
|
||||
export * from './kpi';
|
||||
|
||||
export * from './overview';
|
||||
|
||||
export * from './tls';
|
||||
|
|
|
@ -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>;
|
|
@ -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>;
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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'>;
|
|
@ -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>;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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[];
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
>;
|
|
@ -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[];
|
||||
};
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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'>;
|
|
@ -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>;
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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';
|
|
@ -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'>;
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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';
|
|
@ -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>;
|
||||
}
|
|
@ -175,7 +175,6 @@ const EventsQueryTabBodyComponent: React.FC<EventsQueryTabBodyComponentProps> =
|
|||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
filterQuery={filterQuery}
|
||||
indexNames={indexNames}
|
||||
setQuery={setQuery}
|
||||
{...(showExternalAlerts ? alertsHistogramConfig : eventsHistogramConfig)}
|
||||
subtitle={getHistogramSubtitle}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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';
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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] };
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -60,7 +60,6 @@ const AnomaliesQueryTabBodyComponent: React.FC<AnomaliesQueryTabBodyProps> = ({
|
|||
endDate={endDate}
|
||||
filterQuery={mergedFilterQuery}
|
||||
id={ID}
|
||||
indexNames={indexNames}
|
||||
setQuery={setQuery}
|
||||
startDate={startDate}
|
||||
{...histogramConfigs}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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];
|
||||
};
|
|
@ -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`,
|
||||
}
|
||||
);
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -5,5 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './hosts';
|
||||
export * from './unique_ips';
|
||||
import React from 'react';
|
||||
|
||||
export const KpiBaseComponent = jest.fn().mockReturnValue(<div />);
|
|
@ -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';
|
|
@ -7,4 +7,3 @@
|
|||
|
||||
export { StatItemsComponent } from './stat_items';
|
||||
export type { StatItemsProps, StatItems } from './types';
|
||||
export { useKpiMatrixStatus } from './use_kpi_matrix_status';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
|
@ -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) => (
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
}));
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
};
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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];
|
||||
};
|
|
@ -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`,
|
||||
}
|
||||
);
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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];
|
||||
};
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue