From 74d1e39ea490e8fea45fe8c67806c2dbc2adbcc2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cau=C3=AA=20Marcondes?=
<55978943+cauemarcondes@users.noreply.github.com>
Date: Mon, 21 Dec 2020 12:36:20 +0100
Subject: [PATCH 001/108] [APM] Bug: Service overview: Link to Transactions
list from the Overview page is broken (#86447)
* fixing link and refactoring some stuff
* addressing pr comments
---
.../Waterfall/FlyoutTopLevelProperties.tsx | 8 +++--
.../SpanFlyout/StickySpanProperties.tsx | 8 +++--
.../service_details/service_detail_tabs.tsx | 4 +--
.../service_inventory/ServiceList/index.tsx | 4 +--
.../index.tsx | 33 ++++++++++---------
.../service_overview_errors_table/index.tsx | 9 +++--
.../index.tsx | 15 ++++-----
.../service_overview/table_link_flex_item.tsx | 14 --------
....tsx => service_transactions_overview.tsx} | 7 ++--
.../Links/apm/transaction_overview_ink.tsx | 31 +++++++++++++++++
10 files changed, 78 insertions(+), 55 deletions(-)
delete mode 100644 x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx
rename x-pack/plugins/apm/public/components/shared/Links/apm/{TransactionOverviewLink.tsx => service_transactions_overview.tsx} (86%)
create mode 100644 x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_ink.tsx
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx
index 0187ecd927e65..b0ef28fbb7b0d 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/FlyoutTopLevelProperties.tsx
@@ -13,7 +13,7 @@ import {
import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction';
import { TransactionDetailLink } from '../../../../../shared/Links/apm/TransactionDetailLink';
import { StickyProperties } from '../../../../../shared/StickyProperties';
-import { TransactionOverviewLink } from '../../../../../shared/Links/apm/TransactionOverviewLink';
+import { ServiceOrTransactionsOverviewLink } from '../../../../../shared/Links/apm/service_transactions_overview';
interface Props {
transaction?: Transaction;
@@ -31,9 +31,11 @@ export function FlyoutTopLevelProperties({ transaction }: Props) {
}),
fieldName: SERVICE_NAME,
val: (
-
+
{transaction.service.name}
-
+
),
width: '25%',
},
diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx
index c068fee3cd6c3..ca5b4938ff42e 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/StickySpanProperties.tsx
@@ -15,7 +15,7 @@ import {
import { NOT_AVAILABLE_LABEL } from '../../../../../../../../common/i18n';
import { Span } from '../../../../../../../../typings/es_schemas/ui/span';
import { StickyProperties } from '../../../../../../shared/StickyProperties';
-import { TransactionOverviewLink } from '../../../../../../shared/Links/apm/TransactionOverviewLink';
+import { ServiceOrTransactionsOverviewLink } from '../../../../../../shared/Links/apm/service_transactions_overview';
import { TransactionDetailLink } from '../../../../../../shared/Links/apm/TransactionDetailLink';
interface Props {
@@ -33,9 +33,11 @@ export function StickySpanProperties({ span, transaction }: Props) {
}),
fieldName: SERVICE_NAME,
val: (
-
+
{transaction.service.name}
-
+
),
width: '25%',
},
diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx
index ae0dd85b6a8b5..961320baa6a4e 100644
--- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx
@@ -15,7 +15,7 @@ import { useMetricOverviewHref } from '../../shared/Links/apm/MetricOverviewLink
import { useServiceMapHref } from '../../shared/Links/apm/ServiceMapLink';
import { useServiceNodeOverviewHref } from '../../shared/Links/apm/ServiceNodeOverviewLink';
import { useServiceOverviewHref } from '../../shared/Links/apm/service_overview_link';
-import { useTransactionOverviewHref } from '../../shared/Links/apm/TransactionOverviewLink';
+import { useServiceOrTransactionsOverviewHref } from '../../shared/Links/apm/service_transactions_overview';
import { MainTabs } from '../../shared/main_tabs';
import { ErrorGroupOverview } from '../ErrorGroupOverview';
import { ServiceMap } from '../ServiceMap';
@@ -60,7 +60,7 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) {
const transactionsTab = {
key: 'transactions',
- href: useTransactionOverviewHref(serviceName),
+ href: useServiceOrTransactionsOverviewHref(serviceName),
text: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', {
defaultMessage: 'Transactions',
}),
diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx
index a4c93f95dc53d..157d3ecc738a1 100644
--- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx
@@ -21,7 +21,7 @@ import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import { fontSizes, px, truncate, unit } from '../../../../style/variables';
import { ManagedTable, ITableColumn } from '../../../shared/ManagedTable';
import { EnvironmentBadge } from '../../../shared/EnvironmentBadge';
-import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink';
+import { ServiceOrTransactionsOverviewLink } from '../../../shared/Links/apm/service_transactions_overview';
import { AgentIcon } from '../../../shared/AgentIcon';
import { HealthBadge } from './HealthBadge';
import { ServiceListMetric } from './ServiceListMetric';
@@ -39,7 +39,7 @@ function formatString(value?: string | null) {
return value || NOT_AVAILABLE_LABEL;
}
-const AppLink = styled(TransactionOverviewLink)`
+const AppLink = styled(ServiceOrTransactionsOverviewLink)`
font-size: ${fontSizes.large};
${truncate('100%')};
`;
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx
index ed4f3277a4a04..ae297b840ebc8 100644
--- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx
@@ -4,13 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexItem } from '@elastic/eui';
-import { EuiInMemoryTable } from '@elastic/eui';
-import { EuiTitle } from '@elastic/eui';
-import { EuiBasicTableColumn } from '@elastic/eui';
-import { EuiFlexGroup } from '@elastic/eui';
+import {
+ EuiBasicTableColumn,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiInMemoryTable,
+ EuiTitle,
+} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
+import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values';
import {
asDuration,
asPercent,
@@ -18,20 +21,18 @@ import {
} from '../../../../../common/utils/formatters';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ServiceDependencyItem } from '../../../../../server/lib/services/get_service_dependencies';
-import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values';
-import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
+import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { callApmApi } from '../../../../services/rest/createCallApmApi';
-import { ServiceMapLink } from '../../../shared/Links/apm/ServiceMapLink';
-import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
-import { TableLinkFlexItem } from '../table_link_flex_item';
+import { px, unit } from '../../../../style/variables';
import { AgentIcon } from '../../../shared/AgentIcon';
-import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper';
import { SparkPlot } from '../../../shared/charts/spark_plot';
-import { px, unit } from '../../../../style/variables';
import { ImpactBar } from '../../../shared/ImpactBar';
+import { ServiceMapLink } from '../../../shared/Links/apm/ServiceMapLink';
import { ServiceOverviewLink } from '../../../shared/Links/apm/service_overview_link';
import { SpanIcon } from '../../../shared/span_icon';
+import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper';
+import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
import { ServiceOverviewTableContainer } from '../service_overview_table_container';
interface Props {
@@ -192,8 +193,8 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) {
return (
-
-
+
+
{i18n.translate(
@@ -205,7 +206,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) {
-
+
{i18n.translate(
'xpack.apm.serviceOverview.dependenciesTableLinkText',
@@ -214,7 +215,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) {
}
)}
-
+
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx
index da74a6fc0004d..d14ef648c22d3 100644
--- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx
@@ -25,7 +25,6 @@ import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper';
import { TimestampTooltip } from '../../../shared/TimestampTooltip';
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
import { ServiceOverviewTableContainer } from '../service_overview_table_container';
-import { TableLinkFlexItem } from '../table_link_flex_item';
interface Props {
serviceName: string;
@@ -195,8 +194,8 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
return (
-
-
+
+
{i18n.translate('xpack.apm.serviceOverview.errorsTableTitle', {
@@ -205,13 +204,13 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) {
-
+
{i18n.translate('xpack.apm.serviceOverview.errorsTableLinkText', {
defaultMessage: 'View errors',
})}
-
+
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx
index 6345d546c716f..4b262f1f51319 100644
--- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx
@@ -20,6 +20,7 @@ import {
asPercent,
asTransactionRate,
} from '../../../../../common/utils/formatters';
+import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useLatencyAggregationType } from '../../../../hooks/use_latency_Aggregation_type';
@@ -28,13 +29,11 @@ import {
callApmApi,
} from '../../../../services/rest/createCallApmApi';
import { px, unit } from '../../../../style/variables';
-import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink';
-import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink';
-import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper';
-import { TableLinkFlexItem } from '../table_link_flex_item';
import { SparkPlot } from '../../../shared/charts/spark_plot';
import { ImpactBar } from '../../../shared/ImpactBar';
-import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
+import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink';
+import { TransactionOverviewLink } from '../../../shared/Links/apm/transaction_overview_ink';
+import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper';
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
import { ServiceOverviewTableContainer } from '../service_overview_table_container';
@@ -270,7 +269,7 @@ export function ServiceOverviewTransactionsTable(props: Props) {
-
+
{i18n.translate(
@@ -282,7 +281,7 @@ export function ServiceOverviewTransactionsTable(props: Props) {
-
+
{i18n.translate(
'xpack.apm.serviceOverview.transactionsTableLinkText',
@@ -291,7 +290,7 @@ export function ServiceOverviewTransactionsTable(props: Props) {
}
)}
-
+
diff --git a/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx b/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx
deleted file mode 100644
index 35df003af380d..0000000000000
--- a/x-pack/plugins/apm/public/components/app/service_overview/table_link_flex_item.tsx
+++ /dev/null
@@ -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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { EuiFlexItem } from '@elastic/eui';
-import styled from 'styled-components';
-
-export const TableLinkFlexItem = styled(EuiFlexItem)`
- & > a {
- text-align: right;
- }
-`;
diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview.tsx
similarity index 86%
rename from x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx
rename to x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview.tsx
index 1d99b82a67326..24a78e5d64749 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/apm/TransactionOverviewLink.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/apm/service_transactions_overview.tsx
@@ -18,7 +18,7 @@ const persistedFilters: Array = [
'latencyAggregationType',
];
-export function useTransactionOverviewHref(serviceName: string) {
+export function useServiceOrTransactionsOverviewHref(serviceName: string) {
return useAPMHref(`/services/${serviceName}/transactions`, persistedFilters);
}
@@ -26,7 +26,10 @@ interface Props extends APMLinkExtendProps {
serviceName: string;
}
-export function TransactionOverviewLink({ serviceName, ...rest }: Props) {
+export function ServiceOrTransactionsOverviewLink({
+ serviceName,
+ ...rest
+}: Props) {
const { urlParams } = useUrlParams();
return (
diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_ink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_ink.tsx
new file mode 100644
index 0000000000000..d2978b3c02d53
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_overview_ink.tsx
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { pickKeys } from '../../../../../common/utils/pick_keys';
+import { APMLink, APMLinkExtendProps } from './APMLink';
+import { useUrlParams } from '../../../../context/url_params_context/use_url_params';
+import { APMQueryParams } from '../url_helpers';
+
+interface Props extends APMLinkExtendProps {
+ serviceName: string;
+}
+
+const persistedFilters: Array = [
+ 'latencyAggregationType',
+];
+
+export function TransactionOverviewLink({ serviceName, ...rest }: Props) {
+ const { urlParams } = useUrlParams();
+
+ return (
+
+ );
+}
From c05533ebbda8ea57cacb3d732c439178f72d6e4b Mon Sep 17 00:00:00 2001
From: Thom Heymann <190132+thomheymann@users.noreply.github.com>
Date: Mon, 21 Dec 2020 11:42:51 +0000
Subject: [PATCH 002/108] Fix ECS HTTP scheme and improve docs (#86612)
---
docs/user/security/audit-logging.asciidoc | 189 +++++++++++++++++-
.../server/audit/audit_events.test.ts | 4 +-
.../security/server/audit/audit_events.ts | 18 +-
3 files changed, 191 insertions(+), 20 deletions(-)
diff --git a/docs/user/security/audit-logging.asciidoc b/docs/user/security/audit-logging.asciidoc
index 7facde28e956f..acb0f94cf878c 100644
--- a/docs/user/security/audit-logging.asciidoc
+++ b/docs/user/security/audit-logging.asciidoc
@@ -47,9 +47,11 @@ For information on how to configure `xpack.security.audit.appender`, refer to
Refer to the table of events that can be logged for auditing purposes.
-Each event is broken down into `category`, `type`, `action` and `outcome` fields
+Each event is broken down into <>, <>, <> and <> fields
to make it easy to filter, query and aggregate the resulting logs.
+Refer to <> for a table of fields that get logged with audit event.
+
[NOTE]
============================================================================
To ensure that a record of every operation is persisted even in case of an
@@ -230,3 +232,188 @@ Refer to the corresponding {es} logs for potential write errors.
| `http_request`
| `unknown` | User is making an HTTP request.
|======
+
+
+[[xpack-security-ecs-audit-schema]]
+==== ECS audit schema
+
+Audit logs are written in JSON using https://www.elastic.co/guide/en/ecs/1.6/index.html[Elastic Common Schema (ECS)] specification.
+
+[cols="2*<"]
+|======
+
+2+a| ===== Base Fields
+
+| *Field*
+| *Description*
+
+| `@timestamp`
+| Time when the event was generated.
+
+Example: `2016-05-23T08:05:34.853Z`
+
+| `message`
+| Human readable description of the event.
+
+2+a| ===== Event Fields
+
+| *Field*
+| *Description*
+
+| [[field-event-action]] `event.action`
+| The action captured by the event.
+
+Refer to <> for a table of possible actions.
+
+| [[field-event-category]] `event.category`
+| High level category associated with the event.
+
+This field is closely related to `event.type`, which is used as a subcategory.
+
+Possible values:
+`database`,
+`web`,
+`authentication`
+
+| [[field-event-type]] `event.type`
+| Subcategory associated with the event.
+
+This field can be used along with the `event.category` field to enable filtering events down to a level appropriate for single visualization.
+
+Possible values:
+`creation`,
+`access`,
+`change`,
+`deletion`
+
+| [[field-event-outcome]] `event.outcome`
+| Denotes whether the event represents a success or failure.
+
+Possible values:
+`success`,
+`failure`,
+`unknown`
+
+2+a| ===== User Fields
+
+| *Field*
+| *Description*
+
+| `user.name`
+| Login name of the user.
+
+Example: `jdoe`
+
+| `user.roles[]`
+| Set of user roles at the time of the event.
+
+Example: `[kibana_admin, reporting_user]`
+
+2+a| ===== Kibana Fields
+
+| *Field*
+| *Description*
+
+| `kibana.space_id`
+| ID of the space associated with the event.
+
+Example: `default`
+
+| `kibana.session_id`
+| ID of the user session associated with the event.
+
+Each login attempt results in a unique session id.
+
+| `kibana.saved_object.type`
+| Type of saved object associated with the event.
+
+Example: `dashboard`
+
+| `kibana.saved_object.id`
+| ID of the saved object associated with the event.
+
+| `kibana.authentication_provider`
+| Name of the authentication provider associated with the event.
+
+Example: `my-saml-provider`
+
+| `kibana.authentication_type`
+| Type of the authentication provider associated with the event.
+
+Example: `saml`
+
+| `kibana.authentication_realm`
+| Name of the Elasticsearch realm that has authenticated the user.
+
+Example: `native`
+
+| `kibana.lookup_realm`
+| Name of the Elasticsearch realm where the user details were retrieved from.
+
+Example: `native`
+
+| `kibana.add_to_spaces[]`
+| Set of space IDs that a saved object is being shared to as part of the event.
+
+Example: `[default, marketing]`
+
+| `kibana.delete_from_spaces[]`
+| Set of space IDs that a saved object is being removed from as part of the event.
+
+Example: `[marketing]`
+
+2+a| ===== Error Fields
+
+| *Field*
+| *Description*
+
+| `error.code`
+| Error code describing the error.
+
+| `error.message`
+| Error message.
+
+2+a| ===== HTTP and URL Fields
+
+| *Field*
+| *Description*
+
+| `http.request.method`
+| HTTP request method.
+
+Example: `get`, `post`, `put`, `delete`
+
+| `url.domain`
+| Domain of the url.
+
+Example: `www.elastic.co`
+
+| `url.path`
+| Path of the request.
+
+Example: `/search`
+
+| `url.port`
+| Port of the request.
+
+Example: `443`
+
+| `url.query`
+| The query field describes the query string of the request.
+
+Example: `q=elasticsearch`
+
+| `url.scheme`
+| Scheme of the request.
+
+Example: `https`
+
+2+a| ===== Tracing Fields
+
+| *Field*
+| *Description*
+
+| `trace.id`
+| Unique identifier allowing events of the same transaction from {kib} and {es} to be be correlated.
+
+|======
diff --git a/x-pack/plugins/security/server/audit/audit_events.test.ts b/x-pack/plugins/security/server/audit/audit_events.test.ts
index 9bda628df66dc..f7e41bce674ee 100644
--- a/x-pack/plugins/security/server/audit/audit_events.test.ts
+++ b/x-pack/plugins/security/server/audit/audit_events.test.ts
@@ -284,7 +284,7 @@ describe('#httpRequestEvent', () => {
"path": "/path",
"port": undefined,
"query": undefined,
- "scheme": "http:",
+ "scheme": "http",
},
}
`);
@@ -321,7 +321,7 @@ describe('#httpRequestEvent', () => {
"path": "/original/path",
"port": undefined,
"query": "query=param",
- "scheme": "http:",
+ "scheme": "http",
},
}
`);
diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts
index 7f0dd39162adf..b6538af31bd60 100644
--- a/x-pack/plugins/security/server/audit/audit_events.ts
+++ b/x-pack/plugins/security/server/audit/audit_events.ts
@@ -28,14 +28,9 @@ export interface AuditEvent {
category?: EventCategory;
type?: EventType;
outcome?: EventOutcome;
- module?: string;
- dataset?: string;
};
user?: {
name: string;
- email?: string;
- full_name?: string;
- hash?: string;
roles?: readonly string[];
};
kibana?: {
@@ -87,17 +82,10 @@ export interface AuditEvent {
http?: {
request?: {
method?: string;
- body?: {
- content: string;
- };
- };
- response?: {
- status_code?: number;
};
};
url?: {
domain?: string;
- full?: string;
path?: string;
port?: number;
query?: string;
@@ -108,14 +96,10 @@ export interface AuditEvent {
export enum EventCategory {
DATABASE = 'database',
WEB = 'web',
- IAM = 'iam',
AUTHENTICATION = 'authentication',
- PROCESS = 'process',
}
export enum EventType {
- USER = 'user',
- GROUP = 'group',
CREATION = 'creation',
ACCESS = 'access',
CHANGE = 'change',
@@ -152,7 +136,7 @@ export function httpRequestEvent({ request }: HttpRequestParams): AuditEvent {
path: url.pathname,
port: url.port ? parseInt(url.port, 10) : undefined,
query: url.search ? url.search.slice(1) : undefined,
- scheme: url.protocol,
+ scheme: url.protocol ? url.protocol.substr(0, url.protocol.length - 1) : undefined,
},
};
}
From 6d72042ca4acf2e8bc4b92431ee6f93d7c14cf56 Mon Sep 17 00:00:00 2001
From: James Gowdy
Date: Mon, 21 Dec 2020 12:17:53 +0000
Subject: [PATCH 003/108] [ML] Fixing endpoint schema for can_delete_job
endpoint (#86436)
* [ML] Fixing endpoint schema for can_delete_job endpoint
* changing get to post in docs
* renaming canUntag
* fixing typo
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../plugins/ml/common/types/saved_objects.ts | 4 ++--
.../delete_job_check_modal.tsx | 20 +++++++++----------
x-pack/plugins/ml/server/routes/apidoc.json | 2 +-
.../plugins/ml/server/routes/saved_objects.ts | 20 ++++++++++++++-----
.../ml/server/routes/schemas/saved_objects.ts | 5 +++++
.../plugins/ml/server/saved_objects/checks.ts | 16 +++++++--------
.../apis/ml/saved_objects/can_delete_job.ts | 8 ++++----
7 files changed, 45 insertions(+), 30 deletions(-)
diff --git a/x-pack/plugins/ml/common/types/saved_objects.ts b/x-pack/plugins/ml/common/types/saved_objects.ts
index 8783113502623..259f9be1a1b26 100644
--- a/x-pack/plugins/ml/common/types/saved_objects.ts
+++ b/x-pack/plugins/ml/common/types/saved_objects.ts
@@ -21,7 +21,7 @@ export interface SyncSavedObjectResponse {
export interface CanDeleteJobResponse {
[jobId: string]: {
canDelete: boolean;
- canUntag: boolean;
+ canRemoveFromSpace: boolean;
};
}
@@ -41,5 +41,5 @@ export interface DeleteJobCheckResponse {
export interface DeleteJobPermission {
canDelete: boolean;
- canUntag: boolean;
+ canRemoveFromSpace: boolean;
}
diff --git a/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx b/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx
index 151946ab31fd9..4393d2b2fcf4d 100644
--- a/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx
+++ b/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx
@@ -36,31 +36,31 @@ interface ModalContentReturnType {
interface JobCheckRespSummary {
canDelete: boolean;
- canUntag: boolean;
+ canRemoveFromSpace: boolean;
canTakeAnyAction: boolean;
}
function getRespSummary(resp: CanDeleteJobResponse): JobCheckRespSummary {
const jobsChecked = Object.keys(resp);
// Default to first job's permissions
- const { canDelete, canUntag } = resp[jobsChecked[0]];
+ const { canDelete, canRemoveFromSpace } = resp[jobsChecked[0]];
let canTakeAnyAction = true;
if (jobsChecked.length > 1) {
// Check all jobs and make sure they have the same permissions - otherwise no action can be taken
canTakeAnyAction = jobsChecked.every(
- (id) => resp[id].canDelete === canDelete && resp[id].canUntag === canUntag
+ (id) => resp[id].canDelete === canDelete && resp[id].canRemoveFromSpace === canRemoveFromSpace
);
}
- return { canDelete, canUntag, canTakeAnyAction };
+ return { canDelete, canRemoveFromSpace, canTakeAnyAction };
}
function getModalContent(
jobIds: string[],
respSummary: JobCheckRespSummary
): ModalContentReturnType {
- const { canDelete, canUntag, canTakeAnyAction } = respSummary;
+ const { canDelete, canRemoveFromSpace, canTakeAnyAction } = respSummary;
if (canTakeAnyAction === false) {
return {
@@ -116,7 +116,7 @@ function getModalContent(
),
};
- } else if (canUntag) {
+ } else if (canRemoveFromSpace) {
return {
buttonText: (
= ({
// Do the spaces check and set the content for the modal and buttons depending on results
canDeleteJob(jobType, jobIds).then((resp) => {
const respSummary = getRespSummary(resp);
- const { canDelete, canUntag, canTakeAnyAction } = respSummary;
- if (canTakeAnyAction && canDelete && !canUntag) {
+ const { canDelete, canRemoveFromSpace, canTakeAnyAction } = respSummary;
+ if (canTakeAnyAction && canDelete && !canRemoveFromSpace) {
// Go straight to delete flow if that's the only action available
canDeleteCallback();
return;
@@ -260,7 +260,7 @@ export const DeleteJobCheckModal: FC = ({
{!hasUntagged &&
jobCheckRespSummary?.canTakeAnyAction &&
- jobCheckRespSummary?.canUntag &&
+ jobCheckRespSummary?.canRemoveFromSpace &&
jobCheckRespSummary?.canDelete && (
= ({
size="s"
onClick={
jobCheckRespSummary?.canTakeAnyAction &&
- jobCheckRespSummary?.canUntag &&
+ jobCheckRespSummary?.canRemoveFromSpace &&
!jobCheckRespSummary?.canDelete
? onUntagClick
: onClick
diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json
index bf002907b1a43..015ec6e4ec9c0 100644
--- a/x-pack/plugins/ml/server/routes/apidoc.json
+++ b/x-pack/plugins/ml/server/routes/apidoc.json
@@ -151,7 +151,7 @@
"RemoveJobsFromSpaces",
"RemoveJobsFromCurrentSpace",
"JobsSpaces",
- "DeleteJobCheck",
+ "CanDeleteJob",
"TrainedModels",
"GetTrainedModel",
diff --git a/x-pack/plugins/ml/server/routes/saved_objects.ts b/x-pack/plugins/ml/server/routes/saved_objects.ts
index 29f9b218ea177..a5144ab2a7af7 100644
--- a/x-pack/plugins/ml/server/routes/saved_objects.ts
+++ b/x-pack/plugins/ml/server/routes/saved_objects.ts
@@ -12,8 +12,8 @@ import {
jobsAndCurrentSpace,
syncJobObjects,
jobTypeSchema,
+ canDeleteJobSchema,
} from './schemas/saved_objects';
-import { jobIdsSchema } from './schemas/job_service_schema';
import { spacesUtilsProvider } from '../lib/spaces_utils';
import { JobType } from '../../common/types/saved_objects';
@@ -284,13 +284,23 @@ export function savedObjectsRoutes(
/**
* @apiGroup JobSavedObjects
*
- * @api {get} /api/ml/saved_objects/delete_job_check Check whether user can delete a job
- * @apiName DeleteJobCheck
+ * @api {post} /api/ml/saved_objects/can_delete_job Check whether user can delete a job
+ * @apiName CanDeleteJob
* @apiDescription Check the user's ability to delete jobs. Returns whether they are able
* to fully delete the job and whether they are able to remove it from
* the current space.
+ * Note, this is only for enabling UI controls. A user calling endpoints
+ * directly will still be able to delete or remove the job from a space.
*
- * @apiSchema (body) jobIdsSchema (params) jobTypeSchema
+ * @apiSchema (params) jobTypeSchema
+ * @apiSchema (body) jobIdsSchema
+ * @apiSuccessExample {json} Error-Response:
+ * {
+ * "my_job": {
+ * "canDelete": false,
+ * "canRemoveFromSpace": true
+ * }
+ * }
*
*/
router.post(
@@ -298,7 +308,7 @@ export function savedObjectsRoutes(
path: '/api/ml/saved_objects/can_delete_job/{jobType}',
validate: {
params: jobTypeSchema,
- body: jobIdsSchema,
+ body: canDeleteJobSchema,
},
options: {
tags: ['access:ml:canGetJobs', 'access:ml:canGetDataFrameAnalytics'],
diff --git a/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts b/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts
index 147398694f191..a64d38a9fda70 100644
--- a/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts
+++ b/x-pack/plugins/ml/server/routes/schemas/saved_objects.ts
@@ -22,3 +22,8 @@ export const syncJobObjects = schema.object({ simulate: schema.maybe(schema.bool
export const jobTypeSchema = schema.object({
jobType: schema.string(),
});
+
+export const canDeleteJobSchema = schema.object({
+ /** List of job IDs. */
+ jobIds: schema.arrayOf(schema.maybe(schema.string())),
+});
diff --git a/x-pack/plugins/ml/server/saved_objects/checks.ts b/x-pack/plugins/ml/server/saved_objects/checks.ts
index f682999cd5966..9258b143c9747 100644
--- a/x-pack/plugins/ml/server/saved_objects/checks.ts
+++ b/x-pack/plugins/ml/server/saved_objects/checks.ts
@@ -180,7 +180,7 @@ export function checksFactory(
return jobIds.reduce((results, jobId) => {
results[jobId] = {
canDelete: false,
- canUntag: false,
+ canRemoveFromSpace: false,
};
return results;
}, {} as DeleteJobCheckResponse);
@@ -191,7 +191,7 @@ export function checksFactory(
return jobIds.reduce((results, jobId) => {
results[jobId] = {
canDelete: true,
- canUntag: false,
+ canRemoveFromSpace: false,
};
return results;
}, {} as DeleteJobCheckResponse);
@@ -208,7 +208,7 @@ export function checksFactory(
// job saved object not found
results[jobId] = {
canDelete: false,
- canUntag: false,
+ canRemoveFromSpace: false,
};
return results;
}
@@ -220,7 +220,7 @@ export function checksFactory(
if (canCreateGlobalJobs && isGlobalJob) {
results[jobId] = {
canDelete: true,
- canUntag: false,
+ canRemoveFromSpace: false,
};
return results;
}
@@ -229,20 +229,20 @@ export function checksFactory(
if (isGlobalJob) {
results[jobId] = {
canDelete: false,
- canUntag: false,
+ canRemoveFromSpace: false,
};
return results;
}
// jobs with are in individual spaces can only be untagged
// from current space if the job is in more than 1 space
- const canUntag = namespaces.length > 1;
+ const canRemoveFromSpace = namespaces.length > 1;
// job is in individual spaces, user cannot see all of them - untag only, no delete
if (namespaces.includes('?')) {
results[jobId] = {
canDelete: false,
- canUntag,
+ canRemoveFromSpace,
};
return results;
}
@@ -250,7 +250,7 @@ export function checksFactory(
// job is individual spaces, user can see all of them - delete and option to untag
results[jobId] = {
canDelete: true,
- canUntag,
+ canRemoveFromSpace,
};
return results;
}, {} as DeleteJobCheckResponse);
diff --git a/x-pack/test/api_integration/apis/ml/saved_objects/can_delete_job.ts b/x-pack/test/api_integration/apis/ml/saved_objects/can_delete_job.ts
index d95c90d417203..e80d5a333bbdf 100644
--- a/x-pack/test/api_integration/apis/ml/saved_objects/can_delete_job.ts
+++ b/x-pack/test/api_integration/apis/ml/saved_objects/can_delete_job.ts
@@ -75,7 +75,7 @@ export default ({ getService }: FtrProviderContext) => {
idSpace1
);
- expect(body).to.eql({ [adJobIdSpace12]: { canDelete: false, canUntag: true } });
+ expect(body).to.eql({ [adJobIdSpace12]: { canDelete: false, canRemoveFromSpace: true } });
});
it('job in individual spaces, all spaces user can delete and untag', async () => {
@@ -87,7 +87,7 @@ export default ({ getService }: FtrProviderContext) => {
idSpace1
);
- expect(body).to.eql({ [adJobIdSpace12]: { canDelete: true, canUntag: true } });
+ expect(body).to.eql({ [adJobIdSpace12]: { canDelete: true, canRemoveFromSpace: true } });
});
it('job in * space, single space user can not untag or delete', async () => {
@@ -99,7 +99,7 @@ export default ({ getService }: FtrProviderContext) => {
idSpace1
);
- expect(body).to.eql({ [adJobIdStarSpace]: { canDelete: false, canUntag: false } });
+ expect(body).to.eql({ [adJobIdStarSpace]: { canDelete: false, canRemoveFromSpace: false } });
});
it('job in * space, all spaces user can delete but not untag', async () => {
@@ -111,7 +111,7 @@ export default ({ getService }: FtrProviderContext) => {
idStarSpace
);
- expect(body).to.eql({ [adJobIdStarSpace]: { canDelete: true, canUntag: false } });
+ expect(body).to.eql({ [adJobIdStarSpace]: { canDelete: true, canRemoveFromSpace: false } });
});
});
};
From 388b2508b312d61f3fc480e2dc88379bc66ed9ce Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 21 Dec 2020 14:26:14 +0100
Subject: [PATCH 004/108] [Lens] Make sure Lens does not reload unnecessarily
(#86092)
---
.../public/react_expression_renderer.test.tsx | 38 +++++++++
.../public/react_expression_renderer.tsx | 18 +++--
.../lens/public/app_plugin/mounter.tsx | 80 +++++++++++--------
.../editor_frame/suggestion_panel.tsx | 14 +++-
4 files changed, 108 insertions(+), 42 deletions(-)
diff --git a/src/plugins/expressions/public/react_expression_renderer.test.tsx b/src/plugins/expressions/public/react_expression_renderer.test.tsx
index e52d4d153882f..4ebd626e70fc3 100644
--- a/src/plugins/expressions/public/react_expression_renderer.test.tsx
+++ b/src/plugins/expressions/public/react_expression_renderer.test.tsx
@@ -146,6 +146,44 @@ describe('ExpressionRenderer', () => {
instance.unmount();
});
+ it('waits for debounce period on other loader option change if specified', () => {
+ jest.useFakeTimers();
+
+ const refreshSubject = new Subject();
+ const loaderUpdate = jest.fn();
+
+ (ExpressionLoader as jest.Mock).mockImplementation(() => {
+ return {
+ render$: new Subject(),
+ data$: new Subject(),
+ loading$: new Subject(),
+ update: loaderUpdate,
+ destroy: jest.fn(),
+ };
+ });
+
+ const instance = mount(
+
+ );
+
+ instance.setProps({ searchContext: { from: 'now-30m', to: 'now' } });
+
+ expect(loaderUpdate).toHaveBeenCalledTimes(1);
+
+ act(() => {
+ jest.runAllTimers();
+ });
+
+ expect(loaderUpdate).toHaveBeenCalledTimes(2);
+
+ instance.unmount();
+ });
+
it('should display a custom error message if the user provides one and then remove it after successful render', () => {
const dataSubject = new Subject();
const data$ = dataSubject.asObservable().pipe(share());
diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx
index 894325c8b65f7..eac2371ec66d0 100644
--- a/src/plugins/expressions/public/react_expression_renderer.tsx
+++ b/src/plugins/expressions/public/react_expression_renderer.tsx
@@ -90,21 +90,23 @@ export const ReactExpressionRenderer = ({
null
);
const [debouncedExpression, setDebouncedExpression] = useState(expression);
- useEffect(() => {
+ const [waitingForDebounceToComplete, setDebouncePending] = useState(false);
+ useShallowCompareEffect(() => {
if (debounce === undefined) {
return;
}
+ setDebouncePending(true);
const handler = setTimeout(() => {
setDebouncedExpression(expression);
+ setDebouncePending(false);
}, debounce);
return () => {
clearTimeout(handler);
};
- }, [expression, debounce]);
+ }, [expression, expressionLoaderOptions, debounce]);
const activeExpression = debounce !== undefined ? debouncedExpression : expression;
- const waitingForDebounceToComplete = debounce !== undefined && expression !== debouncedExpression;
/* eslint-disable react-hooks/exhaustive-deps */
// OK to ignore react-hooks/exhaustive-deps because options update is handled by calling .update()
@@ -182,12 +184,16 @@ export const ReactExpressionRenderer = ({
// Re-fetch data automatically when the inputs change
useShallowCompareEffect(
() => {
- if (expressionLoaderRef.current) {
+ // only update the loader if the debounce period is over
+ if (expressionLoaderRef.current && !waitingForDebounceToComplete) {
expressionLoaderRef.current.update(activeExpression, expressionLoaderOptions);
}
},
- // when expression is changed by reference and when any other loaderOption is changed by reference
- [{ activeExpression, ...expressionLoaderOptions }]
+ // when debounced, wait for debounce status to change to update loader.
+ // Otherwise, update when expression is changed by reference and when any other loaderOption is changed by reference
+ debounce === undefined
+ ? [{ activeExpression, ...expressionLoaderOptions }]
+ : [{ waitingForDebounceToComplete }]
);
/* eslint-enable react-hooks/exhaustive-deps */
diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx
index b09ecfdcd5553..fbfd9c5758948 100644
--- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx
@@ -3,11 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { useCallback } from 'react';
import { AppMountParameters, CoreSetup } from 'kibana/public';
import { FormattedMessage, I18nProvider } from '@kbn/i18n/react';
import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom';
+import { History } from 'history';
import { render, unmountComponentAtNode } from 'react-dom';
import { i18n } from '@kbn/i18n';
@@ -86,25 +87,22 @@ export async function mountApp(
})
);
- const getInitialInput = (
- routeProps: RouteComponentProps<{ id?: string }>,
- editByValue?: boolean
- ): LensEmbeddableInput | undefined => {
+ const getInitialInput = (id?: string, editByValue?: boolean): LensEmbeddableInput | undefined => {
if (editByValue) {
return embeddableEditorIncomingState?.valueInput as LensByValueInput;
}
- if (routeProps.match.params.id) {
- return { savedObjectId: routeProps.match.params.id } as LensByReferenceInput;
+ if (id) {
+ return { savedObjectId: id } as LensByReferenceInput;
}
};
- const redirectTo = (routeProps: RouteComponentProps<{ id?: string }>, savedObjectId?: string) => {
+ const redirectTo = (history: History, savedObjectId?: string) => {
if (!savedObjectId) {
- routeProps.history.push({ pathname: '/', search: routeProps.history.location.search });
+ history.push({ pathname: '/', search: history.location.search });
} else {
- routeProps.history.push({
+ history.push({
pathname: `/edit/${savedObjectId}`,
- search: routeProps.history.location.search,
+ search: history.location.search,
});
}
};
@@ -144,27 +142,45 @@ export async function mountApp(
}
};
- const renderEditor = (
- routeProps: RouteComponentProps<{ id?: string }>,
- editByValue?: boolean
+ // const featureFlagConfig = await getByValueFeatureFlag();
+ const EditorRenderer = React.memo(
+ (props: { id?: string; history: History; editByValue?: boolean }) => {
+ const redirectCallback = useCallback(
+ (id?: string) => {
+ redirectTo(props.history, id);
+ },
+ [props.history]
+ );
+ trackUiEvent('loaded');
+ return (
+
+ );
+ }
+ );
+
+ const EditorRoute = (
+ routeProps: RouteComponentProps<{ id?: string }> & { editByValue?: boolean }
) => {
- trackUiEvent('loaded');
return (
- redirectTo(routeProps, savedObjectId)}
- redirectToOrigin={redirectToOrigin}
- redirectToDashboard={redirectToDashboard}
- onAppLeave={params.onAppLeave}
- setHeaderActionMenu={params.setHeaderActionMenu}
+
);
};
@@ -185,13 +201,13 @@ export async function mountApp(
-
+
renderEditor(routeProps, true)}
+ render={(routeProps) => }
/>
-
+
diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx
index e42d4daffbb66..338a998b6b4dc 100644
--- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx
+++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx
@@ -7,7 +7,7 @@
import './suggestion_panel.scss';
import _, { camelCase } from 'lodash';
-import React, { useState, useEffect, useMemo } from 'react';
+import React, { useState, useEffect, useMemo, useRef } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiIcon,
@@ -270,13 +270,19 @@ export function SuggestionPanel({
[frame.query, frame.dateRange.fromDate, frame.dateRange.toDate, frame.filters]
);
+ const contextRef = useRef(context);
+ contextRef.current = context;
+
const AutoRefreshExpressionRenderer = useMemo(() => {
const autoRefreshFetch$ = plugins.data.query.timefilter.timefilter.getAutoRefreshFetch$();
return (props: ReactExpressionRendererProps) => (
-
+
);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [plugins.data.query.timefilter.timefilter, context]);
+ }, [plugins.data.query.timefilter.timefilter, ExpressionRendererComponent]);
const [lastSelectedSuggestion, setLastSelectedSuggestion] = useState(-1);
From 36a8343064849c09fffd7a2d010cb38a028d82d6 Mon Sep 17 00:00:00 2001
From: Joe Reuter
Date: Mon, 21 Dec 2020 15:02:51 +0100
Subject: [PATCH 005/108] fix time scaling bugs (#86444)
---
.../indexpattern_datasource/indexpattern.test.ts | 3 +++
.../suffix_formatter.test.ts | 16 ++++++++++++++++
.../indexpattern_datasource/suffix_formatter.ts | 5 +++++
.../indexpattern_datasource/to_expression.ts | 1 +
4 files changed, 25 insertions(+)
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
index 042ea0353ac63..2e55abf4a429a 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts
@@ -530,6 +530,9 @@ describe('IndexPattern Data Source', () => {
"outputColumnId": Array [
"col1",
],
+ "outputColumnName": Array [
+ "Count of records",
+ ],
"targetUnit": Array [
"h",
],
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts
index ef1739e4424fa..ade6ba099d70e 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.test.ts
@@ -25,4 +25,20 @@ describe('suffix formatter', () => {
expect(convertMock).toHaveBeenCalledWith(12345);
expect(formatFactory).toHaveBeenCalledWith({ id: 'nestedFormatter', params: nestedParams });
});
+
+ it('should not add suffix to empty strings', () => {
+ const convertMock = jest.fn((x) => '');
+ const formatFactory = jest.fn(() => ({ convert: convertMock }));
+ const SuffixFormatter = getSuffixFormatter((formatFactory as unknown) as FormatFactory);
+ const nestedParams = { abc: 123 };
+ const formatterInstance = new SuffixFormatter({
+ unit: 'h',
+ id: 'nestedFormatter',
+ params: nestedParams,
+ });
+
+ const result = formatterInstance.convert(12345);
+
+ expect(result).toEqual('');
+ });
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts
index f5d764acab086..3d9f3be01a11b 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/suffix_formatter.ts
@@ -49,6 +49,11 @@ export function getSuffixFormatter(formatFactory: FormatFactory) {
val
);
+ // do not add suffixes to empty strings
+ if (formattedValue === '') {
+ return '';
+ }
+
if (suffix) {
return `${formattedValue}${suffix}`;
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts
index a85b8920366b5..a5ce4dfbea371 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts
@@ -121,6 +121,7 @@ function getExpressionForLayer(
dateColumnId: [firstDateHistogramColumn![0]],
inputColumnId: [id],
outputColumnId: [id],
+ outputColumnName: [col.label],
targetUnit: [col.timeScale!],
},
};
From 34803ed98bbe6e04a11add288b1f9fae8bc3e201 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20G=C3=B3mez?=
Date: Mon, 21 Dec 2020 15:13:07 +0100
Subject: [PATCH 006/108] [Logs UI] Toggle log entry context menu when user
clicks the trigger button (#86307)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../logging/log_text_stream/log_entry_context_menu.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx
index c627d6eda22a5..fe57b9db0e8b7 100644
--- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx
+++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_context_menu.tsx
@@ -60,7 +60,7 @@ export const LogEntryContextMenu: React.FC = ({
size="s"
fill
aria-label={ariaLabel || DEFAULT_MENU_LABEL}
- onClick={onOpen}
+ onClick={isOpen ? onClose : onOpen}
minWidth="auto"
>
From d744eaec8ce40516fd0d6e81ba784e2dec5ca24f Mon Sep 17 00:00:00 2001
From: Walter Rafelsberger
Date: Mon, 21 Dec 2020 15:22:42 +0100
Subject: [PATCH 007/108] [ML] Data Frame Analytics: Scatterplot Matrix Fixes
(#86357)
- use ml API service from the Kibana context
- adds jest tests for the Vega Lite Spec generator
- fix chart layout overflow with too many fields selected
---
.../scatterplot_matrix.scss | 2 +
.../scatterplot_matrix/scatterplot_matrix.tsx | 6 +-
.../scatterplot_matrix_vega_lite_spec.test.ts | 170 ++++++++++++++++++
.../scatterplot_matrix_vega_lite_spec.ts | 21 ++-
4 files changed, 188 insertions(+), 11 deletions(-)
create mode 100644 x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts
diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss
index f59c27dccc742..6a4fbd97b3bdd 100644
--- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss
+++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss
@@ -1,4 +1,6 @@
.mlScatterplotMatrix {
+ overflow-x: auto;
+
.vega-bind span {
font-size: $euiFontSizeXS;
padding: 0 $euiSizeXS;
diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx
index ad9f63b10ebdd..b1ee9afb17788 100644
--- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx
+++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx
@@ -30,7 +30,7 @@ import { i18n } from '@kbn/i18n';
import type { SearchResponse7 } from '../../../../common/types/es_client';
-import { ml } from '../../services/ml_api_service';
+import { useMlApiContext } from '../../contexts/kibana';
import { getProcessedFields } from '../data_grid';
import { useCurrentEuiTheme } from '../color_range_legend';
@@ -72,6 +72,8 @@ export const ScatterplotMatrix: FC = ({
color,
legendType,
}) => {
+ const { esSearch } = useMlApiContext();
+
// dynamicSize is optionally used for outlier charts where the scatterplot marks
// are sized according to outlier_score
const [dynamicSize, setDynamicSize] = useState(false);
@@ -147,7 +149,7 @@ export const ScatterplotMatrix: FC = ({
}
: { match_all: {} };
- const resp: SearchResponse7 = await ml.esSearch({
+ const resp: SearchResponse7 = await esSearch({
index,
body: {
fields: queryFields,
diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts
new file mode 100644
index 0000000000000..dd467161ff489
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts
@@ -0,0 +1,170 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+// @ts-ignore
+import { compile } from 'vega-lite/build-es5/vega-lite';
+
+import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json';
+
+import {
+ getColorSpec,
+ getScatterplotMatrixVegaLiteSpec,
+ COLOR_OUTLIER,
+ COLOR_RANGE_NOMINAL,
+ DEFAULT_COLOR,
+ LEGEND_TYPES,
+} from './scatterplot_matrix_vega_lite_spec';
+
+describe('getColorSpec()', () => {
+ it('should return the default color for non-outlier specs', () => {
+ const colorSpec = getColorSpec(euiThemeLight, false);
+
+ expect(colorSpec).toEqual({ value: DEFAULT_COLOR });
+ });
+
+ it('should return a conditional spec for outliers', () => {
+ const colorSpec = getColorSpec(euiThemeLight, true);
+
+ expect(colorSpec).toEqual({
+ condition: {
+ test: "(datum['outlier_score'] >= mlOutlierScoreThreshold.cutoff)",
+ value: COLOR_OUTLIER,
+ },
+ value: euiThemeLight.euiColorMediumShade,
+ });
+ });
+
+ it('should return a field based spec for non-outlier specs with legendType supplied', () => {
+ const colorName = 'the-color-field';
+
+ const colorSpec = getColorSpec(euiThemeLight, false, colorName, LEGEND_TYPES.NOMINAL);
+
+ expect(colorSpec).toEqual({
+ field: colorName,
+ scale: {
+ range: COLOR_RANGE_NOMINAL,
+ },
+ type: 'nominal',
+ });
+ });
+});
+
+describe('getScatterplotMatrixVegaLiteSpec()', () => {
+ it('should return the default spec for non-outliers without a legend', () => {
+ const data = [{ x: 1, y: 1 }];
+
+ const vegaLiteSpec = getScatterplotMatrixVegaLiteSpec(data, ['x', 'y'], euiThemeLight);
+
+ // A valid Vega Lite spec shouldn't throw an error when compiled.
+ expect(() => compile(vegaLiteSpec)).not.toThrow();
+
+ expect(vegaLiteSpec.repeat).toEqual({
+ column: ['x', 'y'],
+ row: ['y', 'x'],
+ });
+ expect(vegaLiteSpec.spec.transform).toEqual([
+ { as: 'x', calculate: "datum['x']" },
+ { as: 'y', calculate: "datum['y']" },
+ ]);
+ expect(vegaLiteSpec.spec.data.values).toEqual(data);
+ expect(vegaLiteSpec.spec.mark).toEqual({
+ opacity: 0.75,
+ size: 8,
+ type: 'circle',
+ });
+ expect(vegaLiteSpec.spec.encoding.color).toEqual({ value: DEFAULT_COLOR });
+ expect(vegaLiteSpec.spec.encoding.tooltip).toEqual([
+ { field: 'x', type: 'quantitative' },
+ { field: 'y', type: 'quantitative' },
+ ]);
+ });
+
+ it('should return the spec for outliers', () => {
+ const data = [{ x: 1, y: 1 }];
+
+ const vegaLiteSpec = getScatterplotMatrixVegaLiteSpec(data, ['x', 'y'], euiThemeLight, 'ml');
+
+ // A valid Vega Lite spec shouldn't throw an error when compiled.
+ expect(() => compile(vegaLiteSpec)).not.toThrow();
+
+ expect(vegaLiteSpec.repeat).toEqual({
+ column: ['x', 'y'],
+ row: ['y', 'x'],
+ });
+ expect(vegaLiteSpec.spec.transform).toEqual([
+ { as: 'x', calculate: "datum['x']" },
+ { as: 'y', calculate: "datum['y']" },
+ {
+ as: 'outlier_score',
+ calculate: "datum['ml.outlier_score']",
+ },
+ ]);
+ expect(vegaLiteSpec.spec.data.values).toEqual(data);
+ expect(vegaLiteSpec.spec.mark).toEqual({
+ opacity: 0.75,
+ size: 8,
+ type: 'circle',
+ });
+ expect(vegaLiteSpec.spec.encoding.color).toEqual({
+ condition: {
+ test: "(datum['outlier_score'] >= mlOutlierScoreThreshold.cutoff)",
+ value: COLOR_OUTLIER,
+ },
+ value: euiThemeLight.euiColorMediumShade,
+ });
+ expect(vegaLiteSpec.spec.encoding.tooltip).toEqual([
+ { field: 'x', type: 'quantitative' },
+ { field: 'y', type: 'quantitative' },
+ {
+ field: 'outlier_score',
+ format: '.3f',
+ type: 'quantitative',
+ },
+ ]);
+ });
+
+ it('should return the spec for classification', () => {
+ const data = [{ x: 1, y: 1 }];
+
+ const vegaLiteSpec = getScatterplotMatrixVegaLiteSpec(
+ data,
+ ['x', 'y'],
+ euiThemeLight,
+ undefined,
+ 'the-color-field',
+ LEGEND_TYPES.NOMINAL
+ );
+
+ // A valid Vega Lite spec shouldn't throw an error when compiled.
+ expect(() => compile(vegaLiteSpec)).not.toThrow();
+
+ expect(vegaLiteSpec.repeat).toEqual({
+ column: ['x', 'y'],
+ row: ['y', 'x'],
+ });
+ expect(vegaLiteSpec.spec.transform).toEqual([
+ { as: 'x', calculate: "datum['x']" },
+ { as: 'y', calculate: "datum['y']" },
+ ]);
+ expect(vegaLiteSpec.spec.data.values).toEqual(data);
+ expect(vegaLiteSpec.spec.mark).toEqual({
+ opacity: 0.75,
+ size: 8,
+ type: 'circle',
+ });
+ expect(vegaLiteSpec.spec.encoding.color).toEqual({
+ field: 'the-color-field',
+ scale: {
+ range: COLOR_RANGE_NOMINAL,
+ },
+ type: 'nominal',
+ });
+ expect(vegaLiteSpec.spec.encoding.tooltip).toEqual([
+ { field: 'x', type: 'quantitative' },
+ { field: 'y', type: 'quantitative' },
+ ]);
+ });
+});
diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts
index ee93074d9e034..9e0834dd8b922 100644
--- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts
+++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts
@@ -24,12 +24,12 @@ export const OUTLIER_SCORE_FIELD = 'outlier_score';
const SCATTERPLOT_SIZE = 125;
-const DEFAULT_COLOR = euiPaletteColorBlind()[0];
-const COLOR_OUTLIER = euiPaletteNegative(2)[1];
-const COLOR_RANGE_NOMINAL = euiPaletteColorBlind({ rotations: 2 });
-const COLOR_RANGE_QUANTITATIVE = euiPalettePositive(5);
+export const DEFAULT_COLOR = euiPaletteColorBlind()[0];
+export const COLOR_OUTLIER = euiPaletteNegative(2)[1];
+export const COLOR_RANGE_NOMINAL = euiPaletteColorBlind({ rotations: 2 });
+export const COLOR_RANGE_QUANTITATIVE = euiPalettePositive(5);
-const getColorSpec = (
+export const getColorSpec = (
euiTheme: typeof euiThemeLight,
outliers = true,
color?: string,
@@ -72,10 +72,13 @@ export const getScatterplotMatrixVegaLiteSpec = (
calculate: `datum['${column}']`,
as: column,
}));
- transform.push({
- calculate: `datum['${resultsField}.${OUTLIER_SCORE_FIELD}']`,
- as: OUTLIER_SCORE_FIELD,
- });
+
+ if (resultsField !== undefined) {
+ transform.push({
+ calculate: `datum['${resultsField}.${OUTLIER_SCORE_FIELD}']`,
+ as: OUTLIER_SCORE_FIELD,
+ });
+ }
return {
$schema: 'https://vega.github.io/schema/vega-lite/v4.17.0.json',
From 8d5dd1de63d44b42db7acb8ba6fd0080cee0eeed Mon Sep 17 00:00:00 2001
From: ymao1
Date: Mon, 21 Dec 2020 10:11:19 -0500
Subject: [PATCH 008/108] [Alerting] Updating wording on "Notify" dropdown
(#86453)
* Updating wording
* PR fixes
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../sections/alert_form/alert_notify_when.tsx | 29 ++++++++++++++-----
1 file changed, 22 insertions(+), 7 deletions(-)
diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx
index da872484dda4a..eeb8b9f77a9b8 100644
--- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx
@@ -29,13 +29,18 @@ const DEFAULT_NOTIFY_WHEN_VALUE: AlertNotifyWhenType = 'onActionGroupChange';
const NOTIFY_WHEN_OPTIONS: Array> = [
{
value: 'onActionGroupChange',
- inputDisplay: 'Run only on status change',
+ inputDisplay: i18n.translate(
+ 'xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.display',
+ {
+ defaultMessage: 'Only on status change.',
+ }
+ ),
'data-test-subj': 'onActionGroupChange',
dropdownDisplay: (
@@ -52,13 +57,18 @@ const NOTIFY_WHEN_OPTIONS: Array> = [
},
{
value: 'onActiveAlert',
- inputDisplay: 'Run every time alert is active',
+ inputDisplay: i18n.translate(
+ 'xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActiveAlert.display',
+ {
+ defaultMessage: 'Every time alert is active',
+ }
+ ),
'data-test-subj': 'onActiveAlert',
dropdownDisplay: (
@@ -75,13 +85,18 @@ const NOTIFY_WHEN_OPTIONS: Array> = [
},
{
value: 'onThrottleInterval',
- inputDisplay: 'Set a custom action interval',
+ inputDisplay: i18n.translate(
+ 'xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onThrottleInterval.display',
+ {
+ defaultMessage: 'On a custom action interval',
+ }
+ ),
'data-test-subj': 'onThrottleInterval',
dropdownDisplay: (
@@ -143,7 +158,7 @@ export const AlertNotifyWhen = ({
<>
{' '}
Date: Mon, 21 Dec 2020 08:18:32 -0700
Subject: [PATCH 009/108] [Security Solution] Fixing jest warnings and errors
(#86532)
---
.../components/case_action_bar/index.tsx | 20 ++++++-------
.../event_details/event_details.test.tsx | 30 +++++++++++--------
.../events_viewer/events_viewer.test.tsx | 9 +++++-
.../exceptions/builder/entry_item.test.tsx | 30 +++++++++++--------
.../components/last_updated/index.test.tsx | 14 ++++++---
.../ml/score/anomaly_score.test.tsx | 5 ++--
.../ml/score/anomaly_scores.test.tsx | 5 ++--
.../score/create_descriptions_list.test.tsx | 14 ++++-----
.../properties/use_create_timeline.tsx | 4 +--
.../timeline/query_bar/index.test.tsx | 9 +++---
10 files changed, 83 insertions(+), 57 deletions(-)
diff --git a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx
index 3bad7b7870a53..3a65ea724d764 100644
--- a/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/case_action_bar/index.tsx
@@ -101,17 +101,17 @@ const CaseActionBarComponent: React.FC = ({
+
+
+
+ {i18n.SYNC_ALERTS}
+
+
+
+
+
+
-
-
-
- {i18n.SYNC_ALERTS}
-
-
-
-
-
-
{
@@ -36,18 +37,21 @@ describe('EventDetails', () => {
isAlert: true,
};
- const wrapper = mount(
-
-
-
- );
-
- const alertsWrapper = mount(
-
-
-
- );
-
+ let wrapper: ReactWrapper;
+ let alertsWrapper: ReactWrapper;
+ beforeAll(async () => {
+ wrapper = mount(
+
+
+
+ ) as ReactWrapper;
+ alertsWrapper = mount(
+
+
+
+ ) as ReactWrapper;
+ await waitFor(() => wrapper.update());
+ });
describe('rendering', () => {
test('should match snapshot', () => {
const shallowWrap = shallow();
diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
index 5e5bdebffa182..423b3566e4eb5 100644
--- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx
@@ -30,6 +30,13 @@ jest.mock('../../../timelines/components/graph_overlay', () => ({
GraphOverlay: jest.fn(() => ),
}));
+jest.mock('@elastic/eui', () => {
+ const original = jest.requireActual('@elastic/eui');
+ return {
+ ...original,
+ useDataGridColumnSorting: jest.fn(),
+ };
+});
jest.mock('../../../timelines/containers', () => ({
useTimelineEvents: jest.fn(),
}));
@@ -84,7 +91,7 @@ const eventsViewerDefaultProps = {
sort: [
{
columnId: 'foo',
- sortDirection: 'none' as SortDirection,
+ sortDirection: 'asc' as SortDirection,
},
],
scopeId: SourcererScopeName.timeline,
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx
index 60b2f079c869b..ba894e0410b04 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.test.tsx
@@ -25,6 +25,7 @@ import {
} from '../../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks';
import { getFoundListSchemaMock } from '../../../../../../lists/common/schemas/response/found_list_schema.mock';
import { getEmptyValue } from '../../empty_value';
+import { waitFor } from '@testing-library/dom';
// mock out lists hook
const mockStart = jest.fn();
@@ -583,7 +584,7 @@ describe('BuilderEntryItem', () => {
);
});
- test('it invokes "setErrorsExist" when user touches value input and leaves empty', () => {
+ test('it invokes "setErrorsExist" when user touches value input and leaves empty', async () => {
const mockSetErrorExists = jest.fn();
wrapper = mount(
{
/>
);
- ((wrapper.find(EuiComboBox).at(2).props() as unknown) as {
- onBlur: () => void;
- }).onBlur();
+ await waitFor(() => {
+ ((wrapper.find(EuiComboBox).at(2).props() as unknown) as {
+ onBlur: () => void;
+ }).onBlur();
+ });
expect(mockSetErrorExists).toHaveBeenCalledWith(true);
});
- test('it invokes "setErrorsExist" when invalid value inputted for field value input', () => {
+ test('it invokes "setErrorsExist" when invalid value inputted for field value input', async () => {
const mockSetErrorExists = jest.fn();
wrapper = mount(
{
setErrorsExist={mockSetErrorExists}
/>
);
- ((wrapper.find(EuiComboBox).at(2).props() as unknown) as {
- onBlur: () => void;
- }).onBlur();
- // Invalid input because field type is number
- ((wrapper.find(EuiComboBox).at(2).props() as unknown) as {
- onSearchChange: (arg: string) => void;
- }).onSearchChange('hellooo');
+ await waitFor(() => {
+ ((wrapper.find(EuiComboBox).at(2).props() as unknown) as {
+ onBlur: () => void;
+ }).onBlur();
+
+ // Invalid input because field type is number
+ ((wrapper.find(EuiComboBox).at(2).props() as unknown) as {
+ onSearchChange: (arg: string) => void;
+ }).onSearchChange('hellooo');
+ });
expect(mockSetErrorExists).toHaveBeenCalledWith(true);
});
diff --git a/x-pack/plugins/security_solution/public/common/components/last_updated/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/last_updated/index.test.tsx
index db42794448c53..31ba5530f6082 100644
--- a/x-pack/plugins/security_solution/public/common/components/last_updated/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/last_updated/index.test.tsx
@@ -8,12 +8,18 @@ import { mount } from 'enzyme';
import { I18nProvider } from '@kbn/i18n/react';
import { LastUpdatedAt } from './';
+jest.mock('@kbn/i18n/react', () => {
+ const originalModule = jest.requireActual('@kbn/i18n/react');
+ const FormattedRelative = jest.fn();
+ FormattedRelative.mockImplementation(() => '2 minutes ago');
+
+ return {
+ ...originalModule,
+ FormattedRelative,
+ };
+});
describe('LastUpdatedAt', () => {
- beforeEach(() => {
- Date.now = jest.fn().mockReturnValue(1603995369774);
- });
-
test('it renders correct relative time', () => {
const wrapper = mount(
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx
index cb10c61302d3c..745bd486524ad 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx
@@ -14,6 +14,7 @@ import { mockAnomalies } from '../mock';
import { TestProviders } from '../../../mock/test_providers';
import { useMountAppended } from '../../../utils/use_mount_appended';
import { Anomalies } from '../types';
+import { waitFor } from '@testing-library/dom';
const startDate: string = '2020-07-07T08:20:18.966Z';
const endDate: string = '3000-01-01T00:00:00.000Z';
@@ -57,7 +58,7 @@ describe('anomaly_scores', () => {
expect(wrapper.find('[data-test-subj="anomaly-description-list"]').exists()).toEqual(false);
});
- test('show a popover on a mouse click', () => {
+ test('show a popover on a mouse click', async () => {
const wrapper = mount(
{
);
wrapper.find('[data-test-subj="anomaly-score-popover"]').first().simulate('click');
- wrapper.update();
+ await waitFor(() => wrapper.update());
expect(wrapper.find('[data-test-subj="anomaly-description-list"]').exists()).toEqual(true);
});
});
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx
index 52151f217e01a..edbb3528a098a 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx
@@ -15,6 +15,7 @@ import { TestProviders } from '../../../mock/test_providers';
import { getEmptyValue } from '../../empty_value';
import { Anomalies } from '../types';
import { useMountAppended } from '../../../utils/use_mount_appended';
+import { waitFor } from '@testing-library/dom';
const startDate: string = '2020-07-07T08:20:18.966Z';
const endDate: string = '3000-01-01T00:00:00.000Z';
@@ -121,7 +122,7 @@ describe('anomaly_scores', () => {
expect(wrapper.find('[data-test-subj="anomaly-description-list"]').exists()).toEqual(false);
});
- test('showing a popover on a mouse click', () => {
+ test('showing a popover on a mouse click', async () => {
const wrapper = mount(
{
);
wrapper.find('[data-test-subj="anomaly-score-popover"]').first().simulate('click');
- wrapper.update();
+ await waitFor(() => wrapper.update());
expect(wrapper.find('[data-test-subj="anomaly-description-list"]').exists()).toEqual(true);
});
});
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/create_descriptions_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/create_descriptions_list.test.tsx
index e9dd5f922e26a..695b4dd8caea4 100644
--- a/x-pack/plugins/security_solution/public/common/components/ml/score/create_descriptions_list.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/ml/score/create_descriptions_list.test.tsx
@@ -10,6 +10,7 @@ import { mockAnomalies } from '../mock';
import { createDescriptionList } from './create_description_list';
import { EuiDescriptionList } from '@elastic/eui';
import { Anomaly } from '../types';
+import { waitFor } from '@testing-library/dom';
jest.mock('../../../lib/kibana');
@@ -38,7 +39,7 @@ describe('create_description_list', () => {
expect(wrapper).toMatchSnapshot();
});
- test('it calls the narrow date range function on click', () => {
+ test('it calls the narrow date range function on click', async () => {
const wrapper = mount(
{
.find('[data-test-subj="anomaly-description-narrow-range-link"]')
.first()
.simulate('click');
- wrapper.update();
+ await waitFor(() => wrapper.update());
expect(narrowDateRange.mock.calls.length).toBe(1);
});
- test('it should the narrow date range with the score', () => {
+ test('it should the narrow date range with the score', async () => {
const wrapper = mount(
{
.find('[data-test-subj="anomaly-description-narrow-range-link"]')
.first()
.simulate('click');
- wrapper.update();
+ await waitFor(() => wrapper.update());
const expected: Anomaly = {
detectorIndex: 0,
@@ -119,7 +120,7 @@ describe('create_description_list', () => {
expect(narrowDateRange.mock.calls[0][0]).toEqual(expected);
});
- test('it should call the narrow date range with the interval', () => {
+ test('it should call the narrow date range with the interval', async () => {
const wrapper = mount(
{
.find('[data-test-subj="anomaly-description-narrow-range-link"]')
.first()
.simulate('click');
- wrapper.update();
-
+ await waitFor(() => wrapper.update());
expect(narrowDateRange.mock.calls[0][1]).toEqual('hours');
});
});
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx
index 12845477e0f39..96187329a21c6 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx
@@ -123,13 +123,13 @@ export const useCreateTimelineButton = ({ timelineId, timelineType, closeGearMen
};
const dataTestSubjPrefix =
timelineType === TimelineType.template ? `template-timeline-new` : `timeline-new`;
-
+ const { fill: noThanks, ...propsWithoutFill } = buttonProps;
return outline ? (
{title}
) : (
-
+
{title}
);
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx
index 1226dabe48559..959504249e127 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.test.tsx
@@ -23,6 +23,7 @@ import {
getDataProviderFilter,
TIMELINE_FILTER_DROP_AREA,
} from './index';
+import { waitFor } from '@testing-library/dom';
const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
@@ -181,7 +182,7 @@ describe('Timeline QueryBar ', () => {
});
describe('#onSavedQuery', () => {
- test('is only reference that changed when dataProviders props get updated', () => {
+ test('is only reference that changed when dataProviders props get updated', async () => {
const Proxy = (props: QueryBarTimelineComponentProps) => (
@@ -213,13 +214,13 @@ describe('Timeline QueryBar ', () => {
const onSavedQueryRef = queryBarProps.onSavedQuery;
wrapper.setProps({ dataProviders: mockDataProviders.slice(1, 0) });
- wrapper.update();
+ await waitFor(() => wrapper.update());
expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery);
expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery);
});
- test('is only reference that changed when savedQueryId props get updated', () => {
+ test('is only reference that changed when savedQueryId props get updated', async () => {
const Proxy = (props: QueryBarTimelineComponentProps) => (
@@ -253,7 +254,7 @@ describe('Timeline QueryBar ', () => {
wrapper.setProps({
savedQueryId: 'new',
});
- wrapper.update();
+ await waitFor(() => wrapper.update());
expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery);
expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery);
From 664a7553d23adc22e17110887f5b4ccbb505c004 Mon Sep 17 00:00:00 2001
From: Wylie Conlon
Date: Mon, 21 Dec 2020 11:19:25 -0500
Subject: [PATCH 010/108] [Lens] Refactor param editor to use layers (#86499)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../dimension_panel/dimension_editor.tsx | 33 +-
.../calculations/moving_average.tsx | 13 +-
.../definitions/date_histogram.test.tsx | 458 +++++++++---------
.../operations/definitions/date_histogram.tsx | 12 +-
.../definitions/filters/filters.test.tsx | 190 ++++----
.../definitions/filters/filters.tsx | 10 +-
.../operations/definitions/index.ts | 13 +-
.../definitions/last_value.test.tsx | 152 +++---
.../operations/definitions/last_value.tsx | 18 +-
.../definitions/ranges/ranges.test.tsx | 434 +++++++----------
.../operations/definitions/ranges/ranges.tsx | 56 +--
.../operations/definitions/terms/index.tsx | 46 +-
.../definitions/terms/terms.test.tsx | 274 +++++------
.../operations/layer_helpers.test.ts | 46 +-
.../operations/layer_helpers.ts | 51 +-
15 files changed, 808 insertions(+), 998 deletions(-)
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
index df3b769acf850..c655fc18ab5fa 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx
@@ -107,7 +107,15 @@ export function DimensionEditor(props: DimensionEditorProps) {
layerId,
currentIndexPattern,
hideGrouping,
+ dateRange,
} = props;
+ const services = {
+ data: props.data,
+ uiSettings: props.uiSettings,
+ savedObjectsClient: props.savedObjectsClient,
+ http: props.http,
+ storage: props.storage,
+ };
const { fieldByOperation, operationWithoutField } = operationSupportMatrix;
const setStateWrapper = (layer: IndexPatternLayer) => {
@@ -346,17 +354,13 @@ export function DimensionEditor(props: DimensionEditorProps) {
{!currentFieldIsInvalid && !incompleteInfo && selectedColumn && ParamEditor && (
<>
>
)}
@@ -407,12 +411,15 @@ export function DimensionEditor(props: DimensionEditorProps) {
selectedColumn={selectedColumn}
onChange={(newFormat) => {
setState(
- updateColumnParam({
+ mergeLayer({
state,
layerId,
- currentColumn: selectedColumn,
- paramName: 'format',
- value: newFormat,
+ newLayer: updateColumnParam({
+ layer: state.layers[layerId],
+ columnId,
+ paramName: 'format',
+ value: newFormat,
+ }),
})
);
}}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx
index 522899662fbd1..59d5924b9a370 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/moving_average.tsx
@@ -111,10 +111,10 @@ export const movingAverageOperation: OperationDefinition<
};
function MovingAverageParamEditor({
- state,
- setState,
+ layer,
+ updateLayer,
currentColumn,
- layerId,
+ columnId,
}: ParamEditorProps) {
const [inputValue, setInputValue] = useState(String(currentColumn.params.window));
@@ -124,11 +124,10 @@ function MovingAverageParamEditor({
return;
}
const inputNumber = Number(inputValue);
- setState(
+ updateLayer(
updateColumnParam({
- state,
- layerId,
- currentColumn,
+ layer,
+ columnId,
paramName: 'window',
value: inputNumber,
})
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx
index e40b9ccf89c1a..eadcf8384b1dd 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx
@@ -5,19 +5,19 @@
*/
import React from 'react';
-import { DateHistogramIndexPatternColumn } from './date_histogram';
+import type { DateHistogramIndexPatternColumn } from './date_histogram';
import { dateHistogramOperation } from './index';
import { shallow } from 'enzyme';
import { EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
-import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public';
-import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
+import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public';
+import type { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { UI_SETTINGS } from '../../../../../../../src/plugins/data/public';
import {
dataPluginMock,
getCalculateAutoTimeExpression,
} from '../../../../../../../src/plugins/data/public/mocks';
import { createMockedIndexPattern } from '../../mocks';
-import { IndexPatternPrivateState } from '../../types';
+import type { IndexPatternLayer, IndexPattern } from '../../types';
import { getFieldByNameFactory } from '../../pure_helpers';
const dataStart = dataPluginMock.createStartContract();
@@ -29,6 +29,60 @@ dataStart.search.aggs.calculateAutoTimeExpression = getCalculateAutoTimeExpressi
}
);
+const indexPattern1: IndexPattern = {
+ id: '1',
+ title: 'Mock Indexpattern',
+ timeFieldName: 'timestamp',
+ hasRestrictions: false,
+ fields: [
+ {
+ name: 'timestamp',
+ displayName: 'timestampLabel',
+ type: 'date',
+ esTypes: ['date'],
+ aggregatable: true,
+ searchable: true,
+ },
+ ],
+
+ getFieldByName: getFieldByNameFactory([
+ {
+ name: 'timestamp',
+ displayName: 'timestampLabel',
+ type: 'date',
+ esTypes: ['date'],
+ aggregatable: true,
+ searchable: true,
+ },
+ ]),
+};
+
+const indexPattern2: IndexPattern = {
+ id: '2',
+ title: 'Mock Indexpattern 2',
+ hasRestrictions: false,
+ fields: [
+ {
+ name: 'other_timestamp',
+ displayName: 'other_timestamp',
+ type: 'date',
+ esTypes: ['date'],
+ aggregatable: true,
+ searchable: true,
+ },
+ ],
+ getFieldByName: getFieldByNameFactory([
+ {
+ name: 'other_timestamp',
+ displayName: 'other_timestamp',
+ type: 'date',
+ esTypes: ['date'],
+ aggregatable: true,
+ searchable: true,
+ },
+ ]),
+};
+
const defaultOptions = {
storage: {} as IStorageWrapper,
uiSettings: {} as IUiSettingsClient,
@@ -39,150 +93,47 @@ const defaultOptions = {
},
data: dataStart,
http: {} as HttpSetup,
+ indexPattern: indexPattern1,
};
describe('date_histogram', () => {
- let state: IndexPatternPrivateState;
+ let layer: IndexPatternLayer;
const InlineOptions = dateHistogramOperation.paramEditor!;
beforeEach(() => {
- state = {
- indexPatternRefs: [],
- existingFields: {},
- currentIndexPatternId: '1',
- isFirstExistenceFetch: false,
- indexPatterns: {
- 1: {
- id: '1',
- title: 'Mock Indexpattern',
- timeFieldName: 'timestamp',
- hasRestrictions: false,
- fields: [
- {
- name: 'timestamp',
- displayName: 'timestampLabel',
- type: 'date',
- esTypes: ['date'],
- aggregatable: true,
- searchable: true,
- },
- ],
+ layer = {
+ indexPatternId: '1',
+ columnOrder: ['col1'],
+ columns: {
+ col1: {
+ label: 'Value of timestamp',
+ dataType: 'date',
+ isBucketed: true,
- getFieldByName: getFieldByNameFactory([
- {
- name: 'timestamp',
- displayName: 'timestampLabel',
- type: 'date',
- esTypes: ['date'],
- aggregatable: true,
- searchable: true,
- },
- ]),
- },
- 2: {
- id: '2',
- title: 'Mock Indexpattern 2',
- hasRestrictions: false,
- fields: [
- {
- name: 'other_timestamp',
- displayName: 'other_timestamp',
- type: 'date',
- esTypes: ['date'],
- aggregatable: true,
- searchable: true,
- },
- ],
- getFieldByName: getFieldByNameFactory([
- {
- name: 'other_timestamp',
- displayName: 'other_timestamp',
- type: 'date',
- esTypes: ['date'],
- aggregatable: true,
- searchable: true,
- },
- ]),
- },
- },
- layers: {
- first: {
- indexPatternId: '1',
- columnOrder: ['col1'],
- columns: {
- col1: {
- label: 'Value of timestamp',
- dataType: 'date',
- isBucketed: true,
-
- // Private
- operationType: 'date_histogram',
- params: {
- interval: '42w',
- },
- sourceField: 'timestamp',
- },
- },
- },
- second: {
- indexPatternId: '2',
- columnOrder: ['col2'],
- columns: {
- col2: {
- label: 'Value of timestamp',
- dataType: 'date',
- isBucketed: true,
-
- // Private
- operationType: 'date_histogram',
- params: {
- interval: 'd',
- },
- sourceField: 'other_timestamp',
- },
- },
- },
- third: {
- indexPatternId: '1',
- columnOrder: ['col1'],
- columns: {
- col1: {
- label: 'Value of timestamp',
- dataType: 'date',
- isBucketed: true,
-
- // Private
- operationType: 'date_histogram',
- params: {
- interval: 'auto',
- },
- sourceField: 'timestamp',
- },
+ // Private
+ operationType: 'date_histogram',
+ params: {
+ interval: '42w',
},
+ sourceField: 'timestamp',
},
},
};
});
- function stateWithInterval(interval: string) {
+ function layerWithInterval(interval: string) {
return ({
- ...state,
- layers: {
- ...state.layers,
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col1: {
- ...state.layers.first.columns.col1,
- params: {
- interval,
- },
- },
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col1: {
+ ...layer.columns.col1,
+ params: {
+ interval,
},
},
},
- } as unknown) as IndexPatternPrivateState;
+ } as unknown) as IndexPatternLayer;
}
describe('buildColumn', () => {
@@ -246,9 +197,9 @@ describe('date_histogram', () => {
describe('toEsAggsFn', () => {
it('should reflect params correctly', () => {
const esAggsFn = dateHistogramOperation.toEsAggsFn(
- state.layers.first.columns.col1 as DateHistogramIndexPatternColumn,
+ layer.columns.col1 as DateHistogramIndexPatternColumn,
'col1',
- state.indexPatterns['1']
+ indexPattern1
);
expect(esAggsFn).toEqual(
expect.objectContaining({
@@ -263,10 +214,10 @@ describe('date_histogram', () => {
it('should not use normalized es interval for rollups', () => {
const esAggsFn = dateHistogramOperation.toEsAggsFn(
- state.layers.first.columns.col1 as DateHistogramIndexPatternColumn,
+ layer.columns.col1 as DateHistogramIndexPatternColumn,
'col1',
{
- ...state.indexPatterns['1'],
+ ...indexPattern1,
fields: [
{
name: 'timestamp',
@@ -465,15 +416,14 @@ describe('date_histogram', () => {
describe('param editor', () => {
it('should render current value', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
@@ -482,15 +432,34 @@ describe('date_histogram', () => {
});
it('should render current value for other index pattern', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
+
+ const secondLayer: IndexPatternLayer = {
+ indexPatternId: '2',
+ columnOrder: ['col2'],
+ columns: {
+ col2: {
+ label: 'Value of timestamp',
+ dataType: 'date',
+ isBucketed: true,
+
+ // Private
+ operationType: 'date_histogram',
+ params: {
+ interval: 'd',
+ },
+ sourceField: 'other_timestamp',
+ },
+ },
+ };
const instance = shallow(
);
@@ -499,14 +468,33 @@ describe('date_histogram', () => {
});
it('should render disabled switch and no time interval control for auto interval', () => {
+ const thirdLayer: IndexPatternLayer = {
+ indexPatternId: '1',
+ columnOrder: ['col1'],
+ columns: {
+ col1: {
+ label: 'Value of timestamp',
+ dataType: 'date',
+ isBucketed: true,
+
+ // Private
+ operationType: 'date_histogram',
+ params: {
+ interval: 'auto',
+ },
+ sourceField: 'timestamp',
+ },
+ },
+ };
+
const instance = shallow(
);
expect(instance.find('[data-test-subj="lensDateHistogramValue"]').exists()).toBeFalsy();
@@ -515,35 +503,52 @@ describe('date_histogram', () => {
});
it('should allow switching to manual interval', () => {
- const setStateSpy = jest.fn();
+ const thirdLayer: IndexPatternLayer = {
+ indexPatternId: '1',
+ columnOrder: ['col1'],
+ columns: {
+ col1: {
+ label: 'Value of timestamp',
+ dataType: 'date',
+ isBucketed: true,
+
+ // Private
+ operationType: 'date_histogram',
+ params: {
+ interval: 'auto',
+ },
+ sourceField: 'timestamp',
+ },
+ },
+ };
+
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
instance.find(EuiSwitch).prop('onChange')!({
target: { checked: true },
} as EuiSwitchEvent);
- expect(setStateSpy).toHaveBeenCalled();
- const newState = setStateSpy.mock.calls[0][0];
- expect(newState).toHaveProperty('layers.third.columns.col1.params.interval', '30d');
+ expect(updateLayerSpy).toHaveBeenCalled();
+ const newLayer = updateLayerSpy.mock.calls[0][0];
+ expect(newLayer).toHaveProperty('columns.col1.params.interval', '30d');
});
it('should force calendar values to 1', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
instance.find('[data-test-subj="lensDateHistogramValue"]').prop('onChange')!({
@@ -551,67 +556,63 @@ describe('date_histogram', () => {
value: '2',
},
} as React.ChangeEvent);
- expect(setStateSpy).toHaveBeenCalledWith(stateWithInterval('1w'));
+ expect(updateLayerSpy).toHaveBeenCalledWith(layerWithInterval('1w'));
});
it('should display error if an invalid interval is specified', () => {
- const setStateSpy = jest.fn();
- const testState = stateWithInterval('4quid');
+ const updateLayerSpy = jest.fn();
+ const testLayer = layerWithInterval('4quid');
const instance = shallow(
);
expect(instance.find('[data-test-subj="lensDateHistogramError"]').exists()).toBeTruthy();
});
it('should not display error if interval value is blank', () => {
- const setStateSpy = jest.fn();
- const testState = stateWithInterval('d');
+ const updateLayerSpy = jest.fn();
+ const testLayer = layerWithInterval('d');
const instance = shallow(
);
expect(instance.find('[data-test-subj="lensDateHistogramError"]').exists()).toBeFalsy();
});
it('should display error if interval value is 0', () => {
- const setStateSpy = jest.fn();
- const testState = stateWithInterval('0d');
+ const updateLayerSpy = jest.fn();
+ const testLayer = layerWithInterval('0d');
const instance = shallow(
);
expect(instance.find('[data-test-subj="lensDateHistogramError"]').exists()).toBeTruthy();
});
it('should update the unit', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
instance.find('[data-test-subj="lensDateHistogramUnit"]').prop('onChange')!({
@@ -619,21 +620,20 @@ describe('date_histogram', () => {
value: 'd',
},
} as React.ChangeEvent);
- expect(setStateSpy).toHaveBeenCalledWith(stateWithInterval('42d'));
+ expect(updateLayerSpy).toHaveBeenCalledWith(layerWithInterval('42d'));
});
it('should update the value', () => {
- const setStateSpy = jest.fn();
- const testState = stateWithInterval('42d');
+ const updateLayerSpy = jest.fn();
+ const testLayer = layerWithInterval('42d');
const instance = shallow(
);
instance.find('[data-test-subj="lensDateHistogramValue"]').prop('onChange')!({
@@ -641,50 +641,48 @@ describe('date_histogram', () => {
value: '9',
},
} as React.ChangeEvent);
- expect(setStateSpy).toHaveBeenCalledWith(stateWithInterval('9d'));
+ expect(updateLayerSpy).toHaveBeenCalledWith(layerWithInterval('9d'));
});
it('should not render options if they are restricted', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
+
+ const indexPattern = {
+ ...indexPattern1,
+ fields: [
+ {
+ ...indexPattern1.fields[0],
+ aggregationRestrictions: {
+ date_histogram: {
+ agg: 'date_histogram',
+ time_zone: 'UTC',
+ calendar_interval: '1h',
+ },
+ },
+ },
+ ],
+ getFieldByName: getFieldByNameFactory([
+ {
+ ...indexPattern1.fields[0],
+ aggregationRestrictions: {
+ date_histogram: {
+ agg: 'date_histogram',
+ time_zone: 'UTC',
+ calendar_interval: '1h',
+ },
+ },
+ },
+ ]),
+ };
+
const instance = shallow(
);
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx
index c48b9dad6c0d5..cdd1ccad96a99 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.tsx
@@ -143,12 +143,8 @@ export const dateHistogramOperation: OperationDefinition<
extended_bounds: JSON.stringify({}),
}).toAst();
},
- paramEditor: ({ state, setState, currentColumn, layerId, dateRange, data }) => {
- const field =
- currentColumn &&
- state.indexPatterns[state.layers[layerId].indexPatternId].getFieldByName(
- currentColumn.sourceField
- );
+ paramEditor: ({ layer, columnId, currentColumn, updateLayer, dateRange, data, indexPattern }) => {
+ const field = currentColumn && indexPattern.getFieldByName(currentColumn.sourceField);
const intervalIsRestricted =
field!.aggregationRestrictions && field!.aggregationRestrictions.date_histogram;
@@ -167,14 +163,14 @@ export const dateHistogramOperation: OperationDefinition<
const value = ev.target.checked
? data.search.aggs.calculateAutoTimeExpression({ from: fromDate, to: toDate }) || '1h'
: autoInterval;
- setState(updateColumnParam({ state, layerId, currentColumn, paramName: 'interval', value }));
+ updateLayer(updateColumnParam({ layer, columnId, paramName: 'interval', value }));
}
const setInterval = (newInterval: typeof interval) => {
const isCalendarInterval = calendarOnlyIntervals.has(newInterval.unit);
const value = `${isCalendarInterval ? '1' : newInterval.value}${newInterval.unit || 'd'}`;
- setState(updateColumnParam({ state, layerId, currentColumn, paramName: 'interval', value }));
+ updateLayer(updateColumnParam({ layer, columnId, paramName: 'interval', value }));
};
return (
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx
index bb8e52ba443a2..cf57c35f6f68b 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.test.tsx
@@ -7,12 +7,13 @@
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
-import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public';
-import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
+import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public';
+import type { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks';
-import { FiltersIndexPatternColumn } from '.';
+import type { FiltersIndexPatternColumn } from '.';
import { filtersOperation } from '../index';
-import { IndexPatternPrivateState } from '../../../types';
+import type { IndexPatternLayer } from '../../../types';
+import { createMockedIndexPattern } from '../../../mocks';
import { FilterPopover } from './filter_popover';
const defaultProps = {
@@ -22,6 +23,7 @@ const defaultProps = {
dateRange: { fromDate: 'now-1d', toDate: 'now' },
data: dataPluginMock.createStartContract(),
http: {} as HttpSetup,
+ indexPattern: createMockedIndexPattern(),
};
// mocking random id generator function
@@ -38,49 +40,40 @@ jest.mock('@elastic/eui', () => {
});
describe('filters', () => {
- let state: IndexPatternPrivateState;
+ let layer: IndexPatternLayer;
const InlineOptions = filtersOperation.paramEditor!;
beforeEach(() => {
- state = {
- indexPatternRefs: [],
- indexPatterns: {},
- existingFields: {},
- currentIndexPatternId: '1',
- isFirstExistenceFetch: false,
- layers: {
- first: {
- indexPatternId: '1',
- columnOrder: ['col1', 'col2'],
- columns: {
- col1: {
- label: 'filters',
- dataType: 'document',
- operationType: 'filters',
- scale: 'ordinal',
- isBucketed: true,
- params: {
- filters: [
- {
- input: { query: 'bytes >= 1', language: 'kuery' },
- label: 'More than one',
- },
- {
- input: { query: 'src : 2', language: 'kuery' },
- label: '',
- },
- ],
+ layer = {
+ indexPatternId: '1',
+ columnOrder: ['col1', 'col2'],
+ columns: {
+ col1: {
+ label: 'filters',
+ dataType: 'document',
+ operationType: 'filters',
+ scale: 'ordinal',
+ isBucketed: true,
+ params: {
+ filters: [
+ {
+ input: { query: 'bytes >= 1', language: 'kuery' },
+ label: 'More than one',
},
- },
- col2: {
- label: 'Count',
- dataType: 'number',
- isBucketed: false,
- sourceField: 'Records',
- operationType: 'count',
- },
+ {
+ input: { query: 'src : 2', language: 'kuery' },
+ label: '',
+ },
+ ],
},
},
+ col2: {
+ label: 'Count',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'Records',
+ operationType: 'count',
+ },
},
};
});
@@ -88,9 +81,9 @@ describe('filters', () => {
describe('toEsAggsFn', () => {
it('should reflect params correctly', () => {
const esAggsFn = filtersOperation.toEsAggsFn(
- state.layers.first.columns.col1 as FiltersIndexPatternColumn,
+ layer.columns.col1 as FiltersIndexPatternColumn,
'col1',
- state.indexPatterns['1']
+ createMockedIndexPattern()
);
expect(esAggsFn).toEqual(
expect.objectContaining({
@@ -133,15 +126,14 @@ describe('filters', () => {
}));
it('should update state when changing a filter', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = mount(
);
@@ -156,34 +148,29 @@ describe('filters', () => {
});
});
instance.update();
- expect(setStateSpy).toHaveBeenCalledWith({
- ...state,
- layers: {
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col1: {
- ...state.layers.first.columns.col1,
- params: {
- filters: [
- {
- input: {
- query: 'dest : 5',
- language: 'lucene',
- },
- label: 'Dest5',
- },
- {
- input: {
- language: 'kuery',
- query: 'src : 2',
- },
- label: '',
- },
- ],
+ expect(updateLayerSpy).toHaveBeenCalledWith({
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col1: {
+ ...layer.columns.col1,
+ params: {
+ filters: [
+ {
+ input: {
+ query: 'dest : 5',
+ language: 'lucene',
+ },
+ label: 'Dest5',
},
- },
+ {
+ input: {
+ language: 'kuery',
+ query: 'src : 2',
+ },
+ label: '',
+ },
+ ],
},
},
},
@@ -192,15 +179,14 @@ describe('filters', () => {
describe('Modify filters', () => {
it('should correctly show existing filters ', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = mount(
);
expect(
@@ -218,15 +204,14 @@ describe('filters', () => {
});
it('should remove filter', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = mount(
);
@@ -234,27 +219,22 @@ describe('filters', () => {
.find('[data-test-subj="lns-customBucketContainer-remove"]')
.at(2)
.simulate('click');
- expect(setStateSpy).toHaveBeenCalledWith({
- ...state,
- layers: {
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col1: {
- ...state.layers.first.columns.col1,
- params: {
- filters: [
- {
- input: {
- language: 'kuery',
- query: 'bytes >= 1',
- },
- label: 'More than one',
- },
- ],
+ expect(updateLayerSpy).toHaveBeenCalledWith({
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col1: {
+ ...layer.columns.col1,
+ params: {
+ filters: [
+ {
+ input: {
+ language: 'kuery',
+ query: 'bytes >= 1',
+ },
+ label: 'More than one',
},
- },
+ ],
},
},
},
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx
index b6c0b565f9565..8382aef7cc34f 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx
@@ -128,16 +128,14 @@ export const filtersOperation: OperationDefinition {
- const indexPattern = state.indexPatterns[state.layers[layerId].indexPatternId];
+ paramEditor: ({ layer, columnId, currentColumn, indexPattern, updateLayer, data }) => {
const filters = currentColumn.params.filters;
const setFilters = (newFilters: Filter[]) =>
- setState(
+ updateLayer(
updateColumnParam({
- state,
- layerId,
- currentColumn,
+ layer,
+ columnId,
paramName: 'filters',
value: newFilters,
})
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts
index 1f19b4e770313..6431dac7b381d 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts
@@ -36,12 +36,7 @@ import { countOperation, CountIndexPatternColumn } from './count';
import { lastValueOperation, LastValueIndexPatternColumn } from './last_value';
import { OperationMetadata } from '../../../types';
import type { BaseIndexPatternColumn, ReferenceBasedIndexPatternColumn } from './column_types';
-import {
- IndexPatternPrivateState,
- IndexPattern,
- IndexPatternField,
- IndexPatternLayer,
-} from '../../types';
+import { IndexPattern, IndexPatternField, IndexPatternLayer } from '../../types';
import { DateRange } from '../../../../common';
import { ExpressionAstFunction } from '../../../../../../../src/plugins/expressions/public';
import { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public';
@@ -115,10 +110,10 @@ export {
*/
export interface ParamEditorProps {
currentColumn: C;
- state: IndexPatternPrivateState;
- setState: (newState: IndexPatternPrivateState) => void;
+ layer: IndexPatternLayer;
+ updateLayer: (newLayer: IndexPatternLayer) => void;
columnId: string;
- layerId: string;
+ indexPattern: IndexPattern;
uiSettings: IUiSettingsClient;
storage: IStorageWrapper;
savedObjectsClient: SavedObjectsClientContract;
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx
index 6c896adfce9b1..817958aee5490 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx
@@ -13,7 +13,7 @@ import { dataPluginMock } from '../../../../../../../src/plugins/data/public/moc
import { createMockedIndexPattern } from '../../mocks';
import { LastValueIndexPatternColumn } from './last_value';
import { lastValueOperation } from './index';
-import { IndexPatternPrivateState, IndexPattern, IndexPatternLayer } from '../../types';
+import type { IndexPattern, IndexPatternLayer } from '../../types';
const defaultProps = {
storage: {} as IStorageWrapper,
@@ -22,52 +22,41 @@ const defaultProps = {
dateRange: { fromDate: 'now-1d', toDate: 'now' },
data: dataPluginMock.createStartContract(),
http: {} as HttpSetup,
+ indexPattern: {
+ ...createMockedIndexPattern(),
+ hasRestrictions: false,
+ } as IndexPattern,
};
describe('last_value', () => {
- let state: IndexPatternPrivateState;
+ let layer: IndexPatternLayer;
const InlineOptions = lastValueOperation.paramEditor!;
beforeEach(() => {
- const indexPattern = createMockedIndexPattern();
- state = {
- indexPatternRefs: [],
- indexPatterns: {
- '1': {
- ...indexPattern,
- hasRestrictions: false,
- } as IndexPattern,
- },
- existingFields: {},
- currentIndexPatternId: '1',
- isFirstExistenceFetch: false,
- layers: {
- first: {
- indexPatternId: '1',
- columnOrder: ['col1', 'col2'],
- columns: {
- col1: {
- label: 'Top value of category',
- dataType: 'string',
- isBucketed: true,
- operationType: 'terms',
- params: {
- orderBy: { type: 'alphabetical' },
- size: 3,
- orderDirection: 'asc',
- },
- sourceField: 'category',
- },
- col2: {
- label: 'Last value of a',
- dataType: 'number',
- isBucketed: false,
- sourceField: 'a',
- operationType: 'last_value',
- params: {
- sortField: 'datefield',
- },
- },
+ layer = {
+ indexPatternId: '1',
+ columnOrder: ['col1', 'col2'],
+ columns: {
+ col1: {
+ label: 'Top value of category',
+ dataType: 'string',
+ isBucketed: true,
+ operationType: 'terms',
+ params: {
+ orderBy: { type: 'alphabetical' },
+ size: 3,
+ orderDirection: 'asc',
+ },
+ sourceField: 'category',
+ },
+ col2: {
+ label: 'Last value of a',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'a',
+ operationType: 'last_value',
+ params: {
+ sortField: 'datefield',
},
},
},
@@ -76,7 +65,7 @@ describe('last_value', () => {
describe('toEsAggsFn', () => {
it('should reflect params correctly', () => {
- const lastValueColumn = state.layers.first.columns.col2 as LastValueIndexPatternColumn;
+ const lastValueColumn = layer.columns.col2 as LastValueIndexPatternColumn;
const esAggsFn = lastValueOperation.toEsAggsFn(
{ ...lastValueColumn, params: { ...lastValueColumn.params } },
'col1',
@@ -345,15 +334,14 @@ describe('last_value', () => {
describe('param editor', () => {
it('should render current sortField', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
@@ -363,15 +351,14 @@ describe('last_value', () => {
});
it('should update state when changing sortField', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
@@ -380,20 +367,15 @@ describe('last_value', () => {
.find(EuiComboBox)
.prop('onChange')!([{ label: 'datefield2', value: 'datefield2' }]);
- expect(setStateSpy).toHaveBeenCalledWith({
- ...state,
- layers: {
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col2: {
- ...state.layers.first.columns.col2,
- params: {
- ...(state.layers.first.columns.col2 as LastValueIndexPatternColumn).params,
- sortField: 'datefield2',
- },
- },
+ expect(updateLayerSpy).toHaveBeenCalledWith({
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col2: {
+ ...layer.columns.col2,
+ params: {
+ ...(layer.columns.col2 as LastValueIndexPatternColumn).params,
+ sortField: 'datefield2',
},
},
},
@@ -403,10 +385,10 @@ describe('last_value', () => {
describe('getErrorMessage', () => {
let indexPattern: IndexPattern;
- let layer: IndexPatternLayer;
+ let errorLayer: IndexPatternLayer;
beforeEach(() => {
indexPattern = createMockedIndexPattern();
- layer = {
+ errorLayer = {
columns: {
col1: {
dataType: 'boolean',
@@ -423,53 +405,55 @@ describe('last_value', () => {
};
});
it('returns undefined if sourceField exists and sortField is of type date ', () => {
- expect(lastValueOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual(undefined);
+ expect(lastValueOperation.getErrorMessage!(errorLayer, 'col1', indexPattern)).toEqual(
+ undefined
+ );
});
it('shows error message if the sourceField does not exist in index pattern', () => {
- layer = {
- ...layer,
+ errorLayer = {
+ ...errorLayer,
columns: {
col1: {
- ...layer.columns.col1,
+ ...errorLayer.columns.col1,
sourceField: 'notExisting',
} as LastValueIndexPatternColumn,
},
};
- expect(lastValueOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual([
+ expect(lastValueOperation.getErrorMessage!(errorLayer, 'col1', indexPattern)).toEqual([
'Field notExisting was not found',
]);
});
it('shows error message if the sortField does not exist in index pattern', () => {
- layer = {
- ...layer,
+ errorLayer = {
+ ...errorLayer,
columns: {
col1: {
- ...layer.columns.col1,
+ ...errorLayer.columns.col1,
params: {
- ...layer.columns.col1.params,
+ ...errorLayer.columns.col1.params,
sortField: 'notExisting',
},
} as LastValueIndexPatternColumn,
},
};
- expect(lastValueOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual([
+ expect(lastValueOperation.getErrorMessage!(errorLayer, 'col1', indexPattern)).toEqual([
'Field notExisting was not found',
]);
});
it('shows error message if the sortField is not date', () => {
- layer = {
- ...layer,
+ errorLayer = {
+ ...errorLayer,
columns: {
col1: {
- ...layer.columns.col1,
+ ...errorLayer.columns.col1,
params: {
- ...layer.columns.col1.params,
+ ...errorLayer.columns.col1.params,
sortField: 'bytes',
},
} as LastValueIndexPatternColumn,
},
};
- expect(lastValueOperation.getErrorMessage!(layer, 'col1', indexPattern)).toEqual([
+ expect(lastValueOperation.getErrorMessage!(errorLayer, 'col1', indexPattern)).toEqual([
'Field bytes is not a date field and cannot be used for sorting',
]);
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx
index 4cb2d876c83a1..7b5aee860654a 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.tsx
@@ -186,12 +186,11 @@ export const lastValueOperation: OperationDefinition {
- const currentIndexPattern = state.indexPatterns[state.layers[layerId].indexPatternId];
- const dateFields = getDateFields(currentIndexPattern);
+ paramEditor: ({ layer, updateLayer, columnId, currentColumn, indexPattern }) => {
+ const dateFields = getDateFields(indexPattern);
const isSortFieldInvalid = !!getInvalidSortFieldMessage(
currentColumn.params.sortField,
- currentIndexPattern
+ indexPattern
);
return (
<>
@@ -228,11 +227,10 @@ export const lastValueOperation: OperationDefinition &
React.MouseEvent;
+const sourceField = 'MyField';
const defaultOptions = {
storage: {} as IStorageWrapper,
// need this for MAX_HISTOGRAM value
@@ -64,75 +65,64 @@ const defaultOptions = {
},
data: dataPluginMockValue,
http: {} as HttpSetup,
+ indexPattern: {
+ id: '1',
+ title: 'my_index_pattern',
+ hasRestrictions: false,
+ fields: [{ name: sourceField, type: 'number', displayName: sourceField }],
+ getFieldByName: getFieldByNameFactory([
+ { name: sourceField, type: 'number', displayName: sourceField },
+ ]),
+ },
};
describe('ranges', () => {
- let state: IndexPatternPrivateState;
+ let layer: IndexPatternLayer;
const InlineOptions = rangeOperation.paramEditor!;
- const sourceField = 'MyField';
const MAX_HISTOGRAM_VALUE = 100;
const GRANULARITY_DEFAULT_VALUE = (MAX_HISTOGRAM_VALUE - MIN_HISTOGRAM_BARS) / 2;
const GRANULARITY_STEP = (MAX_HISTOGRAM_VALUE - MIN_HISTOGRAM_BARS) / SLICES;
function setToHistogramMode() {
- const column = state.layers.first.columns.col1 as RangeIndexPatternColumn;
+ const column = layer.columns.col1 as RangeIndexPatternColumn;
column.dataType = 'number';
column.scale = 'interval';
column.params.type = MODES.Histogram;
}
function setToRangeMode() {
- const column = state.layers.first.columns.col1 as RangeIndexPatternColumn;
+ const column = layer.columns.col1 as RangeIndexPatternColumn;
column.dataType = 'string';
column.scale = 'ordinal';
column.params.type = MODES.Range;
}
- function getDefaultState(): IndexPatternPrivateState {
+ function getDefaultLayer(): IndexPatternLayer {
return {
- indexPatternRefs: [],
- indexPatterns: {
- '1': {
- id: '1',
- title: 'my_index_pattern',
- hasRestrictions: false,
- fields: [{ name: sourceField, type: 'number', displayName: sourceField }],
- getFieldByName: getFieldByNameFactory([
- { name: sourceField, type: 'number', displayName: sourceField },
- ]),
- },
- },
- existingFields: {},
- currentIndexPatternId: '1',
- isFirstExistenceFetch: false,
- layers: {
- first: {
- indexPatternId: '1',
- columnOrder: ['col1', 'col2'],
- columns: {
- // Start with the histogram type
- col1: {
- label: sourceField,
- dataType: 'number',
- operationType: 'range',
- scale: 'interval',
- isBucketed: true,
- sourceField,
- params: {
- type: MODES.Histogram,
- ranges: [{ from: 0, to: DEFAULT_INTERVAL, label: '' }],
- maxBars: 'auto',
- },
- },
- col2: {
- label: 'Count',
- dataType: 'number',
- isBucketed: false,
- sourceField: 'Records',
- operationType: 'count',
- },
+ indexPatternId: '1',
+ columnOrder: ['col1', 'col2'],
+ columns: {
+ // Start with the histogram type
+ col1: {
+ label: sourceField,
+ dataType: 'number',
+ operationType: 'range',
+ scale: 'interval',
+ isBucketed: true,
+ sourceField,
+ params: {
+ type: MODES.Histogram,
+ ranges: [{ from: 0, to: DEFAULT_INTERVAL, label: '' }],
+ maxBars: 'auto',
},
},
+ col2: {
+ label: 'Count',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'Records',
+ operationType: 'count',
+ },
},
};
}
@@ -142,7 +132,7 @@ describe('ranges', () => {
});
beforeEach(() => {
- state = getDefaultState();
+ layer = getDefaultLayer();
});
describe('toEsAggsFn', () => {
@@ -150,7 +140,7 @@ describe('ranges', () => {
it('should reflect params correctly', () => {
const esAggsFn = rangeOperation.toEsAggsFn(
- state.layers.first.columns.col1 as RangeIndexPatternColumn,
+ layer.columns.col1 as RangeIndexPatternColumn,
'col1',
{} as IndexPattern
);
@@ -189,10 +179,10 @@ describe('ranges', () => {
});
it('should set maxBars param if provided', () => {
- (state.layers.first.columns.col1 as RangeIndexPatternColumn).params.maxBars = 10;
+ (layer.columns.col1 as RangeIndexPatternColumn).params.maxBars = 10;
const esAggsFn = rangeOperation.toEsAggsFn(
- state.layers.first.columns.col1 as RangeIndexPatternColumn,
+ layer.columns.col1 as RangeIndexPatternColumn,
'col1',
{} as IndexPattern
);
@@ -211,7 +201,7 @@ describe('ranges', () => {
setToRangeMode();
const esAggsFn = rangeOperation.toEsAggsFn(
- state.layers.first.columns.col1 as RangeIndexPatternColumn,
+ layer.columns.col1 as RangeIndexPatternColumn,
'col1',
{} as IndexPattern
);
@@ -225,12 +215,12 @@ describe('ranges', () => {
it('should include custom labels', () => {
setToRangeMode();
- (state.layers.first.columns.col1 as RangeIndexPatternColumn).params.ranges = [
+ (layer.columns.col1 as RangeIndexPatternColumn).params.ranges = [
{ from: 0, to: 100, label: 'customlabel' },
];
const esAggsFn = rangeOperation.toEsAggsFn(
- state.layers.first.columns.col1 as RangeIndexPatternColumn,
+ layer.columns.col1 as RangeIndexPatternColumn,
'col1',
{} as IndexPattern
);
@@ -276,36 +266,30 @@ describe('ranges', () => {
describe('paramEditor', () => {
describe('Modify intervals in basic mode', () => {
beforeEach(() => {
- state = getDefaultState();
+ layer = getDefaultLayer();
});
it('should start update the state with the default maxBars value', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
mount(
);
- expect(setStateSpy).toHaveBeenCalledWith({
- ...state,
- layers: {
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col1: {
- ...state.layers.first.columns.col1,
- params: {
- ...state.layers.first.columns.col1.params,
- maxBars: GRANULARITY_DEFAULT_VALUE,
- },
- },
+ expect(updateLayerSpy).toHaveBeenCalledWith({
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col1: {
+ ...layer.columns.col1,
+ params: {
+ ...layer.columns.col1.params,
+ maxBars: GRANULARITY_DEFAULT_VALUE,
},
},
},
@@ -313,16 +297,15 @@ describe('ranges', () => {
});
it('should update state when changing Max bars number', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = mount(
);
@@ -343,20 +326,15 @@ describe('ranges', () => {
jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4);
});
- expect(setStateSpy).toHaveBeenCalledWith({
- ...state,
- layers: {
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col1: {
- ...state.layers.first.columns.col1,
- params: {
- ...state.layers.first.columns.col1.params,
- maxBars: MAX_HISTOGRAM_VALUE,
- },
- },
+ expect(updateLayerSpy).toHaveBeenCalledWith({
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col1: {
+ ...layer.columns.col1,
+ params: {
+ ...layer.columns.col1.params,
+ maxBars: MAX_HISTOGRAM_VALUE,
},
},
},
@@ -364,16 +342,15 @@ describe('ranges', () => {
});
it('should update the state using the plus or minus buttons by the step amount', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = mount(
);
@@ -389,20 +366,15 @@ describe('ranges', () => {
jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4);
});
- expect(setStateSpy).toHaveBeenCalledWith({
- ...state,
- layers: {
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col1: {
- ...state.layers.first.columns.col1,
- params: {
- ...state.layers.first.columns.col1.params,
- maxBars: GRANULARITY_DEFAULT_VALUE - GRANULARITY_STEP,
- },
- },
+ expect(updateLayerSpy).toHaveBeenCalledWith({
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col1: {
+ ...layer.columns.col1,
+ params: {
+ ...layer.columns.col1.params,
+ maxBars: GRANULARITY_DEFAULT_VALUE - GRANULARITY_STEP,
},
},
},
@@ -417,25 +389,19 @@ describe('ranges', () => {
jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4);
});
- expect(setStateSpy).toHaveBeenCalledWith({
- ...state,
- layers: {
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col1: {
- ...state.layers.first.columns.col1,
- params: {
- ...state.layers.first.columns.col1.params,
- maxBars: GRANULARITY_DEFAULT_VALUE,
- },
- },
+ expect(updateLayerSpy).toHaveBeenCalledWith({
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col1: {
+ ...layer.columns.col1,
+ params: {
+ ...layer.columns.col1.params,
+ maxBars: GRANULARITY_DEFAULT_VALUE,
},
},
},
});
- // });
});
});
@@ -446,16 +412,15 @@ describe('ranges', () => {
beforeEach(() => setToRangeMode());
it('should show one range interval to start with', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = mount(
);
@@ -463,16 +428,15 @@ describe('ranges', () => {
});
it('should add a new range', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = mount(
);
@@ -495,23 +459,18 @@ describe('ranges', () => {
} as React.ChangeEvent);
jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4);
- expect(setStateSpy).toHaveBeenCalledWith({
- ...state,
- layers: {
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col1: {
- ...state.layers.first.columns.col1,
- params: {
- ...state.layers.first.columns.col1.params,
- ranges: [
- { from: 0, to: DEFAULT_INTERVAL, label: '' },
- { from: 50, to: Infinity, label: '' },
- ],
- },
- },
+ expect(updateLayerSpy).toHaveBeenCalledWith({
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col1: {
+ ...layer.columns.col1,
+ params: {
+ ...layer.columns.col1.params,
+ ranges: [
+ { from: 0, to: DEFAULT_INTERVAL, label: '' },
+ { from: 50, to: Infinity, label: '' },
+ ],
},
},
},
@@ -520,16 +479,15 @@ describe('ranges', () => {
});
it('should add a new range with custom label', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = mount(
);
@@ -552,23 +510,18 @@ describe('ranges', () => {
} as React.ChangeEvent);
jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4);
- expect(setStateSpy).toHaveBeenCalledWith({
- ...state,
- layers: {
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col1: {
- ...state.layers.first.columns.col1,
- params: {
- ...state.layers.first.columns.col1.params,
- ranges: [
- { from: 0, to: DEFAULT_INTERVAL, label: '' },
- { from: DEFAULT_INTERVAL, to: Infinity, label: 'customlabel' },
- ],
- },
- },
+ expect(updateLayerSpy).toHaveBeenCalledWith({
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col1: {
+ ...layer.columns.col1,
+ params: {
+ ...layer.columns.col1.params,
+ ranges: [
+ { from: 0, to: DEFAULT_INTERVAL, label: '' },
+ { from: DEFAULT_INTERVAL, to: Infinity, label: 'customlabel' },
+ ],
},
},
},
@@ -577,16 +530,15 @@ describe('ranges', () => {
});
it('should open a popover to edit an existing range', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = mount(
);
@@ -607,20 +559,15 @@ describe('ranges', () => {
} as React.ChangeEvent);
jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4);
- expect(setStateSpy).toHaveBeenCalledWith({
- ...state,
- layers: {
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col1: {
- ...state.layers.first.columns.col1,
- params: {
- ...state.layers.first.columns.col1.params,
- ranges: [{ from: 0, to: 50, label: '' }],
- },
- },
+ expect(updateLayerSpy).toHaveBeenCalledWith({
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col1: {
+ ...layer.columns.col1,
+ params: {
+ ...layer.columns.col1.params,
+ ranges: [{ from: 0, to: 50, label: '' }],
},
},
},
@@ -629,16 +576,15 @@ describe('ranges', () => {
});
it('should not accept invalid ranges', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = mount(
);
@@ -670,10 +616,10 @@ describe('ranges', () => {
});
it('should be possible to remove a range if multiple', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
// Add an extra range
- (state.layers.first.columns.col1 as RangeIndexPatternColumn).params.ranges.push({
+ (layer.columns.col1 as RangeIndexPatternColumn).params.ranges.push({
from: DEFAULT_INTERVAL,
to: 2 * DEFAULT_INTERVAL,
label: '',
@@ -682,11 +628,10 @@ describe('ranges', () => {
const instance = mount(
);
@@ -709,10 +654,10 @@ describe('ranges', () => {
});
it('should handle correctly open ranges when saved', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
// Add an extra open range:
- (state.layers.first.columns.col1 as RangeIndexPatternColumn).params.ranges.push({
+ (layer.columns.col1 as RangeIndexPatternColumn).params.ranges.push({
from: null,
to: null,
label: '',
@@ -721,11 +666,10 @@ describe('ranges', () => {
const instance = mount(
);
@@ -749,21 +693,21 @@ describe('ranges', () => {
});
it('should correctly handle the default formatter for the field', () => {
- const setStateSpy = jest.fn();
-
- // set a default formatter for the sourceField used
- state.indexPatterns['1'].fieldFormatMap = {
- MyField: { id: 'custom', params: {} },
- };
+ const updateLayerSpy = jest.fn();
const instance = mount(
);
@@ -773,15 +717,10 @@ describe('ranges', () => {
});
it('should correctly pick the dimension formatter for the field', () => {
- const setStateSpy = jest.fn();
-
- // set a default formatter for the sourceField used
- state.indexPatterns['1'].fieldFormatMap = {
- MyField: { id: 'custom', params: {} },
- };
+ const updateLayerSpy = jest.fn();
// now set a format on the range operation
- (state.layers.first.columns.col1 as RangeIndexPatternColumn).params.format = {
+ (layer.columns.col1 as RangeIndexPatternColumn).params.format = {
id: 'bytes',
params: { decimals: 0 },
};
@@ -789,11 +728,16 @@ describe('ranges', () => {
const instance = mount(
);
@@ -803,25 +747,24 @@ describe('ranges', () => {
});
it('should not update the state on mount', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
mount(
);
- expect(setStateSpy.mock.calls.length).toBe(0);
+ expect(updateLayerSpy.mock.calls.length).toBe(0);
});
it('should not reset formatters when switching between custom ranges and auto histogram', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
// now set a format on the range operation
- (state.layers.first.columns.col1 as RangeIndexPatternColumn).params.format = {
+ (layer.columns.col1 as RangeIndexPatternColumn).params.format = {
id: 'custom',
params: { decimals: 3 },
};
@@ -829,11 +772,10 @@ describe('ranges', () => {
const instance = mount(
);
@@ -842,7 +784,7 @@ describe('ranges', () => {
instance.find(EuiLink).first().prop('onClick')!({} as ReactMouseEvent);
});
- expect(setStateSpy.mock.calls[1][0].layers.first.columns.col1.params.format).toEqual({
+ expect(updateLayerSpy.mock.calls[1][0].columns.col1.params.format).toEqual({
id: 'custom',
params: { decimals: 3 },
});
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx
index 062e2afb8a5bf..2ba8f5febce5b 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx
@@ -16,7 +16,6 @@ import { RangeEditor } from './range_editor';
import { OperationDefinition } from '../index';
import { FieldBasedIndexPatternColumn } from '../column_types';
import { updateColumnParam } from '../../layer_helpers';
-import { mergeLayer } from '../../../state_helpers';
import { supportedFormats } from '../../../format_column';
import { MODES, AUTO_BARS, DEFAULT_INTERVAL, MIN_HISTOGRAM_BARS, SLICES } from './constants';
import { IndexPattern, IndexPatternField } from '../../../types';
@@ -173,8 +172,15 @@ export const rangeOperation: OperationDefinition {
- const indexPattern = state.indexPatterns[state.layers[layerId].indexPatternId];
+ paramEditor: ({
+ layer,
+ columnId,
+ currentColumn,
+ updateLayer,
+ indexPattern,
+ uiSettings,
+ data,
+ }) => {
const currentField = indexPattern.getFieldByName(currentColumn.sourceField);
const numberFormat = currentColumn.params.format;
const numberFormatterPattern =
@@ -198,11 +204,10 @@ export const rangeOperation: OperationDefinition {
- setState(
+ updateLayer(
updateColumnParam({
- state,
- layerId,
- currentColumn,
+ layer,
+ columnId,
paramName,
value,
})
@@ -217,29 +222,24 @@ export const rangeOperation: OperationDefinition isSortableByColumn(column))
- .map(([columnId, column]) => {
+ const orderOptions = Object.entries(layer.columns)
+ .filter(([sortId, column]) => isSortableByColumn(column))
+ .map(([sortId, column]) => {
return {
- value: toValue({ type: 'column', columnId }),
+ value: toValue({ type: 'column', columnId: sortId }),
text: column.label,
};
});
@@ -220,11 +219,10 @@ export const termsOperation: OperationDefinition {
- setState(
+ updateLayer(
updateColumnParam({
- state,
- layerId,
- currentColumn,
+ layer,
+ columnId,
paramName: 'size',
value,
})
@@ -263,11 +261,10 @@ export const termsOperation: OperationDefinition
- setState(
+ updateLayer(
updateColumnParam({
- state,
- layerId,
- currentColumn,
+ layer,
+ columnId,
paramName: 'otherBucket',
value: e.target.checked,
})
@@ -284,11 +281,10 @@ export const termsOperation: OperationDefinition
- setState(
+ updateLayer(
updateColumnParam({
- state,
- layerId,
- currentColumn,
+ layer,
+ columnId,
paramName: 'missingBucket',
value: e.target.checked,
})
@@ -312,11 +308,10 @@ export const termsOperation: OperationDefinition) =>
- setState(
+ updateLayer(
updateColumnParam({
- state,
- layerId,
- currentColumn,
+ layer,
+ columnId,
paramName: 'orderBy',
value: fromValue(e.target.value),
})
@@ -353,11 +348,10 @@ export const termsOperation: OperationDefinition) =>
- setState(
+ updateLayer(
updateColumnParam({
- state,
- layerId,
- currentColumn,
+ layer,
+ columnId,
paramName: 'orderDirection',
value: e.target.value as 'asc' | 'desc',
})
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx
index fc8f8534bcfb2..eb78bb3ffebff 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx
@@ -8,14 +8,14 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { shallow, mount } from 'enzyme';
import { EuiRange, EuiSelect, EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
-import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public';
-import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
+import type { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public';
+import type { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks';
import { createMockedIndexPattern } from '../../../mocks';
import { ValuesRangeInput } from './values_range_input';
-import { TermsIndexPatternColumn } from '.';
+import type { TermsIndexPatternColumn } from '.';
import { termsOperation } from '../index';
-import { IndexPatternPrivateState, IndexPattern, IndexPatternLayer } from '../../../types';
+import { IndexPattern, IndexPatternLayer } from '../../../types';
const defaultProps = {
storage: {} as IStorageWrapper,
@@ -24,48 +24,36 @@ const defaultProps = {
dateRange: { fromDate: 'now-1d', toDate: 'now' },
data: dataPluginMock.createStartContract(),
http: {} as HttpSetup,
+ indexPattern: createMockedIndexPattern(),
};
describe('terms', () => {
- let state: IndexPatternPrivateState;
+ let layer: IndexPatternLayer;
const InlineOptions = termsOperation.paramEditor!;
beforeEach(() => {
- state = {
- indexPatternRefs: [],
- indexPatterns: {
- '1': {
- hasRestrictions: false,
- } as IndexPattern,
- },
- existingFields: {},
- currentIndexPatternId: '1',
- isFirstExistenceFetch: false,
- layers: {
- first: {
- indexPatternId: '1',
- columnOrder: ['col1', 'col2'],
- columns: {
- col1: {
- label: 'Top value of category',
- dataType: 'string',
- isBucketed: true,
- operationType: 'terms',
- params: {
- orderBy: { type: 'alphabetical' },
- size: 3,
- orderDirection: 'asc',
- },
- sourceField: 'category',
- },
- col2: {
- label: 'Count',
- dataType: 'number',
- isBucketed: false,
- sourceField: 'Records',
- operationType: 'count',
- },
+ layer = {
+ indexPatternId: '1',
+ columnOrder: ['col1', 'col2'],
+ columns: {
+ col1: {
+ label: 'Top value of category',
+ dataType: 'string',
+ isBucketed: true,
+ operationType: 'terms',
+ params: {
+ orderBy: { type: 'alphabetical' },
+ size: 3,
+ orderDirection: 'asc',
},
+ sourceField: 'category',
+ },
+ col2: {
+ label: 'Count',
+ dataType: 'number',
+ isBucketed: false,
+ sourceField: 'Records',
+ operationType: 'count',
},
},
};
@@ -73,7 +61,7 @@ describe('terms', () => {
describe('toEsAggsFn', () => {
it('should reflect params correctly', () => {
- const termsColumn = state.layers.first.columns.col1 as TermsIndexPatternColumn;
+ const termsColumn = layer.columns.col1 as TermsIndexPatternColumn;
const esAggsFn = termsOperation.toEsAggsFn(
{ ...termsColumn, params: { ...termsColumn.params, otherBucket: true } },
'col1',
@@ -92,7 +80,7 @@ describe('terms', () => {
});
it('should not enable missing bucket if other bucket is not set', () => {
- const termsColumn = state.layers.first.columns.col1 as TermsIndexPatternColumn;
+ const termsColumn = layer.columns.col1 as TermsIndexPatternColumn;
const esAggsFn = termsOperation.toEsAggsFn(
{
...termsColumn,
@@ -370,7 +358,7 @@ describe('terms', () => {
it('should use the default size when there is an existing bucket', () => {
const termsColumn = termsOperation.buildColumn({
indexPattern: createMockedIndexPattern(),
- layer: state.layers.first,
+ layer,
field: {
aggregatable: true,
searchable: true,
@@ -526,15 +514,14 @@ describe('terms', () => {
describe('param editor', () => {
it('should render current other bucket value', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
@@ -546,15 +533,18 @@ describe('terms', () => {
});
it('should hide other bucket setting for rollups', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
@@ -562,15 +552,14 @@ describe('terms', () => {
});
it('should disable missing bucket setting as long as other bucket is not set', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
@@ -582,23 +571,22 @@ describe('terms', () => {
});
it('should enable missing bucket setting as long as other bucket is set', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
@@ -610,15 +598,14 @@ describe('terms', () => {
});
it('should update state when clicking other bucket toggle', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
@@ -631,20 +618,15 @@ describe('terms', () => {
},
} as EuiSwitchEvent);
- expect(setStateSpy).toHaveBeenCalledWith({
- ...state,
- layers: {
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col1: {
- ...state.layers.first.columns.col1,
- params: {
- ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params,
- otherBucket: true,
- },
- },
+ expect(updateLayerSpy).toHaveBeenCalledWith({
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col1: {
+ ...layer.columns.col1,
+ params: {
+ ...(layer.columns.col1 as TermsIndexPatternColumn).params,
+ otherBucket: true,
},
},
},
@@ -652,15 +634,14 @@ describe('terms', () => {
});
it('should render current order by value and options', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
@@ -675,15 +656,14 @@ describe('terms', () => {
});
it('should update state with the order by value', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
@@ -696,22 +676,17 @@ describe('terms', () => {
},
} as React.ChangeEvent);
- expect(setStateSpy).toHaveBeenCalledWith({
- ...state,
- layers: {
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col1: {
- ...state.layers.first.columns.col1,
- params: {
- ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params,
- orderBy: {
- type: 'column',
- columnId: 'col2',
- },
- },
+ expect(updateLayerSpy).toHaveBeenCalledWith({
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col1: {
+ ...layer.columns.col1,
+ params: {
+ ...(layer.columns.col1 as TermsIndexPatternColumn).params,
+ orderBy: {
+ type: 'column',
+ columnId: 'col2',
},
},
},
@@ -720,15 +695,14 @@ describe('terms', () => {
});
it('should render current order direction value and options', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
@@ -741,15 +715,14 @@ describe('terms', () => {
});
it('should update state with the order direction value', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = shallow(
);
@@ -762,20 +735,15 @@ describe('terms', () => {
},
} as React.ChangeEvent);
- expect(setStateSpy).toHaveBeenCalledWith({
- ...state,
- layers: {
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col1: {
- ...state.layers.first.columns.col1,
- params: {
- ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params,
- orderDirection: 'desc',
- },
- },
+ expect(updateLayerSpy).toHaveBeenCalledWith({
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col1: {
+ ...layer.columns.col1,
+ params: {
+ ...(layer.columns.col1 as TermsIndexPatternColumn).params,
+ orderDirection: 'desc',
},
},
},
@@ -783,15 +751,14 @@ describe('terms', () => {
});
it('should render current size value', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = mount(
);
@@ -799,15 +766,14 @@ describe('terms', () => {
});
it('should update state with the size value', () => {
- const setStateSpy = jest.fn();
+ const updateLayerSpy = jest.fn();
const instance = mount(
);
@@ -815,20 +781,15 @@ describe('terms', () => {
instance.find(ValuesRangeInput).prop('onChange')!(7);
});
- expect(setStateSpy).toHaveBeenCalledWith({
- ...state,
- layers: {
- first: {
- ...state.layers.first,
- columns: {
- ...state.layers.first.columns,
- col1: {
- ...state.layers.first.columns.col1,
- params: {
- ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params,
- size: 7,
- },
- },
+ expect(updateLayerSpy).toHaveBeenCalledWith({
+ ...layer,
+ columns: {
+ ...layer.columns,
+ col1: {
+ ...layer.columns.col1,
+ params: {
+ ...(layer.columns.col1 as TermsIndexPatternColumn).params,
+ size: 7,
},
},
},
@@ -837,7 +798,6 @@ describe('terms', () => {
});
describe('getErrorMessage', () => {
let indexPattern: IndexPattern;
- let layer: IndexPatternLayer;
beforeEach(() => {
indexPattern = createMockedIndexPattern();
layer = {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
index 93447053a6029..bb09474798fd4 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.test.ts
@@ -18,7 +18,7 @@ import { operationDefinitionMap, OperationType } from '../operations';
import { TermsIndexPatternColumn } from './definitions/terms';
import { DateHistogramIndexPatternColumn } from './definitions/date_histogram';
import { AvgIndexPatternColumn } from './definitions/metrics';
-import type { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from '../types';
+import type { IndexPattern, IndexPatternLayer } from '../types';
import { documentField } from '../document_field';
import { getFieldByNameFactory } from '../pure_helpers';
import { generateId } from '../../id_generator';
@@ -1266,31 +1266,19 @@ describe('state_helpers', () => {
sourceField: 'timestamp',
};
- const state: IndexPatternPrivateState = {
- indexPatternRefs: [],
- existingFields: {},
- indexPatterns: {},
- currentIndexPatternId: '1',
- isFirstExistenceFetch: false,
- layers: {
- first: {
+ expect(
+ updateColumnParam({
+ layer: {
indexPatternId: '1',
columnOrder: ['col1'],
columns: {
col1: currentColumn,
},
},
- },
- };
-
- expect(
- updateColumnParam({
- state,
- layerId: 'first',
- currentColumn,
+ columnId: 'col1',
paramName: 'interval',
value: 'M',
- }).layers.first.columns.col1
+ }).columns.col1
).toEqual({
...currentColumn,
params: { interval: 'M' },
@@ -1307,31 +1295,19 @@ describe('state_helpers', () => {
sourceField: 'bytes',
};
- const state: IndexPatternPrivateState = {
- indexPatternRefs: [],
- existingFields: {},
- indexPatterns: {},
- currentIndexPatternId: '1',
- isFirstExistenceFetch: false,
- layers: {
- first: {
+ expect(
+ updateColumnParam({
+ layer: {
indexPatternId: '1',
columnOrder: ['col1'],
columns: {
col1: currentColumn,
},
},
- },
- };
-
- expect(
- updateColumnParam({
- state,
- layerId: 'first',
- currentColumn,
+ columnId: 'col1',
paramName: 'format',
value: { id: 'bytes' },
- }).layers.first.columns.col1
+ }).columns.col1
).toEqual({
...currentColumn,
params: { format: { id: 'bytes' } },
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
index b16418d44ba33..1619ad907fffc 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts
@@ -13,14 +13,8 @@ import {
IndexPatternColumn,
RequiredReference,
} from './definitions';
-import type {
- IndexPattern,
- IndexPatternField,
- IndexPatternLayer,
- IndexPatternPrivateState,
-} from '../types';
+import type { IndexPattern, IndexPatternField, IndexPatternLayer } from '../types';
import { getSortScoreByPriority } from './operations';
-import { mergeLayer } from '../state_helpers';
import { generateId } from '../../id_generator';
import { ReferenceBasedIndexPatternColumn } from './definitions/column_types';
@@ -390,40 +384,29 @@ export function getMetricOperationTypes(field: IndexPatternField) {
}
export function updateColumnParam({
- state,
- layerId,
- currentColumn,
+ layer,
+ columnId,
paramName,
value,
}: {
- state: IndexPatternPrivateState;
- layerId: string;
- currentColumn: C;
+ layer: IndexPatternLayer;
+ columnId: string;
paramName: string;
value: unknown;
-}): IndexPatternPrivateState {
- const columnId = Object.entries(state.layers[layerId].columns).find(
- ([_columnId, column]) => column === currentColumn
- )![0];
-
- const layer = state.layers[layerId];
-
- return mergeLayer({
- state,
- layerId,
- newLayer: {
- columns: {
- ...layer.columns,
- [columnId]: {
- ...currentColumn,
- params: {
- ...currentColumn.params,
- [paramName]: value,
- },
+}): IndexPatternLayer {
+ return {
+ ...layer,
+ columns: {
+ ...layer.columns,
+ [columnId]: {
+ ...layer.columns[columnId],
+ params: {
+ ...layer.columns[columnId].params,
+ [paramName]: value,
},
},
- },
- });
+ } as Record,
+ };
}
function adjustColumnReferencesForChangedColumn(
From 5bcef6f82e9852a0ff73286381c11d520479ae49 Mon Sep 17 00:00:00 2001
From: James Gowdy
Date: Mon, 21 Dec 2020 16:31:56 +0000
Subject: [PATCH 011/108] [ML] Fix displaying of setup errors in recognizer
wizard (#86430)
* [ML] Fix displaying of setup errors in recognizer wizard
* add saved object error
* sharing error message extractor
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
x-pack/plugins/ml/common/index.ts | 1 +
x-pack/plugins/ml/common/types/modules.ts | 14 +++-----------
.../jobs/new_job/recognize/components/job_item.tsx | 5 +++--
.../recognize/components/kibana_objects.tsx | 3 ++-
.../plugins/uptime/public/state/api/ml_anomaly.ts | 3 ++-
5 files changed, 11 insertions(+), 15 deletions(-)
diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts
index d527a9a9780ad..85770c856ca84 100644
--- a/x-pack/plugins/ml/common/index.ts
+++ b/x-pack/plugins/ml/common/index.ts
@@ -8,3 +8,4 @@ export { SearchResponse7 } from './types/es_client';
export { ANOMALY_SEVERITY, ANOMALY_THRESHOLD, SEVERITY_COLORS } from './constants/anomalies';
export { getSeverityColor, getSeverityType } from './util/anomaly_utils';
export { composeValidators, patternValidator } from './util/validators';
+export { extractErrorMessage } from './util/errors';
diff --git a/x-pack/plugins/ml/common/types/modules.ts b/x-pack/plugins/ml/common/types/modules.ts
index 38f0c8cb9c117..3c5d784d4a9e3 100644
--- a/x-pack/plugins/ml/common/types/modules.ts
+++ b/x-pack/plugins/ml/common/types/modules.ts
@@ -5,6 +5,7 @@
*/
import { SavedObjectAttributes } from 'kibana/public';
import { Datafeed, Job } from './anomaly_detection_jobs';
+import { ErrorType } from '../util/errors';
export interface ModuleJob {
id: string;
@@ -63,22 +64,13 @@ export interface KibanaObjectResponse extends ResultItem {
error?: any;
}
-export interface SetupError {
- body: string;
- msg: string;
- path: string;
- query: {};
- response: string;
- statusCode: number;
-}
-
export interface DatafeedResponse extends ResultItem {
started: boolean;
- error?: SetupError;
+ error?: ErrorType;
}
export interface JobResponse extends ResultItem {
- error?: SetupError;
+ error?: ErrorType;
}
export interface DataRecognizerConfigResponse {
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx
index 8dab25bf492f4..7e972b010cdd7 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx
@@ -21,6 +21,7 @@ import { ModuleJobUI } from '../page';
import { SETUP_RESULTS_WIDTH } from './module_jobs';
import { tabColor } from '../../../../../../common/util/group_color_utils';
import { JobOverride } from '../../../../../../common/types/modules';
+import { extractErrorMessage } from '../../../../../../common/util/errors';
interface JobItemProps {
job: ModuleJobUI;
@@ -94,13 +95,13 @@ export const JobItem: FC = memo(
{setupResult && setupResult.error && (
- {setupResult.error.msg}
+ {extractErrorMessage(setupResult.error)}
)}
{datafeedResult && datafeedResult.error && (
- {datafeedResult.error.msg}
+ {extractErrorMessage(datafeedResult.error)}
)}
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx
index f8ca7926ad7d6..18647e2093757 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx
@@ -18,6 +18,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { KibanaObjectUi } from '../page';
+import { extractErrorMessage } from '../../../../../../common/util/errors';
export interface KibanaObjectItemProps {
objectType: string;
@@ -57,7 +58,7 @@ export const KibanaObjects: FC = memo(
{success === false && error !== undefined && (
- {error.message}
+ {extractErrorMessage(error)}
)}
diff --git a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts
index fa3d7ed834a9c..5c0eb0bff07eb 100644
--- a/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts
+++ b/x-pack/plugins/uptime/public/state/api/ml_anomaly.ts
@@ -13,6 +13,7 @@ import {
JobExistResult,
MlCapabilitiesResponse,
} from '../../../../../plugins/ml/public';
+import { extractErrorMessage } from '../../../../../plugins/ml/common';
import {
CreateMLJobSuccess,
DeleteJobResults,
@@ -62,7 +63,7 @@ export const createMLJob = async ({
};
} else {
const { error } = jobResponse;
- throw new Error(error?.msg);
+ throw new Error(extractErrorMessage(error));
}
} else {
return null;
From 8ecd726b5c3dc1c4b2283f0d4f8db5695f8b3cc2 Mon Sep 17 00:00:00 2001
From: Marco Liberati
Date: Mon, 21 Dec 2020 17:32:19 +0100
Subject: [PATCH 012/108] [Lens] Fix esaggs missing default time field scenario
in Lens (#85754)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
src/plugins/data/common/index_patterns/index_pattern.stub.ts | 1 +
src/plugins/data/common/search/aggs/buckets/date_histogram.ts | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/plugins/data/common/index_patterns/index_pattern.stub.ts b/src/plugins/data/common/index_patterns/index_pattern.stub.ts
index e7384e09494aa..c47a8b605ada3 100644
--- a/src/plugins/data/common/index_patterns/index_pattern.stub.ts
+++ b/src/plugins/data/common/index_patterns/index_pattern.stub.ts
@@ -25,6 +25,7 @@ export const stubIndexPattern: IIndexPattern = {
fields: stubFields,
title: 'logstash-*',
timeFieldName: '@timestamp',
+ getTimeField: () => ({ name: '@timestamp', type: 'date' }),
};
export const stubIndexPatternWithFields: IIndexPattern = {
diff --git a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts
index ba79a4264d603..6b7f1522d19ad 100644
--- a/src/plugins/data/common/search/aggs/buckets/date_histogram.ts
+++ b/src/plugins/data/common/search/aggs/buckets/date_histogram.ts
@@ -149,7 +149,7 @@ export const getDateHistogramBucketAgg = ({
type: 'field',
filterFieldTypes: KBN_FIELD_TYPES.DATE,
default(agg: IBucketDateHistogramAggConfig) {
- return agg.getIndexPattern().timeFieldName;
+ return agg.getIndexPattern().getTimeField?.()?.name;
},
onChange(agg: IBucketDateHistogramAggConfig) {
if (isAutoInterval(get(agg, 'params.interval')) && !agg.fieldIsTimeField()) {
From 805fc4b95cd08b66eb247aa185709eef4fa88718 Mon Sep 17 00:00:00 2001
From: Chris Roberson
Date: Mon, 21 Dec 2020 11:50:28 -0500
Subject: [PATCH 013/108] Fix more sorts by adding unmapped_type (#85837)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../server/kibana_monitoring/collectors/lib/fetch_es_usage.ts | 1 +
.../kibana_monitoring/collectors/lib/fetch_license_type.ts | 1 +
.../monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts | 2 +-
.../plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts | 1 +
.../server/lib/alerts/fetch_missing_monitoring_data.ts | 1 +
x-pack/plugins/monitoring/server/lib/beats/get_beats.ts | 2 +-
6 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.ts
index de0a1b8f99d96..9f054f4f95adf 100644
--- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.ts
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_es_usage.ts
@@ -58,6 +58,7 @@ export async function fetchESUsage(
{
timestamp: {
order: 'desc',
+ unmapped_type: 'long',
},
},
],
diff --git a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts
index f7b8b72637b1f..4e132eac1fba4 100644
--- a/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts
+++ b/x-pack/plugins/monitoring/server/kibana_monitoring/collectors/lib/fetch_license_type.ts
@@ -27,6 +27,7 @@ export async function fetchLicenseType(
{
timestamp: {
order: 'desc',
+ unmapped_type: 'long',
},
},
],
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts
index 5055017051816..a5331beeae067 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts
@@ -70,7 +70,7 @@ describe('fetchLegacyAlerts', () => {
],
body: {
size,
- sort: [{ timestamp: { order: 'desc' } }],
+ sort: [{ timestamp: { order: 'desc', unmapped_type: 'long' } }],
query: {
bool: {
minimum_should_match: 1,
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts
index 0ea37b4ac4daa..c6416a992276e 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts
@@ -28,6 +28,7 @@ export async function fetchLegacyAlerts(
{
timestamp: {
order: 'desc',
+ unmapped_type: 'long',
},
},
],
diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts
index 30706a0b3c922..1bfe3a367fc95 100644
--- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts
+++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_missing_monitoring_data.ts
@@ -97,6 +97,7 @@ export async function fetchMissingMonitoringData(
{
timestamp: {
order: 'desc',
+ unmapped_type: 'long',
},
},
],
diff --git a/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts b/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts
index beda4334b4937..aa5ef81a8de33 100644
--- a/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts
+++ b/x-pack/plugins/monitoring/server/lib/beats/get_beats.ts
@@ -151,7 +151,7 @@ export async function getBeats(req: LegacyRequest, beatsIndexPattern: string, cl
inner_hits: {
name: 'earliest',
size: 1,
- sort: [{ 'beats_stats.timestamp': 'asc' }],
+ sort: [{ 'beats_stats.timestamp': { order: 'asc', unmapped_type: 'long' } }],
},
},
sort: [
From f349d0a398eb7944957b3fa45647be0b1fd62cb6 Mon Sep 17 00:00:00 2001
From: James Gowdy
Date: Mon, 21 Dec 2020 16:56:35 +0000
Subject: [PATCH 014/108] [ML] Fix cloning of partition field in per-partition
categorization jobs (#86635)
* [ML] Fix cloning of partition field in per-partition categorization jobs
* fixing cloned bucket span
---
.../job_creator/categorization_job_creator.ts | 59 ++++++++++++-------
.../new_job/common/job_creator/job_creator.ts | 2 +-
2 files changed, 40 insertions(+), 21 deletions(-)
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts
index a0f2832dc9f97..aaf20ff1571ee 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/categorization_job_creator.ts
@@ -160,26 +160,6 @@ export class CategorizationJobCreator extends JobCreator {
return this._categorizationAnalyzer;
}
- public cloneFromExistingJob(job: Job, datafeed: Datafeed) {
- this._overrideConfigs(job, datafeed);
- this.createdBy = CREATED_BY_LABEL.CATEGORIZATION;
- const detectors = getRichDetectors(job, datafeed, this.additionalFields, false);
-
- const dtr = detectors[0];
- if (detectors.length && dtr.agg !== null && dtr.field !== null) {
- this._detectorType =
- dtr.agg.id === ML_JOB_AGGREGATION.COUNT
- ? ML_JOB_AGGREGATION.COUNT
- : ML_JOB_AGGREGATION.RARE;
-
- const bs = job.analysis_config.bucket_span;
- this.setDetectorType(this._detectorType);
- // set the bucketspan back to the original value
- // as setDetectorType applies a default
- this.bucketSpan = bs;
- }
- }
-
public get categorizationPerPartitionField() {
return this._partitionFieldName;
}
@@ -204,4 +184,43 @@ export class CategorizationJobCreator extends JobCreator {
}
}
}
+
+ // override the setter and getter for the per-partition toggle
+ // so we can remove the partition field in the wizard when
+ // per-partition categorization is disabled.
+ public get perPartitionCategorization() {
+ return this._job_config.analysis_config.per_partition_categorization?.enabled === true;
+ }
+
+ public set perPartitionCategorization(enabled: boolean) {
+ this._initPerPartitionCategorization();
+ this._job_config.analysis_config.per_partition_categorization!.enabled = enabled;
+ if (enabled === false) {
+ this.categorizationPerPartitionField = null;
+ }
+ }
+
+ public cloneFromExistingJob(job: Job, datafeed: Datafeed) {
+ this._overrideConfigs(job, datafeed);
+ this.createdBy = CREATED_BY_LABEL.CATEGORIZATION;
+ const detectors = getRichDetectors(job, datafeed, this.additionalFields, false);
+
+ const dtr = detectors[0];
+ if (dtr !== undefined && dtr.agg !== null && dtr.field !== null) {
+ const detectorType =
+ dtr.agg.id === ML_JOB_AGGREGATION.COUNT
+ ? ML_JOB_AGGREGATION.COUNT
+ : ML_JOB_AGGREGATION.RARE;
+
+ const bs = job.analysis_config.bucket_span;
+ this.setDetectorType(detectorType);
+ if (dtr.partitionField !== null) {
+ this.categorizationPerPartitionField = dtr.partitionField.id;
+ }
+
+ // set the bucketspan back to the original value
+ // as setDetectorType applies a default
+ this.bucketSpan = bs;
+ }
+ }
}
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts
index ba03e86724a15..c3b4471269bb8 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts
@@ -631,7 +631,7 @@ export class JobCreator {
return JSON.stringify(this._datafeed_config, null, 2);
}
- private _initPerPartitionCategorization() {
+ protected _initPerPartitionCategorization() {
if (this._job_config.analysis_config.per_partition_categorization === undefined) {
this._job_config.analysis_config.per_partition_categorization = {};
}
From 1e9e0400a034fb003553234bb517a2385f66a10a Mon Sep 17 00:00:00 2001
From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
Date: Mon, 21 Dec 2020 11:59:03 -0500
Subject: [PATCH 015/108] Added docs for fleet UI extensions (#86654)
---
.../fleet/dev_docs/fleet_ui_extensions.md | 84 +++++++++++++++++++
1 file changed, 84 insertions(+)
create mode 100644 x-pack/plugins/fleet/dev_docs/fleet_ui_extensions.md
diff --git a/x-pack/plugins/fleet/dev_docs/fleet_ui_extensions.md b/x-pack/plugins/fleet/dev_docs/fleet_ui_extensions.md
new file mode 100644
index 0000000000000..a3d82356af14f
--- /dev/null
+++ b/x-pack/plugins/fleet/dev_docs/fleet_ui_extensions.md
@@ -0,0 +1,84 @@
+# Fleet UI Extensions
+
+Fleet's Kibana UI supports two types of UI extensions:
+
+- Custom UI components
+- Navigation redirection
+
+More information about these can be found below.
+
+
+## Custom UI Components
+
+Custom UI component extension can be registered with Fleet using the Plugin's `#start()` exposed interface, which contains a method named `registerExtension()`. In order to take advantage of these extension points, a Kibana plugin must set a dependency on Fleet so that the interface can be made available.
+
+Here is an example Security Solution's Endpoint takes advantage of this functionality to register several custom UI extensions:
+
+```typescript
+export class Plugin {
+ //...
+ start(core: CoreStart, startDep: StartDependencies) {
+ const { registerExtension } = startDep.fleet;
+
+ registerExtension({
+ package: 'endpoint',
+ view: 'package-policy-edit',
+ component: getLazyEndpointPolicyEditExtension(core, plugins),
+ });
+
+ registerExtension({
+ package: 'endpoint',
+ view: 'package-policy-create',
+ component: LazyEndpointPolicyCreateExtension,
+ });
+
+ registerExtension({
+ package: 'endpoint',
+ view: 'package-detail-custom',
+ component: getLazyEndpointPackageCustomExtension(core, plugins),
+ });
+ }
+ //...
+}
+```
+
+> The code above lives in `x-pack/plugins/security_solution/public/plugin.tsx`
+
+For a list of supported Fleet UI extensions, see the `UIExtensionPoint` and associated Union types defined here: `x-pack/plugins/fleet/public/applications/fleet/types/ui_extensions.ts`.
+
+
+
+
+## Navigation Redirection
+
+In order to support better user flows, some of Fleet's pages support changing the behaviour of the certain links and/or button clicks and to where they redirect if a user clicks on it. This type of UI extension does not need a Plugin level dependency on Fleet - it utilizes Route state via `react-router` along with Kibana's `core.application.navigateToApp()` method thus any kibana plugin can take advantage of it.
+
+Here is an example of how to create a link that redirects the user to Fleet's Agent Policy detail page with the Agent Enroll flyout opened, and once the user clicks "done", they are redirected back to the originating page:
+
+```typescript jsx
+const LinkToAgentEnroll = () => {
+ const { services } = useKibana();
+ const handleLinkClick = useCallback((ev) => {
+ ev.preventDefault();
+
+ const fleetPolicyPageRouteState: AgentPolicyDetailsDeployAgentAction = {
+ onDoneNavigateTo: [
+ 'my-app-plugin-id',
+ { path: 'the/page/I/am/currently/on' },
+ ],
+ };
+
+ services.application.navigateTo(
+ 'fleet',
+ {
+ path: '#/policies/123-some-uuid-321?openEnrollmentFlyout=true',
+ state: fleetPolicyPageRouteState
+ }
+ );
+ }, [services.application.navigateTo]);
+
+ return Enroll an agent
+}
+```
+
+For a list of supported Fleet pages, see the type `AnyIntraAppRouteState` and its associated Union types in `x-pack/plugins/fleet/public/applications/fleet/types/intra_app_route_state.ts`
From 256c8cdffb17f6030b657f18189b20e857a1dcf4 Mon Sep 17 00:00:00 2001
From: Jonathan Budzenski
Date: Mon, 21 Dec 2020 11:26:35 -0600
Subject: [PATCH 016/108] skip histogram brushing, #86602
---
test/functional/apps/discover/_discover.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts
index e52c33078029a..1b333c377f777 100644
--- a/test/functional/apps/discover/_discover.ts
+++ b/test/functional/apps/discover/_discover.ts
@@ -110,7 +110,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
- it('should modify the time range when the histogram is brushed', async function () {
+ it.skip('should modify the time range when the histogram is brushed', async function () {
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.discover.brushHistogram();
await PageObjects.discover.waitUntilSearchingHasFinished();
From 7de243e7fd328da7361eeb50b8cc97955babbb42 Mon Sep 17 00:00:00 2001
From: Dima Arnautov
Date: Mon, 21 Dec 2020 19:54:45 +0100
Subject: [PATCH 017/108] [ML] Fix time range adjustment for the swim lane
causing the infinite loop update (#86461)
* [ML] fix swim lane time selection adjustment
* [ML] fix adjustment
* [ML] fix tooManyBuckets condition
* [ML] fix typo
---
.../contexts/kibana/__mocks__/index.ts | 1 +
.../kibana/__mocks__/use_timefilter.ts | 40 ++++++
.../explorer_charts_container_service.js | 5 +-
.../explorer/hooks/use_selected_cells.test.ts | 133 ++++++++++++++++++
.../explorer/hooks/use_selected_cells.ts | 80 +++++------
.../application/routing/routes/explorer.tsx | 2 +-
6 files changed, 219 insertions(+), 42 deletions(-)
create mode 100644 x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/use_timefilter.ts
create mode 100644 x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.test.ts
diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/index.ts b/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/index.ts
index 7051abe6dc34e..dd405ddcc04fa 100644
--- a/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/index.ts
+++ b/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/index.ts
@@ -5,3 +5,4 @@
*/
export { useMlKibana } from './kibana_context';
+export { useTimefilter } from './use_timefilter';
diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/use_timefilter.ts b/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/use_timefilter.ts
new file mode 100644
index 0000000000000..949de51cf2c2d
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/contexts/kibana/__mocks__/use_timefilter.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { TimefilterContract } from '../../../../../../../../src/plugins/data/public';
+import { Observable } from 'rxjs';
+
+/**
+ * Copy from {@link '../../../../../../../../src/plugins/data/public/query/timefilter/timefilter_service.mock'}
+ */
+const timefilterMock: jest.Mocked = {
+ isAutoRefreshSelectorEnabled: jest.fn(),
+ isTimeRangeSelectorEnabled: jest.fn(),
+ isTimeTouched: jest.fn(),
+ getEnabledUpdated$: jest.fn(),
+ getTimeUpdate$: jest.fn(),
+ getRefreshIntervalUpdate$: jest.fn(),
+ getAutoRefreshFetch$: jest.fn(() => new Observable()),
+ getFetch$: jest.fn(),
+ getTime: jest.fn(),
+ setTime: jest.fn(),
+ setRefreshInterval: jest.fn(),
+ getRefreshInterval: jest.fn(),
+ getActiveBounds: jest.fn(),
+ disableAutoRefreshSelector: jest.fn(),
+ disableTimeRangeSelector: jest.fn(),
+ enableAutoRefreshSelector: jest.fn(),
+ enableTimeRangeSelector: jest.fn(),
+ getBounds: jest.fn(),
+ calculateBounds: jest.fn(),
+ createFilter: jest.fn(),
+ getRefreshIntervalDefaults: jest.fn(),
+ getTimeDefaults: jest.fn(),
+};
+
+export const useTimefilter = jest.fn(() => {
+ return timefilterMock;
+});
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
index 0f1b9c77afced..47087e776d6dd 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js
@@ -694,7 +694,10 @@ function calculateChartRange(
chartRange.min = chartRange.min + maxBucketSpanMs;
}
- if (chartRange.min > selectedEarliestMs || chartRange.max < selectedLatestMs) {
+ if (
+ (chartRange.min > selectedEarliestMs || chartRange.max < selectedLatestMs) &&
+ chartRange.max - chartRange.min < selectedLatestMs - selectedEarliestMs
+ ) {
tooManyBuckets = true;
}
diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.test.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.test.ts
new file mode 100644
index 0000000000000..08c8d11987f19
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.test.ts
@@ -0,0 +1,133 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import moment from 'moment';
+import { renderHook } from '@testing-library/react-hooks';
+import { useSelectedCells } from './use_selected_cells';
+import { ExplorerAppState } from '../../../../common/types/ml_url_generator';
+import { TimefilterContract } from '../../../../../../../src/plugins/data/public';
+
+import { useTimefilter } from '../../contexts/kibana';
+
+jest.mock('../../contexts/kibana');
+
+describe('useSelectedCells', () => {
+ test('should not set state when the cell selection is correct', () => {
+ (useTimefilter() as jest.Mocked).getBounds.mockReturnValue({
+ min: moment(1498824778 * 1000),
+ max: moment(1502366798 * 1000),
+ });
+
+ const urlState = {
+ mlExplorerSwimlane: {
+ selectedType: 'overall',
+ selectedLanes: ['Overall'],
+ selectedTimes: [1498780800, 1498867200],
+ showTopFieldValues: true,
+ viewByFieldName: 'apache2.access.remote_ip',
+ viewByFromPage: 1,
+ viewByPerPage: 10,
+ },
+ mlExplorerFilter: {},
+ } as ExplorerAppState;
+
+ const setUrlState = jest.fn();
+
+ const bucketInterval = 86400;
+
+ renderHook(() => useSelectedCells(urlState, setUrlState, bucketInterval));
+
+ expect(setUrlState).not.toHaveBeenCalled();
+ });
+
+ test('should reset cell selection when it is completely out of range', () => {
+ (useTimefilter() as jest.Mocked).getBounds.mockReturnValue({
+ min: moment(1501071178 * 1000),
+ max: moment(1502366798 * 1000),
+ });
+
+ const urlState = {
+ mlExplorerSwimlane: {
+ selectedType: 'overall',
+ selectedLanes: ['Overall'],
+ selectedTimes: [1498780800, 1498867200],
+ showTopFieldValues: true,
+ viewByFieldName: 'apache2.access.remote_ip',
+ viewByFromPage: 1,
+ viewByPerPage: 10,
+ },
+ mlExplorerFilter: {},
+ } as ExplorerAppState;
+
+ const setUrlState = jest.fn();
+
+ const bucketInterval = 86400;
+
+ const { result } = renderHook(() => useSelectedCells(urlState, setUrlState, bucketInterval));
+
+ expect(result.current[0]).toEqual({
+ lanes: ['Overall'],
+ showTopFieldValues: true,
+ times: [1498780800, 1498867200],
+ type: 'overall',
+ viewByFieldName: 'apache2.access.remote_ip',
+ });
+
+ expect(setUrlState).toHaveBeenCalledWith({
+ mlExplorerSwimlane: {
+ viewByFieldName: 'apache2.access.remote_ip',
+ viewByFromPage: 1,
+ viewByPerPage: 10,
+ },
+ });
+ });
+
+ test('should adjust cell selection time boundaries based on the main time range', () => {
+ (useTimefilter() as jest.Mocked).getBounds.mockReturnValue({
+ min: moment(1501071178 * 1000),
+ max: moment(1502366798 * 1000),
+ });
+
+ const urlState = {
+ mlExplorerSwimlane: {
+ selectedType: 'overall',
+ selectedLanes: ['Overall'],
+ selectedTimes: [1498780800, 1502366798],
+ showTopFieldValues: true,
+ viewByFieldName: 'apache2.access.remote_ip',
+ viewByFromPage: 1,
+ viewByPerPage: 10,
+ },
+ mlExplorerFilter: {},
+ } as ExplorerAppState;
+
+ const setUrlState = jest.fn();
+
+ const bucketInterval = 86400;
+
+ const { result } = renderHook(() => useSelectedCells(urlState, setUrlState, bucketInterval));
+
+ expect(result.current[0]).toEqual({
+ lanes: ['Overall'],
+ showTopFieldValues: true,
+ times: [1498780800, 1502366798],
+ type: 'overall',
+ viewByFieldName: 'apache2.access.remote_ip',
+ });
+
+ expect(setUrlState).toHaveBeenCalledWith({
+ mlExplorerSwimlane: {
+ selectedLanes: ['Overall'],
+ selectedTimes: [1500984778, 1502366798],
+ selectedType: 'overall',
+ showTopFieldValues: true,
+ viewByFieldName: 'apache2.access.remote_ip',
+ viewByFromPage: 1,
+ viewByPerPage: 10,
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
index 7a279afc22ae7..4ce828b0b7633 100644
--- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
+++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
@@ -5,7 +5,6 @@
*/
import { useCallback, useEffect, useMemo } from 'react';
-import { Duration } from 'moment';
import { SWIMLANE_TYPE } from '../explorer_constants';
import { AppStateSelectedCells } from '../explorer_utils';
import { ExplorerAppState } from '../../../../common/types/ml_url_generator';
@@ -14,10 +13,9 @@ import { useTimefilter } from '../../contexts/kibana';
export const useSelectedCells = (
appState: ExplorerAppState,
setAppState: (update: Partial) => void,
- bucketInterval: Duration | undefined
+ bucketIntervalInSeconds: number | undefined
): [AppStateSelectedCells | undefined, (swimlaneSelectedCells: AppStateSelectedCells) => void] => {
const timeFilter = useTimefilter();
-
const timeBounds = timeFilter.getBounds();
// keep swimlane selection, restore selectedCells from AppState
@@ -76,43 +74,45 @@ export const useSelectedCells = (
* Adjust cell selection with respect to the time boundaries.
* Reset it entirely when it out of range.
*/
- useEffect(() => {
- if (selectedCells?.times === undefined || bucketInterval === undefined) return;
-
- let [selectedFrom, selectedTo] = selectedCells.times;
-
- const rangeFrom = timeBounds.min!.unix();
- /**
- * Because each cell on the swim lane represent the fixed bucket interval,
- * the selection range could be outside of the time boundaries with
- * correction within the bucket interval.
- */
- const rangeTo = timeBounds.max!.unix() + bucketInterval.asSeconds();
-
- selectedFrom = Math.max(selectedFrom, rangeFrom);
-
- selectedTo = Math.min(selectedTo, rangeTo);
-
- const isSelectionOutOfRange = rangeFrom > selectedTo || rangeTo < selectedFrom;
-
- if (isSelectionOutOfRange) {
- // reset selection
- setSelectedCells();
- return;
- }
-
- if (selectedFrom !== rangeFrom || selectedTo !== rangeTo) {
- setSelectedCells({
- ...selectedCells,
- times: [selectedFrom, selectedTo],
- });
- }
- }, [
- timeBounds.min?.valueOf(),
- timeBounds.max?.valueOf(),
- selectedCells,
- bucketInterval?.asMilliseconds(),
- ]);
+ useEffect(
+ function adjustSwimLaneTimeSelection() {
+ if (selectedCells?.times === undefined || bucketIntervalInSeconds === undefined) return;
+
+ const [selectedFrom, selectedTo] = selectedCells.times;
+
+ /**
+ * Because each cell on the swim lane represent the fixed bucket interval,
+ * the selection range could be outside of the time boundaries with
+ * correction within the bucket interval.
+ */
+ const rangeFrom = timeBounds.min!.unix() - bucketIntervalInSeconds;
+ const rangeTo = timeBounds.max!.unix() + bucketIntervalInSeconds;
+
+ const resultFrom = Math.max(selectedFrom, rangeFrom);
+ const resultTo = Math.min(selectedTo, rangeTo);
+
+ const isSelectionOutOfRange = rangeFrom > resultTo || rangeTo < resultFrom;
+
+ if (isSelectionOutOfRange) {
+ // reset selection
+ setSelectedCells();
+ return;
+ }
+
+ if (selectedFrom === resultFrom && selectedTo === resultTo) {
+ // selection is correct, no need to adjust the range
+ return;
+ }
+
+ if (resultFrom !== rangeFrom || resultTo !== rangeTo) {
+ setSelectedCells({
+ ...selectedCells,
+ times: [resultFrom, resultTo],
+ });
+ }
+ },
+ [timeBounds.min?.unix(), timeBounds.max?.unix(), selectedCells, bucketIntervalInSeconds]
+ );
return [selectedCells, setSelectedCells];
};
diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
index 15ce3847f4d9c..0075846571cee 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
@@ -201,7 +201,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim
const [selectedCells, setSelectedCells] = useSelectedCells(
explorerUrlState,
setExplorerUrlState,
- explorerState?.swimlaneBucketInterval
+ explorerState?.swimlaneBucketInterval?.asSeconds()
);
useEffect(() => {
From 61a0b008029ef63bbaf5f50a4534cf2f5f1a34fd Mon Sep 17 00:00:00 2001
From: "Christiane (Tina) Heiligers"
Date: Mon, 21 Dec 2020 12:09:23 -0700
Subject: [PATCH 018/108] Migrates kql_telemetry usage collector es client
(#86585)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../usage_collector/fetch.test.ts | 70 ++++++++++---------
.../kql_telemetry/usage_collector/fetch.ts | 28 ++++----
2 files changed, 52 insertions(+), 46 deletions(-)
diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts
index 1794df7391cb0..038f340babb1f 100644
--- a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts
+++ b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.test.ts
@@ -18,7 +18,7 @@
*/
import { fetchProvider } from './fetch';
-import { LegacyAPICaller } from 'kibana/server';
+import { ElasticsearchClient } from 'kibana/server';
import { CollectorFetchContext } from 'src/plugins/usage_collection/server';
import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks';
@@ -30,7 +30,7 @@ jest.mock('../../../common', () => ({
}));
let fetch: ReturnType;
-let callCluster: LegacyAPICaller;
+let esClient: ElasticsearchClient;
let collectorFetchContext: CollectorFetchContext;
const collectorFetchContextMock = createCollectorFetchContextMock();
@@ -38,34 +38,33 @@ function setupMockCallCluster(
optCount: { optInCount?: number; optOutCount?: number } | null,
language: string | undefined | null
) {
- callCluster = (jest.fn((method, params) => {
- if (params && 'id' in params && params.id === 'kql-telemetry:kql-telemetry') {
- if (optCount === null) {
- return Promise.resolve({
+ function mockedEsGetMethod() {
+ if (optCount === null) {
+ return Promise.resolve({
+ body: {
_index: '.kibana_1',
_id: 'kql-telemetry:kql-telemetry',
found: false,
- });
- } else {
- return Promise.resolve({
+ },
+ });
+ } else {
+ return Promise.resolve({
+ body: {
_source: {
- 'kql-telemetry': {
- ...optCount,
- },
+ 'kql-telemetry': { ...optCount },
type: 'kql-telemetry',
updated_at: '2018-10-05T20:20:56.258Z',
},
- });
- }
- } else if (params && 'body' in params && params.body.query.term.type === 'config') {
- if (language === 'missingConfigDoc') {
- return Promise.resolve({
- hits: {
- hits: [],
- },
- });
- } else {
- return Promise.resolve({
+ },
+ });
+ }
+ }
+ function mockedEsSearchMethod() {
+ if (language === 'missingConfigDoc') {
+ return Promise.resolve({ body: { hits: { hits: [] } } });
+ } else {
+ return Promise.resolve({
+ body: {
hits: {
hits: [
{
@@ -77,12 +76,15 @@ function setupMockCallCluster(
},
],
},
- });
- }
+ },
+ });
}
-
- throw new Error('invalid call');
- }) as unknown) as LegacyAPICaller;
+ }
+ const esClientMock = ({
+ get: jest.fn().mockImplementation(mockedEsGetMethod),
+ search: jest.fn().mockImplementation(mockedEsSearchMethod),
+ } as unknown) as ElasticsearchClient;
+ esClient = esClientMock;
}
describe('makeKQLUsageCollector', () => {
@@ -95,7 +97,7 @@ describe('makeKQLUsageCollector', () => {
setupMockCallCluster({ optInCount: 1 }, 'kuery');
collectorFetchContext = {
...collectorFetchContextMock,
- callCluster,
+ esClient,
};
const fetchResponse = await fetch(collectorFetchContext);
expect(fetchResponse.optInCount).toBe(1);
@@ -106,7 +108,7 @@ describe('makeKQLUsageCollector', () => {
setupMockCallCluster({ optInCount: 1 }, 'kuery');
collectorFetchContext = {
...collectorFetchContextMock,
- callCluster,
+ esClient,
};
const fetchResponse = await fetch(collectorFetchContext);
expect(fetchResponse.defaultQueryLanguage).toBe('kuery');
@@ -117,7 +119,7 @@ describe('makeKQLUsageCollector', () => {
setupMockCallCluster({ optInCount: 1 }, null);
collectorFetchContext = {
...collectorFetchContextMock,
- callCluster,
+ esClient,
};
const fetchResponse = await fetch(collectorFetchContext);
expect(fetchResponse.defaultQueryLanguage).toBe('lucene');
@@ -127,7 +129,7 @@ describe('makeKQLUsageCollector', () => {
setupMockCallCluster({ optInCount: 1 }, undefined);
collectorFetchContext = {
...collectorFetchContextMock,
- callCluster,
+ esClient,
};
const fetchResponse = await fetch(collectorFetchContext);
expect(fetchResponse.defaultQueryLanguage).toBe('default-lucene');
@@ -137,7 +139,7 @@ describe('makeKQLUsageCollector', () => {
setupMockCallCluster(null, 'kuery');
collectorFetchContext = {
...collectorFetchContextMock,
- callCluster,
+ esClient,
};
const fetchResponse = await fetch(collectorFetchContext);
expect(fetchResponse.optInCount).toBe(0);
@@ -148,7 +150,7 @@ describe('makeKQLUsageCollector', () => {
setupMockCallCluster(null, 'missingConfigDoc');
collectorFetchContext = {
...collectorFetchContextMock,
- callCluster,
+ esClient,
};
const fetchResponse = await fetch(collectorFetchContext);
expect(fetchResponse.defaultQueryLanguage).toBe('default-lucene');
diff --git a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts
index 21a1843d1ec81..5178aa65705d8 100644
--- a/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts
+++ b/src/plugins/data/server/kql_telemetry/usage_collector/fetch.ts
@@ -30,18 +30,22 @@ export interface Usage {
}
export function fetchProvider(index: string) {
- return async ({ callCluster }: CollectorFetchContext): Promise => {
- const [response, config] = await Promise.all([
- callCluster('get', {
- index,
- id: 'kql-telemetry:kql-telemetry',
- ignore: [404],
- }),
- callCluster('search', {
- index,
- body: { query: { term: { type: 'config' } } },
- ignore: [404],
- }),
+ return async ({ esClient }: CollectorFetchContext): Promise => {
+ const [{ body: response }, { body: config }] = await Promise.all([
+ esClient.get(
+ {
+ index,
+ id: 'kql-telemetry:kql-telemetry',
+ },
+ { ignore: [404] }
+ ),
+ esClient.search(
+ {
+ index,
+ body: { query: { term: { type: 'config' } } },
+ },
+ { ignore: [404] }
+ ),
]);
const queryLanguageConfigValue: string | null | undefined = get(
From 2db76660ce809de02fa906a19b9741951809e73a Mon Sep 17 00:00:00 2001
From: Gidi Meir Morris
Date: Mon, 21 Dec 2020 19:17:04 +0000
Subject: [PATCH 019/108] [Alerting] Encourage type safe usage of Alerting
(#86623)
This PR encourages type safe usage of the Alerting framework by replacing the current default Params/State/InstanceState/InstanceContext types (which are `AlertTypeParams`/`AlertTypeState`/etc.) with `never`.
This means that code can continue to omit the specific types for these fields, as long as they aren't referenced.
Once an alert developer wishes to actually reference the parameters (or state/context), then they have to specify the type.
This PR also changed the typing of the `AlertTypeParams` and `AlertTypeState` from `Record` to `Record`, to ensure that where these catch-all types are used they will at least enforce `unknown` rather than `any`.
This change broke some usage in both @elastic/kibana-alerting-services plugins, but also other plugins in the Stack/Solutions. I tried to fix these where I could, but some of these require new types and refactoring in other teams' code, which I decided is best done by the team who own and maintain that code - I've added explicit `TODO` comments in all of these places, describing the required fix.
This PR also introduced a Generics based typing for the `Alert` type so that the `params` field can be typed as something other than `AlertTypeParams`.
---
.../alerting_example/common/constants.ts | 4 +-
.../public/components/view_astros_alert.tsx | 4 +-
.../server/alert_types/astros.ts | 6 +-
x-pack/plugins/alerts/common/alert.ts | 12 +-
.../create_alert_instance_factory.ts | 10 +-
.../alerts/server/alert_type_registry.ts | 43 +++--
.../server/alerts_client/alerts_client.ts | 80 ++++++---
.../server/alerts_client/tests/create.test.ts | 12 +-
...rt_instance_summary_from_event_log.test.ts | 4 +-
.../alert_instance_summary_from_event_log.ts | 2 +-
.../lib/validate_alert_type_params.test.ts | 72 ++------
.../server/lib/validate_alert_type_params.ts | 13 +-
x-pack/plugins/alerts/server/mocks.ts | 22 ++-
.../alerts/server/routes/create.test.ts | 2 +-
x-pack/plugins/alerts/server/routes/create.ts | 12 +-
.../plugins/alerts/server/routes/get.test.ts | 4 +-
.../task_runner/alert_task_instance.test.ts | 4 +-
.../server/task_runner/alert_task_instance.ts | 5 +-
.../create_execution_handler.test.ts | 65 ++++---
.../task_runner/create_execution_handler.ts | 23 ++-
.../server/task_runner/task_runner.test.ts | 168 ++++++++++++++----
.../alerts/server/task_runner/task_runner.ts | 125 ++++++++-----
.../task_runner/task_runner_factory.test.ts | 4 +-
.../server/task_runner/task_runner_factory.ts | 4 +-
x-pack/plugins/alerts/server/types.ts | 40 +++--
.../infra/public/alerting/inventory/index.ts | 14 +-
.../public/alerting/metric_threshold/index.ts | 14 +-
.../inventory_metric_threshold_executor.ts | 16 +-
...r_inventory_metric_threshold_alert_type.ts | 14 +-
.../log_threshold/log_threshold_executor.ts | 39 ++--
.../metric_threshold/lib/evaluate_alert.ts | 18 +-
.../metric_threshold_executor.test.ts | 35 +++-
.../metric_threshold_executor.ts | 19 +-
.../register_metric_threshold_alert_type.ts | 28 ++-
.../plugins/monitoring/common/types/alerts.ts | 6 +-
.../ccr_read_exceptions_alert/index.tsx | 5 +-
.../alerts/components/duration/validation.tsx | 2 +
.../cpu_usage_alert/cpu_usage_alert.tsx | 4 +-
.../public/alerts/disk_usage_alert/index.tsx | 4 +-
.../alerts/memory_usage_alert/index.tsx | 4 +-
.../monitoring/server/alerts/base_alert.ts | 6 +-
.../notifications/create_notifications.ts | 6 +-
.../notifications/find_notifications.ts | 4 +-
.../notifications/read_notifications.ts | 4 +-
.../detection_engine/notifications/types.ts | 32 ++--
.../notifications/update_notifications.ts | 6 +-
.../routes/rules/create_rules_bulk_route.ts | 9 +-
.../routes/rules/create_rules_route.ts | 9 +-
.../detection_engine/routes/rules/utils.ts | 9 +-
.../routes/rules/validate.test.ts | 5 +-
.../detection_engine/routes/rules/validate.ts | 9 +-
.../detection_engine/rules/create_rules.ts | 10 +-
.../lib/detection_engine/rules/find_rules.ts | 3 +-
.../rules/install_prepacked_rules.ts | 6 +-
.../rules/patch_rules.mock.ts | 30 +++-
.../lib/detection_engine/rules/patch_rules.ts | 10 +-
.../lib/detection_engine/rules/read_rules.ts | 3 +-
.../lib/detection_engine/rules/types.ts | 14 +-
.../detection_engine/rules/update_rules.ts | 10 +-
.../signals/signal_params_schema.ts | 2 +-
.../signals/signal_rule_alert_type.test.ts | 5 +-
.../signals/signal_rule_alert_type.ts | 12 +-
.../lib/detection_engine/signals/types.ts | 24 ++-
.../server/lib/detection_engine/types.ts | 3 +-
.../alert_types/geo_containment/types.ts | 3 +-
.../public/alert_types/geo_threshold/types.ts | 3 +-
.../alert_types/threshold/expression.tsx | 12 +-
.../public/alert_types/threshold/types.ts | 4 +-
.../alert_types/geo_containment/alert_type.ts | 38 +++-
.../geo_containment/geo_containment.ts | 43 ++---
.../alert_types/geo_containment/index.ts | 15 +-
.../tests/geo_containment.test.ts | 17 +-
.../alert_types/geo_threshold/alert_type.ts | 45 ++++-
.../geo_threshold/geo_threshold.ts | 34 +---
.../public/application/lib/alert_api.test.ts | 13 +-
.../triggers_actions_ui/public/plugin.ts | 4 +-
.../triggers_actions_ui/public/types.ts | 26 ++-
.../uptime/public/state/actions/types.ts | 5 +-
.../uptime/public/state/alerts/alerts.ts | 27 +--
.../plugins/uptime/public/state/api/alerts.ts | 11 +-
.../lib/alerts/__tests__/status_check.test.ts | 10 +-
.../plugins/uptime/server/lib/alerts/types.ts | 11 +-
.../server/lib/alerts/uptime_alert_wrapper.ts | 28 ++-
.../plugins/alerts/server/alert_types.ts | 14 +-
.../fixtures/plugins/alerts/server/plugin.ts | 11 +-
85 files changed, 1044 insertions(+), 523 deletions(-)
diff --git a/x-pack/examples/alerting_example/common/constants.ts b/x-pack/examples/alerting_example/common/constants.ts
index 40cc298db795a..8e4ea4faf014c 100644
--- a/x-pack/examples/alerting_example/common/constants.ts
+++ b/x-pack/examples/alerting_example/common/constants.ts
@@ -4,11 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { AlertTypeParams } from '../../../plugins/alerts/common';
+
export const ALERTING_EXAMPLE_APP_ID = 'AlertingExample';
// always firing
export const DEFAULT_INSTANCES_TO_GENERATE = 5;
-export interface AlwaysFiringParams {
+export interface AlwaysFiringParams extends AlertTypeParams {
instances?: number;
thresholds?: {
small?: number;
diff --git a/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx b/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx
index e4687c75fa0b7..eb682a86f5ff6 100644
--- a/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx
+++ b/x-pack/examples/alerting_example/public/components/view_astros_alert.tsx
@@ -23,7 +23,7 @@ import { withRouter, RouteComponentProps } from 'react-router-dom';
import { CoreStart } from 'kibana/public';
import { isEmpty } from 'lodash';
import { Alert, AlertTaskState, BASE_ALERT_API_PATH } from '../../../../plugins/alerts/common';
-import { ALERTING_EXAMPLE_APP_ID } from '../../common/constants';
+import { ALERTING_EXAMPLE_APP_ID, AlwaysFiringParams } from '../../common/constants';
type Props = RouteComponentProps & {
http: CoreStart['http'];
@@ -34,7 +34,7 @@ function hasCraft(state: any): state is { craft: string } {
return state && state.craft;
}
export const ViewPeopleInSpaceAlertPage = withRouter(({ http, id }: Props) => {
- const [alert, setAlert] = useState(null);
+ const [alert, setAlert] = useState | null>(null);
const [alertState, setAlertState] = useState(null);
useEffect(() => {
diff --git a/x-pack/examples/alerting_example/server/alert_types/astros.ts b/x-pack/examples/alerting_example/server/alert_types/astros.ts
index 27a8bfc7a53a3..22c2f25c410cd 100644
--- a/x-pack/examples/alerting_example/server/alert_types/astros.ts
+++ b/x-pack/examples/alerting_example/server/alert_types/astros.ts
@@ -38,7 +38,11 @@ function getCraftFilter(craft: string) {
craft === Craft.OuterSpace ? true : craft === person.craft;
}
-export const alertType: AlertType = {
+export const alertType: AlertType<
+ { outerSpaceCapacity: number; craft: string; op: string },
+ { peopleInSpace: number },
+ { craft: string }
+> = {
id: 'example.people-in-space',
name: 'People In Space Right Now',
actionGroups: [{ id: 'default', name: 'default' }],
diff --git a/x-pack/plugins/alerts/common/alert.ts b/x-pack/plugins/alerts/common/alert.ts
index d74f66898eff6..ed3fbcf2ddc9b 100644
--- a/x-pack/plugins/alerts/common/alert.ts
+++ b/x-pack/plugins/alerts/common/alert.ts
@@ -7,10 +7,8 @@
import { SavedObjectAttribute, SavedObjectAttributes } from 'kibana/server';
import { AlertNotifyWhenType } from './alert_notify_when_type';
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type AlertTypeState = Record;
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type AlertTypeParams = Record;
+export type AlertTypeState = Record;
+export type AlertTypeParams = Record;
export interface IntervalSchedule extends SavedObjectAttributes {
interval: string;
@@ -52,7 +50,7 @@ export interface AlertAggregations {
alertExecutionStatus: { [status: string]: number };
}
-export interface Alert {
+export interface Alert {
id: string;
enabled: boolean;
name: string;
@@ -61,7 +59,7 @@ export interface Alert {
consumer: string;
schedule: IntervalSchedule;
actions: AlertAction[];
- params: AlertTypeParams;
+ params: Params;
scheduledTaskId?: string;
createdBy: string | null;
updatedBy: string | null;
@@ -76,7 +74,7 @@ export interface Alert {
executionStatus: AlertExecutionStatus;
}
-export type SanitizedAlert = Omit;
+export type SanitizedAlert = Omit, 'apiKey'>;
export enum HealthStatus {
OK = 'ok',
diff --git a/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts b/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts
index 0b29262ddcc07..47f013a5d0e55 100644
--- a/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts
+++ b/x-pack/plugins/alerts/server/alert_instance/create_alert_instance_factory.ts
@@ -4,12 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { AlertInstanceContext, AlertInstanceState } from '../types';
import { AlertInstance } from './alert_instance';
-export function createAlertInstanceFactory(alertInstances: Record) {
- return (id: string): AlertInstance => {
+export function createAlertInstanceFactory<
+ InstanceState extends AlertInstanceState,
+ InstanceContext extends AlertInstanceContext
+>(alertInstances: Record>) {
+ return (id: string): AlertInstance => {
if (!alertInstances[id]) {
- alertInstances[id] = new AlertInstance();
+ alertInstances[id] = new AlertInstance();
}
return alertInstances[id];
diff --git a/x-pack/plugins/alerts/server/alert_type_registry.ts b/x-pack/plugins/alerts/server/alert_type_registry.ts
index d436d1987c027..5e4188c1f3bc1 100644
--- a/x-pack/plugins/alerts/server/alert_type_registry.ts
+++ b/x-pack/plugins/alerts/server/alert_type_registry.ts
@@ -32,7 +32,7 @@ export interface ConstructorOptions {
export interface RegistryAlertType
extends Pick<
- NormalizedAlertType,
+ UntypedNormalizedAlertType,
| 'name'
| 'actionGroups'
| 'recoveryActionGroup'
@@ -66,16 +66,23 @@ const alertIdSchema = schema.string({
});
export type NormalizedAlertType<
- Params extends AlertTypeParams = AlertTypeParams,
- State extends AlertTypeState = AlertTypeState,
- InstanceState extends AlertInstanceState = AlertInstanceState,
- InstanceContext extends AlertInstanceContext = AlertInstanceContext
+ Params extends AlertTypeParams,
+ State extends AlertTypeState,
+ InstanceState extends AlertInstanceState,
+ InstanceContext extends AlertInstanceContext
> = Omit, 'recoveryActionGroup'> &
Pick>, 'recoveryActionGroup'>;
+export type UntypedNormalizedAlertType = NormalizedAlertType<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+>;
+
export class AlertTypeRegistry {
private readonly taskManager: TaskManagerSetupContract;
- private readonly alertTypes: Map = new Map();
+ private readonly alertTypes: Map = new Map();
private readonly taskRunnerFactory: TaskRunnerFactory;
private readonly licenseState: ILicenseState;
private readonly licensing: LicensingPluginSetup;
@@ -96,10 +103,10 @@ export class AlertTypeRegistry {
}
public register<
- Params extends AlertTypeParams = AlertTypeParams,
- State extends AlertTypeState = AlertTypeState,
- InstanceState extends AlertInstanceState = AlertInstanceState,
- InstanceContext extends AlertInstanceContext = AlertInstanceContext
+ Params extends AlertTypeParams,
+ State extends AlertTypeState,
+ InstanceState extends AlertInstanceState,
+ InstanceContext extends AlertInstanceContext
>(alertType: AlertType) {
if (this.has(alertType.id)) {
throw new Error(
@@ -113,14 +120,22 @@ export class AlertTypeRegistry {
}
alertType.actionVariables = normalizedActionVariables(alertType.actionVariables);
- const normalizedAlertType = augmentActionGroupsWithReserved(alertType as AlertType);
+ const normalizedAlertType = augmentActionGroupsWithReserved<
+ Params,
+ State,
+ InstanceState,
+ InstanceContext
+ >(alertType);
- this.alertTypes.set(alertIdSchema.validate(alertType.id), normalizedAlertType);
+ this.alertTypes.set(
+ alertIdSchema.validate(alertType.id),
+ normalizedAlertType as UntypedNormalizedAlertType
+ );
this.taskManager.registerTaskDefinitions({
[`alerting:${alertType.id}`]: {
title: alertType.name,
createTaskRunner: (context: RunContext) =>
- this.taskRunnerFactory.create(normalizedAlertType, context),
+ this.taskRunnerFactory.create(normalizedAlertType as UntypedNormalizedAlertType, context),
},
});
// No need to notify usage on basic alert types
@@ -170,7 +185,7 @@ export class AlertTypeRegistry {
producer,
minimumLicenseRequired,
},
- ]: [string, NormalizedAlertType]) => ({
+ ]: [string, UntypedNormalizedAlertType]) => ({
id,
name,
actionGroups,
diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
index f21cd2b02943a..e21fee4ce3d61 100644
--- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts
@@ -23,13 +23,13 @@ import {
RawAlert,
AlertTypeRegistry,
AlertAction,
- AlertType,
IntervalSchedule,
SanitizedAlert,
AlertTaskState,
AlertInstanceSummary,
AlertExecutionStatusValues,
AlertNotifyWhenType,
+ AlertTypeParams,
} from '../types';
import {
validateAlertTypeParams,
@@ -44,7 +44,7 @@ import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/se
import { TaskManagerStartContract } from '../../../task_manager/server';
import { taskInstanceToAlertTaskInstance } from '../task_runner/alert_task_instance';
import { deleteTaskIfItExists } from '../lib/delete_task_if_it_exists';
-import { RegistryAlertType } from '../alert_type_registry';
+import { RegistryAlertType, UntypedNormalizedAlertType } from '../alert_type_registry';
import { AlertsAuthorization, WriteOperations, ReadOperations } from '../authorization';
import { IEventLogClient } from '../../../../plugins/event_log/server';
import { parseIsoOrRelativeDate } from '../lib/iso_or_relative_date';
@@ -127,16 +127,16 @@ interface AggregateResult {
alertExecutionStatus: { [status: string]: number };
}
-export interface FindResult {
+export interface FindResult {
page: number;
perPage: number;
total: number;
- data: SanitizedAlert[];
+ data: Array>;
}
-export interface CreateOptions {
+export interface CreateOptions {
data: Omit<
- Alert,
+ Alert,
| 'id'
| 'createdBy'
| 'updatedBy'
@@ -154,14 +154,14 @@ export interface CreateOptions {
};
}
-interface UpdateOptions {
+interface UpdateOptions {
id: string;
data: {
name: string;
tags: string[];
schedule: IntervalSchedule;
actions: NormalizedAlertAction[];
- params: Record;
+ params: Params;
throttle: string | null;
notifyWhen: AlertNotifyWhenType | null;
};
@@ -223,7 +223,10 @@ export class AlertsClient {
this.auditLogger = auditLogger;
}
- public async create({ data, options }: CreateOptions): Promise {
+ public async create({
+ data,
+ options,
+ }: CreateOptions): Promise> {
const id = SavedObjectsUtils.generateId();
try {
@@ -248,7 +251,10 @@ export class AlertsClient {
// Throws an error if alert type isn't registered
const alertType = this.alertTypeRegistry.get(data.alertTypeId);
- const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params);
+ const validatedAlertTypeParams = validateAlertTypeParams(
+ data.params,
+ alertType.validate?.params
+ );
const username = await this.getUserName();
const createdAPIKey = data.enabled
@@ -334,10 +340,14 @@ export class AlertsClient {
});
createdAlert.attributes.scheduledTaskId = scheduledTask.id;
}
- return this.getAlertFromRaw(createdAlert.id, createdAlert.attributes, references);
+ return this.getAlertFromRaw(createdAlert.id, createdAlert.attributes, references);
}
- public async get({ id }: { id: string }): Promise {
+ public async get({
+ id,
+ }: {
+ id: string;
+ }): Promise> {
const result = await this.unsecuredSavedObjectsClient.get('alert', id);
try {
await this.authorization.ensureAuthorized(
@@ -361,7 +371,7 @@ export class AlertsClient {
savedObject: { type: 'alert', id },
})
);
- return this.getAlertFromRaw(result.id, result.attributes, result.references);
+ return this.getAlertFromRaw(result.id, result.attributes, result.references);
}
public async getAlertState({ id }: { id: string }): Promise {
@@ -426,9 +436,9 @@ export class AlertsClient {
});
}
- public async find({
+ public async find({
options: { fields, ...options } = {},
- }: { options?: FindOptions } = {}): Promise {
+ }: { options?: FindOptions } = {}): Promise> {
let authorizationTuple;
try {
authorizationTuple = await this.authorization.getFindAuthorizationFilter();
@@ -475,7 +485,7 @@ export class AlertsClient {
);
throw error;
}
- return this.getAlertFromRaw(
+ return this.getAlertFromRaw(
id,
fields ? (pick(attributes, fields) as RawAlert) : attributes,
references
@@ -605,15 +615,21 @@ export class AlertsClient {
return removeResult;
}
- public async update({ id, data }: UpdateOptions): Promise {
+ public async update({
+ id,
+ data,
+ }: UpdateOptions): Promise> {
return await retryIfConflicts(
this.logger,
`alertsClient.update('${id}')`,
- async () => await this.updateWithOCC({ id, data })
+ async () => await this.updateWithOCC({ id, data })
);
}
- private async updateWithOCC({ id, data }: UpdateOptions): Promise {
+ private async updateWithOCC({
+ id,
+ data,
+ }: UpdateOptions): Promise> {
let alertSavedObject: SavedObject;
try {
@@ -658,7 +674,7 @@ export class AlertsClient {
this.alertTypeRegistry.ensureAlertTypeEnabled(alertSavedObject.attributes.alertTypeId);
- const updateResult = await this.updateAlert({ id, data }, alertSavedObject);
+ const updateResult = await this.updateAlert({ id, data }, alertSavedObject);
await Promise.all([
alertSavedObject.attributes.apiKey
@@ -692,14 +708,17 @@ export class AlertsClient {
return updateResult;
}
- private async updateAlert(
- { id, data }: UpdateOptions,
+ private async updateAlert(
+ { id, data }: UpdateOptions,
{ attributes, version }: SavedObject
- ): Promise {
+ ): Promise> {
const alertType = this.alertTypeRegistry.get(attributes.alertTypeId);
// Validate
- const validatedAlertTypeParams = validateAlertTypeParams(alertType, data.params);
+ const validatedAlertTypeParams = validateAlertTypeParams(
+ data.params,
+ alertType.validate?.params
+ );
this.validateActions(alertType, data.actions);
const { actions, references } = await this.denormalizeActions(data.actions);
@@ -1343,7 +1362,7 @@ export class AlertsClient {
}) as Alert['actions'];
}
- private getAlertFromRaw(
+ private getAlertFromRaw(
id: string,
rawAlert: RawAlert,
references: SavedObjectReference[] | undefined
@@ -1351,14 +1370,14 @@ export class AlertsClient {
// In order to support the partial update API of Saved Objects we have to support
// partial updates of an Alert, but when we receive an actual RawAlert, it is safe
// to cast the result to an Alert
- return this.getPartialAlertFromRaw(id, rawAlert, references) as Alert;
+ return this.getPartialAlertFromRaw(id, rawAlert, references) as Alert;
}
- private getPartialAlertFromRaw(
+ private getPartialAlertFromRaw(
id: string,
{ createdAt, updatedAt, meta, notifyWhen, scheduledTaskId, ...rawAlert }: Partial,
references: SavedObjectReference[] | undefined
- ): PartialAlert {
+ ): PartialAlert {
// Not the prettiest code here, but if we want to use most of the
// alert fields from the rawAlert using `...rawAlert` kind of access, we
// need to specifically delete the executionStatus as it's a different type
@@ -1386,7 +1405,10 @@ export class AlertsClient {
};
}
- private validateActions(alertType: AlertType, actions: NormalizedAlertAction[]): void {
+ private validateActions(
+ alertType: UntypedNormalizedAlertType,
+ actions: NormalizedAlertAction[]
+ ): void {
const { actionGroups: alertTypeActionGroups } = alertType;
const usedAlertActionGroups = actions.map((action) => action.group);
const availableAlertTypeActionGroups = new Set(map(alertTypeActionGroups, 'id'));
diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
index 5f830a6c5bc51..0424a1295c9b9 100644
--- a/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
+++ b/x-pack/plugins/alerts/server/alerts_client/tests/create.test.ts
@@ -59,7 +59,11 @@ beforeEach(() => {
setGlobalDate();
-function getMockData(overwrites: Record = {}): CreateOptions['data'] {
+function getMockData(
+ overwrites: Record = {}
+): CreateOptions<{
+ bar: boolean;
+}>['data'] {
return {
enabled: true,
name: 'abc',
@@ -93,7 +97,11 @@ describe('create()', () => {
});
describe('authorization', () => {
- function tryToExecuteOperation(options: CreateOptions): Promise {
+ function tryToExecuteOperation(
+ options: CreateOptions<{
+ bar: boolean;
+ }>
+ ): Promise {
unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
saved_objects: [
{
diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts
index d6357494546b0..0f91e5d0c24a9 100644
--- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts
+++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.test.ts
@@ -635,11 +635,11 @@ export class EventsFactory {
}
}
-function createAlert(overrides: Partial): SanitizedAlert {
+function createAlert(overrides: Partial): SanitizedAlert<{ bar: boolean }> {
return { ...BaseAlert, ...overrides };
}
-const BaseAlert: SanitizedAlert = {
+const BaseAlert: SanitizedAlert<{ bar: boolean }> = {
id: 'alert-123',
alertTypeId: '123',
schedule: { interval: '10s' },
diff --git a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts
index f540f9a9b884c..a020eecd448a4 100644
--- a/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts
+++ b/x-pack/plugins/alerts/server/lib/alert_instance_summary_from_event_log.ts
@@ -9,7 +9,7 @@ import { IEvent } from '../../../event_log/server';
import { EVENT_LOG_ACTIONS, EVENT_LOG_PROVIDER, LEGACY_EVENT_LOG_ACTIONS } from '../plugin';
export interface AlertInstanceSummaryFromEventLogParams {
- alert: SanitizedAlert;
+ alert: SanitizedAlert<{ bar: boolean }>;
events: IEvent[];
dateStart: string;
dateEnd: string;
diff --git a/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts
index 2814eaef3e02a..634b6885aa59b 100644
--- a/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts
+++ b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.test.ts
@@ -8,51 +8,19 @@ import { schema } from '@kbn/config-schema';
import { validateAlertTypeParams } from './validate_alert_type_params';
test('should return passed in params when validation not defined', () => {
- const result = validateAlertTypeParams(
- {
- id: 'my-alert-type',
- name: 'My description',
- actionGroups: [
- {
- id: 'default',
- name: 'Default',
- },
- ],
- defaultActionGroupId: 'default',
- minimumLicenseRequired: 'basic',
- async executor() {},
- producer: 'alerts',
- },
- {
- foo: true,
- }
- );
+ const result = validateAlertTypeParams({
+ foo: true,
+ });
expect(result).toEqual({ foo: true });
});
test('should validate and apply defaults when params is valid', () => {
const result = validateAlertTypeParams(
- {
- id: 'my-alert-type',
- name: 'My description',
- actionGroups: [
- {
- id: 'default',
- name: 'Default',
- },
- ],
- defaultActionGroupId: 'default',
- minimumLicenseRequired: 'basic',
- validate: {
- params: schema.object({
- param1: schema.string(),
- param2: schema.string({ defaultValue: 'default-value' }),
- }),
- },
- async executor() {},
- producer: 'alerts',
- },
- { param1: 'value' }
+ { param1: 'value' },
+ schema.object({
+ param1: schema.string(),
+ param2: schema.string({ defaultValue: 'default-value' }),
+ })
);
expect(result).toEqual({
param1: 'value',
@@ -63,26 +31,10 @@ test('should validate and apply defaults when params is valid', () => {
test('should validate and throw error when params is invalid', () => {
expect(() =>
validateAlertTypeParams(
- {
- id: 'my-alert-type',
- name: 'My description',
- actionGroups: [
- {
- id: 'default',
- name: 'Default',
- },
- ],
- defaultActionGroupId: 'default',
- minimumLicenseRequired: 'basic',
- validate: {
- params: schema.object({
- param1: schema.string(),
- }),
- },
- async executor() {},
- producer: 'alerts',
- },
- {}
+ {},
+ schema.object({
+ param1: schema.string(),
+ })
)
).toThrowErrorMatchingInlineSnapshot(
`"params invalid: [param1]: expected value of type [string] but got [undefined]"`
diff --git a/x-pack/plugins/alerts/server/lib/validate_alert_type_params.ts b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.ts
index a443143d8cbde..2f510f90a2367 100644
--- a/x-pack/plugins/alerts/server/lib/validate_alert_type_params.ts
+++ b/x-pack/plugins/alerts/server/lib/validate_alert_type_params.ts
@@ -5,15 +5,14 @@
*/
import Boom from '@hapi/boom';
-import { AlertType, AlertExecutorOptions } from '../types';
+import { AlertTypeParams, AlertTypeParamsValidator } from '../types';
-export function validateAlertTypeParams(
- alertType: AlertType,
- params: Record
-): AlertExecutorOptions['params'] {
- const validator = alertType.validate && alertType.validate.params;
+export function validateAlertTypeParams(
+ params: Record,
+ validator?: AlertTypeParamsValidator
+): Params {
if (!validator) {
- return params as AlertExecutorOptions['params'];
+ return params as Params;
}
try {
diff --git a/x-pack/plugins/alerts/server/mocks.ts b/x-pack/plugins/alerts/server/mocks.ts
index cfae4c650bd42..0f042b7a81d6c 100644
--- a/x-pack/plugins/alerts/server/mocks.ts
+++ b/x-pack/plugins/alerts/server/mocks.ts
@@ -11,6 +11,7 @@ import {
elasticsearchServiceMock,
savedObjectsClientMock,
} from '../../../../src/core/server/mocks';
+import { AlertInstanceContext, AlertInstanceState } from './types';
export { alertsClientMock };
@@ -30,8 +31,14 @@ const createStartMock = () => {
return mock;
};
-export type AlertInstanceMock = jest.Mocked;
-const createAlertInstanceFactoryMock = () => {
+export type AlertInstanceMock<
+ State extends AlertInstanceState = AlertInstanceState,
+ Context extends AlertInstanceContext = AlertInstanceContext
+> = jest.Mocked>;
+const createAlertInstanceFactoryMock = <
+ InstanceState extends AlertInstanceState = AlertInstanceState,
+ InstanceContext extends AlertInstanceContext = AlertInstanceContext
+>() => {
const mock = {
hasScheduledActions: jest.fn(),
isThrottled: jest.fn(),
@@ -50,14 +57,17 @@ const createAlertInstanceFactoryMock = () => {
mock.unscheduleActions.mockReturnValue(mock);
mock.scheduleActions.mockReturnValue(mock);
- return (mock as unknown) as AlertInstanceMock;
+ return (mock as unknown) as AlertInstanceMock;
};
-const createAlertServicesMock = () => {
- const alertInstanceFactoryMock = createAlertInstanceFactoryMock();
+const createAlertServicesMock = <
+ InstanceState extends AlertInstanceState = AlertInstanceState,
+ InstanceContext extends AlertInstanceContext = AlertInstanceContext
+>() => {
+ const alertInstanceFactoryMock = createAlertInstanceFactoryMock();
return {
alertInstanceFactory: jest
- .fn, [string]>()
+ .fn>, [string]>()
.mockReturnValue(alertInstanceFactoryMock),
callCluster: elasticsearchServiceMock.createLegacyScopedClusterClient().callAsCurrentUser,
getLegacyScopedClusterClient: jest.fn(),
diff --git a/x-pack/plugins/alerts/server/routes/create.test.ts b/x-pack/plugins/alerts/server/routes/create.test.ts
index 5597b315158cd..fc531821f25b6 100644
--- a/x-pack/plugins/alerts/server/routes/create.test.ts
+++ b/x-pack/plugins/alerts/server/routes/create.test.ts
@@ -49,7 +49,7 @@ describe('createAlertRoute', () => {
],
};
- const createResult: Alert = {
+ const createResult: Alert<{ bar: boolean }> = {
...mockedAlert,
enabled: true,
muteAll: false,
diff --git a/x-pack/plugins/alerts/server/routes/create.ts b/x-pack/plugins/alerts/server/routes/create.ts
index a34a3118985fa..a79a9d40b236f 100644
--- a/x-pack/plugins/alerts/server/routes/create.ts
+++ b/x-pack/plugins/alerts/server/routes/create.ts
@@ -16,7 +16,13 @@ import { ILicenseState } from '../lib/license_state';
import { verifyApiAccess } from '../lib/license_api_access';
import { validateDurationSchema } from '../lib';
import { handleDisabledApiKeysError } from './lib/error_handler';
-import { Alert, AlertNotifyWhenType, BASE_ALERT_API_PATH, validateNotifyWhenType } from '../types';
+import {
+ Alert,
+ AlertNotifyWhenType,
+ AlertTypeParams,
+ BASE_ALERT_API_PATH,
+ validateNotifyWhenType,
+} from '../types';
import { AlertTypeDisabledError } from '../lib/errors/alert_type_disabled';
export const bodySchema = schema.object({
@@ -65,7 +71,9 @@ export const createAlertRoute = (router: IRouter, licenseState: ILicenseState) =
const alert = req.body;
const notifyWhen = alert?.notifyWhen ? (alert.notifyWhen as AlertNotifyWhenType) : null;
try {
- const alertRes: Alert = await alertsClient.create({ data: { ...alert, notifyWhen } });
+ const alertRes: Alert = await alertsClient.create({
+ data: { ...alert, notifyWhen },
+ });
return res.ok({
body: alertRes,
});
diff --git a/x-pack/plugins/alerts/server/routes/get.test.ts b/x-pack/plugins/alerts/server/routes/get.test.ts
index 21e52ece82d2d..747f9b11e2b47 100644
--- a/x-pack/plugins/alerts/server/routes/get.test.ts
+++ b/x-pack/plugins/alerts/server/routes/get.test.ts
@@ -22,7 +22,9 @@ beforeEach(() => {
});
describe('getAlertRoute', () => {
- const mockedAlert: Alert = {
+ const mockedAlert: Alert<{
+ bar: true;
+ }> = {
id: '1',
alertTypeId: '1',
schedule: { interval: '10s' },
diff --git a/x-pack/plugins/alerts/server/task_runner/alert_task_instance.test.ts b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.test.ts
index 09236ec5e0ad1..1bd8b75e2133d 100644
--- a/x-pack/plugins/alerts/server/task_runner/alert_task_instance.test.ts
+++ b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.test.ts
@@ -9,7 +9,9 @@ import { AlertTaskInstance, taskInstanceToAlertTaskInstance } from './alert_task
import uuid from 'uuid';
import { SanitizedAlert } from '../types';
-const alert: SanitizedAlert = {
+const alert: SanitizedAlert<{
+ bar: boolean;
+}> = {
id: 'alert-123',
alertTypeId: '123',
schedule: { interval: '10s' },
diff --git a/x-pack/plugins/alerts/server/task_runner/alert_task_instance.ts b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.ts
index a290f3fa33c70..ab074cfdffa1c 100644
--- a/x-pack/plugins/alerts/server/task_runner/alert_task_instance.ts
+++ b/x-pack/plugins/alerts/server/task_runner/alert_task_instance.ts
@@ -13,6 +13,7 @@ import {
alertParamsSchema,
alertStateSchema,
AlertTaskParams,
+ AlertTypeParams,
} from '../../common';
export interface AlertTaskInstance extends ConcreteTaskInstance {
@@ -23,9 +24,9 @@ export interface AlertTaskInstance extends ConcreteTaskInstance {
const enumerateErrorFields = (e: t.Errors) =>
`${e.map(({ context }) => context.map(({ key }) => key).join('.'))}`;
-export function taskInstanceToAlertTaskInstance(
+export function taskInstanceToAlertTaskInstance(
taskInstance: ConcreteTaskInstance,
- alert?: SanitizedAlert
+ alert?: SanitizedAlert
): AlertTaskInstance {
return {
...taskInstance,
diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts
index b414e726f0101..5603b13a3b1f5 100644
--- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts
+++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts
@@ -4,8 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { AlertType } from '../types';
-import { createExecutionHandler } from './create_execution_handler';
+import { createExecutionHandler, CreateExecutionHandlerOptions } from './create_execution_handler';
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
import {
actionsMock,
@@ -16,12 +15,19 @@ import { eventLoggerMock } from '../../../event_log/server/event_logger.mock';
import { KibanaRequest } from 'kibana/server';
import { asSavedObjectExecutionSource } from '../../../actions/server';
import { InjectActionParamsOpts } from './inject_action_params';
+import { UntypedNormalizedAlertType } from '../alert_type_registry';
+import {
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext,
+} from '../types';
jest.mock('./inject_action_params', () => ({
injectActionParams: jest.fn(),
}));
-const alertType: AlertType = {
+const alertType: UntypedNormalizedAlertType = {
id: 'test',
name: 'Test',
actionGroups: [
@@ -39,18 +45,26 @@ const alertType: AlertType = {
};
const actionsClient = actionsClientMock.create();
-const createExecutionHandlerParams = {
- actionsPlugin: actionsMock.createStart(),
+
+const mockActionsPlugin = actionsMock.createStart();
+const mockEventLogger = eventLoggerMock.create();
+const createExecutionHandlerParams: jest.Mocked<
+ CreateExecutionHandlerOptions<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+ >
+> = {
+ actionsPlugin: mockActionsPlugin,
spaceId: 'default',
alertId: '1',
alertName: 'name-of-alert',
tags: ['tag-A', 'tag-B'],
apiKey: 'MTIzOmFiYw==',
- spaceIdToNamespace: jest.fn().mockReturnValue(undefined),
- getBasePath: jest.fn().mockReturnValue(undefined),
alertType,
logger: loggingSystemMock.create().get(),
- eventLogger: eventLoggerMock.create(),
+ eventLogger: mockEventLogger,
actions: [
{
id: '1',
@@ -79,12 +93,10 @@ beforeEach(() => {
.injectActionParams.mockImplementation(
({ actionParams }: InjectActionParamsOpts) => actionParams
);
- createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
- createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
- createExecutionHandlerParams.actionsPlugin.getActionsClientWithRequest.mockResolvedValue(
- actionsClient
- );
- createExecutionHandlerParams.actionsPlugin.renderActionParameterTemplates.mockImplementation(
+ mockActionsPlugin.isActionTypeEnabled.mockReturnValue(true);
+ mockActionsPlugin.isActionExecutable.mockReturnValue(true);
+ mockActionsPlugin.getActionsClientWithRequest.mockResolvedValue(actionsClient);
+ mockActionsPlugin.renderActionParameterTemplates.mockImplementation(
renderActionParameterTemplatesDefault
);
});
@@ -97,9 +109,9 @@ test('enqueues execution per selected action', async () => {
context: {},
alertInstanceId: '2',
});
- expect(
- createExecutionHandlerParams.actionsPlugin.getActionsClientWithRequest
- ).toHaveBeenCalledWith(createExecutionHandlerParams.request);
+ expect(mockActionsPlugin.getActionsClientWithRequest).toHaveBeenCalledWith(
+ createExecutionHandlerParams.request
+ );
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(1);
expect(actionsClient.enqueueExecution.mock.calls[0]).toMatchInlineSnapshot(`
Array [
@@ -124,9 +136,8 @@ test('enqueues execution per selected action', async () => {
]
`);
- const eventLogger = createExecutionHandlerParams.eventLogger;
- expect(eventLogger.logEvent).toHaveBeenCalledTimes(1);
- expect(eventLogger.logEvent.mock.calls).toMatchInlineSnapshot(`
+ expect(mockEventLogger.logEvent).toHaveBeenCalledTimes(1);
+ expect(mockEventLogger.logEvent.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
@@ -171,9 +182,9 @@ test('enqueues execution per selected action', async () => {
test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => {
// Mock two calls, one for check against actions[0] and the second for actions[1]
- createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValueOnce(false);
- createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(false);
- createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(true);
+ mockActionsPlugin.isActionExecutable.mockReturnValueOnce(false);
+ mockActionsPlugin.isActionTypeEnabled.mockReturnValueOnce(false);
+ mockActionsPlugin.isActionTypeEnabled.mockReturnValueOnce(true);
const executionHandler = createExecutionHandler({
...createExecutionHandlerParams,
actions: [
@@ -214,9 +225,9 @@ test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () =>
});
test('trow error error message when action type is disabled', async () => {
- createExecutionHandlerParams.actionsPlugin.preconfiguredActions = [];
- createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValue(false);
- createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(false);
+ mockActionsPlugin.preconfiguredActions = [];
+ mockActionsPlugin.isActionExecutable.mockReturnValue(false);
+ mockActionsPlugin.isActionTypeEnabled.mockReturnValue(false);
const executionHandler = createExecutionHandler({
...createExecutionHandlerParams,
actions: [
@@ -243,7 +254,7 @@ test('trow error error message when action type is disabled', async () => {
expect(actionsClient.enqueueExecution).toHaveBeenCalledTimes(0);
- createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockImplementation(() => true);
+ mockActionsPlugin.isActionExecutable.mockImplementation(() => true);
const executionHandlerForPreconfiguredAction = createExecutionHandler({
...createExecutionHandlerParams,
actions: [...createExecutionHandlerParams.actions],
diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts
index 8c7ad79483194..8b4412aeb23e5 100644
--- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts
+++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts
@@ -15,14 +15,20 @@ import { EVENT_LOG_ACTIONS } from '../plugin';
import { injectActionParams } from './inject_action_params';
import {
AlertAction,
+ AlertTypeParams,
+ AlertTypeState,
AlertInstanceState,
AlertInstanceContext,
- AlertType,
- AlertTypeParams,
RawAlert,
} from '../types';
+import { NormalizedAlertType } from '../alert_type_registry';
-interface CreateExecutionHandlerOptions {
+export interface CreateExecutionHandlerOptions<
+ Params extends AlertTypeParams,
+ State extends AlertTypeState,
+ InstanceState extends AlertInstanceState,
+ InstanceContext extends AlertInstanceContext
+> {
alertId: string;
alertName: string;
tags?: string[];
@@ -30,7 +36,7 @@ interface CreateExecutionHandlerOptions {
actions: AlertAction[];
spaceId: string;
apiKey: RawAlert['apiKey'];
- alertType: AlertType;
+ alertType: NormalizedAlertType;
logger: Logger;
eventLogger: IEventLogger;
request: KibanaRequest;
@@ -45,7 +51,12 @@ interface ExecutionHandlerOptions {
state: AlertInstanceState;
}
-export function createExecutionHandler({
+export function createExecutionHandler<
+ Params extends AlertTypeParams,
+ State extends AlertTypeState,
+ InstanceState extends AlertInstanceState,
+ InstanceContext extends AlertInstanceContext
+>({
logger,
alertId,
alertName,
@@ -58,7 +69,7 @@ export function createExecutionHandler({
eventLogger,
request,
alertParams,
-}: CreateExecutionHandlerOptions) {
+}: CreateExecutionHandlerOptions) {
const alertTypeActionGroups = new Map(
alertType.actionGroups.map((actionGroup) => [actionGroup.id, actionGroup.name])
);
diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts
index a4b565194e431..967c5263b9730 100644
--- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts
+++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts
@@ -6,7 +6,13 @@
import sinon from 'sinon';
import { schema } from '@kbn/config-schema';
-import { AlertExecutorOptions } from '../types';
+import {
+ AlertExecutorOptions,
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext,
+} from '../types';
import {
ConcreteTaskInstance,
isUnrecoverableError,
@@ -28,9 +34,9 @@ import { IEventLogger } from '../../../event_log/server';
import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
import { Alert, RecoveredActionGroup } from '../../common';
import { omit } from 'lodash';
-import { NormalizedAlertType } from '../alert_type_registry';
+import { UntypedNormalizedAlertType } from '../alert_type_registry';
import { alertTypeRegistryMock } from '../alert_type_registry.mock';
-const alertType = {
+const alertType: jest.Mocked = {
id: 'test',
name: 'My test alert',
actionGroups: [{ id: 'default', name: 'Default' }, RecoveredActionGroup],
@@ -91,7 +97,7 @@ describe('Task Runner', () => {
alertTypeRegistry,
};
- const mockedAlertTypeSavedObject: Alert = {
+ const mockedAlertTypeSavedObject: Alert = {
id: '1',
consumer: 'bar',
createdAt: new Date('2019-02-12T21:01:22.479Z'),
@@ -150,7 +156,7 @@ describe('Task Runner', () => {
test('successfully executes the task', async () => {
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
{
...mockedTaskInstance,
state: {
@@ -254,14 +260,21 @@ describe('Task Runner', () => {
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
alertType.executor.mockImplementation(
- ({ services: executorServices }: AlertExecutorOptions) => {
+ async ({
+ services: executorServices,
+ }: AlertExecutorOptions<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+ >) => {
executorServices
.alertInstanceFactory('1')
.scheduleActionsWithSubGroup('default', 'subDefault');
}
);
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
@@ -407,12 +420,19 @@ describe('Task Runner', () => {
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
alertType.executor.mockImplementation(
- ({ services: executorServices }: AlertExecutorOptions) => {
+ async ({
+ services: executorServices,
+ }: AlertExecutorOptions<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+ >) => {
executorServices.alertInstanceFactory('1').scheduleActions('default');
}
);
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
@@ -516,13 +536,20 @@ describe('Task Runner', () => {
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
alertType.executor.mockImplementation(
- ({ services: executorServices }: AlertExecutorOptions) => {
+ async ({
+ services: executorServices,
+ }: AlertExecutorOptions<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+ >) => {
executorServices.alertInstanceFactory('1').scheduleActions('default');
executorServices.alertInstanceFactory('2').scheduleActions('default');
}
);
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
@@ -562,12 +589,19 @@ describe('Task Runner', () => {
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
alertType.executor.mockImplementation(
- ({ services: executorServices }: AlertExecutorOptions) => {
+ async ({
+ services: executorServices,
+ }: AlertExecutorOptions<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+ >) => {
executorServices.alertInstanceFactory('1').scheduleActions('default');
}
);
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
{
...mockedTaskInstance,
state: {
@@ -656,12 +690,19 @@ describe('Task Runner', () => {
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
alertType.executor.mockImplementation(
- ({ services: executorServices }: AlertExecutorOptions) => {
+ async ({
+ services: executorServices,
+ }: AlertExecutorOptions<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+ >) => {
executorServices.alertInstanceFactory('1').scheduleActions('default');
}
);
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
{
...mockedTaskInstance,
state: {
@@ -696,14 +737,21 @@ describe('Task Runner', () => {
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
alertType.executor.mockImplementation(
- ({ services: executorServices }: AlertExecutorOptions) => {
+ async ({
+ services: executorServices,
+ }: AlertExecutorOptions<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+ >) => {
executorServices
.alertInstanceFactory('1')
.scheduleActionsWithSubGroup('default', 'subgroup1');
}
);
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
{
...mockedTaskInstance,
state: {
@@ -744,12 +792,19 @@ describe('Task Runner', () => {
taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true);
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
alertType.executor.mockImplementation(
- ({ services: executorServices }: AlertExecutorOptions) => {
+ async ({
+ services: executorServices,
+ }: AlertExecutorOptions<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+ >) => {
executorServices.alertInstanceFactory('1').scheduleActions('default');
}
);
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
@@ -912,12 +967,19 @@ describe('Task Runner', () => {
taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true);
alertType.executor.mockImplementation(
- ({ services: executorServices }: AlertExecutorOptions) => {
+ async ({
+ services: executorServices,
+ }: AlertExecutorOptions<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+ >) => {
executorServices.alertInstanceFactory('1').scheduleActions('default');
}
);
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
{
...mockedTaskInstance,
state: {
@@ -1012,12 +1074,19 @@ describe('Task Runner', () => {
};
alertTypeWithCustomRecovery.executor.mockImplementation(
- ({ services: executorServices }: AlertExecutorOptions) => {
+ async ({
+ services: executorServices,
+ }: AlertExecutorOptions<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+ >) => {
executorServices.alertInstanceFactory('1').scheduleActions('default');
}
);
const taskRunner = new TaskRunner(
- alertTypeWithCustomRecovery as NormalizedAlertType,
+ alertTypeWithCustomRecovery,
{
...mockedTaskInstance,
state: {
@@ -1103,13 +1172,20 @@ describe('Task Runner', () => {
test('persists alertInstances passed in from state, only if they are scheduled for execution', async () => {
alertType.executor.mockImplementation(
- ({ services: executorServices }: AlertExecutorOptions) => {
+ async ({
+ services: executorServices,
+ }: AlertExecutorOptions<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+ >) => {
executorServices.alertInstanceFactory('1').scheduleActions('default');
}
);
const date = new Date().toISOString();
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
{
...mockedTaskInstance,
state: {
@@ -1239,7 +1315,7 @@ describe('Task Runner', () => {
param1: schema.string(),
}),
},
- } as NormalizedAlertType,
+ },
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
@@ -1267,7 +1343,7 @@ describe('Task Runner', () => {
test('uses API key when provided', async () => {
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
@@ -1300,7 +1376,7 @@ describe('Task Runner', () => {
test(`doesn't use API key when not provided`, async () => {
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
@@ -1330,7 +1406,7 @@ describe('Task Runner', () => {
test('rescheduled the Alert if the schedule has update during a task run', async () => {
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
@@ -1365,13 +1441,20 @@ describe('Task Runner', () => {
test('recovers gracefully when the AlertType executor throws an exception', async () => {
alertType.executor.mockImplementation(
- ({ services: executorServices }: AlertExecutorOptions) => {
+ async ({
+ services: executorServices,
+ }: AlertExecutorOptions<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+ >) => {
throw new Error('OMG');
}
);
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
@@ -1438,7 +1521,7 @@ describe('Task Runner', () => {
});
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
@@ -1497,7 +1580,7 @@ describe('Task Runner', () => {
});
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
@@ -1564,7 +1647,7 @@ describe('Task Runner', () => {
});
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
@@ -1631,7 +1714,7 @@ describe('Task Runner', () => {
});
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
@@ -1701,7 +1784,7 @@ describe('Task Runner', () => {
const legacyTaskInstance = omit(mockedTaskInstance, 'schedule');
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
legacyTaskInstance,
taskRunnerFactoryInitializerParams
);
@@ -1733,13 +1816,20 @@ describe('Task Runner', () => {
};
alertType.executor.mockImplementation(
- ({ services: executorServices }: AlertExecutorOptions) => {
+ async ({
+ services: executorServices,
+ }: AlertExecutorOptions<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+ >) => {
throw new Error('OMG');
}
);
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
{
...mockedTaskInstance,
state: originalAlertSate,
@@ -1770,7 +1860,7 @@ describe('Task Runner', () => {
});
const taskRunner = new TaskRunner(
- alertType as NormalizedAlertType,
+ alertType,
mockedTaskInstance,
taskRunnerFactoryInitializerParams
);
diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts
index 44cf7dd91be7d..c4187145e5a16 100644
--- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts
+++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts
@@ -26,7 +26,6 @@ import {
RawAlertInstance,
AlertTaskState,
Alert,
- AlertExecutorOptions,
SanitizedAlert,
AlertExecutionStatus,
AlertExecutionStatusErrorReasons,
@@ -39,7 +38,13 @@ import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_l
import { isAlertSavedObjectNotFoundError } from '../lib/is_alert_not_found_error';
import { AlertsClient } from '../alerts_client';
import { partiallyUpdateAlert } from '../saved_objects';
-import { ActionGroup } from '../../common';
+import {
+ ActionGroup,
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext,
+} from '../../common';
import { NormalizedAlertType } from '../alert_type_registry';
const FALLBACK_RETRY_INTERVAL = '5m';
@@ -55,15 +60,20 @@ interface AlertTaskInstance extends ConcreteTaskInstance {
state: AlertTaskState;
}
-export class TaskRunner {
+export class TaskRunner<
+ Params extends AlertTypeParams,
+ State extends AlertTypeState,
+ InstanceState extends AlertInstanceState,
+ InstanceContext extends AlertInstanceContext
+> {
private context: TaskRunnerContext;
private logger: Logger;
private taskInstance: AlertTaskInstance;
- private alertType: NormalizedAlertType;
+ private alertType: NormalizedAlertType;
private readonly alertTypeRegistry: AlertTypeRegistry;
constructor(
- alertType: NormalizedAlertType,
+ alertType: NormalizedAlertType,
taskInstance: ConcreteTaskInstance,
context: TaskRunnerContext
) {
@@ -131,8 +141,8 @@ export class TaskRunner {
tags: string[] | undefined,
spaceId: string,
apiKey: RawAlert['apiKey'],
- actions: Alert['actions'],
- alertParams: RawAlert['params']
+ actions: Alert['actions'],
+ alertParams: Params
) {
return createExecutionHandler({
alertId,
@@ -152,7 +162,7 @@ export class TaskRunner {
async executeAlertInstance(
alertInstanceId: string,
- alertInstance: AlertInstance,
+ alertInstance: AlertInstance,
executionHandler: ReturnType
) {
const {
@@ -168,8 +178,8 @@ export class TaskRunner {
async executeAlertInstances(
services: Services,
- alert: SanitizedAlert,
- params: AlertExecutorOptions['params'],
+ alert: SanitizedAlert,
+ params: Params,
executionHandler: ReturnType,
spaceId: string,
event: Event
@@ -190,9 +200,12 @@ export class TaskRunner {
} = this.taskInstance;
const namespace = this.context.spaceIdToNamespace(spaceId);
- const alertInstances = mapValues, AlertInstance>(
+ const alertInstances = mapValues<
+ Record,
+ AlertInstance
+ >(
alertRawInstances,
- (rawAlertInstance) => new AlertInstance(rawAlertInstance)
+ (rawAlertInstance) => new AlertInstance(rawAlertInstance)
);
const originalAlertInstances = cloneDeep(alertInstances);
@@ -205,10 +218,12 @@ export class TaskRunner {
alertId,
services: {
...services,
- alertInstanceFactory: createAlertInstanceFactory(alertInstances),
+ alertInstanceFactory: createAlertInstanceFactory(
+ alertInstances
+ ),
},
params,
- state: alertTypeState,
+ state: alertTypeState as State,
startedAt: this.taskInstance.startedAt!,
previousStartedAt: previousStartedAt ? new Date(previousStartedAt) : null,
spaceId,
@@ -232,12 +247,15 @@ export class TaskRunner {
event.event.outcome = 'success';
// Cleanup alert instances that are no longer scheduling actions to avoid over populating the alertInstances object
- const instancesWithScheduledActions = pickBy(alertInstances, (alertInstance: AlertInstance) =>
- alertInstance.hasScheduledActions()
+ const instancesWithScheduledActions = pickBy(
+ alertInstances,
+ (alertInstance: AlertInstance) =>
+ alertInstance.hasScheduledActions()
);
const recoveredAlertInstances = pickBy(
alertInstances,
- (alertInstance: AlertInstance) => !alertInstance.hasScheduledActions()
+ (alertInstance: AlertInstance) =>
+ !alertInstance.hasScheduledActions()
);
logActiveAndRecoveredInstances({
@@ -272,7 +290,10 @@ export class TaskRunner {
const instancesToExecute =
notifyWhen === 'onActionGroupChange'
? Object.entries(instancesWithScheduledActions).filter(
- ([alertInstanceName, alertInstance]: [string, AlertInstance]) => {
+ ([alertInstanceName, alertInstance]: [
+ string,
+ AlertInstance
+ ]) => {
const shouldExecuteAction = alertInstance.scheduledActionGroupOrSubgroupHasChanged();
if (!shouldExecuteAction) {
this.logger.debug(
@@ -283,7 +304,10 @@ export class TaskRunner {
}
)
: Object.entries(instancesWithScheduledActions).filter(
- ([alertInstanceName, alertInstance]: [string, AlertInstance]) => {
+ ([alertInstanceName, alertInstance]: [
+ string,
+ AlertInstance
+ ]) => {
const throttled = alertInstance.isThrottled(throttle);
const muted = mutedInstanceIdsSet.has(alertInstanceName);
const shouldExecuteAction = !throttled && !muted;
@@ -299,8 +323,9 @@ export class TaskRunner {
);
await Promise.all(
- instancesToExecute.map(([id, alertInstance]: [string, AlertInstance]) =>
- this.executeAlertInstance(id, alertInstance, executionHandler)
+ instancesToExecute.map(
+ ([id, alertInstance]: [string, AlertInstance]) =>
+ this.executeAlertInstance(id, alertInstance, executionHandler)
)
);
} else {
@@ -309,17 +334,17 @@ export class TaskRunner {
return {
alertTypeState: updatedAlertTypeState || undefined,
- alertInstances: mapValues, RawAlertInstance>(
- instancesWithScheduledActions,
- (alertInstance) => alertInstance.toRaw()
- ),
+ alertInstances: mapValues<
+ Record>,
+ RawAlertInstance
+ >(instancesWithScheduledActions, (alertInstance) => alertInstance.toRaw()),
};
}
async validateAndExecuteAlert(
services: Services,
apiKey: RawAlert['apiKey'],
- alert: SanitizedAlert,
+ alert: SanitizedAlert,
event: Event
) {
const {
@@ -327,7 +352,7 @@ export class TaskRunner {
} = this.taskInstance;
// Validate
- const validatedParams = validateAlertTypeParams(this.alertType, alert.params);
+ const validatedParams = validateAlertTypeParams(alert.params, this.alertType.validate?.params);
const executionHandler = this.getExecutionHandler(
alertId,
alert.name,
@@ -359,7 +384,7 @@ export class TaskRunner {
}
const [services, alertsClient] = this.getServicesWithSpaceLevelPermissions(spaceId, apiKey);
- let alert: SanitizedAlert;
+ let alert: SanitizedAlert;
// Ensure API key is still valid and user has access
try {
@@ -501,19 +526,23 @@ export class TaskRunner {
}
}
-interface GenerateNewAndRecoveredInstanceEventsParams {
+interface GenerateNewAndRecoveredInstanceEventsParams<
+ InstanceState extends AlertInstanceState,
+ InstanceContext extends AlertInstanceContext
+> {
eventLogger: IEventLogger;
- originalAlertInstances: Dictionary;
- currentAlertInstances: Dictionary;
- recoveredAlertInstances: Dictionary;
+ originalAlertInstances: Dictionary>;
+ currentAlertInstances: Dictionary>;
+ recoveredAlertInstances: Dictionary>;
alertId: string;
alertLabel: string;
namespace: string | undefined;
}
-function generateNewAndRecoveredInstanceEvents(
- params: GenerateNewAndRecoveredInstanceEventsParams
-) {
+function generateNewAndRecoveredInstanceEvents<
+ InstanceState extends AlertInstanceState,
+ InstanceContext extends AlertInstanceContext
+>(params: GenerateNewAndRecoveredInstanceEventsParams) {
const {
eventLogger,
alertId,
@@ -584,16 +613,22 @@ function generateNewAndRecoveredInstanceEvents(
}
}
-interface ScheduleActionsForRecoveredInstancesParams {
+interface ScheduleActionsForRecoveredInstancesParams<
+ InstanceState extends AlertInstanceState,
+ InstanceContext extends AlertInstanceContext
+> {
logger: Logger;
recoveryActionGroup: ActionGroup;
- recoveredAlertInstances: Dictionary;
+ recoveredAlertInstances: Dictionary>;
executionHandler: ReturnType;
mutedInstanceIdsSet: Set;
alertLabel: string;
}
-function scheduleActionsForRecoveredInstances(params: ScheduleActionsForRecoveredInstancesParams) {
+function scheduleActionsForRecoveredInstances<
+ InstanceState extends AlertInstanceState,
+ InstanceContext extends AlertInstanceContext
+>(params: ScheduleActionsForRecoveredInstancesParams) {
const {
logger,
recoveryActionGroup,
@@ -623,14 +658,20 @@ function scheduleActionsForRecoveredInstances(params: ScheduleActionsForRecovere
}
}
-interface LogActiveAndRecoveredInstancesParams {
+interface LogActiveAndRecoveredInstancesParams<
+ InstanceState extends AlertInstanceState,
+ InstanceContext extends AlertInstanceContext
+> {
logger: Logger;
- activeAlertInstances: Dictionary;
- recoveredAlertInstances: Dictionary;
+ activeAlertInstances: Dictionary>;
+ recoveredAlertInstances: Dictionary>;
alertLabel: string;
}
-function logActiveAndRecoveredInstances(params: LogActiveAndRecoveredInstancesParams) {
+function logActiveAndRecoveredInstances<
+ InstanceState extends AlertInstanceState,
+ InstanceContext extends AlertInstanceContext
+>(params: LogActiveAndRecoveredInstancesParams) {
const { logger, activeAlertInstances, recoveredAlertInstances, alertLabel } = params;
const activeInstanceIds = Object.keys(activeAlertInstances);
const recoveredInstanceIds = Object.keys(recoveredAlertInstances);
diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts
index 6c58b64fffa92..3a5a130f582ed 100644
--- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts
+++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts
@@ -16,10 +16,10 @@ import {
import { actionsMock } from '../../../actions/server/mocks';
import { alertsMock, alertsClientMock } from '../mocks';
import { eventLoggerMock } from '../../../event_log/server/event_logger.mock';
-import { NormalizedAlertType } from '../alert_type_registry';
+import { UntypedNormalizedAlertType } from '../alert_type_registry';
import { alertTypeRegistryMock } from '../alert_type_registry.mock';
-const alertType: NormalizedAlertType = {
+const alertType: UntypedNormalizedAlertType = {
id: 'test',
name: 'My test alert',
actionGroups: [{ id: 'default', name: 'Default' }],
diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts
index 1fe94972bd4b0..e266608d80880 100644
--- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts
+++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts
@@ -17,7 +17,7 @@ import { AlertTypeRegistry, GetServicesFunction, SpaceIdToNamespaceFunction } fr
import { TaskRunner } from './task_runner';
import { IEventLogger } from '../../../event_log/server';
import { AlertsClient } from '../alerts_client';
-import { NormalizedAlertType } from '../alert_type_registry';
+import { UntypedNormalizedAlertType } from '../alert_type_registry';
export interface TaskRunnerContext {
logger: Logger;
@@ -44,7 +44,7 @@ export class TaskRunnerFactory {
this.taskRunnerContext = taskRunnerContext;
}
- public create(alertType: NormalizedAlertType, { taskInstance }: RunContext) {
+ public create(alertType: UntypedNormalizedAlertType, { taskInstance }: RunContext) {
if (!this.isInitialized) {
throw new Error('TaskRunnerFactory not initialized');
}
diff --git a/x-pack/plugins/alerts/server/types.ts b/x-pack/plugins/alerts/server/types.ts
index 8704068c3e51a..027f875e2d08d 100644
--- a/x-pack/plugins/alerts/server/types.ts
+++ b/x-pack/plugins/alerts/server/types.ts
@@ -61,10 +61,10 @@ export interface AlertServices<
}
export interface AlertExecutorOptions<
- Params extends AlertTypeParams = AlertTypeParams,
- State extends AlertTypeState = AlertTypeState,
- InstanceState extends AlertInstanceState = AlertInstanceState,
- InstanceContext extends AlertInstanceContext = AlertInstanceContext
+ Params extends AlertTypeParams = never,
+ State extends AlertTypeState = never,
+ InstanceState extends AlertInstanceState = never,
+ InstanceContext extends AlertInstanceContext = never
> {
alertId: string;
startedAt: Date;
@@ -85,26 +85,28 @@ export interface ActionVariable {
description: string;
}
-// signature of the alert type executor function
export type ExecutorType<
- Params,
- State,
- InstanceState extends AlertInstanceState = AlertInstanceState,
- InstanceContext extends AlertInstanceContext = AlertInstanceContext
+ Params extends AlertTypeParams = never,
+ State extends AlertTypeState = never,
+ InstanceState extends AlertInstanceState = never,
+ InstanceContext extends AlertInstanceContext = never
> = (
options: AlertExecutorOptions
) => Promise;
+export interface AlertTypeParamsValidator {
+ validate: (object: unknown) => Params;
+}
export interface AlertType<
- Params extends AlertTypeParams = AlertTypeParams,
- State extends AlertTypeState = AlertTypeState,
- InstanceState extends AlertInstanceState = AlertInstanceState,
- InstanceContext extends AlertInstanceContext = AlertInstanceContext
+ Params extends AlertTypeParams = never,
+ State extends AlertTypeState = never,
+ InstanceState extends AlertInstanceState = never,
+ InstanceContext extends AlertInstanceContext = never
> {
id: string;
name: string;
validate?: {
- params?: { validate: (object: unknown) => Params };
+ params?: AlertTypeParamsValidator;
};
actionGroups: ActionGroup[];
defaultActionGroupId: ActionGroup['id'];
@@ -119,6 +121,13 @@ export interface AlertType<
minimumLicenseRequired: LicenseType;
}
+export type UntypedAlertType = AlertType<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+>;
+
export interface RawAlertAction extends SavedObjectAttributes {
group: string;
actionRef: string;
@@ -142,7 +151,8 @@ export interface RawAlertExecutionStatus extends SavedObjectAttributes {
};
}
-export type PartialAlert = Pick & Partial>;
+export type PartialAlert = Pick, 'id'> &
+ Partial, 'id'>>;
export interface RawAlert extends SavedObjectAttributes {
enabled: boolean;
diff --git a/x-pack/plugins/infra/public/alerting/inventory/index.ts b/x-pack/plugins/infra/public/alerting/inventory/index.ts
index 13ce43f77c8b0..da85f363b16ec 100644
--- a/x-pack/plugins/infra/public/alerting/inventory/index.ts
+++ b/x-pack/plugins/infra/public/alerting/inventory/index.ts
@@ -5,13 +5,21 @@
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID } from '../../../server/lib/alerting/inventory_metric_threshold/types';
+import {
+ InventoryMetricConditions,
+ METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
+ // eslint-disable-next-line @kbn/eslint/no-restricted-paths
+} from '../../../server/lib/alerting/inventory_metric_threshold/types';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
+import { AlertTypeParams } from '../../../../alerts/common';
import { validateMetricThreshold } from './components/validation';
-export function createInventoryMetricAlertType(): AlertTypeModel {
+interface InventoryMetricAlertTypeParams extends AlertTypeParams {
+ criteria: InventoryMetricConditions[];
+}
+
+export function createInventoryMetricAlertType(): AlertTypeModel {
return {
id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
description: i18n.translate('xpack.infra.metrics.inventory.alertFlyout.alertDescription', {
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts
index cccd5fbc439d7..9c32c473f4597 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts
@@ -8,10 +8,18 @@ import React from 'react';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
import { validateMetricThreshold } from './components/validation';
-// eslint-disable-next-line @kbn/eslint/no-restricted-paths
-import { METRIC_THRESHOLD_ALERT_TYPE_ID } from '../../../server/lib/alerting/metric_threshold/types';
+import { AlertTypeParams } from '../../../../alerts/common';
+import {
+ MetricExpressionParams,
+ METRIC_THRESHOLD_ALERT_TYPE_ID,
+ // eslint-disable-next-line @kbn/eslint/no-restricted-paths
+} from '../../../server/lib/alerting/metric_threshold/types';
+
+interface MetricThresholdAlertTypeParams extends AlertTypeParams {
+ criteria: MetricExpressionParams[];
+}
-export function createMetricThresholdAlertType(): AlertTypeModel {
+export function createMetricThresholdAlertType(): AlertTypeModel {
return {
id: METRIC_THRESHOLD_ALERT_TYPE_ID,
description: i18n.translate('xpack.infra.metrics.alertFlyout.alertDescription', {
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
index 1941ec6326ddb..54cf8658a3f0d 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts
@@ -9,7 +9,11 @@ import moment from 'moment';
import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label';
import { toMetricOpt } from '../../../../common/snapshot_metric_i18n';
import { AlertStates, InventoryMetricConditions } from './types';
-import { RecoveredActionGroup } from '../../../../../alerts/common';
+import {
+ AlertInstanceContext,
+ AlertInstanceState,
+ RecoveredActionGroup,
+} from '../../../../../alerts/common';
import { AlertExecutorOptions } from '../../../../../alerts/server';
import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types';
import { InfraBackendLibs } from '../../infra_types';
@@ -35,7 +39,15 @@ interface InventoryMetricThresholdParams {
export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) => async ({
services,
params,
-}: AlertExecutorOptions) => {
+}: AlertExecutorOptions<
+ /**
+ * TODO: Remove this use of `any` by utilizing a proper type
+ */
+ Record,
+ Record,
+ AlertInstanceState,
+ AlertInstanceContext
+>) => {
const {
criteria,
filterQuery,
diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts
index 2d1df6e8cb462..a2e8eff34ef98 100644
--- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts
@@ -5,7 +5,7 @@
*/
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
-import { AlertType } from '../../../../../alerts/server';
+import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../../../alerts/server';
import {
createInventoryMetricThresholdExecutor,
FIRED_ACTIONS,
@@ -40,7 +40,17 @@ const condition = schema.object({
),
});
-export const registerMetricInventoryThresholdAlertType = (libs: InfraBackendLibs): AlertType => ({
+export const registerMetricInventoryThresholdAlertType = (
+ libs: InfraBackendLibs
+): AlertType<
+ /**
+ * TODO: Remove this use of `any` by utilizing a proper type
+ */
+ Record,
+ Record,
+ AlertInstanceState,
+ AlertInstanceContext
+> => ({
id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
name: i18n.translate('xpack.infra.metrics.inventory.alertName', {
defaultMessage: 'Inventory',
diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts
index d3d34cd2aad58..dccab5168fb60 100644
--- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts
@@ -9,7 +9,10 @@ import {
AlertExecutorOptions,
AlertServices,
AlertInstance,
+ AlertTypeParams,
+ AlertTypeState,
AlertInstanceContext,
+ AlertInstanceState,
} from '../../../../../alerts/server';
import {
AlertStates,
@@ -34,6 +37,14 @@ import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
import { decodeOrThrow } from '../../../../common/runtime_types';
import { UNGROUPED_FACTORY_KEY } from '../common/utils';
+type LogThresholdAlertServices = AlertServices;
+type LogThresholdAlertExecutorOptions = AlertExecutorOptions<
+ AlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+>;
+
const COMPOSITE_GROUP_SIZE = 40;
const checkValueAgainstComparatorMap: {
@@ -46,7 +57,7 @@ const checkValueAgainstComparatorMap: {
};
export const createLogThresholdExecutor = (libs: InfraBackendLibs) =>
- async function ({ services, params }: AlertExecutorOptions) {
+ async function ({ services, params }: LogThresholdAlertExecutorOptions) {
const { alertInstanceFactory, savedObjectsClient, callCluster } = services;
const { sources } = libs;
@@ -88,8 +99,8 @@ async function executeAlert(
alertParams: CountAlertParams,
timestampField: string,
indexPattern: string,
- callCluster: AlertServices['callCluster'],
- alertInstanceFactory: AlertServices['alertInstanceFactory']
+ callCluster: LogThresholdAlertServices['callCluster'],
+ alertInstanceFactory: LogThresholdAlertServices['alertInstanceFactory']
) {
const query = getESQuery(alertParams, timestampField, indexPattern);
@@ -118,8 +129,8 @@ async function executeRatioAlert(
alertParams: RatioAlertParams,
timestampField: string,
indexPattern: string,
- callCluster: AlertServices['callCluster'],
- alertInstanceFactory: AlertServices['alertInstanceFactory']
+ callCluster: LogThresholdAlertServices['callCluster'],
+ alertInstanceFactory: LogThresholdAlertServices['alertInstanceFactory']
) {
// Ratio alert params are separated out into two standard sets of alert params
const numeratorParams: AlertParams = {
@@ -175,7 +186,7 @@ const getESQuery = (
export const processUngroupedResults = (
results: UngroupedSearchQueryResponse,
params: CountAlertParams,
- alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'],
+ alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'],
alertInstaceUpdater: AlertInstanceUpdater
) => {
const { count, criteria } = params;
@@ -204,7 +215,7 @@ export const processUngroupedRatioResults = (
numeratorResults: UngroupedSearchQueryResponse,
denominatorResults: UngroupedSearchQueryResponse,
params: RatioAlertParams,
- alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'],
+ alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'],
alertInstaceUpdater: AlertInstanceUpdater
) => {
const { count, criteria } = params;
@@ -259,7 +270,7 @@ const getReducedGroupByResults = (
export const processGroupByResults = (
results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'],
params: CountAlertParams,
- alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'],
+ alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'],
alertInstaceUpdater: AlertInstanceUpdater
) => {
const { count, criteria } = params;
@@ -292,7 +303,7 @@ export const processGroupByRatioResults = (
numeratorResults: GroupedSearchQueryResponse['aggregations']['groups']['buckets'],
denominatorResults: GroupedSearchQueryResponse['aggregations']['groups']['buckets'],
params: RatioAlertParams,
- alertInstanceFactory: AlertExecutorOptions['services']['alertInstanceFactory'],
+ alertInstanceFactory: LogThresholdAlertExecutorOptions['services']['alertInstanceFactory'],
alertInstaceUpdater: AlertInstanceUpdater
) => {
const { count, criteria } = params;
@@ -599,11 +610,17 @@ const getQueryMappingForComparator = (comparator: Comparator) => {
return queryMappings[comparator];
};
-const getUngroupedResults = async (query: object, callCluster: AlertServices['callCluster']) => {
+const getUngroupedResults = async (
+ query: object,
+ callCluster: LogThresholdAlertServices['callCluster']
+) => {
return decodeOrThrow(UngroupedSearchQueryResponseRT)(await callCluster('search', query));
};
-const getGroupedResults = async (query: object, callCluster: AlertServices['callCluster']) => {
+const getGroupedResults = async (
+ query: object,
+ callCluster: LogThresholdAlertServices['callCluster']
+) => {
let compositeGroupBuckets: GroupedSearchQueryResponse['aggregations']['groups']['buckets'] = [];
let lastAfterKey: GroupedSearchQueryResponse['aggregations']['groups']['after_key'] | undefined;
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
index 49f82c7ccec0b..d51d9435fc904 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
@@ -12,7 +12,7 @@ import {
import { InfraSource } from '../../../../../common/http_api/source_api';
import { InfraDatabaseSearchResponse } from '../../../adapters/framework/adapter_types';
import { createAfterKeyHandler } from '../../../../utils/create_afterkey_handler';
-import { AlertServices, AlertExecutorOptions } from '../../../../../../alerts/server';
+import { AlertServices } from '../../../../../../alerts/server';
import { getAllCompositeData } from '../../../../utils/get_all_composite_data';
import { DOCUMENT_COUNT_I18N } from '../../common/messages';
import { UNGROUPED_FACTORY_KEY } from '../../common/utils';
@@ -35,17 +35,19 @@ interface CompositeAggregationsResponse {
};
}
-export const evaluateAlert = (
+export interface EvaluatedAlertParams {
+ criteria: MetricExpressionParams[];
+ groupBy: string | undefined | string[];
+ filterQuery: string | undefined;
+}
+
+export const evaluateAlert = (
callCluster: AlertServices['callCluster'],
- params: AlertExecutorOptions['params'],
+ params: Params,
config: InfraSource['configuration'],
timeframe?: { start: number; end: number }
) => {
- const { criteria, groupBy, filterQuery } = params as {
- criteria: MetricExpressionParams[];
- groupBy: string | undefined | string[];
- filterQuery: string | undefined;
- };
+ const { criteria, groupBy, filterQuery } = params;
return Promise.all(
criteria.map(async (criterion) => {
const currentValues = await getMetric(
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
index a1d6428f3b52b..6c9fac9d1133c 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
@@ -7,13 +7,13 @@ import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold
import { Comparator, AlertStates } from './types';
import * as mocks from './test_mocks';
import { RecoveredActionGroup } from '../../../../../alerts/common';
-import { AlertExecutorOptions } from '../../../../../alerts/server';
import {
alertsMock,
AlertServicesMock,
AlertInstanceMock,
} from '../../../../../alerts/server/mocks';
import { InfraSources } from '../../sources';
+import { MetricThresholdAlertExecutorOptions } from './register_metric_threshold_alert_type';
interface AlertTestInstance {
instance: AlertInstanceMock;
@@ -23,11 +23,23 @@ interface AlertTestInstance {
let persistAlertInstances = false;
+const mockOptions = {
+ alertId: '',
+ startedAt: new Date(),
+ previousStartedAt: null,
+ state: {},
+ spaceId: '',
+ name: '',
+ tags: [],
+ createdBy: null,
+ updatedBy: null,
+};
+
describe('The metric threshold alert type', () => {
describe('querying the entire infrastructure', () => {
const instanceID = '*';
const execute = (comparator: Comparator, threshold: number[], sourceId: string = 'default') =>
- executor({
+ executor(({
services,
params: {
sourceId,
@@ -39,7 +51,10 @@ describe('The metric threshold alert type', () => {
},
],
},
- });
+ /**
+ * TODO: Remove this use of `as` by utilizing a proper type
+ */
+ } as unknown) as MetricThresholdAlertExecutorOptions);
test('alerts as expected with the > comparator', async () => {
await execute(Comparator.GT, [0.75]);
expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id);
@@ -109,6 +124,7 @@ describe('The metric threshold alert type', () => {
describe('querying with a groupBy parameter', () => {
const execute = (comparator: Comparator, threshold: number[]) =>
executor({
+ ...mockOptions,
services,
params: {
groupBy: 'something',
@@ -159,6 +175,7 @@ describe('The metric threshold alert type', () => {
groupBy: string = ''
) =>
executor({
+ ...mockOptions,
services,
params: {
groupBy,
@@ -216,6 +233,7 @@ describe('The metric threshold alert type', () => {
const instanceID = '*';
const execute = (comparator: Comparator, threshold: number[]) =>
executor({
+ ...mockOptions,
services,
params: {
criteria: [
@@ -242,6 +260,7 @@ describe('The metric threshold alert type', () => {
const instanceID = '*';
const execute = (comparator: Comparator, threshold: number[]) =>
executor({
+ ...mockOptions,
services,
params: {
criteria: [
@@ -268,6 +287,7 @@ describe('The metric threshold alert type', () => {
const instanceID = '*';
const execute = (comparator: Comparator, threshold: number[]) =>
executor({
+ ...mockOptions,
services,
params: {
criteria: [
@@ -294,6 +314,7 @@ describe('The metric threshold alert type', () => {
const instanceID = '*';
const execute = (alertOnNoData: boolean) =>
executor({
+ ...mockOptions,
services,
params: {
criteria: [
@@ -323,6 +344,7 @@ describe('The metric threshold alert type', () => {
const instanceID = '*';
const execute = () =>
executor({
+ ...mockOptions,
services,
params: {
criteria: [
@@ -348,6 +370,7 @@ describe('The metric threshold alert type', () => {
const instanceID = '*';
const execute = (threshold: number[]) =>
executor({
+ ...mockOptions,
services,
params: {
criteria: [
@@ -392,6 +415,7 @@ describe('The metric threshold alert type', () => {
const instanceID = '*';
const execute = () =>
executor({
+ ...mockOptions,
services,
params: {
sourceId: 'default',
@@ -435,10 +459,7 @@ const mockLibs: any = {
configuration: createMockStaticConfiguration({}),
};
-const executor = createMetricThresholdExecutor(mockLibs) as (opts: {
- params: AlertExecutorOptions['params'];
- services: { callCluster: AlertExecutorOptions['params']['callCluster'] };
-}) => Promise;
+const executor = createMetricThresholdExecutor(mockLibs);
const services: AlertServicesMock = alertsMock.createAlertServices();
services.callCluster.mockImplementation(async (_: string, { body, index }: any) => {
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
index 60790648d9a9b..d63b42cd3b146 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
@@ -7,7 +7,6 @@ import { first, last } from 'lodash';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { RecoveredActionGroup } from '../../../../../alerts/common';
-import { AlertExecutorOptions } from '../../../../../alerts/server';
import { InfraBackendLibs } from '../../infra_types';
import {
buildErrorAlertReason,
@@ -18,10 +17,16 @@ import {
} from '../common/messages';
import { createFormatter } from '../../../../common/formatters';
import { AlertStates } from './types';
-import { evaluateAlert } from './lib/evaluate_alert';
+import { evaluateAlert, EvaluatedAlertParams } from './lib/evaluate_alert';
+import {
+ MetricThresholdAlertExecutorOptions,
+ MetricThresholdAlertType,
+} from './register_metric_threshold_alert_type';
-export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
- async function (options: AlertExecutorOptions) {
+export const createMetricThresholdExecutor = (
+ libs: InfraBackendLibs
+): MetricThresholdAlertType['executor'] =>
+ async function (options: MetricThresholdAlertExecutorOptions) {
const { services, params } = options;
const { criteria } = params;
if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions');
@@ -36,7 +41,11 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) =>
sourceId || 'default'
);
const config = source.configuration;
- const alertResults = await evaluateAlert(services.callCluster, params, config);
+ const alertResults = await evaluateAlert(
+ services.callCluster,
+ params as EvaluatedAlertParams,
+ config
+ );
// Because each alert result has the same group definitions, just grab the groups from the first one.
const groups = Object.keys(first(alertResults)!);
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts
index f04a1015bcbcd..000c89f5899ef 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts
@@ -5,7 +5,12 @@
*/
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
-import { AlertType } from '../../../../../alerts/server';
+import {
+ AlertType,
+ AlertInstanceState,
+ AlertInstanceContext,
+ AlertExecutorOptions,
+} from '../../../../../alerts/server';
import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer';
import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor';
import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types';
@@ -21,7 +26,26 @@ import {
thresholdActionVariableDescription,
} from '../common/messages';
-export function registerMetricThresholdAlertType(libs: InfraBackendLibs): AlertType {
+export type MetricThresholdAlertType = AlertType<
+ /**
+ * TODO: Remove this use of `any` by utilizing a proper type
+ */
+ Record,
+ Record,
+ AlertInstanceState,
+ AlertInstanceContext
+>;
+export type MetricThresholdAlertExecutorOptions = AlertExecutorOptions<
+ /**
+ * TODO: Remove this use of `any` by utilizing a proper type
+ */
+ Record,
+ Record,
+ AlertInstanceState,
+ AlertInstanceContext
+>;
+
+export function registerMetricThresholdAlertType(libs: InfraBackendLibs): MetricThresholdAlertType {
const baseCriterion = {
threshold: schema.arrayOf(schema.number()),
comparator: oneOfLiterals(Object.values(Comparator)),
diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts
index 93807f9df12b0..df6e169f37f7a 100644
--- a/x-pack/plugins/monitoring/common/types/alerts.ts
+++ b/x-pack/plugins/monitoring/common/types/alerts.ts
@@ -4,14 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Alert, SanitizedAlert } from '../../../alerts/common';
+import { Alert, AlertTypeParams, SanitizedAlert } from '../../../alerts/common';
import { AlertParamType, AlertMessageTokenType, AlertSeverity } from '../enums';
-export type CommonAlert = Alert | SanitizedAlert;
+export type CommonAlert = Alert | SanitizedAlert;
export interface CommonAlertStatus {
states: CommonAlertState[];
- rawAlert: Alert | SanitizedAlert;
+ rawAlert: Alert | SanitizedAlert;
}
export interface CommonAlertState {
diff --git a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx
index 4d22d422ecda6..6d7751d91b761 100644
--- a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx
@@ -9,8 +9,9 @@ import { i18n } from '@kbn/i18n';
import { Expression, Props } from '../components/duration/expression';
import { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public';
import { ALERT_CCR_READ_EXCEPTIONS, ALERT_DETAILS } from '../../../common/constants';
+import { AlertTypeParams } from '../../../../alerts/common';
-interface ValidateOptions {
+interface ValidateOptions extends AlertTypeParams {
duration: string;
}
@@ -30,7 +31,7 @@ const validate = (inputValues: ValidateOptions): ValidationResult => {
return validationResult;
};
-export function createCCRReadExceptionsAlertType(): AlertTypeModel {
+export function createCCRReadExceptionsAlertType(): AlertTypeModel {
return {
id: ALERT_CCR_READ_EXCEPTIONS,
description: ALERT_DETAILS[ALERT_CCR_READ_EXCEPTIONS].description,
diff --git a/x-pack/plugins/monitoring/public/alerts/components/duration/validation.tsx b/x-pack/plugins/monitoring/public/alerts/components/duration/validation.tsx
index 892ee0926ca38..c4e5ff343da17 100644
--- a/x-pack/plugins/monitoring/public/alerts/components/duration/validation.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/components/duration/validation.tsx
@@ -5,9 +5,11 @@
*/
import { i18n } from '@kbn/i18n';
+import { AlertTypeParams } from '../../../../../alerts/common';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ValidationResult } from '../../../../../triggers_actions_ui/public/types';
+export type MonitoringAlertTypeParams = ValidateOptions & AlertTypeParams;
interface ValidateOptions {
duration: string;
threshold: number;
diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx
index 1fe40fc8777f4..d2cec006b1b1d 100644
--- a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx
@@ -7,10 +7,10 @@ import React from 'react';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
import { ALERT_CPU_USAGE, ALERT_DETAILS } from '../../../common/constants';
-import { validate } from '../components/duration/validation';
+import { validate, MonitoringAlertTypeParams } from '../components/duration/validation';
import { Expression, Props } from '../components/duration/expression';
-export function createCpuUsageAlertType(): AlertTypeModel {
+export function createCpuUsageAlertType(): AlertTypeModel {
return {
id: ALERT_CPU_USAGE,
description: ALERT_DETAILS[ALERT_CPU_USAGE].description,
diff --git a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx
index 5579b8e1275a3..bea399ee89f6a 100644
--- a/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/disk_usage_alert/index.tsx
@@ -5,14 +5,14 @@
*/
import React from 'react';
-import { validate } from '../components/duration/validation';
+import { validate, MonitoringAlertTypeParams } from '../components/duration/validation';
import { Expression, Props } from '../components/duration/expression';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
import { ALERT_DISK_USAGE, ALERT_DETAILS } from '../../../common/constants';
-export function createDiskUsageAlertType(): AlertTypeModel {
+export function createDiskUsageAlertType(): AlertTypeModel {
return {
id: ALERT_DISK_USAGE,
description: ALERT_DETAILS[ALERT_DISK_USAGE].description,
diff --git a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx
index 0400810a8c379..0428e4e7c733e 100644
--- a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx
@@ -5,14 +5,14 @@
*/
import React from 'react';
-import { validate } from '../components/duration/validation';
+import { validate, MonitoringAlertTypeParams } from '../components/duration/validation';
import { Expression, Props } from '../components/duration/expression';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types';
import { ALERT_MEMORY_USAGE, ALERT_DETAILS } from '../../../common/constants';
-export function createMemoryUsageAlertType(): AlertTypeModel {
+export function createMemoryUsageAlertType(): AlertTypeModel {
return {
id: ALERT_MEMORY_USAGE,
description: ALERT_DETAILS[ALERT_MEMORY_USAGE].description,
diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts
index a3bcc310b8084..46adfebfd17bf 100644
--- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts
+++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts
@@ -13,7 +13,7 @@ import {
AlertsClient,
AlertServices,
} from '../../../alerts/server';
-import { Alert, RawAlertInstance, SanitizedAlert } from '../../../alerts/common';
+import { Alert, AlertTypeParams, RawAlertInstance, SanitizedAlert } from '../../../alerts/common';
import { ActionsClient } from '../../../actions/server';
import {
AlertState,
@@ -135,7 +135,7 @@ export class BaseAlert {
alertsClient: AlertsClient,
actionsClient: ActionsClient,
actions: AlertEnableAction[]
- ): Promise {
+ ): Promise> {
const existingAlertData = await alertsClient.find({
options: {
search: this.alertOptions.id,
@@ -170,7 +170,7 @@ export class BaseAlert {
throttle = '1d',
interval = '1m',
} = this.alertOptions;
- return await alertsClient.create({
+ return await alertsClient.create({
data: {
enabled: true,
tags: [],
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts
index 5731a51aeabc1..f0895f1367289 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/create_notifications.ts
@@ -6,7 +6,7 @@
import { Alert } from '../../../../../alerts/common';
import { SERVER_APP_ID, NOTIFICATIONS_ID } from '../../../../common/constants';
-import { CreateNotificationParams } from './types';
+import { CreateNotificationParams, RuleNotificationAlertTypeParams } from './types';
import { addTags } from './add_tags';
import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions';
@@ -17,8 +17,8 @@ export const createNotifications = async ({
ruleAlertId,
interval,
name,
-}: CreateNotificationParams): Promise =>
- alertsClient.create({
+}: CreateNotificationParams): Promise> =>
+ alertsClient.create({
data: {
name,
tags: addTags([], ruleAlertId),
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/find_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/find_notifications.ts
index 5d3a328dd6fbb..1bf07a1574be6 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/find_notifications.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/find_notifications.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { FindResult } from '../../../../../alerts/server';
+import { AlertTypeParams, FindResult } from '../../../../../alerts/server';
import { NOTIFICATIONS_ID } from '../../../../common/constants';
import { FindNotificationParams } from './types';
@@ -24,7 +24,7 @@ export const findNotifications = async ({
filter,
sortField,
sortOrder,
-}: FindNotificationParams): Promise =>
+}: FindNotificationParams): Promise> =>
alertsClient.find({
options: {
fields,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/read_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/read_notifications.ts
index fe9101335b4f5..74df77bf9bf74 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/read_notifications.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/read_notifications.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SanitizedAlert } from '../../../../../alerts/common';
+import { AlertTypeParams, SanitizedAlert } from '../../../../../alerts/common';
import { ReadNotificationParams, isAlertType } from './types';
import { findNotifications } from './find_notifications';
import { INTERNAL_RULE_ALERT_ID_KEY } from '../../../../common/constants';
@@ -13,7 +13,7 @@ export const readNotifications = async ({
alertsClient,
id,
ruleAlertId,
-}: ReadNotificationParams): Promise => {
+}: ReadNotificationParams): Promise | null> => {
if (id != null) {
try {
const notification = await alertsClient.get({ id });
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/types.ts
index cc9fb149a7e1b..e4e9df552101b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/types.ts
@@ -8,18 +8,20 @@ import {
AlertsClient,
PartialAlert,
AlertType,
+ AlertTypeParams,
AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext,
AlertExecutorOptions,
} from '../../../../../alerts/server';
import { Alert } from '../../../../../alerts/common';
import { NOTIFICATIONS_ID } from '../../../../common/constants';
import { RuleAlertAction } from '../../../../common/detection_engine/types';
-export interface RuleNotificationAlertType extends Alert {
- params: {
- ruleAlertId: string;
- };
+export interface RuleNotificationAlertTypeParams extends AlertTypeParams {
+ ruleAlertId: string;
}
+export type RuleNotificationAlertType = Alert;
export interface FindNotificationParams {
alertsClient: AlertsClient;
@@ -76,32 +78,36 @@ export interface ReadNotificationParams {
}
export const isAlertTypes = (
- partialAlert: PartialAlert[]
+ partialAlert: Array>
): partialAlert is RuleNotificationAlertType[] => {
return partialAlert.every((rule) => isAlertType(rule));
};
export const isAlertType = (
- partialAlert: PartialAlert
+ partialAlert: PartialAlert
): partialAlert is RuleNotificationAlertType => {
return partialAlert.alertTypeId === NOTIFICATIONS_ID;
};
-export type NotificationExecutorOptions = Omit & {
- params: {
- ruleAlertId: string;
- };
-};
+export type NotificationExecutorOptions = AlertExecutorOptions<
+ RuleNotificationAlertTypeParams,
+ AlertTypeState,
+ AlertInstanceState,
+ AlertInstanceContext
+>;
// This returns true because by default a NotificationAlertTypeDefinition is an AlertType
// since we are only increasing the strictness of params.
export const isNotificationAlertExecutor = (
obj: NotificationAlertTypeDefinition
-): obj is AlertType => {
+): obj is AlertType => {
return true;
};
-export type NotificationAlertTypeDefinition = Omit & {
+export type NotificationAlertTypeDefinition = Omit<
+ AlertType,
+ 'executor'
+> & {
executor: ({
services,
params,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/update_notifications.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/update_notifications.ts
index d6c8973215117..8528d53b51f5b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/update_notifications.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/update_notifications.ts
@@ -6,7 +6,7 @@
import { PartialAlert } from '../../../../../alerts/server';
import { readNotifications } from './read_notifications';
-import { UpdateNotificationParams } from './types';
+import { RuleNotificationAlertTypeParams, UpdateNotificationParams } from './types';
import { addTags } from './add_tags';
import { createNotifications } from './create_notifications';
import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions';
@@ -18,11 +18,11 @@ export const updateNotifications = async ({
ruleAlertId,
name,
interval,
-}: UpdateNotificationParams): Promise => {
+}: UpdateNotificationParams): Promise | null> => {
const notification = await readNotifications({ alertsClient, id: undefined, ruleAlertId });
if (interval && notification) {
- return alertsClient.update({
+ return alertsClient.update({
id: notification.id,
data: {
tags: addTags([], ruleAlertId),
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
index b185b8780abe2..3473948b000c7 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts
@@ -22,6 +22,8 @@ import { buildRouteValidation } from '../../../../utils/build_validation/route_v
import { transformBulkError, createBulkErrorObject, buildSiemResponse } from '../utils';
import { updateRulesNotifications } from '../../rules/update_rules_notifications';
import { convertCreateAPIToInternalSchema } from '../../schemas/rule_converters';
+import { RuleTypeParams } from '../../types';
+import { Alert } from '../../../../../../alerts/common';
export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => {
router.post(
@@ -95,9 +97,12 @@ export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) =>
});
}
- const createdRule = await alertsClient.create({
+ /**
+ * TODO: Remove this use of `as` by utilizing the proper type
+ */
+ const createdRule = (await alertsClient.create({
data: internalRule,
- });
+ })) as Alert;
const ruleActions = await updateRulesNotifications({
ruleAlertId: createdRule.id,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts
index b52248f670188..c59d5d2a36fd5 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/create_rules_route.ts
@@ -19,6 +19,8 @@ import { createRulesSchema } from '../../../../../common/detection_engine/schema
import { newTransformValidate } from './validate';
import { createRuleValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/create_rules_type_dependents';
import { convertCreateAPIToInternalSchema } from '../../schemas/rule_converters';
+import { RuleTypeParams } from '../../types';
+import { Alert } from '../../../../../../alerts/common';
export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void => {
router.post(
@@ -85,9 +87,12 @@ export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void
// This will create the endpoint list if it does not exist yet
await context.lists?.getExceptionListClient().createEndpointList();
- const createdRule = await alertsClient.create({
+ /**
+ * TODO: Remove this use of `as` by utilizing the proper type
+ */
+ const createdRule = (await alertsClient.create({
data: internalRule,
- });
+ })) as Alert;
const ruleActions = await updateRulesNotifications({
ruleAlertId: createdRule.id,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts
index 7a6cd707eb185..bd7f11b2d8756 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts
@@ -31,6 +31,7 @@ import {
OutputError,
} from '../utils';
import { RuleActions } from '../../rule_actions/types';
+import { RuleTypeParams } from '../../types';
type PromiseFromStreams = ImportRulesSchemaDecoded | Error;
@@ -172,7 +173,7 @@ export const transformAlertsToRules = (alerts: RuleAlertType[]): Array,
ruleActions: Array,
ruleStatuses?: Array>
): {
@@ -203,7 +204,7 @@ export const transformFindAlerts = (
};
export const transform = (
- alert: PartialAlert,
+ alert: PartialAlert,
ruleActions?: RuleActions | null,
ruleStatus?: SavedObject
): Partial | null => {
@@ -220,7 +221,7 @@ export const transform = (
export const transformOrBulkError = (
ruleId: string,
- alert: PartialAlert,
+ alert: PartialAlert,
ruleActions: RuleActions,
ruleStatus?: unknown
): Partial | BulkError => {
@@ -241,7 +242,7 @@ export const transformOrBulkError = (
export const transformOrImportError = (
ruleId: string,
- alert: PartialAlert,
+ alert: PartialAlert,
existingImportSuccessError: ImportSuccessError
): ImportSuccessError => {
if (isAlertType(alert)) {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts
index 8653bdc0427e4..51b08cb3fce89 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.test.ts
@@ -15,6 +15,7 @@ import { RulesSchema } from '../../../../../common/detection_engine/schemas/resp
import { getResult, getFindResultStatus } from '../__mocks__/request_responses';
import { getListArrayMock } from '../../../../../common/detection_engine/schemas/types/lists.mock';
import { getThreatMock } from '../../../../../common/detection_engine/schemas/types/threat.mock';
+import { RuleTypeParams } from '../../types';
export const ruleOutput = (): RulesSchema => ({
actions: [],
@@ -88,7 +89,7 @@ describe('validate', () => {
describe('transformValidateFindAlerts', () => {
test('it should do a validation correctly of a find alert', () => {
- const findResult: FindResult = {
+ const findResult: FindResult = {
data: [getResult()],
page: 1,
perPage: 0,
@@ -111,7 +112,7 @@ describe('validate', () => {
});
test('it should do an in-validation correctly of a partial alert', () => {
- const findResult: FindResult = {
+ const findResult: FindResult = {
data: [getResult()],
page: 1,
perPage: 0,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts
index 382186df16cd1..3e3b85a407fc2 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/validate.ts
@@ -31,9 +31,10 @@ import {
import { createBulkErrorObject, BulkError } from '../utils';
import { transformFindAlerts, transform, transformAlertToRule } from './utils';
import { RuleActions } from '../../rule_actions/types';
+import { RuleTypeParams } from '../../types';
export const transformValidateFindAlerts = (
- findResults: FindResult,
+ findResults: FindResult,
ruleActions: Array,
ruleStatuses?: Array>
): [
@@ -63,7 +64,7 @@ export const transformValidateFindAlerts = (
};
export const transformValidate = (
- alert: PartialAlert,
+ alert: PartialAlert,
ruleActions?: RuleActions | null,
ruleStatus?: SavedObject
): [RulesSchema | null, string | null] => {
@@ -76,7 +77,7 @@ export const transformValidate = (
};
export const newTransformValidate = (
- alert: PartialAlert,
+ alert: PartialAlert,
ruleActions?: RuleActions | null,
ruleStatus?: SavedObject
): [FullResponseSchema | null, string | null] => {
@@ -90,7 +91,7 @@ export const newTransformValidate = (
export const transformValidateBulkError = (
ruleId: string,
- alert: PartialAlert,
+ alert: PartialAlert,
ruleActions?: RuleActions | null,
ruleStatus?: SavedObjectsFindResponse
): RulesSchema | BulkError => {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
index 0519a98df1fae..6339e92eb012b 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/create_rules.ts
@@ -9,6 +9,7 @@ import { Alert } from '../../../../../alerts/common';
import { SERVER_APP_ID, SIGNALS_ID } from '../../../../common/constants';
import { CreateRulesOptions } from './types';
import { addTags } from './add_tags';
+import { PartialFilter, RuleTypeParams } from '../types';
export const createRules = async ({
alertsClient,
@@ -59,8 +60,8 @@ export const createRules = async ({
version,
exceptionsList,
actions,
-}: CreateRulesOptions): Promise => {
- return alertsClient.create({
+}: CreateRulesOptions): Promise> => {
+ return alertsClient.create({
data: {
name,
tags: addTags(tags, ruleId, immutable),
@@ -95,7 +96,10 @@ export const createRules = async ({
severityMapping,
threat,
threshold,
- threatFilters,
+ /**
+ * TODO: Fix typing inconsistancy between `RuleTypeParams` and `CreateRulesOptions`
+ */
+ threatFilters: threatFilters as PartialFilter[] | undefined,
threatIndex,
threatQuery,
concurrentSearches,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts
index 18b851c440e20..5ab97262515da 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/find_rules.ts
@@ -6,6 +6,7 @@
import { FindResult } from '../../../../../alerts/server';
import { SIGNALS_ID } from '../../../../common/constants';
+import { RuleTypeParams } from '../types';
import { FindRuleOptions } from './types';
export const getFilter = (filter: string | null | undefined) => {
@@ -24,7 +25,7 @@ export const findRules = async ({
filter,
sortField,
sortOrder,
-}: FindRuleOptions): Promise => {
+}: FindRuleOptions): Promise> => {
return alertsClient.find({
options: {
fields,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
index 4c01318f02cde..1f43706c17fec 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/install_prepacked_rules.ts
@@ -5,7 +5,7 @@
*/
import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema';
-import { Alert } from '../../../../../alerts/common';
+import { Alert, AlertTypeParams } from '../../../../../alerts/common';
import { AlertsClient } from '../../../../../alerts/server';
import { createRules } from './create_rules';
import { PartialFilter } from '../types';
@@ -14,8 +14,8 @@ export const installPrepackagedRules = (
alertsClient: AlertsClient,
rules: AddPrepackagedRulesSchemaDecoded[],
outputIndex: string
-): Array> =>
- rules.reduce>>((acc, rule) => {
+): Array>> =>
+ rules.reduce>>>((acc, rule) => {
const {
anomaly_threshold: anomalyThreshold,
author,
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts
index b2303d48b0517..484dac8e31fb4 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts
@@ -9,8 +9,9 @@ import { alertsClientMock } from '../../../../../alerts/server/mocks';
import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks';
import { INTERNAL_RULE_ID_KEY, INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants';
import { SanitizedAlert } from '../../../../../alerts/common';
+import { RuleTypeParams } from '../types';
-const rule: SanitizedAlert = {
+const rule: SanitizedAlert = {
id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd',
name: 'Detect Root/Admin Users',
tags: [`${INTERNAL_RULE_ID_KEY}:rule-1`, `${INTERNAL_IMMUTABLE_KEY}:false`],
@@ -67,6 +68,8 @@ const rule: SanitizedAlert = {
note: '# Investigative notes',
version: 1,
exceptionsList: [
+ /**
+ TODO: fix this mock. Which the typing has revealed is wrong
{
field: 'source.ip',
values_operator: 'included',
@@ -96,8 +99,31 @@ const rule: SanitizedAlert = {
],
},
],
- },
+ },*/
],
+ /**
+ * The fields below were missing as the type was partial and hence not technically correct
+ */
+ author: [],
+ buildingBlockType: undefined,
+ eventCategoryOverride: undefined,
+ license: undefined,
+ savedId: undefined,
+ interval: undefined,
+ riskScoreMapping: undefined,
+ ruleNameOverride: undefined,
+ name: undefined,
+ severityMapping: undefined,
+ tags: undefined,
+ threshold: undefined,
+ threatFilters: undefined,
+ threatIndex: undefined,
+ threatQuery: undefined,
+ threatMapping: undefined,
+ threatLanguage: undefined,
+ concurrentSearches: undefined,
+ itemsPerSearch: undefined,
+ timestampOverride: undefined,
},
createdAt: new Date('2019-12-13T16:40:33.400Z'),
updatedAt: new Date('2019-12-13T16:40:33.400Z'),
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts
index c86526cee9302..c1720c4fa3587 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts
@@ -13,6 +13,7 @@ import { addTags } from './add_tags';
import { calculateVersion, calculateName, calculateInterval, removeUndefined } from './utils';
import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client';
import { internalRuleUpdate } from '../schemas/rule_schemas';
+import { RuleTypeParams } from '../types';
class PatchError extends Error {
public readonly statusCode: number;
@@ -71,7 +72,7 @@ export const patchRules = async ({
anomalyThreshold,
machineLearningJobId,
actions,
-}: PatchRulesOptions): Promise => {
+}: PatchRulesOptions): Promise | null> => {
if (rule == null) {
return null;
}
@@ -185,10 +186,13 @@ export const patchRules = async ({
throw new PatchError(`Applying patch would create invalid rule: ${errors}`, 400);
}
- const update = await alertsClient.update({
+ /**
+ * TODO: Remove this use of `as` by utilizing the proper type
+ */
+ const update = (await alertsClient.update({
id: rule.id,
data: validated,
- });
+ })) as PartialAlert;
if (rule.enabled && enabled === false) {
await alertsClient.disable({ id: rule.id });
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts
index e4bb65a907e2d..bc277bd2089a6 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts
@@ -6,6 +6,7 @@
import { SanitizedAlert } from '../../../../../alerts/common';
import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants';
+import { RuleTypeParams } from '../types';
import { findRules } from './find_rules';
import { isAlertType, ReadRuleOptions } from './types';
@@ -21,7 +22,7 @@ export const readRules = async ({
alertsClient,
id,
ruleId,
-}: ReadRuleOptions): Promise => {
+}: ReadRuleOptions): Promise | null> => {
if (id != null) {
try {
const rule = await alertsClient.get({ id });
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
index aaeb487ad9a78..34dab20c279b4 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts
@@ -103,9 +103,7 @@ import { SIGNALS_ID } from '../../../../common/constants';
import { RuleTypeParams, PartialFilter } from '../types';
import { ListArrayOrUndefined, ListArray } from '../../../../common/detection_engine/schemas/types';
-export interface RuleAlertType extends Alert {
- params: RuleTypeParams;
-}
+export type RuleAlertType = Alert;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface IRuleStatusSOAttributes extends Record {
@@ -173,11 +171,15 @@ export interface Clients {
alertsClient: AlertsClient;
}
-export const isAlertTypes = (partialAlert: PartialAlert[]): partialAlert is RuleAlertType[] => {
+export const isAlertTypes = (
+ partialAlert: Array>
+): partialAlert is RuleAlertType[] => {
return partialAlert.every((rule) => isAlertType(rule));
};
-export const isAlertType = (partialAlert: PartialAlert): partialAlert is RuleAlertType => {
+export const isAlertType = (
+ partialAlert: PartialAlert
+): partialAlert is RuleAlertType => {
return partialAlert.alertTypeId === SIGNALS_ID;
};
@@ -305,7 +307,7 @@ export interface PatchRulesOptions {
version: VersionOrUndefined;
exceptionsList: ListArrayOrUndefined;
actions: RuleAlertAction[] | undefined;
- rule: SanitizedAlert | null;
+ rule: SanitizedAlert | null;
}
export interface ReadRuleOptions {
diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts
index c63bd01cd1813..b3e0aaae715c9 100644
--- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts
+++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts
@@ -15,13 +15,14 @@ import { addTags } from './add_tags';
import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client';
import { typeSpecificSnakeToCamel } from '../schemas/rule_converters';
import { InternalRuleUpdate } from '../schemas/rule_schemas';
+import { RuleTypeParams } from '../types';
export const updateRules = async ({
alertsClient,
savedObjectsClient,
defaultOutputIndex,
ruleUpdate,
-}: UpdateRulesOptions): Promise => {
+}: UpdateRulesOptions): Promise | null> => {
const existingRule = await readRules({
alertsClient,
ruleId: ruleUpdate.rule_id,
@@ -77,10 +78,13 @@ export const updateRules = async ({
notifyWhen: null,
};
- const update = await alertsClient.update({
+ /**
+ * TODO: Remove this use of `as` by utilizing the proper type
+ */
+ const update = (await alertsClient.update({
id: existingRule.id,
data: newInternalRule,
- });
+ })) as PartialAlert